diff --git a/.gitignore b/.gitignore index 7374587f9df901..7b6d7681f9b9ff 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ /git /git-add /git-add--interactive +/git-add--helper /git-am /git-annotate /git-apply diff --git a/Makefile b/Makefile index f0b2299172cf63..1b3c06b63391fa 100644 --- a/Makefile +++ b/Makefile @@ -847,6 +847,7 @@ LIB_H = $(shell $(FIND) . \ -name '*.h' -print) LIB_OBJS += abspath.o +LIB_OBJS += add-interactive.o LIB_OBJS += advice.o LIB_OBJS += alias.o LIB_OBJS += alloc.o @@ -1043,6 +1044,7 @@ LIB_OBJS += xdiff-interface.o LIB_OBJS += zlib.o BUILTIN_OBJS += builtin/add.o +BUILTIN_OBJS += builtin/add--helper.o BUILTIN_OBJS += builtin/am.o BUILTIN_OBJS += builtin/annotate.o BUILTIN_OBJS += builtin/apply.o diff --git a/add-interactive.c b/add-interactive.c new file mode 100644 index 00000000000000..d4f02a5ab5c4ce --- /dev/null +++ b/add-interactive.c @@ -0,0 +1,785 @@ +#include "add-interactive.h" +#include "cache.h" +#include "commit.h" +#include "color.h" +#include "config.h" +#include "diffcore.h" +#include "refs.h" +#include "revision.h" + +#define HEADER_INDENT " " + +#define HEADER_MAXLEN 30 + +struct adddel { uintmax_t add, del; }; + +struct file_stat { + struct hashmap_entry ent; + struct adddel index, worktree; + char name[FLEX_ARRAY]; +}; + +struct collection_status { + int collecting_from_index; + + const char *reference; + struct pathspec pathspec; + + struct hashmap file_map; +}; + +struct command { + char *name; + void (*command_fn)(void); +}; + +struct list_and_choose_options { + int column_n; + unsigned singleton:1; + unsigned list_only:1; + unsigned list_only_file_names:1; + unsigned immediate:1; + struct strbuf header; + const char *prompt; + const char *header_indent; + void (*on_eof_fn)(void); +}; + +struct choice { + struct hashmap_entry e; + size_t prefix_length; + const char *name; +}; + +struct file_choice { + struct choice choice; + struct adddel index, worktree; +}; + +struct choices { + struct choice **choices; + size_t alloc, nr; +}; +#define CHOICES_INIT { NULL, 0, 0 } + +struct prefix_entry { + struct hashmap_entry e; + const char *name; + size_t prefix_length; + struct choice *item; +}; + +static int use_color = -1; +enum color_add_i { + COLOR_PROMPT, + COLOR_HEADER, + COLOR_HELP, + COLOR_ERROR, + COLOR_RESET +}; + +static char list_and_choose_colors[][COLOR_MAXLEN] = { + GIT_COLOR_BOLD_BLUE, /* Prompt */ + GIT_COLOR_BOLD, /* Header */ + GIT_COLOR_BOLD_RED, /* Help */ + GIT_COLOR_BOLD_RED, /* Error */ + GIT_COLOR_RESET /* Reset */ +}; + +static const char *get_color(enum color_add_i ix) +{ + if (want_color(use_color)) + return list_and_choose_colors[ix]; + return ""; +} + +static int parse_color_slot(const char *slot) +{ + if (!strcasecmp(slot, "prompt")) + return COLOR_PROMPT; + if (!strcasecmp(slot, "header")) + return COLOR_HEADER; + if (!strcasecmp(slot, "help")) + return COLOR_HELP; + if (!strcasecmp(slot, "error")) + return COLOR_ERROR; + if (!strcasecmp(slot, "reset")) + return COLOR_RESET; + + return -1; +} + +int add_i_config(const char *var, + const char *value, void *cbdata) +{ + const char *name; + + if (!strcmp(var, "color.interactive")) { + use_color = git_config_colorbool(var, value); + return 0; + } + + if (skip_prefix(var, "color.interactive.", &name)) { + int slot = parse_color_slot(name); + if (slot < 0) + return 0; + if (!value) + return config_error_nonbool(var); + return color_parse(value, list_and_choose_colors[slot]); + } + + return git_default_config(var, value, cbdata); +} + +static int pathname_equal(const void *unused_cmp_data, + const void *entry, const void *entry_or_key, + const void *keydata) +{ + const struct file_stat *e1 = entry, *e2 = entry_or_key; + const char *name = keydata ? keydata : e2->name; + + return strcmp(e1->name, name); +} + +static int pathname_cmp(const void *a, const void *b) +{ + struct file_stat *f1 = *((struct file_stat **)a); + struct file_stat *f2 = *((struct file_stat **)b); + + return strcmp(f1->name, f2->name); +} + +static void populate_adddel(struct adddel *ad, uintmax_t add, uintmax_t del) +{ + ad->add = add; + ad->del = del; +} + +static void collect_changes_cb(struct diff_queue_struct *q, + struct diff_options *options, + void *data) +{ + struct collection_status *s = data; + struct diffstat_t stat = { 0 }; + int i; + + if (!q->nr) + return; + + compute_diffstat(options, &stat, q); + + for (i = 0; i < stat.nr; i++) { + struct file_stat *entry; + const char *name = stat.files[i]->name; + unsigned int hash = strhash(name); + + entry = hashmap_get_from_hash(&s->file_map, hash, name); + if (!entry) { + FLEX_ALLOC_STR(entry, name, name); + hashmap_entry_init(entry, hash); + hashmap_add(&s->file_map, entry); + } + + if (s->collecting_from_index) + populate_adddel(&entry->index, stat.files[i]->added,stat.files[i]->deleted); + else + populate_adddel(&entry->worktree, stat.files[i]->added, stat.files[i]->deleted); + } +} + +static void collect_changes_worktree(struct collection_status *s) +{ + struct rev_info rev; + + s->collecting_from_index = 0; + + init_revisions(&rev, NULL); + setup_revisions(0, NULL, &rev, NULL); + + /* Use the max_count field to specify the unmerged stage, against + * which the working tree file is compared for an unmerged path. + * git diff-files itself when running cmd_diff_files() leaves + * rev.max_count untouched to get a normal output (as opposed to + * the case when it is told to do --base/--ours/--theirs), so it + * ends up passing -1 in this field in such a case. + */ + rev.max_count = 0; + + rev.diffopt.flags.ignore_dirty_submodules = 1; + rev.diffopt.output_format = DIFF_FORMAT_CALLBACK; + rev.diffopt.format_callback = collect_changes_cb; + rev.diffopt.format_callback_data = s; + + run_diff_files(&rev, 0); +} + +static void collect_changes_index(struct collection_status *s) +{ + struct rev_info rev; + struct setup_revision_opt opt = { 0 }; + + s->collecting_from_index = 1; + + init_revisions(&rev, NULL); + opt.def = s->reference; + setup_revisions(0, NULL, &rev, &opt); + + rev.diffopt.output_format = DIFF_FORMAT_CALLBACK; + rev.diffopt.format_callback = collect_changes_cb; + rev.diffopt.format_callback_data = s; + + run_diff_index(&rev, 1); +} + +static int on_unborn_branch(void) +{ + int flags = 0; + struct object_id oid; + const char *ref; + + ref = resolve_ref_unsafe("HEAD", RESOLVE_REF_READING, &oid, &flags); + return !ref && (flags & REF_ISSYMREF); +} + +static const char *get_diff_reference(void) +{ + return on_unborn_branch() ? empty_tree_oid_hex() : "HEAD"; +} + +static struct file_stat **list_modified(struct repository *r, + const char *filter) +{ + int i = 0; + int hashmap_size = 0; + struct collection_status *s = xcalloc(1, sizeof(*s)); + struct hashmap_iter iter; + struct file_stat **files; + struct file_stat *entry; + + if (repo_read_index(r) < 0) { + free(s); + return NULL; + } + + s->reference = get_diff_reference(); + hashmap_init(&s->file_map, pathname_equal, NULL, 0); + + if (!filter) { + collect_changes_index(s); + collect_changes_worktree(s); + } + else if (!strcmp(filter, "index-only")) + collect_changes_index(s); + else if (!strcmp(filter, "file-only")) + collect_changes_worktree(s); + else + BUG("unknown filter parameter\n"); + + hashmap_size = hashmap_get_size(&s->file_map); + + if (hashmap_size < 1) { + free(s); + return NULL; + } + + hashmap_iter_init(&s->file_map, &iter); + + files = xcalloc(hashmap_size + 1, sizeof(struct file_stat)); + while ((entry = hashmap_iter_next(&iter))) { + files[i++] = entry; + } + QSORT(files, hashmap_size, pathname_cmp); + files[hashmap_size] = NULL; + + hashmap_free(&s->file_map, 0); + free(s); + return files; +} + +static void populate_wi_changes(struct strbuf *buf, struct adddel *ad, + char *no_changes) +{ + if (ad->add || ad->del) + strbuf_addf(buf, "+%"PRIuMAX"/-%"PRIuMAX, + ad->add, ad->del); + else + strbuf_addf(buf, "%s", _(no_changes)); +} + +static int map_cmp(const void *unused_cmp_data, + const void *entry, + const void *entry_or_key, + const void *unused_keydata) +{ + const struct choice *a = entry; + const struct choice *b = entry_or_key; + if((a->prefix_length == b->prefix_length) && + (strncmp(a->name, b->name, a->prefix_length) == 0)) + return 0; + return 1; +} + +static struct prefix_entry *new_prefix_entry(const char *name, + size_t prefix_length, + struct choice *item) +{ + struct prefix_entry *result = xcalloc(1, sizeof(*result)); + result->name = name; + result->prefix_length = prefix_length; + result->item = item; + hashmap_entry_init(result, memhash(name, prefix_length)); + return result; +} + +static void find_unique_prefixes(struct choices *data) +{ + int i; + int j; + int soft_limit = 0; + int hard_limit = 4; + struct hashmap map; + + hashmap_init(&map, map_cmp, NULL, 0); + + for (i = 0; i < data->nr; i++) { + struct prefix_entry *e = xcalloc(1, sizeof(*e)); + struct prefix_entry *e2; + e->name = data->choices[i]->name; + e->item = data->choices[i]; + + for (j = soft_limit + 1; j <= hard_limit; j++) { + if (!isascii(e->name[j])) + break; + + e->prefix_length = j; + hashmap_entry_init(e, memhash(e->name, j)); + e2 = hashmap_get(&map, e, NULL); + if (!e2) { + e->item->prefix_length = j; + hashmap_add(&map, e); + e = NULL; + break; + } + + if (!e2->item) { + continue; /* non-unique prefix */ + } + + if (j != e2->item->prefix_length) + BUG("Hashmap entry has unexpected prefix length (%"PRIuMAX"/ != %"PRIuMAX"/)", + (uintmax_t)j, (uintmax_t)e2->item->prefix_length); + + /* skip common prefix */ + for (j++; j <= hard_limit && e->name[j - 1]; j++) { + if (e->item->name[j - 1] != e2->item->name[j - 1]) + break; + hashmap_add(&map, new_prefix_entry(e->name, j, NULL)); + } + if (j <= hard_limit && e2->name[j - 1]) { + e2->item->prefix_length = j; + hashmap_add(&map, new_prefix_entry(e2->name, j, e2->item)); + } + else { + e2->item->prefix_length = 0; + } + e2->item = NULL; + + if (j <= hard_limit && e->name[j - 1]) { + e->item->prefix_length = j; + hashmap_add(&map, new_prefix_entry(e->name, + e->item->prefix_length, e->item)); + e = NULL; + } + else + e->item->prefix_length = 0; + break; + } + + free(e); + } +} + +static int find_unique(char *string, struct choices *data) +{ + int found = 0; + int i = 0; + int hit = 0; + + for (i = 0; i < data->nr; i++) { + struct choice *item = data->choices[i]; + hit = 0; + if (!strcmp(item->name, string)) + hit = 1; + if (hit && found) + return 0; + if (hit) + found = i + 1; + } + + return found; +} + +/* filters out prefixes which have special meaning to list_and_choose() */ +static int is_valid_prefix(const char *prefix) +{ + regex_t *regex; + const char *pattern = "(\\s,)|(^-)|(^[0-9]+)"; + int is_valid = 0; + + regex = xmalloc(sizeof(*regex)); + if (regcomp(regex, pattern, REG_EXTENDED)) + return 0; + + is_valid = prefix && + regexec(regex, prefix, 0, NULL, 0) && + strcmp(prefix, "*") && + strcmp(prefix, "?"); + free(regex); + return is_valid; +} + +/* return a string with the prefix highlighted */ +/* for now use square brackets; later might use ANSI colors (underline, bold) */ +static void highlight_prefix(struct strbuf *buf, struct choice *item) +{ + if (item->prefix_length <= 0 || !is_valid_prefix(item->name)) { + strbuf_addstr(buf, item->name); + return; + } + + strbuf_addf(buf, "%s%.*s%s%s", + use_color ? get_color(COLOR_PROMPT) : "[", + (int)item->prefix_length, item->name, + use_color ? get_color(COLOR_RESET) : "]", + item->name + item->prefix_length); +} + +static void singleton_prompt_help_cmd(void) +{ + const char *help_color = get_color(COLOR_HELP); + color_fprintf_ln(stdout, help_color, "%s", _("Prompt help:")); + color_fprintf_ln(stdout, help_color, "1 - %s", + _("select a numbered item")); + color_fprintf_ln(stdout, help_color, "foo - %s", + _("select item based on unique prefix")); + color_fprintf_ln(stdout, help_color, " - %s", + _("(empty) select nothing")); +} + +static void prompt_help_cmd(void) +{ + const char *help_color = get_color(COLOR_HELP); + color_fprintf_ln(stdout, help_color, "%s", + _("Prompt help:")); + color_fprintf_ln(stdout, help_color, "1 - %s", + _("select a single item")); + color_fprintf_ln(stdout, help_color, "3-5 - %s", + _("select a range of items")); + color_fprintf_ln(stdout, help_color, "2-3,6-9 - %s", + _("select multiple ranges")); + color_fprintf_ln(stdout, help_color, "foo - %s", + _("select item based on unique prefix")); + color_fprintf_ln(stdout, help_color, "-... - %s", + _("unselect specified items")); + color_fprintf_ln(stdout, help_color, "* - %s", + _("choose all items")); + color_fprintf_ln(stdout, help_color, " - %s", + _("(empty) finish selecting")); +} + +static struct choices *list_and_choose(struct choices *data, + struct list_and_choose_options *opts) +{ + int i; + struct strbuf print_buf = STRBUF_INIT; + struct strbuf print = STRBUF_INIT; + struct strbuf index_changes = STRBUF_INIT; + struct strbuf worktree_changes = STRBUF_INIT; + char *chosen_choices = xcalloc(data->nr, sizeof(char *)); + struct choices *results = xcalloc(1, sizeof(*results)); + int chosen_size = 0; + struct strbuf choice = STRBUF_INIT; + struct strbuf token = STRBUF_INIT; + struct strbuf input = STRBUF_INIT; + + if (!data) { + free(chosen_choices); + free(results); + return NULL; + } + + if (!opts->list_only) + find_unique_prefixes(data); + +top: + while (1) { + int j; + int last_lf = 0; + const char *prompt_color = get_color(COLOR_PROMPT); + const char *error_color = get_color(COLOR_ERROR); + char *token_tmp; + regex_t *regex_dash_range; + regex_t *regex_number; + const char *pattern_dash_range; + const char *pattern_number; + const char delim[] = " ,"; + + strbuf_reset(&choice); + strbuf_reset(&token); + strbuf_reset(&input); + + if (opts->header.len) { + const char *header_color = get_color(COLOR_HEADER); + if (opts->header_indent) + fputs(opts->header_indent, stdout); + color_fprintf_ln(stdout, header_color, "%s", opts->header.buf); + } + + for (i = 0; i < data->nr; i++) { + struct choice *c = data->choices[i]; + char chosen = chosen_choices[i]? '*' : ' '; + const char *modified_fmt = _("%12s %12s %s"); + + strbuf_reset(&print_buf); + + if (!opts->list_only) + highlight_prefix(&print_buf, data->choices[i]); + else + strbuf_add(&print_buf, c->name, strlen(c->name)); + + if ((opts->list_only) && (!opts->list_only_file_names)) { + struct file_choice *choice = (struct file_choice*)c; + + strbuf_reset(&print); + strbuf_reset(&index_changes); + strbuf_reset(&worktree_changes); + + populate_wi_changes(&worktree_changes, &choice->worktree, + "nothing"); + populate_wi_changes(&index_changes, &choice->index, + "unchanged"); + + strbuf_addbuf(&print, &print_buf); + strbuf_reset(&print_buf); + strbuf_addf(&print_buf, modified_fmt, index_changes.buf, + worktree_changes.buf, print.buf); + } + + printf("%c%2d: %s", chosen, i + 1, print_buf.buf); + + if ((opts->column_n) && ((i + 1) % (opts->column_n))) { + putchar('\t'); + last_lf = 0; + } + else { + putchar('\n'); + last_lf = 1; + } + } + + if (!last_lf) + putchar('\n'); + + if (opts->list_only) + return NULL; + + color_fprintf(stdout, prompt_color, "%s", opts->prompt); + if(opts->singleton) + fputs("> ", stdout); + else + fputs(">> ", stdout); + + fflush(stdout); + strbuf_getline(&input, stdin); + strbuf_trim(&input); + + if (!input.buf) + break; + + if (!input.buf[0]) { + putchar('\n'); + if (opts->on_eof_fn) + opts->on_eof_fn(); + break; + } + + if (!strcmp(input.buf, "?")) { + opts->singleton? singleton_prompt_help_cmd() : prompt_help_cmd(); + goto top; + } + + token_tmp = strtok(input.buf, delim); + strbuf_add(&token, token_tmp, strlen(token_tmp)); + + while (1) { + int choose = 1; + int bottom = 0, top = 0; + strbuf_addbuf(&choice, &token); + + /* Input that begins with '-'; unchoose */ + pattern_dash_range = "^-"; + regex_dash_range = xmalloc(sizeof(*regex_dash_range)); + + if (regcomp(regex_dash_range, pattern_dash_range, REG_EXTENDED)) + BUG("regex compilation for pattern %s failed", + pattern_dash_range); + if (!regexec(regex_dash_range, choice.buf, 0, NULL, 0)) { + choose = 0; + /* remove dash from input */ + strbuf_remove(&choice, 0, 1); + } + + /* A range can be specified like 5-7 or 5-. */ + pattern_dash_range = "^([0-9]+)-([0-9]*)$"; + pattern_number = "^[0-9]+$"; + regex_number = xmalloc(sizeof(*regex_number)); + + if (regcomp(regex_dash_range, pattern_dash_range, REG_EXTENDED)) + BUG("regex compilation for pattern %s failed", + pattern_dash_range); + if (regcomp(regex_number, pattern_number, REG_EXTENDED)) + BUG("regex compilation for pattern %s failed", pattern_number); + + if (!regexec(regex_dash_range, choice.buf, 0, NULL, 0)) { + const char delim_dash[] = "-"; + char *num = NULL; + num = strtok(choice.buf, delim_dash); + bottom = atoi(num); + num = strtok(NULL, delim_dash); + top = num? atoi(num) : (1 + data->nr); + } + else if (!regexec(regex_number, choice.buf, 0, NULL, 0)) + bottom = top = atoi(choice.buf); + else if (!strcmp(choice.buf, "*")) { + bottom = 1; + top = 1 + data->nr; + } + else { + bottom = top = find_unique(choice.buf, data); + if (!bottom) { + color_fprintf_ln(stdout, error_color, _("Huh (%s)?"), choice.buf); + goto top; + } + } + + if (opts->singleton && bottom != top) { + color_fprintf_ln(stdout, error_color, _("Huh (%s)?"), choice.buf); + goto top; + } + + for (j = bottom - 1; j <= top - 1; j++) { + if (data->nr <= j || j < 0) + continue; + chosen_choices[j] = choose; + if (choose == 1) + chosen_size++; + } + + strbuf_reset(&token); + strbuf_reset(&choice); + + token_tmp = strtok(NULL, delim); + if (!token_tmp) + break; + strbuf_add(&token, token_tmp, strlen(token_tmp)); + } + + if ((opts->immediate) || !(strcmp(choice.buf, "*"))) + break; + } + + strbuf_release(&print_buf); + strbuf_release(&print); + strbuf_release(&index_changes); + strbuf_release(&worktree_changes); + + strbuf_release(&choice); + strbuf_release(&token); + strbuf_release(&input); + + for (i = 0; i < data->nr; i++) { + if (chosen_choices[i]) { + ALLOC_GROW(results->choices, results->nr + 1, results->alloc); + results->choices[results->nr++] = data->choices[i]; + } + } + + free(chosen_choices); + return results; +} + +static struct file_choice *add_file_choice(struct choices *choices, + struct file_stat *file) +{ + struct file_choice *choice; + + FLEXPTR_ALLOC_STR(choice, choice.name, file->name); + choice->choice.prefix_length = 0; + choice->index = file->index; + choice->worktree = file->worktree; + + ALLOC_GROW(choices->choices, choices->nr + 1, choices->alloc); + choices->choices[choices->nr++] = (struct choice*)choice; + + return choice; +} + +static void free_choices(struct choices *choices) +{ + int i; + + for (i = 0; i < choices->nr; i++) + free(choices->choices[i]); + free(choices->choices); + choices->choices = NULL; + choices->nr = choices->alloc = 0; +} + +void add_i_status(void) +{ + int i; + struct file_stat **files; + struct list_and_choose_options opts = { 0 }; + struct choices choices = CHOICES_INIT; + const char *modified_fmt = _("%12s %12s %s"); + + opts.list_only = 1; + opts.header_indent = HEADER_INDENT; + strbuf_init(&opts.header, 0); + strbuf_addf(&opts.header, modified_fmt, _("staged"), + _("unstaged"), _("path")); + + files = list_modified(the_repository, NULL); + if (files == NULL) { + strbuf_release(&opts.header); + putchar('\n'); + return; + } + + for (i = 0; files[i]; i++) + add_file_choice(&choices, files[i]); + + list_and_choose(&choices, &opts); + putchar('\n'); + + strbuf_release(&opts.header); + free(files); + free_choices(&choices); +} + +void add_i_show_help(void) +{ + const char *help_color = get_color(COLOR_HELP); + color_fprintf_ln(stdout, help_color, "status - %s", + _("show paths with changes")); + color_fprintf_ln(stdout, help_color, "update - %s", + _("add working tree state to the staged set of changes")); + color_fprintf_ln(stdout, help_color, "revert - %s", + _("revert staged set of changes back to the HEAD version")); + color_fprintf_ln(stdout, help_color, "patch - %s", + _("pick hunks and update selectively")); + color_fprintf_ln(stdout, help_color, "diff - %s", + _("view diff between HEAD and index")); + color_fprintf_ln(stdout, help_color, "add untracked - %s", + _("add contents of untracked files to the staged set of changes")); +} diff --git a/add-interactive.h b/add-interactive.h new file mode 100644 index 00000000000000..ddeedd3a332ba9 --- /dev/null +++ b/add-interactive.h @@ -0,0 +1,10 @@ +#ifndef ADD_INTERACTIVE_H +#define ADD_INTERACTIVE_H + +int add_i_config(const char *var, const char *value, void *cbdata); + +void add_i_status(void); + +void add_i_show_help(void); + +#endif diff --git a/builtin.h b/builtin.h index 6538932e99a72f..dd811ef7d56304 100644 --- a/builtin.h +++ b/builtin.h @@ -128,6 +128,7 @@ extern void setup_auto_pager(const char *cmd, int def); extern int is_builtin(const char *s); extern int cmd_add(int argc, const char **argv, const char *prefix); +extern int cmd_add__helper(int argc, const char **argv, const char *prefix); extern int cmd_am(int argc, const char **argv, const char *prefix); extern int cmd_annotate(int argc, const char **argv, const char *prefix); extern int cmd_apply(int argc, const char **argv, const char *prefix); diff --git a/builtin/add--helper.c b/builtin/add--helper.c new file mode 100644 index 00000000000000..1fe64bc7fb7694 --- /dev/null +++ b/builtin/add--helper.c @@ -0,0 +1,43 @@ +#include "add-interactive.h" +#include "builtin.h" +#include "config.h" +#include "revision.h" + +static const char * const builtin_add_helper_usage[] = { + N_("git add-interactive--helper "), + NULL +}; + +enum cmd_mode { + DEFAULT = 0, + STATUS, + HELP +}; + +int cmd_add__helper(int argc, const char **argv, const char *prefix) +{ + enum cmd_mode mode = DEFAULT; + + struct option options[] = { + OPT_CMDMODE(0, "status", &mode, + N_("print status information with diffstat"), STATUS), + OPT_CMDMODE(0, "show-help", &mode, + N_("show help"), HELP), + OPT_END() + }; + + git_config(add_i_config, NULL); + argc = parse_options(argc, argv, NULL, options, + builtin_add_helper_usage, + PARSE_OPT_KEEP_ARGV0); + + if (mode == STATUS) + add_i_status(); + else if (mode == HELP) + add_i_show_help(); + else + usage_with_options(builtin_add_helper_usage, + options); + + return 0; +} diff --git a/diff.c b/diff.c index 5306c48652db59..daa5f3a736fee8 100644 --- a/diff.c +++ b/diff.c @@ -2489,22 +2489,6 @@ static void pprint_rename(struct strbuf *name, const char *a, const char *b) } } -struct diffstat_t { - int nr; - int alloc; - struct diffstat_file { - char *from_name; - char *name; - char *print_name; - const char *comments; - unsigned is_unmerged:1; - unsigned is_binary:1; - unsigned is_renamed:1; - unsigned is_interesting:1; - uintmax_t added, deleted; - } **files; -}; - static struct diffstat_file *diffstat_add(struct diffstat_t *diffstat, const char *name_a, const char *name_b) @@ -6001,12 +5985,7 @@ void diff_flush(struct diff_options *options) dirstat_by_line) { struct diffstat_t diffstat; - memset(&diffstat, 0, sizeof(struct diffstat_t)); - for (i = 0; i < q->nr; i++) { - struct diff_filepair *p = q->queue[i]; - if (check_pair_status(p)) - diff_flush_stat(p, options, &diffstat); - } + compute_diffstat(options, &diffstat, q); if (output_format & DIFF_FORMAT_NUMSTAT) show_numstat(&diffstat, options); if (output_format & DIFF_FORMAT_DIFFSTAT) @@ -6306,6 +6285,20 @@ static int is_submodule_ignored(const char *path, struct diff_options *options) return ignored; } +void compute_diffstat(struct diff_options *options, + struct diffstat_t *diffstat, + struct diff_queue_struct *q) +{ + int i; + + memset(diffstat, 0, sizeof(struct diffstat_t)); + for (i = 0; i < q->nr; i++) { + struct diff_filepair *p = q->queue[i]; + if (check_pair_status(p)) + diff_flush_stat(p, options, diffstat); + } +} + void diff_addremove(struct diff_options *options, int addremove, unsigned mode, const struct object_id *oid, diff --git a/diff.h b/diff.h index b512d0477ac3a4..ae9bedfab8c9bd 100644 --- a/diff.h +++ b/diff.h @@ -240,6 +240,22 @@ void diff_emit_submodule_error(struct diff_options *o, const char *err); void diff_emit_submodule_pipethrough(struct diff_options *o, const char *line, int len); +struct diffstat_t { + int nr; + int alloc; + struct diffstat_file { + char *from_name; + char *name; + char *print_name; + const char *comments; + unsigned is_unmerged:1; + unsigned is_binary:1; + unsigned is_renamed:1; + unsigned is_interesting:1; + uintmax_t added, deleted; + } **files; +}; + enum color_diff { DIFF_RESET = 0, DIFF_CONTEXT = 1, @@ -328,6 +344,9 @@ void diff_change(struct diff_options *, struct diff_filepair *diff_unmerge(struct diff_options *, const char *path); +void compute_diffstat(struct diff_options *options, struct diffstat_t *diffstat, + struct diff_queue_struct *q); + #define DIFF_SETUP_REVERSE 1 #define DIFF_SETUP_USE_SIZE_CACHE 4 diff --git a/git-add--interactive.perl b/git-add--interactive.perl index 20eb81cc92f947..88b7be6602493d 100755 --- a/git-add--interactive.perl +++ b/git-add--interactive.perl @@ -597,9 +597,8 @@ sub prompt_help_cmd { } sub status_cmd { - list_and_choose({ LIST_ONLY => 1, HEADER => $status_head }, - list_modified()); - print "\n"; + my @status_cmd = ("git", "add--helper", "--status"); + !system(@status_cmd) or die "@status_cmd exited with code $?"; } sub say_n_paths { @@ -1719,16 +1718,8 @@ sub quit_cmd { } sub help_cmd { -# TRANSLATORS: please do not translate the command names -# 'status', 'update', 'revert', etc. - print colored $help_color, __ <<'EOF' ; -status - show paths with changes -update - add working tree state to the staged set of changes -revert - revert staged set of changes back to the HEAD version -patch - pick hunks and update selectively -diff - view diff between HEAD and index -add untracked - add contents of untracked files to the staged set of changes -EOF + my @help_cmd = ("git", "add--helper", "--show-help"); + !system(@help_cmd) or die "@help_cmd exited with code $?"; } sub process_args { diff --git a/git.c b/git.c index 2dd588674f621e..cb42591f37d733 100644 --- a/git.c +++ b/git.c @@ -444,6 +444,7 @@ static int run_builtin(struct cmd_struct *p, int argc, const char **argv) static struct cmd_struct commands[] = { { "add", cmd_add, RUN_SETUP | NEED_WORK_TREE }, + { "add--helper", cmd_add__helper, RUN_SETUP | NEED_WORK_TREE }, { "am", cmd_am, RUN_SETUP | NEED_WORK_TREE }, { "annotate", cmd_annotate, RUN_SETUP | NO_PARSEOPT }, { "apply", cmd_apply, RUN_SETUP_GENTLY }, diff --git a/t/t3701-add-interactive.sh b/t/t3701-add-interactive.sh index 65dfbc033a027d..91aaef29323276 100755 --- a/t/t3701-add-interactive.sh +++ b/t/t3701-add-interactive.sh @@ -639,4 +639,28 @@ test_expect_success 'add -p patch editing works with pathological context lines' test_cmp expected-2 actual ' +test_expect_success 'show help from add--helper' ' + git reset --hard && + cat >expect <<-EOF && + + *** Commands *** + 1: status 2: update 3: revert 4: add untracked + 5: patch 6: diff 7: quit 8: help + What now> status - show paths with changes + update - add working tree state to the staged set of changes + revert - revert staged set of changes back to the HEAD version + patch - pick hunks and update selectively + diff - view diff between HEAD and index + add untracked - add contents of untracked files to the staged set of changes + *** Commands *** + 1: status 2: update 3: revert 4: add untracked + 5: patch 6: diff 7: quit 8: help + What now>$SP + Bye. + EOF + test_write_lines h | GIT_PAGER_IN_USE=true TERM=vt100 git add -i >actual.colored && + test_decode_color actual && + test_i18ncmp expect actual +' + test_done