/* * "git clean" builtin command * * Copyright (C) 2007 Shawn Bohrer * * Based on git-clean.sh by Pavel Roskin */ #include "builtin.h" #include "abspath.h" #include "config.h" #include "dir.h" #include "gettext.h" #include "parse-options.h" #include "path.h" #include "read-cache-ll.h" #include "repository.h" #include "setup.h" #include "string-list.h" #include "quote.h" #include "column.h" #include "color.h" #include "pathspec.h" #include "help.h" #include "prompt.h" #include "advice.h" static int require_force = -1; /* unset */ static int interactive; static struct string_list del_list = STRING_LIST_INIT_DUP; static unsigned int colopts; static const char *const builtin_clean_usage[] = { N_("git clean [-d] [-f] [-i] [-n] [-q] [-e ] [-x | -X] [--] [...]"), NULL }; static const char *msg_remove = N_("Removing %s\n"); static const char *msg_would_remove = N_("Would remove %s\n"); static const char *msg_skip_git_dir = N_("Skipping repository %s\n"); static const char *msg_would_skip_git_dir = N_("Would skip repository %s\n"); #ifndef CAN_UNLINK_MOUNT_POINTS static const char *msg_skip_mount_point = N_("Skipping mount point %s\n"); static const char *msg_would_skip_mount_point = N_("Would skip mount point %s\n"); #endif static const char *msg_warn_remove_failed = N_("failed to remove %s"); static const char *msg_warn_lstat_failed = N_("could not lstat %s\n"); static const char *msg_skip_cwd = N_("Refusing to remove current working directory\n"); static const char *msg_would_skip_cwd = N_("Would refuse to remove current working directory\n"); enum color_clean { CLEAN_COLOR_RESET = 0, CLEAN_COLOR_PLAIN = 1, CLEAN_COLOR_PROMPT = 2, CLEAN_COLOR_HEADER = 3, CLEAN_COLOR_HELP = 4, CLEAN_COLOR_ERROR = 5 }; static const char *color_interactive_slots[] = { [CLEAN_COLOR_ERROR] = "error", [CLEAN_COLOR_HEADER] = "header", [CLEAN_COLOR_HELP] = "help", [CLEAN_COLOR_PLAIN] = "plain", [CLEAN_COLOR_PROMPT] = "prompt", [CLEAN_COLOR_RESET] = "reset", }; static int clean_use_color = -1; static char clean_colors[][COLOR_MAXLEN] = { [CLEAN_COLOR_ERROR] = GIT_COLOR_BOLD_RED, [CLEAN_COLOR_HEADER] = GIT_COLOR_BOLD, [CLEAN_COLOR_HELP] = GIT_COLOR_BOLD_RED, [CLEAN_COLOR_PLAIN] = GIT_COLOR_NORMAL, [CLEAN_COLOR_PROMPT] = GIT_COLOR_BOLD_BLUE, [CLEAN_COLOR_RESET] = GIT_COLOR_RESET, }; #define MENU_OPTS_SINGLETON 01 #define MENU_OPTS_IMMEDIATE 02 #define MENU_OPTS_LIST_ONLY 04 struct menu_opts { const char *header; const char *prompt; int flags; }; #define MENU_RETURN_NO_LOOP 10 struct menu_item { char hotkey; const char *title; int selected; int (*fn)(void); }; enum menu_stuff_type { MENU_STUFF_TYPE_STRING_LIST = 1, MENU_STUFF_TYPE_MENU_ITEM }; struct menu_stuff { enum menu_stuff_type type; int nr; void *stuff; }; define_list_config_array(color_interactive_slots); static int git_clean_config(const char *var, const char *value, const struct config_context *ctx, void *cb) { const char *slot_name; if (starts_with(var, "column.")) return git_column_config(var, value, "clean", &colopts); /* honors the color.interactive* config variables which also applied in git-add--interactive and git-stash */ if (!strcmp(var, "color.interactive")) { clean_use_color = git_config_colorbool(var, value); return 0; } if (skip_prefix(var, "color.interactive.", &slot_name)) { int slot = LOOKUP_CONFIG(color_interactive_slots, slot_name); if (slot < 0) return 0; if (!value) return config_error_nonbool(var); return color_parse(value, clean_colors[slot]); } if (!strcmp(var, "clean.requireforce")) { require_force = git_config_bool(var, value); return 0; } if (git_color_config(var, value, cb) < 0) return -1; return git_default_config(var, value, ctx, cb); } static const char *clean_get_color(enum color_clean ix) { if (want_color(clean_use_color)) return clean_colors[ix]; return ""; } static void clean_print_color(enum color_clean ix) { printf("%s", clean_get_color(ix)); } static int exclude_cb(const struct option *opt, const char *arg, int unset) { struct string_list *exclude_list = opt->value; BUG_ON_OPT_NEG(unset); string_list_append(exclude_list, arg); return 0; } static int remove_dirs(struct strbuf *path, const char *prefix, int force_flag, int dry_run, int quiet, int *dir_gone) { DIR *dir; struct strbuf quoted = STRBUF_INIT; struct strbuf realpath = STRBUF_INIT; struct strbuf real_ocwd = STRBUF_INIT; struct dirent *e; int res = 0, ret = 0, gone = 1, original_len = path->len, len; struct string_list dels = STRING_LIST_INIT_DUP; *dir_gone = 1; if ((force_flag & REMOVE_DIR_KEEP_NESTED_GIT) && is_nonbare_repository_dir(path)) { if (!quiet) { quote_path(path->buf, prefix, "ed, 0); printf(dry_run ? _(msg_would_skip_git_dir) : _(msg_skip_git_dir), quoted.buf); } *dir_gone = 0; goto out; } if (is_mount_point(path)) { #ifndef CAN_UNLINK_MOUNT_POINTS if (!quiet) { quote_path(path->buf, prefix, "ed, 0); printf(dry_run ? _(msg_would_skip_mount_point) : _(msg_skip_mount_point), quoted.buf); } *dir_gone = 0; #else if (!dry_run && unlink(path->buf)) { int saved_errno = errno; quote_path(path->buf, prefix, "ed, 0); errno = saved_errno; warning_errno(_(msg_warn_remove_failed), quoted.buf); *dir_gone = 0; ret = -1; } #endif goto out; } dir = opendir(path->buf); if (!dir) { /* an empty dir could be removed even if it is unreadble */ res = dry_run ? 0 : rmdir(path->buf); if (res) { int saved_errno = errno; quote_path(path->buf, prefix, "ed, 0); errno = saved_errno; warning_errno(_(msg_warn_remove_failed), quoted.buf); if (saved_errno == ENAMETOOLONG) { advise_if_enabled(ADVICE_NAME_TOO_LONG, _("Setting `core.longPaths` may allow the deletion to succeed.")); } *dir_gone = 0; } ret = res; goto out; } strbuf_complete(path, '/'); len = path->len; while ((e = readdir_skip_dot_and_dotdot(dir)) != NULL) { struct stat st; strbuf_setlen(path, len); strbuf_addstr(path, e->d_name); if (lstat(path->buf, &st)) warning_errno(_(msg_warn_lstat_failed), path->buf); else if (S_ISDIR(st.st_mode)) { if (remove_dirs(path, prefix, force_flag, dry_run, quiet, &gone)) ret = 1; if (gone) { quote_path(path->buf, prefix, "ed, 0); string_list_append(&dels, quoted.buf); } else *dir_gone = 0; continue; } else { res = dry_run ? 0 : unlink(path->buf); if (!res) { quote_path(path->buf, prefix, "ed, 0); string_list_append(&dels, quoted.buf); } else { int saved_errno = errno; quote_path(path->buf, prefix, "ed, 0); errno = saved_errno; warning_errno(_(msg_warn_remove_failed), quoted.buf); if (saved_errno == ENAMETOOLONG) { advise_if_enabled(ADVICE_NAME_TOO_LONG, _("Setting `core.longPaths` may allow the deletion to succeed.")); } *dir_gone = 0; ret = 1; } continue; } /* path too long, stat fails, or non-directory still exists */ *dir_gone = 0; ret = 1; break; } closedir(dir); strbuf_setlen(path, original_len); if (*dir_gone) { /* * Normalize path components in path->buf, e.g. change '\' to * '/' on Windows. */ strbuf_realpath(&realpath, path->buf, 1); /* * path and realpath are absolute; for comparison, we would * like to transform startup_info->original_cwd to an absolute * path too. */ if (startup_info->original_cwd) strbuf_realpath(&real_ocwd, startup_info->original_cwd, 1); if (!strbuf_cmp(&realpath, &real_ocwd)) { printf("%s", dry_run ? _(msg_would_skip_cwd) : _(msg_skip_cwd)); *dir_gone = 0; } else { res = dry_run ? 0 : rmdir(path->buf); if (!res) *dir_gone = 1; else { int saved_errno = errno; quote_path(path->buf, prefix, "ed, 0); errno = saved_errno; warning_errno(_(msg_warn_remove_failed), quoted.buf); if (saved_errno == ENAMETOOLONG) { advise_if_enabled(ADVICE_NAME_TOO_LONG, _("Setting `core.longPaths` may allow the deletion to succeed.")); } *dir_gone = 0; ret = 1; } } } if (!*dir_gone && !quiet) { int i; for (i = 0; i < dels.nr; i++) printf(dry_run ? _(msg_would_remove) : _(msg_remove), dels.items[i].string); } out: strbuf_release(&realpath); strbuf_release(&real_ocwd); strbuf_release("ed); string_list_clear(&dels, 0); return ret; } static void pretty_print_dels(void) { struct string_list list = STRING_LIST_INIT_DUP; struct string_list_item *item; struct strbuf buf = STRBUF_INIT; const char *qname; struct column_options copts; for_each_string_list_item(item, &del_list) { qname = quote_path(item->string, NULL, &buf, 0); string_list_append(&list, qname); } /* * always enable column display, we only consult column.* * about layout strategy and stuff */ colopts = (colopts & ~COL_ENABLE_MASK) | COL_ENABLED; memset(&copts, 0, sizeof(copts)); copts.indent = " "; copts.padding = 2; print_columns(&list, colopts, &copts); strbuf_release(&buf); string_list_clear(&list, 0); } static void pretty_print_menus(struct string_list *menu_list) { unsigned int local_colopts = 0; struct column_options copts; local_colopts = COL_ENABLED | COL_ROW; memset(&copts, 0, sizeof(copts)); copts.indent = " "; copts.padding = 2; print_columns(menu_list, local_colopts, &copts); } static void prompt_help_cmd(int singleton) { clean_print_color(CLEAN_COLOR_HELP); printf(singleton ? _("Prompt help:\n" "1 - select a numbered item\n" "foo - select item based on unique prefix\n" " - (empty) select nothing\n") : _("Prompt help:\n" "1 - select a single item\n" "3-5 - select a range of items\n" "2-3,6-9 - select multiple ranges\n" "foo - select item based on unique prefix\n" "-... - unselect specified items\n" "* - choose all items\n" " - (empty) finish selecting\n")); clean_print_color(CLEAN_COLOR_RESET); } /* * display menu stuff with number prefix and hotkey highlight */ static void print_highlight_menu_stuff(struct menu_stuff *stuff, int **chosen) { struct string_list menu_list = STRING_LIST_INIT_DUP; struct strbuf menu = STRBUF_INIT; struct menu_item *menu_item; struct string_list_item *string_list_item; int i; switch (stuff->type) { default: die("Bad type of menu_stuff when print menu"); case MENU_STUFF_TYPE_MENU_ITEM: menu_item = (struct menu_item *)stuff->stuff; for (i = 0; i < stuff->nr; i++, menu_item++) { const char *p; int highlighted = 0; p = menu_item->title; if ((*chosen)[i] < 0) (*chosen)[i] = menu_item->selected ? 1 : 0; strbuf_addf(&menu, "%s%2d: ", (*chosen)[i] ? "*" : " ", i+1); for (; *p; p++) { if (!highlighted && *p == menu_item->hotkey) { strbuf_addstr(&menu, clean_get_color(CLEAN_COLOR_PROMPT)); strbuf_addch(&menu, *p); strbuf_addstr(&menu, clean_get_color(CLEAN_COLOR_RESET)); highlighted = 1; } else { strbuf_addch(&menu, *p); } } string_list_append(&menu_list, menu.buf); strbuf_reset(&menu); } break; case MENU_STUFF_TYPE_STRING_LIST: i = 0; for_each_string_list_item(string_list_item, (struct string_list *)stuff->stuff) { if ((*chosen)[i] < 0) (*chosen)[i] = 0; strbuf_addf(&menu, "%s%2d: %s", (*chosen)[i] ? "*" : " ", i+1, string_list_item->string); string_list_append(&menu_list, menu.buf); strbuf_reset(&menu); i++; } break; } pretty_print_menus(&menu_list); strbuf_release(&menu); string_list_clear(&menu_list, 0); } static int find_unique(const char *choice, struct menu_stuff *menu_stuff) { struct menu_item *menu_item; struct string_list_item *string_list_item; int i, len, found = 0; len = strlen(choice); switch (menu_stuff->type) { default: die("Bad type of menu_stuff when parse choice"); case MENU_STUFF_TYPE_MENU_ITEM: menu_item = (struct menu_item *)menu_stuff->stuff; for (i = 0; i < menu_stuff->nr; i++, menu_item++) { if (len == 1 && *choice == menu_item->hotkey) { found = i + 1; break; } if (!strncasecmp(choice, menu_item->title, len)) { if (found) { if (len == 1) { /* continue for hotkey matching */ found = -1; } else { found = 0; break; } } else { found = i + 1; } } } break; case MENU_STUFF_TYPE_STRING_LIST: string_list_item = ((struct string_list *)menu_stuff->stuff)->items; for (i = 0; i < menu_stuff->nr; i++, string_list_item++) { if (!strncasecmp(choice, string_list_item->string, len)) { if (found) { found = 0; break; } found = i + 1; } } break; } return found; } /* * Parse user input, and return choice(s) for menu (menu_stuff). * * Input * (for single choice) * 1 - select a numbered item * foo - select item based on menu title * - (empty) select nothing * * (for multiple choice) * 1 - select a single item * 3-5 - select a range of items * 2-3,6-9 - select multiple ranges * foo - select item based on menu title * -... - unselect specified items * * - choose all items * - (empty) finish selecting * * The parse result will be saved in array **chosen, and * return number of total selections. */ static int parse_choice(struct menu_stuff *menu_stuff, int is_single, struct strbuf input, int **chosen) { struct strbuf **choice_list, **ptr; int nr = 0; int i; if (is_single) { choice_list = strbuf_split_max(&input, '\n', 0); } else { char *p = input.buf; do { if (*p == ',') *p = ' '; } while (*p++); choice_list = strbuf_split_max(&input, ' ', 0); } for (ptr = choice_list; *ptr; ptr++) { char *p; int choose = 1; int bottom = 0, top = 0; int is_range, is_number; strbuf_trim(*ptr); if (!(*ptr)->len) continue; /* Input that begins with '-'; unchoose */ if (*(*ptr)->buf == '-') { choose = 0; strbuf_remove((*ptr), 0, 1); } is_range = 0; is_number = 1; for (p = (*ptr)->buf; *p; p++) { if ('-' == *p) { if (!is_range) { is_range = 1; is_number = 0; } else { is_number = 0; is_range = 0; break; } } else if (!isdigit(*p)) { is_number = 0; is_range = 0; break; } } if (is_number) { bottom = atoi((*ptr)->buf); top = bottom; } else if (is_range) { bottom = atoi((*ptr)->buf); /* a range can be specified like 5-7 or 5- */ if (!*(strchr((*ptr)->buf, '-') + 1)) top = menu_stuff->nr; else top = atoi(strchr((*ptr)->buf, '-') + 1); } else if (!strcmp((*ptr)->buf, "*")) { bottom = 1; top = menu_stuff->nr; } else { bottom = find_unique((*ptr)->buf, menu_stuff); top = bottom; } if (top <= 0 || bottom <= 0 || top > menu_stuff->nr || bottom > top || (is_single && bottom != top)) { clean_print_color(CLEAN_COLOR_ERROR); printf(_("Huh (%s)?\n"), (*ptr)->buf); clean_print_color(CLEAN_COLOR_RESET); continue; } for (i = bottom; i <= top; i++) (*chosen)[i-1] = choose; } strbuf_list_free(choice_list); for (i = 0; i < menu_stuff->nr; i++) nr += (*chosen)[i]; return nr; } /* * Implement a git-add-interactive compatible UI, which is borrowed * from add-interactive.c. * * Return value: * * - Return an array of integers * - , and it is up to you to free the allocated memory. * - The array ends with EOF. * - If user pressed CTRL-D (i.e. EOF), no selection returned. */ static int *list_and_choose(struct menu_opts *opts, struct menu_stuff *stuff) { struct strbuf choice = STRBUF_INIT; int *chosen, *result; int nr = 0; int eof = 0; int i; ALLOC_ARRAY(chosen, stuff->nr); /* set chosen as uninitialized */ for (i = 0; i < stuff->nr; i++) chosen[i] = -1; for (;;) { if (opts->header) { printf_ln("%s%s%s", clean_get_color(CLEAN_COLOR_HEADER), _(opts->header), clean_get_color(CLEAN_COLOR_RESET)); } /* chosen will be initialized by print_highlight_menu_stuff */ print_highlight_menu_stuff(stuff, &chosen); if (opts->flags & MENU_OPTS_LIST_ONLY) break; if (opts->prompt) { printf("%s%s%s%s", clean_get_color(CLEAN_COLOR_PROMPT), _(opts->prompt), opts->flags & MENU_OPTS_SINGLETON ? "> " : ">> ", clean_get_color(CLEAN_COLOR_RESET)); } if (git_read_line_interactively(&choice) == EOF) { eof = 1; break; } /* help for prompt */ if (!strcmp(choice.buf, "?")) { prompt_help_cmd(opts->flags & MENU_OPTS_SINGLETON); continue; } /* for a multiple-choice menu, press ENTER (empty) will return back */ if (!(opts->flags & MENU_OPTS_SINGLETON) && !choice.len) break; nr = parse_choice(stuff, opts->flags & MENU_OPTS_SINGLETON, choice, &chosen); if (opts->flags & MENU_OPTS_SINGLETON) { if (nr) break; } else if (opts->flags & MENU_OPTS_IMMEDIATE) { break; } } if (eof) { result = xmalloc(sizeof(int)); *result = EOF; } else { int j = 0; /* * recalculate nr, if return back from menu directly with * default selections. */ if (!nr) { for (i = 0; i < stuff->nr; i++) nr += chosen[i]; } CALLOC_ARRAY(result, st_add(nr, 1)); for (i = 0; i < stuff->nr && j < nr; i++) { if (chosen[i]) result[j++] = i; } result[j] = EOF; } free(chosen); strbuf_release(&choice); return result; } static int clean_cmd(void) { return MENU_RETURN_NO_LOOP; } static int filter_by_patterns_cmd(void) { struct dir_struct dir = DIR_INIT; struct strbuf confirm = STRBUF_INIT; struct strbuf **ignore_list; struct string_list_item *item; struct pattern_list *pl; int changed = -1, i; for (;;) { if (!del_list.nr) break; if (changed) pretty_print_dels(); clean_print_color(CLEAN_COLOR_PROMPT); printf(_("Input ignore patterns>> ")); clean_print_color(CLEAN_COLOR_RESET); if (git_read_line_interactively(&confirm) == EOF) putchar('\n'); /* quit filter_by_pattern mode if press ENTER or Ctrl-D */ if (!confirm.len) break; pl = add_pattern_list(&dir, EXC_CMDL, "manual exclude"); ignore_list = strbuf_split_max(&confirm, ' ', 0); for (i = 0; ignore_list[i]; i++) { strbuf_trim(ignore_list[i]); if (!ignore_list[i]->len) continue; add_pattern(ignore_list[i]->buf, "", 0, pl, -(i+1)); } changed = 0; for_each_string_list_item(item, &del_list) { int dtype = DT_UNKNOWN; if (is_excluded(&dir, the_repository->index, item->string, &dtype)) { *item->string = '\0'; changed++; } } if (changed) { string_list_remove_empty_items(&del_list, 0); } else { clean_print_color(CLEAN_COLOR_ERROR); printf_ln(_("WARNING: Cannot find items matched by: %s"), confirm.buf); clean_print_color(CLEAN_COLOR_RESET); } strbuf_list_free(ignore_list); dir_clear(&dir); } strbuf_release(&confirm); return 0; } static int select_by_numbers_cmd(void) { struct menu_opts menu_opts; struct menu_stuff menu_stuff; struct string_list_item *items; int *chosen; int i, j; menu_opts.header = NULL; menu_opts.prompt = N_("Select items to delete"); menu_opts.flags = 0; menu_stuff.type = MENU_STUFF_TYPE_STRING_LIST; menu_stuff.stuff = &del_list; menu_stuff.nr = del_list.nr; chosen = list_and_choose(&menu_opts, &menu_stuff); items = del_list.items; for (i = 0, j = 0; i < del_list.nr; i++) { if (i < chosen[j]) { *(items[i].string) = '\0'; } else if (i == chosen[j]) { /* delete selected item */ j++; continue; } else { /* end of chosen (chosen[j] == EOF), won't delete */ *(items[i].string) = '\0'; } } string_list_remove_empty_items(&del_list, 0); free(chosen); return 0; } static int ask_each_cmd(void) { struct strbuf confirm = STRBUF_INIT; struct strbuf buf = STRBUF_INIT; struct string_list_item *item; const char *qname; int changed = 0, eof = 0; for_each_string_list_item(item, &del_list) { /* Ctrl-D should stop removing files */ if (!eof) { qname = quote_path(item->string, NULL, &buf, 0); /* TRANSLATORS: Make sure to keep [y/N] as is */ printf(_("Remove %s [y/N]? "), qname); if (git_read_line_interactively(&confirm) == EOF) { putchar('\n'); eof = 1; } } if (!confirm.len || strncasecmp(confirm.buf, "yes", confirm.len)) { *item->string = '\0'; changed++; } } if (changed) string_list_remove_empty_items(&del_list, 0); strbuf_release(&buf); strbuf_release(&confirm); return MENU_RETURN_NO_LOOP; } static int quit_cmd(void) { string_list_clear(&del_list, 0); printf(_("Bye.\n")); return MENU_RETURN_NO_LOOP; } static int help_cmd(void) { clean_print_color(CLEAN_COLOR_HELP); printf_ln(_( "clean - start cleaning\n" "filter by pattern - exclude items from deletion\n" "select by numbers - select items to be deleted by numbers\n" "ask each - confirm each deletion (like \"rm -i\")\n" "quit - stop cleaning\n" "help - this screen\n" "? - help for prompt selection" )); clean_print_color(CLEAN_COLOR_RESET); return 0; } static void interactive_main_loop(void) { while (del_list.nr) { struct menu_opts menu_opts; struct menu_stuff menu_stuff; struct menu_item menus[] = { {'c', "clean", 0, clean_cmd}, {'f', "filter by pattern", 0, filter_by_patterns_cmd}, {'s', "select by numbers", 0, select_by_numbers_cmd}, {'a', "ask each", 0, ask_each_cmd}, {'q', "quit", 0, quit_cmd}, {'h', "help", 0, help_cmd}, }; int *chosen; menu_opts.header = N_("*** Commands ***"); menu_opts.prompt = N_("What now"); menu_opts.flags = MENU_OPTS_SINGLETON; menu_stuff.type = MENU_STUFF_TYPE_MENU_ITEM; menu_stuff.stuff = menus; menu_stuff.nr = sizeof(menus) / sizeof(struct menu_item); clean_print_color(CLEAN_COLOR_HEADER); printf_ln(Q_("Would remove the following item:", "Would remove the following items:", del_list.nr)); clean_print_color(CLEAN_COLOR_RESET); pretty_print_dels(); chosen = list_and_choose(&menu_opts, &menu_stuff); if (*chosen != EOF) { int ret; ret = menus[*chosen].fn(); if (ret != MENU_RETURN_NO_LOOP) { FREE_AND_NULL(chosen); if (!del_list.nr) { clean_print_color(CLEAN_COLOR_ERROR); printf_ln(_("No more files to clean, exiting.")); clean_print_color(CLEAN_COLOR_RESET); break; } continue; } } else { quit_cmd(); } FREE_AND_NULL(chosen); break; } } static void correct_untracked_entries(struct dir_struct *dir) { int src, dst, ign; for (src = dst = ign = 0; src < dir->nr; src++) { /* skip paths in ignored[] that cannot be inside entries[src] */ while (ign < dir->ignored_nr && 0 <= cmp_dir_entry(&dir->entries[src], &dir->ignored[ign])) ign++; if (ign < dir->ignored_nr && check_dir_entry_contains(dir->entries[src], dir->ignored[ign])) { /* entries[src] contains an ignored path, so we drop it */ free(dir->entries[src]); } else { struct dir_entry *ent = dir->entries[src++]; /* entries[src] does not contain an ignored path, so we keep it */ dir->entries[dst++] = ent; /* then discard paths in entries[] contained inside entries[src] */ while (src < dir->nr && check_dir_entry_contains(ent, dir->entries[src])) free(dir->entries[src++]); /* compensate for the outer loop's loop control */ src--; } } dir->nr = dst; } int cmd_clean(int argc, const char **argv, const char *prefix) { int i, res; int dry_run = 0, remove_directories = 0, quiet = 0, ignored = 0; int ignored_only = 0, force = 0, errors = 0, gone = 1; int rm_flags = REMOVE_DIR_KEEP_NESTED_GIT; struct strbuf abs_path = STRBUF_INIT; struct dir_struct dir = DIR_INIT; struct pathspec pathspec; struct strbuf buf = STRBUF_INIT; struct string_list exclude_list = STRING_LIST_INIT_NODUP; struct pattern_list *pl; struct string_list_item *item; const char *qname; struct option options[] = { OPT__QUIET(&quiet, N_("do not print names of files removed")), OPT__DRY_RUN(&dry_run, N_("dry run")), OPT__FORCE(&force, N_("force"), PARSE_OPT_NOCOMPLETE), OPT_BOOL('i', "interactive", &interactive, N_("interactive cleaning")), OPT_BOOL('d', NULL, &remove_directories, N_("remove whole directories")), OPT_CALLBACK_F('e', "exclude", &exclude_list, N_("pattern"), N_("add to ignore rules"), PARSE_OPT_NONEG, exclude_cb), OPT_BOOL('x', NULL, &ignored, N_("remove ignored files, too")), OPT_BOOL('X', NULL, &ignored_only, N_("remove only ignored files")), OPT_END() }; git_config(git_clean_config, NULL); argc = parse_options(argc, argv, prefix, options, builtin_clean_usage, 0); if (require_force != 0 && !force && !interactive && !dry_run) die(_("clean.requireForce is true and -f not given: refusing to clean")); if (force > 1) rm_flags = 0; else dir.flags |= DIR_SKIP_NESTED_GIT; dir.flags |= DIR_SHOW_OTHER_DIRECTORIES; if (ignored && ignored_only) die(_("options '%s' and '%s' cannot be used together"), "-x", "-X"); if (!ignored) setup_standard_excludes(&dir); if (ignored_only) dir.flags |= DIR_SHOW_IGNORED; if (argc) { /* * Remaining args implies pathspecs specified, and we should * recurse within those. */ remove_directories = 1; } if (remove_directories && !ignored_only) { /* * We need to know about ignored files too: * * If (ignored), then we will delete ignored files as well. * * If (!ignored), then even though we not are doing * anything with ignored files, we need to know about them * so that we can avoid deleting a directory of untracked * files that also contains an ignored file within it. * * For the (!ignored) case, since we only need to avoid * deleting ignored files, we can set * DIR_SHOW_IGNORED_TOO_MODE_MATCHING in order to avoid * recursing into a directory which is itself ignored. */ dir.flags |= DIR_SHOW_IGNORED_TOO; if (!ignored) dir.flags |= DIR_SHOW_IGNORED_TOO_MODE_MATCHING; /* * Let the fill_directory() machinery know that we aren't * just recursing to collect the ignored files; we want all * the untracked ones so that we can delete them. (Note: * we could also set DIR_KEEP_UNTRACKED_CONTENTS when * ignored_only is true, since DIR_KEEP_UNTRACKED_CONTENTS * only has effect in combination with DIR_SHOW_IGNORED_TOO. It makes * the code clearer to exclude it, though. */ dir.flags |= DIR_KEEP_UNTRACKED_CONTENTS; } prepare_repo_settings(the_repository); the_repository->settings.command_requires_full_index = 0; if (repo_read_index(the_repository) < 0) die(_("index file corrupt")); enable_fscache(the_repository->index->cache_nr); pl = add_pattern_list(&dir, EXC_CMDL, "--exclude option"); for (i = 0; i < exclude_list.nr; i++) add_pattern(exclude_list.items[i].string, "", 0, pl, -(i+1)); parse_pathspec(&pathspec, 0, PATHSPEC_PREFER_CWD, prefix, argv); fill_directory(&dir, the_repository->index, &pathspec); correct_untracked_entries(&dir); for (i = 0; i < dir.nr; i++) { struct dir_entry *ent = dir.entries[i]; struct stat st; const char *rel; if (!index_name_is_other(the_repository->index, ent->name, ent->len)) continue; if (lstat(ent->name, &st)) die_errno("Cannot lstat '%s'", ent->name); if (S_ISDIR(st.st_mode) && !remove_directories) continue; rel = relative_path(ent->name, prefix, &buf); string_list_append(&del_list, rel); } dir_clear(&dir); if (interactive && del_list.nr > 0) interactive_main_loop(); for_each_string_list_item(item, &del_list) { struct stat st; strbuf_reset(&abs_path); if (prefix) strbuf_addstr(&abs_path, prefix); strbuf_addstr(&abs_path, item->string); /* * we might have removed this as part of earlier * recursive directory removal, so lstat() here could * fail with ENOENT. */ if (lstat(abs_path.buf, &st)) continue; if (S_ISDIR(st.st_mode)) { if (remove_dirs(&abs_path, prefix, rm_flags, dry_run, quiet, &gone)) errors++; if (gone && !quiet) { qname = quote_path(item->string, NULL, &buf, 0); printf(dry_run ? _(msg_would_remove) : _(msg_remove), qname); } } else { res = dry_run ? 0 : unlink(abs_path.buf); if (res) { int saved_errno = errno; qname = quote_path(item->string, NULL, &buf, 0); errno = saved_errno; warning_errno(_(msg_warn_remove_failed), qname); if (saved_errno == ENAMETOOLONG) { advise_if_enabled(ADVICE_NAME_TOO_LONG, _("Setting `core.longPaths` may allow the deletion to succeed.")); } errors++; } else if (!quiet) { qname = quote_path(item->string, NULL, &buf, 0); printf(dry_run ? _(msg_would_remove) : _(msg_remove), qname); } } } disable_fscache(); strbuf_release(&abs_path); strbuf_release(&buf); string_list_clear(&del_list, 0); string_list_clear(&exclude_list, 0); clear_pathspec(&pathspec); return (errors != 0); }