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

[PATCH v6 4/7] git-clean: use a git-add-interactive compatible UI

From
Jiang Xin <worldhello.net@gmail.com>
Date
May 6, 2013, 19:18 UTC
Message-ID
<217e7618e9b9d50a50461959b38984ac8be88651.1367867498.git.worldhello.net@gmail.com>
In-Reply-To
<c77c33fe295bb8577336d5bb4fe0d8da32f69df3.1367867498.git.worldhello.net@gmail.com>

Rewrite menu using a new method `list_and_choose`, which is borrowed from `git-add--interactive.perl`. We can reused this method later for more actions.

Please NOTE:
 * Method `list_and_choose` return an array of integers, and
 * it is up to you to free the allocated memory of the array.
 * The array ends with EOF.
 * If user pressed CTRL-D (i.e. EOF), no selection returned.
Signed-off-by: Jiang Xin <worldhello.net@gmail.com>
---
 builtin/clean.c | 410 ++++++++++++++++++++++++++++++++++++++++++++++++++------
 1 file changed, 367 insertions(+), 43 deletions(-)
diff --git a/builtin/clean.c b/builtin/clean.c
index 6bda3..3b9f3 100644
--- a/builtin/clean.c
+++ b/builtin/clean.c
@@ -16,6 +16,35 @@
 #include "column.h"
 #include "color.h"
 
+#define MENU_OPTS_SINGLETON		01
+#define MENU_OPTS_IMMEDIATE		02
+#define MENU_OPTS_LIST_ONLY		04
+
+#define MENU_RETURN_NO_LOOP		10
+
+struct menu_opts {
+	const char *header;
+	const char *prompt;
+	int flag;
+};
+
+enum menu_stuff_type {
+	MENU_STUFF_TYPE_STRING_LIST = 1,
+	MENU_STUFF_TYPE_MENU_ITEM
+};
+
+struct menu_stuff {
+	enum menu_stuff_type type;
+	int nr;
+	void *stuff;
+};
+
+struct menu_item {
+	char hotkey;
+	char *title;
+	int (*fn)();
+};
+
 static int force = -1; /* unset */
 static int interactive;
 static struct string_list del_list = STRING_LIST_INIT_DUP;
@@ -240,12 +269,284 @@ void pretty_print_dels()
 	copts.indent = "  ";
 	copts.padding = 2;
 	print_columns(&list, colopts, &copts);
-	putchar('\n');
 	strbuf_release(&buf);
 	string_list_clear(&list, 0);
 }
 
-void edit_by_patterns_cmd()
+void pretty_print_menus(struct string_list *menu_list)
+{
+	struct strbuf buf = STRBUF_INIT;
+	unsigned int local_colopts = 0;
+	struct column_options copts;
+
+	/*
+	 * always enable column display, we only consult column.*
+	 * about layout strategy and stuff
+	 */
+	local_colopts = COL_ENABLED | COL_ROW;
+	memset(&copts, 0, sizeof(copts));
+	copts.indent = "  ";
+	copts.padding = 2;
+	print_columns(menu_list, local_colopts, &copts);
+	strbuf_release(&buf);
+}
+
+void prompt_help_cmd(int singleton)
+{
+	clean_print_color(CLEAN_COLOR_HELP);
+	printf_ln(singleton ?
+		  _("Prompt help:\n"
+		    "1          - select a numbered item\n"
+		    "foo        - select item based on unique prefix\n"
+		    "           - (empty) select nothing") :
+		  _("Prompt help:\n"
+		    "1          - select a single item\n"
+		    "3-5        - select a range of items\n"
+		    "2-3,6-9    - select multiple ranges\n"
+		    "foo        - select item based on unique prefix\n"
+		    "-...       - unselect specified items\n"
+		    "*          - choose all items\n"
+		    "           - (empty) finish selecting"));
+	clean_print_color(CLEAN_COLOR_RESET);
+}
+
+/*
+ * Implement a git-add-interactive compatible UI, which is borrowed
+ * from git-add--interactive.perl.
+ *
+ * Return value:
+ *
+ *   - Return an array of integers
+ *   - , and it is up to you to free the allocated memory.
+ *   - The array ends with EOF.
+ *   - If user pressed CTRL-D (i.e. EOF), no selection returned.
+ */
+int *list_and_choose(struct menu_opts *opts, struct menu_stuff *stuff)
+{
+	static struct string_list menu_list = STRING_LIST_INIT_DUP;
+	struct strbuf menu = STRBUF_INIT;
+	struct strbuf choice = STRBUF_INIT;
+	struct strbuf **choice_list;
+	int *chosen, *result;
+	char *p;
+	int nr = 0;
+	int i, j;
+	int eof = 0;
+
+	chosen = xmalloc(sizeof(int) * stuff->nr);
+	memset(chosen, 0, sizeof(int) * stuff->nr);
+
+	while (1) {
+		int i = 0, j = 0;
+		string_list_clear(&menu_list, 0);
+
+		if (opts->header) {
+			printf_ln("%s%s%s",
+				  clean_get_color(CLEAN_COLOR_HEADER),
+				  opts->header,
+				  clean_get_color(CLEAN_COLOR_RESET));
+		}
+
+		/* highlight hotkey in menu */
+		if (MENU_STUFF_TYPE_MENU_ITEM == stuff->type) {
+			struct menu_item *item;
+
+			item = (struct menu_item *)stuff->stuff;
+			for (i = 0; i < stuff->nr; i++, item++) {
+				p = item->title;
+				strbuf_addf(&menu, "%s%2d: ", chosen[i] ? "*" : " ", i+1);
+				for (; *p; p++) {
+					if (*p == item->hotkey) {
+						strbuf_addstr(&menu, clean_get_color(CLEAN_COLOR_PROMPT));
+						strbuf_addch(&menu, *p);
+						strbuf_addstr(&menu, clean_get_color(CLEAN_COLOR_RESET));
+					} else {
+						strbuf_addch(&menu, *p);
+					}
+				}
+				string_list_append(&menu_list, menu.buf);
+				strbuf_reset(&menu);
+			}
+		} else if (MENU_STUFF_TYPE_STRING_LIST == stuff->type) {
+			struct string_list_item *item;
+			struct strbuf buf = STRBUF_INIT;
+			i = 0;
+
+			for_each_string_list_item(item, (struct string_list *)stuff->stuff) {
+				const char *qname;
+
+				qname = quote_path_relative(item->string, -1, &buf, *the_prefix);
+				strbuf_addf(&menu, "%s%2d: %s", chosen[i] ? "*" : " ", ++i, qname);
+				string_list_append(&menu_list, menu.buf);
+				strbuf_reset(&menu);
+			}
+		}
+
+		pretty_print_menus(&menu_list);
+
+		if (opts->flag & MENU_OPTS_LIST_ONLY)
+			break;
+
+		if (opts->prompt) {
+			printf("%s%s%s%s",
+			       clean_get_color(CLEAN_COLOR_PROMPT),
+			       opts->prompt,
+			       opts->flag & MENU_OPTS_SINGLETON ? "> " : ">> ",
+			       clean_get_color(CLEAN_COLOR_RESET));
+		}
+
+		if (strbuf_getline(&choice, stdin, '\n') != EOF) {
+			if (!(opts->flag & MENU_OPTS_SINGLETON)) {
+				char *p = choice.buf;
+				do {
+					if (*p == ',')
+						*p = ' ';
+				} while (*p++);
+			}
+			strbuf_trim(&choice);
+		} else {
+			eof = 1;
+			break;
+		}
+
+		/* help for prompt */
+		if (!strcmp(choice.buf, "?")) {
+			prompt_help_cmd(opts->flag & MENU_OPTS_SINGLETON);
+			continue;
+		}
+
+		if (!(opts->flag & MENU_OPTS_SINGLETON) && !choice.len)
+			break;
+
+		choice_list = strbuf_split_max(&choice, ' ', 0);
+		for (i = 0; choice_list[i]; i++) {
+			int choose = 1;
+			int bottom = 0, top = 0;
+			char *p;
+			int is_range = 0;
+			int is_number = 1;
+
+			strbuf_trim(choice_list[i]);
+			if (!choice_list[i]->len)
+				continue;
+
+			/* Input that begins with '-'; unchoose */
+			if (*choice_list[i]->buf == '-') {
+				choose = 0;
+				strbuf_remove(choice_list[i], 0, 1);
+			}
+
+			p = choice_list[i]->buf;
+			for(; *p; p++) {
+				if ('-' == *p) {
+					if (!is_range) {
+						is_range = 1;
+						is_number = 0;
+					} else {
+						is_number = 0;
+						is_range = 0;
+						break;
+					}
+				} else if (!isdigit(*p)) {
+					is_number = 0;
+					is_range = 0;
+					break;
+				}
+			}
+
+			if (is_number) {
+				bottom = atoi(choice_list[i]->buf);
+				top = bottom;
+			} else if (is_range) {
+				bottom = atoi(choice_list[i]->buf);
+				if (!*(strchr(choice_list[i]->buf, '-') + 1)) {
+					top = stuff->nr - 1;
+				} else {
+					top = atoi(strchr(choice_list[i]->buf, '-') + 1);
+				}
+			} else if (!strcmp(choice_list[i]->buf, "*")) {
+				bottom = 1;
+				top = stuff->nr;
+			} else {
+				if (MENU_STUFF_TYPE_MENU_ITEM == stuff->type) {
+					struct menu_item *item;
+
+					item = (struct menu_item *)stuff->stuff;
+					for (j = 0; j < stuff->nr; j++, item++) {
+						if ((choice_list[i]->len == 1 &&
+						     *choice_list[i]->buf == item->hotkey) ||
+						    !strcasecmp(choice_list[i]->buf, item->title)) {
+							bottom = j + 1;
+							top = bottom;
+							break;
+						}
+					}
+				} else if (MENU_STUFF_TYPE_STRING_LIST == stuff->type) {
+					struct string_list_item *item;
+
+					item = ((struct string_list *)stuff->stuff)->items;
+					for (j = 0; j < stuff->nr; j++, item++) {
+						if (!strcasecmp(choice_list[i]->buf, item->string)) {
+							bottom = j + 1;
+							top = bottom;
+							break;
+						}
+					}
+				}
+			}
+
+			if (top <= 0 || bottom <= 0 || top > stuff-> nr || bottom > top ||
+			    (opts->flag & MENU_OPTS_SINGLETON && bottom != top)) {
+				printf_ln("%sHuh (%s)?%s",
+					  clean_get_color(CLEAN_COLOR_ERROR),
+					  choice_list[i]->buf,
+					  clean_get_color(CLEAN_COLOR_RESET));
+				continue;
+			}
+
+			/* A range can be specified like 5-7 or 5-. */
+			for (j = bottom; j <= top; j++) {
+				chosen[j-1] = choose;
+				nr++;
+			}
+		}
+
+		if (opts->flag & MENU_OPTS_SINGLETON) {
+			if (nr)
+				break;
+		} else if (opts->flag & MENU_OPTS_IMMEDIATE) {
+			break;
+		}
+	}
+
+
+	if (eof) {
+		result = xmalloc(sizeof(int) * 2);
+		result[0] = EOF;
+		result[1] = 0;
+	} else {
+		result = xmalloc(sizeof(int) * (nr + 1));
+		memset(result, 0, sizeof(int) * (nr + 1));
+		for (i = 0, j = 0; i < stuff->nr && j < nr; i++) {
+			if (chosen[i])
+				result[j++] = i;
+		}
+		result[j] = EOF;
+	}
+
+	free(chosen);
+	string_list_clear(&menu_list, 0);
+	strbuf_release(&menu);
+	strbuf_release(&choice);
+	return result;
+}
+
+int clean_cmd()
+{
+	return MENU_RETURN_NO_LOOP;
+}
+
+int edit_by_patterns_cmd()
 {
 	struct dir_struct dir;
 	struct strbuf confirm = STRBUF_INIT;
@@ -257,16 +558,10 @@ void edit_by_patterns_cmd()
 
 	while (1) {
 		/* dels list may become empty when we run string_list_remove_empty_items later */
-		if (!del_list.nr) {
-			clean_print_color(CLEAN_COLOR_ERROR);
-			printf_ln(_("No more files to clean, exiting."));
-			clean_print_color(CLEAN_COLOR_RESET);
+		if (!del_list.nr)
 			break;
-		}
 
 		if (changed) {
-			putchar('\n');
-
 			/* Display dels in columns */
 			pretty_print_dels();
 		}
@@ -324,56 +619,86 @@ void edit_by_patterns_cmd()
 
 	strbuf_release(&buf);
 	strbuf_release(&confirm);
+	return 0;
 }
 
-void interactive_main_loop()
+int quit_cmd()
 {
-	struct strbuf confirm = STRBUF_INIT;
+	string_list_clear(&del_list, 0);
+	printf_ln(_("Bye."));
+	return MENU_RETURN_NO_LOOP;
+}
 
+int help_cmd(int x)
+{
+	clean_print_color(CLEAN_COLOR_HELP);
+	printf_ln(_(
+		    "clean            - start cleaning\n"
+		    "edit by patterns - exclude items from deletion\n"
+		    "quit             - stop cleaning\n"
+		    "help             - this screen\n"
+		    "?                - help for prompt selection"
+		   ));
+	clean_print_color(CLEAN_COLOR_RESET);
+	return 0;
+}
+
+void interactive_main_loop()
+{
 	/* dels list may become empty after return back from edit mode */
 	while (del_list.nr) {
-		putchar('\n');
+		struct menu_opts menu_opts;
+		struct menu_stuff menu_stuff;
+		struct menu_item menus[] = {
+			{'c', "clean",			clean_cmd},
+			{'p', "edit by patterns",	edit_by_patterns_cmd},
+			{'q', "quit",			quit_cmd},
+			{'h', "help",			help_cmd},
+		};
+		int *chosen;
+
+		menu_opts.header = _("*** Commands ***");
+		menu_opts.prompt = "What now";
+		menu_opts.flag = MENU_OPTS_SINGLETON;
+
+		menu_stuff.type = MENU_STUFF_TYPE_MENU_ITEM;
+		menu_stuff.stuff = menus;
+		menu_stuff.nr = sizeof(menus) / sizeof(struct menu_item);
+
 		clean_print_color(CLEAN_COLOR_HEADER);
 		printf_ln(Q_("Would remove the following item:",
 			     "Would remove the following items:",
 			     del_list.nr));
 		clean_print_color(CLEAN_COLOR_RESET);
-		putchar('\n');
 
-		/* Display dels in columns */
+		/* display dels in columns */
 		pretty_print_dels();
 
-		/* Confirmation dialog */
-		clean_print_color(CLEAN_COLOR_PROMPT);
-		printf(_("Remove ([y]es/[n]o/[e]dit) ? "));
-		clean_print_color(CLEAN_COLOR_RESET);
-		if (strbuf_getline(&confirm, stdin, '\n') != EOF) {
-			strbuf_trim(&confirm);
-		} else {
-			/* Ctrl-D is the same as "quit" */
-			string_list_clear(&del_list, 0);
-			putchar('\n');
-			printf_ln("Bye.");
-			break;
-		}
-
-		if (confirm.len) {
-			if (!strncasecmp(confirm.buf, "yes", confirm.len)) {
-				break;
-			} else if (!strncasecmp(confirm.buf, "no", confirm.len) ||
-				   !strncasecmp(confirm.buf, "quit", confirm.len)) {
-				string_list_clear(&del_list, 0);
-				printf_ln("Bye.");
-				break;
-			} else if (!strncasecmp(confirm.buf, "edit", confirm.len)) {
-				edit_by_patterns_cmd();
-			} else {
+		/* main menu */
+		chosen = list_and_choose(&menu_opts, &menu_stuff);
+
+		if (*chosen != EOF) {
+			int ret;
+			ret = menus[*chosen].fn(1);
+			if (ret != MENU_RETURN_NO_LOOP) {
+				free(chosen);
+				chosen = NULL;
+				if (!del_list.nr) {
+					clean_print_color(CLEAN_COLOR_ERROR);
+					printf_ln(_("No more files to clean, exiting."));
+					clean_print_color(CLEAN_COLOR_RESET);
+					break;
+				}
 				continue;
 			}
+		} else {
+			quit_cmd();
 		}
-	}
 
-	strbuf_release(&confirm);
+		free(chosen);
+		chosen = NULL;
+		break;
+	}
 }
 
 int cmd_clean(int argc, const char **argv, const char *prefix)
@@ -499,9 +824,8 @@ int cmd_clean(int argc, const char **argv, const char *prefix)
 		}
 
 		if (S_ISDIR(st.st_mode)) {
-			if (remove_directories || (matches == MATCHED_EXACTLY)) {
+			if (remove_directories || (matches == MATCHED_EXACTLY))
 				string_list_append(&del_list, ent->name);
-			}
 		} else {
 			if (pathspec && !matches)
 				continue;
-- 
1.8.3.rc1.338.gb35aa5d
Previous: Jiang XinNext: Jiang Xin
Message 14 of 22 in “interactive git clean”
  1. 0/3 interactive git cleanJiang Xin, May 3, 2013
  2. 1/3 Add support for -i/--interactive to git-cleanJiang Xin, May 3, 2013
  3. 2/3 Show items of interactive git-clean in columnsJiang Xin, May 3, 2013
  4. 3/3 Add colors to interactive git-cleanJiang Xin, May 3, 2013
  5. Eric SunshineMay 3, 2013
  6. Jiang XinMay 4, 2013
  7. Eric SunshineMay 5, 2013
  8. Matthieu MoyMay 6, 2013
  9. Eric SunshineMay 6, 2013
  10. 0/7 interactive git cleanJiang Xin, May 6, 2013
  11. 1/7 Add support for -i/--interactive to git-cleanJiang Xin, May 6, 2013
  12. 2/7 Show items of interactive git-clean in columnsJiang Xin, May 6, 2013
  13. 3/7 Add colors to interactive git-cleanJiang Xin, May 6, 2013
  14. 4/7 git-clean: use a git-add-interactive compatible UIJiang Xin, May 6, 2013
  15. 5/7 git-clean: interactive cleaning by select numbersJiang Xin, May 6, 2013
  16. 6/7 git-clean: rm -i style interactive cleaningJiang Xin, May 6, 2013
  17. 7/7 git-clean: update document for interactive git-cleanJiang Xin, May 6, 2013
  18. Jiang XinMay 7, 2013
  19. Jiang XinMay 7, 2013
  20. Junio C HamanoMay 7, 2013
  21. Jiang XinMay 8, 2013
  22. Junio C HamanoMay 3, 2013

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.