From c5b960a0ead3902faaebb99492caba7ed546352f Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 5 Mar 2019 23:33:55 +0100 Subject: [PATCH 1/8] built-in add -i: allow filtering the modified files list In the `update` command of `git add -i`, we are primarily interested in the list of modified files that have worktree (i.e. unstaged) changes. At the same time, we need to determine _also_ the staged changes, to be able to produce the full added/deleted information. The Perl script version of `git add -i` has a parameter of the `list_modified()` function for that matter. In C, we can be a lot more precise, using an `enum`. The C implementation of the filter also has an easier time to avoid unnecessary work, simply by using an adaptive order of the `diff-index` and `diff-files` phases, and then skipping files in the second phase when they have not been seen in the first phase. Signed-off-by: Johannes Schindelin --- add-interactive.c | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/add-interactive.c b/add-interactive.c index 4e5241e865..97f816965e 100644 --- a/add-interactive.c +++ b/add-interactive.c @@ -346,6 +346,7 @@ struct collection_status { const char *reference; + unsigned skip_unseen:1; struct string_list *files; struct hashmap file_map; }; @@ -373,6 +374,9 @@ static void collect_changes_cb(struct diff_queue_struct *q, entry = hashmap_get_entry_from_hash(&s->file_map, hash, name, struct pathname_entry, ent); if (!entry) { + if (s->skip_unseen) + continue; + add_file_item(s->files, name); entry = xcalloc(sizeof(*entry), 1); @@ -394,13 +398,22 @@ static void collect_changes_cb(struct diff_queue_struct *q, free_diffstat_info(&stat); } -static int get_modified_files(struct repository *r, struct string_list *files, +enum modified_files_filter { + NO_FILTER = 0, + WORKTREE_ONLY = 1, + INDEX_ONLY = 2, +}; + +static int get_modified_files(struct repository *r, + enum modified_files_filter filter, + struct string_list *files, const struct pathspec *ps) { struct object_id head_oid; int is_initial = !resolve_ref_unsafe("HEAD", RESOLVE_REF_READING, &head_oid, NULL); struct collection_status s = { FROM_WORKTREE }; + int i; if (discard_index(r->index) < 0 || repo_read_index_preload(r, ps, 0) < 0) @@ -410,10 +423,16 @@ static int get_modified_files(struct repository *r, struct string_list *files, s.files = files; hashmap_init(&s.file_map, pathname_entry_cmp, NULL, 0); - for (s.phase = FROM_WORKTREE; s.phase <= FROM_INDEX; s.phase++) { + for (i = 0; i < 2; i++) { struct rev_info rev; struct setup_revision_opt opt = { 0 }; + if (filter == INDEX_ONLY) + s.phase = i ? FROM_WORKTREE : FROM_INDEX; + else + s.phase = i ? FROM_INDEX : FROM_WORKTREE; + s.skip_unseen = filter && i; + opt.def = is_initial ? empty_tree_oid_hex() : oid_to_hex(&head_oid); @@ -497,7 +516,7 @@ static void print_file_item(int i, struct string_list_item *item, static int run_status(struct add_i_state *s, const struct pathspec *ps, struct string_list *files, struct list_options *opts) { - if (get_modified_files(s->r, files, ps) < 0) + if (get_modified_files(s->r, NO_FILTER, files, ps) < 0) return -1; list(s, files, opts); From e8f70db6ad91a2edcacfecebba0b5a896e49da7c Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 7 Mar 2019 21:36:11 +0100 Subject: [PATCH 2/8] built-in add -i: prepare for multi-selection commands The `upgrade`, `revert` and `add-untracked` commands allow selecting multiple entries. Let's extend the `list_and_choose()` function to accommodate those use cases. Signed-off-by: Johannes Schindelin --- add-interactive.c | 106 +++++++++++++++++++++++++++++++++++----------- 1 file changed, 81 insertions(+), 25 deletions(-) diff --git a/add-interactive.c b/add-interactive.c index 97f816965e..a8958e9f4e 100644 --- a/add-interactive.c +++ b/add-interactive.c @@ -72,15 +72,17 @@ static void init_add_i_state(struct add_i_state *s, struct repository *r) struct prefix_item_list { struct string_list items; struct string_list sorted; + int *selected; /* for multi-selections */ size_t min_length, max_length; }; #define PREFIX_ITEM_LIST_INIT \ - { STRING_LIST_INIT_DUP, STRING_LIST_INIT_NODUP, 1, 4 } + { STRING_LIST_INIT_DUP, STRING_LIST_INIT_NODUP, NULL, 1, 4 } static void prefix_item_list_clear(struct prefix_item_list *list) { string_list_clear(&list->items, 1); string_list_clear(&list->sorted, 0); + FREE_AND_NULL(list->selected); } static void extend_prefix_length(struct string_list_item *p, @@ -182,11 +184,12 @@ static ssize_t find_unique(const char *string, struct prefix_item_list *list) struct list_options { int columns; const char *header; - void (*print_item)(int i, struct string_list_item *item, void *print_item_data); + void (*print_item)(int i, int selected, struct string_list_item *item, + void *print_item_data); void *print_item_data; }; -static void list(struct add_i_state *s, struct string_list *list, +static void list(struct add_i_state *s, struct string_list *list, int *selected, struct list_options *opts) { int i, last_lf = 0; @@ -199,7 +202,8 @@ static void list(struct add_i_state *s, struct string_list *list, "%s", opts->header); for (i = 0; i < list->nr; i++) { - opts->print_item(i, list->items + i, opts->print_item_data); + opts->print_item(i, selected ? selected[i] : 0, list->items + i, + opts->print_item_data); if ((opts->columns) && ((i + 1) % (opts->columns))) { putchar('\t'); @@ -218,6 +222,10 @@ struct list_and_choose_options { struct list_options list_opts; const char *prompt; + enum { + SINGLETON = (1<<0), + IMMEDIATE = (1<<1), + } flags; void (*print_help)(struct add_i_state *s); }; @@ -225,7 +233,8 @@ struct list_and_choose_options { #define LIST_AND_CHOOSE_QUIT (-2) /* - * Returns the selected index. + * Returns the selected index in singleton mode, the number of selected items + * otherwise. * * If an error occurred, returns `LIST_AND_CHOOSE_ERROR`. Upon EOF, * `LIST_AND_CHOOSE_QUIT` is returned. @@ -234,8 +243,19 @@ static ssize_t list_and_choose(struct add_i_state *s, struct prefix_item_list *items, struct list_and_choose_options *opts) { + int singleton = opts->flags & SINGLETON; + int immediate = opts->flags & IMMEDIATE; + struct strbuf input = STRBUF_INIT; - ssize_t res = LIST_AND_CHOOSE_ERROR; + ssize_t res = singleton ? LIST_AND_CHOOSE_ERROR : 0; + + if (!singleton) { + free(items->selected); + CALLOC_ARRAY(items->selected, items->items.nr); + } + + if (singleton && !immediate) + BUG("singleton requires immediate"); find_unique_prefixes(items); @@ -244,15 +264,16 @@ static ssize_t list_and_choose(struct add_i_state *s, strbuf_reset(&input); - list(s, &items->items, &opts->list_opts); + list(s, &items->items, items->selected, &opts->list_opts); color_fprintf(stdout, s->prompt_color, "%s", opts->prompt); - fputs("> ", stdout); + fputs(singleton ? "> " : ">> ", stdout); fflush(stdout); if (strbuf_getline(&input, stdin) == EOF) { putchar('\n'); - res = LIST_AND_CHOOSE_QUIT; + if (immediate) + res = LIST_AND_CHOOSE_QUIT; break; } strbuf_trim(&input); @@ -268,7 +289,9 @@ static ssize_t list_and_choose(struct add_i_state *s, p = input.buf; for (;;) { size_t sep = strcspn(p, " \t\r\n,"); - ssize_t index = -1; + int choose = 1; + /* `from` is inclusive, `to` is exclusive */ + ssize_t from = -1, to = -1; if (!sep) { if (!*p) @@ -277,28 +300,60 @@ static ssize_t list_and_choose(struct add_i_state *s, continue; } - if (isdigit(*p)) { - index = strtoul(p, &endp, 10) - 1; - if (endp != p + sep) - index = -1; + /* Input that begins with '-'; de-select */ + if (*p == '-') { + choose = 0; + p++; + sep--; + } + + if (sep == 1 && *p == '*') { + from = 0; + to = items->items.nr; + } else if (isdigit(*p)) { + /* A range can be specified like 5-7 or 5-. */ + from = strtoul(p, &endp, 10) - 1; + if (endp == p + sep) + to = from + 1; + else if (*endp == '-') { + to = strtoul(++endp, &endp, 10); + /* extra characters after the range? */ + if (endp != p + sep) + from = -1; + } } p[sep] = '\0'; - if (index < 0) - index = find_unique(p, items); + if (from < 0) { + from = find_unique(p, items); + if (from >= 0) + to = from + 1; + } - if (index < 0 || index >= items->items.nr) + if (from < 0 || from >= items->items.nr || + (singleton && from + 1 != to)) { color_fprintf_ln(stdout, s->error_color, _("Huh (%s)?"), p); - else { - res = index; + break; + } else if (singleton) { + res = from; break; } + if (to > items->items.nr) + to = items->items.nr; + + for (; from < to; from++) + if (items->selected[from] != choose) { + items->selected[from] = choose; + res += choose ? +1 : -1; + } + p += sep + 1; } - if (res != LIST_AND_CHOOSE_ERROR) + if ((immediate && res != LIST_AND_CHOOSE_ERROR) || + !strcmp(input.buf, "*")) break; } @@ -495,7 +550,7 @@ struct print_file_item_data { struct strbuf buf, index, worktree; }; -static void print_file_item(int i, struct string_list_item *item, +static void print_file_item(int i, int selected, struct string_list_item *item, void *print_file_item_data) { struct file_item *c = item->util; @@ -510,7 +565,7 @@ static void print_file_item(int i, struct string_list_item *item, strbuf_addf(&d->buf, d->modified_fmt, d->index.buf, d->worktree.buf, item->string); - printf(" %2d: %s", i + 1, d->buf.buf); + printf("%c%2d: %s", selected ? '*' : ' ', i + 1, d->buf.buf); } static int run_status(struct add_i_state *s, const struct pathspec *ps, @@ -519,7 +574,7 @@ static int run_status(struct add_i_state *s, const struct pathspec *ps, if (get_modified_files(s->r, NO_FILTER, files, ps) < 0) return -1; - list(s, files, opts); + list(s, files, NULL, opts); putchar('\n'); return 0; @@ -558,7 +613,8 @@ struct print_command_item_data { const char *color, *reset; }; -static void print_command_item(int i, struct string_list_item *item, +static void print_command_item(int i, int selected, + struct string_list_item *item, void *print_command_item_data) { struct print_command_item_data *d = print_command_item_data; @@ -591,7 +647,7 @@ int run_add_i(struct repository *r, const struct pathspec *ps) struct print_command_item_data data = { "[", "]" }; struct list_and_choose_options main_loop_opts = { { 4, N_("*** Commands ***"), print_command_item, &data }, - N_("What now"), command_prompt_help + N_("What now"), SINGLETON | IMMEDIATE, command_prompt_help }; struct { const char *string; From f7b3b9bcaaf405c57e162ae14ae7ad4db5745db9 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 5 Mar 2019 23:58:49 +0100 Subject: [PATCH 3/8] built-in add -i: implement the `update` command After `status` and `help`, it is now turn to port the `update` command to C, the second command that is shown in the main loop menu of `git add -i`. This `git add -i` command is the first one which lets the user choose a subset of a list of files, and as such, this patch lays the groundwork for the other commands of that category: - It teaches the `print_file_item()` function to show a unique prefix if we found any (the code to find it had been added already in the previous patch where we colored the unique prefixes of the main loop commands, but that patch uses the `print_command_item()` function to display the menu items). - This patch also adds the help text that is shown when the user input to select items from the shown list could not be parsed. - As `get_modified_files()` clears the list of files, it now has to take care of clearing the _full_ `prefix_item_list` lest the `sorted` and `selected` fields go stale and inconsistent. Signed-off-by: Johannes Schindelin --- add-interactive.c | 130 +++++++++++++++++++++++++++++++++++++++------- 1 file changed, 110 insertions(+), 20 deletions(-) diff --git a/add-interactive.c b/add-interactive.c index a8958e9f4e..78fbb5be91 100644 --- a/add-interactive.c +++ b/add-interactive.c @@ -6,6 +6,7 @@ #include "revision.h" #include "refs.h" #include "string-list.h" +#include "lockfile.h" struct add_i_state { struct repository *r; @@ -367,6 +368,7 @@ struct adddel { }; struct file_item { + size_t prefix_length; struct adddel index, worktree; }; @@ -461,7 +463,7 @@ enum modified_files_filter { static int get_modified_files(struct repository *r, enum modified_files_filter filter, - struct string_list *files, + struct prefix_item_list *files, const struct pathspec *ps) { struct object_id head_oid; @@ -474,8 +476,8 @@ static int get_modified_files(struct repository *r, repo_read_index_preload(r, ps, 0) < 0) return error(_("could not read index")); - string_list_clear(files, 1); - s.files = files; + prefix_item_list_clear(files); + s.files = &files->items; hashmap_init(&s.file_map, pathname_entry_cmp, NULL, 0); for (i = 0; i < 2; i++) { @@ -511,7 +513,7 @@ static int get_modified_files(struct repository *r, hashmap_free_entries(&s.file_map, struct pathname_entry, ent); /* While the diffs are ordered already, we ran *two* diffs... */ - string_list_sort(files); + string_list_sort(&files->items); return 0; } @@ -546,8 +548,8 @@ static int is_valid_prefix(const char *prefix, size_t prefix_len) } struct print_file_item_data { - const char *modified_fmt; - struct strbuf buf, index, worktree; + const char *modified_fmt, *color, *reset; + struct strbuf buf, name, index, worktree; }; static void print_file_item(int i, int selected, struct string_list_item *item, @@ -555,34 +557,96 @@ static void print_file_item(int i, int selected, struct string_list_item *item, { struct file_item *c = item->util; struct print_file_item_data *d = print_file_item_data; + const char *highlighted = NULL; strbuf_reset(&d->index); strbuf_reset(&d->worktree); strbuf_reset(&d->buf); + /* Format the item with the prefix highlighted. */ + if (c->prefix_length > 0 && + is_valid_prefix(item->string, c->prefix_length)) { + strbuf_reset(&d->name); + strbuf_addf(&d->name, "%s%.*s%s%s", d->color, + (int)c->prefix_length, item->string, d->reset, + item->string + c->prefix_length); + highlighted = d->name.buf; + } + render_adddel(&d->worktree, &c->worktree, _("nothing")); render_adddel(&d->index, &c->index, _("unchanged")); - strbuf_addf(&d->buf, d->modified_fmt, - d->index.buf, d->worktree.buf, item->string); + + strbuf_addf(&d->buf, d->modified_fmt, d->index.buf, d->worktree.buf, + highlighted ? highlighted : item->string); printf("%c%2d: %s", selected ? '*' : ' ', i + 1, d->buf.buf); } static int run_status(struct add_i_state *s, const struct pathspec *ps, - struct string_list *files, struct list_options *opts) + struct prefix_item_list *files, + struct list_and_choose_options *opts) { if (get_modified_files(s->r, NO_FILTER, files, ps) < 0) return -1; - list(s, files, NULL, opts); + list(s, &files->items, NULL, &opts->list_opts); putchar('\n'); return 0; } +static int run_update(struct add_i_state *s, const struct pathspec *ps, + struct prefix_item_list *files, + struct list_and_choose_options *opts) +{ + int res = 0, fd; + size_t count, i; + struct lock_file index_lock; + + if (get_modified_files(s->r, WORKTREE_ONLY, files, ps) < 0) + return -1; + + if (!files->items.nr) { + putchar('\n'); + return 0; + } + + opts->prompt = N_("Update"); + count = list_and_choose(s, files, opts); + if (count <= 0) { + putchar('\n'); + return 0; + } + + fd = repo_hold_locked_index(s->r, &index_lock, LOCK_REPORT_ON_ERROR); + if (fd < 0) { + putchar('\n'); + return -1; + } + + for (i = 0; i < files->items.nr; i++) { + const char *name = files->items.items[i].string; + if (files->selected[i] && + add_file_to_index(s->r->index, name, 0) < 0) { + res = error(_("could not stage '%s'"), name); + break; + } + } + + if (!res && write_locked_index(s->r->index, &index_lock, COMMIT_LOCK) < 0) + res = error(_("could not write index")); + + if (!res) + printf(Q_("updated %d path\n", + "updated %d paths\n", count), (int)count); + + putchar('\n'); + return res; +} + static int run_help(struct add_i_state *s, const struct pathspec *unused_ps, - struct string_list *unused_files, - struct list_options *unused_opts) + struct prefix_item_list *unused_files, + struct list_and_choose_options *opts) { color_fprintf_ln(stdout, s->help_color, "status - %s", _("show paths with changes")); @@ -600,9 +664,29 @@ static int run_help(struct add_i_state *s, const struct pathspec *unused_ps, return 0; } +static void choose_prompt_help(struct add_i_state *s) +{ + color_fprintf_ln(stdout, s->help_color, "%s", + _("Prompt help:")); + color_fprintf_ln(stdout, s->help_color, "1 - %s", + _("select a single item")); + color_fprintf_ln(stdout, s->help_color, "3-5 - %s", + _("select a range of items")); + color_fprintf_ln(stdout, s->help_color, "2-3,6-9 - %s", + _("select multiple ranges")); + color_fprintf_ln(stdout, s->help_color, "foo - %s", + _("select item based on unique prefix")); + color_fprintf_ln(stdout, s->help_color, "-... - %s", + _("unselect specified items")); + color_fprintf_ln(stdout, s->help_color, "* - %s", + _("choose all items")); + color_fprintf_ln(stdout, s->help_color, " - %s", + _("(empty) finish selecting")); +} + typedef int (*command_t)(struct add_i_state *s, const struct pathspec *ps, - struct string_list *files, - struct list_options *opts); + struct prefix_item_list *files, + struct list_and_choose_options *opts); struct command_item { size_t prefix_length; @@ -654,18 +738,21 @@ int run_add_i(struct repository *r, const struct pathspec *ps) command_t command; } command_list[] = { { "status", run_status }, + { "update", run_update }, { "help", run_help }, }; struct prefix_item_list commands = PREFIX_ITEM_LIST_INIT; struct print_file_item_data print_file_item_data = { - "%12s %12s %s", STRBUF_INIT, STRBUF_INIT, STRBUF_INIT + "%12s %12s %s", NULL, NULL, + STRBUF_INIT, STRBUF_INIT, STRBUF_INIT, STRBUF_INIT }; - struct list_options opts = { - 0, NULL, print_file_item, &print_file_item_data + struct list_and_choose_options opts = { + { 0, NULL, print_file_item, &print_file_item_data }, + NULL, 0, choose_prompt_help }; struct strbuf header = STRBUF_INIT; - struct string_list files = STRING_LIST_INIT_DUP; + struct prefix_item_list files = PREFIX_ITEM_LIST_INIT; ssize_t i; int res = 0; @@ -686,11 +773,13 @@ int run_add_i(struct repository *r, const struct pathspec *ps) data.color = s.prompt_color; data.reset = s.reset_color; } + print_file_item_data.color = data.color; + print_file_item_data.reset = data.reset; strbuf_addstr(&header, " "); strbuf_addf(&header, print_file_item_data.modified_fmt, _("staged"), _("unstaged"), _("path")); - opts.header = header.buf; + opts.list_opts.header = header.buf; if (discard_index(r->index) < 0 || repo_read_index(r) < 0 || @@ -714,8 +803,9 @@ int run_add_i(struct repository *r, const struct pathspec *ps) } } - string_list_clear(&files, 1); + prefix_item_list_clear(&files); strbuf_release(&print_file_item_data.buf); + strbuf_release(&print_file_item_data.name); strbuf_release(&print_file_item_data.index); strbuf_release(&print_file_item_data.worktree); strbuf_release(&header); From 33ebf58b808be51111a4cbf2497e4022777536e7 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Wed, 6 Mar 2019 23:06:13 +0100 Subject: [PATCH 4/8] built-in add -i: re-implement `revert` in C This is a relatively straight-forward port from the Perl version, with the notable exception that we imitate `git reset -- ` in the C version rather than the convoluted `git ls-tree HEAD -- | git update-index --index-info` followed by `git update-index --force-remove -- ` for the missed ones. While at it, we fix the pretty obvious bug where the `revert` command offers to unstage files that do not have staged changes. Signed-off-by: Johannes Schindelin --- add-interactive.c | 109 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) diff --git a/add-interactive.c b/add-interactive.c index 78fbb5be91..53e3439b65 100644 --- a/add-interactive.c +++ b/add-interactive.c @@ -644,6 +644,114 @@ static int run_update(struct add_i_state *s, const struct pathspec *ps, return res; } +static void revert_from_diff(struct diff_queue_struct *q, + struct diff_options *opt, void *data) +{ + int i, add_flags = ADD_CACHE_OK_TO_ADD | ADD_CACHE_OK_TO_REPLACE; + + for (i = 0; i < q->nr; i++) { + struct diff_filespec *one = q->queue[i]->one; + struct cache_entry *ce; + + if (!(one->mode && !is_null_oid(&one->oid))) { + remove_file_from_index(opt->repo->index, one->path); + printf(_("note: %s is untracked now.\n"), one->path); + } else { + ce = make_cache_entry(opt->repo->index, one->mode, + &one->oid, one->path, 0, 0); + if (!ce) + die(_("make_cache_entry failed for path '%s'"), + one->path); + add_index_entry(opt->repo->index, ce, add_flags); + } + } +} + +static int run_revert(struct add_i_state *s, const struct pathspec *ps, + struct prefix_item_list *files, + struct list_and_choose_options *opts) +{ + int res = 0, fd; + size_t count, i, j; + + struct object_id oid; + int is_initial = !resolve_ref_unsafe("HEAD", RESOLVE_REF_READING, &oid, + NULL); + struct lock_file index_lock; + const char **paths; + struct tree *tree; + struct diff_options diffopt = { NULL }; + + if (get_modified_files(s->r, INDEX_ONLY, files, ps) < 0) + return -1; + + if (!files->items.nr) { + putchar('\n'); + return 0; + } + + opts->prompt = N_("Revert"); + count = list_and_choose(s, files, opts); + if (count <= 0) + goto finish_revert; + + fd = repo_hold_locked_index(s->r, &index_lock, LOCK_REPORT_ON_ERROR); + if (fd < 0) { + res = -1; + goto finish_revert; + } + + if (is_initial) + oidcpy(&oid, s->r->hash_algo->empty_tree); + else { + tree = parse_tree_indirect(&oid); + if (!tree) { + res = error(_("Could not parse HEAD^{tree}")); + goto finish_revert; + } + oidcpy(&oid, &tree->object.oid); + } + + ALLOC_ARRAY(paths, count + 1); + for (i = j = 0; i < files->items.nr; i++) + if (files->selected[i]) + paths[j++] = files->items.items[i].string; + paths[j] = NULL; + + parse_pathspec(&diffopt.pathspec, 0, + PATHSPEC_PREFER_FULL | PATHSPEC_LITERAL_PATH, + NULL, paths); + + diffopt.output_format = DIFF_FORMAT_CALLBACK; + diffopt.format_callback = revert_from_diff; + diffopt.flags.override_submodule_config = 1; + diffopt.repo = s->r; + + if (do_diff_cache(&oid, &diffopt)) + res = -1; + else { + diffcore_std(&diffopt); + diff_flush(&diffopt); + } + free(paths); + clear_pathspec(&diffopt.pathspec); + + if (!res && write_locked_index(s->r->index, &index_lock, + COMMIT_LOCK) < 0) + res = -1; + else + res = repo_refresh_and_write_index(s->r, REFRESH_QUIET, 0, 1, + NULL, NULL, NULL); + + if (!res) + printf(Q_("reverted %d path\n", + "reverted %d paths\n", count), (int)count); + +finish_revert: + putchar('\n'); + return res; +} + static int run_help(struct add_i_state *s, const struct pathspec *unused_ps, struct prefix_item_list *unused_files, struct list_and_choose_options *opts) @@ -739,6 +847,7 @@ int run_add_i(struct repository *r, const struct pathspec *ps) } command_list[] = { { "status", run_status }, { "update", run_update }, + { "revert", run_revert }, { "help", run_help }, }; struct prefix_item_list commands = PREFIX_ITEM_LIST_INIT; From 47cd5b58fcf7b33e6b09fe451700e10e878e5bd0 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 7 Mar 2019 00:59:24 +0100 Subject: [PATCH 5/8] built-in add -i: re-implement `add-untracked` in C This is yet another command, ported to C. It builds nicely on the support functions introduced for other commands, with the notable difference that only names are displayed for untracked files, no file type or diff summary. Signed-off-by: Johannes Schindelin --- add-interactive.c | 91 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/add-interactive.c b/add-interactive.c index 53e3439b65..1183c40986 100644 --- a/add-interactive.c +++ b/add-interactive.c @@ -7,6 +7,7 @@ #include "refs.h" #include "string-list.h" #include "lockfile.h" +#include "dir.h" struct add_i_state { struct repository *r; @@ -550,6 +551,7 @@ static int is_valid_prefix(const char *prefix, size_t prefix_len) struct print_file_item_data { const char *modified_fmt, *color, *reset; struct strbuf buf, name, index, worktree; + unsigned only_names:1; }; static void print_file_item(int i, int selected, struct string_list_item *item, @@ -573,6 +575,12 @@ static void print_file_item(int i, int selected, struct string_list_item *item, highlighted = d->name.buf; } + if (d->only_names) { + printf("%c%2d: %s", selected ? '*' : ' ', i + 1, + highlighted ? highlighted : item->string); + return; + } + render_adddel(&d->worktree, &c->worktree, _("nothing")); render_adddel(&d->index, &c->index, _("unchanged")); @@ -752,6 +760,88 @@ finish_revert: return res; } +static int get_untracked_files(struct repository *r, + struct prefix_item_list *files, + const struct pathspec *ps) +{ + struct dir_struct dir = { 0 }; + size_t i; + struct strbuf buf = STRBUF_INIT; + + if (repo_read_index(r) < 0) + return error(_("could not read index")); + + prefix_item_list_clear(files); + setup_standard_excludes(&dir); + add_pattern_list(&dir, EXC_CMDL, "--exclude option"); + fill_directory(&dir, r->index, ps); + + for (i = 0; i < dir.nr; i++) { + struct dir_entry *ent = dir.entries[i]; + + if (index_name_is_other(r->index, ent->name, ent->len)) { + strbuf_reset(&buf); + strbuf_add(&buf, ent->name, ent->len); + add_file_item(&files->items, buf.buf); + } + } + + strbuf_release(&buf); + return 0; +} + +static int run_add_untracked(struct add_i_state *s, const struct pathspec *ps, + struct prefix_item_list *files, + struct list_and_choose_options *opts) +{ + struct print_file_item_data *d = opts->list_opts.print_item_data; + int res = 0, fd; + size_t count, i; + struct lock_file index_lock; + + if (get_untracked_files(s->r, files, ps) < 0) + return -1; + + if (!files->items.nr) { + printf(_("No untracked files.\n")); + goto finish_add_untracked; + } + + opts->prompt = N_("Add untracked"); + d->only_names = 1; + count = list_and_choose(s, files, opts); + d->only_names = 0; + if (count <= 0) + goto finish_add_untracked; + + fd = repo_hold_locked_index(s->r, &index_lock, LOCK_REPORT_ON_ERROR); + if (fd < 0) { + res = -1; + goto finish_add_untracked; + } + + for (i = 0; i < files->items.nr; i++) { + const char *name = files->items.items[i].string; + if (files->selected[i] && + add_file_to_index(s->r->index, name, 0) < 0) { + res = error(_("could not stage '%s'"), name); + break; + } + } + + if (!res && + write_locked_index(s->r->index, &index_lock, COMMIT_LOCK) < 0) + res = error(_("could not write index")); + + if (!res) + printf(Q_("added %d path\n", + "added %d paths\n", count), (int)count); + +finish_add_untracked: + putchar('\n'); + return res; +} + static int run_help(struct add_i_state *s, const struct pathspec *unused_ps, struct prefix_item_list *unused_files, struct list_and_choose_options *opts) @@ -848,6 +938,7 @@ int run_add_i(struct repository *r, const struct pathspec *ps) { "status", run_status }, { "update", run_update }, { "revert", run_revert }, + { "add untracked", run_add_untracked }, { "help", run_help }, }; struct prefix_item_list commands = PREFIX_ITEM_LIST_INIT; From 6f8d73f0c6bb4cdbb37d83ce75d37ff74fa80ef1 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Mon, 11 Mar 2019 10:07:49 +0100 Subject: [PATCH 6/8] built-in add -i: implement the `patch` command Well, it is not a full implementation yet. In the interest of making this easy to review (and easy to keep bugs out), we still hand off to the Perl script to do the actual work. The `patch` functionality actually makes up for more than half of the 1,800+ lines of `git-add--interactive.perl`. It will be ported from Perl to C incrementally, later. Signed-off-by: Johannes Schindelin --- add-interactive.c | 91 +++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 84 insertions(+), 7 deletions(-) diff --git a/add-interactive.c b/add-interactive.c index 1183c40986..e95dd4af35 100644 --- a/add-interactive.c +++ b/add-interactive.c @@ -8,6 +8,7 @@ #include "string-list.h" #include "lockfile.h" #include "dir.h" +#include "run-command.h" struct add_i_state { struct repository *r; @@ -365,7 +366,7 @@ static ssize_t list_and_choose(struct add_i_state *s, struct adddel { uintmax_t add, del; - unsigned seen:1, binary:1; + unsigned seen:1, unmerged:1, binary:1; }; struct file_item { @@ -405,6 +406,7 @@ struct collection_status { const char *reference; unsigned skip_unseen:1; + size_t unmerged_count, binary_count; struct string_list *files; struct hashmap file_map; }; @@ -427,7 +429,7 @@ static void collect_changes_cb(struct diff_queue_struct *q, int hash = strhash(name); struct pathname_entry *entry; struct file_item *file_item; - struct adddel *adddel; + struct adddel *adddel, *other_adddel; entry = hashmap_get_entry_from_hash(&s->file_map, hash, name, struct pathname_entry, ent); @@ -447,11 +449,21 @@ static void collect_changes_cb(struct diff_queue_struct *q, file_item = entry->item; adddel = s->phase == FROM_INDEX ? &file_item->index : &file_item->worktree; + other_adddel = s->phase == FROM_INDEX ? + &file_item->worktree : &file_item->index; adddel->seen = 1; adddel->add = stat.files[i]->added; adddel->del = stat.files[i]->deleted; - if (stat.files[i]->is_binary) + if (stat.files[i]->is_binary) { + if (!other_adddel->binary) + s->binary_count++; adddel->binary = 1; + } + if (stat.files[i]->is_unmerged) { + if (!other_adddel->unmerged) + s->unmerged_count++; + adddel->unmerged = 1; + } } free_diffstat_info(&stat); } @@ -465,7 +477,9 @@ enum modified_files_filter { static int get_modified_files(struct repository *r, enum modified_files_filter filter, struct prefix_item_list *files, - const struct pathspec *ps) + const struct pathspec *ps, + size_t *unmerged_count, + size_t *binary_count) { struct object_id head_oid; int is_initial = !resolve_ref_unsafe("HEAD", RESOLVE_REF_READING, @@ -512,6 +526,10 @@ static int get_modified_files(struct repository *r, } } hashmap_free_entries(&s.file_map, struct pathname_entry, ent); + if (unmerged_count) + *unmerged_count = s.unmerged_count; + if (binary_count) + *binary_count = s.binary_count; /* While the diffs are ordered already, we ran *two* diffs... */ string_list_sort(&files->items); @@ -594,7 +612,7 @@ static int run_status(struct add_i_state *s, const struct pathspec *ps, struct prefix_item_list *files, struct list_and_choose_options *opts) { - if (get_modified_files(s->r, NO_FILTER, files, ps) < 0) + if (get_modified_files(s->r, NO_FILTER, files, ps, NULL, NULL) < 0) return -1; list(s, &files->items, NULL, &opts->list_opts); @@ -611,7 +629,7 @@ static int run_update(struct add_i_state *s, const struct pathspec *ps, size_t count, i; struct lock_file index_lock; - if (get_modified_files(s->r, WORKTREE_ONLY, files, ps) < 0) + if (get_modified_files(s->r, WORKTREE_ONLY, files, ps, NULL, NULL) < 0) return -1; if (!files->items.nr) { @@ -690,7 +708,7 @@ static int run_revert(struct add_i_state *s, const struct pathspec *ps, struct tree *tree; struct diff_options diffopt = { NULL }; - if (get_modified_files(s->r, INDEX_ONLY, files, ps) < 0) + if (get_modified_files(s->r, INDEX_ONLY, files, ps, NULL, NULL) < 0) return -1; if (!files->items.nr) { @@ -842,6 +860,64 @@ finish_add_untracked: return res; } +static int run_patch(struct add_i_state *s, const struct pathspec *ps, + struct prefix_item_list *files, + struct list_and_choose_options *opts) +{ + int res = 0; + ssize_t count, i, j; + size_t unmerged_count = 0, binary_count = 0; + + if (get_modified_files(s->r, WORKTREE_ONLY, files, ps, + &unmerged_count, &binary_count) < 0) + return -1; + + if (unmerged_count || binary_count) { + for (i = j = 0; i < files->items.nr; i++) { + struct file_item *item = files->items.items[i].util; + + if (item->index.binary || item->worktree.binary) { + free(item); + free(files->items.items[i].string); + } else if (item->index.unmerged || + item->worktree.unmerged) { + color_fprintf_ln(stderr, s->error_color, + _("ignoring unmerged: %s"), + files->items.items[i].string); + free(item); + free(files->items.items[i].string); + } else + files->items.items[j++] = files->items.items[i]; + } + files->items.nr = j; + } + + if (!files->items.nr) { + if (binary_count) + fprintf(stderr, _("Only binary files changed.\n")); + else + fprintf(stderr, _("No changes.\n")); + return 0; + } + + opts->prompt = N_("Patch update"); + count = list_and_choose(s, files, opts); + if (count >= 0) { + struct argv_array args = ARGV_ARRAY_INIT; + + argv_array_pushl(&args, "git", "add--interactive", "--patch", + "--", NULL); + for (i = 0; i < files->items.nr; i++) + if (files->selected[i]) + argv_array_push(&args, + files->items.items[i].string); + res = run_command_v_opt(args.argv, 0); + argv_array_clear(&args); + } + + return res; +} + static int run_help(struct add_i_state *s, const struct pathspec *unused_ps, struct prefix_item_list *unused_files, struct list_and_choose_options *opts) @@ -939,6 +1015,7 @@ int run_add_i(struct repository *r, const struct pathspec *ps) { "update", run_update }, { "revert", run_revert }, { "add untracked", run_add_untracked }, + { "patch", run_patch }, { "help", run_help }, }; struct prefix_item_list commands = PREFIX_ITEM_LIST_INIT; From 9bddac0d57d86cf10bb2d1d738e9f13894e86960 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 8 Mar 2019 15:58:27 +0100 Subject: [PATCH 7/8] built-in add -i: re-implement the `diff` command It is not only laziness that we simply spawn `git diff -p --cached` here: this command needs to use the pager, and the pager needs to exit when the diff is done. Currently we do not have any way to make that happen if we run the diff in-process. So let's just spawn. Signed-off-by: Johannes Schindelin --- add-interactive.c | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/add-interactive.c b/add-interactive.c index e95dd4af35..ab7b39ee41 100644 --- a/add-interactive.c +++ b/add-interactive.c @@ -918,6 +918,47 @@ static int run_patch(struct add_i_state *s, const struct pathspec *ps, return res; } +static int run_diff(struct add_i_state *s, const struct pathspec *ps, + struct prefix_item_list *files, + struct list_and_choose_options *opts) +{ + int res = 0; + ssize_t count, i; + + struct object_id oid; + int is_initial = !resolve_ref_unsafe("HEAD", RESOLVE_REF_READING, &oid, + NULL); + if (get_modified_files(s->r, INDEX_ONLY, files, ps, NULL, NULL) < 0) + return -1; + + if (!files->items.nr) { + putchar('\n'); + return 0; + } + + opts->prompt = N_("Review diff"); + opts->flags = IMMEDIATE; + count = list_and_choose(s, files, opts); + opts->flags = 0; + if (count >= 0) { + struct argv_array args = ARGV_ARRAY_INIT; + + argv_array_pushl(&args, "git", "diff", "-p", "--cached", + oid_to_hex(!is_initial ? &oid : + s->r->hash_algo->empty_tree), + "--", NULL); + for (i = 0; i < files->items.nr; i++) + if (files->selected[i]) + argv_array_push(&args, + files->items.items[i].string); + res = run_command_v_opt(args.argv, 0); + argv_array_clear(&args); + } + + putchar('\n'); + return res; +} + static int run_help(struct add_i_state *s, const struct pathspec *unused_ps, struct prefix_item_list *unused_files, struct list_and_choose_options *opts) @@ -1016,6 +1057,7 @@ int run_add_i(struct repository *r, const struct pathspec *ps) { "revert", run_revert }, { "add untracked", run_add_untracked }, { "patch", run_patch }, + { "diff", run_diff }, { "help", run_help }, }; struct prefix_item_list commands = PREFIX_ITEM_LIST_INIT; From 6ba19986d74495f9ea951436acf4fdc60ff5e0cc Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 18 Oct 2019 16:29:10 +0200 Subject: [PATCH 8/8] built-in add -i: offer the `quit` command We do not really want to `exit()` here, of course, as this is safely libified code. Signed-off-by: Johannes Schindelin --- add-interactive.c | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/add-interactive.c b/add-interactive.c index ab7b39ee41..0ddad194cc 100644 --- a/add-interactive.c +++ b/add-interactive.c @@ -1058,6 +1058,7 @@ int run_add_i(struct repository *r, const struct pathspec *ps) { "add untracked", run_add_untracked }, { "patch", run_patch }, { "diff", run_diff }, + { "quit", NULL }, { "help", run_help }, }; struct prefix_item_list commands = PREFIX_ITEM_LIST_INIT; @@ -1109,17 +1110,22 @@ int run_add_i(struct repository *r, const struct pathspec *ps) res = run_status(&s, ps, &files, &opts); for (;;) { + struct command_item *util; + i = list_and_choose(&s, &commands, &main_loop_opts); - if (i == LIST_AND_CHOOSE_QUIT) { + if (i < 0 || i >= commands.items.nr) + util = NULL; + else + util = commands.items.items[i].util; + + if (i == LIST_AND_CHOOSE_QUIT || (util && !util->command)) { printf(_("Bye.\n")); res = 0; break; } - if (i != LIST_AND_CHOOSE_ERROR) { - struct command_item *util = - commands.items.items[i].util; + + if (util) res = util->command(&s, ps, &files, &opts); - } } prefix_item_list_clear(&files);