worktree repair: detect relative path in .git file correctly - #2205
worktree repair: detect relative path in .git file correctly#2205yoichi wants to merge 1 commit into
Conversation
|
/submit |
|
Submitted as pull.2205.git.1786799480344.gitgitgadget@gmail.com To fetch this version into To fetch this version to local tag |
|
Junio C Hamano wrote on the Git mailing list (how to reply to this email): "Yoichi NAKAYAMA via GitGitGadget" <gitgitgadget@gmail.com> writes:
> From: Yoichi NAKAYAMA <yoichi.nakayama@gmail.com>
>
> Since read_gitfile_gently() always returns an absolute path, the
> conversion from a relative path to an absolute path was not
> functioning and dead code existed.
This is ugly. What problem is this really fixing? What "conversion
from a relative path to an absolute path" does the above refer to?
What "dead code"? Where in what file and what function? Why does
the caller even care if it is absolute or relative? Shouldn't they
work equally well as long as they point at the right location?
The proposed log message hides so many details to evaluate the claim
that this is a good change, and raises many unanswered questions.
Yes, read_gitfile_gently() always turns the gitfile it reads into an
absolute form. Is there a caller A that wants the underlying
relative form, and if so why? Is it to compare with some other path
that is relative? How did the code B obtained the other path to be
compared that is relative? If that code B used the helper that is
different from read_gitfile_gently() to obtain the other path that
is relative, perhaps the caller A can be changed to call it instead
of calling read_gitfile_gently() and the fix can be done without
churning so many existing call sites?
Stepping back a bit, why does "repair" even care if it is relative?
Is it considered a semi-error when a gitfile records its target as a
relative path? If so, I wonder if a cleaner way may be to add a new
READ_GITFILE_ERR_RELATIVE_PATH constant that is treated as non-fatal
error by the read_gitfile_error_die() function? If that approach
works, that may be the cleanest, as I suspect that "was it recorded
as an absolute path?" will not stay to be the only special case in
niche applications like "repair", but we need to audit callers of
the _gently() function and make sure they do not barf with the new
return code.
If not, perhaps introduce a separate function that returns the path
it read without any conversion, i.e.,
char *read_raw_gitfile(const char *path);
that "repair" thing can use, and have it do the relateve-to-absolute
converaion itself, perhaps? That function would be created by moving
most of the code from read_gitfile_gently() and read_gitfile_gently()
would become a very thin wrapper around that function. Wouldn't that
be the least invasive and cleanest solution, if it works?
Thanks. |
|
Yoichi Nakayama wrote on the Git mailing list (how to reply to this email): On Tue, Aug 18, 2026 at 2:21 AM Junio C Hamano <gitster@pobox.com> wrote:
>
> "Yoichi NAKAYAMA via GitGitGadget" <gitgitgadget@gmail.com> writes:
>
> > From: Yoichi NAKAYAMA <yoichi.nakayama@gmail.com>
> >
> > Since read_gitfile_gently() always returns an absolute path, the
> > conversion from a relative path to an absolute path was not
> > functioning and dead code existed.
>
> This is ugly. What problem is this really fixing? What "conversion
> from a relative path to an absolute path" does the above refer to?
> What "dead code"? Where in what file and what function? Why does
> the caller even care if it is absolute or relative? Shouldn't they
> work equally well as long as they point at the right location?
>
> The proposed log message hides so many details to evaluate the claim
> that this is a good change, and raises many unanswered questions.
I'm sorry, the commit message lacked an explanation.
Let me explain the details of the issue I want to resolve.
When we create a worktree using default settings or with
`worktree.useRelativePaths=false`,
the cross references between the worktree and the repository
(specifically `worktree/id/gitdir`
in the main repository and the `.git` file in the worktree) are
recorded using absolute paths.
% mkdir repo
% cd repo
repo % git init
Initialized empty Git repository in /private/tmp/repo/.git/
repo % git commit --allow-empty -m init
[master (root-commit) bb4f6a1] init
repo % git config worktree.useRelativePaths
repo % git worktree add ../foo --detach
Preparing worktree (detached HEAD bb4f6a1)
HEAD is now at bb4f6a1 init
repo % cat .git/worktrees/foo/gitdir
/private/tmp/foo/.git
repo % cat ../foo/.git
gitdir: /private/tmp/repo/.git/worktrees/foo
In this situation, if we change the setting to
`worktree.useRelativePaths=true` and run
`git worktree repair` within the main worktree, the cross references
are converted to
relative paths (this is an expected behavior).
repo % git config worktree.useRelativePaths true
repo % git worktree repair
repair: .git file absolute/relative path mismatch: /private/tmp/foo
repo % cat .git/worktrees/foo/gitdir
../../../../foo/.git
repo % cat ../foo/.git
gitdir: ../repo/.git/worktrees/foo
On the other hand, given a state where cross references are recorded
using relative paths,
one would expect (by symmetry) that changing
`worktree.useRelativePath` from `true` to `false`
and running `git worktree repair` would convert the cross references
to absolute paths. However,
no "absolute/relative path mismatch" is detected, and the cross
references remain as relative paths.
This is the problem I wanted to fix.
repo % cat .git/worktrees/foo/gitdir
../../../../foo/.git
repo % cat ../foo/.git
gitdir: ../repo/.git/worktrees/foo
repo % git config worktree.useRelativePaths false
repo % git worktree repair
repo % cat .git/worktrees/foo/gitdir
../../../../foo/.git
repo % cat ../foo/.git
gitdir: ../repo/.git/worktrees/foo
The issue has been present since the initial implementation:
717af916cd (worktree: link worktrees with relative paths, 2024-10-07)
Although `dotgit_contents` (retrieved via `read_gitfile_gently()`) is
always an absolute path,
the implementations of `repair_gitfile()` and
`repair_worktree_at_path()` treat it as if the
actual contents of the `.git` file had been returned.
I have confirmed that the above issue can be reproduced even in the
v2.48.0 tag, which was
the first release to include that change.
> Yes, read_gitfile_gently() always turns the gitfile it reads into an
> absolute form. Is there a caller A that wants the underlying
> relative form, and if so why? Is it to compare with some other path
> that is relative? How did the code B obtained the other path to be
> compared that is relative? If that code B used the helper that is
> different from read_gitfile_gently() to obtain the other path that
> is relative, perhaps the caller A can be changed to call it instead
> of calling read_gitfile_gently() and the fix can be done without
> churning so many existing call sites?
>
> Stepping back a bit, why does "repair" even care if it is relative?
> Is it considered a semi-error when a gitfile records its target as a
> relative path? If so, I wonder if a cleaner way may be to add a new
> READ_GITFILE_ERR_RELATIVE_PATH constant that is treated as non-fatal
> error by the read_gitfile_error_die() function? If that approach
> works, that may be the cleanest, as I suspect that "was it recorded
> as an absolute path?" will not stay to be the only special case in
> niche applications like "repair", but we need to audit callers of
> the _gently() function and make sure they do not barf with the new
> return code.
>
> If not, perhaps introduce a separate function that returns the path
> it read without any conversion, i.e.,
>
> char *read_raw_gitfile(const char *path);
>
> that "repair" thing can use, and have it do the relateve-to-absolute
> converaion itself, perhaps? That function would be created by moving
> most of the code from read_gitfile_gently() and read_gitfile_gently()
> would become a very thin wrapper around that function. Wouldn't that
> be the least invasive and cleanest solution, if it works?
You're right; changing the signature of `read_gitfile_gently` for a niche use
case like `worktree repair` isn't a good idea. I'll revise the
approach to introduce
something like the `read_raw_gitfile()` you suggested.
Thanks,
--
Yoichi NAKAYAMA |
|
User |
368293c to
5bcf19e
Compare
|
/submit |
|
Submitted as pull.2205.v2.git.1787240760069.gitgitgadget@gmail.com To fetch this version into To fetch this version to local tag |
|
Junio C Hamano wrote on the Git mailing list (how to reply to this email): "Yoichi NAKAYAMA via GitGitGadget" <gitgitgadget@gmail.com> writes:
> This is because we wrongly use read_gitfile_gently() which always
> returns an absolute path. To fix this, introduce read_gitfile_raw()
> that is almost same as read_gitfile_gently(), but it skips existence
> check of the referenced repository and returns the unmodified path
> read from .git file.
This is more or less what I expected to see, but two function-scope
static variables are worse than one. At least let us not
proliferate the bad pattern that makes the functions non-reentrant.
The attached patch updates read_gitfile_raw() in your patch to take
a caller-prepared strbuf to store the value read from the '.git'
file, returning the error code as an integer. Ideally in the far
future, we would probably want to convert read_gitfile_gently() to
follow a similar function signature, but let us leave it as
#leftoverbits, as it has many more existing callers and all of them
would need adjusting. On the other hand, it is easier to get the API
in read_gitfile_raw() right while it still has only two callers.
setup.c | 9 +++------
setup.h | 2 +-
2 files changed, 4 insertions(+), 7 deletions(-)
diff --git c/setup.c w/setup.c
index af7601ff67..052c7d669b 100644
--- c/setup.c
+++ w/setup.c
@@ -996,7 +996,7 @@ const char *read_gitfile_gently(const char *path, int *return_error_code)
return error_code ? NULL : realpath.buf;
}
-const char *read_gitfile_raw(const char *path, int *return_error_code)
+int read_gitfile_raw(struct strbuf *contents, const char *path)
{
const int max_file_size = 1 << 20; /* 1MB */
int error_code = 0;
@@ -1004,7 +1004,6 @@ const char *read_gitfile_raw(const char *path, int *return_error_code)
struct stat st;
int fd;
ssize_t len;
- static struct strbuf contents = STRBUF_INIT;
if (stat(path, &st)) {
if (errno == ENOENT || errno == ENOTDIR)
@@ -1047,13 +1046,11 @@ const char *read_gitfile_raw(const char *path, int *return_error_code)
error_code = READ_GITFILE_ERR_NO_PATH;
goto cleanup_return;
}
- strbuf_reset(&contents);
- strbuf_add(&contents, buf+8, len-8);
+ strbuf_add(contents, buf+8, len-8);
cleanup_return:
- *return_error_code = error_code;
free(buf);
- return error_code ? NULL : contents.buf;
+ return error_code;
}
static void apply_gitdir_and_environment(struct repository *repo, const char *path)
diff --git c/setup.h w/setup.h
index 4c2fcbbeda..7394473e95 100644
--- c/setup.h
+++ w/setup.h
@@ -40,7 +40,7 @@ int is_nonbare_repository_dir(struct strbuf *path);
#define READ_GITFILE_ERR_IS_A_DIR 10
void read_gitfile_error_die(int error_code, const char *path);
const char *read_gitfile_gently(const char *path, int *return_error_code);
-const char *read_gitfile_raw(const char *path, int *return_error_code);
+int read_gitfile_raw(struct strbuf *contents, const char *path);
#define read_gitfile(path) read_gitfile_gently((path), NULL)
const char *resolve_gitdir_gently(const char *suspect, int *return_error_code);
#define resolve_gitdir(path) resolve_gitdir_gently((path), NULL) |
5bcf19e to
7ee8daa
Compare
Given a state in which the cross-references between the worktree and the repository (specifically worktree/id/gitdir in the main repository and the .git file in the worktree) are recorded using absolute paths, setting 'worktree.useRelativePaths=true' and running 'git worktree repair' within the main worktree converts them to relative paths. Conversely, given a state in which the cross-references are recorded using relative paths, one would expect that setting 'worktree.useRelativePaths=false' and running 'git worktree repair' would convert them to absolute paths. However, they remain as relative paths. This is because we incorrectly use read_gitfile_gently(), which always returns an absolute path. To fix this, introduce read_gitfile_raw(), which is almost identical to read_gitfile_gently(), but skips checking the existence of the referenced repository and returns the path as-is from the .git file. Signed-off-by: Yoichi NAKAYAMA <yoichi.nakayama@gmail.com>
7ee8daa to
1cd25e3
Compare
|
/submit |
|
Submitted as pull.2205.v3.git.1787344586470.gitgitgadget@gmail.com To fetch this version into To fetch this version to local tag |
|
Junio C Hamano wrote on the Git mailing list (how to reply to this email): "Yoichi NAKAYAMA via GitGitGadget" <gitgitgadget@gmail.com> writes:
> From: Yoichi NAKAYAMA <yoichi.nakayama@gmail.com>
>
> Given a state in which the cross-references between the worktree and
> the repository (specifically worktree/id/gitdir in the main repository
> and the .git file in the worktree) are recorded using absolute paths,
> setting 'worktree.useRelativePaths=true' and running 'git worktree
> repair' within the main worktree converts them to relative paths.
>
> Conversely, given a state in which the cross-references are recorded
> using relative paths, one would expect that setting
> 'worktree.useRelativePaths=false' and running 'git worktree repair'
> would convert them to absolute paths. However, they remain as relative
> paths.
>
> This is because we incorrectly use read_gitfile_gently(), which always
> returns an absolute path. To fix this, introduce read_gitfile_raw(),
> which is almost identical to read_gitfile_gently(), but skips checking
> the existence of the referenced repository and returns the path as-is
> from the .git file.
Excellent observation of the problem addressed by the patch. I wish
everybody wrote his or her proposed log message this clearly.
> diff --git a/setup.c b/setup.c
> index 95909e9603..9041827336 100644
> --- a/setup.c
> +++ b/setup.c
> @@ -962,16 +962,48 @@ void read_gitfile_error_die(int error_code, const char *path)
> * cases).
> */
> const char *read_gitfile_gently(const char *path, int *return_error_code)
> +{
> + int error_code = 0;
> + const char *slash;
> + struct strbuf contents = STRBUF_INIT;
> + static struct strbuf realpath = STRBUF_INIT;
> +
> + error_code = read_gitfile_raw(&contents, path);
> + if (error_code)
> + goto cleanup_return;
> +
> + if (!is_absolute_path(contents.buf) && (slash = strrchr(path, '/'))) {
> + size_t pathlen = slash+1 - path;
> + char *dir = xstrfmt("%.*s%s", (int)pathlen, path, contents.buf);
> + strbuf_reset(&contents);
> + strbuf_addstr(&contents, dir);
> + free(dir);
> + }
This massages path = "worktrees/foo/.git" into "worktrees/foo". And
the non-absolute contents.buf "../main/.git/worktrees/foo" that is
relative to gitfile is turned into relative to cwd of our process by
prepending "worktrees/foo" to it.
> + if (!is_git_directory(contents.buf)) {
> + error_code = READ_GITFILE_ERR_NOT_A_REPO;
> + goto cleanup_return;
> + }
This ensures that the thing referenced by .git file (i.e., what
comes after "gitdir:") is a sanely formatted git directory.
> + strbuf_realpath(&realpath, contents.buf, 1);
This turns the thing into an absolute path.
Among these three, the last one obviously belongs here. Leaving the
relative path relative was the reason why we wanted to add
read_gitfile_raw() in the first place.
But moving the other two to here is a bit iffy. The worktree repair
job used to call read_gitfile_gently(), which means it used to
depend on what the first two did for it, namely, to make the
relative path after "gitdir:" from the .git file relative to the
current process to make it usable, and to ensure that the directory
pointed at by .git is indeed a git directory. Is it correct to drop
these from the caller, which now calls read_gitfile_raw() instead?
IOW, I am not sure if the two functions are split correctly. I
expected that the only two things read_gitfile_gently() would do
after read_gitfile_raw() are (1) upon error, jump to cleanup_return,
and (2) otherwise call strbuf_realpath().
Thanks. |
|
Junio C Hamano wrote on the Git mailing list (how to reply to this email): Junio C Hamano <gitster@pobox.com> writes:
> Among these three, the last one obviously belongs here. Leaving the
> relative path relative was the reason why we wanted to add
> read_gitfile_raw() in the first place.
>
> But moving the other two to here is a bit iffy. The worktree repair
> job used to call read_gitfile_gently(), which means it used to
> depend on what the first two did for it, namely, to make the
> relative path after "gitdir:" from the .git file relative to the
> current process to make it usable, and to ensure that the directory
> pointed at by .git is indeed a git directory. Is it correct to drop
> these from the caller, which now calls read_gitfile_raw() instead?
>
> IOW, I am not sure if the two functions are split correctly. I
> expected that the only two things read_gitfile_gently() would do
> after read_gitfile_raw() are (1) upon error, jump to cleanup_return,
> and (2) otherwise call strbuf_realpath().
Actually, I take half of that back. If we pretend the leading part
of the "path", which could be absolute, the result will lose the
relative-ness of the original. Keeping the tweaking of the relative
path in read_gitfile_gently() is reasonable. As is_git_directory()
needs to be called on a usable path, if the relative path tweaking
cannot be done inside read_gitfile_raw(), it cannot check if the
directory is is_git_directory(), either.
So, the change to setup.c is fine as is. I didn't look at the
changes to worktree.c, though.
Thanks. |
cc: Yoichi Nakayama yoichi.nakayama@gmail.com