git-clean: add colors to interactive git-clean
[git/git.git] / builtin / clean.c
CommitLineData
113f10f2
SB
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"
f538a91e 13#include "refs.h"
07de4eba 14#include "string-list.h"
1fb32894 15#include "quote.h"
1b8fd467 16#include "column.h"
7a9b0b80 17#include "color.h"
113f10f2 18
625db1b7 19static int force = -1; /* unset */
17696002 20static int interactive;
396049e5 21static struct string_list del_list = STRING_LIST_INIT_DUP;
1b8fd467 22static unsigned int colopts;
113f10f2
SB
23
24static const char *const builtin_clean_usage[] = {
17696002 25 N_("git clean [-d] [-f] [-i] [-n] [-q] [-e <pattern>] [-x | -X] [--] <paths>..."),
113f10f2
SB
26 NULL
27};
28
f538a91e
ZK
29static const char *msg_remove = N_("Removing %s\n");
30static const char *msg_would_remove = N_("Would remove %s\n");
31static const char *msg_skip_git_dir = N_("Skipping repository %s\n");
32static const char *msg_would_skip_git_dir = N_("Would skip repository %s\n");
33static const char *msg_warn_remove_failed = N_("failed to remove %s");
34
7a9b0b80
JX
35static int clean_use_color = -1;
36static 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};
44enum 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
53static 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
ef90d6d4 70static int git_clean_config(const char *var, const char *value, void *cb)
113f10f2 71{
1b8fd467
JX
72 if (!prefixcmp(var, "column."))
73 return git_column_config(var, value, "clean", &colopts);
74
7a9b0b80
JX
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
1b8fd467 92 if (!strcmp(var, "clean.requireforce")) {
113f10f2 93 force = !git_config_bool(var, value);
1b8fd467
JX
94 return 0;
95 }
7a9b0b80
JX
96
97 /* inspect the color.ui config variable and others */
98 return git_color_default_config(var, value, cb);
99}
100
101static 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
108static void clean_print_color(enum color_clean ix)
109{
110 printf("%s", clean_get_color(ix));
113f10f2
SB
111}
112
07de4eba
JH
113static 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
f538a91e
ZK
120static 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) {
39598f99 135 quote_path_relative(path->buf, prefix, &quoted);
f538a91e
ZK
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) {
39598f99 149 quote_path_relative(path->buf, prefix, &quoted);
f538a91e
ZK
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) {
39598f99 173 quote_path_relative(path->buf, prefix, &quoted);
f538a91e
ZK
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) {
39598f99 181 quote_path_relative(path->buf, prefix, &quoted);
f538a91e
ZK
182 string_list_append(&dels, quoted.buf);
183 } else {
39598f99 184 quote_path_relative(path->buf, prefix, &quoted);
f538a91e
ZK
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 {
39598f99 206 quote_path_relative(path->buf, prefix, &quoted);
f538a91e
ZK
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
1b8fd467 221static void pretty_print_dels(void)
17696002 222{
1b8fd467 223 struct string_list list = STRING_LIST_INIT_DUP;
17696002 224 struct string_list_item *item;
1b8fd467 225 struct strbuf buf = STRBUF_INIT;
17696002 226 const char *qname;
1b8fd467
JX
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
248static void interactive_main_loop(void)
249{
250 struct strbuf confirm = STRBUF_INIT;
17696002
JX
251
252 while (del_list.nr) {
253 putchar('\n');
7a9b0b80 254 clean_print_color(CLEAN_COLOR_HEADER);
1b8fd467
JX
255 printf_ln(Q_("Would remove the following item:",
256 "Would remove the following items:",
257 del_list.nr));
7a9b0b80 258 clean_print_color(CLEAN_COLOR_RESET);
17696002
JX
259 putchar('\n');
260
1b8fd467
JX
261 pretty_print_dels();
262
7a9b0b80 263 clean_print_color(CLEAN_COLOR_PROMPT);
17696002 264 printf(_("Remove [y/n]? "));
7a9b0b80 265 clean_print_color(CLEAN_COLOR_RESET);
17696002
JX
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
17696002
JX
290 strbuf_release(&confirm);
291}
292
113f10f2
SB
293int cmd_clean(int argc, const char **argv, const char *prefix)
294{
f538a91e
ZK
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;
a0f4afbe 298 int rm_flags = REMOVE_DIR_KEEP_NESTED_GIT;
396049e5 299 struct strbuf abs_path = STRBUF_INIT;
113f10f2 300 struct dir_struct dir;
113f10f2 301 static const char **pathspec;
f285a2d7 302 struct strbuf buf = STRBUF_INIT;
bdab6a59 303 struct string_list exclude_list = STRING_LIST_INIT_NODUP;
72aeb187 304 struct exclude_list *el;
396049e5 305 struct string_list_item *item;
1fb32894 306 const char *qname;
d871c869 307 char *seen = NULL;
113f10f2 308 struct option options[] = {
145f9c81 309 OPT__QUIET(&quiet, N_("do not print names of files removed")),
f538a91e 310 OPT__DRY_RUN(&dry_run, N_("dry run")),
145f9c81 311 OPT__FORCE(&force, N_("force")),
17696002 312 OPT_BOOL('i', "interactive", &interactive, N_("interactive cleaning")),
113f10f2 313 OPT_BOOLEAN('d', NULL, &remove_directories,
145f9c81
NTND
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")),
113f10f2 318 OPT_BOOLEAN('X', NULL, &ignored_only,
145f9c81 319 N_("remove only ignored files")),
113f10f2
SB
320 OPT_END()
321 };
322
ef90d6d4 323 git_config(git_clean_config, NULL);
625db1b7
JH
324 if (force < 0)
325 force = 0;
326 else
327 config_set = 1;
328
37782920
SB
329 argc = parse_options(argc, argv, prefix, options, builtin_clean_usage,
330 0);
113f10f2
SB
331
332 memset(&dir, 0, sizeof(dir));
1617adc7 333 if (ignored_only)
7c4c97c0 334 dir.flags |= DIR_SHOW_IGNORED;
113f10f2
SB
335
336 if (ignored && ignored_only)
2da57add 337 die(_("-x and -X cannot be used together"));
113f10f2 338
17696002 339 if (!interactive && !dry_run && !force) {
a66f9b2a 340 if (config_set)
17696002 341 die(_("clean.requireForce set to true and neither -i, -n nor -f given; "
a66f9b2a
ÆAB
342 "refusing to clean"));
343 else
17696002 344 die(_("clean.requireForce defaults to true and neither -i, -n nor -f given; "
a66f9b2a
ÆAB
345 "refusing to clean"));
346 }
113f10f2 347
a0f4afbe
JH
348 if (force > 1)
349 rm_flags = 0;
350
7c4c97c0 351 dir.flags |= DIR_SHOW_OTHER_DIRECTORIES;
113f10f2 352
c28b3d6e 353 if (read_cache() < 0)
2da57add 354 die(_("index file corrupt"));
c28b3d6e 355
1617adc7
SB
356 if (!ignored)
357 setup_standard_excludes(&dir);
113f10f2 358
72aeb187 359 el = add_exclude_list(&dir, EXC_CMDL, "--exclude option");
07de4eba 360 for (i = 0; i < exclude_list.nr; i++)
72aeb187 361 add_exclude(exclude_list.items[i].string, "", 0, el, -(i+1));
07de4eba 362
113f10f2 363 pathspec = get_pathspec(prefix, argv);
113f10f2 364
1d8842d9 365 fill_directory(&dir, pathspec);
113f10f2 366
d871c869 367 if (pathspec)
a8db80c2 368 seen = xmalloc(argc > 0 ? argc : 1);
d871c869
JH
369
370 for (i = 0; i < dir.nr; i++) {
371 struct dir_entry *ent = dir.entries[i];
f2d0df71
SB
372 int len, pos;
373 int matches = 0;
113f10f2
SB
374 struct cache_entry *ce;
375 struct stat st;
396049e5 376 const char *rel;
113f10f2
SB
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
d871c869 396 if (lstat(ent->name, &st))
396049e5 397 die_errno("Cannot lstat '%s'", ent->name);
d871c869
JH
398
399 if (pathspec) {
a8db80c2 400 memset(seen, 0, argc > 0 ? argc : 1);
f2d0df71 401 matches = match_pathspec(pathspec, ent->name, len,
1c7d402b 402 0, seen);
d871c869
JH
403 }
404
405 if (S_ISDIR(st.st_mode)) {
f538a91e 406 if (remove_directories || (matches == MATCHED_EXACTLY)) {
396049e5
JX
407 rel = relative_path(ent->name, prefix, &buf);
408 string_list_append(&del_list, rel);
113f10f2 409 }
113f10f2 410 } else {
d871c869
JH
411 if (pathspec && !matches)
412 continue;
396049e5
JX
413 rel = relative_path(ent->name, prefix, &buf);
414 string_list_append(&del_list, rel);
415 }
416 }
417
17696002
JX
418 if (interactive && del_list.nr > 0)
419 interactive_main_loop();
396049e5
JX
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);
f538a91e 446 if (res) {
396049e5 447 qname = quote_path_relative(item->string, NULL, &buf);
f538a91e 448 warning(_(msg_warn_remove_failed), qname);
aa9c83c2 449 errors++;
f538a91e 450 } else if (!quiet) {
396049e5 451 qname = quote_path_relative(item->string, NULL, &buf);
f538a91e 452 printf(dry_run ? _(msg_would_remove) : _(msg_remove), qname);
aa9c83c2 453 }
113f10f2 454 }
396049e5 455 strbuf_reset(&abs_path);
113f10f2 456 }
d871c869 457 free(seen);
113f10f2 458
396049e5
JX
459 strbuf_release(&abs_path);
460 strbuf_release(&buf);
461 string_list_clear(&del_list, 0);
07de4eba 462 string_list_clear(&exclude_list, 0);
aa9c83c2 463 return (errors != 0);
113f10f2 464}