From 8cdd7678de04e02a7d51edb12a48faa07718f4db Mon Sep 17 00:00:00 2001 From: Sverre Rabbelier Date: Sun, 24 Jul 2011 15:54:04 +0200 Subject: [PATCH 01/98] t9350: point out that refs are not updated correctly This happens only when the corresponding commits are not exported in the current fast-export run. This can happen either when the relevant commit is already marked, or when the commit is explicitly marked as UNINTERESTING with a negative ref by another argument. This breaks fast-export basec remote helpers. Signed-off-by: Sverre Rabbelier --- t/t9350-fast-export.sh | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/t/t9350-fast-export.sh b/t/t9350-fast-export.sh index 2e4e214815..73e28e21a1 100755 --- a/t/t9350-fast-export.sh +++ b/t/t9350-fast-export.sh @@ -746,4 +746,15 @@ test_expect_success 'merge commit gets exported with --import-marks' ' ) ' +cat > expected << EOF +reset refs/heads/master +from $(git rev-parse master) + +EOF + +test_expect_failure 'refs are updated even if no commits need to be exported' ' + git fast-export master..master > actual && + test_cmp expected actual +' + test_done From bac2140a9baee68f45c3f3518af6afe1b2fe871c Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Wed, 7 Sep 2016 18:07:04 +0200 Subject: [PATCH 02/98] reset: support the experimental --stdin option Just like with other Git commands, this option makes it read the paths from the standard input. It comes in handy when resetting many, many paths at once and wildcards are not an option (e.g. when the paths are generated by a tool). Note: we first parse the entire list and perform the actual reset action only in a second phase. Not only does this make things simpler, it also helps performance, as do_diff_cache() traverses the index and the (sorted) pathspecs in simultaneously to avoid unnecessary lookups. This feature is marked experimental because it is still under review in the upstream Git project. Signed-off-by: Johannes Schindelin --- Documentation/git-reset.txt | 10 +++++++ builtin/reset.c | 53 ++++++++++++++++++++++++++++++++++++- t/t7108-reset-stdin.sh | 32 ++++++++++++++++++++++ 3 files changed, 94 insertions(+), 1 deletion(-) create mode 100755 t/t7108-reset-stdin.sh diff --git a/Documentation/git-reset.txt b/Documentation/git-reset.txt index 97e0544d9e..dde5096ccc 100644 --- a/Documentation/git-reset.txt +++ b/Documentation/git-reset.txt @@ -10,6 +10,7 @@ SYNOPSIS [verse] 'git reset' [-q] [] [--] ... 'git reset' (--patch | -p) [] [--] [...] +EXPERIMENTAL: 'git reset' [-q] [--stdin [-z]] [] 'git reset' [--soft | --mixed [-N] | --hard | --merge | --keep] [-q] [] DESCRIPTION @@ -101,6 +102,15 @@ OPTIONS `reset.quiet` config option. `--quiet` and `--no-quiet` will override the default behavior. +--stdin:: + EXPERIMENTAL: Instead of taking list of paths from the + command line, read list of paths from the standard input. + Paths are separated by LF (i.e. one path per line) by + default. + +-z:: + EXPERIMENTAL: Only meaningful with `--stdin`; paths are + separated with NUL character instead of LF. EXAMPLES -------- diff --git a/builtin/reset.c b/builtin/reset.c index fdd572168b..968245ff9a 100644 --- a/builtin/reset.c +++ b/builtin/reset.c @@ -25,12 +25,15 @@ #include "cache-tree.h" #include "submodule.h" #include "submodule-config.h" +#include "strbuf.h" +#include "quote.h" #define REFRESH_INDEX_DELAY_WARNING_IN_MS (2 * 1000) static const char * const git_reset_usage[] = { N_("git reset [--mixed | --soft | --hard | --merge | --keep] [-q] []"), N_("git reset [-q] [] [--] ..."), + N_("EXPERIMENTAL: git reset [-q] [--stdin [-z]] []"), N_("git reset --patch [] [--] [...]"), NULL }; @@ -284,7 +287,9 @@ static int git_reset_config(const char *var, const char *value, void *cb) int cmd_reset(int argc, const char **argv, const char *prefix) { int reset_type = NONE, update_ref_status = 0, quiet = 0; - int patch_mode = 0, unborn; + int patch_mode = 0, nul_term_line = 0, read_from_stdin = 0, unborn; + char **stdin_paths = NULL; + int stdin_nr = 0, stdin_alloc = 0; const char *rev; struct object_id oid; struct pathspec pathspec; @@ -306,6 +311,10 @@ int cmd_reset(int argc, const char **argv, const char *prefix) OPT_BOOL('p', "patch", &patch_mode, N_("select hunks interactively")), OPT_BOOL('N', "intent-to-add", &intent_to_add, N_("record only the fact that removed paths will be added later")), + OPT_BOOL('z', NULL, &nul_term_line, + N_("EXPERIMENTAL: paths are separated with NUL character")), + OPT_BOOL(0, "stdin", &read_from_stdin, + N_("EXPERIMENTAL: read paths from ")), OPT_END() }; @@ -316,6 +325,42 @@ int cmd_reset(int argc, const char **argv, const char *prefix) PARSE_OPT_KEEP_DASHDASH); parse_args(&pathspec, argv, prefix, patch_mode, &rev); + if (read_from_stdin) { + strbuf_getline_fn getline_fn = nul_term_line ? + strbuf_getline_nul : strbuf_getline_lf; + int flags = PATHSPEC_PREFER_FULL; + struct strbuf buf = STRBUF_INIT; + struct strbuf unquoted = STRBUF_INIT; + + if (patch_mode) + die(_("--stdin is incompatible with --patch")); + + if (pathspec.nr) + die(_("--stdin is incompatible with path arguments")); + + while (getline_fn(&buf, stdin) != EOF) { + if (!nul_term_line && buf.buf[0] == '"') { + strbuf_reset(&unquoted); + if (unquote_c_style(&unquoted, buf.buf, NULL)) + die(_("line is badly quoted")); + strbuf_swap(&buf, &unquoted); + } + ALLOC_GROW(stdin_paths, stdin_nr + 1, stdin_alloc); + stdin_paths[stdin_nr++] = xstrdup(buf.buf); + strbuf_reset(&buf); + } + strbuf_release(&unquoted); + strbuf_release(&buf); + + ALLOC_GROW(stdin_paths, stdin_nr + 1, stdin_alloc); + stdin_paths[stdin_nr++] = NULL; + flags |= PATHSPEC_LITERAL_PATH; + parse_pathspec(&pathspec, 0, flags, prefix, + (const char **)stdin_paths); + + } else if (nul_term_line) + die(_("-z requires --stdin")); + unborn = !strcmp(rev, "HEAD") && get_oid("HEAD", &oid); if (unborn) { /* reset on unborn branch: treat as reset to empty tree */ @@ -423,5 +468,11 @@ int cmd_reset(int argc, const char **argv, const char *prefix) if (!pathspec.nr) remove_branch_state(the_repository, 0); + if (stdin_paths) { + while (stdin_nr) + free(stdin_paths[--stdin_nr]); + free(stdin_paths); + } + return update_ref_status; } diff --git a/t/t7108-reset-stdin.sh b/t/t7108-reset-stdin.sh new file mode 100755 index 0000000000..b7cbcbf869 --- /dev/null +++ b/t/t7108-reset-stdin.sh @@ -0,0 +1,32 @@ +#!/bin/sh + +test_description='reset --stdin' + +. ./test-lib.sh + +test_expect_success 'reset --stdin' ' + test_commit hello && + git rm hello.t && + test -z "$(git ls-files hello.t)" && + echo hello.t | git reset --stdin && + test hello.t = "$(git ls-files hello.t)" +' + +test_expect_success 'reset --stdin -z' ' + test_commit world && + git rm hello.t world.t && + test -z "$(git ls-files hello.t world.t)" && + printf world.tQworld.tQhello.tQ | q_to_nul | git reset --stdin -z && + printf "hello.t\nworld.t\n" >expect && + git ls-files >actual && + test_cmp expect actual +' + +test_expect_success '--stdin requires --mixed' ' + echo hello.t >list && + test_must_fail git reset --soft --stdin Date: Sat, 28 Aug 2010 20:49:01 -0500 Subject: [PATCH 03/98] transport-helper: add trailing -- [PT: ensure we add an additional element to the argv array] --- transport-helper.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/transport-helper.c b/transport-helper.c index a9d690297e..b6dd7171c3 100644 --- a/transport-helper.c +++ b/transport-helper.c @@ -472,6 +472,8 @@ static int get_exporter(struct transport *transport, for (i = 0; i < revlist_args->nr; i++) argv_array_push(&fastexport->args, revlist_args->items[i].string); + argv_array_push(&fastexport->args, "--"); + fastexport->git_cmd = 1; return start_command(fastexport); } From 6469e8473c56499239dbf745227500be89286d80 Mon Sep 17 00:00:00 2001 From: Sverre Rabbelier Date: Sun, 24 Jul 2011 00:06:00 +0200 Subject: [PATCH 04/98] remote-helper: check helper status after import/export Signed-off-by: Johannes Schindelin Signed-off-by: Sverre Rabbelier --- builtin/clone.c | 4 +++- t/t5801-remote-helpers.sh | 2 +- transport-helper.c | 15 +++++++++++++++ 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/builtin/clone.c b/builtin/clone.c index c46ee29f0a..87c3a0ef54 100644 --- a/builtin/clone.c +++ b/builtin/clone.c @@ -1198,7 +1198,9 @@ int cmd_clone(int argc, const char **argv, const char *prefix) } if (!is_local && !complete_refs_before_fetch) - transport_fetch_refs(transport, mapped_refs); + if (transport_fetch_refs(transport, mapped_refs)) + die(_("could not fetch refs from %s"), + transport->url); remote_head = find_ref_by_name(refs, "HEAD"); remote_head_points_at = diff --git a/t/t5801-remote-helpers.sh b/t/t5801-remote-helpers.sh index 121e5c6edb..1b46389b19 100755 --- a/t/t5801-remote-helpers.sh +++ b/t/t5801-remote-helpers.sh @@ -230,7 +230,7 @@ test_expect_success 'push update refs failure' ' echo "update fail" >>file && git commit -a -m "update fail" && git rev-parse --verify testgit/origin/heads/update >expect && - test_expect_code 1 env GIT_REMOTE_TESTGIT_FAILURE="non-fast forward" \ + test_must_fail env GIT_REMOTE_TESTGIT_FAILURE="non-fast forward" \ git push origin update && git rev-parse --verify testgit/origin/heads/update >actual && test_cmp expect actual diff --git a/transport-helper.c b/transport-helper.c index b6dd7171c3..0961d80bd8 100644 --- a/transport-helper.c +++ b/transport-helper.c @@ -478,6 +478,19 @@ static int get_exporter(struct transport *transport, return start_command(fastexport); } +static void check_helper_status(struct helper_data *data) +{ + int pid, status; + + pid = waitpid(data->helper->pid, &status, WNOHANG); + if (pid < 0) + die("Could not retrieve status of remote helper '%s'", + data->name); + if (pid > 0 && WIFEXITED(status)) + die("Remote helper '%s' died with %d", + data->name, WEXITSTATUS(status)); +} + static int fetch_with_import(struct transport *transport, int nr_heads, struct ref **to_fetch) { @@ -514,6 +527,7 @@ static int fetch_with_import(struct transport *transport, if (finish_command(&fastimport)) die(_("error while running fast-import")); + check_helper_status(data); /* * The fast-import stream of a remote helper that advertises @@ -1020,6 +1034,7 @@ static int push_refs_with_export(struct transport *transport, if (finish_command(&exporter)) die(_("error while running fast-export")); + check_helper_status(data); if (push_update_refs_status(data, remote_refs, flags)) return 1; From 847d6d4a4e9785fc47c59804f81e3d88c828c6c9 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 18 Apr 2017 12:09:08 +0200 Subject: [PATCH 05/98] mingw: demonstrate a problem with certain absolute paths On Windows, there are several categories of absolute paths. One such category starts with a backslash and is implicitly relative to the drive associated with the current working directory. Example: c: git clone https://github.com/git-for-windows/git \G4W should clone into C:\G4W. There is currently a problem with that, in that mingw_mktemp() does not expect the _wmktemp() function to prefix the absolute path with the drive prefix, and as a consequence, the resulting path does not fit into the originally-passed string buffer. The symptom is a "Result too large" error. Reported by Juan Carlos Arevalo Baeza. Signed-off-by: Johannes Schindelin --- t/t5580-clone-push-unc.sh | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/t/t5580-clone-push-unc.sh b/t/t5580-clone-push-unc.sh index b3c8a92450..91db6cee97 100755 --- a/t/t5580-clone-push-unc.sh +++ b/t/t5580-clone-push-unc.sh @@ -17,14 +17,11 @@ fi UNCPATH="$(winpwd)" case "$UNCPATH" in [A-Z]:*) + WITHOUTDRIVE="${UNCPATH#?:}" # Use administrative share e.g. \\localhost\C$\git-sdk-64\usr\src\git # (we use forward slashes here because MSYS2 and Git accept them, and # they are easier on the eyes) - UNCPATH="//localhost/${UNCPATH%%:*}\$/${UNCPATH#?:}" - test -d "$UNCPATH" || { - skip_all='could not access administrative share; skipping' - test_done - } + UNCPATH="//localhost/${UNCPATH%%:*}\$$WITHOUTDRIVE" ;; *) skip_all='skipping UNC path tests, cannot determine current path as UNC' @@ -32,6 +29,18 @@ case "$UNCPATH" in ;; esac +test_expect_failure 'clone into absolute path lacking a drive prefix' ' + USINGBACKSLASHES="$(echo "$WITHOUTDRIVE"/without-drive-prefix | + tr / \\\\)" && + git clone . "$USINGBACKSLASHES" && + test -f without-drive-prefix/.git/HEAD +' + +test -d "$UNCPATH" || { + skip_all='could not access administrative share; skipping' + test_done +} + test_expect_success setup ' test_commit initial ' From aadca100195de7834d29c1406db5e16819ff4d95 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Mon, 9 Apr 2012 13:04:35 -0500 Subject: [PATCH 06/98] Always auto-gc after calling a fast-import transport After importing anything with fast-import, we should always let the garbage collector do its job, since the objects are written to disk inefficiently. This brings down an initial import of http://selenic.com/hg from about 230 megabytes to about 14. In the future, we may want to make this configurable on a per-remote basis, or maybe teach fast-import about it in the first place. Signed-off-by: Johannes Schindelin --- transport-helper.c | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/transport-helper.c b/transport-helper.c index 0961d80bd8..6c46f9e36d 100644 --- a/transport-helper.c +++ b/transport-helper.c @@ -16,6 +16,8 @@ #include "protocol.h" static int debug; +/* TODO: put somewhere sensible, e.g. git_transport_options? */ +static int auto_gc = 1; struct helper_data { const char *name; @@ -561,6 +563,12 @@ static int fetch_with_import(struct transport *transport, } } strbuf_release(&buf); + if (auto_gc) { + const char *argv_gc_auto[] = { + "gc", "--auto", "--quiet", NULL, + }; + run_command_v_opt(argv_gc_auto, RUN_GIT_CMD); + } return 0; } From 4d9303f70201fc44e340a2c2e9d75c12ffcc6dbf Mon Sep 17 00:00:00 2001 From: Jeff Hostetler Date: Thu, 15 Dec 2016 11:34:39 -0500 Subject: [PATCH 07/98] diffcore-rename: speed up register_rename_src Teach register_rename_src() to see if new file pair can simply be appended to the rename_src[] array before performing the binary search to find the proper insertion point. This is a performance optimization. This routine is called during run_diff_files in status and the caller is iterating over the sorted index, so we should expect to be able to append in the normal case. The existing insert logic is preserved so we don't have to assume that, but simply take advantage of it if possible. Signed-off-by: Jeff Hostetler --- diffcore-rename.c | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/diffcore-rename.c b/diffcore-rename.c index 531d7adeaf..36c1883e49 100644 --- a/diffcore-rename.c +++ b/diffcore-rename.c @@ -82,6 +82,18 @@ static struct diff_rename_src *register_rename_src(struct diff_filepair *p) first = 0; last = rename_src_nr; + + if (last > 0) { + struct diff_rename_src *src = &(rename_src[last-1]); + int cmp = strcmp(one->path, src->p->one->path); + if (!cmp) + return src; + if (cmp > 0) { + first = last; + goto append_it; + } + } + while (last > first) { int next = first + ((last - first) >> 1); struct diff_rename_src *src = &(rename_src[next]); @@ -95,6 +107,7 @@ static struct diff_rename_src *register_rename_src(struct diff_filepair *p) first = next+1; } +append_it: /* insert to make it at "first" */ ALLOC_GROW(rename_src, rename_src_nr + 1, rename_src_alloc); rename_src_nr++; From beb1b6047abb7992a151c8a2de9c161a17de76ce Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 18 Apr 2017 12:38:30 +0200 Subject: [PATCH 08/98] mingw: allow absolute paths without drive prefix When specifying an absolute path without a drive prefix, we convert that path internally. Let's make sure that we handle that case properly, too ;-) This fixes the command git clone https://github.com/git-for-windows/git \G4W Signed-off-by: Johannes Schindelin --- compat/mingw.c | 10 +++++++++- t/t5580-clone-push-unc.sh | 2 +- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/compat/mingw.c b/compat/mingw.c index fe609239dd..a4c55c0466 100644 --- a/compat/mingw.c +++ b/compat/mingw.c @@ -931,11 +931,19 @@ unsigned int sleep (unsigned int seconds) char *mingw_mktemp(char *template) { wchar_t wtemplate[MAX_PATH]; + int offset = 0; + if (xutftowcs_path(wtemplate, template) < 0) return NULL; + + if (is_dir_sep(template[0]) && !is_dir_sep(template[1]) && + iswalpha(wtemplate[0]) && wtemplate[1] == L':') { + /* We have an absolute path missing the drive prefix */ + offset = 2; + } if (!_wmktemp(wtemplate)) return NULL; - if (xwcstoutf(template, wtemplate, strlen(template) + 1) < 0) + if (xwcstoutf(template, wtemplate + offset, strlen(template) + 1) < 0) return NULL; return template; } diff --git a/t/t5580-clone-push-unc.sh b/t/t5580-clone-push-unc.sh index 91db6cee97..d9fe012de4 100755 --- a/t/t5580-clone-push-unc.sh +++ b/t/t5580-clone-push-unc.sh @@ -29,7 +29,7 @@ case "$UNCPATH" in ;; esac -test_expect_failure 'clone into absolute path lacking a drive prefix' ' +test_expect_success 'clone into absolute path lacking a drive prefix' ' USINGBACKSLASHES="$(echo "$WITHOUTDRIVE"/without-drive-prefix | tr / \\\\)" && git clone . "$USINGBACKSLASHES" && From ae0d04ce080a8b49d2f3eb4f77764dae5ed8686f Mon Sep 17 00:00:00 2001 From: Rohit Ashiwal Date: Fri, 15 Feb 2019 19:03:57 +0530 Subject: [PATCH 09/98] archive: replace write_or_die() calls with write_block_or_die() MinGit for Windows comes without `gzip` bundled inside, git-archive uses `gzip -cn` to compress tar files but for this to work, gzip needs to be present on the host system. In the next commit, we will change the gzip compression so that we no longer spawn `gzip` but let zlib perform the compression in the same process instead. In preparation for this, we consolidate all the block writes into a single function. This closes https://github.com/git-for-windows/git/issues/1970 Signed-off-by: Rohit Ashiwal Signed-off-by: Johannes Schindelin --- archive-tar.c | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/archive-tar.c b/archive-tar.c index e16d3f756d..3bc0c8ebce 100644 --- a/archive-tar.c +++ b/archive-tar.c @@ -17,6 +17,8 @@ static unsigned long offset; static int tar_umask = 002; +static gzFile gzip; + static int write_tar_filter_archive(const struct archiver *ar, struct archiver_args *args); @@ -38,11 +40,21 @@ static int write_tar_filter_archive(const struct archiver *ar, #define USTAR_MAX_MTIME 077777777777ULL #endif +/* writes out the whole block, or dies if fails */ +static void write_block_or_die(const char *block) { + if (gzip) { + if (gzwrite(gzip, block, (unsigned) BLOCKSIZE) != BLOCKSIZE) + die(_("gzwrite failed")); + } else { + write_or_die(1, block, BLOCKSIZE); + } +} + /* writes out the whole block, but only if it is full */ static void write_if_needed(void) { if (offset == BLOCKSIZE) { - write_or_die(1, block, BLOCKSIZE); + write_block_or_die(block); offset = 0; } } @@ -66,7 +78,7 @@ static void do_write_blocked(const void *data, unsigned long size) write_if_needed(); } while (size >= BLOCKSIZE) { - write_or_die(1, buf, BLOCKSIZE); + write_block_or_die(buf); size -= BLOCKSIZE; buf += BLOCKSIZE; } @@ -101,10 +113,10 @@ static void write_trailer(void) { int tail = BLOCKSIZE - offset; memset(block + offset, 0, tail); - write_or_die(1, block, BLOCKSIZE); + write_block_or_die(block); if (tail < 2 * RECORDSIZE) { memset(block, 0, offset); - write_or_die(1, block, BLOCKSIZE); + write_block_or_die(block); } } From 50f32210c75e5c119879e29c3d21ae374279ed66 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 7 Mar 2019 13:05:42 +0100 Subject: [PATCH 10/98] Start to implement a built-in version of `git add --interactive` This is hardly the first conversion of a Git command that is implemented as a script to a built-in. So far, the most successful strategy for such conversions has been to add a built-in helper and call that for more and more functionality from the script, as more and more parts are converted. With the interactive add, we choose a different strategy. The sole reason for this is that on Windows (where such a conversion has the most benefits in terms of speed and robustness) 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`. And that just does not work for us here, as it would stop the main loop as soon as any interactive command was performed by the helper. Which is almost all of the commands in `git add -i`. It is almost as if Perl told us once again that it does not want us to use it on Windows. Instead, we follow the opposite route where we start with a bare-bones version of the built-in interactive add, guarded by the new `add.interactive.useBuiltin` config variable, and then add more and more functionality to it, until it is feature complete. At this point, the built-in version of `git add -i` only states that it cannot do anything yet ;-) Signed-off-by: Johannes Schindelin --- Documentation/config/add.txt | 5 +++++ Makefile | 1 + add-interactive.c | 7 +++++++ add-interactive.h | 8 ++++++++ builtin/add.c | 10 ++++++++++ t/README | 4 ++++ 6 files changed, 35 insertions(+) create mode 100644 add-interactive.c create mode 100644 add-interactive.h diff --git a/Documentation/config/add.txt b/Documentation/config/add.txt index 4d753f006e..c9f748f81c 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 58b92af54b..6c4a1e0ee5 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 0000000000..482e458dc6 --- /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 0000000000..7043b8741d --- /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 dd18e5c9b6..4f625691b5 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,14 @@ 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 (use_builtin_add_i < 0) + git_config_get_bool("add.interactive.usebuiltin", + &use_builtin_add_i); + + if (use_builtin_add_i == 1 && !patch_mode) + return !!run_add_i(the_repository, pathspec); argv_array_push(&argv, "add--interactive"); if (patch_mode) @@ -319,6 +328,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 60d5b77bcc..5132ec83f8 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 From e5fd203785abc56055ae02e2ea2950cc74122dd6 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 13 Jul 2017 14:28:42 +0200 Subject: [PATCH 11/98] t5580: test cloning without file://, test fetching via UNC paths It gets a bit silly to add the commands to the name of the test script, so let's just rename it while we're testing more UNC stuff. Signed-off-by: Johannes Schindelin --- t/{t5580-clone-push-unc.sh => t5580-unc-paths.sh} | 12 ++++++++++++ 1 file changed, 12 insertions(+) rename t/{t5580-clone-push-unc.sh => t5580-unc-paths.sh} (89%) diff --git a/t/t5580-clone-push-unc.sh b/t/t5580-unc-paths.sh similarity index 89% rename from t/t5580-clone-push-unc.sh rename to t/t5580-unc-paths.sh index b3c8a92450..2057f9e7cc 100755 --- a/t/t5580-clone-push-unc.sh +++ b/t/t5580-unc-paths.sh @@ -40,11 +40,23 @@ test_expect_success clone ' git clone "file://$UNCPATH" clone ' +test_expect_success 'clone without file://' ' + git clone "$UNCPATH" clone-without-file +' + test_expect_success 'clone with backslashed path' ' BACKSLASHED="$(echo "$UNCPATH" | tr / \\\\)" && git clone "$BACKSLASHED" backslashed ' +test_expect_success fetch ' + git init to-fetch && + ( + cd to-fetch && + git fetch "$UNCPATH" master + ) +' + test_expect_success push ' ( cd clone && From 93a179f296f7a9e7ec228a2ad4c74f8e95979700 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 11 Dec 2015 06:59:13 +0100 Subject: [PATCH 12/98] mingw: handle absolute paths in expand_user_path() On Windows, an absolute POSIX path needs to be turned into a Windows one. Signed-off-by: Johannes Schindelin --- path.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/path.c b/path.c index e3da1f3c4e..96852dbfeb 100644 --- a/path.c +++ b/path.c @@ -11,6 +11,7 @@ #include "path.h" #include "packfile.h" #include "object-store.h" +#include "exec-cmd.h" static int get_st_mode_bits(const char *path, int *mode) { @@ -714,6 +715,10 @@ char *expand_user_path(const char *path, int real_home) if (path == NULL) goto return_null; +#ifdef __MINGW32__ + if (path[0] == '/') + return system_path(path + 1); +#endif if (path[0] == '~') { const char *first_slash = strchrnul(path, '/'); const char *username = path + 1; From d05867c49126d44d654feaa2e09631bf94ab2649 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 6 Nov 2018 18:01:55 +0100 Subject: [PATCH 13/98] mingw: add a helper function to attach GDB to the current process When debugging Git, the criss-cross spawning of processes can make things quite a bit difficult, especially when a Unix shell script is thrown in the mix that calls a `git.exe` that then segfaults. To help debugging such things, we introduce the `open_in_gdb()` function which can be called at a code location where the segfault happens (or as close as one can get); This will open a new MinTTY window with a GDB that already attached to the current process. Inspired by Derrick Stolee. Signed-off-by: Johannes Schindelin --- compat/mingw.c | 13 +++++++++++++ compat/mingw.h | 10 ++++++++++ 2 files changed, 23 insertions(+) diff --git a/compat/mingw.c b/compat/mingw.c index fe609239dd..5265367812 100644 --- a/compat/mingw.c +++ b/compat/mingw.c @@ -13,6 +13,19 @@ static const int delay[] = { 0, 1, 10, 20, 40 }; +void open_in_gdb(void) +{ + static struct child_process cp = CHILD_PROCESS_INIT; + extern char *_pgmptr; + + argv_array_pushl(&cp.args, "mintty", "gdb", NULL); + argv_array_pushf(&cp.args, "--pid=%d", getpid()); + cp.clean_on_exit = 1; + if (start_command(&cp) < 0) + die_errno("Could not start gdb"); + sleep(1); +} + int err_win_to_posix(DWORD winerr) { int error = ENOSYS; diff --git a/compat/mingw.h b/compat/mingw.h index 9ad204c57c..c7ee778f9b 100644 --- a/compat/mingw.h +++ b/compat/mingw.h @@ -577,6 +577,16 @@ extern CRITICAL_SECTION pinfo_cs; int wmain(int argc, const wchar_t **w_argv); int main(int argc, const char **argv); +/* + * For debugging: if a problem occurs, say, in a Git process that is spawned + * from another Git process which in turn is spawned from yet another Git + * process, it can be quite daunting to figure out what is going on. + * + * Call this function to open a new MinTTY (this assumes you are in Git for + * Windows' SDK) with a GDB that attaches to the current process right away. + */ +extern void open_in_gdb(void); + /* * Used by Pthread API implementation for Windows */ From 6ee329f6a9fee658e7d89c0ba09b42631717bf00 Mon Sep 17 00:00:00 2001 From: Rohit Ashiwal Date: Tue, 19 Feb 2019 22:28:41 +0530 Subject: [PATCH 14/98] archive: avoid spawning `gzip` As we already link to the zlib library, we can perform the compression without even requiring gzip on the host machine. Signed-off-by: Rohit Ashiwal Signed-off-by: Johannes Schindelin --- archive-tar.c | 34 +++++++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/archive-tar.c b/archive-tar.c index 3bc0c8ebce..a8fafbec64 100644 --- a/archive-tar.c +++ b/archive-tar.c @@ -473,18 +473,34 @@ static int write_tar_filter_archive(const struct archiver *ar, filter.use_shell = 1; filter.in = -1; - if (start_command(&filter) < 0) - die_errno(_("unable to start '%s' filter"), argv[0]); - close(1); - if (dup2(filter.in, 1) < 0) - die_errno(_("unable to redirect descriptor")); - close(filter.in); + if (!strcmp("gzip -cn", ar->data)) { + char outmode[4] = "wb\0"; + + if (args->compression_level >= 0 && args->compression_level <= 9) + outmode[2] = '0' + args->compression_level; + + gzip = gzdopen(fileno(stdout), outmode); + if (!gzip) + die(_("Could not gzdopen stdout")); + } else { + if (start_command(&filter) < 0) + die_errno(_("unable to start '%s' filter"), argv[0]); + close(1); + if (dup2(filter.in, 1) < 0) + die_errno(_("unable to redirect descriptor")); + close(filter.in); + } r = write_tar_archive(ar, args); - close(1); - if (finish_command(&filter) != 0) - die(_("'%s' filter reported error"), argv[0]); + if (gzip) { + if (gzclose(gzip) != Z_OK) + die(_("gzclose failed")); + } else { + close(1); + if (finish_command(&filter) != 0) + die(_("'%s' filter reported error"), argv[0]); + } strbuf_release(&cmd); return r; From 9fbca294e3e86d290dccebcbed0d90017d5e203b Mon Sep 17 00:00:00 2001 From: Daniel Ferreira Date: Tue, 16 May 2017 01:00:31 -0300 Subject: [PATCH 15/98] 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 --- diff.c | 39 ++++++++++++++++----------------------- diff.h | 20 ++++++++++++++++++++ 2 files changed, 36 insertions(+), 23 deletions(-) diff --git a/diff.c b/diff.c index afe4400a60..5703a9b78f 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 7f8f024feb..d986ddc3b5 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 From 008477c7e1057da9dbb247fdcb5210c8c80867b4 Mon Sep 17 00:00:00 2001 From: Daniel Ferreira Date: Tue, 16 May 2017 01:00:32 -0300 Subject: [PATCH 16/98] 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 --- add-interactive.c | 251 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 250 insertions(+), 1 deletion(-) diff --git a/add-interactive.c b/add-interactive.c index 482e458dc6..aa35184d87 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; } From cfc303a3e88de1c735291b3fe148739a2e16ff49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Slavica=20=C4=90uki=C4=87?= Date: Sun, 5 May 2019 23:10:52 +0200 Subject: [PATCH 17/98] 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 --- add-interactive.c | 41 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/add-interactive.c b/add-interactive.c index aa35184d87..174e07ce83 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; From 1e6623828257943f38582d9e4c13bb7608a05ed6 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 7 Mar 2019 13:05:42 +0100 Subject: [PATCH 18/98] 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. Apart from the "and choose" part, there are more differences between the way the `status` command calls the `list_and_choose()` function in the Perl version of `git add -i` compared to the other callers of said function. The most important ones: - The list is not only shown, but the user is also asked to make a choice, possibly selecting multiple entries. - The list of items is prefixed with a marker indicating what items have been selected, if multi-selection is allowed. - Initially, for each item a unique prefix (if there exists any within the given parameters) is determined, and shown in the list, and accepted as a shortcut for the selection. These features will be implemented later, except the part where the user can choose a command. At this stage, though, the built-in `git add -i` still only supports the `status` command, with the remaining commands to follow over the course of the next commits. In addition, we also modify `list()` to support displaying the commands in columns, even if there is currently only one. The Perl script `git-add--interactive.perl` mixed the purposes of the "list" and the "and choose" part into the same function. In the C version, we will keep them separate instead, calling the `list()` function from the `list_and_choose()` function. 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 --- add-interactive.c | 137 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 134 insertions(+), 3 deletions(-) diff --git a/add-interactive.c b/add-interactive.c index 174e07ce83..c6f7fbad36 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,96 @@ 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); - putchar('\n'); + + 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, *endp; + + 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)) { + index = strtoul(p, &endp, 10) - 1; + if (endp != p + sep) + index = -1; + } + + p[sep] = '\0'; + if (index < 0 || index >= items->nr) + printf(_("Huh (%s)?\n"), p); + else { + res = index; + break; + } + + p += sep + 1; + } + + if (res != LIST_AND_CHOOSE_ERROR) + break; + } + + strbuf_release(&input); + return res; } struct adddel { @@ -252,20 +341,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 +396,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; } From ee39564ee8d959c74038e798c703a3d15bcc50a2 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 18 Oct 2019 14:08:41 +0200 Subject: [PATCH 19/98] 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 --- add-interactive.c | 188 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 177 insertions(+), 11 deletions(-) diff --git a/add-interactive.c b/add-interactive.c index c6f7fbad36..eb559555ad 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, *endp; strbuf_reset(&input); - list(s, items, &opts->list_opts); + list(s, &items->items, &opts->list_opts); printf("%s%s", opts->prompt, "> "); fflush(stdout); @@ -140,7 +269,10 @@ static ssize_t list_and_choose(struct add_i_state *s, struct string_list *items, } 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; @@ -306,6 +438,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; @@ -345,10 +494,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) @@ -364,7 +526,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 @@ -377,9 +539,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); @@ -404,8 +569,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); } } @@ -414,7 +580,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; } From 16459c7079937ecbe7af11fe593b6402cbc981b4 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Wed, 6 Mar 2019 00:06:57 +0100 Subject: [PATCH 20/98] 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 --- add-interactive.c | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/add-interactive.c b/add-interactive.c index eb559555ad..d96e18fce5 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,"); @@ -513,12 +521,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; From b39de6551ffc6da657d7ca01c747cb94191ba30d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Slavica=20=C4=90uki=C4=87?= Date: Sun, 3 Mar 2019 13:19:27 +0100 Subject: [PATCH 21/98] 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 --- add-interactive.c | 35 +++++++++++++++++++++++++++++------ 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/add-interactive.c b/add-interactive.c index d96e18fce5..64d84ba1dc 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) { @@ -281,7 +288,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; @@ -507,18 +515,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) @@ -536,8 +549,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 { @@ -568,6 +582,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")); From c5b960a0ead3902faaebb99492caba7ed546352f Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 5 Mar 2019 23:33:55 +0100 Subject: [PATCH 22/98] built-in add -i: allow filtering the modified files list In the `update` command of `git add -i`, we are primarily interested in the list of modified files that have worktree (i.e. unstaged) changes. At the same time, we need to determine _also_ the staged changes, to be able to produce the full added/deleted information. The Perl script version of `git add -i` has a parameter of the `list_modified()` function for that matter. In C, we can be a lot more precise, using an `enum`. The C implementation of the filter also has an easier time to avoid unnecessary work, simply by using an adaptive order of the `diff-index` and `diff-files` phases, and then skipping files in the second phase when they have not been seen in the first phase. Signed-off-by: Johannes Schindelin --- add-interactive.c | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/add-interactive.c b/add-interactive.c index 4e5241e865..97f816965e 100644 --- a/add-interactive.c +++ b/add-interactive.c @@ -346,6 +346,7 @@ struct collection_status { const char *reference; + unsigned skip_unseen:1; struct string_list *files; struct hashmap file_map; }; @@ -373,6 +374,9 @@ static void collect_changes_cb(struct diff_queue_struct *q, entry = hashmap_get_entry_from_hash(&s->file_map, hash, name, struct pathname_entry, ent); if (!entry) { + if (s->skip_unseen) + continue; + add_file_item(s->files, name); entry = xcalloc(sizeof(*entry), 1); @@ -394,13 +398,22 @@ static void collect_changes_cb(struct diff_queue_struct *q, free_diffstat_info(&stat); } -static int get_modified_files(struct repository *r, struct string_list *files, +enum modified_files_filter { + NO_FILTER = 0, + WORKTREE_ONLY = 1, + INDEX_ONLY = 2, +}; + +static int get_modified_files(struct repository *r, + enum modified_files_filter filter, + struct 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 }; + int i; if (discard_index(r->index) < 0 || repo_read_index_preload(r, ps, 0) < 0) @@ -410,10 +423,16 @@ static int get_modified_files(struct repository *r, struct string_list *files, s.files = files; hashmap_init(&s.file_map, pathname_entry_cmp, NULL, 0); - for (s.phase = FROM_WORKTREE; s.phase <= FROM_INDEX; s.phase++) { + for (i = 0; i < 2; i++) { struct rev_info rev; struct setup_revision_opt opt = { 0 }; + if (filter == INDEX_ONLY) + s.phase = i ? FROM_WORKTREE : FROM_INDEX; + else + s.phase = i ? FROM_INDEX : FROM_WORKTREE; + s.skip_unseen = filter && i; + opt.def = is_initial ? empty_tree_oid_hex() : oid_to_hex(&head_oid); @@ -497,7 +516,7 @@ static void print_file_item(int i, struct string_list_item *item, 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) + if (get_modified_files(s->r, NO_FILTER, files, ps) < 0) return -1; list(s, files, opts); From add05aef5175419d5636338a76d3c5f092ebe9e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Slavica=20=C4=90uki=C4=87?= Date: Thu, 14 Feb 2019 11:41:46 +0100 Subject: [PATCH 23/98] 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 --- add-interactive.c | 21 +++++++++++++++++++++ t/t3701-add-interactive.sh | 25 +++++++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/add-interactive.c b/add-interactive.c index 64d84ba1dc..4e5241e865 100644 --- a/add-interactive.c +++ b/add-interactive.c @@ -506,6 +506,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); @@ -559,6 +579,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 d50e165ca8..d4f9386621 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 From e8f70db6ad91a2edcacfecebba0b5a896e49da7c Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 7 Mar 2019 21:36:11 +0100 Subject: [PATCH 24/98] built-in add -i: prepare for multi-selection commands The `upgrade`, `revert` and `add-untracked` commands allow selecting multiple entries. Let's extend the `list_and_choose()` function to accommodate those use cases. Signed-off-by: Johannes Schindelin --- add-interactive.c | 106 +++++++++++++++++++++++++++++++++++----------- 1 file changed, 81 insertions(+), 25 deletions(-) diff --git a/add-interactive.c b/add-interactive.c index 97f816965e..a8958e9f4e 100644 --- a/add-interactive.c +++ b/add-interactive.c @@ -72,15 +72,17 @@ static void init_add_i_state(struct add_i_state *s, struct repository *r) struct prefix_item_list { struct string_list items; struct string_list sorted; + int *selected; /* for multi-selections */ size_t min_length, max_length; }; #define PREFIX_ITEM_LIST_INIT \ - { STRING_LIST_INIT_DUP, STRING_LIST_INIT_NODUP, 1, 4 } + { STRING_LIST_INIT_DUP, STRING_LIST_INIT_NODUP, NULL, 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); + FREE_AND_NULL(list->selected); } static void extend_prefix_length(struct string_list_item *p, @@ -182,11 +184,12 @@ static ssize_t find_unique(const char *string, struct prefix_item_list *list) 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)(int i, int selected, struct string_list_item *item, + void *print_item_data); void *print_item_data; }; -static void list(struct add_i_state *s, struct string_list *list, +static void list(struct add_i_state *s, struct string_list *list, int *selected, struct list_options *opts) { int i, last_lf = 0; @@ -199,7 +202,8 @@ static void list(struct add_i_state *s, struct string_list *list, "%s", opts->header); for (i = 0; i < list->nr; i++) { - opts->print_item(i, list->items + i, opts->print_item_data); + opts->print_item(i, selected ? selected[i] : 0, list->items + i, + opts->print_item_data); if ((opts->columns) && ((i + 1) % (opts->columns))) { putchar('\t'); @@ -218,6 +222,10 @@ struct list_and_choose_options { struct list_options list_opts; const char *prompt; + enum { + SINGLETON = (1<<0), + IMMEDIATE = (1<<1), + } flags; void (*print_help)(struct add_i_state *s); }; @@ -225,7 +233,8 @@ struct list_and_choose_options { #define LIST_AND_CHOOSE_QUIT (-2) /* - * Returns the selected index. + * Returns the selected index in singleton mode, the number of selected items + * otherwise. * * If an error occurred, returns `LIST_AND_CHOOSE_ERROR`. Upon EOF, * `LIST_AND_CHOOSE_QUIT` is returned. @@ -234,8 +243,19 @@ static ssize_t list_and_choose(struct add_i_state *s, struct prefix_item_list *items, struct list_and_choose_options *opts) { + int singleton = opts->flags & SINGLETON; + int immediate = opts->flags & IMMEDIATE; + struct strbuf input = STRBUF_INIT; - ssize_t res = LIST_AND_CHOOSE_ERROR; + ssize_t res = singleton ? LIST_AND_CHOOSE_ERROR : 0; + + if (!singleton) { + free(items->selected); + CALLOC_ARRAY(items->selected, items->items.nr); + } + + if (singleton && !immediate) + BUG("singleton requires immediate"); find_unique_prefixes(items); @@ -244,15 +264,16 @@ static ssize_t list_and_choose(struct add_i_state *s, strbuf_reset(&input); - list(s, &items->items, &opts->list_opts); + list(s, &items->items, items->selected, &opts->list_opts); color_fprintf(stdout, s->prompt_color, "%s", opts->prompt); - fputs("> ", stdout); + fputs(singleton ? "> " : ">> ", stdout); fflush(stdout); if (strbuf_getline(&input, stdin) == EOF) { putchar('\n'); - res = LIST_AND_CHOOSE_QUIT; + if (immediate) + res = LIST_AND_CHOOSE_QUIT; break; } strbuf_trim(&input); @@ -268,7 +289,9 @@ static ssize_t list_and_choose(struct add_i_state *s, p = input.buf; for (;;) { size_t sep = strcspn(p, " \t\r\n,"); - ssize_t index = -1; + int choose = 1; + /* `from` is inclusive, `to` is exclusive */ + ssize_t from = -1, to = -1; if (!sep) { if (!*p) @@ -277,28 +300,60 @@ static ssize_t list_and_choose(struct add_i_state *s, continue; } - if (isdigit(*p)) { - index = strtoul(p, &endp, 10) - 1; - if (endp != p + sep) - index = -1; + /* Input that begins with '-'; de-select */ + if (*p == '-') { + choose = 0; + p++; + sep--; + } + + if (sep == 1 && *p == '*') { + from = 0; + to = items->items.nr; + } else if (isdigit(*p)) { + /* A range can be specified like 5-7 or 5-. */ + from = strtoul(p, &endp, 10) - 1; + if (endp == p + sep) + to = from + 1; + else if (*endp == '-') { + to = strtoul(++endp, &endp, 10); + /* extra characters after the range? */ + if (endp != p + sep) + from = -1; + } } p[sep] = '\0'; - if (index < 0) - index = find_unique(p, items); + if (from < 0) { + from = find_unique(p, items); + if (from >= 0) + to = from + 1; + } - if (index < 0 || index >= items->items.nr) + if (from < 0 || from >= items->items.nr || + (singleton && from + 1 != to)) { color_fprintf_ln(stdout, s->error_color, _("Huh (%s)?"), p); - else { - res = index; + break; + } else if (singleton) { + res = from; break; } + if (to > items->items.nr) + to = items->items.nr; + + for (; from < to; from++) + if (items->selected[from] != choose) { + items->selected[from] = choose; + res += choose ? +1 : -1; + } + p += sep + 1; } - if (res != LIST_AND_CHOOSE_ERROR) + if ((immediate && res != LIST_AND_CHOOSE_ERROR) || + !strcmp(input.buf, "*")) break; } @@ -495,7 +550,7 @@ struct print_file_item_data { struct strbuf buf, index, worktree; }; -static void print_file_item(int i, struct string_list_item *item, +static void print_file_item(int i, int selected, struct string_list_item *item, void *print_file_item_data) { struct file_item *c = item->util; @@ -510,7 +565,7 @@ static void print_file_item(int i, struct string_list_item *item, strbuf_addf(&d->buf, d->modified_fmt, d->index.buf, d->worktree.buf, item->string); - printf(" %2d: %s", i + 1, d->buf.buf); + printf("%c%2d: %s", selected ? '*' : ' ', i + 1, d->buf.buf); } static int run_status(struct add_i_state *s, const struct pathspec *ps, @@ -519,7 +574,7 @@ static int run_status(struct add_i_state *s, const struct pathspec *ps, if (get_modified_files(s->r, NO_FILTER, files, ps) < 0) return -1; - list(s, files, opts); + list(s, files, NULL, opts); putchar('\n'); return 0; @@ -558,7 +613,8 @@ struct print_command_item_data { const char *color, *reset; }; -static void print_command_item(int i, struct string_list_item *item, +static void print_command_item(int i, int selected, + struct string_list_item *item, void *print_command_item_data) { struct print_command_item_data *d = print_command_item_data; @@ -591,7 +647,7 @@ int run_add_i(struct repository *r, const struct pathspec *ps) struct print_command_item_data data = { "[", "]" }; struct list_and_choose_options main_loop_opts = { { 4, N_("*** Commands ***"), print_command_item, &data }, - N_("What now"), command_prompt_help + N_("What now"), SINGLETON | IMMEDIATE, command_prompt_help }; struct { const char *string; From f7b3b9bcaaf405c57e162ae14ae7ad4db5745db9 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 5 Mar 2019 23:58:49 +0100 Subject: [PATCH 25/98] built-in add -i: implement the `update` command After `status` and `help`, it is now turn to port the `update` command to C, the second command that is shown in the main loop menu of `git add -i`. This `git add -i` command is the first one which lets the user choose a subset of a list of files, and as such, this patch lays the groundwork for the other commands of that category: - It teaches the `print_file_item()` function to show a unique prefix if we found any (the code to find it had been added already in the previous patch where we colored the unique prefixes of the main loop commands, but that patch uses the `print_command_item()` function to display the menu items). - This patch also adds the help text that is shown when the user input to select items from the shown list could not be parsed. - As `get_modified_files()` clears the list of files, it now has to take care of clearing the _full_ `prefix_item_list` lest the `sorted` and `selected` fields go stale and inconsistent. Signed-off-by: Johannes Schindelin --- add-interactive.c | 130 +++++++++++++++++++++++++++++++++++++++------- 1 file changed, 110 insertions(+), 20 deletions(-) diff --git a/add-interactive.c b/add-interactive.c index a8958e9f4e..78fbb5be91 100644 --- a/add-interactive.c +++ b/add-interactive.c @@ -6,6 +6,7 @@ #include "revision.h" #include "refs.h" #include "string-list.h" +#include "lockfile.h" struct add_i_state { struct repository *r; @@ -367,6 +368,7 @@ struct adddel { }; struct file_item { + size_t prefix_length; struct adddel index, worktree; }; @@ -461,7 +463,7 @@ enum modified_files_filter { static int get_modified_files(struct repository *r, enum modified_files_filter filter, - struct string_list *files, + struct prefix_item_list *files, const struct pathspec *ps) { struct object_id head_oid; @@ -474,8 +476,8 @@ static int get_modified_files(struct repository *r, repo_read_index_preload(r, ps, 0) < 0) return error(_("could not read index")); - string_list_clear(files, 1); - s.files = files; + prefix_item_list_clear(files); + s.files = &files->items; hashmap_init(&s.file_map, pathname_entry_cmp, NULL, 0); for (i = 0; i < 2; i++) { @@ -511,7 +513,7 @@ static int get_modified_files(struct repository *r, hashmap_free_entries(&s.file_map, struct pathname_entry, ent); /* While the diffs are ordered already, we ran *two* diffs... */ - string_list_sort(files); + string_list_sort(&files->items); return 0; } @@ -546,8 +548,8 @@ static int is_valid_prefix(const char *prefix, size_t prefix_len) } struct print_file_item_data { - const char *modified_fmt; - struct strbuf buf, index, worktree; + const char *modified_fmt, *color, *reset; + struct strbuf buf, name, index, worktree; }; static void print_file_item(int i, int selected, struct string_list_item *item, @@ -555,34 +557,96 @@ static void print_file_item(int i, int selected, struct string_list_item *item, { struct file_item *c = item->util; struct print_file_item_data *d = print_file_item_data; + const char *highlighted = NULL; strbuf_reset(&d->index); strbuf_reset(&d->worktree); strbuf_reset(&d->buf); + /* Format the item with the prefix highlighted. */ + if (c->prefix_length > 0 && + is_valid_prefix(item->string, c->prefix_length)) { + strbuf_reset(&d->name); + strbuf_addf(&d->name, "%s%.*s%s%s", d->color, + (int)c->prefix_length, item->string, d->reset, + item->string + c->prefix_length); + highlighted = d->name.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); + + strbuf_addf(&d->buf, d->modified_fmt, d->index.buf, d->worktree.buf, + highlighted ? highlighted : item->string); printf("%c%2d: %s", selected ? '*' : ' ', 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) + struct prefix_item_list *files, + struct list_and_choose_options *opts) { if (get_modified_files(s->r, NO_FILTER, files, ps) < 0) return -1; - list(s, files, NULL, opts); + list(s, &files->items, NULL, &opts->list_opts); putchar('\n'); return 0; } +static int run_update(struct add_i_state *s, const struct pathspec *ps, + struct prefix_item_list *files, + struct list_and_choose_options *opts) +{ + int res = 0, fd; + size_t count, i; + struct lock_file index_lock; + + if (get_modified_files(s->r, WORKTREE_ONLY, files, ps) < 0) + return -1; + + if (!files->items.nr) { + putchar('\n'); + return 0; + } + + opts->prompt = N_("Update"); + count = list_and_choose(s, files, opts); + if (count <= 0) { + putchar('\n'); + return 0; + } + + fd = repo_hold_locked_index(s->r, &index_lock, LOCK_REPORT_ON_ERROR); + if (fd < 0) { + putchar('\n'); + return -1; + } + + for (i = 0; i < files->items.nr; i++) { + const char *name = files->items.items[i].string; + if (files->selected[i] && + add_file_to_index(s->r->index, name, 0) < 0) { + res = error(_("could not stage '%s'"), name); + break; + } + } + + if (!res && write_locked_index(s->r->index, &index_lock, COMMIT_LOCK) < 0) + res = error(_("could not write index")); + + if (!res) + printf(Q_("updated %d path\n", + "updated %d paths\n", count), (int)count); + + putchar('\n'); + return res; +} + static int run_help(struct add_i_state *s, const struct pathspec *unused_ps, - struct string_list *unused_files, - struct list_options *unused_opts) + struct prefix_item_list *unused_files, + struct list_and_choose_options *opts) { color_fprintf_ln(stdout, s->help_color, "status - %s", _("show paths with changes")); @@ -600,9 +664,29 @@ static int run_help(struct add_i_state *s, const struct pathspec *unused_ps, return 0; } +static void choose_prompt_help(struct add_i_state *s) +{ + color_fprintf_ln(stdout, s->help_color, "%s", + _("Prompt help:")); + color_fprintf_ln(stdout, s->help_color, "1 - %s", + _("select a single item")); + color_fprintf_ln(stdout, s->help_color, "3-5 - %s", + _("select a range of items")); + color_fprintf_ln(stdout, s->help_color, "2-3,6-9 - %s", + _("select multiple ranges")); + color_fprintf_ln(stdout, s->help_color, "foo - %s", + _("select item based on unique prefix")); + color_fprintf_ln(stdout, s->help_color, "-... - %s", + _("unselect specified items")); + color_fprintf_ln(stdout, s->help_color, "* - %s", + _("choose all items")); + color_fprintf_ln(stdout, s->help_color, " - %s", + _("(empty) finish selecting")); +} + typedef int (*command_t)(struct add_i_state *s, const struct pathspec *ps, - struct string_list *files, - struct list_options *opts); + struct prefix_item_list *files, + struct list_and_choose_options *opts); struct command_item { size_t prefix_length; @@ -654,18 +738,21 @@ int run_add_i(struct repository *r, const struct pathspec *ps) command_t command; } command_list[] = { { "status", run_status }, + { "update", run_update }, { "help", run_help }, }; 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 + "%12s %12s %s", NULL, NULL, + STRBUF_INIT, STRBUF_INIT, STRBUF_INIT, STRBUF_INIT }; - struct list_options opts = { - 0, NULL, print_file_item, &print_file_item_data + struct list_and_choose_options opts = { + { 0, NULL, print_file_item, &print_file_item_data }, + NULL, 0, choose_prompt_help }; struct strbuf header = STRBUF_INIT; - struct string_list files = STRING_LIST_INIT_DUP; + struct prefix_item_list files = PREFIX_ITEM_LIST_INIT; ssize_t i; int res = 0; @@ -686,11 +773,13 @@ int run_add_i(struct repository *r, const struct pathspec *ps) data.color = s.prompt_color; data.reset = s.reset_color; } + print_file_item_data.color = data.color; + print_file_item_data.reset = data.reset; strbuf_addstr(&header, " "); strbuf_addf(&header, print_file_item_data.modified_fmt, _("staged"), _("unstaged"), _("path")); - opts.header = header.buf; + opts.list_opts.header = header.buf; if (discard_index(r->index) < 0 || repo_read_index(r) < 0 || @@ -714,8 +803,9 @@ int run_add_i(struct repository *r, const struct pathspec *ps) } } - string_list_clear(&files, 1); + prefix_item_list_clear(&files); strbuf_release(&print_file_item_data.buf); + strbuf_release(&print_file_item_data.name); strbuf_release(&print_file_item_data.index); strbuf_release(&print_file_item_data.worktree); strbuf_release(&header); From 33ebf58b808be51111a4cbf2497e4022777536e7 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Wed, 6 Mar 2019 23:06:13 +0100 Subject: [PATCH 26/98] built-in add -i: re-implement `revert` in C This is a relatively straight-forward port from the Perl version, with the notable exception that we imitate `git reset -- ` in the C version rather than the convoluted `git ls-tree HEAD -- | git update-index --index-info` followed by `git update-index --force-remove -- ` for the missed ones. While at it, we fix the pretty obvious bug where the `revert` command offers to unstage files that do not have staged changes. Signed-off-by: Johannes Schindelin --- add-interactive.c | 109 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) diff --git a/add-interactive.c b/add-interactive.c index 78fbb5be91..53e3439b65 100644 --- a/add-interactive.c +++ b/add-interactive.c @@ -644,6 +644,114 @@ static int run_update(struct add_i_state *s, const struct pathspec *ps, return res; } +static void revert_from_diff(struct diff_queue_struct *q, + struct diff_options *opt, void *data) +{ + int i, add_flags = ADD_CACHE_OK_TO_ADD | ADD_CACHE_OK_TO_REPLACE; + + for (i = 0; i < q->nr; i++) { + struct diff_filespec *one = q->queue[i]->one; + struct cache_entry *ce; + + if (!(one->mode && !is_null_oid(&one->oid))) { + remove_file_from_index(opt->repo->index, one->path); + printf(_("note: %s is untracked now.\n"), one->path); + } else { + ce = make_cache_entry(opt->repo->index, one->mode, + &one->oid, one->path, 0, 0); + if (!ce) + die(_("make_cache_entry failed for path '%s'"), + one->path); + add_index_entry(opt->repo->index, ce, add_flags); + } + } +} + +static int run_revert(struct add_i_state *s, const struct pathspec *ps, + struct prefix_item_list *files, + struct list_and_choose_options *opts) +{ + int res = 0, fd; + size_t count, i, j; + + struct object_id oid; + int is_initial = !resolve_ref_unsafe("HEAD", RESOLVE_REF_READING, &oid, + NULL); + struct lock_file index_lock; + const char **paths; + struct tree *tree; + struct diff_options diffopt = { NULL }; + + if (get_modified_files(s->r, INDEX_ONLY, files, ps) < 0) + return -1; + + if (!files->items.nr) { + putchar('\n'); + return 0; + } + + opts->prompt = N_("Revert"); + count = list_and_choose(s, files, opts); + if (count <= 0) + goto finish_revert; + + fd = repo_hold_locked_index(s->r, &index_lock, LOCK_REPORT_ON_ERROR); + if (fd < 0) { + res = -1; + goto finish_revert; + } + + if (is_initial) + oidcpy(&oid, s->r->hash_algo->empty_tree); + else { + tree = parse_tree_indirect(&oid); + if (!tree) { + res = error(_("Could not parse HEAD^{tree}")); + goto finish_revert; + } + oidcpy(&oid, &tree->object.oid); + } + + ALLOC_ARRAY(paths, count + 1); + for (i = j = 0; i < files->items.nr; i++) + if (files->selected[i]) + paths[j++] = files->items.items[i].string; + paths[j] = NULL; + + parse_pathspec(&diffopt.pathspec, 0, + PATHSPEC_PREFER_FULL | PATHSPEC_LITERAL_PATH, + NULL, paths); + + diffopt.output_format = DIFF_FORMAT_CALLBACK; + diffopt.format_callback = revert_from_diff; + diffopt.flags.override_submodule_config = 1; + diffopt.repo = s->r; + + if (do_diff_cache(&oid, &diffopt)) + res = -1; + else { + diffcore_std(&diffopt); + diff_flush(&diffopt); + } + free(paths); + clear_pathspec(&diffopt.pathspec); + + if (!res && write_locked_index(s->r->index, &index_lock, + COMMIT_LOCK) < 0) + res = -1; + else + res = repo_refresh_and_write_index(s->r, REFRESH_QUIET, 0, 1, + NULL, NULL, NULL); + + if (!res) + printf(Q_("reverted %d path\n", + "reverted %d paths\n", count), (int)count); + +finish_revert: + putchar('\n'); + return res; +} + static int run_help(struct add_i_state *s, const struct pathspec *unused_ps, struct prefix_item_list *unused_files, struct list_and_choose_options *opts) @@ -739,6 +847,7 @@ int run_add_i(struct repository *r, const struct pathspec *ps) } command_list[] = { { "status", run_status }, { "update", run_update }, + { "revert", run_revert }, { "help", run_help }, }; struct prefix_item_list commands = PREFIX_ITEM_LIST_INIT; From 47cd5b58fcf7b33e6b09fe451700e10e878e5bd0 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 7 Mar 2019 00:59:24 +0100 Subject: [PATCH 27/98] built-in add -i: re-implement `add-untracked` in C This is yet another command, ported to C. It builds nicely on the support functions introduced for other commands, with the notable difference that only names are displayed for untracked files, no file type or diff summary. Signed-off-by: Johannes Schindelin --- add-interactive.c | 91 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/add-interactive.c b/add-interactive.c index 53e3439b65..1183c40986 100644 --- a/add-interactive.c +++ b/add-interactive.c @@ -7,6 +7,7 @@ #include "refs.h" #include "string-list.h" #include "lockfile.h" +#include "dir.h" struct add_i_state { struct repository *r; @@ -550,6 +551,7 @@ static int is_valid_prefix(const char *prefix, size_t prefix_len) struct print_file_item_data { const char *modified_fmt, *color, *reset; struct strbuf buf, name, index, worktree; + unsigned only_names:1; }; static void print_file_item(int i, int selected, struct string_list_item *item, @@ -573,6 +575,12 @@ static void print_file_item(int i, int selected, struct string_list_item *item, highlighted = d->name.buf; } + if (d->only_names) { + printf("%c%2d: %s", selected ? '*' : ' ', i + 1, + highlighted ? highlighted : item->string); + return; + } + render_adddel(&d->worktree, &c->worktree, _("nothing")); render_adddel(&d->index, &c->index, _("unchanged")); @@ -752,6 +760,88 @@ finish_revert: return res; } +static int get_untracked_files(struct repository *r, + struct prefix_item_list *files, + const struct pathspec *ps) +{ + struct dir_struct dir = { 0 }; + size_t i; + struct strbuf buf = STRBUF_INIT; + + if (repo_read_index(r) < 0) + return error(_("could not read index")); + + prefix_item_list_clear(files); + setup_standard_excludes(&dir); + add_pattern_list(&dir, EXC_CMDL, "--exclude option"); + fill_directory(&dir, r->index, ps); + + for (i = 0; i < dir.nr; i++) { + struct dir_entry *ent = dir.entries[i]; + + if (index_name_is_other(r->index, ent->name, ent->len)) { + strbuf_reset(&buf); + strbuf_add(&buf, ent->name, ent->len); + add_file_item(&files->items, buf.buf); + } + } + + strbuf_release(&buf); + return 0; +} + +static int run_add_untracked(struct add_i_state *s, const struct pathspec *ps, + struct prefix_item_list *files, + struct list_and_choose_options *opts) +{ + struct print_file_item_data *d = opts->list_opts.print_item_data; + int res = 0, fd; + size_t count, i; + struct lock_file index_lock; + + if (get_untracked_files(s->r, files, ps) < 0) + return -1; + + if (!files->items.nr) { + printf(_("No untracked files.\n")); + goto finish_add_untracked; + } + + opts->prompt = N_("Add untracked"); + d->only_names = 1; + count = list_and_choose(s, files, opts); + d->only_names = 0; + if (count <= 0) + goto finish_add_untracked; + + fd = repo_hold_locked_index(s->r, &index_lock, LOCK_REPORT_ON_ERROR); + if (fd < 0) { + res = -1; + goto finish_add_untracked; + } + + for (i = 0; i < files->items.nr; i++) { + const char *name = files->items.items[i].string; + if (files->selected[i] && + add_file_to_index(s->r->index, name, 0) < 0) { + res = error(_("could not stage '%s'"), name); + break; + } + } + + if (!res && + write_locked_index(s->r->index, &index_lock, COMMIT_LOCK) < 0) + res = error(_("could not write index")); + + if (!res) + printf(Q_("added %d path\n", + "added %d paths\n", count), (int)count); + +finish_add_untracked: + putchar('\n'); + return res; +} + static int run_help(struct add_i_state *s, const struct pathspec *unused_ps, struct prefix_item_list *unused_files, struct list_and_choose_options *opts) @@ -848,6 +938,7 @@ int run_add_i(struct repository *r, const struct pathspec *ps) { "status", run_status }, { "update", run_update }, { "revert", run_revert }, + { "add untracked", run_add_untracked }, { "help", run_help }, }; struct prefix_item_list commands = PREFIX_ITEM_LIST_INIT; From 6f8d73f0c6bb4cdbb37d83ce75d37ff74fa80ef1 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Mon, 11 Mar 2019 10:07:49 +0100 Subject: [PATCH 28/98] built-in add -i: implement the `patch` command Well, it is not a full implementation yet. In the interest of making this easy to review (and easy to keep bugs out), we still hand off to the Perl script to do the actual work. The `patch` functionality actually makes up for more than half of the 1,800+ lines of `git-add--interactive.perl`. It will be ported from Perl to C incrementally, later. Signed-off-by: Johannes Schindelin --- add-interactive.c | 91 +++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 84 insertions(+), 7 deletions(-) diff --git a/add-interactive.c b/add-interactive.c index 1183c40986..e95dd4af35 100644 --- a/add-interactive.c +++ b/add-interactive.c @@ -8,6 +8,7 @@ #include "string-list.h" #include "lockfile.h" #include "dir.h" +#include "run-command.h" struct add_i_state { struct repository *r; @@ -365,7 +366,7 @@ static ssize_t list_and_choose(struct add_i_state *s, struct adddel { uintmax_t add, del; - unsigned seen:1, binary:1; + unsigned seen:1, unmerged:1, binary:1; }; struct file_item { @@ -405,6 +406,7 @@ struct collection_status { const char *reference; unsigned skip_unseen:1; + size_t unmerged_count, binary_count; struct string_list *files; struct hashmap file_map; }; @@ -427,7 +429,7 @@ static void collect_changes_cb(struct diff_queue_struct *q, int hash = strhash(name); struct pathname_entry *entry; struct file_item *file_item; - struct adddel *adddel; + struct adddel *adddel, *other_adddel; entry = hashmap_get_entry_from_hash(&s->file_map, hash, name, struct pathname_entry, ent); @@ -447,11 +449,21 @@ static void collect_changes_cb(struct diff_queue_struct *q, file_item = entry->item; adddel = s->phase == FROM_INDEX ? &file_item->index : &file_item->worktree; + other_adddel = s->phase == FROM_INDEX ? + &file_item->worktree : &file_item->index; adddel->seen = 1; adddel->add = stat.files[i]->added; adddel->del = stat.files[i]->deleted; - if (stat.files[i]->is_binary) + if (stat.files[i]->is_binary) { + if (!other_adddel->binary) + s->binary_count++; adddel->binary = 1; + } + if (stat.files[i]->is_unmerged) { + if (!other_adddel->unmerged) + s->unmerged_count++; + adddel->unmerged = 1; + } } free_diffstat_info(&stat); } @@ -465,7 +477,9 @@ enum modified_files_filter { static int get_modified_files(struct repository *r, enum modified_files_filter filter, struct prefix_item_list *files, - const struct pathspec *ps) + const struct pathspec *ps, + size_t *unmerged_count, + size_t *binary_count) { struct object_id head_oid; int is_initial = !resolve_ref_unsafe("HEAD", RESOLVE_REF_READING, @@ -512,6 +526,10 @@ static int get_modified_files(struct repository *r, } } hashmap_free_entries(&s.file_map, struct pathname_entry, ent); + if (unmerged_count) + *unmerged_count = s.unmerged_count; + if (binary_count) + *binary_count = s.binary_count; /* While the diffs are ordered already, we ran *two* diffs... */ string_list_sort(&files->items); @@ -594,7 +612,7 @@ static int run_status(struct add_i_state *s, const struct pathspec *ps, struct prefix_item_list *files, struct list_and_choose_options *opts) { - if (get_modified_files(s->r, NO_FILTER, files, ps) < 0) + if (get_modified_files(s->r, NO_FILTER, files, ps, NULL, NULL) < 0) return -1; list(s, &files->items, NULL, &opts->list_opts); @@ -611,7 +629,7 @@ static int run_update(struct add_i_state *s, const struct pathspec *ps, size_t count, i; struct lock_file index_lock; - if (get_modified_files(s->r, WORKTREE_ONLY, files, ps) < 0) + if (get_modified_files(s->r, WORKTREE_ONLY, files, ps, NULL, NULL) < 0) return -1; if (!files->items.nr) { @@ -690,7 +708,7 @@ static int run_revert(struct add_i_state *s, const struct pathspec *ps, struct tree *tree; struct diff_options diffopt = { NULL }; - if (get_modified_files(s->r, INDEX_ONLY, files, ps) < 0) + if (get_modified_files(s->r, INDEX_ONLY, files, ps, NULL, NULL) < 0) return -1; if (!files->items.nr) { @@ -842,6 +860,64 @@ finish_add_untracked: return res; } +static int run_patch(struct add_i_state *s, const struct pathspec *ps, + struct prefix_item_list *files, + struct list_and_choose_options *opts) +{ + int res = 0; + ssize_t count, i, j; + size_t unmerged_count = 0, binary_count = 0; + + if (get_modified_files(s->r, WORKTREE_ONLY, files, ps, + &unmerged_count, &binary_count) < 0) + return -1; + + if (unmerged_count || binary_count) { + for (i = j = 0; i < files->items.nr; i++) { + struct file_item *item = files->items.items[i].util; + + if (item->index.binary || item->worktree.binary) { + free(item); + free(files->items.items[i].string); + } else if (item->index.unmerged || + item->worktree.unmerged) { + color_fprintf_ln(stderr, s->error_color, + _("ignoring unmerged: %s"), + files->items.items[i].string); + free(item); + free(files->items.items[i].string); + } else + files->items.items[j++] = files->items.items[i]; + } + files->items.nr = j; + } + + if (!files->items.nr) { + if (binary_count) + fprintf(stderr, _("Only binary files changed.\n")); + else + fprintf(stderr, _("No changes.\n")); + return 0; + } + + opts->prompt = N_("Patch update"); + count = list_and_choose(s, files, opts); + if (count >= 0) { + struct argv_array args = ARGV_ARRAY_INIT; + + argv_array_pushl(&args, "git", "add--interactive", "--patch", + "--", NULL); + for (i = 0; i < files->items.nr; i++) + if (files->selected[i]) + argv_array_push(&args, + files->items.items[i].string); + res = run_command_v_opt(args.argv, 0); + argv_array_clear(&args); + } + + return res; +} + static int run_help(struct add_i_state *s, const struct pathspec *unused_ps, struct prefix_item_list *unused_files, struct list_and_choose_options *opts) @@ -939,6 +1015,7 @@ int run_add_i(struct repository *r, const struct pathspec *ps) { "update", run_update }, { "revert", run_revert }, { "add untracked", run_add_untracked }, + { "patch", run_patch }, { "help", run_help }, }; struct prefix_item_list commands = PREFIX_ITEM_LIST_INIT; From d3f22f56beb3ebe360d0de269671929b4e985301 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Wed, 3 Apr 2019 22:15:39 +0200 Subject: [PATCH 29/98] t3701: add a test for advanced split-hunk editing In this developer's workflows, it often happens that a hunk needs to be edited in a way that adds lines, and even reduces the context Let's add a regression test for this. Note that just like the preceding test case, the new test case is *not* handled gracefully by the current `git add -p`. It will be handled correctly by the upcoming built-in `git add -p`, though. Signed-off-by: Johannes Schindelin --- t/t3701-add-interactive.sh | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/t/t3701-add-interactive.sh b/t/t3701-add-interactive.sh index d4f9386621..4da99e27af 100755 --- a/t/t3701-add-interactive.sh +++ b/t/t3701-add-interactive.sh @@ -403,6 +403,28 @@ test_expect_failure 'split hunk "add -p (no, yes, edit)"' ' ! grep "^+31" actual ' +test_expect_failure 'edit, adding lines to the first hunk' ' + test_write_lines 10 11 20 30 40 50 51 60 >test && + git reset && + tr _ " " >patch <<-EOF && + @@ -1,5 +1,6 @@ + _10 + +11 + +12 + _20 + +21 + +22 + _30 + EOF + # test sequence is s(plit), e(dit), n(o) + # q n q q is there to make sure we exit at the end. + printf "%s\n" s e n q n q q | + EDITOR=./fake_editor.sh git add -p 2>error && + test_must_be_empty error && + git diff --cached >actual && + grep "^+22" actual +' + test_expect_success 'patch mode ignores unmerged entries' ' git reset --hard && test_commit conflict && From 9bddac0d57d86cf10bb2d1d738e9f13894e86960 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 8 Mar 2019 15:58:27 +0100 Subject: [PATCH 30/98] built-in add -i: re-implement the `diff` command It is not only laziness that we simply spawn `git diff -p --cached` here: this command needs to use the pager, and the pager needs to exit when the diff is done. Currently we do not have any way to make that happen if we run the diff in-process. So let's just spawn. Signed-off-by: Johannes Schindelin --- add-interactive.c | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/add-interactive.c b/add-interactive.c index e95dd4af35..ab7b39ee41 100644 --- a/add-interactive.c +++ b/add-interactive.c @@ -918,6 +918,47 @@ static int run_patch(struct add_i_state *s, const struct pathspec *ps, return res; } +static int run_diff(struct add_i_state *s, const struct pathspec *ps, + struct prefix_item_list *files, + struct list_and_choose_options *opts) +{ + int res = 0; + ssize_t count, i; + + struct object_id oid; + int is_initial = !resolve_ref_unsafe("HEAD", RESOLVE_REF_READING, &oid, + NULL); + if (get_modified_files(s->r, INDEX_ONLY, files, ps, NULL, NULL) < 0) + return -1; + + if (!files->items.nr) { + putchar('\n'); + return 0; + } + + opts->prompt = N_("Review diff"); + opts->flags = IMMEDIATE; + count = list_and_choose(s, files, opts); + opts->flags = 0; + if (count >= 0) { + struct argv_array args = ARGV_ARRAY_INIT; + + argv_array_pushl(&args, "git", "diff", "-p", "--cached", + oid_to_hex(!is_initial ? &oid : + s->r->hash_algo->empty_tree), + "--", NULL); + for (i = 0; i < files->items.nr; i++) + if (files->selected[i]) + argv_array_push(&args, + files->items.items[i].string); + res = run_command_v_opt(args.argv, 0); + argv_array_clear(&args); + } + + putchar('\n'); + return res; +} + static int run_help(struct add_i_state *s, const struct pathspec *unused_ps, struct prefix_item_list *unused_files, struct list_and_choose_options *opts) @@ -1016,6 +1057,7 @@ int run_add_i(struct repository *r, const struct pathspec *ps) { "revert", run_revert }, { "add untracked", run_add_untracked }, { "patch", run_patch }, + { "diff", run_diff }, { "help", run_help }, }; struct prefix_item_list commands = PREFIX_ITEM_LIST_INIT; From 3b2062965899a7b702679c8102ba47bb088ef018 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Sat, 23 Mar 2019 20:05:24 +0100 Subject: [PATCH 31/98] t3701: avoid depending on the TTY prerequisite The TTY prerequisite is a rather heavy one: it not only requires Perl to work, but also the IO/Pty.pm module (with native support, and it requires pseudo terminals, too). In particular, test cases marked with the TTY prerequisite would be skipped in Git for Windows' SDK. In the case of `git add -p`, we do not actually need that big a hammer, as we do not want to test any functionality that requires a pseudo terminal; all we want is to talk the interactive add command to use color, even when being called from within the test suite. And we found exactly such a trick earlier already: when we added a test case to verify that the main loop of `git add -i` is colored appropriately. Let's use that trick instead of the TTY prerequisite. While at it, we avoid the pipes, as we do not want a SIGPIPE to break the regression test cases (which will be much more likely when we do not run everything through Perl because that is inherently slower). Signed-off-by: Johannes Schindelin --- t/t3701-add-interactive.sh | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/t/t3701-add-interactive.sh b/t/t3701-add-interactive.sh index 4da99e27af..793ce28297 100755 --- a/t/t3701-add-interactive.sh +++ b/t/t3701-add-interactive.sh @@ -23,6 +23,17 @@ diff_cmp () { test_cmp "$1.filtered" "$2.filtered" } +# This function uses a trick to manipulate the interactive add to use color: +# the `want_color()` function special-cases the situation where a pager was +# spawned and Git now wants to output colored text: to detect that situation, +# the environment variable `GIT_PAGER_IN_USE` is set. However, color is +# suppressed despite that environment variable if the `TERM` variable +# indicates a dumb terminal, so we set that variable, too. + +force_color () { + env GIT_PAGER_IN_USE=true TERM=vt100 "$@" +} + test_expect_success 'setup (initial)' ' echo content >file && git add file && @@ -451,35 +462,38 @@ test_expect_success 'patch mode ignores unmerged entries' ' diff_cmp expected diff ' -test_expect_success TTY 'diffs can be colorized' ' +test_expect_success 'diffs can be colorized' ' git reset --hard && echo content >test && - printf y | test_terminal git add -p >output 2>&1 && + printf y >y && + force_color git add -p >output 2>&1 test && test_config interactive.diffFilter "sed s/^/foo:/" && - printf y | test_terminal git add -p >output 2>&1 && + printf y >y && + force_color git add -p >output 2>&1 test && test_config interactive.diffFilter "echo too-short" && - printf y | test_must_fail test_terminal git add -p + printf y >y && + test_must_fail force_color git add -p What now>$SP Bye. EOF - test_write_lines h | GIT_PAGER_IN_USE=true TERM=vt100 git add -i >actual.colored && + test_write_lines h | force_color git add -i >actual.colored && test_decode_color actual && test_i18ncmp expect actual ' From 6ba19986d74495f9ea951436acf4fdc60ff5e0cc Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 18 Oct 2019 16:29:10 +0200 Subject: [PATCH 32/98] built-in add -i: offer the `quit` command We do not really want to `exit()` here, of course, as this is safely libified code. Signed-off-by: Johannes Schindelin --- add-interactive.c | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/add-interactive.c b/add-interactive.c index ab7b39ee41..0ddad194cc 100644 --- a/add-interactive.c +++ b/add-interactive.c @@ -1058,6 +1058,7 @@ int run_add_i(struct repository *r, const struct pathspec *ps) { "add untracked", run_add_untracked }, { "patch", run_patch }, { "diff", run_diff }, + { "quit", NULL }, { "help", run_help }, }; struct prefix_item_list commands = PREFIX_ITEM_LIST_INIT; @@ -1109,17 +1110,22 @@ int run_add_i(struct repository *r, const struct pathspec *ps) res = run_status(&s, ps, &files, &opts); for (;;) { + struct command_item *util; + i = list_and_choose(&s, &commands, &main_loop_opts); - if (i == LIST_AND_CHOOSE_QUIT) { + if (i < 0 || i >= commands.items.nr) + util = NULL; + else + util = commands.items.items[i].util; + + if (i == LIST_AND_CHOOSE_QUIT || (util && !util->command)) { printf(_("Bye.\n")); res = 0; break; } - if (i != LIST_AND_CHOOSE_ERROR) { - struct command_item *util = - commands.items.items[i].util; + + if (util) res = util->command(&s, ps, &files, &opts); - } } prefix_item_list_clear(&files); From 522bb4270fefe54e382b41a758c235d36d8d1a62 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Sat, 23 Mar 2019 20:36:47 +0100 Subject: [PATCH 33/98] t3701: add a test for the different `add -p` prompts The `git add -p` command offers different prompts for regular diff hunks vs mode change pseudo hunks vs diffs deleting files. Let's cover this in the regresion test suite, in preparation for re-implementing `git add -p` in C. For the mode change prompt, we use a trick that lets this test case pass even on systems without executable bit, i.e. where `core.filemode = false` (such as Windows): we first add the file to the index with `git add --chmod=+x`, and then call `git add -p` with `core.filemode` forced to `true`. The file on disk has no executable bit set, therefore we will see a mode change. Signed-off-by: Johannes Schindelin --- t/t3701-add-interactive.sh | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/t/t3701-add-interactive.sh b/t/t3701-add-interactive.sh index 793ce28297..c90aaa25b0 100755 --- a/t/t3701-add-interactive.sh +++ b/t/t3701-add-interactive.sh @@ -105,7 +105,6 @@ test_expect_success 'revert works (commit)' ' grep "unchanged *+3/-0 file" output ' - test_expect_success 'setup expected' ' cat >expected <<-\EOF EOF @@ -274,6 +273,24 @@ test_expect_success FILEMODE 'stage mode and hunk' ' # end of tests disabled when filemode is not usable +test_expect_success 'different prompts for mode change/deleted' ' + git reset --hard && + >file && + >deleted && + git add --chmod=+x file deleted && + echo changed >file && + rm deleted && + test_write_lines n n n | + git -c core.filemode=true add -p >actual && + sed -n "s/^\(([0-9/]*) Stage .*?\).*/\1/p" actual >actual.filtered && + cat >expect <<-\EOF && + (1/1) Stage deletion [y,n,q,a,d,?]? + (1/2) Stage mode change [y,n,q,a,d,j,J,g,/,?]? + (2/2) Stage this hunk [y,n,q,a,d,K,g,/,e,?]? + EOF + test_cmp expect actual.filtered +' + test_expect_success 'setup again' ' git reset --hard && test_chmod +x file && From f5531786a10b60478fc7cd68ea4b3b1b3258d214 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Mon, 11 Mar 2019 22:01:22 +0100 Subject: [PATCH 34/98] built-in add -i: start implementing the `patch` functionality in C In the previous steps, we re-implemented the main loop of `git add -i` in C, and most of the commands. Notably, we left out the actual functionality of `patch`, as the relevant code makes up more than half of `git-add--interactive.perl`, and is actually pretty independent of the rest of the commands. With this commit, we start to tackle that `patch` part. For better separation of concerns, we keep the code in a separate file, `add-patch.c`. The new code is still guarded behind the `add.interactive.useBuiltin` config setting, and for the moment, it can only be called via `git add -p`. The actual functionality follows the original implementation of 5cde71d64aff (git-add --interactive, 2006-12-10), but not too closely (for example, we use string offsets rather than copying strings around, and we also remember which previous/next hunk was undecided, rather than looking again when the user asked to jump there). As a further deviation from that commit, We also use a comma instead of a slash to separate the available commands in the prompt, as the current version of the Perl script does this, and we also add a line about the question mark ("print help") to the help text. Signed-off-by: Johannes Schindelin --- Makefile | 1 + add-interactive.h | 1 + add-patch.c | 267 ++++++++++++++++++++++++++++++++++++++++++++++ builtin/add.c | 10 +- 4 files changed, 277 insertions(+), 2 deletions(-) create mode 100644 add-patch.c diff --git a/Makefile b/Makefile index 6c4a1e0ee5..0345d7408b 100644 --- a/Makefile +++ b/Makefile @@ -824,6 +824,7 @@ LIB_H := $(sort $(patsubst ./%,%,$(shell git ls-files '*.h' ':!t/' ':!Documentat LIB_OBJS += abspath.o LIB_OBJS += add-interactive.o +LIB_OBJS += add-patch.o LIB_OBJS += advice.o LIB_OBJS += alias.o LIB_OBJS += alloc.o diff --git a/add-interactive.h b/add-interactive.h index 7043b8741d..0e3d93acc9 100644 --- a/add-interactive.h +++ b/add-interactive.h @@ -4,5 +4,6 @@ struct repository; struct pathspec; int run_add_i(struct repository *r, const struct pathspec *ps); +int run_add_p(struct repository *r, const struct pathspec *ps); #endif diff --git a/add-patch.c b/add-patch.c new file mode 100644 index 0000000000..c4846947f6 --- /dev/null +++ b/add-patch.c @@ -0,0 +1,267 @@ +#include "cache.h" +#include "add-interactive.h" +#include "strbuf.h" +#include "run-command.h" +#include "argv-array.h" +#include "pathspec.h" + +struct hunk { + size_t start, end; + enum { UNDECIDED_HUNK = 0, SKIP_HUNK, USE_HUNK } use; +}; + +struct add_p_state { + struct repository *r; + struct strbuf answer, buf; + + /* parsed diff */ + struct strbuf plain; + struct hunk head; + struct hunk *hunk; + size_t hunk_nr, hunk_alloc; +}; + +static void setup_child_process(struct child_process *cp, + struct add_p_state *s, ...) +{ + va_list ap; + const char *arg; + + va_start(ap, s); + while((arg = va_arg(ap, const char *))) + argv_array_push(&cp->args, arg); + va_end(ap); + + cp->git_cmd = 1; + argv_array_pushf(&cp->env_array, + INDEX_ENVIRONMENT "=%s", s->r->index_file); +} + +static int parse_diff(struct add_p_state *s, const struct pathspec *ps) +{ + struct strbuf *plain = &s->plain; + struct child_process cp = CHILD_PROCESS_INIT; + char *p, *pend; + size_t i; + struct hunk *hunk = NULL; + int res; + + /* Use `--no-color` explicitly, just in case `diff.color = always`. */ + setup_child_process(&cp, s, + "diff-files", "-p", "--no-color", "--", NULL); + for (i = 0; i < ps->nr; i++) + argv_array_push(&cp.args, ps->items[i].original); + + res = capture_command(&cp, plain, 0); + if (res) + return error(_("could not parse diff")); + if (!plain->len) + return 0; + strbuf_complete_line(plain); + + /* parse hunks */ + p = plain->buf; + pend = p + plain->len; + while (p != pend) { + char *eol = memchr(p, '\n', pend - p); + if (!eol) + eol = pend; + + if (starts_with(p, "diff ")) { + if (p != plain->buf) + BUG("multi-file diff not yet handled"); + hunk = &s->head; + } else if (p == plain->buf) + BUG("diff starts with unexpected line:\n" + "%.*s\n", (int)(eol - p), p); + else if (starts_with(p, "@@ ")) { + s->hunk_nr++; + ALLOC_GROW(s->hunk, s->hunk_nr, + s->hunk_alloc); + hunk = s->hunk + s->hunk_nr - 1; + memset(hunk, 0, sizeof(*hunk)); + + hunk->start = p - plain->buf; + } + + p = eol == pend ? pend : eol + 1; + hunk->end = p - plain->buf; + } + + return 0; +} + +static void render_hunk(struct add_p_state *s, struct hunk *hunk, + struct strbuf *out) +{ + strbuf_add(out, s->plain.buf + hunk->start, + hunk->end - hunk->start); +} + +static void reassemble_patch(struct add_p_state *s, struct strbuf *out) +{ + struct hunk *hunk; + size_t i; + + render_hunk(s, &s->head, out); + + for (i = 0; i < s->hunk_nr; i++) { + hunk = s->hunk + i; + if (hunk->use == USE_HUNK) + render_hunk(s, hunk, out); + } +} + +static const char help_patch_text[] = +N_("y - stage this hunk\n" + "n - do not stage this hunk\n" + "a - stage this and all the remaining hunks\n" + "d - do not stage this hunk nor any of the remaining hunks\n" + "j - leave this hunk undecided, see next undecided hunk\n" + "J - leave this hunk undecided, see next hunk\n" + "k - leave this hunk undecided, see previous undecided hunk\n" + "K - leave this hunk undecided, see previous hunk\n" + "? - print help\n"); + +static int patch_update_file(struct add_p_state *s) +{ + size_t hunk_index = 0; + ssize_t i, undecided_previous, undecided_next; + struct hunk *hunk; + char ch; + struct child_process cp = CHILD_PROCESS_INIT; + + if (!s->hunk_nr) + return 0; + + strbuf_reset(&s->buf); + render_hunk(s, &s->head, &s->buf); + fputs(s->buf.buf, stdout); + for (;;) { + if (hunk_index >= s->hunk_nr) + hunk_index = 0; + hunk = s->hunk + hunk_index; + + undecided_previous = -1; + for (i = hunk_index - 1; i >= 0; i--) + if (s->hunk[i].use == UNDECIDED_HUNK) { + undecided_previous = i; + break; + } + + undecided_next = -1; + for (i = hunk_index + 1; i < s->hunk_nr; i++) + if (s->hunk[i].use == UNDECIDED_HUNK) { + undecided_next = i; + break; + } + + /* Everything decided? */ + if (undecided_previous < 0 && undecided_next < 0 && + hunk->use != UNDECIDED_HUNK) + break; + + strbuf_reset(&s->buf); + render_hunk(s, hunk, &s->buf); + fputs(s->buf.buf, stdout); + + strbuf_reset(&s->buf); + if (undecided_previous >= 0) + strbuf_addstr(&s->buf, ",k"); + if (hunk_index) + strbuf_addstr(&s->buf, ",K"); + if (undecided_next >= 0) + strbuf_addstr(&s->buf, ",j"); + if (hunk_index + 1 < s->hunk_nr) + strbuf_addstr(&s->buf, ",J"); + printf("(%"PRIuMAX"/%"PRIuMAX") ", + (uintmax_t)hunk_index + 1, (uintmax_t)s->hunk_nr); + printf(_("Stage this hunk [y,n,a,d%s,?]? "), s->buf.buf); + fflush(stdout); + if (strbuf_getline(&s->answer, stdin) == EOF) + break; + strbuf_trim_trailing_newline(&s->answer); + + if (!s->answer.len) + continue; + ch = tolower(s->answer.buf[0]); + if (ch == 'y') { + hunk->use = USE_HUNK; +soft_increment: + while (++hunk_index < s->hunk_nr && + s->hunk[hunk_index].use + != UNDECIDED_HUNK) + ; /* continue looking */ + } else if (ch == 'n') { + hunk->use = SKIP_HUNK; + goto soft_increment; + } else if (ch == 'a') { + for (; hunk_index < s->hunk_nr; hunk_index++) { + hunk = s->hunk + hunk_index; + if (hunk->use == UNDECIDED_HUNK) + hunk->use = USE_HUNK; + } + } else if (ch == 'd') { + for (; hunk_index < s->hunk_nr; hunk_index++) { + hunk = s->hunk + hunk_index; + if (hunk->use == UNDECIDED_HUNK) + hunk->use = SKIP_HUNK; + } + } else if (hunk_index && s->answer.buf[0] == 'K') + hunk_index--; + else if (hunk_index + 1 < s->hunk_nr && + s->answer.buf[0] == 'J') + hunk_index++; + else if (undecided_previous >= 0 && + s->answer.buf[0] == 'k') + hunk_index = undecided_previous; + else if (undecided_next >= 0 && s->answer.buf[0] == 'j') + hunk_index = undecided_next; + else + puts(_(help_patch_text)); + } + + /* Any hunk to be used? */ + for (i = 0; i < s->hunk_nr; i++) + if (s->hunk[i].use == USE_HUNK) + break; + + if (i < s->hunk_nr) { + /* At least one hunk selected: apply */ + strbuf_reset(&s->buf); + reassemble_patch(s, &s->buf); + + discard_index(s->r->index); + setup_child_process(&cp, s, "apply", "--cached", NULL); + if (pipe_command(&cp, s->buf.buf, s->buf.len, + NULL, 0, NULL, 0)) + error(_("'git apply --cached' failed")); + if (!repo_read_index(s->r)) + repo_refresh_and_write_index(s->r, REFRESH_QUIET, 0, + 1, NULL, NULL, NULL); + } + + putchar('\n'); + return 0; +} + +int run_add_p(struct repository *r, const struct pathspec *ps) +{ + struct add_p_state s = { r, STRBUF_INIT, STRBUF_INIT, STRBUF_INIT }; + + 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 || + parse_diff(&s, ps) < 0) { + strbuf_release(&s.plain); + return -1; + } + + if (s.hunk_nr) + patch_update_file(&s); + + strbuf_release(&s.answer); + strbuf_release(&s.buf); + strbuf_release(&s.plain); + return 0; +} diff --git a/builtin/add.c b/builtin/add.c index 4f625691b5..cf02e63ae0 100644 --- a/builtin/add.c +++ b/builtin/add.c @@ -192,8 +192,14 @@ int run_add_interactive(const char *revision, const char *patch_mode, git_config_get_bool("add.interactive.usebuiltin", &use_builtin_add_i); - if (use_builtin_add_i == 1 && !patch_mode) - return !!run_add_i(the_repository, pathspec); + if (use_builtin_add_i == 1) { + if (!patch_mode) + return !!run_add_i(the_repository, pathspec); + if (strcmp(patch_mode, "--patch")) + die("'%s' not yet supported in the built-in add -p", + patch_mode); + return !!run_add_p(the_repository, pathspec); + } argv_array_push(&argv, "add--interactive"); if (patch_mode) From d2dce89c0c155f573cd2022ac7d36e4a33bc299c Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Sun, 24 Mar 2019 22:51:02 +0100 Subject: [PATCH 35/98] t3701: verify the shown messages when nothing can be added In preparation for re-implementing `git add -p` in pure C (where we will purposefully keep the implementation of `git add -p` separate from the implementation of `git add -i`), let's verify that the user is told the same things as in the Perl version when the diff file is either empty or contains only entries about binary files. Signed-off-by: Johannes Schindelin --- t/t3701-add-interactive.sh | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/t/t3701-add-interactive.sh b/t/t3701-add-interactive.sh index c90aaa25b0..797610e96d 100755 --- a/t/t3701-add-interactive.sh +++ b/t/t3701-add-interactive.sh @@ -291,6 +291,17 @@ test_expect_success 'different prompts for mode change/deleted' ' test_cmp expect actual.filtered ' +test_expect_success 'correct message when there is nothing to do' ' + git reset --hard && + git add -p 2>err && + test_i18ngrep "No changes" err && + printf "\\0123" >binary && + git add binary && + printf "\\0abc" >binary && + git add -p 2>err && + test_i18ngrep "Only binary files changed" err +' + test_expect_success 'setup again' ' git reset --hard && test_chmod +x file && From f028c857ec7488870d7bac1d454bf25ee735cfda Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 12 Mar 2019 12:52:51 +0100 Subject: [PATCH 36/98] built-in add -i: wire up the new C code for the `patch` command The code in `git-add--interactive.perl` that takes care of the `patch` command can look quite intimidating. There are so many modes in which it can be called, for example. But for the `patch` command in `git add -i`, only one mode is relevant: the `stage` mode. And we just implemented the beginnings of that mode in C so far. So let's use it when `add.interactive.useBuiltin=true`. Now, while the code in `add-patch.c` is far from reaching feature parity with the code in `git-add--interactive.perl` (color is not implemented, the diff algorithm cannot be configured, the colored diff cannot be post-processed via `interactive.diffFilter`, many commands are unimplemented yet, etc), hooking it all up with the part of `git add -i` that is already converted to C makes it easier to test and develop it. Note: at this stage, both the `add.interactive.useBuiltin` config setting is still safely opt-in, and will probably be fore quite some time, to allow for thorough testing "in the wild" without adversely affecting existing users. Signed-off-by: Johannes Schindelin --- add-interactive.c | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/add-interactive.c b/add-interactive.c index 0ddad194cc..bf5a44859e 100644 --- a/add-interactive.c +++ b/add-interactive.c @@ -904,15 +904,18 @@ static int run_patch(struct add_i_state *s, const struct pathspec *ps, count = list_and_choose(s, files, opts); if (count >= 0) { struct argv_array args = ARGV_ARRAY_INIT; + struct pathspec ps_selected = { 0 }; - argv_array_pushl(&args, "git", "add--interactive", "--patch", - "--", NULL); for (i = 0; i < files->items.nr; i++) if (files->selected[i]) argv_array_push(&args, files->items.items[i].string); - res = run_command_v_opt(args.argv, 0); + parse_pathspec(&ps_selected, + PATHSPEC_ALL_MAGIC & ~PATHSPEC_LITERAL, + PATHSPEC_LITERAL_PATH, "", args.argv); + res = run_add_p(s->r, &ps_selected); argv_array_clear(&args); + clear_pathspec(&ps_selected); } return res; From 7fea65fd460a7e3ef5dd7221352c3fd0a4ed2f0b Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Sat, 23 Mar 2019 19:36:22 +0100 Subject: [PATCH 37/98] t3701: verify that the diff.algorithm config setting is handled Without this patch, there is actually no test in Git's test suite that covers the diff.algorithm feature. Let's add one. We do this by passing a bogus value and then expecting `git diff-files` to produce the appropriate error message. Signed-off-by: Johannes Schindelin --- t/t3701-add-interactive.sh | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/t/t3701-add-interactive.sh b/t/t3701-add-interactive.sh index 797610e96d..f43634102e 100755 --- a/t/t3701-add-interactive.sh +++ b/t/t3701-add-interactive.sh @@ -524,6 +524,16 @@ test_expect_success 'detect bogus diffFilter output' ' test_must_fail force_color git add -p file && + git add file && + echo changed >file && + git -c diff.algorithm=bogus add -p 2>err && + test_i18ngrep "error: option diff-algorithm accepts " err +' + test_expect_success 'patch-mode via -i prompts for files' ' git reset --hard && From 70e601b375e21c576a08ed4989bf477fd691718f Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 12 Mar 2019 15:42:10 +0100 Subject: [PATCH 38/98] built-in add -p: show colored hunks by default Just like the Perl version, we now generate two diffs if `color.diff` is set: one with and one without color. Then we parse them in parallel and record which hunks start at which offsets in both. Note that this is a (slight) deviation from the way the Perl version did it: we are no longer reading the output of `diff-files` line by line (which is more natural for Perl than for C), but in one go, and parse everything later, so we might just as well do it in synchrony. Signed-off-by: Johannes Schindelin --- add-patch.c | 79 +++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 62 insertions(+), 17 deletions(-) diff --git a/add-patch.c b/add-patch.c index c4846947f6..99df599cb7 100644 --- a/add-patch.c +++ b/add-patch.c @@ -4,9 +4,10 @@ #include "run-command.h" #include "argv-array.h" #include "pathspec.h" +#include "color.h" struct hunk { - size_t start, end; + size_t start, end, colored_start, colored_end; enum { UNDECIDED_HUNK = 0, SKIP_HUNK, USE_HUNK } use; }; @@ -15,7 +16,7 @@ struct add_p_state { struct strbuf answer, buf; /* parsed diff */ - struct strbuf plain; + struct strbuf plain, colored; struct hunk head; struct hunk *hunk; size_t hunk_nr, hunk_alloc; @@ -39,26 +40,50 @@ static void setup_child_process(struct child_process *cp, static int parse_diff(struct add_p_state *s, const struct pathspec *ps) { - struct strbuf *plain = &s->plain; + struct argv_array args = ARGV_ARRAY_INIT; + struct strbuf *plain = &s->plain, *colored = NULL; struct child_process cp = CHILD_PROCESS_INIT; - char *p, *pend; - size_t i; + char *p, *pend, *colored_p = NULL, *colored_pend = NULL; + size_t i, color_arg_index; struct hunk *hunk = NULL; int res; /* Use `--no-color` explicitly, just in case `diff.color = always`. */ - setup_child_process(&cp, s, - "diff-files", "-p", "--no-color", "--", NULL); + argv_array_pushl(&args, "diff-files", "-p", "--no-color", "--", NULL); + color_arg_index = args.argc - 2; for (i = 0; i < ps->nr; i++) - argv_array_push(&cp.args, ps->items[i].original); + argv_array_push(&args, ps->items[i].original); + setup_child_process(&cp, s, NULL); + cp.argv = args.argv; res = capture_command(&cp, plain, 0); - if (res) + if (res) { + argv_array_clear(&args); return error(_("could not parse diff")); - if (!plain->len) + } + if (!plain->len) { + argv_array_clear(&args); return 0; + } strbuf_complete_line(plain); + if (want_color_fd(1, -1)) { + struct child_process colored_cp = CHILD_PROCESS_INIT; + + setup_child_process(&colored_cp, s, NULL); + xsnprintf((char *)args.argv[color_arg_index], 8, "--color"); + colored_cp.argv = args.argv; + colored = &s->colored; + res = capture_command(&colored_cp, colored, 0); + argv_array_clear(&args); + if (res) + return error(_("could not parse colored diff")); + strbuf_complete_line(colored); + colored_p = colored->buf; + colored_pend = colored_p + colored->len; + } + argv_array_clear(&args); + /* parse hunks */ p = plain->buf; pend = p + plain->len; @@ -82,20 +107,37 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps) memset(hunk, 0, sizeof(*hunk)); hunk->start = p - plain->buf; + if (colored) + hunk->colored_start = colored_p - colored->buf; } p = eol == pend ? pend : eol + 1; hunk->end = p - plain->buf; + + if (colored) { + char *colored_eol = memchr(colored_p, '\n', + colored_pend - colored_p); + if (colored_eol) + colored_p = colored_eol + 1; + else + colored_p = colored_pend; + + hunk->colored_end = colored_p - colored->buf; + } } return 0; } static void render_hunk(struct add_p_state *s, struct hunk *hunk, - struct strbuf *out) + int colored, struct strbuf *out) { - strbuf_add(out, s->plain.buf + hunk->start, - hunk->end - hunk->start); + if (colored) + strbuf_add(out, s->colored.buf + hunk->colored_start, + hunk->colored_end - hunk->colored_start); + else + strbuf_add(out, s->plain.buf + hunk->start, + hunk->end - hunk->start); } static void reassemble_patch(struct add_p_state *s, struct strbuf *out) @@ -103,12 +145,12 @@ static void reassemble_patch(struct add_p_state *s, struct strbuf *out) struct hunk *hunk; size_t i; - render_hunk(s, &s->head, out); + render_hunk(s, &s->head, 0, out); for (i = 0; i < s->hunk_nr; i++) { hunk = s->hunk + i; if (hunk->use == USE_HUNK) - render_hunk(s, hunk, out); + render_hunk(s, hunk, 0, out); } } @@ -130,12 +172,13 @@ static int patch_update_file(struct add_p_state *s) struct hunk *hunk; char ch; struct child_process cp = CHILD_PROCESS_INIT; + int colored = !!s->colored.len; if (!s->hunk_nr) return 0; strbuf_reset(&s->buf); - render_hunk(s, &s->head, &s->buf); + render_hunk(s, &s->head, colored, &s->buf); fputs(s->buf.buf, stdout); for (;;) { if (hunk_index >= s->hunk_nr) @@ -162,7 +205,7 @@ static int patch_update_file(struct add_p_state *s) break; strbuf_reset(&s->buf); - render_hunk(s, hunk, &s->buf); + render_hunk(s, hunk, colored, &s->buf); fputs(s->buf.buf, stdout); strbuf_reset(&s->buf); @@ -254,6 +297,7 @@ int run_add_p(struct repository *r, const struct pathspec *ps) NULL, NULL, NULL) < 0 || parse_diff(&s, ps) < 0) { strbuf_release(&s.plain); + strbuf_release(&s.colored); return -1; } @@ -263,5 +307,6 @@ int run_add_p(struct repository *r, const struct pathspec *ps) strbuf_release(&s.answer); strbuf_release(&s.buf); strbuf_release(&s.plain); + strbuf_release(&s.colored); return 0; } From 0f512c107a84e48d18312de898f39efbcc5a9056 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Sat, 23 Mar 2019 17:37:45 +0100 Subject: [PATCH 39/98] git add -p: use non-zero exit code when the diff generation failed The first thing `git add -p` does is to generate a diff. If this diff cannot be generated, `git add -p` should not continue as if nothing happened, but instead fail. What we *actually* do here is much broader: we now verify for *every* `run_cmd_pipe()` call that the spawned process actually succeeded. Note that we have to change two callers in this patch, as we need to store the spawned process' output in a local variable, which means that the callers can no longer decide whether to interpret the `return <$fh>` in array or in scalar context. This bug was noticed while writing a test case for the diff.algorithm feature, and we let that test case double as a regression test for this fixed bug, too. Signed-off-by: Johannes Schindelin --- git-add--interactive.perl | 8 +++++--- t/t3701-add-interactive.sh | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/git-add--interactive.perl b/git-add--interactive.perl index 52659bb74c..10fd30ae16 100755 --- a/git-add--interactive.perl +++ b/git-add--interactive.perl @@ -177,7 +177,9 @@ sub run_cmd_pipe { } else { my $fh = undef; open($fh, '-|', @_) or die; - return <$fh>; + my @out = <$fh>; + close $fh || die "Cannot close @_ ($!)"; + return @out; } } @@ -224,7 +226,7 @@ my $status_head = sprintf($status_fmt, __('staged'), __('unstaged'), __('path')) sub get_empty_tree { return $empty_tree if defined $empty_tree; - $empty_tree = run_cmd_pipe(qw(git hash-object -t tree /dev/null)); + ($empty_tree) = run_cmd_pipe(qw(git hash-object -t tree /dev/null)); chomp $empty_tree; return $empty_tree; } @@ -1127,7 +1129,7 @@ aborted and the hunk is left unchanged. EOF2 close $fh; - chomp(my $editor = run_cmd_pipe(qw(git var GIT_EDITOR))); + chomp(my ($editor) = run_cmd_pipe(qw(git var GIT_EDITOR))); system('sh', '-c', $editor.' "$@"', $editor, $hunkfile); if ($? != 0) { diff --git a/t/t3701-add-interactive.sh b/t/t3701-add-interactive.sh index f43634102e..5db6432e33 100755 --- a/t/t3701-add-interactive.sh +++ b/t/t3701-add-interactive.sh @@ -530,7 +530,7 @@ test_expect_success 'diff.algorithm is passed to `git diff-files`' ' >file && git add file && echo changed >file && - git -c diff.algorithm=bogus add -p 2>err && + test_must_fail git -c diff.algorithm=bogus add -p 2>err && test_i18ngrep "error: option diff-algorithm accepts " err ' From a7e0aa80abdb474616c748e9a90d8d07398e91e2 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 21 Mar 2019 09:40:20 +0100 Subject: [PATCH 40/98] built-in add -p: adjust hunk headers as needed When skipping a hunk that adds a different number of lines than it removes, we need to adjust the subsequent hunk headers of non-skipped hunks: in pathological cases, the context is not enough to determine precisely where the patch should be applied. This problem was identified in 23fea4c240 (t3701: add failing test for pathological context lines, 2018-03-01) and fixed in the Perl version in fecc6f3a68 (add -p: adjust offsets of subsequent hunks when one is skipped, 2018-03-01). And this patch fixes it in the C version of `git add -p`. In contrast to the Perl version, we try to keep the extra text on the hunk header (which typically contains the signature of the function whose code is changed in the hunk) intact. Note: while the C version does not support staging mode changes at this stage, we already prepare for this by simply skipping the hunk header if both old and new offset is 0 (this cannot happen for regular hunks, and we will use this as an indicator that we are looking at a special hunk). Likewise, we already prepare for hunk splitting by handling the absence of extra text in the hunk header gracefully: only the first split hunk will have that text, the others will not (indicated by an empty extra text start/end range). Preparing for hunk splitting already at this stage avoids an indentation change of the entire hunk header-printing block later, and is almost as easy to review as without that handling. Signed-off-by: Johannes Schindelin --- add-interactive.c | 14 +---- add-interactive.h | 15 +++++ add-patch.c | 145 ++++++++++++++++++++++++++++++++++++++++++---- 3 files changed, 151 insertions(+), 23 deletions(-) diff --git a/add-interactive.c b/add-interactive.c index bf5a44859e..fc0479a2bc 100644 --- a/add-interactive.c +++ b/add-interactive.c @@ -10,16 +10,6 @@ #include "dir.h" #include "run-command.h" -struct add_i_state { - struct repository *r; - 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, const char *slot_name, char *dst, const char *default_color) @@ -36,7 +26,7 @@ static void init_color(struct repository *r, struct add_i_state *s, free(key); } -static void init_add_i_state(struct add_i_state *s, struct repository *r) +void init_add_i_state(struct add_i_state *s, struct repository *r) { const char *value; @@ -54,6 +44,8 @@ static void init_add_i_state(struct add_i_state *s, struct repository *r) 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); + init_color(r, s, "fraginfo", s->fraginfo_color, + diff_get_color(s->use_color, DIFF_FRAGINFO)); } /* diff --git a/add-interactive.h b/add-interactive.h index 0e3d93acc9..584f304a9a 100644 --- a/add-interactive.h +++ b/add-interactive.h @@ -1,6 +1,21 @@ #ifndef ADD_INTERACTIVE_H #define ADD_INTERACTIVE_H +#include "color.h" + +struct add_i_state { + struct repository *r; + 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]; + char fraginfo_color[COLOR_MAXLEN]; +}; + +void init_add_i_state(struct add_i_state *s, struct repository *r); + struct repository; struct pathspec; int run_add_i(struct repository *r, const struct pathspec *ps); diff --git a/add-patch.c b/add-patch.c index 99df599cb7..18e3be0b66 100644 --- a/add-patch.c +++ b/add-patch.c @@ -5,14 +5,26 @@ #include "argv-array.h" #include "pathspec.h" #include "color.h" +#include "diff.h" + +struct hunk_header { + unsigned long old_offset, old_count, new_offset, new_count; + /* + * Start/end offsets to the extra text after the second `@@` in the + * hunk header, e.g. the function signature. This is expected to + * include the newline. + */ + size_t extra_start, extra_end, colored_extra_start, colored_extra_end; +}; struct hunk { size_t start, end, colored_start, colored_end; enum { UNDECIDED_HUNK = 0, SKIP_HUNK, USE_HUNK } use; + struct hunk_header header; }; struct add_p_state { - struct repository *r; + struct add_i_state s; struct strbuf answer, buf; /* parsed diff */ @@ -35,7 +47,70 @@ static void setup_child_process(struct child_process *cp, cp->git_cmd = 1; argv_array_pushf(&cp->env_array, - INDEX_ENVIRONMENT "=%s", s->r->index_file); + INDEX_ENVIRONMENT "=%s", s->s.r->index_file); +} + +static int parse_range(const char **p, + unsigned long *offset, unsigned long *count) +{ + char *pend; + + *offset = strtoul(*p, &pend, 10); + if (pend == *p) + return -1; + if (*pend != ',') { + *count = 1; + *p = pend; + return 0; + } + *count = strtoul(pend + 1, (char **)p, 10); + return *p == pend + 1 ? -1 : 0; +} + +static int parse_hunk_header(struct add_p_state *s, struct hunk *hunk) +{ + struct hunk_header *header = &hunk->header; + const char *line = s->plain.buf + hunk->start, *p = line; + char *eol = memchr(p, '\n', s->plain.len - hunk->start); + + if (!eol) + eol = s->plain.buf + s->plain.len; + + if (!skip_prefix(p, "@@ -", &p) || + parse_range(&p, &header->old_offset, &header->old_count) < 0 || + !skip_prefix(p, " +", &p) || + parse_range(&p, &header->new_offset, &header->new_count) < 0 || + !skip_prefix(p, " @@", &p)) + return error(_("could not parse hunk header '%.*s'"), + (int)(eol - line), line); + + hunk->start = eol - s->plain.buf + (*eol == '\n'); + header->extra_start = p - s->plain.buf; + header->extra_end = hunk->start; + + if (!s->colored.len) { + header->colored_extra_start = header->colored_extra_end = 0; + return 0; + } + + /* Now find the extra text in the colored diff */ + line = s->colored.buf + hunk->colored_start; + eol = memchr(line, '\n', s->colored.len - hunk->colored_start); + if (!eol) + eol = s->colored.buf + s->colored.len; + p = memmem(line, eol - line, "@@ -", 4); + if (!p) + return error(_("could not parse colored hunk header '%.*s'"), + (int)(eol - line), line); + p = memmem(p + 4, eol - p - 4, " @@", 3); + if (!p) + return error(_("could not parse colored hunk header '%.*s'"), + (int)(eol - line), line); + hunk->colored_start = eol - s->colored.buf + (*eol == '\n'); + header->colored_extra_start = p + 3 - s->colored.buf; + header->colored_extra_end = hunk->colored_start; + + return 0; } static int parse_diff(struct add_p_state *s, const struct pathspec *ps) @@ -109,6 +184,9 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps) hunk->start = p - plain->buf; if (colored) hunk->colored_start = colored_p - colored->buf; + + if (parse_hunk_header(s, hunk) < 0) + return -1; } p = eol == pend ? pend : eol + 1; @@ -130,8 +208,43 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps) } static void render_hunk(struct add_p_state *s, struct hunk *hunk, - int colored, struct strbuf *out) + ssize_t delta, int colored, struct strbuf *out) { + struct hunk_header *header = &hunk->header; + + if (hunk->header.old_offset != 0 || hunk->header.new_offset != 0) { + /* + * Generate the hunk header dynamically, except for special + * hunks (such as the diff header). + */ + const char *p; + size_t len; + unsigned long old_offset = header->old_offset; + unsigned long new_offset = header->new_offset; + + if (!colored) { + p = s->plain.buf + header->extra_start; + len = header->extra_end - header->extra_start; + } else { + strbuf_addstr(out, s->s.fraginfo_color); + p = s->colored.buf + header->colored_extra_start; + len = header->colored_extra_end + - header->colored_extra_start; + } + + new_offset += delta; + + strbuf_addf(out, "@@ -%lu,%lu +%lu,%lu @@", + old_offset, header->old_count, + new_offset, header->new_count); + if (len) + strbuf_add(out, p, len); + else if (colored) + strbuf_addf(out, "%s\n", GIT_COLOR_RESET); + else + strbuf_addch(out, '\n'); + } + if (colored) strbuf_add(out, s->colored.buf + hunk->colored_start, hunk->colored_end - hunk->colored_start); @@ -144,13 +257,17 @@ static void reassemble_patch(struct add_p_state *s, struct strbuf *out) { struct hunk *hunk; size_t i; + ssize_t delta = 0; - render_hunk(s, &s->head, 0, out); + render_hunk(s, &s->head, 0, 0, out); for (i = 0; i < s->hunk_nr; i++) { hunk = s->hunk + i; - if (hunk->use == USE_HUNK) - render_hunk(s, hunk, 0, out); + if (hunk->use != USE_HUNK) + delta += hunk->header.old_count + - hunk->header.new_count; + else + render_hunk(s, hunk, delta, 0, out); } } @@ -178,7 +295,7 @@ static int patch_update_file(struct add_p_state *s) return 0; strbuf_reset(&s->buf); - render_hunk(s, &s->head, colored, &s->buf); + render_hunk(s, &s->head, 0, colored, &s->buf); fputs(s->buf.buf, stdout); for (;;) { if (hunk_index >= s->hunk_nr) @@ -205,7 +322,7 @@ static int patch_update_file(struct add_p_state *s) break; strbuf_reset(&s->buf); - render_hunk(s, hunk, colored, &s->buf); + render_hunk(s, hunk, 0, colored, &s->buf); fputs(s->buf.buf, stdout); strbuf_reset(&s->buf); @@ -274,13 +391,13 @@ soft_increment: strbuf_reset(&s->buf); reassemble_patch(s, &s->buf); - discard_index(s->r->index); + discard_index(s->s.r->index); setup_child_process(&cp, s, "apply", "--cached", NULL); if (pipe_command(&cp, s->buf.buf, s->buf.len, NULL, 0, NULL, 0)) error(_("'git apply --cached' failed")); - if (!repo_read_index(s->r)) - repo_refresh_and_write_index(s->r, REFRESH_QUIET, 0, + if (!repo_read_index(s->s.r)) + repo_refresh_and_write_index(s->s.r, REFRESH_QUIET, 0, 1, NULL, NULL, NULL); } @@ -290,7 +407,11 @@ soft_increment: int run_add_p(struct repository *r, const struct pathspec *ps) { - struct add_p_state s = { r, STRBUF_INIT, STRBUF_INIT, STRBUF_INIT }; + struct add_p_state s = { + { r }, STRBUF_INIT, STRBUF_INIT, STRBUF_INIT, STRBUF_INIT + }; + + init_add_i_state(&s.s, r); if (discard_index(r->index) < 0 || repo_read_index(r) < 0 || repo_refresh_and_write_index(r, REFRESH_QUIET, 0, 1, From 5a158606bd45dd5bd788e4828fa8c76b7f3a90c9 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Wed, 20 Mar 2019 12:10:11 +0100 Subject: [PATCH 41/98] apply --allow-overlap: fix a corner case Yes, yes, this is supposed to be only a band-aid option for `git add -p` not Doing The Right Thing. But as long as we carry the `--allow-overlap` option, we might just as well get it right. This fixes the case where one hunk inserts a line before the first one, and a hunk whose context overlaps with the first one's appends a line at the end. Signed-off-by: Johannes Schindelin --- apply.c | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/apply.c b/apply.c index f8a046a6a5..720a631eaa 100644 --- a/apply.c +++ b/apply.c @@ -2661,6 +2661,16 @@ static int find_pos(struct apply_state *state, unsigned long backwards, forwards, current; int backwards_lno, forwards_lno, current_lno; + /* + * When running with --allow-overlap, it is possible that a hunk is + * seen that pretends to start at the beginning (but no longer does), + * and that *still* needs to match the end. So trust `match_end` more + * than `match_beginning`. + */ + if (state->allow_overlap && match_beginning && match_end && + img->nr - preimage->nr != 0) + match_beginning = 0; + /* * If match_beginning or match_end is specified, there is no * point starting from a wrong line that will never match and From fb22ad91a62d9f93d2523ceda1d990692b41bcd0 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 12 Mar 2019 16:40:46 +0100 Subject: [PATCH 42/98] built-in add -p: color the prompt and the help text ... just like the Perl version ;-) Note that this requires the `get_add_i_color()` function being defined globally, which is the entire reason why we gave it such a descriptive name in the first place. Signed-off-by: Johannes Schindelin --- add-interactive.h | 9 +++++++++ add-patch.c | 12 ++++++++---- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/add-interactive.h b/add-interactive.h index 584f304a9a..0a3ee3474a 100644 --- a/add-interactive.h +++ b/add-interactive.h @@ -16,6 +16,15 @@ struct add_i_state { void init_add_i_state(struct add_i_state *s, struct repository *r); +enum color_add_i { + COLOR_HEADER = 0, + COLOR_HELP, + COLOR_PROMPT, + COLOR_ERROR, + COLOR_RESET, +}; +const char *get_add_i_color(enum color_add_i ix); + struct repository; struct pathspec; int run_add_i(struct repository *r, const struct pathspec *ps); diff --git a/add-patch.c b/add-patch.c index 18e3be0b66..37d6ef3ddf 100644 --- a/add-patch.c +++ b/add-patch.c @@ -334,9 +334,12 @@ static int patch_update_file(struct add_p_state *s) strbuf_addstr(&s->buf, ",j"); if (hunk_index + 1 < s->hunk_nr) strbuf_addstr(&s->buf, ",J"); - printf("(%"PRIuMAX"/%"PRIuMAX") ", - (uintmax_t)hunk_index + 1, (uintmax_t)s->hunk_nr); - printf(_("Stage this hunk [y,n,a,d%s,?]? "), s->buf.buf); + color_fprintf(stdout, s->s.prompt_color, + "(%"PRIuMAX"/%"PRIuMAX") ", + (uintmax_t)hunk_index + 1, (uintmax_t)s->hunk_nr); + color_fprintf(stdout, s->s.prompt_color, + _("Stage this hunk [y,n,a,d%s,?]? "), + s->buf.buf); fflush(stdout); if (strbuf_getline(&s->answer, stdin) == EOF) break; @@ -378,7 +381,8 @@ soft_increment: else if (undecided_next >= 0 && s->answer.buf[0] == 'j') hunk_index = undecided_next; else - puts(_(help_patch_text)); + color_fprintf(stdout, s->s.help_color, + _(help_patch_text)); } /* Any hunk to be used? */ From efae448aad31882120534f48f3a934e893f301ec Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Sun, 17 Mar 2019 21:10:47 +0100 Subject: [PATCH 43/98] built-in add -p: offer a helpful error message when hunk navigation failed ... just like the Perl version currently does... Signed-off-by: Johannes Schindelin --- add-patch.c | 44 +++++++++++++++++++++++++++++++++----------- 1 file changed, 33 insertions(+), 11 deletions(-) diff --git a/add-patch.c b/add-patch.c index 37d6ef3ddf..2a7ad6b9c0 100644 --- a/add-patch.c +++ b/add-patch.c @@ -34,6 +34,18 @@ struct add_p_state { size_t hunk_nr, hunk_alloc; }; +static void err(struct add_p_state *s, const char *fmt, ...) +{ + va_list args; + + va_start(args, fmt); + fputs(s->s.error_color, stderr); + vfprintf(stderr, fmt, args); + fputs(s->s.reset_color, stderr); + fputc('\n', stderr); + va_end(args); +} + static void setup_child_process(struct child_process *cp, struct add_p_state *s, ...) { @@ -370,17 +382,27 @@ soft_increment: if (hunk->use == UNDECIDED_HUNK) hunk->use = SKIP_HUNK; } - } else if (hunk_index && s->answer.buf[0] == 'K') - hunk_index--; - else if (hunk_index + 1 < s->hunk_nr && - s->answer.buf[0] == 'J') - hunk_index++; - else if (undecided_previous >= 0 && - s->answer.buf[0] == 'k') - hunk_index = undecided_previous; - else if (undecided_next >= 0 && s->answer.buf[0] == 'j') - hunk_index = undecided_next; - else + } else if (s->answer.buf[0] == 'K') { + if (hunk_index) + hunk_index--; + else + err(s, _("No previous hunk")); + } else if (s->answer.buf[0] == 'J') { + if (hunk_index + 1 < s->hunk_nr) + hunk_index++; + else + err(s, _("No next hunk")); + } else if (s->answer.buf[0] == 'k') { + if (undecided_previous >= 0) + hunk_index = undecided_previous; + else + err(s, _("No previous hunk")); + } else if (s->answer.buf[0] == 'j') { + if (undecided_next >= 0) + hunk_index = undecided_next; + else + err(s, _("No next hunk")); + } else color_fprintf(stdout, s->s.help_color, _(help_patch_text)); } From 7647db902995a83b8dc4164bfd32dfc06047c836 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 15 Mar 2019 11:11:43 +0100 Subject: [PATCH 44/98] built-in add -p: support multi-file diffs For simplicity, the initial implementation in C handled only a single modified file. Now it handles an arbitrary number of files. Signed-off-by: Johannes Schindelin --- add-patch.c | 93 +++++++++++++++++++++++++++++++---------------------- 1 file changed, 54 insertions(+), 39 deletions(-) diff --git a/add-patch.c b/add-patch.c index 2a7ad6b9c0..c4364a86b6 100644 --- a/add-patch.c +++ b/add-patch.c @@ -29,9 +29,12 @@ struct add_p_state { /* parsed diff */ struct strbuf plain, colored; - struct hunk head; - struct hunk *hunk; - size_t hunk_nr, hunk_alloc; + struct file_diff { + struct hunk head; + struct hunk *hunk; + size_t hunk_nr, hunk_alloc; + } *file_diff; + size_t file_diff_nr; }; static void err(struct add_p_state *s, const char *fmt, ...) @@ -131,7 +134,8 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps) struct strbuf *plain = &s->plain, *colored = NULL; struct child_process cp = CHILD_PROCESS_INIT; char *p, *pend, *colored_p = NULL, *colored_pend = NULL; - size_t i, color_arg_index; + size_t file_diff_alloc = 0, i, color_arg_index; + struct file_diff *file_diff = NULL; struct hunk *hunk = NULL; int res; @@ -171,7 +175,7 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps) } argv_array_clear(&args); - /* parse hunks */ + /* parse files and hunks */ p = plain->buf; pend = p + plain->len; while (p != pend) { @@ -180,17 +184,23 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps) eol = pend; if (starts_with(p, "diff ")) { - if (p != plain->buf) - BUG("multi-file diff not yet handled"); - hunk = &s->head; + s->file_diff_nr++; + ALLOC_GROW(s->file_diff, s->file_diff_nr, + file_diff_alloc); + file_diff = s->file_diff + s->file_diff_nr - 1; + memset(file_diff, 0, sizeof(*file_diff)); + hunk = &file_diff->head; + hunk->start = p - plain->buf; + if (colored_p) + hunk->colored_start = colored_p - colored->buf; } else if (p == plain->buf) BUG("diff starts with unexpected line:\n" "%.*s\n", (int)(eol - p), p); else if (starts_with(p, "@@ ")) { - s->hunk_nr++; - ALLOC_GROW(s->hunk, s->hunk_nr, - s->hunk_alloc); - hunk = s->hunk + s->hunk_nr - 1; + file_diff->hunk_nr++; + ALLOC_GROW(file_diff->hunk, file_diff->hunk_nr, + file_diff->hunk_alloc); + hunk = file_diff->hunk + file_diff->hunk_nr - 1; memset(hunk, 0, sizeof(*hunk)); hunk->start = p - plain->buf; @@ -265,16 +275,17 @@ static void render_hunk(struct add_p_state *s, struct hunk *hunk, hunk->end - hunk->start); } -static void reassemble_patch(struct add_p_state *s, struct strbuf *out) +static void reassemble_patch(struct add_p_state *s, + struct file_diff *file_diff, struct strbuf *out) { struct hunk *hunk; size_t i; ssize_t delta = 0; - render_hunk(s, &s->head, 0, 0, out); + render_hunk(s, &file_diff->head, 0, 0, out); - for (i = 0; i < s->hunk_nr; i++) { - hunk = s->hunk + i; + for (i = 0; i < file_diff->hunk_nr; i++) { + hunk = file_diff->hunk + i; if (hunk->use != USE_HUNK) delta += hunk->header.old_count - hunk->header.new_count; @@ -294,7 +305,8 @@ N_("y - stage this hunk\n" "K - leave this hunk undecided, see previous hunk\n" "? - print help\n"); -static int patch_update_file(struct add_p_state *s) +static int patch_update_file(struct add_p_state *s, + struct file_diff *file_diff) { size_t hunk_index = 0; ssize_t i, undecided_previous, undecided_next; @@ -303,27 +315,27 @@ static int patch_update_file(struct add_p_state *s) struct child_process cp = CHILD_PROCESS_INIT; int colored = !!s->colored.len; - if (!s->hunk_nr) + if (!file_diff->hunk_nr) return 0; strbuf_reset(&s->buf); - render_hunk(s, &s->head, 0, colored, &s->buf); + render_hunk(s, &file_diff->head, 0, colored, &s->buf); fputs(s->buf.buf, stdout); for (;;) { - if (hunk_index >= s->hunk_nr) + if (hunk_index >= file_diff->hunk_nr) hunk_index = 0; - hunk = s->hunk + hunk_index; + hunk = file_diff->hunk + hunk_index; undecided_previous = -1; for (i = hunk_index - 1; i >= 0; i--) - if (s->hunk[i].use == UNDECIDED_HUNK) { + if (file_diff->hunk[i].use == UNDECIDED_HUNK) { undecided_previous = i; break; } undecided_next = -1; - for (i = hunk_index + 1; i < s->hunk_nr; i++) - if (s->hunk[i].use == UNDECIDED_HUNK) { + for (i = hunk_index + 1; i < file_diff->hunk_nr; i++) + if (file_diff->hunk[i].use == UNDECIDED_HUNK) { undecided_next = i; break; } @@ -344,11 +356,12 @@ static int patch_update_file(struct add_p_state *s) strbuf_addstr(&s->buf, ",K"); if (undecided_next >= 0) strbuf_addstr(&s->buf, ",j"); - if (hunk_index + 1 < s->hunk_nr) + if (hunk_index + 1 < file_diff->hunk_nr) strbuf_addstr(&s->buf, ",J"); color_fprintf(stdout, s->s.prompt_color, "(%"PRIuMAX"/%"PRIuMAX") ", - (uintmax_t)hunk_index + 1, (uintmax_t)s->hunk_nr); + (uintmax_t)hunk_index + 1, + (uintmax_t)file_diff->hunk_nr); color_fprintf(stdout, s->s.prompt_color, _("Stage this hunk [y,n,a,d%s,?]? "), s->buf.buf); @@ -363,22 +376,22 @@ static int patch_update_file(struct add_p_state *s) if (ch == 'y') { hunk->use = USE_HUNK; soft_increment: - while (++hunk_index < s->hunk_nr && - s->hunk[hunk_index].use + while (++hunk_index < file_diff->hunk_nr && + file_diff->hunk[hunk_index].use != UNDECIDED_HUNK) ; /* continue looking */ } else if (ch == 'n') { hunk->use = SKIP_HUNK; goto soft_increment; } else if (ch == 'a') { - for (; hunk_index < s->hunk_nr; hunk_index++) { - hunk = s->hunk + hunk_index; + for (; hunk_index < file_diff->hunk_nr; hunk_index++) { + hunk = file_diff->hunk + hunk_index; if (hunk->use == UNDECIDED_HUNK) hunk->use = USE_HUNK; } } else if (ch == 'd') { - for (; hunk_index < s->hunk_nr; hunk_index++) { - hunk = s->hunk + hunk_index; + for (; hunk_index < file_diff->hunk_nr; hunk_index++) { + hunk = file_diff->hunk + hunk_index; if (hunk->use == UNDECIDED_HUNK) hunk->use = SKIP_HUNK; } @@ -388,7 +401,7 @@ soft_increment: else err(s, _("No previous hunk")); } else if (s->answer.buf[0] == 'J') { - if (hunk_index + 1 < s->hunk_nr) + if (hunk_index + 1 < file_diff->hunk_nr) hunk_index++; else err(s, _("No next hunk")); @@ -408,14 +421,14 @@ soft_increment: } /* Any hunk to be used? */ - for (i = 0; i < s->hunk_nr; i++) - if (s->hunk[i].use == USE_HUNK) + for (i = 0; i < file_diff->hunk_nr; i++) + if (file_diff->hunk[i].use == USE_HUNK) break; - if (i < s->hunk_nr) { + if (i < file_diff->hunk_nr) { /* At least one hunk selected: apply */ strbuf_reset(&s->buf); - reassemble_patch(s, &s->buf); + reassemble_patch(s, file_diff, &s->buf); discard_index(s->s.r->index); setup_child_process(&cp, s, "apply", "--cached", NULL); @@ -436,6 +449,7 @@ int run_add_p(struct repository *r, const struct pathspec *ps) struct add_p_state s = { { r }, STRBUF_INIT, STRBUF_INIT, STRBUF_INIT, STRBUF_INIT }; + size_t i; init_add_i_state(&s.s, r); @@ -448,8 +462,9 @@ int run_add_p(struct repository *r, const struct pathspec *ps) return -1; } - if (s.hunk_nr) - patch_update_file(&s); + for (i = 0; i < s.file_diff_nr; i++) + if (patch_update_file(&s, s.file_diff + i)) + break; strbuf_release(&s.answer); strbuf_release(&s.buf); From 0f827c4563cd02594d0e66aa2e0e9eede23957af Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 15 Mar 2019 17:32:44 +0100 Subject: [PATCH 45/98] built-in add -p: handle deleted empty files This addresses the same problem as 24ab81ae4d (add-interactive: handle deletion of empty files, 2009-10-27), although in a different way: we not only stick the "deleted file" line into its own pseudo hunk, but also the entire remainder (if any) of the same diff. That way, we do not have to play any funny games with regards to coalescing the diff after the user selected what (possibly pseudo-)hunks to stage. Signed-off-by: Johannes Schindelin --- add-patch.c | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/add-patch.c b/add-patch.c index c4364a86b6..0edc39c928 100644 --- a/add-patch.c +++ b/add-patch.c @@ -33,6 +33,7 @@ struct add_p_state { struct hunk head; struct hunk *hunk; size_t hunk_nr, hunk_alloc; + unsigned deleted:1; } *file_diff; size_t file_diff_nr; }; @@ -180,6 +181,8 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps) pend = p + plain->len; while (p != pend) { char *eol = memchr(p, '\n', pend - p); + const char *deleted = NULL; + if (!eol) eol = pend; @@ -196,7 +199,11 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps) } else if (p == plain->buf) BUG("diff starts with unexpected line:\n" "%.*s\n", (int)(eol - p), p); - else if (starts_with(p, "@@ ")) { + else if (file_diff->deleted) + ; /* keep the rest of the file in a single "hunk" */ + else if (starts_with(p, "@@ ") || + (hunk == &file_diff->head && + skip_prefix(p, "deleted file", &deleted))) { file_diff->hunk_nr++; ALLOC_GROW(file_diff->hunk, file_diff->hunk_nr, file_diff->hunk_alloc); @@ -207,7 +214,9 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps) if (colored) hunk->colored_start = colored_p - colored->buf; - if (parse_hunk_header(s, hunk) < 0) + if (deleted) + file_diff->deleted = 1; + else if (parse_hunk_header(s, hunk) < 0) return -1; } From cc101c047b99e860c04e4da4075a840063ce4674 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 15 Mar 2019 17:59:11 +0100 Subject: [PATCH 46/98] built-in app -p: allow selecting a mode change as a "hunk" This imitates the way the Perl version treats mode changes: it offers the mode change up for the user to decide, as if it was a diff hunk. In contrast to the Perl version, we make use of the fact that the mode line is the first hunk, and explicitly strip out that line from the diff header if that "hunk" was not selected to be applied, and skipping that hunk while coalescing the diff. The Perl version plays some kind of diff line lego instead. Signed-off-by: Johannes Schindelin --- add-patch.c | 80 +++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 75 insertions(+), 5 deletions(-) diff --git a/add-patch.c b/add-patch.c index 0edc39c928..8b26844363 100644 --- a/add-patch.c +++ b/add-patch.c @@ -33,7 +33,7 @@ struct add_p_state { struct hunk head; struct hunk *hunk; size_t hunk_nr, hunk_alloc; - unsigned deleted:1; + unsigned deleted:1, mode_change:1; } *file_diff; size_t file_diff_nr; }; @@ -129,6 +129,14 @@ static int parse_hunk_header(struct add_p_state *s, struct hunk *hunk) return 0; } +static int is_octal(const char *p, size_t len) +{ + while (len--) + if (*p < '0' || *(p++) > '7') + return 0; + return 1; +} + static int parse_diff(struct add_p_state *s, const struct pathspec *ps) { struct argv_array args = ARGV_ARRAY_INIT; @@ -181,7 +189,7 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps) pend = p + plain->len; while (p != pend) { char *eol = memchr(p, '\n', pend - p); - const char *deleted = NULL; + const char *deleted = NULL, *mode_change = NULL; if (!eol) eol = pend; @@ -218,8 +226,30 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps) file_diff->deleted = 1; else if (parse_hunk_header(s, hunk) < 0) return -1; + } else if (hunk == &file_diff->head && + ((skip_prefix(p, "old mode ", &mode_change) || + skip_prefix(p, "new mode ", &mode_change)) && + is_octal(mode_change, eol - mode_change))) { + if (!file_diff->mode_change) { + if (file_diff->hunk_nr++) + BUG("mode change before first hunk"); + ALLOC_GROW(file_diff->hunk, file_diff->hunk_nr, + file_diff->hunk_alloc); + memset(file_diff->hunk, 0, sizeof(struct hunk)); + file_diff->hunk->start = p - plain->buf; + if (colored_p) + file_diff->hunk->colored_start = + colored_p - colored->buf; + file_diff->mode_change = 1; + } else if (file_diff->hunk_nr != 1) + BUG("mode change after first hunk?"); } + if (file_diff->deleted && file_diff->mode_change) + BUG("diff contains delete *and* a mode change?!?\n%.*s", + (int)(eol - (plain->buf + file_diff->head.start)), + plain->buf + file_diff->head.start); + p = eol == pend ? pend : eol + 1; hunk->end = p - plain->buf; @@ -233,6 +263,13 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps) hunk->colored_end = colored_p - colored->buf; } + + if (mode_change) { + file_diff->hunk->end = hunk->end; + if (colored_p) + file_diff->hunk->colored_end = + hunk->colored_end; + } } return 0; @@ -284,6 +321,39 @@ static void render_hunk(struct add_p_state *s, struct hunk *hunk, hunk->end - hunk->start); } +static void render_diff_header(struct add_p_state *s, + struct file_diff *file_diff, int colored, + struct strbuf *out) +{ + /* + * If there was a mode change, the first hunk is a pseudo hunk that + * corresponds to the mode line in the header. If the user did not want + * to stage that "hunk", we actually have to cut it out from the header. + */ + int skip_mode_change = + file_diff->mode_change && file_diff->hunk->use != USE_HUNK; + struct hunk *head = &file_diff->head, *first = file_diff->hunk; + + if (!skip_mode_change) { + render_hunk(s, head, 0, colored, out); + return; + } + + if (colored) { + const char *p = s->colored.buf; + + strbuf_add(out, p + head->colored_start, + first->colored_start - head->colored_start); + strbuf_add(out, p + first->colored_end, + head->colored_end - first->colored_end); + } else { + const char *p = s->plain.buf; + + strbuf_add(out, p + head->start, first->start - head->start); + strbuf_add(out, p + first->end, head->end - first->end); + } +} + static void reassemble_patch(struct add_p_state *s, struct file_diff *file_diff, struct strbuf *out) { @@ -291,9 +361,9 @@ static void reassemble_patch(struct add_p_state *s, size_t i; ssize_t delta = 0; - render_hunk(s, &file_diff->head, 0, 0, out); + render_diff_header(s, file_diff, 0, out); - for (i = 0; i < file_diff->hunk_nr; i++) { + for (i = file_diff->mode_change; i < file_diff->hunk_nr; i++) { hunk = file_diff->hunk + i; if (hunk->use != USE_HUNK) delta += hunk->header.old_count @@ -328,7 +398,7 @@ static int patch_update_file(struct add_p_state *s, return 0; strbuf_reset(&s->buf); - render_hunk(s, &file_diff->head, 0, colored, &s->buf); + render_diff_header(s, file_diff, colored, &s->buf); fputs(s->buf.buf, stdout); for (;;) { if (hunk_index >= file_diff->hunk_nr) From a0e4df514f0341826f7afe2d7322adefe474fb64 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Sat, 23 Mar 2019 21:33:55 +0100 Subject: [PATCH 47/98] built-in add -p: show different prompts for mode changes and deletions Just like the Perl version, we now helpfully ask the user whether they want to stage a mode change, or a deletion. Note that we define the prompts in an array, in preparation for a later patch that changes those prompts to yet different versions for `git reset -p`, `git stash -p` and `git checkout -p` (which all call the `git add -p` machinery to do the actual work). Signed-off-by: Johannes Schindelin --- add-patch.c | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/add-patch.c b/add-patch.c index 8b26844363..888040b789 100644 --- a/add-patch.c +++ b/add-patch.c @@ -7,6 +7,16 @@ #include "color.h" #include "diff.h" +enum prompt_mode_type { + PROMPT_MODE_CHANGE = 0, PROMPT_DELETION, PROMPT_HUNK +}; + +static const char *prompt_mode[] = { + N_("Stage mode change [y,n,a,d%s,?]? "), + N_("Stage deletion [y,n,a,d%s,?]? "), + N_("Stage this hunk [y,n,a,d%s,?]? ") +}; + struct hunk_header { unsigned long old_offset, old_count, new_offset, new_count; /* @@ -393,6 +403,7 @@ static int patch_update_file(struct add_p_state *s, char ch; struct child_process cp = CHILD_PROCESS_INIT; int colored = !!s->colored.len; + enum prompt_mode_type prompt_mode_type; if (!file_diff->hunk_nr) return 0; @@ -437,13 +448,20 @@ static int patch_update_file(struct add_p_state *s, strbuf_addstr(&s->buf, ",j"); if (hunk_index + 1 < file_diff->hunk_nr) strbuf_addstr(&s->buf, ",J"); + + if (file_diff->deleted) + prompt_mode_type = PROMPT_DELETION; + else if (file_diff->mode_change && !hunk_index) + prompt_mode_type = PROMPT_MODE_CHANGE; + else + prompt_mode_type = PROMPT_HUNK; + color_fprintf(stdout, s->s.prompt_color, "(%"PRIuMAX"/%"PRIuMAX") ", (uintmax_t)hunk_index + 1, (uintmax_t)file_diff->hunk_nr); color_fprintf(stdout, s->s.prompt_color, - _("Stage this hunk [y,n,a,d%s,?]? "), - s->buf.buf); + _(prompt_mode[prompt_mode_type]), s->buf.buf); fflush(stdout); if (strbuf_getline(&s->answer, stdin) == EOF) break; From b336dc342d5cc1f16ea4b1dedbe9e71504979e95 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Sun, 17 Mar 2019 21:12:41 +0100 Subject: [PATCH 48/98] built-in add -p: implement the hunk splitting feature If this developer's workflow is any indication, then this is *the* most useful feature of Git's interactive `add `command. Note: once again, this is not a verbatim conversion from the Perl code to C: the `hunk_splittable()` function, for example, essentially did all the work of splitting the hunk, just to find out whether more than one hunk would have been the result (and then tossed that result into the trash). In C we instead count the number of resulting hunks (without actually doing the work of splitting, but just counting the transitions from non-context lines to context lines), and store that information with the hunk, and we do that *while* parsing the diff in the first place. Another deviation: the built-in `git add -p` was designed with a single strbuf holding the diff (and another one holding the colored diff, if that one was asked for) in mind, and hunks essentially store just the start and end offsets pointing into that strbuf. As a consequence, when we split hunks, we now use a special mode where the hunk header is generated dynamically, and only the rest of the hunk is stored using such start/end offsets. This way, we also avoid the frequent formatting/re-parsing of the hunk header of the Perl version. Signed-off-by: Johannes Schindelin --- add-patch.c | 210 ++++++++++++++++++++++++++++++++++++- t/t3701-add-interactive.sh | 12 +++ 2 files changed, 220 insertions(+), 2 deletions(-) diff --git a/add-patch.c b/add-patch.c index 888040b789..c13418ed42 100644 --- a/add-patch.c +++ b/add-patch.c @@ -28,7 +28,7 @@ struct hunk_header { }; struct hunk { - size_t start, end, colored_start, colored_end; + size_t start, end, colored_start, colored_end, splittable_into; enum { UNDECIDED_HUNK = 0, SKIP_HUNK, USE_HUNK } use; struct hunk_header header; }; @@ -152,7 +152,7 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps) struct argv_array args = ARGV_ARRAY_INIT; struct strbuf *plain = &s->plain, *colored = NULL; struct child_process cp = CHILD_PROCESS_INIT; - char *p, *pend, *colored_p = NULL, *colored_pend = NULL; + char *p, *pend, *colored_p = NULL, *colored_pend = NULL, marker = '\0'; size_t file_diff_alloc = 0, i, color_arg_index; struct file_diff *file_diff = NULL; struct hunk *hunk = NULL; @@ -222,6 +222,13 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps) else if (starts_with(p, "@@ ") || (hunk == &file_diff->head && skip_prefix(p, "deleted file", &deleted))) { + if (marker == '-' || marker == '+') + /* + * Should not happen; previous hunk did not end + * in a context line? Handle it anyway. + */ + hunk->splittable_into++; + file_diff->hunk_nr++; ALLOC_GROW(file_diff->hunk, file_diff->hunk_nr, file_diff->hunk_alloc); @@ -236,6 +243,12 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps) file_diff->deleted = 1; else if (parse_hunk_header(s, hunk) < 0) return -1; + + /* + * Start counting into how many hunks this one can be + * split + */ + marker = *p; } else if (hunk == &file_diff->head && ((skip_prefix(p, "old mode ", &mode_change) || skip_prefix(p, "new mode ", &mode_change)) && @@ -260,6 +273,11 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps) (int)(eol - (plain->buf + file_diff->head.start)), plain->buf + file_diff->head.start); + if ((marker == '-' || marker == '+') && *p == ' ') + hunk->splittable_into++; + if (marker && *p != '\\') + marker = *p; + p = eol == pend ? pend : eol + 1; hunk->end = p - plain->buf; @@ -282,9 +300,30 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps) } } + if (marker == '-' || marker == '+') + /* + * Last hunk ended in non-context line (i.e. it appended lines + * to the file, so there are no trailing context lines). + */ + hunk->splittable_into++; + return 0; } +static size_t find_next_line(struct strbuf *sb, size_t offset) +{ + char *eol; + + if (offset >= sb->len) + BUG("looking for next line beyond buffer (%d >= %d)\n%s", + (int)offset, (int)sb->len, sb->buf); + + eol = memchr(sb->buf + offset, '\n', sb->len - offset); + if (!eol) + return sb->len; + return eol - sb->buf + 1; +} + static void render_hunk(struct add_p_state *s, struct hunk *hunk, ssize_t delta, int colored, struct strbuf *out) { @@ -383,6 +422,161 @@ static void reassemble_patch(struct add_p_state *s, } } +static int split_hunk(struct add_p_state *s, struct file_diff *file_diff, + size_t hunk_index) +{ + int colored = !!s->colored.len, first = 1; + struct hunk *hunk = file_diff->hunk + hunk_index; + size_t splittable_into; + size_t end, colored_end, current, colored_current = 0, context_line_count; + struct hunk_header remaining, *header; + char marker, ch; + + if (hunk_index >= file_diff->hunk_nr) + BUG("invalid hunk index: %d (must be >= 0 and < %d)", + (int)hunk_index, (int)file_diff->hunk_nr); + + if (hunk->splittable_into < 2) + return 0; + splittable_into = hunk->splittable_into; + + end = hunk->end; + colored_end = hunk->colored_end; + + memcpy(&remaining, &hunk->header, sizeof(remaining)); + + file_diff->hunk_nr += splittable_into - 1; + ALLOC_GROW(file_diff->hunk, file_diff->hunk_nr, file_diff->hunk_alloc); + if (hunk_index + splittable_into < file_diff->hunk_nr) + memmove(file_diff->hunk + hunk_index + splittable_into, + file_diff->hunk + hunk_index + 1, + (file_diff->hunk_nr - hunk_index - splittable_into) + * sizeof(*hunk)); + hunk = file_diff->hunk + hunk_index; + hunk->splittable_into = 1; + memset(hunk + 1, 0, (splittable_into - 1) * sizeof(*hunk)); + + header = &hunk->header; + header->old_count = header->new_count = 0; + + current = hunk->start; + if (colored) + colored_current = hunk->colored_start; + marker = '\0'; + context_line_count = 0; + + while (splittable_into > 1) { + ch = s->plain.buf[current]; + + if (!ch) + BUG("buffer overrun while splitting hunks"); + + /* + * Is this the first context line after a chain of +/- lines? + * Then record the start of the next split hunk. + */ + if ((marker == '-' || marker == '+') && ch == ' ') { + first = 0; + hunk[1].start = current; + if (colored) + hunk[1].colored_start = colored_current; + context_line_count = 0; + } + + /* + * Was the previous line a +/- one? Alternatively, is this the + * first line (and not a +/- one)? + * + * Then just increment the appropriate counter and continue + * with the next line. + * + * Otherwise this is the first of a chain of +/- lines. + * neither the first of a chain of context lines? + */ + if (marker != ' ' || (ch != '-' && ch != '+')) { +next_hunk_line: + /* Comment lines are attached to the previous line */ + if (ch == '\\') + ch = marker ? marker : ' '; + + /* current hunk not done yet */ + if (ch == ' ') + context_line_count++; + else if (ch == '-') + header->old_count++; + else if (ch == '+') + header->new_count++; + else + BUG("unhandled diff marker: '%c'", ch); + marker = ch; + current = find_next_line(&s->plain, current); + if (colored) + colored_current = + find_next_line(&s->colored, + colored_current); + continue; + } + + if (first) { + if (header->old_count || header->new_count) + BUG("counts are off: %d/%d", + (int)header->old_count, + (int)header->new_count); + + header->old_count = context_line_count; + header->new_count = context_line_count; + context_line_count = 0; + first = 0; + goto next_hunk_line; + } + + remaining.old_offset += header->old_count; + remaining.old_count -= header->old_count; + remaining.new_offset += header->new_count; + remaining.new_count -= header->new_count; + + /* initialize next hunk header's offsets */ + hunk[1].header.old_offset = + header->old_offset + header->old_count; + hunk[1].header.new_offset = + header->new_offset + header->new_count; + + /* add one split hunk */ + header->old_count += context_line_count; + header->new_count += context_line_count; + + hunk->end = current; + if (colored) + hunk->colored_end = colored_current; + + hunk++; + hunk->splittable_into = 1; + hunk->use = hunk[-1].use; + header = &hunk->header; + + header->old_count = header->new_count = context_line_count; + context_line_count = 0; + + splittable_into--; + marker = ch; + } + + /* last hunk simply gets the rest */ + if (header->old_offset != remaining.old_offset) + BUG("miscounted old_offset: %lu != %lu", + header->old_offset, remaining.old_offset); + if (header->new_offset != remaining.new_offset) + BUG("miscounted new_offset: %lu != %lu", + header->new_offset, remaining.new_offset); + header->old_count = remaining.old_count; + header->new_count = remaining.new_count; + hunk->end = end; + if (colored) + hunk->colored_end = colored_end; + + return 0; +} + static const char help_patch_text[] = N_("y - stage this hunk\n" "n - do not stage this hunk\n" @@ -392,6 +586,7 @@ N_("y - stage this hunk\n" "J - leave this hunk undecided, see next hunk\n" "k - leave this hunk undecided, see previous undecided hunk\n" "K - leave this hunk undecided, see previous hunk\n" + "s - split the current hunk into smaller hunks\n" "? - print help\n"); static int patch_update_file(struct add_p_state *s, @@ -448,6 +643,8 @@ static int patch_update_file(struct add_p_state *s, strbuf_addstr(&s->buf, ",j"); if (hunk_index + 1 < file_diff->hunk_nr) strbuf_addstr(&s->buf, ",J"); + if (hunk->splittable_into > 1) + strbuf_addstr(&s->buf, ",s"); if (file_diff->deleted) prompt_mode_type = PROMPT_DELETION; @@ -512,6 +709,15 @@ soft_increment: hunk_index = undecided_next; else err(s, _("No next hunk")); + } else if (s->answer.buf[0] == 's') { + size_t splittable_into = hunk->splittable_into; + if (splittable_into < 2) + err(s, _("Sorry, cannot split this hunk")); + else if (!split_hunk(s, file_diff, + hunk - file_diff->hunk)) + color_fprintf_ln(stdout, s->s.header_color, + _("Split into %d hunks."), + (int)splittable_into); } else color_fprintf(stdout, s->s.help_color, _(help_patch_text)); diff --git a/t/t3701-add-interactive.sh b/t/t3701-add-interactive.sh index 5db6432e33..fe383be50e 100755 --- a/t/t3701-add-interactive.sh +++ b/t/t3701-add-interactive.sh @@ -442,6 +442,18 @@ test_expect_failure 'split hunk "add -p (no, yes, edit)"' ' ! grep "^+31" actual ' +test_expect_success 'split hunk with incomplete line at end' ' + git reset --hard && + printf "missing LF" >>test && + git add test && + test_write_lines before 10 20 30 40 50 60 70 >test && + git grep --cached missing && + test_write_lines s n y q | git add -p && + test_must_fail git grep --cached missing && + git grep before && + test_must_fail git grep --cached before +' + test_expect_failure 'edit, adding lines to the first hunk' ' test_write_lines 10 11 20 30 40 50 51 60 >test && git reset && From 45499e91b3d7418688950fa16410519b842771e8 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 19 Mar 2019 20:50:14 +0100 Subject: [PATCH 49/98] built-in add -p: coalesce hunks after splitting them This is considered "the right thing to do", according to 933e44d3a0 ("add -p": work-around an old laziness that does not coalesce hunks, 2011-04-06). Note: we cannot simply modify the hunks while merging them; Once we implement hunk editing, we will call `reassemble_patch()` whenever a hunk is edited, therefore we must not modify the hunks (because the user might e.g. hit `K` and change their mind whether to stage the previous hunk). Signed-off-by: Johannes Schindelin --- add-patch.c | 51 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/add-patch.c b/add-patch.c index c13418ed42..79a08adcee 100644 --- a/add-patch.c +++ b/add-patch.c @@ -403,6 +403,48 @@ static void render_diff_header(struct add_p_state *s, } } +/* Coalesce hunks again that were split */ +static int merge_hunks(struct add_p_state *s, struct file_diff *file_diff, + size_t *hunk_index, struct hunk *temp) +{ + size_t i = *hunk_index; + struct hunk *hunk = file_diff->hunk + i; + struct hunk_header *header = &temp->header, *next; + + if (hunk->use != USE_HUNK) + return 0; + + memcpy(temp, hunk, sizeof(*temp)); + /* We simply skip the colored part (if any) when merging hunks */ + temp->colored_start = temp->colored_end = 0; + + for (; i + 1 < file_diff->hunk_nr; i++) { + hunk++; + next = &hunk->header; + + if (hunk->use != USE_HUNK || + header->new_offset >= next->new_offset || + header->new_offset + header->new_count < next->new_offset || + temp->start >= hunk->start || + temp->end < hunk->start) + break; + + temp->end = hunk->end; + temp->colored_end = hunk->colored_end; + + header->old_count = next->old_offset + next->old_count + - header->old_offset; + header->new_count = next->new_offset + next->new_count + - header->new_offset; + } + + if (i == *hunk_index) + return 0; + + *hunk_index = i; + return 1; +} + static void reassemble_patch(struct add_p_state *s, struct file_diff *file_diff, struct strbuf *out) { @@ -413,12 +455,19 @@ static void reassemble_patch(struct add_p_state *s, render_diff_header(s, file_diff, 0, out); for (i = file_diff->mode_change; i < file_diff->hunk_nr; i++) { + struct hunk temp = { 0 }; + hunk = file_diff->hunk + i; if (hunk->use != USE_HUNK) delta += hunk->header.old_count - hunk->header.new_count; - else + else { + /* merge overlapping hunks into a temporary hunk */ + if (merge_hunks(s, file_diff, &i, &temp)) + hunk = &temp; + render_hunk(s, hunk, delta, 0, out); + } } } From 991ff42d62a359817ae6e5d356f2dc9690df4bdb Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 19 Mar 2019 22:27:51 +0100 Subject: [PATCH 50/98] built-in add -p: implement hunk editing Just like `git add --edit` allows the user to edit the diff before it is being applied to the index, this feature allows the user to edit the diff *hunk*. Naturally, it gets a bit more complicated here because the result has to play well with the remaining hunks of the overall diff. Therefore, we have to do a loop in which we let the user edit the hunk, then test whether the result would work, and if not, drop the edits and let the user decide whether to try editing the hunk again. Note: in contrast to the Perl version, we use the same diff "coalescing" (i.e. merging overlapping hunks into a single one) also for the check after editing, and we introduce a new flag for that purpose that asks the `reassemble_patch()` function to pretend that all hunks were selected for use. This allows us to continue to run `git apply` *without* the `--allow-overlap` option (unlike the Perl version), and it also fixes two known breakages in `t3701-add-interactive.sh` (which we cannot mark as resolved so far because the Perl script version is still the default and continues to have those breakages). Signed-off-by: Johannes Schindelin --- add-interactive.c | 6 + add-interactive.h | 3 + add-patch.c | 351 +++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 343 insertions(+), 17 deletions(-) diff --git a/add-interactive.c b/add-interactive.c index fc0479a2bc..0671236e41 100644 --- a/add-interactive.c +++ b/add-interactive.c @@ -46,6 +46,12 @@ void init_add_i_state(struct add_i_state *s, struct repository *r) init_color(r, s, "reset", s->reset_color, GIT_COLOR_RESET); init_color(r, s, "fraginfo", s->fraginfo_color, diff_get_color(s->use_color, DIFF_FRAGINFO)); + init_color(r, s, "context", s->context_color, + diff_get_color(s->use_color, DIFF_CONTEXT)); + init_color(r, s, "old", s->file_old_color, + 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)); } /* diff --git a/add-interactive.h b/add-interactive.h index 0a3ee3474a..0f87fee209 100644 --- a/add-interactive.h +++ b/add-interactive.h @@ -12,6 +12,9 @@ struct add_i_state { char error_color[COLOR_MAXLEN]; char reset_color[COLOR_MAXLEN]; char fraginfo_color[COLOR_MAXLEN]; + char context_color[COLOR_MAXLEN]; + char file_old_color[COLOR_MAXLEN]; + char file_new_color[COLOR_MAXLEN]; }; void init_add_i_state(struct add_i_state *s, struct repository *r); diff --git a/add-patch.c b/add-patch.c index 79a08adcee..4b9d9d5066 100644 --- a/add-patch.c +++ b/add-patch.c @@ -29,6 +29,7 @@ struct hunk_header { struct hunk { size_t start, end, colored_start, colored_end, splittable_into; + ssize_t delta; enum { UNDECIDED_HUNK = 0, SKIP_HUNK, USE_HUNK } use; struct hunk_header header; }; @@ -405,13 +406,13 @@ static void render_diff_header(struct add_p_state *s, /* Coalesce hunks again that were split */ static int merge_hunks(struct add_p_state *s, struct file_diff *file_diff, - size_t *hunk_index, struct hunk *temp) + size_t *hunk_index, int use_all, struct hunk *temp) { - size_t i = *hunk_index; + size_t i = *hunk_index, delta; struct hunk *hunk = file_diff->hunk + i; struct hunk_header *header = &temp->header, *next; - if (hunk->use != USE_HUNK) + if (!use_all && hunk->use != USE_HUNK) return 0; memcpy(temp, hunk, sizeof(*temp)); @@ -422,20 +423,94 @@ static int merge_hunks(struct add_p_state *s, struct file_diff *file_diff, hunk++; next = &hunk->header; - if (hunk->use != USE_HUNK || - header->new_offset >= next->new_offset || - header->new_offset + header->new_count < next->new_offset || - temp->start >= hunk->start || - temp->end < hunk->start) + if ((!use_all && hunk->use != USE_HUNK) || + header->new_offset >= next->new_offset + temp->delta || + header->new_offset + header->new_count + < next->new_offset + temp->delta) break; - temp->end = hunk->end; - temp->colored_end = hunk->colored_end; + if (temp->start < hunk->start && temp->end > hunk->start) { + temp->end = hunk->end; + temp->colored_end = hunk->colored_end; + delta = 0; + } else { + const char *plain = s->plain.buf; + size_t overlapping_line_count = header->new_offset + + header->new_count - temp->delta + - next->new_offset; + size_t overlap_end = hunk->start; + size_t overlap_start = overlap_end; + size_t overlap_next, len, i; + + /* + * One of the hunks was edited; let's ensure that at + * least the last context line of the first hunk + * overlaps with the corresponding line of the second + * hunk, and then merge. + */ + + for (i = 0; i < overlapping_line_count; i++) { + overlap_next = find_next_line(&s->plain, + overlap_end); + + if (overlap_next > hunk->end) + BUG("failed to find %d context lines " + "in:\n%.*s", + (int)overlapping_line_count, + (int)(hunk->end - hunk->start), + plain + hunk->start); + + if (plain[overlap_end] != ' ') + return error(_("expected context line " + "#%d in\n%.*s"), + (int)(i + 1), + (int)(hunk->end + - hunk->start), + plain + hunk->start); + + overlap_start = overlap_end; + overlap_end = overlap_next; + } + len = overlap_end - overlap_start; + + if (len > temp->end - temp->start || + memcmp(plain + temp->end - len, + plain + overlap_start, len)) + return error(_("hunks do not overlap:\n%.*s\n" + "\tdoes not end with:\n%.*s"), + (int)(temp->end - temp->start), + plain + temp->start, + (int)len, plain + overlap_start); + + /* + * Since the start-end ranges are not adjacent, we + * cannot simply take the union of the ranges. To + * address that, we temporarily append the union of the + * lines to the `plain` strbuf. + */ + if (temp->end != s->plain.len) { + size_t start = s->plain.len; + + strbuf_add(&s->plain, plain + temp->start, + temp->end - temp->start); + plain = s->plain.buf; + temp->start = start; + temp->end = s->plain.len; + } + + strbuf_add(&s->plain, + plain + overlap_end, + hunk->end - overlap_end); + temp->end = s->plain.len; + temp->splittable_into += hunk->splittable_into; + delta = temp->delta; + temp->delta += hunk->delta; + } header->old_count = next->old_offset + next->old_count - header->old_offset; - header->new_count = next->new_offset + next->new_count - - header->new_offset; + header->new_count = next->new_offset + delta + + next->new_count - header->new_offset; } if (i == *hunk_index) @@ -446,10 +521,11 @@ static int merge_hunks(struct add_p_state *s, struct file_diff *file_diff, } static void reassemble_patch(struct add_p_state *s, - struct file_diff *file_diff, struct strbuf *out) + struct file_diff *file_diff, int use_all, + struct strbuf *out) { struct hunk *hunk; - size_t i; + size_t save_len = s->plain.len, i; ssize_t delta = 0; render_diff_header(s, file_diff, 0, out); @@ -458,15 +534,24 @@ static void reassemble_patch(struct add_p_state *s, struct hunk temp = { 0 }; hunk = file_diff->hunk + i; - if (hunk->use != USE_HUNK) + if (!use_all && hunk->use != USE_HUNK) delta += hunk->header.old_count - hunk->header.new_count; else { /* merge overlapping hunks into a temporary hunk */ - if (merge_hunks(s, file_diff, &i, &temp)) + if (merge_hunks(s, file_diff, &i, use_all, &temp)) hunk = &temp; render_hunk(s, hunk, delta, 0, out); + + /* + * In case `merge_hunks()` used `plain` as a scratch + * pad (this happens when an edited hunk had to be + * coalesced with another hunk). + */ + strbuf_setlen(&s->plain, save_len); + + delta += hunk->delta; } } } @@ -626,6 +711,227 @@ next_hunk_line: return 0; } +static void recolor_hunk(struct add_p_state *s, struct hunk *hunk) +{ + const char *plain = s->plain.buf; + size_t current, eol, next; + + if (!s->colored.len) + return; + + hunk->colored_start = s->colored.len; + for (current = hunk->start; current < hunk->end; ) { + for (eol = current; eol < hunk->end; eol++) + if (plain[eol] == '\n') + break; + next = eol + (eol < hunk->end); + if (eol > current && plain[eol - 1] == '\r') + eol--; + + strbuf_addstr(&s->colored, + plain[current] == '-' ? + s->s.file_old_color : + plain[current] == '+' ? + s->s.file_new_color : + s->s.context_color); + strbuf_add(&s->colored, plain + current, eol - current); + strbuf_addstr(&s->colored, GIT_COLOR_RESET); + if (next > eol) + strbuf_add(&s->colored, plain + eol, next - eol); + current = next; + } + hunk->colored_end = s->colored.len; +} + +static int edit_hunk_manually(struct add_p_state *s, struct hunk *hunk) +{ + char *path = xstrdup(git_path("addp-hunk-edit.diff")); + int fd = xopen(path, O_WRONLY | O_CREAT | O_TRUNC, 0666); + struct strbuf buf = STRBUF_INIT; + size_t i, j; + int res, copy; + + if (fd < 0) { + res = error_errno(_("could not open '%s' for writing"), path); + goto edit_hunk_manually_finish; + } + + strbuf_commented_addf(&buf, _("Manual hunk edit mode -- see bottom for " + "a quick guide.\n")); + render_hunk(s, hunk, 0, 0, &buf); + strbuf_commented_addf(&buf, + _("---\n" + "To remove '%c' lines, make them ' ' lines " + "(context).\n" + "To remove '%c' lines, delete them.\n" + "Lines starting with %c will be removed.\n"), + '-', '+', comment_line_char); + strbuf_commented_addf(&buf, + _("If the patch applies cleanly, the edited hunk " + "will immediately be\n" + "marked for staging.\n")); + /* + * TRANSLATORS: 'it' refers to the patch mentioned in the previous + * messages. + */ + strbuf_commented_addf(&buf, + _("If it does not apply cleanly, you will be " + "given an opportunity to\n" + "edit again. If all lines of the hunk are " + "removed, then the edit is\n" + "aborted and the hunk is left unchanged.\n")); + if (write_in_full(fd, buf.buf, buf.len) < 0) { + res = error_errno(_("could not write to '%s'"), path); + goto edit_hunk_manually_finish; + } + + res = close(fd); + fd = -1; + if (res < 0) + goto edit_hunk_manually_finish; + + hunk->start = s->plain.len; + if (launch_editor(path, &s->plain, NULL) < 0) { + res = error_errno(_("could not edit '%s'"), path); + goto edit_hunk_manually_finish; + } + unlink(path); + + /* strip out commented lines */ + copy = s->plain.buf[hunk->start] != comment_line_char; + for (i = j = hunk->start; i < s->plain.len; ) { + if (copy) + s->plain.buf[j++] = s->plain.buf[i]; + if (s->plain.buf[i++] == '\n') + copy = s->plain.buf[i] != comment_line_char; + } + + if (j == hunk->start) + /* User aborted by deleting everything */ + goto edit_hunk_manually_finish; + + res = 1; + strbuf_setlen(&s->plain, j); + hunk->end = j; + recolor_hunk(s, hunk); + if (s->plain.buf[hunk->start] == '@' && + /* If the hunk header was deleted, simply use the original one. */ + parse_hunk_header(s, hunk) < 0) + res = -1; + +edit_hunk_manually_finish: + if (fd >= 0) + close(fd); + free(path); + strbuf_release(&buf); + + return res; +} + +static ssize_t recount_edited_hunk(struct add_p_state *s, struct hunk *hunk, + size_t orig_old_count, size_t orig_new_count) +{ + struct hunk_header *header = &hunk->header; + size_t i; + + header->old_count = header->new_count = 0; + for (i = hunk->start; i < hunk->end; ) { + switch (s->plain.buf[i]) { + case '-': + header->old_count++; + break; + case '+': + header->new_count++; + break; + case ' ': case '\r': case '\n': + header->old_count++; + header->new_count++; + break; + } + + i = find_next_line(&s->plain, i); + } + + return orig_old_count - orig_new_count + - header->old_count + header->new_count; +} + +static int run_apply_check(struct add_p_state *s, + struct file_diff *file_diff) +{ + struct child_process cp = CHILD_PROCESS_INIT; + + strbuf_reset(&s->buf); + reassemble_patch(s, file_diff, 1, &s->buf); + + setup_child_process(&cp, s, + "apply", "--cached", "--check", NULL); + if (pipe_command(&cp, s->buf.buf, s->buf.len, NULL, 0, NULL, 0)) + return error(_("'git apply --cached' failed")); + + 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) + return -1; + strbuf_trim_trailing_newline(&s->answer); + switch (tolower(s->answer.buf[0])) { + case 'n': return 0; + case 'y': return 1; + } + } +} + +static int edit_hunk_loop(struct add_p_state *s, + struct file_diff *file_diff, struct hunk *hunk) +{ + size_t plain_len = s->plain.len, colored_len = s->colored.len; + struct hunk backup; + + memcpy(&backup, hunk, sizeof(backup)); + + for (;;) { + int res = edit_hunk_manually(s, hunk); + if (res == 0) { + /* abandonded */ + memcpy(hunk, &backup, sizeof(backup)); + return -1; + } + + if (res > 0) { + hunk->delta += + recount_edited_hunk(s, hunk, + backup.header.old_count, + backup.header.new_count); + if (!run_apply_check(s, file_diff)) + return 0; + } + + /* Drop edits (they were appended to s->plain) */ + strbuf_setlen(&s->plain, plain_len); + strbuf_setlen(&s->colored, colored_len); + memcpy(hunk, &backup, sizeof(backup)); + + /* + * TRANSLATORS: do not translate [y/n] + * The program will only accept that input at this point. + * Consider translating (saying "no" discards!) as + * (saying "n" for "no" discards!) if the translation + * of the word "no" does not start with n. + */ + res = prompt_yesno(s, _("Your edited hunk does not apply. " + "Edit again (saying \"no\" discards!) " + "[y/n]? ")); + if (res < 1) + return -1; + } +} + static const char help_patch_text[] = N_("y - stage this hunk\n" "n - do not stage this hunk\n" @@ -636,6 +942,7 @@ N_("y - stage this hunk\n" "k - leave this hunk undecided, see previous undecided hunk\n" "K - leave this hunk undecided, see previous hunk\n" "s - split the current hunk into smaller hunks\n" + "e - manually edit the current hunk\n" "? - print help\n"); static int patch_update_file(struct add_p_state *s, @@ -694,6 +1001,9 @@ static int patch_update_file(struct add_p_state *s, strbuf_addstr(&s->buf, ",J"); if (hunk->splittable_into > 1) strbuf_addstr(&s->buf, ",s"); + if (hunk_index + 1 > file_diff->mode_change && + !file_diff->deleted) + strbuf_addstr(&s->buf, ",e"); if (file_diff->deleted) prompt_mode_type = PROMPT_DELETION; @@ -767,6 +1077,13 @@ soft_increment: color_fprintf_ln(stdout, s->s.header_color, _("Split into %d hunks."), (int)splittable_into); + } else if (s->answer.buf[0] == 'e') { + if (hunk_index + 1 == file_diff->mode_change) + err(s, _("Sorry, cannot edit this hunk")); + else if (edit_hunk_loop(s, file_diff, hunk) >= 0) { + hunk->use = USE_HUNK; + goto soft_increment; + } } else color_fprintf(stdout, s->s.help_color, _(help_patch_text)); @@ -780,7 +1097,7 @@ soft_increment: if (i < file_diff->hunk_nr) { /* At least one hunk selected: apply */ strbuf_reset(&s->buf); - reassemble_patch(s, file_diff, &s->buf); + reassemble_patch(s, file_diff, 0, &s->buf); discard_index(s->s.r->index); setup_child_process(&cp, s, "apply", "--cached", NULL); From 8fca5f8ad5eb768a719f50c3284b5dc445d5db75 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Sat, 23 Mar 2019 22:33:49 +0100 Subject: [PATCH 51/98] built-in add -p: prepare for patch modes other than "stage" The Perl script backing `git add -p` is used not only for that command, but also for `git stash -p`, `git reset -p` and `git checkout -p`. In preparation for teaching the C version of `git add -p` to support also the latter commands, let's abstract away what is "stage" specific into a dedicated data structure describing the differences between the patch modes. As we prepare for calling the built-in `git add -p` in `run_add_interactive()` via code paths that have not let `add_config()` do its work, we have to make sure to re-parse the config using that function in those cases. Finally, please note that the Perl version tries to make sure that the diffs are only generated for the modified files. This is not actually necessary, as the calls to Git's diff machinery already perform that work, and perform it well. This makes it unnecessary to port the `FILTER` field of the `%patch_modes` struct, as well as the `get_diff_reference()` function. Signed-off-by: Johannes Schindelin --- add-interactive.c | 2 +- add-interactive.h | 8 ++++- add-patch.c | 88 +++++++++++++++++++++++++++++++++-------------- builtin/add.c | 12 +++++-- 4 files changed, 81 insertions(+), 29 deletions(-) diff --git a/add-interactive.c b/add-interactive.c index 0671236e41..daf95de2d8 100644 --- a/add-interactive.c +++ b/add-interactive.c @@ -911,7 +911,7 @@ static int run_patch(struct add_i_state *s, const struct pathspec *ps, parse_pathspec(&ps_selected, PATHSPEC_ALL_MAGIC & ~PATHSPEC_LITERAL, PATHSPEC_LITERAL_PATH, "", args.argv); - res = run_add_p(s->r, &ps_selected); + res = run_add_p(s->r, ADD_P_STAGE, NULL, &ps_selected); argv_array_clear(&args); clear_pathspec(&ps_selected); } diff --git a/add-interactive.h b/add-interactive.h index 0f87fee209..05d7505a6c 100644 --- a/add-interactive.h +++ b/add-interactive.h @@ -31,6 +31,12 @@ const char *get_add_i_color(enum color_add_i ix); struct repository; struct pathspec; int run_add_i(struct repository *r, const struct pathspec *ps); -int run_add_p(struct repository *r, const struct pathspec *ps); + +enum add_p_mode { + ADD_P_STAGE, +}; + +int run_add_p(struct repository *r, enum add_p_mode mode, + const char *revision, const struct pathspec *ps); #endif diff --git a/add-patch.c b/add-patch.c index 6e7d9d1569..a0d950e9c9 100644 --- a/add-patch.c +++ b/add-patch.c @@ -11,10 +11,33 @@ enum prompt_mode_type { PROMPT_MODE_CHANGE = 0, PROMPT_DELETION, PROMPT_HUNK }; -static const char *prompt_mode[] = { - N_("Stage mode change [y,n,a,q,d%s,?]? "), - N_("Stage deletion [y,n,a,q,d%s,?]? "), - N_("Stage this hunk [y,n,a,q,d%s,?]? ") +struct patch_mode { + const char *diff[4], *apply[4], *apply_check[4]; + unsigned is_reverse:1, apply_for_checkout:1; + const char *prompt_mode[PROMPT_HUNK + 1]; + const char *edit_hunk_hint, *help_patch_text; +}; + +static struct patch_mode patch_mode_stage = { + .diff = { "diff-files", NULL }, + .apply = { "--cached", NULL }, + .apply_check = { "--cached", NULL }, + .is_reverse = 0, + .prompt_mode = { + N_("Stage mode change [y,n,q,a,d%s,?]? "), + N_("Stage deletion [y,n,q,a,d%s,?]? "), + N_("Stage this hunk [y,n,q,a,d%s,?]? ") + }, + .edit_hunk_hint = N_("If the patch applies cleanly, the edited hunk " + "will immediately be marked for staging."), + .help_patch_text = + N_("y - stage this hunk\n" + "n - do not stage this hunk\n" + "q - quit; do not stage this hunk or any of the remaining " + "ones\n" + "a - stage this hunk and all later hunks in the file\n" + "d - do not stage this hunk or any of the later hunks in " + "the file\n") }; struct hunk_header { @@ -47,6 +70,10 @@ struct add_p_state { unsigned deleted:1, mode_change:1,binary:1; } *file_diff; size_t file_diff_nr; + + /* patch mode */ + struct patch_mode *mode; + const char *revision; }; static void err(struct add_p_state *s, const char *fmt, ...) @@ -159,9 +186,18 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps) struct hunk *hunk = NULL; int res; + argv_array_pushv(&args, s->mode->diff); + if (s->revision) { + struct object_id oid; + argv_array_push(&args, + /* could be on an unborn branch */ + !strcmp("HEAD", s->revision) && + get_oid("HEAD", &oid) ? + empty_tree_oid_hex() : s->revision); + } + color_arg_index = args.argc; /* Use `--no-color` explicitly, just in case `diff.color = always`. */ - argv_array_pushl(&args, "diff-files", "-p", "--no-color", "--", NULL); - color_arg_index = args.argc - 2; + argv_array_pushl(&args, "--no-color", "-p", "--", NULL); for (i = 0; i < ps->nr; i++) argv_array_push(&args, ps->items[i].original); @@ -352,7 +388,10 @@ static void render_hunk(struct add_p_state *s, struct hunk *hunk, - header->colored_extra_start; } - new_offset += delta; + if (s->mode->is_reverse) + old_offset -= delta; + else + new_offset += delta; strbuf_addf(out, "@@ -%lu,%lu +%lu,%lu @@", old_offset, header->old_count, @@ -767,11 +806,10 @@ static int edit_hunk_manually(struct add_p_state *s, struct hunk *hunk) "(context).\n" "To remove '%c' lines, delete them.\n" "Lines starting with %c will be removed.\n"), - '-', '+', comment_line_char); - strbuf_commented_addf(&buf, - _("If the patch applies cleanly, the edited hunk " - "will immediately be\n" - "marked for staging.\n")); + s->mode->is_reverse ? '+' : '-', + s->mode->is_reverse ? '-' : '+', + comment_line_char); + strbuf_commented_addf(&buf, "%s", _(s->mode->edit_hunk_hint)); /* * TRANSLATORS: 'it' refers to the patch mentioned in the previous * messages. @@ -867,7 +905,8 @@ static int run_apply_check(struct add_p_state *s, reassemble_patch(s, file_diff, 1, &s->buf); setup_child_process(&cp, s, - "apply", "--cached", "--check", NULL); + "apply", "--check", NULL); + argv_array_pushv(&cp.args, s->mode->apply_check); if (pipe_command(&cp, s->buf.buf, s->buf.len, NULL, 0, NULL, 0)) return error(_("'git apply --cached' failed")); @@ -982,13 +1021,6 @@ static size_t display_hunks(struct add_p_state *s, return end_index; } -static const char help_patch_text[] = -N_("y - stage this hunk\n" - "n - do not stage this hunk\n" - "q - quit; do not stage this hunk or any of the remaining ones\n" - "a - stage this and all the remaining hunks\n" - "d - do not stage this hunk nor any of the remaining hunks\n"); - static const char help_patch_remainder[] = N_("j - leave this hunk undecided, see next undecided hunk\n" "J - leave this hunk undecided, see next hunk\n" @@ -1074,7 +1106,8 @@ static int patch_update_file(struct add_p_state *s, (uintmax_t)hunk_index + 1, (uintmax_t)file_diff->hunk_nr); color_fprintf(stdout, s->s.prompt_color, - _(prompt_mode[prompt_mode_type]), s->buf.buf); + _(s->mode->prompt_mode[prompt_mode_type]), + s->buf.buf); fflush(stdout); if (strbuf_getline(&s->answer, stdin) == EOF) break; @@ -1231,7 +1264,7 @@ soft_increment: const char *p = _(help_patch_remainder), *eol = p; color_fprintf(stdout, s->s.help_color, "%s", - _(help_patch_text)); + _(s->mode->help_patch_text)); /* * Show only those lines of the remainder that are @@ -1265,10 +1298,11 @@ soft_increment: reassemble_patch(s, file_diff, 0, &s->buf); discard_index(s->s.r->index); - setup_child_process(&cp, s, "apply", "--cached", NULL); + setup_child_process(&cp, s, "apply", NULL); + argv_array_pushv(&cp.args, s->mode->apply); if (pipe_command(&cp, s->buf.buf, s->buf.len, NULL, 0, NULL, 0)) - error(_("'git apply --cached' failed")); + error(_("'git apply' failed")); if (!repo_read_index(s->s.r)) repo_refresh_and_write_index(s->s.r, REFRESH_QUIET, 0, 1, NULL, NULL, NULL); @@ -1278,7 +1312,8 @@ soft_increment: return quit; } -int run_add_p(struct repository *r, const struct pathspec *ps) +int run_add_p(struct repository *r, enum add_p_mode mode, + const char *revision, const struct pathspec *ps) { struct add_p_state s = { { r }, STRBUF_INIT, STRBUF_INIT, STRBUF_INIT, STRBUF_INIT @@ -1287,6 +1322,9 @@ int run_add_p(struct repository *r, const struct pathspec *ps) init_add_i_state(&s.s, r); + s.mode = &patch_mode_stage; + s.revision = revision; + 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 || diff --git a/builtin/add.c b/builtin/add.c index cf02e63ae0..6806637185 100644 --- a/builtin/add.c +++ b/builtin/add.c @@ -181,6 +181,8 @@ static void refresh(int verbose, const struct pathspec *pathspec) free(seen); } +static int add_config(const char *var, const char *value, void *cb); + int run_add_interactive(const char *revision, const char *patch_mode, const struct pathspec *pathspec) { @@ -193,12 +195,18 @@ int run_add_interactive(const char *revision, const char *patch_mode, &use_builtin_add_i); if (use_builtin_add_i == 1) { + enum add_p_mode mode; + if (!patch_mode) return !!run_add_i(the_repository, pathspec); - if (strcmp(patch_mode, "--patch")) + + if (!strcmp(patch_mode, "--patch")) + mode = ADD_P_STAGE; + else die("'%s' not yet supported in the built-in add -p", patch_mode); - return !!run_add_p(the_repository, pathspec); + + return !!run_add_p(the_repository, mode, revision, pathspec); } argv_array_push(&argv, "add--interactive"); From 9cd1a973bd2d822edbd84a73524a08a585d44e79 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 21 Mar 2019 23:50:53 +0100 Subject: [PATCH 52/98] built-in add -p: implement the 'g' ("goto") command With this patch, it is now possible to see a summary of the available hunks and to navigate between them (by number). A test is added to verify that this behavior matches the one of the Perl version of `git add -p`. Signed-off-by: Johannes Schindelin --- add-patch.c | 86 ++++++++++++++++++++++++++++++++++++++ t/t3701-add-interactive.sh | 16 +++++++ 2 files changed, 102 insertions(+) diff --git a/add-patch.c b/add-patch.c index 4b9d9d5066..f79f9271d9 100644 --- a/add-patch.c +++ b/add-patch.c @@ -932,6 +932,54 @@ static int edit_hunk_loop(struct add_p_state *s, } } +#define SUMMARY_HEADER_WIDTH 20 +#define SUMMARY_LINE_WIDTH 80 +static void summarize_hunk(struct add_p_state *s, struct hunk *hunk, + struct strbuf *out) +{ + struct hunk_header *header = &hunk->header; + struct strbuf *plain = &s->plain; + size_t len = out->len, i; + + strbuf_addf(out, " -%lu,%lu +%lu,%lu ", + header->old_offset, header->old_count, + header->new_offset, header->new_count); + if (out->len - len < SUMMARY_HEADER_WIDTH) + strbuf_addchars(out, ' ', + SUMMARY_HEADER_WIDTH + len - out->len); + for (i = hunk->start; i < hunk->end; i = find_next_line(plain, i)) + if (plain->buf[i] != ' ') + break; + if (i < hunk->end) + strbuf_add(out, plain->buf + i, find_next_line(plain, i) - i); + if (out->len - len > SUMMARY_LINE_WIDTH) + strbuf_setlen(out, len + SUMMARY_LINE_WIDTH); + strbuf_complete_line(out); +} + +#define DISPLAY_HUNKS_LINES 20 +static size_t display_hunks(struct add_p_state *s, + struct file_diff *file_diff, size_t start_index) +{ + size_t end_index = start_index + DISPLAY_HUNKS_LINES; + + if (end_index > file_diff->hunk_nr) + end_index = file_diff->hunk_nr; + + while (start_index < end_index) { + struct hunk *hunk = file_diff->hunk + start_index++; + + strbuf_reset(&s->buf); + strbuf_addf(&s->buf, "%c%2d: ", hunk->use == USE_HUNK ? '+' + : hunk->use == SKIP_HUNK ? '-' : ' ', + (int)start_index); + summarize_hunk(s, hunk, &s->buf); + fputs(s->buf.buf, stdout); + } + + return end_index; +} + static const char help_patch_text[] = N_("y - stage this hunk\n" "n - do not stage this hunk\n" @@ -941,6 +989,7 @@ N_("y - stage this hunk\n" "J - leave this hunk undecided, see next hunk\n" "k - leave this hunk undecided, see previous undecided hunk\n" "K - leave this hunk undecided, see previous hunk\n" + "g - select a hunk to go to\n" "s - split the current hunk into smaller hunks\n" "e - manually edit the current hunk\n" "? - print help\n"); @@ -999,6 +1048,8 @@ static int patch_update_file(struct add_p_state *s, strbuf_addstr(&s->buf, ",j"); if (hunk_index + 1 < file_diff->hunk_nr) strbuf_addstr(&s->buf, ",J"); + if (file_diff->hunk_nr > 1) + strbuf_addstr(&s->buf, ",g"); if (hunk->splittable_into > 1) strbuf_addstr(&s->buf, ",s"); if (hunk_index + 1 > file_diff->mode_change && @@ -1068,6 +1119,41 @@ soft_increment: hunk_index = undecided_next; else err(s, _("No next hunk")); + } else if (s->answer.buf[0] == 'g') { + char *pend; + unsigned long response; + + if (file_diff->hunk_nr < 2) { + err(s, _("No other hunks to goto")); + continue; + } + strbuf_remove(&s->answer, 0, 1); + strbuf_trim(&s->answer); + i = hunk_index > 10 ? hunk_index - 10 : 0; + while (s->answer.len == 0) { + i = display_hunks(s, file_diff, i); + printf("%s", i < file_diff->hunk_nr ? + _("go to which hunk ( to see " + "more)? ") : _("go to which hunk? ")); + fflush(stdout); + if (strbuf_getline(&s->answer, + stdin) == EOF) + break; + strbuf_trim_trailing_newline(&s->answer); + } + + strbuf_trim(&s->answer); + response = strtoul(s->answer.buf, &pend, 10); + if (*pend || pend == s->answer.buf) + err(s, _("Invalid number: '%s'"), + s->answer.buf); + else if (0 < response && response <= file_diff->hunk_nr) + hunk_index = response - 1; + else + err(s, Q_("Sorry, only %d hunk available.", + "Sorry, only %d hunks available.", + file_diff->hunk_nr), + (int)file_diff->hunk_nr); } else if (s->answer.buf[0] == 's') { size_t splittable_into = hunk->splittable_into; if (splittable_into < 2) diff --git a/t/t3701-add-interactive.sh b/t/t3701-add-interactive.sh index fe383be50e..57c656a20c 100755 --- a/t/t3701-add-interactive.sh +++ b/t/t3701-add-interactive.sh @@ -413,6 +413,22 @@ test_expect_success 'split hunk setup' ' test_write_lines 10 15 20 21 22 23 24 30 40 50 60 >test ' +test_expect_success 'goto hunk' ' + test_when_finished "git reset" && + tr _ " " >expect <<-EOF && + (2/2) Stage this hunk [y,n,q,a,d,K,g,/,e,?]? + 1: -1,2 +1,3 +15 + _ 2: -2,4 +3,8 +21 + go to which hunk? @@ -1,2 +1,3 @@ + _10 + +15 + _20 + (1/2) Stage this hunk [y,n,q,a,d,j,J,g,/,e,?]?_ + EOF + test_write_lines s y g 1 | git add -p >actual && + tail -n 7 actual.trimmed && + test_cmp expect actual.trimmed +' + test_expect_success 'split hunk "add -p (edit)"' ' # Split, say Edit and do nothing. Then: # From d09568bf8569a4a6e0a6e6a228d3f3dd0c9e0ca6 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Sat, 23 Mar 2019 22:38:02 +0100 Subject: [PATCH 53/98] built-in add -p: implement the "stash" and "reset" patch modes The `git stash` and `git reset` commands support a `--patch` option, and both simply hand off to `git add -p` to perform that work. Let's teach the built-in version of `git add -p` do perform that work, too. Signed-off-by: Johannes Schindelin --- add-interactive.h | 2 ++ add-patch.c | 76 ++++++++++++++++++++++++++++++++++++++++++++++- builtin/add.c | 4 +++ 3 files changed, 81 insertions(+), 1 deletion(-) diff --git a/add-interactive.h b/add-interactive.h index 05d7505a6c..d03ed8a60c 100644 --- a/add-interactive.h +++ b/add-interactive.h @@ -34,6 +34,8 @@ int run_add_i(struct repository *r, const struct pathspec *ps); enum add_p_mode { ADD_P_STAGE, + ADD_P_STASH, + ADD_P_RESET, }; int run_add_p(struct repository *r, enum add_p_mode mode, diff --git a/add-patch.c b/add-patch.c index a0d950e9c9..ceab6fdced 100644 --- a/add-patch.c +++ b/add-patch.c @@ -40,6 +40,72 @@ static struct patch_mode patch_mode_stage = { "the file\n") }; +static struct patch_mode patch_mode_stash = { + .diff = { "diff-index", "HEAD", NULL }, + .apply = { "--cached", NULL }, + .apply_check = { "--cached", NULL }, + .is_reverse = 0, + .prompt_mode = { + N_("Stash mode change [y,n,q,a,d%s,?]? "), + N_("Stash deletion [y,n,q,a,d%s,?]? "), + N_("Stash this hunk [y,n,q,a,d%s,?]? "), + }, + .edit_hunk_hint = N_("If the patch applies cleanly, the edited hunk " + "will immediately be marked for stashing."), + .help_patch_text = + N_("y - stash this hunk\n" + "n - do not stash this hunk\n" + "q - quit; do not stash this hunk or any of the remaining " + "ones\n" + "a - stash this hunk and all later hunks in the file\n" + "d - do not stash this hunk or any of the later hunks in " + "the file\n"), +}; + +static struct patch_mode patch_mode_reset_head = { + .diff = { "diff-index", "--cached", NULL }, + .apply = { "-R", "--cached", NULL }, + .apply_check = { "-R", "--cached", NULL }, + .is_reverse = 1, + .prompt_mode = { + N_("Unstage mode change [y,n,q,a,d%s,?]? "), + N_("Unstage deletion [y,n,q,a,d%s,?]? "), + N_("Unstage this hunk [y,n,q,a,d%s,?]? "), + }, + .edit_hunk_hint = N_("If the patch applies cleanly, the edited hunk " + "will immediately be marked for unstaging."), + .help_patch_text = + N_("y - unstage this hunk\n" + "n - do not unstage this hunk\n" + "q - quit; do not unstage this hunk or any of the remaining " + "ones\n" + "a - unstage this hunk and all later hunks in the file\n" + "d - do not unstage this hunk or any of the later hunks in " + "the file\n"), +}; + +static struct patch_mode patch_mode_reset_nothead = { + .diff = { "diff-index", "-R", "--cached", NULL }, + .apply = { "--cached", NULL }, + .apply_check = { "--cached", NULL }, + .is_reverse = 0, + .prompt_mode = { + N_("Apply mode change to index [y,n,q,a,d%s,?]? "), + N_("Apply deletion to index [y,n,q,a,d%s,?]? "), + N_("Apply this hunk to index [y,n,q,a,d%s,?]? "), + }, + .edit_hunk_hint = N_("If the patch applies cleanly, the edited hunk " + "will immediately be marked for applying."), + .help_patch_text = + N_("y - apply this hunk to index\n" + "n - do not apply this hunk to index\n" + "q - quit; do not apply this hunk or any of the remaining " + "ones\n" + "a - apply this hunk and all later hunks in the file\n" + "d - do not apply this hunk or any of the later hunks in " + "the file\n"), +}; + struct hunk_header { unsigned long old_offset, old_count, new_offset, new_count; /* @@ -1322,7 +1388,15 @@ int run_add_p(struct repository *r, enum add_p_mode mode, init_add_i_state(&s.s, r); - s.mode = &patch_mode_stage; + if (mode == ADD_P_STASH) + s.mode = &patch_mode_stash; + else if (mode == ADD_P_RESET) { + if (!revision || !strcmp(revision, "HEAD")) + s.mode = &patch_mode_reset_head; + else + s.mode = &patch_mode_reset_nothead; + } else + s.mode = &patch_mode_stage; s.revision = revision; if (discard_index(r->index) < 0 || repo_read_index(r) < 0 || diff --git a/builtin/add.c b/builtin/add.c index 6806637185..4e8130e9ff 100644 --- a/builtin/add.c +++ b/builtin/add.c @@ -202,6 +202,10 @@ int run_add_interactive(const char *revision, const char *patch_mode, if (!strcmp(patch_mode, "--patch")) mode = ADD_P_STAGE; + else if (!strcmp(patch_mode, "--patch=stash")) + mode = ADD_P_STASH; + else if (!strcmp(patch_mode, "--patch=reset")) + mode = ADD_P_RESET; else die("'%s' not yet supported in the built-in add -p", patch_mode); From 5dbae4baa437b91c3aea6b6270d6103dd19a2296 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 21 Mar 2019 23:50:53 +0100 Subject: [PATCH 54/98] built-in add -p: implement the '/' ("search regex") command This patch implements the hunk searching feature in the C version of `git add -p`. A test is added to verify that this behavior matches the one of the Perl version of `git add -p`. Note that this involves a change of behavior: the Perl version uses (of course) the Perl flavor of regular expressions, while this patch uses the regcomp()/regexec(), i.e. POSIX extended regular expressions. In practice, this behavior change is unlikely to matter. Signed-off-by: Johannes Schindelin --- add-patch.c | 50 +++++++++++++++++++++++++++++++++++++- t/t3701-add-interactive.sh | 14 +++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/add-patch.c b/add-patch.c index f79f9271d9..89f63bb11d 100644 --- a/add-patch.c +++ b/add-patch.c @@ -990,6 +990,7 @@ N_("y - stage this hunk\n" "k - leave this hunk undecided, see previous undecided hunk\n" "K - leave this hunk undecided, see previous hunk\n" "g - select a hunk to go to\n" + "/ - search for a hunk matching the given regex\n" "s - split the current hunk into smaller hunks\n" "e - manually edit the current hunk\n" "? - print help\n"); @@ -1049,7 +1050,7 @@ static int patch_update_file(struct add_p_state *s, if (hunk_index + 1 < file_diff->hunk_nr) strbuf_addstr(&s->buf, ",J"); if (file_diff->hunk_nr > 1) - strbuf_addstr(&s->buf, ",g"); + strbuf_addstr(&s->buf, ",g,/"); if (hunk->splittable_into > 1) strbuf_addstr(&s->buf, ",s"); if (hunk_index + 1 > file_diff->mode_change && @@ -1154,6 +1155,53 @@ soft_increment: "Sorry, only %d hunks available.", file_diff->hunk_nr), (int)file_diff->hunk_nr); + } else if (s->answer.buf[0] == '/') { + regex_t regex; + int ret; + + if (file_diff->hunk_nr < 2) { + err(s, _("No other hunks to search")); + continue; + } + strbuf_remove(&s->answer, 0, 1); + strbuf_trim_trailing_newline(&s->answer); + if (s->answer.len == 0) { + printf("%s", _("search for regex? ")); + fflush(stdout); + if (strbuf_getline(&s->answer, + stdin) == EOF) + break; + strbuf_trim_trailing_newline(&s->answer); + if (s->answer.len == 0) + continue; + } + ret = regcomp(®ex, s->answer.buf, + REG_EXTENDED | REG_NOSUB | REG_NEWLINE); + if (ret) { + char errbuf[1024]; + + regerror(ret, ®ex, errbuf, sizeof(errbuf)); + err(s, _("Malformed search regexp %s: %s"), + s->answer.buf, errbuf); + continue; + } + i = hunk_index; + for (;;) { + /* render the hunk into a scratch buffer */ + render_hunk(s, file_diff->hunk + i, 0, 0, + &s->buf); + if (regexec(®ex, s->buf.buf, 0, NULL, 0) + != REG_NOMATCH) + break; + i++; + if (i == file_diff->hunk_nr) + i = 0; + if (i != hunk_index) + continue; + err(s, _("No hunk matches the given pattern")); + break; + } + hunk_index = i; } else if (s->answer.buf[0] == 's') { size_t splittable_into = hunk->splittable_into; if (splittable_into < 2) diff --git a/t/t3701-add-interactive.sh b/t/t3701-add-interactive.sh index 57c656a20c..12ee321707 100755 --- a/t/t3701-add-interactive.sh +++ b/t/t3701-add-interactive.sh @@ -429,6 +429,20 @@ test_expect_success 'goto hunk' ' test_cmp expect actual.trimmed ' +test_expect_success 'navigate to hunk via regex' ' + test_when_finished "git reset" && + tr _ " " >expect <<-EOF && + (2/2) Stage this hunk [y,n,q,a,d,K,g,/,e,?]? @@ -1,2 +1,3 @@ + _10 + +15 + _20 + (1/2) Stage this hunk [y,n,q,a,d,j,J,g,/,e,?]?_ + EOF + test_write_lines s y /1,2 | git add -p >actual && + tail -n 5 actual.trimmed && + test_cmp expect actual.trimmed +' + test_expect_success 'split hunk "add -p (edit)"' ' # Split, say Edit and do nothing. Then: # From 96604247ec9a3627950f667a03bac6a80811b55f Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Sun, 24 Mar 2019 19:55:08 +0100 Subject: [PATCH 55/98] legacy stash -p: respect the add.interactive.usebuiltin setting As `git add` traditionally did not expose the `--patch=` modes via command-line options, the scripted version of `git stash` had to call `git add--interactive` directly. But this prevents the built-in `add -p` from kicking in, as `add--interactive` is the Perl script. So let's introduce support for an optional `` argument in `git add --patch[=]`, and use that in the scripted version of `git stash -p`, so that the built-in interactive add can do its job if configured. Signed-off-by: Johannes Schindelin --- builtin/add.c | 21 +++++++++++++++------ builtin/commit.c | 3 ++- commit.h | 3 ++- git-legacy-stash.sh | 2 +- 4 files changed, 20 insertions(+), 9 deletions(-) diff --git a/builtin/add.c b/builtin/add.c index 4e8130e9ff..0d81247e80 100644 --- a/builtin/add.c +++ b/builtin/add.c @@ -26,7 +26,8 @@ static const char * const builtin_add_usage[] = { N_("git add [] [--] ..."), NULL }; -static int patch_interactive, add_interactive, edit_interactive; +static const char *patch_interactive; +static int add_interactive, edit_interactive; static int take_worktree_changes; static int add_renormalize; @@ -228,9 +229,11 @@ int run_add_interactive(const char *revision, const char *patch_mode, return status; } -int interactive_add(int argc, const char **argv, const char *prefix, int patch) +int interactive_add(int argc, const char **argv, const char *prefix, + const char *patch_mode) { struct pathspec pathspec; + char buffer[64]; parse_pathspec(&pathspec, 0, PATHSPEC_PREFER_FULL | @@ -238,9 +241,13 @@ int interactive_add(int argc, const char **argv, const char *prefix, int patch) PATHSPEC_PREFIX_ORIGIN, prefix, argv); - return run_add_interactive(NULL, - patch ? "--patch" : NULL, - &pathspec); + if (patch_mode) { + xsnprintf(buffer, sizeof(buffer), "--patch%s%s", + *patch_mode ? "=" : "", patch_mode); + patch_mode = buffer; + } + + return run_add_interactive(NULL, patch_mode, &pathspec); } static int edit_patch(int argc, const char **argv, const char *prefix) @@ -318,7 +325,9 @@ static struct option builtin_add_options[] = { OPT__VERBOSE(&verbose, N_("be verbose")), OPT_GROUP(""), OPT_BOOL('i', "interactive", &add_interactive, N_("interactive picking")), - OPT_BOOL('p', "patch", &patch_interactive, N_("select hunks interactively")), + { OPTION_STRING, 'p', "patch", &patch_interactive, N_("patch-mode"), + N_("select hunks interactively"), PARSE_OPT_OPTARG, NULL, + (intptr_t) "" }, OPT_BOOL('e', "edit", &edit_interactive, N_("edit current diff and apply")), OPT__FORCE(&ignored_too, N_("allow adding otherwise ignored files"), 0), OPT_BOOL('u', "update", &take_worktree_changes, N_("update tracked files")), diff --git a/builtin/commit.c b/builtin/commit.c index e588bc6ad3..ae2e1bb124 100644 --- a/builtin/commit.c +++ b/builtin/commit.c @@ -358,7 +358,8 @@ static const char *prepare_index(int argc, const char **argv, const char *prefix old_index_env = xstrdup_or_null(getenv(INDEX_ENVIRONMENT)); setenv(INDEX_ENVIRONMENT, get_lock_file_path(&index_lock), 1); - if (interactive_add(argc, argv, prefix, patch_interactive) != 0) + if (interactive_add(argc, argv, prefix, + patch_interactive ? "" : NULL) != 0) die(_("interactive add failed")); if (old_index_env && *old_index_env) diff --git a/commit.h b/commit.h index f5295ca7f3..e9f96fefd7 100644 --- a/commit.h +++ b/commit.h @@ -295,7 +295,8 @@ int delayed_reachability_test(struct shallow_info *si, int c); void prune_shallow(unsigned options); extern struct trace_key trace_shallow; -int interactive_add(int argc, const char **argv, const char *prefix, int patch); +int interactive_add(int argc, const char **argv, const char *prefix, + const char *patch_mode); int run_add_interactive(const char *revision, const char *patch_mode, const struct pathspec *pathspec); diff --git a/git-legacy-stash.sh b/git-legacy-stash.sh index 07ad4a5459..5d9bcf77e0 100755 --- a/git-legacy-stash.sh +++ b/git-legacy-stash.sh @@ -206,7 +206,7 @@ create_stash () { # find out what the user wants GIT_INDEX_FILE="$TMP-index" \ - git add--interactive --patch=stash -- "$@" && + git add --patch=stash -- "$@" && # state of the working tree w_tree=$(GIT_INDEX_FILE="$TMP-index" git write-tree) || From f225674d25e26ba35757c06ca3b4e99ddeb3c9c6 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 22 Mar 2019 01:24:47 +0100 Subject: [PATCH 56/98] built-in add -p: implement the 'q' ("quit") command This command is actually very similar to the 'd' ("do not stage this hunk or any of the later hunks in the file") command: it just does something on top, namely leave the loop and return a value indicating that we're quittin'. Signed-off-by: Johannes Schindelin --- add-patch.c | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/add-patch.c b/add-patch.c index 89f63bb11d..4715b0fdb0 100644 --- a/add-patch.c +++ b/add-patch.c @@ -12,9 +12,9 @@ enum prompt_mode_type { }; static const char *prompt_mode[] = { - N_("Stage mode change [y,n,a,d%s,?]? "), - N_("Stage deletion [y,n,a,d%s,?]? "), - N_("Stage this hunk [y,n,a,d%s,?]? ") + N_("Stage mode change [y,n,a,q,d%s,?]? "), + N_("Stage deletion [y,n,a,q,d%s,?]? "), + N_("Stage this hunk [y,n,a,q,d%s,?]? ") }; struct hunk_header { @@ -983,6 +983,7 @@ static size_t display_hunks(struct add_p_state *s, static const char help_patch_text[] = N_("y - stage this hunk\n" "n - do not stage this hunk\n" + "q - quit; do not stage this hunk or any of the remaining ones\n" "a - stage this and all the remaining hunks\n" "d - do not stage this hunk nor any of the remaining hunks\n" "j - leave this hunk undecided, see next undecided hunk\n" @@ -1003,7 +1004,7 @@ static int patch_update_file(struct add_p_state *s, struct hunk *hunk; char ch; struct child_process cp = CHILD_PROCESS_INIT; - int colored = !!s->colored.len; + int colored = !!s->colored.len, quit = 0; enum prompt_mode_type prompt_mode_type; if (!file_diff->hunk_nr) @@ -1094,12 +1095,16 @@ soft_increment: if (hunk->use == UNDECIDED_HUNK) hunk->use = USE_HUNK; } - } else if (ch == 'd') { + } else if (ch == 'd' || ch == 'q') { for (; hunk_index < file_diff->hunk_nr; hunk_index++) { hunk = file_diff->hunk + hunk_index; if (hunk->use == UNDECIDED_HUNK) hunk->use = SKIP_HUNK; } + if (ch == 'q') { + quit = 1; + break; + } } else if (s->answer.buf[0] == 'K') { if (hunk_index) hunk_index--; @@ -1244,7 +1249,7 @@ soft_increment: } putchar('\n'); - return 0; + return quit; } int run_add_p(struct repository *r, const struct pathspec *ps) From 92757acc16a77451ac1d2b09a3c600d10764a4a8 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 4 Apr 2019 15:33:03 +0200 Subject: [PATCH 57/98] built-in stash: use the built-in `git add -p` if so configured The scripted version of `git stash` called directly into the Perl script `git-add--interactive.perl`, and this was faithfully converted to C. However, we have a much better way to do this now: call `git add --patch=`, which incidentally also respects the config setting `add.interactive.useBuiltin`. Let's do this. Signed-off-by: Johannes Schindelin --- builtin/stash.c | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/builtin/stash.c b/builtin/stash.c index 4e806176b0..2dafd97766 100644 --- a/builtin/stash.c +++ b/builtin/stash.c @@ -999,9 +999,9 @@ static int stash_patch(struct stash_info *info, const struct pathspec *ps, { int ret = 0; struct child_process cp_read_tree = CHILD_PROCESS_INIT; - struct child_process cp_add_i = CHILD_PROCESS_INIT; struct child_process cp_diff_tree = CHILD_PROCESS_INIT; struct index_state istate = { NULL }; + char *old_index_env = NULL, *old_repo_index_file; remove_path(stash_index_path.buf); @@ -1015,16 +1015,19 @@ static int stash_patch(struct stash_info *info, const struct pathspec *ps, } /* Find out what the user wants. */ - cp_add_i.git_cmd = 1; - argv_array_pushl(&cp_add_i.args, "add--interactive", "--patch=stash", - "--", NULL); - add_pathspecs(&cp_add_i.args, ps); - argv_array_pushf(&cp_add_i.env_array, "GIT_INDEX_FILE=%s", - stash_index_path.buf); - if (run_command(&cp_add_i)) { - ret = -1; - goto done; - } + old_repo_index_file = the_repository->index_file; + the_repository->index_file = stash_index_path.buf; + old_index_env = xstrdup_or_null(getenv(INDEX_ENVIRONMENT)); + setenv(INDEX_ENVIRONMENT, the_repository->index_file, 1); + + ret = run_add_interactive(NULL, "--patch=stash", ps); + + the_repository->index_file = old_repo_index_file; + if (old_index_env && *old_index_env) + setenv(INDEX_ENVIRONMENT, old_index_env, 1); + else + unsetenv(INDEX_ENVIRONMENT); + FREE_AND_NULL(old_index_env); /* State of the working tree. */ if (write_index_as_tree(&info->w_tree, &istate, stash_index_path.buf, 0, From 69efacc55cece7fc940b3437ec22502cdb1ca9f7 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Sun, 24 Mar 2019 23:26:58 +0100 Subject: [PATCH 58/98] built-in add -p: only show the applicable parts of the help text When displaying the only hunk in a file's diff, the prompt already excludes the commands to navigate to the previous/next hunk. Let's also let the `?` command show only the help lines corresponding to the commands that are displayed in the prompt. Signed-off-by: Johannes Schindelin --- add-patch.c | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/add-patch.c b/add-patch.c index 4715b0fdb0..63aa54a1a8 100644 --- a/add-patch.c +++ b/add-patch.c @@ -985,8 +985,10 @@ N_("y - stage this hunk\n" "n - do not stage this hunk\n" "q - quit; do not stage this hunk or any of the remaining ones\n" "a - stage this and all the remaining hunks\n" - "d - do not stage this hunk nor any of the remaining hunks\n" - "j - leave this hunk undecided, see next undecided hunk\n" + "d - do not stage this hunk nor any of the remaining hunks\n"); + +static const char help_patch_remainder[] = +N_("j - leave this hunk undecided, see next undecided hunk\n" "J - leave this hunk undecided, see next hunk\n" "k - leave this hunk undecided, see previous undecided hunk\n" "K - leave this hunk undecided, see previous hunk\n" @@ -1223,9 +1225,31 @@ soft_increment: hunk->use = USE_HUNK; goto soft_increment; } - } else - color_fprintf(stdout, s->s.help_color, + } else { + const char *p = _(help_patch_remainder), *eol = p; + + color_fprintf(stdout, s->s.help_color, "%s", _(help_patch_text)); + + /* + * Show only those lines of the remainder that are + * actually applicable with the current hunk. + */ + for (; *p; p = eol + (*eol == '\n')) { + eol = strchrnul(p, '\n'); + + /* + * `s->buf` still contains the part of the + * commands shown in the prompt that are not + * always available. + */ + if (*p != '?' && !strchr(s->buf.buf, *p)) + continue; + + color_fprintf_ln(stdout, s->s.help_color, + "%.*s", (int)(eol - p), p); + } + } } /* Any hunk to be used? */ From a1706ffa892a5e3fbd840c41b270dcf5031839d8 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Sat, 23 Mar 2019 22:38:02 +0100 Subject: [PATCH 59/98] built-in add -p: implement the "checkout" patch modes This patch teaches the built-in `git add -p` machinery all the tricks it needs to know in order to act as the work horse for `git checkout -p`. Apart from the minor changes (slightly reworded messages, different `diff` and `apply --check` invocations), it requires a new function to actually apply the changes, as `git checkout -p` is a bit special in that respect: when the desired changes do not apply to the index, but apply to the work tree, Git does not fail straight away, but asks the user whether to apply the changes to the worktree at least. Signed-off-by: Johannes Schindelin --- add-interactive.h | 1 + add-patch.c | 139 ++++++++++++++++++++++++++++++++++++++++++++-- builtin/add.c | 5 +- 3 files changed, 138 insertions(+), 7 deletions(-) diff --git a/add-interactive.h b/add-interactive.h index d03ed8a60c..c6b3d202e8 100644 --- a/add-interactive.h +++ b/add-interactive.h @@ -36,6 +36,7 @@ enum add_p_mode { ADD_P_STAGE, ADD_P_STASH, ADD_P_RESET, + ADD_P_CHECKOUT, }; int run_add_p(struct repository *r, enum add_p_mode mode, diff --git a/add-patch.c b/add-patch.c index ceab6fdced..066031c7f5 100644 --- a/add-patch.c +++ b/add-patch.c @@ -106,6 +106,72 @@ static struct patch_mode patch_mode_reset_nothead = { "the file\n"), }; +static struct patch_mode patch_mode_checkout_index = { + .diff = { "diff-files", NULL }, + .apply = { "-R", NULL }, + .apply_check = { "-R", NULL }, + .is_reverse = 1, + .prompt_mode = { + N_("Discard mode change from worktree [y,n,q,a,d%s,?]? "), + N_("Discard deletion from worktree [y,n,q,a,d%s,?]? "), + N_("Discard this hunk from worktree [y,n,q,a,d%s,?]? "), + }, + .edit_hunk_hint = N_("If the patch applies cleanly, the edited hunk " + "will immediately be marked for discarding."), + .help_patch_text = + N_("y - discard this hunk from worktree\n" + "n - do not discard this hunk from worktree\n" + "q - quit; do not discard this hunk or any of the remaining " + "ones\n" + "a - discard this hunk and all later hunks in the file\n" + "d - do not discard this hunk or any of the later hunks in " + "the file\n"), +}; + +static struct patch_mode patch_mode_checkout_head = { + .diff = { "diff-index", NULL }, + .apply_for_checkout = 1, + .apply_check = { "-R", NULL }, + .is_reverse = 1, + .prompt_mode = { + N_("Discard mode change from index and worktree [y,n,q,a,d%s,?]? "), + N_("Discard deletion from index and worktree [y,n,q,a,d%s,?]? "), + N_("Discard this hunk from index and worktree [y,n,q,a,d%s,?]? "), + }, + .edit_hunk_hint = N_("If the patch applies cleanly, the edited hunk " + "will immediately be marked for discarding."), + .help_patch_text = + N_("y - discard this hunk from index and worktree\n" + "n - do not discard this hunk from index and worktree\n" + "q - quit; do not discard this hunk or any of the remaining " + "ones\n" + "a - discard this hunk and all later hunks in the file\n" + "d - do not discard this hunk or any of the later hunks in " + "the file\n"), +}; + +static struct patch_mode patch_mode_checkout_nothead = { + .diff = { "diff-index", "-R", NULL }, + .apply_for_checkout = 1, + .apply_check = { NULL }, + .is_reverse = 0, + .prompt_mode = { + N_("Apply mode change to index and worktree [y,n,q,a,d%s,?]? "), + N_("Apply deletion to index and worktree [y,n,q,a,d%s,?]? "), + N_("Apply this hunk to index and worktree [y,n,q,a,d%s,?]? "), + }, + .edit_hunk_hint = N_("If the patch applies cleanly, the edited hunk " + "will immediately be marked for applying."), + .help_patch_text = + N_("y - apply this hunk to index and worktree\n" + "n - do not apply this hunk to index and worktree\n" + "q - quit; do not apply this hunk or any of the remaining " + "ones\n" + "a - apply this hunk and all later hunks in the file\n" + "d - do not apply this hunk or any of the later hunks in " + "the file\n"), +}; + struct hunk_header { unsigned long old_offset, old_count, new_offset, new_count; /* @@ -1039,6 +1105,57 @@ static int edit_hunk_loop(struct add_p_state *s, } } +static int apply_for_checkout(struct add_p_state *s, struct strbuf *diff, + int is_reverse) +{ + const char *reverse = is_reverse ? "-R" : NULL; + struct child_process check_index = CHILD_PROCESS_INIT; + struct child_process check_worktree = CHILD_PROCESS_INIT; + struct child_process apply_index = CHILD_PROCESS_INIT; + struct child_process apply_worktree = CHILD_PROCESS_INIT; + int applies_index, applies_worktree; + + setup_child_process(&check_index, s, + "apply", "--cached", "--check", reverse, NULL); + applies_index = !pipe_command(&check_index, diff->buf, diff->len, + NULL, 0, NULL, 0); + + setup_child_process(&check_worktree, s, + "apply", "--check", reverse, NULL); + applies_worktree = !pipe_command(&check_worktree, diff->buf, diff->len, + NULL, 0, NULL, 0); + + if (applies_worktree && applies_index) { + setup_child_process(&apply_index, s, + "apply", "--cached", reverse, NULL); + pipe_command(&apply_index, diff->buf, diff->len, + NULL, 0, NULL, 0); + + setup_child_process(&apply_worktree, s, + "apply", reverse, NULL); + pipe_command(&apply_worktree, diff->buf, diff->len, + NULL, 0, NULL, 0); + + return 1; + } + + if (!applies_index) { + err(s, _("The selected hunks do not apply to the index!")); + if (prompt_yesno(s, _("Apply them to the worktree " + "anyway? ")) > 0) { + setup_child_process(&apply_worktree, s, + "apply", reverse, NULL); + return pipe_command(&apply_worktree, diff->buf, + diff->len, NULL, 0, NULL, 0); + } + err(s, _("Nothing was applied.\n")); + } else + /* As a last resort, show the diff to the user */ + fwrite(diff->buf, diff->len, 1, stderr); + + return 0; +} + #define SUMMARY_HEADER_WIDTH 20 #define SUMMARY_LINE_WIDTH 80 static void summarize_hunk(struct add_p_state *s, struct hunk *hunk, @@ -1364,11 +1481,16 @@ soft_increment: reassemble_patch(s, file_diff, 0, &s->buf); discard_index(s->s.r->index); - setup_child_process(&cp, s, "apply", NULL); - argv_array_pushv(&cp.args, s->mode->apply); - if (pipe_command(&cp, s->buf.buf, s->buf.len, - NULL, 0, NULL, 0)) - error(_("'git apply' failed")); + if (s->mode->apply_for_checkout) + apply_for_checkout(s, &s->buf, + s->mode->is_reverse); + else { + setup_child_process(&cp, s, "apply", NULL); + argv_array_pushv(&cp.args, s->mode->apply); + if (pipe_command(&cp, s->buf.buf, s->buf.len, + NULL, 0, NULL, 0)) + error(_("'git apply' failed")); + } if (!repo_read_index(s->s.r)) repo_refresh_and_write_index(s->s.r, REFRESH_QUIET, 0, 1, NULL, NULL, NULL); @@ -1395,6 +1517,13 @@ int run_add_p(struct repository *r, enum add_p_mode mode, s.mode = &patch_mode_reset_head; else s.mode = &patch_mode_reset_nothead; + } else if (mode == ADD_P_CHECKOUT) { + if (!revision) + s.mode = &patch_mode_checkout_index; + else if (!strcmp(revision, "HEAD")) + s.mode = &patch_mode_checkout_head; + else + s.mode = &patch_mode_checkout_nothead; } else s.mode = &patch_mode_stage; s.revision = revision; diff --git a/builtin/add.c b/builtin/add.c index 0d81247e80..79bc14033b 100644 --- a/builtin/add.c +++ b/builtin/add.c @@ -207,9 +207,10 @@ int run_add_interactive(const char *revision, const char *patch_mode, mode = ADD_P_STASH; else if (!strcmp(patch_mode, "--patch=reset")) mode = ADD_P_RESET; + else if (!strcmp(patch_mode, "--patch=checkout")) + mode = ADD_P_CHECKOUT; else - die("'%s' not yet supported in the built-in add -p", - patch_mode); + die("'%s' not supported", patch_mode); return !!run_add_p(the_repository, mode, revision, pathspec); } From 24df6a1c4e26d8f4ad376dbc97b5209c11f1e0e0 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Sun, 24 Mar 2019 22:54:01 +0100 Subject: [PATCH 60/98] built-in add -p: show helpful hint when nothing can be staged This patch will make `git add -p` show "No changes." or "Only binary files changed." in that case. Signed-off-by: Johannes Schindelin --- add-patch.c | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/add-patch.c b/add-patch.c index 63aa54a1a8..6e7d9d1569 100644 --- a/add-patch.c +++ b/add-patch.c @@ -44,7 +44,7 @@ struct add_p_state { struct hunk head; struct hunk *hunk; size_t hunk_nr, hunk_alloc; - unsigned deleted:1, mode_change:1; + unsigned deleted:1, mode_change:1,binary:1; } *file_diff; size_t file_diff_nr; }; @@ -267,7 +267,9 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps) file_diff->mode_change = 1; } else if (file_diff->hunk_nr != 1) BUG("mode change after first hunk?"); - } + } else if (hunk == &file_diff->head && + starts_with(p, "Binary files ")) + file_diff->binary = 1; if (file_diff->deleted && file_diff->mode_change) BUG("diff contains delete *and* a mode change?!?\n%.*s", @@ -1281,7 +1283,7 @@ int run_add_p(struct repository *r, const struct pathspec *ps) struct add_p_state s = { { r }, STRBUF_INIT, STRBUF_INIT, STRBUF_INIT, STRBUF_INIT }; - size_t i; + size_t i, binary_count = 0; init_add_i_state(&s.s, r); @@ -1295,9 +1297,16 @@ int run_add_p(struct repository *r, const struct pathspec *ps) } for (i = 0; i < s.file_diff_nr; i++) - if (patch_update_file(&s, s.file_diff + i)) + if (s.file_diff[i].binary && !s.file_diff[i].hunk_nr) + binary_count++; + else if (patch_update_file(&s, s.file_diff + i)) break; + if (s.file_diff_nr == 0) + fprintf(stderr, _("No changes.\n")); + else if (binary_count == s.file_diff_nr) + fprintf(stderr, _("Only binary files changed.\n")); + strbuf_release(&s.answer); strbuf_release(&s.buf); strbuf_release(&s.plain); From e7bbd99438b73e7f7ec66caac66d13daf29b983f Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 12 Mar 2019 15:45:03 +0100 Subject: [PATCH 61/98] 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 --- add-interactive.c | 12 ++++++++++++ add-interactive.h | 4 ++++ add-patch.c | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+) diff --git a/add-interactive.c b/add-interactive.c index daf95de2d8..9e1ea03efe 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; } /* @@ -1136,6 +1147,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 6852cc7804..55a5961755 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); enum color_add_i { COLOR_HEADER = 0, @@ -27,6 +30,7 @@ enum color_add_i { COLOR_RESET, }; const char *get_add_i_color(enum color_add_i ix); +const char *get_interactive_diff_filter(void); struct repository; struct pathspec; diff --git a/add-patch.c b/add-patch.c index 3aefd7fd0e..767cd22a59 100644 --- a/add-patch.c +++ b/add-patch.c @@ -392,6 +392,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(&colored_cp, s, NULL); xsnprintf((char *)args.argv[color_arg_index], 8, "--color"); @@ -401,6 +402,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(&filter_cp, s, + 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; @@ -501,6 +520,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; @@ -522,6 +544,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; } @@ -1585,6 +1616,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; } @@ -1603,5 +1635,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 0c950c271751899212459bd2d38ebf43deee04ec Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Sat, 23 Mar 2019 15:42:52 +0100 Subject: [PATCH 62/98] 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. To make sure that this works as intended, we add a regression test case that tries to specify a bogus diff algorithm and then verifies that `git diff-files` produced the expected error message. Note: In that new test case, we actually ignore the exit code of `git add -p`. The reason is that the C version exits with failure (as one might expect), but the Perl version does not. In fact, the Perl version continues happily after the uncolored diff failed, trying to generate the colored diff, still not catching the problem, and then it pretends to have succeeded (with exit code 0). This is arguably a bug in the Perl version, and fixing it is safely outside the scope of this patch. Signed-off-by: Johannes Schindelin --- add-interactive.c | 5 +++++ add-interactive.h | 3 ++- add-patch.c | 3 +++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/add-interactive.c b/add-interactive.c index 9e1ea03efe..57fcc2e05b 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 55a5961755..1de0251c1e 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); @@ -31,6 +31,7 @@ enum color_add_i { }; const char *get_add_i_color(enum color_add_i ix); const char *get_interactive_diff_filter(void); +const char *get_interactive_diff_algorithm(void); struct repository; struct pathspec; diff --git a/add-patch.c b/add-patch.c index 767cd22a59..2a687cdcd3 100644 --- a/add-patch.c +++ b/add-patch.c @@ -354,6 +354,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'; @@ -363,6 +364,8 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps) int res; argv_array_pushv(&args, s->mode->diff); + if (diff_algorithm) + argv_array_pushf(&args, "--diff-algorithm=%s", diff_algorithm); if (s->revision) { struct object_id oid; argv_array_push(&args, From cf6734cbea3ae71f94a92ef78c021fee65a9f0f2 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 4 Apr 2019 22:17:07 +0200 Subject: [PATCH 63/98] 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 --- 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 f9bbce6615aa111efe117bc79a764e755a872e63 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 4 Apr 2019 22:21:20 +0200 Subject: [PATCH 64/98] 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 --- 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 f0df8ce4eb7de60b2dd88d699d0363127b78af1e Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 26 Mar 2019 21:28:10 +0100 Subject: [PATCH 65/98] 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 --- 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 66aabfdd8937a252ead373d4f45fd3d3d3e0bd03 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Sat, 23 Mar 2019 22:38:02 +0100 Subject: [PATCH 66/98] built-in add -p: implement the "worktree" patch modes This is a straight-forward port of 2f0896ec3ad4 (restore: support --patch, 2019-04-25) which added support for `git restore -p`. Signed-off-by: Johannes Schindelin --- add-interactive.h | 1 + add-patch.c | 51 +++++++++++++++++++++++++++++++++++++++++++++++ builtin/add.c | 2 ++ 3 files changed, 54 insertions(+) diff --git a/add-interactive.h b/add-interactive.h index c6b3d202e8..6852cc7804 100644 --- a/add-interactive.h +++ b/add-interactive.h @@ -37,6 +37,7 @@ enum add_p_mode { ADD_P_STASH, ADD_P_RESET, ADD_P_CHECKOUT, + ADD_P_WORKTREE, }; int run_add_p(struct repository *r, enum add_p_mode mode, diff --git a/add-patch.c b/add-patch.c index 066031c7f5..3aefd7fd0e 100644 --- a/add-patch.c +++ b/add-patch.c @@ -172,6 +172,50 @@ static struct patch_mode patch_mode_checkout_nothead = { "the file\n"), }; +static struct patch_mode patch_mode_worktree_head = { + .diff = { "diff-index", NULL }, + .apply = { "-R", NULL }, + .apply_check = { "-R", NULL }, + .is_reverse = 1, + .prompt_mode = { + N_("Discard mode change from index and worktree [y,n,q,a,d%s,?]? "), + N_("Discard deletion from index and worktree [y,n,q,a,d%s,?]? "), + N_("Discard this hunk from index and worktree [y,n,q,a,d%s,?]? "), + }, + .edit_hunk_hint = N_("If the patch applies cleanly, the edited hunk " + "will immediately be marked for discarding."), + .help_patch_text = + N_("y - discard this hunk from worktree\n" + "n - do not discard this hunk from worktree\n" + "q - quit; do not discard this hunk or any of the remaining " + "ones\n" + "a - discard this hunk and all later hunks in the file\n" + "d - do not discard this hunk or any of the later hunks in " + "the file\n"), +}; + +static struct patch_mode patch_mode_worktree_nothead = { + .diff = { "diff-index", "-R", NULL }, + .apply = { NULL }, + .apply_check = { NULL }, + .is_reverse = 0, + .prompt_mode = { + N_("Apply mode change to index and worktree [y,n,q,a,d%s,?]? "), + N_("Apply deletion to index and worktree [y,n,q,a,d%s,?]? "), + N_("Apply this hunk to index and worktree [y,n,q,a,d%s,?]? "), + }, + .edit_hunk_hint = N_("If the patch applies cleanly, the edited hunk " + "will immediately be marked for applying."), + .help_patch_text = + N_("y - apply this hunk to worktree\n" + "n - do not apply this hunk to worktree\n" + "q - quit; do not apply this hunk or any of the remaining " + "ones\n" + "a - apply this hunk and all later hunks in the file\n" + "d - do not apply this hunk or any of the later hunks in " + "the file\n"), +}; + struct hunk_header { unsigned long old_offset, old_count, new_offset, new_count; /* @@ -1524,6 +1568,13 @@ int run_add_p(struct repository *r, enum add_p_mode mode, s.mode = &patch_mode_checkout_head; else s.mode = &patch_mode_checkout_nothead; + } else if (mode == ADD_P_WORKTREE) { + if (!revision) + s.mode = &patch_mode_checkout_index; + else if (!strcmp(revision, "HEAD")) + s.mode = &patch_mode_worktree_head; + else + s.mode = &patch_mode_worktree_nothead; } else s.mode = &patch_mode_stage; s.revision = revision; diff --git a/builtin/add.c b/builtin/add.c index 79bc14033b..a615045254 100644 --- a/builtin/add.c +++ b/builtin/add.c @@ -209,6 +209,8 @@ int run_add_interactive(const char *revision, const char *patch_mode, mode = ADD_P_RESET; else if (!strcmp(patch_mode, "--patch=checkout")) mode = ADD_P_CHECKOUT; + else if (!strcmp(patch_mode, "--patch=worktree")) + mode = ADD_P_WORKTREE; else die("'%s' not supported", patch_mode); From 068db526a6fed81fefaca05bd8e8b412faceb202 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 26 Mar 2019 21:37:27 +0100 Subject: [PATCH 67/98] 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 --- add-interactive.c | 2 ++ add-interactive.h | 2 ++ add-patch.c | 21 +++++++++++++++++---- 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/add-interactive.c b/add-interactive.c index 57fcc2e05b..e9b9b81ed0 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 1de0251c1e..c269214da2 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; }; @@ -32,6 +33,7 @@ enum color_add_i { const char *get_add_i_color(enum color_add_i ix); const char *get_interactive_diff_filter(void); const char *get_interactive_diff_algorithm(void); +int get_interactive_use_single_key(void); struct repository; struct pathspec; diff --git a/add-patch.c b/add-patch.c index 2a687cdcd3..aeff1e9af6 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 @@ -1123,14 +1124,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; @@ -1370,9 +1384,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 5ff37622eccc8699e4a9242f10991b8dea8cee83 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 29 Mar 2019 15:03:09 +0100 Subject: [PATCH 68/98] commit --interactive: make it work with the built-in `add -i` The built-in `git add -i` machinery obviously has its `the_repository` structure initialized at the point where `cmd_commit()` calls it, and therefore does not look at the environment variable `GIT_INDEX_FILE`. But it has to, because the index was already locked, and we want to ask the interactive add machinery to work on the `index.lock` file instead of the `index` file. Technically, we could teach `run_add_i()` (and `run_add_p()`) to look specifically at that environment variable, but the entire idea of passing in a parameter of type `struct repository *` is to allow working on multiple repositories (and their index files) independently. So let's instead override the `index_file` field of that structure temporarily. Signed-off-by: Johannes Schindelin --- builtin/commit.c | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/builtin/commit.c b/builtin/commit.c index ae2e1bb124..ede7c7f70f 100644 --- a/builtin/commit.c +++ b/builtin/commit.c @@ -347,7 +347,7 @@ static const char *prepare_index(int argc, const char **argv, const char *prefix die(_("index file corrupt")); if (interactive) { - char *old_index_env = NULL; + char *old_index_env = NULL, *old_repo_index_file; hold_locked_index(&index_lock, LOCK_DIE_ON_ERROR); refresh_cache_or_die(refresh_flags); @@ -355,13 +355,17 @@ static const char *prepare_index(int argc, const char **argv, const char *prefix if (write_locked_index(&the_index, &index_lock, 0)) die(_("unable to create temporary index")); + old_repo_index_file = the_repository->index_file; + the_repository->index_file = + (char *)get_lock_file_path(&index_lock); old_index_env = xstrdup_or_null(getenv(INDEX_ENVIRONMENT)); - setenv(INDEX_ENVIRONMENT, get_lock_file_path(&index_lock), 1); + setenv(INDEX_ENVIRONMENT, the_repository->index_file, 1); if (interactive_add(argc, argv, prefix, patch_interactive ? "" : NULL) != 0) die(_("interactive add failed")); + the_repository->index_file = old_repo_index_file; if (old_index_env && *old_index_env) setenv(INDEX_ENVIRONMENT, old_index_env, 1); else From 4fed9c991e5acfe4979df564d90bb6b98fdda443 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Wed, 27 Mar 2019 17:14:02 +0100 Subject: [PATCH 69/98] 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 --- 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 d7fa28e0f5143276ba1ea98d812def2436f70731 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Wed, 27 Mar 2019 17:14:02 +0100 Subject: [PATCH 70/98] 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 --- 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 c34f32d49c67b3216cb40170bdb470e54661fc47 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Sat, 6 Apr 2019 22:31:40 +0200 Subject: [PATCH 71/98] t3904: fix incorrect demonstration of a bug In 7e9e048661 (stash -p: demonstrate failure of split with mixed y/n, 2015-04-16), a regression test for a known breakage that was added to the test script `t3904-stash-patch.sh` that demonstrated that splitting a hunk and trying to stash only part of that split hunk fails (but shouldn't). As expected, it still fails, but for the wrong reason: once the bug is fixed, we would expect stderr to show nothing, yet the regression test expects stderr to show something. Let's fix that by telling that regression test case to expect nothing to be printed to stderr. While at it, also drop the obvious left-over from debugging where the regression test did not mind `git stash -p` to return a non-zero exit status. Of course, the regression test still fails, but this time for the correct reason. Signed-off-by: Johannes Schindelin --- t/t3904-stash-patch.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/t/t3904-stash-patch.sh b/t/t3904-stash-patch.sh index 9546b6f8a4..ab7d7aa6de 100755 --- a/t/t3904-stash-patch.sh +++ b/t/t3904-stash-patch.sh @@ -106,8 +106,8 @@ test_expect_failure 'stash -p with split hunk' ' ccc EOF printf "%s\n" s n y q | - test_might_fail git stash -p 2>error && - ! test_must_be_empty error && + git stash -p 2>error && + test_must_be_empty error && grep "added line 1" test && ! grep "added line 2" test ' From 2f4eda1b0874ab066d657ccf62958bec44f353fc Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 28 Mar 2019 20:06:37 +0100 Subject: [PATCH 72/98] 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_MULTI_PACK_INDEX` into that fray. Signed-off-by: Johannes Schindelin --- 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) From 72ae41e5f5a9f2155e871de343635439452c8be0 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Sat, 6 Apr 2019 22:46:09 +0200 Subject: [PATCH 73/98] stash -p: (partially) fix bug concerning split hunks When trying to stash part of the worktree changes by splitting a hunk and then only partially accepting the split bits and pieces, the user is presented with a rather cryptic error: error: patch failed: : error: test: patch does not apply Cannot remove worktree changes and the command would fail to stash the desired parts of the worktree changes (even if the `stash` ref was actually updated correctly). We even have a test case demonstrating that failure, carrying it for four years already. The explanation: when splitting a hunk, the changed lines are no longer separated by more than 3 lines (which is the amount of context lines Git's diffs use by default), but less than that. So when staging only part of the diff hunk for stashing, the resulting diff that we want to apply to the worktree in reverse will contain those changes to be dropped surrounded by three context lines, but since the diff is relative to HEAD rather than to the worktree, these context lines will not match. Example time. Let's assume that the file README contains these lines: We the people and the worktree added some lines so that it contains these lines instead: We are the kind people and the user tries to stash the line containing "are", then the command will internally stage this line to a temporary index file and try to revert the diff between HEAD and that index file. The diff hunk that `git stash` tries to revert will look somewhat like this: @@ -1776,3 +1776,4 We +are the people It is obvious, now, that the trailing context lines overlap with the part of the original diff hunk that the user did *not* want to stash. Keeping in mind that context lines in diffs serve the primary purpose of finding the exact location when the diff does not apply precisely (but when the exact line number in the file to be patched differs from the line number indicated in the diff), we work around this by reducing the amount of context lines: the diff was just generated. Note: this is not a *full* fix for the issue. Just as demonstrated in t3701's 'add -p works with pathological context lines' test case, there are ambiguities in the diff format. It is very rare in practice, of course, to encounter such repeated lines. The full solution for such cases would be to replace the approach of generating a diff from the stash and then applying it in reverse by emulating `git revert` (i.e. doing a 3-way merge). However, in `git stash -p` it would not apply to `HEAD` but instead to the worktree, which makes this non-trivial to implement as long as we also maintain a scripted version of `add -i`. Signed-off-by: Johannes Schindelin --- builtin/stash.c | 2 +- git-legacy-stash.sh | 2 +- t/t3904-stash-patch.sh | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/builtin/stash.c b/builtin/stash.c index 2dafd97766..dbe964b30a 100644 --- a/builtin/stash.c +++ b/builtin/stash.c @@ -1037,7 +1037,7 @@ static int stash_patch(struct stash_info *info, const struct pathspec *ps, } cp_diff_tree.git_cmd = 1; - argv_array_pushl(&cp_diff_tree.args, "diff-tree", "-p", "HEAD", + argv_array_pushl(&cp_diff_tree.args, "diff-tree", "-p", "-U1", "HEAD", oid_to_hex(&info->w_tree), "--", NULL); if (pipe_command(&cp_diff_tree, NULL, 0, out_patch, 0, NULL, 0)) { ret = -1; diff --git a/git-legacy-stash.sh b/git-legacy-stash.sh index 5d9bcf77e0..4f0479e071 100755 --- a/git-legacy-stash.sh +++ b/git-legacy-stash.sh @@ -212,7 +212,7 @@ create_stash () { w_tree=$(GIT_INDEX_FILE="$TMP-index" git write-tree) || die "$(gettext "Cannot save the current worktree state")" - git diff-tree -p HEAD $w_tree -- >"$TMP-patch" && + git diff-tree -p -U1 HEAD $w_tree -- >"$TMP-patch" && test -s "$TMP-patch" || die "$(gettext "No changes selected")" diff --git a/t/t3904-stash-patch.sh b/t/t3904-stash-patch.sh index ab7d7aa6de..accfe3845c 100755 --- a/t/t3904-stash-patch.sh +++ b/t/t3904-stash-patch.sh @@ -89,7 +89,7 @@ test_expect_success 'none of this moved HEAD' ' verify_saved_head ' -test_expect_failure 'stash -p with split hunk' ' +test_expect_success 'stash -p with split hunk' ' git reset --hard && cat >test <<-\EOF && aaa From 7517225972499e0c4310745a0e5be30cbc9da67d Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Thu, 25 Apr 2019 12:05:29 -0400 Subject: [PATCH 74/98] gitk: prevent overly long command lines To avoid running into command line limitations, some of Git's commands support the `--stdin` option. Let's use exactly this option in the three rev-list/log invocations in gitk that would otherwise possibly run the danger of trying to invoke a too-long command line. While it is easy to redirect either stdin or stdout in Tcl/Tk scripts, what we need here is both. We need to capture the output, yet we also need to pipe in the revs/files arguments via stdin (because stdin does not have any limit, unlike the command line). To help this, we use the neat Tcl feature where you can capture stdout and at the same time feed a fixed string as stdin to the spawned process. One non-obvious aspect about this change is that the `--stdin` option allows to specify revs, the double-dash, and files, but *no* other options such as `--not`. This is addressed by prefixing the "negative" revs with `^` explicitly rather than relying on the `--not` option (thanks for coming up with that idea, Max!). This fixes https://github.com/git-for-windows/git/issues/1987 Analysis-and-initial-patch-by: Max Kirillov Signed-off-by: Johannes Schindelin --- gitk-git/gitk | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/gitk-git/gitk b/gitk-git/gitk index abe4805ade..6f295a74ab 100755 --- a/gitk-git/gitk +++ b/gitk-git/gitk @@ -406,14 +406,16 @@ proc start_rev_list {view} { if {$revs eq {}} { return 0 } - set args [concat $vflags($view) $revs] + set args $vflags($view) } else { + set revs {} set args $vorigargs($view) } if {[catch { set fd [open [concat | git log --no-color -z --pretty=raw $show_notes \ - --parents --boundary $args "--" $files] r] + --parents --boundary $args --stdin \ + "<<[join [concat $revs "--" $files] "\\n"]"] r] } err]} { error_popup "[mc "Error executing git log:"] $err" return 0 @@ -555,13 +557,19 @@ proc updatecommits {} { set revs $newrevs set vposids($view) [lsort -unique [concat $oldpos $vposids($view)]] } - set args [concat $vflags($view) $revs --not $oldpos] + set args $vflags($view) + foreach r $oldpos { + lappend revs "^$r" + } } else { + set revs {} set args $vorigargs($view) } if {[catch { set fd [open [concat | git log --no-color -z --pretty=raw $show_notes \ - --parents --boundary $args "--" $vfilelimit($view)] r] + --parents --boundary $args --stdin \ + "<<[join [concat $revs "--" \ + $vfilelimit($view)] "\\n"]"] r] } err]} { error_popup "[mc "Error executing git log:"] $err" return @@ -10210,10 +10218,16 @@ proc getallcommits {} { foreach id $seeds { lappend ids "^$id" } + lappend ids "--" } } if {$ids ne {}} { - set fd [open [concat $cmd $ids] r] + if {$ids eq "--all"} { + set cmd [concat $cmd "--all"] + } else { + set cmd [concat $cmd --stdin "<<[join $ids "\\n"]"] + } + set fd [open $cmd r] fconfigure $fd -blocking 0 incr allcommits nowbusy allcommits From 3eac2776094c5cb01c39d3cab7cb1e76c65fbd96 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Mon, 4 Sep 2017 11:59:45 +0200 Subject: [PATCH 75/98] mingw: change core.fsyncObjectFiles = 1 by default MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From the documentation of said setting: This boolean will enable fsync() when writing object files. This is a total waste of time and effort on a filesystem that orders data writes properly, but can be useful for filesystems that do not use journalling (traditional UNIX filesystems) or that only journal metadata and not file contents (OS X’s HFS+, or Linux ext3 with "data=writeback"). The most common file system on Windows (NTFS) does not guarantee that order, therefore a sudden loss of power (or any other event causing an unclean shutdown) would cause corrupt files (i.e. files filled with NULs). Therefore we need to change the default. Note that the documentation makes it sound as if this causes really bad performance. In reality, writing loose objects is something that is done only rarely, and only a handful of files at a time. Signed-off-by: Johannes Schindelin --- compat/mingw.c | 1 + 1 file changed, 1 insertion(+) diff --git a/compat/mingw.c b/compat/mingw.c index fe609239dd..f86d6df258 100644 --- a/compat/mingw.c +++ b/compat/mingw.c @@ -2485,6 +2485,7 @@ int wmain(int argc, const wchar_t **wargv) #endif maybe_redirect_std_handles(); + fsync_object_files = 1; /* determine size of argv and environ conversion buffer */ maxlen = wcslen(wargv[0]); From 8a3a3677f52d2708acde342cd2fa7dd4949ecd83 Mon Sep 17 00:00:00 2001 From: Nico Rieck Date: Mon, 19 Aug 2019 20:46:12 +0200 Subject: [PATCH 76/98] gitk: Escape file paths before piping to git log Fixes https://github.com/git-for-windows/git/issues/2293 Signed-off-by: Nico Rieck --- gitk-git/gitk | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/gitk-git/gitk b/gitk-git/gitk index 6f295a74ab..06b48b42a7 100755 --- a/gitk-git/gitk +++ b/gitk-git/gitk @@ -354,6 +354,16 @@ proc parseviewrevs {view revs} { return $ret } +# Escapes a list of filter paths to be passed to git log via stdin. Note that +# paths must not be quoted. +proc escape_filter_paths {paths} { + set escaped [list] + foreach path $paths { + lappend escaped [string map {\\ \\\\ "\ " "\\\ "} $path] + } + return $escaped +} + # Start off a git log process and arrange to read its output proc start_rev_list {view} { global startmsecs commitidx viewcomplete curview @@ -415,7 +425,8 @@ proc start_rev_list {view} { if {[catch { set fd [open [concat | git log --no-color -z --pretty=raw $show_notes \ --parents --boundary $args --stdin \ - "<<[join [concat $revs "--" $files] "\\n"]"] r] + "<<[join [concat $revs "--" \ + [escape_filter_paths $files]] "\\n"]"] r] } err]} { error_popup "[mc "Error executing git log:"] $err" return 0 @@ -569,7 +580,8 @@ proc updatecommits {} { set fd [open [concat | git log --no-color -z --pretty=raw $show_notes \ --parents --boundary $args --stdin \ "<<[join [concat $revs "--" \ - $vfilelimit($view)] "\\n"]"] r] + [escape_filter_paths + $vfilelimit($view)]] "\\n"]"] r] } err]} { error_popup "[mc "Error executing git log:"] $err" return From 97e21fb230cd899725d274dfdee04a3afd2e9cef Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Wed, 11 Jan 2017 21:08:15 +0100 Subject: [PATCH 77/98] t9001, t9116: avoid pipes When grepping through the output of a command in the test suite, there is always a chance that something goes wrong, in which case there would not be anything useful to debug. Let's redirect the output into a file instead, and grep that file, so that the log can be inspected easily if the grep fails. Signed-off-by: Johannes Schindelin --- t/t9001-send-email.sh | 4 ++-- t/t9116-git-svn-log.sh | 12 ++++++++---- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/t/t9001-send-email.sh b/t/t9001-send-email.sh index 997f90b42b..7c5ef114ac 100755 --- a/t/t9001-send-email.sh +++ b/t/t9001-send-email.sh @@ -1194,8 +1194,8 @@ test_expect_success $PREREQ 'in-reply-to but no threading' ' --to=nobody@example.com \ --in-reply-to="" \ --no-thread \ - $patches | - grep "In-Reply-To: " + $patches >out && + grep "In-Reply-To: " out ' test_expect_success $PREREQ 'no in-reply-to and no threading' ' diff --git a/t/t9116-git-svn-log.sh b/t/t9116-git-svn-log.sh index 45773ee560..0a9f1ef366 100755 --- a/t/t9116-git-svn-log.sh +++ b/t/t9116-git-svn-log.sh @@ -43,14 +43,18 @@ test_expect_success 'setup repository and import' ' test_expect_success 'run log' " git reset --hard origin/a && - git svn log -r2 origin/trunk | grep ^r2 && - git svn log -r4 origin/trunk | grep ^r4 && - git svn log -r3 | grep ^r3 + git svn log -r2 origin/trunk >out && + grep ^r2 out && + git svn log -r4 origin/trunk >out && + grep ^r4 out && + git svn log -r3 >out && + grep ^r3 out " test_expect_success 'run log against a from trunk' " git reset --hard origin/trunk && - git svn log -r3 origin/a | grep ^r3 + git svn log -r3 origin/a >out && + grep ^r3 out " printf 'r1 \nr2 \nr4 \n' > expected-range-r1-r2-r4 From 61ad94d0b8ec5d83c11511e33de8cc8761339cb8 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 7 Dec 2018 13:39:30 +0100 Subject: [PATCH 78/98] clean: do not traverse mount points It seems to be not exactly rare on Windows to install NTFS junction points (the equivalent of "bind mounts" on Linux/Unix) in worktrees, e.g. to map some development tools into a subdirectory. In such a scenario, it is pretty horrible if `git clean -dfx` traverses into the mapped directory and starts to "clean up". Let's just not do that. Let's make sure before we traverse into a directory that it is not a mount point (or junction). This addresses https://github.com/git-for-windows/git/issues/607 Signed-off-by: Johannes Schindelin --- builtin/clean.c | 14 ++++++++++++++ cache.h | 1 + compat/mingw.c | 22 ++++++++++++++++++++++ compat/mingw.h | 3 +++ git-compat-util.h | 4 ++++ path.c | 39 +++++++++++++++++++++++++++++++++++++++ t/t7300-clean.sh | 9 +++++++++ 7 files changed, 92 insertions(+) diff --git a/builtin/clean.c b/builtin/clean.c index 5abf087e7c..fbbc55cb7e 100644 --- a/builtin/clean.c +++ b/builtin/clean.c @@ -33,6 +33,8 @@ static const char *msg_remove = N_("Removing %s\n"); static const char *msg_would_remove = N_("Would remove %s\n"); static const char *msg_skip_git_dir = N_("Skipping repository %s\n"); static const char *msg_would_skip_git_dir = N_("Would skip repository %s\n"); +static const char *msg_skip_mount_point = N_("Skipping mount point %s\n"); +static const char *msg_would_skip_mount_point = N_("Would skip mount point %s\n"); static const char *msg_warn_remove_failed = N_("failed to remove %s"); static const char *msg_warn_lstat_failed = N_("could not lstat %s\n"); @@ -170,6 +172,18 @@ static int remove_dirs(struct strbuf *path, const char *prefix, int force_flag, goto out; } + if (is_mount_point(path)) { + if (!quiet) { + quote_path_relative(path->buf, prefix, "ed); + printf(dry_run ? + _(msg_would_skip_mount_point) : + _(msg_skip_mount_point), quoted.buf); + } + *dir_gone = 0; + + goto out; + } + dir = opendir(path->buf); if (!dir) { /* an empty dir could be removed even if it is unreadble */ diff --git a/cache.h b/cache.h index 04cabaac11..6448398cb3 100644 --- a/cache.h +++ b/cache.h @@ -1288,6 +1288,7 @@ int normalize_path_copy_len(char *dst, const char *src, int *prefix_len); int normalize_path_copy(char *dst, const char *src); int longest_ancestor_length(const char *path, struct string_list *prefixes); char *strip_path_suffix(const char *path, const char *suffix); +int is_mount_point_via_stat(struct strbuf *path); int daemon_avoid_alias(const char *path); /* diff --git a/compat/mingw.c b/compat/mingw.c index fe609239dd..1ba0dbbf08 100644 --- a/compat/mingw.c +++ b/compat/mingw.c @@ -2229,6 +2229,28 @@ pid_t waitpid(pid_t pid, int *status, int options) return -1; } +int mingw_is_mount_point(struct strbuf *path) +{ + WIN32_FIND_DATAW findbuf = { 0 }; + HANDLE handle; + wchar_t wfilename[MAX_PATH]; + int wlen = xutftowcs_path(wfilename, path->buf); + if (wlen < 0) + die(_("could not get long path for '%s'"), path->buf); + + /* remove trailing slash, if any */ + if (wlen > 0 && wfilename[wlen - 1] == L'/') + wfilename[--wlen] = L'\0'; + + handle = FindFirstFileW(wfilename, &findbuf); + if (handle == INVALID_HANDLE_VALUE) + return 0; + FindClose(handle); + + return (findbuf.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) && + (findbuf.dwReserved0 == IO_REPARSE_TAG_MOUNT_POINT); +} + int xutftowcsn(wchar_t *wcs, const char *utfs, size_t wcslen, int utflen) { int upos = 0, wpos = 0; diff --git a/compat/mingw.h b/compat/mingw.h index 9ad204c57c..338e8c0bf6 100644 --- a/compat/mingw.h +++ b/compat/mingw.h @@ -442,6 +442,9 @@ static inline void convert_slashes(char *path) if (*path == '\\') *path = '/'; } +struct strbuf; +int mingw_is_mount_point(struct strbuf *path); +#define is_mount_point mingw_is_mount_point #define PATH_SEP ';' char *mingw_query_user_email(void); #define query_user_email mingw_query_user_email diff --git a/git-compat-util.h b/git-compat-util.h index 607dca7534..35eaa9defa 100644 --- a/git-compat-util.h +++ b/git-compat-util.h @@ -405,6 +405,10 @@ static inline char *git_find_last_dir_sep(const char *path) #define find_last_dir_sep git_find_last_dir_sep #endif +#ifndef is_mount_point +#define is_mount_point is_mount_point_via_stat +#endif + #ifndef query_user_email #define query_user_email() NULL #endif diff --git a/path.c b/path.c index e3da1f3c4e..38a3115b41 100644 --- a/path.c +++ b/path.c @@ -1269,6 +1269,45 @@ char *strip_path_suffix(const char *path, const char *suffix) return offset == -1 ? NULL : xstrndup(path, offset); } +int is_mount_point_via_stat(struct strbuf *path) +{ + size_t len = path->len; + unsigned int current_dev; + struct stat st; + + if (!strcmp("/", path->buf)) + return 1; + + strbuf_addstr(path, "/."); + if (lstat(path->buf, &st)) { + /* + * If we cannot access the current directory, we cannot say + * that it is a bind mount. + */ + strbuf_setlen(path, len); + return 0; + } + current_dev = st.st_dev; + + /* Now look at the parent directory */ + strbuf_addch(path, '.'); + if (lstat(path->buf, &st)) { + /* + * If we cannot access the parent directory, we cannot say + * that it is a bind mount. + */ + strbuf_setlen(path, len); + return 0; + } + strbuf_setlen(path, len); + + /* + * If the device ID differs between current and parent directory, + * then it is a bind mount. + */ + return current_dev != st.st_dev; +} + int daemon_avoid_alias(const char *p) { int sl, ndot; diff --git a/t/t7300-clean.sh b/t/t7300-clean.sh index 6e6d24c1c3..a4b2898558 100755 --- a/t/t7300-clean.sh +++ b/t/t7300-clean.sh @@ -737,4 +737,13 @@ test_expect_success MINGW 'handle clean & core.longpaths = false nicely' ' test_i18ngrep "too long" .git/err ' +test_expect_success MINGW 'clean does not traverse mount points' ' + mkdir target && + >target/dont-clean-me && + git init with-mountpoint && + cmd //c "mklink /j with-mountpoint\\mountpoint target" && + git -C with-mountpoint clean -dfx && + test_path_is_file target/dont-clean-me +' + test_done From e38285ba997d96c20818f44c2bb76eb51133a9c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=A7=88=EB=88=84=EC=97=98?= Date: Thu, 16 Apr 2015 11:45:05 +0100 Subject: [PATCH 79/98] mingw: explicitly `fflush` stdout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For performance reasons `stdout` is buffered by default. That leads to problems if after printing to `stdout` a read on `stdin` is performed. For that reason interactive commands like `git clean -i` do not function properly anymore if the `stdout` is not flushed by `fflush(stdout)` before trying to read from `stdin`. So let's precede all reads on `stdin` in `git clean -i` by flushing `stdout`. Signed-off-by: 마누엘 Signed-off-by: Johannes Schindelin --- builtin/clean.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/builtin/clean.c b/builtin/clean.c index 5abf087e7c..2bd06d1339 100644 --- a/builtin/clean.c +++ b/builtin/clean.c @@ -580,6 +580,7 @@ static int *list_and_choose(struct menu_opts *opts, struct menu_stuff *stuff) clean_get_color(CLEAN_COLOR_RESET)); } + fflush(stdout); if (strbuf_getline_lf(&choice, stdin) != EOF) { strbuf_trim(&choice); } else { @@ -662,6 +663,7 @@ static int filter_by_patterns_cmd(void) clean_print_color(CLEAN_COLOR_PROMPT); printf(_("Input ignore patterns>> ")); clean_print_color(CLEAN_COLOR_RESET); + fflush(stdout); if (strbuf_getline_lf(&confirm, stdin) != EOF) strbuf_trim(&confirm); else @@ -760,6 +762,7 @@ static int ask_each_cmd(void) qname = quote_path_relative(item->string, NULL, &buf); /* TRANSLATORS: Make sure to keep [y/N] as is */ printf(_("Remove %s [y/N]? "), qname); + fflush(stdout); if (strbuf_getline_lf(&confirm, stdin) != EOF) { strbuf_trim(&confirm); } else { From 4713be92df0007a3167db207fecb109f4ac49cd9 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 21 Feb 2017 20:34:38 +0100 Subject: [PATCH 80/98] mingw: make is_hidden tests in t0001/t5611 more robust We should not actually expect the first `attrib.exe` in the PATH to be the one we are looking for. Or that it is in the PATH, for that matter. Signed-off-by: Johannes Schindelin --- t/t0001-init.sh | 2 +- t/t5611-clone-config.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/t/t0001-init.sh b/t/t0001-init.sh index 26f8206326..2456688b28 100755 --- a/t/t0001-init.sh +++ b/t/t0001-init.sh @@ -395,7 +395,7 @@ test_expect_success SYMLINKS 're-init to move gitdir symlink' ' # Tests for the hidden file attribute on windows is_hidden () { # Use the output of `attrib`, ignore the absolute path - case "$(attrib "$1")" in *H*?:*) return 0;; esac + case "$("$SYSTEMROOT"/system32/attrib "$1")" in *H*?:*) return 0;; esac return 1 } diff --git a/t/t5611-clone-config.sh b/t/t5611-clone-config.sh index 60c1ba951b..87b8073cd7 100755 --- a/t/t5611-clone-config.sh +++ b/t/t5611-clone-config.sh @@ -95,7 +95,7 @@ test_expect_success 'clone -c remote..fetch= --origin=' ' # Tests for the hidden file attribute on windows is_hidden () { # Use the output of `attrib`, ignore the absolute path - case "$(attrib "$1")" in *H*?:*) return 0;; esac + case "$("$SYSTEMROOT"/system32/attrib "$1")" in *H*?:*) return 0;; esac return 1 } From 7d2b6fe6a5aee06dc470cd9d8c9f28e87083e8e6 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 20 Feb 2015 09:52:07 +0000 Subject: [PATCH 81/98] Help debugging with MSys2 by optionally executing bash with strace MSys2's strace facility is very useful for debugging... With this patch, the bash will be executed through strace if the environment variable GIT_STRACE_COMMANDS is set, which comes in real handy when investigating issues in the test suite. Also support passing a path to a log file via GIT_STRACE_COMMANDS to force Git to call strace.exe with the `-o ` argument, i.e. to log into a file rather than print the log directly. That comes in handy when the output would otherwise misinterpreted by a calling process as part of Git's output. Note: the values "1", "yes" or "true" are *not* specifying paths, but tell Git to let strace.exe log directly to the console. Signed-off-by: Johannes Schindelin --- compat/mingw.c | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/compat/mingw.c b/compat/mingw.c index fe609239dd..b8a3b8388b 100644 --- a/compat/mingw.c +++ b/compat/mingw.c @@ -1407,6 +1407,7 @@ static pid_t mingw_spawnve_fd(const char *cmd, const char **argv, char **deltaen HANDLE cons; const char *(*quote_arg)(const char *arg) = is_msys2_sh(*argv) ? quote_arg_msys2 : quote_arg_msvc; + const char *strace_env; do_unset_environment_variables(); @@ -1465,6 +1466,31 @@ static pid_t mingw_spawnve_fd(const char *cmd, const char **argv, char **deltaen free(quoted); } + strace_env = getenv("GIT_STRACE_COMMANDS"); + if (strace_env) { + char *p = path_lookup("strace.exe", 1); + if (!p) + return error("strace not found!"); + if (xutftowcs_path(wcmd, p) < 0) { + free(p); + return -1; + } + free(p); + if (!strcmp("1", strace_env) || + !strcasecmp("yes", strace_env) || + !strcasecmp("true", strace_env)) + strbuf_insert(&args, 0, "strace ", 7); + else { + const char *quoted = quote_arg(strace_env); + struct strbuf buf = STRBUF_INIT; + strbuf_addf(&buf, "strace -o %s ", quoted); + if (quoted != strace_env) + free((char *)quoted); + strbuf_insert(&args, 0, buf.buf, buf.len); + strbuf_release(&buf); + } + } + ALLOC_ARRAY(wargs, st_add(st_mult(2, args.len), 1)); xutftowcs(wargs, args.buf, 2 * args.len + 1); strbuf_release(&args); From cfaed7098447c5d3e5a86d73c4a8dc58ae689ef4 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Tue, 11 Dec 2018 12:55:26 +0100 Subject: [PATCH 82/98] clean: remove mount points when possible Windows' equivalent to "bind mounts", NTFS junction points, can be unlinked without affecting the mount target. This is clearly what users expect to happen when they call `git clean -dfx` in a worktree that contains NTFS junction points: the junction should be removed, and the target directory of said junction should be left alone (unless it is inside the worktree). Signed-off-by: Johannes Schindelin --- builtin/clean.c | 13 +++++++++++++ compat/mingw.h | 1 + t/t7300-clean.sh | 1 + 3 files changed, 15 insertions(+) diff --git a/builtin/clean.c b/builtin/clean.c index fbbc55cb7e..daf591584f 100644 --- a/builtin/clean.c +++ b/builtin/clean.c @@ -33,8 +33,10 @@ static const char *msg_remove = N_("Removing %s\n"); static const char *msg_would_remove = N_("Would remove %s\n"); static const char *msg_skip_git_dir = N_("Skipping repository %s\n"); static const char *msg_would_skip_git_dir = N_("Would skip repository %s\n"); +#ifndef CAN_UNLINK_MOUNT_POINTS static const char *msg_skip_mount_point = N_("Skipping mount point %s\n"); static const char *msg_would_skip_mount_point = N_("Would skip mount point %s\n"); +#endif static const char *msg_warn_remove_failed = N_("failed to remove %s"); static const char *msg_warn_lstat_failed = N_("could not lstat %s\n"); @@ -173,6 +175,7 @@ static int remove_dirs(struct strbuf *path, const char *prefix, int force_flag, } if (is_mount_point(path)) { +#ifndef CAN_UNLINK_MOUNT_POINTS if (!quiet) { quote_path_relative(path->buf, prefix, "ed); printf(dry_run ? @@ -180,6 +183,16 @@ static int remove_dirs(struct strbuf *path, const char *prefix, int force_flag, _(msg_skip_mount_point), quoted.buf); } *dir_gone = 0; +#else + if (!dry_run && unlink(path->buf)) { + int saved_errno = errno; + quote_path_relative(path->buf, prefix, "ed); + errno = saved_errno; + warning_errno(_(msg_warn_remove_failed), quoted.buf); + *dir_gone = 0; + ret = -1; + } +#endif goto out; } diff --git a/compat/mingw.h b/compat/mingw.h index 338e8c0bf6..54a0987af3 100644 --- a/compat/mingw.h +++ b/compat/mingw.h @@ -445,6 +445,7 @@ static inline void convert_slashes(char *path) struct strbuf; int mingw_is_mount_point(struct strbuf *path); #define is_mount_point mingw_is_mount_point +#define CAN_UNLINK_MOUNT_POINTS 1 #define PATH_SEP ';' char *mingw_query_user_email(void); #define query_user_email mingw_query_user_email diff --git a/t/t7300-clean.sh b/t/t7300-clean.sh index a4b2898558..1aee0d5949 100755 --- a/t/t7300-clean.sh +++ b/t/t7300-clean.sh @@ -743,6 +743,7 @@ test_expect_success MINGW 'clean does not traverse mount points' ' git init with-mountpoint && cmd //c "mklink /j with-mountpoint\\mountpoint target" && git -C with-mountpoint clean -dfx && + test_path_is_missing with-mountpoint/mountpoint && test_path_is_file target/dont-clean-me ' From eff1290ede92bc0aa253f447eb0e339210cccb50 Mon Sep 17 00:00:00 2001 From: Philip Oakley Date: Sun, 6 Oct 2019 18:40:55 +0100 Subject: [PATCH 83/98] vcpkg_install: detect lack of Git The vcpkg_install batch file depends on the availability of a working Git on the CMD path. This may not be present if the user has selected the 'bash only' option during Git-for-Windows install. Detect and tell the user about their lack of a working Git in the CMD window. Fixes #2348. A separate PR https://github.com/git-for-windows/build-extra/pull/258 now highlights the recommended path setting during install. Signed-off-by: Philip Oakley --- compat/vcbuild/vcpkg_install.bat | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/compat/vcbuild/vcpkg_install.bat b/compat/vcbuild/vcpkg_install.bat index ebd0bad242..bcbbf536af 100644 --- a/compat/vcbuild/vcpkg_install.bat +++ b/compat/vcbuild/vcpkg_install.bat @@ -36,6 +36,13 @@ REM ================================================================ dir vcpkg\vcpkg.exe >nul 2>nul && GOTO :install_libraries + git.exe version 2>nul + IF ERRORLEVEL 1 ( + echo "***" + echo "Git not found. Please adjust your CMD path or Git install option." + echo "***" + EXIT /B 1 ) + echo Fetching vcpkg in %cwd%vcpkg git.exe clone https://github.com/Microsoft/vcpkg vcpkg IF ERRORLEVEL 1 ( EXIT /B 1 ) From 058c8faae5e509c1e6a43a7b6cfaf7344c1a464c Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Sun, 6 Oct 2019 22:58:49 +0200 Subject: [PATCH 84/98] mingw: move Git for Windows' system config where users expect it Git for Windows' prefix is `/mingw64/` (or `/mingw32/` for 32-bit versions), therefore the system config is located at the clunky location `C:\Program Files\Git\mingw64\etc\gitconfig`. This moves the system config into a more logical location: the `mingw64` part of `C:\Program Files\Git\mingw64\etc\gitconfig` never made sense, as it is a mere implementation detail. Let's skip the `mingw64` part and move this to `C:\Program Files\Git\etc\gitconfig`. Side note: in the rare (and not recommended) case a user chooses to install 32-bit Git for Windows on a 64-bit system, the path will of course be `C:\Program Files (x86)\Git\etc\gitconfig`. Background: During the Git for Windows v1.x days, the system config was located at `C:\Program Files (x86)\Git\etc\gitconfig`. With Git for Windows v2.x, it moved to `C:\Program Files\Git\mingw64\gitconfig` (or `C:\Program Files (x86)\Git\mingw32\gitconfig`). Rather than fixing it back then, we tried to introduce a "Windows-wide" config, but that never caught on. Likewise, we move the system `gitattributes` into the same directory. Obviously, we are cautious to do this only for the known install locations `/mingw64` and `/mingw32`; If anybody wants to override that while building their version of Git (e.g. via `make prefix=$HOME`), we leave the default location of the system config and gitattributes alone. Signed-off-by: Johannes Schindelin --- config.mak.uname | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/config.mak.uname b/config.mak.uname index cc8efd95b1..973b3a00f1 100644 --- a/config.mak.uname +++ b/config.mak.uname @@ -424,6 +424,11 @@ ifeq ($(uname_S),Windows) NO_POSIX_GOODIES = UnfortunatelyYes NATIVE_CRLF = YesPlease DEFAULT_HELP_FORMAT = html +ifeq (/mingw64,$(subst 32,64,$(prefix))) + # Move system config into top-level /etc/ + ETC_GITCONFIG = ../etc/gitconfig + ETC_GITATTRIBUTES = ../etc/gitattributes +endif CC = compat/vcbuild/scripts/clink.pl AR = compat/vcbuild/scripts/lib.pl @@ -670,6 +675,11 @@ else NO_LIBPCRE1_JIT = UnfortunatelyYes NO_CURL = USE_NED_ALLOCATOR = YesPlease + ifeq (/mingw64,$(subst 32,64,$(prefix))) + # Move system config into top-level /etc/ + ETC_GITCONFIG = ../etc/gitconfig + ETC_GITATTRIBUTES = ../etc/gitattributes + endif else COMPAT_CFLAGS += -D__USE_MINGW_ANSI_STDIO NO_CURL = YesPlease From 0f163ab3a6bdf21e5c069cdeb66358c19998d89b Mon Sep 17 00:00:00 2001 From: Nathan Sanders Date: Thu, 4 Jul 2019 13:24:53 +0200 Subject: [PATCH 85/98] mingw: cope with the Isilon network file system On certain network filesystems (currently encounterd with Isilon, but in theory more network storage solutions could be causing the same issue), when the directory in question is missing, `raceproof_create_file()` fails with an `ERROR_INVALID_PARAMETER` instead of an `ERROR_PATH_NOT_FOUND`. Since it is highly unlikely that we produce such an error by mistake (the parameters we pass are fairly benign), we can be relatively certain that the directory is missing in this instance. So let's just translate that error automagically. This fixes https://github.com/git-for-windows/git/issues/1345. Signed-off-by: Nathan Sanders Signed-off-by: Johannes Schindelin --- compat/mingw.c | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/compat/mingw.c b/compat/mingw.c index fe609239dd..39aed476ab 100644 --- a/compat/mingw.c +++ b/compat/mingw.c @@ -430,8 +430,19 @@ static int mingw_open_append(wchar_t const *wfilename, int oflags, ...) handle = CreateFileW(wfilename, FILE_APPEND_DATA, FILE_SHARE_WRITE | FILE_SHARE_READ, NULL, create, FILE_ATTRIBUTE_NORMAL, NULL); - if (handle == INVALID_HANDLE_VALUE) - return errno = err_win_to_posix(GetLastError()), -1; + if (handle == INVALID_HANDLE_VALUE) { + DWORD err = GetLastError(); + /* + * Some network storage solutions (e.g. Isilon) might return + * ERROR_INVALID_PARAMETER instead of expected error + * ERROR_PATH_NOT_FOUND, which results in a unknow error. If + * so, the error is now forced to be an ERROR_PATH_NOT_FOUND + * error instead. + */ + if (err == ERROR_INVALID_PARAMETER) + err = ERROR_PATH_NOT_FOUND; + return errno = err_win_to_posix(err), -1; + } /* * No O_APPEND here, because the CRT uses it only to reset the From c55ee6efeb2131180e732005ef5349931fcb554c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlo=20Marcelo=20Arenas=20Bel=C3=B3n?= Date: Sat, 31 Aug 2019 06:26:10 -0700 Subject: [PATCH 86/98] config.mak.uname: PCRE1 cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit no longer relevant after moving to PCRE2 Signed-off-by: Carlo Marcelo Arenas Belón --- config.mak.uname | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/config.mak.uname b/config.mak.uname index cc8efd95b1..56a072f1cd 100644 --- a/config.mak.uname +++ b/config.mak.uname @@ -666,8 +666,7 @@ else HAVE_LIBCHARSET_H = YesPlease NO_GETTEXT = USE_GETTEXT_SCHEME = fallthrough - USE_LIBPCRE= YesPlease - NO_LIBPCRE1_JIT = UnfortunatelyYes + USE_LIBPCRE = YesPlease NO_CURL = USE_NED_ALLOCATOR = YesPlease else From 3d170b096eee92037c65215896a42bf67f2872cd Mon Sep 17 00:00:00 2001 From: Matt R Date: Thu, 29 Aug 2019 18:32:19 -0400 Subject: [PATCH 87/98] rebase -r: let `label` generate safer labels MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `label` todo command in interactive rebases creates temporary refs in the `refs/rewritten/` namespace. These refs are stored as loose refs, i.e. as files in `.git/refs/rewritten/`, therefore they have to conform with file name limitations on the current filesystem. This poses a problem in particular on NTFS/FAT, where e.g. the colon character is not a valid part of a file name. Let's safeguard against this by replacing not only white-space characters by dashes, but all non-alpha-numeric ones. However, we exempt non-ASCII UTF-8 characters from that, as it should be quite possible to reflect branch names such as `↯↯↯` in refs/file names. Signed-off-by: Matthew Rogers Signed-off-by: Johannes Schindelin --- sequencer.c | 12 +++++++++++- t/t3430-rebase-merges.sh | 6 ++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/sequencer.c b/sequencer.c index 9d5964fd81..4960a26ac3 100644 --- a/sequencer.c +++ b/sequencer.c @@ -4598,8 +4598,18 @@ static int make_script_with_merges(struct pretty_print_context *pp, else strbuf_addbuf(&label, &oneline); + /* + * Sanitize labels by replacing non-alpha-numeric characters + * (including white-space ones) by dashes, as they might be + * illegal in file names (and hence in ref names). + * + * Note that we retain non-ASCII UTF-8 characters (identified + * via the most significant bit). They should be all acceptable + * in file names. We do not validate the UTF-8 here, that's not + * the job of this function. + */ for (p1 = label.buf; *p1; p1++) - if (isspace(*p1)) + if (!(*p1 & 0x80) && !isalnum(*p1)) *(char *)p1 = '-'; strbuf_reset(&buf); diff --git a/t/t3430-rebase-merges.sh b/t/t3430-rebase-merges.sh index 9efcf4808a..f728aba995 100755 --- a/t/t3430-rebase-merges.sh +++ b/t/t3430-rebase-merges.sh @@ -468,4 +468,10 @@ test_expect_success '--rebase-merges with strategies' ' test_cmp expect G.t ' +test_expect_success '--rebase-merges with commit that can generate bad characters for filename' ' + git checkout -b colon-in-label E && + git merge -m "colon: this should work" G && + git rebase --rebase-merges --force-rebase E +' + test_done From 824b7dd44d009aa52275ce34da3555ae1cd87758 Mon Sep 17 00:00:00 2001 From: Philip Oakley Date: Sun, 6 Oct 2019 18:43:57 +0100 Subject: [PATCH 88/98] vcpkg_install: add comment regarding slow network connections The vcpkg downloads may not succeed. Warn careful readers of the time out. A simple retry will usually resolve the issue. Signed-off-by: Philip Oakley Signed-off-by: Johannes Schindelin --- compat/vcbuild/vcpkg_install.bat | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/compat/vcbuild/vcpkg_install.bat b/compat/vcbuild/vcpkg_install.bat index bcbbf536af..8330d8120f 100644 --- a/compat/vcbuild/vcpkg_install.bat +++ b/compat/vcbuild/vcpkg_install.bat @@ -80,6 +80,12 @@ REM ================================================================ :sub__install_one echo Installing package %1... + REM vcpkg may not be reliable on slow, intermittent or proxy + REM connections, see e.g. + REM https://social.msdn.microsoft.com/Forums/windowsdesktop/en-US/4a8f7be5-5e15-4213-a7bb-ddf424a954e6/winhttpsendrequest-ends-with-12002-errorhttptimeout-after-21-seconds-no-matter-what-timeout?forum=windowssdk + REM which explains the hidden 21 second timeout + REM (last post by Dave : Microsoft - Windows Networking team) + .\vcpkg.exe install %1:%arch% IF ERRORLEVEL 1 ( EXIT /B 1 ) From aa6df6967dda44431d0dbb52f2c667b5b4226a34 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 25 Oct 2019 01:02:33 +0200 Subject: [PATCH 89/98] config: normalize the path of the system gitconfig Git for Windows is compiled with a runtime prefix, and that runtime prefix is typically `C:/Program Files/Git/mingw64`. As we want the system gitconfig to live in the sibling directory `etc`, we define the relative path as `../etc/gitconfig`. However, as reported by Philip Oakley, the output of `git config --show-origin --system -l` looks rather ugly, as it shows the path as `file:C:/Program Files/Git/mingw64/../etc/gitconfig`, i.e. with the `mingw64/../` part. By normalizing the path, we get a prettier path. Signed-off-by: Johannes Schindelin --- config.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/config.c b/config.c index e7052b3977..a5f15ea748 100644 --- a/config.c +++ b/config.c @@ -1657,9 +1657,11 @@ static int git_config_from_blob_ref(config_fn_t fn, const char *git_etc_gitconfig(void) { - static const char *system_wide; - if (!system_wide) + static char *system_wide; + if (!system_wide) { system_wide = system_path(ETC_GITCONFIG); + normalize_path_copy(system_wide, system_wide); + } return system_wide; } From 81b74a86626bacbec6834b017999cf3f5a21d9b9 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 26 Jan 2018 16:34:59 +0100 Subject: [PATCH 90/98] mingw: demonstrate that all file handles are inherited by child processes When spawning child processes, we really should be careful which file handles we let them inherit. This is doubly important on Windows, where we cannot rename, delete, or modify files if there is still a file handle open. Sadly, we have to guard this test inside #ifdef WIN32: we need to use the value of the HANDLE directly, and that concept does not exist on Linux/Unix. Signed-off-by: Johannes Schindelin --- t/helper/test-run-command.c | 44 +++++++++++++++++++++++++++++++++++++ t/t0061-run-command.sh | 4 ++++ 2 files changed, 48 insertions(+) diff --git a/t/helper/test-run-command.c b/t/helper/test-run-command.c index ead6dc611a..40ec4dbb6e 100644 --- a/t/helper/test-run-command.c +++ b/t/helper/test-run-command.c @@ -200,6 +200,46 @@ static int testsuite(int argc, const char **argv) return !!ret; } +static int inherit_handle(const char *argv0) +{ + struct child_process cp = CHILD_PROCESS_INIT; + char path[PATH_MAX]; + int tmp; + + /* First, open an inheritable handle */ + xsnprintf(path, sizeof(path), "out-XXXXXX"); + tmp = xmkstemp(path); + + argv_array_pushl(&cp.args, + "test-tool", argv0, "inherited-handle-child", NULL); + cp.in = -1; + cp.no_stdout = cp.no_stderr = 1; + if (start_command(&cp) < 0) + die("Could not start child process"); + + /* Then close it, and try to delete it. */ + close(tmp); + if (unlink(path)) + die("Could not delete '%s'", path); + + if (close(cp.in) < 0 || finish_command(&cp) < 0) + die("Child did not finish"); + + return 0; +} + +static int inherit_handle_child(void) +{ + struct strbuf buf = STRBUF_INIT; + + if (strbuf_read(&buf, 0, 0) < 0) + die("Could not read stdin"); + printf("Received %s\n", buf.buf); + strbuf_release(&buf); + + return 0; +} + int cmd__run_command(int argc, const char **argv) { struct child_process proc = CHILD_PROCESS_INIT; @@ -207,6 +247,10 @@ int cmd__run_command(int argc, const char **argv) if (argc > 1 && !strcmp(argv[1], "testsuite")) exit(testsuite(argc - 1, argv + 1)); + if (!strcmp(argv[1], "inherited-handle")) + exit(inherit_handle(argv[0])); + if (!strcmp(argv[1], "inherited-handle-child")) + exit(inherit_handle_child()); if (argc < 3) return 1; diff --git a/t/t0061-run-command.sh b/t/t0061-run-command.sh index 17c9c0f3bb..473a3405ef 100755 --- a/t/t0061-run-command.sh +++ b/t/t0061-run-command.sh @@ -12,6 +12,10 @@ cat >hello-script <<-EOF cat hello-script EOF +test_expect_failure MINGW 'subprocess inherits only std handles' ' + test-tool run-command inherited-handle +' + test_expect_success 'start_command reports ENOENT (slash)' ' test-tool run-command start-command-ENOENT ./does-not-exist 2>err && test_i18ngrep "\./does-not-exist" err From 604477e836d4a6b021268e6a5ef11553919b6e2b Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Wed, 7 Feb 2018 13:50:03 +0100 Subject: [PATCH 91/98] mingw: work around incorrect standard handles For some reason, when being called via TortoiseGit the standard handles, or at least what is returned by _get_osfhandle(0) for standard input, can take on the value (HANDLE)-2 (which is not a legal value, according to the documentation). Even if this value is not documented anywhere, CreateProcess() seems to work fine without complaints if hStdInput set to this value. In contrast, the upcoming code to restrict which file handles get inherited by spawned processes would result in `ERROR_INVALID_PARAMETER` when including such handle values in the list. To help this, special-case the value (HANDLE)-2 returned by _get_osfhandle() and replace it with INVALID_HANDLE_VALUE, which will hopefully let the handle inheritance restriction work even when called from TortoiseGit. This fixes https://github.com/git-for-windows/git/issues/1481 Signed-off-by: Johannes Schindelin --- compat/winansi.c | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/compat/winansi.c b/compat/winansi.c index 54fd701cbf..c27b20a79d 100644 --- a/compat/winansi.c +++ b/compat/winansi.c @@ -662,10 +662,20 @@ void winansi_init(void) */ HANDLE winansi_get_osfhandle(int fd) { + HANDLE ret; + if (fd == 1 && (fd_is_interactive[1] & FD_SWAPPED)) return hconsole1; if (fd == 2 && (fd_is_interactive[2] & FD_SWAPPED)) return hconsole2; - return (HANDLE)_get_osfhandle(fd); + ret = (HANDLE)_get_osfhandle(fd); + + /* + * There are obviously circumstances under which _get_osfhandle() + * returns (HANDLE)-2. This is not documented anywhere, but that is so + * clearly an invalid handle value that we can just work around this + * and return the correct value for invalid handles. + */ + return ret == (HANDLE)-2 ? INVALID_HANDLE_VALUE : ret; } From 6fa32478ab73fee8d4469d376f677a8b7e5e5f85 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 26 Jan 2018 15:37:38 +0100 Subject: [PATCH 92/98] mingw: spawned processes need to inherit only standard handles By default, CreateProcess() does not inherit any open file handles, unless the bInheritHandles parameter is set to TRUE. Which we do need to set because we need to pass in stdin/stdout/stderr to talk to the child processes. Sadly, this means that all file handles (unless marked via O_NOINHERIT) are inherited. This lead to problems in GVFS Git, where a long-running read-object hook is used to hydrate missing objects, and depending on the circumstances, might only be called *after* Git opened a file handle. Ideally, we would not open files without O_NOINHERIT unless *really* necessary (i.e. when we want to pass the opened file handle as standard handle into a child process), but apparently it is all-too-easy to introduce incorrect open() calls: this happened, and prevented updating a file after the read-object hook was started because the hook still held a handle on said file. Happily, there is a solution: as described in the "Old New Thing" https://blogs.msdn.microsoft.com/oldnewthing/20111216-00/?p=8873 there is a way, starting with Windows Vista, that lets us define precisely which handles should be inherited by the child process. And since we bumped the minimum Windows version for use with Git for Windows to Vista with v2.10.1 (i.e. a *long* time ago), we can use this method. So let's do exactly that. We need to make sure that the list of handles to inherit does not contain duplicates; Otherwise CreateProcessW() would fail with ERROR_INVALID_ARGUMENT. While at it, stop setting errno to ENOENT unless it really is the correct value. Also, fall back to not limiting handle inheritance under certain error conditions (e.g. on Windows 7, which is a lot stricter in what handles you can specify to limit to). Signed-off-by: Johannes Schindelin --- compat/mingw.c | 120 +++++++++++++++++++++++++++++++++++++---- t/t0061-run-command.sh | 2 +- 2 files changed, 110 insertions(+), 12 deletions(-) diff --git a/compat/mingw.c b/compat/mingw.c index fe609239dd..cac18cc3da 100644 --- a/compat/mingw.c +++ b/compat/mingw.c @@ -1398,8 +1398,13 @@ static pid_t mingw_spawnve_fd(const char *cmd, const char **argv, char **deltaen const char *dir, int prepend_cmd, int fhin, int fhout, int fherr) { - STARTUPINFOW si; + static int restrict_handle_inheritance = 1; + STARTUPINFOEXW si; PROCESS_INFORMATION pi; + LPPROC_THREAD_ATTRIBUTE_LIST attr_list = NULL; + HANDLE stdhandles[3]; + DWORD stdhandles_count = 0; + SIZE_T size; struct strbuf args; wchar_t wcmd[MAX_PATH], wdir[MAX_PATH], *wargs, *wenvblk = NULL; unsigned flags = CREATE_UNICODE_ENVIRONMENT; @@ -1435,11 +1440,23 @@ static pid_t mingw_spawnve_fd(const char *cmd, const char **argv, char **deltaen CloseHandle(cons); } memset(&si, 0, sizeof(si)); - si.cb = sizeof(si); - si.dwFlags = STARTF_USESTDHANDLES; - si.hStdInput = winansi_get_osfhandle(fhin); - si.hStdOutput = winansi_get_osfhandle(fhout); - si.hStdError = winansi_get_osfhandle(fherr); + si.StartupInfo.cb = sizeof(si); + si.StartupInfo.hStdInput = winansi_get_osfhandle(fhin); + si.StartupInfo.hStdOutput = winansi_get_osfhandle(fhout); + si.StartupInfo.hStdError = winansi_get_osfhandle(fherr); + + /* The list of handles cannot contain duplicates */ + if (si.StartupInfo.hStdInput != INVALID_HANDLE_VALUE) + stdhandles[stdhandles_count++] = si.StartupInfo.hStdInput; + if (si.StartupInfo.hStdOutput != INVALID_HANDLE_VALUE && + si.StartupInfo.hStdOutput != si.StartupInfo.hStdInput) + stdhandles[stdhandles_count++] = si.StartupInfo.hStdOutput; + if (si.StartupInfo.hStdError != INVALID_HANDLE_VALUE && + si.StartupInfo.hStdError != si.StartupInfo.hStdInput && + si.StartupInfo.hStdError != si.StartupInfo.hStdOutput) + stdhandles[stdhandles_count++] = si.StartupInfo.hStdError; + if (stdhandles_count) + si.StartupInfo.dwFlags |= STARTF_USESTDHANDLES; if (*argv && !strcmp(cmd, *argv)) wcmd[0] = L'\0'; @@ -1472,16 +1489,97 @@ static pid_t mingw_spawnve_fd(const char *cmd, const char **argv, char **deltaen wenvblk = make_environment_block(deltaenv); memset(&pi, 0, sizeof(pi)); - ret = CreateProcessW(*wcmd ? wcmd : NULL, wargs, NULL, NULL, TRUE, - flags, wenvblk, dir ? wdir : NULL, &si, &pi); + if (restrict_handle_inheritance && stdhandles_count && + (InitializeProcThreadAttributeList(NULL, 1, 0, &size) || + GetLastError() == ERROR_INSUFFICIENT_BUFFER) && + (attr_list = (LPPROC_THREAD_ATTRIBUTE_LIST) + (HeapAlloc(GetProcessHeap(), 0, size))) && + InitializeProcThreadAttributeList(attr_list, 1, 0, &size) && + UpdateProcThreadAttribute(attr_list, 0, + PROC_THREAD_ATTRIBUTE_HANDLE_LIST, + stdhandles, + stdhandles_count * sizeof(HANDLE), + NULL, NULL)) { + si.lpAttributeList = attr_list; + flags |= EXTENDED_STARTUPINFO_PRESENT; + } + + ret = CreateProcessW(*wcmd ? wcmd : NULL, wargs, NULL, NULL, + stdhandles_count ? TRUE : FALSE, + flags, wenvblk, dir ? wdir : NULL, + &si.StartupInfo, &pi); + + /* + * On Windows 2008 R2, it seems that specifying certain types of handles + * (such as FILE_TYPE_CHAR or FILE_TYPE_PIPE) will always produce an + * error. Rather than playing finicky and fragile games, let's just try + * to detect this situation and simply try again without restricting any + * handle inheritance. This is still better than failing to create + * processes. + */ + if (!ret && restrict_handle_inheritance && stdhandles_count) { + DWORD err = GetLastError(); + struct strbuf buf = STRBUF_INIT; + + if (err != ERROR_NO_SYSTEM_RESOURCES && + /* + * On Windows 7 and earlier, handles on pipes and character + * devices are inherited automatically, and cannot be + * specified in the thread handle list. Rather than trying + * to catch each and every corner case (and running the + * chance of *still* forgetting a few), let's just fall + * back to creating the process without trying to limit the + * handle inheritance. + */ + !(err == ERROR_INVALID_PARAMETER && + GetVersion() >> 16 < 9200) && + !getenv("SUPPRESS_HANDLE_INHERITANCE_WARNING")) { + DWORD fl = 0; + int i; + + setenv("SUPPRESS_HANDLE_INHERITANCE_WARNING", "1", 1); + + for (i = 0; i < stdhandles_count; i++) { + HANDLE h = stdhandles[i]; + strbuf_addf(&buf, "handle #%d: %p (type %lx, " + "handle info (%d) %lx\n", i, h, + GetFileType(h), + GetHandleInformation(h, &fl), + fl); + } + strbuf_addstr(&buf, "\nThis is a bug; please report it " + "at\nhttps://github.com/git-for-windows/" + "git/issues/new\n\n" + "To suppress this warning, please set " + "the environment variable\n\n" + "\tSUPPRESS_HANDLE_INHERITANCE_WARNING=1" + "\n"); + } + restrict_handle_inheritance = 0; + flags &= ~EXTENDED_STARTUPINFO_PRESENT; + ret = CreateProcessW(*wcmd ? wcmd : NULL, wargs, NULL, NULL, + TRUE, flags, wenvblk, dir ? wdir : NULL, + &si.StartupInfo, &pi); + if (ret && buf.len) { + errno = err_win_to_posix(GetLastError()); + warning("failed to restrict file handles (%ld)\n\n%s", + err, buf.buf); + } + strbuf_release(&buf); + } else if (!ret) + errno = err_win_to_posix(GetLastError()); + + if (si.lpAttributeList) + DeleteProcThreadAttributeList(si.lpAttributeList); + if (attr_list) + HeapFree(GetProcessHeap(), 0, attr_list); free(wenvblk); free(wargs); - if (!ret) { - errno = ENOENT; + if (!ret) return -1; - } + CloseHandle(pi.hThread); /* diff --git a/t/t0061-run-command.sh b/t/t0061-run-command.sh index 473a3405ef..7d599675e3 100755 --- a/t/t0061-run-command.sh +++ b/t/t0061-run-command.sh @@ -12,7 +12,7 @@ cat >hello-script <<-EOF cat hello-script EOF -test_expect_failure MINGW 'subprocess inherits only std handles' ' +test_expect_success MINGW 'subprocess inherits only std handles' ' test-tool run-command inherited-handle ' From cba298e17c5af2338bf5a026de9e6865510ce401 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Mon, 16 Feb 2015 14:06:59 +0100 Subject: [PATCH 93/98] mingw: include the Python parts in the build While Git for Windows does not _ship_ Python (in order to save on bandwidth), MSYS2 provides very fine Python interpreters that users can easily take advantage of, by using Git for Windows within its SDK. Signed-off-by: Johannes Schindelin --- config.mak.uname | 1 + 1 file changed, 1 insertion(+) diff --git a/config.mak.uname b/config.mak.uname index 973b3a00f1..0efb2c9f83 100644 --- a/config.mak.uname +++ b/config.mak.uname @@ -675,6 +675,7 @@ else NO_LIBPCRE1_JIT = UnfortunatelyYes NO_CURL = USE_NED_ALLOCATOR = YesPlease + NO_PYTHON = ifeq (/mingw64,$(subst 32,64,$(prefix))) # Move system config into top-level /etc/ ETC_GITCONFIG = ../etc/gitconfig From a86420d323cada791203e924546d85d1098cd119 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 31 May 2019 19:57:39 +0200 Subject: [PATCH 94/98] mingw: restrict file handle inheritance only on Windows 7 and later Turns out that it don't work so well on Vista, see https://github.com/git-for-windows/git/issues/1742 for details. According to https://devblogs.microsoft.com/oldnewthing/?p=8873, it *should* work on Windows Vista and later. But apparently there are issues on Windows Vista when pipes are involved. Given that Windows Vista is past its end of life (official support ended on April 11th, 2017), let's not spend *too* much time on this issue and just disable the file handle inheritance restriction on any Windows version earlier than Windows 7. Signed-off-by: Johannes Schindelin --- Documentation/config/core.txt | 6 ++++++ compat/mingw.c | 22 +++++++++++++++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/Documentation/config/core.txt b/Documentation/config/core.txt index 852d2ba37a..ad4fa4dccd 100644 --- a/Documentation/config/core.txt +++ b/Documentation/config/core.txt @@ -559,6 +559,12 @@ core.unsetenvvars:: Defaults to `PERL5LIB` to account for the fact that Git for Windows insists on using its own Perl interpreter. +core.restrictinheritedhandles:: + Windows-only: override whether spawned processes inherit only standard + file handles (`stdin`, `stdout` and `stderr`) or all handles. Can be + `auto`, `true` or `false`. Defaults to `auto`, which means `true` on + Windows 7 and later, and `false` on older Windows versions. + core.createObject:: You can set this to 'link', in which case a hardlink followed by a delete of the source are used to make sure that object creation diff --git a/compat/mingw.c b/compat/mingw.c index cac18cc3da..2b6eca2f56 100644 --- a/compat/mingw.c +++ b/compat/mingw.c @@ -212,6 +212,7 @@ enum hide_dotfiles_type { HIDE_DOTFILES_DOTGITONLY }; +static int core_restrict_inherited_handles = -1; static enum hide_dotfiles_type hide_dotfiles = HIDE_DOTFILES_DOTGITONLY; static char *unset_environment_variables; @@ -231,6 +232,15 @@ int mingw_core_config(const char *var, const char *value, void *cb) return 0; } + if (!strcmp(var, "core.restrictinheritedhandles")) { + if (value && !strcasecmp(value, "auto")) + core_restrict_inherited_handles = -1; + else + core_restrict_inherited_handles = + git_config_bool(var, value); + return 0; + } + return 0; } @@ -1398,7 +1408,7 @@ static pid_t mingw_spawnve_fd(const char *cmd, const char **argv, char **deltaen const char *dir, int prepend_cmd, int fhin, int fhout, int fherr) { - static int restrict_handle_inheritance = 1; + static int restrict_handle_inheritance = -1; STARTUPINFOEXW si; PROCESS_INFORMATION pi; LPPROC_THREAD_ATTRIBUTE_LIST attr_list = NULL; @@ -1413,6 +1423,16 @@ static pid_t mingw_spawnve_fd(const char *cmd, const char **argv, char **deltaen const char *(*quote_arg)(const char *arg) = is_msys2_sh(*argv) ? quote_arg_msys2 : quote_arg_msvc; + if (restrict_handle_inheritance < 0) + restrict_handle_inheritance = core_restrict_inherited_handles; + /* + * The following code to restrict which handles are inherited seems + * to work properly only on Windows 7 and later, so let's disable it + * on Windows Vista and 2008. + */ + if (restrict_handle_inheritance < 0) + restrict_handle_inheritance = GetVersion() >> 16 >= 7601; + do_unset_environment_variables(); /* Determine whether or not we are associated to a console */ From 0e2e99d15c26ebf9535d85e7720905c4cccdef65 Mon Sep 17 00:00:00 2001 From: Thomas Braun Date: Thu, 8 May 2014 21:43:24 +0200 Subject: [PATCH 95/98] Config option to disable side-band-64k for transport Since commit 0c499ea60f the send-pack builtin uses the side-band-64k capability if advertised by the server. Unfortunately this breaks pushing over the dump git protocol if used over a network connection. The detailed reasons for this breakage are (by courtesy of Jeff Preshing, quoted from ttps://groups.google.com/d/msg/msysgit/at8D7J-h7mw/eaLujILGUWoJ): ---------------------------------------------------------------------------- MinGW wraps Windows sockets in CRT file descriptors in order to mimic the functionality of POSIX sockets. This causes msvcrt.dll to treat sockets as Installable File System (IFS) handles, calling ReadFile, WriteFile, DuplicateHandle and CloseHandle on them. This approach works well in simple cases on recent versions of Windows, but does not support all usage patterns. In particular, using this approach, any attempt to read & write concurrently on the same socket (from one or more processes) will deadlock in a scenario where the read waits for a response from the server which is only invoked after the write. This is what send_pack currently attempts to do in the use_sideband codepath. ---------------------------------------------------------------------------- The new config option "sendpack.sideband" allows to override the side-band-64k capability of the server, and thus makes the dump git protocol work. Other transportation methods like ssh and http/https still benefit from the sideband channel, therefore the default value of "sendpack.sideband" is still true. [jes: split out the documentation into Documentation/config/] Signed-off-by: Thomas Braun Signed-off-by: Johannes Schindelin Signed-off-by: Oliver Schneider --- Documentation/config.txt | 2 ++ Documentation/config/sendpack.txt | 5 +++++ send-pack.c | 14 +++++++++++++- 3 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 Documentation/config/sendpack.txt diff --git a/Documentation/config.txt b/Documentation/config.txt index f50f1b4128..5bd5d994df 100644 --- a/Documentation/config.txt +++ b/Documentation/config.txt @@ -429,6 +429,8 @@ include::config/reset.txt[] include::config/sendemail.txt[] +include::config/sendpack.txt[] + include::config/sequencer.txt[] include::config/showbranch.txt[] diff --git a/Documentation/config/sendpack.txt b/Documentation/config/sendpack.txt new file mode 100644 index 0000000000..e306f657fb --- /dev/null +++ b/Documentation/config/sendpack.txt @@ -0,0 +1,5 @@ +sendpack.sideband:: + Allows to disable the side-band-64k capability for send-pack even + when it is advertised by the server. Makes it possible to work + around a limitation in the git for windows implementation together + with the dump git protocol. Defaults to true. diff --git a/send-pack.c b/send-pack.c index 34c77cbb1a..610029a0d4 100644 --- a/send-pack.c +++ b/send-pack.c @@ -38,6 +38,16 @@ int option_parse_push_signed(const struct option *opt, die("bad %s argument: %s", opt->long_name, arg); } +static int config_use_sideband = 1; + +static int send_pack_config(const char *var, const char *value, void *unused) +{ + if (!strcmp("sendpack.sideband", var)) + config_use_sideband = git_config_bool(var, value); + + return 0; +} + static void feed_object(const struct object_id *oid, FILE *fh, int negative) { if (negative && @@ -391,6 +401,8 @@ int send_pack(struct send_pack_args *args, const char *push_cert_nonce = NULL; struct packet_reader reader; + git_config(send_pack_config, NULL); + /* Does the other end support the reporting? */ if (server_supports("report-status")) status_report = 1; @@ -398,7 +410,7 @@ int send_pack(struct send_pack_args *args, allow_deleting_refs = 1; if (server_supports("ofs-delta")) args->use_ofs_delta = 1; - if (server_supports("side-band-64k")) + if (config_use_sideband && server_supports("side-band-64k")) use_sideband = 1; if (server_supports("quiet")) quiet_supported = 1; From 06c0ae22b44db86f7671ff4999372b6c6e68b7a3 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 25 Oct 2019 23:44:08 +0200 Subject: [PATCH 96/98] vreportf(): avoid relying on stdio buffering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The MSVC runtime behavior differs from glibc's with respect to `fprintf(stderr, ...)` in that the former writes out the message character by character. In t5516, this leads to a funny problem where a `git fetch` process as well as the `git upload-pack` process spawned by it _both_ call `die()` at the same time. The output can look like this: fatal: git uploadfata-lp: raemcokte :error: upload-pnot our arcef k6: n4ot our ea4cr1e3f 36d45ea94fca1398e86a771eda009872d63adb28598f6a9 8e86a771eda009872d6ab2886 Let's avoid this predicament altogether by rendering the entire message, including the prefix and the trailing newline, into the buffer we already have (and which is still fixed size) and then write it out via `write_in_full()`. We still clip the message to at most 4095 characters. The history of `vreportf()` with regard to this issue includes the following commits: d048a96e (2007-11-09) - 'char msg[256]' is introduced to avoid interleaving 389d1767 (2009-03-25) - Buffer size increased to 1024 to avoid truncation 625a860c (2009-11-22) - Buffer size increased to 4096 to avoid truncation f4c3edc0 (2015-08-11) - Buffer removed to avoid truncation b5a9e435 (2017-01-11) - Reverts f4c3edc0 to be able to replace control chars before sending to stderr 9ac13ec9 (2006-10-11) - Another attempt to solve interleaving. This is seemingly related to d048a96e. 137a0d0e (2007-11-19) - Addresses out-of-order for display() 34df8aba (2009-03-10) - Switches xwrite() to fprintf() in recv_sideband() to support UTF-8 emulation eac14f89 (2012-01-14) - Removes the need for fprintf() for UTF-8 emulation, so it's safe to use xwrite() again 5e5be9e2 (2016-06-28) - recv_sideband() uses xwrite() again Note that we print nothing if the `vsnprintf()` call failed to render the error message; There is little we can do in that case, and it should not happen anyway. The process may have written to `stderr` and there may be something left in the buffer kept in the stdio layer. Call `fflush(stderr)` before writing the message we prepare in this function. Helped-by: Jeff King Helped-by: Alexandr Miloslavskiy Helped-by: SZEDER Gábor Helped-by: Junio C Hamano Signed-off-by: Johannes Schindelin --- usage.c | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/usage.c b/usage.c index 2fdb20086b..58fb5fff5f 100644 --- a/usage.c +++ b/usage.c @@ -9,14 +9,26 @@ void vreportf(const char *prefix, const char *err, va_list params) { char msg[4096]; - char *p; + char *p, *pend = msg + sizeof(msg); + size_t prefix_len = strlen(prefix); - vsnprintf(msg, sizeof(msg), err, params); - for (p = msg; *p; p++) { + if (sizeof(msg) <= prefix_len) { + fprintf(stderr, "BUG!!! too long a prefix '%s'\n", prefix); + abort(); + } + memcpy(msg, prefix, prefix_len); + p = msg + prefix_len; + if (vsnprintf(p, pend - p, err, params) < 0) + *p = '\0'; /* vsnprintf() failed, clip at prefix */ + + for (; p != pend - 1 && *p; p++) { if (iscntrl(*p) && *p != '\t' && *p != '\n') *p = '?'; } - fprintf(stderr, "%s%s\n", prefix, msg); + + *(p++) = '\n'; /* we no longer need a NUL */ + fflush(stderr); + write_in_full(2, msg, p - msg); } static NORETURN void usage_builtin(const char *err, va_list params) From aa5ef349fd9371f6ea3b97961d35a9b667b484af Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Wed, 25 Sep 2019 22:56:02 +0200 Subject: [PATCH 97/98] update-index: optionally leave skip-worktree entries alone While `git update-index` mostly ignores paths referring to index entries whose skip-worktree bit is set, in b4d1690df11 (Teach Git to respect skip-worktree bit (reading part), 2009-08-20), for reasons that are not entirely obvious, the `--remove` option was made special: it _does_ remove index entries even if their skip-worktree bit is set. Seeing as this behavior has been in place for a decade now, it does not make sense to change it. However, in preparation for fixing a bug in `git stash` where it pretends that skip-worktree entries have actually been removed, we need a mode where `git update-index` leaves all skip-worktree entries alone, even if the `--remove` option was passed. Signed-off-by: Johannes Schindelin --- Documentation/git-update-index.txt | 6 ++++++ builtin/update-index.c | 6 +++++- t/t7012-skip-worktree-writing.sh | 15 +++++++++++++++ 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/Documentation/git-update-index.txt b/Documentation/git-update-index.txt index 1c4d146a41..08393445e7 100644 --- a/Documentation/git-update-index.txt +++ b/Documentation/git-update-index.txt @@ -16,6 +16,7 @@ SYNOPSIS [--chmod=(+|-)x] [--[no-]assume-unchanged] [--[no-]skip-worktree] + [--[no-]ignore-skip-worktree-entries] [--[no-]fsmonitor-valid] [--ignore-submodules] [--[no-]split-index] @@ -113,6 +114,11 @@ you will need to handle the situation manually. set and unset the "skip-worktree" bit for the paths. See section "Skip-worktree bit" below for more information. + +--[no-]ignore-skip-worktree-entries:: + Do not remove skip-worktree (AKA "index-only") entries even when + the `--remove` option was specified. + --[no-]fsmonitor-valid:: When one of these flags is specified, the object name recorded for the paths are not updated. Instead, these options diff --git a/builtin/update-index.c b/builtin/update-index.c index 49302d98c5..d527b8f106 100644 --- a/builtin/update-index.c +++ b/builtin/update-index.c @@ -35,6 +35,7 @@ static int verbose; static int mark_valid_only; static int mark_skip_worktree_only; static int mark_fsmonitor_only; +static int ignore_skip_worktree_entries; #define MARK_FLAG 1 #define UNMARK_FLAG 2 static struct strbuf mtime_dir = STRBUF_INIT; @@ -381,7 +382,8 @@ static int process_path(const char *path, struct stat *st, int stat_errno) * so updating it does not make sense. * On the other hand, removing it from index should work */ - if (allow_remove && remove_file_from_cache(path)) + if (!ignore_skip_worktree_entries && allow_remove && + remove_file_from_cache(path)) return error("%s: cannot remove from the index", path); return 0; } @@ -1014,6 +1016,8 @@ int cmd_update_index(int argc, const char **argv, const char *prefix) {OPTION_SET_INT, 0, "no-skip-worktree", &mark_skip_worktree_only, NULL, N_("clear skip-worktree bit"), PARSE_OPT_NOARG | PARSE_OPT_NONEG, NULL, UNMARK_FLAG}, + OPT_BOOL(0, "ignore-skip-worktree-entries", &ignore_skip_worktree_entries, + N_("do not touch index-only entries")), OPT_SET_INT(0, "info-only", &info_only, N_("add to index only; do not add content to object database"), 1), OPT_SET_INT(0, "force-remove", &force_remove, diff --git a/t/t7012-skip-worktree-writing.sh b/t/t7012-skip-worktree-writing.sh index 9d1abe50ef..7476781979 100755 --- a/t/t7012-skip-worktree-writing.sh +++ b/t/t7012-skip-worktree-writing.sh @@ -134,6 +134,21 @@ test_expect_success 'git-clean, dirty case' ' test_i18ncmp expected result ' +test_expect_success '--ignore-skip-worktree-entries leaves worktree alone' ' + test_commit keep-me && + git update-index --skip-worktree keep-me.t && + rm keep-me.t && + + : ignoring the worktree && + git update-index --remove --ignore-skip-worktree-entries keep-me.t && + git diff-index --cached --exit-code HEAD && + + : not ignoring the worktree, a deletion is staged && + git update-index --remove keep-me.t && + test_must_fail git diff-index --cached --exit-code HEAD \ + --diff-filter=D -- keep-me.t +' + #TODO test_expect_failure 'git-apply adds file' false #TODO test_expect_failure 'git-apply updates file' false #TODO test_expect_failure 'git-apply removes file' false From bd7aed58e9a1741cc1e8bb1dc1595266099e5664 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Wed, 25 Sep 2019 22:16:56 +0200 Subject: [PATCH 98/98] stash: handle staged changes in skip-worktree files correctly When calling `git stash` while changes were staged for files that are marked with the `skip-worktree` bit (e.g. files that are excluded in a sparse checkout), the files are recorded as _deleted_ instead. The reason is that `git stash` tries to construct the tree reflecting the worktree essentially by copying the index to a temporary one and then updating the files from the worktree. Crucially, it calls `git diff-index` to update also those files that are in the HEAD but have been unstaged in the index. However, when the temporary index is updated via `git update-index --add --remove`, skip-worktree entries mark the files as deleted by mistake. Let's use the newly-introduced `--ignore-skip-worktree-entries` option of `git update-index` to prevent exactly this from happening. Note that the regression test case deliberately avoids replicating the scenario described above and instead tries to recreate just the symptom. Reported by Dan Thompson. Signed-off-by: Johannes Schindelin --- builtin/stash.c | 5 +++-- git-legacy-stash.sh | 3 ++- t/t3903-stash.sh | 11 +++++++++++ 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/builtin/stash.c b/builtin/stash.c index 4e806176b0..d913487a43 100644 --- a/builtin/stash.c +++ b/builtin/stash.c @@ -1088,8 +1088,9 @@ static int stash_working_tree(struct stash_info *info, const struct pathspec *ps } cp_upd_index.git_cmd = 1; - argv_array_pushl(&cp_upd_index.args, "update-index", "-z", "--add", - "--remove", "--stdin", NULL); + argv_array_pushl(&cp_upd_index.args, "update-index", + "--ignore-skip-worktree-entries", + "-z", "--add", "--remove", "--stdin", NULL); argv_array_pushf(&cp_upd_index.env_array, "GIT_INDEX_FILE=%s", stash_index_path.buf); diff --git a/git-legacy-stash.sh b/git-legacy-stash.sh index 07ad4a5459..53fa574301 100755 --- a/git-legacy-stash.sh +++ b/git-legacy-stash.sh @@ -193,7 +193,8 @@ create_stash () { GIT_INDEX_FILE="$TMPindex" && export GIT_INDEX_FILE && git diff-index --name-only -z HEAD -- "$@" >"$TMP-stagenames" && - git update-index -z --add --remove --stdin <"$TMP-stagenames" && + git update-index --ignore-skip-worktree-entries \ + -z --add --remove --stdin <"$TMP-stagenames" && git write-tree && rm -f "$TMPindex" ) ) || diff --git a/t/t3903-stash.sh b/t/t3903-stash.sh index 580bfbdc23..a4da72f0ab 100755 --- a/t/t3903-stash.sh +++ b/t/t3903-stash.sh @@ -1269,4 +1269,15 @@ test_expect_success 'stash apply should succeed with unmodified file' ' git stash apply ' +test_expect_success 'stash handles skip-worktree entries nicely' ' + test_commit A && + echo changed >A.t && + git add A.t && + git update-index --skip-worktree A.t && + rm A.t && + git stash && + + git rev-parse --verify refs/stash:A.t +' + test_done