git-clean: add colors to interactive git-clean
[git/raj.git] / builtin / clean.c
blobdfa99bfd5d8b615b16de831b7cbd9eae89c93bdd
1 /*
2 * "git clean" builtin command
4 * Copyright (C) 2007 Shawn Bohrer
6 * Based on git-clean.sh by Pavel Roskin
7 */
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"
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;
24 static const char *const builtin_clean_usage[] = {
25 N_("git clean [-d] [-f] [-i] [-n] [-q] [-e <pattern>] [-x | -X] [--] <paths>..."),
26 NULL
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");
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 */
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,
53 static int parse_clean_color_slot(const char *var)
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;
70 static int git_clean_config(const char *var, const char *value, void *cb)
72 if (!prefixcmp(var, "column."))
73 return git_column_config(var, value, "clean", &colopts);
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;
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;
92 if (!strcmp(var, "clean.requireforce")) {
93 force = !git_config_bool(var, value);
94 return 0;
97 /* inspect the color.ui config variable and others */
98 return git_color_default_config(var, value, cb);
101 static const char *clean_get_color(enum color_clean ix)
103 if (want_color(clean_use_color))
104 return clean_colors[ix];
105 return "";
108 static void clean_print_color(enum color_clean ix)
110 printf("%s", clean_get_color(ix));
113 static int exclude_cb(const struct option *opt, const char *arg, int unset)
115 struct string_list *exclude_list = opt->value;
116 string_list_append(exclude_list, arg);
117 return 0;
120 static int remove_dirs(struct strbuf *path, const char *prefix, int force_flag,
121 int dry_run, int quiet, int *dir_gone)
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;
130 *dir_gone = 1;
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);
140 *dir_gone = 0;
141 return 0;
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;
153 return res;
156 if (path->buf[original_len - 1] != '/')
157 strbuf_addch(path, '/');
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;
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;
189 continue;
192 /* path too long, stat fails, or non-directory still exists */
193 *dir_gone = 0;
194 ret = 1;
195 break;
197 closedir(dir);
199 strbuf_setlen(path, original_len);
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;
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);
217 string_list_clear(&dels, 0);
218 return ret;
221 static void pretty_print_dels(void)
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;
229 for_each_string_list_item(item, &del_list) {
230 qname = quote_path_relative(item->string, NULL, &buf);
231 string_list_append(&list, qname);
235 * always enable column display, we only consult column.*
236 * about layout strategy and stuff
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);
248 static void interactive_main_loop(void)
250 struct strbuf confirm = STRBUF_INIT;
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');
261 pretty_print_dels();
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;
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;
290 strbuf_release(&confirm);
293 int cmd_clean(int argc, const char **argv, const char *prefix)
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()
323 git_config(git_clean_config, NULL);
324 if (force < 0)
325 force = 0;
326 else
327 config_set = 1;
329 argc = parse_options(argc, argv, prefix, options, builtin_clean_usage,
332 memset(&dir, 0, sizeof(dir));
333 if (ignored_only)
334 dir.flags |= DIR_SHOW_IGNORED;
336 if (ignored && ignored_only)
337 die(_("-x and -X cannot be used together"));
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"));
348 if (force > 1)
349 rm_flags = 0;
351 dir.flags |= DIR_SHOW_OTHER_DIRECTORIES;
353 if (read_cache() < 0)
354 die(_("index file corrupt"));
356 if (!ignored)
357 setup_standard_excludes(&dir);
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));
363 pathspec = get_pathspec(prefix, argv);
365 fill_directory(&dir, pathspec);
367 if (pathspec)
368 seen = xmalloc(argc > 0 ? argc : 1);
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;
379 * Remove the '/' at the end that directory
380 * walking adds for directory entries.
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 */
396 if (lstat(ent->name, &st))
397 die_errno("Cannot lstat '%s'", ent->name);
399 if (pathspec) {
400 memset(seen, 0, argc > 0 ? argc : 1);
401 matches = match_pathspec(pathspec, ent->name, len,
402 0, seen);
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);
410 } else {
411 if (pathspec && !matches)
412 continue;
413 rel = relative_path(ent->name, prefix, &buf);
414 string_list_append(&del_list, rel);
418 if (interactive && del_list.nr > 0)
419 interactive_main_loop();
421 for_each_string_list_item(item, &del_list) {
422 struct stat st;
424 if (prefix)
425 strbuf_addstr(&abs_path, prefix);
427 strbuf_addstr(&abs_path, item->string);
430 * we might have removed this as part of earlier
431 * recursive directory removal, so lstat() here could
432 * fail with ENOENT.
434 if (lstat(abs_path.buf, &st))
435 continue;
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);
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);
455 strbuf_reset(&abs_path);
457 free(seen);
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);