{"thread":{"id":"52445","subject":"[PATCH 00/19] Implement the git add --patch backend in C","startedAt":"2019-12-13T08:08:11Z","lastAt":"2019-12-13T08:08:44Z","messageCount":20,"participants":["Johannes Schindelin via GitGitGadget"],"isPatch":true,"patchVersion":1,"patchTotal":19},"messages":[{"id":"388082","messageId":"pull.173.git.1576224486.gitgitgadget@gmail.com","threadId":"52445","inReplyTo":null,"subject":"[PATCH 00/19] Implement the git add --patch backend in C","fromName":"Johannes Schindelin via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2019-12-13T08:07:47Z","receivedAt":"2019-12-13T08:08:11Z","isPatch":true,"sender":{"key":"johannes.schindelin@gmx.de","avatar":"https://avatars.githubusercontent.com/u/127790?v=4"},"body":"Out of all the patch series on the journey to provide git add --interactive \nand git add --patch in built-in versions, this is the big one, as can be\nexpected from the fact that the git add --patch functionality makes up over\nhalf of the 1,800+ lines of git-add--interactive.perl.\n\nThe two patches that stick out are of course the ones to implement hunk\nsplitting and hunk editing: these operations are fundamentally more\ncomplicated, and less obvious, than the entire rest of the operations.\n\nJohannes Schindelin (19):\n  built-in add -i: start implementing the `patch` functionality in C\n  built-in add -i: wire up the new C code for the `patch` command\n  built-in add -p: show colored hunks by default\n  built-in add -p: adjust hunk headers as needed\n  built-in add -p: color the prompt and the help text\n  built-in add -p: offer a helpful error message when hunk navigation\n    failed\n  built-in add -p: support multi-file diffs\n  built-in add -p: handle deleted empty files\n  built-in app -p: allow selecting a mode change as a \"hunk\"\n  built-in add -p: show different prompts for mode changes and deletions\n  built-in add -p: implement the hunk splitting feature\n  built-in add -p: coalesce hunks after splitting them\n  strbuf: add a helper function to call the editor \"on an strbuf\"\n  built-in add -p: implement hunk editing\n  built-in add -p: implement the 'g' (\"goto\") command\n  built-in add -p: implement the '/' (\"search regex\") command\n  built-in add -p: implement the 'q' (\"quit\") command\n  built-in add -p: only show the applicable parts of the help text\n  built-in add -p: show helpful hint when nothing can be staged\n\n Makefile                   |    1 +\n add-interactive.c          |   29 +-\n add-interactive.h          |   19 +\n add-patch.c                | 1338 ++++++++++++++++++++++++++++++++++++\n builtin/add.c              |   15 +-\n strbuf.c                   |   28 +\n strbuf.h                   |   11 +\n t/t3701-add-interactive.sh |   42 ++\n 8 files changed, 1464 insertions(+), 19 deletions(-)\n create mode 100644 add-patch.c\n\n\nbase-commit: b4bbbbd5a247e0e75d079bca591b657ec9084a46\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-173%2Fdscho%2Fadd-p-in-c-v1\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-173/dscho/add-p-in-c-v1\nPull-Request: https://github.com/gitgitgadget/git/pull/173\n-- \ngitgitgadget\n"},{"id":"388083","messageId":"03feb2f28bbfb4779afb4a26009cc30c939f0436.1576224486.git.gitgitgadget@gmail.com","threadId":"52445","inReplyTo":"pull.173.git.1576224486.gitgitgadget@gmail.com","subject":"[PATCH 02/19] built-in add -i: wire up the new C code for the `patch` command","fromName":"Johannes Schindelin via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2019-12-13T08:07:49Z","receivedAt":"2019-12-13T08:08:13Z","isPatch":true,"sender":{"key":"johannes.schindelin@gmx.de","avatar":"https://avatars.githubusercontent.com/u/127790?v=4"},"body":"From: Johannes Schindelin <johannes.schindelin@gmx.de>\n\nThe code in `git-add--interactive.perl` that takes care of the `patch`\ncommand can look quite intimidating. There are so many modes in which it\ncan be called, for example.\n\nBut for the `patch` command in `git add -i`, only one mode is relevant:\nthe `stage` mode. And we just implemented the beginnings of that mode in\nC so far. So let's use it when `add.interactive.useBuiltin=true`.\n\nNow, while the code in `add-patch.c` is far from reaching feature parity\nwith the code in `git-add--interactive.perl` (color is not implemented,\nthe diff algorithm cannot be configured, the colored diff cannot be\npost-processed via `interactive.diffFilter`, many commands are\nunimplemented yet, etc), hooking it all up with the part of `git add -i`\nthat is already converted to C makes it easier to test and develop it.\n\nNote: at this stage, both the `add.interactive.useBuiltin` config\nsetting is still safely opt-in, and will probably be fore quite some\ntime, to allow for thorough testing \"in the wild\" without adversely\naffecting existing users.\n\nSigned-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n---\n add-interactive.c | 9 ++++++---\n 1 file changed, 6 insertions(+), 3 deletions(-)\n\ndiff --git a/add-interactive.c b/add-interactive.c\nindex f395d54c08..034c1dc02f 100644\n--- a/add-interactive.c\n+++ b/add-interactive.c\n@@ -917,15 +917,18 @@ static int run_patch(struct add_i_state *s, const struct pathspec *ps,\n \tcount = list_and_choose(s, files, opts);\n \tif (count >= 0) {\n \t\tstruct argv_array args = ARGV_ARRAY_INIT;\n+\t\tstruct pathspec ps_selected = { 0 };\n \n-\t\targv_array_pushl(&args, \"git\", \"add--interactive\", \"--patch\",\n-\t\t\t\t \"--\", NULL);\n \t\tfor (i = 0; i < files->items.nr; i++)\n \t\t\tif (files->selected[i])\n \t\t\t\targv_array_push(&args,\n \t\t\t\t\t\tfiles->items.items[i].string);\n-\t\tres = run_command_v_opt(args.argv, 0);\n+\t\tparse_pathspec(&ps_selected,\n+\t\t\t       PATHSPEC_ALL_MAGIC & ~PATHSPEC_LITERAL,\n+\t\t\t       PATHSPEC_LITERAL_PATH, \"\", args.argv);\n+\t\tres = run_add_p(s->r, &ps_selected);\n \t\targv_array_clear(&args);\n+\t\tclear_pathspec(&ps_selected);\n \t}\n \n \treturn res;\n-- \ngitgitgadget\n\n"},{"id":"388084","messageId":"63b571f351beb95828fb754bc94dac36347e1083.1576224486.git.gitgitgadget@gmail.com","threadId":"52445","inReplyTo":"pull.173.git.1576224486.gitgitgadget@gmail.com","subject":"[PATCH 01/19] built-in add -i: start implementing the `patch` functionality in C","fromName":"Johannes Schindelin via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2019-12-13T08:07:48Z","receivedAt":"2019-12-13T08:08:15Z","isPatch":true,"sender":{"key":"johannes.schindelin@gmx.de","avatar":"https://avatars.githubusercontent.com/u/127790?v=4"},"body":"From: Johannes Schindelin <johannes.schindelin@gmx.de>\n\nIn the previous steps, we re-implemented the main loop of `git add -i`\nin C, and most of the commands.\n\nNotably, we left out the actual functionality of `patch`, as the\nrelevant code makes up more than half of `git-add--interactive.perl`,\nand is actually pretty independent of the rest of the commands.\n\nWith this commit, we start to tackle that `patch` part. For better\nseparation of concerns, we keep the code in a separate file,\n`add-patch.c`. The new code is still guarded behind the\n`add.interactive.useBuiltin` config setting, and for the moment,\nit can only be called via `git add -p`.\n\nThe actual functionality follows the original implementation of\n5cde71d64aff (git-add --interactive, 2006-12-10), but not too closely\n(for example, we use string offsets rather than copying strings around,\nand after seeing whether the `k` and `j` commands are applicable, in the\nC version we remember which previous/next hunk was undecided, and use it\nrather than looking again when the user asked to jump).\n\nAs a further deviation from that commit, We also use a comma instead of\na slash to separate the available commands in the prompt, as the current\nversion of the Perl script does this, and we also add a line about the\nquestion mark (\"print help\") to the help text.\n\nWhile it is tempting to use this conversion of `git add -p` as an excuse\nto work on `apply_all_patches()` so that it does _not_ want to read a\nfile from `stdin` or from a file, but accepts, say, an `strbuf` instead,\nwe will refrain from this particular rabbit hole at this stage.\n\nSigned-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n---\n Makefile          |   1 +\n add-interactive.h |   1 +\n add-patch.c       | 265 ++++++++++++++++++++++++++++++++++++++++++++++\n builtin/add.c     |  15 ++-\n 4 files changed, 277 insertions(+), 5 deletions(-)\n create mode 100644 add-patch.c\n\ndiff --git a/Makefile b/Makefile\nindex 6c4a1e0ee5..0345d7408b 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -824,6 +824,7 @@ LIB_H := $(sort $(patsubst ./%,%,$(shell git ls-files '*.h' ':!t/' ':!Documentat\n \n LIB_OBJS += abspath.o\n LIB_OBJS += add-interactive.o\n+LIB_OBJS += add-patch.o\n LIB_OBJS += advice.o\n LIB_OBJS += alias.o\n LIB_OBJS += alloc.o\ndiff --git a/add-interactive.h b/add-interactive.h\nindex 7043b8741d..0e3d93acc9 100644\n--- a/add-interactive.h\n+++ b/add-interactive.h\n@@ -4,5 +4,6 @@\n struct repository;\n struct pathspec;\n int run_add_i(struct repository *r, const struct pathspec *ps);\n+int run_add_p(struct repository *r, const struct pathspec *ps);\n \n #endif\ndiff --git a/add-patch.c b/add-patch.c\nnew file mode 100644\nindex 0000000000..d1b1a080e4\n--- /dev/null\n+++ b/add-patch.c\n@@ -0,0 +1,265 @@\n+#include \"cache.h\"\n+#include \"add-interactive.h\"\n+#include \"strbuf.h\"\n+#include \"run-command.h\"\n+#include \"argv-array.h\"\n+#include \"pathspec.h\"\n+\n+struct hunk {\n+\tsize_t start, end;\n+\tenum { UNDECIDED_HUNK = 0, SKIP_HUNK, USE_HUNK } use;\n+};\n+\n+struct add_p_state {\n+\tstruct repository *r;\n+\tstruct strbuf answer, buf;\n+\n+\t/* parsed diff */\n+\tstruct strbuf plain;\n+\tstruct hunk head;\n+\tstruct hunk *hunk;\n+\tsize_t hunk_nr, hunk_alloc;\n+};\n+\n+static void setup_child_process(struct add_p_state *s,\n+\t\t\t\tstruct child_process *cp, ...)\n+{\n+\tva_list ap;\n+\tconst char *arg;\n+\n+\tva_start(ap, cp);\n+\twhile ((arg = va_arg(ap, const char *)))\n+\t\targv_array_push(&cp->args, arg);\n+\tva_end(ap);\n+\n+\tcp->git_cmd = 1;\n+\targv_array_pushf(&cp->env_array,\n+\t\t\t INDEX_ENVIRONMENT \"=%s\", s->r->index_file);\n+}\n+\n+static int parse_diff(struct add_p_state *s, const struct pathspec *ps)\n+{\n+\tstruct strbuf *plain = &s->plain;\n+\tstruct child_process cp = CHILD_PROCESS_INIT;\n+\tchar *p, *pend;\n+\tsize_t i;\n+\tstruct hunk *hunk = NULL;\n+\tint res;\n+\n+\t/* Use `--no-color` explicitly, just in case `diff.color = always`. */\n+\tsetup_child_process(s, &cp,\n+\t\t\t \"diff-files\", \"-p\", \"--no-color\", \"--\", NULL);\n+\tfor (i = 0; i < ps->nr; i++)\n+\t\targv_array_push(&cp.args, ps->items[i].original);\n+\n+\tres = capture_command(&cp, plain, 0);\n+\tif (res)\n+\t\treturn error(_(\"could not parse diff\"));\n+\tif (!plain->len)\n+\t\treturn 0;\n+\tstrbuf_complete_line(plain);\n+\n+\t/* parse hunks */\n+\tp = plain->buf;\n+\tpend = p + plain->len;\n+\twhile (p != pend) {\n+\t\tchar *eol = memchr(p, '\\n', pend - p);\n+\t\tif (!eol)\n+\t\t\teol = pend;\n+\n+\t\tif (starts_with(p, \"diff \")) {\n+\t\t\tif (p != plain->buf)\n+\t\t\t\tBUG(\"multi-file diff not yet handled\");\n+\t\t\thunk = &s->head;\n+\t\t} else if (p == plain->buf)\n+\t\t\tBUG(\"diff starts with unexpected line:\\n\"\n+\t\t\t    \"%.*s\\n\", (int)(eol - p), p);\n+\t\telse if (starts_with(p, \"@@ \")) {\n+\t\t\ts->hunk_nr++;\n+\t\t\tALLOC_GROW(s->hunk, s->hunk_nr,\n+\t\t\t\t   s->hunk_alloc);\n+\t\t\thunk = s->hunk + s->hunk_nr - 1;\n+\t\t\tmemset(hunk, 0, sizeof(*hunk));\n+\n+\t\t\thunk->start = p - plain->buf;\n+\t\t}\n+\n+\t\tp = eol == pend ? pend : eol + 1;\n+\t\thunk->end = p - plain->buf;\n+\t}\n+\n+\treturn 0;\n+}\n+\n+static void render_hunk(struct add_p_state *s, struct hunk *hunk,\n+\t\t\tstruct strbuf *out)\n+{\n+\tstrbuf_add(out, s->plain.buf + hunk->start,\n+\t\t   hunk->end - hunk->start);\n+}\n+\n+static void reassemble_patch(struct add_p_state *s, struct strbuf *out)\n+{\n+\tstruct hunk *hunk;\n+\tsize_t i;\n+\n+\trender_hunk(s, &s->head, out);\n+\n+\tfor (i = 0; i < s->hunk_nr; i++) {\n+\t\thunk = s->hunk + i;\n+\t\tif (hunk->use == USE_HUNK)\n+\t\t\trender_hunk(s, hunk, out);\n+\t}\n+}\n+\n+static const char help_patch_text[] =\n+N_(\"y - stage this hunk\\n\"\n+   \"n - do not stage this hunk\\n\"\n+   \"a - stage this and all the remaining hunks\\n\"\n+   \"d - do not stage this hunk nor any of the remaining hunks\\n\"\n+   \"j - leave this hunk undecided, see next undecided hunk\\n\"\n+   \"J - leave this hunk undecided, see next hunk\\n\"\n+   \"k - leave this hunk undecided, see previous undecided hunk\\n\"\n+   \"K - leave this hunk undecided, see previous hunk\\n\"\n+   \"? - print help\\n\");\n+\n+static int patch_update_file(struct add_p_state *s)\n+{\n+\tsize_t hunk_index = 0;\n+\tssize_t i, undecided_previous, undecided_next;\n+\tstruct hunk *hunk;\n+\tchar ch;\n+\tstruct child_process cp = CHILD_PROCESS_INIT;\n+\n+\tif (!s->hunk_nr)\n+\t\treturn 0;\n+\n+\tstrbuf_reset(&s->buf);\n+\trender_hunk(s, &s->head, &s->buf);\n+\tfputs(s->buf.buf, stdout);\n+\tfor (;;) {\n+\t\tif (hunk_index >= s->hunk_nr)\n+\t\t\thunk_index = 0;\n+\t\thunk = s->hunk + hunk_index;\n+\n+\t\tundecided_previous = -1;\n+\t\tfor (i = hunk_index - 1; i >= 0; i--)\n+\t\t\tif (s->hunk[i].use == UNDECIDED_HUNK) {\n+\t\t\t\tundecided_previous = i;\n+\t\t\t\tbreak;\n+\t\t\t}\n+\n+\t\tundecided_next = -1;\n+\t\tfor (i = hunk_index + 1; i < s->hunk_nr; i++)\n+\t\t\tif (s->hunk[i].use == UNDECIDED_HUNK) {\n+\t\t\t\tundecided_next = i;\n+\t\t\t\tbreak;\n+\t\t\t}\n+\n+\t\t/* Everything decided? */\n+\t\tif (undecided_previous < 0 && undecided_next < 0 &&\n+\t\t    hunk->use != UNDECIDED_HUNK)\n+\t\t\tbreak;\n+\n+\t\tstrbuf_reset(&s->buf);\n+\t\trender_hunk(s, hunk, &s->buf);\n+\t\tfputs(s->buf.buf, stdout);\n+\n+\t\tstrbuf_reset(&s->buf);\n+\t\tif (undecided_previous >= 0)\n+\t\t\tstrbuf_addstr(&s->buf, \",k\");\n+\t\tif (hunk_index)\n+\t\t\tstrbuf_addstr(&s->buf, \",K\");\n+\t\tif (undecided_next >= 0)\n+\t\t\tstrbuf_addstr(&s->buf, \",j\");\n+\t\tif (hunk_index + 1 < s->hunk_nr)\n+\t\t\tstrbuf_addstr(&s->buf, \",J\");\n+\t\tprintf(\"(%\"PRIuMAX\"/%\"PRIuMAX\") \",\n+\t\t       (uintmax_t)hunk_index + 1, (uintmax_t)s->hunk_nr);\n+\t\tprintf(_(\"Stage this hunk [y,n,a,d%s,?]? \"), s->buf.buf);\n+\t\tfflush(stdout);\n+\t\tif (strbuf_getline(&s->answer, stdin) == EOF)\n+\t\t\tbreak;\n+\t\tstrbuf_trim_trailing_newline(&s->answer);\n+\n+\t\tif (!s->answer.len)\n+\t\t\tcontinue;\n+\t\tch = tolower(s->answer.buf[0]);\n+\t\tif (ch == 'y') {\n+\t\t\thunk->use = USE_HUNK;\n+soft_increment:\n+\t\t\thunk_index = undecided_next < 0 ?\n+\t\t\t\ts->hunk_nr : undecided_next;\n+\t\t} else if (ch == 'n') {\n+\t\t\thunk->use = SKIP_HUNK;\n+\t\t\tgoto soft_increment;\n+\t\t} else if (ch == 'a') {\n+\t\t\tfor (; hunk_index < s->hunk_nr; hunk_index++) {\n+\t\t\t\thunk = s->hunk + hunk_index;\n+\t\t\t\tif (hunk->use == UNDECIDED_HUNK)\n+\t\t\t\t\thunk->use = USE_HUNK;\n+\t\t\t}\n+\t\t} else if (ch == 'd') {\n+\t\t\tfor (; hunk_index < s->hunk_nr; hunk_index++) {\n+\t\t\t\thunk = s->hunk + hunk_index;\n+\t\t\t\tif (hunk->use == UNDECIDED_HUNK)\n+\t\t\t\t\thunk->use = SKIP_HUNK;\n+\t\t\t}\n+\t\t} else if (hunk_index && s->answer.buf[0] == 'K')\n+\t\t\thunk_index--;\n+\t\telse if (hunk_index + 1 < s->hunk_nr &&\n+\t\t\t s->answer.buf[0] == 'J')\n+\t\t\thunk_index++;\n+\t\telse if (undecided_previous >= 0 &&\n+\t\t\t s->answer.buf[0] == 'k')\n+\t\t\thunk_index = undecided_previous;\n+\t\telse if (undecided_next >= 0 && s->answer.buf[0] == 'j')\n+\t\t\thunk_index = undecided_next;\n+\t\telse\n+\t\t\tputs(_(help_patch_text));\n+\t}\n+\n+\t/* Any hunk to be used? */\n+\tfor (i = 0; i < s->hunk_nr; i++)\n+\t\tif (s->hunk[i].use == USE_HUNK)\n+\t\t\tbreak;\n+\n+\tif (i < s->hunk_nr) {\n+\t\t/* At least one hunk selected: apply */\n+\t\tstrbuf_reset(&s->buf);\n+\t\treassemble_patch(s, &s->buf);\n+\n+\t\tdiscard_index(s->r->index);\n+\t\tsetup_child_process(s, &cp, \"apply\", \"--cached\", NULL);\n+\t\tif (pipe_command(&cp, s->buf.buf, s->buf.len,\n+\t\t\t\t NULL, 0, NULL, 0))\n+\t\t\terror(_(\"'git apply --cached' failed\"));\n+\t\tif (!repo_read_index(s->r))\n+\t\t\trepo_refresh_and_write_index(s->r, REFRESH_QUIET, 0,\n+\t\t\t\t\t\t     1, NULL, NULL, NULL);\n+\t}\n+\n+\tputchar('\\n');\n+\treturn 0;\n+}\n+\n+int run_add_p(struct repository *r, const struct pathspec *ps)\n+{\n+\tstruct add_p_state s = { r, STRBUF_INIT, STRBUF_INIT, STRBUF_INIT };\n+\n+\tif (discard_index(r->index) < 0 || repo_read_index(r) < 0 ||\n+\t    repo_refresh_and_write_index(r, REFRESH_QUIET, 0, 1,\n+\t\t\t\t\t NULL, NULL, NULL) < 0 ||\n+\t    parse_diff(&s, ps) < 0) {\n+\t\tstrbuf_release(&s.plain);\n+\t\treturn -1;\n+\t}\n+\n+\tif (s.hunk_nr)\n+\t\tpatch_update_file(&s);\n+\n+\tstrbuf_release(&s.answer);\n+\tstrbuf_release(&s.buf);\n+\tstrbuf_release(&s.plain);\n+\treturn 0;\n+}\ndiff --git a/builtin/add.c b/builtin/add.c\nindex d4686d5218..1deb59a642 100644\n--- a/builtin/add.c\n+++ b/builtin/add.c\n@@ -189,12 +189,17 @@ int run_add_interactive(const char *revision, const char *patch_mode,\n \tint use_builtin_add_i =\n \t\tgit_env_bool(\"GIT_TEST_ADD_I_USE_BUILTIN\", -1);\n \n-\tif (!patch_mode) {\n-\t\tif (use_builtin_add_i < 0)\n-\t\t\tgit_config_get_bool(\"add.interactive.usebuiltin\",\n-\t\t\t\t\t    &use_builtin_add_i);\n-\t\tif (use_builtin_add_i == 1)\n+\tif (use_builtin_add_i < 0)\n+\t\tgit_config_get_bool(\"add.interactive.usebuiltin\",\n+\t\t\t\t    &use_builtin_add_i);\n+\n+\tif (use_builtin_add_i == 1) {\n+\t\tif (!patch_mode)\n \t\t\treturn !!run_add_i(the_repository, pathspec);\n+\t\tif (strcmp(patch_mode, \"--patch\"))\n+\t\t\tdie(\"'%s' not yet supported in the built-in add -p\",\n+\t\t\t    patch_mode);\n+\t\treturn !!run_add_p(the_repository, pathspec);\n \t}\n \n \targv_array_push(&argv, \"add--interactive\");\n-- \ngitgitgadget\n\n"},{"id":"388085","messageId":"438e6519fb7e81c451ebba3ea9efffc26b4c7a17.1576224486.git.gitgitgadget@gmail.com","threadId":"52445","inReplyTo":"pull.173.git.1576224486.gitgitgadget@gmail.com","subject":"[PATCH 03/19] built-in add -p: show colored hunks by default","fromName":"Johannes Schindelin via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2019-12-13T08:07:50Z","receivedAt":"2019-12-13T08:08:16Z","isPatch":true,"sender":{"key":"johannes.schindelin@gmx.de","avatar":"https://avatars.githubusercontent.com/u/127790?v=4"},"body":"From: Johannes Schindelin <johannes.schindelin@gmx.de>\n\nJust like the Perl version, we now generate two diffs if `color.diff` is\nset: one with and one without color. Then we parse them in parallel and\nrecord which hunks start at which offsets in both.\n\nNote that this is a (slight) deviation from the way the Perl version did\nit: we are no longer reading the output of `diff-files` line by line\n(which is more natural for Perl than for C), but in one go, and parse\neverything later, so we might just as well do it in synchrony.\n\nSigned-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n---\n add-patch.c | 79 +++++++++++++++++++++++++++++++++++++++++------------\n 1 file changed, 62 insertions(+), 17 deletions(-)\n\ndiff --git a/add-patch.c b/add-patch.c\nindex d1b1a080e4..79eefa9505 100644\n--- a/add-patch.c\n+++ b/add-patch.c\n@@ -4,9 +4,10 @@\n #include \"run-command.h\"\n #include \"argv-array.h\"\n #include \"pathspec.h\"\n+#include \"color.h\"\n \n struct hunk {\n-\tsize_t start, end;\n+\tsize_t start, end, colored_start, colored_end;\n \tenum { UNDECIDED_HUNK = 0, SKIP_HUNK, USE_HUNK } use;\n };\n \n@@ -15,7 +16,7 @@ struct add_p_state {\n \tstruct strbuf answer, buf;\n \n \t/* parsed diff */\n-\tstruct strbuf plain;\n+\tstruct strbuf plain, colored;\n \tstruct hunk head;\n \tstruct hunk *hunk;\n \tsize_t hunk_nr, hunk_alloc;\n@@ -39,26 +40,50 @@ static void setup_child_process(struct add_p_state *s,\n \n static int parse_diff(struct add_p_state *s, const struct pathspec *ps)\n {\n-\tstruct strbuf *plain = &s->plain;\n+\tstruct argv_array args = ARGV_ARRAY_INIT;\n+\tstruct strbuf *plain = &s->plain, *colored = NULL;\n \tstruct child_process cp = CHILD_PROCESS_INIT;\n-\tchar *p, *pend;\n-\tsize_t i;\n+\tchar *p, *pend, *colored_p = NULL, *colored_pend = NULL;\n+\tsize_t i, color_arg_index;\n \tstruct hunk *hunk = NULL;\n \tint res;\n \n \t/* Use `--no-color` explicitly, just in case `diff.color = always`. */\n-\tsetup_child_process(s, &cp,\n-\t\t\t \"diff-files\", \"-p\", \"--no-color\", \"--\", NULL);\n+\targv_array_pushl(&args, \"diff-files\", \"-p\", \"--no-color\", \"--\", NULL);\n+\tcolor_arg_index = args.argc - 2;\n \tfor (i = 0; i < ps->nr; i++)\n-\t\targv_array_push(&cp.args, ps->items[i].original);\n+\t\targv_array_push(&args, ps->items[i].original);\n \n+\tsetup_child_process(s, &cp, NULL);\n+\tcp.argv = args.argv;\n \tres = capture_command(&cp, plain, 0);\n-\tif (res)\n+\tif (res) {\n+\t\targv_array_clear(&args);\n \t\treturn error(_(\"could not parse diff\"));\n-\tif (!plain->len)\n+\t}\n+\tif (!plain->len) {\n+\t\targv_array_clear(&args);\n \t\treturn 0;\n+\t}\n \tstrbuf_complete_line(plain);\n \n+\tif (want_color_fd(1, -1)) {\n+\t\tstruct child_process colored_cp = CHILD_PROCESS_INIT;\n+\n+\t\tsetup_child_process(s, &colored_cp, NULL);\n+\t\txsnprintf((char *)args.argv[color_arg_index], 8, \"--color\");\n+\t\tcolored_cp.argv = args.argv;\n+\t\tcolored = &s->colored;\n+\t\tres = capture_command(&colored_cp, colored, 0);\n+\t\targv_array_clear(&args);\n+\t\tif (res)\n+\t\t\treturn error(_(\"could not parse colored diff\"));\n+\t\tstrbuf_complete_line(colored);\n+\t\tcolored_p = colored->buf;\n+\t\tcolored_pend = colored_p + colored->len;\n+\t}\n+\targv_array_clear(&args);\n+\n \t/* parse hunks */\n \tp = plain->buf;\n \tpend = p + plain->len;\n@@ -82,20 +107,37 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps)\n \t\t\tmemset(hunk, 0, sizeof(*hunk));\n \n \t\t\thunk->start = p - plain->buf;\n+\t\t\tif (colored)\n+\t\t\t\thunk->colored_start = colored_p - colored->buf;\n \t\t}\n \n \t\tp = eol == pend ? pend : eol + 1;\n \t\thunk->end = p - plain->buf;\n+\n+\t\tif (colored) {\n+\t\t\tchar *colored_eol = memchr(colored_p, '\\n',\n+\t\t\t\t\t\t   colored_pend - colored_p);\n+\t\t\tif (colored_eol)\n+\t\t\t\tcolored_p = colored_eol + 1;\n+\t\t\telse\n+\t\t\t\tcolored_p = colored_pend;\n+\n+\t\t\thunk->colored_end = colored_p - colored->buf;\n+\t\t}\n \t}\n \n \treturn 0;\n }\n \n static void render_hunk(struct add_p_state *s, struct hunk *hunk,\n-\t\t\tstruct strbuf *out)\n+\t\t\tint colored, struct strbuf *out)\n {\n-\tstrbuf_add(out, s->plain.buf + hunk->start,\n-\t\t   hunk->end - hunk->start);\n+\tif (colored)\n+\t\tstrbuf_add(out, s->colored.buf + hunk->colored_start,\n+\t\t\t   hunk->colored_end - hunk->colored_start);\n+\telse\n+\t\tstrbuf_add(out, s->plain.buf + hunk->start,\n+\t\t\t   hunk->end - hunk->start);\n }\n \n static void reassemble_patch(struct add_p_state *s, struct strbuf *out)\n@@ -103,12 +145,12 @@ static void reassemble_patch(struct add_p_state *s, struct strbuf *out)\n \tstruct hunk *hunk;\n \tsize_t i;\n \n-\trender_hunk(s, &s->head, out);\n+\trender_hunk(s, &s->head, 0, out);\n \n \tfor (i = 0; i < s->hunk_nr; i++) {\n \t\thunk = s->hunk + i;\n \t\tif (hunk->use == USE_HUNK)\n-\t\t\trender_hunk(s, hunk, out);\n+\t\t\trender_hunk(s, hunk, 0, out);\n \t}\n }\n \n@@ -130,12 +172,13 @@ static int patch_update_file(struct add_p_state *s)\n \tstruct hunk *hunk;\n \tchar ch;\n \tstruct child_process cp = CHILD_PROCESS_INIT;\n+\tint colored = !!s->colored.len;\n \n \tif (!s->hunk_nr)\n \t\treturn 0;\n \n \tstrbuf_reset(&s->buf);\n-\trender_hunk(s, &s->head, &s->buf);\n+\trender_hunk(s, &s->head, colored, &s->buf);\n \tfputs(s->buf.buf, stdout);\n \tfor (;;) {\n \t\tif (hunk_index >= s->hunk_nr)\n@@ -162,7 +205,7 @@ static int patch_update_file(struct add_p_state *s)\n \t\t\tbreak;\n \n \t\tstrbuf_reset(&s->buf);\n-\t\trender_hunk(s, hunk, &s->buf);\n+\t\trender_hunk(s, hunk, colored, &s->buf);\n \t\tfputs(s->buf.buf, stdout);\n \n \t\tstrbuf_reset(&s->buf);\n@@ -252,6 +295,7 @@ int run_add_p(struct repository *r, const struct pathspec *ps)\n \t\t\t\t\t NULL, NULL, NULL) < 0 ||\n \t    parse_diff(&s, ps) < 0) {\n \t\tstrbuf_release(&s.plain);\n+\t\tstrbuf_release(&s.colored);\n \t\treturn -1;\n \t}\n \n@@ -261,5 +305,6 @@ int run_add_p(struct repository *r, const struct pathspec *ps)\n \tstrbuf_release(&s.answer);\n \tstrbuf_release(&s.buf);\n \tstrbuf_release(&s.plain);\n+\tstrbuf_release(&s.colored);\n \treturn 0;\n }\n-- \ngitgitgadget\n\n"},{"id":"388086","messageId":"0cd1522044c238286c4762dfb18d413e856f59e8.1576224486.git.gitgitgadget@gmail.com","threadId":"52445","inReplyTo":"pull.173.git.1576224486.gitgitgadget@gmail.com","subject":"[PATCH 07/19] built-in add -p: support multi-file diffs","fromName":"Johannes Schindelin via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2019-12-13T08:07:54Z","receivedAt":"2019-12-13T08:08:20Z","isPatch":true,"sender":{"key":"johannes.schindelin@gmx.de","avatar":"https://avatars.githubusercontent.com/u/127790?v=4"},"body":"From: Johannes Schindelin <johannes.schindelin@gmx.de>\n\nFor simplicity, the initial implementation in C handled only a single\nmodified file. Now it handles an arbitrary number of files.\n\nSigned-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n---\n add-patch.c | 91 +++++++++++++++++++++++++++++++----------------------\n 1 file changed, 53 insertions(+), 38 deletions(-)\n\ndiff --git a/add-patch.c b/add-patch.c\nindex f59471cdf2..7c1b3b3935 100644\n--- a/add-patch.c\n+++ b/add-patch.c\n@@ -29,9 +29,12 @@ struct add_p_state {\n \n \t/* parsed diff */\n \tstruct strbuf plain, colored;\n-\tstruct hunk head;\n-\tstruct hunk *hunk;\n-\tsize_t hunk_nr, hunk_alloc;\n+\tstruct file_diff {\n+\t\tstruct hunk head;\n+\t\tstruct hunk *hunk;\n+\t\tsize_t hunk_nr, hunk_alloc;\n+\t} *file_diff;\n+\tsize_t file_diff_nr;\n };\n \n static void err(struct add_p_state *s, const char *fmt, ...)\n@@ -131,7 +134,8 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps)\n \tstruct strbuf *plain = &s->plain, *colored = NULL;\n \tstruct child_process cp = CHILD_PROCESS_INIT;\n \tchar *p, *pend, *colored_p = NULL, *colored_pend = NULL;\n-\tsize_t i, color_arg_index;\n+\tsize_t file_diff_alloc = 0, i, color_arg_index;\n+\tstruct file_diff *file_diff = NULL;\n \tstruct hunk *hunk = NULL;\n \tint res;\n \n@@ -171,7 +175,7 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps)\n \t}\n \targv_array_clear(&args);\n \n-\t/* parse hunks */\n+\t/* parse files and hunks */\n \tp = plain->buf;\n \tpend = p + plain->len;\n \twhile (p != pend) {\n@@ -180,17 +184,23 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps)\n \t\t\teol = pend;\n \n \t\tif (starts_with(p, \"diff \")) {\n-\t\t\tif (p != plain->buf)\n-\t\t\t\tBUG(\"multi-file diff not yet handled\");\n-\t\t\thunk = &s->head;\n+\t\t\ts->file_diff_nr++;\n+\t\t\tALLOC_GROW(s->file_diff, s->file_diff_nr,\n+\t\t\t\t   file_diff_alloc);\n+\t\t\tfile_diff = s->file_diff + s->file_diff_nr - 1;\n+\t\t\tmemset(file_diff, 0, sizeof(*file_diff));\n+\t\t\thunk = &file_diff->head;\n+\t\t\thunk->start = p - plain->buf;\n+\t\t\tif (colored_p)\n+\t\t\t\thunk->colored_start = colored_p - colored->buf;\n \t\t} else if (p == plain->buf)\n \t\t\tBUG(\"diff starts with unexpected line:\\n\"\n \t\t\t    \"%.*s\\n\", (int)(eol - p), p);\n \t\telse if (starts_with(p, \"@@ \")) {\n-\t\t\ts->hunk_nr++;\n-\t\t\tALLOC_GROW(s->hunk, s->hunk_nr,\n-\t\t\t\t   s->hunk_alloc);\n-\t\t\thunk = s->hunk + s->hunk_nr - 1;\n+\t\t\tfile_diff->hunk_nr++;\n+\t\t\tALLOC_GROW(file_diff->hunk, file_diff->hunk_nr,\n+\t\t\t\t   file_diff->hunk_alloc);\n+\t\t\thunk = file_diff->hunk + file_diff->hunk_nr - 1;\n \t\t\tmemset(hunk, 0, sizeof(*hunk));\n \n \t\t\thunk->start = p - plain->buf;\n@@ -265,16 +275,17 @@ static void render_hunk(struct add_p_state *s, struct hunk *hunk,\n \t\t\t   hunk->end - hunk->start);\n }\n \n-static void reassemble_patch(struct add_p_state *s, struct strbuf *out)\n+static void reassemble_patch(struct add_p_state *s,\n+\t\t\t     struct file_diff *file_diff, struct strbuf *out)\n {\n \tstruct hunk *hunk;\n \tsize_t i;\n \tssize_t delta = 0;\n \n-\trender_hunk(s, &s->head, 0, 0, out);\n+\trender_hunk(s, &file_diff->head, 0, 0, out);\n \n-\tfor (i = 0; i < s->hunk_nr; i++) {\n-\t\thunk = s->hunk + i;\n+\tfor (i = 0; i < file_diff->hunk_nr; i++) {\n+\t\thunk = file_diff->hunk + i;\n \t\tif (hunk->use != USE_HUNK)\n \t\t\tdelta += hunk->header.old_count\n \t\t\t\t- hunk->header.new_count;\n@@ -294,7 +305,8 @@ N_(\"y - stage this hunk\\n\"\n    \"K - leave this hunk undecided, see previous hunk\\n\"\n    \"? - print help\\n\");\n \n-static int patch_update_file(struct add_p_state *s)\n+static int patch_update_file(struct add_p_state *s,\n+\t\t\t     struct file_diff *file_diff)\n {\n \tsize_t hunk_index = 0;\n \tssize_t i, undecided_previous, undecided_next;\n@@ -303,27 +315,27 @@ static int patch_update_file(struct add_p_state *s)\n \tstruct child_process cp = CHILD_PROCESS_INIT;\n \tint colored = !!s->colored.len;\n \n-\tif (!s->hunk_nr)\n+\tif (!file_diff->hunk_nr)\n \t\treturn 0;\n \n \tstrbuf_reset(&s->buf);\n-\trender_hunk(s, &s->head, 0, colored, &s->buf);\n+\trender_hunk(s, &file_diff->head, 0, colored, &s->buf);\n \tfputs(s->buf.buf, stdout);\n \tfor (;;) {\n-\t\tif (hunk_index >= s->hunk_nr)\n+\t\tif (hunk_index >= file_diff->hunk_nr)\n \t\t\thunk_index = 0;\n-\t\thunk = s->hunk + hunk_index;\n+\t\thunk = file_diff->hunk + hunk_index;\n \n \t\tundecided_previous = -1;\n \t\tfor (i = hunk_index - 1; i >= 0; i--)\n-\t\t\tif (s->hunk[i].use == UNDECIDED_HUNK) {\n+\t\t\tif (file_diff->hunk[i].use == UNDECIDED_HUNK) {\n \t\t\t\tundecided_previous = i;\n \t\t\t\tbreak;\n \t\t\t}\n \n \t\tundecided_next = -1;\n-\t\tfor (i = hunk_index + 1; i < s->hunk_nr; i++)\n-\t\t\tif (s->hunk[i].use == UNDECIDED_HUNK) {\n+\t\tfor (i = hunk_index + 1; i < file_diff->hunk_nr; i++)\n+\t\t\tif (file_diff->hunk[i].use == UNDECIDED_HUNK) {\n \t\t\t\tundecided_next = i;\n \t\t\t\tbreak;\n \t\t\t}\n@@ -344,11 +356,12 @@ static int patch_update_file(struct add_p_state *s)\n \t\t\tstrbuf_addstr(&s->buf, \",K\");\n \t\tif (undecided_next >= 0)\n \t\t\tstrbuf_addstr(&s->buf, \",j\");\n-\t\tif (hunk_index + 1 < s->hunk_nr)\n+\t\tif (hunk_index + 1 < file_diff->hunk_nr)\n \t\t\tstrbuf_addstr(&s->buf, \",J\");\n \t\tcolor_fprintf(stdout, s->s.prompt_color,\n \t\t\t      \"(%\"PRIuMAX\"/%\"PRIuMAX\") \",\n-\t\t\t      (uintmax_t)hunk_index + 1, (uintmax_t)s->hunk_nr);\n+\t\t\t      (uintmax_t)hunk_index + 1,\n+\t\t\t      (uintmax_t)file_diff->hunk_nr);\n \t\tcolor_fprintf(stdout, s->s.prompt_color,\n \t\t\t      _(\"Stage this hunk [y,n,a,d%s,?]? \"),\n \t\t\t      s->buf.buf);\n@@ -364,19 +377,19 @@ static int patch_update_file(struct add_p_state *s)\n \t\t\thunk->use = USE_HUNK;\n soft_increment:\n \t\t\thunk_index = undecided_next < 0 ?\n-\t\t\t\ts->hunk_nr : undecided_next;\n+\t\t\t\tfile_diff->hunk_nr : undecided_next;\n \t\t} else if (ch == 'n') {\n \t\t\thunk->use = SKIP_HUNK;\n \t\t\tgoto soft_increment;\n \t\t} else if (ch == 'a') {\n-\t\t\tfor (; hunk_index < s->hunk_nr; hunk_index++) {\n-\t\t\t\thunk = s->hunk + hunk_index;\n+\t\t\tfor (; hunk_index < file_diff->hunk_nr; hunk_index++) {\n+\t\t\t\thunk = file_diff->hunk + hunk_index;\n \t\t\t\tif (hunk->use == UNDECIDED_HUNK)\n \t\t\t\t\thunk->use = USE_HUNK;\n \t\t\t}\n \t\t} else if (ch == 'd') {\n-\t\t\tfor (; hunk_index < s->hunk_nr; hunk_index++) {\n-\t\t\t\thunk = s->hunk + hunk_index;\n+\t\t\tfor (; hunk_index < file_diff->hunk_nr; hunk_index++) {\n+\t\t\t\thunk = file_diff->hunk + hunk_index;\n \t\t\t\tif (hunk->use == UNDECIDED_HUNK)\n \t\t\t\t\thunk->use = SKIP_HUNK;\n \t\t\t}\n@@ -386,7 +399,7 @@ static int patch_update_file(struct add_p_state *s)\n \t\t\telse\n \t\t\t\terr(s, _(\"No previous hunk\"));\n \t\t} else if (s->answer.buf[0] == 'J') {\n-\t\t\tif (hunk_index + 1 < s->hunk_nr)\n+\t\t\tif (hunk_index + 1 < file_diff->hunk_nr)\n \t\t\t\thunk_index++;\n \t\t\telse\n \t\t\t\terr(s, _(\"No next hunk\"));\n@@ -406,14 +419,14 @@ static int patch_update_file(struct add_p_state *s)\n \t}\n \n \t/* Any hunk to be used? */\n-\tfor (i = 0; i < s->hunk_nr; i++)\n-\t\tif (s->hunk[i].use == USE_HUNK)\n+\tfor (i = 0; i < file_diff->hunk_nr; i++)\n+\t\tif (file_diff->hunk[i].use == USE_HUNK)\n \t\t\tbreak;\n \n-\tif (i < s->hunk_nr) {\n+\tif (i < file_diff->hunk_nr) {\n \t\t/* At least one hunk selected: apply */\n \t\tstrbuf_reset(&s->buf);\n-\t\treassemble_patch(s, &s->buf);\n+\t\treassemble_patch(s, file_diff, &s->buf);\n \n \t\tdiscard_index(s->s.r->index);\n \t\tsetup_child_process(s, &cp, \"apply\", \"--cached\", NULL);\n@@ -434,6 +447,7 @@ int run_add_p(struct repository *r, const struct pathspec *ps)\n \tstruct add_p_state s = {\n \t\t{ r }, STRBUF_INIT, STRBUF_INIT, STRBUF_INIT, STRBUF_INIT\n \t};\n+\tsize_t i;\n \n \tinit_add_i_state(&s.s, r);\n \n@@ -446,8 +460,9 @@ int run_add_p(struct repository *r, const struct pathspec *ps)\n \t\treturn -1;\n \t}\n \n-\tif (s.hunk_nr)\n-\t\tpatch_update_file(&s);\n+\tfor (i = 0; i < s.file_diff_nr; i++)\n+\t\tif (patch_update_file(&s, s.file_diff + i))\n+\t\t\tbreak;\n \n \tstrbuf_release(&s.answer);\n \tstrbuf_release(&s.buf);\n-- \ngitgitgadget\n\n"},{"id":"388087","messageId":"9ef0db2d8826f2b5c884d9e9834eedc89cd29c39.1576224486.git.gitgitgadget@gmail.com","threadId":"52445","inReplyTo":"pull.173.git.1576224486.gitgitgadget@gmail.com","subject":"[PATCH 06/19] built-in add -p: offer a helpful error message when hunk navigation failed","fromName":"Johannes Schindelin via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2019-12-13T08:07:53Z","receivedAt":"2019-12-13T08:08:21Z","isPatch":true,"sender":{"key":"johannes.schindelin@gmx.de","avatar":"https://avatars.githubusercontent.com/u/127790?v=4"},"body":"From: Johannes Schindelin <johannes.schindelin@gmx.de>\n\n... just like the Perl version currently does...\n\nSigned-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n---\n add-patch.c | 44 +++++++++++++++++++++++++++++++++-----------\n 1 file changed, 33 insertions(+), 11 deletions(-)\n\ndiff --git a/add-patch.c b/add-patch.c\nindex dab2ff2381..f59471cdf2 100644\n--- a/add-patch.c\n+++ b/add-patch.c\n@@ -34,6 +34,18 @@ struct add_p_state {\n \tsize_t hunk_nr, hunk_alloc;\n };\n \n+static void err(struct add_p_state *s, const char *fmt, ...)\n+{\n+\tva_list args;\n+\n+\tva_start(args, fmt);\n+\tfputs(s->s.error_color, stderr);\n+\tvfprintf(stderr, fmt, args);\n+\tfputs(s->s.reset_color, stderr);\n+\tfputc('\\n', stderr);\n+\tva_end(args);\n+}\n+\n static void setup_child_process(struct add_p_state *s,\n \t\t\t\tstruct child_process *cp, ...)\n {\n@@ -368,17 +380,27 @@ static int patch_update_file(struct add_p_state *s)\n \t\t\t\tif (hunk->use == UNDECIDED_HUNK)\n \t\t\t\t\thunk->use = SKIP_HUNK;\n \t\t\t}\n-\t\t} else if (hunk_index && s->answer.buf[0] == 'K')\n-\t\t\thunk_index--;\n-\t\telse if (hunk_index + 1 < s->hunk_nr &&\n-\t\t\t s->answer.buf[0] == 'J')\n-\t\t\thunk_index++;\n-\t\telse if (undecided_previous >= 0 &&\n-\t\t\t s->answer.buf[0] == 'k')\n-\t\t\thunk_index = undecided_previous;\n-\t\telse if (undecided_next >= 0 && s->answer.buf[0] == 'j')\n-\t\t\thunk_index = undecided_next;\n-\t\telse\n+\t\t} else if (s->answer.buf[0] == 'K') {\n+\t\t\tif (hunk_index)\n+\t\t\t\thunk_index--;\n+\t\t\telse\n+\t\t\t\terr(s, _(\"No previous hunk\"));\n+\t\t} else if (s->answer.buf[0] == 'J') {\n+\t\t\tif (hunk_index + 1 < s->hunk_nr)\n+\t\t\t\thunk_index++;\n+\t\t\telse\n+\t\t\t\terr(s, _(\"No next hunk\"));\n+\t\t} else if (s->answer.buf[0] == 'k') {\n+\t\t\tif (undecided_previous >= 0)\n+\t\t\t\thunk_index = undecided_previous;\n+\t\t\telse\n+\t\t\t\terr(s, _(\"No previous hunk\"));\n+\t\t} else if (s->answer.buf[0] == 'j') {\n+\t\t\tif (undecided_next >= 0)\n+\t\t\t\thunk_index = undecided_next;\n+\t\t\telse\n+\t\t\t\terr(s, _(\"No next hunk\"));\n+\t\t} else\n \t\t\tcolor_fprintf(stdout, s->s.help_color,\n \t\t\t\t      _(help_patch_text));\n \t}\n-- \ngitgitgadget\n\n"},{"id":"388092","messageId":"8d588896603e560b82f37e1924dd012a63df6d95.1576224486.git.gitgitgadget@gmail.com","threadId":"52445","inReplyTo":"pull.173.git.1576224486.gitgitgadget@gmail.com","subject":"[PATCH 10/19] built-in add -p: show different prompts for mode changes and deletions","fromName":"Johannes Schindelin via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2019-12-13T08:07:57Z","receivedAt":"2019-12-13T08:08:22Z","isPatch":true,"sender":{"key":"johannes.schindelin@gmx.de","avatar":"https://avatars.githubusercontent.com/u/127790?v=4"},"body":"From: Johannes Schindelin <johannes.schindelin@gmx.de>\n\nJust like the Perl version, we now helpfully ask the user whether they\nwant to stage a mode change, or a deletion.\n\nNote that we define the prompts in an array, in preparation for a later\npatch that changes those prompts to yet different versions for `git\nreset -p`, `git stash -p` and `git checkout -p` (which all call the `git\nadd -p` machinery to do the actual work).\n\nSigned-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n---\n add-patch.c | 22 ++++++++++++++++++++--\n 1 file changed, 20 insertions(+), 2 deletions(-)\n\ndiff --git a/add-patch.c b/add-patch.c\nindex 2007f55e04..171025b08d 100644\n--- a/add-patch.c\n+++ b/add-patch.c\n@@ -7,6 +7,16 @@\n #include \"color.h\"\n #include \"diff.h\"\n \n+enum prompt_mode_type {\n+\tPROMPT_MODE_CHANGE = 0, PROMPT_DELETION, PROMPT_HUNK\n+};\n+\n+static const char *prompt_mode[] = {\n+\tN_(\"Stage mode change [y,n,a,d%s,?]? \"),\n+\tN_(\"Stage deletion [y,n,a,d%s,?]? \"),\n+\tN_(\"Stage this hunk [y,n,a,d%s,?]? \")\n+};\n+\n struct hunk_header {\n \tunsigned long old_offset, old_count, new_offset, new_count;\n \t/*\n@@ -422,6 +432,7 @@ static int patch_update_file(struct add_p_state *s,\n \tchar ch;\n \tstruct child_process cp = CHILD_PROCESS_INIT;\n \tint colored = !!s->colored.len;\n+\tenum prompt_mode_type prompt_mode_type;\n \n \tif (!file_diff->hunk_nr)\n \t\treturn 0;\n@@ -466,13 +477,20 @@ static int patch_update_file(struct add_p_state *s,\n \t\t\tstrbuf_addstr(&s->buf, \",j\");\n \t\tif (hunk_index + 1 < file_diff->hunk_nr)\n \t\t\tstrbuf_addstr(&s->buf, \",J\");\n+\n+\t\tif (file_diff->deleted)\n+\t\t\tprompt_mode_type = PROMPT_DELETION;\n+\t\telse if (file_diff->mode_change && !hunk_index)\n+\t\t\tprompt_mode_type = PROMPT_MODE_CHANGE;\n+\t\telse\n+\t\t\tprompt_mode_type = PROMPT_HUNK;\n+\n \t\tcolor_fprintf(stdout, s->s.prompt_color,\n \t\t\t      \"(%\"PRIuMAX\"/%\"PRIuMAX\") \",\n \t\t\t      (uintmax_t)hunk_index + 1,\n \t\t\t      (uintmax_t)file_diff->hunk_nr);\n \t\tcolor_fprintf(stdout, s->s.prompt_color,\n-\t\t\t      _(\"Stage this hunk [y,n,a,d%s,?]? \"),\n-\t\t\t      s->buf.buf);\n+\t\t\t      _(prompt_mode[prompt_mode_type]), s->buf.buf);\n \t\tfflush(stdout);\n \t\tif (strbuf_getline(&s->answer, stdin) == EOF)\n \t\t\tbreak;\n-- \ngitgitgadget\n\n"},{"id":"388088","messageId":"a3624d720fa6aa77a3ef0f34618f27c417526fe7.1576224486.git.gitgitgadget@gmail.com","threadId":"52445","inReplyTo":"pull.173.git.1576224486.gitgitgadget@gmail.com","subject":"[PATCH 09/19] built-in app -p: allow selecting a mode change as a \"hunk\"","fromName":"Johannes Schindelin via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2019-12-13T08:07:56Z","receivedAt":"2019-12-13T08:08:23Z","isPatch":true,"sender":{"key":"johannes.schindelin@gmx.de","avatar":"https://avatars.githubusercontent.com/u/127790?v=4"},"body":"From: Johannes Schindelin <johannes.schindelin@gmx.de>\n\nThis imitates the way the Perl version treats mode changes: it offers\nthe mode change up for the user to decide, as if it was a diff hunk.\n\nIn contrast to the Perl version, we make use of the fact that the mode\nline is the first hunk, and explicitly strip out that line from the diff\nheader if that \"hunk\" was not selected to be applied, and skipping that\nhunk while coalescing the diff. The Perl version plays some kind of diff\nline lego instead.\n\nSigned-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n---\n add-patch.c | 109 +++++++++++++++++++++++++++++++++++++++++++++++++---\n 1 file changed, 104 insertions(+), 5 deletions(-)\n\ndiff --git a/add-patch.c b/add-patch.c\nindex c32541f46d..2007f55e04 100644\n--- a/add-patch.c\n+++ b/add-patch.c\n@@ -33,7 +33,7 @@ struct add_p_state {\n \t\tstruct hunk head;\n \t\tstruct hunk *hunk;\n \t\tsize_t hunk_nr, hunk_alloc;\n-\t\tunsigned deleted:1;\n+\t\tunsigned deleted:1, mode_change:1;\n \t} *file_diff;\n \tsize_t file_diff_nr;\n };\n@@ -129,6 +129,17 @@ static int parse_hunk_header(struct add_p_state *s, struct hunk *hunk)\n \treturn 0;\n }\n \n+static int is_octal(const char *p, size_t len)\n+{\n+\tif (!len)\n+\t\treturn 0;\n+\n+\twhile (len--)\n+\t\tif (*p < '0' || *(p++) > '7')\n+\t\t\treturn 0;\n+\treturn 1;\n+}\n+\n static int parse_diff(struct add_p_state *s, const struct pathspec *ps)\n {\n \tstruct argv_array args = ARGV_ARRAY_INIT;\n@@ -181,7 +192,7 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps)\n \tpend = p + plain->len;\n \twhile (p != pend) {\n \t\tchar *eol = memchr(p, '\\n', pend - p);\n-\t\tconst char *deleted = NULL;\n+\t\tconst char *deleted = NULL, *mode_change = NULL;\n \n \t\tif (!eol)\n \t\t\teol = pend;\n@@ -218,8 +229,53 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps)\n \t\t\t\tfile_diff->deleted = 1;\n \t\t\telse if (parse_hunk_header(s, hunk) < 0)\n \t\t\t\treturn -1;\n+\t\t} else if (hunk == &file_diff->head &&\n+\t\t\t   skip_prefix(p, \"old mode \", &mode_change) &&\n+\t\t\t   is_octal(mode_change, eol - mode_change)) {\n+\t\t\tif (file_diff->mode_change)\n+\t\t\t\tBUG(\"double mode change?\\n\\n%.*s\",\n+\t\t\t\t    (int)(eol - plain->buf), plain->buf);\n+\t\t\tif (file_diff->hunk_nr++)\n+\t\t\t\tBUG(\"mode change in the middle?\\n\\n%.*s\",\n+\t\t\t\t    (int)(eol - plain->buf), plain->buf);\n+\n+\t\t\t/*\n+\t\t\t * Do *not* change `hunk`: the mode change pseudo-hunk\n+\t\t\t * is _part of_ the header \"hunk\".\n+\t\t\t */\n+\t\t\tfile_diff->mode_change = 1;\n+\t\t\tALLOC_GROW(file_diff->hunk, file_diff->hunk_nr,\n+\t\t\t\t   file_diff->hunk_alloc);\n+\t\t\tmemset(file_diff->hunk, 0, sizeof(struct hunk));\n+\t\t\tfile_diff->hunk->start = p - plain->buf;\n+\t\t\tif (colored_p)\n+\t\t\t\tfile_diff->hunk->colored_start =\n+\t\t\t\t\tcolored_p - colored->buf;\n+\t\t} else if (hunk == &file_diff->head &&\n+\t\t\t   skip_prefix(p, \"new mode \", &mode_change) &&\n+\t\t\t   is_octal(mode_change, eol - mode_change)) {\n+\n+\t\t\t/*\n+\t\t\t * Extend the \"mode change\" pseudo-hunk to include also\n+\t\t\t * the \"new mode\" line.\n+\t\t\t */\n+\t\t\tif (!file_diff->mode_change)\n+\t\t\t\tBUG(\"'new mode' without 'old mode'?\\n\\n%.*s\",\n+\t\t\t\t    (int)(eol - plain->buf), plain->buf);\n+\t\t\tif (file_diff->hunk_nr != 1)\n+\t\t\t\tBUG(\"mode change in the middle?\\n\\n%.*s\",\n+\t\t\t\t    (int)(eol - plain->buf), plain->buf);\n+\t\t\tif (p - plain->buf != file_diff->hunk->end)\n+\t\t\t\tBUG(\"'new mode' does not immediately follow \"\n+\t\t\t\t    \"'old mode'?\\n\\n%.*s\",\n+\t\t\t\t    (int)(eol - plain->buf), plain->buf);\n \t\t}\n \n+\t\tif (file_diff->deleted && file_diff->mode_change)\n+\t\t\tBUG(\"diff contains delete *and* a mode change?!?\\n%.*s\",\n+\t\t\t    (int)(eol - (plain->buf + file_diff->head.start)),\n+\t\t\t    plain->buf + file_diff->head.start);\n+\n \t\tp = eol == pend ? pend : eol + 1;\n \t\thunk->end = p - plain->buf;\n \n@@ -233,6 +289,16 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps)\n \n \t\t\thunk->colored_end = colored_p - colored->buf;\n \t\t}\n+\n+\t\tif (mode_change) {\n+\t\t\tif (file_diff->hunk_nr != 1)\n+\t\t\t\tBUG(\"mode change in hunk #%d???\",\n+\t\t\t\t    (int)file_diff->hunk_nr);\n+\t\t\t/* Adjust the end of the \"mode change\" pseudo-hunk */\n+\t\t\tfile_diff->hunk->end = hunk->end;\n+\t\t\tif (colored)\n+\t\t\t\tfile_diff->hunk->colored_end = hunk->colored_end;\n+\t\t}\n \t}\n \n \treturn 0;\n@@ -284,6 +350,39 @@ static void render_hunk(struct add_p_state *s, struct hunk *hunk,\n \t\t\t   hunk->end - hunk->start);\n }\n \n+static void render_diff_header(struct add_p_state *s,\n+\t\t\t       struct file_diff *file_diff, int colored,\n+\t\t\t       struct strbuf *out)\n+{\n+\t/*\n+\t * If there was a mode change, the first hunk is a pseudo hunk that\n+\t * corresponds to the mode line in the header. If the user did not want\n+\t * to stage that \"hunk\", we actually have to cut it out from the header.\n+\t */\n+\tint skip_mode_change =\n+\t\tfile_diff->mode_change && file_diff->hunk->use != USE_HUNK;\n+\tstruct hunk *head = &file_diff->head, *first = file_diff->hunk;\n+\n+\tif (!skip_mode_change) {\n+\t\trender_hunk(s, head, 0, colored, out);\n+\t\treturn;\n+\t}\n+\n+\tif (colored) {\n+\t\tconst char *p = s->colored.buf;\n+\n+\t\tstrbuf_add(out, p + head->colored_start,\n+\t\t\t    first->colored_start - head->colored_start);\n+\t\tstrbuf_add(out, p + first->colored_end,\n+\t\t\t    head->colored_end - first->colored_end);\n+\t} else {\n+\t\tconst char *p = s->plain.buf;\n+\n+\t\tstrbuf_add(out, p + head->start, first->start - head->start);\n+\t\tstrbuf_add(out, p + first->end, head->end - first->end);\n+\t}\n+}\n+\n static void reassemble_patch(struct add_p_state *s,\n \t\t\t     struct file_diff *file_diff, struct strbuf *out)\n {\n@@ -291,9 +390,9 @@ static void reassemble_patch(struct add_p_state *s,\n \tsize_t i;\n \tssize_t delta = 0;\n \n-\trender_hunk(s, &file_diff->head, 0, 0, out);\n+\trender_diff_header(s, file_diff, 0, out);\n \n-\tfor (i = 0; i < file_diff->hunk_nr; i++) {\n+\tfor (i = file_diff->mode_change; i < file_diff->hunk_nr; i++) {\n \t\thunk = file_diff->hunk + i;\n \t\tif (hunk->use != USE_HUNK)\n \t\t\tdelta += hunk->header.old_count\n@@ -328,7 +427,7 @@ static int patch_update_file(struct add_p_state *s,\n \t\treturn 0;\n \n \tstrbuf_reset(&s->buf);\n-\trender_hunk(s, &file_diff->head, 0, colored, &s->buf);\n+\trender_diff_header(s, file_diff, colored, &s->buf);\n \tfputs(s->buf.buf, stdout);\n \tfor (;;) {\n \t\tif (hunk_index >= file_diff->hunk_nr)\n-- \ngitgitgadget\n\n"},{"id":"388089","messageId":"18c008fbe119036700faf2a82711c2b0f453df6a.1576224486.git.gitgitgadget@gmail.com","threadId":"52445","inReplyTo":"pull.173.git.1576224486.gitgitgadget@gmail.com","subject":"[PATCH 13/19] strbuf: add a helper function to call the editor \"on an strbuf\"","fromName":"Johannes Schindelin via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2019-12-13T08:08:00Z","receivedAt":"2019-12-13T08:08:25Z","isPatch":true,"sender":{"key":"johannes.schindelin@gmx.de","avatar":"https://avatars.githubusercontent.com/u/127790?v=4"},"body":"From: Johannes Schindelin <johannes.schindelin@gmx.de>\n\nThis helper supports the scenario where Git has a populated `strbuf` and\nwants to let the user edit it interactively.\n\nIn `git add -p`, we will use this to allow interactive hunk editing: the\ndiff hunks are already in memory, but we need to write them out to a\nfile so that an editor can be launched, then read everything back once\nthe user is done editing.\n\nSigned-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n---\n strbuf.c | 28 ++++++++++++++++++++++++++++\n strbuf.h | 11 +++++++++++\n 2 files changed, 39 insertions(+)\n\ndiff --git a/strbuf.c b/strbuf.c\nindex aa48d179a9..f19da55b07 100644\n--- a/strbuf.c\n+++ b/strbuf.c\n@@ -1125,3 +1125,31 @@ int strbuf_normalize_path(struct strbuf *src)\n \tstrbuf_release(&dst);\n \treturn 0;\n }\n+\n+int strbuf_edit_interactively(struct strbuf *buffer, const char *path,\n+\t\t\t      const char *const *env)\n+{\n+\tchar *path2 = NULL;\n+\tint fd, res = 0;\n+\n+\tif (!is_absolute_path(path))\n+\t\tpath = path2 = xstrdup(git_path(\"%s\", path));\n+\n+\tfd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0666);\n+\tif (fd < 0)\n+\t\tres = error_errno(_(\"could not open '%s' for writing\"), path);\n+\telse if (write_in_full(fd, buffer->buf, buffer->len) < 0) {\n+\t\tres = error_errno(_(\"could not write to '%s'\"), path);\n+\t\tclose(fd);\n+\t} else if (close(fd) < 0)\n+\t\tres = error_errno(_(\"could not close '%s'\"), path);\n+\telse {\n+\t\tstrbuf_reset(buffer);\n+\t\tif (launch_editor(path, buffer, env) < 0)\n+\t\t\tres = error_errno(_(\"could not edit '%s'\"), path);\n+\t\tunlink(path);\n+\t}\n+\n+\tfree(path2);\n+\treturn res;\n+}\ndiff --git a/strbuf.h b/strbuf.h\nindex 84cf969721..bfa66569a4 100644\n--- a/strbuf.h\n+++ b/strbuf.h\n@@ -621,6 +621,17 @@ int launch_editor(const char *path, struct strbuf *buffer,\n int launch_sequence_editor(const char *path, struct strbuf *buffer,\n \t\t\t   const char *const *env);\n \n+/*\n+ * In contrast to `launch_editor()`, this function writes out the contents\n+ * of the specified file first, then clears the `buffer`, then launches\n+ * the editor and reads back in the file contents into the `buffer`.\n+ * Finally, it deletes the temporary file.\n+ *\n+ * If `path` is relative, it refers to a file in the `.git` directory.\n+ */\n+int strbuf_edit_interactively(struct strbuf *buffer, const char *path,\n+\t\t\t      const char *const *env);\n+\n void strbuf_add_lines(struct strbuf *sb,\n \t\t      const char *prefix,\n \t\t      const char *buf,\n-- \ngitgitgadget\n\n"},{"id":"388091","messageId":"da950fdfc5c3e15c59ee2f9cf301590b776e8dca.1576224486.git.gitgitgadget@gmail.com","threadId":"52445","inReplyTo":"pull.173.git.1576224486.gitgitgadget@gmail.com","subject":"[PATCH 12/19] built-in add -p: coalesce hunks after splitting them","fromName":"Johannes Schindelin via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2019-12-13T08:07:59Z","receivedAt":"2019-12-13T08:08:25Z","isPatch":true,"sender":{"key":"johannes.schindelin@gmx.de","avatar":"https://avatars.githubusercontent.com/u/127790?v=4"},"body":"From: Johannes Schindelin <johannes.schindelin@gmx.de>\n\nThis is considered \"the right thing to do\", according to 933e44d3a0\n(\"add -p\": work-around an old laziness that does not coalesce hunks,\n2011-04-06).\n\nNote: we cannot simply modify the hunks while merging them; Once we\nimplement hunk editing, we will call `reassemble_patch()` whenever a\nhunk is edited, therefore we must not modify the hunks (because the user\nmight e.g. hit `K` and change their mind whether to stage the previous\nhunk).\n\nSigned-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n---\n add-patch.c | 58 ++++++++++++++++++++++++++++++++++++++++++++++++++++-\n 1 file changed, 57 insertions(+), 1 deletion(-)\n\ndiff --git a/add-patch.c b/add-patch.c\nindex 2d34ddd7f4..c8d84aec68 100644\n--- a/add-patch.c\n+++ b/add-patch.c\n@@ -433,6 +433,55 @@ static void render_diff_header(struct add_p_state *s,\n \t}\n }\n \n+/* Coalesce hunks again that were split */\n+static int merge_hunks(struct add_p_state *s, struct file_diff *file_diff,\n+\t\t       size_t *hunk_index, struct hunk *merged)\n+{\n+\tsize_t i = *hunk_index;\n+\tstruct hunk *hunk = file_diff->hunk + i;\n+\t/* `header` corresponds to the merged hunk */\n+\tstruct hunk_header *header = &merged->header, *next;\n+\n+\tif (hunk->use != USE_HUNK)\n+\t\treturn 0;\n+\n+\t*merged = *hunk;\n+\t/* We simply skip the colored part (if any) when merging hunks */\n+\tmerged->colored_start = merged->colored_end = 0;\n+\n+\tfor (; i + 1 < file_diff->hunk_nr; i++) {\n+\t\thunk++;\n+\t\tnext = &hunk->header;\n+\n+\t\t/*\n+\t\t * Stop merging hunks when:\n+\t\t *\n+\t\t * - the hunk is not selected for use, or\n+\t\t * - the hunk does not overlap with the already-merged hunk(s)\n+\t\t */\n+\t\tif (hunk->use != USE_HUNK ||\n+\t\t    header->new_offset >= next->new_offset ||\n+\t\t    header->new_offset + header->new_count < next->new_offset ||\n+\t\t    merged->start >= hunk->start ||\n+\t\t    merged->end < hunk->start)\n+\t\t\tbreak;\n+\n+\t\tmerged->end = hunk->end;\n+\t\tmerged->colored_end = hunk->colored_end;\n+\n+\t\theader->old_count = next->old_offset + next->old_count\n+\t\t\t- header->old_offset;\n+\t\theader->new_count = next->new_offset + next->new_count\n+\t\t\t- header->new_offset;\n+\t}\n+\n+\tif (i == *hunk_index)\n+\t\treturn 0;\n+\n+\t*hunk_index = i;\n+\treturn 1;\n+}\n+\n static void reassemble_patch(struct add_p_state *s,\n \t\t\t     struct file_diff *file_diff, struct strbuf *out)\n {\n@@ -443,12 +492,19 @@ static void reassemble_patch(struct add_p_state *s,\n \trender_diff_header(s, file_diff, 0, out);\n \n \tfor (i = file_diff->mode_change; i < file_diff->hunk_nr; i++) {\n+\t\tstruct hunk merged = { 0 };\n+\n \t\thunk = file_diff->hunk + i;\n \t\tif (hunk->use != USE_HUNK)\n \t\t\tdelta += hunk->header.old_count\n \t\t\t\t- hunk->header.new_count;\n-\t\telse\n+\t\telse {\n+\t\t\t/* merge overlapping hunks into a temporary hunk */\n+\t\t\tif (merge_hunks(s, file_diff, &i, &merged))\n+\t\t\t\thunk = &merged;\n+\n \t\t\trender_hunk(s, hunk, delta, 0, out);\n+\t\t}\n \t}\n }\n \n-- \ngitgitgadget\n\n"},{"id":"388093","messageId":"9a2cf7ed71fe012de93a5d23b72c9c3aa0bdcdf9.1576224486.git.gitgitgadget@gmail.com","threadId":"52445","inReplyTo":"pull.173.git.1576224486.gitgitgadget@gmail.com","subject":"[PATCH 15/19] built-in add -p: implement the 'g' (\"goto\") command","fromName":"Johannes Schindelin via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2019-12-13T08:08:02Z","receivedAt":"2019-12-13T08:08:27Z","isPatch":true,"sender":{"key":"johannes.schindelin@gmx.de","avatar":"https://avatars.githubusercontent.com/u/127790?v=4"},"body":"From: Johannes Schindelin <johannes.schindelin@gmx.de>\n\nWith this patch, it is now possible to see a summary of the available\nhunks and to navigate between them (by number).\n\nA test is added to verify that this behavior matches the one of the Perl\nversion of `git add -p`.\n\nSigned-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n---\n add-patch.c                | 88 ++++++++++++++++++++++++++++++++++++++\n t/t3701-add-interactive.sh | 16 +++++++\n 2 files changed, 104 insertions(+)\n\ndiff --git a/add-patch.c b/add-patch.c\nindex ea863ca09d..fdbb1e3e22 100644\n--- a/add-patch.c\n+++ b/add-patch.c\n@@ -955,6 +955,54 @@ static int edit_hunk_loop(struct add_p_state *s,\n \t}\n }\n \n+#define SUMMARY_HEADER_WIDTH 20\n+#define SUMMARY_LINE_WIDTH 80\n+static void summarize_hunk(struct add_p_state *s, struct hunk *hunk,\n+\t\t\t   struct strbuf *out)\n+{\n+\tstruct hunk_header *header = &hunk->header;\n+\tstruct strbuf *plain = &s->plain;\n+\tsize_t len = out->len, i;\n+\n+\tstrbuf_addf(out, \" -%lu,%lu +%lu,%lu \",\n+\t\t    header->old_offset, header->old_count,\n+\t\t    header->new_offset, header->new_count);\n+\tif (out->len - len < SUMMARY_HEADER_WIDTH)\n+\t\tstrbuf_addchars(out, ' ',\n+\t\t\t\tSUMMARY_HEADER_WIDTH + len - out->len);\n+\tfor (i = hunk->start; i < hunk->end; i = find_next_line(plain, i))\n+\t\tif (plain->buf[i] != ' ')\n+\t\t\tbreak;\n+\tif (i < hunk->end)\n+\t\tstrbuf_add(out, plain->buf + i, find_next_line(plain, i) - i);\n+\tif (out->len - len > SUMMARY_LINE_WIDTH)\n+\t\tstrbuf_setlen(out, len + SUMMARY_LINE_WIDTH);\n+\tstrbuf_complete_line(out);\n+}\n+\n+#define DISPLAY_HUNKS_LINES 20\n+static size_t display_hunks(struct add_p_state *s,\n+\t\t\t    struct file_diff *file_diff, size_t start_index)\n+{\n+\tsize_t end_index = start_index + DISPLAY_HUNKS_LINES;\n+\n+\tif (end_index > file_diff->hunk_nr)\n+\t\tend_index = file_diff->hunk_nr;\n+\n+\twhile (start_index < end_index) {\n+\t\tstruct hunk *hunk = file_diff->hunk + start_index++;\n+\n+\t\tstrbuf_reset(&s->buf);\n+\t\tstrbuf_addf(&s->buf, \"%c%2d: \", hunk->use == USE_HUNK ? '+'\n+\t\t\t    : hunk->use == SKIP_HUNK ? '-' : ' ',\n+\t\t\t    (int)start_index);\n+\t\tsummarize_hunk(s, hunk, &s->buf);\n+\t\tfputs(s->buf.buf, stdout);\n+\t}\n+\n+\treturn end_index;\n+}\n+\n static const char help_patch_text[] =\n N_(\"y - stage this hunk\\n\"\n    \"n - do not stage this hunk\\n\"\n@@ -964,6 +1012,7 @@ N_(\"y - stage this hunk\\n\"\n    \"J - leave this hunk undecided, see next hunk\\n\"\n    \"k - leave this hunk undecided, see previous undecided hunk\\n\"\n    \"K - leave this hunk undecided, see previous hunk\\n\"\n+   \"g - select a hunk to go to\\n\"\n    \"s - split the current hunk into smaller hunks\\n\"\n    \"e - manually edit the current hunk\\n\"\n    \"? - print help\\n\");\n@@ -1022,6 +1071,8 @@ static int patch_update_file(struct add_p_state *s,\n \t\t\tstrbuf_addstr(&s->buf, \",j\");\n \t\tif (hunk_index + 1 < file_diff->hunk_nr)\n \t\t\tstrbuf_addstr(&s->buf, \",J\");\n+\t\tif (file_diff->hunk_nr > 1)\n+\t\t\tstrbuf_addstr(&s->buf, \",g\");\n \t\tif (hunk->splittable_into > 1)\n \t\t\tstrbuf_addstr(&s->buf, \",s\");\n \t\tif (hunk_index + 1 > file_diff->mode_change &&\n@@ -1089,6 +1140,43 @@ static int patch_update_file(struct add_p_state *s,\n \t\t\t\thunk_index = undecided_next;\n \t\t\telse\n \t\t\t\terr(s, _(\"No next hunk\"));\n+\t\t} else if (s->answer.buf[0] == 'g') {\n+\t\t\tchar *pend;\n+\t\t\tunsigned long response;\n+\n+\t\t\tif (file_diff->hunk_nr < 2) {\n+\t\t\t\terr(s, _(\"No other hunks to goto\"));\n+\t\t\t\tcontinue;\n+\t\t\t}\n+\t\t\tstrbuf_remove(&s->answer, 0, 1);\n+\t\t\tstrbuf_trim(&s->answer);\n+\t\t\ti = hunk_index - DISPLAY_HUNKS_LINES / 2;\n+\t\t\tif (i < file_diff->mode_change)\n+\t\t\t\ti = file_diff->mode_change;\n+\t\t\twhile (s->answer.len == 0) {\n+\t\t\t\ti = display_hunks(s, file_diff, i);\n+\t\t\t\tprintf(\"%s\", i < file_diff->hunk_nr ?\n+\t\t\t\t       _(\"go to which hunk (<ret> to see \"\n+\t\t\t\t\t \"more)? \") : _(\"go to which hunk? \"));\n+\t\t\t\tfflush(stdout);\n+\t\t\t\tif (strbuf_getline(&s->answer,\n+\t\t\t\t\t\t   stdin) == EOF)\n+\t\t\t\t\tbreak;\n+\t\t\t\tstrbuf_trim_trailing_newline(&s->answer);\n+\t\t\t}\n+\n+\t\t\tstrbuf_trim(&s->answer);\n+\t\t\tresponse = strtoul(s->answer.buf, &pend, 10);\n+\t\t\tif (*pend || pend == s->answer.buf)\n+\t\t\t\terr(s, _(\"Invalid number: '%s'\"),\n+\t\t\t\t    s->answer.buf);\n+\t\t\telse if (0 < response && response <= file_diff->hunk_nr)\n+\t\t\t\thunk_index = response - 1;\n+\t\t\telse\n+\t\t\t\terr(s, Q_(\"Sorry, only %d hunk available.\",\n+\t\t\t\t\t  \"Sorry, only %d hunks available.\",\n+\t\t\t\t\t  file_diff->hunk_nr),\n+\t\t\t\t    (int)file_diff->hunk_nr);\n \t\t} else if (s->answer.buf[0] == 's') {\n \t\t\tsize_t splittable_into = hunk->splittable_into;\n \t\t\tif (splittable_into < 2)\ndiff --git a/t/t3701-add-interactive.sh b/t/t3701-add-interactive.sh\nindex fe383be50e..57c656a20c 100755\n--- a/t/t3701-add-interactive.sh\n+++ b/t/t3701-add-interactive.sh\n@@ -413,6 +413,22 @@ test_expect_success 'split hunk setup' '\n \ttest_write_lines 10 15 20 21 22 23 24 30 40 50 60 >test\n '\n \n+test_expect_success 'goto hunk' '\n+\ttest_when_finished \"git reset\" &&\n+\ttr _ \" \" >expect <<-EOF &&\n+\t(2/2) Stage this hunk [y,n,q,a,d,K,g,/,e,?]? + 1:  -1,2 +1,3          +15\n+\t_ 2:  -2,4 +3,8          +21\n+\tgo to which hunk? @@ -1,2 +1,3 @@\n+\t_10\n+\t+15\n+\t_20\n+\t(1/2) Stage this hunk [y,n,q,a,d,j,J,g,/,e,?]?_\n+\tEOF\n+\ttest_write_lines s y g 1 | git add -p >actual &&\n+\ttail -n 7 <actual >actual.trimmed &&\n+\ttest_cmp expect actual.trimmed\n+'\n+\n test_expect_success 'split hunk \"add -p (edit)\"' '\n \t# Split, say Edit and do nothing.  Then:\n \t#\n-- \ngitgitgadget\n\n"},{"id":"388090","messageId":"1e23fcd44e17b2980da5997344d6aaa7efe0754a.1576224486.git.gitgitgadget@gmail.com","threadId":"52445","inReplyTo":"pull.173.git.1576224486.gitgitgadget@gmail.com","subject":"[PATCH 14/19] built-in add -p: implement hunk editing","fromName":"Johannes Schindelin via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2019-12-13T08:08:01Z","receivedAt":"2019-12-13T08:08:29Z","isPatch":true,"sender":{"key":"johannes.schindelin@gmx.de","avatar":"https://avatars.githubusercontent.com/u/127790?v=4"},"body":"From: Johannes Schindelin <johannes.schindelin@gmx.de>\n\nJust like `git add --edit` allows the user to edit the diff before it is\nbeing applied to the index, this feature allows the user to edit the\ndiff *hunk*.\n\nNaturally, it gets a bit more complicated here because the result has\nto play well with the remaining hunks of the overall diff. Therefore,\nwe have to do a loop in which we let the user edit the hunk, then test\nwhether the result would work, and if not, drop the edits and let the\nuser decide whether to try editing the hunk again.\n\nNote: in contrast to the Perl version, we use the same diff\n\"coalescing\" (i.e. merging overlapping hunks into a single one) also for\nthe check after editing, and we introduce a new flag for that purpose\nthat asks the `reassemble_patch()` function to pretend that all hunks\nwere selected for use.\n\nThis allows us to continue to run `git apply` *without* the\n`--allow-overlap` option (unlike the Perl version), and it also fixes\ntwo known breakages in `t3701-add-interactive.sh` (which we cannot mark\nas resolved so far because the Perl script version is still the default\nand continues to have those breakages).\n\nSigned-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n---\n add-interactive.c |   6 +\n add-interactive.h |   3 +\n add-patch.c       | 333 +++++++++++++++++++++++++++++++++++++++++++---\n 3 files changed, 325 insertions(+), 17 deletions(-)\n\ndiff --git a/add-interactive.c b/add-interactive.c\nindex 29356c5aa2..6a5048c83e 100644\n--- a/add-interactive.c\n+++ b/add-interactive.c\n@@ -46,6 +46,12 @@ void init_add_i_state(struct add_i_state *s, struct repository *r)\n \tinit_color(r, s, \"reset\", s->reset_color, GIT_COLOR_RESET);\n \tinit_color(r, s, \"fraginfo\", s->fraginfo_color,\n \t\t   diff_get_color(s->use_color, DIFF_FRAGINFO));\n+\tinit_color(r, s, \"context\", s->context_color,\n+\t\tdiff_get_color(s->use_color, DIFF_CONTEXT));\n+\tinit_color(r, s, \"old\", s->file_old_color,\n+\t\tdiff_get_color(s->use_color, DIFF_FILE_OLD));\n+\tinit_color(r, s, \"new\", s->file_new_color,\n+\t\tdiff_get_color(s->use_color, DIFF_FILE_NEW));\n }\n \n /*\ndiff --git a/add-interactive.h b/add-interactive.h\nindex 584f304a9a..062dc3646c 100644\n--- a/add-interactive.h\n+++ b/add-interactive.h\n@@ -12,6 +12,9 @@ struct add_i_state {\n \tchar error_color[COLOR_MAXLEN];\n \tchar reset_color[COLOR_MAXLEN];\n \tchar fraginfo_color[COLOR_MAXLEN];\n+\tchar context_color[COLOR_MAXLEN];\n+\tchar file_old_color[COLOR_MAXLEN];\n+\tchar file_new_color[COLOR_MAXLEN];\n };\n \n void init_add_i_state(struct add_i_state *s, struct repository *r);\ndiff --git a/add-patch.c b/add-patch.c\nindex c8d84aec68..ea863ca09d 100644\n--- a/add-patch.c\n+++ b/add-patch.c\n@@ -29,6 +29,7 @@ struct hunk_header {\n \n struct hunk {\n \tsize_t start, end, colored_start, colored_end, splittable_into;\n+\tssize_t delta;\n \tenum { UNDECIDED_HUNK = 0, SKIP_HUNK, USE_HUNK } use;\n \tstruct hunk_header header;\n };\n@@ -435,14 +436,14 @@ static void render_diff_header(struct add_p_state *s,\n \n /* Coalesce hunks again that were split */\n static int merge_hunks(struct add_p_state *s, struct file_diff *file_diff,\n-\t\t       size_t *hunk_index, struct hunk *merged)\n+\t\t       size_t *hunk_index, int use_all, struct hunk *merged)\n {\n-\tsize_t i = *hunk_index;\n+\tsize_t i = *hunk_index, delta;\n \tstruct hunk *hunk = file_diff->hunk + i;\n \t/* `header` corresponds to the merged hunk */\n \tstruct hunk_header *header = &merged->header, *next;\n \n-\tif (hunk->use != USE_HUNK)\n+\tif (!use_all && hunk->use != USE_HUNK)\n \t\treturn 0;\n \n \t*merged = *hunk;\n@@ -459,20 +460,99 @@ static int merge_hunks(struct add_p_state *s, struct file_diff *file_diff,\n \t\t * - the hunk is not selected for use, or\n \t\t * - the hunk does not overlap with the already-merged hunk(s)\n \t\t */\n-\t\tif (hunk->use != USE_HUNK ||\n-\t\t    header->new_offset >= next->new_offset ||\n-\t\t    header->new_offset + header->new_count < next->new_offset ||\n-\t\t    merged->start >= hunk->start ||\n-\t\t    merged->end < hunk->start)\n+\t\tif ((!use_all && hunk->use != USE_HUNK) ||\n+\t\t    header->new_offset >= next->new_offset + merged->delta ||\n+\t\t    header->new_offset + header->new_count\n+\t\t    < next->new_offset + merged->delta)\n \t\t\tbreak;\n \n-\t\tmerged->end = hunk->end;\n-\t\tmerged->colored_end = hunk->colored_end;\n+\t\t/*\n+\t\t * If the hunks were not edited, and overlap, we can simply\n+\t\t * extend the line range.\n+\t\t */\n+\t\tif (merged->start < hunk->start && merged->end > hunk->start) {\n+\t\t\tmerged->end = hunk->end;\n+\t\t\tmerged->colored_end = hunk->colored_end;\n+\t\t\tdelta = 0;\n+\t\t} else {\n+\t\t\tconst char *plain = s->plain.buf;\n+\t\t\tsize_t  overlapping_line_count = header->new_offset\n+\t\t\t\t+ header->new_count - merged->delta\n+\t\t\t\t- next->new_offset;\n+\t\t\tsize_t overlap_end = hunk->start;\n+\t\t\tsize_t overlap_start = overlap_end;\n+\t\t\tsize_t overlap_next, len, j;\n+\n+\t\t\t/*\n+\t\t\t * One of the hunks was edited: the modified hunk was\n+\t\t\t * appended to the strbuf `s->plain`.\n+\t\t\t *\n+\t\t\t * Let's ensure that at least the last context line of\n+\t\t\t * the first hunk overlaps with the corresponding line\n+\t\t\t * of the second hunk, and then merge.\n+\t\t\t */\n+\t\t\tfor (j = 0; j < overlapping_line_count; j++) {\n+\t\t\t\toverlap_next = find_next_line(&s->plain,\n+\t\t\t\t\t\t\t      overlap_end);\n+\n+\t\t\t\tif (overlap_next > hunk->end)\n+\t\t\t\t\tBUG(\"failed to find %d context lines \"\n+\t\t\t\t\t    \"in:\\n%.*s\",\n+\t\t\t\t\t    (int)overlapping_line_count,\n+\t\t\t\t\t    (int)(hunk->end - hunk->start),\n+\t\t\t\t\t    plain + hunk->start);\n+\n+\t\t\t\tif (plain[overlap_end] != ' ')\n+\t\t\t\t\treturn error(_(\"expected context line \"\n+\t\t\t\t\t\t       \"#%d in\\n%.*s\"),\n+\t\t\t\t\t\t     (int)(j + 1),\n+\t\t\t\t\t\t     (int)(hunk->end\n+\t\t\t\t\t\t\t   - hunk->start),\n+\t\t\t\t\t\t     plain + hunk->start);\n+\n+\t\t\t\toverlap_start = overlap_end;\n+\t\t\t\toverlap_end = overlap_next;\n+\t\t\t}\n+\t\t\tlen = overlap_end - overlap_start;\n+\n+\t\t\tif (len > merged->end - merged->start ||\n+\t\t\t    memcmp(plain + merged->end - len,\n+\t\t\t\t   plain + overlap_start, len))\n+\t\t\t\treturn error(_(\"hunks do not overlap:\\n%.*s\\n\"\n+\t\t\t\t\t       \"\\tdoes not end with:\\n%.*s\"),\n+\t\t\t\t\t     (int)(merged->end - merged->start),\n+\t\t\t\t\t     plain + merged->start,\n+\t\t\t\t\t     (int)len, plain + overlap_start);\n+\n+\t\t\t/*\n+\t\t\t * Since the start-end ranges are not adjacent, we\n+\t\t\t * cannot simply take the union of the ranges. To\n+\t\t\t * address that, we temporarily append the union of the\n+\t\t\t * lines to the `plain` strbuf.\n+\t\t\t */\n+\t\t\tif (merged->end != s->plain.len) {\n+\t\t\t\tsize_t start = s->plain.len;\n+\n+\t\t\t\tstrbuf_add(&s->plain, plain + merged->start,\n+\t\t\t\t\t   merged->end - merged->start);\n+\t\t\t\tplain = s->plain.buf;\n+\t\t\t\tmerged->start = start;\n+\t\t\t\tmerged->end = s->plain.len;\n+\t\t\t}\n+\n+\t\t\tstrbuf_add(&s->plain,\n+\t\t\t\t   plain + overlap_end,\n+\t\t\t\t   hunk->end - overlap_end);\n+\t\t\tmerged->end = s->plain.len;\n+\t\t\tmerged->splittable_into += hunk->splittable_into;\n+\t\t\tdelta = merged->delta;\n+\t\t\tmerged->delta += hunk->delta;\n+\t\t}\n \n \t\theader->old_count = next->old_offset + next->old_count\n \t\t\t- header->old_offset;\n-\t\theader->new_count = next->new_offset + next->new_count\n-\t\t\t- header->new_offset;\n+\t\theader->new_count = next->new_offset + delta\n+\t\t\t+ next->new_count - header->new_offset;\n \t}\n \n \tif (i == *hunk_index)\n@@ -483,10 +563,11 @@ static int merge_hunks(struct add_p_state *s, struct file_diff *file_diff,\n }\n \n static void reassemble_patch(struct add_p_state *s,\n-\t\t\t     struct file_diff *file_diff, struct strbuf *out)\n+\t\t\t     struct file_diff *file_diff, int use_all,\n+\t\t\t     struct strbuf *out)\n {\n \tstruct hunk *hunk;\n-\tsize_t i;\n+\tsize_t save_len = s->plain.len, i;\n \tssize_t delta = 0;\n \n \trender_diff_header(s, file_diff, 0, out);\n@@ -495,15 +576,24 @@ static void reassemble_patch(struct add_p_state *s,\n \t\tstruct hunk merged = { 0 };\n \n \t\thunk = file_diff->hunk + i;\n-\t\tif (hunk->use != USE_HUNK)\n+\t\tif (!use_all && hunk->use != USE_HUNK)\n \t\t\tdelta += hunk->header.old_count\n \t\t\t\t- hunk->header.new_count;\n \t\telse {\n \t\t\t/* merge overlapping hunks into a temporary hunk */\n-\t\t\tif (merge_hunks(s, file_diff, &i, &merged))\n+\t\t\tif (merge_hunks(s, file_diff, &i, use_all, &merged))\n \t\t\t\thunk = &merged;\n \n \t\t\trender_hunk(s, hunk, delta, 0, out);\n+\n+\t\t\t/*\n+\t\t\t * In case `merge_hunks()` used `plain` as a scratch\n+\t\t\t * pad (this happens when an edited hunk had to be\n+\t\t\t * coalesced with another hunk).\n+\t\t\t */\n+\t\t\tstrbuf_setlen(&s->plain, save_len);\n+\n+\t\t\tdelta += hunk->delta;\n \t\t}\n \t}\n }\n@@ -667,6 +757,204 @@ static int split_hunk(struct add_p_state *s, struct file_diff *file_diff,\n \treturn 0;\n }\n \n+static void recolor_hunk(struct add_p_state *s, struct hunk *hunk)\n+{\n+\tconst char *plain = s->plain.buf;\n+\tsize_t current, eol, next;\n+\n+\tif (!s->colored.len)\n+\t\treturn;\n+\n+\thunk->colored_start = s->colored.len;\n+\tfor (current = hunk->start; current < hunk->end; ) {\n+\t\tfor (eol = current; eol < hunk->end; eol++)\n+\t\t\tif (plain[eol] == '\\n')\n+\t\t\t\tbreak;\n+\t\tnext = eol + (eol < hunk->end);\n+\t\tif (eol > current && plain[eol - 1] == '\\r')\n+\t\t\teol--;\n+\n+\t\tstrbuf_addstr(&s->colored,\n+\t\t\t      plain[current] == '-' ?\n+\t\t\t      s->s.file_old_color :\n+\t\t\t      plain[current] == '+' ?\n+\t\t\t      s->s.file_new_color :\n+\t\t\t      s->s.context_color);\n+\t\tstrbuf_add(&s->colored, plain + current, eol - current);\n+\t\tstrbuf_addstr(&s->colored, GIT_COLOR_RESET);\n+\t\tif (next > eol)\n+\t\t\tstrbuf_add(&s->colored, plain + eol, next - eol);\n+\t\tcurrent = next;\n+\t}\n+\thunk->colored_end = s->colored.len;\n+}\n+\n+static int edit_hunk_manually(struct add_p_state *s, struct hunk *hunk)\n+{\n+\tsize_t i;\n+\n+\tstrbuf_reset(&s->buf);\n+\tstrbuf_commented_addf(&s->buf, _(\"Manual hunk edit mode -- see bottom for \"\n+\t\t\t\t      \"a quick guide.\\n\"));\n+\trender_hunk(s, hunk, 0, 0, &s->buf);\n+\tstrbuf_commented_addf(&s->buf,\n+\t\t\t      _(\"---\\n\"\n+\t\t\t\t\"To remove '%c' lines, make them ' ' lines \"\n+\t\t\t\t\"(context).\\n\"\n+\t\t\t\t\"To remove '%c' lines, delete them.\\n\"\n+\t\t\t\t\"Lines starting with %c will be removed.\\n\"),\n+\t\t\t      '-', '+', comment_line_char);\n+\tstrbuf_commented_addf(&s->buf,\n+\t\t\t      _(\"If the patch applies cleanly, the edited hunk \"\n+\t\t\t\t\"will immediately be\\n\"\n+\t\t\t\t\"marked for staging.\\n\"));\n+\t/*\n+\t * TRANSLATORS: 'it' refers to the patch mentioned in the previous\n+\t * messages.\n+\t */\n+\tstrbuf_commented_addf(&s->buf,\n+\t\t\t      _(\"If it does not apply cleanly, you will be \"\n+\t\t\t\t\"given an opportunity to\\n\"\n+\t\t\t\t\"edit again.  If all lines of the hunk are \"\n+\t\t\t\t\"removed, then the edit is\\n\"\n+\t\t\t\t\"aborted and the hunk is left unchanged.\\n\"));\n+\n+\tif (strbuf_edit_interactively(&s->buf, \"addp-hunk-edit.diff\", NULL) < 0)\n+\t\treturn -1;\n+\n+\t/* strip out commented lines */\n+\thunk->start = s->plain.len;\n+\tfor (i = 0; i < s->buf.len; ) {\n+\t\tsize_t next = find_next_line(&s->buf, i);\n+\n+\t\tif (s->buf.buf[i] != comment_line_char)\n+\t\t\tstrbuf_add(&s->plain, s->buf.buf + i, next - i);\n+\t\ti = next;\n+\t}\n+\n+\thunk->end = s->plain.len;\n+\tif (hunk->end == hunk->start)\n+\t\t/* The user aborted editing by deleting everything */\n+\t\treturn 0;\n+\n+\trecolor_hunk(s, hunk);\n+\n+\t/*\n+\t * If the hunk header is intact, parse it, otherwise simply use the\n+\t * hunk header prior to editing (which will adjust `hunk->start` to\n+\t * skip the hunk header).\n+\t */\n+\tif (s->plain.buf[hunk->start] == '@' &&\n+\t    parse_hunk_header(s, hunk) < 0)\n+\t\treturn error(_(\"could not parse hunk header\"));\n+\n+\treturn 1;\n+}\n+\n+static ssize_t recount_edited_hunk(struct add_p_state *s, struct hunk *hunk,\n+\t\t\t\t   size_t orig_old_count, size_t orig_new_count)\n+{\n+\tstruct hunk_header *header = &hunk->header;\n+\tsize_t i;\n+\n+\theader->old_count = header->new_count = 0;\n+\tfor (i = hunk->start; i < hunk->end; ) {\n+\t\tswitch (s->plain.buf[i]) {\n+\t\tcase '-':\n+\t\t\theader->old_count++;\n+\t\t\tbreak;\n+\t\tcase '+':\n+\t\t\theader->new_count++;\n+\t\t\tbreak;\n+\t\tcase ' ': case '\\r': case '\\n':\n+\t\t\theader->old_count++;\n+\t\t\theader->new_count++;\n+\t\t\tbreak;\n+\t\t}\n+\n+\t\ti = find_next_line(&s->plain, i);\n+\t}\n+\n+\treturn orig_old_count - orig_new_count\n+\t\t- header->old_count + header->new_count;\n+}\n+\n+static int run_apply_check(struct add_p_state *s,\n+\t\t\t   struct file_diff *file_diff)\n+{\n+\tstruct child_process cp = CHILD_PROCESS_INIT;\n+\n+\tstrbuf_reset(&s->buf);\n+\treassemble_patch(s, file_diff, 1, &s->buf);\n+\n+\tsetup_child_process(s, &cp,\n+\t\t\t    \"apply\", \"--cached\", \"--check\", NULL);\n+\tif (pipe_command(&cp, s->buf.buf, s->buf.len, NULL, 0, NULL, 0))\n+\t\treturn error(_(\"'git apply --cached' failed\"));\n+\n+\treturn 0;\n+}\n+\n+static int prompt_yesno(struct add_p_state *s, const char *prompt)\n+{\n+\tfor (;;) {\n+\t\tcolor_fprintf(stdout, s->s.prompt_color, \"%s\", _(prompt));\n+\t\tfflush(stdout);\n+\t\tif (strbuf_getline(&s->answer, stdin) == EOF)\n+\t\t\treturn -1;\n+\t\tstrbuf_trim_trailing_newline(&s->answer);\n+\t\tswitch (tolower(s->answer.buf[0])) {\n+\t\tcase 'n': return 0;\n+\t\tcase 'y': return 1;\n+\t\t}\n+\t}\n+}\n+\n+static int edit_hunk_loop(struct add_p_state *s,\n+\t\t\t  struct file_diff *file_diff, struct hunk *hunk)\n+{\n+\tsize_t plain_len = s->plain.len, colored_len = s->colored.len;\n+\tstruct hunk backup;\n+\n+\tbackup = *hunk;\n+\n+\tfor (;;) {\n+\t\tint res = edit_hunk_manually(s, hunk);\n+\t\tif (res == 0) {\n+\t\t\t/* abandonded */\n+\t\t\t*hunk = backup;\n+\t\t\treturn -1;\n+\t\t}\n+\n+\t\tif (res > 0) {\n+\t\t\thunk->delta +=\n+\t\t\t\trecount_edited_hunk(s, hunk,\n+\t\t\t\t\t\t    backup.header.old_count,\n+\t\t\t\t\t\t    backup.header.new_count);\n+\t\t\tif (!run_apply_check(s, file_diff))\n+\t\t\t\treturn 0;\n+\t\t}\n+\n+\t\t/* Drop edits (they were appended to s->plain) */\n+\t\tstrbuf_setlen(&s->plain, plain_len);\n+\t\tstrbuf_setlen(&s->colored, colored_len);\n+\t\t*hunk = backup;\n+\n+\t\t/*\n+\t\t * TRANSLATORS: do not translate [y/n]\n+\t\t * The program will only accept that input at this point.\n+\t\t * Consider translating (saying \"no\" discards!) as\n+\t\t * (saying \"n\" for \"no\" discards!) if the translation\n+\t\t * of the word \"no\" does not start with n.\n+\t\t */\n+\t\tres = prompt_yesno(s, _(\"Your edited hunk does not apply. \"\n+\t\t\t\t\t\"Edit again (saying \\\"no\\\" discards!) \"\n+\t\t\t\t\t\"[y/n]? \"));\n+\t\tif (res < 1)\n+\t\t\treturn -1;\n+\t}\n+}\n+\n static const char help_patch_text[] =\n N_(\"y - stage this hunk\\n\"\n    \"n - do not stage this hunk\\n\"\n@@ -677,6 +965,7 @@ N_(\"y - stage this hunk\\n\"\n    \"k - leave this hunk undecided, see previous undecided hunk\\n\"\n    \"K - leave this hunk undecided, see previous hunk\\n\"\n    \"s - split the current hunk into smaller hunks\\n\"\n+   \"e - manually edit the current hunk\\n\"\n    \"? - print help\\n\");\n \n static int patch_update_file(struct add_p_state *s,\n@@ -735,6 +1024,9 @@ static int patch_update_file(struct add_p_state *s,\n \t\t\tstrbuf_addstr(&s->buf, \",J\");\n \t\tif (hunk->splittable_into > 1)\n \t\t\tstrbuf_addstr(&s->buf, \",s\");\n+\t\tif (hunk_index + 1 > file_diff->mode_change &&\n+\t\t    !file_diff->deleted)\n+\t\t\tstrbuf_addstr(&s->buf, \",e\");\n \n \t\tif (file_diff->deleted)\n \t\t\tprompt_mode_type = PROMPT_DELETION;\n@@ -806,6 +1098,13 @@ static int patch_update_file(struct add_p_state *s,\n \t\t\t\tcolor_fprintf_ln(stdout, s->s.header_color,\n \t\t\t\t\t\t _(\"Split into %d hunks.\"),\n \t\t\t\t\t\t (int)splittable_into);\n+\t\t} else if (s->answer.buf[0] == 'e') {\n+\t\t\tif (hunk_index + 1 == file_diff->mode_change)\n+\t\t\t\terr(s, _(\"Sorry, cannot edit this hunk\"));\n+\t\t\telse if (edit_hunk_loop(s, file_diff, hunk) >= 0) {\n+\t\t\t\thunk->use = USE_HUNK;\n+\t\t\t\tgoto soft_increment;\n+\t\t\t}\n \t\t} else\n \t\t\tcolor_fprintf(stdout, s->s.help_color,\n \t\t\t\t      _(help_patch_text));\n@@ -819,7 +1118,7 @@ static int patch_update_file(struct add_p_state *s,\n \tif (i < file_diff->hunk_nr) {\n \t\t/* At least one hunk selected: apply */\n \t\tstrbuf_reset(&s->buf);\n-\t\treassemble_patch(s, file_diff, &s->buf);\n+\t\treassemble_patch(s, file_diff, 0, &s->buf);\n \n \t\tdiscard_index(s->s.r->index);\n \t\tsetup_child_process(s, &cp, \"apply\", \"--cached\", NULL);\n-- \ngitgitgadget\n\n"},{"id":"388094","messageId":"2cd9855fffe97ce31640dfa5ed22bffa98047403.1576224486.git.gitgitgadget@gmail.com","threadId":"52445","inReplyTo":"pull.173.git.1576224486.gitgitgadget@gmail.com","subject":"[PATCH 11/19] built-in add -p: implement the hunk splitting feature","fromName":"Johannes Schindelin via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2019-12-13T08:07:58Z","receivedAt":"2019-12-13T08:08:29Z","isPatch":true,"sender":{"key":"johannes.schindelin@gmx.de","avatar":"https://avatars.githubusercontent.com/u/127790?v=4"},"body":"From: Johannes Schindelin <johannes.schindelin@gmx.de>\n\nIf this developer's workflow is any indication, then this is *the* most\nuseful feature of Git's interactive `add `command.\n\nNote: once again, this is not a verbatim conversion from the Perl code\nto C: the `hunk_splittable()` function, for example, essentially did all\nthe work of splitting the hunk, just to find out whether more than one\nhunk would have been the result (and then tossed that result into the\ntrash). In C we instead count the number of resulting hunks (without\nactually doing the work of splitting, but just counting the transitions\nfrom non-context lines to context lines), and store that information\nwith the hunk, and we do that *while* parsing the diff in the first\nplace.\n\nAnother deviation: the built-in `git add -p` was designed with a single\nstrbuf holding the diff (and another one holding the colored diff, if\nthat one was asked for) in mind, and hunks essentially store just the\nstart and end offsets pointing into that strbuf. As a consequence, when\nwe split hunks, we now use a special mode where the hunk header is\ngenerated dynamically, and only the rest of the hunk is stored using\nsuch start/end offsets. This way, we also avoid the frequent\nformatting/re-parsing of the hunk header of the Perl version.\n\nSigned-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n---\n add-patch.c                | 215 ++++++++++++++++++++++++++++++++++++-\n t/t3701-add-interactive.sh |  12 +++\n 2 files changed, 225 insertions(+), 2 deletions(-)\n\ndiff --git a/add-patch.c b/add-patch.c\nindex 171025b08d..2d34ddd7f4 100644\n--- a/add-patch.c\n+++ b/add-patch.c\n@@ -28,7 +28,7 @@ struct hunk_header {\n };\n \n struct hunk {\n-\tsize_t start, end, colored_start, colored_end;\n+\tsize_t start, end, colored_start, colored_end, splittable_into;\n \tenum { UNDECIDED_HUNK = 0, SKIP_HUNK, USE_HUNK } use;\n \tstruct hunk_header header;\n };\n@@ -155,7 +155,7 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps)\n \tstruct argv_array args = ARGV_ARRAY_INIT;\n \tstruct strbuf *plain = &s->plain, *colored = NULL;\n \tstruct child_process cp = CHILD_PROCESS_INIT;\n-\tchar *p, *pend, *colored_p = NULL, *colored_pend = NULL;\n+\tchar *p, *pend, *colored_p = NULL, *colored_pend = NULL, marker = '\\0';\n \tsize_t file_diff_alloc = 0, i, color_arg_index;\n \tstruct file_diff *file_diff = NULL;\n \tstruct hunk *hunk = NULL;\n@@ -217,6 +217,7 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps)\n \t\t\thunk->start = p - plain->buf;\n \t\t\tif (colored_p)\n \t\t\t\thunk->colored_start = colored_p - colored->buf;\n+\t\t\tmarker = '\\0';\n \t\t} else if (p == plain->buf)\n \t\t\tBUG(\"diff starts with unexpected line:\\n\"\n \t\t\t    \"%.*s\\n\", (int)(eol - p), p);\n@@ -225,6 +226,13 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps)\n \t\telse if (starts_with(p, \"@@ \") ||\n \t\t\t (hunk == &file_diff->head &&\n \t\t\t  skip_prefix(p, \"deleted file\", &deleted))) {\n+\t\t\tif (marker == '-' || marker == '+')\n+\t\t\t\t/*\n+\t\t\t\t * Should not happen; previous hunk did not end\n+\t\t\t\t * in a context line? Handle it anyway.\n+\t\t\t\t */\n+\t\t\t\thunk->splittable_into++;\n+\n \t\t\tfile_diff->hunk_nr++;\n \t\t\tALLOC_GROW(file_diff->hunk, file_diff->hunk_nr,\n \t\t\t\t   file_diff->hunk_alloc);\n@@ -239,6 +247,12 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps)\n \t\t\t\tfile_diff->deleted = 1;\n \t\t\telse if (parse_hunk_header(s, hunk) < 0)\n \t\t\t\treturn -1;\n+\n+\t\t\t/*\n+\t\t\t * Start counting into how many hunks this one can be\n+\t\t\t * split\n+\t\t\t */\n+\t\t\tmarker = *p;\n \t\t} else if (hunk == &file_diff->head &&\n \t\t\t   skip_prefix(p, \"old mode \", &mode_change) &&\n \t\t\t   is_octal(mode_change, eol - mode_change)) {\n@@ -286,6 +300,11 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps)\n \t\t\t    (int)(eol - (plain->buf + file_diff->head.start)),\n \t\t\t    plain->buf + file_diff->head.start);\n \n+\t\tif ((marker == '-' || marker == '+') && *p == ' ')\n+\t\t\thunk->splittable_into++;\n+\t\tif (marker && *p != '\\\\')\n+\t\t\tmarker = *p;\n+\n \t\tp = eol == pend ? pend : eol + 1;\n \t\thunk->end = p - plain->buf;\n \n@@ -311,9 +330,30 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps)\n \t\t}\n \t}\n \n+\tif (marker == '-' || marker == '+')\n+\t\t/*\n+\t\t * Last hunk ended in non-context line (i.e. it appended lines\n+\t\t * to the file, so there are no trailing context lines).\n+\t\t */\n+\t\thunk->splittable_into++;\n+\n \treturn 0;\n }\n \n+static size_t find_next_line(struct strbuf *sb, size_t offset)\n+{\n+\tchar *eol;\n+\n+\tif (offset >= sb->len)\n+\t\tBUG(\"looking for next line beyond buffer (%d >= %d)\\n%s\",\n+\t\t    (int)offset, (int)sb->len, sb->buf);\n+\n+\teol = memchr(sb->buf + offset, '\\n', sb->len - offset);\n+\tif (!eol)\n+\t\treturn sb->len;\n+\treturn eol - sb->buf + 1;\n+}\n+\n static void render_hunk(struct add_p_state *s, struct hunk *hunk,\n \t\t\tssize_t delta, int colored, struct strbuf *out)\n {\n@@ -412,6 +452,165 @@ static void reassemble_patch(struct add_p_state *s,\n \t}\n }\n \n+static int split_hunk(struct add_p_state *s, struct file_diff *file_diff,\n+\t\t       size_t hunk_index)\n+{\n+\tint colored = !!s->colored.len, first = 1;\n+\tstruct hunk *hunk = file_diff->hunk + hunk_index;\n+\tsize_t splittable_into;\n+\tsize_t end, colored_end, current, colored_current = 0, context_line_count;\n+\tstruct hunk_header remaining, *header;\n+\tchar marker, ch;\n+\n+\tif (hunk_index >= file_diff->hunk_nr)\n+\t\tBUG(\"invalid hunk index: %d (must be >= 0 and < %d)\",\n+\t\t    (int)hunk_index, (int)file_diff->hunk_nr);\n+\n+\tif (hunk->splittable_into < 2)\n+\t\treturn 0;\n+\tsplittable_into = hunk->splittable_into;\n+\n+\tend = hunk->end;\n+\tcolored_end = hunk->colored_end;\n+\n+\tremaining = hunk->header;\n+\n+\tfile_diff->hunk_nr += splittable_into - 1;\n+\tALLOC_GROW(file_diff->hunk, file_diff->hunk_nr, file_diff->hunk_alloc);\n+\tif (hunk_index + splittable_into < file_diff->hunk_nr)\n+\t\tmemmove(file_diff->hunk + hunk_index + splittable_into,\n+\t\t\tfile_diff->hunk + hunk_index + 1,\n+\t\t\t(file_diff->hunk_nr - hunk_index - splittable_into)\n+\t\t\t* sizeof(*hunk));\n+\thunk = file_diff->hunk + hunk_index;\n+\thunk->splittable_into = 1;\n+\tmemset(hunk + 1, 0, (splittable_into - 1) * sizeof(*hunk));\n+\n+\theader = &hunk->header;\n+\theader->old_count = header->new_count = 0;\n+\n+\tcurrent = hunk->start;\n+\tif (colored)\n+\t\tcolored_current = hunk->colored_start;\n+\tmarker = '\\0';\n+\tcontext_line_count = 0;\n+\n+\twhile (splittable_into > 1) {\n+\t\tch = s->plain.buf[current];\n+\n+\t\tif (!ch)\n+\t\t\tBUG(\"buffer overrun while splitting hunks\");\n+\n+\t\t/*\n+\t\t * Is this the first context line after a chain of +/- lines?\n+\t\t * Then record the start of the next split hunk.\n+\t\t */\n+\t\tif ((marker == '-' || marker == '+') && ch == ' ') {\n+\t\t\tfirst = 0;\n+\t\t\thunk[1].start = current;\n+\t\t\tif (colored)\n+\t\t\t\thunk[1].colored_start = colored_current;\n+\t\t\tcontext_line_count = 0;\n+\t\t}\n+\n+\t\t/*\n+\t\t * Was the previous line a +/- one? Alternatively, is this the\n+\t\t * first line (and not a +/- one)?\n+\t\t *\n+\t\t * Then just increment the appropriate counter and continue\n+\t\t * with the next line.\n+\t\t */\n+\t\tif (marker != ' ' || (ch != '-' && ch != '+')) {\n+next_hunk_line:\n+\t\t\t/* Comment lines are attached to the previous line */\n+\t\t\tif (ch == '\\\\')\n+\t\t\t\tch = marker ? marker : ' ';\n+\n+\t\t\t/* current hunk not done yet */\n+\t\t\tif (ch == ' ')\n+\t\t\t\tcontext_line_count++;\n+\t\t\telse if (ch == '-')\n+\t\t\t\theader->old_count++;\n+\t\t\telse if (ch == '+')\n+\t\t\t\theader->new_count++;\n+\t\t\telse\n+\t\t\t\tBUG(\"unhandled diff marker: '%c'\", ch);\n+\t\t\tmarker = ch;\n+\t\t\tcurrent = find_next_line(&s->plain, current);\n+\t\t\tif (colored)\n+\t\t\t\tcolored_current =\n+\t\t\t\t\tfind_next_line(&s->colored,\n+\t\t\t\t\t\t       colored_current);\n+\t\t\tcontinue;\n+\t\t}\n+\n+\t\t/*\n+\t\t * We got us the start of a new hunk!\n+\t\t *\n+\t\t * This is a context line, so it is shared with the previous\n+\t\t * hunk, if any.\n+\t\t */\n+\n+\t\tif (first) {\n+\t\t\tif (header->old_count || header->new_count)\n+\t\t\t\tBUG(\"counts are off: %d/%d\",\n+\t\t\t\t    (int)header->old_count,\n+\t\t\t\t    (int)header->new_count);\n+\n+\t\t\theader->old_count = context_line_count;\n+\t\t\theader->new_count = context_line_count;\n+\t\t\tcontext_line_count = 0;\n+\t\t\tfirst = 0;\n+\t\t\tgoto next_hunk_line;\n+\t\t}\n+\n+\t\tremaining.old_offset += header->old_count;\n+\t\tremaining.old_count -= header->old_count;\n+\t\tremaining.new_offset += header->new_count;\n+\t\tremaining.new_count -= header->new_count;\n+\n+\t\t/* initialize next hunk header's offsets */\n+\t\thunk[1].header.old_offset =\n+\t\t\theader->old_offset + header->old_count;\n+\t\thunk[1].header.new_offset =\n+\t\t\theader->new_offset + header->new_count;\n+\n+\t\t/* add one split hunk */\n+\t\theader->old_count += context_line_count;\n+\t\theader->new_count += context_line_count;\n+\n+\t\thunk->end = current;\n+\t\tif (colored)\n+\t\t\thunk->colored_end = colored_current;\n+\n+\t\thunk++;\n+\t\thunk->splittable_into = 1;\n+\t\thunk->use = hunk[-1].use;\n+\t\theader = &hunk->header;\n+\n+\t\theader->old_count = header->new_count = context_line_count;\n+\t\tcontext_line_count = 0;\n+\n+\t\tsplittable_into--;\n+\t\tmarker = ch;\n+\t}\n+\n+\t/* last hunk simply gets the rest */\n+\tif (header->old_offset != remaining.old_offset)\n+\t\tBUG(\"miscounted old_offset: %lu != %lu\",\n+\t\t    header->old_offset, remaining.old_offset);\n+\tif (header->new_offset != remaining.new_offset)\n+\t\tBUG(\"miscounted new_offset: %lu != %lu\",\n+\t\t    header->new_offset, remaining.new_offset);\n+\theader->old_count = remaining.old_count;\n+\theader->new_count = remaining.new_count;\n+\thunk->end = end;\n+\tif (colored)\n+\t\thunk->colored_end = colored_end;\n+\n+\treturn 0;\n+}\n+\n static const char help_patch_text[] =\n N_(\"y - stage this hunk\\n\"\n    \"n - do not stage this hunk\\n\"\n@@ -421,6 +620,7 @@ N_(\"y - stage this hunk\\n\"\n    \"J - leave this hunk undecided, see next hunk\\n\"\n    \"k - leave this hunk undecided, see previous undecided hunk\\n\"\n    \"K - leave this hunk undecided, see previous hunk\\n\"\n+   \"s - split the current hunk into smaller hunks\\n\"\n    \"? - print help\\n\");\n \n static int patch_update_file(struct add_p_state *s,\n@@ -477,6 +677,8 @@ static int patch_update_file(struct add_p_state *s,\n \t\t\tstrbuf_addstr(&s->buf, \",j\");\n \t\tif (hunk_index + 1 < file_diff->hunk_nr)\n \t\t\tstrbuf_addstr(&s->buf, \",J\");\n+\t\tif (hunk->splittable_into > 1)\n+\t\t\tstrbuf_addstr(&s->buf, \",s\");\n \n \t\tif (file_diff->deleted)\n \t\t\tprompt_mode_type = PROMPT_DELETION;\n@@ -539,6 +741,15 @@ static int patch_update_file(struct add_p_state *s,\n \t\t\t\thunk_index = undecided_next;\n \t\t\telse\n \t\t\t\terr(s, _(\"No next hunk\"));\n+\t\t} else if (s->answer.buf[0] == 's') {\n+\t\t\tsize_t splittable_into = hunk->splittable_into;\n+\t\t\tif (splittable_into < 2)\n+\t\t\t\terr(s, _(\"Sorry, cannot split this hunk\"));\n+\t\t\telse if (!split_hunk(s, file_diff,\n+\t\t\t\t\t     hunk - file_diff->hunk))\n+\t\t\t\tcolor_fprintf_ln(stdout, s->s.header_color,\n+\t\t\t\t\t\t _(\"Split into %d hunks.\"),\n+\t\t\t\t\t\t (int)splittable_into);\n \t\t} else\n \t\t\tcolor_fprintf(stdout, s->s.help_color,\n \t\t\t\t      _(help_patch_text));\ndiff --git a/t/t3701-add-interactive.sh b/t/t3701-add-interactive.sh\nindex 5db6432e33..fe383be50e 100755\n--- a/t/t3701-add-interactive.sh\n+++ b/t/t3701-add-interactive.sh\n@@ -442,6 +442,18 @@ test_expect_failure 'split hunk \"add -p (no, yes, edit)\"' '\n \t! grep \"^+31\" actual\n '\n \n+test_expect_success 'split hunk with incomplete line at end' '\n+\tgit reset --hard &&\n+\tprintf \"missing LF\" >>test &&\n+\tgit add test &&\n+\ttest_write_lines before 10 20 30 40 50 60 70 >test &&\n+\tgit grep --cached missing &&\n+\ttest_write_lines s n y q | git add -p &&\n+\ttest_must_fail git grep --cached missing &&\n+\tgit grep before &&\n+\ttest_must_fail git grep --cached before\n+'\n+\n test_expect_failure 'edit, adding lines to the first hunk' '\n \ttest_write_lines 10 11 20 30 40 50 51 60 >test &&\n \tgit reset &&\n-- \ngitgitgadget\n\n"},{"id":"388096","messageId":"a5bbd28d173bf84adf4f431abfd314633c805f92.1576224486.git.gitgitgadget@gmail.com","threadId":"52445","inReplyTo":"pull.173.git.1576224486.gitgitgadget@gmail.com","subject":"[PATCH 19/19] built-in add -p: show helpful hint when nothing can be staged","fromName":"Johannes Schindelin via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2019-12-13T08:08:06Z","receivedAt":"2019-12-13T08:08:31Z","isPatch":true,"sender":{"key":"johannes.schindelin@gmx.de","avatar":"https://avatars.githubusercontent.com/u/127790?v=4"},"body":"From: Johannes Schindelin <johannes.schindelin@gmx.de>\n\nThis patch will make `git add -p` show \"No changes.\" or \"Only binary\nfiles changed.\" in that case.\n\nSigned-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n---\n add-patch.c | 17 +++++++++++++----\n 1 file changed, 13 insertions(+), 4 deletions(-)\n\ndiff --git a/add-patch.c b/add-patch.c\nindex 1eb0ab97bb..2c46fe5b33 100644\n--- a/add-patch.c\n+++ b/add-patch.c\n@@ -44,7 +44,7 @@ struct add_p_state {\n \t\tstruct hunk head;\n \t\tstruct hunk *hunk;\n \t\tsize_t hunk_nr, hunk_alloc;\n-\t\tunsigned deleted:1, mode_change:1;\n+\t\tunsigned deleted:1, mode_change:1,binary:1;\n \t} *file_diff;\n \tsize_t file_diff_nr;\n };\n@@ -294,7 +294,9 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps)\n \t\t\t\tBUG(\"'new mode' does not immediately follow \"\n \t\t\t\t    \"'old mode'?\\n\\n%.*s\",\n \t\t\t\t    (int)(eol - plain->buf), plain->buf);\n-\t\t}\n+\t\t} else if (hunk == &file_diff->head &&\n+\t\t\t   starts_with(p, \"Binary files \"))\n+\t\t\tfile_diff->binary = 1;\n \n \t\tif (file_diff->deleted && file_diff->mode_change)\n \t\t\tBUG(\"diff contains delete *and* a mode change?!?\\n%.*s\",\n@@ -1304,7 +1306,7 @@ int run_add_p(struct repository *r, const struct pathspec *ps)\n \tstruct add_p_state s = {\n \t\t{ r }, STRBUF_INIT, STRBUF_INIT, STRBUF_INIT, STRBUF_INIT\n \t};\n-\tsize_t i;\n+\tsize_t i, binary_count = 0;\n \n \tinit_add_i_state(&s.s, r);\n \n@@ -1318,9 +1320,16 @@ int run_add_p(struct repository *r, const struct pathspec *ps)\n \t}\n \n \tfor (i = 0; i < s.file_diff_nr; i++)\n-\t\tif (patch_update_file(&s, s.file_diff + i))\n+\t\tif (s.file_diff[i].binary && !s.file_diff[i].hunk_nr)\n+\t\t\tbinary_count++;\n+\t\telse if (patch_update_file(&s, s.file_diff + i))\n \t\t\tbreak;\n \n+\tif (s.file_diff_nr == 0)\n+\t\tfprintf(stderr, _(\"No changes.\\n\"));\n+\telse if (binary_count == s.file_diff_nr)\n+\t\tfprintf(stderr, _(\"Only binary files changed.\\n\"));\n+\n \tstrbuf_release(&s.answer);\n \tstrbuf_release(&s.buf);\n \tstrbuf_release(&s.plain);\n-- \ngitgitgadget\n"},{"id":"388098","messageId":"0c95067fb3aba3c13afe0976d52cb092903d3a9f.1576224486.git.gitgitgadget@gmail.com","threadId":"52445","inReplyTo":"pull.173.git.1576224486.gitgitgadget@gmail.com","subject":"[PATCH 18/19] built-in add -p: only show the applicable parts of the help text","fromName":"Johannes Schindelin via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2019-12-13T08:08:05Z","receivedAt":"2019-12-13T08:08:32Z","isPatch":true,"sender":{"key":"johannes.schindelin@gmx.de","avatar":"https://avatars.githubusercontent.com/u/127790?v=4"},"body":"From: Johannes Schindelin <johannes.schindelin@gmx.de>\n\nWhen displaying the only hunk in a file's diff, the prompt already\nexcludes the commands to navigate to the previous/next hunk.\n\nLet's also let the `?` command show only the help lines corresponding to\nthe commands that are displayed in the prompt.\n\nSigned-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n---\n add-patch.c | 32 ++++++++++++++++++++++++++++----\n 1 file changed, 28 insertions(+), 4 deletions(-)\n\ndiff --git a/add-patch.c b/add-patch.c\nindex 5e9829a8b4..1eb0ab97bb 100644\n--- a/add-patch.c\n+++ b/add-patch.c\n@@ -1008,8 +1008,10 @@ N_(\"y - stage this hunk\\n\"\n    \"n - do not stage this hunk\\n\"\n    \"q - quit; do not stage this hunk or any of the remaining ones\\n\"\n    \"a - stage this and all the remaining hunks\\n\"\n-   \"d - do not stage this hunk nor any of the remaining hunks\\n\"\n-   \"j - leave this hunk undecided, see next undecided hunk\\n\"\n+   \"d - do not stage this hunk nor any of the remaining hunks\\n\");\n+\n+static const char help_patch_remainder[] =\n+N_(\"j - leave this hunk undecided, see next undecided hunk\\n\"\n    \"J - leave this hunk undecided, see next hunk\\n\"\n    \"k - leave this hunk undecided, see previous undecided hunk\\n\"\n    \"K - leave this hunk undecided, see previous hunk\\n\"\n@@ -1246,9 +1248,31 @@ static int patch_update_file(struct add_p_state *s,\n \t\t\t\thunk->use = USE_HUNK;\n \t\t\t\tgoto soft_increment;\n \t\t\t}\n-\t\t} else\n-\t\t\tcolor_fprintf(stdout, s->s.help_color,\n+\t\t} else {\n+\t\t\tconst char *p = _(help_patch_remainder), *eol = p;\n+\n+\t\t\tcolor_fprintf(stdout, s->s.help_color, \"%s\",\n \t\t\t\t      _(help_patch_text));\n+\n+\t\t\t/*\n+\t\t\t * Show only those lines of the remainder that are\n+\t\t\t * actually applicable with the current hunk.\n+\t\t\t */\n+\t\t\tfor (; *p; p = eol + (*eol == '\\n')) {\n+\t\t\t\teol = strchrnul(p, '\\n');\n+\n+\t\t\t\t/*\n+\t\t\t\t * `s->buf` still contains the part of the\n+\t\t\t\t * commands shown in the prompt that are not\n+\t\t\t\t * always available.\n+\t\t\t\t */\n+\t\t\t\tif (*p != '?' && !strchr(s->buf.buf, *p))\n+\t\t\t\t\tcontinue;\n+\n+\t\t\t\tcolor_fprintf_ln(stdout, s->s.help_color,\n+\t\t\t\t\t\t \"%.*s\", (int)(eol - p), p);\n+\t\t\t}\n+\t\t}\n \t}\n \n \t/* Any hunk to be used? */\n-- \ngitgitgadget\n\n"},{"id":"388095","messageId":"e7d218793b506d7e9116d9f3a3fabf220191bc72.1576224486.git.gitgitgadget@gmail.com","threadId":"52445","inReplyTo":"pull.173.git.1576224486.gitgitgadget@gmail.com","subject":"[PATCH 16/19] built-in add -p: implement the '/' (\"search regex\") command","fromName":"Johannes Schindelin via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2019-12-13T08:08:03Z","receivedAt":"2019-12-13T08:08:38Z","isPatch":true,"sender":{"key":"johannes.schindelin@gmx.de","avatar":"https://avatars.githubusercontent.com/u/127790?v=4"},"body":"From: Johannes Schindelin <johannes.schindelin@gmx.de>\n\nThis patch implements the hunk searching feature in the C version of\n`git add -p`.\n\nA test is added to verify that this behavior matches the one of the Perl\nversion of `git add -p`.\n\nNote that this involves a change of behavior: the Perl version uses (of\ncourse) the Perl flavor of regular expressions, while this patch uses\nthe regcomp()/regexec(), i.e. POSIX extended regular expressions. In\npractice, this behavior change is unlikely to matter.\n\nSigned-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n---\n add-patch.c                | 50 +++++++++++++++++++++++++++++++++++++-\n t/t3701-add-interactive.sh | 14 +++++++++++\n 2 files changed, 63 insertions(+), 1 deletion(-)\n\ndiff --git a/add-patch.c b/add-patch.c\nindex fdbb1e3e22..fd72850c65 100644\n--- a/add-patch.c\n+++ b/add-patch.c\n@@ -1013,6 +1013,7 @@ N_(\"y - stage this hunk\\n\"\n    \"k - leave this hunk undecided, see previous undecided hunk\\n\"\n    \"K - leave this hunk undecided, see previous hunk\\n\"\n    \"g - select a hunk to go to\\n\"\n+   \"/ - search for a hunk matching the given regex\\n\"\n    \"s - split the current hunk into smaller hunks\\n\"\n    \"e - manually edit the current hunk\\n\"\n    \"? - print help\\n\");\n@@ -1072,7 +1073,7 @@ static int patch_update_file(struct add_p_state *s,\n \t\tif (hunk_index + 1 < file_diff->hunk_nr)\n \t\t\tstrbuf_addstr(&s->buf, \",J\");\n \t\tif (file_diff->hunk_nr > 1)\n-\t\t\tstrbuf_addstr(&s->buf, \",g\");\n+\t\t\tstrbuf_addstr(&s->buf, \",g,/\");\n \t\tif (hunk->splittable_into > 1)\n \t\t\tstrbuf_addstr(&s->buf, \",s\");\n \t\tif (hunk_index + 1 > file_diff->mode_change &&\n@@ -1177,6 +1178,53 @@ static int patch_update_file(struct add_p_state *s,\n \t\t\t\t\t  \"Sorry, only %d hunks available.\",\n \t\t\t\t\t  file_diff->hunk_nr),\n \t\t\t\t    (int)file_diff->hunk_nr);\n+\t\t} else if (s->answer.buf[0] == '/') {\n+\t\t\tregex_t regex;\n+\t\t\tint ret;\n+\n+\t\t\tif (file_diff->hunk_nr < 2) {\n+\t\t\t\terr(s, _(\"No other hunks to search\"));\n+\t\t\t\tcontinue;\n+\t\t\t}\n+\t\t\tstrbuf_remove(&s->answer, 0, 1);\n+\t\t\tstrbuf_trim_trailing_newline(&s->answer);\n+\t\t\tif (s->answer.len == 0) {\n+\t\t\t\tprintf(\"%s\", _(\"search for regex? \"));\n+\t\t\t\tfflush(stdout);\n+\t\t\t\tif (strbuf_getline(&s->answer,\n+\t\t\t\t\t\t   stdin) == EOF)\n+\t\t\t\t\tbreak;\n+\t\t\t\tstrbuf_trim_trailing_newline(&s->answer);\n+\t\t\t\tif (s->answer.len == 0)\n+\t\t\t\t\tcontinue;\n+\t\t\t}\n+\t\t\tret = regcomp(&regex, s->answer.buf,\n+\t\t\t\t      REG_EXTENDED | REG_NOSUB | REG_NEWLINE);\n+\t\t\tif (ret) {\n+\t\t\t\tchar errbuf[1024];\n+\n+\t\t\t\tregerror(ret, &regex, errbuf, sizeof(errbuf));\n+\t\t\t\terr(s, _(\"Malformed search regexp %s: %s\"),\n+\t\t\t\t    s->answer.buf, errbuf);\n+\t\t\t\tcontinue;\n+\t\t\t}\n+\t\t\ti = hunk_index;\n+\t\t\tfor (;;) {\n+\t\t\t\t/* render the hunk into a scratch buffer */\n+\t\t\t\trender_hunk(s, file_diff->hunk + i, 0, 0,\n+\t\t\t\t\t    &s->buf);\n+\t\t\t\tif (regexec(&regex, s->buf.buf, 0, NULL, 0)\n+\t\t\t\t    != REG_NOMATCH)\n+\t\t\t\t\tbreak;\n+\t\t\t\ti++;\n+\t\t\t\tif (i == file_diff->hunk_nr)\n+\t\t\t\t\ti = 0;\n+\t\t\t\tif (i != hunk_index)\n+\t\t\t\t\tcontinue;\n+\t\t\t\terr(s, _(\"No hunk matches the given pattern\"));\n+\t\t\t\tbreak;\n+\t\t\t}\n+\t\t\thunk_index = i;\n \t\t} else if (s->answer.buf[0] == 's') {\n \t\t\tsize_t splittable_into = hunk->splittable_into;\n \t\t\tif (splittable_into < 2)\ndiff --git a/t/t3701-add-interactive.sh b/t/t3701-add-interactive.sh\nindex 57c656a20c..12ee321707 100755\n--- a/t/t3701-add-interactive.sh\n+++ b/t/t3701-add-interactive.sh\n@@ -429,6 +429,20 @@ test_expect_success 'goto hunk' '\n \ttest_cmp expect actual.trimmed\n '\n \n+test_expect_success 'navigate to hunk via regex' '\n+\ttest_when_finished \"git reset\" &&\n+\ttr _ \" \" >expect <<-EOF &&\n+\t(2/2) Stage this hunk [y,n,q,a,d,K,g,/,e,?]? @@ -1,2 +1,3 @@\n+\t_10\n+\t+15\n+\t_20\n+\t(1/2) Stage this hunk [y,n,q,a,d,j,J,g,/,e,?]?_\n+\tEOF\n+\ttest_write_lines s y /1,2 | git add -p >actual &&\n+\ttail -n 5 <actual >actual.trimmed &&\n+\ttest_cmp expect actual.trimmed\n+'\n+\n test_expect_success 'split hunk \"add -p (edit)\"' '\n \t# Split, say Edit and do nothing.  Then:\n \t#\n-- \ngitgitgadget\n\n"},{"id":"388101","messageId":"6c33fbb68453707c874fde14347ce368b555c449.1576224486.git.gitgitgadget@gmail.com","threadId":"52445","inReplyTo":"pull.173.git.1576224486.gitgitgadget@gmail.com","subject":"[PATCH 17/19] built-in add -p: implement the 'q' (\"quit\") command","fromName":"Johannes Schindelin via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2019-12-13T08:08:04Z","receivedAt":"2019-12-13T08:08:39Z","isPatch":true,"sender":{"key":"johannes.schindelin@gmx.de","avatar":"https://avatars.githubusercontent.com/u/127790?v=4"},"body":"From: Johannes Schindelin <johannes.schindelin@gmx.de>\n\nThis command is actually very similar to the 'd' (\"do not stage this\nhunk or any of the later hunks in the file\") command: it just does\nsomething on top, namely leave the loop and return a value indicating\nthat we're quittin'.\n\nSigned-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n---\n add-patch.c | 17 +++++++++++------\n 1 file changed, 11 insertions(+), 6 deletions(-)\n\ndiff --git a/add-patch.c b/add-patch.c\nindex fd72850c65..5e9829a8b4 100644\n--- a/add-patch.c\n+++ b/add-patch.c\n@@ -12,9 +12,9 @@ enum prompt_mode_type {\n };\n \n static const char *prompt_mode[] = {\n-\tN_(\"Stage mode change [y,n,a,d%s,?]? \"),\n-\tN_(\"Stage deletion [y,n,a,d%s,?]? \"),\n-\tN_(\"Stage this hunk [y,n,a,d%s,?]? \")\n+\tN_(\"Stage mode change [y,n,a,q,d%s,?]? \"),\n+\tN_(\"Stage deletion [y,n,a,q,d%s,?]? \"),\n+\tN_(\"Stage this hunk [y,n,a,q,d%s,?]? \")\n };\n \n struct hunk_header {\n@@ -1006,6 +1006,7 @@ static size_t display_hunks(struct add_p_state *s,\n static const char help_patch_text[] =\n N_(\"y - stage this hunk\\n\"\n    \"n - do not stage this hunk\\n\"\n+   \"q - quit; do not stage this hunk or any of the remaining ones\\n\"\n    \"a - stage this and all the remaining hunks\\n\"\n    \"d - do not stage this hunk nor any of the remaining hunks\\n\"\n    \"j - leave this hunk undecided, see next undecided hunk\\n\"\n@@ -1026,7 +1027,7 @@ static int patch_update_file(struct add_p_state *s,\n \tstruct hunk *hunk;\n \tchar ch;\n \tstruct child_process cp = CHILD_PROCESS_INIT;\n-\tint colored = !!s->colored.len;\n+\tint colored = !!s->colored.len, quit = 0;\n \tenum prompt_mode_type prompt_mode_type;\n \n \tif (!file_diff->hunk_nr)\n@@ -1115,12 +1116,16 @@ static int patch_update_file(struct add_p_state *s,\n \t\t\t\tif (hunk->use == UNDECIDED_HUNK)\n \t\t\t\t\thunk->use = USE_HUNK;\n \t\t\t}\n-\t\t} else if (ch == 'd') {\n+\t\t} else if (ch == 'd' || ch == 'q') {\n \t\t\tfor (; hunk_index < file_diff->hunk_nr; hunk_index++) {\n \t\t\t\thunk = file_diff->hunk + hunk_index;\n \t\t\t\tif (hunk->use == UNDECIDED_HUNK)\n \t\t\t\t\thunk->use = SKIP_HUNK;\n \t\t\t}\n+\t\t\tif (ch == 'q') {\n+\t\t\t\tquit = 1;\n+\t\t\t\tbreak;\n+\t\t\t}\n \t\t} else if (s->answer.buf[0] == 'K') {\n \t\t\tif (hunk_index)\n \t\t\t\thunk_index--;\n@@ -1267,7 +1272,7 @@ static int patch_update_file(struct add_p_state *s,\n \t}\n \n \tputchar('\\n');\n-\treturn 0;\n+\treturn quit;\n }\n \n int run_add_p(struct repository *r, const struct pathspec *ps)\n-- \ngitgitgadget\n\n"},{"id":"388097","messageId":"556604361d28377e4f453b851527293ae865c27a.1576224486.git.gitgitgadget@gmail.com","threadId":"52445","inReplyTo":"pull.173.git.1576224486.gitgitgadget@gmail.com","subject":"[PATCH 08/19] built-in add -p: handle deleted empty files","fromName":"Johannes Schindelin via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2019-12-13T08:07:55Z","receivedAt":"2019-12-13T08:08:40Z","isPatch":true,"sender":{"key":"johannes.schindelin@gmx.de","avatar":"https://avatars.githubusercontent.com/u/127790?v=4"},"body":"From: Johannes Schindelin <johannes.schindelin@gmx.de>\n\nThis addresses the same problem as 24ab81ae4d (add-interactive: handle\ndeletion of empty files, 2009-10-27), although in a different way: we\nnot only stick the \"deleted file\" line into its own pseudo hunk, but\nalso the entire remainder (if any) of the same diff.\n\nThat way, we do not have to play any funny games with regards to\ncoalescing the diff after the user selected what (possibly pseudo-)hunks\nto stage.\n\nSigned-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n---\n add-patch.c | 13 +++++++++++--\n 1 file changed, 11 insertions(+), 2 deletions(-)\n\ndiff --git a/add-patch.c b/add-patch.c\nindex 7c1b3b3935..c32541f46d 100644\n--- a/add-patch.c\n+++ b/add-patch.c\n@@ -33,6 +33,7 @@ struct add_p_state {\n \t\tstruct hunk head;\n \t\tstruct hunk *hunk;\n \t\tsize_t hunk_nr, hunk_alloc;\n+\t\tunsigned deleted:1;\n \t} *file_diff;\n \tsize_t file_diff_nr;\n };\n@@ -180,6 +181,8 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps)\n \tpend = p + plain->len;\n \twhile (p != pend) {\n \t\tchar *eol = memchr(p, '\\n', pend - p);\n+\t\tconst char *deleted = NULL;\n+\n \t\tif (!eol)\n \t\t\teol = pend;\n \n@@ -196,7 +199,11 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps)\n \t\t} else if (p == plain->buf)\n \t\t\tBUG(\"diff starts with unexpected line:\\n\"\n \t\t\t    \"%.*s\\n\", (int)(eol - p), p);\n-\t\telse if (starts_with(p, \"@@ \")) {\n+\t\telse if (file_diff->deleted)\n+\t\t\t; /* keep the rest of the file in a single \"hunk\" */\n+\t\telse if (starts_with(p, \"@@ \") ||\n+\t\t\t (hunk == &file_diff->head &&\n+\t\t\t  skip_prefix(p, \"deleted file\", &deleted))) {\n \t\t\tfile_diff->hunk_nr++;\n \t\t\tALLOC_GROW(file_diff->hunk, file_diff->hunk_nr,\n \t\t\t\t   file_diff->hunk_alloc);\n@@ -207,7 +214,9 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps)\n \t\t\tif (colored)\n \t\t\t\thunk->colored_start = colored_p - colored->buf;\n \n-\t\t\tif (parse_hunk_header(s, hunk) < 0)\n+\t\t\tif (deleted)\n+\t\t\t\tfile_diff->deleted = 1;\n+\t\t\telse if (parse_hunk_header(s, hunk) < 0)\n \t\t\t\treturn -1;\n \t\t}\n \n-- \ngitgitgadget\n\n"},{"id":"388099","messageId":"ab94f43c0d1c190430da56b3ce08ee48b486a7db.1576224486.git.gitgitgadget@gmail.com","threadId":"52445","inReplyTo":"pull.173.git.1576224486.gitgitgadget@gmail.com","subject":"[PATCH 04/19] built-in add -p: adjust hunk headers as needed","fromName":"Johannes Schindelin via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2019-12-13T08:07:51Z","receivedAt":"2019-12-13T08:08:41Z","isPatch":true,"sender":{"key":"johannes.schindelin@gmx.de","avatar":"https://avatars.githubusercontent.com/u/127790?v=4"},"body":"From: Johannes Schindelin <johannes.schindelin@gmx.de>\n\nWhen skipping a hunk that adds a different number of lines than it\nremoves, we need to adjust the subsequent hunk headers of non-skipped\nhunks: in pathological cases, the context is not enough to determine\nprecisely where the patch should be applied.\n\nThis problem was identified in 23fea4c240 (t3701: add failing test for\npathological context lines, 2018-03-01) and fixed in the Perl version in\nfecc6f3a68 (add -p: adjust offsets of subsequent hunks when one is\nskipped, 2018-03-01).\n\nAnd this patch fixes it in the C version of `git add -p`.\n\nIn contrast to the Perl version, we try to keep the extra text on the\nhunk header (which typically contains the signature of the function\nwhose code is changed in the hunk) intact.\n\nNote: while the C version does not support staging mode changes at this\nstage, we already prepare for this by simply skipping the hunk header if\nboth old and new offset is 0 (this cannot happen for regular hunks, and\nwe will use this as an indicator that we are looking at a special hunk).\n\nLikewise, we already prepare for hunk splitting by handling the absence\nof extra text in the hunk header gracefully: only the first split hunk\nwill have that text, the others will not (indicated by an empty extra\ntext start/end range). Preparing for hunk splitting already at this\nstage avoids an indentation change of the entire hunk header-printing\nblock later, and is almost as easy to review as without that handling.\n\nSigned-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n---\n add-interactive.c |  14 +----\n add-interactive.h |  15 +++++\n add-patch.c       | 145 ++++++++++++++++++++++++++++++++++++++++++----\n 3 files changed, 151 insertions(+), 23 deletions(-)\n\ndiff --git a/add-interactive.c b/add-interactive.c\nindex 034c1dc02f..29356c5aa2 100644\n--- a/add-interactive.c\n+++ b/add-interactive.c\n@@ -10,16 +10,6 @@\n #include \"dir.h\"\n #include \"run-command.h\"\n \n-struct add_i_state {\n-\tstruct repository *r;\n-\tint use_color;\n-\tchar header_color[COLOR_MAXLEN];\n-\tchar help_color[COLOR_MAXLEN];\n-\tchar prompt_color[COLOR_MAXLEN];\n-\tchar error_color[COLOR_MAXLEN];\n-\tchar reset_color[COLOR_MAXLEN];\n-};\n-\n static void init_color(struct repository *r, struct add_i_state *s,\n \t\t       const char *slot_name, char *dst,\n \t\t       const char *default_color)\n@@ -36,7 +26,7 @@ static void init_color(struct repository *r, struct add_i_state *s,\n \tfree(key);\n }\n \n-static void init_add_i_state(struct add_i_state *s, struct repository *r)\n+void init_add_i_state(struct add_i_state *s, struct repository *r)\n {\n \tconst char *value;\n \n@@ -54,6 +44,8 @@ static void init_add_i_state(struct add_i_state *s, struct repository *r)\n \tinit_color(r, s, \"prompt\", s->prompt_color, GIT_COLOR_BOLD_BLUE);\n \tinit_color(r, s, \"error\", s->error_color, GIT_COLOR_BOLD_RED);\n \tinit_color(r, s, \"reset\", s->reset_color, GIT_COLOR_RESET);\n+\tinit_color(r, s, \"fraginfo\", s->fraginfo_color,\n+\t\t   diff_get_color(s->use_color, DIFF_FRAGINFO));\n }\n \n /*\ndiff --git a/add-interactive.h b/add-interactive.h\nindex 0e3d93acc9..584f304a9a 100644\n--- a/add-interactive.h\n+++ b/add-interactive.h\n@@ -1,6 +1,21 @@\n #ifndef ADD_INTERACTIVE_H\n #define ADD_INTERACTIVE_H\n \n+#include \"color.h\"\n+\n+struct add_i_state {\n+\tstruct repository *r;\n+\tint use_color;\n+\tchar header_color[COLOR_MAXLEN];\n+\tchar help_color[COLOR_MAXLEN];\n+\tchar prompt_color[COLOR_MAXLEN];\n+\tchar error_color[COLOR_MAXLEN];\n+\tchar reset_color[COLOR_MAXLEN];\n+\tchar fraginfo_color[COLOR_MAXLEN];\n+};\n+\n+void init_add_i_state(struct add_i_state *s, struct repository *r);\n+\n struct repository;\n struct pathspec;\n int run_add_i(struct repository *r, const struct pathspec *ps);\ndiff --git a/add-patch.c b/add-patch.c\nindex 79eefa9505..e266a96ca7 100644\n--- a/add-patch.c\n+++ b/add-patch.c\n@@ -5,14 +5,26 @@\n #include \"argv-array.h\"\n #include \"pathspec.h\"\n #include \"color.h\"\n+#include \"diff.h\"\n+\n+struct hunk_header {\n+\tunsigned long old_offset, old_count, new_offset, new_count;\n+\t/*\n+\t * Start/end offsets to the extra text after the second `@@` in the\n+\t * hunk header, e.g. the function signature. This is expected to\n+\t * include the newline.\n+\t */\n+\tsize_t extra_start, extra_end, colored_extra_start, colored_extra_end;\n+};\n \n struct hunk {\n \tsize_t start, end, colored_start, colored_end;\n \tenum { UNDECIDED_HUNK = 0, SKIP_HUNK, USE_HUNK } use;\n+\tstruct hunk_header header;\n };\n \n struct add_p_state {\n-\tstruct repository *r;\n+\tstruct add_i_state s;\n \tstruct strbuf answer, buf;\n \n \t/* parsed diff */\n@@ -35,7 +47,70 @@ static void setup_child_process(struct add_p_state *s,\n \n \tcp->git_cmd = 1;\n \targv_array_pushf(&cp->env_array,\n-\t\t\t INDEX_ENVIRONMENT \"=%s\", s->r->index_file);\n+\t\t\t INDEX_ENVIRONMENT \"=%s\", s->s.r->index_file);\n+}\n+\n+static int parse_range(const char **p,\n+\t\t       unsigned long *offset, unsigned long *count)\n+{\n+\tchar *pend;\n+\n+\t*offset = strtoul(*p, &pend, 10);\n+\tif (pend == *p)\n+\t\treturn -1;\n+\tif (*pend != ',') {\n+\t\t*count = 1;\n+\t\t*p = pend;\n+\t\treturn 0;\n+\t}\n+\t*count = strtoul(pend + 1, (char **)p, 10);\n+\treturn *p == pend + 1 ? -1 : 0;\n+}\n+\n+static int parse_hunk_header(struct add_p_state *s, struct hunk *hunk)\n+{\n+\tstruct hunk_header *header = &hunk->header;\n+\tconst char *line = s->plain.buf + hunk->start, *p = line;\n+\tchar *eol = memchr(p, '\\n', s->plain.len - hunk->start);\n+\n+\tif (!eol)\n+\t\teol = s->plain.buf + s->plain.len;\n+\n+\tif (!skip_prefix(p, \"@@ -\", &p) ||\n+\t    parse_range(&p, &header->old_offset, &header->old_count) < 0 ||\n+\t    !skip_prefix(p, \" +\", &p) ||\n+\t    parse_range(&p, &header->new_offset, &header->new_count) < 0 ||\n+\t    !skip_prefix(p, \" @@\", &p))\n+\t\treturn error(_(\"could not parse hunk header '%.*s'\"),\n+\t\t\t     (int)(eol - line), line);\n+\n+\thunk->start = eol - s->plain.buf + (*eol == '\\n');\n+\theader->extra_start = p - s->plain.buf;\n+\theader->extra_end = hunk->start;\n+\n+\tif (!s->colored.len) {\n+\t\theader->colored_extra_start = header->colored_extra_end = 0;\n+\t\treturn 0;\n+\t}\n+\n+\t/* Now find the extra text in the colored diff */\n+\tline = s->colored.buf + hunk->colored_start;\n+\teol = memchr(line, '\\n', s->colored.len - hunk->colored_start);\n+\tif (!eol)\n+\t\teol = s->colored.buf + s->colored.len;\n+\tp = memmem(line, eol - line, \"@@ -\", 4);\n+\tif (!p)\n+\t\treturn error(_(\"could not parse colored hunk header '%.*s'\"),\n+\t\t\t     (int)(eol - line), line);\n+\tp = memmem(p + 4, eol - p - 4, \" @@\", 3);\n+\tif (!p)\n+\t\treturn error(_(\"could not parse colored hunk header '%.*s'\"),\n+\t\t\t     (int)(eol - line), line);\n+\thunk->colored_start = eol - s->colored.buf + (*eol == '\\n');\n+\theader->colored_extra_start = p + 3 - s->colored.buf;\n+\theader->colored_extra_end = hunk->colored_start;\n+\n+\treturn 0;\n }\n \n static int parse_diff(struct add_p_state *s, const struct pathspec *ps)\n@@ -109,6 +184,9 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps)\n \t\t\thunk->start = p - plain->buf;\n \t\t\tif (colored)\n \t\t\t\thunk->colored_start = colored_p - colored->buf;\n+\n+\t\t\tif (parse_hunk_header(s, hunk) < 0)\n+\t\t\t\treturn -1;\n \t\t}\n \n \t\tp = eol == pend ? pend : eol + 1;\n@@ -130,8 +208,43 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps)\n }\n \n static void render_hunk(struct add_p_state *s, struct hunk *hunk,\n-\t\t\tint colored, struct strbuf *out)\n+\t\t\tssize_t delta, int colored, struct strbuf *out)\n {\n+\tstruct hunk_header *header = &hunk->header;\n+\n+\tif (hunk->header.old_offset != 0 || hunk->header.new_offset != 0) {\n+\t\t/*\n+\t\t * Generate the hunk header dynamically, except for special\n+\t\t * hunks (such as the diff header).\n+\t\t */\n+\t\tconst char *p;\n+\t\tsize_t len;\n+\t\tunsigned long old_offset = header->old_offset;\n+\t\tunsigned long new_offset = header->new_offset;\n+\n+\t\tif (!colored) {\n+\t\t\tp = s->plain.buf + header->extra_start;\n+\t\t\tlen = header->extra_end - header->extra_start;\n+\t\t} else {\n+\t\t\tstrbuf_addstr(out, s->s.fraginfo_color);\n+\t\t\tp = s->colored.buf + header->colored_extra_start;\n+\t\t\tlen = header->colored_extra_end\n+\t\t\t\t- header->colored_extra_start;\n+\t\t}\n+\n+\t\tnew_offset += delta;\n+\n+\t\tstrbuf_addf(out, \"@@ -%lu,%lu +%lu,%lu @@\",\n+\t\t\t    old_offset, header->old_count,\n+\t\t\t    new_offset, header->new_count);\n+\t\tif (len)\n+\t\t\tstrbuf_add(out, p, len);\n+\t\telse if (colored)\n+\t\t\tstrbuf_addf(out, \"%s\\n\", GIT_COLOR_RESET);\n+\t\telse\n+\t\t\tstrbuf_addch(out, '\\n');\n+\t}\n+\n \tif (colored)\n \t\tstrbuf_add(out, s->colored.buf + hunk->colored_start,\n \t\t\t   hunk->colored_end - hunk->colored_start);\n@@ -144,13 +257,17 @@ static void reassemble_patch(struct add_p_state *s, struct strbuf *out)\n {\n \tstruct hunk *hunk;\n \tsize_t i;\n+\tssize_t delta = 0;\n \n-\trender_hunk(s, &s->head, 0, out);\n+\trender_hunk(s, &s->head, 0, 0, out);\n \n \tfor (i = 0; i < s->hunk_nr; i++) {\n \t\thunk = s->hunk + i;\n-\t\tif (hunk->use == USE_HUNK)\n-\t\t\trender_hunk(s, hunk, 0, out);\n+\t\tif (hunk->use != USE_HUNK)\n+\t\t\tdelta += hunk->header.old_count\n+\t\t\t\t- hunk->header.new_count;\n+\t\telse\n+\t\t\trender_hunk(s, hunk, delta, 0, out);\n \t}\n }\n \n@@ -178,7 +295,7 @@ static int patch_update_file(struct add_p_state *s)\n \t\treturn 0;\n \n \tstrbuf_reset(&s->buf);\n-\trender_hunk(s, &s->head, colored, &s->buf);\n+\trender_hunk(s, &s->head, 0, colored, &s->buf);\n \tfputs(s->buf.buf, stdout);\n \tfor (;;) {\n \t\tif (hunk_index >= s->hunk_nr)\n@@ -205,7 +322,7 @@ static int patch_update_file(struct add_p_state *s)\n \t\t\tbreak;\n \n \t\tstrbuf_reset(&s->buf);\n-\t\trender_hunk(s, hunk, colored, &s->buf);\n+\t\trender_hunk(s, hunk, 0, colored, &s->buf);\n \t\tfputs(s->buf.buf, stdout);\n \n \t\tstrbuf_reset(&s->buf);\n@@ -272,13 +389,13 @@ static int patch_update_file(struct add_p_state *s)\n \t\tstrbuf_reset(&s->buf);\n \t\treassemble_patch(s, &s->buf);\n \n-\t\tdiscard_index(s->r->index);\n+\t\tdiscard_index(s->s.r->index);\n \t\tsetup_child_process(s, &cp, \"apply\", \"--cached\", NULL);\n \t\tif (pipe_command(&cp, s->buf.buf, s->buf.len,\n \t\t\t\t NULL, 0, NULL, 0))\n \t\t\terror(_(\"'git apply --cached' failed\"));\n-\t\tif (!repo_read_index(s->r))\n-\t\t\trepo_refresh_and_write_index(s->r, REFRESH_QUIET, 0,\n+\t\tif (!repo_read_index(s->s.r))\n+\t\t\trepo_refresh_and_write_index(s->s.r, REFRESH_QUIET, 0,\n \t\t\t\t\t\t     1, NULL, NULL, NULL);\n \t}\n \n@@ -288,7 +405,11 @@ static int patch_update_file(struct add_p_state *s)\n \n int run_add_p(struct repository *r, const struct pathspec *ps)\n {\n-\tstruct add_p_state s = { r, STRBUF_INIT, STRBUF_INIT, STRBUF_INIT };\n+\tstruct add_p_state s = {\n+\t\t{ r }, STRBUF_INIT, STRBUF_INIT, STRBUF_INIT, STRBUF_INIT\n+\t};\n+\n+\tinit_add_i_state(&s.s, r);\n \n \tif (discard_index(r->index) < 0 || repo_read_index(r) < 0 ||\n \t    repo_refresh_and_write_index(r, REFRESH_QUIET, 0, 1,\n-- \ngitgitgadget\n\n"},{"id":"388100","messageId":"7d3a59dd117a670734e8420093e7ccbe28d1650c.1576224486.git.gitgitgadget@gmail.com","threadId":"52445","inReplyTo":"pull.173.git.1576224486.gitgitgadget@gmail.com","subject":"[PATCH 05/19] built-in add -p: color the prompt and the help text","fromName":"Johannes Schindelin via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2019-12-13T08:07:52Z","receivedAt":"2019-12-13T08:08:44Z","isPatch":true,"sender":{"key":"johannes.schindelin@gmx.de","avatar":"https://avatars.githubusercontent.com/u/127790?v=4"},"body":"From: Johannes Schindelin <johannes.schindelin@gmx.de>\n\n... just like the Perl version ;-)\n\nSigned-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>\n---\n add-patch.c | 12 ++++++++----\n 1 file changed, 8 insertions(+), 4 deletions(-)\n\ndiff --git a/add-patch.c b/add-patch.c\nindex e266a96ca7..dab2ff2381 100644\n--- a/add-patch.c\n+++ b/add-patch.c\n@@ -334,9 +334,12 @@ static int patch_update_file(struct add_p_state *s)\n \t\t\tstrbuf_addstr(&s->buf, \",j\");\n \t\tif (hunk_index + 1 < s->hunk_nr)\n \t\t\tstrbuf_addstr(&s->buf, \",J\");\n-\t\tprintf(\"(%\"PRIuMAX\"/%\"PRIuMAX\") \",\n-\t\t       (uintmax_t)hunk_index + 1, (uintmax_t)s->hunk_nr);\n-\t\tprintf(_(\"Stage this hunk [y,n,a,d%s,?]? \"), s->buf.buf);\n+\t\tcolor_fprintf(stdout, s->s.prompt_color,\n+\t\t\t      \"(%\"PRIuMAX\"/%\"PRIuMAX\") \",\n+\t\t\t      (uintmax_t)hunk_index + 1, (uintmax_t)s->hunk_nr);\n+\t\tcolor_fprintf(stdout, s->s.prompt_color,\n+\t\t\t      _(\"Stage this hunk [y,n,a,d%s,?]? \"),\n+\t\t\t      s->buf.buf);\n \t\tfflush(stdout);\n \t\tif (strbuf_getline(&s->answer, stdin) == EOF)\n \t\t\tbreak;\n@@ -376,7 +379,8 @@ static int patch_update_file(struct add_p_state *s)\n \t\telse if (undecided_next >= 0 && s->answer.buf[0] == 'j')\n \t\t\thunk_index = undecided_next;\n \t\telse\n-\t\t\tputs(_(help_patch_text));\n+\t\t\tcolor_fprintf(stdout, s->s.help_color,\n+\t\t\t\t      _(help_patch_text));\n \t}\n \n \t/* Any hunk to be used? */\n-- \ngitgitgadget\n\n"}]}