git-clean: add support for -i/--interactive
[git] / builtin / clean.c
1 /*
2  * "git clean" builtin command
3  *
4  * Copyright (C) 2007 Shawn Bohrer
5  *
6  * Based on git-clean.sh by Pavel Roskin
7  */
8
9 #include "builtin.h"
10 #include "cache.h"
11 #include "dir.h"
12 #include "parse-options.h"
13 #include "refs.h"
14 #include "string-list.h"
15 #include "quote.h"
16
17 static int force = -1; /* unset */
18 static int interactive;
19 static struct string_list del_list = STRING_LIST_INIT_DUP;
20
21 static const char *const builtin_clean_usage[] = {
22         N_("git clean [-d] [-f] [-i] [-n] [-q] [-e <pattern>] [-x | -X] [--] <paths>..."),
23         NULL
24 };
25
26 static const char *msg_remove = N_("Removing %s\n");
27 static const char *msg_would_remove = N_("Would remove %s\n");
28 static const char *msg_skip_git_dir = N_("Skipping repository %s\n");
29 static const char *msg_would_skip_git_dir = N_("Would skip repository %s\n");
30 static const char *msg_warn_remove_failed = N_("failed to remove %s");
31
32 static int git_clean_config(const char *var, const char *value, void *cb)
33 {
34         if (!strcmp(var, "clean.requireforce"))
35                 force = !git_config_bool(var, value);
36         return git_default_config(var, value, cb);
37 }
38
39 static int exclude_cb(const struct option *opt, const char *arg, int unset)
40 {
41         struct string_list *exclude_list = opt->value;
42         string_list_append(exclude_list, arg);
43         return 0;
44 }
45
46 static int remove_dirs(struct strbuf *path, const char *prefix, int force_flag,
47                 int dry_run, int quiet, int *dir_gone)
48 {
49         DIR *dir;
50         struct strbuf quoted = STRBUF_INIT;
51         struct dirent *e;
52         int res = 0, ret = 0, gone = 1, original_len = path->len, len, i;
53         unsigned char submodule_head[20];
54         struct string_list dels = STRING_LIST_INIT_DUP;
55
56         *dir_gone = 1;
57
58         if ((force_flag & REMOVE_DIR_KEEP_NESTED_GIT) &&
59                         !resolve_gitlink_ref(path->buf, "HEAD", submodule_head)) {
60                 if (!quiet) {
61                         quote_path_relative(path->buf, prefix, &quoted);
62                         printf(dry_run ?  _(msg_would_skip_git_dir) : _(msg_skip_git_dir),
63                                         quoted.buf);
64                 }
65
66                 *dir_gone = 0;
67                 return 0;
68         }
69
70         dir = opendir(path->buf);
71         if (!dir) {
72                 /* an empty dir could be removed even if it is unreadble */
73                 res = dry_run ? 0 : rmdir(path->buf);
74                 if (res) {
75                         quote_path_relative(path->buf, prefix, &quoted);
76                         warning(_(msg_warn_remove_failed), quoted.buf);
77                         *dir_gone = 0;
78                 }
79                 return res;
80         }
81
82         if (path->buf[original_len - 1] != '/')
83                 strbuf_addch(path, '/');
84
85         len = path->len;
86         while ((e = readdir(dir)) != NULL) {
87                 struct stat st;
88                 if (is_dot_or_dotdot(e->d_name))
89                         continue;
90
91                 strbuf_setlen(path, len);
92                 strbuf_addstr(path, e->d_name);
93                 if (lstat(path->buf, &st))
94                         ; /* fall thru */
95                 else if (S_ISDIR(st.st_mode)) {
96                         if (remove_dirs(path, prefix, force_flag, dry_run, quiet, &gone))
97                                 ret = 1;
98                         if (gone) {
99                                 quote_path_relative(path->buf, prefix, &quoted);
100                                 string_list_append(&dels, quoted.buf);
101                         } else
102                                 *dir_gone = 0;
103                         continue;
104                 } else {
105                         res = dry_run ? 0 : unlink(path->buf);
106                         if (!res) {
107                                 quote_path_relative(path->buf, prefix, &quoted);
108                                 string_list_append(&dels, quoted.buf);
109                         } else {
110                                 quote_path_relative(path->buf, prefix, &quoted);
111                                 warning(_(msg_warn_remove_failed), quoted.buf);
112                                 *dir_gone = 0;
113                                 ret = 1;
114                         }
115                         continue;
116                 }
117
118                 /* path too long, stat fails, or non-directory still exists */
119                 *dir_gone = 0;
120                 ret = 1;
121                 break;
122         }
123         closedir(dir);
124
125         strbuf_setlen(path, original_len);
126
127         if (*dir_gone) {
128                 res = dry_run ? 0 : rmdir(path->buf);
129                 if (!res)
130                         *dir_gone = 1;
131                 else {
132                         quote_path_relative(path->buf, prefix, &quoted);
133                         warning(_(msg_warn_remove_failed), quoted.buf);
134                         *dir_gone = 0;
135                         ret = 1;
136                 }
137         }
138
139         if (!*dir_gone && !quiet) {
140                 for (i = 0; i < dels.nr; i++)
141                         printf(dry_run ?  _(msg_would_remove) : _(msg_remove), dels.items[i].string);
142         }
143         string_list_clear(&dels, 0);
144         return ret;
145 }
146
147 static void interactive_main_loop(void)
148 {
149         struct strbuf confirm = STRBUF_INIT;
150         struct strbuf buf = STRBUF_INIT;
151         struct string_list_item *item;
152         const char *qname;
153
154         while (del_list.nr) {
155                 putchar('\n');
156                 for_each_string_list_item(item, &del_list) {
157                         qname = quote_path_relative(item->string, NULL, &buf);
158                         printf(_(msg_would_remove), qname);
159                 }
160                 putchar('\n');
161
162                 printf(_("Remove [y/n]? "));
163                 if (strbuf_getline(&confirm, stdin, '\n') != EOF) {
164                         strbuf_trim(&confirm);
165                 } else {
166                         /* Ctrl-D is the same as "quit" */
167                         string_list_clear(&del_list, 0);
168                         putchar('\n');
169                         printf_ln("Bye.");
170                         break;
171                 }
172
173                 if (confirm.len) {
174                         if (!strncasecmp(confirm.buf, "yes", confirm.len)) {
175                                 break;
176                         } else if (!strncasecmp(confirm.buf, "no", confirm.len) ||
177                                    !strncasecmp(confirm.buf, "quit", confirm.len)) {
178                                 string_list_clear(&del_list, 0);
179                                 printf_ln("Bye.");
180                                 break;
181                         } else {
182                                 continue;
183                         }
184                 }
185         }
186
187         strbuf_release(&buf);
188         strbuf_release(&confirm);
189 }
190
191 int cmd_clean(int argc, const char **argv, const char *prefix)
192 {
193         int i, res;
194         int dry_run = 0, remove_directories = 0, quiet = 0, ignored = 0;
195         int ignored_only = 0, config_set = 0, errors = 0, gone = 1;
196         int rm_flags = REMOVE_DIR_KEEP_NESTED_GIT;
197         struct strbuf abs_path = STRBUF_INIT;
198         struct dir_struct dir;
199         static const char **pathspec;
200         struct strbuf buf = STRBUF_INIT;
201         struct string_list exclude_list = STRING_LIST_INIT_NODUP;
202         struct exclude_list *el;
203         struct string_list_item *item;
204         const char *qname;
205         char *seen = NULL;
206         struct option options[] = {
207                 OPT__QUIET(&quiet, N_("do not print names of files removed")),
208                 OPT__DRY_RUN(&dry_run, N_("dry run")),
209                 OPT__FORCE(&force, N_("force")),
210                 OPT_BOOL('i', "interactive", &interactive, N_("interactive cleaning")),
211                 OPT_BOOLEAN('d', NULL, &remove_directories,
212                                 N_("remove whole directories")),
213                 { OPTION_CALLBACK, 'e', "exclude", &exclude_list, N_("pattern"),
214                   N_("add <pattern> to ignore rules"), PARSE_OPT_NONEG, exclude_cb },
215                 OPT_BOOLEAN('x', NULL, &ignored, N_("remove ignored files, too")),
216                 OPT_BOOLEAN('X', NULL, &ignored_only,
217                                 N_("remove only ignored files")),
218                 OPT_END()
219         };
220
221         git_config(git_clean_config, NULL);
222         if (force < 0)
223                 force = 0;
224         else
225                 config_set = 1;
226
227         argc = parse_options(argc, argv, prefix, options, builtin_clean_usage,
228                              0);
229
230         memset(&dir, 0, sizeof(dir));
231         if (ignored_only)
232                 dir.flags |= DIR_SHOW_IGNORED;
233
234         if (ignored && ignored_only)
235                 die(_("-x and -X cannot be used together"));
236
237         if (!interactive && !dry_run && !force) {
238                 if (config_set)
239                         die(_("clean.requireForce set to true and neither -i, -n nor -f given; "
240                                   "refusing to clean"));
241                 else
242                         die(_("clean.requireForce defaults to true and neither -i, -n nor -f given; "
243                                   "refusing to clean"));
244         }
245
246         if (force > 1)
247                 rm_flags = 0;
248
249         dir.flags |= DIR_SHOW_OTHER_DIRECTORIES;
250
251         if (read_cache() < 0)
252                 die(_("index file corrupt"));
253
254         if (!ignored)
255                 setup_standard_excludes(&dir);
256
257         el = add_exclude_list(&dir, EXC_CMDL, "--exclude option");
258         for (i = 0; i < exclude_list.nr; i++)
259                 add_exclude(exclude_list.items[i].string, "", 0, el, -(i+1));
260
261         pathspec = get_pathspec(prefix, argv);
262
263         fill_directory(&dir, pathspec);
264
265         if (pathspec)
266                 seen = xmalloc(argc > 0 ? argc : 1);
267
268         for (i = 0; i < dir.nr; i++) {
269                 struct dir_entry *ent = dir.entries[i];
270                 int len, pos;
271                 int matches = 0;
272                 struct cache_entry *ce;
273                 struct stat st;
274                 const char *rel;
275
276                 /*
277                  * Remove the '/' at the end that directory
278                  * walking adds for directory entries.
279                  */
280                 len = ent->len;
281                 if (len && ent->name[len-1] == '/')
282                         len--;
283                 pos = cache_name_pos(ent->name, len);
284                 if (0 <= pos)
285                         continue;       /* exact match */
286                 pos = -pos - 1;
287                 if (pos < active_nr) {
288                         ce = active_cache[pos];
289                         if (ce_namelen(ce) == len &&
290                             !memcmp(ce->name, ent->name, len))
291                                 continue; /* Yup, this one exists unmerged */
292                 }
293
294                 if (lstat(ent->name, &st))
295                         die_errno("Cannot lstat '%s'", ent->name);
296
297                 if (pathspec) {
298                         memset(seen, 0, argc > 0 ? argc : 1);
299                         matches = match_pathspec(pathspec, ent->name, len,
300                                                  0, seen);
301                 }
302
303                 if (S_ISDIR(st.st_mode)) {
304                         if (remove_directories || (matches == MATCHED_EXACTLY)) {
305                                 rel = relative_path(ent->name, prefix, &buf);
306                                 string_list_append(&del_list, rel);
307                         }
308                 } else {
309                         if (pathspec && !matches)
310                                 continue;
311                         rel = relative_path(ent->name, prefix, &buf);
312                         string_list_append(&del_list, rel);
313                 }
314         }
315
316         if (interactive && del_list.nr > 0)
317                 interactive_main_loop();
318
319         for_each_string_list_item(item, &del_list) {
320                 struct stat st;
321
322                 if (prefix)
323                         strbuf_addstr(&abs_path, prefix);
324
325                 strbuf_addstr(&abs_path, item->string);
326
327                 /*
328                  * we might have removed this as part of earlier
329                  * recursive directory removal, so lstat() here could
330                  * fail with ENOENT.
331                  */
332                 if (lstat(abs_path.buf, &st))
333                         continue;
334
335                 if (S_ISDIR(st.st_mode)) {
336                         if (remove_dirs(&abs_path, prefix, rm_flags, dry_run, quiet, &gone))
337                                 errors++;
338                         if (gone && !quiet) {
339                                 qname = quote_path_relative(item->string, NULL, &buf);
340                                 printf(dry_run ? _(msg_would_remove) : _(msg_remove), qname);
341                         }
342                 } else {
343                         res = dry_run ? 0 : unlink(abs_path.buf);
344                         if (res) {
345                                 qname = quote_path_relative(item->string, NULL, &buf);
346                                 warning(_(msg_warn_remove_failed), qname);
347                                 errors++;
348                         } else if (!quiet) {
349                                 qname = quote_path_relative(item->string, NULL, &buf);
350                                 printf(dry_run ? _(msg_would_remove) : _(msg_remove), qname);
351                         }
352                 }
353                 strbuf_reset(&abs_path);
354         }
355         free(seen);
356
357         strbuf_release(&abs_path);
358         strbuf_release(&buf);
359         string_list_clear(&del_list, 0);
360         string_list_clear(&exclude_list, 0);
361         return (errors != 0);
362 }