worktree: new place for "git prune --worktrees"

Commit 23af91d (prune: strategies for linked checkouts - 2014-11-30) adds "--worktrees" to "git prune" without realizing that "git prune" is for object database only. This patch moves the same functionality to a new command "git worktree". Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com>

Nguyễn Thái Ngọc Duy committed Jun 29, 2015 at 19:51 UTC df0b6cfbda88144714541664fb501146d6465a82
11 files changed +198 -114
.gitignore
+1
@@ -171,6 +171,7 @@
171 /git-verify-tag
172 /git-web--browse
173 /git-whatchanged
174 +/git-worktree
175 /git-write-tree
176 /git-core-*/?*
177 /gitweb/GITWEB-BUILD-OPTIONS
Documentation/git-prune.txt
-3
@@ -48,9 +48,6 @@ OPTIONS
48 --expire <time>::
49 Only expire loose objects older than <time>.
50
51 ---worktrees::
52 - Prune dead working tree information in $GIT_DIR/worktrees.
53 -
51 <head>...::
52 In addition to objects
53 reachable from any of our references, keep objects
Documentation/git-worktree.txt new
+48
@@ -0,0 +1,48 @@
1 +git-worktree(1)
2 +===============
3 +
4 +NAME
5 +----
6 +git-worktree - Manage multiple worktrees
7 +
8 +
9 +SYNOPSIS
10 +--------
11 +[verse]
12 +'git worktree prune' [-n] [-v] [--expire <expire>]
13 +
14 +DESCRIPTION
15 +-----------
16 +
17 +Manage multiple worktrees attached to the same repository. These are
18 +created by the command `git checkout --to`.
19 +
20 +COMMANDS
21 +--------
22 +prune::
23 +
24 +Prune working tree information in $GIT_DIR/worktrees.
25 +
26 +OPTIONS
27 +-------
28 +
29 +-n::
30 +--dry-run::
31 + Do not remove anything; just report what it would
32 + remove.
33 +
34 +-v::
35 +--verbose::
36 + Report all removals.
37 +
38 +--expire <time>::
39 + Only expire unused worktrees older than <time>.
40 +
41 +SEE ALSO
42 +--------
43 +
44 +linkgit:git-checkout[1]
45 +
46 +GIT
47 +---
48 +Part of the linkgit:git[1] suite
Makefile
+1
@@ -886,6 +886,7 @@ BUILTIN_OBJS += builtin/var.o
886 BUILTIN_OBJS += builtin/verify-commit.o
887 BUILTIN_OBJS += builtin/verify-pack.o
888 BUILTIN_OBJS += builtin/verify-tag.o
889 +BUILTIN_OBJS += builtin/worktree.o
890 BUILTIN_OBJS += builtin/write-tree.o
891
892 GITLIBS = $(LIB_FILE) $(XDIFF_LIB)
builtin.h
+1
@@ -133,6 +133,7 @@ extern int cmd_verify_commit(int argc, const char **argv, const char *prefix);
133 extern int cmd_verify_tag(int argc, const char **argv, const char *prefix);
134 extern int cmd_version(int argc, const char **argv, const char *prefix);
135 extern int cmd_whatchanged(int argc, const char **argv, const char *prefix);
136 +extern int cmd_worktree(int argc, const char **argv, const char *prefix);
137 extern int cmd_write_tree(int argc, const char **argv, const char *prefix);
138 extern int cmd_verify_pack(int argc, const char **argv, const char *prefix);
139 extern int cmd_show_ref(int argc, const char **argv, const char *prefix);
builtin/gc.c
+1 -1
@@ -293,7 +293,7 @@ int cmd_gc(int argc, const char **argv, const char *prefix)
293 argv_array_pushl(&reflog, "reflog", "expire", "--all", NULL);
294 argv_array_pushl(&repack, "repack", "-d", "-l", NULL);
295 argv_array_pushl(&prune, "prune", "--expire", NULL);
296 - argv_array_pushl(&prune_worktrees, "prune", "--worktrees", "--expire", NULL);
296 + argv_array_pushl(&prune_worktrees, "worktree", "prune", "--expire", NULL);
297 argv_array_pushl(&rerere, "rerere", "gc", NULL);
298
299 gc_config();
builtin/prune.c
-99
@@ -6,7 +6,6 @@
6 #include "reachable.h"
7 #include "parse-options.h"
8 #include "progress.h"
9 -#include "dir.h"
9
10 static const char * const prune_usage[] = {
11 N_("git prune [-n] [-v] [--expire <time>] [--] [<head>...]"),
@@ -76,95 +75,6 @@ static int prune_subdir(int nr, const char *path, void *data)
75 return 0;
76 }
77
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 - if (st.st_mtime <= expire) {
124 - strbuf_addf(reason, _("Removing worktrees/%s: gitdir file points to non-existent location"), id);
125 - return 1;
126 - } else {
127 - return 0;
128 - }
129 - }
130 - free(path);
131 - return 0;
132 -}
133 -
134 -static void prune_worktrees(void)
135 -{
136 - struct strbuf reason = STRBUF_INIT;
137 - struct strbuf path = STRBUF_INIT;
138 - DIR *dir = opendir(git_path("worktrees"));
139 - struct dirent *d;
140 - int ret;
141 - if (!dir)
142 - return;
143 - while ((d = readdir(dir)) != NULL) {
144 - if (!strcmp(d->d_name, ".") || !strcmp(d->d_name, ".."))
145 - continue;
146 - strbuf_reset(&reason);
147 - if (!prune_worktree(d->d_name, &reason))
148 - continue;
149 - if (show_only || verbose)
150 - printf("%s\n", reason.buf);
151 - if (show_only)
152 - continue;
153 - strbuf_reset(&path);
154 - strbuf_addstr(&path, git_path("worktrees/%s", d->d_name));
155 - ret = remove_dir_recursively(&path, 0);
156 - if (ret < 0 && errno == ENOTDIR)
157 - ret = unlink(path.buf);
158 - if (ret)
159 - error(_("failed to remove: %s"), strerror(errno));
160 - }
161 - closedir(dir);
162 - if (!show_only)
163 - rmdir(git_path("worktrees"));
164 - strbuf_release(&reason);
165 - strbuf_release(&path);
166 -}
167 -
78 /*
79 * Write errors (particularly out of space) can result in
80 * failed temporary packs (and more rarely indexes and other
@@ -191,12 +101,10 @@ int cmd_prune(int argc, const char **argv, const char *prefix)
101 {
102 struct rev_info revs;
103 struct progress *progress = NULL;
194 - int do_prune_worktrees = 0;
104 const struct option options[] = {
105 OPT__DRY_RUN(&show_only, N_("do not remove, show only")),
106 OPT__VERBOSE(&verbose, N_("report pruned objects")),
107 OPT_BOOL(0, "progress", &show_progress, N_("show progress")),
199 - OPT_BOOL(0, "worktrees", &do_prune_worktrees, N_("prune .git/worktrees")),
108 OPT_EXPIRY_DATE(0, "expire", &expire,
109 N_("expire objects older than <time>")),
110 OPT_END()
@@ -210,13 +118,6 @@ int cmd_prune(int argc, const char **argv, const char *prefix)
118
119 argc = parse_options(argc, argv, prefix, options, prune_usage, 0);
120
213 - if (do_prune_worktrees) {
214 - if (argc)
215 - die(_("--worktrees does not take extra arguments"));
216 - prune_worktrees();
217 - return 0;
218 - }
219 -
121 while (argc--) {
122 unsigned char sha1[20];
123 const char *name = *argv++;
builtin/worktree.c new
+133
@@ -0,0 +1,133 @@
1 +#include "cache.h"
2 +#include "builtin.h"
3 +#include "dir.h"
4 +#include "parse-options.h"
5 +
6 +static const char * const worktree_usage[] = {
7 + N_("git worktree prune [<options>]"),
8 + NULL
9 +};
10 +
11 +static int show_only;
12 +static int verbose;
13 +static unsigned long expire;
14 +
15 +static int prune_worktree(const char *id, struct strbuf *reason)
16 +{
17 + struct stat st;
18 + char *path;
19 + int fd, len;
20 +
21 + if (!is_directory(git_path("worktrees/%s", id))) {
22 + strbuf_addf(reason, _("Removing worktrees/%s: not a valid directory"), id);
23 + return 1;
24 + }
25 + if (file_exists(git_path("worktrees/%s/locked", id)))
26 + return 0;
27 + if (stat(git_path("worktrees/%s/gitdir", id), &st)) {
28 + strbuf_addf(reason, _("Removing worktrees/%s: gitdir file does not exist"), id);
29 + return 1;
30 + }
31 + fd = open(git_path("worktrees/%s/gitdir", id), O_RDONLY);
32 + if (fd < 0) {
33 + strbuf_addf(reason, _("Removing worktrees/%s: unable to read gitdir file (%s)"),
34 + id, strerror(errno));
35 + return 1;
36 + }
37 + len = st.st_size;
38 + path = xmalloc(len + 1);
39 + read_in_full(fd, path, len);
40 + close(fd);
41 + while (len && (path[len - 1] == '\n' || path[len - 1] == '\r'))
42 + len--;
43 + if (!len) {
44 + strbuf_addf(reason, _("Removing worktrees/%s: invalid gitdir file"), id);
45 + free(path);
46 + return 1;
47 + }
48 + path[len] = '\0';
49 + if (!file_exists(path)) {
50 + struct stat st_link;
51 + free(path);
52 + /*
53 + * the repo is moved manually and has not been
54 + * accessed since?
55 + */
56 + if (!stat(git_path("worktrees/%s/link", id), &st_link) &&
57 + st_link.st_nlink > 1)
58 + return 0;
59 + if (st.st_mtime <= expire) {
60 + strbuf_addf(reason, _("Removing worktrees/%s: gitdir file points to non-existent location"), id);
61 + return 1;
62 + } else {
63 + return 0;
64 + }
65 + }
66 + free(path);
67 + return 0;
68 +}
69 +
70 +static void prune_worktrees(void)
71 +{
72 + struct strbuf reason = STRBUF_INIT;
73 + struct strbuf path = STRBUF_INIT;
74 + DIR *dir = opendir(git_path("worktrees"));
75 + struct dirent *d;
76 + int ret;
77 + if (!dir)
78 + return;
79 + while ((d = readdir(dir)) != NULL) {
80 + if (!strcmp(d->d_name, ".") || !strcmp(d->d_name, ".."))
81 + continue;
82 + strbuf_reset(&reason);
83 + if (!prune_worktree(d->d_name, &reason))
84 + continue;
85 + if (show_only || verbose)
86 + printf("%s\n", reason.buf);
87 + if (show_only)
88 + continue;
89 + strbuf_reset(&path);
90 + strbuf_addstr(&path, git_path("worktrees/%s", d->d_name));
91 + ret = remove_dir_recursively(&path, 0);
92 + if (ret < 0 && errno == ENOTDIR)
93 + ret = unlink(path.buf);
94 + if (ret)
95 + error(_("failed to remove: %s"), strerror(errno));
96 + }
97 + closedir(dir);
98 + if (!show_only)
99 + rmdir(git_path("worktrees"));
100 + strbuf_release(&reason);
101 + strbuf_release(&path);
102 +}
103 +
104 +static int prune(int ac, const char **av, const char *prefix)
105 +{
106 + struct option options[] = {
107 + OPT__DRY_RUN(&show_only, N_("do not remove, show only")),
108 + OPT__VERBOSE(&verbose, N_("report pruned objects")),
109 + OPT_EXPIRY_DATE(0, "expire", &expire,
110 + N_("expire objects older than <time>")),
111 + OPT_END()
112 + };
113 +
114 + expire = ULONG_MAX;
115 + ac = parse_options(ac, av, prefix, options, worktree_usage, 0);
116 + if (ac)
117 + usage_with_options(worktree_usage, options);
118 + prune_worktrees();
119 + return 0;
120 +}
121 +
122 +int cmd_worktree(int ac, const char **av, const char *prefix)
123 +{
124 + struct option options[] = {
125 + OPT_END()
126 + };
127 +
128 + if (ac < 2)
129 + usage_with_options(worktree_usage, options);
130 + if (!strcmp(av[1], "prune"))
131 + return prune(ac - 1, av + 1, prefix);
132 + usage_with_options(worktree_usage, options);
133 +}
command-list.txt
+1
@@ -138,4 +138,5 @@ git-verify-pack plumbinginterrogators
138 git-verify-tag ancillaryinterrogators
139 gitweb ancillaryinterrogators
140 git-whatchanged ancillaryinterrogators
141 +git-worktree mainporcelain
142 git-write-tree plumbingmanipulators
git.c
+1
@@ -484,6 +484,7 @@ static struct cmd_struct commands[] = {
484 { "verify-tag", cmd_verify_tag, RUN_SETUP },
485 { "version", cmd_version },
486 { "whatchanged", cmd_whatchanged, RUN_SETUP },
487 + { "worktree", cmd_worktree, RUN_SETUP },
488 { "write-tree", cmd_write_tree, RUN_SETUP },
489 };
490
t/t2026-prune-linked-checkouts.sh
+11 -11
@@ -8,15 +8,15 @@ test_expect_success initialize '
8 git commit --allow-empty -m init
9 '
10
11 -test_expect_success 'prune --worktrees on normal repo' '
12 - git prune --worktrees &&
13 - test_must_fail git prune --worktrees abc
11 +test_expect_success 'worktree prune on normal repo' '
12 + git worktree prune &&
13 + test_must_fail git worktree prune abc
14 '
15
16 test_expect_success 'prune files inside $GIT_DIR/worktrees' '
17 mkdir .git/worktrees &&
18 : >.git/worktrees/abc &&
19 - git prune --worktrees --verbose >actual &&
19 + git worktree prune --verbose >actual &&
20 cat >expect <<EOF &&
21 Removing worktrees/abc: not a valid directory
22 EOF
@@ -31,7 +31,7 @@ test_expect_success 'prune directories without gitdir' '
31 cat >expect <<EOF &&
32 Removing worktrees/def: gitdir file does not exist
33 EOF
34 - git prune --worktrees --verbose >actual &&
34 + git worktree prune --verbose >actual &&
35 test_i18ncmp expect actual &&
36 ! test -d .git/worktrees/def &&
37 ! test -d .git/worktrees
@@ -42,7 +42,7 @@ test_expect_success SANITY 'prune directories with unreadable gitdir' '
42 : >.git/worktrees/def/def &&
43 : >.git/worktrees/def/gitdir &&
44 chmod u-r .git/worktrees/def/gitdir &&
45 - git prune --worktrees --verbose >actual &&
45 + git worktree prune --verbose >actual &&
46 test_i18ngrep "Removing worktrees/def: unable to read gitdir file" actual &&
47 ! test -d .git/worktrees/def &&
48 ! test -d .git/worktrees
@@ -52,7 +52,7 @@ test_expect_success 'prune directories with invalid gitdir' '
52 mkdir -p .git/worktrees/def/abc &&
53 : >.git/worktrees/def/def &&
54 : >.git/worktrees/def/gitdir &&
55 - git prune --worktrees --verbose >actual &&
55 + git worktree prune --verbose >actual &&
56 test_i18ngrep "Removing worktrees/def: invalid gitdir file" actual &&
57 ! test -d .git/worktrees/def &&
58 ! test -d .git/worktrees
@@ -62,7 +62,7 @@ test_expect_success 'prune directories with gitdir pointing to nowhere' '
62 mkdir -p .git/worktrees/def/abc &&
63 : >.git/worktrees/def/def &&
64 echo "$(pwd)"/nowhere >.git/worktrees/def/gitdir &&
65 - git prune --worktrees --verbose >actual &&
65 + git worktree prune --verbose >actual &&
66 test_i18ngrep "Removing worktrees/def: gitdir file points to non-existent location" actual &&
67 ! test -d .git/worktrees/def &&
68 ! test -d .git/worktrees
@@ -72,7 +72,7 @@ test_expect_success 'not prune locked checkout' '
72 test_when_finished rm -r .git/worktrees &&
73 mkdir -p .git/worktrees/ghi &&
74 : >.git/worktrees/ghi/locked &&
75 - git prune --worktrees &&
75 + git worktree prune &&
76 test -d .git/worktrees/ghi
77 '
78
@@ -82,14 +82,14 @@ test_expect_success 'not prune recent checkouts' '
82 mkdir -p .git/worktrees/jlm &&
83 echo "$(pwd)"/zz >.git/worktrees/jlm/gitdir &&
84 rmdir zz &&
85 - git prune --worktrees --verbose --expire=2.days.ago &&
85 + git worktree prune --verbose --expire=2.days.ago &&
86 test -d .git/worktrees/jlm
87 '
88
89 test_expect_success 'not prune proper checkouts' '
90 test_when_finished rm -r .git/worktrees &&
91 git checkout "--to=$PWD/nop" --detach master &&
92 - git prune --worktrees &&
92 + git worktree prune &&
93 test -d .git/worktrees/nop
94 '
95