From f3b3f0ed911e1bc2be0f9b17fb40d46c8f00124e 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 `update` command of `git add -i`, we are primarily interested in the list of modified files that have worktree (i.e. unstaged) changes. 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` calls, and then not adding unnecessary entries in the first place. Signed-off-by: Johannes Schindelin --- add-interactive.c | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/add-interactive.c b/add-interactive.c index c431c72e3f..37d6783d2e 100644 --- a/add-interactive.c +++ b/add-interactive.c @@ -268,6 +268,7 @@ struct collection_status { const char *reference; + unsigned skip_unseen:1; struct file_list *list; struct hashmap file_map; }; @@ -296,6 +297,8 @@ static void collect_changes_cb(struct diff_queue_struct *q, entry = hashmap_get_from_hash(&s->file_map, hash, name); if (entry) file_index = entry->index; + else if (s->skip_unseen) + continue; else { FLEX_ALLOC_STR(entry, pathname, name); hashmap_entry_init(entry, hash); @@ -315,13 +318,22 @@ static void collect_changes_cb(struct diff_queue_struct *q, } } -static int get_modified_files(struct repository *r, struct file_list *list, +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 file_list *list, 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 (repo_read_index_preload(r, ps, 0) < 0) return error(_("could not read index")); @@ -329,10 +341,16 @@ static int get_modified_files(struct repository *r, struct file_list *list, s.list = list; 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); @@ -418,7 +436,7 @@ static int run_status(struct add_i_state *s, const struct pathspec *ps, { reset_file_list(files); - if (get_modified_files(s->r, files, ps) < 0) + if (get_modified_files(s->r, 0, files, ps) < 0) return -1; if (files->nr) From bd5156e9140be34e199f69db2f0d85ebba363d7a 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 | 105 ++++++++++++++++++++++++++++++++++------------ 1 file changed, 78 insertions(+), 27 deletions(-) diff --git a/add-interactive.c b/add-interactive.c index 37d6783d2e..ff54fff5c2 100644 --- a/add-interactive.c +++ b/add-interactive.c @@ -75,12 +75,12 @@ static ssize_t find_unique(const char *string, struct list_options { int columns; const char *header; - void (*print_item)(int i, struct prefix_item *item, + void (*print_item)(int i, int selected, struct prefix_item *item, void *print_item_data); void *print_item_data; }; -static void list(struct prefix_item **list, size_t nr, +static void list(struct prefix_item **list, int *selected, size_t nr, struct add_i_state *s, struct list_options *opts) { int i, last_lf = 0; @@ -93,7 +93,8 @@ static void list(struct prefix_item **list, size_t nr, "%s", opts->header); for (i = 0; i < nr; i++) { - opts->print_item(i, list[i], opts->print_item_data); + opts->print_item(i, selected ? selected[i] : 0, list[i], + opts->print_item_data); if ((opts->columns) && ((i + 1) % (opts->columns))) { putchar('\t'); @@ -112,6 +113,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); }; @@ -119,17 +124,27 @@ 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. */ -static ssize_t list_and_choose(struct prefix_item **items, size_t nr, - struct add_i_state *s, +static ssize_t list_and_choose(struct prefix_item **items, int *selected, + size_t nr, struct add_i_state *s, 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 (!selected && !singleton) + BUG("need a selected array in non-singleton mode"); + + if (singleton && !immediate) + BUG("singleton requires immediate"); find_unique_prefixes(items, nr, 1, 4); @@ -138,15 +153,16 @@ static ssize_t list_and_choose(struct prefix_item **items, size_t nr, strbuf_reset(&input); - list(items, nr, s, &opts->list_opts); + list(items, selected, nr, s, &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); @@ -162,7 +178,9 @@ static ssize_t list_and_choose(struct prefix_item **items, size_t nr, 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) @@ -171,28 +189,60 @@ static ssize_t list_and_choose(struct prefix_item **items, size_t nr, continue; } - if (isdigit(*p)) { - index = strtoul(p, &endp, 10) - 1; - if (endp != p + sep) - index = -1; + /* Input that begins with '-'; unchoose */ + if (*p == '-') { + choose = 0; + p++; + sep--; + } + + if (sep == 1 && *p == '*') { + from = 0; + to = 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, nr); + if (from < 0) { + from = find_unique(p, items, nr); + if (from >= 0) + to = from + 1; + } - if (index < 0 || index >= nr) + if (from < 0 || from >= 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 > nr) + to = nr; + + for (; from < to; from++) + if (selected[from] != choose) { + 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; } @@ -413,7 +463,7 @@ struct print_file_item_data { struct strbuf buf, index, worktree; }; -static void print_file_item(int i, struct prefix_item *item, +static void print_file_item(int i, int selected, struct prefix_item *item, void *print_file_item_data) { struct file_item *c = (struct file_item *)item; @@ -428,7 +478,7 @@ static void print_file_item(int i, struct prefix_item *item, strbuf_addf(&d->buf, d->modified_fmt, d->index.buf, d->worktree.buf, item->name); - 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, @@ -440,7 +490,8 @@ static int run_status(struct add_i_state *s, const struct pathspec *ps, return -1; if (files->nr) - list((struct prefix_item **)files->file, files->nr, s, opts); + list((struct prefix_item **)files->file, NULL, files->nr, + s, opts); putchar('\n'); return 0; @@ -471,7 +522,7 @@ struct print_command_item_data { const char *color, *reset; }; -static void print_command_item(int i, struct prefix_item *item, +static void print_command_item(int i, int selected, struct prefix_item *item, void *print_command_item_data) { struct print_command_item_data *d = print_command_item_data; @@ -509,7 +560,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 command_item status = { { "status" }, run_status }, @@ -555,7 +606,7 @@ int run_add_i(struct repository *r, const struct pathspec *ps) res = -1; for (;;) { - i = list_and_choose((struct prefix_item **)commands, + i = list_and_choose((struct prefix_item **)commands, NULL, ARRAY_SIZE(commands), &s, &main_loop_opts); if (i == LIST_AND_CHOOSE_QUIT) { printf(_("Bye.\n")); From f4602ba5bf8db0490cfc566795f2d1a5f66af4e4 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. Signed-off-by: Johannes Schindelin --- add-interactive.c | 124 +++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 112 insertions(+), 12 deletions(-) diff --git a/add-interactive.c b/add-interactive.c index ff54fff5c2..ef807ec3bd 100644 --- a/add-interactive.c +++ b/add-interactive.c @@ -6,6 +6,7 @@ #include "revision.h" #include "refs.h" #include "prefix-map.h" +#include "lockfile.h" struct add_i_state { struct repository *r; @@ -459,8 +460,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 prefix_item *item, @@ -468,21 +469,34 @@ static void print_file_item(int i, int selected, struct prefix_item *item, { struct file_item *c = (struct file_item *)item; 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 (item->prefix_length > 0 && + is_valid_prefix(item->name, item->prefix_length)) { + strbuf_reset(&d->name); + strbuf_addf(&d->name, "%s%.*s%s%s", d->color, + (int)item->prefix_length, item->name, d->reset, + item->name + item->prefix_length); + highlighted = d->name.buf; + } + populate_wi_changes(&d->worktree, &c->worktree, _("nothing")); populate_wi_changes(&d->index, &c->index, _("unchanged")); strbuf_addf(&d->buf, d->modified_fmt, - d->index.buf, d->worktree.buf, item->name); + d->index.buf, d->worktree.buf, + highlighted ? highlighted : item->name); printf("%c%2d: %s", selected ? '*' : ' ', i + 1, d->buf.buf); } static int run_status(struct add_i_state *s, const struct pathspec *ps, - struct file_list *files, struct list_options *opts) + struct file_list *files, + struct list_and_choose_options *opts) { reset_file_list(files); @@ -491,14 +505,72 @@ static int run_status(struct add_i_state *s, const struct pathspec *ps, if (files->nr) list((struct prefix_item **)files->file, NULL, files->nr, - s, opts); + s, &opts->list_opts); putchar('\n'); return 0; } +static int run_update(struct add_i_state *s, const struct pathspec *ps, + struct file_list *files, + struct list_and_choose_options *opts) +{ + int res = 0, fd, *selected = NULL; + size_t count, i; + struct lock_file index_lock; + + reset_file_list(files); + + if (get_modified_files(s->r, WORKTREE_ONLY, files, ps) < 0) + return -1; + + if (!files->nr) { + putchar('\n'); + return 0; + } + + opts->prompt = N_("Update"); + CALLOC_ARRAY(selected, files->nr); + + count = list_and_choose((struct prefix_item **)files->file, + selected, files->nr, s, opts); + if (count <= 0) { + putchar('\n'); + free(selected); + return 0; + } + + fd = repo_hold_locked_index(s->r, &index_lock, LOCK_REPORT_ON_ERROR); + if (fd < 0) { + putchar('\n'); + free(selected); + return -1; + } + + for (i = 0; i < files->nr; i++) { + const char *name = files->file[i]->item.name; + if (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'); + free(selected); + return res; +} + static int run_help(struct add_i_state *s, const struct pathspec *ps, - struct file_list *files, struct list_options *opts) + struct file_list *files, + struct list_and_choose_options *opts) { const char *help_color = s->help_color; @@ -518,6 +590,27 @@ static int run_help(struct add_i_state *s, const struct pathspec *ps, return 0; } +static void choose_prompt_help(struct add_i_state *s) +{ + const char *help_color = s->help_color; + 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")); +} + struct print_command_item_data { const char *color, *reset; }; @@ -539,7 +632,8 @@ static void print_command_item(int i, int selected, struct prefix_item *item, struct command_item { struct prefix_item item; int (*command)(struct add_i_state *s, const struct pathspec *ps, - struct file_list *files, struct list_options *opts); + struct file_list *files, + struct list_and_choose_options *opts); }; static void command_prompt_help(struct add_i_state *s) @@ -564,17 +658,20 @@ int run_add_i(struct repository *r, const struct pathspec *ps) }; struct command_item status = { { "status" }, run_status }, + update = { { "update" }, run_update }, help = { { "help" }, run_help }; struct command_item *commands[] = { - &status, + &status, &update, &help }; 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 file_list files = { NULL }; @@ -595,11 +692,13 @@ int run_add_i(struct repository *r, const struct pathspec *ps) data.color = "["; data.reset = "]"; } + 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; repo_refresh_and_write_index(r, REFRESH_QUIET, 1); if (run_status(&s, ps, &files, &opts) < 0) @@ -619,6 +718,7 @@ int run_add_i(struct repository *r, const struct pathspec *ps) release_file_list(&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 d3c4b7b72407e6e30978ac9e43cef5e55f1e7643 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 | 114 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 113 insertions(+), 1 deletion(-) diff --git a/add-interactive.c b/add-interactive.c index ef807ec3bd..d938a2b98f 100644 --- a/add-interactive.c +++ b/add-interactive.c @@ -568,6 +568,117 @@ 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 file_list *files, + struct list_and_choose_options *opts) +{ + int res = 0, fd, *selected = NULL; + 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 }; + + reset_file_list(files); + if (get_modified_files(s->r, INDEX_ONLY, files, ps) < 0) + return -1; + + if (!files->nr) { + putchar('\n'); + return 0; + } + + opts->prompt = N_("Revert"); + CALLOC_ARRAY(selected, files->nr); + + count = list_and_choose((struct prefix_item **)files->file, + selected, files->nr, s, 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->nr; i++) + if (selected[i]) + paths[j++] = files->file[i]->item.name; + 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, 1); + if (!res) + printf(Q_("reverted %d path\n", + "reverted %d paths\n", count), (int)count); + +finish_revert: + putchar('\n'); + free(selected); + return res; +} + static int run_help(struct add_i_state *s, const struct pathspec *ps, struct file_list *files, struct list_and_choose_options *opts) @@ -659,9 +770,10 @@ int run_add_i(struct repository *r, const struct pathspec *ps) struct command_item status = { { "status" }, run_status }, update = { { "update" }, run_update }, + revert = { { "revert" }, run_revert }, help = { { "help" }, run_help }; struct command_item *commands[] = { - &status, &update, + &status, &update, &revert, &help }; From c5fac3487ade8061a505cee1a43c4c193f23c6cd 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 | 98 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 97 insertions(+), 1 deletion(-) diff --git a/add-interactive.c b/add-interactive.c index d938a2b98f..8edb962dec 100644 --- a/add-interactive.c +++ b/add-interactive.c @@ -7,6 +7,8 @@ #include "refs.h" #include "prefix-map.h" #include "lockfile.h" +#include "pathspec.h" +#include "dir.h" struct add_i_state { struct repository *r; @@ -462,6 +464,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 prefix_item *item, @@ -485,6 +488,12 @@ static void print_file_item(int i, int selected, struct prefix_item *item, highlighted = d->name.buf; } + if (d->only_names) { + printf("%c%2d: %s", selected ? '*' : ' ', i + 1, + highlighted ? highlighted : item->name); + return; + } + populate_wi_changes(&d->worktree, &c->worktree, _("nothing")); populate_wi_changes(&d->index, &c->index, _("unchanged")); strbuf_addf(&d->buf, d->modified_fmt, @@ -679,6 +688,92 @@ finish_revert: return res; } +static int get_untracked_files(struct repository *r, struct file_list *list, + const struct pathspec *ps) +{ + struct dir_struct dir = { 0 }; + size_t i; + + if (repo_read_index(r) < 0) + return error(_("could not read index")); + + setup_standard_excludes(&dir); + add_exclude_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)) { + struct file_item *item; + + FLEXPTR_ALLOC_MEM(item, item.name, ent->name, ent->len); + + ALLOC_GROW(list->file, list->nr + 1, list->alloc); + list->file[list->nr++] = item; + } + } + + return 0; +} + +static int run_add_untracked(struct add_i_state *s, const struct pathspec *ps, + struct file_list *files, + struct list_and_choose_options *opts) +{ + struct print_file_item_data *d = opts->list_opts.print_item_data; + int res = 0, fd, *selected = NULL; + size_t count, i; + + struct lock_file index_lock; + + reset_file_list(files); + if (get_untracked_files(s->r, files, ps) < 0) + return -1; + + if (!files->nr) { + printf(_("No untracked files.\n")); + goto finish_add_untracked; + } + + opts->prompt = N_("Add untracked"); + CALLOC_ARRAY(selected, files->nr); + + d->only_names = 1; + count = list_and_choose((struct prefix_item **)files->file, + selected, files->nr, s, 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->nr; i++) { + const char *name = files->file[i]->item.name; + if (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'); + free(selected); + return res; +} + static int run_help(struct add_i_state *s, const struct pathspec *ps, struct file_list *files, struct list_and_choose_options *opts) @@ -771,9 +866,10 @@ int run_add_i(struct repository *r, const struct pathspec *ps) status = { { "status" }, run_status }, update = { { "update" }, run_update }, revert = { { "revert" }, run_revert }, + add_untracked = { { "add untracked" }, run_add_untracked }, help = { { "help" }, run_help }; struct command_item *commands[] = { - &status, &update, &revert, + &status, &update, &revert, &add_untracked, &help }; From f22f7b783cf471a475375c350bcca299a8ab9d1b 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 | 97 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 88 insertions(+), 9 deletions(-) diff --git a/add-interactive.c b/add-interactive.c index 8edb962dec..fe413664b4 100644 --- a/add-interactive.c +++ b/add-interactive.c @@ -9,6 +9,8 @@ #include "lockfile.h" #include "pathspec.h" #include "dir.h" +#include "argv-array.h" +#include "run-command.h" struct add_i_state { struct repository *r; @@ -255,7 +257,7 @@ static ssize_t list_and_choose(struct prefix_item **items, int *selected, struct adddel { uintmax_t add, del; - unsigned seen:1, binary:1; + unsigned seen:1, unmerged:1, binary:1; }; struct file_list { @@ -322,6 +324,7 @@ struct collection_status { const char *reference; unsigned skip_unseen:1; + size_t unmerged_count, binary_count; struct file_list *list; struct hashmap file_map; }; @@ -345,12 +348,14 @@ static void collect_changes_cb(struct diff_queue_struct *q, struct pathname_entry *entry; size_t file_index; struct file_item *file; - struct adddel *adddel; + struct adddel *adddel, *other_adddel; entry = hashmap_get_from_hash(&s->file_map, hash, name); - if (entry) + if (entry) { + if (entry->index == (size_t)-1) + continue; file_index = entry->index; - else if (s->skip_unseen) + } else if (s->skip_unseen) continue; else { FLEX_ALLOC_STR(entry, pathname, name); @@ -363,11 +368,20 @@ static void collect_changes_cb(struct diff_queue_struct *q, file = s->list->file[file_index]; adddel = s->phase == FROM_INDEX ? &file->index : &file->worktree; + other_adddel = s->phase == FROM_INDEX ? &file->worktree : &file->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; + } } } @@ -379,6 +393,8 @@ enum modified_files_filter { static int get_modified_files(struct repository *r, enum modified_files_filter filter, + size_t *unmerged_count, + size_t *binary_count, struct file_list *list, const struct pathspec *ps) { @@ -425,6 +441,10 @@ static int get_modified_files(struct repository *r, } } hashmap_free(&s.file_map, 1); + 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... */ QSORT(list->file, list->nr, file_item_cmp); @@ -509,7 +529,7 @@ static int run_status(struct add_i_state *s, const struct pathspec *ps, { reset_file_list(files); - if (get_modified_files(s->r, 0, files, ps) < 0) + if (get_modified_files(s->r, 0, NULL, NULL, files, ps) < 0) return -1; if (files->nr) @@ -530,7 +550,7 @@ static int run_update(struct add_i_state *s, const struct pathspec *ps, reset_file_list(files); - if (get_modified_files(s->r, WORKTREE_ONLY, files, ps) < 0) + if (get_modified_files(s->r, WORKTREE_ONLY, NULL, NULL, files, ps) < 0) return -1; if (!files->nr) { @@ -616,7 +636,7 @@ static int run_revert(struct add_i_state *s, const struct pathspec *ps, struct diff_options diffopt = { NULL }; reset_file_list(files); - if (get_modified_files(s->r, INDEX_ONLY, files, ps) < 0) + if (get_modified_files(s->r, INDEX_ONLY, NULL, NULL, files, ps) < 0) return -1; if (!files->nr) { @@ -774,6 +794,64 @@ finish_add_untracked: return res; } +static int run_patch(struct add_i_state *s, const struct pathspec *ps, + struct file_list *files, + struct list_and_choose_options *opts) +{ + struct prefix_item **items = (struct prefix_item **)files->file; + int res = 0, *selected = NULL; + ssize_t count, i, j; + size_t unmerged_count = 0, binary_count = 0; + + reset_file_list(files); + if (get_modified_files(s->r, WORKTREE_ONLY, &unmerged_count, + &binary_count, files, ps) < 0) + return -1; + + if (unmerged_count || binary_count) { + for (i = j = 0; i < files->nr; i++) + if (files->file[i]->index.binary || + files->file[i]->worktree.binary) + free(items[i]); + else if (files->file[i]->index.unmerged || + files->file[i]->worktree.unmerged) { + color_fprintf_ln(stderr, s->error_color, + _("ignoring unmerged: %s"), + files->file[i]->item.name); + free(items[i]); + } else + items[j++] = items[i]; + files->nr = j; + } + + if (!files->nr) { + if (binary_count) + fprintf(stderr, _("Only binary files changed.\n")); + else + fprintf(stderr, _("No changes.\n")); + return 0; + } + + opts->prompt = N_("Patch update"); + CALLOC_ARRAY(selected, files->nr); + + count = list_and_choose(items, selected, files->nr, s, 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->nr; i++) + if (selected[i]) + argv_array_push(&args, items[i]->name); + res = run_command_v_opt(args.argv, 0); + argv_array_clear(&args); + } + + free(selected); + return res; +} + static int run_help(struct add_i_state *s, const struct pathspec *ps, struct file_list *files, struct list_and_choose_options *opts) @@ -867,10 +945,11 @@ int run_add_i(struct repository *r, const struct pathspec *ps) update = { { "update" }, run_update }, revert = { { "revert" }, run_revert }, add_untracked = { { "add untracked" }, run_add_untracked }, + patch = { { "patch" }, run_patch }, help = { { "help" }, run_help }; struct command_item *commands[] = { &status, &update, &revert, &add_untracked, - &help + &patch, &help }; struct print_file_item_data print_file_item_data = { From 040d8e771055de4e7fa0e4e86b838ede300da74e 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 | 48 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/add-interactive.c b/add-interactive.c index fe413664b4..000b07c47d 100644 --- a/add-interactive.c +++ b/add-interactive.c @@ -852,6 +852,51 @@ 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 file_list *files, + struct list_and_choose_options *opts) +{ + struct prefix_item **items = (struct prefix_item **)files->file; + int res = 0, *selected = NULL; + ssize_t count, i; + + struct object_id oid; + int is_initial = !resolve_ref_unsafe("HEAD", RESOLVE_REF_READING, &oid, + NULL); + reset_file_list(files); + if (get_modified_files(s->r, INDEX_ONLY, NULL, NULL, files, ps) < 0) + return -1; + + if (!files->nr) { + putchar('\n'); + return 0; + } + + opts->prompt = N_("Review diff"); + CALLOC_ARRAY(selected, files->nr); + + opts->flags = IMMEDIATE; + count = list_and_choose(items, selected, files->nr, s, 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->nr; i++) + if (selected[i]) + argv_array_push(&args, items[i]->name); + res = run_command_v_opt(args.argv, 0); + argv_array_clear(&args); + } + + putchar('\n'); + free(selected); + return res; +} + static int run_help(struct add_i_state *s, const struct pathspec *ps, struct file_list *files, struct list_and_choose_options *opts) @@ -946,10 +991,11 @@ int run_add_i(struct repository *r, const struct pathspec *ps) revert = { { "revert" }, run_revert }, add_untracked = { { "add untracked" }, run_add_untracked }, patch = { { "patch" }, run_patch }, + diff = { { "diff" }, run_diff }, help = { { "help" }, run_help }; struct command_item *commands[] = { &status, &update, &revert, &add_untracked, - &patch, &help + &patch, &diff, &help }; struct print_file_item_data print_file_item_data = { From 0efdd4fca2e391a665b5a1cf16a41ca2f00ad469 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 7 Mar 2019 23:37:23 +0100 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 | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/add-interactive.c b/add-interactive.c index 000b07c47d..7bc999e929 100644 --- a/add-interactive.c +++ b/add-interactive.c @@ -992,10 +992,11 @@ int run_add_i(struct repository *r, const struct pathspec *ps) add_untracked = { { "add untracked" }, run_add_untracked }, patch = { { "patch" }, run_patch }, diff = { { "diff" }, run_diff }, + quit = { { "quit" }, NULL }, help = { { "help" }, run_help }; struct command_item *commands[] = { &status, &update, &revert, &add_untracked, - &patch, &diff, &help + &patch, &diff, &quit, &help }; struct print_file_item_data print_file_item_data = { @@ -1040,7 +1041,8 @@ int run_add_i(struct repository *r, const struct pathspec *ps) for (;;) { i = list_and_choose((struct prefix_item **)commands, NULL, ARRAY_SIZE(commands), &s, &main_loop_opts); - if (i == LIST_AND_CHOOSE_QUIT) { + if (i == LIST_AND_CHOOSE_QUIT || + (i != LIST_AND_CHOOSE_ERROR && !commands[i]->command)) { printf(_("Bye.\n")); res = 0; break;