dfa99bfd5d8b615b16de831b7cbd9eae89c93bdd
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
19static int force = -1; /* unset */
20static int interactive;
21static struct string_list del_list = STRING_LIST_INIT_DUP;
22static unsigned int colopts;
23
24static const char *const builtin_clean_usage[] = {
25 N_("git clean [-d] [-f] [-i] [-n] [-q] [-e <pattern>] [-x | -X] [--] <paths>..."),
26 NULL
27};
28
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
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
70static 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
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));
111}
112
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
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) {
135 quote_path_relative(path->buf, prefix, "ed);
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, "ed);
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, "ed);
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, "ed);
182 string_list_append(&dels, quoted.buf);
183 } else {
184 quote_path_relative(path->buf, prefix, "ed);
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, "ed);
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
221static 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
248static 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
293int 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}