1
0
Fork 0
mirror of https://github.com/git/git.git synced 2024-10-28 04:49:43 +01:00

worktree: link worktrees with relative paths

Git currently stores absolute paths to both the main repository and
linked worktrees. However, this causes problems when moving repositories
or working in containerized environments where absolute paths differ
between systems. The worktree links break, and users are required to
manually execute `worktree repair` to repair them, leading to workflow
disruptions. Additionally, mapping repositories inside of containerized
environments renders the repository unusable inside the containers, and
this is not repairable as repairing the worktrees inside the containers
will result in them being broken outside the containers.

To address this, this patch makes Git always write relative paths when
linking worktrees. Relative paths increase the resilience of the
worktree links across various systems and environments, particularly
when the worktrees are self-contained inside the main repository (such
as when using a bare repository with worktrees). This improves
portability, workflow efficiency, and reduces overall breakages.

Although Git now writes relative paths, existing repositories with
absolute paths are still supported. There are no breaking changes
to workflows based on absolute paths, ensuring backward compatibility.

At a low level, the changes involve modifying functions in `worktree.c`
and `builtin/worktree.c` to use `relative_path()` when writing the
worktree’s `.git` file and the main repository’s `gitdir` reference.
Instead of hardcoding absolute paths, Git now computes the relative path
between the worktree and the repository, ensuring that these links are
portable. Locations where these respective file are read have also been
updated to properly handle both absolute and relative paths. Generally,
relative paths are always resolved into absolute paths before any
operations or comparisons are performed.

Additionally, `repair_worktrees_after_gitdir_move()` has been introduced
to address the case where both the `<worktree>/.git` and
`<repo>/worktrees/<id>/gitdir` links are broken after the gitdir is
moved (such as during a re-initialization). This function repairs both
sides of the worktree link using the old gitdir path to reestablish the
correct paths after a move.

The `worktree.path` struct member has also been updated to always store
the absolute path of a worktree. This ensures that worktree consumers
never have to worry about trying to resolve the absolute path themselves.

Signed-off-by: Caleb White <cdwhite3@pm.me>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
This commit is contained in:
Caleb White 2024-10-07 22:12:31 -05:00 committed by Junio C Hamano
parent bb4a883584
commit 717af916cd
5 changed files with 223 additions and 51 deletions

View file

@ -414,7 +414,8 @@ static int add_worktree(const char *path, const char *refname,
const struct add_opts *opts)
{
struct strbuf sb_git = STRBUF_INIT, sb_repo = STRBUF_INIT;
struct strbuf sb = STRBUF_INIT, realpath = STRBUF_INIT;
struct strbuf sb = STRBUF_INIT, sb_tmp = STRBUF_INIT;
struct strbuf sb_path_realpath = STRBUF_INIT, sb_repo_realpath = STRBUF_INIT;
const char *name;
struct strvec child_env = STRVEC_INIT;
unsigned int counter = 0;
@ -490,11 +491,10 @@ static int add_worktree(const char *path, const char *refname,
strbuf_reset(&sb);
strbuf_addf(&sb, "%s/gitdir", sb_repo.buf);
strbuf_realpath(&realpath, sb_git.buf, 1);
write_file(sb.buf, "%s", realpath.buf);
strbuf_realpath(&realpath, repo_get_common_dir(the_repository), 1);
write_file(sb_git.buf, "gitdir: %s/worktrees/%s",
realpath.buf, name);
strbuf_realpath(&sb_path_realpath, path, 1);
strbuf_realpath(&sb_repo_realpath, sb_repo.buf, 1);
write_file(sb.buf, "%s/.git", relative_path(sb_path_realpath.buf, sb_repo_realpath.buf, &sb_tmp));
write_file(sb_git.buf, "gitdir: %s", relative_path(sb_repo_realpath.buf, sb_path_realpath.buf, &sb_tmp));
strbuf_reset(&sb);
strbuf_addf(&sb, "%s/commondir", sb_repo.buf);
write_file(sb.buf, "../..");
@ -578,11 +578,13 @@ static int add_worktree(const char *path, const char *refname,
strvec_clear(&child_env);
strbuf_release(&sb);
strbuf_release(&sb_tmp);
strbuf_release(&symref);
strbuf_release(&sb_repo);
strbuf_release(&sb_repo_realpath);
strbuf_release(&sb_git);
strbuf_release(&sb_path_realpath);
strbuf_release(&sb_name);
strbuf_release(&realpath);
free_worktree(wt);
return ret;
}

View file

@ -2420,7 +2420,7 @@ static void separate_git_dir(const char *git_dir, const char *git_link)
if (rename(src, git_dir))
die_errno(_("unable to move %s to %s"), src, git_dir);
repair_worktrees(NULL, NULL);
repair_worktrees_after_gitdir_move(src);
}
write_file(git_link, "gitdir: %s", git_dir);

39
t/t2408-worktree-relative.sh Executable file
View file

@ -0,0 +1,39 @@
#!/bin/sh
test_description='test worktrees linked with relative paths'
TEST_PASSES_SANITIZE_LEAK=true
. ./test-lib.sh
test_expect_success 'links worktrees with relative paths' '
test_when_finished rm -rf repo &&
git init repo &&
(
cd repo &&
test_commit initial &&
git worktree add wt1 &&
echo "../../../wt1/.git" >expected_gitdir &&
cat .git/worktrees/wt1/gitdir >actual_gitdir &&
echo "gitdir: ../.git/worktrees/wt1" >expected_git &&
cat wt1/.git >actual_git &&
test_cmp expected_gitdir actual_gitdir &&
test_cmp expected_git actual_git
)
'
test_expect_success 'move repo without breaking relative internal links' '
test_when_finished rm -rf repo moved &&
git init repo &&
(
cd repo &&
test_commit initial &&
git worktree add wt1 &&
cd .. &&
mv repo moved &&
cd moved/wt1 &&
git status >out 2>err &&
test_must_be_empty err
)
'
test_done

View file

@ -110,6 +110,12 @@ struct worktree *get_linked_worktree(const char *id,
strbuf_rtrim(&worktree_path);
strbuf_strip_suffix(&worktree_path, "/.git");
if (!is_absolute_path(worktree_path.buf)) {
strbuf_strip_suffix(&path, "gitdir");
strbuf_addbuf(&path, &worktree_path);
strbuf_realpath_forgiving(&worktree_path, path.buf, 0);
}
CALLOC_ARRAY(worktree, 1);
worktree->repo = the_repository;
worktree->path = strbuf_detach(&worktree_path, NULL);
@ -373,18 +379,29 @@ int validate_worktree(const struct worktree *wt, struct strbuf *errmsg,
void update_worktree_location(struct worktree *wt, const char *path_)
{
struct strbuf path = STRBUF_INIT;
struct strbuf repo = STRBUF_INIT;
struct strbuf file = STRBUF_INIT;
struct strbuf tmp = STRBUF_INIT;
if (is_main_worktree(wt))
BUG("can't relocate main worktree");
strbuf_realpath(&repo, git_common_path("worktrees/%s", wt->id), 1);
strbuf_realpath(&path, path_, 1);
if (fspathcmp(wt->path, path.buf)) {
write_file(git_common_path("worktrees/%s/gitdir", wt->id),
"%s/.git", path.buf);
strbuf_addf(&file, "%s/gitdir", repo.buf);
write_file(file.buf, "%s/.git", relative_path(path.buf, repo.buf, &tmp));
strbuf_reset(&file);
strbuf_addf(&file, "%s/.git", path.buf);
write_file(file.buf, "gitdir: %s", relative_path(repo.buf, path.buf, &tmp));
free(wt->path);
wt->path = strbuf_detach(&path, NULL);
}
strbuf_release(&path);
strbuf_release(&repo);
strbuf_release(&file);
strbuf_release(&tmp);
}
int is_worktree_being_rebased(const struct worktree *wt,
@ -564,38 +581,52 @@ static void repair_gitfile(struct worktree *wt,
{
struct strbuf dotgit = STRBUF_INIT;
struct strbuf repo = STRBUF_INIT;
char *backlink;
struct strbuf backlink = STRBUF_INIT;
struct strbuf tmp = STRBUF_INIT;
char *dotgit_contents = NULL;
const char *repair = NULL;
int err;
/* missing worktree can't be repaired */
if (!file_exists(wt->path))
return;
goto done;
if (!is_directory(wt->path)) {
fn(1, wt->path, _("not a directory"), cb_data);
return;
goto done;
}
strbuf_realpath(&repo, git_common_path("worktrees/%s", wt->id), 1);
strbuf_addf(&dotgit, "%s/.git", wt->path);
backlink = xstrdup_or_null(read_gitfile_gently(dotgit.buf, &err));
dotgit_contents = xstrdup_or_null(read_gitfile_gently(dotgit.buf, &err));
if (dotgit_contents) {
if (is_absolute_path(dotgit_contents)) {
strbuf_addstr(&backlink, dotgit_contents);
} else {
strbuf_addf(&backlink, "%s/%s", wt->path, dotgit_contents);
strbuf_realpath_forgiving(&backlink, backlink.buf, 0);
}
}
if (err == READ_GITFILE_ERR_NOT_A_FILE)
fn(1, wt->path, _(".git is not a file"), cb_data);
else if (err)
repair = _(".git file broken");
else if (fspathcmp(backlink, repo.buf))
else if (fspathcmp(backlink.buf, repo.buf))
repair = _(".git file incorrect");
if (repair) {
fn(0, wt->path, repair, cb_data);
write_file(dotgit.buf, "gitdir: %s", repo.buf);
write_file(dotgit.buf, "gitdir: %s", relative_path(repo.buf, wt->path, &tmp));
}
free(backlink);
done:
free(dotgit_contents);
strbuf_release(&repo);
strbuf_release(&dotgit);
strbuf_release(&backlink);
strbuf_release(&tmp);
}
static void repair_noop(int iserr UNUSED,
@ -618,6 +649,59 @@ void repair_worktrees(worktree_repair_fn fn, void *cb_data)
free_worktrees(worktrees);
}
void repair_worktree_after_gitdir_move(struct worktree *wt, const char *old_path)
{
struct strbuf path = STRBUF_INIT;
struct strbuf repo = STRBUF_INIT;
struct strbuf gitdir = STRBUF_INIT;
struct strbuf dotgit = STRBUF_INIT;
struct strbuf olddotgit = STRBUF_INIT;
struct strbuf tmp = STRBUF_INIT;
if (is_main_worktree(wt))
goto done;
strbuf_realpath(&repo, git_common_path("worktrees/%s", wt->id), 1);
strbuf_addf(&gitdir, "%s/gitdir", repo.buf);
if (strbuf_read_file(&olddotgit, gitdir.buf, 0) < 0)
goto done;
strbuf_rtrim(&olddotgit);
if (is_absolute_path(olddotgit.buf)) {
strbuf_addbuf(&dotgit, &olddotgit);
} else {
strbuf_addf(&dotgit, "%s/worktrees/%s/%s", old_path, wt->id, olddotgit.buf);
strbuf_realpath_forgiving(&dotgit, dotgit.buf, 0);
}
if (!file_exists(dotgit.buf))
goto done;
strbuf_addbuf(&path, &dotgit);
strbuf_strip_suffix(&path, "/.git");
write_file(dotgit.buf, "gitdir: %s", relative_path(repo.buf, path.buf, &tmp));
write_file(gitdir.buf, "%s", relative_path(dotgit.buf, repo.buf, &tmp));
done:
strbuf_release(&path);
strbuf_release(&repo);
strbuf_release(&gitdir);
strbuf_release(&dotgit);
strbuf_release(&olddotgit);
strbuf_release(&tmp);
}
void repair_worktrees_after_gitdir_move(const char *old_path)
{
struct worktree **worktrees = get_worktrees_internal(1);
struct worktree **wt = worktrees + 1; /* +1 skips main worktree */
for (; *wt; wt++)
repair_worktree_after_gitdir_move(*wt, old_path);
free_worktrees(worktrees);
}
static int is_main_worktree_path(const char *path)
{
struct strbuf target = STRBUF_INIT;
@ -684,6 +768,8 @@ void repair_worktree_at_path(const char *path,
struct strbuf inferred_backlink = STRBUF_INIT;
struct strbuf gitdir = STRBUF_INIT;
struct strbuf olddotgit = STRBUF_INIT;
struct strbuf realolddotgit = STRBUF_INIT;
struct strbuf tmp = STRBUF_INIT;
char *dotgit_contents = NULL;
const char *repair = NULL;
int err;
@ -701,9 +787,17 @@ void repair_worktree_at_path(const char *path,
}
infer_backlink(realdotgit.buf, &inferred_backlink);
strbuf_realpath_forgiving(&inferred_backlink, inferred_backlink.buf, 0);
dotgit_contents = xstrdup_or_null(read_gitfile_gently(realdotgit.buf, &err));
if (dotgit_contents) {
strbuf_addstr(&backlink, dotgit_contents);
if (is_absolute_path(dotgit_contents)) {
strbuf_addstr(&backlink, dotgit_contents);
} else {
strbuf_addbuf(&backlink, &realdotgit);
strbuf_strip_suffix(&backlink, ".git");
strbuf_addstr(&backlink, dotgit_contents);
strbuf_realpath_forgiving(&backlink, backlink.buf, 0);
}
} else if (err == READ_GITFILE_ERR_NOT_A_FILE) {
fn(1, realdotgit.buf, _("unable to locate repository; .git is not a file"), cb_data);
goto done;
@ -721,7 +815,7 @@ void repair_worktree_at_path(const char *path,
fn(1, realdotgit.buf, _("unable to locate repository; .git file does not reference a repository"), cb_data);
goto done;
}
} else if (err) {
} else {
fn(1, realdotgit.buf, _("unable to locate repository; .git file broken"), cb_data);
goto done;
}
@ -753,90 +847,117 @@ void repair_worktree_at_path(const char *path,
repair = _("gitdir unreadable");
else {
strbuf_rtrim(&olddotgit);
if (fspathcmp(olddotgit.buf, realdotgit.buf))
if (is_absolute_path(olddotgit.buf)) {
strbuf_addbuf(&realolddotgit, &olddotgit);
} else {
strbuf_addf(&realolddotgit, "%s/%s", backlink.buf, olddotgit.buf);
strbuf_realpath_forgiving(&realolddotgit, realolddotgit.buf, 0);
}
if (fspathcmp(realolddotgit.buf, realdotgit.buf))
repair = _("gitdir incorrect");
}
if (repair) {
fn(0, gitdir.buf, repair, cb_data);
write_file(gitdir.buf, "%s", realdotgit.buf);
write_file(gitdir.buf, "%s", relative_path(realdotgit.buf, backlink.buf, &tmp));
}
done:
free(dotgit_contents);
strbuf_release(&olddotgit);
strbuf_release(&realolddotgit);
strbuf_release(&backlink);
strbuf_release(&inferred_backlink);
strbuf_release(&gitdir);
strbuf_release(&realdotgit);
strbuf_release(&dotgit);
strbuf_release(&tmp);
}
int should_prune_worktree(const char *id, struct strbuf *reason, char **wtpath, timestamp_t expire)
{
struct stat st;
char *path;
struct strbuf dotgit = STRBUF_INIT;
struct strbuf gitdir = STRBUF_INIT;
struct strbuf repo = STRBUF_INIT;
struct strbuf file = STRBUF_INIT;
char *path = NULL;
int rc = 0;
int fd;
size_t len;
ssize_t read_result;
*wtpath = NULL;
if (!is_directory(git_path("worktrees/%s", id))) {
strbuf_realpath(&repo, git_common_path("worktrees/%s", id), 1);
strbuf_addf(&gitdir, "%s/gitdir", repo.buf);
if (!is_directory(repo.buf)) {
strbuf_addstr(reason, _("not a valid directory"));
return 1;
rc = 1;
goto done;
}
if (file_exists(git_path("worktrees/%s/locked", id)))
return 0;
if (stat(git_path("worktrees/%s/gitdir", id), &st)) {
strbuf_addf(&file, "%s/locked", repo.buf);
if (file_exists(file.buf)) {
goto done;
}
if (stat(gitdir.buf, &st)) {
strbuf_addstr(reason, _("gitdir file does not exist"));
return 1;
rc = 1;
goto done;
}
fd = open(git_path("worktrees/%s/gitdir", id), O_RDONLY);
fd = open(gitdir.buf, O_RDONLY);
if (fd < 0) {
strbuf_addf(reason, _("unable to read gitdir file (%s)"),
strerror(errno));
return 1;
rc = 1;
goto done;
}
len = xsize_t(st.st_size);
path = xmallocz(len);
read_result = read_in_full(fd, path, len);
close(fd);
if (read_result < 0) {
strbuf_addf(reason, _("unable to read gitdir file (%s)"),
strerror(errno));
close(fd);
free(path);
return 1;
}
close(fd);
if (read_result != len) {
rc = 1;
goto done;
} else if (read_result != len) {
strbuf_addf(reason,
_("short read (expected %"PRIuMAX" bytes, read %"PRIuMAX")"),
(uintmax_t)len, (uintmax_t)read_result);
free(path);
return 1;
rc = 1;
goto done;
}
while (len && (path[len - 1] == '\n' || path[len - 1] == '\r'))
len--;
if (!len) {
strbuf_addstr(reason, _("invalid gitdir file"));
free(path);
return 1;
rc = 1;
goto done;
}
path[len] = '\0';
if (!file_exists(path)) {
if (stat(git_path("worktrees/%s/index", id), &st) ||
st.st_mtime <= expire) {
if (is_absolute_path(path)) {
strbuf_addstr(&dotgit, path);
} else {
strbuf_addf(&dotgit, "%s/%s", repo.buf, path);
strbuf_realpath_forgiving(&dotgit, dotgit.buf, 0);
}
if (!file_exists(dotgit.buf)) {
strbuf_reset(&file);
strbuf_addf(&file, "%s/index", repo.buf);
if (stat(file.buf, &st) || st.st_mtime <= expire) {
strbuf_addstr(reason, _("gitdir file points to non-existent location"));
free(path);
return 1;
} else {
*wtpath = path;
return 0;
rc = 1;
goto done;
}
}
*wtpath = path;
return 0;
*wtpath = strbuf_detach(&dotgit, NULL);
done:
free(path);
strbuf_release(&dotgit);
strbuf_release(&gitdir);
strbuf_release(&repo);
strbuf_release(&file);
return rc;
}
static int move_config_setting(const char *key, const char *value,

View file

@ -131,6 +131,16 @@ typedef void (* worktree_repair_fn)(int iserr, const char *path,
*/
void repair_worktrees(worktree_repair_fn, void *cb_data);
/*
* Repair the linked worktrees after the gitdir has been moved.
*/
void repair_worktrees_after_gitdir_move(const char *old_path);
/*
* Repair the linked worktree after the gitdir has been moved.
*/
void repair_worktree_after_gitdir_move(struct worktree *wt, const char *old_path);
/*
* Repair administrative files corresponding to the worktree at the given path.
* The worktree's .git file pointing at the repository must be intact for the