Skip to content

Commit 23af91d

Browse files
pcloudsgitster
authored andcommitted
prune: strategies for linked checkouts
(alias R=$GIT_COMMON_DIR/worktrees/<id>) - linked checkouts are supposed to keep its location in $R/gitdir up to date. The use case is auto fixup after a manual checkout move. - linked checkouts are supposed to update mtime of $R/gitdir. If $R/gitdir's mtime is older than a limit, and it points to nowhere, worktrees/<id> is to be pruned. - If $R/locked exists, worktrees/<id> is not supposed to be pruned. If $R/locked exists and $R/gitdir's mtime is older than a really long limit, warn about old unused repo. - "git checkout --to" is supposed to make a hard link named $R/link pointing to the .git file on supported file systems to help detect the user manually deleting the checkout. If $R/link exists and its link count is greated than 1, the repo is kept. Helped-by: Marc Branchaud <[email protected]> Helped-by: Eric Sunshine <[email protected]> Helped-by: Johannes Sixt <[email protected]> Signed-off-by: Marc Branchaud <[email protected]> Signed-off-by: Nguyễn Thái Ngọc Duy <[email protected]> Signed-off-by: Junio C Hamano <[email protected]>
1 parent 529fef2 commit 23af91d

File tree

7 files changed

+251
-2
lines changed

7 files changed

+251
-2
lines changed

Documentation/git-checkout.txt

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -434,6 +434,26 @@ thumb is do not make any assumption about whether a path belongs to
434434
$GIT_DIR or $GIT_COMMON_DIR when you need to directly access something
435435
inside $GIT_DIR. Use `git rev-parse --git-path` to get the final path.
436436

437+
When you are done with a linked working tree you can simply delete it.
438+
You can clean up any stale $GIT_DIR/worktrees entries via `git prune
439+
--worktrees` in the main or any linked working tree.
440+
441+
If you move a linked working directory to another file system, or
442+
within a file system that does not support hard links, you need to run
443+
at least one git command inside the linked working directory
444+
(e.g. `git status`) in order to update its entry in $GIT_DIR/worktrees
445+
so that it does not get automatically removed.
446+
447+
To prevent `git prune --worktrees` from deleting a $GIT_DIR/worktrees
448+
entry (which can be useful in some situations, such as when the
449+
entry's working tree is stored on a portable device), add a file named
450+
'locked' to the entry's directory. The file contains the reason in
451+
plain text. For example, if a linked working tree's `.git` file points
452+
to `/path/main/.git/worktrees/test-next` then a file named
453+
`/path/main/.git/worktrees/test-next/locked` will prevent the
454+
`test-next` entry from being pruned. See
455+
linkgit:gitrepository-layout[5] for details.
456+
437457
EXAMPLES
438458
--------
439459

Documentation/git-prune.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ OPTIONS
4848
--expire <time>::
4949
Only expire loose objects older than <time>.
5050

51+
--worktrees::
52+
Prune dead working tree information in $GIT_DIR/worktrees.
53+
5154
<head>...::
5255
In addition to objects
5356
reachable from any of our references, keep objects

Documentation/gitrepository-layout.txt

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,25 @@ worktrees::
259259
$GIT_COMMON_DIR is set and "$GIT_COMMON_DIR/worktrees" will be
260260
used instead.
261261

262+
worktrees/<id>/gitdir::
263+
A text file containing the absolute path back to the .git file
264+
that points to here. This is used to check if the linked
265+
repository has been manually removed and there is no need to
266+
keep this directory any more. mtime of this file should be
267+
updated every time the linked repository is accessed.
268+
269+
worktrees/<id>/locked::
270+
If this file exists, the linked repository may be on a
271+
portable device and not available. It does not mean that the
272+
linked repository is gone and `worktrees/<id>` could be
273+
removed. The file's content contains a reason string on why
274+
the repository is locked.
275+
276+
worktrees/<id>/link::
277+
If this file exists, it is a hard link to the linked .git
278+
file. It is used to detect if the linked repository is
279+
manually removed.
280+
262281
SEE ALSO
263282
--------
264283
linkgit:git-init[1],

builtin/checkout.c

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -826,7 +826,7 @@ static int prepare_linked_checkout(const struct checkout_opts *opts,
826826
const char *path = opts->new_worktree, *name;
827827
struct stat st;
828828
struct child_process cp;
829-
int counter = 0, len;
829+
int counter = 0, len, ret;
830830

831831
if (!new->commit)
832832
die(_("no branch specified"));
@@ -857,11 +857,21 @@ static int prepare_linked_checkout(const struct checkout_opts *opts,
857857
if (mkdir(sb_repo.buf, 0777))
858858
die_errno(_("could not create directory of '%s'"), sb_repo.buf);
859859

860+
/*
861+
* lock the incomplete repo so prune won't delete it, unlock
862+
* after the preparation is over.
863+
*/
864+
strbuf_addf(&sb, "%s/locked", sb_repo.buf);
865+
write_file(sb.buf, 1, "initializing\n");
866+
860867
strbuf_addf(&sb_git, "%s/.git", path);
861868
if (safe_create_leading_directories_const(sb_git.buf))
862869
die_errno(_("could not create leading directories of '%s'"),
863870
sb_git.buf);
864871

872+
strbuf_reset(&sb);
873+
strbuf_addf(&sb, "%s/gitdir", sb_repo.buf);
874+
write_file(sb.buf, 1, "%s\n", real_path(sb_git.buf));
865875
write_file(sb_git.buf, 1, "gitdir: %s/worktrees/%s\n",
866876
real_path(get_git_common_dir()), name);
867877
/*
@@ -870,6 +880,7 @@ static int prepare_linked_checkout(const struct checkout_opts *opts,
870880
* value would do because this value will be ignored and
871881
* replaced at the next (real) checkout.
872882
*/
883+
strbuf_reset(&sb);
873884
strbuf_addf(&sb, "%s/HEAD", sb_repo.buf);
874885
write_file(sb.buf, 1, "%s\n", sha1_to_hex(new->commit->object.sha1));
875886
strbuf_reset(&sb);
@@ -885,7 +896,11 @@ static int prepare_linked_checkout(const struct checkout_opts *opts,
885896
memset(&cp, 0, sizeof(cp));
886897
cp.git_cmd = 1;
887898
cp.argv = opts->saved_argv;
888-
return run_command(&cp);
899+
ret = run_command(&cp);
900+
strbuf_reset(&sb);
901+
strbuf_addf(&sb, "%s/locked", sb_repo.buf);
902+
unlink_or_warn(sb.buf);
903+
return ret;
889904
}
890905

891906
static int git_checkout_config(const char *var, const char *value, void *cb)

builtin/prune.c

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,91 @@ static int prune_subdir(int nr, const char *path, void *data)
7676
return 0;
7777
}
7878

79+
static int prune_worktree(const char *id, struct strbuf *reason)
80+
{
81+
struct stat st;
82+
char *path;
83+
int fd, len;
84+
85+
if (!is_directory(git_path("worktrees/%s", id))) {
86+
strbuf_addf(reason, _("Removing worktrees/%s: not a valid directory"), id);
87+
return 1;
88+
}
89+
if (file_exists(git_path("worktrees/%s/locked", id)))
90+
return 0;
91+
if (stat(git_path("worktrees/%s/gitdir", id), &st)) {
92+
strbuf_addf(reason, _("Removing worktrees/%s: gitdir file does not exist"), id);
93+
return 1;
94+
}
95+
fd = open(git_path("worktrees/%s/gitdir", id), O_RDONLY);
96+
if (fd < 0) {
97+
strbuf_addf(reason, _("Removing worktrees/%s: unable to read gitdir file (%s)"),
98+
id, strerror(errno));
99+
return 1;
100+
}
101+
len = st.st_size;
102+
path = xmalloc(len + 1);
103+
read_in_full(fd, path, len);
104+
close(fd);
105+
while (len && (path[len - 1] == '\n' || path[len - 1] == '\r'))
106+
len--;
107+
if (!len) {
108+
strbuf_addf(reason, _("Removing worktrees/%s: invalid gitdir file"), id);
109+
free(path);
110+
return 1;
111+
}
112+
path[len] = '\0';
113+
if (!file_exists(path)) {
114+
struct stat st_link;
115+
free(path);
116+
/*
117+
* the repo is moved manually and has not been
118+
* accessed since?
119+
*/
120+
if (!stat(git_path("worktrees/%s/link", id), &st_link) &&
121+
st_link.st_nlink > 1)
122+
return 0;
123+
strbuf_addf(reason, _("Removing worktrees/%s: gitdir file points to non-existent location"), id);
124+
return 1;
125+
}
126+
free(path);
127+
return st.st_mtime <= expire;
128+
}
129+
130+
static void prune_worktrees(void)
131+
{
132+
struct strbuf reason = STRBUF_INIT;
133+
struct strbuf path = STRBUF_INIT;
134+
DIR *dir = opendir(git_path("worktrees"));
135+
struct dirent *d;
136+
int ret;
137+
if (!dir)
138+
return;
139+
while ((d = readdir(dir)) != NULL) {
140+
if (!strcmp(d->d_name, ".") || !strcmp(d->d_name, ".."))
141+
continue;
142+
strbuf_reset(&reason);
143+
if (!prune_worktree(d->d_name, &reason))
144+
continue;
145+
if (show_only || verbose)
146+
printf("%s\n", reason.buf);
147+
if (show_only)
148+
continue;
149+
strbuf_reset(&path);
150+
strbuf_addstr(&path, git_path("worktrees/%s", d->d_name));
151+
ret = remove_dir_recursively(&path, 0);
152+
if (ret < 0 && errno == ENOTDIR)
153+
ret = unlink(path.buf);
154+
if (ret)
155+
error(_("failed to remove: %s"), strerror(errno));
156+
}
157+
closedir(dir);
158+
if (!show_only)
159+
rmdir(git_path("worktrees"));
160+
strbuf_release(&reason);
161+
strbuf_release(&path);
162+
}
163+
79164
/*
80165
* Write errors (particularly out of space) can result in
81166
* failed temporary packs (and more rarely indexes and other
@@ -102,10 +187,12 @@ int cmd_prune(int argc, const char **argv, const char *prefix)
102187
{
103188
struct rev_info revs;
104189
struct progress *progress = NULL;
190+
int do_prune_worktrees = 0;
105191
const struct option options[] = {
106192
OPT__DRY_RUN(&show_only, N_("do not remove, show only")),
107193
OPT__VERBOSE(&verbose, N_("report pruned objects")),
108194
OPT_BOOL(0, "progress", &show_progress, N_("show progress")),
195+
OPT_BOOL(0, "worktrees", &do_prune_worktrees, N_("prune .git/worktrees")),
109196
OPT_EXPIRY_DATE(0, "expire", &expire,
110197
N_("expire objects older than <time>")),
111198
OPT_END()
@@ -118,6 +205,14 @@ int cmd_prune(int argc, const char **argv, const char *prefix)
118205
init_revisions(&revs, prefix);
119206

120207
argc = parse_options(argc, argv, prefix, options, prune_usage, 0);
208+
209+
if (do_prune_worktrees) {
210+
if (argc)
211+
die(_("--worktrees does not take extra arguments"));
212+
prune_worktrees();
213+
return 0;
214+
}
215+
121216
while (argc--) {
122217
unsigned char sha1[20];
123218
const char *name = *argv++;

setup.c

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,17 @@ static int check_repository_format_gently(const char *gitdir, int *nongit_ok)
390390
return ret;
391391
}
392392

393+
static void update_linked_gitdir(const char *gitfile, const char *gitdir)
394+
{
395+
struct strbuf path = STRBUF_INIT;
396+
struct stat st;
397+
398+
strbuf_addf(&path, "%s/gitfile", gitdir);
399+
if (stat(path.buf, &st) || st.st_mtime + 24 * 3600 < time(NULL))
400+
write_file(path.buf, 0, "%s\n", gitfile);
401+
strbuf_release(&path);
402+
}
403+
393404
/*
394405
* Try to read the location of the git directory from the .git file,
395406
* return path to git directory if found.
@@ -438,6 +449,8 @@ const char *read_gitfile(const char *path)
438449

439450
if (!is_git_directory(dir))
440451
die("Not a git repository: %s", dir);
452+
453+
update_linked_gitdir(path, dir);
441454
path = real_path(dir);
442455

443456
free(buf);

t/t2026-prune-linked-checkouts.sh

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
#!/bin/sh
2+
3+
test_description='prune $GIT_DIR/worktrees'
4+
5+
. ./test-lib.sh
6+
7+
test_expect_success 'prune --worktrees on normal repo' '
8+
git prune --worktrees &&
9+
test_must_fail git prune --worktrees abc
10+
'
11+
12+
test_expect_success 'prune files inside $GIT_DIR/worktrees' '
13+
mkdir .git/worktrees &&
14+
: >.git/worktrees/abc &&
15+
git prune --worktrees --verbose >actual &&
16+
cat >expect <<EOF &&
17+
Removing worktrees/abc: not a valid directory
18+
EOF
19+
test_i18ncmp expect actual &&
20+
! test -f .git/worktrees/abc &&
21+
! test -d .git/worktrees
22+
'
23+
24+
test_expect_success 'prune directories without gitdir' '
25+
mkdir -p .git/worktrees/def/abc &&
26+
: >.git/worktrees/def/def &&
27+
cat >expect <<EOF &&
28+
Removing worktrees/def: gitdir file does not exist
29+
EOF
30+
git prune --worktrees --verbose >actual &&
31+
test_i18ncmp expect actual &&
32+
! test -d .git/worktrees/def &&
33+
! test -d .git/worktrees
34+
'
35+
36+
test_expect_success POSIXPERM 'prune directories with unreadable gitdir' '
37+
mkdir -p .git/worktrees/def/abc &&
38+
: >.git/worktrees/def/def &&
39+
: >.git/worktrees/def/gitdir &&
40+
chmod u-r .git/worktrees/def/gitdir &&
41+
git prune --worktrees --verbose >actual &&
42+
test_i18ngrep "Removing worktrees/def: unable to read gitdir file" actual &&
43+
! test -d .git/worktrees/def &&
44+
! test -d .git/worktrees
45+
'
46+
47+
test_expect_success 'prune directories with invalid gitdir' '
48+
mkdir -p .git/worktrees/def/abc &&
49+
: >.git/worktrees/def/def &&
50+
: >.git/worktrees/def/gitdir &&
51+
git prune --worktrees --verbose >actual &&
52+
test_i18ngrep "Removing worktrees/def: invalid gitdir file" actual &&
53+
! test -d .git/worktrees/def &&
54+
! test -d .git/worktrees
55+
'
56+
57+
test_expect_success 'prune directories with gitdir pointing to nowhere' '
58+
mkdir -p .git/worktrees/def/abc &&
59+
: >.git/worktrees/def/def &&
60+
echo "$(pwd)"/nowhere >.git/worktrees/def/gitdir &&
61+
git prune --worktrees --verbose >actual &&
62+
test_i18ngrep "Removing worktrees/def: gitdir file points to non-existent location" actual &&
63+
! test -d .git/worktrees/def &&
64+
! test -d .git/worktrees
65+
'
66+
67+
test_expect_success 'not prune locked checkout' '
68+
test_when_finished rm -r .git/worktrees
69+
mkdir -p .git/worktrees/ghi &&
70+
: >.git/worktrees/ghi/locked &&
71+
git prune --worktrees &&
72+
test -d .git/worktrees/ghi
73+
'
74+
75+
test_expect_success 'not prune recent checkouts' '
76+
test_when_finished rm -r .git/worktrees
77+
mkdir zz &&
78+
mkdir -p .git/worktrees/jlm &&
79+
echo "$(pwd)"/zz >.git/worktrees/jlm/gitdir &&
80+
git prune --worktrees --verbose --expire=2.days.ago &&
81+
test -d .git/worktrees/jlm
82+
'
83+
84+
test_done

0 commit comments

Comments
 (0)