worktree: add -b/-B options
[git] / builtin / worktree.c
1 #include "cache.h"
2 #include "builtin.h"
3 #include "dir.h"
4 #include "parse-options.h"
5 #include "argv-array.h"
6 #include "run-command.h"
7
8 static const char * const worktree_usage[] = {
9         N_("git worktree add [<options>] <path> <branch>"),
10         N_("git worktree prune [<options>]"),
11         NULL
12 };
13
14 static int show_only;
15 static int verbose;
16 static unsigned long expire;
17
18 static int prune_worktree(const char *id, struct strbuf *reason)
19 {
20         struct stat st;
21         char *path;
22         int fd, len;
23
24         if (!is_directory(git_path("worktrees/%s", id))) {
25                 strbuf_addf(reason, _("Removing worktrees/%s: not a valid directory"), id);
26                 return 1;
27         }
28         if (file_exists(git_path("worktrees/%s/locked", id)))
29                 return 0;
30         if (stat(git_path("worktrees/%s/gitdir", id), &st)) {
31                 strbuf_addf(reason, _("Removing worktrees/%s: gitdir file does not exist"), id);
32                 return 1;
33         }
34         fd = open(git_path("worktrees/%s/gitdir", id), O_RDONLY);
35         if (fd < 0) {
36                 strbuf_addf(reason, _("Removing worktrees/%s: unable to read gitdir file (%s)"),
37                             id, strerror(errno));
38                 return 1;
39         }
40         len = st.st_size;
41         path = xmalloc(len + 1);
42         read_in_full(fd, path, len);
43         close(fd);
44         while (len && (path[len - 1] == '\n' || path[len - 1] == '\r'))
45                 len--;
46         if (!len) {
47                 strbuf_addf(reason, _("Removing worktrees/%s: invalid gitdir file"), id);
48                 free(path);
49                 return 1;
50         }
51         path[len] = '\0';
52         if (!file_exists(path)) {
53                 struct stat st_link;
54                 free(path);
55                 /*
56                  * the repo is moved manually and has not been
57                  * accessed since?
58                  */
59                 if (!stat(git_path("worktrees/%s/link", id), &st_link) &&
60                     st_link.st_nlink > 1)
61                         return 0;
62                 if (st.st_mtime <= expire) {
63                         strbuf_addf(reason, _("Removing worktrees/%s: gitdir file points to non-existent location"), id);
64                         return 1;
65                 } else {
66                         return 0;
67                 }
68         }
69         free(path);
70         return 0;
71 }
72
73 static void prune_worktrees(void)
74 {
75         struct strbuf reason = STRBUF_INIT;
76         struct strbuf path = STRBUF_INIT;
77         DIR *dir = opendir(git_path("worktrees"));
78         struct dirent *d;
79         int ret;
80         if (!dir)
81                 return;
82         while ((d = readdir(dir)) != NULL) {
83                 if (!strcmp(d->d_name, ".") || !strcmp(d->d_name, ".."))
84                         continue;
85                 strbuf_reset(&reason);
86                 if (!prune_worktree(d->d_name, &reason))
87                         continue;
88                 if (show_only || verbose)
89                         printf("%s\n", reason.buf);
90                 if (show_only)
91                         continue;
92                 strbuf_reset(&path);
93                 strbuf_addstr(&path, git_path("worktrees/%s", d->d_name));
94                 ret = remove_dir_recursively(&path, 0);
95                 if (ret < 0 && errno == ENOTDIR)
96                         ret = unlink(path.buf);
97                 if (ret)
98                         error(_("failed to remove: %s"), strerror(errno));
99         }
100         closedir(dir);
101         if (!show_only)
102                 rmdir(git_path("worktrees"));
103         strbuf_release(&reason);
104         strbuf_release(&path);
105 }
106
107 static int prune(int ac, const char **av, const char *prefix)
108 {
109         struct option options[] = {
110                 OPT__DRY_RUN(&show_only, N_("do not remove, show only")),
111                 OPT__VERBOSE(&verbose, N_("report pruned objects")),
112                 OPT_EXPIRY_DATE(0, "expire", &expire,
113                                 N_("expire objects older than <time>")),
114                 OPT_END()
115         };
116
117         expire = ULONG_MAX;
118         ac = parse_options(ac, av, prefix, options, worktree_usage, 0);
119         if (ac)
120                 usage_with_options(worktree_usage, options);
121         prune_worktrees();
122         return 0;
123 }
124
125 static int add(int ac, const char **av, const char *prefix)
126 {
127         struct child_process c;
128         int force = 0, detach = 0;
129         const char *new_branch = NULL, *new_branch_force = NULL;
130         const char *path, *branch;
131         struct argv_array cmd = ARGV_ARRAY_INIT;
132         struct option options[] = {
133                 OPT__FORCE(&force, N_("checkout <branch> even if already checked out in other worktree")),
134                 OPT_STRING('b', NULL, &new_branch, N_("branch"),
135                            N_("create a new branch")),
136                 OPT_STRING('B', NULL, &new_branch_force, N_("branch"),
137                            N_("create or reset a branch")),
138                 OPT_BOOL(0, "detach", &detach, N_("detach HEAD at named commit")),
139                 OPT_END()
140         };
141
142         ac = parse_options(ac, av, prefix, options, worktree_usage, 0);
143         if (new_branch && new_branch_force)
144                 die(_("-b and -B are mutually exclusive"));
145         if (ac != 2)
146                 usage_with_options(worktree_usage, options);
147
148         path = prefix ? prefix_filename(prefix, strlen(prefix), av[0]) : av[0];
149         branch = av[1];
150
151         argv_array_push(&cmd, "checkout");
152         argv_array_pushl(&cmd, "--to", path, NULL);
153         if (force)
154                 argv_array_push(&cmd, "--ignore-other-worktrees");
155         if (new_branch)
156                 argv_array_pushl(&cmd, "-b", new_branch, NULL);
157         if (new_branch_force)
158                 argv_array_pushl(&cmd, "-B", new_branch_force, NULL);
159         if (detach)
160                 argv_array_push(&cmd, "--detach");
161         argv_array_push(&cmd, branch);
162
163         memset(&c, 0, sizeof(c));
164         c.git_cmd = 1;
165         c.argv = cmd.argv;
166         return run_command(&c);
167 }
168
169 int cmd_worktree(int ac, const char **av, const char *prefix)
170 {
171         struct option options[] = {
172                 OPT_END()
173         };
174
175         if (ac < 2)
176                 usage_with_options(worktree_usage, options);
177         if (!strcmp(av[1], "add"))
178                 return add(ac - 1, av + 1, prefix);
179         if (!strcmp(av[1], "prune"))
180                 return prune(ac - 1, av + 1, prefix);
181         usage_with_options(worktree_usage, options);
182 }