use git_ref::bstr; use git_repository as git; mod with_core_worktree_config { use crate::repository::worktree::Baseline; use git_repository as git; use std::io::BufRead; #[test] fn relative() -> crate::Result { for (name, is_relative) in [("absolute-worktree", false), ("relative-worktree", true)] { let repo = repo(name); if is_relative { assert_eq!( repo.work_dir().unwrap(), repo.git_dir().parent().unwrap().parent().unwrap().join("worktree"), "work_dir is set to core.worktree config value, relative paths are appended to `git_dir() and made absolute`" ); } else { assert_eq!( repo.work_dir().unwrap(), git_path::realpath(repo.git_dir().parent().unwrap().parent().unwrap().join("worktree"))?, "absolute workdirs are left untouched" ); } assert_eq!( repo.worktree().expect("present").base(), repo.work_dir().unwrap(), "current worktree is based on work-tree dir" ); let baseline = Baseline::collect(repo.git_dir())?; assert_eq!(baseline.len(), 1, "git lists the main worktree"); assert_eq!( baseline[0].root, git_path::realpath(repo.git_dir().parent().unwrap())?, "git lists the original worktree, to which we have no access anymore" ); assert_eq!( repo.worktrees()?.len(), 0, "we only list linked worktrees, and there are none" ); assert_eq!( repo.index()?.entries().len(), count_deleted(repo.git_dir()), "git considers all worktree entries missing as the overridden worktree is an empty dir" ); assert_eq!(repo.index()?.entries().len(), 3, "just to be sure"); } Ok(()) } #[test] fn non_existing_relative() { let repo = repo("relative-nonexisting-worktree"); assert_eq!( count_deleted(repo.git_dir()), 0, "git can't chdir into missing worktrees, has no error handling there" ); assert_eq!( repo.work_dir(), repo.git_dir().parent(), "we just ignore missing configured worktree dirs and fall back to the default one" ) } #[test] fn relative_file() { let repo = repo("relative-worktree-file"); assert_eq!(count_deleted(repo.git_dir()), 0, "git can't chdir into a file"); assert_eq!( repo.work_dir(), repo.git_dir().parent(), "we just ignore missing configured worktree dirs and fall back to the default one" ) } #[test] fn bare_relative() -> crate::Result { let repo = repo("bare-relative-worktree"); assert_eq!( count_deleted(repo.git_dir()), 0, "git refuses to mix bare with core.worktree" ); assert!( repo.work_dir().is_none(), "we simply don't load core.worktree in bare repos either to match this behaviour" ); Ok(()) } fn repo(name: &str) -> git::Repository { let dir = git_testtools::scripted_fixture_repo_read_only("make_core_worktree_repo.sh").unwrap(); git::open_opts(dir.join(name), crate::restricted()).unwrap() } fn count_deleted(git_dir: &std::path::Path) -> usize { std::fs::read(git_dir.join("status.baseline")) .unwrap() .lines() .filter_map(Result::ok) .filter(|line| line.contains(" D ")) .count() } } struct Baseline<'a> { lines: bstr::Lines<'a>, } mod baseline { use std::path::{Path, PathBuf}; use git_object::bstr::BStr; use git_repository::bstr::{BString, ByteSlice}; use super::Baseline; impl<'a> Baseline<'a> { pub fn collect(dir: impl AsRef) -> std::io::Result> { let content = std::fs::read(dir.as_ref().join("worktree-list.baseline"))?; Ok(Baseline { lines: content.lines() }.collect()) } } pub type Reason = BString; #[derive(Debug)] #[allow(dead_code)] pub struct Worktree { pub root: PathBuf, pub bare: bool, pub locked: Option, pub peeled: git_hash::ObjectId, pub branch: Option, pub prunable: Option, } impl<'a> Iterator for Baseline<'a> { type Item = Worktree; fn next(&mut self) -> Option { let root = git_path::from_bstr(fields(self.lines.next()?).1).into_owned(); let mut bare = false; let mut branch = None; let mut peeled = git_hash::ObjectId::null(git_hash::Kind::Sha1); let mut locked = None; let mut prunable = None; for line in self.lines.by_ref() { if line.is_empty() { break; } if line == b"bare" { bare = true; continue; } else if line == b"detached" { continue; } let (field, value) = fields(line); match field { f if f == "HEAD" => peeled = git_hash::ObjectId::from_hex(value).expect("valid hash"), f if f == "branch" => branch = Some(value.to_owned()), f if f == "locked" => locked = Some(value.to_owned()), f if f == "prunable" => prunable = Some(value.to_owned()), _ => unreachable!("unknown field: {}", field), } } Some(Worktree { root, bare, locked, peeled, branch, prunable, }) } } fn fields(line: &[u8]) -> (&BStr, &BStr) { let (a, b) = line.split_at(line.find_byte(b' ').expect("at least a space")); (a.as_bstr(), b[1..].as_bstr()) } } #[test] fn from_bare_parent_repo() { if git_testtools::should_skip_as_git_version_is_smaller_than(2, 31, 0) { return; } let dir = git_testtools::scripted_fixture_repo_read_only_with_args("make_worktree_repo.sh", ["bare"]).unwrap(); let repo = git::open(dir.join("repo.git")).unwrap(); run_assertions(repo, true /* bare */); } #[test] fn from_nonbare_parent_repo() { if git_testtools::should_skip_as_git_version_is_smaller_than(2, 31, 0) { return; } let dir = git_testtools::scripted_fixture_repo_read_only("make_worktree_repo.sh").unwrap(); let repo = git::open(dir.join("repo")).unwrap(); run_assertions(repo, false /* bare */); } fn run_assertions(main_repo: git::Repository, should_be_bare: bool) { assert_eq!(main_repo.is_bare(), should_be_bare); let mut baseline = Baseline::collect( main_repo .work_dir() .map(|p| p.parent()) .unwrap_or_else(|| main_repo.git_dir().parent()) .expect("a temp dir as parent"), ) .unwrap(); let expected_main = baseline.remove(0); assert_eq!(expected_main.bare, should_be_bare); if should_be_bare { assert!(main_repo.worktree().is_none()); } else { assert_eq!( main_repo.work_dir().expect("non-bare").canonicalize().unwrap(), expected_main.root.canonicalize().unwrap() ); assert_eq!(main_repo.head_id().unwrap(), expected_main.peeled); assert_eq!( main_repo.head_name().unwrap().expect("no detached head").as_bstr(), expected_main.branch.unwrap() ); let worktree = main_repo.worktree().expect("not bare"); assert!( worktree.lock_reason().is_none(), "main worktrees, bare or not, are never locked" ); assert!(!worktree.is_locked()); assert!(worktree.is_main()); } assert_eq!(main_repo.main_repo().unwrap(), main_repo, "main repo stays main repo"); let actual = main_repo.worktrees().unwrap(); assert_eq!(actual.len(), baseline.len()); for actual in actual { let base = actual.base().unwrap(); let expected = baseline .iter() .find(|exp| exp.root == base) .expect("we get the same root and it matches"); assert!( !expected.bare, "only the main worktree can be bare, and we don't see it in this loop" ); let proxy_lock_reason = actual.lock_reason(); assert_eq!(proxy_lock_reason, expected.locked); let proxy_is_locked = actual.is_locked(); assert_eq!(proxy_is_locked, proxy_lock_reason.is_some()); // TODO: check id of expected worktree, but need access to .gitdir from worktree base let proxy_id = actual.id().to_owned(); assert_eq!( base.is_dir(), expected.prunable.is_none(), "in our case prunable repos have no worktree base" ); let repo = if base.is_dir() { let repo = actual.into_repo().unwrap(); assert_eq!( &git::open(base).unwrap(), &repo, "repos are considered the same no matter if opened from worktree or from git dir" ); repo } else { assert!( matches!( actual.clone().into_repo(), Err(git::worktree::proxy::into_repo::Error::MissingWorktree { .. }) ), "missing bases are detected" ); actual.into_repo_with_possibly_inaccessible_worktree().unwrap() }; let worktree = repo.worktree().expect("linked worktrees have at least a base path"); assert!(!worktree.is_main()); assert_eq!(worktree.lock_reason(), proxy_lock_reason); assert_eq!(worktree.is_locked(), proxy_is_locked); assert_eq!(worktree.id(), Some(proxy_id.as_ref())); assert_eq!( repo.main_repo().unwrap(), main_repo, "main repo from worktree repo is the actual main repo" ); } }