Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 27 additions & 6 deletions t/t2406-worktree-repair.sh
Original file line number Diff line number Diff line change
Expand Up @@ -56,15 +56,12 @@ test_expect_success 'repair missing .git file' '
'

test_expect_success 'repair bogus .git file' '
test_corrupt_gitfile "echo \"gitdir: /nowhere\" >corrupt/.git" \
test_corrupt_gitfile "echo \"contents not started with gitdir:\" >corrupt/.git" \
".git file broken"
'

test_expect_success 'repair incorrect .git file' '
test_when_finished "rm -rf other && git worktree prune" &&
test_create_repo other &&
other=$(git -C other rev-parse --absolute-git-dir) &&
test_corrupt_gitfile "echo \"gitdir: $other\" >corrupt/.git" \
test_expect_success 'repair unlinked .git file' '
test_corrupt_gitfile "echo \"gitdir: /nowhere/worktrees/corrupt\" >corrupt/.git" \
".git file incorrect"
'

Expand All @@ -89,6 +86,18 @@ test_expect_success 'repair .git file from bare.git' '
test_cmp expect actual
'

test_expect_success 'skip unrelated .git file' '
test_when_finished "rm -rf corrupt other && git worktree prune" &&
git worktree add --detach corrupt &&
rm -rf corrupt &&
git worktree add --detach other &&
mv other corrupt &&
cat corrupt/.git >expect &&
test_must_fail git worktree repair 2>err &&
test_cmp expect corrupt/.git &&
test_grep "unrelated .git file" err
'

test_expect_success 'invalid worktree path' '
test_must_fail git worktree repair /notvalid >out 2>err &&
test_must_be_empty out &&
Expand All @@ -113,6 +122,18 @@ test_expect_success 'repo not found; .git not referencing repo' '
test_grep ".git file does not reference a repository" err
'

test_expect_success 'repo not found; .git not for worktree' '
test_when_finished "rm -rf side other-repo && git worktree prune" &&
test_create_repo other-repo &&
git worktree add --detach side &&
cat .git/worktrees/side/gitdir >expect &&
cp -R side other-repo/side &&
test_must_fail git -C other-repo worktree repair side >out 2>err &&
test_cmp expect .git/worktrees/side/gitdir &&
test_must_be_empty out &&
test_grep ".git file is not for a linked worktree" err
'

test_expect_success 'repo not found; .git file broken' '
test_when_finished "rm -rf orig moved && git worktree prune" &&
git worktree add --detach orig &&
Expand Down
105 changes: 56 additions & 49 deletions worktree.c
Original file line number Diff line number Diff line change
Expand Up @@ -637,6 +637,18 @@ int other_head_refs(struct repository *repo,
return ret;
}

static const char *get_worktree_id(const char *dotgit_contents)
{
const char *slash = find_last_dir_sep(dotgit_contents);
const char *prefix = "/worktrees";
int prefixlen = strlen(prefix);
if (!slash ||
slash - dotgit_contents < prefixlen ||
strncmp(slash - prefixlen, prefix, prefixlen))
return "";
return slash + 1;
}

/*
* Repair worktree's /path/to/worktree/.git file if missing, corrupt, or not
* pointing at <repo>/worktrees/<id>.
Expand Down Expand Up @@ -684,8 +696,10 @@ static void repair_gitfile(struct worktree *wt,
if (err == READ_GITFILE_ERR_NOT_A_FILE ||
err == READ_GITFILE_ERR_IS_A_DIR)
fn(1, wt->path, _(".git is not a file"), cb_data);
else if (err || !is_git_directory(backlink.buf))
else if (err)
repair = _(".git file broken");
else if (strcmp(get_worktree_id(dotgit_contents), wt->id))
fn(1, wt->path, _("unrelated .git file"), cb_data);
else if (fspathcmp(backlink.buf, repo.buf))
repair = _(".git file incorrect");
else if (use_relative_paths == is_absolute_path(dotgit_contents))
Expand Down Expand Up @@ -798,30 +812,20 @@ static int is_main_worktree_path(struct repository *repo, const char *path)
* Returns -1 on failure and strbuf.len on success.
*/
static ssize_t infer_backlink(struct repository *repo,
const char *gitfile,
const char *dotgit_contents,
struct strbuf *inferred)
{
struct strbuf actual = STRBUF_INIT;
const char *id;

if (strbuf_read_file(&actual, gitfile, 0) < 0)
goto error;
if (!starts_with(actual.buf, "gitdir:"))
goto error;
if (!(id = find_last_dir_sep(actual.buf)))
goto error;
strbuf_trim(&actual);
id++; /* advance past '/' to point at <id> */
id = get_worktree_id(dotgit_contents);
if (!*id)
goto error;
repo_common_path_replace(repo, inferred, "worktrees/%s", id);
if (!is_directory(inferred->buf))
if (!is_git_directory(inferred->buf))
goto error;

strbuf_release(&actual);
return inferred->len;
error:
strbuf_release(&actual);
strbuf_reset(inferred); /* clear invalid path */
return -1;
}
Expand All @@ -840,7 +844,8 @@ void repair_worktree_at_path(struct repository *repo,
struct strbuf inferred_backlink = STRBUF_INIT;
struct strbuf gitdir = STRBUF_INIT;
struct strbuf olddotgit = STRBUF_INIT;
char *dotgit_contents = NULL;
struct strbuf contents = STRBUF_INIT;
const char *dotgit_contents = NULL;
const char *repair = NULL;
int err;

Expand All @@ -856,53 +861,55 @@ void repair_worktree_at_path(struct repository *repo,
goto done;
}

infer_backlink(repo, dotgit.buf, &inferred_backlink);
strbuf_realpath_forgiving(&inferred_backlink, inferred_backlink.buf, 0);
dotgit_contents = xstrdup_or_null(read_gitfile_gently(dotgit.buf, &err));
if (dotgit_contents) {
strbuf_addstr(&backlink, dotgit_contents);
} else if (err == READ_GITFILE_ERR_NOT_A_FILE ||
err == READ_GITFILE_ERR_IS_A_DIR) {
err = read_gitfile_raw(&contents, dotgit.buf);
if (err == READ_GITFILE_ERR_NOT_A_FILE ||
err == READ_GITFILE_ERR_IS_A_DIR) {
fn(1, dotgit.buf, _("unable to locate repository; .git is not a file"), cb_data);
goto done;
} else if (err == READ_GITFILE_ERR_NOT_A_REPO) {
if (inferred_backlink.len) {
/*
* Worktree's .git file does not point at a repository
* but we found a .git/worktrees/<id> in this
* repository with the same <id> as recorded in the
* worktree's .git file so make the worktree point at
* the discovered .git/worktrees/<id>.
*/
strbuf_swap(&backlink, &inferred_backlink);
} else {
fn(1, dotgit.buf, _("unable to locate repository; .git file does not reference a repository"), cb_data);
goto done;
}
} else {
} else if (err) {
fn(1, dotgit.buf, _("unable to locate repository; .git file broken"), cb_data);
goto done;
}

dotgit_contents = contents.buf;
infer_backlink(repo, dotgit_contents, &inferred_backlink);
strbuf_realpath_forgiving(&inferred_backlink, inferred_backlink.buf, 0);

if (is_absolute_path(dotgit_contents)) {
strbuf_addstr(&backlink, dotgit_contents);
} else {
strbuf_addbuf(&backlink, &dotgit);
strbuf_strip_suffix(&backlink, ".git");
strbuf_addstr(&backlink, dotgit_contents);
strbuf_realpath_forgiving(&backlink, backlink.buf, 0);
}

if (!is_git_directory(backlink.buf) && !inferred_backlink.len) {
fn(1, dotgit.buf, _("unable to locate repository; .git file does not reference a repository"), cb_data);
goto done;
}
if (!inferred_backlink.len) {
fn(1, dotgit.buf, _("unable to locate repository; .git file is not for a linked worktree"), cb_data);
goto done;
}

/*
* If we got this far, either the worktree's .git file pointed at a
* valid repository (i.e. read_gitfile_gently() returned success) or
* valid repository (i.e. is_git_directory() returned true) or
* the .git file did not point at a repository but we were able to
* infer a suitable new value for the .git file by locating a
* .git/worktrees/<id> in *this* repository corresponding to the <id>
* recorded in the worktree's .git file.
*
* However, if, at this point, inferred_backlink is non-NULL (i.e. we
* found a suitable .git/worktrees/<id> in *this* repository) *and* the
* worktree's .git file points at a valid repository *and* those two
* paths differ, then that indicates that the user probably *copied*
* the main and linked worktrees to a new location as a unit rather
* than *moving* them. Thus, the copied worktree's .git file actually
* points at the .git/worktrees/<id> in the *original* repository, not
* in the "copy" repository. In this case, point the "copy" worktree's
* .git file at the "copy" repository.
* Even if the worktree's .git file pointed at a valid repository,
* it doesn't always mean that the backlink is correct. For example,
* the user might have *copied* the main and linked worktrees to a
* new location as a unit rather than *moving* them (the copied
* worktree's .git file actually points at the .git/worktrees/<id>
* in the *original* repository, not in the "copy" repository).
* Therefore, we prioritize inferred_backlink over backlink.
*/
if (inferred_backlink.len && fspathcmp(backlink.buf, inferred_backlink.buf))
if (fspathcmp(backlink.buf, inferred_backlink.buf))
strbuf_swap(&backlink, &inferred_backlink);

strbuf_addf(&gitdir, "%s/gitdir", backlink.buf);
Expand All @@ -926,12 +933,12 @@ void repair_worktree_at_path(struct repository *repo,
gitdir.buf, use_relative_paths);
}
done:
free(dotgit_contents);
strbuf_release(&olddotgit);
strbuf_release(&backlink);
strbuf_release(&inferred_backlink);
strbuf_release(&gitdir);
strbuf_release(&dotgit);
strbuf_release(&contents);
}

int should_prune_worktree(struct repository *repo,
Expand Down
Loading