git/list[1] front-page[2] threads[3] people[4] search[5] about
 

[PATCH v5 07/10] add-interactive.c: add support for list_only option

From
SGSlavica Djukic via GitGitGadget <gitgitgadget@gmail.com>
Date
Feb 20, 2019, 11:41 UTC
Message-ID
<7912f375170ca266a4ce0f9f755f8d8fecdf3d37.1550662887.git.gitgitgadget@gmail.com>
In-Reply-To
<pull.103.v5.git.gitgitgadget@gmail.com>
From: Slavica Djukic <slawica92@hotmail.com>

If list_only option is not set, (i.e. we want to pick elements from the list, not just display them), highlight unique prefixes of list elements and let user make a choice as shown in prompt_help_cmd and singleton_prompt_help_cmd.

Input that is expected from user is full line. Although that's also the case with Perl script in this particular situation, there is also sub prompt_single_character, which deals with single keystroke.

Ever since f7a4cea25e3ee1c8f27777bc4293dca0210fa573, we did not use _getch() in our code base's C code, and this would be the first user. There are portability issues with _getch() (or getch()) that we would like to avoid. That said, from now on, every other input will also be expected to be full line, rather than single keystroke.

Mentored-by: Johannes Schindelin <Johannes.Schindelin@gmx.de>
Signed-off-by: Slavica Djukic <slawica92@hotmail.com>
---
 add-interactive.c | 378 +++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 373 insertions(+), 5 deletions(-)
diff --git a/add-interactive.c b/add-interactive.c
index 6bf8a90d9d..3c2e972413 100644
--- a/add-interactive.c
+++ b/add-interactive.c
@@ -70,18 +70,27 @@ struct choices {
 };
 #define CHOICES_INIT { NULL, 0, 0 }
 
+struct prefix_entry {
+	struct hashmap_entry e;
+	const char *name;
+	size_t prefix_length;
+	struct choice *item;
+};
+
 static int use_color = -1;
 enum color_add_i {
 	COLOR_PROMPT,
 	COLOR_HEADER,
 	COLOR_HELP,
-	COLOR_ERROR
+	COLOR_ERROR,
+	COLOR_RESET
 };
 
 static char colors[][COLOR_MAXLEN] = {
 	GIT_COLOR_BOLD_BLUE, /* Prompt */
 	GIT_COLOR_BOLD,      /* Header */
 	GIT_COLOR_BOLD_RED,  /* Help */
+	GIT_COLOR_BOLD_RED,  /* Error */
 	GIT_COLOR_RESET      /* Reset */
 };
 
@@ -102,6 +111,8 @@ static int parse_color_slot(const char *slot)
 		return COLOR_HELP;
 	if (!strcasecmp(slot, "error"))
 		return COLOR_ERROR;
+	if (!strcasecmp(slot, "reset"))
+		return COLOR_RESET;
 
 	return -1;
 }
@@ -288,14 +299,248 @@ static struct collection_status *list_modified(struct repository *r, const char
 	return s;
 }
 
+static int map_cmp(const void *unused_cmp_data,
+		   const void *entry,
+		   const void *entry_or_key,
+		   const void *unused_keydata)
+{
+	const struct choice *a = entry;
+	const struct choice *b = entry_or_key;
+	if((a->prefix_length == b->prefix_length) &&
+	  (strncmp(a->name, b->name, a->prefix_length) == 0))
+		return 0;
+	return 1;
+}
+
+static struct prefix_entry *new_prefix_entry(const char *name,
+					     size_t prefix_length,
+					     struct choice *item)
+{
+	struct prefix_entry *result = xcalloc(1, sizeof(*result));
+	result->name = name;
+	result->prefix_length = prefix_length;
+	result->item = item;
+	hashmap_entry_init(result, memhash(name, prefix_length));
+	return result;
+}
+
+static void find_unique_prefixes(struct choices *data)
+{
+	int soft_limit = 0;
+	int hard_limit = 4;
+	struct hashmap map;
+
+	hashmap_init(&map, map_cmp, NULL, 0);
+
+	for (int i = 0; i < data->nr; i++) {
+		struct prefix_entry *e = xcalloc(1, sizeof(*e));
+		struct prefix_entry *e2;
+		e->name = data->choices[i]->name;
+		e->item = data->choices[i];
+
+		for (int j = soft_limit + 1; j <= hard_limit; j++) {
+			if (!isascii(e->name[j]))
+				break;
+
+			e->prefix_length = j;
+			hashmap_entry_init(e, memhash(e->name, j));
+			e2 = hashmap_get(&map, e, NULL);
+			if (!e2) {
+				e->item->prefix_length = j;
+				hashmap_add(&map, e);
+				e = NULL;
+				break;
+			}
+
+			if (!e2->item) {
+				continue; /* non-unique prefix */
+			}
+
+			if (j != e2->item->prefix_length)
+				BUG("Hashmap entry has unexpected prefix length (%"PRIuMAX"/ != %"PRIuMAX"/)",
+				   (uintmax_t)j, (uintmax_t)e2->item->prefix_length);
+
+			/* skip common prefix */
+			for (j++; j <= hard_limit && e->name[j - 1]; j++) {
+				if (e->item->name[j - 1] != e2->item->name[j - 1])
+					break;
+				hashmap_add(&map, new_prefix_entry(e->name, j, NULL));
+			}
+			if (j <= hard_limit && e2->name[j - 1]) {
+				e2->item->prefix_length = j;
+				hashmap_add(&map, new_prefix_entry(e2->name, j, e2->item));
+			}
+			else {
+				e2->item->prefix_length = 0;
+			}
+			e2->item = NULL;
+
+			if (j <= hard_limit && e->name[j - 1]) {
+				e->item->prefix_length = j;
+				hashmap_add(&map, new_prefix_entry(e->name,
+								   e->item->prefix_length, e->item));
+				e = NULL;
+			}
+			else
+				e->item->prefix_length = 0;
+			break;
+		}
+
+		free(e);
+	}
+}
+
+static int find_unique(char *string, struct choices *data)
+{
+	int found = 0;
+	int i = 0;
+	int hit = 0;
+
+	for (i = 0; i < data->nr; i++) {
+		struct choice *item = data->choices[i];
+		hit = 0;
+		if (!strcmp(item->name, string))
+			hit = 1;
+		if (hit && found)
+			return 0;
+		if (hit)
+			found = i + 1;
+	}
+
+	return found;
+}
+
+/* filters out prefixes which have special meaning to list_and_choose() */
+static int is_valid_prefix(const char *prefix)
+{
+	regex_t *regex;
+	const char *pattern = "(\\s,)|(^-)|(^[0-9]+)";
+	int is_valid = 0;
+
+	regex = xmalloc(sizeof(*regex));
+	if (regcomp(regex, pattern, REG_EXTENDED))
+		return 0;
+
+	is_valid = prefix &&
+		   regexec(regex, prefix, 0, NULL, 0) &&
+		   strcmp(prefix, "*") &&
+		   strcmp(prefix, "?");
+	free(regex);
+	return is_valid;
+}
+
+/* return a string with the prefix highlighted */
+/* for now use square brackets; later might use ANSI colors (underline, bold) */
+static char *highlight_prefix(struct choice *item)
+{
+	struct strbuf buf;
+	struct strbuf prefix;
+	struct strbuf remainder;
+	const char *prompt_color = get_color(COLOR_PROMPT);
+	const char *reset_color = get_color(COLOR_RESET);
+	int remainder_size = strlen(item->name) - item->prefix_length;
+
+	strbuf_init(&buf, 0);
+
+	strbuf_init(&prefix, 0);
+	strbuf_add(&prefix, item->name, item->prefix_length);
+
+	strbuf_init(&remainder, 0);
+	strbuf_add(&remainder, item->name + item->prefix_length,
+		   remainder_size + 1);
+
+	if(!prefix.buf) {
+		strbuf_release(&buf);
+		strbuf_release(&prefix);
+		return remainder.buf;
+	}
+	
+	if (!is_valid_prefix(prefix.buf)) {
+		strbuf_addstr(&buf, prefix.buf);
+		strbuf_addstr(&buf, remainder.buf);
+	}
+	else if (!use_color) {
+		strbuf_addstr(&buf, "[");
+		strbuf_addstr(&buf, prefix.buf);
+		strbuf_addstr(&buf, "]");
+		strbuf_addstr(&buf, remainder.buf);
+	}
+	else {
+		strbuf_addstr(&buf, prompt_color);
+		strbuf_addstr(&buf, prefix.buf);
+		strbuf_addstr(&buf, reset_color);
+		strbuf_addstr(&buf, remainder.buf);
+	}
+
+	strbuf_release(&prefix);
+	strbuf_release(&remainder);
+
+	return buf.buf;
+}
+
+static void singleton_prompt_help_cmd(void)
+{
+	const char *help_color = get_color(COLOR_HELP);
+	color_fprintf_ln(stdout, help_color, "%s", _("Prompt help:"));
+	color_fprintf_ln(stdout, help_color, "1          - %s",
+			 _("select a numbered item"));
+	color_fprintf_ln(stdout, help_color, "foo        - %s",
+			 _("select item based on unique prefix"));
+	color_fprintf_ln(stdout, help_color, "           - %s",
+			 _("(empty) select nothing"));
+}
+
+static void prompt_help_cmd(void)
+{
+	const char *help_color = get_color(COLOR_HELP);
+	color_fprintf_ln(stdout, help_color, "%s",
+			 _("Prompt help:"));
+	color_fprintf_ln(stdout, help_color, "1          - %s",
+			 _("select a single item"));
+	color_fprintf_ln(stdout, help_color, "3-5        - %s",
+			 _("select a range of items"));
+	color_fprintf_ln(stdout, help_color, "2-3,6-9    - %s",
+			 _("select multiple ranges"));
+	color_fprintf_ln(stdout, help_color, "foo        - %s",
+			 _("select item based on unique prefix"));
+	color_fprintf_ln(stdout, help_color, "-...       - %s",
+			 _("unselect specified items"));
+	color_fprintf_ln(stdout, help_color, "*          - %s",
+			 _("choose all items"));
+	color_fprintf_ln(stdout, help_color, "           - %s",
+			 _("(empty) finish selecting"));
+}
+
 static struct choices *list_and_choose(struct choices *data,
 				       struct list_and_choose_options *opts)
 {
-	if (!data)
+	char *chosen_choices = xcalloc(data->nr, sizeof(char *));
+	struct choices *results = xcalloc(1, sizeof(*results));
+	int chosen_size = 0;
+
+	if (!data) {
+		free(chosen_choices);
+		free(results);
 		return NULL;
+	}
+	
+	if (!opts->list_only)
+		find_unique_prefixes(data);
 
+top:
 	while (1) {
 		int last_lf = 0;
+		const char *prompt_color = get_color(COLOR_PROMPT);
+		const char *error_color = get_color(COLOR_ERROR);
+		struct strbuf input = STRBUF_INIT;
+		struct strbuf choice;
+		struct strbuf token;
+		char *token_tmp;
+		regex_t *regex_dash_range;
+		regex_t *regex_number;
+		const char *pattern_dash_range;
+		const char *pattern_number;
+		const char delim[] = " ,";
 
 		if (opts->header) {
 			const char *header_color = get_color(COLOR_HEADER);
@@ -306,13 +551,17 @@ static struct choices *list_and_choose(struct choices *data,
 
 		for (int i = 0; i < data->nr; i++) {
 			struct choice *c = data->choices[i];
+			char chosen = chosen_choices[i]? '*' : ' ';
 			char *print;
 			const char *modified_fmt = _("%12s %12s %s");
 			char worktree_changes[50];
 			char index_changes[50];
 			char print_buf[100];
 
-			print = (char *)c->name;
+			if (!opts->list_only)
+				print = highlight_prefix(data->choices[i]);
+			else
+				print = (char *)c->name;
 			
 			if ((data->choices[i]->type == 'f') && (!opts->list_only_file_names)) {
 				uintmax_t worktree_added = c->u.file.worktree.added;
@@ -338,7 +587,7 @@ static struct choices *list_and_choose(struct choices *data,
 				snprintf(print, 100, "%s", print_buf);
 			}
 
-			printf(" %2d: %s", i + 1, print);
+			printf("%c%2d: %s", chosen, i + 1, print);
 
 			if ((opts->list_flat) && ((i + 1) % (opts->column_n))) {
 				printf("\t");
@@ -354,8 +603,126 @@ static struct choices *list_and_choose(struct choices *data,
 		if (!last_lf)
 			printf("\n");
 
-		return NULL;
+		if (opts->list_only)
+			return NULL;
+
+		color_fprintf(stdout, prompt_color, "%s", opts->prompt);
+		if(opts->singleton)
+			printf("> ");
+		else
+			printf(">> ");
+
+		fflush(stdout);
+		strbuf_getline(&input, stdin);
+		strbuf_trim(&input);
+
+		if (!input.buf)
+			break;
+		
+		if (!input.buf[0]) {
+			printf("\n");
+			if (opts->on_eof_fn)
+				opts->on_eof_fn();
+			break;
+		}
+
+		if (!strcmp(input.buf, "?")) {
+			opts->singleton? singleton_prompt_help_cmd() : prompt_help_cmd();
+			goto top;
+		}
+
+		token_tmp = strtok(input.buf, delim);
+		strbuf_init(&token, 0);
+		strbuf_add(&token, token_tmp, strlen(token_tmp));
+
+		while (1) {
+			int choose = 1;
+			int bottom = 0, top = 0;
+			strbuf_init(&choice, 0);
+			strbuf_addbuf(&choice, &token);
+
+			/* Input that begins with '-'; unchoose */
+			pattern_dash_range = "^-";
+			regex_dash_range = xmalloc(sizeof(*regex_dash_range));
+
+			if (regcomp(regex_dash_range, pattern_dash_range, REG_EXTENDED))
+				BUG("regex compilation for pattern %s failed",
+				   pattern_dash_range);
+			if (!regexec(regex_dash_range, choice.buf, 0, NULL, 0)) {
+				choose = 0;
+				/* remove dash from input */
+				strbuf_remove(&choice, 0, 1);
+			}
+
+			/* A range can be specified like 5-7 or 5-. */
+			pattern_dash_range = "^([0-9]+)-([0-9]*)$";
+			pattern_number = "^[0-9]+$";
+			regex_number = xmalloc(sizeof(*regex_number));
+
+			if (regcomp(regex_dash_range, pattern_dash_range, REG_EXTENDED))
+				BUG("regex compilation for pattern %s failed",
+				   pattern_dash_range);
+			if (regcomp(regex_number, pattern_number, REG_EXTENDED))
+				BUG("regex compilation for pattern %s failed", pattern_number);
+
+			if (!regexec(regex_dash_range, choice.buf, 0, NULL, 0)) {
+				const char delim_dash[] = "-";
+				char *num = NULL;
+				num = strtok(choice.buf, delim_dash);
+				bottom = atoi(num);
+				num = strtok(NULL, delim_dash);
+				top = num? atoi(num) : (1 + data->nr);
+			}
+			else if (!regexec(regex_number, choice.buf, 0, NULL, 0))
+				bottom = top = atoi(choice.buf);
+			else if (!strcmp(choice.buf, "*")) {
+				bottom = 1;
+				top = 1 + data->nr;
+			}
+			else {
+				bottom = top = find_unique(choice.buf, data);
+				if (!bottom) {
+					color_fprintf_ln(stdout, error_color, _("Huh (%s)?"), choice.buf);
+					goto top;
+				}
+			}
+
+			if (opts->singleton && bottom != top) {
+				color_fprintf_ln(stdout, error_color, _("Huh (%s)?"), choice.buf);
+				goto top;
+			}
+
+			for (int i = bottom - 1; i <= top - 1; i++) {
+				if (data->nr <= i || i < 0)
+					continue;
+				chosen_choices[i] = choose;
+				if (choose == 1)
+					chosen_size++;
+			}
+
+			strbuf_release(&token);
+			strbuf_release(&choice);
+
+			token_tmp = strtok(NULL, delim);
+			if (!token_tmp)
+				break;
+			strbuf_init(&token, 0);
+			strbuf_add(&token, token_tmp, strlen(token_tmp));
+		}
+
+		if ((opts->immediate) || !(strcmp(choice.buf, "*")))
+			break;
 	}
+
+	for (int i = 0; i < data->nr; i++) {
+		if (chosen_choices[i]) {
+			ALLOC_GROW(results->choices, results->nr + 1, results->alloc);
+			results->choices[results->nr++] = data->choices[i];
+		}
+	}
+
+	free(chosen_choices);
+	return results;
 }
 
 static struct choice *make_choice(const char *name )
@@ -412,6 +779,7 @@ void add_i_status(void)
 	const char *modified_fmt = _("%12s %12s %s");
 	const char type = 'f';
 
+	opts.list_only = 1;
 	opts.header = xmalloc(sizeof(char) * (HEADER_MAXLEN + 1));
 	snprintf(opts.header, HEADER_MAXLEN + 1, modified_fmt,
 		 _("staged"), _("unstaged"), _("path"));
-- 
gitgitgadget
Previous: Slavica DjukicNext: Slavica Djukic via GitGitGadget
Message 64 of 76 in “Turn git add-i into built-in”
  1. 0/7 Turn git add-i into built-inJohannes Schindelin, Dec 20, 2018
  2. 1/7 diff: export diffstat interfaceDaniel Ferreira via GitGitGadget, Dec 20, 2018
  3. 2/7 add--helper: create builtin helper for interactive addDaniel Ferreira via GitGitGadget, Dec 20, 2018
  4. 4/7 add--interactive.perl: use add--helper --status for status_cmdDaniel Ferreira via GitGitGadget, Dec 20, 2018
  5. 3/7 add-interactive.c: implement status commandDaniel Ferreira via GitGitGadget, Dec 20, 2018
  6. 5/7 add-interactive.c: implement show-help commandSlavica Djukic via GitGitGadget, Dec 20, 2018
  7. Phillip WoodJan 14, 2019
  8. 6/7 Git.pm: introduce environment variable GIT_TEST_PRETEND_TTYSlavica Djukic via GitGitGadget, Dec 20, 2018
  9. Phillip WoodJan 14, 2019
  10. Slavica DjukicJan 15, 2019
  11. Johannes SchindelinJan 15, 2019
  12. Phillip WoodJan 15, 2019
  13. 7/7 add--interactive.perl: use add--helper --show-help for help_cmdSlavica Djukic via GitGitGadget, Dec 20, 2018
  14. Phillip WoodJan 14, 2019
  15. Johannes SchindelinDec 20, 2018
  16. Slavica DjukicJan 11, 2019
  17. 0/7 Turn git add-i into built-inSlavica Đukić via GitGitGadget, Jan 18, 2019
  18. 1/7 diff: export diffstat interfaceDaniel Ferreira via GitGitGadget, Jan 18, 2019
  19. 2/7 add--helper: create builtin helper for interactive addDaniel Ferreira via GitGitGadget, Jan 18, 2019
  20. 4/7 add--interactive.perl: use add--helper --status for status_cmdDaniel Ferreira via GitGitGadget, Jan 18, 2019
  21. 5/7 add-interactive.c: implement show-help commandSlavica Djukic via GitGitGadget, Jan 18, 2019
  22. Phillip WoodJan 18, 2019
  23. Slavica DjukicJan 18, 2019
  24. 3/7 add-interactive.c: implement status commandDaniel Ferreira via GitGitGadget, Jan 18, 2019
  25. 7/7 add--interactive.perl: use add--helper --show-help for help_cmdSlavica Djukic via GitGitGadget, Jan 18, 2019
  26. 6/7 t3701-add-interactive: test add_i_show_help()Slavica Djukic via GitGitGadget, Jan 18, 2019
  27. Phillip WoodJan 18, 2019
  28. 0/7 Turn git add-i into built-inSlavica Đukić via GitGitGadget, Jan 21, 2019
  29. 1/7 diff: export diffstat interfaceDaniel Ferreira via GitGitGadget, Jan 21, 2019
  30. 2/7 add--helper: create builtin helper for interactive addDaniel Ferreira via GitGitGadget, Jan 21, 2019
  31. 4/7 add--interactive.perl: use add--helper --status for status_cmdDaniel Ferreira via GitGitGadget, Jan 21, 2019
  32. 5/7 add-interactive.c: implement show-help commandSlavica Djukic via GitGitGadget, Jan 21, 2019
  33. 6/7 t3701-add-interactive: test add_i_show_help()Slavica Djukic via GitGitGadget, Jan 21, 2019
  34. Phillip WoodJan 25, 2019
  35. Slavica DjukicJan 25, 2019
  36. 7/7 add--interactive.perl: use add--helper --show-help for help_cmdSlavica Djukic via GitGitGadget, Jan 21, 2019
  37. Ævar Arnfjörð BjarmasonJan 21, 2019
  38. Slavica DjukicJan 21, 2019
  39. 3/7 add-interactive.c: implement status commandDaniel Ferreira via GitGitGadget, Jan 21, 2019
  40. 0/7 Turn git add-i into built-inSlavica Đukić via GitGitGadget, Jan 25, 2019
  41. 1/7 diff: export diffstat interfaceDaniel Ferreira via GitGitGadget, Jan 25, 2019
  42. 2/7 add--helper: create builtin helper for interactive addDaniel Ferreira via GitGitGadget, Jan 25, 2019
  43. 3/7 add-interactive.c: implement status commandDaniel Ferreira via GitGitGadget, Jan 25, 2019
  44. 6/7 t3701-add-interactive: test add_i_show_help()Slavica Djukic via GitGitGadget, Jan 25, 2019
  45. 5/7 add-interactive.c: implement show-help commandSlavica Djukic via GitGitGadget, Jan 25, 2019
  46. 4/7 add--interactive.perl: use add--helper --status for status_cmdDaniel Ferreira via GitGitGadget, Jan 25, 2019
  47. 7/7 add--interactive.perl: use add--helper --show-help for help_cmdSlavica Djukic via GitGitGadget, Jan 25, 2019
  48. Slavica DjukicJan 25, 2019
  49. Phillip WoodFeb 1, 2019
  50. 00/10 Turn git add-i into built-inSlavica Đukić via GitGitGadget, Feb 20, 2019
  51. 01/10 diff: export diffstat interfaceDaniel Ferreira via GitGitGadget, Feb 20, 2019
  52. Junio C HamanoFeb 21, 2019
  53. Slavica DjukicFeb 22, 2019
  54. 02/10 add--helper: create builtin helper for interactive addDaniel Ferreira via GitGitGadget, Feb 20, 2019
  55. Junio C HamanoFeb 21, 2019
  56. Johannes SchindelinMar 8, 2019
  57. 08/10 add-interactive.c: implement show-help commandSlavica Djukic via GitGitGadget, Feb 20, 2019
  58. 10/10 add--interactive.perl: use add--helper --show-help for help_cmdSlavica Djukic via GitGitGadget, Feb 20, 2019
  59. 09/10 t3701-add-interactive: test add_i_show_help()Slavica Djukic via GitGitGadget, Feb 20, 2019
  60. 06/10 add--interactive.perl: use add--helper --status for status_cmdDaniel Ferreira via GitGitGadget, Feb 20, 2019
  61. 04/10 add-interactive.c: implement list_and_chooseSlavica Djukic via GitGitGadget, Feb 20, 2019
  62. Junio C HamanoFeb 22, 2019
  63. Slavica DjukicMar 1, 2019
  64. 07/10 add-interactive.c: add support for list_only optionSlavica Djukic via GitGitGadget, Feb 20, 2019
  65. 05/10 add-interactive.c: implement status commandSlavica Djukic via GitGitGadget, Feb 20, 2019
  66. Junio C HamanoFeb 22, 2019
  67. Slavica DjukicMar 1, 2019
  68. 03/10 add-interactive.c: implement list_modifiedSlavica Djukic via GitGitGadget, Feb 20, 2019
  69. Junio C HamanoFeb 21, 2019
  70. Junio C HamanoFeb 21, 2019
  71. Slavica DjukicFeb 22, 2019
  72. Slavica DjukicFeb 22, 2019
  73. Junio C HamanoFeb 22, 2019
  74. End of Outreachy internshipSlavica Djukic, Mar 4, 2019
  75. Phillip WoodJan 18, 2019
  76. Johannes SchindelinJan 18, 2019

Read the whole thread, see it on lore, or plain text.

$ cat FOOTERMessages come from the public archive at lore.kernel.org/git, fetched every hour. The front page is chosen and written each morning by an AI editor and can be wrong; the threads themselves are the record. About and API. For agents: an MCP server at https://gitlist.dev/mcp, and any thread, story or person page as Markdown by adding .md to its URL (or sending Accept: text/markdown). Details in /llms.txt.