Skip to content

Commit eb7460f

Browse files
committed
Merge branch 'es/worktree-repair'
"git worktree" gained a "repair" subcommand to help users recover after moving the worktrees or repository manually without telling Git. Also, "git init --separate-git-dir" no longer corrupts administrative data related to linked worktrees. * es/worktree-repair: init: make --separate-git-dir work from within linked worktree init: teach --separate-git-dir to repair linked worktrees worktree: teach "repair" to fix outgoing links to worktrees worktree: teach "repair" to fix worktree back-links to main worktree worktree: add skeleton "repair" command
2 parents 1aadb47 + 59d876c commit eb7460f

File tree

7 files changed

+445
-2
lines changed

7 files changed

+445
-2
lines changed

Documentation/git-worktree.txt

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ SYNOPSIS
1515
'git worktree move' <worktree> <new-path>
1616
'git worktree prune' [-n] [-v] [--expire <expire>]
1717
'git worktree remove' [-f] <worktree>
18+
'git worktree repair' [<path>...]
1819
'git worktree unlock' <worktree>
1920

2021
DESCRIPTION
@@ -97,7 +98,10 @@ with `--reason`.
9798
move::
9899

99100
Move a working tree to a new location. Note that the main working tree
100-
or linked working trees containing submodules cannot be moved.
101+
or linked working trees containing submodules cannot be moved with this
102+
command. (The `git worktree repair` command, however, can reestablish
103+
the connection with linked working trees if you move the main working
104+
tree manually.)
101105

102106
prune::
103107

@@ -110,6 +114,23 @@ and no modification in tracked files) can be removed. Unclean working
110114
trees or ones with submodules can be removed with `--force`. The main
111115
working tree cannot be removed.
112116

117+
repair [<path>...]::
118+
119+
Repair working tree administrative files, if possible, if they have
120+
become corrupted or outdated due to external factors.
121+
+
122+
For instance, if the main working tree (or bare repository) is moved,
123+
linked working trees will be unable to locate it. Running `repair` in
124+
the main working tree will reestablish the connection from linked
125+
working trees back to the main working tree.
126+
+
127+
Similarly, if a linked working tree is moved without using `git worktree
128+
move`, the main working tree (or bare repository) will be unable to
129+
locate it. Running `repair` within the recently-moved working tree will
130+
reestablish the connection. If multiple linked working trees are moved,
131+
running `repair` from any working tree with each tree's new `<path>` as
132+
an argument, will reestablish the connection to all the specified paths.
133+
113134
unlock::
114135

115136
Unlock a working tree, allowing it to be pruned, moved or deleted.
@@ -303,7 +324,8 @@ in the entry's directory. For example, if a linked working tree is moved
303324
to `/newpath/test-next` and its `.git` file points to
304325
`/path/main/.git/worktrees/test-next`, then update
305326
`/path/main/.git/worktrees/test-next/gitdir` to reference `/newpath/test-next`
306-
instead.
327+
instead. Better yet, run `git worktree repair` to reestablish the connection
328+
automatically.
307329

308330
To prevent a `$GIT_DIR/worktrees` entry from being pruned (which
309331
can be useful in some situations, such as when the

builtin/init-db.c

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#include "builtin.h"
1010
#include "exec-cmd.h"
1111
#include "parse-options.h"
12+
#include "worktree.h"
1213

1314
#ifndef DEFAULT_GIT_TEMPLATE_DIR
1415
#define DEFAULT_GIT_TEMPLATE_DIR "/usr/share/git-core/templates"
@@ -364,6 +365,7 @@ static void separate_git_dir(const char *git_dir, const char *git_link)
364365

365366
if (rename(src, git_dir))
366367
die_errno(_("unable to move %s to %s"), src, git_dir);
368+
repair_worktrees(NULL, NULL);
367369
}
368370

369371
write_file(git_link, "gitdir: %s", git_dir);
@@ -640,6 +642,30 @@ int cmd_init_db(int argc, const char **argv, const char *prefix)
640642
if (!git_dir)
641643
git_dir = DEFAULT_GIT_DIR_ENVIRONMENT;
642644

645+
/*
646+
* When --separate-git-dir is used inside a linked worktree, take
647+
* care to ensure that the common .git/ directory is relocated, not
648+
* the worktree-specific .git/worktrees/<id>/ directory.
649+
*/
650+
if (real_git_dir) {
651+
int err;
652+
const char *p;
653+
struct strbuf sb = STRBUF_INIT;
654+
655+
p = read_gitfile_gently(git_dir, &err);
656+
if (p && get_common_dir(&sb, p)) {
657+
struct strbuf mainwt = STRBUF_INIT;
658+
659+
strbuf_addbuf(&mainwt, &sb);
660+
strbuf_strip_suffix(&mainwt, "/.git");
661+
if (chdir(mainwt.buf) < 0)
662+
die_errno(_("cannot chdir to %s"), mainwt.buf);
663+
strbuf_release(&mainwt);
664+
git_dir = strbuf_detach(&sb, NULL);
665+
}
666+
strbuf_release(&sb);
667+
}
668+
643669
if (is_bare_repository_cfg < 0)
644670
is_bare_repository_cfg = guess_repository_type(git_dir);
645671

builtin/worktree.c

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1028,6 +1028,34 @@ static int remove_worktree(int ac, const char **av, const char *prefix)
10281028
return ret;
10291029
}
10301030

1031+
static void report_repair(int iserr, const char *path, const char *msg, void *cb_data)
1032+
{
1033+
if (!iserr) {
1034+
printf_ln(_("repair: %s: %s"), msg, path);
1035+
} else {
1036+
int *exit_status = (int *)cb_data;
1037+
fprintf_ln(stderr, _("error: %s: %s"), msg, path);
1038+
*exit_status = 1;
1039+
}
1040+
}
1041+
1042+
static int repair(int ac, const char **av, const char *prefix)
1043+
{
1044+
const char **p;
1045+
const char *self[] = { ".", NULL };
1046+
struct option options[] = {
1047+
OPT_END()
1048+
};
1049+
int rc = 0;
1050+
1051+
ac = parse_options(ac, av, prefix, options, worktree_usage, 0);
1052+
repair_worktrees(report_repair, &rc);
1053+
p = ac > 0 ? av : self;
1054+
for (; *p; p++)
1055+
repair_worktree_at_path(*p, report_repair, &rc);
1056+
return rc;
1057+
}
1058+
10311059
int cmd_worktree(int ac, const char **av, const char *prefix)
10321060
{
10331061
struct option options[] = {
@@ -1054,5 +1082,7 @@ int cmd_worktree(int ac, const char **av, const char *prefix)
10541082
return move_worktree(ac - 1, av + 1, prefix);
10551083
if (!strcmp(av[1], "remove"))
10561084
return remove_worktree(ac - 1, av + 1, prefix);
1085+
if (!strcmp(av[1], "repair"))
1086+
return repair(ac - 1, av + 1, prefix);
10571087
usage_with_options(worktree_usage, options);
10581088
}

t/t0001-init.sh

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,15 @@ test_expect_success 'implicit bare & --separate-git-dir incompatible' '
329329
test_i18ngrep "incompatible" err
330330
'
331331

332+
test_expect_success 'bare & --separate-git-dir incompatible within worktree' '
333+
test_when_finished "rm -rf bare.git linkwt seprepo" &&
334+
test_commit gumby &&
335+
git clone --bare . bare.git &&
336+
git -C bare.git worktree add --detach ../linkwt &&
337+
test_must_fail git -C linkwt init --separate-git-dir seprepo 2>err &&
338+
test_i18ngrep "incompatible" err
339+
'
340+
332341
test_lazy_prereq GETCWD_IGNORES_PERMS '
333342
base=GETCWD_TEST_BASE_DIR &&
334343
mkdir -p $base/dir &&
@@ -405,6 +414,25 @@ test_expect_success SYMLINKS 're-init to move gitdir symlink' '
405414
test_path_is_dir realgitdir/refs
406415
'
407416

417+
sep_git_dir_worktree () {
418+
test_when_finished "rm -rf mainwt linkwt seprepo" &&
419+
git init mainwt &&
420+
test_commit -C mainwt gumby &&
421+
git -C mainwt worktree add --detach ../linkwt &&
422+
git -C "$1" init --separate-git-dir ../seprepo &&
423+
git -C mainwt rev-parse --git-common-dir >expect &&
424+
git -C linkwt rev-parse --git-common-dir >actual &&
425+
test_cmp expect actual
426+
}
427+
428+
test_expect_success 're-init to move gitdir with linked worktrees' '
429+
sep_git_dir_worktree mainwt
430+
'
431+
432+
test_expect_success 're-init to move gitdir within linked worktree' '
433+
sep_git_dir_worktree linkwt
434+
'
435+
408436
test_expect_success MINGW '.git hidden' '
409437
rm -rf newdir &&
410438
(

t/t2406-worktree-repair.sh

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
#!/bin/sh
2+
3+
test_description='test git worktree repair'
4+
5+
. ./test-lib.sh
6+
7+
test_expect_success setup '
8+
test_commit init
9+
'
10+
11+
test_expect_success 'skip missing worktree' '
12+
test_when_finished "git worktree prune" &&
13+
git worktree add --detach missing &&
14+
rm -rf missing &&
15+
git worktree repair >out 2>err &&
16+
test_must_be_empty out &&
17+
test_must_be_empty err
18+
'
19+
20+
test_expect_success 'worktree path not directory' '
21+
test_when_finished "git worktree prune" &&
22+
git worktree add --detach notdir &&
23+
rm -rf notdir &&
24+
>notdir &&
25+
test_must_fail git worktree repair >out 2>err &&
26+
test_must_be_empty out &&
27+
test_i18ngrep "not a directory" err
28+
'
29+
30+
test_expect_success "don't clobber .git repo" '
31+
test_when_finished "rm -rf repo && git worktree prune" &&
32+
git worktree add --detach repo &&
33+
rm -rf repo &&
34+
test_create_repo repo &&
35+
test_must_fail git worktree repair >out 2>err &&
36+
test_must_be_empty out &&
37+
test_i18ngrep ".git is not a file" err
38+
'
39+
40+
test_corrupt_gitfile () {
41+
butcher=$1 &&
42+
problem=$2 &&
43+
repairdir=${3:-.} &&
44+
test_when_finished 'rm -rf corrupt && git worktree prune' &&
45+
git worktree add --detach corrupt &&
46+
git -C corrupt rev-parse --absolute-git-dir >expect &&
47+
eval "$butcher" &&
48+
git -C "$repairdir" worktree repair >out 2>err &&
49+
test_i18ngrep "$problem" out &&
50+
test_must_be_empty err &&
51+
git -C corrupt rev-parse --absolute-git-dir >actual &&
52+
test_cmp expect actual
53+
}
54+
55+
test_expect_success 'repair missing .git file' '
56+
test_corrupt_gitfile "rm -f corrupt/.git" ".git file broken"
57+
'
58+
59+
test_expect_success 'repair bogus .git file' '
60+
test_corrupt_gitfile "echo \"gitdir: /nowhere\" >corrupt/.git" \
61+
".git file broken"
62+
'
63+
64+
test_expect_success 'repair incorrect .git file' '
65+
test_when_finished "rm -rf other && git worktree prune" &&
66+
test_create_repo other &&
67+
other=$(git -C other rev-parse --absolute-git-dir) &&
68+
test_corrupt_gitfile "echo \"gitdir: $other\" >corrupt/.git" \
69+
".git file incorrect"
70+
'
71+
72+
test_expect_success 'repair .git file from main/.git' '
73+
test_corrupt_gitfile "rm -f corrupt/.git" ".git file broken" .git
74+
'
75+
76+
test_expect_success 'repair .git file from linked worktree' '
77+
test_when_finished "rm -rf other && git worktree prune" &&
78+
git worktree add --detach other &&
79+
test_corrupt_gitfile "rm -f corrupt/.git" ".git file broken" other
80+
'
81+
82+
test_expect_success 'repair .git file from bare.git' '
83+
test_when_finished "rm -rf bare.git corrupt && git worktree prune" &&
84+
git clone --bare . bare.git &&
85+
git -C bare.git worktree add --detach ../corrupt &&
86+
git -C corrupt rev-parse --absolute-git-dir >expect &&
87+
rm -f corrupt/.git &&
88+
git -C bare.git worktree repair &&
89+
git -C corrupt rev-parse --absolute-git-dir >actual &&
90+
test_cmp expect actual
91+
'
92+
93+
test_expect_success 'invalid worktree path' '
94+
test_must_fail git worktree repair /notvalid >out 2>err &&
95+
test_must_be_empty out &&
96+
test_i18ngrep "not a valid path" err
97+
'
98+
99+
test_expect_success 'repo not found; .git not file' '
100+
test_when_finished "rm -rf not-a-worktree" &&
101+
test_create_repo not-a-worktree &&
102+
test_must_fail git worktree repair not-a-worktree >out 2>err &&
103+
test_must_be_empty out &&
104+
test_i18ngrep ".git is not a file" err
105+
'
106+
107+
test_expect_success 'repo not found; .git file broken' '
108+
test_when_finished "rm -rf orig moved && git worktree prune" &&
109+
git worktree add --detach orig &&
110+
echo /invalid >orig/.git &&
111+
mv orig moved &&
112+
test_must_fail git worktree repair moved >out 2>err &&
113+
test_must_be_empty out &&
114+
test_i18ngrep ".git file broken" err
115+
'
116+
117+
test_expect_success 'repair broken gitdir' '
118+
test_when_finished "rm -rf orig moved && git worktree prune" &&
119+
git worktree add --detach orig &&
120+
sed s,orig/\.git$,moved/.git, .git/worktrees/orig/gitdir >expect &&
121+
rm .git/worktrees/orig/gitdir &&
122+
mv orig moved &&
123+
git worktree repair moved >out 2>err &&
124+
test_cmp expect .git/worktrees/orig/gitdir &&
125+
test_i18ngrep "gitdir unreadable" out &&
126+
test_must_be_empty err
127+
'
128+
129+
test_expect_success 'repair incorrect gitdir' '
130+
test_when_finished "rm -rf orig moved && git worktree prune" &&
131+
git worktree add --detach orig &&
132+
sed s,orig/\.git$,moved/.git, .git/worktrees/orig/gitdir >expect &&
133+
mv orig moved &&
134+
git worktree repair moved >out 2>err &&
135+
test_cmp expect .git/worktrees/orig/gitdir &&
136+
test_i18ngrep "gitdir incorrect" out &&
137+
test_must_be_empty err
138+
'
139+
140+
test_expect_success 'repair gitdir (implicit) from linked worktree' '
141+
test_when_finished "rm -rf orig moved && git worktree prune" &&
142+
git worktree add --detach orig &&
143+
sed s,orig/\.git$,moved/.git, .git/worktrees/orig/gitdir >expect &&
144+
mv orig moved &&
145+
git -C moved worktree repair >out 2>err &&
146+
test_cmp expect .git/worktrees/orig/gitdir &&
147+
test_i18ngrep "gitdir incorrect" out &&
148+
test_must_be_empty err
149+
'
150+
151+
test_expect_success 'unable to repair gitdir (implicit) from main worktree' '
152+
test_when_finished "rm -rf orig moved && git worktree prune" &&
153+
git worktree add --detach orig &&
154+
cat .git/worktrees/orig/gitdir >expect &&
155+
mv orig moved &&
156+
git worktree repair >out 2>err &&
157+
test_cmp expect .git/worktrees/orig/gitdir &&
158+
test_must_be_empty out &&
159+
test_must_be_empty err
160+
'
161+
162+
test_expect_success 'repair multiple gitdir files' '
163+
test_when_finished "rm -rf orig1 orig2 moved1 moved2 &&
164+
git worktree prune" &&
165+
git worktree add --detach orig1 &&
166+
git worktree add --detach orig2 &&
167+
sed s,orig1/\.git$,moved1/.git, .git/worktrees/orig1/gitdir >expect1 &&
168+
sed s,orig2/\.git$,moved2/.git, .git/worktrees/orig2/gitdir >expect2 &&
169+
mv orig1 moved1 &&
170+
mv orig2 moved2 &&
171+
git worktree repair moved1 moved2 >out 2>err &&
172+
test_cmp expect1 .git/worktrees/orig1/gitdir &&
173+
test_cmp expect2 .git/worktrees/orig2/gitdir &&
174+
test_i18ngrep "gitdir incorrect:.*orig1/gitdir$" out &&
175+
test_i18ngrep "gitdir incorrect:.*orig2/gitdir$" out &&
176+
test_must_be_empty err
177+
'
178+
179+
test_done

0 commit comments

Comments
 (0)