git-clean: add colors to interactive git-clean
[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 #include "column.h"
17 #include "color.h"
18
19 static int force = -1; /* unset */
20 static int interactive;
21 static struct string_list del_list = STRING_LIST_INIT_DUP;
22 static unsigned int colopts;
23
24 static const char *const builtin_clean_usage[] = {
25         N_("git clean [-d] [-f] [-i] [-n] [-q] [-e <pattern>] [-x | -X] [--] <paths>..."),
26         NULL
27 };
28
29 static const char *msg_remove = N_("Removing %s\n");
30 static const char *msg_would_remove = N_("Would remove %s\n");
31 static const char *msg_skip_git_dir = N_("Skipping repository %s\n");
32 static const char *msg_would_skip_git_dir = N_("Would skip repository %s\n");
33 static const char *msg_warn_remove_failed = N_("failed to remove %s");
34
35 static int clean_use_color = -1;
36 static char clean_colors[][COLOR_MAXLEN] = {
37         GIT_COLOR_RESET,
38         GIT_COLOR_NORMAL,       /* PLAIN */
39         GIT_COLOR_BOLD_BLUE,    /* PROMPT */
40         GIT_COLOR_BOLD,         /* HEADER */
41         GIT_COLOR_BOLD_RED,     /* HELP */
42         GIT_COLOR_BOLD_RED,     /* ERROR */
43 };
44 enum color_clean {
45         CLEAN_COLOR_RESET = 0,
46         CLEAN_COLOR_PLAIN = 1,
47         CLEAN_COLOR_PROMPT = 2,
48         CLEAN_COLOR_HEADER = 3,
49         CLEAN_COLOR_HELP = 4,
50         CLEAN_COLOR_ERROR = 5,
51 };
52
53 static int parse_clean_color_slot(const char *var)
54 {
55         if (!strcasecmp(var, "reset"))
56                 return CLEAN_COLOR_RESET;
57         if (!strcasecmp(var, "plain"))
58                 return CLEAN_COLOR_PLAIN;
59         if (!strcasecmp(var, "prompt"))
60                 return CLEAN_COLOR_PROMPT;
61         if (!strcasecmp(var, "header"))
62                 return CLEAN_COLOR_HEADER;
63         if (!strcasecmp(var, "help"))
64                 return CLEAN_COLOR_HELP;
65         if (!strcasecmp(var, "error"))
66                 return CLEAN_COLOR_ERROR;
67         return -1;
68 }
69
70 static int git_clean_config(const char *var, const char *value, void *cb)
71 {
72         if (!prefixcmp(var, "column."))
73                 return git_column_config(var, value, "clean", &colopts);
74
75         /* honors the color.interactive* config variables which also
76            applied in git-add--interactive and git-stash */
77         if (!strcmp(var, "color.interactive")) {
78                 clean_use_color = git_config_colorbool(var, value);
79                 return 0;
80         }
81         if (!prefixcmp(var, "color.interactive.")) {
82                 int slot = parse_clean_color_slot(var +
83                                                   strlen("color.interactive."));
84                 if (slot < 0)
85                         return 0;
86                 if (!value)
87                         return config_error_nonbool(var);
88                 color_parse(value, var, clean_colors[slot]);
89                 return 0;
90         }
91
92         if (!strcmp(var, "clean.requireforce")) {
93                 force = !git_config_bool(var, value);
94                 return 0;
95         }
96
97         /* inspect the color.ui config variable and others */
98         return git_color_default_config(var, value, cb);
99 }
100
101 static const char *clean_get_color(enum color_clean ix)
102 {
103         if (want_color(clean_use_color))
104                 return clean_colors[ix];
105         return "";
106 }
107
108 static void clean_print_color(enum color_clean ix)
109 {
110         printf("%s", clean_get_color(ix));
111 }
112
113 static int exclude_cb(const struct option *opt, const char *arg, int unset)
114 {
115         struct string_list *exclude_list = opt->value;
116         string_list_append(exclude_list, arg);
117         return 0;
118 }
119
120 static int remove_dirs(struct strbuf *path, const char *prefix, int force_flag,
121                 int dry_run, int quiet, int *dir_gone)
122 {
123         DIR *dir;
124         struct strbuf quoted = STRBUF_INIT;
125         struct dirent *e;
126         int res = 0, ret = 0, gone = 1, original_len = path->len, len, i;
127         unsigned char submodule_head[20];
128         struct string_list dels = STRING_LIST_INIT_DUP;
129
130         *dir_gone = 1;
131
132         if ((force_flag & REMOVE_DIR_KEEP_NESTED_GIT) &&
133                         !resolve_gitlink_ref(path->buf, "HEAD", submodule_head)) {
134                 if (!quiet) {
135                         quote_path_relative(path->buf, prefix, &quoted);
136                         printf(dry_run ?  _(msg_would_skip_git_dir) : _(msg_skip_git_dir),
137                                         quoted.buf);
138                 }
139
140                 *dir_gone = 0;
141                 return 0;
142         }
143
144         dir = opendir(path->buf);
145         if (!dir) {
146                 /* an empty dir could be removed even if it is unreadble */
147                 res = dry_run ? 0 : rmdir(path->buf);
148                 if (res) {
149                         quote_path_relative(path->buf, prefix, &quoted);
150                         warning(_(msg_warn_remove_failed), quoted.buf);
151                         *dir_gone = 0;
152                 }
153                 return res;
154         }
155
156         if (path->buf[original_len - 1] != '/')
157                 strbuf_addch(path, '/');
158
159         len = path->len;
160         while ((e = readdir(dir)) != NULL) {
161                 struct stat st;
162                 if (is_dot_or_dotdot(e->d_name))
163                         continue;
164
165                 strbuf_setlen(path, len);
166                 strbuf_addstr(path, e->d_name);
167                 if (lstat(path->buf, &st))
168                         ; /* fall thru */
169                 else if (S_ISDIR(st.st_mode)) {
170                         if (remove_dirs(path, prefix, force_flag, dry_run, quiet, &gone))
171                                 ret = 1;
172                         if (gone) {
173                                 quote_path_relative(path->buf, prefix, &quoted);
174                                 string_list_append(&dels, quoted.buf);
175                         } else
176                                 *dir_gone = 0;
177                         continue;
178                 } else {
179                         res = dry_run ? 0 : unlink(path->buf);
180                         if (!res) {
181                                 quote_path_relative(path->buf, prefix, &quoted);
182                                 string_list_append(&dels, quoted.buf);
183                         } else {
184                                 quote_path_relative(path->buf, prefix, &quoted);
185                                 warning(_(msg_warn_remove_failed), quoted.buf);
186                                 *dir_gone = 0;
187                                 ret = 1;
188                         }
189                         continue;
190                 }
191
192                 /* path too long, stat fails, or non-directory still exists */
193                 *dir_gone = 0;
194                 ret = 1;
195                 break;
196         }
197         closedir(dir);
198
199         strbuf_setlen(path, original_len);
200
201         if (*dir_gone) {
202                 res = dry_run ? 0 : rmdir(path->buf);
203                 if (!res)
204                         *dir_gone = 1;
205                 else {
206                         quote_path_relative(path->buf, prefix, &quoted);
207                         warning(_(msg_warn_remove_failed), quoted.buf);
208                         *dir_gone = 0;
209                         ret = 1;
210                 }
211         }
212
213         if (!*dir_gone && !quiet) {
214                 for (i = 0; i < dels.nr; i++)
215                         printf(dry_run ?  _(msg_would_remove) : _(msg_remove), dels.items[i].string);
216         }
217         string_list_clear(&dels, 0);
218         return ret;
219 }
220
221 static void pretty_print_dels(void)
222 {
223         struct string_list list = STRING_LIST_INIT_DUP;
224         struct string_list_item *item;
225         struct strbuf buf = STRBUF_INIT;
226         const char *qname;
227         struct column_options copts;
228
229         for_each_string_list_item(item, &del_list) {
230                 qname = quote_path_relative(item->string, NULL, &buf);
231                 string_list_append(&list, qname);
232         }
233
234         /*
235          * always enable column display, we only consult column.*
236          * about layout strategy and stuff
237          */
238         colopts = (colopts & ~COL_ENABLE_MASK) | COL_ENABLED;
239         memset(&copts, 0, sizeof(copts));
240         copts.indent = "  ";
241         copts.padding = 2;
242         print_columns(&list, colopts, &copts);
243         putchar('\n');
244         strbuf_release(&buf);
245         string_list_clear(&list, 0);
246 }
247
248 static void interactive_main_loop(void)
249 {
250         struct strbuf confirm = STRBUF_INIT;
251
252         while (del_list.nr) {
253                 putchar('\n');
254                 clean_print_color(CLEAN_COLOR_HEADER);
255                 printf_ln(Q_("Would remove the following item:",
256                              "Would remove the following items:",
257                              del_list.nr));
258                 clean_print_color(CLEAN_COLOR_RESET);
259                 putchar('\n');
260
261                 pretty_print_dels();
262
263                 clean_print_color(CLEAN_COLOR_PROMPT);
264                 printf(_("Remove [y/n]? "));
265                 clean_print_color(CLEAN_COLOR_RESET);
266                 if (strbuf_getline(&confirm, stdin, '\n') != EOF) {
267                         strbuf_trim(&confirm);
268                 } else {
269                         /* Ctrl-D is the same as "quit" */
270                         string_list_clear(&del_list, 0);
271                         putchar('\n');
272                         printf_ln("Bye.");
273                         break;
274                 }
275
276                 if (confirm.len) {
277                         if (!strncasecmp(confirm.buf, "yes", confirm.len)) {
278                                 break;
279                         } else if (!strncasecmp(confirm.buf, "no", confirm.len) ||
280                                    !strncasecmp(confirm.buf, "quit", confirm.len)) {
281                                 string_list_clear(&del_list, 0);
282                                 printf_ln("Bye.");
283                                 break;
284                         } else {
285                                 continue;
286                         }
287                 }
288         }
289
290         strbuf_release(&confirm);
291 }
292
293 int cmd_clean(int argc, const char **argv, const char *prefix)
294 {
295         int i, res;
296         int dry_run = 0, remove_directories = 0, quiet = 0, ignored = 0;
297         int ignored_only = 0, config_set = 0, errors = 0, gone = 1;
298         int rm_flags = REMOVE_DIR_KEEP_NESTED_GIT;
299         struct strbuf abs_path = STRBUF_INIT;
300         struct dir_struct dir;
301         static const char **pathspec;
302         struct strbuf buf = STRBUF_INIT;
303         struct string_list exclude_list = STRING_LIST_INIT_NODUP;
304         struct exclude_list *el;
305         struct string_list_item *item;
306         const char *qname;
307         char *seen = NULL;
308         struct option options[] = {
309                 OPT__QUIET(&quiet, N_("do not print names of files removed")),
310                 OPT__DRY_RUN(&dry_run, N_("dry run")),
311                 OPT__FORCE(&force, N_("force")),
312                 OPT_BOOL('i', "interactive", &interactive, N_("interactive cleaning")),
313                 OPT_BOOLEAN('d', NULL, &remove_directories,
314                                 N_("remove whole directories")),
315                 { OPTION_CALLBACK, 'e', "exclude", &exclude_list, N_("pattern"),
316                   N_("add <pattern> to ignore rules"), PARSE_OPT_NONEG, exclude_cb },
317                 OPT_BOOLEAN('x', NULL, &ignored, N_("remove ignored files, too")),
318                 OPT_BOOLEAN('X', NULL, &ignored_only,
319                                 N_("remove only ignored files")),
320                 OPT_END()
321         };
322
323         git_config(git_clean_config, NULL);
324         if (force < 0)
325                 force = 0;
326         else
327                 config_set = 1;
328
329         argc = parse_options(argc, argv, prefix, options, builtin_clean_usage,
330                              0);
331
332         memset(&dir, 0, sizeof(dir));
333         if (ignored_only)
334                 dir.flags |= DIR_SHOW_IGNORED;
335
336         if (ignored && ignored_only)
337                 die(_("-x and -X cannot be used together"));
338
339         if (!interactive && !dry_run && !force) {
340                 if (config_set)
341                         die(_("clean.requireForce set to true and neither -i, -n nor -f given; "
342                                   "refusing to clean"));
343                 else
344                         die(_("clean.requireForce defaults to true and neither -i, -n nor -f given; "
345                                   "refusing to clean"));
346         }
347
348         if (force > 1)
349                 rm_flags = 0;
350
351         dir.flags |= DIR_SHOW_OTHER_DIRECTORIES;
352
353         if (read_cache() < 0)
354                 die(_("index file corrupt"));
355
356         if (!ignored)
357                 setup_standard_excludes(&dir);
358
359         el = add_exclude_list(&dir, EXC_CMDL, "--exclude option");
360         for (i = 0; i < exclude_list.nr; i++)
361                 add_exclude(exclude_list.items[i].string, "", 0, el, -(i+1));
362
363         pathspec = get_pathspec(prefix, argv);
364
365         fill_directory(&dir, pathspec);
366
367         if (pathspec)
368                 seen = xmalloc(argc > 0 ? argc : 1);
369
370         for (i = 0; i < dir.nr; i++) {
371                 struct dir_entry *ent = dir.entries[i];
372                 int len, pos;
373                 int matches = 0;
374                 struct cache_entry *ce;
375                 struct stat st;
376                 const char *rel;
377
378                 /*
379                  * Remove the '/' at the end that directory
380                  * walking adds for directory entries.
381                  */
382                 len = ent->len;
383                 if (len && ent->name[len-1] == '/')
384                         len--;
385                 pos = cache_name_pos(ent->name, len);
386                 if (0 <= pos)
387                         continue;       /* exact match */
388                 pos = -pos - 1;
389                 if (pos < active_nr) {
390                         ce = active_cache[pos];
391                         if (ce_namelen(ce) == len &&
392                             !memcmp(ce->name, ent->name, len))
393                                 continue; /* Yup, this one exists unmerged */
394                 }
395
396                 if (lstat(ent->name, &st))
397                         die_errno("Cannot lstat '%s'", ent->name);
398
399                 if (pathspec) {
400                         memset(seen, 0, argc > 0 ? argc : 1);
401                         matches = match_pathspec(pathspec, ent->name, len,
402                                                  0, seen);
403                 }
404
405                 if (S_ISDIR(st.st_mode)) {
406                         if (remove_directories || (matches == MATCHED_EXACTLY)) {
407                                 rel = relative_path(ent->name, prefix, &buf);
408                                 string_list_append(&del_list, rel);
409                         }
410                 } else {
411                         if (pathspec && !matches)
412                                 continue;
413                         rel = relative_path(ent->name, prefix, &buf);
414                         string_list_append(&del_list, rel);
415                 }
416         }
417
418         if (interactive && del_list.nr > 0)
419                 interactive_main_loop();
420
421         for_each_string_list_item(item, &del_list) {
422                 struct stat st;
423
424                 if (prefix)
425                         strbuf_addstr(&abs_path, prefix);
426
427                 strbuf_addstr(&abs_path, item->string);
428
429                 /*
430                  * we might have removed this as part of earlier
431                  * recursive directory removal, so lstat() here could
432                  * fail with ENOENT.
433                  */
434                 if (lstat(abs_path.buf, &st))
435                         continue;
436
437                 if (S_ISDIR(st.st_mode)) {
438                         if (remove_dirs(&abs_path, prefix, rm_flags, dry_run, quiet, &gone))
439                                 errors++;
440                         if (gone && !quiet) {
441                                 qname = quote_path_relative(item->string, NULL, &buf);
442                                 printf(dry_run ? _(msg_would_remove) : _(msg_remove), qname);
443                         }
444                 } else {
445                         res = dry_run ? 0 : unlink(abs_path.buf);
446                         if (res) {
447                                 qname = quote_path_relative(item->string, NULL, &buf);
448                                 warning(_(msg_warn_remove_failed), qname);
449                                 errors++;
450                         } else if (!quiet) {
451                                 qname = quote_path_relative(item->string, NULL, &buf);
452                                 printf(dry_run ? _(msg_would_remove) : _(msg_remove), qname);
453                         }
454                 }
455                 strbuf_reset(&abs_path);
456         }
457         free(seen);
458
459         strbuf_release(&abs_path);
460         strbuf_release(&buf);
461         string_list_clear(&del_list, 0);
462         string_list_clear(&exclude_list, 0);
463         return (errors != 0);
464 }