{"thread":{"id":"65116","subject":"[PATCH 0/8] history: introduce \"split\" subcommand","startedAt":"2026-03-02T12:13:13Z","lastAt":"2026-03-16T07:13:32Z","messageCount":16,"participants":["Patrick Steinhardt","D. Ben Knoble","Junio C Hamano"],"isPatch":true,"patchVersion":1,"patchTotal":8},"messages":[{"id":"537507","messageId":"20260302-pks-history-split-v1-0-444fc987a324@pks.im","threadId":"65116","inReplyTo":null,"subject":"[PATCH 0/8] history: introduce \"split\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-03-02T12:13:04Z","receivedAt":"2026-03-02T12:13:13Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Hi,\n\nthis patch series introduces `git history split` as an easy way to split\nup one commit into multiple commits. This subcommand has already been\nintroduced in earlier versions of my git-history(1) patch series, but I\neventually decided to evict them from this series so that we can rather\nfocus more on basic decisions.\n\nIn any case, the current version of this patch series matches (to the\nbest of my knowledge) the latest agreements on the mailing list around\nits behaviour. Most importantly:\n\n  - It will ask for commit messages for both commits, not only the first\n    commit, which has been a bit of a discussion point.\n\n  - It is not possible to edit hunks. This results in a mode where\n    conflicts are not possible as the tree of the second commit will\n    always match the tree of the original commit. Conflict handling for\n    subsequent subcommands will be a bigger topic, as it probably\n    depends on support for first-class conflicts.\n\n  - We also update dependent branches, same as with the latest iteration\n    of `git history reword`.\n\nThe patch series is built on top of 2cc7191751 (The 8th batch,\n2026-02-27) with aa/add-p-no-auto-advance at 417b181f99 (add-patch:\nallow interfile navigation when selecting hunks, 2026-02-14) merged into\nit.\n\nThanks!\n\nPatrick\n\n---\nPatrick Steinhardt (8):\n      add-patch: split out header from \"add-interactive.h\"\n      add-patch: split out `struct interactive_options`\n      add-patch: remove dependency on \"add-interactive\" subsystem\n      add-patch: add support for in-memory index patching\n      add-patch: allow disabling editing of hunks\n      cache-tree: allow writing in-memory index as tree\n      builtin/history: split out extended function to create commits\n      builtin/history: implement \"split\" subcommand\n\n Documentation/git-history.adoc |  62 ++++\n add-interactive.c              | 177 ++--------\n add-interactive.h              |  48 +--\n add-patch.c                    | 363 ++++++++++++++++----\n add-patch.h                    |  74 ++++\n builtin/add.c                  |  26 +-\n builtin/checkout.c             |   9 +-\n builtin/commit.c               |  16 +-\n builtin/history.c              | 317 +++++++++++++++--\n builtin/reset.c                |  20 +-\n builtin/stash.c                |  54 +--\n cache-tree.c                   |   4 +-\n cache-tree.h                   |   3 +-\n commit.h                       |   2 +-\n t/meson.build                  |   1 +\n t/t3452-history-split.sh       | 757 +++++++++++++++++++++++++++++++++++++++++\n 16 files changed, 1592 insertions(+), 341 deletions(-)\n\n\n---\nbase-commit: caec355702b7a86dadec53344ba65ae33558cfa8\nchange-id: 20260202-pks-history-split-fb3a93df61ba\n\n"},{"id":"537508","messageId":"20260302-pks-history-split-v1-1-444fc987a324@pks.im","threadId":"65116","inReplyTo":"20260302-pks-history-split-v1-0-444fc987a324@pks.im","subject":"[PATCH 1/8] add-patch: split out header from \"add-interactive.h\"","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-03-02T12:13:05Z","receivedAt":"2026-03-02T12:13:15Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"While we have a \"add-patch.c\" code file, its declarations are part of\n\"add-interactive.h\". This makes it somewhat harder than necessary to\nfind relevant code and to identify clear boundaries between the two\nsubsystems.\n\nSplit up concerns and move declarations that relate to \"add-patch.c\"\ninto a new \"add-patch.h\" header.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n add-interactive.h | 24 +++---------------------\n add-patch.c       |  1 +\n add-patch.h       | 27 +++++++++++++++++++++++++++\n 3 files changed, 31 insertions(+), 21 deletions(-)\n\ndiff --git a/add-interactive.h b/add-interactive.h\nindex 7843397775..6c62489bfe 100644\n--- a/add-interactive.h\n+++ b/add-interactive.h\n@@ -1,15 +1,11 @@\n #ifndef ADD_INTERACTIVE_H\n #define ADD_INTERACTIVE_H\n \n+#include \"add-patch.h\"\n #include \"color.h\"\n \n-struct add_p_opt {\n-\tint context;\n-\tint interhunkcontext;\n-\tint auto_advance;\n-};\n-\n-#define ADD_P_OPT_INIT { .context = -1, .interhunkcontext = -1, .auto_advance = 1 }\n+struct pathspec;\n+struct repository;\n \n struct add_i_state {\n \tstruct repository *r;\n@@ -37,21 +33,7 @@ void init_add_i_state(struct add_i_state *s, struct repository *r,\n \t\t      struct add_p_opt *add_p_opt);\n void clear_add_i_state(struct add_i_state *s);\n \n-struct repository;\n-struct pathspec;\n int run_add_i(struct repository *r, const struct pathspec *ps,\n \t      struct add_p_opt *add_p_opt);\n \n-enum add_p_mode {\n-\tADD_P_ADD,\n-\tADD_P_STASH,\n-\tADD_P_RESET,\n-\tADD_P_CHECKOUT,\n-\tADD_P_WORKTREE,\n-};\n-\n-int run_add_p(struct repository *r, enum add_p_mode mode,\n-\t      struct add_p_opt *o, const char *revision,\n-\t      const struct pathspec *ps);\n-\n #endif\ndiff --git a/add-patch.c b/add-patch.c\nindex 8c03f710d3..8ce2fc02f6 100644\n--- a/add-patch.c\n+++ b/add-patch.c\n@@ -3,6 +3,7 @@\n \n #include \"git-compat-util.h\"\n #include \"add-interactive.h\"\n+#include \"add-patch.h\"\n #include \"advice.h\"\n #include \"editor.h\"\n #include \"environment.h\"\ndiff --git a/add-patch.h b/add-patch.h\nnew file mode 100644\nindex 0000000000..88b00ca788\n--- /dev/null\n+++ b/add-patch.h\n@@ -0,0 +1,27 @@\n+#ifndef ADD_PATCH_H\n+#define ADD_PATCH_H\n+\n+struct pathspec;\n+struct repository;\n+\n+struct add_p_opt {\n+\tint context;\n+\tint interhunkcontext;\n+\tint auto_advance;\n+};\n+\n+#define ADD_P_OPT_INIT { .context = -1, .interhunkcontext = -1, .auto_advance = 1 }\n+\n+enum add_p_mode {\n+\tADD_P_ADD,\n+\tADD_P_STASH,\n+\tADD_P_RESET,\n+\tADD_P_CHECKOUT,\n+\tADD_P_WORKTREE,\n+};\n+\n+int run_add_p(struct repository *r, enum add_p_mode mode,\n+\t      struct add_p_opt *o, const char *revision,\n+\t      const struct pathspec *ps);\n+\n+#endif\n\n-- \n2.53.0.697.g625c4fb2da.dirty\n\n"},{"id":"537509","messageId":"20260302-pks-history-split-v1-2-444fc987a324@pks.im","threadId":"65116","inReplyTo":"20260302-pks-history-split-v1-0-444fc987a324@pks.im","subject":"[PATCH 2/8] add-patch: split out `struct interactive_options`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-03-02T12:13:06Z","receivedAt":"2026-03-02T12:13:17Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The `struct add_p_opt` is reused both by our infra for \"git add -p\" and\n\"git add -i\". Users of `run_add_i()` for example are expected to pass\n`struct add_p_opt`. This is somewhat confusing and raises the question\nof which options apply to what part of the stack.\n\nBut things are even more confusing than that: while callers are expected\nto pass in `struct add_p_opt`, these options ultimately get used to\ninitialize a `struct add_i_state` that is used by both subsystems. So we\nare basically going full circle here.\n\nRefactor the code and split out a new `struct interactive_options` that\nhosts common options used by both. These options are then applied to a\n`struct interactive_config` that hosts common configuration.\n\nThis refactoring doesn't yet fully detangle the two subsystems from one\nanother, as we still end up calling `init_add_i_state()` in the \"git add\n-p\" subsystem. This will be fixed in a subsequent commit.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n add-interactive.c  | 177 +++++++++++---------------------------------------\n add-interactive.h  |  24 +------\n add-patch.c        | 187 +++++++++++++++++++++++++++++++++++++++++++----------\n add-patch.h        |  38 ++++++++++-\n builtin/add.c      |  26 ++++----\n builtin/checkout.c |   4 +-\n builtin/commit.c   |  16 ++---\n builtin/reset.c    |  20 +++---\n builtin/stash.c    |  54 ++++++++--------\n commit.h           |   2 +-\n 10 files changed, 290 insertions(+), 258 deletions(-)\n\ndiff --git a/add-interactive.c b/add-interactive.c\nindex 1580639682..152e2a0297 100644\n--- a/add-interactive.c\n+++ b/add-interactive.c\n@@ -3,7 +3,6 @@\n #include \"git-compat-util.h\"\n #include \"add-interactive.h\"\n #include \"color.h\"\n-#include \"config.h\"\n #include \"diffcore.h\"\n #include \"gettext.h\"\n #include \"hash.h\"\n@@ -20,120 +19,18 @@\n #include \"prompt.h\"\n #include \"tree.h\"\n \n-static void init_color(struct repository *r, enum git_colorbool use_color,\n-\t\t       const char *section_and_slot, char *dst,\n-\t\t       const char *default_color)\n-{\n-\tchar *key = xstrfmt(\"color.%s\", section_and_slot);\n-\tconst char *value;\n-\n-\tif (!want_color(use_color))\n-\t\tdst[0] = '\\0';\n-\telse if (repo_config_get_value(r, key, &value) ||\n-\t\t color_parse(value, dst))\n-\t\tstrlcpy(dst, default_color, COLOR_MAXLEN);\n-\n-\tfree(key);\n-}\n-\n-static enum git_colorbool check_color_config(struct repository *r, const char *var)\n-{\n-\tconst char *value;\n-\tenum git_colorbool ret;\n-\n-\tif (repo_config_get_value(r, var, &value))\n-\t\tret = GIT_COLOR_UNKNOWN;\n-\telse\n-\t\tret = git_config_colorbool(var, value);\n-\n-\t/*\n-\t * Do not rely on want_color() to fall back to color.ui for us. It uses\n-\t * the value parsed by git_color_config(), which may not have been\n-\t * called by the main command.\n-\t */\n-\tif (ret == GIT_COLOR_UNKNOWN &&\n-\t    !repo_config_get_value(r, \"color.ui\", &value))\n-\t\tret = git_config_colorbool(\"color.ui\", value);\n-\n-\treturn ret;\n-}\n-\n void init_add_i_state(struct add_i_state *s, struct repository *r,\n-\t\t      struct add_p_opt *add_p_opt)\n+\t\t      struct interactive_options *opts)\n {\n \ts->r = r;\n-\ts->context = -1;\n-\ts->interhunkcontext = -1;\n-\ts->auto_advance = add_p_opt->auto_advance;\n-\n-\ts->use_color_interactive = check_color_config(r, \"color.interactive\");\n-\n-\tinit_color(r, s->use_color_interactive, \"interactive.header\",\n-\t\t   s->header_color, GIT_COLOR_BOLD);\n-\tinit_color(r, s->use_color_interactive, \"interactive.help\",\n-\t\t   s->help_color, GIT_COLOR_BOLD_RED);\n-\tinit_color(r, s->use_color_interactive, \"interactive.prompt\",\n-\t\t   s->prompt_color, GIT_COLOR_BOLD_BLUE);\n-\tinit_color(r, s->use_color_interactive, \"interactive.error\",\n-\t\t   s->error_color, GIT_COLOR_BOLD_RED);\n-\tstrlcpy(s->reset_color_interactive,\n-\t\twant_color(s->use_color_interactive) ? GIT_COLOR_RESET : \"\", COLOR_MAXLEN);\n-\n-\ts->use_color_diff = check_color_config(r, \"color.diff\");\n-\n-\tinit_color(r, s->use_color_diff, \"diff.frag\", s->fraginfo_color,\n-\t\t   diff_get_color(s->use_color_diff, DIFF_FRAGINFO));\n-\tinit_color(r, s->use_color_diff, \"diff.context\", s->context_color,\n-\t\t   \"fall back\");\n-\tif (!strcmp(s->context_color, \"fall back\"))\n-\t\tinit_color(r, s->use_color_diff, \"diff.plain\",\n-\t\t\t   s->context_color,\n-\t\t\t   diff_get_color(s->use_color_diff, DIFF_CONTEXT));\n-\tinit_color(r, s->use_color_diff, \"diff.old\", s->file_old_color,\n-\t\t   diff_get_color(s->use_color_diff, DIFF_FILE_OLD));\n-\tinit_color(r, s->use_color_diff, \"diff.new\", s->file_new_color,\n-\t\t   diff_get_color(s->use_color_diff, DIFF_FILE_NEW));\n-\tstrlcpy(s->reset_color_diff,\n-\t\twant_color(s->use_color_diff) ? GIT_COLOR_RESET : \"\", COLOR_MAXLEN);\n-\n-\tFREE_AND_NULL(s->interactive_diff_filter);\n-\trepo_config_get_string(r, \"interactive.difffilter\",\n-\t\t\t       &s->interactive_diff_filter);\n-\n-\tFREE_AND_NULL(s->interactive_diff_algorithm);\n-\trepo_config_get_string(r, \"diff.algorithm\",\n-\t\t\t       &s->interactive_diff_algorithm);\n-\n-\tif (!repo_config_get_int(r, \"diff.context\", &s->context))\n-\t\tif (s->context < 0)\n-\t\t\tdie(_(\"%s cannot be negative\"), \"diff.context\");\n-\tif (!repo_config_get_int(r, \"diff.interHunkContext\", &s->interhunkcontext))\n-\t\tif (s->interhunkcontext < 0)\n-\t\t\tdie(_(\"%s cannot be negative\"), \"diff.interHunkContext\");\n-\n-\trepo_config_get_bool(r, \"interactive.singlekey\", &s->use_single_key);\n-\tif (s->use_single_key)\n-\t\tsetbuf(stdin, NULL);\n-\n-\tif (add_p_opt->context != -1) {\n-\t\tif (add_p_opt->context < 0)\n-\t\t\tdie(_(\"%s cannot be negative\"), \"--unified\");\n-\t\ts->context = add_p_opt->context;\n-\t}\n-\tif (add_p_opt->interhunkcontext != -1) {\n-\t\tif (add_p_opt->interhunkcontext < 0)\n-\t\t\tdie(_(\"%s cannot be negative\"), \"--inter-hunk-context\");\n-\t\ts->interhunkcontext = add_p_opt->interhunkcontext;\n-\t}\n+\tinteractive_config_init(&s->cfg, r, opts);\n }\n \n void clear_add_i_state(struct add_i_state *s)\n {\n-\tFREE_AND_NULL(s->interactive_diff_filter);\n-\tFREE_AND_NULL(s->interactive_diff_algorithm);\n+\tinteractive_config_clear(&s->cfg);\n \tmemset(s, 0, sizeof(*s));\n-\ts->use_color_interactive = GIT_COLOR_UNKNOWN;\n-\ts->use_color_diff = GIT_COLOR_UNKNOWN;\n+\tinteractive_config_clear(&s->cfg);\n }\n \n /*\n@@ -287,7 +184,7 @@ static void list(struct add_i_state *s, struct string_list *list, int *selected,\n \t\treturn;\n \n \tif (opts->header)\n-\t\tcolor_fprintf_ln(stdout, s->header_color,\n+\t\tcolor_fprintf_ln(stdout, s->cfg.header_color,\n \t\t\t\t \"%s\", opts->header);\n \n \tfor (i = 0; i < list->nr; i++) {\n@@ -355,7 +252,7 @@ static ssize_t list_and_choose(struct add_i_state *s,\n \n \t\tlist(s, &items->items, items->selected, &opts->list_opts);\n \n-\t\tcolor_fprintf(stdout, s->prompt_color, \"%s\", opts->prompt);\n+\t\tcolor_fprintf(stdout, s->cfg.prompt_color, \"%s\", opts->prompt);\n \t\tfputs(singleton ? \"> \" : \">> \", stdout);\n \t\tfflush(stdout);\n \n@@ -433,7 +330,7 @@ static ssize_t list_and_choose(struct add_i_state *s,\n \n \t\t\tif (from < 0 || from >= items->items.nr ||\n \t\t\t    (singleton && from + 1 != to)) {\n-\t\t\t\tcolor_fprintf_ln(stderr, s->error_color,\n+\t\t\t\tcolor_fprintf_ln(stderr, s->cfg.error_color,\n \t\t\t\t\t\t _(\"Huh (%s)?\"), p);\n \t\t\t\tbreak;\n \t\t\t} else if (singleton) {\n@@ -993,7 +890,7 @@ static int run_patch(struct add_i_state *s, const struct pathspec *ps,\n \t\t\t\tfree(files->items.items[i].string);\n \t\t\t} else if (item->index.unmerged ||\n \t\t\t\t item->worktree.unmerged) {\n-\t\t\t\tcolor_fprintf_ln(stderr, s->error_color,\n+\t\t\t\tcolor_fprintf_ln(stderr, s->cfg.error_color,\n \t\t\t\t\t\t _(\"ignoring unmerged: %s\"),\n \t\t\t\t\t\t files->items.items[i].string);\n \t\t\t\tfree(item);\n@@ -1015,10 +912,10 @@ static int run_patch(struct add_i_state *s, const struct pathspec *ps,\n \topts->prompt = N_(\"Patch update\");\n \tcount = list_and_choose(s, files, opts);\n \tif (count > 0) {\n-\t\tstruct add_p_opt add_p_opt = {\n-\t\t\t.context = s->context,\n-\t\t\t.interhunkcontext = s->interhunkcontext,\n-\t\t\t.auto_advance = s->auto_advance\n+\t\tstruct interactive_options opts = {\n+\t\t\t.context = s->cfg.context,\n+\t\t\t.interhunkcontext = s->cfg.interhunkcontext,\n+\t\t\t.auto_advance = s->cfg.auto_advance,\n \t\t};\n \t\tstruct strvec args = STRVEC_INIT;\n \t\tstruct pathspec ps_selected = { 0 };\n@@ -1030,7 +927,7 @@ static int run_patch(struct add_i_state *s, const struct pathspec *ps,\n \t\tparse_pathspec(&ps_selected,\n \t\t\t       PATHSPEC_ALL_MAGIC & ~PATHSPEC_LITERAL,\n \t\t\t       PATHSPEC_LITERAL_PATH, \"\", args.v);\n-\t\tres = run_add_p(s->r, ADD_P_ADD, &add_p_opt, NULL, &ps_selected);\n+\t\tres = run_add_p(s->r, ADD_P_ADD, &opts, NULL, &ps_selected);\n \t\tstrvec_clear(&args);\n \t\tclear_pathspec(&ps_selected);\n \t}\n@@ -1066,10 +963,10 @@ static int run_diff(struct add_i_state *s, const struct pathspec *ps,\n \t\tstruct child_process cmd = CHILD_PROCESS_INIT;\n \n \t\tstrvec_pushl(&cmd.args, \"git\", \"diff\", \"-p\", \"--cached\", NULL);\n-\t\tif (s->context != -1)\n-\t\t\tstrvec_pushf(&cmd.args, \"--unified=%i\", s->context);\n-\t\tif (s->interhunkcontext != -1)\n-\t\t\tstrvec_pushf(&cmd.args, \"--inter-hunk-context=%i\", s->interhunkcontext);\n+\t\tif (s->cfg.context != -1)\n+\t\t\tstrvec_pushf(&cmd.args, \"--unified=%i\", s->cfg.context);\n+\t\tif (s->cfg.interhunkcontext != -1)\n+\t\t\tstrvec_pushf(&cmd.args, \"--inter-hunk-context=%i\", s->cfg.interhunkcontext);\n \t\tstrvec_pushl(&cmd.args, oid_to_hex(!is_initial ? &oid :\n \t\t\t     s->r->hash_algo->empty_tree), \"--\", NULL);\n \t\tfor (i = 0; i < files->items.nr; i++)\n@@ -1087,17 +984,17 @@ static int run_help(struct add_i_state *s, const struct pathspec *ps UNUSED,\n \t\t    struct prefix_item_list *files UNUSED,\n \t\t    struct list_and_choose_options *opts UNUSED)\n {\n-\tcolor_fprintf_ln(stdout, s->help_color, \"status        - %s\",\n+\tcolor_fprintf_ln(stdout, s->cfg.help_color, \"status        - %s\",\n \t\t\t _(\"show paths with changes\"));\n-\tcolor_fprintf_ln(stdout, s->help_color, \"update        - %s\",\n+\tcolor_fprintf_ln(stdout, s->cfg.help_color, \"update        - %s\",\n \t\t\t _(\"add working tree state to the staged set of changes\"));\n-\tcolor_fprintf_ln(stdout, s->help_color, \"revert        - %s\",\n+\tcolor_fprintf_ln(stdout, s->cfg.help_color, \"revert        - %s\",\n \t\t\t _(\"revert staged set of changes back to the HEAD version\"));\n-\tcolor_fprintf_ln(stdout, s->help_color, \"patch         - %s\",\n+\tcolor_fprintf_ln(stdout, s->cfg.help_color, \"patch         - %s\",\n \t\t\t _(\"pick hunks and update selectively\"));\n-\tcolor_fprintf_ln(stdout, s->help_color, \"diff          - %s\",\n+\tcolor_fprintf_ln(stdout, s->cfg.help_color, \"diff          - %s\",\n \t\t\t _(\"view diff between HEAD and index\"));\n-\tcolor_fprintf_ln(stdout, s->help_color, \"add untracked - %s\",\n+\tcolor_fprintf_ln(stdout, s->cfg.help_color, \"add untracked - %s\",\n \t\t\t _(\"add contents of untracked files to the staged set of changes\"));\n \n \treturn 0;\n@@ -1105,21 +1002,21 @@ static int run_help(struct add_i_state *s, const struct pathspec *ps UNUSED,\n \n static void choose_prompt_help(struct add_i_state *s)\n {\n-\tcolor_fprintf_ln(stdout, s->help_color, \"%s\",\n+\tcolor_fprintf_ln(stdout, s->cfg.help_color, \"%s\",\n \t\t\t _(\"Prompt help:\"));\n-\tcolor_fprintf_ln(stdout, s->help_color, \"1          - %s\",\n+\tcolor_fprintf_ln(stdout, s->cfg.help_color, \"1          - %s\",\n \t\t\t _(\"select a single item\"));\n-\tcolor_fprintf_ln(stdout, s->help_color, \"3-5        - %s\",\n+\tcolor_fprintf_ln(stdout, s->cfg.help_color, \"3-5        - %s\",\n \t\t\t _(\"select a range of items\"));\n-\tcolor_fprintf_ln(stdout, s->help_color, \"2-3,6-9    - %s\",\n+\tcolor_fprintf_ln(stdout, s->cfg.help_color, \"2-3,6-9    - %s\",\n \t\t\t _(\"select multiple ranges\"));\n-\tcolor_fprintf_ln(stdout, s->help_color, \"foo        - %s\",\n+\tcolor_fprintf_ln(stdout, s->cfg.help_color, \"foo        - %s\",\n \t\t\t _(\"select item based on unique prefix\"));\n-\tcolor_fprintf_ln(stdout, s->help_color, \"-...       - %s\",\n+\tcolor_fprintf_ln(stdout, s->cfg.help_color, \"-...       - %s\",\n \t\t\t _(\"unselect specified items\"));\n-\tcolor_fprintf_ln(stdout, s->help_color, \"*          - %s\",\n+\tcolor_fprintf_ln(stdout, s->cfg.help_color, \"*          - %s\",\n \t\t\t _(\"choose all items\"));\n-\tcolor_fprintf_ln(stdout, s->help_color, \"           - %s\",\n+\tcolor_fprintf_ln(stdout, s->cfg.help_color, \"           - %s\",\n \t\t\t _(\"(empty) finish selecting\"));\n }\n \n@@ -1154,7 +1051,7 @@ static void print_command_item(int i, int selected UNUSED,\n \n static void command_prompt_help(struct add_i_state *s)\n {\n-\tconst char *help_color = s->help_color;\n+\tconst char *help_color = s->cfg.help_color;\n \tcolor_fprintf_ln(stdout, help_color, \"%s\", _(\"Prompt help:\"));\n \tcolor_fprintf_ln(stdout, help_color, \"1          - %s\",\n \t\t\t _(\"select a numbered item\"));\n@@ -1165,7 +1062,7 @@ static void command_prompt_help(struct add_i_state *s)\n }\n \n int run_add_i(struct repository *r, const struct pathspec *ps,\n-\t      struct add_p_opt *add_p_opt)\n+\t      struct interactive_options *interactive_opts)\n {\n \tstruct add_i_state s = { NULL };\n \tstruct print_command_item_data data = { \"[\", \"]\" };\n@@ -1208,15 +1105,15 @@ int run_add_i(struct repository *r, const struct pathspec *ps,\n \t\t\t->util = util;\n \t}\n \n-\tinit_add_i_state(&s, r, add_p_opt);\n+\tinit_add_i_state(&s, r, interactive_opts);\n \n \t/*\n \t * When color was asked for, use the prompt color for\n \t * highlighting, otherwise use square brackets.\n \t */\n-\tif (want_color(s.use_color_interactive)) {\n-\t\tdata.color = s.prompt_color;\n-\t\tdata.reset = s.reset_color_interactive;\n+\tif (want_color(s.cfg.use_color_interactive)) {\n+\t\tdata.color = s.cfg.prompt_color;\n+\t\tdata.reset = s.cfg.reset_color_interactive;\n \t}\n \tprint_file_item_data.color = data.color;\n \tprint_file_item_data.reset = data.reset;\ndiff --git a/add-interactive.h b/add-interactive.h\nindex 6c62489bfe..eefa2edc7c 100644\n--- a/add-interactive.h\n+++ b/add-interactive.h\n@@ -2,38 +2,20 @@\n #define ADD_INTERACTIVE_H\n \n #include \"add-patch.h\"\n-#include \"color.h\"\n \n struct pathspec;\n struct repository;\n \n struct add_i_state {\n \tstruct repository *r;\n-\tenum git_colorbool use_color_interactive;\n-\tenum git_colorbool use_color_diff;\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_interactive[COLOR_MAXLEN];\n-\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-\tchar reset_color_diff[COLOR_MAXLEN];\n-\n-\tint use_single_key;\n-\tchar *interactive_diff_filter, *interactive_diff_algorithm;\n-\tint context, interhunkcontext;\n-\tint auto_advance;\n+\tstruct interactive_config cfg;\n };\n \n void init_add_i_state(struct add_i_state *s, struct repository *r,\n-\t\t      struct add_p_opt *add_p_opt);\n+\t\t      struct interactive_options *opts);\n void clear_add_i_state(struct add_i_state *s);\n \n int run_add_i(struct repository *r, const struct pathspec *ps,\n-\t      struct add_p_opt *add_p_opt);\n+\t      struct interactive_options *opts);\n \n #endif\ndiff --git a/add-patch.c b/add-patch.c\nindex 8ce2fc02f6..756143eb84 100644\n--- a/add-patch.c\n+++ b/add-patch.c\n@@ -5,6 +5,8 @@\n #include \"add-interactive.h\"\n #include \"add-patch.h\"\n #include \"advice.h\"\n+#include \"config.h\"\n+#include \"diff.h\"\n #include \"editor.h\"\n #include \"environment.h\"\n #include \"gettext.h\"\n@@ -279,6 +281,123 @@ struct add_p_state {\n \tconst char *revision;\n };\n \n+static void init_color(struct repository *r,\n+\t\t       enum git_colorbool use_color,\n+\t\t       const char *section_and_slot, char *dst,\n+\t\t       const char *default_color)\n+{\n+\tchar *key = xstrfmt(\"color.%s\", section_and_slot);\n+\tconst char *value;\n+\n+\tif (!want_color(use_color))\n+\t\tdst[0] = '\\0';\n+\telse if (repo_config_get_value(r, key, &value) ||\n+\t\t color_parse(value, dst))\n+\t\tstrlcpy(dst, default_color, COLOR_MAXLEN);\n+\n+\tfree(key);\n+}\n+\n+static enum git_colorbool check_color_config(struct repository *r, const char *var)\n+{\n+\tconst char *value;\n+\tenum git_colorbool ret;\n+\n+\tif (repo_config_get_value(r, var, &value))\n+\t\tret = GIT_COLOR_UNKNOWN;\n+\telse\n+\t\tret = git_config_colorbool(var, value);\n+\n+\t/*\n+\t * Do not rely on want_color() to fall back to color.ui for us. It uses\n+\t * the value parsed by git_color_config(), which may not have been\n+\t * called by the main command.\n+\t */\n+\tif (ret == GIT_COLOR_UNKNOWN &&\n+\t    !repo_config_get_value(r, \"color.ui\", &value))\n+\t\tret = git_config_colorbool(\"color.ui\", value);\n+\n+\treturn ret;\n+}\n+\n+void interactive_config_init(struct interactive_config *cfg,\n+\t\t\t     struct repository *r,\n+\t\t\t     struct interactive_options *opts)\n+{\n+\tcfg->context = -1;\n+\tcfg->interhunkcontext = -1;\n+\tcfg->auto_advance = opts->auto_advance;\n+\n+\tcfg->use_color_interactive = check_color_config(r, \"color.interactive\");\n+\n+\tinit_color(r, cfg->use_color_interactive, \"interactive.header\",\n+\t\t   cfg->header_color, GIT_COLOR_BOLD);\n+\tinit_color(r, cfg->use_color_interactive, \"interactive.help\",\n+\t\t   cfg->help_color, GIT_COLOR_BOLD_RED);\n+\tinit_color(r, cfg->use_color_interactive, \"interactive.prompt\",\n+\t\t   cfg->prompt_color, GIT_COLOR_BOLD_BLUE);\n+\tinit_color(r, cfg->use_color_interactive, \"interactive.error\",\n+\t\t   cfg->error_color, GIT_COLOR_BOLD_RED);\n+\tstrlcpy(cfg->reset_color_interactive,\n+\t\twant_color(cfg->use_color_interactive) ? GIT_COLOR_RESET : \"\", COLOR_MAXLEN);\n+\n+\tcfg->use_color_diff = check_color_config(r, \"color.diff\");\n+\n+\tinit_color(r, cfg->use_color_diff, \"diff.frag\", cfg->fraginfo_color,\n+\t\t   diff_get_color(cfg->use_color_diff, DIFF_FRAGINFO));\n+\tinit_color(r, cfg->use_color_diff, \"diff.context\", cfg->context_color,\n+\t\t   \"fall back\");\n+\tif (!strcmp(cfg->context_color, \"fall back\"))\n+\t\tinit_color(r, cfg->use_color_diff, \"diff.plain\",\n+\t\t\t   cfg->context_color,\n+\t\t\t   diff_get_color(cfg->use_color_diff, DIFF_CONTEXT));\n+\tinit_color(r, cfg->use_color_diff, \"diff.old\", cfg->file_old_color,\n+\t\tdiff_get_color(cfg->use_color_diff, DIFF_FILE_OLD));\n+\tinit_color(r, cfg->use_color_diff, \"diff.new\", cfg->file_new_color,\n+\t\tdiff_get_color(cfg->use_color_diff, DIFF_FILE_NEW));\n+\tstrlcpy(cfg->reset_color_diff,\n+\t\twant_color(cfg->use_color_diff) ? GIT_COLOR_RESET : \"\", COLOR_MAXLEN);\n+\n+\tFREE_AND_NULL(cfg->interactive_diff_filter);\n+\trepo_config_get_string(r, \"interactive.difffilter\",\n+\t\t\t       &cfg->interactive_diff_filter);\n+\n+\tFREE_AND_NULL(cfg->interactive_diff_algorithm);\n+\trepo_config_get_string(r, \"diff.algorithm\",\n+\t\t\t       &cfg->interactive_diff_algorithm);\n+\n+\tif (!repo_config_get_int(r, \"diff.context\", &cfg->context))\n+\t\tif (cfg->context < 0)\n+\t\t\tdie(_(\"%s cannot be negative\"), \"diff.context\");\n+\tif (!repo_config_get_int(r, \"diff.interHunkContext\", &cfg->interhunkcontext))\n+\t\tif (cfg->interhunkcontext < 0)\n+\t\t\tdie(_(\"%s cannot be negative\"), \"diff.interHunkContext\");\n+\n+\trepo_config_get_bool(r, \"interactive.singlekey\", &cfg->use_single_key);\n+\tif (cfg->use_single_key)\n+\t\tsetbuf(stdin, NULL);\n+\n+\tif (opts->context != -1) {\n+\t\tif (opts->context < 0)\n+\t\t\tdie(_(\"%s cannot be negative\"), \"--unified\");\n+\t\tcfg->context = opts->context;\n+\t}\n+\tif (opts->interhunkcontext != -1) {\n+\t\tif (opts->interhunkcontext < 0)\n+\t\t\tdie(_(\"%s cannot be negative\"), \"--inter-hunk-context\");\n+\t\tcfg->interhunkcontext = opts->interhunkcontext;\n+\t}\n+}\n+\n+void interactive_config_clear(struct interactive_config *cfg)\n+{\n+\tFREE_AND_NULL(cfg->interactive_diff_filter);\n+\tFREE_AND_NULL(cfg->interactive_diff_algorithm);\n+\tmemset(cfg, 0, sizeof(*cfg));\n+\tcfg->use_color_interactive = GIT_COLOR_UNKNOWN;\n+\tcfg->use_color_diff = GIT_COLOR_UNKNOWN;\n+}\n+\n static void add_p_state_clear(struct add_p_state *s)\n {\n \tsize_t i;\n@@ -299,9 +418,9 @@ static void err(struct add_p_state *s, const char *fmt, ...)\n \tva_list args;\n \n \tva_start(args, fmt);\n-\tfputs(s->s.error_color, stdout);\n+\tfputs(s->s.cfg.error_color, stdout);\n \tvprintf(fmt, args);\n-\tputs(s->s.reset_color_interactive);\n+\tputs(s->s.cfg.reset_color_interactive);\n \tva_end(args);\n }\n \n@@ -424,12 +543,12 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps)\n \tint res;\n \n \tstrvec_pushv(&args, s->mode->diff_cmd);\n-\tif (s->s.context != -1)\n-\t\tstrvec_pushf(&args, \"--unified=%i\", s->s.context);\n-\tif (s->s.interhunkcontext != -1)\n-\t\tstrvec_pushf(&args, \"--inter-hunk-context=%i\", s->s.interhunkcontext);\n-\tif (s->s.interactive_diff_algorithm)\n-\t\tstrvec_pushf(&args, \"--diff-algorithm=%s\", s->s.interactive_diff_algorithm);\n+\tif (s->s.cfg.context != -1)\n+\t\tstrvec_pushf(&args, \"--unified=%i\", s->s.cfg.context);\n+\tif (s->s.cfg.interhunkcontext != -1)\n+\t\tstrvec_pushf(&args, \"--inter-hunk-context=%i\", s->s.cfg.interhunkcontext);\n+\tif (s->s.cfg.interactive_diff_algorithm)\n+\t\tstrvec_pushf(&args, \"--diff-algorithm=%s\", s->s.cfg.interactive_diff_algorithm);\n \tif (s->revision) {\n \t\tstruct object_id oid;\n \t\tstrvec_push(&args,\n@@ -458,9 +577,9 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps)\n \t}\n \tstrbuf_complete_line(plain);\n \n-\tif (want_color_fd(1, s->s.use_color_diff)) {\n+\tif (want_color_fd(1, s->s.cfg.use_color_diff)) {\n \t\tstruct child_process colored_cp = CHILD_PROCESS_INIT;\n-\t\tconst char *diff_filter = s->s.interactive_diff_filter;\n+\t\tconst char *diff_filter = s->s.cfg.interactive_diff_filter;\n \n \t\tsetup_child_process(s, &colored_cp, NULL);\n \t\txsnprintf((char *)args.v[color_arg_index], 8, \"--color\");\n@@ -693,7 +812,7 @@ static void render_hunk(struct add_p_state *s, struct hunk *hunk,\n \t\t\t\t   hunk->colored_end - hunk->colored_start);\n \t\t\treturn;\n \t\t} else {\n-\t\t\tstrbuf_addstr(out, s->s.fraginfo_color);\n+\t\t\tstrbuf_addstr(out, s->s.cfg.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@@ -715,7 +834,7 @@ static void render_hunk(struct add_p_state *s, struct hunk *hunk,\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\", s->s.reset_color_diff);\n+\t\t\tstrbuf_addf(out, \"%s\\n\", s->s.cfg.reset_color_diff);\n \t\telse\n \t\t\tstrbuf_addch(out, '\\n');\n \t}\n@@ -1104,12 +1223,12 @@ static void recolor_hunk(struct add_p_state *s, struct hunk *hunk)\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      s->s.cfg.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\t\t      s->s.cfg.file_new_color :\n+\t\t\t      s->s.cfg.context_color);\n \t\tstrbuf_add(&s->colored, plain + current, eol - current);\n-\t\tstrbuf_addstr(&s->colored, s->s.reset_color_diff);\n+\t\tstrbuf_addstr(&s->colored, s->s.cfg.reset_color_diff);\n \t\tif (next > eol)\n \t\t\tstrbuf_add(&s->colored, plain + eol, next - eol);\n \t\tcurrent = next;\n@@ -1238,7 +1357,7 @@ static int run_apply_check(struct add_p_state *s,\n \n static int read_single_character(struct add_p_state *s)\n {\n-\tif (s->s.use_single_key) {\n+\tif (s->s.cfg.use_single_key) {\n \t\tint res = read_key_without_echo(&s->answer);\n \t\tprintf(\"%s\\n\", res == EOF ? \"\" : s->answer.buf);\n \t\treturn res;\n@@ -1252,7 +1371,7 @@ static int read_single_character(struct add_p_state *s)\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\tcolor_fprintf(stdout, s->s.cfg.prompt_color, \"%s\", _(prompt));\n \t\tfflush(stdout);\n \t\tif (read_single_character(s) == EOF)\n \t\t\treturn -1;\n@@ -1541,7 +1660,7 @@ static size_t patch_update_file(struct add_p_state *s, size_t idx)\n \t\t/* Everything decided? */\n \t\tif (undecided_previous < 0 && undecided_next < 0 &&\n \t\t    hunk->use != UNDECIDED_HUNK) {\n-\t\t\t\tif (!s->s.auto_advance)\n+\t\t\t\tif (!s->s.cfg.auto_advance)\n \t\t\t\t\tall_decided = 1;\n \t\t\t\telse {\n \t\t\t\t\tpatch_update_resp++;\n@@ -1595,11 +1714,11 @@ static size_t patch_update_file(struct add_p_state *s, size_t idx)\n \t\t\t\tpermitted |= ALLOW_EDIT;\n \t\t\t\tstrbuf_addstr(&s->buf, \",e\");\n \t\t\t}\n-\t\t\tif (!s->s.auto_advance && s->file_diff_nr > 1) {\n+\t\t\tif (!s->s.cfg.auto_advance && s->file_diff_nr > 1) {\n \t\t\t\tpermitted |= ALLOW_GOTO_NEXT_FILE;\n \t\t\t\tstrbuf_addstr(&s->buf, \",>\");\n \t\t\t}\n-\t\t\tif (!s->s.auto_advance && s->file_diff_nr > 1) {\n+\t\t\tif (!s->s.cfg.auto_advance && s->file_diff_nr > 1) {\n \t\t\t\tpermitted |= ALLOW_GOTO_PREVIOUS_FILE;\n \t\t\t\tstrbuf_addstr(&s->buf, \",<\");\n \t\t\t}\n@@ -1614,7 +1733,7 @@ static size_t patch_update_file(struct add_p_state *s, size_t idx)\n \t\telse\n \t\t\tprompt_mode_type = PROMPT_HUNK;\n \n-\t\tprintf(\"%s(%\"PRIuMAX\"/%\"PRIuMAX\") \", s->s.prompt_color,\n+\t\tprintf(\"%s(%\"PRIuMAX\"/%\"PRIuMAX\") \", s->s.cfg.prompt_color,\n \t\t\t      (uintmax_t)hunk_index + 1,\n \t\t\t      (uintmax_t)(file_diff->hunk_nr\n \t\t\t\t\t\t? file_diff->hunk_nr\n@@ -1627,8 +1746,8 @@ static size_t patch_update_file(struct add_p_state *s, size_t idx)\n \t\t}\n \t\tprintf(_(s->mode->prompt_mode[prompt_mode_type]),\n \t\t\thunk_use_decision, s->buf.buf);\n-\t\tif (*s->s.reset_color_interactive)\n-\t\t\tfputs(s->s.reset_color_interactive, stdout);\n+\t\tif (*s->s.cfg.reset_color_interactive)\n+\t\t\tfputs(s->s.cfg.reset_color_interactive, stdout);\n \t\tfflush(stdout);\n \t\tif (read_single_character(s) == EOF) {\n \t\t\tpatch_update_resp = s->file_diff_nr;\n@@ -1679,7 +1798,7 @@ static size_t patch_update_file(struct add_p_state *s, size_t idx)\n \t\t} else if (ch == 'q') {\n \t\t\tpatch_update_resp = s->file_diff_nr;\n \t\t\tbreak;\n-\t\t} else if (!s->s.auto_advance && s->answer.buf[0] == '>') {\n+\t\t} else if (!s->s.cfg.auto_advance && s->answer.buf[0] == '>') {\n \t\t\tif (permitted & ALLOW_GOTO_NEXT_FILE) {\n \t\t\t\tif (patch_update_resp == s->file_diff_nr - 1)\n \t\t\t\t\tpatch_update_resp = 0;\n@@ -1690,7 +1809,7 @@ static size_t patch_update_file(struct add_p_state *s, size_t idx)\n \t\t\t\terr(s, _(\"No next file\"));\n \t\t\t\tcontinue;\n \t\t\t}\n-\t\t} else if (!s->s.auto_advance && s->answer.buf[0] == '<') {\n+\t\t} else if (!s->s.cfg.auto_advance && s->answer.buf[0] == '<') {\n \t\t\tif (permitted & ALLOW_GOTO_PREVIOUS_FILE) {\n \t\t\t\tif (patch_update_resp == 0)\n \t\t\t\t\tpatch_update_resp = s->file_diff_nr - 1;\n@@ -1813,7 +1932,7 @@ static size_t patch_update_file(struct add_p_state *s, size_t idx)\n \t\t\t\terr(s, _(\"Sorry, cannot split this hunk\"));\n \t\t\t} else 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\tcolor_fprintf_ln(stdout, s->s.cfg.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\t\trendered_hunk_index = -1;\n@@ -1831,7 +1950,7 @@ static size_t patch_update_file(struct add_p_state *s, size_t idx)\n \t\t} else if (s->answer.buf[0] == '?') {\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\tcolor_fprintf(stdout, s->s.cfg.help_color, \"%s\",\n \t\t\t\t      _(s->mode->help_patch_text));\n \n \t\t\t/*\n@@ -1855,13 +1974,13 @@ static size_t patch_update_file(struct add_p_state *s, size_t idx)\n \t\t\t\t\t\tif (file_diff->hunk[i].use == SKIP_HUNK)\n \t\t\t\t\t\t\tskipped += 1;\n \t\t\t\t\t}\n-\t\t\t\t\tcolor_fprintf_ln(stdout, s->s.help_color, _(p),\n+\t\t\t\t\tcolor_fprintf_ln(stdout, s->s.cfg.help_color, _(p),\n \t\t\t\t\t\t\t total, used, skipped);\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\tcolor_fprintf_ln(stdout, s->s.cfg.help_color,\n \t\t\t\t\t\t \"%.*s\", (int)(eol - p), p);\n \t\t\t}\n \t\t} else {\n@@ -1870,7 +1989,7 @@ static size_t patch_update_file(struct add_p_state *s, size_t idx)\n \t\t}\n \t}\n \n-\tif (s->s.auto_advance)\n+\tif (s->s.cfg.auto_advance)\n \t\tapply_patch(s, file_diff);\n \n \tputchar('\\n');\n@@ -1878,7 +1997,7 @@ static size_t patch_update_file(struct add_p_state *s, size_t idx)\n }\n \n int run_add_p(struct repository *r, enum add_p_mode mode,\n-\t      struct add_p_opt *o, const char *revision,\n+\t      struct interactive_options *opts, const char *revision,\n \t      const struct pathspec *ps)\n {\n \tstruct add_p_state s = {\n@@ -1886,7 +2005,7 @@ int run_add_p(struct repository *r, enum add_p_mode mode,\n \t};\n \tsize_t i, binary_count = 0;\n \n-\tinit_add_i_state(&s.s, r, o);\n+\tinit_add_i_state(&s.s, r, opts);\n \n \tif (mode == ADD_P_STASH)\n \t\ts.mode = &patch_mode_stash;\n@@ -1932,7 +2051,7 @@ int run_add_p(struct repository *r, enum add_p_mode mode,\n \t\t if ((i = patch_update_file(&s, i)) == s.file_diff_nr)\n \t\t\tbreak;\n     }\n-\tif (!s.s.auto_advance)\n+\tif (!s.s.cfg.auto_advance)\n \t\tfor (i = 0; i < s.file_diff_nr; i++)\n \t\t\tapply_patch(&s, s.file_diff + i);\n \ndiff --git a/add-patch.h b/add-patch.h\nindex 88b00ca788..e6868c60a2 100644\n--- a/add-patch.h\n+++ b/add-patch.h\n@@ -1,16 +1,48 @@\n #ifndef ADD_PATCH_H\n #define ADD_PATCH_H\n \n+#include \"color.h\"\n+\n struct pathspec;\n struct repository;\n \n-struct add_p_opt {\n+struct interactive_options {\n \tint context;\n \tint interhunkcontext;\n \tint auto_advance;\n };\n \n-#define ADD_P_OPT_INIT { .context = -1, .interhunkcontext = -1, .auto_advance = 1 }\n+#define INTERACTIVE_OPTIONS_INIT { \\\n+\t.context = -1, \\\n+\t.interhunkcontext = -1, \\\n+\t.auto_advance = 1, \\\n+}\n+\n+struct interactive_config {\n+\tenum git_colorbool use_color_interactive;\n+\tenum git_colorbool use_color_diff;\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_interactive[COLOR_MAXLEN];\n+\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+\tchar reset_color_diff[COLOR_MAXLEN];\n+\n+\tint use_single_key;\n+\tchar *interactive_diff_filter, *interactive_diff_algorithm;\n+\tint context, interhunkcontext;\n+\tint auto_advance;\n+};\n+\n+void interactive_config_init(struct interactive_config *cfg,\n+\t\t\t     struct repository *r,\n+\t\t\t     struct interactive_options *opts);\n+void interactive_config_clear(struct interactive_config *cfg);\n \n enum add_p_mode {\n \tADD_P_ADD,\n@@ -21,7 +53,7 @@ enum add_p_mode {\n };\n \n int run_add_p(struct repository *r, enum add_p_mode mode,\n-\t      struct add_p_opt *o, const char *revision,\n+\t      struct interactive_options *opts, const char *revision,\n \t      const struct pathspec *ps);\n \n #endif\ndiff --git a/builtin/add.c b/builtin/add.c\nindex 4357f87b7f..84f9bcb789 100644\n--- a/builtin/add.c\n+++ b/builtin/add.c\n@@ -31,7 +31,7 @@ static const char * const builtin_add_usage[] = {\n \tNULL\n };\n static int patch_interactive, add_interactive, edit_interactive;\n-static struct add_p_opt add_p_opt = ADD_P_OPT_INIT;\n+static struct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;\n static int take_worktree_changes;\n static int add_renormalize;\n static int pathspec_file_nul;\n@@ -160,7 +160,7 @@ static int refresh(struct repository *repo, int verbose, const struct pathspec *\n int interactive_add(struct repository *repo,\n \t\t    const char **argv,\n \t\t    const char *prefix,\n-\t\t    int patch, struct add_p_opt *add_p_opt)\n+\t\t    int patch, struct interactive_options *interactive_opts)\n {\n \tstruct pathspec pathspec;\n \tint ret;\n@@ -172,9 +172,9 @@ int interactive_add(struct repository *repo,\n \t\t       prefix, argv);\n \n \tif (patch)\n-\t\tret = !!run_add_p(repo, ADD_P_ADD, add_p_opt, NULL, &pathspec);\n+\t\tret = !!run_add_p(repo, ADD_P_ADD, interactive_opts, NULL, &pathspec);\n \telse\n-\t\tret = !!run_add_i(repo, &pathspec, add_p_opt);\n+\t\tret = !!run_add_i(repo, &pathspec, interactive_opts);\n \n \tclear_pathspec(&pathspec);\n \treturn ret;\n@@ -256,10 +256,10 @@ static struct option builtin_add_options[] = {\n \tOPT_GROUP(\"\"),\n \tOPT_BOOL('i', \"interactive\", &add_interactive, N_(\"interactive picking\")),\n \tOPT_BOOL('p', \"patch\", &patch_interactive, N_(\"select hunks interactively\")),\n-\tOPT_BOOL(0, \"auto-advance\", &add_p_opt.auto_advance,\n+\tOPT_BOOL(0, \"auto-advance\", &interactive_opts.auto_advance,\n \t\t N_(\"auto advance to the next file when selecting hunks interactively\")),\n-\tOPT_DIFF_UNIFIED(&add_p_opt.context),\n-\tOPT_DIFF_INTERHUNK_CONTEXT(&add_p_opt.interhunkcontext),\n+\tOPT_DIFF_UNIFIED(&interactive_opts.context),\n+\tOPT_DIFF_INTERHUNK_CONTEXT(&interactive_opts.interhunkcontext),\n \tOPT_BOOL('e', \"edit\", &edit_interactive, N_(\"edit current diff and apply\")),\n \tOPT__FORCE(&ignored_too, N_(\"allow adding otherwise ignored files\"), 0),\n \tOPT_BOOL('u', \"update\", &take_worktree_changes, N_(\"update tracked files\")),\n@@ -402,9 +402,9 @@ int cmd_add(int argc,\n \tprepare_repo_settings(repo);\n \trepo->settings.command_requires_full_index = 0;\n \n-\tif (add_p_opt.context < -1)\n+\tif (interactive_opts.context < -1)\n \t\tdie(_(\"'%s' cannot be negative\"), \"--unified\");\n-\tif (add_p_opt.interhunkcontext < -1)\n+\tif (interactive_opts.interhunkcontext < -1)\n \t\tdie(_(\"'%s' cannot be negative\"), \"--inter-hunk-context\");\n \n \tif (patch_interactive)\n@@ -414,13 +414,13 @@ int cmd_add(int argc,\n \t\t\tdie(_(\"options '%s' and '%s' cannot be used together\"), \"--dry-run\", \"--interactive/--patch\");\n \t\tif (pathspec_from_file)\n \t\t\tdie(_(\"options '%s' and '%s' cannot be used together\"), \"--pathspec-from-file\", \"--interactive/--patch\");\n-\t\texit(interactive_add(repo, argv + 1, prefix, patch_interactive, &add_p_opt));\n+\t\texit(interactive_add(repo, argv + 1, prefix, patch_interactive, &interactive_opts));\n \t} else {\n-\t\tif (add_p_opt.context != -1)\n+\t\tif (interactive_opts.context != -1)\n \t\t\tdie(_(\"the option '%s' requires '%s'\"), \"--unified\", \"--interactive/--patch\");\n-\t\tif (add_p_opt.interhunkcontext != -1)\n+\t\tif (interactive_opts.interhunkcontext != -1)\n \t\t\tdie(_(\"the option '%s' requires '%s'\"), \"--inter-hunk-context\", \"--interactive/--patch\");\n-\t\tif (!add_p_opt.auto_advance)\n+\t\tif (!interactive_opts.auto_advance)\n \t\t\tdie(_(\"the option '%s' requires '%s'\"), \"--no-auto-advance\", \"--interactive/--patch\");\n \t}\n \ndiff --git a/builtin/checkout.c b/builtin/checkout.c\nindex eefefd5d0f..bebe18c1d9 100644\n--- a/builtin/checkout.c\n+++ b/builtin/checkout.c\n@@ -532,7 +532,7 @@ static int checkout_paths(const struct checkout_opts *opts,\n \n \tif (opts->patch_mode) {\n \t\tenum add_p_mode patch_mode;\n-\t\tstruct add_p_opt add_p_opt = {\n+\t\tstruct interactive_options interactive_opts = {\n \t\t\t.context = opts->patch_context,\n \t\t\t.interhunkcontext = opts->patch_interhunk_context,\n \t\t\t.auto_advance = opts->auto_advance\n@@ -562,7 +562,7 @@ static int checkout_paths(const struct checkout_opts *opts,\n \t\telse\n \t\t\tBUG(\"either flag must have been set, worktree=%d, index=%d\",\n \t\t\t    opts->checkout_worktree, opts->checkout_index);\n-\t\treturn !!run_add_p(the_repository, patch_mode, &add_p_opt,\n+\t\treturn !!run_add_p(the_repository, patch_mode, &interactive_opts,\n \t\t\t\t   rev, &opts->pathspec);\n \t}\n \ndiff --git a/builtin/commit.c b/builtin/commit.c\nindex 9e3a09d532..a1d64dd699 100644\n--- a/builtin/commit.c\n+++ b/builtin/commit.c\n@@ -123,7 +123,7 @@ static const char *edit_message, *use_message;\n static char *fixup_message, *fixup_commit, *squash_message;\n static const char *fixup_prefix;\n static int all, also, interactive, patch_interactive, only, amend, signoff;\n-static struct add_p_opt add_p_opt = ADD_P_OPT_INIT;\n+static struct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;\n static int edit_flag = -1; /* unspecified */\n static int quiet, verbose, no_verify, allow_empty, dry_run, renew_authorship;\n static int config_commit_verbose = -1; /* unspecified */\n@@ -357,9 +357,9 @@ static const char *prepare_index(const char **argv, const char *prefix,\n \tconst char *ret;\n \tchar *path = NULL;\n \n-\tif (add_p_opt.context < -1)\n+\tif (interactive_opts.context < -1)\n \t\tdie(_(\"'%s' cannot be negative\"), \"--unified\");\n-\tif (add_p_opt.interhunkcontext < -1)\n+\tif (interactive_opts.interhunkcontext < -1)\n \t\tdie(_(\"'%s' cannot be negative\"), \"--inter-hunk-context\");\n \n \tif (is_status)\n@@ -408,7 +408,7 @@ static const char *prepare_index(const char **argv, const char *prefix,\n \t\told_index_env = xstrdup_or_null(getenv(INDEX_ENVIRONMENT));\n \t\tsetenv(INDEX_ENVIRONMENT, the_repository->index_file, 1);\n \n-\t\tif (interactive_add(the_repository, argv, prefix, patch_interactive, &add_p_opt) != 0)\n+\t\tif (interactive_add(the_repository, argv, prefix, patch_interactive, &interactive_opts) != 0)\n \t\t\tdie(_(\"interactive add failed\"));\n \n \t\tthe_repository->index_file = old_repo_index_file;\n@@ -433,9 +433,9 @@ static const char *prepare_index(const char **argv, const char *prefix,\n \t\tret = get_lock_file_path(&index_lock);\n \t\tgoto out;\n \t} else {\n-\t\tif (add_p_opt.context != -1)\n+\t\tif (interactive_opts.context != -1)\n \t\t\tdie(_(\"the option '%s' requires '%s'\"), \"--unified\", \"--interactive/--patch\");\n-\t\tif (add_p_opt.interhunkcontext != -1)\n+\t\tif (interactive_opts.interhunkcontext != -1)\n \t\t\tdie(_(\"the option '%s' requires '%s'\"), \"--inter-hunk-context\", \"--interactive/--patch\");\n \t}\n \n@@ -1743,8 +1743,8 @@ int cmd_commit(int argc,\n \t\tOPT_BOOL('i', \"include\", &also, N_(\"add specified files to index for commit\")),\n \t\tOPT_BOOL(0, \"interactive\", &interactive, N_(\"interactively add files\")),\n \t\tOPT_BOOL('p', \"patch\", &patch_interactive, N_(\"interactively add changes\")),\n-\t\tOPT_DIFF_UNIFIED(&add_p_opt.context),\n-\t\tOPT_DIFF_INTERHUNK_CONTEXT(&add_p_opt.interhunkcontext),\n+\t\tOPT_DIFF_UNIFIED(&interactive_opts.context),\n+\t\tOPT_DIFF_INTERHUNK_CONTEXT(&interactive_opts.interhunkcontext),\n \t\tOPT_BOOL('o', \"only\", &only, N_(\"commit only specified files\")),\n \t\tOPT_BOOL('n', \"no-verify\", &no_verify, N_(\"bypass pre-commit and commit-msg hooks\")),\n \t\tOPT_BOOL(0, \"dry-run\", &dry_run, N_(\"show what would be committed\")),\ndiff --git a/builtin/reset.c b/builtin/reset.c\nindex 88f95f9fc7..4a74a82c0a 100644\n--- a/builtin/reset.c\n+++ b/builtin/reset.c\n@@ -346,7 +346,7 @@ int cmd_reset(int argc,\n \tstruct object_id oid;\n \tstruct pathspec pathspec;\n \tint intent_to_add = 0;\n-\tstruct add_p_opt add_p_opt = ADD_P_OPT_INIT;\n+\tstruct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;\n \tconst struct option options[] = {\n \t\tOPT__QUIET(&quiet, N_(\"be quiet, only report errors\")),\n \t\tOPT_BOOL(0, \"no-refresh\", &no_refresh,\n@@ -371,10 +371,10 @@ int cmd_reset(int argc,\n \t\t\t       PARSE_OPT_OPTARG,\n \t\t\t       option_parse_recurse_submodules_worktree_updater),\n \t\tOPT_BOOL('p', \"patch\", &patch_mode, N_(\"select hunks interactively\")),\n-\t\tOPT_BOOL(0, \"auto-advance\", &add_p_opt.auto_advance,\n+\t\tOPT_BOOL(0, \"auto-advance\", &interactive_opts.auto_advance,\n \t\t\t N_(\"auto advance to the next file when selecting hunks interactively\")),\n-\t\tOPT_DIFF_UNIFIED(&add_p_opt.context),\n-\t\tOPT_DIFF_INTERHUNK_CONTEXT(&add_p_opt.interhunkcontext),\n+\t\tOPT_DIFF_UNIFIED(&interactive_opts.context),\n+\t\tOPT_DIFF_INTERHUNK_CONTEXT(&interactive_opts.interhunkcontext),\n \t\tOPT_BOOL('N', \"intent-to-add\", &intent_to_add,\n \t\t\t\tN_(\"record only the fact that removed paths will be added later\")),\n \t\tOPT_PATHSPEC_FROM_FILE(&pathspec_from_file),\n@@ -425,9 +425,9 @@ int cmd_reset(int argc,\n \t\toidcpy(&oid, &tree->object.oid);\n \t}\n \n-\tif (add_p_opt.context < -1)\n+\tif (interactive_opts.context < -1)\n \t\tdie(_(\"'%s' cannot be negative\"), \"--unified\");\n-\tif (add_p_opt.interhunkcontext < -1)\n+\tif (interactive_opts.interhunkcontext < -1)\n \t\tdie(_(\"'%s' cannot be negative\"), \"--inter-hunk-context\");\n \n \tprepare_repo_settings(the_repository);\n@@ -438,14 +438,14 @@ int cmd_reset(int argc,\n \t\t\tdie(_(\"options '%s' and '%s' cannot be used together\"), \"--patch\", \"--{hard,mixed,soft}\");\n \t\ttrace2_cmd_mode(\"patch-interactive\");\n \t\tupdate_ref_status = !!run_add_p(the_repository, ADD_P_RESET,\n-\t\t\t\t\t\t&add_p_opt, rev, &pathspec);\n+\t\t\t\t\t\t&interactive_opts, rev, &pathspec);\n \t\tgoto cleanup;\n \t} else {\n-\t\tif (add_p_opt.context != -1)\n+\t\tif (interactive_opts.context != -1)\n \t\t\tdie(_(\"the option '%s' requires '%s'\"), \"--unified\", \"--patch\");\n-\t\tif (add_p_opt.interhunkcontext != -1)\n+\t\tif (interactive_opts.interhunkcontext != -1)\n \t\t\tdie(_(\"the option '%s' requires '%s'\"), \"--inter-hunk-context\", \"--patch\");\n-\t\tif (!add_p_opt.auto_advance)\n+\t\tif (!interactive_opts.auto_advance)\n \t\t\tdie(_(\"the option '%s' requires '%s'\"), \"--no-auto-advance\", \"--patch\");\n \t}\n \ndiff --git a/builtin/stash.c b/builtin/stash.c\nindex e79d612e57..c467c02c7f 100644\n--- a/builtin/stash.c\n+++ b/builtin/stash.c\n@@ -1306,7 +1306,7 @@ static int stash_staged(struct stash_info *info, struct strbuf *out_patch,\n \n static int stash_patch(struct stash_info *info, const struct pathspec *ps,\n \t\t       struct strbuf *out_patch, int quiet,\n-\t\t       struct add_p_opt *add_p_opt)\n+\t\t       struct interactive_options *interactive_opts)\n {\n \tint ret = 0;\n \tstruct child_process cp_read_tree = CHILD_PROCESS_INIT;\n@@ -1331,7 +1331,7 @@ static int stash_patch(struct stash_info *info, const struct pathspec *ps,\n \told_index_env = xstrdup_or_null(getenv(INDEX_ENVIRONMENT));\n \tsetenv(INDEX_ENVIRONMENT, the_repository->index_file, 1);\n \n-\tret = !!run_add_p(the_repository, ADD_P_STASH, add_p_opt, NULL, ps);\n+\tret = !!run_add_p(the_repository, ADD_P_STASH, interactive_opts, NULL, ps);\n \n \tthe_repository->index_file = old_repo_index_file;\n \tif (old_index_env && *old_index_env)\n@@ -1427,7 +1427,8 @@ static int stash_working_tree(struct stash_info *info, const struct pathspec *ps\n }\n \n static int do_create_stash(const struct pathspec *ps, struct strbuf *stash_msg_buf,\n-\t\t\t   int include_untracked, int patch_mode, struct add_p_opt *add_p_opt,\n+\t\t\t   int include_untracked, int patch_mode,\n+\t\t\t   struct interactive_options *interactive_opts,\n \t\t\t   int only_staged, struct stash_info *info, struct strbuf *patch,\n \t\t\t   int quiet)\n {\n@@ -1509,7 +1510,7 @@ static int do_create_stash(const struct pathspec *ps, struct strbuf *stash_msg_b\n \t\tuntracked_commit_option = 1;\n \t}\n \tif (patch_mode) {\n-\t\tret = stash_patch(info, ps, patch, quiet, add_p_opt);\n+\t\tret = stash_patch(info, ps, patch, quiet, interactive_opts);\n \t\tif (ret < 0) {\n \t\t\tif (!quiet)\n \t\t\t\tfprintf_ln(stderr, _(\"Cannot save the current \"\n@@ -1595,7 +1596,8 @@ static int create_stash(int argc, const char **argv, const char *prefix UNUSED,\n }\n \n static int do_push_stash(const struct pathspec *ps, const char *stash_msg, int quiet,\n-\t\t\t int keep_index, int patch_mode, struct add_p_opt *add_p_opt,\n+\t\t\t int keep_index, int patch_mode,\n+\t\t\t struct interactive_options *interactive_opts,\n \t\t\t int include_untracked, int only_staged)\n {\n \tint ret = 0;\n@@ -1667,7 +1669,7 @@ static int do_push_stash(const struct pathspec *ps, const char *stash_msg, int q\n \tif (stash_msg)\n \t\tstrbuf_addstr(&stash_msg_buf, stash_msg);\n \tif (do_create_stash(ps, &stash_msg_buf, include_untracked, patch_mode,\n-\t\t\t    add_p_opt, only_staged, &info, &patch, quiet)) {\n+\t\t\t    interactive_opts, only_staged, &info, &patch, quiet)) {\n \t\tret = -1;\n \t\tgoto done;\n \t}\n@@ -1841,7 +1843,7 @@ static int push_stash(int argc, const char **argv, const char *prefix,\n \tconst char *stash_msg = NULL;\n \tchar *pathspec_from_file = NULL;\n \tstruct pathspec ps;\n-\tstruct add_p_opt add_p_opt = ADD_P_OPT_INIT;\n+\tstruct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;\n \tstruct option options[] = {\n \t\tOPT_BOOL('k', \"keep-index\", &keep_index,\n \t\t\t N_(\"keep index\")),\n@@ -1849,10 +1851,10 @@ static int push_stash(int argc, const char **argv, const char *prefix,\n \t\t\t N_(\"stash staged changes only\")),\n \t\tOPT_BOOL('p', \"patch\", &patch_mode,\n \t\t\t N_(\"stash in patch mode\")),\n-\t\tOPT_BOOL(0, \"auto-advance\", &add_p_opt.auto_advance,\n+\t\tOPT_BOOL(0, \"auto-advance\", &interactive_opts.auto_advance,\n \t\t\t N_(\"auto advance to the next file when selecting hunks interactively\")),\n-\t\tOPT_DIFF_UNIFIED(&add_p_opt.context),\n-\t\tOPT_DIFF_INTERHUNK_CONTEXT(&add_p_opt.interhunkcontext),\n+\t\tOPT_DIFF_UNIFIED(&interactive_opts.context),\n+\t\tOPT_DIFF_INTERHUNK_CONTEXT(&interactive_opts.interhunkcontext),\n \t\tOPT__QUIET(&quiet, N_(\"quiet mode\")),\n \t\tOPT_BOOL('u', \"include-untracked\", &include_untracked,\n \t\t\t N_(\"include untracked files in stash\")),\n@@ -1909,21 +1911,21 @@ static int push_stash(int argc, const char **argv, const char *prefix,\n \t}\n \n \tif (!patch_mode) {\n-\t\tif (add_p_opt.context != -1)\n+\t\tif (interactive_opts.context != -1)\n \t\t\tdie(_(\"the option '%s' requires '%s'\"), \"--unified\", \"--patch\");\n-\t\tif (add_p_opt.interhunkcontext != -1)\n+\t\tif (interactive_opts.interhunkcontext != -1)\n \t\t\tdie(_(\"the option '%s' requires '%s'\"), \"--inter-hunk-context\", \"--patch\");\n-\t\tif (!add_p_opt.auto_advance)\n+\t\tif (!interactive_opts.auto_advance)\n \t\t\tdie(_(\"the option '%s' requires '%s'\"), \"--no-auto-advance\", \"--patch\");\n \t}\n \n-\tif (add_p_opt.context < -1)\n+\tif (interactive_opts.context < -1)\n \t\tdie(_(\"'%s' cannot be negative\"), \"--unified\");\n-\tif (add_p_opt.interhunkcontext < -1)\n+\tif (interactive_opts.interhunkcontext < -1)\n \t\tdie(_(\"'%s' cannot be negative\"), \"--inter-hunk-context\");\n \n \tret = do_push_stash(&ps, stash_msg, quiet, keep_index, patch_mode,\n-\t\t\t    &add_p_opt, include_untracked, only_staged);\n+\t\t\t    &interactive_opts, include_untracked, only_staged);\n \n \tclear_pathspec(&ps);\n \tfree(pathspec_from_file);\n@@ -1948,7 +1950,7 @@ static int save_stash(int argc, const char **argv, const char *prefix,\n \tconst char *stash_msg = NULL;\n \tstruct pathspec ps;\n \tstruct strbuf stash_msg_buf = STRBUF_INIT;\n-\tstruct add_p_opt add_p_opt = ADD_P_OPT_INIT;\n+\tstruct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;\n \tstruct option options[] = {\n \t\tOPT_BOOL('k', \"keep-index\", &keep_index,\n \t\t\t N_(\"keep index\")),\n@@ -1956,10 +1958,10 @@ static int save_stash(int argc, const char **argv, const char *prefix,\n \t\t\t N_(\"stash staged changes only\")),\n \t\tOPT_BOOL('p', \"patch\", &patch_mode,\n \t\t\t N_(\"stash in patch mode\")),\n-\t\tOPT_BOOL(0, \"auto-advance\", &add_p_opt.auto_advance,\n+\t\tOPT_BOOL(0, \"auto-advance\", &interactive_opts.auto_advance,\n \t\t\t N_(\"auto advance to the next file when selecting hunks interactively\")),\n-\t\tOPT_DIFF_UNIFIED(&add_p_opt.context),\n-\t\tOPT_DIFF_INTERHUNK_CONTEXT(&add_p_opt.interhunkcontext),\n+\t\tOPT_DIFF_UNIFIED(&interactive_opts.context),\n+\t\tOPT_DIFF_INTERHUNK_CONTEXT(&interactive_opts.interhunkcontext),\n \t\tOPT__QUIET(&quiet, N_(\"quiet mode\")),\n \t\tOPT_BOOL('u', \"include-untracked\", &include_untracked,\n \t\t\t N_(\"include untracked files in stash\")),\n@@ -1979,22 +1981,22 @@ static int save_stash(int argc, const char **argv, const char *prefix,\n \n \tmemset(&ps, 0, sizeof(ps));\n \n-\tif (add_p_opt.context < -1)\n+\tif (interactive_opts.context < -1)\n \t\tdie(_(\"'%s' cannot be negative\"), \"--unified\");\n-\tif (add_p_opt.interhunkcontext < -1)\n+\tif (interactive_opts.interhunkcontext < -1)\n \t\tdie(_(\"'%s' cannot be negative\"), \"--inter-hunk-context\");\n \n \tif (!patch_mode) {\n-\t\tif (add_p_opt.context != -1)\n+\t\tif (interactive_opts.context != -1)\n \t\t\tdie(_(\"the option '%s' requires '%s'\"), \"--unified\", \"--patch\");\n-\t\tif (add_p_opt.interhunkcontext != -1)\n+\t\tif (interactive_opts.interhunkcontext != -1)\n \t\t\tdie(_(\"the option '%s' requires '%s'\"), \"--inter-hunk-context\", \"--patch\");\n-\t\tif (!add_p_opt.auto_advance)\n+\t\tif (!interactive_opts.auto_advance)\n \t\t\tdie(_(\"the option '%s' requires '%s'\"), \"--no-auto-advance\", \"--patch\");\n \t}\n \n \tret = do_push_stash(&ps, stash_msg, quiet, keep_index,\n-\t\t\t    patch_mode, &add_p_opt, include_untracked,\n+\t\t\t    patch_mode, &interactive_opts, include_untracked,\n \t\t\t    only_staged);\n \n \tstrbuf_release(&stash_msg_buf);\ndiff --git a/commit.h b/commit.h\nindex 1635de418b..a2337a5af0 100644\n--- a/commit.h\n+++ b/commit.h\n@@ -277,7 +277,7 @@ int for_each_commit_graft(each_commit_graft_fn, void *);\n int interactive_add(struct repository *repo,\n \t\t    const char **argv,\n \t\t    const char *prefix,\n-\t\t    int patch, struct add_p_opt *add_p_opt);\n+\t\t    int patch, struct interactive_options *opts);\n \n struct commit_extra_header {\n \tstruct commit_extra_header *next;\n\n-- \n2.53.0.697.g625c4fb2da.dirty\n\n"},{"id":"537510","messageId":"20260302-pks-history-split-v1-3-444fc987a324@pks.im","threadId":"65116","inReplyTo":"20260302-pks-history-split-v1-0-444fc987a324@pks.im","subject":"[PATCH 3/8] add-patch: remove dependency on \"add-interactive\" subsystem","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-03-02T12:13:07Z","receivedAt":"2026-03-02T12:13:20Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"With the preceding commit we have split out interactive configuration\nthat is used by both \"git add -p\" and \"git add -i\". But we still\ninitialize that configuration in the \"add -p\" subsystem by calling\n`init_add_i_state()`, even though we only do so to initialize the\ninteractive configuration as well as a repository pointer.\n\nStop doing so and instead store and initialize the interactive\nconfiguration in `struct add_p_state` directly.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n add-patch.c | 88 ++++++++++++++++++++++++++++++++-----------------------------\n 1 file changed, 46 insertions(+), 42 deletions(-)\n\ndiff --git a/add-patch.c b/add-patch.c\nindex 756143eb84..4f089c82d0 100644\n--- a/add-patch.c\n+++ b/add-patch.c\n@@ -2,7 +2,6 @@\n #define DISABLE_SIGN_COMPARE_WARNINGS\n \n #include \"git-compat-util.h\"\n-#include \"add-interactive.h\"\n #include \"add-patch.h\"\n #include \"advice.h\"\n #include \"config.h\"\n@@ -263,7 +262,8 @@ struct hunk {\n };\n \n struct add_p_state {\n-\tstruct add_i_state s;\n+\tstruct repository *r;\n+\tstruct interactive_config cfg;\n \tstruct strbuf answer, buf;\n \n \t/* parsed diff */\n@@ -409,7 +409,7 @@ static void add_p_state_clear(struct add_p_state *s)\n \tfor (i = 0; i < s->file_diff_nr; i++)\n \t\tfree(s->file_diff[i].hunk);\n \tfree(s->file_diff);\n-\tclear_add_i_state(&s->s);\n+\tinteractive_config_clear(&s->cfg);\n }\n \n __attribute__((format (printf, 2, 3)))\n@@ -418,9 +418,9 @@ static void err(struct add_p_state *s, const char *fmt, ...)\n \tva_list args;\n \n \tva_start(args, fmt);\n-\tfputs(s->s.cfg.error_color, stdout);\n+\tfputs(s->cfg.error_color, stdout);\n \tvprintf(fmt, args);\n-\tputs(s->s.cfg.reset_color_interactive);\n+\tputs(s->cfg.reset_color_interactive);\n \tva_end(args);\n }\n \n@@ -438,7 +438,7 @@ static void setup_child_process(struct add_p_state *s,\n \n \tcp->git_cmd = 1;\n \tstrvec_pushf(&cp->env,\n-\t\t     INDEX_ENVIRONMENT \"=%s\", s->s.r->index_file);\n+\t\t     INDEX_ENVIRONMENT \"=%s\", s->r->index_file);\n }\n \n static int parse_range(const char **p,\n@@ -543,12 +543,12 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps)\n \tint res;\n \n \tstrvec_pushv(&args, s->mode->diff_cmd);\n-\tif (s->s.cfg.context != -1)\n-\t\tstrvec_pushf(&args, \"--unified=%i\", s->s.cfg.context);\n-\tif (s->s.cfg.interhunkcontext != -1)\n-\t\tstrvec_pushf(&args, \"--inter-hunk-context=%i\", s->s.cfg.interhunkcontext);\n-\tif (s->s.cfg.interactive_diff_algorithm)\n-\t\tstrvec_pushf(&args, \"--diff-algorithm=%s\", s->s.cfg.interactive_diff_algorithm);\n+\tif (s->cfg.context != -1)\n+\t\tstrvec_pushf(&args, \"--unified=%i\", s->cfg.context);\n+\tif (s->cfg.interhunkcontext != -1)\n+\t\tstrvec_pushf(&args, \"--inter-hunk-context=%i\", s->cfg.interhunkcontext);\n+\tif (s->cfg.interactive_diff_algorithm)\n+\t\tstrvec_pushf(&args, \"--diff-algorithm=%s\", s->cfg.interactive_diff_algorithm);\n \tif (s->revision) {\n \t\tstruct object_id oid;\n \t\tstrvec_push(&args,\n@@ -577,9 +577,9 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps)\n \t}\n \tstrbuf_complete_line(plain);\n \n-\tif (want_color_fd(1, s->s.cfg.use_color_diff)) {\n+\tif (want_color_fd(1, s->cfg.use_color_diff)) {\n \t\tstruct child_process colored_cp = CHILD_PROCESS_INIT;\n-\t\tconst char *diff_filter = s->s.cfg.interactive_diff_filter;\n+\t\tconst char *diff_filter = s->cfg.interactive_diff_filter;\n \n \t\tsetup_child_process(s, &colored_cp, NULL);\n \t\txsnprintf((char *)args.v[color_arg_index], 8, \"--color\");\n@@ -812,7 +812,7 @@ static void render_hunk(struct add_p_state *s, struct hunk *hunk,\n \t\t\t\t   hunk->colored_end - hunk->colored_start);\n \t\t\treturn;\n \t\t} else {\n-\t\t\tstrbuf_addstr(out, s->s.cfg.fraginfo_color);\n+\t\t\tstrbuf_addstr(out, s->cfg.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@@ -834,7 +834,7 @@ static void render_hunk(struct add_p_state *s, struct hunk *hunk,\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\", s->s.cfg.reset_color_diff);\n+\t\t\tstrbuf_addf(out, \"%s\\n\", s->cfg.reset_color_diff);\n \t\telse\n \t\t\tstrbuf_addch(out, '\\n');\n \t}\n@@ -1223,12 +1223,12 @@ static void recolor_hunk(struct add_p_state *s, struct hunk *hunk)\n \n \t\tstrbuf_addstr(&s->colored,\n \t\t\t      plain[current] == '-' ?\n-\t\t\t      s->s.cfg.file_old_color :\n+\t\t\t      s->cfg.file_old_color :\n \t\t\t      plain[current] == '+' ?\n-\t\t\t      s->s.cfg.file_new_color :\n-\t\t\t      s->s.cfg.context_color);\n+\t\t\t      s->cfg.file_new_color :\n+\t\t\t      s->cfg.context_color);\n \t\tstrbuf_add(&s->colored, plain + current, eol - current);\n-\t\tstrbuf_addstr(&s->colored, s->s.cfg.reset_color_diff);\n+\t\tstrbuf_addstr(&s->colored, s->cfg.reset_color_diff);\n \t\tif (next > eol)\n \t\t\tstrbuf_add(&s->colored, plain + eol, next - eol);\n \t\tcurrent = next;\n@@ -1357,7 +1357,7 @@ static int run_apply_check(struct add_p_state *s,\n \n static int read_single_character(struct add_p_state *s)\n {\n-\tif (s->s.cfg.use_single_key) {\n+\tif (s->cfg.use_single_key) {\n \t\tint res = read_key_without_echo(&s->answer);\n \t\tprintf(\"%s\\n\", res == EOF ? \"\" : s->answer.buf);\n \t\treturn res;\n@@ -1371,7 +1371,7 @@ static int read_single_character(struct add_p_state *s)\n static int prompt_yesno(struct add_p_state *s, const char *prompt)\n {\n \tfor (;;) {\n-\t\tcolor_fprintf(stdout, s->s.cfg.prompt_color, \"%s\", _(prompt));\n+\t\tcolor_fprintf(stdout, s->cfg.prompt_color, \"%s\", _(prompt));\n \t\tfflush(stdout);\n \t\tif (read_single_character(s) == EOF)\n \t\t\treturn -1;\n@@ -1559,7 +1559,7 @@ static void apply_patch(struct add_p_state *s, struct file_diff *file_diff)\n \t\tstrbuf_reset(&s->buf);\n \t\treassemble_patch(s, file_diff, 0, &s->buf);\n \n-\t\tdiscard_index(s->s.r->index);\n+\t\tdiscard_index(s->r->index);\n \t\tif (s->mode->apply_for_checkout)\n \t\t\tapply_for_checkout(s, &s->buf,\n \t\t\t\t\ts->mode->is_reverse);\n@@ -1570,9 +1570,9 @@ static void apply_patch(struct add_p_state *s, struct file_diff *file_diff)\n \t\t\t\t\tNULL, 0, NULL, 0))\n \t\t\t\terror(_(\"'git apply' failed\"));\n \t\t}\n-\t\tif (repo_read_index(s->s.r) >= 0)\n-\t\t\trepo_refresh_and_write_index(s->s.r, REFRESH_QUIET, 0,\n-\t\t\t\t\t\t\t1, NULL, NULL, NULL);\n+\t\tif (repo_read_index(s->r) >= 0)\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 }\n@@ -1660,7 +1660,7 @@ static size_t patch_update_file(struct add_p_state *s, size_t idx)\n \t\t/* Everything decided? */\n \t\tif (undecided_previous < 0 && undecided_next < 0 &&\n \t\t    hunk->use != UNDECIDED_HUNK) {\n-\t\t\t\tif (!s->s.cfg.auto_advance)\n+\t\t\t\tif (!s->cfg.auto_advance)\n \t\t\t\t\tall_decided = 1;\n \t\t\t\telse {\n \t\t\t\t\tpatch_update_resp++;\n@@ -1714,11 +1714,11 @@ static size_t patch_update_file(struct add_p_state *s, size_t idx)\n \t\t\t\tpermitted |= ALLOW_EDIT;\n \t\t\t\tstrbuf_addstr(&s->buf, \",e\");\n \t\t\t}\n-\t\t\tif (!s->s.cfg.auto_advance && s->file_diff_nr > 1) {\n+\t\t\tif (!s->cfg.auto_advance && s->file_diff_nr > 1) {\n \t\t\t\tpermitted |= ALLOW_GOTO_NEXT_FILE;\n \t\t\t\tstrbuf_addstr(&s->buf, \",>\");\n \t\t\t}\n-\t\t\tif (!s->s.cfg.auto_advance && s->file_diff_nr > 1) {\n+\t\t\tif (!s->cfg.auto_advance && s->file_diff_nr > 1) {\n \t\t\t\tpermitted |= ALLOW_GOTO_PREVIOUS_FILE;\n \t\t\t\tstrbuf_addstr(&s->buf, \",<\");\n \t\t\t}\n@@ -1733,7 +1733,7 @@ static size_t patch_update_file(struct add_p_state *s, size_t idx)\n \t\telse\n \t\t\tprompt_mode_type = PROMPT_HUNK;\n \n-\t\tprintf(\"%s(%\"PRIuMAX\"/%\"PRIuMAX\") \", s->s.cfg.prompt_color,\n+\t\tprintf(\"%s(%\"PRIuMAX\"/%\"PRIuMAX\") \", s->cfg.prompt_color,\n \t\t\t      (uintmax_t)hunk_index + 1,\n \t\t\t      (uintmax_t)(file_diff->hunk_nr\n \t\t\t\t\t\t? file_diff->hunk_nr\n@@ -1746,8 +1746,8 @@ static size_t patch_update_file(struct add_p_state *s, size_t idx)\n \t\t}\n \t\tprintf(_(s->mode->prompt_mode[prompt_mode_type]),\n \t\t\thunk_use_decision, s->buf.buf);\n-\t\tif (*s->s.cfg.reset_color_interactive)\n-\t\t\tfputs(s->s.cfg.reset_color_interactive, stdout);\n+\t\tif (*s->cfg.reset_color_interactive)\n+\t\t\tfputs(s->cfg.reset_color_interactive, stdout);\n \t\tfflush(stdout);\n \t\tif (read_single_character(s) == EOF) {\n \t\t\tpatch_update_resp = s->file_diff_nr;\n@@ -1798,7 +1798,7 @@ static size_t patch_update_file(struct add_p_state *s, size_t idx)\n \t\t} else if (ch == 'q') {\n \t\t\tpatch_update_resp = s->file_diff_nr;\n \t\t\tbreak;\n-\t\t} else if (!s->s.cfg.auto_advance && s->answer.buf[0] == '>') {\n+\t\t} else if (!s->cfg.auto_advance && s->answer.buf[0] == '>') {\n \t\t\tif (permitted & ALLOW_GOTO_NEXT_FILE) {\n \t\t\t\tif (patch_update_resp == s->file_diff_nr - 1)\n \t\t\t\t\tpatch_update_resp = 0;\n@@ -1809,7 +1809,7 @@ static size_t patch_update_file(struct add_p_state *s, size_t idx)\n \t\t\t\terr(s, _(\"No next file\"));\n \t\t\t\tcontinue;\n \t\t\t}\n-\t\t} else if (!s->s.cfg.auto_advance && s->answer.buf[0] == '<') {\n+\t\t} else if (!s->cfg.auto_advance && s->answer.buf[0] == '<') {\n \t\t\tif (permitted & ALLOW_GOTO_PREVIOUS_FILE) {\n \t\t\t\tif (patch_update_resp == 0)\n \t\t\t\t\tpatch_update_resp = s->file_diff_nr - 1;\n@@ -1932,7 +1932,7 @@ static size_t patch_update_file(struct add_p_state *s, size_t idx)\n \t\t\t\terr(s, _(\"Sorry, cannot split this hunk\"));\n \t\t\t} else 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.cfg.header_color,\n+\t\t\t\tcolor_fprintf_ln(stdout, s->cfg.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\t\trendered_hunk_index = -1;\n@@ -1950,7 +1950,7 @@ static size_t patch_update_file(struct add_p_state *s, size_t idx)\n \t\t} else if (s->answer.buf[0] == '?') {\n \t\t\tconst char *p = _(help_patch_remainder), *eol = p;\n \n-\t\t\tcolor_fprintf(stdout, s->s.cfg.help_color, \"%s\",\n+\t\t\tcolor_fprintf(stdout, s->cfg.help_color, \"%s\",\n \t\t\t\t      _(s->mode->help_patch_text));\n \n \t\t\t/*\n@@ -1974,13 +1974,13 @@ static size_t patch_update_file(struct add_p_state *s, size_t idx)\n \t\t\t\t\t\tif (file_diff->hunk[i].use == SKIP_HUNK)\n \t\t\t\t\t\t\tskipped += 1;\n \t\t\t\t\t}\n-\t\t\t\t\tcolor_fprintf_ln(stdout, s->s.cfg.help_color, _(p),\n+\t\t\t\t\tcolor_fprintf_ln(stdout, s->cfg.help_color, _(p),\n \t\t\t\t\t\t\t total, used, skipped);\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.cfg.help_color,\n+\t\t\t\tcolor_fprintf_ln(stdout, s->cfg.help_color,\n \t\t\t\t\t\t \"%.*s\", (int)(eol - p), p);\n \t\t\t}\n \t\t} else {\n@@ -1989,7 +1989,7 @@ static size_t patch_update_file(struct add_p_state *s, size_t idx)\n \t\t}\n \t}\n \n-\tif (s->s.cfg.auto_advance)\n+\tif (s->cfg.auto_advance)\n \t\tapply_patch(s, file_diff);\n \n \tputchar('\\n');\n@@ -2001,11 +2001,15 @@ int run_add_p(struct repository *r, enum add_p_mode mode,\n \t      const struct pathspec *ps)\n {\n \tstruct add_p_state s = {\n-\t\t{ r }, STRBUF_INIT, STRBUF_INIT, STRBUF_INIT, STRBUF_INIT\n+\t\t.r = r,\n+\t\t.answer = STRBUF_INIT,\n+\t\t.buf = STRBUF_INIT,\n+\t\t.plain = STRBUF_INIT,\n+\t\t.colored = STRBUF_INIT,\n \t};\n \tsize_t i, binary_count = 0;\n \n-\tinit_add_i_state(&s.s, r, opts);\n+\tinteractive_config_init(&s.cfg, r, opts);\n \n \tif (mode == ADD_P_STASH)\n \t\ts.mode = &patch_mode_stash;\n@@ -2051,7 +2055,7 @@ int run_add_p(struct repository *r, enum add_p_mode mode,\n \t\t if ((i = patch_update_file(&s, i)) == s.file_diff_nr)\n \t\t\tbreak;\n     }\n-\tif (!s.s.cfg.auto_advance)\n+\tif (!s.cfg.auto_advance)\n \t\tfor (i = 0; i < s.file_diff_nr; i++)\n \t\t\tapply_patch(&s, s.file_diff + i);\n \n\n-- \n2.53.0.697.g625c4fb2da.dirty\n\n"},{"id":"537511","messageId":"20260302-pks-history-split-v1-4-444fc987a324@pks.im","threadId":"65116","inReplyTo":"20260302-pks-history-split-v1-0-444fc987a324@pks.im","subject":"[PATCH 4/8] add-patch: add support for in-memory index patching","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-03-02T12:13:08Z","receivedAt":"2026-03-02T12:13:22Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"With `run_add_p()` callers have the ability to apply changes from a\nspecific revision to a repository's index. This infra supports several\ndifferent modes, like for example applying changes to the index,\nworking tree or both.\n\nOne feature that is missing though is the ability to apply changes to an\nin-memory index different from the repository's index. Add a new\nfunction `run_add_p_index()` to plug this gap.\n\nThis new function will be used in a subsequent commit.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n add-patch.c | 149 ++++++++++++++++++++++++++++++++++++++++++++++++++----------\n add-patch.h |   8 ++++\n 2 files changed, 132 insertions(+), 25 deletions(-)\n\ndiff --git a/add-patch.c b/add-patch.c\nindex 4f089c82d0..b4dc7d2293 100644\n--- a/add-patch.c\n+++ b/add-patch.c\n@@ -4,11 +4,13 @@\n #include \"git-compat-util.h\"\n #include \"add-patch.h\"\n #include \"advice.h\"\n+#include \"commit.h\"\n #include \"config.h\"\n #include \"diff.h\"\n #include \"editor.h\"\n #include \"environment.h\"\n #include \"gettext.h\"\n+#include \"hex.h\"\n #include \"object-name.h\"\n #include \"pager.h\"\n #include \"read-cache-ll.h\"\n@@ -263,6 +265,8 @@ struct hunk {\n \n struct add_p_state {\n \tstruct repository *r;\n+\tstruct index_state *index;\n+\tconst char *index_file;\n \tstruct interactive_config cfg;\n \tstruct strbuf answer, buf;\n \n@@ -438,7 +442,7 @@ static void setup_child_process(struct add_p_state *s,\n \n \tcp->git_cmd = 1;\n \tstrvec_pushf(&cp->env,\n-\t\t     INDEX_ENVIRONMENT \"=%s\", s->r->index_file);\n+\t\t     INDEX_ENVIRONMENT \"=%s\", s->index_file);\n }\n \n static int parse_range(const char **p,\n@@ -1559,7 +1563,7 @@ static void apply_patch(struct add_p_state *s, struct file_diff *file_diff)\n \t\tstrbuf_reset(&s->buf);\n \t\treassemble_patch(s, file_diff, 0, &s->buf);\n \n-\t\tdiscard_index(s->r->index);\n+\t\tdiscard_index(s->index);\n \t\tif (s->mode->apply_for_checkout)\n \t\t\tapply_for_checkout(s, &s->buf,\n \t\t\t\t\ts->mode->is_reverse);\n@@ -1570,9 +1574,11 @@ static void apply_patch(struct add_p_state *s, struct file_diff *file_diff)\n \t\t\t\t\tNULL, 0, NULL, 0))\n \t\t\t\terror(_(\"'git apply' failed\"));\n \t\t}\n-\t\tif (repo_read_index(s->r) >= 0)\n+\t\tif (read_index_from(s->index, s->index_file, s->r->gitdir) >= 0 &&\n+\t\t    s->index == s->r->index) {\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\t}\n \t}\n \n }\n@@ -1996,18 +2002,51 @@ static size_t patch_update_file(struct add_p_state *s, size_t idx)\n \treturn patch_update_resp;\n }\n \n+static int run_add_p_common(struct add_p_state *state,\n+\t\t\t    const struct pathspec *ps)\n+{\n+\tsize_t binary_count = 0;\n+\tsize_t i;\n+\n+\tif (parse_diff(state, ps) < 0)\n+\t\treturn -1;\n+\n+\tfor (i = 0; i < state->file_diff_nr;) {\n+\t\tif (state->file_diff[i].binary && !state->file_diff[i].hunk_nr) {\n+\t\t\tbinary_count++;\n+\t\t\ti++;\n+\t\t\tcontinue;\n+\t\t}\n+\t\tif ((i = patch_update_file(state, i)) == state->file_diff_nr)\n+\t\t\tbreak;\n+\t}\n+\n+\tif (!state->cfg.auto_advance)\n+\t\tfor (i = 0; i < state->file_diff_nr; i++)\n+\t\t\tapply_patch(state, state->file_diff + i);\n+\n+\tif (state->file_diff_nr == 0)\n+\t\terr(state, _(\"No changes.\"));\n+\telse if (binary_count == state->file_diff_nr)\n+\t\terr(state, _(\"Only binary files changed.\"));\n+\n+\treturn 0;\n+}\n+\n int run_add_p(struct repository *r, enum add_p_mode mode,\n \t      struct interactive_options *opts, const char *revision,\n \t      const struct pathspec *ps)\n {\n \tstruct add_p_state s = {\n \t\t.r = r,\n+\t\t.index = r->index,\n+\t\t.index_file = r->index_file,\n \t\t.answer = STRBUF_INIT,\n \t\t.buf = STRBUF_INIT,\n \t\t.plain = STRBUF_INIT,\n \t\t.colored = STRBUF_INIT,\n \t};\n-\tsize_t i, binary_count = 0;\n+\tint ret;\n \n \tinteractive_config_init(&s.cfg, r, opts);\n \n@@ -2040,30 +2079,90 @@ int run_add_p(struct repository *r, enum add_p_mode mode,\n \tif (repo_read_index(r) < 0 ||\n \t    (!s.mode->index_only &&\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\tadd_p_state_clear(&s);\n-\t\treturn -1;\n+\t\t\t\t\t  NULL, NULL, NULL) < 0)) {\n+\t\tret = -1;\n+\t\tgoto out;\n \t}\n \n-\tfor (i = 0; i < s.file_diff_nr;) {\n-\t\tif (s.file_diff[i].binary && !s.file_diff[i].hunk_nr) {\n-\t\t\tbinary_count++;\n-\t\t\ti++;\n-\t\t\tcontinue;\n-\t\t}\n-\t\t if ((i = patch_update_file(&s, i)) == s.file_diff_nr)\n-\t\t\tbreak;\n-    }\n-\tif (!s.cfg.auto_advance)\n-\t\tfor (i = 0; i < s.file_diff_nr; i++)\n-\t\t\tapply_patch(&s, s.file_diff + i);\n+\tret = run_add_p_common(&s, ps);\n+\tif (ret < 0)\n+\t\tgoto out;\n \n-\tif (s.file_diff_nr == 0)\n-\t\terr(&s, _(\"No changes.\"));\n-\telse if (binary_count == s.file_diff_nr)\n-\t\terr(&s, _(\"Only binary files changed.\"));\n+\tret = 0;\n \n+out:\n \tadd_p_state_clear(&s);\n-\treturn 0;\n+\treturn ret;\n+}\n+\n+int run_add_p_index(struct repository *r,\n+\t\t    struct index_state *index,\n+\t\t    const char *index_file,\n+\t\t    struct interactive_options *opts,\n+\t\t    const char *revision,\n+\t\t    const struct pathspec *ps)\n+{\n+\tstruct patch_mode mode = {\n+\t\t.apply_args = { \"--cached\", NULL },\n+\t\t.apply_check_args = { \"--cached\", NULL },\n+\t\t.prompt_mode = {\n+\t\t\tN_(\"Stage mode change [y,n,q,a,d%s,?]? \"),\n+\t\t\tN_(\"Stage deletion [y,n,q,a,d%s,?]? \"),\n+\t\t\tN_(\"Stage addition [y,n,q,a,d%s,?]? \"),\n+\t\t\tN_(\"Stage this hunk [y,n,q,a,d%s,?]? \")\n+\t\t},\n+\t\t.edit_hunk_hint = N_(\"If the patch applies cleanly, the edited hunk \"\n+\t\t\t\t     \"will immediately be marked for staging.\"),\n+\t\t.help_patch_text =\n+\t\t\tN_(\"y - stage this hunk\\n\"\n+\t\t\t   \"n - do not stage this hunk\\n\"\n+\t\t\t   \"q - quit; do not stage this hunk or any of the remaining \"\n+\t\t\t\t\"ones\\n\"\n+\t\t\t   \"a - stage this hunk and all later hunks in the file\\n\"\n+\t\t\t   \"d - do not stage this hunk or any of the later hunks in \"\n+\t\t\t\t\"the file\\n\"),\n+\t\t.index_only = 1,\n+\t};\n+\tstruct add_p_state s = {\n+\t\t.r = r,\n+\t\t.index = index,\n+\t\t.index_file = index_file,\n+\t\t.answer = STRBUF_INIT,\n+\t\t.buf = STRBUF_INIT,\n+\t\t.plain = STRBUF_INIT,\n+\t\t.colored = STRBUF_INIT,\n+\t\t.mode = &mode,\n+\t\t.revision = revision,\n+\t};\n+\tchar parent_tree_oid[GIT_MAX_HEXSZ + 1];\n+\tstruct commit *commit;\n+\tint ret;\n+\n+\tinteractive_config_init(&s.cfg, r, opts);\n+\n+\tcommit = lookup_commit_reference_by_name(revision);\n+\tif (!commit) {\n+\t\terr(&s, _(\"Revision does not refer to a commit\"));\n+\t\tret = -1;\n+\t\tgoto out;\n+\t}\n+\n+\tif (commit->parents)\n+\t\toid_to_hex_r(parent_tree_oid, get_commit_tree_oid(commit->parents->item));\n+\telse\n+\t\toid_to_hex_r(parent_tree_oid, r->hash_algo->empty_tree);\n+\n+\tmode.diff_cmd[0] = \"diff-tree\";\n+\tmode.diff_cmd[1] = \"-r\";\n+\tmode.diff_cmd[2] = parent_tree_oid;\n+\n+\tret = run_add_p_common(&s, ps);\n+\tif (ret < 0)\n+\t\tgoto out;\n+\n+\tret = 0;\n+\n+out:\n+\tadd_p_state_clear(&s);\n+\treturn ret;\n }\ndiff --git a/add-patch.h b/add-patch.h\nindex e6868c60a2..cf2a31a40f 100644\n--- a/add-patch.h\n+++ b/add-patch.h\n@@ -3,6 +3,7 @@\n \n #include \"color.h\"\n \n+struct index_state;\n struct pathspec;\n struct repository;\n \n@@ -56,4 +57,11 @@ int run_add_p(struct repository *r, enum add_p_mode mode,\n \t      struct interactive_options *opts, const char *revision,\n \t      const struct pathspec *ps);\n \n+int run_add_p_index(struct repository *r,\n+\t\t    struct index_state *index,\n+\t\t    const char *index_file,\n+\t\t    struct interactive_options *opts,\n+\t\t    const char *revision,\n+\t\t    const struct pathspec *ps);\n+\n #endif\n\n-- \n2.53.0.697.g625c4fb2da.dirty\n\n"},{"id":"537512","messageId":"20260302-pks-history-split-v1-5-444fc987a324@pks.im","threadId":"65116","inReplyTo":"20260302-pks-history-split-v1-0-444fc987a324@pks.im","subject":"[PATCH 5/8] add-patch: allow disabling editing of hunks","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-03-02T12:13:09Z","receivedAt":"2026-03-02T12:13:26Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The \"add-patch\" mode allows the user to edit hunks to apply custom\nchanges. This is incompatible with a new `git history split` command\nthat we're about to introduce in a subsequent commit, so we need a way\nto disable this mode.\n\nAdd a new flag to disable editing hunks.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n add-interactive.c  |  2 +-\n add-patch.c        | 22 ++++++++++++++--------\n add-patch.h        | 11 +++++++++--\n builtin/add.c      |  2 +-\n builtin/checkout.c |  2 +-\n builtin/reset.c    |  2 +-\n builtin/stash.c    |  2 +-\n 7 files changed, 28 insertions(+), 15 deletions(-)\n\ndiff --git a/add-interactive.c b/add-interactive.c\nindex 152e2a0297..3cf8a1dbf8 100644\n--- a/add-interactive.c\n+++ b/add-interactive.c\n@@ -927,7 +927,7 @@ static int run_patch(struct add_i_state *s, const struct pathspec *ps,\n \t\tparse_pathspec(&ps_selected,\n \t\t\t       PATHSPEC_ALL_MAGIC & ~PATHSPEC_LITERAL,\n \t\t\t       PATHSPEC_LITERAL_PATH, \"\", args.v);\n-\t\tres = run_add_p(s->r, ADD_P_ADD, &opts, NULL, &ps_selected);\n+\t\tres = run_add_p(s->r, ADD_P_ADD, &opts, NULL, &ps_selected, 0);\n \t\tstrvec_clear(&args);\n \t\tclear_pathspec(&ps_selected);\n \t}\ndiff --git a/add-patch.c b/add-patch.c\nindex b4dc7d2293..4e28e5c187 100644\n--- a/add-patch.c\n+++ b/add-patch.c\n@@ -1604,7 +1604,9 @@ static bool get_first_undecided(const struct file_diff *file_diff, size_t *idx)\n \treturn false;\n }\n \n-static size_t patch_update_file(struct add_p_state *s, size_t idx)\n+static size_t patch_update_file(struct add_p_state *s,\n+\t\t\t\tsize_t idx,\n+\t\t\t\tunsigned flags)\n {\n \tsize_t hunk_index = 0;\n \tssize_t i, undecided_previous, undecided_next, rendered_hunk_index = -1;\n@@ -1715,7 +1717,8 @@ static size_t patch_update_file(struct add_p_state *s, size_t idx)\n \t\t\t\tpermitted |= ALLOW_SPLIT;\n \t\t\t\tstrbuf_addstr(&s->buf, \",s\");\n \t\t\t}\n-\t\t\tif (hunk_index + 1 > file_diff->mode_change &&\n+\t\t\tif (!(flags & ADD_P_DISALLOW_EDIT) &&\n+\t\t\t    hunk_index + 1 > file_diff->mode_change &&\n \t\t\t    !file_diff->deleted) {\n \t\t\t\tpermitted |= ALLOW_EDIT;\n \t\t\t\tstrbuf_addstr(&s->buf, \",e\");\n@@ -2003,7 +2006,8 @@ static size_t patch_update_file(struct add_p_state *s, size_t idx)\n }\n \n static int run_add_p_common(struct add_p_state *state,\n-\t\t\t    const struct pathspec *ps)\n+\t\t\t    const struct pathspec *ps,\n+\t\t\t    unsigned flags)\n {\n \tsize_t binary_count = 0;\n \tsize_t i;\n@@ -2017,7 +2021,7 @@ static int run_add_p_common(struct add_p_state *state,\n \t\t\ti++;\n \t\t\tcontinue;\n \t\t}\n-\t\tif ((i = patch_update_file(state, i)) == state->file_diff_nr)\n+\t\tif ((i = patch_update_file(state, i, flags)) == state->file_diff_nr)\n \t\t\tbreak;\n \t}\n \n@@ -2035,7 +2039,8 @@ static int run_add_p_common(struct add_p_state *state,\n \n int run_add_p(struct repository *r, enum add_p_mode mode,\n \t      struct interactive_options *opts, const char *revision,\n-\t      const struct pathspec *ps)\n+\t      const struct pathspec *ps,\n+\t      unsigned flags)\n {\n \tstruct add_p_state s = {\n \t\t.r = r,\n@@ -2084,7 +2089,7 @@ int run_add_p(struct repository *r, enum add_p_mode mode,\n \t\tgoto out;\n \t}\n \n-\tret = run_add_p_common(&s, ps);\n+\tret = run_add_p_common(&s, ps, flags);\n \tif (ret < 0)\n \t\tgoto out;\n \n@@ -2100,7 +2105,8 @@ int run_add_p_index(struct repository *r,\n \t\t    const char *index_file,\n \t\t    struct interactive_options *opts,\n \t\t    const char *revision,\n-\t\t    const struct pathspec *ps)\n+\t\t    const struct pathspec *ps,\n+\t\t    unsigned flags)\n {\n \tstruct patch_mode mode = {\n \t\t.apply_args = { \"--cached\", NULL },\n@@ -2156,7 +2162,7 @@ int run_add_p_index(struct repository *r,\n \tmode.diff_cmd[1] = \"-r\";\n \tmode.diff_cmd[2] = parent_tree_oid;\n \n-\tret = run_add_p_common(&s, ps);\n+\tret = run_add_p_common(&s, ps, flags);\n \tif (ret < 0)\n \t\tgoto out;\n \ndiff --git a/add-patch.h b/add-patch.h\nindex cf2a31a40f..fb6d975b68 100644\n--- a/add-patch.h\n+++ b/add-patch.h\n@@ -53,15 +53,22 @@ enum add_p_mode {\n \tADD_P_WORKTREE,\n };\n \n+enum add_p_flags {\n+\t/* Disallow \"editing\" hunks. */\n+\tADD_P_DISALLOW_EDIT = (1 << 0),\n+};\n+\n int run_add_p(struct repository *r, enum add_p_mode mode,\n \t      struct interactive_options *opts, const char *revision,\n-\t      const struct pathspec *ps);\n+\t      const struct pathspec *ps,\n+\t      unsigned flags);\n \n int run_add_p_index(struct repository *r,\n \t\t    struct index_state *index,\n \t\t    const char *index_file,\n \t\t    struct interactive_options *opts,\n \t\t    const char *revision,\n-\t\t    const struct pathspec *ps);\n+\t\t    const struct pathspec *ps,\n+\t\t    unsigned flags);\n \n #endif\ndiff --git a/builtin/add.c b/builtin/add.c\nindex 84f9bcb789..eeab779328 100644\n--- a/builtin/add.c\n+++ b/builtin/add.c\n@@ -172,7 +172,7 @@ int interactive_add(struct repository *repo,\n \t\t       prefix, argv);\n \n \tif (patch)\n-\t\tret = !!run_add_p(repo, ADD_P_ADD, interactive_opts, NULL, &pathspec);\n+\t\tret = !!run_add_p(repo, ADD_P_ADD, interactive_opts, NULL, &pathspec, 0);\n \telse\n \t\tret = !!run_add_i(repo, &pathspec, interactive_opts);\n \ndiff --git a/builtin/checkout.c b/builtin/checkout.c\nindex bebe18c1d9..a8863277f2 100644\n--- a/builtin/checkout.c\n+++ b/builtin/checkout.c\n@@ -563,7 +563,7 @@ static int checkout_paths(const struct checkout_opts *opts,\n \t\t\tBUG(\"either flag must have been set, worktree=%d, index=%d\",\n \t\t\t    opts->checkout_worktree, opts->checkout_index);\n \t\treturn !!run_add_p(the_repository, patch_mode, &interactive_opts,\n-\t\t\t\t   rev, &opts->pathspec);\n+\t\t\t\t   rev, &opts->pathspec, 0);\n \t}\n \n \trepo_hold_locked_index(the_repository, &lock_file, LOCK_DIE_ON_ERROR);\ndiff --git a/builtin/reset.c b/builtin/reset.c\nindex 4a74a82c0a..3590be57a5 100644\n--- a/builtin/reset.c\n+++ b/builtin/reset.c\n@@ -438,7 +438,7 @@ int cmd_reset(int argc,\n \t\t\tdie(_(\"options '%s' and '%s' cannot be used together\"), \"--patch\", \"--{hard,mixed,soft}\");\n \t\ttrace2_cmd_mode(\"patch-interactive\");\n \t\tupdate_ref_status = !!run_add_p(the_repository, ADD_P_RESET,\n-\t\t\t\t\t\t&interactive_opts, rev, &pathspec);\n+\t\t\t\t\t\t&interactive_opts, rev, &pathspec, 0);\n \t\tgoto cleanup;\n \t} else {\n \t\tif (interactive_opts.context != -1)\ndiff --git a/builtin/stash.c b/builtin/stash.c\nindex c467c02c7f..7c68a1d7f9 100644\n--- a/builtin/stash.c\n+++ b/builtin/stash.c\n@@ -1331,7 +1331,7 @@ static int stash_patch(struct stash_info *info, const struct pathspec *ps,\n \told_index_env = xstrdup_or_null(getenv(INDEX_ENVIRONMENT));\n \tsetenv(INDEX_ENVIRONMENT, the_repository->index_file, 1);\n \n-\tret = !!run_add_p(the_repository, ADD_P_STASH, interactive_opts, NULL, ps);\n+\tret = !!run_add_p(the_repository, ADD_P_STASH, interactive_opts, NULL, ps, 0);\n \n \tthe_repository->index_file = old_repo_index_file;\n \tif (old_index_env && *old_index_env)\n\n-- \n2.53.0.697.g625c4fb2da.dirty\n\n"},{"id":"537513","messageId":"20260302-pks-history-split-v1-6-444fc987a324@pks.im","threadId":"65116","inReplyTo":"20260302-pks-history-split-v1-0-444fc987a324@pks.im","subject":"[PATCH 6/8] cache-tree: allow writing in-memory index as tree","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-03-02T12:13:10Z","receivedAt":"2026-03-02T12:13:28Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The function `write_in_core_index_as_tree()` takes a repository and\nwrites its index into a tree object. What this function cannot do though\nis to take an _arbitrary_ in-memory index.\n\nIntroduce a new `struct index_state` parameter so that the caller can\npass a different index than the one belonging to the repository. This\nwill be used in a subsequent commit.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/checkout.c | 3 ++-\n cache-tree.c       | 4 ++--\n cache-tree.h       | 3 ++-\n 3 files changed, 6 insertions(+), 4 deletions(-)\n\ndiff --git a/builtin/checkout.c b/builtin/checkout.c\nindex a8863277f2..f8b3a7b08c 100644\n--- a/builtin/checkout.c\n+++ b/builtin/checkout.c\n@@ -891,7 +891,8 @@ static int merge_working_tree(const struct checkout_opts *opts,\n \t\t\t\t\t   0);\n \t\t\tinit_ui_merge_options(&o, the_repository);\n \t\t\to.verbosity = 0;\n-\t\t\twork = write_in_core_index_as_tree(the_repository);\n+\t\t\twork = write_in_core_index_as_tree(the_repository,\n+\t\t\t\t\t\t\t   the_repository->index);\n \n \t\t\tret = reset_tree(new_tree,\n \t\t\t\t\t opts, 1,\ndiff --git a/cache-tree.c b/cache-tree.c\nindex 16c3a36b48..60bcc07c3b 100644\n--- a/cache-tree.c\n+++ b/cache-tree.c\n@@ -723,11 +723,11 @@ static int write_index_as_tree_internal(struct object_id *oid,\n \treturn 0;\n }\n \n-struct tree* write_in_core_index_as_tree(struct repository *repo) {\n+struct tree *write_in_core_index_as_tree(struct repository *repo,\n+\t\t\t\t\t struct index_state *index_state) {\n \tstruct object_id o;\n \tint was_valid, ret;\n \n-\tstruct index_state *index_state\t= repo->index;\n \twas_valid = index_state->cache_tree &&\n \t\t    cache_tree_fully_valid(index_state->cache_tree);\n \ndiff --git a/cache-tree.h b/cache-tree.h\nindex b82c4963e7..f8bddae523 100644\n--- a/cache-tree.h\n+++ b/cache-tree.h\n@@ -47,7 +47,8 @@ int cache_tree_verify(struct repository *, struct index_state *);\n #define WRITE_TREE_UNMERGED_INDEX (-2)\n #define WRITE_TREE_PREFIX_ERROR (-3)\n \n-struct tree* write_in_core_index_as_tree(struct repository *repo);\n+struct tree *write_in_core_index_as_tree(struct repository *repo,\n+\t\t\t\t\t struct index_state *index_state);\n int write_index_as_tree(struct object_id *oid, struct index_state *index_state, const char *index_path, int flags, const char *prefix);\n void prime_cache_tree(struct repository *, struct index_state *, struct tree *);\n \n\n-- \n2.53.0.697.g625c4fb2da.dirty\n\n"},{"id":"537514","messageId":"20260302-pks-history-split-v1-7-444fc987a324@pks.im","threadId":"65116","inReplyTo":"20260302-pks-history-split-v1-0-444fc987a324@pks.im","subject":"[PATCH 7/8] builtin/history: split out extended function to create commits","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-03-02T12:13:11Z","receivedAt":"2026-03-02T12:13:31Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"In the next commit we're about to introduce a new command that splits up\na commit into two. Most of the logic will be shared with rewording\ncommits, except that we also need to have control over the parents and\nthe old/new trees.\n\nExtract a new function `commit_tree_with_edited_message_ext()` to\nprepare for this commit.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/history.c | 67 +++++++++++++++++++++++++++++++++----------------------\n 1 file changed, 40 insertions(+), 27 deletions(-)\n\ndiff --git a/builtin/history.c b/builtin/history.c\nindex 1cf6c668cf..80726ce14b 100644\n--- a/builtin/history.c\n+++ b/builtin/history.c\n@@ -83,10 +83,13 @@ static int fill_commit_message(struct repository *repo,\n \treturn 0;\n }\n \n-static int commit_tree_with_edited_message(struct repository *repo,\n-\t\t\t\t\t   const char *action,\n-\t\t\t\t\t   struct commit *original,\n-\t\t\t\t\t   struct commit **out)\n+static int commit_tree_with_edited_message_ext(struct repository *repo,\n+\t\t\t\t\t       const char *action,\n+\t\t\t\t\t       struct commit *commit_with_message,\n+\t\t\t\t\t       const struct commit_list *parents,\n+\t\t\t\t\t       const struct object_id *old_tree,\n+\t\t\t\t\t       const struct object_id *new_tree,\n+\t\t\t\t\t       struct commit **out)\n {\n \tconst char *exclude_gpgsig[] = {\n \t\t/* We reencode the message, so the encoding needs to be stripped. */\n@@ -100,44 +103,27 @@ static int commit_tree_with_edited_message(struct repository *repo,\n \tstruct commit_extra_header *original_extra_headers = NULL;\n \tstruct strbuf commit_message = STRBUF_INIT;\n \tstruct object_id rewritten_commit_oid;\n-\tstruct object_id original_tree_oid;\n-\tstruct object_id parent_tree_oid;\n \tchar *original_author = NULL;\n-\tstruct commit *parent;\n \tsize_t len;\n \tint ret;\n \n-\toriginal_tree_oid = repo_get_commit_tree(repo, original)->object.oid;\n-\n-\tparent = original->parents ? original->parents->item : NULL;\n-\tif (parent) {\n-\t\tif (repo_parse_commit(repo, parent)) {\n-\t\t\tret = error(_(\"unable to parse parent commit %s\"),\n-\t\t\t\t    oid_to_hex(&parent->object.oid));\n-\t\t\tgoto out;\n-\t\t}\n-\n-\t\tparent_tree_oid = repo_get_commit_tree(repo, parent)->object.oid;\n-\t} else {\n-\t\toidcpy(&parent_tree_oid, repo->hash_algo->empty_tree);\n-\t}\n-\n \t/* We retain authorship of the original commit. */\n-\toriginal_message = repo_logmsg_reencode(repo, original, NULL, NULL);\n+\toriginal_message = repo_logmsg_reencode(repo, commit_with_message, NULL, NULL);\n \tptr = find_commit_header(original_message, \"author\", &len);\n \tif (ptr)\n \t\toriginal_author = xmemdupz(ptr, len);\n \tfind_commit_subject(original_message, &original_body);\n \n-\tret = fill_commit_message(repo, &parent_tree_oid, &original_tree_oid,\n+\tret = fill_commit_message(repo, old_tree, new_tree,\n \t\t\t\t  original_body, action, &commit_message);\n \tif (ret < 0)\n \t\tgoto out;\n \n-\toriginal_extra_headers = read_commit_extra_headers(original, exclude_gpgsig);\n+\toriginal_extra_headers = read_commit_extra_headers(commit_with_message,\n+\t\t\t\t\t\t\t   exclude_gpgsig);\n \n-\tret = commit_tree_extended(commit_message.buf, commit_message.len, &original_tree_oid,\n-\t\t\t\t   original->parents, &rewritten_commit_oid, original_author,\n+\tret = commit_tree_extended(commit_message.buf, commit_message.len, new_tree,\n+\t\t\t\t   parents, &rewritten_commit_oid, original_author,\n \t\t\t\t   NULL, NULL, original_extra_headers);\n \tif (ret < 0)\n \t\tgoto out;\n@@ -151,6 +137,33 @@ static int commit_tree_with_edited_message(struct repository *repo,\n \treturn ret;\n }\n \n+static int commit_tree_with_edited_message(struct repository *repo,\n+\t\t\t\t\t   const char *action,\n+\t\t\t\t\t   struct commit *original,\n+\t\t\t\t\t   struct commit **out)\n+{\n+\tstruct object_id parent_tree_oid;\n+\tconst struct object_id *tree_oid;\n+\tstruct commit *parent;\n+\n+\ttree_oid = &repo_get_commit_tree(repo, original)->object.oid;\n+\n+\tparent = original->parents ? original->parents->item : NULL;\n+\tif (parent) {\n+\t\tif (repo_parse_commit(repo, parent)) {\n+\t\t\treturn error(_(\"unable to parse parent commit %s\"),\n+\t\t\t\t     oid_to_hex(&parent->object.oid));\n+\t\t}\n+\n+\t\tparent_tree_oid = repo_get_commit_tree(repo, parent)->object.oid;\n+\t} else {\n+\t\toidcpy(&parent_tree_oid, repo->hash_algo->empty_tree);\n+\t}\n+\n+\treturn commit_tree_with_edited_message_ext(repo, action, original, original->parents,\n+\t\t\t\t\t\t   &parent_tree_oid, tree_oid, out);\n+}\n+\n enum ref_action {\n \tREF_ACTION_DEFAULT,\n \tREF_ACTION_BRANCHES,\n\n-- \n2.53.0.697.g625c4fb2da.dirty\n\n"},{"id":"537515","messageId":"20260302-pks-history-split-v1-8-444fc987a324@pks.im","threadId":"65116","inReplyTo":"20260302-pks-history-split-v1-0-444fc987a324@pks.im","subject":"[PATCH 8/8] builtin/history: implement \"split\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-03-02T12:13:12Z","receivedAt":"2026-03-02T12:13:33Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"It is quite a common use case that one wants to split up one commit into\nmultiple commits by moving parts of the changes of the original commit\nout into a separate commit. This is quite an involved operation though:\n\n  1. Identify the commit in question that is to be dropped.\n\n  2. Perform an interactive rebase on top of that commit's parent.\n\n  3. Modify the instruction sheet to \"edit\" the commit that is to be\n     split up.\n\n  4. Drop the commit via \"git reset HEAD~\".\n\n  5. Stage changes that should go into the first commit and commit it.\n\n  6. Stage changes that should go into the second commit and commit it.\n\n  7. Finalize the rebase.\n\nThis is quite complex, and overall I would claim that most people who\nare not experts in Git would struggle with this flow.\n\nIntroduce a new \"split\" subcommand for git-history(1) to make this way\neasier. All the user needs to do is to say `git history split $COMMIT`.\nFrom hereon, Git asks the user which parts of the commit shall be moved\nout into a separate commit and, once done, asks the user for the commit\nmessage. Git then creates that split-out commit and applies the original\ncommit on top of it.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/git-history.adoc |  62 ++++\n builtin/history.c              | 250 ++++++++++++++\n t/meson.build                  |   1 +\n t/t3452-history-split.sh       | 757 +++++++++++++++++++++++++++++++++++++++++\n 4 files changed, 1070 insertions(+)\n\ndiff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc\nindex cc019de697..24dc907033 100644\n--- a/Documentation/git-history.adoc\n+++ b/Documentation/git-history.adoc\n@@ -9,6 +9,7 @@ SYNOPSIS\n --------\n [synopsis]\n git history reword <commit> [--dry-run] [--update-refs=(branches|head)]\n+git history split <commit> [--dry-run] [--update-refs=(branches|head)] [--] [<pathspec>...]\n \n DESCRIPTION\n -----------\n@@ -57,6 +58,26 @@ The following commands are available to rewrite history in different ways:\n \tdetails of this commit remain unchanged. This command will spawn an\n \teditor with the current message of that commit.\n \n+`split <commit> [--] [<pathspec>...]`::\n+\tInteractively split up <commit> into two commits by choosing\n+\thunks introduced by it that will be moved into the new split-out\n+\tcommit. These hunks will then be written into a new commit that\n+\tbecomes the parent of the previous commit. The original commit\n+\tstays intact, except that its parent will be the newly split-out\n+\tcommit.\n++\n+The commit messages of the split-up commits will be asked for by launching\n+the configured editor. Authorship of the commit will be the same as for the\n+original commit.\n++\n+If passed, _<pathspec>_ can be used to limit which changes shall be split out\n+of the original commit. Files not matching any of the pathspecs will remain\n+part of the original commit. For more details, see the 'pathspec' entry in\n+linkgit:gitglossary[7].\n++\n+It is invalid to select either all or no hunks, as that would lead to\n+one of the commits becoming empty.\n+\n OPTIONS\n -------\n \n@@ -72,6 +93,47 @@ OPTIONS\n \tdescendants of the original commit will be rewritten. With `head`, only\n \tthe current `HEAD` reference will be rewritten. Defaults to `branches`.\n \n+EXAMPLES\n+--------\n+\n+Split a commit\n+~~~~~~~~~~~~~~\n+\n+----------\n+$ git log --stat --oneline\n+3f81232 (HEAD -> main) original\n+ bar | 1 +\n+ foo | 1 +\n+ 2 files changed, 2 insertions(+)\n+\n+$ git history split HEAD\n+diff --git a/bar b/bar\n+new file mode 100644\n+index 0000000..5716ca5\n+--- /dev/null\n++++ b/bar\n+@@ -0,0 +1 @@\n++bar\n+(1/1) Stage addition [y,n,q,a,d,p,?]? y\n+\n+diff --git a/foo b/foo\n+new file mode 100644\n+index 0000000..257cc56\n+--- /dev/null\n++++ b/foo\n+@@ -0,0 +1 @@\n++foo\n+(1/1) Stage addition [y,n,q,a,d,p,?]? n\n+\n+$ git log --stat --oneline\n+7cebe64 (HEAD -> main) original\n+ foo | 1 +\n+ 1 file changed, 1 insertion(+)\n+d1582f3 split-out commit\n+ bar | 1 +\n+ 1 file changed, 1 insertion(+)\n+----------\n+\n GIT\n ---\n Part of the linkgit:git[1] suite\ndiff --git a/builtin/history.c b/builtin/history.c\nindex 80726ce14b..dfb9d3f180 100644\n--- a/builtin/history.c\n+++ b/builtin/history.c\n@@ -1,6 +1,7 @@\n #define USE_THE_REPOSITORY_VARIABLE\n \n #include \"builtin.h\"\n+#include \"cache-tree.h\"\n #include \"commit.h\"\n #include \"commit-reach.h\"\n #include \"config.h\"\n@@ -8,17 +9,24 @@\n #include \"environment.h\"\n #include \"gettext.h\"\n #include \"hex.h\"\n+#include \"lockfile.h\"\n+#include \"oidmap.h\"\n #include \"parse-options.h\"\n+#include \"path.h\"\n+#include \"read-cache.h\"\n #include \"refs.h\"\n #include \"replay.h\"\n #include \"revision.h\"\n #include \"sequencer.h\"\n #include \"strvec.h\"\n #include \"tree.h\"\n+#include \"unpack-trees.h\"\n #include \"wt-status.h\"\n \n #define GIT_HISTORY_REWORD_USAGE \\\n \tN_(\"git history reword <commit> [--dry-run] [--update-refs=(branches|head)]\")\n+#define GIT_HISTORY_SPLIT_USAGE \\\n+\tN_(\"git history split <commit> [--dry-run] [--update-refs=(branches|head)] [--] [<pathspec>...]\")\n \n static void change_data_free(void *util, const char *str UNUSED)\n {\n@@ -484,6 +492,246 @@ static int cmd_history_reword(int argc,\n \treturn ret;\n }\n \n+static int write_ondisk_index(struct repository *repo,\n+\t\t\t      struct object_id *oid,\n+\t\t\t      const char *path)\n+{\n+\tstruct unpack_trees_options opts = { 0 };\n+\tstruct lock_file lock = LOCK_INIT;\n+\tstruct tree_desc tree_desc;\n+\tstruct index_state index;\n+\tstruct tree *tree;\n+\tint ret;\n+\n+\tindex_state_init(&index, repo);\n+\n+\topts.head_idx = -1;\n+\topts.src_index = &index;\n+\topts.dst_index = &index;\n+\n+\ttree = repo_parse_tree_indirect(repo, oid);\n+\tinit_tree_desc(&tree_desc, &tree->object.oid, tree->buffer, tree->size);\n+\n+\tif (unpack_trees(1, &tree_desc, &opts)) {\n+\t\tret = error(_(\"unable to populate index with tree\"));\n+\t\tgoto out;\n+\t}\n+\n+\tprime_cache_tree(repo, &index, tree);\n+\n+\tif (hold_lock_file_for_update(&lock, path, 0) < 0) {\n+\t\tret = error_errno(_(\"unable to acquire index lock\"));\n+\t\tgoto out;\n+\t}\n+\n+\tif (write_locked_index(&index, &lock, COMMIT_LOCK)) {\n+\t\tret = error(_(\"unable to write new index file\"));\n+\t\tgoto out;\n+\t}\n+\n+\tret = 0;\n+\n+out:\n+\trollback_lock_file(&lock);\n+\trelease_index(&index);\n+\treturn ret;\n+}\n+\n+static int split_commit(struct repository *repo,\n+\t\t\tstruct commit *original,\n+\t\t\tstruct pathspec *pathspec,\n+\t\t\tstruct commit **out)\n+{\n+\tstruct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;\n+\tstruct strbuf index_file = STRBUF_INIT;\n+\tstruct index_state index = INDEX_STATE_INIT(repo);\n+\tconst struct object_id *original_commit_tree_oid;\n+\tconst struct object_id *old_tree_oid, *new_tree_oid;\n+\tstruct object_id parent_tree_oid;\n+\tchar original_commit_oid[GIT_MAX_HEXSZ + 1];\n+\tstruct commit *first_commit, *second_commit;\n+\tstruct commit_list *parents = NULL;\n+\tstruct tree *split_tree;\n+\tint ret;\n+\n+\tif (original->parents) {\n+\t\tif (repo_parse_commit(repo, original->parents->item)) {\n+\t\t\tret = error(_(\"unable to parse parent commit %s\"),\n+\t\t\t\t    oid_to_hex(&original->parents->item->object.oid));\n+\t\t\tgoto out;\n+\t\t}\n+\n+\t\tparent_tree_oid = *get_commit_tree_oid(original->parents->item);\n+\t} else {\n+\t\toidcpy(&parent_tree_oid, repo->hash_algo->empty_tree);\n+\t}\n+\toriginal_commit_tree_oid = get_commit_tree_oid(original);\n+\n+\t/*\n+\t * Construct the first commit. This is done by taking the original\n+\t * commit parent's tree and selectively patching changes from the diff\n+\t * between that parent and its child.\n+\t */\n+\trepo_git_path_replace(repo, &index_file, \"%s\", \"history-split.index\");\n+\n+\tret = write_ondisk_index(repo, &parent_tree_oid, index_file.buf);\n+\tif (ret < 0)\n+\t\tgoto out;\n+\n+\tret = read_index_from(&index, index_file.buf, repo->gitdir);\n+\tif (ret < 0) {\n+\t\tret = error(_(\"failed reading temporary index\"));\n+\t\tgoto out;\n+\t}\n+\n+\toid_to_hex_r(original_commit_oid, &original->object.oid);\n+\tret = run_add_p_index(repo, &index, index_file.buf, &interactive_opts,\n+\t\t\t      original_commit_oid, pathspec, ADD_P_DISALLOW_EDIT);\n+\tif (ret < 0)\n+\t\tgoto out;\n+\n+\tsplit_tree = write_in_core_index_as_tree(repo, &index);\n+\tif (!split_tree) {\n+\t\tret = error(_(\"failed split tree\"));\n+\t\tgoto out;\n+\t}\n+\n+\tunlink(index_file.buf);\n+\tstrbuf_release(&index_file);\n+\n+\t/*\n+\t * We disallow the cases where either the split-out commit or the\n+\t * original commit would become empty. Consequently, if we see that the\n+\t * new tree ID matches either of those trees we abort.\n+\t */\n+\tif (oideq(&split_tree->object.oid, &parent_tree_oid)) {\n+\t\tret = error(_(\"split commit is empty\"));\n+\t\tgoto out;\n+\t} else if (oideq(&split_tree->object.oid, original_commit_tree_oid)) {\n+\t\tret = error(_(\"split commit tree matches original commit\"));\n+\t\tgoto out;\n+\t}\n+\n+\t/*\n+\t * The first commit is constructed from the split-out tree. The base\n+\t * that shall be diffed against is the parent of the original commit.\n+\t */\n+\tret = commit_tree_with_edited_message_ext(repo, \"split-out\", original,\n+\t\t\t\t\t\t  original->parents, &parent_tree_oid,\n+\t\t\t\t\t\t  &split_tree->object.oid, &first_commit);\n+\tif (ret < 0) {\n+\t\tret = error(_(\"failed writing first commit\"));\n+\t\tgoto out;\n+\t}\n+\n+\t/*\n+\t * The second commit is constructed from the original tree. The base to\n+\t * diff against and the parent in this case is the first split-out\n+\t * commit.\n+\t */\n+\tcommit_list_append(first_commit, &parents);\n+\n+\told_tree_oid = &repo_get_commit_tree(repo, first_commit)->object.oid;\n+\tnew_tree_oid = &repo_get_commit_tree(repo, original)->object.oid;\n+\n+\tret = commit_tree_with_edited_message_ext(repo, \"split-out\", original,\n+\t\t\t\t\t\t  parents, old_tree_oid,\n+\t\t\t\t\t\t  new_tree_oid, &second_commit);\n+\tif (ret < 0) {\n+\t\tret = error(_(\"failed writing second commit\"));\n+\t\tgoto out;\n+\t}\n+\n+\t*out = second_commit;\n+\tret = 0;\n+\n+out:\n+\tif (index_file.len)\n+\t\tunlink(index_file.buf);\n+\tstrbuf_release(&index_file);\n+\tfree_commit_list(parents);\n+\trelease_index(&index);\n+\treturn ret;\n+}\n+\n+static int cmd_history_split(int argc,\n+\t\t\t     const char **argv,\n+\t\t\t     const char *prefix,\n+\t\t\t     struct repository *repo)\n+{\n+\tconst char * const usage[] = {\n+\t\tGIT_HISTORY_SPLIT_USAGE,\n+\t\tNULL,\n+\t};\n+\tenum ref_action action = REF_ACTION_DEFAULT;\n+\tint dry_run = 0;\n+\tstruct option options[] = {\n+\t\tOPT_CALLBACK_F(0, \"update-refs\", &action, N_(\"<refs>\"),\n+\t\t\t       N_(\"control ref update behavior (branches|head|print)\"),\n+\t\t\t       PARSE_OPT_NONEG, parse_ref_action),\n+\t\tOPT_BOOL('n', \"dry-run\", &dry_run,\n+\t\t\t N_(\"perform a dry-run without updating any refs\")),\n+\t\tOPT_END(),\n+\t};\n+\tstruct commit *original, *rewritten = NULL;\n+\tstruct strbuf reflog_msg = STRBUF_INIT;\n+\tstruct pathspec pathspec = { 0 };\n+\tstruct rev_info revs = { 0 };\n+\tint ret;\n+\n+\targc = parse_options(argc, argv, prefix, options, usage, 0);\n+\tif (argc < 1) {\n+\t\tret = error(_(\"command expects a committish\"));\n+\t\tgoto out;\n+\t}\n+\trepo_config(repo, git_default_config, NULL);\n+\n+\tif (action == REF_ACTION_DEFAULT)\n+\t\taction = REF_ACTION_BRANCHES;\n+\n+\tparse_pathspec(&pathspec, 0,\n+\t\t       PATHSPEC_PREFER_FULL |\n+\t\t       PATHSPEC_SYMLINK_LEADING_PATH |\n+\t\t       PATHSPEC_PREFIX_ORIGIN,\n+\t\t       prefix, argv + 1);\n+\n+\toriginal = lookup_commit_reference_by_name(argv[0]);\n+\tif (!original) {\n+\t\tret = error(_(\"commit cannot be found: %s\"), argv[0]);\n+\t\tgoto out;\n+\t}\n+\n+\tret = setup_revwalk(repo, action, original, &revs);\n+\tif (ret < 0)\n+\t\tgoto out;\n+\n+\tif (original->parents && original->parents->next) {\n+\t\tret = error(_(\"cannot split up merge commit\"));\n+\t\tgoto out;\n+\t}\n+\n+\tret = split_commit(repo, original, &pathspec, &rewritten);\n+\tif (ret < 0)\n+\t\tgoto out;\n+\n+\tstrbuf_addf(&reflog_msg, \"split: updating %s\", argv[0]);\n+\n+\tret = handle_reference_updates(&revs, action, original, rewritten,\n+\t\t\t\t       reflog_msg.buf, dry_run);\n+\tif (ret < 0) {\n+\t\tret = error(_(\"failed replaying descendants\"));\n+\t\tgoto out;\n+\t}\n+\n+\tret = 0;\n+\n+out:\n+\tstrbuf_release(&reflog_msg);\n+\tclear_pathspec(&pathspec);\n+\trelease_revisions(&revs);\n+\treturn ret;\n+}\n+\n int cmd_history(int argc,\n \t\tconst char **argv,\n \t\tconst char *prefix,\n@@ -491,11 +739,13 @@ int cmd_history(int argc,\n {\n \tconst char * const usage[] = {\n \t\tGIT_HISTORY_REWORD_USAGE,\n+\t\tGIT_HISTORY_SPLIT_USAGE,\n \t\tNULL,\n \t};\n \tparse_opt_subcommand_fn *fn = NULL;\n \tstruct option options[] = {\n \t\tOPT_SUBCOMMAND(\"reword\", &fn, cmd_history_reword),\n+\t\tOPT_SUBCOMMAND(\"split\", &fn, cmd_history_split),\n \t\tOPT_END(),\n \t};\n \ndiff --git a/t/meson.build b/t/meson.build\nindex 6d91470ebc..2d578ef58b 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -392,6 +392,7 @@ integration_tests = [\n   't3438-rebase-broken-files.sh',\n   't3450-history.sh',\n   't3451-history-reword.sh',\n+  't3452-history-split.sh',\n   't3500-cherry.sh',\n   't3501-revert-cherry-pick.sh',\n   't3502-cherry-pick-merge.sh',\ndiff --git a/t/t3452-history-split.sh b/t/t3452-history-split.sh\nnew file mode 100755\nindex 0000000000..8ed0cebb50\n--- /dev/null\n+++ b/t/t3452-history-split.sh\n@@ -0,0 +1,757 @@\n+#!/bin/sh\n+\n+test_description='tests for git-history split subcommand'\n+\n+. ./test-lib.sh\n+. \"$TEST_DIRECTORY/lib-log-graph.sh\"\n+\n+# The fake editor takes multiple arguments, each of which represents a commit\n+# message. Subsequent invocations of the editor will then yield those messages\n+# in order.\n+#\n+set_fake_editor () {\n+\tprintf \"%s\\n\" \"$@\" >fake-input &&\n+\twrite_script fake-editor.sh <<-\\EOF &&\n+\thead -n1 fake-input >\"$1\"\n+\tsed 1d fake-input >fake-input.trimmed &&\n+\tmv fake-input.trimmed fake-input\n+\tEOF\n+\ttest_set_editor \"$(pwd)\"/fake-editor.sh\n+}\n+\n+expect_graph () {\n+\tcat >expect &&\n+\tlib_test_cmp_graph --graph --format=%s \"$@\"\n+}\n+\n+expect_log () {\n+\tgit log --format=\"%s\" >actual &&\n+\tcat >expect &&\n+\ttest_cmp expect actual\n+}\n+\n+expect_tree_entries () {\n+\tgit ls-tree --name-only \"$1\" >actual &&\n+\tcat >expect &&\n+\ttest_cmp expect actual\n+}\n+\n+test_expect_success 'refuses to work with merge commits' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit base &&\n+\t\tgit branch branch &&\n+\t\ttest_commit ours &&\n+\t\tgit switch branch &&\n+\t\ttest_commit theirs &&\n+\t\tgit switch - &&\n+\t\tgit merge theirs &&\n+\t\ttest_must_fail git history split HEAD 2>err &&\n+\t\ttest_grep \"cannot split up merge commit\" err &&\n+\t\ttest_must_fail git history split HEAD~ 2>err &&\n+\t\ttest_grep \"replaying merge commits is not supported yet\" err\n+\t)\n+'\n+\n+test_expect_success 'errors on missing commit argument' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit initial &&\n+\t\ttest_must_fail git history split 2>err &&\n+\t\ttest_grep \"command expects a committish\" err\n+\t)\n+'\n+\n+test_expect_success 'errors on unknown revision' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit initial &&\n+\t\ttest_must_fail git history split does-not-exist 2>err &&\n+\t\ttest_grep \"commit cannot be found\" err\n+\t)\n+'\n+\n+test_expect_success '--dry-run does not modify any refs' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit base &&\n+\t\ttouch bar foo &&\n+\t\tgit add . &&\n+\t\tgit commit -m split-me &&\n+\n+\t\tgit refs list --include-root-refs >before &&\n+\n+\t\tset_fake_editor \"first\" \"second\" &&\n+\t\tgit history split --dry-run HEAD <<-EOF &&\n+\t\ty\n+\t\tn\n+\t\tEOF\n+\n+\t\tgit refs list --include-root-refs >after &&\n+\t\ttest_cmp before after\n+\t)\n+'\n+\n+test_expect_success 'can split up tip commit' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit initial &&\n+\t\ttouch bar foo &&\n+\t\tgit add . &&\n+\t\tgit commit -m split-me &&\n+\n+\t\tgit symbolic-ref HEAD >expect &&\n+\t\tset_fake_editor \"first\" \"second\" &&\n+\t\tgit history split HEAD <<-EOF &&\n+\t\ty\n+\t\tn\n+\t\tEOF\n+\t\tgit symbolic-ref HEAD >actual &&\n+\t\ttest_cmp expect actual &&\n+\n+\t\texpect_log <<-EOF &&\n+\t\tsecond\n+\t\tfirst\n+\t\tinitial\n+\t\tEOF\n+\n+\t\texpect_tree_entries HEAD~ <<-EOF &&\n+\t\tbar\n+\t\tinitial.t\n+\t\tEOF\n+\n+\t\texpect_tree_entries HEAD <<-EOF &&\n+\t\tbar\n+\t\tfoo\n+\t\tinitial.t\n+\t\tEOF\n+\n+\t\tgit reflog >reflog &&\n+\t\ttest_grep \"split: updating HEAD\" reflog\n+\t)\n+'\n+\n+test_expect_success 'can split up root commit' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttouch bar foo &&\n+\t\tgit add . &&\n+\t\tgit commit -m root &&\n+\t\ttest_commit tip &&\n+\n+\t\tset_fake_editor \"first\" \"second\" &&\n+\t\tgit history split HEAD~ <<-EOF &&\n+\t\ty\n+\t\tn\n+\t\tEOF\n+\n+\t\texpect_log <<-EOF &&\n+\t\ttip\n+\t\tsecond\n+\t\tfirst\n+\t\tEOF\n+\n+\t\texpect_tree_entries HEAD~2 <<-EOF &&\n+\t\tbar\n+\t\tEOF\n+\n+\t\texpect_tree_entries HEAD~ <<-EOF &&\n+\t\tbar\n+\t\tfoo\n+\t\tEOF\n+\n+\t\texpect_tree_entries HEAD <<-EOF\n+\t\tbar\n+\t\tfoo\n+\t\ttip.t\n+\t\tEOF\n+\t)\n+'\n+\n+test_expect_success 'can split up in-between commit' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit initial &&\n+\t\ttouch bar foo &&\n+\t\tgit add . &&\n+\t\tgit commit -m split-me &&\n+\t\ttest_commit tip &&\n+\n+\t\tset_fake_editor \"first\" \"second\" &&\n+\t\tgit history split HEAD~ <<-EOF &&\n+\t\ty\n+\t\tn\n+\t\tEOF\n+\n+\t\texpect_log <<-EOF &&\n+\t\ttip\n+\t\tsecond\n+\t\tfirst\n+\t\tinitial\n+\t\tEOF\n+\n+\t\texpect_tree_entries HEAD~2 <<-EOF &&\n+\t\tbar\n+\t\tinitial.t\n+\t\tEOF\n+\n+\t\texpect_tree_entries HEAD~ <<-EOF &&\n+\t\tbar\n+\t\tfoo\n+\t\tinitial.t\n+\t\tEOF\n+\n+\t\texpect_tree_entries HEAD <<-EOF\n+\t\tbar\n+\t\tfoo\n+\t\tinitial.t\n+\t\ttip.t\n+\t\tEOF\n+\t)\n+'\n+\n+test_expect_success 'can split HEAD only' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit base &&\n+\t\ttouch a b &&\n+\t\tgit add . &&\n+\t\tgit commit -m split-me &&\n+\t\tgit branch unrelated &&\n+\n+\t\tset_fake_editor \"ours-a\" \"ours-b\" &&\n+\t\tgit history split --update-refs=head HEAD <<-EOF &&\n+\t\ty\n+\t\tn\n+\t\tEOF\n+\t\texpect_graph --branches <<-EOF\n+\t\t* ours-b\n+\t\t* ours-a\n+\t\t| * split-me\n+\t\t|/\n+\t\t* base\n+\t\tEOF\n+\t)\n+'\n+\n+test_expect_success 'can split detached HEAD' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit initial &&\n+\t\ttouch bar foo &&\n+\t\tgit add . &&\n+\t\tgit commit -m split-me &&\n+\t\tgit checkout --detach HEAD &&\n+\n+\t\tset_fake_editor \"first\" \"second\" &&\n+\t\tgit history split --update-refs=head HEAD <<-EOF &&\n+\t\ty\n+\t\tn\n+\t\tEOF\n+\n+\t\t# HEAD should be detached and updated.\n+\t\ttest_must_fail git symbolic-ref HEAD &&\n+\n+\t\texpect_log <<-EOF\n+\t\tsecond\n+\t\tfirst\n+\t\tinitial\n+\t\tEOF\n+\t)\n+'\n+\n+test_expect_success 'can split commit in unrelated branch' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit base &&\n+\t\tgit branch ours &&\n+\t\tgit switch --create theirs &&\n+\t\ttouch theirs-a theirs-b &&\n+\t\tgit add . &&\n+\t\tgit commit -m theirs &&\n+\t\tgit switch ours &&\n+\t\ttest_commit ours &&\n+\n+\t\t# With --update-refs=head it is not possible to split up a\n+\t\t# commit that is unrelated to HEAD.\n+\t\ttest_must_fail git history split --update-refs=head theirs 2>err &&\n+\t\ttest_grep \"rewritten commit must be an ancestor of HEAD\" err &&\n+\n+\t\tset_fake_editor \"theirs-rewritten-a\" \"theirs-rewritten-b\" &&\n+\t\tgit history split theirs <<-EOF &&\n+\t\ty\n+\t\tn\n+\t\tEOF\n+\t\texpect_graph --branches <<-EOF &&\n+\t\t* ours\n+\t\t| * theirs-rewritten-b\n+\t\t| * theirs-rewritten-a\n+\t\t|/\n+\t\t* base\n+\t\tEOF\n+\n+\t\texpect_tree_entries theirs~ <<-EOF &&\n+\t\tbase.t\n+\t\ttheirs-a\n+\t\tEOF\n+\n+\t\texpect_tree_entries theirs <<-EOF\n+\t\tbase.t\n+\t\ttheirs-a\n+\t\ttheirs-b\n+\t\tEOF\n+\t)\n+'\n+\n+test_expect_success 'updates multiple descendant branches' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo --initial-branch=main &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit base &&\n+\t\ttouch file-a file-b &&\n+\t\tgit add . &&\n+\t\tgit commit -m split-me &&\n+\t\tgit branch branch &&\n+\t\ttest_commit on-main &&\n+\t\tgit switch branch &&\n+\t\ttest_commit on-branch &&\n+\t\tgit switch main &&\n+\n+\t\tset_fake_editor \"split-a\" \"split-b\" &&\n+\t\tgit history split HEAD~ <<-EOF &&\n+\t\ty\n+\t\tn\n+\t\tEOF\n+\n+\t\t# Both branches should now descend from the split commits.\n+\t\texpect_graph --branches <<-EOF\n+\t\t* on-branch\n+\t\t| * on-main\n+\t\t|/\n+\t\t* split-b\n+\t\t* split-a\n+\t\t* base\n+\t\tEOF\n+\t)\n+'\n+\n+test_expect_success 'can pick multiple hunks' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttouch bar baz foo qux &&\n+\t\tgit add . &&\n+\t\tgit commit -m split-me &&\n+\n+\t\tset_fake_editor \"first\" \"second\" &&\n+\t\tgit history split HEAD <<-EOF &&\n+\t\ty\n+\t\tn\n+\t\ty\n+\t\tn\n+\t\tEOF\n+\n+\t\texpect_tree_entries HEAD~ <<-EOF &&\n+\t\tbar\n+\t\tfoo\n+\t\tEOF\n+\n+\t\texpect_tree_entries HEAD <<-EOF\n+\t\tbar\n+\t\tbaz\n+\t\tfoo\n+\t\tqux\n+\t\tEOF\n+\t)\n+'\n+\n+test_expect_success 'can use only last hunk' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttouch bar foo &&\n+\t\tgit add . &&\n+\t\tgit commit -m split-me &&\n+\n+\t\tset_fake_editor \"first\" \"second\" &&\n+\t\tgit history split HEAD <<-EOF &&\n+\t\tn\n+\t\ty\n+\t\tEOF\n+\n+\t\texpect_log <<-EOF &&\n+\t\tsecond\n+\t\tfirst\n+\t\tEOF\n+\n+\t\texpect_tree_entries HEAD~ <<-EOF &&\n+\t\tfoo\n+\t\tEOF\n+\n+\t\texpect_tree_entries HEAD <<-EOF\n+\t\tbar\n+\t\tfoo\n+\t\tEOF\n+\t)\n+'\n+\n+test_expect_success 'can split commit with file deletions' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\techo a >a &&\n+\t\techo b >b &&\n+\t\techo c >c &&\n+\t\tgit add . &&\n+\t\tgit commit -m base &&\n+\t\tgit rm a b &&\n+\t\tgit commit -m delete-both &&\n+\n+\t\tset_fake_editor \"delete-a\" \"delete-b\" &&\n+\t\tgit history split HEAD <<-EOF &&\n+\t\ty\n+\t\tn\n+\t\tEOF\n+\n+\t\texpect_log <<-EOF &&\n+\t\tdelete-b\n+\t\tdelete-a\n+\t\tbase\n+\t\tEOF\n+\n+\t\texpect_tree_entries HEAD~ <<-EOF &&\n+\t\tb\n+\t\tc\n+\t\tEOF\n+\n+\t\texpect_tree_entries HEAD <<-EOF\n+\t\tc\n+\t\tEOF\n+\t)\n+'\n+\n+test_expect_success 'preserves original authorship' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit initial &&\n+\t\ttouch bar foo &&\n+\t\tgit add . &&\n+\t\tGIT_AUTHOR_NAME=\"Other Author\" \\\n+\t\tGIT_AUTHOR_EMAIL=\"other@example.com\" \\\n+\t\tgit commit -m split-me &&\n+\n+\t\tset_fake_editor \"first\" \"second\" &&\n+\t\tgit history split HEAD <<-EOF &&\n+\t\ty\n+\t\tn\n+\t\tEOF\n+\n+\t\tgit log -1 --format=\"%an <%ae>\" HEAD~ >actual &&\n+\t\techo \"Other Author <other@example.com>\" >expect &&\n+\t\ttest_cmp expect actual &&\n+\n+\t\tgit log -1 --format=\"%an <%ae>\" HEAD >actual &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+test_expect_success 'aborts with empty commit message' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttouch bar foo &&\n+\t\tgit add . &&\n+\t\tgit commit -m split-me &&\n+\n+\t\tset_fake_editor \"\" &&\n+\t\ttest_must_fail git history split HEAD <<-EOF 2>err &&\n+\t\ty\n+\t\tn\n+\t\tEOF\n+\t\ttest_grep \"Aborting commit due to empty commit message.\" err\n+\t)\n+'\n+\n+test_expect_success 'commit message editor sees split-out changes' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttouch bar foo &&\n+\t\tgit add . &&\n+\t\tgit commit -m split-me &&\n+\n+\t\twrite_script fake-editor.sh <<-\\EOF &&\n+\t\tcat \"$1\" >>MESSAGES &&\n+\t\techo \"some commit message\" >\"$1\"\n+\t\tEOF\n+\t\ttest_set_editor \"$(pwd)\"/fake-editor.sh &&\n+\n+\t\tgit history split HEAD <<-EOF &&\n+\t\ty\n+\t\tn\n+\t\tEOF\n+\n+\t\t# Note that we expect to see the messages twice, once for each\n+\t\t# of the commits. The committed files are different though.\n+\t\tcat >expect <<-EOF &&\n+\t\tsplit-me\n+\n+\t\t# Please enter the commit message for the split-out changes. Lines starting\n+\t\t# with ${SQ}#${SQ} will be ignored, and an empty message aborts the commit.\n+\t\t# Changes to be committed:\n+\t\t#\tnew file:   bar\n+\t\t#\n+\t\tsplit-me\n+\n+\t\t# Please enter the commit message for the split-out changes. Lines starting\n+\t\t# with ${SQ}#${SQ} will be ignored, and an empty message aborts the commit.\n+\t\t# Changes to be committed:\n+\t\t#\tnew file:   foo\n+\t\t#\n+\t\tEOF\n+\t\ttest_cmp expect MESSAGES &&\n+\n+\t\texpect_log <<-EOF\n+\t\tsome commit message\n+\t\tsome commit message\n+\t\tEOF\n+\t)\n+'\n+\n+test_expect_success 'can use pathspec to limit what gets split' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttouch bar foo &&\n+\t\tgit add . &&\n+\t\tgit commit -m split-me &&\n+\n+\t\tset_fake_editor \"first\" \"second\" &&\n+\t\tgit history split HEAD -- foo <<-EOF &&\n+\t\ty\n+\t\tEOF\n+\n+\t\texpect_tree_entries HEAD~ <<-EOF &&\n+\t\tfoo\n+\t\tEOF\n+\n+\t\texpect_tree_entries HEAD <<-EOF\n+\t\tbar\n+\t\tfoo\n+\t\tEOF\n+\t)\n+'\n+\n+test_expect_success 'pathspec matching no files produces empty split error' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit initial &&\n+\t\ttouch bar foo &&\n+\t\tgit add . &&\n+\t\tgit commit -m split-me &&\n+\n+\t\tset_fake_editor \"first\" \"second\" &&\n+\t\ttest_must_fail git history split HEAD -- nonexistent 2>err &&\n+\t\ttest_grep \"split commit is empty\" err\n+\t)\n+'\n+\n+test_expect_success 'split with multiple pathspecs' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit initial &&\n+\t\ttouch a b c d &&\n+\t\tgit add . &&\n+\t\tgit commit -m split-me &&\n+\n+\t\t# Only a and c should be offered for splitting.\n+\t\tset_fake_editor \"split-ac\" \"remainder\" &&\n+\t\tgit history split HEAD -- a c <<-EOF &&\n+\t\ty\n+\t\ty\n+\t\tEOF\n+\n+\t\texpect_tree_entries HEAD~ <<-EOF &&\n+\t\ta\n+\t\tc\n+\t\tinitial.t\n+\t\tEOF\n+\n+\t\texpect_tree_entries HEAD <<-EOF\n+\t\ta\n+\t\tb\n+\t\tc\n+\t\td\n+\t\tinitial.t\n+\t\tEOF\n+\t)\n+'\n+\n+test_expect_success 'split with file mode change' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\techo content >script &&\n+\t\tgit add . &&\n+\t\tgit commit -m base &&\n+\t\ttest_chmod +x script &&\n+\t\techo change >script &&\n+\t\tgit commit -a -m \"mode and content change\" &&\n+\n+\t\tset_fake_editor \"mode-change\" \"content-change\" &&\n+\t\tgit history split HEAD <<-EOF &&\n+\t\ty\n+\t\tn\n+\t\tEOF\n+\n+\t\texpect_log <<-EOF\n+\t\tcontent-change\n+\t\tmode-change\n+\t\tbase\n+\t\tEOF\n+\t)\n+'\n+\n+test_expect_success 'refuses to create empty split-out commit' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit base &&\n+\t\ttouch bar foo &&\n+\t\tgit add . &&\n+\t\tgit commit -m split-me &&\n+\n+\t\ttest_must_fail git history split HEAD 2>err <<-EOF &&\n+\t\tn\n+\t\tn\n+\t\tEOF\n+\t\ttest_grep \"split commit is empty\" err\n+\t)\n+'\n+\n+test_expect_success 'hooks are not executed for rewritten commits' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttouch bar foo &&\n+\t\tgit add . &&\n+\t\tgit commit -m split-me &&\n+\t\told_head=$(git rev-parse HEAD) &&\n+\n+\t\tORIG_PATH=\"$(pwd)\" &&\n+\t\texport ORIG_PATH &&\n+\t\tfor hook in prepare-commit-msg pre-commit post-commit post-rewrite commit-msg\n+\t\tdo\n+\t\t\twrite_script .git/hooks/$hook <<-\\EOF || exit 1\n+\t\t\ttouch \"$ORIG_PATH\"/hooks.log\n+\t\t\tEOF\n+\t\tdone &&\n+\n+\t\tset_fake_editor \"first\" \"second\" &&\n+\t\tgit history split HEAD <<-EOF &&\n+\t\ty\n+\t\tn\n+\t\tEOF\n+\n+\t\texpect_log <<-EOF &&\n+\t\tsecond\n+\t\tfirst\n+\t\tEOF\n+\n+\t\ttest_path_is_missing hooks.log\n+\t)\n+'\n+\n+test_expect_success 'refuses to create empty original commit' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttouch bar foo &&\n+\t\tgit add . &&\n+\t\tgit commit -m split-me &&\n+\n+\t\ttest_must_fail git history split HEAD 2>err <<-EOF &&\n+\t\ty\n+\t\ty\n+\t\tEOF\n+\t\ttest_grep \"split commit tree matches original commit\" err\n+\t)\n+'\n+\n+test_expect_success 'retains changes in the worktree and index' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\techo a >a &&\n+\t\techo b >b &&\n+\t\tgit add . &&\n+\t\tgit commit -m \"initial commit\" &&\n+\t\techo a-modified >a &&\n+\t\techo b-modified >b &&\n+\t\tgit add b &&\n+\t\tset_fake_editor \"a-only\" \"remainder\" &&\n+\t\tgit history split HEAD <<-EOF &&\n+\t\ty\n+\t\tn\n+\t\tEOF\n+\n+\t\texpect_tree_entries HEAD~ <<-EOF &&\n+\t\ta\n+\t\tEOF\n+\t\texpect_tree_entries HEAD <<-EOF &&\n+\t\ta\n+\t\tb\n+\t\tEOF\n+\n+\t\tcat >expect <<-\\EOF &&\n+\t\t M a\n+\t\tM  b\n+\t\t?? actual\n+\t\t?? expect\n+\t\t?? fake-editor.sh\n+\t\t?? fake-input\n+\t\tEOF\n+\t\tgit status --porcelain >actual &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+test_done\n\n-- \n2.53.0.697.g625c4fb2da.dirty\n\n"},{"id":"537715","messageId":"CALnO6CDMF1G2AFMDXu=xhCv5XcgKm_-JXY2RvLc4TsD9Kh-K6Q@mail.gmail.com","threadId":"65116","inReplyTo":"20260302-pks-history-split-v1-0-444fc987a324@pks.im","subject":"Re: [PATCH 0/8] history: introduce \"split\" subcommand","fromName":"D. Ben Knoble","fromEmail":"ben.knoble@gmail.com","sentAt":"2026-03-03T18:41:09Z","receivedAt":"2026-03-03T18:41:22Z","isPatch":true,"sender":{"key":"ben.knoble@gmail.com","avatar":"https://avatars.githubusercontent.com/u/22802209?v=4"},"body":"On Mon, Mar 2, 2026 at 7:15 AM Patrick Steinhardt <ps@pks.im> wrote:\n>\n> Hi,\n>\n> this patch series introduces `git history split` as an easy way to split\n> up one commit into multiple commits.\n\nExciting news, thanks!\n\n> This subcommand has already been\n> introduced in earlier versions of my git-history(1) patch series, but I\n> eventually decided to evict them from this series so that we can rather\n> focus more on basic decisions.\n>\n> In any case, the current version of this patch series matches (to the\n> best of my knowledge) the latest agreements on the mailing list around\n> its behaviour. Most importantly:\n>\n>   - It will ask for commit messages for both commits, not only the first\n>     commit, which has been a bit of a discussion point.\n>\n>   - It is not possible to edit hunks. This results in a mode where\n>     conflicts are not possible as the tree of the second commit will\n>     always match the tree of the original commit. Conflict handling for\n>     subsequent subcommands will be a bigger topic, as it probably\n>     depends on support for first-class conflicts.\n>\n>   - We also update dependent branches, same as with the latest iteration\n>     of `git history reword`.\n\nMatches my memory. I think the dependent branches bit should probably\ndepend on the same --update-refs option, so I'll expect to see that\n(not sure why we'd make any other choice ;).\n\n-- \nD. Ben Knoble\n"},{"id":"537717","messageId":"CALnO6CC5FB29bHPtyKD=L5EWxTCLx3K2qd+wGySdck7tCvvs_w@mail.gmail.com","threadId":"65116","inReplyTo":"20260302-pks-history-split-v1-7-444fc987a324@pks.im","subject":"Re: [PATCH 7/8] builtin/history: split out extended function to create commits","fromName":"D. Ben Knoble","fromEmail":"ben.knoble@gmail.com","sentAt":"2026-03-03T18:43:12Z","receivedAt":"2026-03-03T18:43:24Z","isPatch":true,"sender":{"key":"ben.knoble@gmail.com","avatar":"https://avatars.githubusercontent.com/u/22802209?v=4"},"body":"On Mon, Mar 2, 2026 at 7:17 AM Patrick Steinhardt <ps@pks.im> wrote:\n>\n> In the next commit we're about to introduce a new command that splits up\n> a commit into two. Most of the logic will be shared with rewording\n> commits, except that we also need to have control over the parents and\n> the old/new trees.\n>\n> Extract a new function `commit_tree_with_edited_message_ext()` to\n> prepare for this commit.\n\nCurious—what's the \"ext\" suffix mean here. Extracted? External? (Maybe\nI'll get a better clue in the next patch.)\n\n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n>  builtin/history.c | 67 +++++++++++++++++++++++++++++++++----------------------\n>  1 file changed, 40 insertions(+), 27 deletions(-)\n>\n> diff --git a/builtin/history.c b/builtin/history.c\n> index 1cf6c668cf..80726ce14b 100644\n> --- a/builtin/history.c\n> +++ b/builtin/history.c\n> @@ -83,10 +83,13 @@ static int fill_commit_message(struct repository *repo,\n>         return 0;\n>  }\n>\n> -static int commit_tree_with_edited_message(struct repository *repo,\n> -                                          const char *action,\n> -                                          struct commit *original,\n> -                                          struct commit **out)\n> +static int commit_tree_with_edited_message_ext(struct repository *repo,\n> +                                              const char *action,\n> +                                              struct commit *commit_with_message,\n> +                                              const struct commit_list *parents,\n> +                                              const struct object_id *old_tree,\n> +                                              const struct object_id *new_tree,\n> +                                              struct commit **out)\n>  {\n>         const char *exclude_gpgsig[] = {\n>                 /* We reencode the message, so the encoding needs to be stripped. */\n> @@ -100,44 +103,27 @@ static int commit_tree_with_edited_message(struct repository *repo,\n>         struct commit_extra_header *original_extra_headers = NULL;\n>         struct strbuf commit_message = STRBUF_INIT;\n>         struct object_id rewritten_commit_oid;\n> -       struct object_id original_tree_oid;\n> -       struct object_id parent_tree_oid;\n>         char *original_author = NULL;\n> -       struct commit *parent;\n>         size_t len;\n>         int ret;\n>\n> -       original_tree_oid = repo_get_commit_tree(repo, original)->object.oid;\n> -\n> -       parent = original->parents ? original->parents->item : NULL;\n> -       if (parent) {\n> -               if (repo_parse_commit(repo, parent)) {\n> -                       ret = error(_(\"unable to parse parent commit %s\"),\n> -                                   oid_to_hex(&parent->object.oid));\n> -                       goto out;\n> -               }\n> -\n> -               parent_tree_oid = repo_get_commit_tree(repo, parent)->object.oid;\n> -       } else {\n> -               oidcpy(&parent_tree_oid, repo->hash_algo->empty_tree);\n> -       }\n> -\n>         /* We retain authorship of the original commit. */\n> -       original_message = repo_logmsg_reencode(repo, original, NULL, NULL);\n> +       original_message = repo_logmsg_reencode(repo, commit_with_message, NULL, NULL);\n>         ptr = find_commit_header(original_message, \"author\", &len);\n>         if (ptr)\n>                 original_author = xmemdupz(ptr, len);\n>         find_commit_subject(original_message, &original_body);\n>\n> -       ret = fill_commit_message(repo, &parent_tree_oid, &original_tree_oid,\n> +       ret = fill_commit_message(repo, old_tree, new_tree,\n>                                   original_body, action, &commit_message);\n>         if (ret < 0)\n>                 goto out;\n>\n> -       original_extra_headers = read_commit_extra_headers(original, exclude_gpgsig);\n> +       original_extra_headers = read_commit_extra_headers(commit_with_message,\n> +                                                          exclude_gpgsig);\n>\n> -       ret = commit_tree_extended(commit_message.buf, commit_message.len, &original_tree_oid,\n> -                                  original->parents, &rewritten_commit_oid, original_author,\n> +       ret = commit_tree_extended(commit_message.buf, commit_message.len, new_tree,\n> +                                  parents, &rewritten_commit_oid, original_author,\n>                                    NULL, NULL, original_extra_headers);\n>         if (ret < 0)\n>                 goto out;\n> @@ -151,6 +137,33 @@ static int commit_tree_with_edited_message(struct repository *repo,\n>         return ret;\n>  }\n>\n> +static int commit_tree_with_edited_message(struct repository *repo,\n> +                                          const char *action,\n> +                                          struct commit *original,\n> +                                          struct commit **out)\n> +{\n> +       struct object_id parent_tree_oid;\n> +       const struct object_id *tree_oid;\n> +       struct commit *parent;\n> +\n> +       tree_oid = &repo_get_commit_tree(repo, original)->object.oid;\n> +\n> +       parent = original->parents ? original->parents->item : NULL;\n> +       if (parent) {\n> +               if (repo_parse_commit(repo, parent)) {\n> +                       return error(_(\"unable to parse parent commit %s\"),\n> +                                    oid_to_hex(&parent->object.oid));\n> +               }\n> +\n> +               parent_tree_oid = repo_get_commit_tree(repo, parent)->object.oid;\n> +       } else {\n> +               oidcpy(&parent_tree_oid, repo->hash_algo->empty_tree);\n> +       }\n> +\n> +       return commit_tree_with_edited_message_ext(repo, action, original, original->parents,\n> +                                                  &parent_tree_oid, tree_oid, out);\n> +}\n> +\n>  enum ref_action {\n>         REF_ACTION_DEFAULT,\n>         REF_ACTION_BRANCHES,\n>\n> --\n> 2.53.0.697.g625c4fb2da.dirty\n>\n>\n\n\n-- \nD. Ben Knoble\n"},{"id":"537718","messageId":"CALnO6CC_UMnQvcyCe37mCan8eASugknK-WbVp-KWXWptvrsJDg@mail.gmail.com","threadId":"65116","inReplyTo":"20260302-pks-history-split-v1-8-444fc987a324@pks.im","subject":"Re: [PATCH 8/8] builtin/history: implement \"split\" subcommand","fromName":"D. Ben Knoble","fromEmail":"ben.knoble@gmail.com","sentAt":"2026-03-03T18:47:27Z","receivedAt":"2026-03-03T18:47:40Z","isPatch":true,"sender":{"key":"ben.knoble@gmail.com","avatar":"https://avatars.githubusercontent.com/u/22802209?v=4"},"body":"On Mon, Mar 2, 2026 at 7:13 AM Patrick Steinhardt <ps@pks.im> wrote:\n>\n> It is quite a common use case that one wants to split up one commit into\n> multiple commits by moving parts of the changes of the original commit\n> out into a separate commit. This is quite an involved operation though:\n>\n>   1. Identify the commit in question that is to be dropped.\n>\n>   2. Perform an interactive rebase on top of that commit's parent.\n>\n>   3. Modify the instruction sheet to \"edit\" the commit that is to be\n>      split up.\n>\n>   4. Drop the commit via \"git reset HEAD~\".\n>\n>   5. Stage changes that should go into the first commit and commit it.\n>\n>   6. Stage changes that should go into the second commit and commit it.\n>\n>   7. Finalize the rebase.\n>\n> This is quite complex, and overall I would claim that most people who\n> are not experts in Git would struggle with this flow.\n>\n> Introduce a new \"split\" subcommand for git-history(1) to make this way\n> easier. All the user needs to do is to say `git history split $COMMIT`.\n> From hereon, Git asks the user which parts of the commit shall be moved\n> out into a separate commit and, once done, asks the user for the commit\n> message. Git then creates that split-out commit and applies the original\n> commit on top of it.\n>\n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n>  Documentation/git-history.adoc |  62 ++++\n>  builtin/history.c              | 250 ++++++++++++++\n>  t/meson.build                  |   1 +\n>  t/t3452-history-split.sh       | 757 +++++++++++++++++++++++++++++++++++++++++\n>  4 files changed, 1070 insertions(+)\n>\n> diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc\n> index cc019de697..24dc907033 100644\n> --- a/Documentation/git-history.adoc\n> +++ b/Documentation/git-history.adoc\n> @@ -9,6 +9,7 @@ SYNOPSIS\n>  --------\n>  [synopsis]\n>  git history reword <commit> [--dry-run] [--update-refs=(branches|head)]\n> +git history split <commit> [--dry-run] [--update-refs=(branches|head)] [--] [<pathspec>...]\n>\n>  DESCRIPTION\n>  -----------\n> @@ -57,6 +58,26 @@ The following commands are available to rewrite history in different ways:\n>         details of this commit remain unchanged. This command will spawn an\n>         editor with the current message of that commit.\n>\n> +`split <commit> [--] [<pathspec>...]`::\n> +       Interactively split up <commit> into two commits by choosing\n> +       hunks introduced by it that will be moved into the new split-out\n> +       commit. These hunks will then be written into a new commit that\n> +       becomes the parent of the previous commit. The original commit\n> +       stays intact, except that its parent will be the newly split-out\n> +       commit.\n> ++\n> +The commit messages of the split-up commits will be asked for by launching\n> +the configured editor. Authorship of the commit will be the same as for the\n> +original commit.\n> ++\n> +If passed, _<pathspec>_ can be used to limit which changes shall be split out\n> +of the original commit. Files not matching any of the pathspecs will remain\n> +part of the original commit. For more details, see the 'pathspec' entry in\n> +linkgit:gitglossary[7].\n\nThat is quite convenient when changes to 2 independent areas have\nbecome mixed. Nice.\n\n> +It is invalid to select either all or no hunks, as that would lead to\n> +one of the commits becoming empty.\n\nIs it easy to make this a no-op? It could be done later if that\nsuggestion is contentions. But I figure rather than error we can\nsilently do nothing, since we have performed the desired split. (Or\neven use this to split an \"--allow-empty\" commit, but… why that's\ndesirable, I can't guess.)\n\nSo yeah, probably for later.\n\n> +\n>  OPTIONS\n>  -------\n>\n> @@ -72,6 +93,47 @@ OPTIONS\n>         descendants of the original commit will be rewritten. With `head`, only\n>         the current `HEAD` reference will be rewritten. Defaults to `branches`.\n>\n> +EXAMPLES\n> +--------\n> +\n> +Split a commit\n> +~~~~~~~~~~~~~~\n> +\n> +----------\n> +$ git log --stat --oneline\n> +3f81232 (HEAD -> main) original\n> + bar | 1 +\n> + foo | 1 +\n> + 2 files changed, 2 insertions(+)\n> +\n> +$ git history split HEAD\n> +diff --git a/bar b/bar\n> +new file mode 100644\n> +index 0000000..5716ca5\n> +--- /dev/null\n> ++++ b/bar\n> +@@ -0,0 +1 @@\n> ++bar\n> +(1/1) Stage addition [y,n,q,a,d,p,?]? y\n> +\n> +diff --git a/foo b/foo\n> +new file mode 100644\n> +index 0000000..257cc56\n> +--- /dev/null\n> ++++ b/foo\n> +@@ -0,0 +1 @@\n> ++foo\n> +(1/1) Stage addition [y,n,q,a,d,p,?]? n\n> +\n> +$ git log --stat --oneline\n> +7cebe64 (HEAD -> main) original\n> + foo | 1 +\n> + 1 file changed, 1 insertion(+)\n> +d1582f3 split-out commit\n> + bar | 1 +\n> + 1 file changed, 1 insertion(+)\n> +----------\n> +\n>  GIT\n>  ---\n>  Part of the linkgit:git[1] suite\n> diff --git a/builtin/history.c b/builtin/history.c\n> index 80726ce14b..dfb9d3f180 100644\n> --- a/builtin/history.c\n> +++ b/builtin/history.c\n> @@ -1,6 +1,7 @@\n>  #define USE_THE_REPOSITORY_VARIABLE\n>\n>  #include \"builtin.h\"\n> +#include \"cache-tree.h\"\n>  #include \"commit.h\"\n>  #include \"commit-reach.h\"\n>  #include \"config.h\"\n> @@ -8,17 +9,24 @@\n>  #include \"environment.h\"\n>  #include \"gettext.h\"\n>  #include \"hex.h\"\n> +#include \"lockfile.h\"\n> +#include \"oidmap.h\"\n>  #include \"parse-options.h\"\n> +#include \"path.h\"\n> +#include \"read-cache.h\"\n>  #include \"refs.h\"\n>  #include \"replay.h\"\n>  #include \"revision.h\"\n>  #include \"sequencer.h\"\n>  #include \"strvec.h\"\n>  #include \"tree.h\"\n> +#include \"unpack-trees.h\"\n>  #include \"wt-status.h\"\n>\n>  #define GIT_HISTORY_REWORD_USAGE \\\n>         N_(\"git history reword <commit> [--dry-run] [--update-refs=(branches|head)]\")\n> +#define GIT_HISTORY_SPLIT_USAGE \\\n> +       N_(\"git history split <commit> [--dry-run] [--update-refs=(branches|head)] [--] [<pathspec>...]\")\n>\n>  static void change_data_free(void *util, const char *str UNUSED)\n>  {\n> @@ -484,6 +492,246 @@ static int cmd_history_reword(int argc,\n>         return ret;\n>  }\n>\n> +static int write_ondisk_index(struct repository *repo,\n> +                             struct object_id *oid,\n> +                             const char *path)\n> +{\n> +       struct unpack_trees_options opts = { 0 };\n> +       struct lock_file lock = LOCK_INIT;\n> +       struct tree_desc tree_desc;\n> +       struct index_state index;\n> +       struct tree *tree;\n> +       int ret;\n> +\n> +       index_state_init(&index, repo);\n> +\n> +       opts.head_idx = -1;\n> +       opts.src_index = &index;\n> +       opts.dst_index = &index;\n> +\n> +       tree = repo_parse_tree_indirect(repo, oid);\n> +       init_tree_desc(&tree_desc, &tree->object.oid, tree->buffer, tree->size);\n> +\n> +       if (unpack_trees(1, &tree_desc, &opts)) {\n> +               ret = error(_(\"unable to populate index with tree\"));\n> +               goto out;\n> +       }\n> +\n> +       prime_cache_tree(repo, &index, tree);\n> +\n> +       if (hold_lock_file_for_update(&lock, path, 0) < 0) {\n> +               ret = error_errno(_(\"unable to acquire index lock\"));\n> +               goto out;\n> +       }\n> +\n> +       if (write_locked_index(&index, &lock, COMMIT_LOCK)) {\n> +               ret = error(_(\"unable to write new index file\"));\n> +               goto out;\n> +       }\n> +\n> +       ret = 0;\n> +\n> +out:\n> +       rollback_lock_file(&lock);\n> +       release_index(&index);\n> +       return ret;\n> +}\n> +\n> +static int split_commit(struct repository *repo,\n> +                       struct commit *original,\n> +                       struct pathspec *pathspec,\n> +                       struct commit **out)\n> +{\n> +       struct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;\n> +       struct strbuf index_file = STRBUF_INIT;\n> +       struct index_state index = INDEX_STATE_INIT(repo);\n> +       const struct object_id *original_commit_tree_oid;\n> +       const struct object_id *old_tree_oid, *new_tree_oid;\n> +       struct object_id parent_tree_oid;\n> +       char original_commit_oid[GIT_MAX_HEXSZ + 1];\n> +       struct commit *first_commit, *second_commit;\n> +       struct commit_list *parents = NULL;\n> +       struct tree *split_tree;\n> +       int ret;\n> +\n> +       if (original->parents) {\n> +               if (repo_parse_commit(repo, original->parents->item)) {\n> +                       ret = error(_(\"unable to parse parent commit %s\"),\n> +                                   oid_to_hex(&original->parents->item->object.oid));\n> +                       goto out;\n> +               }\n> +\n> +               parent_tree_oid = *get_commit_tree_oid(original->parents->item);\n> +       } else {\n> +               oidcpy(&parent_tree_oid, repo->hash_algo->empty_tree);\n> +       }\n> +       original_commit_tree_oid = get_commit_tree_oid(original);\n> +\n> +       /*\n> +        * Construct the first commit. This is done by taking the original\n> +        * commit parent's tree and selectively patching changes from the diff\n> +        * between that parent and its child.\n> +        */\n> +       repo_git_path_replace(repo, &index_file, \"%s\", \"history-split.index\");\n> +\n> +       ret = write_ondisk_index(repo, &parent_tree_oid, index_file.buf);\n> +       if (ret < 0)\n> +               goto out;\n> +\n> +       ret = read_index_from(&index, index_file.buf, repo->gitdir);\n> +       if (ret < 0) {\n> +               ret = error(_(\"failed reading temporary index\"));\n> +               goto out;\n> +       }\n> +\n> +       oid_to_hex_r(original_commit_oid, &original->object.oid);\n> +       ret = run_add_p_index(repo, &index, index_file.buf, &interactive_opts,\n> +                             original_commit_oid, pathspec, ADD_P_DISALLOW_EDIT);\n> +       if (ret < 0)\n> +               goto out;\n> +\n> +       split_tree = write_in_core_index_as_tree(repo, &index);\n> +       if (!split_tree) {\n> +               ret = error(_(\"failed split tree\"));\n> +               goto out;\n> +       }\n> +\n> +       unlink(index_file.buf);\n> +       strbuf_release(&index_file);\n> +\n> +       /*\n> +        * We disallow the cases where either the split-out commit or the\n> +        * original commit would become empty. Consequently, if we see that the\n> +        * new tree ID matches either of those trees we abort.\n> +        */\n> +       if (oideq(&split_tree->object.oid, &parent_tree_oid)) {\n> +               ret = error(_(\"split commit is empty\"));\n> +               goto out;\n> +       } else if (oideq(&split_tree->object.oid, original_commit_tree_oid)) {\n> +               ret = error(_(\"split commit tree matches original commit\"));\n> +               goto out;\n> +       }\n> +\n> +       /*\n> +        * The first commit is constructed from the split-out tree. The base\n> +        * that shall be diffed against is the parent of the original commit.\n> +        */\n> +       ret = commit_tree_with_edited_message_ext(repo, \"split-out\", original,\n> +                                                 original->parents, &parent_tree_oid,\n> +                                                 &split_tree->object.oid, &first_commit);\n> +       if (ret < 0) {\n> +               ret = error(_(\"failed writing first commit\"));\n> +               goto out;\n> +       }\n> +\n> +       /*\n> +        * The second commit is constructed from the original tree. The base to\n> +        * diff against and the parent in this case is the first split-out\n> +        * commit.\n> +        */\n> +       commit_list_append(first_commit, &parents);\n> +\n> +       old_tree_oid = &repo_get_commit_tree(repo, first_commit)->object.oid;\n> +       new_tree_oid = &repo_get_commit_tree(repo, original)->object.oid;\n> +\n> +       ret = commit_tree_with_edited_message_ext(repo, \"split-out\", original,\n> +                                                 parents, old_tree_oid,\n> +                                                 new_tree_oid, &second_commit);\n> +       if (ret < 0) {\n> +               ret = error(_(\"failed writing second commit\"));\n> +               goto out;\n> +       }\n> +\n> +       *out = second_commit;\n> +       ret = 0;\n> +\n> +out:\n> +       if (index_file.len)\n> +               unlink(index_file.buf);\n> +       strbuf_release(&index_file);\n> +       free_commit_list(parents);\n> +       release_index(&index);\n> +       return ret;\n> +}\n> +\n> +static int cmd_history_split(int argc,\n> +                            const char **argv,\n> +                            const char *prefix,\n> +                            struct repository *repo)\n> +{\n> +       const char * const usage[] = {\n> +               GIT_HISTORY_SPLIT_USAGE,\n> +               NULL,\n> +       };\n> +       enum ref_action action = REF_ACTION_DEFAULT;\n> +       int dry_run = 0;\n> +       struct option options[] = {\n> +               OPT_CALLBACK_F(0, \"update-refs\", &action, N_(\"<refs>\"),\n> +                              N_(\"control ref update behavior (branches|head|print)\"),\n> +                              PARSE_OPT_NONEG, parse_ref_action),\n> +               OPT_BOOL('n', \"dry-run\", &dry_run,\n> +                        N_(\"perform a dry-run without updating any refs\")),\n> +               OPT_END(),\n> +       };\n> +       struct commit *original, *rewritten = NULL;\n> +       struct strbuf reflog_msg = STRBUF_INIT;\n> +       struct pathspec pathspec = { 0 };\n> +       struct rev_info revs = { 0 };\n> +       int ret;\n> +\n> +       argc = parse_options(argc, argv, prefix, options, usage, 0);\n> +       if (argc < 1) {\n> +               ret = error(_(\"command expects a committish\"));\n> +               goto out;\n> +       }\n> +       repo_config(repo, git_default_config, NULL);\n> +\n> +       if (action == REF_ACTION_DEFAULT)\n> +               action = REF_ACTION_BRANCHES;\n> +\n> +       parse_pathspec(&pathspec, 0,\n> +                      PATHSPEC_PREFER_FULL |\n> +                      PATHSPEC_SYMLINK_LEADING_PATH |\n> +                      PATHSPEC_PREFIX_ORIGIN,\n> +                      prefix, argv + 1);\n> +\n> +       original = lookup_commit_reference_by_name(argv[0]);\n> +       if (!original) {\n> +               ret = error(_(\"commit cannot be found: %s\"), argv[0]);\n> +               goto out;\n> +       }\n> +\n> +       ret = setup_revwalk(repo, action, original, &revs);\n> +       if (ret < 0)\n> +               goto out;\n> +\n> +       if (original->parents && original->parents->next) {\n> +               ret = error(_(\"cannot split up merge commit\"));\n> +               goto out;\n> +       }\n> +\n> +       ret = split_commit(repo, original, &pathspec, &rewritten);\n> +       if (ret < 0)\n> +               goto out;\n> +\n> +       strbuf_addf(&reflog_msg, \"split: updating %s\", argv[0]);\n> +\n> +       ret = handle_reference_updates(&revs, action, original, rewritten,\n> +                                      reflog_msg.buf, dry_run);\n> +       if (ret < 0) {\n> +               ret = error(_(\"failed replaying descendants\"));\n> +               goto out;\n> +       }\n> +\n> +       ret = 0;\n> +\n> +out:\n> +       strbuf_release(&reflog_msg);\n> +       clear_pathspec(&pathspec);\n> +       release_revisions(&revs);\n> +       return ret;\n> +}\n> +\n>  int cmd_history(int argc,\n>                 const char **argv,\n>                 const char *prefix,\n> @@ -491,11 +739,13 @@ int cmd_history(int argc,\n>  {\n>         const char * const usage[] = {\n>                 GIT_HISTORY_REWORD_USAGE,\n> +               GIT_HISTORY_SPLIT_USAGE,\n>                 NULL,\n>         };\n>         parse_opt_subcommand_fn *fn = NULL;\n>         struct option options[] = {\n>                 OPT_SUBCOMMAND(\"reword\", &fn, cmd_history_reword),\n> +               OPT_SUBCOMMAND(\"split\", &fn, cmd_history_split),\n>                 OPT_END(),\n>         };\n>\n> diff --git a/t/meson.build b/t/meson.build\n> index 6d91470ebc..2d578ef58b 100644\n> --- a/t/meson.build\n> +++ b/t/meson.build\n> @@ -392,6 +392,7 @@ integration_tests = [\n>    't3438-rebase-broken-files.sh',\n>    't3450-history.sh',\n>    't3451-history-reword.sh',\n> +  't3452-history-split.sh',\n>    't3500-cherry.sh',\n>    't3501-revert-cherry-pick.sh',\n>    't3502-cherry-pick-merge.sh',\n> diff --git a/t/t3452-history-split.sh b/t/t3452-history-split.sh\n> new file mode 100755\n> index 0000000000..8ed0cebb50\n> --- /dev/null\n> +++ b/t/t3452-history-split.sh\n> @@ -0,0 +1,757 @@\n> +#!/bin/sh\n> +\n> +test_description='tests for git-history split subcommand'\n> +\n> +. ./test-lib.sh\n> +. \"$TEST_DIRECTORY/lib-log-graph.sh\"\n> +\n> +# The fake editor takes multiple arguments, each of which represents a commit\n> +# message. Subsequent invocations of the editor will then yield those messages\n> +# in order.\n> +#\n> +set_fake_editor () {\n> +       printf \"%s\\n\" \"$@\" >fake-input &&\n> +       write_script fake-editor.sh <<-\\EOF &&\n> +       head -n1 fake-input >\"$1\"\n> +       sed 1d fake-input >fake-input.trimmed &&\n> +       mv fake-input.trimmed fake-input\n> +       EOF\n> +       test_set_editor \"$(pwd)\"/fake-editor.sh\n> +}\n> +\n> +expect_graph () {\n> +       cat >expect &&\n> +       lib_test_cmp_graph --graph --format=%s \"$@\"\n> +}\n> +\n> +expect_log () {\n> +       git log --format=\"%s\" >actual &&\n> +       cat >expect &&\n> +       test_cmp expect actual\n> +}\n> +\n> +expect_tree_entries () {\n> +       git ls-tree --name-only \"$1\" >actual &&\n> +       cat >expect &&\n> +       test_cmp expect actual\n> +}\n> +\n> +test_expect_success 'refuses to work with merge commits' '\n> +       test_when_finished \"rm -rf repo\" &&\n> +       git init repo &&\n> +       (\n> +               cd repo &&\n> +               test_commit base &&\n> +               git branch branch &&\n> +               test_commit ours &&\n> +               git switch branch &&\n> +               test_commit theirs &&\n> +               git switch - &&\n> +               git merge theirs &&\n> +               test_must_fail git history split HEAD 2>err &&\n> +               test_grep \"cannot split up merge commit\" err &&\n> +               test_must_fail git history split HEAD~ 2>err &&\n> +               test_grep \"replaying merge commits is not supported yet\" err\n> +       )\n> +'\n> +\n> +test_expect_success 'errors on missing commit argument' '\n> +       test_when_finished \"rm -rf repo\" &&\n> +       git init repo &&\n> +       (\n> +               cd repo &&\n> +               test_commit initial &&\n> +               test_must_fail git history split 2>err &&\n> +               test_grep \"command expects a committish\" err\n> +       )\n> +'\n> +\n> +test_expect_success 'errors on unknown revision' '\n> +       test_when_finished \"rm -rf repo\" &&\n> +       git init repo &&\n> +       (\n> +               cd repo &&\n> +               test_commit initial &&\n> +               test_must_fail git history split does-not-exist 2>err &&\n> +               test_grep \"commit cannot be found\" err\n> +       )\n> +'\n> +\n> +test_expect_success '--dry-run does not modify any refs' '\n> +       test_when_finished \"rm -rf repo\" &&\n> +       git init repo &&\n> +       (\n> +               cd repo &&\n> +               test_commit base &&\n> +               touch bar foo &&\n> +               git add . &&\n> +               git commit -m split-me &&\n> +\n> +               git refs list --include-root-refs >before &&\n> +\n> +               set_fake_editor \"first\" \"second\" &&\n> +               git history split --dry-run HEAD <<-EOF &&\n> +               y\n> +               n\n> +               EOF\n> +\n> +               git refs list --include-root-refs >after &&\n> +               test_cmp before after\n> +       )\n> +'\n> +\n> +test_expect_success 'can split up tip commit' '\n> +       test_when_finished \"rm -rf repo\" &&\n> +       git init repo &&\n> +       (\n> +               cd repo &&\n> +               test_commit initial &&\n> +               touch bar foo &&\n> +               git add . &&\n> +               git commit -m split-me &&\n> +\n> +               git symbolic-ref HEAD >expect &&\n> +               set_fake_editor \"first\" \"second\" &&\n> +               git history split HEAD <<-EOF &&\n> +               y\n> +               n\n> +               EOF\n> +               git symbolic-ref HEAD >actual &&\n> +               test_cmp expect actual &&\n> +\n> +               expect_log <<-EOF &&\n> +               second\n> +               first\n> +               initial\n> +               EOF\n> +\n> +               expect_tree_entries HEAD~ <<-EOF &&\n> +               bar\n> +               initial.t\n> +               EOF\n> +\n> +               expect_tree_entries HEAD <<-EOF &&\n> +               bar\n> +               foo\n> +               initial.t\n> +               EOF\n> +\n> +               git reflog >reflog &&\n> +               test_grep \"split: updating HEAD\" reflog\n> +       )\n> +'\n> +\n> +test_expect_success 'can split up root commit' '\n> +       test_when_finished \"rm -rf repo\" &&\n> +       git init repo &&\n> +       (\n> +               cd repo &&\n> +               touch bar foo &&\n> +               git add . &&\n> +               git commit -m root &&\n> +               test_commit tip &&\n> +\n> +               set_fake_editor \"first\" \"second\" &&\n> +               git history split HEAD~ <<-EOF &&\n> +               y\n> +               n\n> +               EOF\n> +\n> +               expect_log <<-EOF &&\n> +               tip\n> +               second\n> +               first\n> +               EOF\n> +\n> +               expect_tree_entries HEAD~2 <<-EOF &&\n> +               bar\n> +               EOF\n> +\n> +               expect_tree_entries HEAD~ <<-EOF &&\n> +               bar\n> +               foo\n> +               EOF\n> +\n> +               expect_tree_entries HEAD <<-EOF\n> +               bar\n> +               foo\n> +               tip.t\n> +               EOF\n> +       )\n> +'\n> +\n> +test_expect_success 'can split up in-between commit' '\n> +       test_when_finished \"rm -rf repo\" &&\n> +       git init repo &&\n> +       (\n> +               cd repo &&\n> +               test_commit initial &&\n> +               touch bar foo &&\n> +               git add . &&\n> +               git commit -m split-me &&\n> +               test_commit tip &&\n> +\n> +               set_fake_editor \"first\" \"second\" &&\n> +               git history split HEAD~ <<-EOF &&\n> +               y\n> +               n\n> +               EOF\n> +\n> +               expect_log <<-EOF &&\n> +               tip\n> +               second\n> +               first\n> +               initial\n> +               EOF\n> +\n> +               expect_tree_entries HEAD~2 <<-EOF &&\n> +               bar\n> +               initial.t\n> +               EOF\n> +\n> +               expect_tree_entries HEAD~ <<-EOF &&\n> +               bar\n> +               foo\n> +               initial.t\n> +               EOF\n> +\n> +               expect_tree_entries HEAD <<-EOF\n> +               bar\n> +               foo\n> +               initial.t\n> +               tip.t\n> +               EOF\n> +       )\n> +'\n> +\n> +test_expect_success 'can split HEAD only' '\n> +       test_when_finished \"rm -rf repo\" &&\n> +       git init repo &&\n> +       (\n> +               cd repo &&\n> +               test_commit base &&\n> +               touch a b &&\n> +               git add . &&\n> +               git commit -m split-me &&\n> +               git branch unrelated &&\n> +\n> +               set_fake_editor \"ours-a\" \"ours-b\" &&\n> +               git history split --update-refs=head HEAD <<-EOF &&\n> +               y\n> +               n\n> +               EOF\n> +               expect_graph --branches <<-EOF\n> +               * ours-b\n> +               * ours-a\n> +               | * split-me\n> +               |/\n> +               * base\n> +               EOF\n> +       )\n> +'\n> +\n> +test_expect_success 'can split detached HEAD' '\n> +       test_when_finished \"rm -rf repo\" &&\n> +       git init repo &&\n> +       (\n> +               cd repo &&\n> +               test_commit initial &&\n> +               touch bar foo &&\n> +               git add . &&\n> +               git commit -m split-me &&\n> +               git checkout --detach HEAD &&\n> +\n> +               set_fake_editor \"first\" \"second\" &&\n> +               git history split --update-refs=head HEAD <<-EOF &&\n> +               y\n> +               n\n> +               EOF\n> +\n> +               # HEAD should be detached and updated.\n> +               test_must_fail git symbolic-ref HEAD &&\n> +\n> +               expect_log <<-EOF\n> +               second\n> +               first\n> +               initial\n> +               EOF\n> +       )\n> +'\n> +\n> +test_expect_success 'can split commit in unrelated branch' '\n> +       test_when_finished \"rm -rf repo\" &&\n> +       git init repo &&\n> +       (\n> +               cd repo &&\n> +               test_commit base &&\n> +               git branch ours &&\n> +               git switch --create theirs &&\n> +               touch theirs-a theirs-b &&\n> +               git add . &&\n> +               git commit -m theirs &&\n> +               git switch ours &&\n> +               test_commit ours &&\n> +\n> +               # With --update-refs=head it is not possible to split up a\n> +               # commit that is unrelated to HEAD.\n> +               test_must_fail git history split --update-refs=head theirs 2>err &&\n> +               test_grep \"rewritten commit must be an ancestor of HEAD\" err &&\n> +\n> +               set_fake_editor \"theirs-rewritten-a\" \"theirs-rewritten-b\" &&\n> +               git history split theirs <<-EOF &&\n> +               y\n> +               n\n> +               EOF\n> +               expect_graph --branches <<-EOF &&\n> +               * ours\n> +               | * theirs-rewritten-b\n> +               | * theirs-rewritten-a\n> +               |/\n> +               * base\n> +               EOF\n> +\n> +               expect_tree_entries theirs~ <<-EOF &&\n> +               base.t\n> +               theirs-a\n> +               EOF\n> +\n> +               expect_tree_entries theirs <<-EOF\n> +               base.t\n> +               theirs-a\n> +               theirs-b\n> +               EOF\n> +       )\n> +'\n> +\n> +test_expect_success 'updates multiple descendant branches' '\n> +       test_when_finished \"rm -rf repo\" &&\n> +       git init repo --initial-branch=main &&\n> +       (\n> +               cd repo &&\n> +               test_commit base &&\n> +               touch file-a file-b &&\n> +               git add . &&\n> +               git commit -m split-me &&\n> +               git branch branch &&\n> +               test_commit on-main &&\n> +               git switch branch &&\n> +               test_commit on-branch &&\n> +               git switch main &&\n> +\n> +               set_fake_editor \"split-a\" \"split-b\" &&\n> +               git history split HEAD~ <<-EOF &&\n> +               y\n> +               n\n> +               EOF\n> +\n> +               # Both branches should now descend from the split commits.\n> +               expect_graph --branches <<-EOF\n> +               * on-branch\n> +               | * on-main\n> +               |/\n> +               * split-b\n> +               * split-a\n> +               * base\n> +               EOF\n> +       )\n> +'\n> +\n> +test_expect_success 'can pick multiple hunks' '\n> +       test_when_finished \"rm -rf repo\" &&\n> +       git init repo &&\n> +       (\n> +               cd repo &&\n> +               touch bar baz foo qux &&\n> +               git add . &&\n> +               git commit -m split-me &&\n> +\n> +               set_fake_editor \"first\" \"second\" &&\n> +               git history split HEAD <<-EOF &&\n> +               y\n> +               n\n> +               y\n> +               n\n> +               EOF\n> +\n> +               expect_tree_entries HEAD~ <<-EOF &&\n> +               bar\n> +               foo\n> +               EOF\n> +\n> +               expect_tree_entries HEAD <<-EOF\n> +               bar\n> +               baz\n> +               foo\n> +               qux\n> +               EOF\n> +       )\n> +'\n> +\n> +test_expect_success 'can use only last hunk' '\n> +       test_when_finished \"rm -rf repo\" &&\n> +       git init repo &&\n> +       (\n> +               cd repo &&\n> +               touch bar foo &&\n> +               git add . &&\n> +               git commit -m split-me &&\n> +\n> +               set_fake_editor \"first\" \"second\" &&\n> +               git history split HEAD <<-EOF &&\n> +               n\n> +               y\n> +               EOF\n> +\n> +               expect_log <<-EOF &&\n> +               second\n> +               first\n> +               EOF\n> +\n> +               expect_tree_entries HEAD~ <<-EOF &&\n> +               foo\n> +               EOF\n> +\n> +               expect_tree_entries HEAD <<-EOF\n> +               bar\n> +               foo\n> +               EOF\n> +       )\n> +'\n> +\n> +test_expect_success 'can split commit with file deletions' '\n> +       test_when_finished \"rm -rf repo\" &&\n> +       git init repo &&\n> +       (\n> +               cd repo &&\n> +               echo a >a &&\n> +               echo b >b &&\n> +               echo c >c &&\n> +               git add . &&\n> +               git commit -m base &&\n> +               git rm a b &&\n> +               git commit -m delete-both &&\n> +\n> +               set_fake_editor \"delete-a\" \"delete-b\" &&\n> +               git history split HEAD <<-EOF &&\n> +               y\n> +               n\n> +               EOF\n> +\n> +               expect_log <<-EOF &&\n> +               delete-b\n> +               delete-a\n> +               base\n> +               EOF\n> +\n> +               expect_tree_entries HEAD~ <<-EOF &&\n> +               b\n> +               c\n> +               EOF\n> +\n> +               expect_tree_entries HEAD <<-EOF\n> +               c\n> +               EOF\n> +       )\n> +'\n> +\n> +test_expect_success 'preserves original authorship' '\n> +       test_when_finished \"rm -rf repo\" &&\n> +       git init repo &&\n> +       (\n> +               cd repo &&\n> +               test_commit initial &&\n> +               touch bar foo &&\n> +               git add . &&\n> +               GIT_AUTHOR_NAME=\"Other Author\" \\\n> +               GIT_AUTHOR_EMAIL=\"other@example.com\" \\\n> +               git commit -m split-me &&\n> +\n> +               set_fake_editor \"first\" \"second\" &&\n> +               git history split HEAD <<-EOF &&\n> +               y\n> +               n\n> +               EOF\n> +\n> +               git log -1 --format=\"%an <%ae>\" HEAD~ >actual &&\n> +               echo \"Other Author <other@example.com>\" >expect &&\n> +               test_cmp expect actual &&\n> +\n> +               git log -1 --format=\"%an <%ae>\" HEAD >actual &&\n> +               test_cmp expect actual\n> +       )\n> +'\n> +\n> +test_expect_success 'aborts with empty commit message' '\n> +       test_when_finished \"rm -rf repo\" &&\n> +       git init repo &&\n> +       (\n> +               cd repo &&\n> +               touch bar foo &&\n> +               git add . &&\n> +               git commit -m split-me &&\n> +\n> +               set_fake_editor \"\" &&\n> +               test_must_fail git history split HEAD <<-EOF 2>err &&\n> +               y\n> +               n\n> +               EOF\n> +               test_grep \"Aborting commit due to empty commit message.\" err\n> +       )\n> +'\n> +\n> +test_expect_success 'commit message editor sees split-out changes' '\n> +       test_when_finished \"rm -rf repo\" &&\n> +       git init repo &&\n> +       (\n> +               cd repo &&\n> +               touch bar foo &&\n> +               git add . &&\n> +               git commit -m split-me &&\n> +\n> +               write_script fake-editor.sh <<-\\EOF &&\n> +               cat \"$1\" >>MESSAGES &&\n> +               echo \"some commit message\" >\"$1\"\n> +               EOF\n> +               test_set_editor \"$(pwd)\"/fake-editor.sh &&\n> +\n> +               git history split HEAD <<-EOF &&\n> +               y\n> +               n\n> +               EOF\n> +\n> +               # Note that we expect to see the messages twice, once for each\n> +               # of the commits. The committed files are different though.\n> +               cat >expect <<-EOF &&\n> +               split-me\n> +\n> +               # Please enter the commit message for the split-out changes. Lines starting\n> +               # with ${SQ}#${SQ} will be ignored, and an empty message aborts the commit.\n> +               # Changes to be committed:\n> +               #       new file:   bar\n> +               #\n> +               split-me\n> +\n> +               # Please enter the commit message for the split-out changes. Lines starting\n> +               # with ${SQ}#${SQ} will be ignored, and an empty message aborts the commit.\n> +               # Changes to be committed:\n> +               #       new file:   foo\n> +               #\n> +               EOF\n> +               test_cmp expect MESSAGES &&\n> +\n> +               expect_log <<-EOF\n> +               some commit message\n> +               some commit message\n> +               EOF\n> +       )\n> +'\n> +\n> +test_expect_success 'can use pathspec to limit what gets split' '\n> +       test_when_finished \"rm -rf repo\" &&\n> +       git init repo &&\n> +       (\n> +               cd repo &&\n> +               touch bar foo &&\n> +               git add . &&\n> +               git commit -m split-me &&\n> +\n> +               set_fake_editor \"first\" \"second\" &&\n> +               git history split HEAD -- foo <<-EOF &&\n> +               y\n> +               EOF\n> +\n> +               expect_tree_entries HEAD~ <<-EOF &&\n> +               foo\n> +               EOF\n> +\n> +               expect_tree_entries HEAD <<-EOF\n> +               bar\n> +               foo\n> +               EOF\n> +       )\n> +'\n> +\n> +test_expect_success 'pathspec matching no files produces empty split error' '\n> +       test_when_finished \"rm -rf repo\" &&\n> +       git init repo &&\n> +       (\n> +               cd repo &&\n> +               test_commit initial &&\n> +               touch bar foo &&\n> +               git add . &&\n> +               git commit -m split-me &&\n> +\n> +               set_fake_editor \"first\" \"second\" &&\n> +               test_must_fail git history split HEAD -- nonexistent 2>err &&\n> +               test_grep \"split commit is empty\" err\n> +       )\n> +'\n> +\n> +test_expect_success 'split with multiple pathspecs' '\n> +       test_when_finished \"rm -rf repo\" &&\n> +       git init repo &&\n> +       (\n> +               cd repo &&\n> +               test_commit initial &&\n> +               touch a b c d &&\n> +               git add . &&\n> +               git commit -m split-me &&\n> +\n> +               # Only a and c should be offered for splitting.\n> +               set_fake_editor \"split-ac\" \"remainder\" &&\n> +               git history split HEAD -- a c <<-EOF &&\n> +               y\n> +               y\n> +               EOF\n> +\n> +               expect_tree_entries HEAD~ <<-EOF &&\n> +               a\n> +               c\n> +               initial.t\n> +               EOF\n> +\n> +               expect_tree_entries HEAD <<-EOF\n> +               a\n> +               b\n> +               c\n> +               d\n> +               initial.t\n> +               EOF\n> +       )\n> +'\n> +\n> +test_expect_success 'split with file mode change' '\n> +       test_when_finished \"rm -rf repo\" &&\n> +       git init repo &&\n> +       (\n> +               cd repo &&\n> +               echo content >script &&\n> +               git add . &&\n> +               git commit -m base &&\n> +               test_chmod +x script &&\n> +               echo change >script &&\n> +               git commit -a -m \"mode and content change\" &&\n> +\n> +               set_fake_editor \"mode-change\" \"content-change\" &&\n> +               git history split HEAD <<-EOF &&\n> +               y\n> +               n\n> +               EOF\n> +\n> +               expect_log <<-EOF\n> +               content-change\n> +               mode-change\n> +               base\n> +               EOF\n> +       )\n> +'\n> +\n> +test_expect_success 'refuses to create empty split-out commit' '\n> +       test_when_finished \"rm -rf repo\" &&\n> +       git init repo &&\n> +       (\n> +               cd repo &&\n> +               test_commit base &&\n> +               touch bar foo &&\n> +               git add . &&\n> +               git commit -m split-me &&\n> +\n> +               test_must_fail git history split HEAD 2>err <<-EOF &&\n> +               n\n> +               n\n> +               EOF\n> +               test_grep \"split commit is empty\" err\n> +       )\n> +'\n> +\n> +test_expect_success 'hooks are not executed for rewritten commits' '\n> +       test_when_finished \"rm -rf repo\" &&\n> +       git init repo &&\n> +       (\n> +               cd repo &&\n> +               touch bar foo &&\n> +               git add . &&\n> +               git commit -m split-me &&\n> +               old_head=$(git rev-parse HEAD) &&\n> +\n> +               ORIG_PATH=\"$(pwd)\" &&\n> +               export ORIG_PATH &&\n> +               for hook in prepare-commit-msg pre-commit post-commit post-rewrite commit-msg\n> +               do\n> +                       write_script .git/hooks/$hook <<-\\EOF || exit 1\n> +                       touch \"$ORIG_PATH\"/hooks.log\n> +                       EOF\n> +               done &&\n> +\n> +               set_fake_editor \"first\" \"second\" &&\n> +               git history split HEAD <<-EOF &&\n> +               y\n> +               n\n> +               EOF\n> +\n> +               expect_log <<-EOF &&\n> +               second\n> +               first\n> +               EOF\n> +\n> +               test_path_is_missing hooks.log\n> +       )\n> +'\n> +\n> +test_expect_success 'refuses to create empty original commit' '\n> +       test_when_finished \"rm -rf repo\" &&\n> +       git init repo &&\n> +       (\n> +               cd repo &&\n> +               touch bar foo &&\n> +               git add . &&\n> +               git commit -m split-me &&\n> +\n> +               test_must_fail git history split HEAD 2>err <<-EOF &&\n> +               y\n> +               y\n> +               EOF\n> +               test_grep \"split commit tree matches original commit\" err\n> +       )\n> +'\n> +\n> +test_expect_success 'retains changes in the worktree and index' '\n> +       test_when_finished \"rm -rf repo\" &&\n> +       git init repo &&\n> +       (\n> +               cd repo &&\n> +               echo a >a &&\n> +               echo b >b &&\n> +               git add . &&\n> +               git commit -m \"initial commit\" &&\n> +               echo a-modified >a &&\n> +               echo b-modified >b &&\n> +               git add b &&\n> +               set_fake_editor \"a-only\" \"remainder\" &&\n> +               git history split HEAD <<-EOF &&\n> +               y\n> +               n\n> +               EOF\n> +\n> +               expect_tree_entries HEAD~ <<-EOF &&\n> +               a\n> +               EOF\n> +               expect_tree_entries HEAD <<-EOF &&\n> +               a\n> +               b\n> +               EOF\n> +\n> +               cat >expect <<-\\EOF &&\n> +                M a\n> +               M  b\n> +               ?? actual\n> +               ?? expect\n> +               ?? fake-editor.sh\n> +               ?? fake-input\n> +               EOF\n> +               git status --porcelain >actual &&\n> +               test_cmp expect actual\n> +       )\n> +'\n> +\n> +test_done\n>\n> --\n> 2.53.0.697.g625c4fb2da.dirty\n>\n>\n\nI didn't look particularly closely at the rest. I didn't really figure\nout what \"ext\" meant though ;)\n\n-- \nD. Ben Knoble\n"},{"id":"538571","messageId":"abE1NwXLCsXanSjy@pks.im","threadId":"65116","inReplyTo":"CALnO6CC5FB29bHPtyKD=L5EWxTCLx3K2qd+wGySdck7tCvvs_w@mail.gmail.com","subject":"Re: [PATCH 7/8] builtin/history: split out extended function to create commits","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-03-11T09:26:15Z","receivedAt":"2026-03-11T09:26:22Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Tue, Mar 03, 2026 at 01:43:12PM -0500, D. Ben Knoble wrote:\n> On Mon, Mar 2, 2026 at 7:17 AM Patrick Steinhardt <ps@pks.im> wrote:\n> >\n> > In the next commit we're about to introduce a new command that splits up\n> > a commit into two. Most of the logic will be shared with rewording\n> > commits, except that we also need to have control over the parents and\n> > the old/new trees.\n> >\n> > Extract a new function `commit_tree_with_edited_message_ext()` to\n> > prepare for this commit.\n> \n> Curious—what's the \"ext\" suffix mean here. Extracted? External? (Maybe\n> I'll get a better clue in the next patch.)\n\nIt stands for \"extended\". I thought that this was already common use in\nour code base:\n\n  - `refs_for_each_ref_ext()`\n  - `odb_write_object_ext()`\n  - `peel_object_ext()`\n\nBut Junio recently asked the same, so maybe I'm biased here (I am, two\nof these functions are my doing). Happy to take an alternative suffix.\n\nPatrick\n"},{"id":"538572","messageId":"abE1PPWRdPaHMaAs@pks.im","threadId":"65116","inReplyTo":"CALnO6CC_UMnQvcyCe37mCan8eASugknK-WbVp-KWXWptvrsJDg@mail.gmail.com","subject":"Re: [PATCH 8/8] builtin/history: implement \"split\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-03-11T09:26:20Z","receivedAt":"2026-03-11T09:26:24Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Tue, Mar 03, 2026 at 01:47:27PM -0500, D. Ben Knoble wrote:\n> On Mon, Mar 2, 2026 at 7:13 AM Patrick Steinhardt <ps@pks.im> wrote:\n> > diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc\n> > index cc019de697..24dc907033 100644\n> > --- a/Documentation/git-history.adoc\n> > +++ b/Documentation/git-history.adoc\n> > @@ -57,6 +58,26 @@ The following commands are available to rewrite history in different ways:\n> >         details of this commit remain unchanged. This command will spawn an\n> >         editor with the current message of that commit.\n> >\n> > +`split <commit> [--] [<pathspec>...]`::\n> > +       Interactively split up <commit> into two commits by choosing\n> > +       hunks introduced by it that will be moved into the new split-out\n> > +       commit. These hunks will then be written into a new commit that\n> > +       becomes the parent of the previous commit. The original commit\n> > +       stays intact, except that its parent will be the newly split-out\n> > +       commit.\n> > ++\n> > +The commit messages of the split-up commits will be asked for by launching\n> > +the configured editor. Authorship of the commit will be the same as for the\n> > +original commit.\n> > ++\n> > +If passed, _<pathspec>_ can be used to limit which changes shall be split out\n> > +of the original commit. Files not matching any of the pathspecs will remain\n> > +part of the original commit. For more details, see the 'pathspec' entry in\n> > +linkgit:gitglossary[7].\n> \n> That is quite convenient when changes to 2 independent areas have\n> become mixed. Nice.\n> \n> > +It is invalid to select either all or no hunks, as that would lead to\n> > +one of the commits becoming empty.\n> \n> Is it easy to make this a no-op? It could be done later if that\n> suggestion is contentions. But I figure rather than error we can\n> silently do nothing, since we have performed the desired split. (Or\n> even use this to split an \"--allow-empty\" commit, but… why that's\n> desirable, I can't guess.)\n> \n> So yeah, probably for later.\n\nI mean we could make it a no-op, but wouldn't that make the interface\neven more confusing? You don't really split a commit in the case where\nyou select everything or nothing, as you'd only end up with a single\ncommit in that case. Making one of the commits completely empty would\nprobably be an accident in almost all cases, I would claim.\n\nBut yeah, if there actually are use cases for this I would say that we\ncould then introduce \"--allow-empty\" for this command at a later point.\n\nPatrick\n"},{"id":"538940","messageId":"xmqqy0jv1hor.fsf@gitster.g","threadId":"65116","inReplyTo":"20260302-pks-history-split-v1-0-444fc987a324@pks.im","subject":"Re: [PATCH 0/8] history: introduce \"split\" subcommand","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-03-13T22:35:32Z","receivedAt":"2026-03-13T22:35:35Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> this patch series introduces `git history split` as an easy way to split\n> up one commit into multiple commits. This subcommand has already been\n> introduced in earlier versions of my git-history(1) patch series, but I\n> eventually decided to evict them from this series so that we can rather\n> focus more on basic decisions.\n\nWe saw a few exchanges and comments but didn't see an blocker.\nEverybody happy with the series?\n\nTHanks.\n"},{"id":"539071","messageId":"abetlsCTZaWPNK6U@pks.im","threadId":"65116","inReplyTo":"xmqqy0jv1hor.fsf@gitster.g","subject":"Re: [PATCH 0/8] history: introduce \"split\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-03-16T07:13:26Z","receivedAt":"2026-03-16T07:13:32Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Fri, Mar 13, 2026 at 03:35:32PM -0700, Junio C Hamano wrote:\n> Patrick Steinhardt <ps@pks.im> writes:\n> \n> > this patch series introduces `git history split` as an easy way to split\n> > up one commit into multiple commits. This subcommand has already been\n> > introduced in earlier versions of my git-history(1) patch series, but I\n> > eventually decided to evict them from this series so that we can rather\n> > focus more on basic decisions.\n> \n> We saw a few exchanges and comments but didn't see an blocker.\n> Everybody happy with the series?\n\nThere wasn't a ton of discussion on this series, but a lot of it already\nhappened back when it was still part of the initial git-history(1)\nintroduction. So it has already evolved significantly, even if that's\nnot quite visible here.\n\nFrom my perspective this is ready to go.\n\nThanks!\n\nPatrick\n"}]}