From f83dff60a7880f1abfc3dc3c6b8e2972c4889da5 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Wed, 13 Nov 2019 12:40:57 +0000 Subject: Start to implement a built-in version of `git add --interactive` Unlike previous conversions to C, where we started with a built-in helper, we start this conversion by adding an interception in the `run_add_interactive()` function when the new opt-in `add.interactive.useBuiltin` config knob is turned on (or the corresponding environment variable `GIT_TEST_ADD_I_USE_BUILTIN`), and calling the new internal API function `run_add_i()` that is implemented directly in libgit.a. At this point, the built-in version of `git add -i` only states that it cannot do anything yet. In subsequent patches/patch series, the `run_add_i()` function will gain more and more functionality, until it is feature complete. The whole arc of the conversion can be found in the PRs #170-175 at https://github.com/gitgitgadget/git. The "--helper approach" can unfortunately not be used here: on Windows we face the very specific problem that a `system()` call in Perl seems to close `stdin` in the parent process when the spawned process consumes even one character from `stdin`. Which prevents us from implementing the main loop in C and still trying to hand off to the Perl script. The very real downside of the approach we have to take here is that the test suite won't pass with `GIT_TEST_ADD_I_USE_BUILTIN=true` until the conversion is complete (the `--helper` approach would have let it pass, even at each of the incremental conversion steps). Signed-off-by: Johannes Schindelin Signed-off-by: Junio C Hamano diff --git a/Documentation/config/add.txt b/Documentation/config/add.txt index 4d753f0..c9f748f 100644 --- a/Documentation/config/add.txt +++ b/Documentation/config/add.txt @@ -5,3 +5,8 @@ add.ignore-errors (deprecated):: option of linkgit:git-add[1]. `add.ignore-errors` is deprecated, as it does not follow the usual naming convention for configuration variables. + +add.interactive.useBuiltin:: + [EXPERIMENTAL] Set to `true` to use the experimental built-in + implementation of the interactive version of linkgit:git-add[1] + instead of the Perl script version. Is `false` by default. diff --git a/Makefile b/Makefile index 58b92af..6c4a1e0 100644 --- a/Makefile +++ b/Makefile @@ -823,6 +823,7 @@ LIB_H := $(sort $(patsubst ./%,%,$(shell git ls-files '*.h' ':!t/' ':!Documentat -name '*.h' -print))) LIB_OBJS += abspath.o +LIB_OBJS += add-interactive.o LIB_OBJS += advice.o LIB_OBJS += alias.o LIB_OBJS += alloc.o diff --git a/add-interactive.c b/add-interactive.c new file mode 100644 index 0000000..482e458 --- /dev/null +++ b/add-interactive.c @@ -0,0 +1,7 @@ +#include "cache.h" +#include "add-interactive.h" + +int run_add_i(struct repository *r, const struct pathspec *ps) +{ + die(_("No commands are available in the built-in `git add -i` yet!")); +} diff --git a/add-interactive.h b/add-interactive.h new file mode 100644 index 0000000..7043b87 --- /dev/null +++ b/add-interactive.h @@ -0,0 +1,8 @@ +#ifndef ADD_INTERACTIVE_H +#define ADD_INTERACTIVE_H + +struct repository; +struct pathspec; +int run_add_i(struct repository *r, const struct pathspec *ps); + +#endif diff --git a/builtin/add.c b/builtin/add.c index dd18e5c..d4686d5 100644 --- a/builtin/add.c +++ b/builtin/add.c @@ -20,6 +20,7 @@ #include "bulk-checkin.h" #include "argv-array.h" #include "submodule.h" +#include "add-interactive.h" static const char * const builtin_add_usage[] = { N_("git add [] [--] ..."), @@ -185,6 +186,16 @@ int run_add_interactive(const char *revision, const char *patch_mode, { int status, i; struct argv_array argv = ARGV_ARRAY_INIT; + int use_builtin_add_i = + git_env_bool("GIT_TEST_ADD_I_USE_BUILTIN", -1); + + if (!patch_mode) { + if (use_builtin_add_i < 0) + git_config_get_bool("add.interactive.usebuiltin", + &use_builtin_add_i); + if (use_builtin_add_i == 1) + return !!run_add_i(the_repository, pathspec); + } argv_array_push(&argv, "add--interactive"); if (patch_mode) @@ -319,6 +330,7 @@ static int add_config(const char *var, const char *value, void *cb) ignore_add_errors = git_config_bool(var, value); return 0; } + return git_default_config(var, value, cb); } diff --git a/t/README b/t/README index 60d5b77..5132ec8 100644 --- a/t/README +++ b/t/README @@ -397,6 +397,10 @@ GIT_TEST_STASH_USE_BUILTIN=, when false, disables the built-in version of git-stash. See 'stash.useBuiltin' in git-config(1). +GIT_TEST_ADD_I_USE_BUILTIN=, when true, enables the +built-in version of git add -i. See 'add.interactive.useBuiltin' in +git-config(1). + GIT_TEST_INDEX_THREADS= enables exercising the multi-threaded loading of the index for the whole test suite by bypassing the default number of cache entries and thread minimums. Setting this to 1 will make the -- cgit v0.10.2-6-g49f6 From e4cb659ebdd7c21976f9088226cbcc96acaa1dff Mon Sep 17 00:00:00 2001 From: Daniel Ferreira Date: Wed, 13 Nov 2019 12:40:58 +0000 Subject: diff: export diffstat interface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Make the diffstat interface (namely, the diffstat_t struct and compute_diffstat) no longer be internal to diff.c and allow it to be used by other parts of git. This is helpful for code that may want to easily extract information from files using the diff machinery, while flushing it differently from how the show_* functions used by diff_flush() do it. One example is the builtin implementation of git-add--interactive's status. Signed-off-by: Daniel Ferreira Signed-off-by: Slavica Đukić Signed-off-by: Johannes Schindelin Signed-off-by: Junio C Hamano diff --git a/diff.c b/diff.c index afe4400..5703a9b 100644 --- a/diff.c +++ b/diff.c @@ -2495,22 +2495,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) @@ -3157,7 +3141,7 @@ static void show_dirstat_by_line(struct diffstat_t *data, struct diff_options *o gather_dirstat(options, &dir, changed, "", 0); } -static void free_diffstat_info(struct diffstat_t *diffstat) +void free_diffstat_info(struct diffstat_t *diffstat) { int i; for (i = 0; i < diffstat->nr; i++) { @@ -6283,12 +6267,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) @@ -6621,6 +6600,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 7f8f024..d986ddc 100644 --- a/diff.h +++ b/diff.h @@ -245,6 +245,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, @@ -334,6 +350,10 @@ 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); +void free_diffstat_info(struct diffstat_t *diffstat); + #define DIFF_SETUP_REVERSE 1 #define DIFF_SETUP_USE_SIZE_CACHE 4 -- cgit v0.10.2-6-g49f6 From 5e82b9e4d2bac376a817b2b48849ec061c137e49 Mon Sep 17 00:00:00 2001 From: Daniel Ferreira Date: Wed, 13 Nov 2019 12:40:59 +0000 Subject: built-in add -i: implement the `status` command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This implements the `status` command of `git add -i`. The data structures introduced in this commit will be extended later, as needed. At this point, we re-implement only part of the `list_and_choose()` function of the Perl script `git-add--interactive.perl` and call it `list()`. It does not yet color anything, or do columns, or allow user input. Over the course of the next commits, we will introduce a `list_and_choose()` function that uses `list()` to display the list of options and let the user choose one or more of the displayed items. This will be used to implement the main loop of the built-in `git add -i`, at which point the new `status` command can actually be used. Signed-off-by: Daniel Ferreira Signed-off-by: Slavica Đukić Signed-off-by: Johannes Schindelin Signed-off-by: Junio C Hamano diff --git a/add-interactive.c b/add-interactive.c index 482e458..aa35184 100644 --- a/add-interactive.c +++ b/add-interactive.c @@ -1,7 +1,256 @@ #include "cache.h" #include "add-interactive.h" +#include "diffcore.h" +#include "revision.h" +#include "refs.h" +#include "string-list.h" + +struct add_i_state { + struct repository *r; +}; + +static void init_add_i_state(struct add_i_state *s, struct repository *r) +{ + s->r = r; +} + +struct list_options { + const char *header; + void (*print_item)(int i, struct string_list_item *item, void *print_item_data); + void *print_item_data; +}; + +static void list(struct string_list *list, struct list_options *opts) +{ + int i; + + if (!list->nr) + return; + + if (opts->header) + printf("%s\n", opts->header); + + for (i = 0; i < list->nr; i++) { + opts->print_item(i, list->items + i, opts->print_item_data); + putchar('\n'); + } +} + +struct adddel { + uintmax_t add, del; + unsigned seen:1, binary:1; +}; + +struct file_item { + struct adddel index, worktree; +}; + +static void add_file_item(struct string_list *files, const char *name) +{ + struct file_item *item = xcalloc(sizeof(*item), 1); + + string_list_append(files, name)->util = item; +} + +struct pathname_entry { + struct hashmap_entry ent; + const char *name; + struct file_item *item; +}; + +static int pathname_entry_cmp(const void *unused_cmp_data, + const struct hashmap_entry *he1, + const struct hashmap_entry *he2, + const void *name) +{ + const struct pathname_entry *e1 = + container_of(he1, const struct pathname_entry, ent); + const struct pathname_entry *e2 = + container_of(he2, const struct pathname_entry, ent); + + return strcmp(e1->name, name ? (const char *)name : e2->name); +} + +struct collection_status { + enum { FROM_WORKTREE = 0, FROM_INDEX = 1 } phase; + + const char *reference; + + struct string_list *files; + struct hashmap file_map; +}; + +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++) { + const char *name = stat.files[i]->name; + int hash = strhash(name); + struct pathname_entry *entry; + struct file_item *file_item; + struct adddel *adddel; + + entry = hashmap_get_entry_from_hash(&s->file_map, hash, name, + struct pathname_entry, ent); + if (!entry) { + add_file_item(s->files, name); + + entry = xcalloc(sizeof(*entry), 1); + hashmap_entry_init(&entry->ent, hash); + entry->name = s->files->items[s->files->nr - 1].string; + entry->item = s->files->items[s->files->nr - 1].util; + hashmap_add(&s->file_map, &entry->ent); + } + + file_item = entry->item; + adddel = s->phase == FROM_INDEX ? + &file_item->index : &file_item->worktree; + adddel->seen = 1; + adddel->add = stat.files[i]->added; + adddel->del = stat.files[i]->deleted; + if (stat.files[i]->is_binary) + adddel->binary = 1; + } + free_diffstat_info(&stat); +} + +static int get_modified_files(struct repository *r, 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 }; + + if (discard_index(r->index) < 0 || + repo_read_index_preload(r, ps, 0) < 0) + return error(_("could not read index")); + + string_list_clear(files, 1); + s.files = files; + hashmap_init(&s.file_map, pathname_entry_cmp, NULL, 0); + + for (s.phase = FROM_WORKTREE; s.phase <= FROM_INDEX; s.phase++) { + struct rev_info rev; + struct setup_revision_opt opt = { 0 }; + + opt.def = is_initial ? + empty_tree_oid_hex() : oid_to_hex(&head_oid); + + init_revisions(&rev, NULL); + 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; + + if (ps) + copy_pathspec(&rev.prune_data, ps); + + if (s.phase == FROM_INDEX) + run_diff_index(&rev, 1); + else { + rev.diffopt.flags.ignore_dirty_submodules = 1; + run_diff_files(&rev, 0); + } + } + hashmap_free_entries(&s.file_map, struct pathname_entry, ent); + + /* While the diffs are ordered already, we ran *two* diffs... */ + string_list_sort(files); + + return 0; +} + +static void render_adddel(struct strbuf *buf, + struct adddel *ad, const char *no_changes) +{ + if (ad->binary) + strbuf_addstr(buf, _("binary")); + else if (ad->seen) + strbuf_addf(buf, "+%"PRIuMAX"/-%"PRIuMAX, + (uintmax_t)ad->add, (uintmax_t)ad->del); + else + strbuf_addstr(buf, no_changes); +} + +struct print_file_item_data { + const char *modified_fmt; + struct strbuf buf, index, worktree; +}; + +static void print_file_item(int i, struct string_list_item *item, + void *print_file_item_data) +{ + struct file_item *c = item->util; + struct print_file_item_data *d = print_file_item_data; + + strbuf_reset(&d->index); + strbuf_reset(&d->worktree); + strbuf_reset(&d->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); + + printf(" %2d: %s", 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) +{ + if (get_modified_files(s->r, files, ps) < 0) + return -1; + + list(files, opts); + putchar('\n'); + + return 0; +} int run_add_i(struct repository *r, const struct pathspec *ps) { - die(_("No commands are available in the built-in `git add -i` yet!")); + struct add_i_state s = { NULL }; + struct print_file_item_data print_file_item_data = { + "%12s %12s %s", STRBUF_INIT, STRBUF_INIT, STRBUF_INIT + }; + struct list_options opts = { + NULL, print_file_item, &print_file_item_data + }; + struct strbuf header = STRBUF_INIT; + struct string_list files = STRING_LIST_INIT_DUP; + int res = 0; + + init_add_i_state(&s, r); + strbuf_addstr(&header, " "); + strbuf_addf(&header, print_file_item_data.modified_fmt, + _("staged"), _("unstaged"), _("path")); + opts.header = header.buf; + + if (discard_index(r->index) < 0 || + repo_read_index(r) < 0 || + repo_refresh_and_write_index(r, REFRESH_QUIET, 0, 1, + NULL, NULL, NULL) < 0) + warning(_("could not refresh index")); + + res = run_status(&s, ps, &files, &opts); + + string_list_clear(&files, 1); + strbuf_release(&print_file_item_data.buf); + strbuf_release(&print_file_item_data.index); + strbuf_release(&print_file_item_data.worktree); + strbuf_release(&header); + + return res; } -- cgit v0.10.2-6-g49f6 From 1daaebcaa55b43198f3e91135e738fc9e06ec549 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Slavica=20=C4=90uki=C4=87?= Date: Wed, 13 Nov 2019 12:41:00 +0000 Subject: built-in add -i: color the header in the `status` command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For simplicity, we only implemented the `status` command without colors. This patch starts adding color, matching what the Perl script `git-add--interactive.perl` does. Original-Patch-By: Daniel Ferreira Signed-off-by: Slavica Đukić Signed-off-by: Johannes Schindelin Signed-off-by: Junio C Hamano diff --git a/add-interactive.c b/add-interactive.c index aa35184..174e07c 100644 --- a/add-interactive.c +++ b/add-interactive.c @@ -1,5 +1,7 @@ #include "cache.h" #include "add-interactive.h" +#include "color.h" +#include "config.h" #include "diffcore.h" #include "revision.h" #include "refs.h" @@ -7,11 +9,40 @@ struct add_i_state { struct repository *r; + int use_color; + char header_color[COLOR_MAXLEN]; }; +static void init_color(struct repository *r, struct add_i_state *s, + const char *slot_name, char *dst, + const char *default_color) +{ + char *key = xstrfmt("color.interactive.%s", slot_name); + const char *value; + + if (!s->use_color) + dst[0] = '\0'; + else if (repo_config_get_value(r, key, &value) || + color_parse(value, dst)) + strlcpy(dst, default_color, COLOR_MAXLEN); + + free(key); +} + static void init_add_i_state(struct add_i_state *s, struct repository *r) { - s->r = r; + const char *value; + + s->r = r; + + if (repo_config_get_value(r, "color.interactive", &value)) + s->use_color = -1; + else + s->use_color = + git_config_colorbool("color.interactive", value); + s->use_color = want_color(s->use_color); + + init_color(r, s, "header", s->header_color, GIT_COLOR_BOLD); } struct list_options { @@ -20,7 +51,8 @@ struct list_options { void *print_item_data; }; -static void list(struct string_list *list, struct list_options *opts) +static void list(struct add_i_state *s, struct string_list *list, + struct list_options *opts) { int i; @@ -28,7 +60,8 @@ static void list(struct string_list *list, struct list_options *opts) return; if (opts->header) - printf("%s\n", opts->header); + color_fprintf_ln(stdout, s->header_color, + "%s", opts->header); for (i = 0; i < list->nr; i++) { opts->print_item(i, list->items + i, opts->print_item_data); @@ -213,7 +246,7 @@ static int run_status(struct add_i_state *s, const struct pathspec *ps, if (get_modified_files(s->r, files, ps) < 0) return -1; - list(files, opts); + list(s, files, opts); putchar('\n'); return 0; -- cgit v0.10.2-6-g49f6 From 6348bfba5843a17efc33b4ca13db36a982f1baed Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 15 Nov 2019 11:11:16 +0000 Subject: built-in add -i: implement the main loop The reason why we did not start with the main loop to begin with is that it is the first user of `list_and_choose()`, which uses the `list()` function that we conveniently introduced for use by the `status` command. In contrast to the Perl version, in the built-in interactive `add`, we will keep the `list()` function (which only displays items) and the `list_and_choose()` function (which uses `list()` to display the items, and only takes care of the "and choose" part) separate. The `list_and_choose()` function, as implemented in `git-add--interactive.perl` knows a few more tricks than the function we introduce in this patch: - There is a flag to let the user select multiple items. - In multi-select mode, the list of items is prefixed with a marker indicating what items have been selected. - Initially, for each item a unique prefix is determined (if there exists any within the given parameters), and shown in the list, and accepted as a shortcut for the selection. These features will be implemented in the C version later. This patch does not add any new main loop command, of course, the built-in `git add -i` still only supports the `status` command. The remaining commands to follow over the course of the next commits. To accommodate for listing the commands in columns, preparing for the commands that will be implemented over the course of the next patches/patch series, we teach the `list()` function to do precisely that. Note that we only have a prompt ending in a single ">" at this stage; later commits will add commands that display a double ">>" to indicate that the user is in a different loop than the main one. Signed-off-by: Johannes Schindelin Signed-off-by: Junio C Hamano diff --git a/add-interactive.c b/add-interactive.c index 174e07c..76d9824 100644 --- a/add-interactive.c +++ b/add-interactive.c @@ -46,6 +46,7 @@ static void init_add_i_state(struct add_i_state *s, struct repository *r) } 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_data; @@ -54,7 +55,7 @@ struct list_options { static void list(struct add_i_state *s, struct string_list *list, struct list_options *opts) { - int i; + int i, last_lf = 0; if (!list->nr) return; @@ -65,8 +66,98 @@ static void list(struct add_i_state *s, struct string_list *list, for (i = 0; i < list->nr; i++) { opts->print_item(i, list->items + i, opts->print_item_data); + + if ((opts->columns) && ((i + 1) % (opts->columns))) { + putchar('\t'); + last_lf = 0; + } + else { + putchar('\n'); + last_lf = 1; + } + } + + if (!last_lf) putchar('\n'); +} +struct list_and_choose_options { + struct list_options list_opts; + + const char *prompt; +}; + +#define LIST_AND_CHOOSE_ERROR (-1) +#define LIST_AND_CHOOSE_QUIT (-2) + +/* + * Returns the selected index. + * + * If an error occurred, returns `LIST_AND_CHOOSE_ERROR`. Upon EOF, + * `LIST_AND_CHOOSE_QUIT` is returned. + */ +static ssize_t list_and_choose(struct add_i_state *s, struct string_list *items, + struct list_and_choose_options *opts) +{ + struct strbuf input = STRBUF_INIT; + ssize_t res = LIST_AND_CHOOSE_ERROR; + + for (;;) { + char *p; + + strbuf_reset(&input); + + list(s, items, &opts->list_opts); + + printf("%s%s", opts->prompt, "> "); + fflush(stdout); + + if (strbuf_getline(&input, stdin) == EOF) { + putchar('\n'); + res = LIST_AND_CHOOSE_QUIT; + break; + } + strbuf_trim(&input); + + if (!input.len) + break; + + p = input.buf; + for (;;) { + size_t sep = strcspn(p, " \t\r\n,"); + ssize_t index = -1; + + if (!sep) { + if (!*p) + break; + p++; + continue; + } + + if (isdigit(*p)) { + char *endp; + index = strtoul(p, &endp, 10) - 1; + if (endp != p + sep) + index = -1; + } + + if (p[sep]) + p[sep++] = '\0'; + if (index < 0 || index >= items->nr) + printf(_("Huh (%s)?\n"), p); + else { + res = index; + break; + } + + p += sep; + } + + if (res != LIST_AND_CHOOSE_ERROR) + break; } + + strbuf_release(&input); + return res; } struct adddel { @@ -252,20 +343,48 @@ static int run_status(struct add_i_state *s, const struct pathspec *ps, return 0; } +typedef int (*command_t)(struct add_i_state *s, const struct pathspec *ps, + struct string_list *files, + struct list_options *opts); + +static void print_command_item(int i, struct string_list_item *item, + void *print_command_item_data) +{ + printf(" %2d: %s", i + 1, item->string); +} + int run_add_i(struct repository *r, const struct pathspec *ps) { struct add_i_state s = { NULL }; + struct list_and_choose_options main_loop_opts = { + { 4, N_("*** Commands ***"), print_command_item, NULL }, + N_("What now") + }; + struct { + const char *string; + command_t command; + } command_list[] = { + { "status", run_status }, + }; + struct string_list commands = STRING_LIST_INIT_NODUP; + struct print_file_item_data print_file_item_data = { "%12s %12s %s", STRBUF_INIT, STRBUF_INIT, STRBUF_INIT }; struct list_options opts = { - NULL, print_file_item, &print_file_item_data + 0, NULL, print_file_item, &print_file_item_data }; struct strbuf header = STRBUF_INIT; struct string_list files = STRING_LIST_INIT_DUP; + ssize_t i; int res = 0; + for (i = 0; i < ARRAY_SIZE(command_list); i++) + string_list_append(&commands, command_list[i].string) + ->util = command_list[i].command; + init_add_i_state(&s, r); + strbuf_addstr(&header, " "); strbuf_addf(&header, print_file_item_data.modified_fmt, _("staged"), _("unstaged"), _("path")); @@ -279,11 +398,25 @@ int run_add_i(struct repository *r, const struct pathspec *ps) res = run_status(&s, ps, &files, &opts); + for (;;) { + i = list_and_choose(&s, &commands, &main_loop_opts); + if (i == LIST_AND_CHOOSE_QUIT) { + printf(_("Bye.\n")); + res = 0; + break; + } + if (i != LIST_AND_CHOOSE_ERROR) { + command_t command = commands.items[i].util; + res = command(&s, ps, &files, &opts); + } + } + string_list_clear(&files, 1); strbuf_release(&print_file_item_data.buf); strbuf_release(&print_file_item_data.index); strbuf_release(&print_file_item_data.worktree); strbuf_release(&header); + string_list_clear(&commands, 0); return res; } -- cgit v0.10.2-6-g49f6 From 76b743234c6d122d9e194556f6f94f61493094f6 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 15 Nov 2019 11:11:17 +0000 Subject: built-in add -i: show unique prefixes of the commands MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Just like in the Perl script `git-add--interactive.perl`, for each command a unique prefix is determined (if there exists any within the given parameters), and shown in the list, and accepted as a shortcut for the command. To determine the unique prefixes, as well as to look up the command in question, we use a copy of the list and sort it. While this might seem like overkill for a single command, it will make much more sense when all the commands are implemented, and when we reuse the same logic to present a list of files to edit, with convenient unique prefixes. At the start of the development of this patch series, a dedicated data structure was introduced that imitated the Trie that the Perl version implements. However, this was deemed overkill, and we now simply sort the list before determining the length of the unique prefixes by looking at each item's neighbor. As a bonus, we now use the same sorted list to perform a binary search using the user-provided prefix as search key. Original-patch-by: Slavica Đukić Helped-by: SZEDER Gábor Signed-off-by: Johannes Schindelin Signed-off-by: Junio C Hamano diff --git a/add-interactive.c b/add-interactive.c index 76d9824..c5d95d4 100644 --- a/add-interactive.c +++ b/add-interactive.c @@ -45,6 +45,132 @@ static void init_add_i_state(struct add_i_state *s, struct repository *r) init_color(r, s, "header", s->header_color, GIT_COLOR_BOLD); } +/* + * A "prefix item list" is a list of items that are identified by a string, and + * a unique prefix (if any) is determined for each item. + * + * It is implemented in the form of a pair of `string_list`s, the first one + * duplicating the strings, with the `util` field pointing at a structure whose + * first field must be `size_t prefix_length`. + * + * That `prefix_length` field will be computed by `find_unique_prefixes()`; It + * will be set to zero if no valid, unique prefix could be found. + * + * The second `string_list` is called `sorted` and does _not_ duplicate the + * strings but simply reuses the first one's, with the `util` field pointing at + * the `string_item_list` of the first `string_list`. It will be populated and + * sorted by `find_unique_prefixes()`. + */ +struct prefix_item_list { + struct string_list items; + struct string_list sorted; + size_t min_length, max_length; +}; +#define PREFIX_ITEM_LIST_INIT \ + { STRING_LIST_INIT_DUP, STRING_LIST_INIT_NODUP, 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); +} + +static void extend_prefix_length(struct string_list_item *p, + const char *other_string, size_t max_length) +{ + size_t *len = p->util; + + if (!*len || memcmp(p->string, other_string, *len)) + return; + + for (;;) { + char c = p->string[*len]; + + /* + * Is `p` a strict prefix of `other`? Or have we exhausted the + * maximal length of the prefix? Or is the current character a + * multi-byte UTF-8 one? If so, there is no valid, unique + * prefix. + */ + if (!c || ++*len > max_length || !isascii(c)) { + *len = 0; + break; + } + + if (c != other_string[*len - 1]) + break; + } +} + +static void find_unique_prefixes(struct prefix_item_list *list) +{ + size_t i; + + if (list->sorted.nr == list->items.nr) + return; + + string_list_clear(&list->sorted, 0); + /* Avoid reallocating incrementally */ + list->sorted.items = xmalloc(st_mult(sizeof(*list->sorted.items), + list->items.nr)); + list->sorted.nr = list->sorted.alloc = list->items.nr; + + for (i = 0; i < list->items.nr; i++) { + list->sorted.items[i].string = list->items.items[i].string; + list->sorted.items[i].util = list->items.items + i; + } + + string_list_sort(&list->sorted); + + for (i = 0; i < list->sorted.nr; i++) { + struct string_list_item *sorted_item = list->sorted.items + i; + struct string_list_item *item = sorted_item->util; + size_t *len = item->util; + + *len = 0; + while (*len < list->min_length) { + char c = item->string[(*len)++]; + + if (!c || !isascii(c)) { + *len = 0; + break; + } + } + + if (i > 0) + extend_prefix_length(item, sorted_item[-1].string, + list->max_length); + if (i + 1 < list->sorted.nr) + extend_prefix_length(item, sorted_item[1].string, + list->max_length); + } +} + +static ssize_t find_unique(const char *string, struct prefix_item_list *list) +{ + int index = string_list_find_insert_index(&list->sorted, string, 1); + struct string_list_item *item; + + if (list->items.nr != list->sorted.nr) + BUG("prefix_item_list in inconsistent state (%"PRIuMAX + " vs %"PRIuMAX")", + (uintmax_t)list->items.nr, (uintmax_t)list->sorted.nr); + + if (index < 0) + item = list->sorted.items[-1 - index].util; + else if (index > 0 && + starts_with(list->sorted.items[index - 1].string, string)) + return -1; + else if (index + 1 < list->sorted.nr && + starts_with(list->sorted.items[index + 1].string, string)) + return -1; + else if (index < list->sorted.nr) + item = list->sorted.items[index].util; + else + return -1; + return item - list->items.items; +} + struct list_options { int columns; const char *header; @@ -95,18 +221,21 @@ struct list_and_choose_options { * If an error occurred, returns `LIST_AND_CHOOSE_ERROR`. Upon EOF, * `LIST_AND_CHOOSE_QUIT` is returned. */ -static ssize_t list_and_choose(struct add_i_state *s, struct string_list *items, +static ssize_t list_and_choose(struct add_i_state *s, + struct prefix_item_list *items, struct list_and_choose_options *opts) { struct strbuf input = STRBUF_INIT; ssize_t res = LIST_AND_CHOOSE_ERROR; + find_unique_prefixes(items); + for (;;) { char *p; strbuf_reset(&input); - list(s, items, &opts->list_opts); + list(s, &items->items, &opts->list_opts); printf("%s%s", opts->prompt, "> "); fflush(stdout); @@ -142,7 +271,10 @@ static ssize_t list_and_choose(struct add_i_state *s, struct string_list *items, if (p[sep]) p[sep++] = '\0'; - if (index < 0 || index >= items->nr) + if (index < 0) + index = find_unique(p, items); + + if (index < 0 || index >= items->items.nr) printf(_("Huh (%s)?\n"), p); else { res = index; @@ -308,6 +440,23 @@ static void render_adddel(struct strbuf *buf, strbuf_addstr(buf, no_changes); } +/* filters out prefixes which have special meaning to list_and_choose() */ +static int is_valid_prefix(const char *prefix, size_t prefix_len) +{ + return prefix_len && prefix && + /* + * We expect `prefix` to be NUL terminated, therefore this + * `strcspn()` call is okay, even if it might do much more + * work than strictly necessary. + */ + strcspn(prefix, " \t\r\n,") >= prefix_len && /* separators */ + *prefix != '-' && /* deselection */ + !isdigit(*prefix) && /* selection */ + (prefix_len != 1 || + (*prefix != '*' && /* "all" wildcard */ + *prefix != '?')); /* prompt help */ +} + struct print_file_item_data { const char *modified_fmt; struct strbuf buf, index, worktree; @@ -347,10 +496,23 @@ typedef int (*command_t)(struct add_i_state *s, const struct pathspec *ps, struct string_list *files, struct list_options *opts); +struct command_item { + size_t prefix_length; + command_t command; +}; + static void print_command_item(int i, struct string_list_item *item, void *print_command_item_data) { - printf(" %2d: %s", i + 1, item->string); + struct command_item *util = item->util; + + if (!util->prefix_length || + !is_valid_prefix(item->string, util->prefix_length)) + printf(" %2d: %s", i + 1, item->string); + else + printf(" %2d: [%.*s]%s", i + 1, + (int)util->prefix_length, item->string, + item->string + util->prefix_length); } int run_add_i(struct repository *r, const struct pathspec *ps) @@ -366,7 +528,7 @@ int run_add_i(struct repository *r, const struct pathspec *ps) } command_list[] = { { "status", run_status }, }; - struct string_list commands = STRING_LIST_INIT_NODUP; + 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 @@ -379,9 +541,12 @@ int run_add_i(struct repository *r, const struct pathspec *ps) ssize_t i; int res = 0; - for (i = 0; i < ARRAY_SIZE(command_list); i++) - string_list_append(&commands, command_list[i].string) - ->util = command_list[i].command; + for (i = 0; i < ARRAY_SIZE(command_list); i++) { + struct command_item *util = xcalloc(sizeof(*util), 1); + util->command = command_list[i].command; + string_list_append(&commands.items, command_list[i].string) + ->util = util; + } init_add_i_state(&s, r); @@ -406,8 +571,9 @@ int run_add_i(struct repository *r, const struct pathspec *ps) break; } if (i != LIST_AND_CHOOSE_ERROR) { - command_t command = commands.items[i].util; - res = command(&s, ps, &files, &opts); + struct command_item *util = + commands.items.items[i].util; + res = util->command(&s, ps, &files, &opts); } } @@ -416,7 +582,7 @@ int run_add_i(struct repository *r, const struct pathspec *ps) strbuf_release(&print_file_item_data.index); strbuf_release(&print_file_item_data.worktree); strbuf_release(&header); - string_list_clear(&commands, 0); + prefix_item_list_clear(&commands); return res; } -- cgit v0.10.2-6-g49f6 From 68db1cbf8eb412e81ff84b18f6c2ab8d15df99c7 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 15 Nov 2019 11:11:18 +0000 Subject: built-in add -i: support `?` (prompt help) With this change, we print out the same colored help text that the Perl-based `git add -i` prints in the main loop when question mark is entered. Signed-off-by: Johannes Schindelin Signed-off-by: Junio C Hamano diff --git a/add-interactive.c b/add-interactive.c index c5d95d4..0a03b90 100644 --- a/add-interactive.c +++ b/add-interactive.c @@ -11,6 +11,7 @@ struct add_i_state { struct repository *r; int use_color; char header_color[COLOR_MAXLEN]; + char help_color[COLOR_MAXLEN]; }; static void init_color(struct repository *r, struct add_i_state *s, @@ -43,6 +44,7 @@ static void init_add_i_state(struct add_i_state *s, struct repository *r) s->use_color = want_color(s->use_color); init_color(r, s, "header", s->header_color, GIT_COLOR_BOLD); + init_color(r, s, "help", s->help_color, GIT_COLOR_BOLD_RED); } /* @@ -210,6 +212,7 @@ struct list_and_choose_options { struct list_options list_opts; const char *prompt; + void (*print_help)(struct add_i_state *s); }; #define LIST_AND_CHOOSE_ERROR (-1) @@ -250,6 +253,11 @@ static ssize_t list_and_choose(struct add_i_state *s, if (!input.len) break; + if (!strcmp(input.buf, "?")) { + opts->print_help(s); + continue; + } + p = input.buf; for (;;) { size_t sep = strcspn(p, " \t\r\n,"); @@ -515,12 +523,24 @@ static void print_command_item(int i, struct string_list_item *item, item->string + util->prefix_length); } +static void command_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 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")); +} + int run_add_i(struct repository *r, const struct pathspec *ps) { struct add_i_state s = { NULL }; struct list_and_choose_options main_loop_opts = { { 4, N_("*** Commands ***"), print_command_item, NULL }, - N_("What now") + N_("What now"), command_prompt_help }; struct { const char *string; -- cgit v0.10.2-6-g49f6 From 3d965c76744482b50669a7d1b445c0ddbf92209a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Slavica=20=C4=90uki=C4=87?= Date: Fri, 15 Nov 2019 11:11:19 +0000 Subject: built-in add -i: use color in the main loop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The error messages as well as the unique prefixes are colored in `git add -i` by default; We need to do the same in the built-in version. Signed-off-by: Slavica Đukić Signed-off-by: Johannes Schindelin Signed-off-by: Junio C Hamano diff --git a/add-interactive.c b/add-interactive.c index 0a03b90..170a580 100644 --- a/add-interactive.c +++ b/add-interactive.c @@ -12,6 +12,9 @@ struct add_i_state { int use_color; char header_color[COLOR_MAXLEN]; char help_color[COLOR_MAXLEN]; + char prompt_color[COLOR_MAXLEN]; + char error_color[COLOR_MAXLEN]; + char reset_color[COLOR_MAXLEN]; }; static void init_color(struct repository *r, struct add_i_state *s, @@ -45,6 +48,9 @@ static void init_add_i_state(struct add_i_state *s, struct repository *r) init_color(r, s, "header", s->header_color, GIT_COLOR_BOLD); init_color(r, s, "help", s->help_color, GIT_COLOR_BOLD_RED); + init_color(r, s, "prompt", s->prompt_color, GIT_COLOR_BOLD_BLUE); + init_color(r, s, "error", s->error_color, GIT_COLOR_BOLD_RED); + init_color(r, s, "reset", s->reset_color, GIT_COLOR_RESET); } /* @@ -240,7 +246,8 @@ static ssize_t list_and_choose(struct add_i_state *s, list(s, &items->items, &opts->list_opts); - printf("%s%s", opts->prompt, "> "); + color_fprintf(stdout, s->prompt_color, "%s", opts->prompt); + fputs("> ", stdout); fflush(stdout); if (strbuf_getline(&input, stdin) == EOF) { @@ -283,7 +290,8 @@ static ssize_t list_and_choose(struct add_i_state *s, index = find_unique(p, items); if (index < 0 || index >= items->items.nr) - printf(_("Huh (%s)?\n"), p); + color_fprintf_ln(stdout, s->error_color, + _("Huh (%s)?"), p); else { res = index; break; @@ -509,18 +517,23 @@ struct command_item { command_t command; }; +struct print_command_item_data { + const char *color, *reset; +}; + static void print_command_item(int i, struct string_list_item *item, void *print_command_item_data) { + struct print_command_item_data *d = print_command_item_data; struct command_item *util = item->util; if (!util->prefix_length || !is_valid_prefix(item->string, util->prefix_length)) printf(" %2d: %s", i + 1, item->string); else - printf(" %2d: [%.*s]%s", i + 1, - (int)util->prefix_length, item->string, - item->string + util->prefix_length); + printf(" %2d: %s%.*s%s%s", i + 1, + d->color, (int)util->prefix_length, item->string, + d->reset, item->string + util->prefix_length); } static void command_prompt_help(struct add_i_state *s) @@ -538,8 +551,9 @@ static void command_prompt_help(struct add_i_state *s) int run_add_i(struct repository *r, const struct pathspec *ps) { struct add_i_state s = { NULL }; + struct print_command_item_data data = { "[", "]" }; struct list_and_choose_options main_loop_opts = { - { 4, N_("*** Commands ***"), print_command_item, NULL }, + { 4, N_("*** Commands ***"), print_command_item, &data }, N_("What now"), command_prompt_help }; struct { @@ -570,6 +584,15 @@ int run_add_i(struct repository *r, const struct pathspec *ps) init_add_i_state(&s, r); + /* + * When color was asked for, use the prompt color for + * highlighting, otherwise use square brackets. + */ + if (s.use_color) { + data.color = s.prompt_color; + data.reset = s.reset_color; + } + strbuf_addstr(&header, " "); strbuf_addf(&header, print_file_item_data.modified_fmt, _("staged"), _("unstaged"), _("path")); -- cgit v0.10.2-6-g49f6 From 8c159044625e46de67cd8467f07424f38eb8301e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Slavica=20=C4=90uki=C4=87?= Date: Fri, 15 Nov 2019 11:11:20 +0000 Subject: built-in add -i: implement the `help` command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This imitates the code to show the help text from the Perl script `git-add--interactive.perl` in the built-in version. To make sure that it renders exactly like the Perl version of `git add -i`, we also add a test case for that to `t3701-add-interactive.sh`. Signed-off-by: Slavica Đukić Signed-off-by: Johannes Schindelin Signed-off-by: Junio C Hamano diff --git a/add-interactive.c b/add-interactive.c index 170a580..d6cb98c 100644 --- a/add-interactive.c +++ b/add-interactive.c @@ -508,6 +508,26 @@ static int run_status(struct add_i_state *s, const struct pathspec *ps, return 0; } +static int run_help(struct add_i_state *s, const struct pathspec *unused_ps, + struct string_list *unused_files, + struct list_options *unused_opts) +{ + color_fprintf_ln(stdout, s->help_color, "status - %s", + _("show paths with changes")); + color_fprintf_ln(stdout, s->help_color, "update - %s", + _("add working tree state to the staged set of changes")); + color_fprintf_ln(stdout, s->help_color, "revert - %s", + _("revert staged set of changes back to the HEAD version")); + color_fprintf_ln(stdout, s->help_color, "patch - %s", + _("pick hunks and update selectively")); + color_fprintf_ln(stdout, s->help_color, "diff - %s", + _("view diff between HEAD and index")); + color_fprintf_ln(stdout, s->help_color, "add untracked - %s", + _("add contents of untracked files to the staged set of changes")); + + return 0; +} + typedef int (*command_t)(struct add_i_state *s, const struct pathspec *ps, struct string_list *files, struct list_options *opts); @@ -561,6 +581,7 @@ int run_add_i(struct repository *r, const struct pathspec *ps) command_t command; } command_list[] = { { "status", run_status }, + { "help", run_help }, }; struct prefix_item_list commands = PREFIX_ITEM_LIST_INIT; diff --git a/t/t3701-add-interactive.sh b/t/t3701-add-interactive.sh index d50e165..d4f9386 100755 --- a/t/t3701-add-interactive.sh +++ b/t/t3701-add-interactive.sh @@ -647,4 +647,29 @@ test_expect_success 'checkout -p works with pathological context lines' ' test_write_lines a b a b a a b a b a >expect && test_cmp expect a ' + +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 -- cgit v0.10.2-6-g49f6