diff --git a/gix/src/discover.rs b/gix/src/discover.rs index 7979c68ea77..37e28fa98ca 100644 --- a/gix/src/discover.rs +++ b/gix/src/discover.rs @@ -42,7 +42,7 @@ impl ThreadSafeRepository { options.git_dir_trust = trust.into(); // Note that we will adjust the `current_dir` later so it matches the value of `core.precomposeUnicode`. options.current_dir = Some(gix_fs::current_dir(false).map_err(upwards::Error::CurrentDir)?); - Self::open_from_paths(git_dir, worktree_dir, options).map_err(Into::into) + Self::open_from_paths(git_dir, worktree_dir, options, None).map_err(Into::into) } /// Try to open a git repository directly from the environment. diff --git a/gix/src/init.rs b/gix/src/init.rs index b537c021c4a..5c1992c110f 100644 --- a/gix/src/init.rs +++ b/gix/src/init.rs @@ -76,7 +76,7 @@ impl ThreadSafeRepository { open_options.git_dir_trust = Some(gix_sec::Trust::Full); // The repo will use `core.precomposeUnicode` to adjust the value as needed. open_options.current_dir = gix_fs::current_dir(false)?.into(); - let repo = ThreadSafeRepository::open_from_paths(git_dir, worktree_dir, open_options)?; + let repo = ThreadSafeRepository::open_from_paths(git_dir, worktree_dir, open_options, None)?; let branch_name = repo .config diff --git a/gix/src/open/repository.rs b/gix/src/open/repository.rs index 5dc4bf47c1d..940e31ffef2 100644 --- a/gix/src/open/repository.rs +++ b/gix/src/open/repository.rs @@ -103,7 +103,7 @@ impl ThreadSafeRepository { options.git_dir_trust = gix_sec::Trust::from_path_ownership(&git_dir)?.into(); } options.current_dir = Some(cwd); - ThreadSafeRepository::open_from_paths(git_dir, worktree_dir, options) + ThreadSafeRepository::open_from_paths(git_dir, worktree_dir, options, None) } /// Try to open a git repository in `fallback_directory` (can be worktree or `.git` directory) only if there is no override @@ -154,13 +154,14 @@ impl ThreadSafeRepository { let mut options = trust_map.into_value_by_level(git_dir_trust); options.git_dir_trust = git_dir_trust.into(); options.current_dir = Some(cwd); - ThreadSafeRepository::open_from_paths(git_dir, worktree_dir, options) + ThreadSafeRepository::open_from_paths(git_dir, worktree_dir, options, None) } pub(crate) fn open_from_paths( mut git_dir: PathBuf, mut worktree_dir: Option, mut options: Options, + known_common_dir: Option, ) -> Result { let _span = gix_trace::detail!("open_from_paths()"); options.open_path_as_is = false; @@ -185,9 +186,12 @@ impl ThreadSafeRepository { } = options; let git_dir_trust = git_dir_trust.as_mut().expect("trust must be determined by now"); - let mut common_dir = gix_discover::path::from_plain_file(git_dir.join("commondir").as_ref()) - .transpose()? - .map(|cd| git_dir.join(cd)); + let mut common_dir = match known_common_dir { + Some(common_dir) => Some(common_dir), + None => gix_discover::path::from_plain_file(git_dir.join("commondir").as_ref()) + .transpose()? + .map(|cd| git_dir.join(cd)), + }; let repo_config = config::cache::StageOne::new( common_dir.as_deref().unwrap_or(&git_dir), git_dir.as_ref(), diff --git a/gix/src/remote/connection/fetch/update_refs/tests.rs b/gix/src/remote/connection/fetch/update_refs/tests.rs index cec52444205..b7a029a11c0 100644 --- a/gix/src/remote/connection/fetch/update_refs/tests.rs +++ b/gix/src/remote/connection/fetch/update_refs/tests.rs @@ -262,6 +262,55 @@ mod update { Ok(()) } + #[test] + fn incomplete_linked_worktrees_without_a_head_are_ignored() -> Result { + let (repo, _tmp) = repo_rw("two-origins"); + let git_dir = repo.common_dir().join("worktrees/incomplete"); + std::fs::create_dir_all(&git_dir)?; + std::fs::write(git_dir.join("gitdir"), b"missing/.git\n")?; + + let (mappings, specs) = mapping_from_spec("refs/heads/main:refs/remotes/origin/incomplete", &repo); + let update = || { + fetch::refs::update( + &repo, + prefixed("action"), + &mappings, + &specs, + &[], + fetch::Tags::None, + fetch::DryRun::Yes, + fetch::WritePackedRefs::Never, + ) + }; + let expected = vec![fetch::refs::Update { + mode: fetch::refs::update::Mode::New, + type_change: None, + edit_index: Some(0), + }]; + + assert_eq!( + update()?.updates, + expected, + "the incomplete worktree contributes no checked-out branch" + ); + + std::fs::write(git_dir.join("locked"), b"still in use\n")?; + assert_eq!( + update()?.updates, + expected, + "locking does not make an unreadable head protect a branch" + ); + + std::fs::remove_file(git_dir.join("locked"))?; + std::fs::write(git_dir.join("commondir"), b"missing\n")?; + assert_eq!( + update()?.updates, + expected, + "the parent repository supplies the authoritative common directory" + ); + Ok(()) + } + #[test] fn unborn_remote_branches_can_be_created_locally_if_they_are_new() -> Result { let repo = named_repo("unborn"); diff --git a/gix/src/worktree/proxy.rs b/gix/src/worktree/proxy.rs index 6ff08b95715..bb9198afb1e 100644 --- a/gix/src/worktree/proxy.rs +++ b/gix/src/worktree/proxy.rs @@ -43,18 +43,20 @@ impl<'repo> Proxy<'repo> { } impl Proxy<'_> { - /// Read the location of the checkout, the base of the work tree. - /// Note that the location might not exist. - pub fn base(&self) -> std::io::Result { + fn dot_git(&self) -> std::io::Result { let git_dir = self.git_dir.join("gitdir"); - let base_dot_git = gix_discover::path::from_plain_file_relative_to_file(&git_dir).ok_or_else(|| { + gix_discover::path::from_plain_file_relative_to_file(&git_dir).ok_or_else(|| { std::io::Error::new( std::io::ErrorKind::NotFound, format!("Required file '{}' does not exist", git_dir.display()), ) - })??; + })? + } - Ok(gix_discover::path::without_dot_git_dir(base_dot_git)) + /// Read the location of the checkout, the base of the work tree. + /// Note that the location might not exist. + pub fn base(&self) -> std::io::Result { + Ok(gix_discover::path::without_dot_git_dir(self.dot_git()?)) } /// The git directory for the work tree, typically contained within the parent git dir. @@ -70,7 +72,18 @@ impl Proxy<'_> { /// Return true if the worktree cannot be pruned, moved or deleted, which is useful if it is located on an external storage device. pub fn is_locked(&self) -> bool { - self.git_dir.join("locked").is_file() + self.git_dir.join("locked").symlink_metadata().is_ok() + } + + /// Return true if this worktree can be pruned without an expiry grace period. + /// + /// Locked worktrees are never prunable. Otherwise, an unreadable `gitdir` file or missing target + /// makes the worktree prunable. + pub fn is_prunable(&self) -> bool { + !self.is_locked() + && self + .dot_git() + .map_or(true, |dot_git| dot_git.symlink_metadata().is_err()) } /// Provide a reason for the locking of this worktree, if it is locked at all. @@ -92,7 +105,8 @@ impl Proxy<'_> { pub fn into_repo_with_possibly_inaccessible_worktree(self) -> Result { let base = self.base().ok(); let options = self.parent.options.clone().without_repository_environment_overrides(); - let repo = ThreadSafeRepository::open_from_paths(self.git_dir, base, options)?; + let common_dir = self.parent.common_dir().to_owned(); + let repo = ThreadSafeRepository::open_from_paths(self.git_dir, base, options, Some(common_dir))?; Ok(repo.into()) } @@ -106,7 +120,8 @@ impl Proxy<'_> { return Err(into_repo::Error::MissingWorktree { base }); } let options = self.parent.options.clone().without_repository_environment_overrides(); - let repo = ThreadSafeRepository::open_from_paths(self.git_dir, base.into(), options)?; + let common_dir = self.parent.common_dir().to_owned(); + let repo = ThreadSafeRepository::open_from_paths(self.git_dir, base.into(), options, Some(common_dir))?; Ok(repo.into()) } } diff --git a/gix/tests/gix/repository/worktree.rs b/gix/tests/gix/repository/worktree.rs index 32e1d2a4d0f..b99c5519418 100644 --- a/gix/tests/gix/repository/worktree.rs +++ b/gix/tests/gix/repository/worktree.rs @@ -439,6 +439,11 @@ fn run_assertions(main_repo: gix::Repository, should_be_bare: bool) { assert_eq!(proxy_lock_reason, expected.locked); let proxy_is_locked = actual.is_locked(); assert_eq!(proxy_is_locked, proxy_lock_reason.is_some()); + assert_eq!( + actual.is_prunable(), + expected.prunable.is_some(), + "prunability matches `git worktree list --porcelain`" + ); // TODO: check id of expected worktree, but need access to .gitdir from worktree base let proxy_id = actual.id().to_owned(); assert_eq!( @@ -485,15 +490,15 @@ fn run_assertions(main_repo: gix::Repository, should_be_bare: bool) { let proxy_by_id = repo .worktree_proxy_by_id(actual.id()) .expect("can get the proxy from a linked repo as well"); - assert_ne!( + assert_eq!( proxy_by_id.git_dir(), actual.git_dir(), - "The git directories might not look the same…" + "The git directories are the same" ); assert_eq!( gix_path::realpath(proxy_by_id.git_dir()).ok(), gix_path::realpath(actual.git_dir()).ok(), - "…but they are the same effectively" + "the git directories are effectively the same" ); } }