From 1e4ffc765db69c1c2061452c122caf17c7fb25f3 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 14 Jan 2020 18:43:44 +0000 Subject: [PATCH 01/10] t3701: adjust difffilter test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In 42f7d45428e (add--interactive: detect bogus diffFilter output, 2018-03-03), we added a test case that verifies that the diffFilter feature complains appropriately when the output is too short. In preparation for the upcoming change where the built-in `add -p` is taught to respect that setting, let's adjust that test a little. The problem is that `echo too-short` is configured as diffFilter, and it does not read the `stdin`. When calling it through `pipe_command()`, it is therefore possible that we try to feed the `diff` to it while it is no longer listening, and we receive a `SIGPIPE`. The Perl code apparently handles this in a way similar to an end-of-file, but taking a step back, we realize that a diffFilter that does not even _look_ at its standard input is very unrealistic. The entire point of this feature is to transform the diff, not to ignore it altogether. So let's modify the test case to reflect that insight: instead of printing some bogus text, let's use a diffFilter that deletes the first line of the diff instead. This still tests for the same thing, but it does not confuse the built-in `add -p` with that `SIGPIPE`. Helped-by: SZEDER Gábor Helped-by: Jeff King Signed-off-by: Johannes Schindelin Signed-off-by: Junio C Hamano --- t/t3701-add-interactive.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/t/t3701-add-interactive.sh b/t/t3701-add-interactive.sh index 12ee321707..ac43f835a5 100755 --- a/t/t3701-add-interactive.sh +++ b/t/t3701-add-interactive.sh @@ -561,7 +561,7 @@ test_expect_success 'detect bogus diffFilter output' ' git reset --hard && echo content >test && - test_config interactive.diffFilter "echo too-short" && + test_config interactive.diffFilter "sed 1d" && printf y >y && test_must_fail force_color git add -p Date: Tue, 14 Jan 2020 18:43:45 +0000 Subject: [PATCH 02/10] built-in add -p: support interactive.diffFilter The Perl version supports post-processing the colored diff (that is generated in addition to the uncolored diff, intended to offer a prettier user experience) by a command configured via that config setting, and now the built-in version does that, too. Signed-off-by: Johannes Schindelin Signed-off-by: Junio C Hamano --- add-interactive.c | 12 ++++++++++++ add-interactive.h | 3 +++ add-patch.c | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 48 insertions(+) diff --git a/add-interactive.c b/add-interactive.c index a5bb14f2f4..1786ea29c4 100644 --- a/add-interactive.c +++ b/add-interactive.c @@ -52,6 +52,17 @@ void init_add_i_state(struct add_i_state *s, struct repository *r) diff_get_color(s->use_color, DIFF_FILE_OLD)); init_color(r, s, "new", s->file_new_color, diff_get_color(s->use_color, DIFF_FILE_NEW)); + + FREE_AND_NULL(s->interactive_diff_filter); + git_config_get_string("interactive.difffilter", + &s->interactive_diff_filter); +} + +void clear_add_i_state(struct add_i_state *s) +{ + FREE_AND_NULL(s->interactive_diff_filter); + memset(s, 0, sizeof(*s)); + s->use_color = -1; } /* @@ -1149,6 +1160,7 @@ int run_add_i(struct repository *r, const struct pathspec *ps) strbuf_release(&print_file_item_data.worktree); strbuf_release(&header); prefix_item_list_clear(&commands); + clear_add_i_state(&s); return res; } diff --git a/add-interactive.h b/add-interactive.h index b2f23479c5..46c73867ad 100644 --- a/add-interactive.h +++ b/add-interactive.h @@ -15,9 +15,12 @@ struct add_i_state { char context_color[COLOR_MAXLEN]; char file_old_color[COLOR_MAXLEN]; char file_new_color[COLOR_MAXLEN]; + + char *interactive_diff_filter; }; void init_add_i_state(struct add_i_state *s, struct repository *r); +void clear_add_i_state(struct add_i_state *s); struct repository; struct pathspec; diff --git a/add-patch.c b/add-patch.c index 46c6c183d5..78bde41df0 100644 --- a/add-patch.c +++ b/add-patch.c @@ -398,6 +398,7 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps) if (want_color_fd(1, -1)) { struct child_process colored_cp = CHILD_PROCESS_INIT; + const char *diff_filter = s->s.interactive_diff_filter; setup_child_process(s, &colored_cp, NULL); xsnprintf((char *)args.argv[color_arg_index], 8, "--color"); @@ -407,6 +408,24 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps) argv_array_clear(&args); if (res) return error(_("could not parse colored diff")); + + if (diff_filter) { + struct child_process filter_cp = CHILD_PROCESS_INIT; + + setup_child_process(s, &filter_cp, + diff_filter, NULL); + filter_cp.git_cmd = 0; + filter_cp.use_shell = 1; + strbuf_reset(&s->buf); + if (pipe_command(&filter_cp, + colored->buf, colored->len, + &s->buf, colored->len, + NULL, 0) < 0) + return error(_("failed to run '%s'"), + diff_filter); + strbuf_swap(colored, &s->buf); + } + strbuf_complete_line(colored); colored_p = colored->buf; colored_pend = colored_p + colored->len; @@ -531,6 +550,9 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps) colored_pend - colored_p); if (colored_eol) colored_p = colored_eol + 1; + else if (p != pend) + /* colored shorter than non-colored? */ + goto mismatched_output; else colored_p = colored_pend; @@ -555,6 +577,15 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps) */ hunk->splittable_into++; + /* non-colored shorter than colored? */ + if (colored_p != colored_pend) { +mismatched_output: + error(_("mismatched output from interactive.diffFilter")); + advise(_("Your filter must maintain a one-to-one correspondence\n" + "between its input and output lines.")); + return -1; + } + return 0; } @@ -1612,6 +1643,7 @@ int run_add_p(struct repository *r, enum add_p_mode mode, parse_diff(&s, ps) < 0) { strbuf_release(&s.plain); strbuf_release(&s.colored); + clear_add_i_state(&s.s); return -1; } @@ -1630,5 +1662,6 @@ int run_add_p(struct repository *r, enum add_p_mode mode, strbuf_release(&s.buf); strbuf_release(&s.plain); strbuf_release(&s.colored); + clear_add_i_state(&s.s); return 0; } From 08b1ea4c39b08b76fe2d1240ccbf6077ef19226a Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 14 Jan 2020 18:43:46 +0000 Subject: [PATCH 03/10] built-in add -p: handle diff.algorithm The Perl version of `git add -p` reads the config setting `diff.algorithm` and if set, uses it to generate the diff using the specified algorithm. This patch ports that functionality to the C version. Note: just like `git-add--interactive.perl`, we do _not_ respect this config setting in `git add -i`'s `diff` command, but _only_ in the `patch` command. Signed-off-by: Johannes Schindelin Signed-off-by: Junio C Hamano --- add-interactive.c | 5 +++++ add-interactive.h | 2 +- add-patch.c | 3 +++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/add-interactive.c b/add-interactive.c index 1786ea29c4..9e4bcb382c 100644 --- a/add-interactive.c +++ b/add-interactive.c @@ -56,11 +56,16 @@ void init_add_i_state(struct add_i_state *s, struct repository *r) FREE_AND_NULL(s->interactive_diff_filter); git_config_get_string("interactive.difffilter", &s->interactive_diff_filter); + + FREE_AND_NULL(s->interactive_diff_algorithm); + git_config_get_string("diff.algorithm", + &s->interactive_diff_algorithm); } void clear_add_i_state(struct add_i_state *s) { FREE_AND_NULL(s->interactive_diff_filter); + FREE_AND_NULL(s->interactive_diff_algorithm); memset(s, 0, sizeof(*s)); s->use_color = -1; } diff --git a/add-interactive.h b/add-interactive.h index 46c73867ad..923efaf527 100644 --- a/add-interactive.h +++ b/add-interactive.h @@ -16,7 +16,7 @@ struct add_i_state { char file_old_color[COLOR_MAXLEN]; char file_new_color[COLOR_MAXLEN]; - char *interactive_diff_filter; + char *interactive_diff_filter, *interactive_diff_algorithm; }; void init_add_i_state(struct add_i_state *s, struct repository *r); diff --git a/add-patch.c b/add-patch.c index 78bde41df0..8f2ee8688b 100644 --- a/add-patch.c +++ b/add-patch.c @@ -360,6 +360,7 @@ static int is_octal(const char *p, size_t len) static int parse_diff(struct add_p_state *s, const struct pathspec *ps) { struct argv_array args = ARGV_ARRAY_INIT; + const char *diff_algorithm = s->s.interactive_diff_algorithm; struct strbuf *plain = &s->plain, *colored = NULL; struct child_process cp = CHILD_PROCESS_INIT; char *p, *pend, *colored_p = NULL, *colored_pend = NULL, marker = '\0'; @@ -369,6 +370,8 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps) int res; argv_array_pushv(&args, s->mode->diff_cmd); + if (diff_algorithm) + argv_array_pushf(&args, "--diff-algorithm=%s", diff_algorithm); if (s->revision) { struct object_id oid; argv_array_push(&args, From 94ac3c31f730ab278e1373a942fb4503829f4279 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 14 Jan 2020 18:43:47 +0000 Subject: [PATCH 04/10] terminal: make the code of disable_echo() reusable We are about to introduce the function `enable_non_canonical()`, which shares almost the complete code with `disable_echo()`. Let's prepare for that, by refactoring out that shared code. Signed-off-by: Johannes Schindelin Signed-off-by: Junio C Hamano --- compat/terminal.c | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/compat/terminal.c b/compat/terminal.c index fa13ee672d..1fb40b3a0a 100644 --- a/compat/terminal.c +++ b/compat/terminal.c @@ -32,7 +32,7 @@ static void restore_term(void) term_fd = -1; } -static int disable_echo(void) +static int disable_bits(tcflag_t bits) { struct termios t; @@ -43,7 +43,7 @@ static int disable_echo(void) old_term = t; sigchain_push_common(restore_term_on_signal); - t.c_lflag &= ~ECHO; + t.c_lflag &= ~bits; if (!tcsetattr(term_fd, TCSAFLUSH, &t)) return 0; @@ -53,6 +53,11 @@ error: return -1; } +static int disable_echo(void) +{ + return disable_bits(ECHO); +} + #elif defined(GIT_WINDOWS_NATIVE) #define INPUT_PATH "CONIN$" @@ -72,7 +77,7 @@ static void restore_term(void) hconin = INVALID_HANDLE_VALUE; } -static int disable_echo(void) +static int disable_bits(DWORD bits) { hconin = CreateFile("CONIN$", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ, NULL, OPEN_EXISTING, @@ -82,7 +87,7 @@ static int disable_echo(void) GetConsoleMode(hconin, &cmode); sigchain_push_common(restore_term_on_signal); - if (!SetConsoleMode(hconin, cmode & (~ENABLE_ECHO_INPUT))) { + if (!SetConsoleMode(hconin, cmode & ~bits)) { CloseHandle(hconin); hconin = INVALID_HANDLE_VALUE; return -1; @@ -91,6 +96,12 @@ static int disable_echo(void) return 0; } +static int disable_echo(void) +{ + return disable_bits(ENABLE_ECHO_INPUT); +} + + #endif #ifndef FORCE_TEXT From 9ea416cb511e87f830462b08ae74b8a78fcca223 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 14 Jan 2020 18:43:48 +0000 Subject: [PATCH 05/10] terminal: accommodate Git for Windows' default terminal Git for Windows' Git Bash runs in MinTTY by default, which does not have a Win32 Console instance, but uses MSYS2 pseudo terminals instead. This is a problem, as Git for Windows does not want to use the MSYS2 emulation layer for Git itself, and therefore has no direct way to interact with that pseudo terminal. As a workaround, use the `stty` utility (which is included in Git for Windows, and which *is* an MSYS2 program, so it knows how to deal with the pseudo terminal). Note: If Git runs in a regular CMD or PowerShell window, there *is* a regular Win32 Console to work with. This is not a problem for the MSYS2 `stty`: it copes with this scenario just fine. Also note that we introduce support for more bits than would be necessary for a mere `disable_echo()` here, in preparation for the upcoming `enable_non_canonical()` function. Signed-off-by: Johannes Schindelin Signed-off-by: Junio C Hamano --- compat/terminal.c | 50 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/compat/terminal.c b/compat/terminal.c index 1fb40b3a0a..16e9949da1 100644 --- a/compat/terminal.c +++ b/compat/terminal.c @@ -2,6 +2,8 @@ #include "compat/terminal.h" #include "sigchain.h" #include "strbuf.h" +#include "run-command.h" +#include "string-list.h" #if defined(HAVE_DEV_TTY) || defined(GIT_WINDOWS_NATIVE) @@ -64,11 +66,28 @@ static int disable_echo(void) #define OUTPUT_PATH "CONOUT$" #define FORCE_TEXT "t" +static int use_stty = 1; +static struct string_list stty_restore = STRING_LIST_INIT_DUP; static HANDLE hconin = INVALID_HANDLE_VALUE; static DWORD cmode; static void restore_term(void) { + if (use_stty) { + int i; + struct child_process cp = CHILD_PROCESS_INIT; + + if (stty_restore.nr == 0) + return; + + argv_array_push(&cp.args, "stty"); + for (i = 0; i < stty_restore.nr; i++) + argv_array_push(&cp.args, stty_restore.items[i].string); + run_command(&cp); + string_list_clear(&stty_restore, 0); + return; + } + if (hconin == INVALID_HANDLE_VALUE) return; @@ -79,6 +98,37 @@ static void restore_term(void) static int disable_bits(DWORD bits) { + if (use_stty) { + struct child_process cp = CHILD_PROCESS_INIT; + + argv_array_push(&cp.args, "stty"); + + if (bits & ENABLE_LINE_INPUT) { + string_list_append(&stty_restore, "icanon"); + argv_array_push(&cp.args, "-icanon"); + } + + if (bits & ENABLE_ECHO_INPUT) { + string_list_append(&stty_restore, "echo"); + argv_array_push(&cp.args, "-echo"); + } + + if (bits & ENABLE_PROCESSED_INPUT) { + string_list_append(&stty_restore, "-ignbrk"); + string_list_append(&stty_restore, "intr"); + string_list_append(&stty_restore, "^c"); + argv_array_push(&cp.args, "ignbrk"); + argv_array_push(&cp.args, "intr"); + argv_array_push(&cp.args, ""); + } + + if (run_command(&cp) == 0) + return 0; + + /* `stty` could not be executed; access the Console directly */ + use_stty = 0; + } + hconin = CreateFile("CONIN$", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); From a5e46e6b0101b960c131dd39b50999cc0e69ed2b Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 14 Jan 2020 18:43:49 +0000 Subject: [PATCH 06/10] terminal: add a new function to read a single keystroke Typically, input on the command-line is line-based. It is actually not really easy to get single characters (or better put: keystrokes). We provide two implementations here: - One that handles `/dev/tty` based systems as well as native Windows. The former uses the `tcsetattr()` function to put the terminal into "raw mode", which allows us to read individual keystrokes, one by one. The latter uses `stty.exe` to do the same, falling back to direct Win32 Console access. Thanks to the refactoring leading up to this commit, this is a single function, with the platform-specific details hidden away in conditionally-compiled code blocks. - A fall-back which simply punts and reads back an entire line. Note that the function writes the keystroke into an `strbuf` rather than a `char`, in preparation for reading Escape sequences (e.g. when the user hit an arrow key). This is also required for UTF-8 sequences in case the keystroke corresponds to a non-ASCII letter. Signed-off-by: Johannes Schindelin Signed-off-by: Junio C Hamano --- compat/terminal.c | 55 +++++++++++++++++++++++++++++++++++++++++++++++ compat/terminal.h | 3 +++ 2 files changed, 58 insertions(+) diff --git a/compat/terminal.c b/compat/terminal.c index 16e9949da1..1b2564042a 100644 --- a/compat/terminal.c +++ b/compat/terminal.c @@ -60,6 +60,11 @@ static int disable_echo(void) return disable_bits(ECHO); } +static int enable_non_canonical(void) +{ + return disable_bits(ICANON | ECHO); +} + #elif defined(GIT_WINDOWS_NATIVE) #define INPUT_PATH "CONIN$" @@ -151,6 +156,10 @@ static int disable_echo(void) return disable_bits(ENABLE_ECHO_INPUT); } +static int enable_non_canonical(void) +{ + return disable_bits(ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT | ENABLE_PROCESSED_INPUT); +} #endif @@ -198,6 +207,33 @@ char *git_terminal_prompt(const char *prompt, int echo) return buf.buf; } +int read_key_without_echo(struct strbuf *buf) +{ + static int warning_displayed; + int ch; + + if (warning_displayed || enable_non_canonical() < 0) { + if (!warning_displayed) { + warning("reading single keystrokes not supported on " + "this platform; reading line instead"); + warning_displayed = 1; + } + + return strbuf_getline(buf, stdin); + } + + strbuf_reset(buf); + ch = getchar(); + if (ch == EOF) { + restore_term(); + return EOF; + } + + strbuf_addch(buf, ch); + restore_term(); + return 0; +} + #else char *git_terminal_prompt(const char *prompt, int echo) @@ -205,4 +241,23 @@ char *git_terminal_prompt(const char *prompt, int echo) return getpass(prompt); } +int read_key_without_echo(struct strbuf *buf) +{ + static int warning_displayed; + const char *res; + + if (!warning_displayed) { + warning("reading single keystrokes not supported on this " + "platform; reading line instead"); + warning_displayed = 1; + } + + res = getpass(""); + strbuf_reset(buf); + if (!res) + return EOF; + strbuf_addstr(buf, res); + return 0; +} + #endif diff --git a/compat/terminal.h b/compat/terminal.h index 97db7cd69d..a9d52b8464 100644 --- a/compat/terminal.h +++ b/compat/terminal.h @@ -3,4 +3,7 @@ char *git_terminal_prompt(const char *prompt, int echo); +/* Read a single keystroke, without echoing it to the terminal */ +int read_key_without_echo(struct strbuf *buf); + #endif /* COMPAT_TERMINAL_H */ From 04f816b125dc2649e70aad686d79b05bdc1d1c61 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 14 Jan 2020 18:43:50 +0000 Subject: [PATCH 07/10] built-in add -p: respect the `interactive.singlekey` config setting The Perl version of `git add -p` supports this config setting to allow users to input commands via single characters (as opposed to having to press the key afterwards). This is an opt-in feature because it requires Perl packages (Term::ReadKey and Term::Cap, where it tries to handle an absence of the latter package gracefully) to work. Note that at least on Ubuntu, that Perl package is not installed by default (it needs to be installed via `sudo apt-get install libterm-readkey-perl`), so this feature is probably not used a whole lot. In C, we obviously do not have these packages available, but we just introduced `read_single_keystroke()` that is similar to what Term::ReadKey provides, and we use that here. Signed-off-by: Johannes Schindelin Signed-off-by: Junio C Hamano --- add-interactive.c | 2 ++ add-interactive.h | 1 + add-patch.c | 21 +++++++++++++++++---- 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/add-interactive.c b/add-interactive.c index 9e4bcb382c..39c3896494 100644 --- a/add-interactive.c +++ b/add-interactive.c @@ -60,6 +60,8 @@ void init_add_i_state(struct add_i_state *s, struct repository *r) FREE_AND_NULL(s->interactive_diff_algorithm); git_config_get_string("diff.algorithm", &s->interactive_diff_algorithm); + + git_config_get_bool("interactive.singlekey", &s->use_single_key); } void clear_add_i_state(struct add_i_state *s) diff --git a/add-interactive.h b/add-interactive.h index 923efaf527..693f125e8e 100644 --- a/add-interactive.h +++ b/add-interactive.h @@ -16,6 +16,7 @@ struct add_i_state { char file_old_color[COLOR_MAXLEN]; char file_new_color[COLOR_MAXLEN]; + int use_single_key; char *interactive_diff_filter, *interactive_diff_algorithm; }; diff --git a/add-patch.c b/add-patch.c index 8f2ee8688b..d8dafa8168 100644 --- a/add-patch.c +++ b/add-patch.c @@ -6,6 +6,7 @@ #include "pathspec.h" #include "color.h" #include "diff.h" +#include "compat/terminal.h" enum prompt_mode_type { PROMPT_MODE_CHANGE = 0, PROMPT_DELETION, PROMPT_HUNK, @@ -1149,14 +1150,27 @@ static int run_apply_check(struct add_p_state *s, return 0; } +static int read_single_character(struct add_p_state *s) +{ + if (s->s.use_single_key) { + int res = read_key_without_echo(&s->answer); + printf("%s\n", res == EOF ? "" : s->answer.buf); + return res; + } + + if (strbuf_getline(&s->answer, stdin) == EOF) + return EOF; + strbuf_trim_trailing_newline(&s->answer); + return 0; +} + static int prompt_yesno(struct add_p_state *s, const char *prompt) { for (;;) { color_fprintf(stdout, s->s.prompt_color, "%s", _(prompt)); fflush(stdout); - if (strbuf_getline(&s->answer, stdin) == EOF) + if (read_single_character(s) == EOF) return -1; - strbuf_trim_trailing_newline(&s->answer); switch (tolower(s->answer.buf[0])) { case 'n': return 0; case 'y': return 1; @@ -1396,9 +1410,8 @@ static int patch_update_file(struct add_p_state *s, _(s->mode->prompt_mode[prompt_mode_type]), s->buf.buf); fflush(stdout); - if (strbuf_getline(&s->answer, stdin) == EOF) + if (read_single_character(s) == EOF) break; - strbuf_trim_trailing_newline(&s->answer); if (!s->answer.len) continue; From e118f06396bb298f2852070f648c6b4bb221a925 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 14 Jan 2020 18:43:51 +0000 Subject: [PATCH 08/10] built-in add -p: handle Escape sequences in interactive.singlekey mode This recapitulates part of b5cc003253c8 (add -i: ignore terminal escape sequences, 2011-05-17): add -i: ignore terminal escape sequences On the author's terminal, the up-arrow input sequence is ^[[A, and thus fat-fingering an up-arrow into 'git checkout -p' is quite dangerous: git-add--interactive.perl will ignore the ^[ and [ characters and happily treat A as "discard everything". As a band-aid fix, use Term::Cap to get all terminal capabilities. Then use the heuristic that any capability value that starts with ^[ (i.e., \e in perl) must be a key input sequence. Finally, given an input that starts with ^[, read more characters until we have read a full escape sequence, then return that to the caller. We use a timeout of 0.5 seconds on the subsequent reads to avoid getting stuck if the user actually input a lone ^[. Since none of the currently recognized keys start with ^[, the net result is that the sequence as a whole will be ignored and the help displayed. Note that we leave part for later which uses "Term::Cap to get all terminal capabilities", for several reasons: 1. it is actually not really necessary, as the timeout of 0.5 seconds should be plenty sufficient to catch Escape sequences, 2. it is cleaner to keep the change to special-case Escape sequences separate from the change that reads all terminal capabilities to speed things up, and 3. in practice, relying on the terminal capabilities is a bit overrated, as the information could be incomplete, or plain wrong. For example, in this developer's tmux sessions, the terminal capabilities claim that the "cursor up" sequence is ^[M, but the actual sequence produced by the "cursor up" key is ^[[A. Signed-off-by: Johannes Schindelin Signed-off-by: Junio C Hamano --- compat/terminal.c | 56 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/compat/terminal.c b/compat/terminal.c index 1b2564042a..b7f58d1781 100644 --- a/compat/terminal.c +++ b/compat/terminal.c @@ -161,6 +161,37 @@ static int enable_non_canonical(void) return disable_bits(ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT | ENABLE_PROCESSED_INPUT); } +/* + * Override `getchar()`, as the default implementation does not use + * `ReadFile()`. + * + * This poses a problem when we want to see whether the standard + * input has more characters, as the default of Git for Windows is to start the + * Bash in a MinTTY, which uses a named pipe to emulate a pty, in which case + * our `poll()` emulation calls `PeekNamedPipe()`, which seems to require + * `ReadFile()` to be called first to work properly (it only reports 0 + * available bytes, otherwise). + * + * So let's just override `getchar()` with a version backed by `ReadFile()` and + * go our merry ways from here. + */ +static int mingw_getchar(void) +{ + DWORD read = 0; + unsigned char ch; + + if (!ReadFile(GetStdHandle(STD_INPUT_HANDLE), &ch, 1, &read, NULL)) + return EOF; + + if (!read) { + error("Unexpected 0 read"); + return EOF; + } + + return ch; +} +#define getchar mingw_getchar + #endif #ifndef FORCE_TEXT @@ -228,8 +259,31 @@ int read_key_without_echo(struct strbuf *buf) restore_term(); return EOF; } - strbuf_addch(buf, ch); + + if (ch == '\033' /* ESC */) { + /* + * We are most likely looking at an Escape sequence. Let's try + * to read more bytes, waiting at most half a second, assuming + * that the sequence is complete if we did not receive any byte + * within that time. + * + * Start by replacing the Escape byte with ^[ */ + strbuf_splice(buf, buf->len - 1, 1, "^[", 2); + + for (;;) { + struct pollfd pfd = { .fd = 0, .events = POLLIN }; + + if (poll(&pfd, 1, 500) < 1) + break; + + ch = getchar(); + if (ch == EOF) + return 0; + strbuf_addch(buf, ch); + } + } + restore_term(); return 0; } From 12acdf573aafb8633f710fdbe5ac1282165fc6cc Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 14 Jan 2020 18:43:52 +0000 Subject: [PATCH 09/10] built-in add -p: handle Escape sequences more efficiently When `interactive.singlekey = true`, we react immediately to keystrokes, even to Escape sequences (e.g. when pressing a cursor key). The problem with Escape sequences is that we do not really know when they are done, and as a heuristic we poll standard input for half a second to make sure that we got all of it. While waiting half a second is not asking for a whole lot, it can become quite annoying over time, therefore with this patch, we read the terminal capabilities (if available) and extract known Escape sequences from there, then stop polling immediately when we detected that the user pressed a key that generated such a known sequence. This recapitulates the remaining part of b5cc003253c8 (add -i: ignore terminal escape sequences, 2011-05-17). Note: We do *not* query the terminal capabilities directly. That would either require a lot of platform-specific code, or it would require linking to a library such as ncurses. Linking to a library in the built-ins is something we try very hard to avoid (we even kicked the libcurl dependency to a non-built-in remote helper, just to shave off a tiny fraction of a second from Git's startup time). And the platform-specific code would be a maintenance nightmare. Even worse: in Git for Windows' case, we would need to query MSYS2 pseudo terminals, which `git.exe` simply cannot do (because it is intentionally *not* an MSYS2 program). To address this, we simply spawn `infocmp -L -1` and parse its output (which works even in Git for Windows, because that helper is included in the end-user facing installations). This is done only once, as in the Perl version, but it is done only when the first Escape sequence is encountered, not upon startup of `git add -i`; This saves on startup time, yet makes reacting to the first Escape sequence slightly more sluggish. But it allows us to keep the terminal-related code encapsulated in the `compat/terminal.c` file. Signed-off-by: Johannes Schindelin Signed-off-by: Junio C Hamano --- compat/terminal.c | 73 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 72 insertions(+), 1 deletion(-) diff --git a/compat/terminal.c b/compat/terminal.c index b7f58d1781..35bca03d14 100644 --- a/compat/terminal.c +++ b/compat/terminal.c @@ -4,6 +4,7 @@ #include "strbuf.h" #include "run-command.h" #include "string-list.h" +#include "hashmap.h" #if defined(HAVE_DEV_TTY) || defined(GIT_WINDOWS_NATIVE) @@ -238,6 +239,71 @@ char *git_terminal_prompt(const char *prompt, int echo) return buf.buf; } +/* + * The `is_known_escape_sequence()` function returns 1 if the passed string + * corresponds to an Escape sequence that the terminal capabilities contains. + * + * To avoid depending on ncurses or other platform-specific libraries, we rely + * on the presence of the `infocmp` executable to do the job for us (failing + * silently if the program is not available or refused to run). + */ +struct escape_sequence_entry { + struct hashmap_entry entry; + char sequence[FLEX_ARRAY]; +}; + +static int sequence_entry_cmp(const void *hashmap_cmp_fn_data, + const struct escape_sequence_entry *e1, + const struct escape_sequence_entry *e2, + const void *keydata) +{ + return strcmp(e1->sequence, keydata ? keydata : e2->sequence); +} + +static int is_known_escape_sequence(const char *sequence) +{ + static struct hashmap sequences; + static int initialized; + + if (!initialized) { + struct child_process cp = CHILD_PROCESS_INIT; + struct strbuf buf = STRBUF_INIT; + char *p, *eol; + + hashmap_init(&sequences, (hashmap_cmp_fn)sequence_entry_cmp, + NULL, 0); + + argv_array_pushl(&cp.args, "infocmp", "-L", "-1", NULL); + if (pipe_command(&cp, NULL, 0, &buf, 0, NULL, 0)) + strbuf_setlen(&buf, 0); + + for (eol = p = buf.buf; *p; p = eol + 1) { + p = strchr(p, '='); + if (!p) + break; + p++; + eol = strchrnul(p, '\n'); + + if (starts_with(p, "\\E")) { + char *comma = memchr(p, ',', eol - p); + struct escape_sequence_entry *e; + + p[0] = '^'; + p[1] = '['; + FLEX_ALLOC_MEM(e, sequence, p, comma - p); + hashmap_entry_init(&e->entry, + strhash(e->sequence)); + hashmap_add(&sequences, &e->entry); + } + if (!*eol) + break; + } + initialized = 1; + } + + return !!hashmap_get_from_hash(&sequences, strhash(sequence), sequence); +} + int read_key_without_echo(struct strbuf *buf) { static int warning_displayed; @@ -271,7 +337,12 @@ int read_key_without_echo(struct strbuf *buf) * Start by replacing the Escape byte with ^[ */ strbuf_splice(buf, buf->len - 1, 1, "^[", 2); - for (;;) { + /* + * Query the terminal capabilities once about all the Escape + * sequences it knows about, so that we can avoid waiting for + * half a second when we know that the sequence is complete. + */ + while (!is_known_escape_sequence(buf->buf)) { struct pollfd pfd = { .fd = 0, .events = POLLIN }; if (poll(&pfd, 1, 500) < 1) From b2627cc3d4be2f8086711097f99d79a32c6a703a Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 14 Jan 2020 18:43:53 +0000 Subject: [PATCH 10/10] ci: include the built-in `git add -i` in the `linux-gcc` job This job runs the test suite twice, once in regular mode, and once with a whole slew of `GIT_TEST_*` variables set. Now that the built-in version of `git add --interactive` is feature-complete, let's also throw `GIT_TEST_ADD_I_USE_BUILTIN` into that fray. Signed-off-by: Johannes Schindelin Signed-off-by: Junio C Hamano --- ci/run-build-and-tests.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/ci/run-build-and-tests.sh b/ci/run-build-and-tests.sh index ff0ef7f08e..4df54c4efe 100755 --- a/ci/run-build-and-tests.sh +++ b/ci/run-build-and-tests.sh @@ -20,6 +20,7 @@ linux-gcc) export GIT_TEST_OE_DELTA_SIZE=5 export GIT_TEST_COMMIT_GRAPH=1 export GIT_TEST_MULTI_PACK_INDEX=1 + export GIT_TEST_ADD_I_USE_BUILTIN=1 make test ;; linux-gcc-4.8)