From 3ee9b63dfdc3190f322338c7c4da2e7b3718e309 Mon Sep 17 00:00:00 2001 From: Arumugam Ramaswamy Date: Thu, 25 Sep 2025 05:17:47 +0200 Subject: [PATCH 1/2] fix: worktree when .git is symlink (#2052) Co-authored-by: Eliah Kagan --- gix/src/open/repository.rs | 13 ++++++++- .../fixtures/generated-archives/.gitignore | 3 +- .../make_submodule_with_symlinked_git_dir.sh | 28 +++++++++++++++++++ gix/tests/gix/status.rs | 21 ++++++++++++++ 4 files changed, 63 insertions(+), 2 deletions(-) create mode 100755 gix/tests/fixtures/make_submodule_with_symlinked_git_dir.sh diff --git a/gix/src/open/repository.rs b/gix/src/open/repository.rs index 1b367497ffa..bff4ab93098 100644 --- a/gix/src/open/repository.rs +++ b/gix/src/open/repository.rs @@ -301,7 +301,18 @@ impl ThreadSafeRepository { | gix_config::Source::EnvOverride => wt_path, _ => git_dir.join(wt_path).into(), }; - worktree_dir = gix_path::normalize(wt_path, current_dir).map(Cow::into_owned); + + // the reason we use realpath instead of gix_path::normalize here is because there + // could be any intermediate symlinks (for example due to a symlinked .git + // directory) + worktree_dir = gix_path::realpath(&wt_path).ok(); + // restore the relative path if possible after resolving the absolute path + if wt_path.is_relative() { + if let Some(rel_path) = worktree_dir.as_deref().and_then(|p| p.strip_prefix(current_dir).ok()) { + worktree_dir = Some(rel_path.to_path_buf()); + } + } + #[allow(unused_variables)] if let Some(worktree_path) = worktree_dir.as_deref().filter(|wtd| !wtd.is_dir()) { gix_trace::warn!("The configured worktree path '{}' is not a directory or doesn't exist - `core.worktree` may be misleading", worktree_path.display()); diff --git a/gix/tests/fixtures/generated-archives/.gitignore b/gix/tests/fixtures/generated-archives/.gitignore index 9279e744abb..82408d215eb 100644 --- a/gix/tests/fixtures/generated-archives/.gitignore +++ b/gix/tests/fixtures/generated-archives/.gitignore @@ -8,4 +8,5 @@ /make_signatures_repo.tar /make_diff_repos.tar /make_submodule_with_worktree.tar -/repo_with_untracked_files.tar \ No newline at end of file +/repo_with_untracked_files.tar +/make_submodule_with_symlinked_git_dir.tar diff --git a/gix/tests/fixtures/make_submodule_with_symlinked_git_dir.sh b/gix/tests/fixtures/make_submodule_with_symlinked_git_dir.sh new file mode 100755 index 00000000000..1ea5a1071ac --- /dev/null +++ b/gix/tests/fixtures/make_submodule_with_symlinked_git_dir.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +set -eu -o pipefail + +git init -q module1 +(cd module1 + touch this + mkdir subdir + touch subdir/that + git add . + git commit -q -m c1 + echo hello >> this + git commit -q -am c2 + touch untracked +) + +mkdir symlinked-git-dir +(cd symlinked-git-dir + git init -q r1 + (cd r1 + git commit -q --allow-empty -m "init" + ) + + git config -f r1/.git/config core.worktree "$(pwd)" + ln -s r1/.git .git + + git -c protocol.file.allow=always submodule add ../module1 m1 + git commit -m "add module 1" +) diff --git a/gix/tests/gix/status.rs b/gix/tests/gix/status.rs index 8b832f5c2c8..cb68289de4c 100644 --- a/gix/tests/gix/status.rs +++ b/gix/tests/gix/status.rs @@ -310,6 +310,27 @@ mod index_worktree { ); } + #[test] + fn submodule_in_symlinked_dir() -> crate::Result { + use crate::util::named_subrepo_opts; + let repo = named_subrepo_opts( + "make_submodule_with_symlinked_git_dir.sh", + "symlinked-git-dir", + gix::open::Options::isolated(), + )?; + let status = repo + .status(gix::progress::Discard)? + .index_worktree_options_mut(|opts| { + opts.sorting = + Some(gix::status::plumbing::index_as_worktree_with_renames::Sorting::ByPathCaseSensitive); + }) + .into_index_worktree_iter(None)?; + for change in status { + change?; + } + Ok(()) + } + #[test] fn submodule_modification() -> crate::Result { let repo = submodule_repo("modified-untracked-and-submodule-head-changed-and-modified")?; From 3222556bee55fe06750378fe80398140f4f989a0 Mon Sep 17 00:00:00 2001 From: ralphmodales Date: Tue, 30 Sep 2025 05:09:49 +0800 Subject: [PATCH 2/2] fix: make realpath conditional --- gix/src/open/repository.rs | 17 ++++++++++++++--- gix/tests/gix/status.rs | 21 --------------------- 2 files changed, 14 insertions(+), 24 deletions(-) diff --git a/gix/src/open/repository.rs b/gix/src/open/repository.rs index bff4ab93098..0a26a3184e9 100644 --- a/gix/src/open/repository.rs +++ b/gix/src/open/repository.rs @@ -272,7 +272,13 @@ impl ThreadSafeRepository { section .path .as_deref() - .and_then(|p| gix_path::normalize(p.into(), current_dir)) + .and_then(|p| { + if p.exists() { + gix_path::realpath_opts(p, current_dir, gix_path::realpath::MAX_SYMLINKS).ok() + } else { + gix_path::normalize(p.into(), current_dir).map(Cow::into_owned) + } + }) .is_some_and(|config_path| config_path.starts_with(git_dir)) } let worktree_path = config @@ -305,9 +311,14 @@ impl ThreadSafeRepository { // the reason we use realpath instead of gix_path::normalize here is because there // could be any intermediate symlinks (for example due to a symlinked .git // directory) - worktree_dir = gix_path::realpath(&wt_path).ok(); + let is_relative = wt_path.is_relative(); + worktree_dir = if wt_path.exists() { + gix_path::realpath(&wt_path).ok() + } else { + Some(wt_path.into_owned()) + }; // restore the relative path if possible after resolving the absolute path - if wt_path.is_relative() { + if is_relative { if let Some(rel_path) = worktree_dir.as_deref().and_then(|p| p.strip_prefix(current_dir).ok()) { worktree_dir = Some(rel_path.to_path_buf()); } diff --git a/gix/tests/gix/status.rs b/gix/tests/gix/status.rs index cb68289de4c..8b832f5c2c8 100644 --- a/gix/tests/gix/status.rs +++ b/gix/tests/gix/status.rs @@ -310,27 +310,6 @@ mod index_worktree { ); } - #[test] - fn submodule_in_symlinked_dir() -> crate::Result { - use crate::util::named_subrepo_opts; - let repo = named_subrepo_opts( - "make_submodule_with_symlinked_git_dir.sh", - "symlinked-git-dir", - gix::open::Options::isolated(), - )?; - let status = repo - .status(gix::progress::Discard)? - .index_worktree_options_mut(|opts| { - opts.sorting = - Some(gix::status::plumbing::index_as_worktree_with_renames::Sorting::ByPathCaseSensitive); - }) - .into_index_worktree_iter(None)?; - for change in status { - change?; - } - Ok(()) - } - #[test] fn submodule_modification() -> crate::Result { let repo = submodule_repo("modified-untracked-and-submodule-head-changed-and-modified")?;