{"thread":{"id":"63986","subject":"[PATCH RFC 00/11] Introduce git-history(1) command for easy history editing","startedAt":"2025-08-19T10:56:04Z","lastAt":"2026-02-05T08:19:38Z","messageCount":325,"participants":["Patrick Steinhardt","D. Ben Knoble","Ben Knoble","Junio C Hamano","Sergey Organov","Jean-Noël AVILA","Martin von Zweigbergk","Kristoffer Haugsbakk","Elijah Newren","Phillip Wood","Oswald Buddenhagen","SZEDER Gábor","Karthik Nayak","Matthias Beyer"],"isPatch":true,"patchVersion":1,"patchTotal":11},"messages":[{"id":"524423","messageId":"20250819-b4-pks-history-builtin-v1-0-9b77c32688fe@pks.im","threadId":"63986","inReplyTo":null,"subject":"[PATCH RFC 00/11] Introduce git-history(1) command for easy history editing","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-08-19T10:55:56Z","receivedAt":"2025-08-19T10:56:04Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Hi,\n\nover recent months I've been playing around with Jujutsu quite\nfrequently. While I still prefer using Git, there's been a couple\nfeatures in it that I really like and that I'd like to have in Git, as\nwell.\n\nA copule of these features relate to history editing. Most importantly,\nI really dig the following commands:\n\n  - jj-abandon(1) to drop a specific commit from your history.\n\n  - jj-absorb(1) to take some changes and automatically apply them to\n    commits in your history that last modified the respective hunks.\n\n  - jj-split(1) to split a commit into two.\n\n  - jj-new(1) to insert a new commit after or before a specific other\n    commit.\n\nNot all of these commands can be ported directly into Git. jj-new(1) for\nexample doesn't really make a ton of sense for us, I'd claim. But some\nof these commands _do_ make sense.\n\nI thus had a look at implementing some of these commands in Git itself,\nwhere the result is this patch series. Specifically, the following\ncommands are introduced by this patch series:\n\n  - `git history drop` to drop a specific commit. This is basically the\n    same as jj-abandon(1).\n\n  - `git history reorder` to reorder a specific commit before or after\n    another commit. This is inspired by jj-new(1).\n\n  - `git history split` takes a commit and splits it into two. This is\n    basically the same as jj-split(1).\n\nIf this is something we want to have I think it'd be just a starting\npoint. There's other commands that I think are quite common and that\nmight make sense to introduce eventually:\n\n  - An equivalent to jj-absorb(1) would be awesome to have.\n\n  - `git history reword` to change only the commit message of a specific\n    commit.\n\n  - `git history squash` to squash together multiple commits into one.\n\nIn the end, I'd like us to learn from what people like about Jujutsu and\napply those learnings to Git. We won't be able to apply all learnings\nfrom Jujutsu, as the workflow is quite different there due to the lack\nof the index. But other things we certainly can apply to Git directly.\n\nNote: This patch series currently builds on the cherry-pick infra.\nAs such, when one hits a merge conflict one needs to `git cherry-pick\n--continue`, which is quite suboptimal. I didn't want to overpolish this\nseries before getting some feedback, but it is something I'll fix in\nsubsequent versions. Furthermore, the command for now bails out in the\ncase where there's any merge commits in the history that is being\nrewritten. This is another restriction that can be lifted in the future.\n\nThanks!\n\nPatrick\n\n---\nPatrick Steinhardt (11):\n      sequencer: optionally skip printing commit summary\n      sequencer: add option to rewind HEAD after picking commits\n      cache-tree: allow writing in-memory index as tree\n      builtin: add new \"history\" command\n      builtin/history: implement \"drop\" subcommand\n      builtin/history: implement \"reorder\" subcommand\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      builtin/history: implement \"split\" subcommand\n\n .gitignore                     |   1 +\n Documentation/git-history.adoc | 159 ++++++++++\n Documentation/meson.build      |   1 +\n Makefile                       |   1 +\n add-interactive.c              | 151 +++------\n add-interactive.h              |  43 +--\n add-patch.c                    | 271 ++++++++++++++--\n add-patch.h                    |  61 ++++\n builtin.h                      |   1 +\n builtin/add.c                  |  22 +-\n builtin/checkout.c             |   7 +-\n builtin/commit.c               |  16 +-\n builtin/history.c              | 691 +++++++++++++++++++++++++++++++++++++++++\n builtin/reset.c                |  16 +-\n builtin/stash.c                |  46 +--\n cache-tree.c                   |   5 +-\n cache-tree.h                   |   3 +-\n commit.h                       |   2 +-\n git.c                          |   1 +\n meson.build                    |   1 +\n sequencer.c                    |  36 ++-\n sequencer.h                    |   4 +\n t/meson.build                  |   5 +-\n t/t3450-history-drop.sh        | 127 ++++++++\n t/t3451-history-reorder.sh     | 218 +++++++++++++\n t/t3452-history-split.sh       | 304 ++++++++++++++++++\n 26 files changed, 1947 insertions(+), 246 deletions(-)\n\n\n---\nbase-commit: c44beea485f0f2feaf460e2ac87fdd5608d63cf0\nchange-id: 20250819-b4-pks-history-builtin-83398f9a05f0\n\n"},{"id":"524424","messageId":"20250819-b4-pks-history-builtin-v1-1-9b77c32688fe@pks.im","threadId":"63986","inReplyTo":"20250819-b4-pks-history-builtin-v1-0-9b77c32688fe@pks.im","subject":"[PATCH RFC 01/11] sequencer: optionally skip printing commit summary","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-08-19T10:55:57Z","receivedAt":"2025-08-19T10:56:06Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"When picking commits by using for example git-cherry-pick(1) we end up\nprinting a commit summary that gives the reader information around what\nexactly we have been picking:\n\n```\n$ git cherry-pick main\n[other 76c8456] bar\n Date: Tue Aug 19 08:07:26 2025 +0200\n 1 file changed, 1 insertion(+)\n create mode 100644 bar\n```\n\nWhile useful for some commands, we're about to introduce a new command\nwhere this output will be less so. But right now there is no way to\ndisable printing this commit summary.\n\nIntroduce a new `skip_commit_summary` replay option that does so.\nPersist the option into the sequencer configuration so that it persists\nacross different processes, e.g. when we need to stop due to a merge\nconflict.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n sequencer.c | 12 +++++++++---\n sequencer.h |  1 +\n 2 files changed, 10 insertions(+), 3 deletions(-)\n\ndiff --git a/sequencer.c b/sequencer.c\nindex aaf2e4df64..7066cdc939 100644\n--- a/sequencer.c\n+++ b/sequencer.c\n@@ -1742,7 +1742,7 @@ static int do_commit(struct repository *r,\n \t\t\trefs_delete_ref(get_main_ref_store(r), \"\",\n \t\t\t\t\t\"CHERRY_PICK_HEAD\", NULL, REF_NO_DEREF);\n \t\t\tunlink(git_path_merge_msg(r));\n-\t\t\tif (!is_rebase_i(opts))\n+\t\t\tif (!is_rebase_i(opts) && !opts->skip_commit_summary)\n \t\t\t\tprint_commit_summary(r, NULL, &oid,\n \t\t\t\t\t\tSUMMARY_SHOW_AUTHOR_DATE);\n \t\t\treturn res;\n@@ -3139,8 +3139,12 @@ static int populate_opts_cb(const char *key, const char *value,\n \telse if (!strcmp(key, \"options.default-msg-cleanup\")) {\n \t\topts->explicit_cleanup = 1;\n \t\topts->default_msg_cleanup = get_cleanup_mode(value, 1);\n-\t} else\n+\t} else if (!strcmp(key, \"options.skip-commit-summary\")) {\n+\t\topts->skip_commit_summary =\n+\t\t\tgit_config_bool_or_int(key, value, ctx->kvi, &error_flag);\n+\t} else {\n \t\treturn error(_(\"invalid key: %s\"), key);\n+\t}\n \n \tif (!error_flag)\n \t\treturn error(_(\"invalid value for '%s': '%s'\"), key, value);\n@@ -3698,11 +3702,13 @@ static int save_opts(struct replay_opts *opts)\n \t\t\t\t\"options.allow-rerere-auto\", NULL,\n \t\t\t\topts->allow_rerere_auto == RERERE_AUTOUPDATE ?\n \t\t\t\t\"true\" : \"false\");\n-\n \tif (opts->explicit_cleanup)\n \t\tres |= repo_config_set_in_file_gently(the_repository, opts_file,\n \t\t\t\t\"options.default-msg-cleanup\", NULL,\n \t\t\t\tdescribe_cleanup_mode(opts->default_msg_cleanup));\n+\tif (opts->skip_commit_summary)\n+\t\tres |= repo_config_set_in_file_gently(the_repository, opts_file,\n+\t\t\t\t\t\"options.skip-commit-summary\", NULL, \"true\");\n \treturn res;\n }\n \ndiff --git a/sequencer.h b/sequencer.h\nindex 304ba4b4d3..1767fd737e 100644\n--- a/sequencer.h\n+++ b/sequencer.h\n@@ -52,6 +52,7 @@ struct replay_opts {\n \tint keep_redundant_commits;\n \tint verbose;\n \tint quiet;\n+\tint skip_commit_summary;\n \tint reschedule_failed_exec;\n \tint committer_date_is_author_date;\n \tint ignore_date;\n\n-- \n2.51.0.261.g7ce5a0a67e.dirty\n\n"},{"id":"524425","messageId":"20250819-b4-pks-history-builtin-v1-2-9b77c32688fe@pks.im","threadId":"63986","inReplyTo":"20250819-b4-pks-history-builtin-v1-0-9b77c32688fe@pks.im","subject":"[PATCH RFC 02/11] sequencer: add option to rewind HEAD after picking commits","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-08-19T10:55:58Z","receivedAt":"2025-08-19T10:56:09Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"While the sequencer infrastructure knows to rewind \"HEAD\" to whatever it\nwas pointing to before a rebase, it doesn't do the same for non-rebase\noperations like cherry-picks. This is because the expectation is that\nthe user directly picks commits on top of whatever \"HEAD\" points to, and\nwe advance the reference pointed to by \"HEAD\" instead of updating it\ndirectly.\n\nWe're about to introduce a new command though that needs to detach\n\"HEAD\" while being more similar to git-cherry-pick(1) rathen than to\ngit-rebase(1). As such, we'll want to restore \"HEAD\" to point to the\nbranch that we started on while not using the more heavy-weight rebase\nmachinery.\n\nIntroduce a new option `restore_head_target` to do so. Persist the\noption into the sequencer configuration so that it persists across\ndifferent processes, e.g. when we need to stop due to a merge conflict.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n sequencer.c | 24 ++++++++++++++++++++++++\n sequencer.h |  3 +++\n 2 files changed, 27 insertions(+)\n\ndiff --git a/sequencer.c b/sequencer.c\nindex 7066cdc939..b13348ba34 100644\n--- a/sequencer.c\n+++ b/sequencer.c\n@@ -413,6 +413,7 @@ void replay_opts_release(struct replay_opts *opts)\n \tstruct replay_ctx *ctx = opts->ctx;\n \n \tfree(opts->gpg_sign);\n+\tfree(opts->restore_head_target);\n \tfree(opts->reflog_action);\n \tfree(opts->default_strategy);\n \tfree(opts->strategy);\n@@ -3142,6 +3143,8 @@ static int populate_opts_cb(const char *key, const char *value,\n \t} else if (!strcmp(key, \"options.skip-commit-summary\")) {\n \t\topts->skip_commit_summary =\n \t\t\tgit_config_bool_or_int(key, value, ctx->kvi, &error_flag);\n+\t} else if (!strcmp(key, \"options.restore-head-target\")) {\n+\t\tgit_config_string_dup(&opts->restore_head_target, key, value);\n \t} else {\n \t\treturn error(_(\"invalid key: %s\"), key);\n \t}\n@@ -3709,6 +3712,10 @@ static int save_opts(struct replay_opts *opts)\n \tif (opts->skip_commit_summary)\n \t\tres |= repo_config_set_in_file_gently(the_repository, opts_file,\n \t\t\t\t\t\"options.skip-commit-summary\", NULL, \"true\");\n+\tif (opts->restore_head_target)\n+\t\tres |= repo_config_set_in_file_gently(the_repository, opts_file,\n+\t\t\t\t\"options.restore-head-target\", NULL, opts->restore_head_target);\n+\n \treturn res;\n }\n \n@@ -5177,6 +5184,23 @@ static int pick_commits(struct repository *r,\n \t\t\treturn -1;\n \t}\n \n+\tif (opts->restore_head_target) {\n+\t\tstruct reset_head_opts reset_opts = { 0 };\n+\t\tconst char *msg;\n+\n+\t\tmsg = reflog_message(opts, \"finish\", \"returning to %s\", opts->restore_head_target);\n+\n+\t\treset_opts.branch = opts->restore_head_target;\n+\t\treset_opts.flags = RESET_HEAD_REFS_ONLY;\n+\t\treset_opts.branch_msg = msg;\n+\t\treset_opts.head_msg = msg;\n+\n+\t\tif (reset_head(r, &reset_opts)) {\n+\t\t\terror(_(\"could not switch HEAD back to %s\"), opts->restore_head_target);\n+\t\t\treturn -1;\n+\t\t}\n+\t}\n+\n \t/*\n \t * Sequence of picks finished successfully; cleanup by\n \t * removing the .git/sequencer directory\ndiff --git a/sequencer.h b/sequencer.h\nindex 1767fd737e..a905f6afc7 100644\n--- a/sequencer.h\n+++ b/sequencer.h\n@@ -72,6 +72,9 @@ struct replay_opts {\n \t/* Reflog */\n \tchar *reflog_action;\n \n+\t/* Reference to which HEAD shall be reset to after the operation. */\n+\tchar *restore_head_target;\n+\n \t/* placeholder commit for -i --root */\n \tstruct object_id squash_onto;\n \tint have_squash_onto;\n\n-- \n2.51.0.261.g7ce5a0a67e.dirty\n\n"},{"id":"524426","messageId":"20250819-b4-pks-history-builtin-v1-3-9b77c32688fe@pks.im","threadId":"63986","inReplyTo":"20250819-b4-pks-history-builtin-v1-0-9b77c32688fe@pks.im","subject":"[PATCH RFC 03/11] cache-tree: allow writing in-memory index as tree","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-08-19T10:55:59Z","receivedAt":"2025-08-19T10:56:12Z","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       | 5 ++---\n cache-tree.h       | 3 ++-\n 3 files changed, 6 insertions(+), 5 deletions(-)\n\ndiff --git a/builtin/checkout.c b/builtin/checkout.c\nindex f9453473fe2..43583c8d1be 100644\n--- a/builtin/checkout.c\n+++ b/builtin/checkout.c\n@@ -902,7 +902,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 66ef2becbe0..029ec933abe 100644\n--- a/cache-tree.c\n+++ b/cache-tree.c\n@@ -699,11 +699,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 \n@@ -723,7 +723,6 @@ struct tree* write_in_core_index_as_tree(struct repository *repo) {\n \treturn lookup_tree(repo, &index_state->cache_tree->oid);\n }\n \n-\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 {\n \tint entries, was_valid;\ndiff --git a/cache-tree.h b/cache-tree.h\nindex b82c4963e7c..f8bddae5235 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.51.0.261.g7ce5a0a67e.dirty\n\n"},{"id":"524427","messageId":"20250819-b4-pks-history-builtin-v1-4-9b77c32688fe@pks.im","threadId":"63986","inReplyTo":"20250819-b4-pks-history-builtin-v1-0-9b77c32688fe@pks.im","subject":"[PATCH RFC 04/11] builtin: add new \"history\" command","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-08-19T10:56:00Z","receivedAt":"2025-08-19T10:56:15Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"When rewriting history via git-rebase(1) there are a couple of very\ncommon use cases:\n\n  - The ordering of two commits should be reversed.\n\n  - A commit should be split up into two commits.\n\n  - A commit should be dropped from the history completely.\n\n  - Multiple commits should be squashed into one.\n\nWhile these operations are all doable, it often feels needlessly cludgy\nto do so by doing an interactive rebase, using the editor to say what\none wants, and then perform the actions. Furthermore, some operations\nlike splitting up a commit into two are way more involved than that and\nrequire a whole series of commands.\n\nAdd a new \"history\" command to plug this gap. This command will have\nseveral different subcommands to imperatively rewrite history for common\nuse cases like the above. These commands will be implemented in\nsubsequent commits.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n .gitignore                     |  1 +\n Documentation/git-history.adoc | 43 ++++++++++++++++++++++++++++++++++++++++++\n Documentation/meson.build      |  1 +\n Makefile                       |  1 +\n builtin.h                      |  1 +\n builtin/history.c              | 20 ++++++++++++++++++++\n git.c                          |  1 +\n meson.build                    |  1 +\n 8 files changed, 69 insertions(+)\n\ndiff --git a/.gitignore b/.gitignore\nindex 04c444404e..3932d4d618 100644\n--- a/.gitignore\n+++ b/.gitignore\n@@ -77,6 +77,7 @@\n /git-grep\n /git-hash-object\n /git-help\n+/git-history\n /git-hook\n /git-http-backend\n /git-http-fetch\ndiff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc\nnew file mode 100644\nindex 0000000000..9dafb8fc16\n--- /dev/null\n+++ b/Documentation/git-history.adoc\n@@ -0,0 +1,43 @@\n+git-history(1)\n+==============\n+\n+NAME\n+----\n+git-history - Rewrite history of the current branch\n+\n+SYNOPSIS\n+--------\n+[synopsis]\n+git history [<options>]\n+\n+DESCRIPTION\n+-----------\n+\n+Rewrite history by rearranging or modifying specific commits in the\n+history.\n+\n+This command is similar to linkgit:git-rebase[1] and uses the same\n+underlying machinery. You should use rebases if you either want to\n+reapply a range of commits onto a different base, or interactive rebases\n+if you want to edit a range of commits.\n+\n+Note that this command does not (yet) work with histories that contain\n+merges. You should use linkgit:git-rebase[1] with the `--rebase-merges`\n+flag instead.\n+\n+COMMANDS\n+--------\n+\n+This command requires a subcommand. Several subcommands are available to\n+rewrite history in different ways.\n+\n+CONFIGURATION\n+-------------\n+\n+include::includes/cmd-config-section-all.adoc[]\n+\n+include::config/sequencer.adoc[]\n+\n+GIT\n+---\n+Part of the linkgit:git[1] suite\ndiff --git a/Documentation/meson.build b/Documentation/meson.build\nindex 4404c623f0..a30b5307fd 100644\n--- a/Documentation/meson.build\n+++ b/Documentation/meson.build\n@@ -64,6 +64,7 @@ manpages = {\n   'git-gui.adoc' : 1,\n   'git-hash-object.adoc' : 1,\n   'git-help.adoc' : 1,\n+  'git-history.adoc' : 1,\n   'git-hook.adoc' : 1,\n   'git-http-backend.adoc' : 1,\n   'git-http-fetch.adoc' : 1,\ndiff --git a/Makefile b/Makefile\nindex e11340c1ae..bed6eda5e6 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -1261,6 +1261,7 @@ BUILTIN_OBJS += builtin/get-tar-commit-id.o\n BUILTIN_OBJS += builtin/grep.o\n BUILTIN_OBJS += builtin/hash-object.o\n BUILTIN_OBJS += builtin/help.o\n+BUILTIN_OBJS += builtin/history.o\n BUILTIN_OBJS += builtin/hook.o\n BUILTIN_OBJS += builtin/index-pack.o\n BUILTIN_OBJS += builtin/init-db.o\ndiff --git a/builtin.h b/builtin.h\nindex bff13e3069..2934f4479a 100644\n--- a/builtin.h\n+++ b/builtin.h\n@@ -172,6 +172,7 @@ int cmd_get_tar_commit_id(int argc, const char **argv, const char *prefix, struc\n int cmd_grep(int argc, const char **argv, const char *prefix, struct repository *repo);\n int cmd_hash_object(int argc, const char **argv, const char *prefix, struct repository *repo);\n int cmd_help(int argc, const char **argv, const char *prefix, struct repository *repo);\n+int cmd_history(int argc, const char **argv, const char *prefix, struct repository *repo);\n int cmd_hook(int argc, const char **argv, const char *prefix, struct repository *repo);\n int cmd_index_pack(int argc, const char **argv, const char *prefix, struct repository *repo);\n int cmd_init_db(int argc, const char **argv, const char *prefix, struct repository *repo);\ndiff --git a/builtin/history.c b/builtin/history.c\nnew file mode 100644\nindex 0000000000..d1a40368e0\n--- /dev/null\n+++ b/builtin/history.c\n@@ -0,0 +1,20 @@\n+#include \"builtin.h\"\n+#include \"gettext.h\"\n+#include \"parse-options.h\"\n+\n+int cmd_history(int argc,\n+\t\tconst char **argv,\n+\t\tconst char *prefix,\n+\t\tstruct repository *repo UNUSED)\n+{\n+\tconst char * const usage[] = {\n+\t\tN_(\"git history [<options>]\"),\n+\t\tNULL,\n+\t};\n+\tstruct option options[] = {\n+\t\tOPT_END(),\n+\t};\n+\n+\targc = parse_options(argc, argv, prefix, options, usage, 0);\n+\treturn 0;\n+}\ndiff --git a/git.c b/git.c\nindex 83eac0aeab..9d2cba2906 100644\n--- a/git.c\n+++ b/git.c\n@@ -560,6 +560,7 @@ static struct cmd_struct commands[] = {\n \t{ \"grep\", cmd_grep, RUN_SETUP_GENTLY },\n \t{ \"hash-object\", cmd_hash_object },\n \t{ \"help\", cmd_help },\n+\t{ \"history\", cmd_history, RUN_SETUP },\n \t{ \"hook\", cmd_hook, RUN_SETUP },\n \t{ \"index-pack\", cmd_index_pack, RUN_SETUP_GENTLY | NO_PARSEOPT },\n \t{ \"init\", cmd_init_db },\ndiff --git a/meson.build b/meson.build\nindex 5dd299b496..0e40778a23 100644\n--- a/meson.build\n+++ b/meson.build\n@@ -603,6 +603,7 @@ builtin_sources = [\n   'builtin/grep.c',\n   'builtin/hash-object.c',\n   'builtin/help.c',\n+  'builtin/history.c',\n   'builtin/hook.c',\n   'builtin/index-pack.c',\n   'builtin/init-db.c',\n\n-- \n2.51.0.261.g7ce5a0a67e.dirty\n\n"},{"id":"524428","messageId":"20250819-b4-pks-history-builtin-v1-5-9b77c32688fe@pks.im","threadId":"63986","inReplyTo":"20250819-b4-pks-history-builtin-v1-0-9b77c32688fe@pks.im","subject":"[PATCH RFC 05/11] builtin/history: implement \"drop\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-08-19T10:56:01Z","receivedAt":"2025-08-19T10:56:19Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"It is a fairly common operation to perform an interactive rebase so that\none of the commits can be dropped from history. Doing this is not very\nhard in general, but still requires the user to perform multiple steps:\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. Edit the instruction sheet to drop that commit.\n\nThis is needlessly complex for such a supposedly-trivial operation.\nFurthermore, the second step doesn't account for certain edge cases like\nfor example dropping the root commit.\n\nIntroduce a new \"drop\" subcommand to make this use case significantly\nsimpler: all the user needs to do is to say `git history drop $COMMIT`\nand they're done.\n\nNote that for now, this command only allows users to drop a single\ncommit at once. It should be easy enough though to expand the command at\na later point in time to support dropping whole commit ranges.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/git-history.adoc |  27 +++-\n builtin/history.c              | 297 ++++++++++++++++++++++++++++++++++++++++-\n t/meson.build                  |   3 +-\n t/t3450-history-drop.sh        | 127 ++++++++++++++++++\n 4 files changed, 449 insertions(+), 5 deletions(-)\n\ndiff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc\nindex 9dafb8fc16..3012445ddc 100644\n--- a/Documentation/git-history.adoc\n+++ b/Documentation/git-history.adoc\n@@ -8,7 +8,7 @@ git-history - Rewrite history of the current branch\n SYNOPSIS\n --------\n [synopsis]\n-git history [<options>]\n+git history drop [<options>] <revision>\n \n DESCRIPTION\n -----------\n@@ -31,6 +31,31 @@ COMMANDS\n This command requires a subcommand. Several subcommands are available to\n rewrite history in different ways.\n \n+drop <revision>::\n+\tDrop a commit from the history and reapply all children of that\n+\tcommit on top of the commit's parent. The commit that is to be\n+\tdropped must be reachable from the current `HEAD` commit.\n++\n+Dropping the root commit converts the child of that commit into the new\n+root commit. It is invalid to drop a root commit that does not have any\n+child commits, as that would lead to an empty branch.\n+\n+EXAMPLES\n+--------\n+\n+* Drop a commit from history.\n++\n+----------\n+$ git log --oneline\n+2d4cd6d third\n+125a0f3 second\n+e098c27 first\n+$ git history drop HEAD~\n+$ git log\n+b1bc1bd third\n+e098c27 first\n+----------\n+\n CONFIGURATION\n -------------\n \ndiff --git a/builtin/history.c b/builtin/history.c\nindex d1a40368e0..183ab9d5f7 100644\n--- a/builtin/history.c\n+++ b/builtin/history.c\n@@ -1,20 +1,311 @@\n #include \"builtin.h\"\n+#include \"commit.h\"\n+#include \"commit-reach.h\"\n+#include \"config.h\"\n+#include \"environment.h\"\n #include \"gettext.h\"\n+#include \"hex.h\"\n+#include \"object-name.h\"\n #include \"parse-options.h\"\n+#include \"refs.h\"\n+#include \"reset.h\"\n+#include \"revision.h\"\n+#include \"sequencer.h\"\n+\n+static int collect_commits(struct repository *repo,\n+\t\t\t   struct commit *old_commit,\n+\t\t\t   struct commit *new_commit,\n+\t\t\t   struct strvec *out)\n+{\n+\tstruct setup_revision_opt revision_opts = {\n+\t\t.assume_dashdash = 1,\n+\t};\n+\tstruct strvec revisions = STRVEC_INIT;\n+\tstruct commit_list *from_list = NULL;\n+\tstruct commit *child;\n+\tstruct rev_info rev = { 0 };\n+\tint ret;\n+\n+\t/*\n+\t * Check that the old actually is an ancestor of HEAD. If not\n+\t * the whole request becomes nonsensical.\n+\t*/\n+\tif (old_commit) {\n+\t\tcommit_list_insert(old_commit, &from_list);\n+\t\tif (!repo_is_descendant_of(repo, new_commit, from_list)) {\n+\t\t\tret = error(_(\"commit must be reachable from current HEAD commit\"));\n+\t\t\tgoto out;\n+\t\t}\n+\t}\n+\n+\trepo_init_revisions(repo, &rev, NULL);\n+\tstrvec_push(&revisions, \"\");\n+\tstrvec_push(&revisions, oid_to_hex(&new_commit->object.oid));\n+\tif (old_commit)\n+\t\tstrvec_pushf(&revisions, \"^%s\", oid_to_hex(&old_commit->object.oid));\n+\tif (setup_revisions(revisions.nr, revisions.v, &rev, &revision_opts) != 1 ||\n+\t    prepare_revision_walk(&rev)) {\n+\t\tret = error(_(\"revision walk setup failed\"));\n+\t\tgoto out;\n+\t}\n+\n+\twhile ((child = get_revision(&rev))) {\n+\t\tif (old_commit && !child->parents)\n+\t\t\tBUG(\"revision walk did not find child commit\");\n+\t\tif (child->parents && child->parents->next) {\n+\t\t\tret = error(_(\"cannot rearrange commit history with merges\"));\n+\t\t\tgoto out;\n+\t\t}\n+\n+\t\tstrvec_push(out, oid_to_hex(&child->object.oid));\n+\n+\t\tif (child->parents && old_commit &&\n+\t\t    commit_list_contains(old_commit, child->parents))\n+\t\t\tbreak;\n+\t}\n+\n+\t/*\n+\t * Revisions are in newest-order-first. We have to reverse the\n+\t * array though so that we pick the oldest commits first. Note\n+\t * that we keep the first string untouched, as it is the\n+\t * equivalent of `argv[0]` to `setup_revisions()`.\n+\t */\n+\tfor (size_t i = 0, j = out->nr - 1; i < j; i++, j--)\n+\t\tSWAP(out->v[i], out->v[j]);\n+\n+\tret = 0;\n+\n+out:\n+\tfree_commit_list(from_list);\n+\tstrvec_clear(&revisions);\n+\trelease_revisions(&rev);\n+\treset_revision_walk();\n+\treturn ret;\n+}\n+\n+static int apply_commits(struct repository *repo,\n+\t\t\t const struct strvec *commits,\n+\t\t\t struct commit *head,\n+\t\t\t struct commit *base,\n+\t\t\t const char *action)\n+{\n+\tstruct setup_revision_opt revision_opts = {\n+\t\t.assume_dashdash = 1,\n+\t};\n+\tstruct replay_opts replay_opts = REPLAY_OPTS_INIT;\n+\tstruct reset_head_opts reset_opts = { 0 };\n+\tstruct object_id root_commit;\n+\tstruct strvec args = STRVEC_INIT;\n+\tstruct strbuf buf = STRBUF_INIT;\n+\tchar hex[GIT_MAX_HEXSZ + 1];\n+\tint ref_flags, ret;\n+\n+\t/*\n+\t * We have performed all safety checks, so we now prepare\n+\t * replaying the commits.\n+\t*/\n+\treplay_opts.action = REPLAY_PICK;\n+\tsequencer_init_config(&replay_opts);\n+\treplay_opts.quiet = 1;\n+\treplay_opts.skip_commit_summary = 1;\n+\tif (!replay_opts.strategy && replay_opts.default_strategy) {\n+\t\treplay_opts.strategy = replay_opts.default_strategy;\n+\t\treplay_opts.default_strategy = NULL;\n+\t}\n+\n+\tstrvec_push(&args, \"\");\n+\tstrvec_pushv(&args, commits->v);\n+\n+\treplay_opts.revs = xmalloc(sizeof(*replay_opts.revs));\n+\trepo_init_revisions(repo, replay_opts.revs, NULL);\n+\treplay_opts.revs->no_walk = 1;\n+\treplay_opts.revs->unsorted_input = 1;\n+\tif (setup_revisions(args.nr, args.v, replay_opts.revs,\n+\t\t\t    &revision_opts) != 1) {\n+\t\tret = error(_(\"setting up revisions failed\"));\n+\t\tgoto out;\n+\t}\n+\n+\t/*\n+\t * If we're dropping the root commit we first need to create\n+\t * a new empty root. We then instruct the seqencer machinery to\n+\t * squash that root commit with the first commit we're picking\n+\t * onto it.\n+\t */\n+\tif (!base) {\n+\t\tif (commit_tree(\"\", 0, repo->hash_algo->empty_tree, NULL,\n+\t\t\t\t&root_commit, NULL, NULL) < 0) {\n+\t\t\tret = error(_(\"Could not create new root commit\"));\n+\t\t\tgoto out;\n+\t\t}\n+\n+\t\treplay_opts.squash_onto = root_commit;\n+\t\treplay_opts.have_squash_onto = 1;\n+\t\treset_opts.oid = &root_commit;\n+\t} else {\n+\t\treset_opts.oid = &base->object.oid;\n+\t}\n+\n+\treplay_opts.restore_head_target =\n+\t\txstrdup_or_null(refs_resolve_ref_unsafe(get_main_ref_store(repo),\n+\t\t\t\t\t\t\t\"HEAD\", 0, NULL, &ref_flags));\n+\tif (!(ref_flags & REF_ISSYMREF))\n+\t\tFREE_AND_NULL(replay_opts.restore_head_target);\n+\n+\t/*\n+\t * Perform a hard-reset to the parent of our commit that is to\n+\t * be dropped. This is the new base onto which we'll pick all\n+\t * the descendants.\n+\t */\n+\tstrbuf_addf(&buf, \"%s (start): checkout %s\", action,\n+\t\t    oid_to_hex_r(hex, reset_opts.oid));\n+\treset_opts.orig_head = &head->object.oid;\n+\treset_opts.flags = RESET_HEAD_DETACH | RESET_ORIG_HEAD;\n+\treset_opts.head_msg = buf.buf;\n+\treset_opts.default_reflog_action = action;\n+\tif (reset_head(repo, &reset_opts) < 0) {\n+\t\tret = error(_(\"could not switch to %s\"), oid_to_hex(reset_opts.oid));\n+\t\tgoto out;\n+\t}\n+\n+\tret = sequencer_pick_revisions(repo, &replay_opts);\n+\tif (ret < 0) {\n+\t\tret = error(_(\"could not pick commits\"));\n+\t\tgoto out;\n+\t} else if (ret > 0) {\n+\t\t/*\n+\t\t * A positive return value indicates we've got a merge\n+\t\t * conflict. Bail out, but don't print a message as\n+\t\t * `sequencer_pick_revisions()` already printed enough\n+\t\t * information.\n+\t\t */\n+\t\tret = -1;\n+\t\tgoto out;\n+\t}\n+\n+\tret = 0;\n+\n+out:\n+\treplay_opts_release(&replay_opts);\n+\tstrbuf_release(&buf);\n+\tstrvec_clear(&args);\n+\treturn ret;\n+}\n+\n+static int cmd_history_drop(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\tN_(\"git history drop [<options>] <revision>\"),\n+\t\tNULL,\n+\t};\n+\tstruct option options[] = {\n+\t\tOPT_END(),\n+\t};\n+\tstruct commit *commit_to_drop, *head;\n+\tstruct strvec commits = STRVEC_INIT;\n+\tstruct strbuf buf = STRBUF_INIT;\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 single revision\"));\n+\t\tgoto out;\n+\t}\n+\trepo_config(repo, git_default_config, NULL);\n+\n+\tcommit_to_drop = lookup_commit_reference_by_name(argv[0]);\n+\tif (!commit_to_drop) {\n+\t\tret = error(_(\"commit to be dropped cannot be found: %s\"), argv[0]);\n+\t\tgoto out;\n+\t}\n+\tif (commit_to_drop->parents && commit_to_drop->parents->next) {\n+\t\tret = error(_(\"commit to be dropped must not be a merge commit\"));\n+\t\tgoto out;\n+\t}\n+\n+\thead = lookup_commit_reference_by_name(\"HEAD\");\n+\tif (!head) {\n+\t\tret = error(_(\"could not resolve HEAD to a commit\"));\n+\t\tgoto out;\n+\t}\n+\n+\tif (oideq(&commit_to_drop->object.oid, &head->object.oid)) {\n+\t\t/*\n+\t\t * If we want to drop the tip of the current branch we don't\n+\t\t * have to perform any rebase at all. Instead, we simply\n+\t\t * perform a hard reset to the parent commit.\n+\t\t */\n+\t\tstruct reset_head_opts reset_opts = {\n+\t\t\t.orig_head = &head->object.oid,\n+\t\t\t.flags = RESET_ORIG_HEAD,\n+\t\t\t.default_reflog_action = \"drop\",\n+\t\t};\n+\t\tchar hex[GIT_MAX_HEXSZ + 1];\n+\n+\t\tif (!commit_to_drop->parents) {\n+\t\t\tret = error(_(\"cannot drop the only commit on this branch\"));\n+\t\t\tgoto out;\n+\t\t}\n+\n+\t\toid_to_hex_r(hex, &commit_to_drop->parents->item->object.oid);\n+\t\tstrbuf_addf(&buf, \"drop (start): checkout %s\", hex);\n+\t\treset_opts.oid = &commit_to_drop->parents->item->object.oid;\n+\t\treset_opts.head_msg = buf.buf;\n+\n+\t\tif (reset_head(repo, &reset_opts) < 0) {\n+\t\t\tret = error(_(\"could not switch to %s\"), hex);\n+\t\t\tgoto out;\n+\t\t}\n+\t} else {\n+\t\t/*\n+\t\t * Prepare a revision walk from old commit to the commit that is\n+\t\t * about to be dropped. This serves three purposes:\n+\t\t *\n+\t\t *   - We verify that the history doesn't contain any merges.\n+\t\t *     For now, merges aren't yet handled by us.\n+\t\t *\n+\t\t *   - We need to find the child of the commit-to-be-dropped.\n+\t\t *     This child is what will be adopted by the parent of the\n+\t\t *     commit that we are about to drop.\n+\t\t *\n+\t\t *   - We compute the list of commits-to-be-picked.\n+\t\t */\n+\t\tret = collect_commits(repo, commit_to_drop, head, &commits);\n+\t\tif (ret < 0)\n+\t\t\tgoto out;\n+\n+\t\tret = apply_commits(repo, &commits, head, commit_to_drop->parents ?\n+\t\t\t\t    commit_to_drop->parents->item : NULL, \"drop\");\n+\t\tif (ret < 0)\n+\t\t\tgoto out;\n+\t}\n+\n+\tret = 0;\n+\n+out:\n+\tstrvec_clear(&commits);\n+\tstrbuf_release(&buf);\n+\treturn ret;\n+}\n \n int cmd_history(int argc,\n \t\tconst char **argv,\n \t\tconst char *prefix,\n-\t\tstruct repository *repo UNUSED)\n+\t\tstruct repository *repo)\n {\n \tconst char * const usage[] = {\n-\t\tN_(\"git history [<options>]\"),\n+\t\tN_(\"git history drop [<options>] <revision>\"),\n \t\tNULL,\n \t};\n+\tparse_opt_subcommand_fn *fn = NULL;\n \tstruct option options[] = {\n+\t\tOPT_SUBCOMMAND(\"drop\", &fn, cmd_history_drop),\n \t\tOPT_END(),\n \t};\n \n \targc = parse_options(argc, argv, prefix, options, usage, 0);\n-\treturn 0;\n+\treturn fn(argc, argv, prefix, repo);\n }\ndiff --git a/t/meson.build b/t/meson.build\nindex bbeba1a8d5..859c388987 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -376,6 +376,7 @@ integration_tests = [\n   't3436-rebase-more-options.sh',\n   't3437-rebase-fixup-options.sh',\n   't3438-rebase-broken-files.sh',\n+  't3450-history-drop.sh',\n   't3500-cherry.sh',\n   't3501-revert-cherry-pick.sh',\n   't3502-cherry-pick-merge.sh',\n@@ -1214,4 +1215,4 @@ if perl.found() and time.found()\n       timeout: 0,\n     )\n   endforeach\n-endif\n\\ No newline at end of file\n+endif\ndiff --git a/t/t3450-history-drop.sh b/t/t3450-history-drop.sh\nnew file mode 100755\nindex 0000000000..4782144da0\n--- /dev/null\n+++ b/t/t3450-history-drop.sh\n@@ -0,0 +1,127 @@\n+#!/bin/sh\n+\n+test_description='tests for git-history drop subcommand'\n+\n+. ./test-lib.sh\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 drop HEAD 2>err &&\n+\t\ttest_grep \"commit to be dropped must not be a merge commit\" err &&\n+\t\ttest_must_fail git history drop HEAD~ 2>err &&\n+\t\ttest_grep \"cannot rearrange commit history with merges\" err\n+\t)\n+'\n+\n+test_expect_success 'refuses to work when history becomes empty' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit base &&\n+\t\ttest_must_fail git history drop HEAD 2>err &&\n+\t\ttest_grep \"cannot drop the only commit on this branch\" err\n+\t)\n+'\n+\n+test_expect_success 'can drop tip of a branch' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit first &&\n+\t\ttest_commit second &&\n+\t\ttest_commit third &&\n+\n+\t\tgit symbolic-ref HEAD >expect &&\n+\t\tgit history drop HEAD &&\n+\t\tgit symbolic-ref HEAD >actual &&\n+\t\ttest_cmp expect actual &&\n+\n+\t\tcat >expect <<-EOF &&\n+\t\tsecond\n+\t\tfirst\n+\t\tEOF\n+\t\tgit log --format=%s >actual &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+test_expect_success 'can drop commit in the middle' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit first &&\n+\t\ttest_commit second &&\n+\t\ttest_commit third &&\n+\t\ttest_commit fourth &&\n+\t\ttest_commit fifth &&\n+\n+\t\tgit symbolic-ref HEAD >expect &&\n+\t\tgit history drop HEAD~2 &&\n+\t\tgit symbolic-ref HEAD >actual &&\n+\t\ttest_cmp expect actual &&\n+\n+\t\tcat >expect <<-EOF &&\n+\t\tfifth\n+\t\tfourth\n+\t\tsecond\n+\t\tfirst\n+\t\tEOF\n+\t\tgit log --format=%s >actual &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+test_expect_success 'correct order is retained' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit first &&\n+\t\ttest_commit second &&\n+\t\ttest_commit third &&\n+\t\ttest_commit fourth &&\n+\t\ttest_commit fifth &&\n+\t\tgit history drop HEAD~3 &&\n+\t\tcat >expect <<-EOF &&\n+\t\tfifth\n+\t\tfourth\n+\t\tthird\n+\t\tfirst\n+\t\tEOF\n+\t\tgit log --format=%s >actual &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+test_expect_success 'can drop root commit' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit first &&\n+\t\ttest_commit second &&\n+\t\ttest_commit third &&\n+\t\tgit history drop HEAD~2 &&\n+\t\tcat >expect <<-EOF &&\n+\t\tthird\n+\t\tsecond\n+\t\tEOF\n+\t\tgit log --format=%s >actual &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+test_done\n\n-- \n2.51.0.261.g7ce5a0a67e.dirty\n\n"},{"id":"524429","messageId":"20250819-b4-pks-history-builtin-v1-6-9b77c32688fe@pks.im","threadId":"63986","inReplyTo":"20250819-b4-pks-history-builtin-v1-0-9b77c32688fe@pks.im","subject":"[PATCH RFC 06/11] builtin/history: implement \"reorder\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-08-19T10:56:02Z","receivedAt":"2025-08-19T10:56:22Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"When working in projects where having nice commits matters it's quite\ncommon that developers end up reordering commits a lot. Tihs is\ntypically done via interactive rebases, where they can then rearrange\ncommits in the instruction sheet.\n\nStill, this operation is a frequent-enough operation to provide a more\ndirect of doing this imperatively. As such, introduce a new \"reorder\"\nsubcommand where users can reorder a commit A to come after or before\nanother commit B:\n\n    $ git log --oneline\n    a978f73 fifth\n    57594ee fourth\n    04eb1c4 third\n    d535e30 second\n    bf7438d first\n\n    $ git history reorder :/fourth --before=:/second\n    $ git log --oneline\n    1610fe0 fifth\n    444f97d third\n    2f90797 second\n    b0ae659 fourth\n    bf7438d first\n\n    $ git history reorder :/fourth --after=:/second\n    $ git log --oneline\n    c48729d fifth\n    f44a46e third\n    26693b8 fourth\n    8cb4171 second\n    bf7438d first\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/git-history.adoc |  32 ++++++\n builtin/history.c              | 135 +++++++++++++++++++++++++\n t/meson.build                  |   1 +\n t/t3451-history-reorder.sh     | 218 +++++++++++++++++++++++++++++++++++++++++\n 4 files changed, 386 insertions(+)\n\ndiff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc\nindex 3012445ddc..6e8b4e1326 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 drop [<options>] <revision>\n+git history reorder [<options>] <revision> --(before|after)=<revision>\n \n DESCRIPTION\n -----------\n@@ -40,6 +41,12 @@ Dropping the root commit converts the child of that commit into the new\n root commit. It is invalid to drop a root commit that does not have any\n child commits, as that would lead to an empty branch.\n \n+reorder <revision> (--before=<revision>|--after=<revision>)::\n+\tReorder the commit so that it becomes either the parent\n+\t(`--before=`) or child (`--after=`) of the other specified\n+\tcommit. The commits must be related to one another and must be\n+\treachable from the current `HEAD` commit.\n+\n EXAMPLES\n --------\n \n@@ -56,6 +63,31 @@ b1bc1bd third\n e098c27 first\n ----------\n \n+* Reorder a commit.\n++\n+----------\n+$ git log --oneline\n+a978f73 fifth\n+57594ee fourth\n+04eb1c4 third\n+d535e30 second\n+bf7438d first\n+$ git history reorder :/fourth --before=:/second\n+$ git log --oneline\n+1610fe0 fifth\n+444f97d third\n+2f90797 second\n+b0ae659 fourth\n+bf7438d first\n+$ git history reorder :/fourth --after=:/second\n+$ git log --oneline\n+c48729d fifth\n+f44a46e third\n+26693b8 fourth\n+8cb4171 second\n+bf7438d first\n+----------\n+\n CONFIGURATION\n -------------\n \ndiff --git a/builtin/history.c b/builtin/history.c\nindex 183ab9d5f7..de6073f557 100644\n--- a/builtin/history.c\n+++ b/builtin/history.c\n@@ -83,6 +83,33 @@ static int collect_commits(struct repository *repo,\n \treturn ret;\n }\n \n+static void replace_commits(struct strvec *commits,\n+\t\t\t    const struct object_id *commit_to_replace,\n+\t\t\t    const struct object_id *replacements,\n+\t\t\t    size_t replacements_nr)\n+{\n+\tchar commit_to_replace_oid[GIT_MAX_HEXSZ + 1];\n+\tstruct strvec replacement_oids = STRVEC_INIT;\n+\tbool found = false;\n+\tsize_t i;\n+\n+\toid_to_hex_r(commit_to_replace_oid, commit_to_replace);\n+\tfor (i = 0; i < replacements_nr; i++)\n+\t\tstrvec_push(&replacement_oids, oid_to_hex(&replacements[i]));\n+\n+\tfor (i = 0; i < commits->nr; i++) {\n+\t\tif (strcmp(commits->v[i], commit_to_replace_oid))\n+\t\t\tcontinue;\n+\t\tstrvec_splice(commits, i, 1, replacement_oids.v, replacement_oids.nr);\n+\t\tfound = true;\n+\t\tbreak;\n+\t}\n+\tif (!found)\n+\t\tBUG(\"could not find commit to replace\");\n+\n+\tstrvec_clear(&replacement_oids);\n+}\n+\n static int apply_commits(struct repository *repo,\n \t\t\t const struct strvec *commits,\n \t\t\t struct commit *head,\n@@ -291,6 +318,112 @@ static int cmd_history_drop(int argc,\n \treturn ret;\n }\n \n+static int cmd_history_reorder(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\tN_(\"git history reorder [<options>] <revision> (--before=<commit>|--after=<commit>)\"),\n+\t\tNULL,\n+\t};\n+\tconst char *before = NULL, *after = NULL;\n+\tstruct option options[] = {\n+\t\tOPT_STRING(0, \"before\", &before, N_(\"commit\"), N_(\"reorder before this commit\")),\n+\t\tOPT_STRING(0, \"after\", &after, N_(\"commit\"), N_(\"reorder after this commit\")),\n+\t\tOPT_END(),\n+\t};\n+\tstruct commit *commit_to_reorder, *head, *anchor, *old;\n+\tstruct strvec commits = STRVEC_INIT;\n+\tstruct object_id replacement[2];\n+\tstruct commit_list *list = NULL;\n+\tint ret;\n+\n+\targc = parse_options(argc, argv, prefix, options, usage, 0);\n+\tif (argc != 1)\n+\t\tdie(_(\"command expects a single revision\"));\n+\tif (!before && !after)\n+\t\tdie(_(\"exactly one option of 'before' or 'after' must be given\"));\n+\tdie_for_incompatible_opt2(!!before, \"before\", !!after, \"after\");\n+\n+\trepo_config(repo, git_default_config, NULL);\n+\n+\tcommit_to_reorder = lookup_commit_reference_by_name(argv[0]);\n+\tif (!commit_to_reorder)\n+\t\tdie(_(\"commit to be reordered cannot be found: %s\"), argv[0]);\n+\tif (commit_to_reorder->parents && commit_to_reorder->parents->next)\n+\t\tdie(_(\"commit to be reordered must not be a merge commit\"));\n+\n+\tanchor = lookup_commit_reference_by_name(before ? before : after);\n+\tif (!commit_to_reorder)\n+\t\tdie(_(\"anchor commit cannot be found: %s\"), before ? before : after);\n+\n+\tif (oideq(&commit_to_reorder->object.oid, &anchor->object.oid))\n+\t\tdie(_(\"commit to reorder and anchor must not be the same\"));\n+\n+\thead = lookup_commit_reference_by_name(\"HEAD\");\n+\tif (!head)\n+\t\tdie(_(\"could not resolve HEAD to a commit\"));\n+\n+\tcommit_list_append(commit_to_reorder, &list);\n+\tif (!repo_is_descendant_of(repo, commit_to_reorder, list))\n+\t\tdie(_(\"reordered commit must be reachable from current HEAD commit\"));\n+\n+\t/*\n+\t * There is no requirement for the user to have either one of the\n+\t * provided commits be the parent or child. We thus have to figure out\n+\t * ourselves which one is which.\n+\t*/\n+\tif (repo_is_descendant_of(repo, anchor, list))\n+\t\told = commit_to_reorder;\n+\telse\n+\t\told = anchor;\n+\n+\t/*\n+\t * Select the whole range of commits, including the boundary commit\n+\t * itself. In case the old commit is the root commit we simply pass no\n+\t * boundary.\n+\t*/\n+\tret = collect_commits(repo, old->parents ? old->parents->item : NULL,\n+\t\t\t      head, &commits);\n+\tif (ret < 0)\n+\t\tgoto out;\n+\n+\t/*\n+\t * Perform the reordering of commits in the strvec. This is done by:\n+\t *\n+\t *   - Deleting the to-be-reordered commit from the range of commits.\n+\t *\n+\t *   - Replacing the anchor commit with the anchor commit plus the\n+\t *     to-be-reordered commit.\n+\t */\n+\tif (before) {\n+\t\treplacement[0] = commit_to_reorder->object.oid;\n+\t\treplacement[1] = anchor->object.oid;\n+\t} else {\n+\t\treplacement[0] = anchor->object.oid;\n+\t\treplacement[1] = commit_to_reorder->object.oid;\n+\t}\n+\treplace_commits(&commits, &commit_to_reorder->object.oid, NULL, 0);\n+\treplace_commits(&commits, &anchor->object.oid, replacement, ARRAY_SIZE(replacement));\n+\n+\t/*\n+\t * And now we pick commits in the new order on top of either the root\n+\t * commit or on top the old commit's parent.\n+\t */\n+\tret = apply_commits(repo, &commits, head,\n+\t\t\t    old->parents ? old->parents->item : NULL, \"reorder\");\n+\tif (ret < 0)\n+\t\tgoto out;\n+\n+\tret = 0;\n+\n+out:\n+\tfree_commit_list(list);\n+\tstrvec_clear(&commits);\n+\treturn ret;\n+}\n+\n int cmd_history(int argc,\n \t\tconst char **argv,\n \t\tconst char *prefix,\n@@ -298,11 +431,13 @@ int cmd_history(int argc,\n {\n \tconst char * const usage[] = {\n \t\tN_(\"git history drop [<options>] <revision>\"),\n+\t\tN_(\"git history reorder [<options>] <revision> --(before|after)=<revision>\"),\n \t\tNULL,\n \t};\n \tparse_opt_subcommand_fn *fn = NULL;\n \tstruct option options[] = {\n \t\tOPT_SUBCOMMAND(\"drop\", &fn, cmd_history_drop),\n+\t\tOPT_SUBCOMMAND(\"reorder\", &fn, cmd_history_reorder),\n \t\tOPT_END(),\n \t};\n \ndiff --git a/t/meson.build b/t/meson.build\nindex 859c388987..8eded9ec1b 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -377,6 +377,7 @@ integration_tests = [\n   't3437-rebase-fixup-options.sh',\n   't3438-rebase-broken-files.sh',\n   't3450-history-drop.sh',\n+  't3451-history-reorder.sh',\n   't3500-cherry.sh',\n   't3501-revert-cherry-pick.sh',\n   't3502-cherry-pick-merge.sh',\ndiff --git a/t/t3451-history-reorder.sh b/t/t3451-history-reorder.sh\nnew file mode 100755\nindex 0000000000..cc311ba190\n--- /dev/null\n+++ b/t/t3451-history-reorder.sh\n@@ -0,0 +1,218 @@\n+#!/bin/sh\n+\n+test_description='tests for git-history reorder subcommand'\n+\n+. ./test-lib.sh\n+\n+test_expect_success 'reorder 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 reorder HEAD --before=HEAD~ 2>err &&\n+\t\ttest_grep \"commit to be reordered must not be a merge commit\" err &&\n+\t\ttest_must_fail git history reorder HEAD~ --after=HEAD 2>err &&\n+\t\ttest_grep \"cannot rearrange commit history with merges\" err\n+\t)\n+'\n+\n+test_expect_success 'reorder requires exactly one of --before or --after' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit first &&\n+\t\ttest_commit second &&\n+\t\ttest_must_fail git history reorder HEAD 2>err &&\n+\t\ttest_grep \"exactly one option of ${SQ}before${SQ} or ${SQ}after${SQ} must be given\" err &&\n+\t\ttest_must_fail git history reorder HEAD --before=a --after=b 2>err &&\n+\t\ttest_grep \"options ${SQ}before${SQ} and ${SQ}after${SQ} cannot be used together\" err\n+\t)\n+'\n+\n+test_expect_success 'reorder refuses to reorder commit with itself' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit first &&\n+\t\ttest_commit second &&\n+\t\ttest_must_fail git history reorder HEAD --after=HEAD 2>err &&\n+\t\ttest_grep \"commit to reorder and anchor must not be the same\" err\n+\t)\n+'\n+\n+test_expect_success '--before can move commit back in history' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit first &&\n+\t\ttest_commit second &&\n+\t\ttest_commit third &&\n+\t\ttest_commit fourth &&\n+\t\ttest_commit fifth &&\n+\t\tgit history reorder :/fourth --before=:/second &&\n+\t\tcat >expect <<-EOF &&\n+\t\tfifth\n+\t\tthird\n+\t\tsecond\n+\t\tfourth\n+\t\tfirst\n+\t\tEOF\n+\t\tgit log --format=%s >actual &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+test_expect_success '--before can move commit forward in history' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit first &&\n+\t\ttest_commit second &&\n+\t\ttest_commit third &&\n+\t\ttest_commit fourth &&\n+\t\ttest_commit fifth &&\n+\t\tgit history reorder :/second --before=:/fourth &&\n+\t\tcat >expect <<-EOF &&\n+\t\tfifth\n+\t\tfourth\n+\t\tsecond\n+\t\tthird\n+\t\tfirst\n+\t\tEOF\n+\t\tgit log --format=%s >actual &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+test_expect_success '--before can make a commit a root commit' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit first &&\n+\t\ttest_commit second &&\n+\t\ttest_commit third &&\n+\t\tgit history reorder :/third --before=:/first &&\n+\t\tcat >expect <<-EOF &&\n+\t\tsecond\n+\t\tfirst\n+\t\tthird\n+\t\tEOF\n+\t\tgit log --format=%s >actual &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+test_expect_success '--after can move commit back in history' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit first &&\n+\t\ttest_commit second &&\n+\t\ttest_commit third &&\n+\t\ttest_commit fourth &&\n+\t\ttest_commit fifth &&\n+\t\tgit history reorder :/fourth --after=:/second &&\n+\t\tcat >expect <<-EOF &&\n+\t\tfifth\n+\t\tthird\n+\t\tfourth\n+\t\tsecond\n+\t\tfirst\n+\t\tEOF\n+\t\tgit log --format=%s >actual &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+test_expect_success '--after can move commit forward in history' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit first &&\n+\t\ttest_commit second &&\n+\t\ttest_commit third &&\n+\t\ttest_commit fourth &&\n+\t\ttest_commit fifth &&\n+\t\tgit history reorder :/second --after=:/fourth &&\n+\t\tcat >expect <<-EOF &&\n+\t\tfifth\n+\t\tsecond\n+\t\tfourth\n+\t\tthird\n+\t\tfirst\n+\t\tEOF\n+\t\tgit log --format=%s >actual &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+test_expect_success '--after can make commit the tip' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit first &&\n+\t\ttest_commit second &&\n+\t\ttest_commit third &&\n+\t\tgit history reorder :/first --after=:/third &&\n+\t\tcat >expect <<-EOF &&\n+\t\tfirst\n+\t\tthird\n+\t\tsecond\n+\t\tEOF\n+\t\tgit log --format=%s >actual &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+test_expect_success 'conflicts are detected' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\techo base >file &&\n+\t\tgit add file &&\n+\t\tgit commit -m base &&\n+\t\techo \"first edit\" >file &&\n+\t\tgit commit -am \"first edit\" &&\n+\t\techo \"second edit\" >file &&\n+\t\tgit commit -am \"second edit\" &&\n+\n+\t\tgit symbolic-ref HEAD >expect-head &&\n+\t\ttest_must_fail git history reorder HEAD --before=HEAD~ &&\n+\t\ttest_must_fail git symbolic-ref HEAD &&\n+\t\techo \"second edit\" >file &&\n+\t\tgit add file &&\n+\t\ttest_must_fail git cherry-pick --continue &&\n+\t\techo \"first edit\" >file &&\n+\t\tgit add file &&\n+\t\tgit cherry-pick --continue &&\n+\n+\t\tcat >expect <<-EOF &&\n+\t\tfirst edit\n+\t\tsecond edit\n+\t\tbase\n+\t\tEOF\n+\t\tgit log --format=%s >actual &&\n+\t\ttest_cmp expect actual &&\n+\n+\t\tgit symbolic-ref HEAD >actual-head &&\n+\t\ttest_cmp expect-head actual-head\n+\t)\n+'\n+\n+test_done\n\n-- \n2.51.0.261.g7ce5a0a67e.dirty\n\n"},{"id":"524430","messageId":"20250819-b4-pks-history-builtin-v1-7-9b77c32688fe@pks.im","threadId":"63986","inReplyTo":"20250819-b4-pks-history-builtin-v1-0-9b77c32688fe@pks.im","subject":"[PATCH RFC 07/11] add-patch: split out header from \"add-interactive.h\"","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-08-19T10:56:03Z","receivedAt":"2025-08-19T10:56:25Z","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 | 23 +++--------------------\n add-patch.c       |  1 +\n add-patch.h       | 26 ++++++++++++++++++++++++++\n 3 files changed, 30 insertions(+), 20 deletions(-)\n\ndiff --git a/add-interactive.h b/add-interactive.h\nindex 4213dcd67b..fb95b6ee05 100644\n--- a/add-interactive.h\n+++ b/add-interactive.h\n@@ -1,14 +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-};\n-\n-#define ADD_P_OPT_INIT { .context = -1, .interhunkcontext = -1 }\n+struct pathspec;\n+struct repository;\n \n struct add_i_state {\n \tstruct repository *r;\n@@ -32,21 +29,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 302e6ba7d9..e2b002fa73 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..4394c74107\n--- /dev/null\n+++ b/add-patch.h\n@@ -0,0 +1,26 @@\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+};\n+\n+#define ADD_P_OPT_INIT { .context = -1, .interhunkcontext = -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.51.0.261.g7ce5a0a67e.dirty\n\n"},{"id":"524431","messageId":"20250819-b4-pks-history-builtin-v1-8-9b77c32688fe@pks.im","threadId":"63986","inReplyTo":"20250819-b4-pks-history-builtin-v1-0-9b77c32688fe@pks.im","subject":"[PATCH RFC 08/11] add-patch: split out `struct interactive_options`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-08-19T10:56:04Z","receivedAt":"2025-08-19T10:56:28Z","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 the infra for \"git add -p\"\nand \"git add -i\". Users of `run_add_i()` for example are expected to\npass `struct add_p_opt`. This is somewhat confusing and raises the\nquestion 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  | 151 +++++++++++++----------------------------------------\n add-interactive.h  |  20 ++-----\n add-patch.c        | 145 +++++++++++++++++++++++++++++++++++++++++---------\n add-patch.h        |  33 ++++++++++--\n builtin/add.c      |  22 ++++----\n builtin/checkout.c |   4 +-\n builtin/commit.c   |  16 +++---\n builtin/reset.c    |  16 +++---\n builtin/stash.c    |  46 ++++++++--------\n commit.h           |   2 +-\n 10 files changed, 243 insertions(+), 212 deletions(-)\n\ndiff --git a/add-interactive.c b/add-interactive.c\nindex 3e692b47ec..3babc3e013 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,96 +19,18 @@\n #include \"prompt.h\"\n #include \"tree.h\"\n \n-static void init_color(struct repository *r, struct add_i_state *s,\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 (!s->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 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-\tconst char *value;\n-\n \ts->r = r;\n-\ts->context = -1;\n-\ts->interhunkcontext = -1;\n-\n-\tif (repo_config_get_value(r, \"color.interactive\", &value))\n-\t\ts->use_color = -1;\n-\telse\n-\t\ts->use_color =\n-\t\t\tgit_config_colorbool(\"color.interactive\", value);\n-\ts->use_color = want_color(s->use_color);\n-\n-\tinit_color(r, s, \"interactive.header\", s->header_color, GIT_COLOR_BOLD);\n-\tinit_color(r, s, \"interactive.help\", s->help_color, GIT_COLOR_BOLD_RED);\n-\tinit_color(r, s, \"interactive.prompt\", s->prompt_color,\n-\t\t   GIT_COLOR_BOLD_BLUE);\n-\tinit_color(r, s, \"interactive.error\", s->error_color,\n-\t\t   GIT_COLOR_BOLD_RED);\n-\n-\tinit_color(r, s, \"diff.frag\", s->fraginfo_color,\n-\t\t   diff_get_color(s->use_color, DIFF_FRAGINFO));\n-\tinit_color(r, s, \"diff.context\", s->context_color, \"fall back\");\n-\tif (!strcmp(s->context_color, \"fall back\"))\n-\t\tinit_color(r, s, \"diff.plain\", s->context_color,\n-\t\t\t   diff_get_color(s->use_color, DIFF_CONTEXT));\n-\tinit_color(r, s, \"diff.old\", s->file_old_color,\n-\t\tdiff_get_color(s->use_color, DIFF_FILE_OLD));\n-\tinit_color(r, s, \"diff.new\", s->file_new_color,\n-\t\tdiff_get_color(s->use_color, DIFF_FILE_NEW));\n-\n-\tstrlcpy(s->reset_color,\n-\t\ts->use_color ? 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 = -1;\n+\tinteractive_config_clear(&s->cfg);\n }\n \n /*\n@@ -262,7 +183,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@@ -330,7 +251,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@@ -408,7 +329,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@@ -968,7 +889,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@@ -990,9 +911,9 @@ 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\tstruct interactive_options opts = {\n+\t\t\t.context = s->cfg.context,\n+\t\t\t.interhunkcontext = s->cfg.interhunkcontext,\n \t\t};\n \t\tstruct strvec args = STRVEC_INIT;\n \t\tstruct pathspec ps_selected = { 0 };\n@@ -1004,7 +925,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@@ -1040,10 +961,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@@ -1061,17 +982,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@@ -1079,21 +1000,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@@ -1128,7 +1049,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@@ -1139,7 +1060,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@@ -1182,15 +1103,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 (s.use_color) {\n-\t\tdata.color = s.prompt_color;\n-\t\tdata.reset = s.reset_color;\n+\tif (s.cfg.use_color) {\n+\t\tdata.color = s.cfg.prompt_color;\n+\t\tdata.reset = s.cfg.reset_color;\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 fb95b6ee05..eefa2edc7c 100644\n--- a/add-interactive.h\n+++ b/add-interactive.h\n@@ -2,34 +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-\tint use_color;\n-\tchar header_color[COLOR_MAXLEN];\n-\tchar help_color[COLOR_MAXLEN];\n-\tchar prompt_color[COLOR_MAXLEN];\n-\tchar error_color[COLOR_MAXLEN];\n-\tchar reset_color[COLOR_MAXLEN];\n-\tchar fraginfo_color[COLOR_MAXLEN];\n-\tchar context_color[COLOR_MAXLEN];\n-\tchar file_old_color[COLOR_MAXLEN];\n-\tchar file_new_color[COLOR_MAXLEN];\n-\n-\tint use_single_key;\n-\tchar *interactive_diff_filter, *interactive_diff_algorithm;\n-\tint context, interhunkcontext;\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 e2b002fa73..45bc254e0c 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,99 @@ struct add_p_state {\n \tconst char *revision;\n };\n \n+static void init_color(struct repository *r,\n+\t\t       struct interactive_config *cfg,\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 (!cfg->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+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+\tconst char *value;\n+\n+\tcfg->context = -1;\n+\tcfg->interhunkcontext = -1;\n+\n+\tif (repo_config_get_value(r, \"color.interactive\", &value))\n+\t\tcfg->use_color = -1;\n+\telse\n+\t\tcfg->use_color =\n+\t\t\tgit_config_colorbool(\"color.interactive\", value);\n+\tcfg->use_color = want_color(cfg->use_color);\n+\n+\tinit_color(r, cfg, \"interactive.header\", cfg->header_color, GIT_COLOR_BOLD);\n+\tinit_color(r, cfg, \"interactive.help\", cfg->help_color, GIT_COLOR_BOLD_RED);\n+\tinit_color(r, cfg, \"interactive.prompt\", cfg->prompt_color,\n+\t\t   GIT_COLOR_BOLD_BLUE);\n+\tinit_color(r, cfg, \"interactive.error\", cfg->error_color,\n+\t\t   GIT_COLOR_BOLD_RED);\n+\n+\tinit_color(r, cfg, \"diff.frag\", cfg->fraginfo_color,\n+\t\t   diff_get_color(cfg->use_color, DIFF_FRAGINFO));\n+\tinit_color(r, cfg, \"diff.context\", cfg->context_color, \"fall back\");\n+\tif (!strcmp(cfg->context_color, \"fall back\"))\n+\t\tinit_color(r, cfg, \"diff.plain\", cfg->context_color,\n+\t\t\t   diff_get_color(cfg->use_color, DIFF_CONTEXT));\n+\tinit_color(r, cfg, \"diff.old\", cfg->file_old_color,\n+\t\tdiff_get_color(cfg->use_color, DIFF_FILE_OLD));\n+\tinit_color(r, cfg, \"diff.new\", cfg->file_new_color,\n+\t\tdiff_get_color(cfg->use_color, DIFF_FILE_NEW));\n+\n+\tstrlcpy(cfg->reset_color,\n+\t\tcfg->use_color ? 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 = -1;\n+}\n+\n static void add_p_state_clear(struct add_p_state *s)\n {\n \tsize_t i;\n@@ -299,9 +394,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);\n+\tputs(s->s.cfg.reset_color);\n \tva_end(args);\n }\n \n@@ -424,12 +519,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@@ -460,7 +555,7 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps)\n \n \tif (want_color_fd(1, -1)) {\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 +788,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 +810,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);\n+\t\t\tstrbuf_addf(out, \"%s\\n\", s->s.cfg.reset_color);\n \t\telse\n \t\t\tstrbuf_addch(out, '\\n');\n \t}\n@@ -1103,12 +1198,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);\n+\t\tstrbuf_addstr(&s->colored, s->s.cfg.reset_color);\n \t\tif (next > eol)\n \t\t\tstrbuf_add(&s->colored, plain + eol, next - eol);\n \t\tcurrent = next;\n@@ -1227,7 +1322,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@@ -1241,7 +1336,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@@ -1522,15 +1617,15 @@ static int patch_update_file(struct add_p_state *s,\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 \t\t\t\t\t\t: 1));\n \t\tprintf(_(s->mode->prompt_mode[prompt_mode_type]),\n \t\t       s->buf.buf);\n-\t\tif (*s->s.reset_color)\n-\t\t\tfputs(s->s.reset_color, stdout);\n+\t\tif (*s->s.cfg.reset_color)\n+\t\t\tfputs(s->s.cfg.reset_color, stdout);\n \t\tfflush(stdout);\n \t\tif (read_single_character(s) == EOF)\n \t\t\tbreak;\n@@ -1687,7 +1782,7 @@ static int patch_update_file(struct add_p_state *s,\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@@ -1705,7 +1800,7 @@ static int patch_update_file(struct add_p_state *s,\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@@ -1723,7 +1818,7 @@ static int patch_update_file(struct add_p_state *s,\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@@ -1764,7 +1859,7 @@ static int patch_update_file(struct add_p_state *s,\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@@ -1772,7 +1867,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;\ndiff --git a/add-patch.h b/add-patch.h\nindex 4394c74107..51c0d7bce9 100644\n--- a/add-patch.h\n+++ b/add-patch.h\n@@ -1,15 +1,42 @@\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 };\n \n-#define ADD_P_OPT_INIT { .context = -1, .interhunkcontext = -1 }\n+#define INTERACTIVE_OPTIONS_INIT { \\\n+\t.context = -1, \\\n+\t.interhunkcontext = -1, \\\n+}\n+\n+struct interactive_config {\n+\tint use_color;\n+\tchar header_color[COLOR_MAXLEN];\n+\tchar help_color[COLOR_MAXLEN];\n+\tchar prompt_color[COLOR_MAXLEN];\n+\tchar error_color[COLOR_MAXLEN];\n+\tchar reset_color[COLOR_MAXLEN];\n+\tchar fraginfo_color[COLOR_MAXLEN];\n+\tchar context_color[COLOR_MAXLEN];\n+\tchar file_old_color[COLOR_MAXLEN];\n+\tchar file_new_color[COLOR_MAXLEN];\n+\n+\tint use_single_key;\n+\tchar *interactive_diff_filter, *interactive_diff_algorithm;\n+\tint context, interhunkcontext;\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@@ -20,7 +47,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 0235854f80..a94c826c14 100644\n--- a/builtin/add.c\n+++ b/builtin/add.c\n@@ -30,7 +30,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@@ -159,7 +159,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@@ -171,9 +171,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@@ -255,8 +255,8 @@ 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_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@@ -398,9 +398,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@@ -410,11 +410,11 @@ 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}\n \ndiff --git a/builtin/checkout.c b/builtin/checkout.c\nindex 43583c8d1b..0b90f398fe 100644\n--- a/builtin/checkout.c\n+++ b/builtin/checkout.c\n@@ -546,7 +546,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};\n@@ -575,7 +575,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 b5b9608813..767351fd87 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@@ -356,9 +356,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@@ -407,7 +407,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@@ -432,9 +432,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@@ -1738,8 +1738,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 ed35802af1..088449e120 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,8 +371,8 @@ 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_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@@ -423,9 +423,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@@ -436,12 +436,12 @@ 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}\n \ndiff --git a/builtin/stash.c b/builtin/stash.c\nindex 1977e50df2..5070b8a88f 100644\n--- a/builtin/stash.c\n+++ b/builtin/stash.c\n@@ -1302,7 +1302,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@@ -1327,7 +1327,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@@ -1422,7 +1422,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@@ -1504,7 +1505,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@@ -1590,7 +1591,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@@ -1662,7 +1664,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@@ -1835,7 +1837,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@@ -1843,8 +1845,8 @@ 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_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@@ -1901,19 +1903,19 @@ 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}\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@@ -1938,7 +1940,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@@ -1946,8 +1948,8 @@ 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_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@@ -1967,20 +1969,20 @@ 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}\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 1d6e0c7518..7b6e59d6c1 100644\n--- a/commit.h\n+++ b/commit.h\n@@ -258,7 +258,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.51.0.261.g7ce5a0a67e.dirty\n\n"},{"id":"524432","messageId":"20250819-b4-pks-history-builtin-v1-9-9b77c32688fe@pks.im","threadId":"63986","inReplyTo":"20250819-b4-pks-history-builtin-v1-0-9b77c32688fe@pks.im","subject":"[PATCH RFC 09/11] add-patch: remove dependency on \"add-interactive\" subsystem","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-08-19T10:56:05Z","receivedAt":"2025-08-19T10:56:30Z","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 | 68 ++++++++++++++++++++++++++++++++-----------------------------\n 1 file changed, 36 insertions(+), 32 deletions(-)\n\ndiff --git a/add-patch.c b/add-patch.c\nindex 45bc254e0c..1bcbc91de9 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@@ -385,7 +385,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@@ -394,9 +394,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);\n+\tputs(s->cfg.reset_color);\n \tva_end(args);\n }\n \n@@ -414,7 +414,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@@ -519,12 +519,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@@ -555,7 +555,7 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps)\n \n \tif (want_color_fd(1, -1)) {\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@@ -788,7 +788,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@@ -810,7 +810,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);\n+\t\t\tstrbuf_addf(out, \"%s\\n\", s->cfg.reset_color);\n \t\telse\n \t\t\tstrbuf_addch(out, '\\n');\n \t}\n@@ -1198,12 +1198,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);\n+\t\tstrbuf_addstr(&s->colored, s->cfg.reset_color);\n \t\tif (next > eol)\n \t\t\tstrbuf_add(&s->colored, plain + eol, next - eol);\n \t\tcurrent = next;\n@@ -1322,7 +1322,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@@ -1336,7 +1336,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@@ -1617,15 +1617,15 @@ static int patch_update_file(struct add_p_state *s,\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 \t\t\t\t\t\t: 1));\n \t\tprintf(_(s->mode->prompt_mode[prompt_mode_type]),\n \t\t       s->buf.buf);\n-\t\tif (*s->s.cfg.reset_color)\n-\t\t\tfputs(s->s.cfg.reset_color, stdout);\n+\t\tif (*s->cfg.reset_color)\n+\t\t\tfputs(s->cfg.reset_color, stdout);\n \t\tfflush(stdout);\n \t\tif (read_single_character(s) == EOF)\n \t\t\tbreak;\n@@ -1782,7 +1782,7 @@ static int patch_update_file(struct add_p_state *s,\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@@ -1800,7 +1800,7 @@ static int patch_update_file(struct add_p_state *s,\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@@ -1818,7 +1818,7 @@ static int patch_update_file(struct add_p_state *s,\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@@ -1838,7 +1838,7 @@ static int patch_update_file(struct add_p_state *s,\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\t   s->mode->is_reverse);\n@@ -1849,8 +1849,8 @@ static int patch_update_file(struct add_p_state *s,\n \t\t\t\t\t NULL, 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\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@@ -1863,11 +1863,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\n-- \n2.51.0.261.g7ce5a0a67e.dirty\n\n"},{"id":"524433","messageId":"20250819-b4-pks-history-builtin-v1-10-9b77c32688fe@pks.im","threadId":"63986","inReplyTo":"20250819-b4-pks-history-builtin-v1-0-9b77c32688fe@pks.im","subject":"[PATCH RFC 10/11] add-patch: add support for in-memory index patching","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-08-19T10:56:06Z","receivedAt":"2025-08-19T10:56:34Z","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,\nworktree 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 | 111 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--\n add-patch.h |   8 +++++\n 2 files changed, 116 insertions(+), 3 deletions(-)\n\ndiff --git a/add-patch.c b/add-patch.c\nindex 1bcbc91de9..adef20c02b 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@@ -414,7 +418,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@@ -1838,7 +1842,7 @@ static int patch_update_file(struct add_p_state *s,\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\t   s->mode->is_reverse);\n@@ -1849,9 +1853,12 @@ static int patch_update_file(struct add_p_state *s,\n \t\t\t\t\t NULL, 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\tread_index_from(s->index, s->index_file, s->r->gitdir);\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 \tputchar('\\n');\n@@ -1864,6 +1871,8 @@ int run_add_p(struct repository *r, enum add_p_mode mode,\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@@ -1922,3 +1931,99 @@ int run_add_p(struct repository *r, enum add_p_mode mode,\n \tadd_p_state_clear(&s);\n \treturn 0;\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+\tstruct strbuf parent_revision = STRBUF_INIT;\n+\tchar parent_tree_oid[GIT_MAX_HEXSZ + 1];\n+\tsize_t binary_count = 0;\n+\tstruct commit *commit;\n+\tint ret;\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+\tstrbuf_addf(&parent_revision, \"%s~\", revision);\n+\tmode.diff_cmd[0] = \"diff-tree\";\n+\tmode.diff_cmd[1] = \"-r\";\n+\tmode.diff_cmd[2] = parent_tree_oid;\n+\n+\tinteractive_config_init(&s.cfg, r, opts);\n+\n+\tif (parse_diff(&s, ps) < 0) {\n+\t\tret = -1;\n+\t\tgoto out;\n+\t}\n+\n+\tfor (size_t i = 0; i < s.file_diff_nr; i++) {\n+\t\tif (s.file_diff[i].binary && !s.file_diff[i].hunk_nr)\n+\t\t\tbinary_count++;\n+\t\telse if (patch_update_file(&s, s.file_diff + i))\n+\t\t\tbreak;\n+\t}\n+\n+\tif (s.file_diff_nr == 0) {\n+\t\terr(&s, _(\"No changes.\"));\n+\t\tret = -1;\n+\t\tgoto out;\n+\t}\n+\n+\tif (binary_count == s.file_diff_nr) {\n+\t\terr(&s, _(\"Only binary files changed.\"));\n+\t\tret = -1;\n+\t\tgoto out;\n+\t}\n+\n+\tret = 0;\n+\n+out:\n+\tstrbuf_release(&parent_revision);\n+\tadd_p_state_clear(&s);\n+\treturn ret;\n+}\ndiff --git a/add-patch.h b/add-patch.h\nindex 51c0d7bce9..d0edfec936 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@@ -50,4 +51,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.51.0.261.g7ce5a0a67e.dirty\n\n"},{"id":"524434","messageId":"20250819-b4-pks-history-builtin-v1-11-9b77c32688fe@pks.im","threadId":"63986","inReplyTo":"20250819-b4-pks-history-builtin-v1-0-9b77c32688fe@pks.im","subject":"[PATCH RFC 11/11] builtin/history: implement \"split\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-08-19T10:56:07Z","receivedAt":"2025-08-19T10:56:37Z","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 of it into a separate commit. This is quite an involved operation\nthough:\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 |  59 ++++++++\n builtin/history.c              | 245 +++++++++++++++++++++++++++++++++\n t/meson.build                  |   1 +\n t/t3452-history-split.sh       | 304 +++++++++++++++++++++++++++++++++++++++++\n 4 files changed, 609 insertions(+)\n\ndiff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc\nindex 6e8b4e1326..f0f1f2a093 100644\n--- a/Documentation/git-history.adoc\n+++ b/Documentation/git-history.adoc\n@@ -10,6 +10,7 @@ SYNOPSIS\n [synopsis]\n git history drop [<options>] <revision>\n git history reorder [<options>] <revision> --(before|after)=<revision>\n+git history split [<options>] <revision> [--] [<pathspec>...]\n \n DESCRIPTION\n -----------\n@@ -47,6 +48,26 @@ reorder <revision> (--before=<revision>|--after=<revision>)::\n \tcommit. The commits must be related to one another and must be\n \treachable from the current `HEAD` commit.\n \n+split <revision> [--message=<message>] [--] [<pathspec>...]::\n+\tInteractively split up the 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 message of the new commit will be asked for by launching the\n+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 about the _<pathspec>_ syntax,\n+see the 'pathspec' entry.\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 EXAMPLES\n --------\n \n@@ -88,6 +109,44 @@ f44a46e third\n bf7438d first\n ----------\n \n+* Split a commit.\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 --message=\"split-out commit\"\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,e,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,e,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+\n CONFIGURATION\n -------------\n \ndiff --git a/builtin/history.c b/builtin/history.c\nindex de6073f557..b2dab826ac 100644\n--- a/builtin/history.c\n+++ b/builtin/history.c\n@@ -1,16 +1,26 @@\n+/* Required for `comment_line_str`. */\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+#include \"editor.h\"\n #include \"environment.h\"\n #include \"gettext.h\"\n #include \"hex.h\"\n #include \"object-name.h\"\n #include \"parse-options.h\"\n+#include \"path.h\"\n+#include \"pathspec.h\"\n+#include \"read-cache-ll.h\"\n #include \"refs.h\"\n #include \"reset.h\"\n #include \"revision.h\"\n+#include \"run-command.h\"\n #include \"sequencer.h\"\n+#include \"sparse-index.h\"\n \n static int collect_commits(struct repository *repo,\n \t\t\t   struct commit *old_commit,\n@@ -424,6 +434,239 @@ static int cmd_history_reorder(int argc,\n \treturn ret;\n }\n \n+static int split_commit(struct repository *repo,\n+\t\t\tstruct commit *original_commit,\n+\t\t\tstruct pathspec *pathspec,\n+\t\t\tconst char *commit_message,\n+\t\t\tstruct object_id *out)\n+{\n+\tstruct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;\n+\tstruct strbuf index_file = STRBUF_INIT, split_message = STRBUF_INIT;\n+\tstruct child_process read_tree_cmd = CHILD_PROCESS_INIT;\n+\tstruct index_state index = INDEX_STATE_INIT(repo);\n+\tstruct object_id original_commit_tree_oid, parent_tree_oid;\n+\tconst char *original_message, *original_body, *ptr;\n+\tchar original_commit_oid[GIT_MAX_HEXSZ + 1];\n+\tchar *split_message_path = NULL, *original_author = NULL;\n+\tstruct commit_list *parents = NULL;\n+\tstruct commit *first_commit;\n+\tstruct tree *split_tree;\n+\tsize_t len;\n+\tint ret;\n+\n+\tif (original_commit->parents)\n+\t\tparent_tree_oid = *get_commit_tree_oid(original_commit->parents->item);\n+\telse\n+\t\toidcpy(&parent_tree_oid, repo->hash_algo->empty_tree);\n+\toriginal_commit_tree_oid = *get_commit_tree_oid(original_commit);\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+\tread_tree_cmd.git_cmd = 1;\n+\tstrvec_pushf(&read_tree_cmd.env, \"GIT_INDEX_FILE=%s\", index_file.buf);\n+\tstrvec_push(&read_tree_cmd.args, \"read-tree\");\n+\tstrvec_push(&read_tree_cmd.args, oid_to_hex(&parent_tree_oid));\n+\tret = run_command(&read_tree_cmd);\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_commit->object.oid);\n+\tret = run_add_p_index(repo, &index, index_file.buf, &interactive_opts,\n+\t\t\t      original_commit_oid, pathspec);\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+\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/* We retain authorship of the original commit. */\n+\toriginal_message = repo_logmsg_reencode(repo, original_commit, NULL, NULL);\n+\tptr = find_commit_header(original_message, \"author\", &len);\n+\tif (ptr)\n+\t\toriginal_author = xmemdupz(ptr, len);\n+\n+\t/*\n+\t * But we do ask the user for a new commit message. This is in contrast\n+\t * to the second commit, where we'll retain the original commit\n+\t * message.\n+\t */\n+\tif (!commit_message) {\n+\t\tsplit_message_path = repo_git_path(repo, \"SPLIT_MSG\");\n+\t\tstrbuf_addch(&split_message, '\\n');\n+\t\tstrbuf_commented_addf(&split_message, comment_line_str,\n+\t\t\t\t      _(\"Please enter a commit message for the split-out changes.\"));\n+\t\twrite_file_buf(split_message_path, split_message.buf, split_message.len);\n+\n+\t\tstrbuf_reset(&split_message);\n+\t\tif (launch_editor(split_message_path, &split_message, NULL)) {\n+\t\t\tfprintf(stderr, _(\"Please supply the message using either -m or -F option.\\n\"));\n+\t\t\tret = -1;\n+\t\t\tgoto out;\n+\t\t}\n+\t\tstrbuf_stripspace(&split_message, comment_line_str);\n+\t} else {\n+\t\tstrbuf_addstr(&split_message, commit_message);\n+\t}\n+\tcleanup_message(&split_message, COMMIT_MSG_CLEANUP_ALL, 0);\n+\n+\tret = commit_tree(split_message.buf, split_message.len, &split_tree->object.oid,\n+\t\t\t  original_commit->parents, &out[0], original_author, NULL);\n+\tif (ret < 0) {\n+\t\tret = error(_(\"failed writing split-out commit\"));\n+\t\tgoto out;\n+\t}\n+\n+\t/*\n+\t * The second commit is much simpler to construct, as we can simply use\n+\t * the original commit details, except that we adjust its parent to be\n+\t * the newly split-out commit.\n+\t */\n+\tfind_commit_subject(original_message, &original_body);\n+\tfirst_commit = lookup_commit_reference(repo, &out[0]);\n+\tcommit_list_append(first_commit, &parents);\n+\n+\tret = commit_tree(original_body, strlen(original_body), &original_commit_tree_oid,\n+\t\t\t  parents, &out[1], original_author, NULL);\n+\tif (ret < 0) {\n+\t\tret = error(_(\"failed writing second commit\"));\n+\t\tgoto out;\n+\t}\n+\n+\tret = 0;\n+\n+out:\n+\tif (index_file.len)\n+\t\tunlink(index_file.buf);\n+\tstrbuf_release(&split_message);\n+\tstrbuf_release(&index_file);\n+\tfree_commit_list(parents);\n+\tfree(split_message_path);\n+\tfree(original_author);\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\tN_(\"git history split [<options>] <revision>\"),\n+\t\tNULL,\n+\t};\n+\tconst char *commit_message = NULL;\n+\tstruct option options[] = {\n+\t\tOPT_STRING('m', \"message\", &commit_message, N_(\"message\"), N_(\"commit message\")),\n+\t\tOPT_END(),\n+\t};\n+\tstruct commit *original_commit, *head;\n+\tstruct strvec commits = STRVEC_INIT;\n+\tstruct commit_list *list = NULL;\n+\tstruct object_id split_commits[2];\n+\tstruct pathspec pathspec = { 0 };\n+\tint ret;\n+\n+\targc = parse_options(argc, argv, prefix, options, usage, 0);\n+\trepo_config(repo, git_default_config, NULL);\n+\n+\toriginal_commit = lookup_commit_reference_by_name(argv[0]);\n+\tif (!original_commit) {\n+\t\tret = error(_(\"commit to be split cannot be found: %s\"), argv[0]);\n+\t\tgoto out;\n+\t}\n+\n+\tif (original_commit->parents && original_commit->parents->next) {\n+\t\tret = error(_(\"commit to be split must not be a merge commit\"));\n+\t\tgoto out;\n+\t}\n+\n+\thead = lookup_commit_reference_by_name(\"HEAD\");\n+\tif (!head) {\n+\t\tret = error(_(\"could not resolve HEAD to a commit\"));\n+\t\tgoto out;\n+\t}\n+\n+\tcommit_list_append(original_commit, &list);\n+\tif (!repo_is_descendant_of(repo, original_commit, list)) {\n+\t\tret = error (_(\"split commit must be reachable from current HEAD commit\"));\n+\t\tgoto out;\n+\t}\n+\n+\tparse_pathspec(&pathspec, 0,\n+\t\t       PATHSPEC_PREFER_FULL | PATHSPEC_SYMLINK_LEADING_PATH | PATHSPEC_PREFIX_ORIGIN,\n+\t\t       prefix, argv + 1);\n+\n+\t/*\n+\t * Collect the list of commits that we'll have to reapply now already.\n+\t * This ensures that we'll abort early on in case the range of commits\n+\t * contains merges, which we do not yet handle.\n+\t */\n+\tret = collect_commits(repo, original_commit->parents ? original_commit->parents->item : NULL,\n+\t\t\t      head, &commits);\n+\tif (ret < 0)\n+\t\tgoto out;\n+\n+\t/*\n+\t * Then we split up the commit and replace the original commit with the\n+\t * new new ones.\n+\t */\n+\tret = split_commit(repo, original_commit, &pathspec,\n+\t\t\t   commit_message, split_commits);\n+\tif (ret < 0)\n+\t\tgoto out;\n+\n+\treplace_commits(&commits, &original_commit->object.oid,\n+\t\t\tsplit_commits, ARRAY_SIZE(split_commits));\n+\n+\t/*\n+\t * And now we pick commits in the new order on top of either the root\n+\t * commit or on top the old commit's parent.\n+\t */\n+\tret = apply_commits(repo, &commits, head,\n+\t\t\t    original_commit->parents ? original_commit->parents->item : NULL,\n+\t\t\t    \"split\");\n+\tif (ret < 0)\n+\t\tgoto out;\n+\n+\tret = 0;\n+\n+out:\n+\tclear_pathspec(&pathspec);\n+\tstrvec_clear(&commits);\n+\tfree_commit_list(list);\n+\treturn ret;\n+}\n+\n int cmd_history(int argc,\n \t\tconst char **argv,\n \t\tconst char *prefix,\n@@ -432,12 +675,14 @@ int cmd_history(int argc,\n \tconst char * const usage[] = {\n \t\tN_(\"git history drop [<options>] <revision>\"),\n \t\tN_(\"git history reorder [<options>] <revision> --(before|after)=<revision>\"),\n+\t\tN_(\"git history split [<options>] <revision> [--] [<pathspec>...]\"),\n \t\tNULL,\n \t};\n \tparse_opt_subcommand_fn *fn = NULL;\n \tstruct option options[] = {\n \t\tOPT_SUBCOMMAND(\"drop\", &fn, cmd_history_drop),\n \t\tOPT_SUBCOMMAND(\"reorder\", &fn, cmd_history_reorder),\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 8eded9ec1b..80beac8c1f 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -378,6 +378,7 @@ integration_tests = [\n   't3438-rebase-broken-files.sh',\n   't3450-history-drop.sh',\n   't3451-history-reorder.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..bc965b15b2\n--- /dev/null\n+++ b/t/t3452-history-split.sh\n@@ -0,0 +1,304 @@\n+#!/bin/sh\n+\n+test_description='tests for git-history split subcommand'\n+\n+. ./test-lib.sh\n+\n+set_fake_editor () {\n+\twrite_script fake-editor.sh <<-\\EOF &&\n+\techo \"split-out commit\" >\"$1\"\n+\tEOF\n+\ttest_set_editor \"$(pwd)\"/fake-editor.sh\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 \"commit to be split must not be a merge commit\" err &&\n+\t\ttest_must_fail git history split HEAD~ 2>err &&\n+\t\ttest_grep \"cannot rearrange commit history with merges\" err\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\tset_fake_editor &&\n+\t\tgit history split HEAD <<-EOF &&\n+\t\ty\n+\t\tn\n+\t\tEOF\n+\n+\t\texpect_log <<-EOF &&\n+\t\tsplit-me\n+\t\tsplit-out commit\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+\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 &&\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\troot\n+\t\tsplit-out commit\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 &&\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\tsplit-me\n+\t\tsplit-out commit\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 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\tgit history split HEAD -m \"split-out commit\" <<-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+\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\tgit history split HEAD -m \"split-out commit\" <<-EOF &&\n+\t\tn\n+\t\ty\n+\t\tEOF\n+\n+\t\texpect_log <<-EOF &&\n+\t\tsplit-me\n+\t\tsplit-out commit\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 specify message via option' '\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\tgit history split HEAD -m \"message option\" <<-EOF &&\n+\t\ty\n+\t\tn\n+\t\tEOF\n+\n+\t\texpect_log <<-EOF\n+\t\tsplit-me\n+\t\tmessage option\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\tgit history split HEAD -m \"message option\" -- 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 '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 '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_done\n\n-- \n2.51.0.261.g7ce5a0a67e.dirty\n\n"},{"id":"524491","messageId":"CALnO6CBDu14P8chvJ=1QfZ2apVg78E5P+d+P+4=8Whpu_EMi_g@mail.gmail.com","threadId":"63986","inReplyTo":"20250819-b4-pks-history-builtin-v1-0-9b77c32688fe@pks.im","subject":"Re: [PATCH RFC 00/11] Introduce git-history(1) command for easy history editing","fromName":"D. Ben Knoble","fromEmail":"ben.knoble@gmail.com","sentAt":"2025-08-19T21:28:48Z","receivedAt":"2025-08-19T21:29:01Z","isPatch":true,"sender":{"key":"ben.knoble@gmail.com","avatar":"https://avatars.githubusercontent.com/u/22802209?v=4"},"body":"On Tue, Aug 19, 2025 at 6:57 AM Patrick Steinhardt <ps@pks.im> wrote:\n>\n> Hi,\n>\n> over recent months I've been playing around with Jujutsu quite\n> frequently. While I still prefer using Git, there's been a couple\n> features in it that I really like and that I'd like to have in Git, as\n> well.\n\nExcellent! I'm looking forward to reading this series and playing with it.\n\nUnfortunately, patches 8–11 got dropped on their way to me (but I see\nthe lore archive has them). Odd. (Not in spam or deleted messages,\neither.)\n"},{"id":"524511","messageId":"aKVxIYBqA2nPTFYV@pks.im","threadId":"63986","inReplyTo":"CALnO6CBDu14P8chvJ=1QfZ2apVg78E5P+d+P+4=8Whpu_EMi_g@mail.gmail.com","subject":"Re: [PATCH RFC 00/11] Introduce git-history(1) command for easy history editing","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-08-20T06:54:33Z","receivedAt":"2025-08-20T06:54:45Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Tue, Aug 19, 2025 at 05:28:48PM -0400, D. Ben Knoble wrote:\n> On Tue, Aug 19, 2025 at 6:57 AM Patrick Steinhardt <ps@pks.im> wrote:\n> >\n> > Hi,\n> >\n> > over recent months I've been playing around with Jujutsu quite\n> > frequently. While I still prefer using Git, there's been a couple\n> > features in it that I really like and that I'd like to have in Git, as\n> > well.\n> \n> Excellent! I'm looking forward to reading this series and playing with it.\n> \n> Unfortunately, patches 8–11 got dropped on their way to me (but I see\n> the lore archive has them). Odd. (Not in spam or deleted messages,\n> either.)\n\nIt's a common issue with GMail that mails from the LKML get rate limited\nquite aggressively. Konstantin Ryabitsev (kernel.org admin) often gives\nthe recommendation to not use GMail for mailing lists.\n\nPatrick\n"},{"id":"524537","messageId":"EB8D70D8-A91C-49B3-956B-7FA14EA10D57@gmail.com","threadId":"63986","inReplyTo":"aKVxIYBqA2nPTFYV@pks.im","subject":"Re: [PATCH RFC 00/11] Introduce git-history(1) command for easy history editing","fromName":"Ben Knoble","fromEmail":"ben.knoble@gmail.com","sentAt":"2025-08-20T16:55:48Z","receivedAt":"2025-08-20T16:56:01Z","isPatch":true,"sender":{"key":"ben.knoble@gmail.com","avatar":"https://avatars.githubusercontent.com/u/22802209?v=4"},"body":"\n> Le 20 août 2025 à 03:56, Patrick Steinhardt <ps@pks.im> a écrit :\n> \n> ﻿On Tue, Aug 19, 2025 at 05:28:48PM -0400, D. Ben Knoble wrote:\n>>> On Tue, Aug 19, 2025 at 6:57 AM Patrick Steinhardt <ps@pks.im> wrote:\n>>> \n>>> Hi,\n>>> \n>>> over recent months I've been playing around with Jujutsu quite\n>>> frequently. While I still prefer using Git, there's been a couple\n>>> features in it that I really like and that I'd like to have in Git, as\n>>> well.\n>> \n>> Excellent! I'm looking forward to reading this series and playing with it.\n>> \n>> Unfortunately, patches 8–11 got dropped on their way to me (but I see\n>> the lore archive has them). Odd. (Not in spam or deleted messages,\n>> either.)\n> \n> It's a common issue with GMail that mails from the LKML get rate limited\n> quite aggressively. Konstantin Ryabitsev (kernel.org admin) often gives\n> the recommendation to not use GMail for mailing lists.\n> \n> Patrick\n\nIndeed I just saw a mastodon thread on that topic today. I’ll get switched over eventually, but it’s not a top priority for me and hasn’t hindered my side so far. My apologies for any stress it puts on the ML infra, though. "},{"id":"524540","messageId":"xmqqms7tao9o.fsf@gitster.g","threadId":"63986","inReplyTo":"20250819-b4-pks-history-builtin-v1-0-9b77c32688fe@pks.im","subject":"Re: [PATCH RFC 00/11] Introduce git-history(1) command for easy history editing","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-08-20T17:36:35Z","receivedAt":"2025-08-20T17:36:37Z","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> In the end, I'd like us to learn from what people like about Jujutsu and\n> apply those learnings to Git. We won't be able to apply all learnings\n> from Jujutsu, as the workflow is quite different there due to the lack\n> of the index. But other things we certainly can apply to Git directly.\n>\n> Note: This patch series currently builds on the cherry-pick infra.\n> As such, when one hits a merge conflict one needs to `git cherry-pick\n> --continue`, which is quite suboptimal. I didn't want to overpolish this\n> series before getting some feedback, but it is something I'll fix in\n> subsequent versions. Furthermore, the command for now bails out in the\n> case where there's any merge commits in the history that is being\n> rewritten. This is another restriction that can be lifted in the future.\n\nTwo comments.\n\n - You would want to honor notes.rewriteref yourself, as cherry-pick\n   does not and that is deliberate [*].\n\n - It is a sensible design decision to limit it to linear single\n   strand of pearls history.  \"history reword <commit>\" when\n   <commit> can be reached from many branches along linear history\n   that rewrites all these commits on these branches would be handy.\n   There may need some way to say \"these branches are protected, if\n   'history reword <commit>' needs to touch commits on any of these,\n   abort\" and things like that.\n\n\n[Footnote]\n\n * \"history edit\" (aka \"rebase\") is an operation that \"edits\" the\n   history, once the edit finishes, the result is *the* history you\n   want, and the previous one is to be discarded (except for in\n   reflog).  \"cherry-pick\" on the other hand is \"I have this good\n   thing on this development track, I want an equivalent _copy_ of\n   it on _another_ track\"---it merely is an easier and quicker way\n   than typing the same thing yourself on top of the other track,\n   and does not duplicate notes.\n"},{"id":"524541","messageId":"3600D877-4999-4EE3-8C1C-893E12D35B6A@gmail.com","threadId":"63986","inReplyTo":"xmqqms7tao9o.fsf@gitster.g","subject":"Re: [PATCH RFC 00/11] Introduce git-history(1) command for easy history editing","fromName":"Ben Knoble","fromEmail":"ben.knoble@gmail.com","sentAt":"2025-08-20T17:49:40Z","receivedAt":"2025-08-20T17:49:53Z","isPatch":true,"sender":{"key":"ben.knoble@gmail.com","avatar":"https://avatars.githubusercontent.com/u/22802209?v=4"},"body":"\n> Le 20 août 2025 à 13:39, Junio C Hamano <gitster@pobox.com> a écrit :\n> \n> ﻿Patrick Steinhardt <ps@pks.im> writes:\n> \n>> In the end, I'd like us to learn from what people like about Jujutsu and\n>> apply those learnings to Git. We won't be able to apply all learnings\n>> from Jujutsu, as the workflow is quite different there due to the lack\n>> of the index. But other things we certainly can apply to Git directly.\n>> \n>> Note: This patch series currently builds on the cherry-pick infra.\n>> As such, when one hits a merge conflict one needs to `git cherry-pick\n>> --continue`, which is quite suboptimal. I didn't want to overpolish this\n>> series before getting some feedback, but it is something I'll fix in\n>> subsequent versions. Furthermore, the command for now bails out in the\n>> case where there's any merge commits in the history that is being\n>> rewritten. This is another restriction that can be lifted in the future.\n> \n> Two comments.\n> \n> - You would want to honor notes.rewriteref yourself, as cherry-pick\n>   does not and that is deliberate [*].\n\nSeconded\n\n> - It is a sensible design decision to limit it to linear single\n>   strand of pearls history.  \"history reword <commit>\" when\n>   <commit> can be reached from many branches along linear history\n>   that rewrites all these commits on these branches would be handy.\n>   There may need some way to say \"these branches are protected, if\n>   'history reword <commit>' needs to touch commits on any of these,\n>   abort\" and things like that.\n\nThis reminds me of looking at rebase’s update-refs through a mirror, and I think is similar to what jj actually does. In particular, editing a commit that is reachable from multiple non-overlapping branches could update all of them.\n\nA reasonable heuristic for safety is “pushed,” but I would also want to be able to edit something in @{u}.. even if it’s in @{push} so that I can force-push a new version. I suspect jj might also have heuristics we can borrow.\n\nPS thanks all for being willing to borrow improvements! Reminds me of Neovim inspiring improvements in Vim, which I get to benefit from without switching :)"},{"id":"524557","messageId":"F0D09FB0-5978-4BFA-87D1-BE5FA49EA839@gmail.com","threadId":"63986","inReplyTo":"20250819-b4-pks-history-builtin-v1-5-9b77c32688fe@pks.im","subject":"Re: [PATCH RFC 05/11] builtin/history: implement \"drop\" subcommand","fromName":"Ben Knoble","fromEmail":"ben.knoble@gmail.com","sentAt":"2025-08-20T20:39:34Z","receivedAt":"2025-08-20T20:39:47Z","isPatch":true,"sender":{"key":"ben.knoble@gmail.com","avatar":"https://avatars.githubusercontent.com/u/22802209?v=4"},"body":"\n> Le 19 août 2025 à 06:57, Patrick Steinhardt <ps@pks.im> a écrit :\n> \n> ﻿It is a fairly common operation to perform an interactive rebase so that\n> one of the commits can be dropped from history.\n\n\n> diff --git a/builtin/history.c b/builtin/history.c\n> index d1a40368e0..183ab9d5f7 100644\n> --- a/builtin/history.c\n> +++ b/builtin/history.c\n> @@ -1,20 +1,311 @@\n> #include \"builtin.h\"\n> +#include \"commit.h\"\n> +#include \"commit-reach.h\"\n> +#include \"config.h\"\n> +#include \"environment.h\"\n> #include \"gettext.h\"\n> +#include \"hex.h\"\n> +#include \"object-name.h\"\n> #include \"parse-options.h\"\n> +#include \"refs.h\"\n> +#include \"reset.h\"\n> +#include \"revision.h\"\n> +#include \"sequencer.h\"\n> +\n> +static int collect_commits(struct repository *repo,\n> +               struct commit *old_commit,\n> +               struct commit *new_commit,\n> +               struct strvec *out)\n> +{\n> +    struct setup_revision_opt revision_opts = {\n> +        .assume_dashdash = 1,\n> +    };\n> +    struct strvec revisions = STRVEC_INIT;\n> +    struct commit_list *from_list = NULL;\n> +    struct commit *child;\n> +    struct rev_info rev = { 0 };\n> +    int ret;\n> +\n> +    /*\n> +     * Check that the old actually is an ancestor of HEAD. If not\n\nThe “old commit” perhaps?\n\n> +     * the whole request becomes nonsensical.\n> +    */\n> +    if (old_commit) {\n> +        commit_list_insert(old_commit, &from_list);\n> +        if (!repo_is_descendant_of(repo, new_commit, from_list)) {\n> +            ret = error(_(\"commit must be reachable from current HEAD commit\"));\n> +            goto out;\n> +        }\n> +    }\n> +\n> +    repo_init_revisions(repo, &rev, NULL);\n> +    strvec_push(&revisions, \"\");\n> +    strvec_push(&revisions, oid_to_hex(&new_commit->object.oid));\n> +    if (old_commit)\n> +        strvec_pushf(&revisions, \"^%s\", oid_to_hex(&old_commit->object.oid));\n> +    if (setup_revisions(revisions.nr, revisions.v, &rev, &revision_opts) != 1 ||\n> +        prepare_revision_walk(&rev)) {\n> +        ret = error(_(\"revision walk setup failed\"));\n> +        goto out;\n> +    }\n> +\n> +    while ((child = get_revision(&rev))) {\n> +        if (old_commit && !child->parents)\n> +            BUG(\"revision walk did not find child commit\");\n> +        if (child->parents && child->parents->next) {\n> +            ret = error(_(\"cannot rearrange commit history with merges\"));\n> +            goto out;\n> +        }\n> +\n> +        strvec_push(out, oid_to_hex(&child->object.oid));\n> +\n> +        if (child->parents && old_commit &&\n> +            commit_list_contains(old_commit, child->parents))\n> +            break;\n> +    }\n> +\n> +    /*\n> +     * Revisions are in newest-order-first. We have to reverse the\n> +     * array though so that we pick the oldest commits first. Note\n> +     * that we keep the first string untouched, as it is the\n> +     * equivalent of `argv[0]` to `setup_revisions()`.\n> +     */\n> +    for (size_t i = 0, j = out->nr - 1; i < j; i++, j--)\n> +        SWAP(out->v[i], out->v[j]);\n> +\n\nBut doesn’t this swap out->v[0] on first iteration? I only skimmed the code that built it up, but it doesn’t look the comment is right 🤔\n\nRest looked reasonable, but I don’t know the sequencer APIs very well. "},{"id":"524571","messageId":"CALnO6CAPugqDC8sGO1Si=5Wk-pz52BF84Ai7ZQjwb3vLMs6A=A@mail.gmail.com","threadId":"63986","inReplyTo":"20250819-b4-pks-history-builtin-v1-10-9b77c32688fe@pks.im","subject":"Re: [PATCH RFC 10/11] add-patch: add support for in-memory index patching","fromName":"D. Ben Knoble","fromEmail":"ben.knoble@gmail.com","sentAt":"2025-08-20T21:15:03Z","receivedAt":"2025-08-20T21:15:16Z","isPatch":true,"sender":{"key":"ben.knoble@gmail.com","avatar":"https://avatars.githubusercontent.com/u/22802209?v=4"},"body":"On Wed, Aug 20, 2025 at 4:17 AM Patrick Steinhardt <ps@pks.im> wrote:\n>\n> With `run_add_p()` callers have the ability to apply changes from a\n> specific revision to a repository's index. This infra supports several\n> different modes, like for example applying changes to the index,\n> worktree or both.\n>\n> One feature that is missing though is the ability to apply changes to an\n> in-memory index different from the repository's index. Add a new\n> function `run_add_p_index()` to plug this gap.\n>\n> This new function will be used in a subsequent commit.\n>\n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n>  add-patch.c | 111 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--\n>  add-patch.h |   8 +++++\n>  2 files changed, 116 insertions(+), 3 deletions(-)\n>\n> diff --git a/add-patch.c b/add-patch.c\n> index 1bcbc91de9..adef20c02b 100644\n> --- a/add-patch.c\n> +++ b/add-patch.c\n> @@ -1849,9 +1853,12 @@ static int patch_update_file(struct add_p_state *s,\n>                                          NULL, 0, NULL, 0))\n>                                 error(_(\"'git apply' failed\"));\n>                 }\n> -               if (repo_read_index(s->r) >= 0)\n> +               read_index_from(s->index, s->index_file, s->r->gitdir);\n> +               if (read_index_from(s->index, s->index_file, s->r->gitdir) >= 0 &&\n> +                   s->index == s->r->index) {\n>                         repo_refresh_and_write_index(s->r, REFRESH_QUIET, 0,\n>                                                      1, NULL, NULL, NULL);\n> +               }\n>         }\n\nIs this call to read_index_from duplicated? I don't see anything that\nindicates that would be desirable here.\n\n>\n>         putchar('\\n');\n> @@ -1864,6 +1871,8 @@ int run_add_p(struct repository *r, enum add_p_mode mode,\n>  {\n>         struct add_p_state s = {\n>                 .r = r,\n> +               .index = r->index,\n> +               .index_file = r->index_file,\n>                 .answer = STRBUF_INIT,\n>                 .buf = STRBUF_INIT,\n>                 .plain = STRBUF_INIT,\n> @@ -1922,3 +1931,99 @@ int run_add_p(struct repository *r, enum add_p_mode mode,\n>         add_p_state_clear(&s);\n>         return 0;\n>  }\n> +\n> +int run_add_p_index(struct repository *r,\n> +                   struct index_state *index,\n> +                   const char *index_file,\n> +                   struct interactive_options *opts,\n> +                   const char *revision,\n> +                   const struct pathspec *ps)\n> +{\n> +       struct patch_mode mode = {\n> +               .apply_args = { \"--cached\", NULL },\n> +               .apply_check_args = { \"--cached\", NULL },\n> +               .prompt_mode = {\n> +                       N_(\"Stage mode change [y,n,q,a,d%s,?]? \"),\n> +                       N_(\"Stage deletion [y,n,q,a,d%s,?]? \"),\n> +                       N_(\"Stage addition [y,n,q,a,d%s,?]? \"),\n> +                       N_(\"Stage this hunk [y,n,q,a,d%s,?]? \")\n> +               },\n> +               .edit_hunk_hint = N_(\"If the patch applies cleanly, the edited hunk \"\n> +                                    \"will immediately be marked for staging.\"),\n> +               .help_patch_text =\n> +                       N_(\"y - stage this hunk\\n\"\n> +                          \"n - do not stage this hunk\\n\"\n> +                          \"q - quit; do not stage this hunk or any of the remaining \"\n> +                               \"ones\\n\"\n> +                          \"a - stage this hunk and all later hunks in the file\\n\"\n> +                          \"d - do not stage this hunk or any of the later hunks in \"\n> +                               \"the file\\n\"),\n> +               .index_only = 1,\n> +       };\n> +       struct add_p_state s = {\n> +               .r = r,\n> +               .index = index,\n> +               .index_file = index_file,\n> +               .answer = STRBUF_INIT,\n> +               .buf = STRBUF_INIT,\n> +               .plain = STRBUF_INIT,\n> +               .colored = STRBUF_INIT,\n> +               .mode = &mode,\n> +               .revision = revision,\n> +       };\n> +       struct strbuf parent_revision = STRBUF_INIT;\n> +       char parent_tree_oid[GIT_MAX_HEXSZ + 1];\n> +       size_t binary_count = 0;\n> +       struct commit *commit;\n> +       int ret;\n> +\n> +       commit = lookup_commit_reference_by_name(revision);\n> +       if (!commit) {\n> +               err(&s, _(\"Revision does not refer to a commit\"));\n> +               ret = -1;\n> +               goto out;\n> +       }\n> +\n> +       if (commit->parents)\n> +               oid_to_hex_r(parent_tree_oid, get_commit_tree_oid(commit->parents->item));\n> +       else\n> +               oid_to_hex_r(parent_tree_oid, r->hash_algo->empty_tree);\n> +\n> +       strbuf_addf(&parent_revision, \"%s~\", revision);\n> +       mode.diff_cmd[0] = \"diff-tree\";\n> +       mode.diff_cmd[1] = \"-r\";\n> +       mode.diff_cmd[2] = parent_tree_oid;\n> +\n> +       interactive_config_init(&s.cfg, r, opts);\n> +\n> +       if (parse_diff(&s, ps) < 0) {\n\nI noticed run_add_p() calls discard_index() right before parse_diff()\n[but it also reads/refreshes the index there]. Sounds like that's not\nsomething we need for in-memory indices?\n\n> +               ret = -1;\n> +               goto out;\n> +       }\n> +\n> +       for (size_t i = 0; i < s.file_diff_nr; i++) {\n> +               if (s.file_diff[i].binary && !s.file_diff[i].hunk_nr)\n> +                       binary_count++;\n> +               else if (patch_update_file(&s, s.file_diff + i))\n> +                       break;\n> +       }\n> +\n> +       if (s.file_diff_nr == 0) {\n> +               err(&s, _(\"No changes.\"));\n> +               ret = -1;\n> +               goto out;\n> +       }\n> +\n> +       if (binary_count == s.file_diff_nr) {\n> +               err(&s, _(\"Only binary files changed.\"));\n> +               ret = -1;\n> +               goto out;\n> +       }\n> +\n> +       ret = 0;\n> +\n> +out:\n> +       strbuf_release(&parent_revision);\n> +       add_p_state_clear(&s);\n> +       return ret;\n> +}\n\nThis single call to add_p_state_clear() is probably easier to follow\nthan the original in run_add_p(). Nice.\n\n-- \nD. Ben Knoble\n"},{"id":"524576","messageId":"CALnO6CBuwDVMZ-QTay+PUiXKsWMsABJcs1pAB=uUXf7-DJ4Mnw@mail.gmail.com","threadId":"63986","inReplyTo":"20250819-b4-pks-history-builtin-v1-11-9b77c32688fe@pks.im","subject":"Re: [PATCH RFC 11/11] builtin/history: implement \"split\" subcommand","fromName":"D. Ben Knoble","fromEmail":"ben.knoble@gmail.com","sentAt":"2025-08-20T21:27:32Z","receivedAt":"2025-08-20T21:27:45Z","isPatch":true,"sender":{"key":"ben.knoble@gmail.com","avatar":"https://avatars.githubusercontent.com/u/22802209?v=4"},"body":"On Wed, Aug 20, 2025 at 5:05 AM Patrick Steinhardt <ps@pks.im> wrote:\n> diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc\n> index 6e8b4e1326..f0f1f2a093 100644\n> --- a/Documentation/git-history.adoc\n> +++ b/Documentation/git-history.adoc\n> @@ -47,6 +48,26 @@ reorder <revision> (--before=<revision>|--after=<revision>)::\n>         commit. The commits must be related to one another and must be\n>         reachable from the current `HEAD` commit.\n>\n> +split <revision> [--message=<message>] [--] [<pathspec>...]::\n> +       Interactively split up the 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 message of the new commit will be asked for by launching the\n> +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 about the _<pathspec>_ syntax,\n> +see the 'pathspec' entry.\n\nGlossary entry?\n\n> +       /*\n> +        * But we do ask the user for a new commit message. This is in contrast\n> +        * to the second commit, where we'll retain the original commit\n> +        * message.\n> +        */\n\nInteresting. I can see using the original as the template for _both_,\nor the first instead of the second. jj's split works a little\ndifferently (especially with their notion of descriptions), so I can't\nuse them as a reference for the behavior.\n\nI suppose this is one of those \"everybody has their preference\"\nthings, but I think giving the message in both new commits as the\ntemplate gives splitters the most information available when writing\nthe message. (Of course, in my editor, I can presumably do something\nlike \":Git show -s <split-commit-ish>\" if I want.)\n\n> +       if (!commit_message) {\n> +               split_message_path = repo_git_path(repo, \"SPLIT_MSG\");\n> +               strbuf_addch(&split_message, '\\n');\n> +               strbuf_commented_addf(&split_message, comment_line_str,\n> +                                     _(\"Please enter a commit message for the split-out changes.\"));\n> +               write_file_buf(split_message_path, split_message.buf, split_message.len);\n\nI also noticed the commented template differs substantially from the\nregular commit template, and my editor doesn't recognize \"SPLIT_MSG\"\nas a commit message file.\n\nThe latter can be fixed elsewhere, but for the former: perhaps it's\nworth using the usual template with the wording here prepended?\nRespecting commit.verbose / commit.status, too.\n\nBTW, if I quit the editor with an error here, I'm left back where I\nstarted. So I'd have to re-stage changes if I wanted to split again,\nwhich is a bit different from how interactive rebase will leave me\nwith the partially staged changes. Obviously that's harder to do with\nthe in-memory index + automatic re-application of remaining patch when\nfinished, so maybe a note in the docs about this being \"all or\nnothing\"?\n\nBest,\nD. Ben Knoble\n"},{"id":"524668","messageId":"87a53sr67i.fsf@osv.gnss.ru","threadId":"63986","inReplyTo":"xmqqms7tao9o.fsf@gitster.g","subject":"Re: [PATCH RFC 00/11] Introduce git-history(1) command for easy history editing","fromName":"Sergey Organov","fromEmail":"sorganov@gmail.com","sentAt":"2025-08-21T16:26:57Z","receivedAt":"2025-08-21T16:27:00Z","isPatch":true,"sender":{"key":"sorganov@gmail.com","avatar":"https://avatars.githubusercontent.com/u/8501568?v=4"},"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n> Patrick Steinhardt <ps@pks.im> writes:\n>\n>> In the end, I'd like us to learn from what people like about Jujutsu and\n>> apply those learnings to Git. We won't be able to apply all learnings\n>> from Jujutsu, as the workflow is quite different there due to the lack\n>> of the index. But other things we certainly can apply to Git directly.\n>>\n>> Note: This patch series currently builds on the cherry-pick infra.\n>> As such, when one hits a merge conflict one needs to `git cherry-pick\n>> --continue`, which is quite suboptimal. I didn't want to overpolish this\n>> series before getting some feedback, but it is something I'll fix in\n>> subsequent versions. Furthermore, the command for now bails out in the\n>> case where there's any merge commits in the history that is being\n>> rewritten. This is another restriction that can be lifted in the future.\n>\n> Two comments.\n>\n>  - You would want to honor notes.rewriteref yourself, as cherry-pick\n>    does not and that is deliberate [*].\n>\n>  - It is a sensible design decision to limit it to linear single\n>    strand of pearls history.  \"history reword <commit>\" when\n>    <commit> can be reached from many branches along linear history\n>    that rewrites all these commits on these branches would be handy.\n>    There may need some way to say \"these branches are protected, if\n>    'history reword <commit>' needs to touch commits on any of these,\n>    abort\" and things like that.\n>\n>\n> [Footnote]\n>\n>  * \"history edit\" (aka \"rebase\") is an operation that \"edits\" the\n>    history, once the edit finishes, the result is *the* history you\n>    want, and the previous one is to be discarded (except for in\n>    reflog).  \"cherry-pick\" on the other hand is \"I have this good\n>    thing on this development track, I want an equivalent _copy_ of\n>    it on _another_ track\"---it merely is an easier and quicker way\n>    than typing the same thing yourself on top of the other track,\n>    and does not duplicate notes.\n\nUnless I'm ignorant, \"git rebase\" (aka \"history edit\") lacks essential\nfeature though: in addition to saying: get \"this\" history and rebase it\n\"there\", one should be able to say: get \"that\" history, and rebase it\n\"here\" (aka cherry-pick on steroids), that also would eliminate the need\nfor 'git cherry-pick <range>' that (poorly) duplicates rebase\nfunctionality.\n\nIf not to implement this as, say, \"git rebase --pick\", I'd like to see\nthis feature in the new \"git history\" command, but then it'd eventually\nbecome yet another \"git rebase\"? Or will Git then aim to eventually\nfactor-out a lot of \"git rebase\" functionality into new \"git history\nrebase <what> <where>\", or something like this?\n\nThanks,\nSergey Organov\n"},{"id":"524670","messageId":"6EC11FA7-D80D-4DA9-A615-C3523967CF93@gmail.com","threadId":"63986","inReplyTo":"87a53sr67i.fsf@osv.gnss.ru","subject":"Re: [PATCH RFC 00/11] Introduce git-history(1) command for easy history editing","fromName":"Ben Knoble","fromEmail":"ben.knoble@gmail.com","sentAt":"2025-08-21T17:21:56Z","receivedAt":"2025-08-21T17:22:08Z","isPatch":true,"sender":{"key":"ben.knoble@gmail.com","avatar":"https://avatars.githubusercontent.com/u/22802209?v=4"},"body":"\n> Le 21 août 2025 à 12:51, Sergey Organov <sorganov@gmail.com> a écrit :\n> \n> ﻿Junio C Hamano <gitster@pobox.com> writes:\n> \n>> Patrick Steinhardt <ps@pks.im> writes:\n>> \n>>> In the end, I'd like us to learn from what people like about Jujutsu and\n>>> apply those learnings to Git. We won't be able to apply all learnings\n>>> from Jujutsu, as the workflow is quite different there due to the lack\n>>> of the index. But other things we certainly can apply to Git directly.\n>>> \n>>> Note: This patch series currently builds on the cherry-pick infra.\n>>> As such, when one hits a merge conflict one needs to `git cherry-pick\n>>> --continue`, which is quite suboptimal. I didn't want to overpolish this\n>>> series before getting some feedback, but it is something I'll fix in\n>>> subsequent versions. Furthermore, the command for now bails out in the\n>>> case where there's any merge commits in the history that is being\n>>> rewritten. This is another restriction that can be lifted in the future.\n>> \n>> Two comments.\n>> \n>> - You would want to honor notes.rewriteref yourself, as cherry-pick\n>>   does not and that is deliberate [*].\n>> \n>> - It is a sensible design decision to limit it to linear single\n>>   strand of pearls history.  \"history reword <commit>\" when\n>>   <commit> can be reached from many branches along linear history\n>>   that rewrites all these commits on these branches would be handy.\n>>   There may need some way to say \"these branches are protected, if\n>>   'history reword <commit>' needs to touch commits on any of these,\n>>   abort\" and things like that.\n>> \n>> \n>> [Footnote]\n>> \n>> * \"history edit\" (aka \"rebase\") is an operation that \"edits\" the\n>>   history, once the edit finishes, the result is *the* history you\n>>   want, and the previous one is to be discarded (except for in\n>>   reflog).  \"cherry-pick\" on the other hand is \"I have this good\n>>   thing on this development track, I want an equivalent _copy_ of\n>>   it on _another_ track\"---it merely is an easier and quicker way\n>>   than typing the same thing yourself on top of the other track,\n>>   and does not duplicate notes.\n> \n> Unless I'm ignorant, \"git rebase\" (aka \"history edit\") lacks essential\n> feature though: in addition to saying: get \"this\" history and rebase it\n> \"there\", one should be able to say: get \"that\" history, and rebase it\n> \"here\" (aka cherry-pick on steroids), that also would eliminate the need\n> for 'git cherry-pick <range>' that (poorly) duplicates rebase\n> functionality.\n\nBut isn’t that\n\n    git rebase --onto=<here> <that-upstream> <that>\n\n? And why is that cherry-picking a range is a poor substitute [it is rather rebase that duplicates cherry-pick ;)]?"},{"id":"524673","messageId":"871pp4r16l.fsf@osv.gnss.ru","threadId":"63986","inReplyTo":"6EC11FA7-D80D-4DA9-A615-C3523967CF93@gmail.com","subject":"Re: [PATCH RFC 00/11] Introduce git-history(1) command for easy history editing","fromName":"Sergey Organov","fromEmail":"sorganov@gmail.com","sentAt":"2025-08-21T18:15:30Z","receivedAt":"2025-08-21T18:15:33Z","isPatch":true,"sender":{"key":"sorganov@gmail.com","avatar":"https://avatars.githubusercontent.com/u/8501568?v=4"},"body":"Ben Knoble <ben.knoble@gmail.com> writes:\n\n>> Le 21 août 2025 à 12:51, Sergey Organov <sorganov@gmail.com> a écrit :\n>> \n>> ﻿Junio C Hamano <gitster@pobox.com> writes:\n>> \n>>> Patrick Steinhardt <ps@pks.im> writes:\n>>> \n>>>> In the end, I'd like us to learn from what people like about Jujutsu and\n>>>> apply those learnings to Git. We won't be able to apply all learnings\n>>>> from Jujutsu, as the workflow is quite different there due to the lack\n>>>> of the index. But other things we certainly can apply to Git directly.\n>>>> \n>>>> Note: This patch series currently builds on the cherry-pick infra.\n>>>> As such, when one hits a merge conflict one needs to `git cherry-pick\n>>>> --continue`, which is quite suboptimal. I didn't want to overpolish this\n>>>> series before getting some feedback, but it is something I'll fix in\n>>>> subsequent versions. Furthermore, the command for now bails out in the\n>>>> case where there's any merge commits in the history that is being\n>>>> rewritten. This is another restriction that can be lifted in the future.\n>>> \n>>> Two comments.\n>>> \n>>> - You would want to honor notes.rewriteref yourself, as cherry-pick\n>>>   does not and that is deliberate [*].\n>>> \n>>> - It is a sensible design decision to limit it to linear single\n>>>   strand of pearls history.  \"history reword <commit>\" when\n>>>   <commit> can be reached from many branches along linear history\n>>>   that rewrites all these commits on these branches would be handy.\n>>>   There may need some way to say \"these branches are protected, if\n>>>   'history reword <commit>' needs to touch commits on any of these,\n>>>   abort\" and things like that.\n>>> \n>>> \n>>> [Footnote]\n>>> \n>>> * \"history edit\" (aka \"rebase\") is an operation that \"edits\" the\n>>>   history, once the edit finishes, the result is *the* history you\n>>>   want, and the previous one is to be discarded (except for in\n>>>   reflog).  \"cherry-pick\" on the other hand is \"I have this good\n>>>   thing on this development track, I want an equivalent _copy_ of\n>>>   it on _another_ track\"---it merely is an easier and quicker way\n>>>   than typing the same thing yourself on top of the other track,\n>>>   and does not duplicate notes.\n>> \n>> Unless I'm ignorant, \"git rebase\" (aka \"history edit\") lacks essential\n>> feature though: in addition to saying: get \"this\" history and rebase it\n>> \"there\", one should be able to say: get \"that\" history, and rebase it\n>> \"here\" (aka cherry-pick on steroids), that also would eliminate the need\n>> for 'git cherry-pick <range>' that (poorly) duplicates rebase\n>> functionality.\n>\n> But isn’t that\n>\n>     git rebase --onto=<here> <that-upstream> <that>\n\nCause we end-up being on modified <that> (unless my memory fails me)\nrather than on modified <here> when intent is to keep <that> intact.\n\n> ? And why is that cherry-picking a range is a poor substitute [it is\n> rather rebase that duplicates cherry-pick ;)]?\n\nBecause rebase has a lot of useful features compared to cherry-pick,\nsuch as --interactive, --exec, merges handling, etc. In fact, IMHO,\n\"cherry-pick\" should better be Git plumbing, and every-day-user-needs\nshould be handled by \"rebase\".\n\nHistorically, when ranges support was added to \"cherry-pick\", it was\nprobably a mistake, as \"rebase\" appears to be a better place for needed\nfunctionality, and what was needed is rather adding \"--pick\" support to\n\"rebase\".\n\n-- \nSergey Organov\n"},{"id":"524707","messageId":"aKhg0NBb1usxw-dq@pks.im","threadId":"63986","inReplyTo":"3600D877-4999-4EE3-8C1C-893E12D35B6A@gmail.com","subject":"Re: [PATCH RFC 00/11] Introduce git-history(1) command for easy history editing","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-08-22T12:21:36Z","receivedAt":"2025-08-22T12:21:46Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Wed, Aug 20, 2025 at 01:49:40PM -0400, Ben Knoble wrote:\n> \n> > Le 20 août 2025 à 13:39, Junio C Hamano <gitster@pobox.com> a écrit :\n> > \n> > ﻿Patrick Steinhardt <ps@pks.im> writes:\n> > \n> >> In the end, I'd like us to learn from what people like about Jujutsu and\n> >> apply those learnings to Git. We won't be able to apply all learnings\n> >> from Jujutsu, as the workflow is quite different there due to the lack\n> >> of the index. But other things we certainly can apply to Git directly.\n> >> \n> >> Note: This patch series currently builds on the cherry-pick infra.\n> >> As such, when one hits a merge conflict one needs to `git cherry-pick\n> >> --continue`, which is quite suboptimal. I didn't want to overpolish this\n> >> series before getting some feedback, but it is something I'll fix in\n> >> subsequent versions. Furthermore, the command for now bails out in the\n> >> case where there's any merge commits in the history that is being\n> >> rewritten. This is another restriction that can be lifted in the future.\n> > \n> > Two comments.\n> > \n> > - You would want to honor notes.rewriteref yourself, as cherry-pick\n> >   does not and that is deliberate [*].\n> \n> Seconded\n\nOkay, I'll have a look at that. I'll probably not do that in v2 yet, but\nwill try to get it into v3.\n\n> > - It is a sensible design decision to limit it to linear single\n> >   strand of pearls history.  \"history reword <commit>\" when\n> >   <commit> can be reached from many branches along linear history\n> >   that rewrites all these commits on these branches would be handy.\n> >   There may need some way to say \"these branches are protected, if\n> >   'history reword <commit>' needs to touch commits on any of these,\n> >   abort\" and things like that.\n> \n> This reminds me of looking at rebase’s update-refs through a mirror,\n> and I think is similar to what jj actually does. In particular,\n> editing a commit that is reachable from multiple non-overlapping\n> branches could update all of them.\n> \n> A reasonable heuristic for safety is “pushed,” but I would also want\n> to be able to edit something in @{u}.. even if it’s in @{push} so that\n> I can force-push a new version. I suspect jj might also have\n> heuristics we can borrow.\n> \n> PS thanks all for being willing to borrow improvements! Reminds me of\n> Neovim inspiring improvements in Vim, which I get to benefit from\n> without switching :)\n\nAt least initially I want to keep it simple, just to get this in at\nfirst. So I'm trying to be quite defensive overall and die in all kinds\nof situations that require more thought. That way it becomes way easier\nto eventually extend the different subcommands to maybe lift some of the\ncurrent restrictions.\n\nPatrick\n"},{"id":"524708","messageId":"aKhg2WxKC_1fVJZW@pks.im","threadId":"63986","inReplyTo":"F0D09FB0-5978-4BFA-87D1-BE5FA49EA839@gmail.com","subject":"Re: [PATCH RFC 05/11] builtin/history: implement \"drop\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-08-22T12:21:45Z","receivedAt":"2025-08-22T12:21:54Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Wed, Aug 20, 2025 at 04:39:34PM -0400, Ben Knoble wrote:\n> > diff --git a/builtin/history.c b/builtin/history.c\n> > index d1a40368e0..183ab9d5f7 100644\n> > --- a/builtin/history.c\n> > +++ b/builtin/history.c\n> > @@ -1,20 +1,311 @@\n> > #include \"builtin.h\"\n> > +#include \"commit.h\"\n> > +#include \"commit-reach.h\"\n> > +#include \"config.h\"\n> > +#include \"environment.h\"\n> > #include \"gettext.h\"\n> > +#include \"hex.h\"\n> > +#include \"object-name.h\"\n> > #include \"parse-options.h\"\n> > +#include \"refs.h\"\n> > +#include \"reset.h\"\n> > +#include \"revision.h\"\n> > +#include \"sequencer.h\"\n> > +\n> > +static int collect_commits(struct repository *repo,\n> > +               struct commit *old_commit,\n> > +               struct commit *new_commit,\n> > +               struct strvec *out)\n> > +{\n> > +    struct setup_revision_opt revision_opts = {\n> > +        .assume_dashdash = 1,\n> > +    };\n> > +    struct strvec revisions = STRVEC_INIT;\n> > +    struct commit_list *from_list = NULL;\n> > +    struct commit *child;\n> > +    struct rev_info rev = { 0 };\n> > +    int ret;\n> > +\n> > +    /*\n> > +     * Check that the old actually is an ancestor of HEAD. If not\n> \n> The “old commit” perhaps?\n\nYup, indeed.\n\n> > +     * the whole request becomes nonsensical.\n> > +    */\n> > +    if (old_commit) {\n> > +        commit_list_insert(old_commit, &from_list);\n> > +        if (!repo_is_descendant_of(repo, new_commit, from_list)) {\n> > +            ret = error(_(\"commit must be reachable from current HEAD commit\"));\n> > +            goto out;\n> > +        }\n> > +    }\n> > +\n> > +    repo_init_revisions(repo, &rev, NULL);\n> > +    strvec_push(&revisions, \"\");\n> > +    strvec_push(&revisions, oid_to_hex(&new_commit->object.oid));\n> > +    if (old_commit)\n> > +        strvec_pushf(&revisions, \"^%s\", oid_to_hex(&old_commit->object.oid));\n> > +    if (setup_revisions(revisions.nr, revisions.v, &rev, &revision_opts) != 1 ||\n> > +        prepare_revision_walk(&rev)) {\n> > +        ret = error(_(\"revision walk setup failed\"));\n> > +        goto out;\n> > +    }\n> > +\n> > +    while ((child = get_revision(&rev))) {\n> > +        if (old_commit && !child->parents)\n> > +            BUG(\"revision walk did not find child commit\");\n> > +        if (child->parents && child->parents->next) {\n> > +            ret = error(_(\"cannot rearrange commit history with merges\"));\n> > +            goto out;\n> > +        }\n> > +\n> > +        strvec_push(out, oid_to_hex(&child->object.oid));\n> > +\n> > +        if (child->parents && old_commit &&\n> > +            commit_list_contains(old_commit, child->parents))\n> > +            break;\n> > +    }\n> > +\n> > +    /*\n> > +     * Revisions are in newest-order-first. We have to reverse the\n> > +     * array though so that we pick the oldest commits first. Note\n> > +     * that we keep the first string untouched, as it is the\n> > +     * equivalent of `argv[0]` to `setup_revisions()`.\n> > +     */\n> > +    for (size_t i = 0, j = out->nr - 1; i < j; i++, j--)\n> > +        SWAP(out->v[i], out->v[j]);\n> > +\n> \n> But doesn’t this swap out->v[0] on first iteration? I only skimmed the\n> code that built it up, but it doesn’t look the comment is right 🤔\n\nVery true. This comment is indeed stale from a previous iteration, where\n`out->v[0]` was indeed `argv[0]`. Will fix.\n\n> Rest looked reasonable, but I don’t know the sequencer APIs very well.\n\nThe sequencer API is quite complex overall, I cannot blame you :) Took\nme quite a while to get the hang of it.\n\nPatrick\n"},{"id":"524709","messageId":"aKhg5o-dwp2UDZ4y@pks.im","threadId":"63986","inReplyTo":"CALnO6CAPugqDC8sGO1Si=5Wk-pz52BF84Ai7ZQjwb3vLMs6A=A@mail.gmail.com","subject":"Re: [PATCH RFC 10/11] add-patch: add support for in-memory index patching","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-08-22T12:21:58Z","receivedAt":"2025-08-22T12:22:09Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Wed, Aug 20, 2025 at 05:15:03PM -0400, D. Ben Knoble wrote:\n> On Wed, Aug 20, 2025 at 4:17 AM Patrick Steinhardt <ps@pks.im> wrote:\n> > diff --git a/add-patch.c b/add-patch.c\n> > index 1bcbc91de9..adef20c02b 100644\n> > --- a/add-patch.c\n> > +++ b/add-patch.c\n> > @@ -1849,9 +1853,12 @@ static int patch_update_file(struct add_p_state *s,\n> >                                          NULL, 0, NULL, 0))\n> >                                 error(_(\"'git apply' failed\"));\n> >                 }\n> > -               if (repo_read_index(s->r) >= 0)\n> > +               read_index_from(s->index, s->index_file, s->r->gitdir);\n> > +               if (read_index_from(s->index, s->index_file, s->r->gitdir) >= 0 &&\n> > +                   s->index == s->r->index) {\n> >                         repo_refresh_and_write_index(s->r, REFRESH_QUIET, 0,\n> >                                                      1, NULL, NULL, NULL);\n> > +               }\n> >         }\n> \n> Is this call to read_index_from duplicated? I don't see anything that\n> indicates that would be desirable here.\n\nIndeed, good catch!\n\n> > @@ -1922,3 +1931,99 @@ int run_add_p(struct repository *r, enum add_p_mode mode,\n> >         add_p_state_clear(&s);\n> >         return 0;\n> >  }\n> > +\n> > +int run_add_p_index(struct repository *r,\n> > +                   struct index_state *index,\n> > +                   const char *index_file,\n> > +                   struct interactive_options *opts,\n> > +                   const char *revision,\n> > +                   const struct pathspec *ps)\n> > +{\n> > +       struct patch_mode mode = {\n> > +               .apply_args = { \"--cached\", NULL },\n> > +               .apply_check_args = { \"--cached\", NULL },\n> > +               .prompt_mode = {\n> > +                       N_(\"Stage mode change [y,n,q,a,d%s,?]? \"),\n> > +                       N_(\"Stage deletion [y,n,q,a,d%s,?]? \"),\n> > +                       N_(\"Stage addition [y,n,q,a,d%s,?]? \"),\n> > +                       N_(\"Stage this hunk [y,n,q,a,d%s,?]? \")\n> > +               },\n> > +               .edit_hunk_hint = N_(\"If the patch applies cleanly, the edited hunk \"\n> > +                                    \"will immediately be marked for staging.\"),\n> > +               .help_patch_text =\n> > +                       N_(\"y - stage this hunk\\n\"\n> > +                          \"n - do not stage this hunk\\n\"\n> > +                          \"q - quit; do not stage this hunk or any of the remaining \"\n> > +                               \"ones\\n\"\n> > +                          \"a - stage this hunk and all later hunks in the file\\n\"\n> > +                          \"d - do not stage this hunk or any of the later hunks in \"\n> > +                               \"the file\\n\"),\n> > +               .index_only = 1,\n> > +       };\n> > +       struct add_p_state s = {\n> > +               .r = r,\n> > +               .index = index,\n> > +               .index_file = index_file,\n> > +               .answer = STRBUF_INIT,\n> > +               .buf = STRBUF_INIT,\n> > +               .plain = STRBUF_INIT,\n> > +               .colored = STRBUF_INIT,\n> > +               .mode = &mode,\n> > +               .revision = revision,\n> > +       };\n> > +       struct strbuf parent_revision = STRBUF_INIT;\n> > +       char parent_tree_oid[GIT_MAX_HEXSZ + 1];\n> > +       size_t binary_count = 0;\n> > +       struct commit *commit;\n> > +       int ret;\n> > +\n> > +       commit = lookup_commit_reference_by_name(revision);\n> > +       if (!commit) {\n> > +               err(&s, _(\"Revision does not refer to a commit\"));\n> > +               ret = -1;\n> > +               goto out;\n> > +       }\n> > +\n> > +       if (commit->parents)\n> > +               oid_to_hex_r(parent_tree_oid, get_commit_tree_oid(commit->parents->item));\n> > +       else\n> > +               oid_to_hex_r(parent_tree_oid, r->hash_algo->empty_tree);\n> > +\n> > +       strbuf_addf(&parent_revision, \"%s~\", revision);\n> > +       mode.diff_cmd[0] = \"diff-tree\";\n> > +       mode.diff_cmd[1] = \"-r\";\n> > +       mode.diff_cmd[2] = parent_tree_oid;\n> > +\n> > +       interactive_config_init(&s.cfg, r, opts);\n> > +\n> > +       if (parse_diff(&s, ps) < 0) {\n> \n> I noticed run_add_p() calls discard_index() right before parse_diff()\n> [but it also reads/refreshes the index there]. Sounds like that's not\n> something we need for in-memory indices?\n\nNo. We don't want to discard contents, as it might even be that somebody\nhas an index that we want to apply multiple revisions to. Discarding it\nis not sensible in that case.\n\nPatrick\n"},{"id":"524710","messageId":"aKhg8q-AAlsGDvFS@pks.im","threadId":"63986","inReplyTo":"CALnO6CBuwDVMZ-QTay+PUiXKsWMsABJcs1pAB=uUXf7-DJ4Mnw@mail.gmail.com","subject":"Re: [PATCH RFC 11/11] builtin/history: implement \"split\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-08-22T12:22:10Z","receivedAt":"2025-08-22T12:22:19Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Wed, Aug 20, 2025 at 05:27:32PM -0400, D. Ben Knoble wrote:\n> On Wed, Aug 20, 2025 at 5:05 AM Patrick Steinhardt <ps@pks.im> wrote:\n> > diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc\n> > index 6e8b4e1326..f0f1f2a093 100644\n> > --- a/Documentation/git-history.adoc\n> > +++ b/Documentation/git-history.adoc\n> > @@ -47,6 +48,26 @@ reorder <revision> (--before=<revision>|--after=<revision>)::\n> >         commit. The commits must be related to one another and must be\n> >         reachable from the current `HEAD` commit.\n> >\n> > +split <revision> [--message=<message>] [--] [<pathspec>...]::\n> > +       Interactively split up the 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 message of the new commit will be asked for by launching the\n> > +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 about the _<pathspec>_ syntax,\n> > +see the 'pathspec' entry.\n> \n> Glossary entry?\n\nYup.\n\n> > +       /*\n> > +        * But we do ask the user for a new commit message. This is in contrast\n> > +        * to the second commit, where we'll retain the original commit\n> > +        * message.\n> > +        */\n> \n> Interesting. I can see using the original as the template for _both_,\n> or the first instead of the second. jj's split works a little\n> differently (especially with their notion of descriptions), so I can't\n> use them as a reference for the behavior.\n> \n> I suppose this is one of those \"everybody has their preference\"\n> things, but I think giving the message in both new commits as the\n> template gives splitters the most information available when writing\n> the message. (Of course, in my editor, I can presumably do something\n> like \":Git show -s <split-commit-ish>\" if I want.)\n\nI think giving only the split-out changes is a reasonable default, but I\ncan totally see that we might eventually want to add a command line\noption to change the behaviour.\n\n> > +       if (!commit_message) {\n> > +               split_message_path = repo_git_path(repo, \"SPLIT_MSG\");\n> > +               strbuf_addch(&split_message, '\\n');\n> > +               strbuf_commented_addf(&split_message, comment_line_str,\n> > +                                     _(\"Please enter a commit message for the split-out changes.\"));\n> > +               write_file_buf(split_message_path, split_message.buf, split_message.len);\n> \n> I also noticed the commented template differs substantially from the\n> regular commit template, and my editor doesn't recognize \"SPLIT_MSG\"\n> as a commit message file.\n> \n> The latter can be fixed elsewhere, but for the former: perhaps it's\n> worth using the usual template with the wording here prepended?\n> Respecting commit.verbose / commit.status, too.\n\nYeah, that's something I wanted to get around to, but haven't yet. I\nalso noticed that it's not exactly easy to figure out what you're\ncurrently editing without that lack of context.\n\nI'll include that in v2.\n\n> BTW, if I quit the editor with an error here, I'm left back where I\n> started. So I'd have to re-stage changes if I wanted to split again,\n> which is a bit different from how interactive rebase will leave me\n> with the partially staged changes. Obviously that's harder to do with\n> the in-memory index + automatic re-application of remaining patch when\n> finished, so maybe a note in the docs about this being \"all or\n> nothing\"?\n\nYeah, fair. I guess adding a note for now is the best way to go about\nit, but this is certainly something we can and should iterate on in the\nfuture.\n\nPatrick\n"},{"id":"524722","messageId":"xmqqy0rbw84c.fsf@gitster.g","threadId":"63986","inReplyTo":"aKhg0NBb1usxw-dq@pks.im","subject":"Re: [PATCH RFC 00/11] Introduce git-history(1) command for easy history editing","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-08-22T17:58:59Z","receivedAt":"2025-08-22T17:59:02Z","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> At least initially I want to keep it simple, just to get this in at\n> first. So I'm trying to be quite defensive overall and die in all kinds\n> of situations that require more thought. That way it becomes way easier\n> to eventually extend the different subcommands to maybe lift some of the\n> current restrictions.\n\nI like that approach very much.  Thanks.\n"},{"id":"524724","messageId":"xmqqplcnw7op.fsf@gitster.g","threadId":"63986","inReplyTo":"aKhg8q-AAlsGDvFS@pks.im","subject":"Re: [PATCH RFC 11/11] builtin/history: implement \"split\" subcommand","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-08-22T18:08:22Z","receivedAt":"2025-08-22T18:08:24Z","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>> Interesting. I can see using the original as the template for _both_,\n>> or the first instead of the second. jj's split works a little\n>> differently (especially with their notion of descriptions), so I can't\n>> use them as a reference for the behavior.\n>> \n>> I suppose this is one of those \"everybody has their preference\"\n>> things, but I think giving the message in both new commits as the\n>> template gives splitters the most information available when writing\n>> the message. (Of course, in my editor, I can presumably do something\n>> like \":Git show -s <split-commit-ish>\" if I want.)\n\nIn other words, removing is easy, while remembering and retyping is\nharder.\n\nWhen I split an existing commit, that is almost always because after\ndoing too many things in a single commit and the time I realize it\nis when I am writing the commit message.  So I would suggest to give\nthe same original message to both, to avoid losing information.\n\n"},{"id":"524784","messageId":"3005363.e9J7NaK4W3@cayenne","threadId":"63986","inReplyTo":"20250819-b4-pks-history-builtin-v1-5-9b77c32688fe@pks.im","subject":"Re: [PATCH RFC 05/11] builtin/history: implement \"drop\" subcommand","fromName":"Jean-Noël AVILA","fromEmail":"jn.avila@free.fr","sentAt":"2025-08-23T16:15:08Z","receivedAt":"2025-08-23T16:15:24Z","isPatch":true,"sender":{"key":"jn.avila@free.fr","avatar":"https://avatars.githubusercontent.com/u/156172?v=4"},"body":"On Tuesday, 19 August 2025 12:56:01 CEST Patrick Steinhardt wrote:\n> It is a fairly common operation to perform an interactive rebase so that\n> one of the commits can be dropped from history. Doing this is not very\n> hard in general, but still requires the user to perform multiple steps:\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. Edit the instruction sheet to drop that commit.\n> \n> This is needlessly complex for such a supposedly-trivial operation.\n> Furthermore, the second step doesn't account for certain edge cases like\n> for example dropping the root commit.\n> \n> Introduce a new \"drop\" subcommand to make this use case significantly\n> simpler: all the user needs to do is to say `git history drop $COMMIT`\n> and they're done.\n> \n> Note that for now, this command only allows users to drop a single\n> commit at once. It should be easy enough though to expand the command at\n> a later point in time to support dropping whole commit ranges.\n> \n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n>  Documentation/git-history.adoc |  27 +++-\n>  builtin/history.c              | 297 ++++++++++++++++++++++++++++++++++++++\n++-\n>  t/meson.build                  |   3 +-\n>  t/t3450-history-drop.sh        | 127 ++++++++++++++++++\n>  4 files changed, 449 insertions(+), 5 deletions(-)\n> \n> diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc\n> index 9dafb8fc16..3012445ddc 100644\n> --- a/Documentation/git-history.adoc\n> +++ b/Documentation/git-history.adoc\n> @@ -8,7 +8,7 @@ git-history - Rewrite history of the current branch\n>  SYNOPSIS\n>  --------\n>  [synopsis]\n> -git history [<options>]\n> +git history drop [<options>] <revision>\n> \n\nGrepping through the documentation for the <revision> placeholder does not \nyield a lot of matches. Can <revision> be replaced by <commit> or <commit-ish>  \nin this context; these ones seem widely used.\n\n\n>  DESCRIPTION\n>  -----------\n> @@ -31,6 +31,31 @@ COMMANDS\n>  This command requires a subcommand. Several subcommands are available to\n>  rewrite history in different ways.\n> \n> +drop <revision>::\n\nMy linting patch series[1] does not catch this kind of synopsis miss, but \nhere, backticks are missing because this is a part of synopsis:\n\n`drop <revision>`::\n\n\n> +\tDrop a commit from the history and reapply all children of that\n> +\tcommit on top of the commit's parent. The commit that is to be\n> +\tdropped must be reachable from the current `HEAD` commit.\n> ++\n> +Dropping the root commit converts the child of that commit into the new\n> +root commit. It is invalid to drop a root commit that does not have any\n> +child commits, as that would lead to an empty branch.\n> +\n> +EXAMPLES\n> +--------\n> +\n> +* Drop a commit from history.\n> ++\n\nAs the examples are quite long, it would make sense to declare each example as \na sub-section:\n\nDrop a commit from history\n~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n> +----------\n> +$ git log --oneline\n> +2d4cd6d third\n> +125a0f3 second\n> +e098c27 first\n> +$ git history drop HEAD~\n> +$ git log\n> +b1bc1bd third\n> +e098c27 first\n> +----------\n> +\n>  CONFIGURATION\n>  -------------\n> \n\n\n\n"},{"id":"524785","messageId":"2320291.iZASKD2KPV@cayenne","threadId":"63986","inReplyTo":"20250819-b4-pks-history-builtin-v1-6-9b77c32688fe@pks.im","subject":"Re: [PATCH RFC 06/11] builtin/history: implement \"reorder\" subcommand","fromName":"Jean-Noël AVILA","fromEmail":"jn.avila@free.fr","sentAt":"2025-08-23T16:24:07Z","receivedAt":"2025-08-23T16:24:09Z","isPatch":true,"sender":{"key":"jn.avila@free.fr","avatar":"https://avatars.githubusercontent.com/u/156172?v=4"},"body":"On Tuesday, 19 August 2025 12:56:02 CEST Patrick Steinhardt wrote:\n> When working in projects where having nice commits matters it's quite\n> common that developers end up reordering commits a lot. Tihs is\n> typically done via interactive rebases, where they can then rearrange\n> commits in the instruction sheet.\n> \n> Still, this operation is a frequent-enough operation to provide a more\n> direct of doing this imperatively. As such, introduce a new \"reorder\"\n> subcommand where users can reorder a commit A to come after or before\n> another commit B:\n> \n>     $ git log --oneline\n>     a978f73 fifth\n>     57594ee fourth\n>     04eb1c4 third\n>     d535e30 second\n>     bf7438d first\n> \n>     $ git history reorder :/fourth --before=:/second\n>     $ git log --oneline\n>     1610fe0 fifth\n>     444f97d third\n>     2f90797 second\n>     b0ae659 fourth\n>     bf7438d first\n> \n>     $ git history reorder :/fourth --after=:/second\n>     $ git log --oneline\n>     c48729d fifth\n>     f44a46e third\n>     26693b8 fourth\n>     8cb4171 second\n>     bf7438d first\n> \n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n>  Documentation/git-history.adoc |  32 ++++++\n>  builtin/history.c              | 135 +++++++++++++++++++++++++\n>  t/meson.build                  |   1 +\n>  t/t3451-history-reorder.sh     | 218 ++++++++++++++++++++++++++++++++++++++\n+++\n>  4 files changed, 386 insertions(+)\n> \n> diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc\n> index 3012445ddc..6e8b4e1326 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 drop [<options>] <revision>\n> +git history reorder [<options>] <revision> --(before|after)=<revision>\n> \n\nSame here about <revision> vs <commit>/<commit-ish>. \n\nAlso, the form --(foo|bar) has never been used in synopsis. I think it would \nbetter be unrolled, just like in the description. Can you rename each \noccurrence of placeholder <revision> on the line so that the description can \nrefer to them differently?\n\n>  DESCRIPTION\n>  -----------\n>@@ -40,6 +41,12 @@ Dropping the root commit converts the child of that commit \n>into the new\n> root commit. It is invalid to drop a root commit that does not have any\n> child commits, as that would lead to an empty branch.\n> \n>+reorder <revision> (--before=<revision>|--after=<revision>)::\n>+       Reorder the commit so that it becomes either the parent\n>+       (`--before=`) or child (`--after=`) of the other specified\n>+       commit. The commits must be related to one another and must be\n>+       reachable from the current `HEAD` commit.\n\nwith the renamed placeholders, this can be turned into something like:\n\nMove <commit> so that it becomes either the parent of <following-commit> or \nthe child of <preceding-commit>.\n\n\n\n\n"},{"id":"524788","messageId":"3298210.5fSG56mABF@cayenne","threadId":"63986","inReplyTo":"20250819-b4-pks-history-builtin-v1-11-9b77c32688fe@pks.im","subject":"Re: [PATCH RFC 11/11] builtin/history: implement \"split\" subcommand","fromName":"Jean-Noël AVILA","fromEmail":"jn.avila@free.fr","sentAt":"2025-08-23T16:37:23Z","receivedAt":"2025-08-23T16:37:25Z","isPatch":true,"sender":{"key":"jn.avila@free.fr","avatar":"https://avatars.githubusercontent.com/u/156172?v=4"},"body":"On Tuesday, 19 August 2025 12:56:07 CEST Patrick Steinhardt wrote:\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 of it into a separate commit. This is quite an involved operation\n> 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 |  59 ++++++++\n>  builtin/history.c              | 245 +++++++++++++++++++++++++++++++++\n>  t/meson.build                  |   1 +\n>  t/t3452-history-split.sh       | 304 ++++++++++++++++++++++++++++++++++++++\n+++\n>  4 files changed, 609 insertions(+)\n> \n> diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc\n> index 6e8b4e1326..f0f1f2a093 100644\n> --- a/Documentation/git-history.adoc\n> +++ b/Documentation/git-history.adoc\n> @@ -10,6 +10,7 @@ SYNOPSIS\n>  [synopsis]\n>  git history drop [<options>] <revision>\n>  git history reorder [<options>] <revision> --(before|after)=<revision>\n> +git history split [<options>] <revision> [--] [<pathspec>...]\n\nI just realized that there are no other <options> available for the first two \ncommands! So maybe simplify the synopsis for the time being.\n\nThe --message is specific to split, so maybe also cite is specifically here.\nI \n> \n>  DESCRIPTION\n>  -----------\n> @@ -47,6 +48,26 @@ reorder <revision> (--before=<revision>|--\nafter=<revision>)::\n>  \tcommit. The commits must be related to one another and must be\n>  \treachable from the current `HEAD` commit.\n> \n> +split <revision> [--message=<message>] [--] [<pathspec>...]::\n\nmissing backticks. Also the order of options and <commit> are different from \nthe general synopsis. Is this order allowed?\n\n> +\tInteractively split up the commit into two commits by choosing\n\nYou can use the placeholder in this sentence: \"Interactively split up \n<commit>...\"\n\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 message of the new commit will be asked for by launching the\n> +configured editor. Authorship of the commit will be the same as for the\n> +original commit.\n> ++\n\nI guess this only happens if --message is not passed?\n\n> +If passed, _<pathspec>_ can be used to limit which changes shall be split \nout\n> +of the original commit. Files not matching any of the pathspecs will remain\n> +part of the original commit. For more details about the _<pathspec>_ \nsyntax,\n> +see the 'pathspec' entry.\n\nYou may have meant: \n+\nFor more details, see the 'pathspec' entry in linkgit:gitglossary[7].\n\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>  EXAMPLES\n>  --------\n> \n> @@ -88,6 +109,44 @@ f44a46e third\n>  bf7438d first\n>  ----------\n> \n> +* Split a commit.\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 --message=\"split-out commit\"\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,e,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,e,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> +\n>  CONFIGURATION\n>  -------------\n\n\n\n"},{"id":"524802","messageId":"CANiSa6gXbrZaMLVq-AAg38sm+7qgzn4w98WWowLtUHA2P+BdLw@mail.gmail.com","threadId":"63986","inReplyTo":"20250819-b4-pks-history-builtin-v1-0-9b77c32688fe@pks.im","subject":"Re: [PATCH RFC 00/11] Introduce git-history(1) command for easy history editing","fromName":"Martin von Zweigbergk","fromEmail":"martinvonz@gmail.com","sentAt":"2025-08-24T01:25:06Z","receivedAt":"2025-08-24T01:25:18Z","isPatch":true,"sender":{"key":"martinvonz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/891642?v=4"},"body":"On Tue, Aug 19, 2025 at 3:57 AM Patrick Steinhardt <ps@pks.im> wrote:\n>\n> Hi,\n>\n> over recent months I've been playing around with Jujutsu quite\n> frequently. While I still prefer using Git, there's been a couple\n> features in it that I really like and that I'd like to have in Git, as\n> well.\n>\n> A copule of these features relate to history editing. Most importantly,\n> I really dig the following commands:\n>\n>   - jj-abandon(1) to drop a specific commit from your history.\n\nIt also rebases all descendants on top of the parent(s) of the\nabandoned commit(s). Branches pointing to the rebased commits are also\nrepointed. So is the working copy if it points to a rebased commit\n(the closest equivalent in Git would be HEAD). Do you plan to make all\nthe `git history` commands behave that way too?\n\n>\n>   - jj-absorb(1) to take some changes and automatically apply them to\n>     commits in your history that last modified the respective hunks.\n>\n>   - jj-split(1) to split a commit into two.\n>\n>   - jj-new(1) to insert a new commit after or before a specific other\n>     commit.\n>\n> Not all of these commands can be ported directly into Git. jj-new(1) for\n> example doesn't really make a ton of sense for us, I'd claim. But some\n> of these commands _do_ make sense.\n>\n> I thus had a look at implementing some of these commands in Git itself,\n> where the result is this patch series. Specifically, the following\n> commands are introduced by this patch series:\n>\n>   - `git history drop` to drop a specific commit. This is basically the\n>     same as jj-abandon(1).\n>\n>   - `git history reorder` to reorder a specific commit before or after\n>     another commit. This is inspired by jj-new(1).\n\nIt seems more similar to `jj rebase -r X -A/-B Y`, which rips X out of\nthe graph and inserts it after/before Y. Just FYI; I'm not asking for\nany changes.\n\n>\n>   - `git history split` takes a commit and splits it into two. This is\n>     basically the same as jj-split(1).\n\nFYI, the default behavior of `jj split` is to split the commit into\nparent and child, but there's also `-A/-B X` to take the selected\nchanges and insert them after/before X, or `-d X` to put them on top\nof X.\n\n>\n> If this is something we want to have I think it'd be just a starting\n> point. There's other commands that I think are quite common and that\n> might make sense to introduce eventually:\n>\n>   - An equivalent to jj-absorb(1) would be awesome to have.\n>\n>   - `git history reword` to change only the commit message of a specific\n>     commit.\n\nFYI, `jj describe` can also change the commit message of multiple\ncommits at once (e.g. `jj describe main..@` to edit your current chain\nof commits). It concatenates each description with some separators\nbetween in that case so you can update them all at once in your\n$EDITOR.\n\nI'm letting you know these things in case it impacts planning for the\nCLI arguments.\n\n>\n>   - `git history squash` to squash together multiple commits into one.\n>\n> In the end, I'd like us to learn from what people like about Jujutsu and\n> apply those learnings to Git. We won't be able to apply all learnings\n> from Jujutsu, as the workflow is quite different there due to the lack\n> of the index. But other things we certainly can apply to Git directly.\n\nPerhaps the simplest thing to copy is revsets (which we copied from\nMercurial). See https://jj-vcs.github.io/jj/latest/revsets/. It's not\nat all simple to implement, but I think it should be relatively simple\nfrom a UX point of view because it can probably be done in a mostly\nbackwards compatible way.\n\n>\n> Note: This patch series currently builds on the cherry-pick infra.\n> As such, when one hits a merge conflict one needs to `git cherry-pick\n> --continue`, which is quite suboptimal. I didn't want to overpolish this\n> series before getting some feedback, but it is something I'll fix in\n> subsequent versions. Furthermore, the command for now bails out in the\n> case where there's any merge commits in the history that is being\n> rewritten. This is another restriction that can be lifted in the future.\n>\n> Thanks!\n>\n> Patrick\n>\n> ---\n> Patrick Steinhardt (11):\n>       sequencer: optionally skip printing commit summary\n>       sequencer: add option to rewind HEAD after picking commits\n>       cache-tree: allow writing in-memory index as tree\n>       builtin: add new \"history\" command\n>       builtin/history: implement \"drop\" subcommand\n>       builtin/history: implement \"reorder\" subcommand\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>       builtin/history: implement \"split\" subcommand\n>\n>  .gitignore                     |   1 +\n>  Documentation/git-history.adoc | 159 ++++++++++\n>  Documentation/meson.build      |   1 +\n>  Makefile                       |   1 +\n>  add-interactive.c              | 151 +++------\n>  add-interactive.h              |  43 +--\n>  add-patch.c                    | 271 ++++++++++++++--\n>  add-patch.h                    |  61 ++++\n>  builtin.h                      |   1 +\n>  builtin/add.c                  |  22 +-\n>  builtin/checkout.c             |   7 +-\n>  builtin/commit.c               |  16 +-\n>  builtin/history.c              | 691 +++++++++++++++++++++++++++++++++++++++++\n>  builtin/reset.c                |  16 +-\n>  builtin/stash.c                |  46 +--\n>  cache-tree.c                   |   5 +-\n>  cache-tree.h                   |   3 +-\n>  commit.h                       |   2 +-\n>  git.c                          |   1 +\n>  meson.build                    |   1 +\n>  sequencer.c                    |  36 ++-\n>  sequencer.h                    |   4 +\n>  t/meson.build                  |   5 +-\n>  t/t3450-history-drop.sh        | 127 ++++++++\n>  t/t3451-history-reorder.sh     | 218 +++++++++++++\n>  t/t3452-history-split.sh       | 304 ++++++++++++++++++\n>  26 files changed, 1947 insertions(+), 246 deletions(-)\n>\n>\n> ---\n> base-commit: c44beea485f0f2feaf460e2ac87fdd5608d63cf0\n> change-id: 20250819-b4-pks-history-builtin-83398f9a05f0\n>\n>\n"},{"id":"524809","messageId":"aKs3myBRRAiyIvpS@pks.im","threadId":"63986","inReplyTo":"3005363.e9J7NaK4W3@cayenne","subject":"Re: [PATCH RFC 05/11] builtin/history: implement \"drop\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-08-24T16:02:35Z","receivedAt":"2025-08-24T16:02:50Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Sat, Aug 23, 2025 at 06:15:08PM +0200, Jean-Noël AVILA wrote:\n> On Tuesday, 19 August 2025 12:56:01 CEST Patrick Steinhardt wrote:\n> > diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc\n> > index 9dafb8fc16..3012445ddc 100644\n> > --- a/Documentation/git-history.adoc\n> > +++ b/Documentation/git-history.adoc\n> > @@ -8,7 +8,7 @@ git-history - Rewrite history of the current branch\n> >  SYNOPSIS\n> >  --------\n> >  [synopsis]\n> > -git history [<options>]\n> > +git history drop [<options>] <revision>\n> > \n> \n> Grepping through the documentation for the <revision> placeholder does not \n> yield a lot of matches. Can <revision> be replaced by <commit> or <commit-ish>  \n> in this context; these ones seem widely used.\n\nYup, makes sense.\n\n> >  DESCRIPTION\n> >  -----------\n> > @@ -31,6 +31,31 @@ COMMANDS\n> >  This command requires a subcommand. Several subcommands are available to\n> >  rewrite history in different ways.\n> > \n> > +drop <revision>::\n> \n> My linting patch series[1] does not catch this kind of synopsis miss, but \n> here, backticks are missing because this is a part of synopsis:\n> \n> `drop <revision>`::\n\nOkay.\n\n> > +\tDrop a commit from the history and reapply all children of that\n> > +\tcommit on top of the commit's parent. The commit that is to be\n> > +\tdropped must be reachable from the current `HEAD` commit.\n> > ++\n> > +Dropping the root commit converts the child of that commit into the new\n> > +root commit. It is invalid to drop a root commit that does not have any\n> > +child commits, as that would lead to an empty branch.\n> > +\n> > +EXAMPLES\n> > +--------\n> > +\n> > +* Drop a commit from history.\n> > ++\n> \n> As the examples are quite long, it would make sense to declare each example as \n> a sub-section:\n> \n> Drop a commit from history\n> ~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nMakes sense.\n\nThanks by the way for caring about our documentation and trying to make\nsure that we're being more consistent. I really appreciate this kind of\nwork!\n\nPatrick\n"},{"id":"524810","messageId":"aKs3qu5W2eqnF1kM@pks.im","threadId":"63986","inReplyTo":"3298210.5fSG56mABF@cayenne","subject":"Re: [PATCH RFC 11/11] builtin/history: implement \"split\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-08-24T16:02:50Z","receivedAt":"2025-08-24T16:02:58Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Sat, Aug 23, 2025 at 06:37:23PM +0200, Jean-Noël AVILA wrote:\n> On Tuesday, 19 August 2025 12:56:07 CEST Patrick Steinhardt wrote:\n> > @@ -47,6 +48,26 @@ reorder <revision> (--before=<revision>|--\n> after=<revision>)::\n> >  \tcommit. The commits must be related to one another and must be\n> >  \treachable from the current `HEAD` commit.\n> > \n> > +split <revision> [--message=<message>] [--] [<pathspec>...]::\n> \n> missing backticks. Also the order of options and <commit> are different from \n> the general synopsis. Is this order allowed?\n\nIt is, but this usage definitely wasn't intended. Will fix.\n\nPatrick\n"},{"id":"524811","messageId":"aKs3tqjE510MF0T-@pks.im","threadId":"63986","inReplyTo":"CANiSa6gXbrZaMLVq-AAg38sm+7qgzn4w98WWowLtUHA2P+BdLw@mail.gmail.com","subject":"Re: [PATCH RFC 00/11] Introduce git-history(1) command for easy history editing","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-08-24T16:03:02Z","receivedAt":"2025-08-24T16:03:10Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Sat, Aug 23, 2025 at 06:25:06PM -0700, Martin von Zweigbergk wrote:\n> On Tue, Aug 19, 2025 at 3:57 AM Patrick Steinhardt <ps@pks.im> wrote:\n> >\n> > Hi,\n> >\n> > over recent months I've been playing around with Jujutsu quite\n> > frequently. While I still prefer using Git, there's been a couple\n> > features in it that I really like and that I'd like to have in Git, as\n> > well.\n> >\n> > A copule of these features relate to history editing. Most importantly,\n> > I really dig the following commands:\n> >\n> >   - jj-abandon(1) to drop a specific commit from your history.\n> \n> It also rebases all descendants on top of the parent(s) of the\n> abandoned commit(s). Branches pointing to the rebased commits are also\n> repointed. So is the working copy if it points to a rebased commit\n> (the closest equivalent in Git would be HEAD). Do you plan to make all\n> the `git history` commands behave that way too?\n\nYup.\n\n[snip]\n> >   - `git history reorder` to reorder a specific commit before or after\n> >     another commit. This is inspired by jj-new(1).\n> \n> It seems more similar to `jj rebase -r X -A/-B Y`, which rips X out of\n> the graph and inserts it after/before Y. Just FYI; I'm not asking for\n> any changes.\n\nFair enough. I think calling this `git history rebase` would be\nproblematic though given that \"rebase\" is already a widely used term in\nGit that means something else.\n\n> >   - `git history split` takes a commit and splits it into two. This is\n> >     basically the same as jj-split(1).\n> \n> FYI, the default behavior of `jj split` is to split the commit into\n> parent and child, but there's also `-A/-B X` to take the selected\n> changes and insert them after/before X, or `-d X` to put them on top\n> of X.\n\nOh, that's neat and I can totally see how that is useful. I'll not yet\nadd such a change, but that's certainly something we can add at a later\npoint in time rather easily.\n\n> > If this is something we want to have I think it'd be just a starting\n> > point. There's other commands that I think are quite common and that\n> > might make sense to introduce eventually:\n> >\n> >   - An equivalent to jj-absorb(1) would be awesome to have.\n> >\n> >   - `git history reword` to change only the commit message of a specific\n> >     commit.\n> \n> FYI, `jj describe` can also change the commit message of multiple\n> commits at once (e.g. `jj describe main..@` to edit your current chain\n> of commits). It concatenates each description with some separators\n> between in that case so you can update them all at once in your\n> $EDITOR.\n> \n> I'm letting you know these things in case it impacts planning for the\n> CLI arguments.\n\nAnd I very much appreciate it :) A bunch of the functionality in Git\nwill have different terminology so that it fits better into Git overall.\nThere for example is already the \"reword\" action in the interactive\nrebase code, so it makes sense to keep on using it. Same for \"drop\", for\nexample.\n\n> >   - `git history squash` to squash together multiple commits into one.\n> >\n> > In the end, I'd like us to learn from what people like about Jujutsu and\n> > apply those learnings to Git. We won't be able to apply all learnings\n> > from Jujutsu, as the workflow is quite different there due to the lack\n> > of the index. But other things we certainly can apply to Git directly.\n> \n> Perhaps the simplest thing to copy is revsets (which we copied from\n> Mercurial). See https://jj-vcs.github.io/jj/latest/revsets/. It's not\n> at all simple to implement, but I think it should be relatively simple\n> from a UX point of view because it can probably be done in a mostly\n> backwards compatible way.\n\nTrue, I very much like the way revsets work in JJ. Whether it can be\nused in a backwards-compatible way... no idea. I'd have to think about\nthis more closely, but for now I think there's other, lower-hanging\nfruit :)\n\nPatrick\n"},{"id":"524812","messageId":"aKs3wnuzaObmf_PS@pks.im","threadId":"63986","inReplyTo":"xmqqplcnw7op.fsf@gitster.g","subject":"Re: [PATCH RFC 11/11] builtin/history: implement \"split\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-08-24T16:03:14Z","receivedAt":"2025-08-24T16:03:23Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Fri, Aug 22, 2025 at 11:08:22AM -0700, Junio C Hamano wrote:\n> Patrick Steinhardt <ps@pks.im> writes:\n> \n> >> Interesting. I can see using the original as the template for _both_,\n> >> or the first instead of the second. jj's split works a little\n> >> differently (especially with their notion of descriptions), so I can't\n> >> use them as a reference for the behavior.\n> >> \n> >> I suppose this is one of those \"everybody has their preference\"\n> >> things, but I think giving the message in both new commits as the\n> >> template gives splitters the most information available when writing\n> >> the message. (Of course, in my editor, I can presumably do something\n> >> like \":Git show -s <split-commit-ish>\" if I want.)\n> \n> In other words, removing is easy, while remembering and retyping is\n> harder.\n> \n> When I split an existing commit, that is almost always because after\n> doing too many things in a single commit and the time I realize it\n> is when I am writing the commit message.  So I would suggest to give\n> the same original message to both, to avoid losing information.\n\nFor now I'll rework this a bit so that the editor will list all changes\nin the split-out commit, similar to how git-commit(1) does it. That at\nleast makes it way easier to see what you're currently changing. I'll\nthink about this some more though.\n\nPatrick\n"},{"id":"524821","messageId":"f9f4a4d7-3111-4671-8b5f-43c96702f83e@app.fastmail.com","threadId":"63986","inReplyTo":"20250819-b4-pks-history-builtin-v1-6-9b77c32688fe@pks.im","subject":"Re: [PATCH RFC 06/11] builtin/history: implement \"reorder\" subcommand","fromName":"Kristoffer Haugsbakk","fromEmail":"kristofferhaugsbakk@fastmail.com","sentAt":"2025-08-24T17:25:48Z","receivedAt":"2025-08-24T17:26:11Z","isPatch":true,"sender":{"key":"kristofferhaugsbakk@fastmail.com","avatar":null},"body":"Disclaimer that I’ve never used Jujutsu.\n\nOn Tue, Aug 19, 2025, at 12:56, Patrick Steinhardt wrote:\n> When working in projects where having nice commits matters it's quite\n> common that developers end up reordering commits a lot. Tihs is\n\ns/Tihs/This/\n\n> typically done via interactive rebases, where they can then rearrange\n> commits in the instruction sheet.\n>\n> Still, this operation is a frequent-enough operation to provide a more\n> direct of doing this imperatively. As such, introduce a new \"reorder\"\n\ns/direct of/direct way of/\n\nWhat’s a use-case for doing this imperatively?  With a nice rebase\nfrontend you get to shuffle around some lines in your preferred editor.\nThis seems like using Ex mode in Vim.  Which is legitimate but I don’t\nquite see when you would do it.\n\nThe thing with e.g. the Drop subcommand is that I might have some\ncommits marked `TEMP` that I wanna quickly drop at some point.  That’s\nsort of a semi-interactive use case; I might want to lightly script it,\nbut I am always going to invoke it interactively.  Doing that light\nscripting on top of git-rebase(1) sounds like a hassle though.\n\nBut in this case I don’t understand when this would save you time over a\nnice Rebase frontend.  Because I don’t see when you want\nsemi-interactive history reordering.\n\n> subcommand where users can reorder a commit A to come after or before\n> another commit B:\n>\n>     $ git log --oneline\n>     a978f73 fifth\n>     57594ee fourth\n>     04eb1c4 third\n>     d535e30 second\n>     bf7438d first\n>\n>     $ git history reorder :/fourth --before=:/second\n>     $ git log --oneline\n\nThe `:/` notation makes sense here.  You’ve probably just made these\ncommits so you want to match the newest ones.\n\nIs this example meant to demonstrate how it works or to also demonstrate\nhow you would use it (i.e. with revision syntax)?\n\n>     1610fe0 fifth\n>     444f97d third\n>     2f90797 second\n>     b0ae659 fourth\n>     bf7438d first\n>\n>     $ git history reorder :/fourth --after=:/second\n>     $ git log --oneline\n>     c48729d fifth\n>     f44a46e third\n>     26693b8 fourth\n>     8cb4171 second\n>     bf7438d first\n>\n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n>  Documentation/git-history.adoc |  32 ++++++\n>  builtin/history.c              | 135 +++++++++++++++++++++++++\n>  t/meson.build                  |   1 +\n>  t/t3451-history-reorder.sh     | 218 +++++++++++++++++++++++++++++++++++++++++\n>  4 files changed, 386 insertions(+)\n>\n> diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc\n> index 3012445ddc..6e8b4e1326 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 drop [<options>] <revision>\n> +git history reorder [<options>] <revision> --(before|after)=<revision>\n>\n>  DESCRIPTION\n>  -----------\n> @@ -40,6 +41,12 @@ Dropping the root commit converts the child of that\n> commit into the new\n>  root commit. It is invalid to drop a root commit that does not have any\n>  child commits, as that would lead to an empty branch.\n>\n> +reorder <revision> (--before=<revision>|--after=<revision>)::\n> +\tReorder the commit so that it becomes either the parent\n> +\t(`--before=`) or child (`--after=`) of the other specified\n> +\tcommit. The commits must be related to one another and must be\n> +\treachable from the current `HEAD` commit.\n\ns/current `HEAD` commit/current commit/ ?\n\n> +\n>  EXAMPLES\n>  --------\n>[snip]\n"},{"id":"524822","messageId":"cfb5eab0-ae72-477c-91a7-e9198ef7464d@app.fastmail.com","threadId":"63986","inReplyTo":"20250819-b4-pks-history-builtin-v1-0-9b77c32688fe@pks.im","subject":"Re: [PATCH RFC 00/11] Introduce git-history(1) command for easy history editing","fromName":"Kristoffer Haugsbakk","fromEmail":"kristofferhaugsbakk@fastmail.com","sentAt":"2025-08-24T17:31:45Z","receivedAt":"2025-08-24T17:32:19Z","isPatch":true,"sender":{"key":"kristofferhaugsbakk@fastmail.com","avatar":null},"body":"On Tue, Aug 19, 2025, at 12:55, Patrick Steinhardt wrote:\n> Hi,\n>\n> over recent months I've been playing around with Jujutsu quite\n> frequently. While I still prefer using Git, there's been a couple\n> features in it that I really like and that I'd like to have in Git, as\n> well.\n>\n> A copule of these features relate to history editing. Most importantly,\n> I really dig the following commands:\n>\n>   - jj-abandon(1) to drop a specific commit from your history.\n>\n>   - jj-absorb(1) to take some changes and automatically apply them to\n>     commits in your history that last modified the respective hunks.\n>\n>   - jj-split(1) to split a commit into two.\n>\n>   - jj-new(1) to insert a new commit after or before a specific other\n>     commit.\n>\n> Not all of these commands can be ported directly into Git. jj-new(1) for\n> example doesn't really make a ton of sense for us, I'd claim. But some\n> of these commands _do_ make sense.\n>\n> I thus had a look at implementing some of these commands in Git itself,\n> where the result is this patch series. Specifically, the following\n> commands are introduced by this patch series:\n>\n>   - `git history drop` to drop a specific commit. This is basically the\n>     same as jj-abandon(1).\n>\n>   - `git history reorder` to reorder a specific commit before or after\n>     another commit. This is inspired by jj-new(1).\n>\n>   - `git history split` takes a commit and splits it into two. This is\n>     basically the same as jj-split(1).\n\nI think it would be nice if git-history(1) called the `post-rewrite` hook.\nIn particular for Split; then all the possible rewrite modes are covered\n(one-to-one, many-to-one (squash), and one-to-many).\n"},{"id":"524823","messageId":"aKtNO3z3xfiu-5aF@pks.im","threadId":"63986","inReplyTo":"f9f4a4d7-3111-4671-8b5f-43c96702f83e@app.fastmail.com","subject":"Re: [PATCH RFC 06/11] builtin/history: implement \"reorder\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-08-24T17:34:51Z","receivedAt":"2025-08-24T17:34:59Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Sun, Aug 24, 2025 at 07:25:48PM +0200, Kristoffer Haugsbakk wrote:\n> On Tue, Aug 19, 2025, at 12:56, Patrick Steinhardt wrote:\n> > When working in projects where having nice commits matters it's quite\n> > common that developers end up reordering commits a lot. Tihs is\n> \n> s/Tihs/This/\n> \n> > typically done via interactive rebases, where they can then rearrange\n> > commits in the instruction sheet.\n> >\n> > Still, this operation is a frequent-enough operation to provide a more\n> > direct of doing this imperatively. As such, introduce a new \"reorder\"\n> \n> s/direct of/direct way of/\n> \n> What’s a use-case for doing this imperatively?  With a nice rebase\n> frontend you get to shuffle around some lines in your preferred editor.\n> This seems like using Ex mode in Vim.  Which is legitimate but I don’t\n> quite see when you would do it.\n> \n> The thing with e.g. the Drop subcommand is that I might have some\n> commits marked `TEMP` that I wanna quickly drop at some point.  That’s\n> sort of a semi-interactive use case; I might want to lightly script it,\n> but I am always going to invoke it interactively.  Doing that light\n> scripting on top of git-rebase(1) sounds like a hassle though.\n> \n> But in this case I don’t understand when this would save you time over a\n> nice Rebase frontend.  Because I don’t see when you want\n> semi-interactive history reordering.\n\nAll I can say is that I ended up using this feature quite regularly with\nJujutsu. It sometimes just feels a less heavy-weight to tell Git what to\ndo directly instead of doing so via the editor.\n\nMost often this happens when I just created a new smallish commit\nbecause I noticed something and quickly want to reorder it somewhere\nearlier.\n\n> > subcommand where users can reorder a commit A to come after or before\n> > another commit B:\n> >\n> >     $ git log --oneline\n> >     a978f73 fifth\n> >     57594ee fourth\n> >     04eb1c4 third\n> >     d535e30 second\n> >     bf7438d first\n> >\n> >     $ git history reorder :/fourth --before=:/second\n> >     $ git log --oneline\n> \n> The `:/` notation makes sense here.  You’ve probably just made these\n> commits so you want to match the newest ones.\n> \n> Is this example meant to demonstrate how it works or to also demonstrate\n> how you would use it (i.e. with revision syntax)?\n\nBoth, I guess. I often use this revision syntax myself, but in a lot of\nother cases, especially when you just want to rebase HEAD, it may be\neasier to just say \"HEAD\".\n\n> > diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc\n> > index 3012445ddc..6e8b4e1326 100644\n> > --- a/Documentation/git-history.adoc\n> > +++ b/Documentation/git-history.adoc\n> > @@ -40,6 +41,12 @@ Dropping the root commit converts the child of that\n> > commit into the new\n> >  root commit. It is invalid to drop a root commit that does not have any\n> >  child commits, as that would lead to an empty branch.\n> >\n> > +reorder <revision> (--before=<revision>|--after=<revision>)::\n> > +\tReorder the commit so that it becomes either the parent\n> > +\t(`--before=`) or child (`--after=`) of the other specified\n> > +\tcommit. The commits must be related to one another and must be\n> > +\treachable from the current `HEAD` commit.\n> \n> s/current `HEAD` commit/current commit/ ?\n\nLet's maybe say \"currently checked-out commit\".\n\nPatrick\n"},{"id":"524824","messageId":"aKtOAFKpxCK0RZJH@pks.im","threadId":"63986","inReplyTo":"cfb5eab0-ae72-477c-91a7-e9198ef7464d@app.fastmail.com","subject":"Re: [PATCH RFC 00/11] Introduce git-history(1) command for easy history editing","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-08-24T17:38:08Z","receivedAt":"2025-08-24T17:38:16Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Sun, Aug 24, 2025 at 07:31:45PM +0200, Kristoffer Haugsbakk wrote:\n> On Tue, Aug 19, 2025, at 12:55, Patrick Steinhardt wrote:\n> > Hi,\n> >\n> > over recent months I've been playing around with Jujutsu quite\n> > frequently. While I still prefer using Git, there's been a couple\n> > features in it that I really like and that I'd like to have in Git, as\n> > well.\n> >\n> > A copule of these features relate to history editing. Most importantly,\n> > I really dig the following commands:\n> >\n> >   - jj-abandon(1) to drop a specific commit from your history.\n> >\n> >   - jj-absorb(1) to take some changes and automatically apply them to\n> >     commits in your history that last modified the respective hunks.\n> >\n> >   - jj-split(1) to split a commit into two.\n> >\n> >   - jj-new(1) to insert a new commit after or before a specific other\n> >     commit.\n> >\n> > Not all of these commands can be ported directly into Git. jj-new(1) for\n> > example doesn't really make a ton of sense for us, I'd claim. But some\n> > of these commands _do_ make sense.\n> >\n> > I thus had a look at implementing some of these commands in Git itself,\n> > where the result is this patch series. Specifically, the following\n> > commands are introduced by this patch series:\n> >\n> >   - `git history drop` to drop a specific commit. This is basically the\n> >     same as jj-abandon(1).\n> >\n> >   - `git history reorder` to reorder a specific commit before or after\n> >     another commit. This is inspired by jj-new(1).\n> >\n> >   - `git history split` takes a commit and splits it into two. This is\n> >     basically the same as jj-split(1).\n> \n> I think it would be nice if git-history(1) called the `post-rewrite` hook.\n> In particular for Split; then all the possible rewrite modes are covered\n> (one-to-one, many-to-one (squash), and one-to-many).\n\nThat's a sensible thing indeed. I'll add this to my todo list for v3.\n\nPatrick\n"},{"id":"524825","messageId":"20250824-b4-pks-history-builtin-v2-0-964ac12f65bd@pks.im","threadId":"63986","inReplyTo":"20250819-b4-pks-history-builtin-v1-0-9b77c32688fe@pks.im","subject":"[PATCH RFC v2 00/16] Introduce git-history(1) command for easy history editing","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-08-24T17:42:13Z","receivedAt":"2025-08-24T17:42:38Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Hi,\n\nover recent months I've been playing around with Jujutsu quite\nfrequently. While I still prefer using Git, there's been a couple\nfeatures in it that I really like and that I'd like to have in Git, as\nwell.\n\nA copule of these features relate to history editing. Most importantly,\nI really dig the following commands:\n\n  - jj-abandon(1) to drop a specific commit from your history.\n\n  - jj-absorb(1) to take some changes and automatically apply them to\n    commits in your history that last modified the respective hunks.\n\n  - jj-split(1) to split a commit into two.\n\n  - jj-new(1) to insert a new commit after or before a specific other\n    commit.\n\nNot all of these commands can be ported directly into Git. jj-new(1) for\nexample doesn't really make a ton of sense for us, I'd claim. But some\nof these commands _do_ make sense.\n\nI thus had a look at implementing some of these commands in Git itself,\nwhere the result is this patch series. Specifically, the following\ncommands are introduced by this patch series:\n\n  - `git history drop` to drop a specific commit. This is basically the\n    same as jj-abandon(1).\n\n  - `git history reorder` to reorder a specific commit before or after\n    another commit. This is inspired by jj-new(1).\n\n  - `git history split` takes a commit and splits it into two. This is\n    basically the same as jj-split(1).\n\nIf this is something we want to have I think it'd be just a starting\npoint. There's other commands that I think are quite common and that\nmight make sense to introduce eventually:\n\n  - An equivalent to jj-absorb(1) would be awesome to have.\n\n  - `git history reword` to change only the commit message of a specific\n    commit.\n\n  - `git history squash` to squash together multiple commits into one.\n\nIn the end, I'd like us to learn from what people like about Jujutsu and\napply those learnings to Git. We won't be able to apply all learnings\nfrom Jujutsu, as the workflow is quite different there due to the lack\nof the index. But other things we certainly can apply to Git directly.\n\nNote: This patch series currently builds on the cherry-pick infra.\nAs such, when one hits a merge conflict one needs to `git cherry-pick\n--continue`, which is quite suboptimal. I didn't want to overpolish this\nseries before getting some feedback, but it is something I'll fix in\nsubsequent versions. Furthermore, the command for now bails out in the\ncase where there's any merge commits in the history that is being\nrewritten. This is another restriction that can be lifted in the future.\n\nChanges in v2:\n  - Add a new \"reword\" subcommand.\n  - List git-history(1) in \"command-list.txt\".\n  - Add some missing error handling.\n  - Simplify calling convention of `apply_commits()` to handle root\n    commits internally instead of requiring every caller to do so.\n  - Add tests to verify that git-history(1) refuses to work with changes\n    in the worktree or index.\n  - Mark git-history(1) as experimental.\n  - Introduce commands to manage interrupted history edits.\n  - A bunch of improvements to the manpage.\n  - Link to v1: https://lore.kernel.org/r/20250819-b4-pks-history-builtin-v1-0-9b77c32688fe@pks.im\n\nThanks!\n\nPatrick\n\n---\nPatrick Steinhardt (16):\n      sequencer: optionally skip printing commit summary\n      sequencer: add option to rewind HEAD after picking commits\n      sequencer: introduce new history editing mode\n      sequencer: stop using `the_repository` in `sequencer_remove_state()`\n      cache-tree: allow writing in-memory index as tree\n      builtin: add new \"history\" command\n      builtin/history: introduce subcommands to manage interrupted rewrites\n      builtin/history: implement \"drop\" subcommand\n      builtin/history: implement \"reorder\" subcommand\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      wt-status: provide function to expose status for trees\n      builtin/history: implement \"split\" subcommand\n      builtin/history: implement \"reword\" subcommand\n\n .gitignore                     |   1 +\n Documentation/git-history.adoc | 188 +++++++++\n Documentation/meson.build      |   1 +\n Makefile                       |   1 +\n add-interactive.c              | 151 ++-----\n add-interactive.h              |  43 +-\n add-patch.c                    | 270 ++++++++++--\n add-patch.h                    |  61 +++\n builtin.h                      |   1 +\n builtin/add.c                  |  22 +-\n builtin/checkout.c             |   7 +-\n builtin/commit.c               |  16 +-\n builtin/history.c              | 932 +++++++++++++++++++++++++++++++++++++++++\n builtin/rebase.c               |   4 +-\n builtin/reset.c                |  16 +-\n builtin/revert.c               |   2 +-\n builtin/stash.c                |  46 +-\n cache-tree.c                   |   5 +-\n cache-tree.h                   |   3 +-\n command-list.txt               |   1 +\n commit.h                       |   2 +-\n git.c                          |   1 +\n meson.build                    |   1 +\n sequencer.c                    | 187 +++++++--\n sequencer.h                    |   9 +-\n t/meson.build                  |   7 +-\n t/t3450-history.sh             |  42 ++\n t/t3451-history-drop.sh        | 174 ++++++++\n t/t3452-history-reorder.sh     | 234 +++++++++++\n t/t3453-history-split.sh       | 387 +++++++++++++++++\n t/t3454-history-reword.sh      | 158 +++++++\n wt-status.c                    |  24 ++\n wt-status.h                    |   3 +\n 33 files changed, 2714 insertions(+), 286 deletions(-)\n\nRange-diff versus v1:\n\n 1:  c1ce1b2e20 =  1:  6348d1ff69 sequencer: optionally skip printing commit summary\n 2:  969d896da1 !  2:  d70cb727bc sequencer: add option to rewind HEAD after picking commits\n    @@ sequencer.c: static int pick_commits(struct repository *r,\n      \t/*\n      \t * Sequence of picks finished successfully; cleanup by\n      \t * removing the .git/sequencer directory\n    +@@ sequencer.c: int sequencer_pick_revisions(struct repository *r,\n    + \tif (opts->revs->cmdline.nr == 1 &&\n    + \t    opts->revs->cmdline.rev->whence == REV_CMD_REV &&\n    + \t    opts->revs->no_walk &&\n    +-\t    !opts->revs->cmdline.rev->flags) {\n    ++\t    !opts->revs->cmdline.rev->flags &&\n    ++\t    !opts->restore_head_target) {\n    + \t\tstruct commit *cmit;\n    + \n    + \t\tif (prepare_revision_walk(opts->revs)) {\n     \n      ## sequencer.h ##\n     @@ sequencer.h: struct replay_opts {\n -:  ---------- >  3:  9717385fba sequencer: introduce new history editing mode\n -:  ---------- >  4:  360e5cf08f sequencer: stop using `the_repository` in `sequencer_remove_state()`\n 3:  658cff279e =  5:  74be65b8f6 cache-tree: allow writing in-memory index as tree\n 4:  886ea1e088 !  6:  a84728f097 builtin: add new \"history\" command\n    @@ Documentation/git-history.adoc (new)\n     +\n     +NAME\n     +----\n    -+git-history - Rewrite history of the current branch\n    ++git-history - EXPERIMENTAL: Rewrite history of the current branch\n     +\n     +SYNOPSIS\n     +--------\n    @@ Documentation/git-history.adoc (new)\n     +merges. You should use linkgit:git-rebase[1] with the `--rebase-merges`\n     +flag instead.\n     +\n    ++THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.\n    ++\n     +COMMANDS\n     +--------\n     +\n     +This command requires a subcommand. Several subcommands are available to\n    -+rewrite history in different ways.\n    ++rewrite history in different ways:\n     +\n     +CONFIGURATION\n     +-------------\n    @@ builtin/history.c (new)\n     +\treturn 0;\n     +}\n     \n    + ## command-list.txt ##\n    +@@ command-list.txt: git-grep                                mainporcelain           info\n    + git-gui                                 mainporcelain\n    + git-hash-object                         plumbingmanipulators\n    + git-help                                ancillaryinterrogators          complete\n    ++git-history                             mainporcelain           history\n    + git-hook                                purehelpers\n    + git-http-backend                        synchingrepositories\n    + git-http-fetch                          synchelpers\n    +\n      ## git.c ##\n     @@ git.c: static struct cmd_struct commands[] = {\n      \t{ \"grep\", cmd_grep, RUN_SETUP_GENTLY },\n -:  ---------- >  7:  234ad69878 builtin/history: introduce subcommands to manage interrupted rewrites\n 5:  084312482f !  8:  a81669a4d1 builtin/history: implement \"drop\" subcommand\n    @@ Commit message\n         Signed-off-by: Patrick Steinhardt <ps@pks.im>\n     \n      ## Documentation/git-history.adoc ##\n    -@@ Documentation/git-history.adoc: git-history - Rewrite history of the current branch\n    - SYNOPSIS\n    - --------\n    - [synopsis]\n    --git history [<options>]\n    -+git history drop [<options>] <revision>\n    +@@ Documentation/git-history.adoc: SYNOPSIS\n    + git history abort\n    + git history continue\n    + git history quit\n    ++git history drop <commit>\n      \n      DESCRIPTION\n      -----------\n     @@ Documentation/git-history.adoc: COMMANDS\n      This command requires a subcommand. Several subcommands are available to\n    - rewrite history in different ways.\n    + rewrite history in different ways:\n      \n    -+drop <revision>::\n    ++`drop <commit>`::\n     +\tDrop a commit from the history and reapply all children of that\n     +\tcommit on top of the commit's parent. The commit that is to be\n     +\tdropped must be reachable from the current `HEAD` commit.\n    @@ Documentation/git-history.adoc: COMMANDS\n     +root commit. It is invalid to drop a root commit that does not have any\n     +child commits, as that would lead to an empty branch.\n     +\n    + The following commands are used to manage an interrupted history-rewriting\n    + operation:\n    + \n    +@@ Documentation/git-history.adoc: operation:\n    + \tthe original branch. The index and working tree are also left unchanged\n    + \tas a result.\n    + \n     +EXAMPLES\n     +--------\n     +\n    -+* Drop a commit from history.\n    -++\n    ++Drop a commit from history\n    ++~~~~~~~~~~~~~~~~~~~~~~~~~~\n    ++\n     +----------\n     +$ git log --oneline\n     +2d4cd6d third\n    @@ Documentation/git-history.adoc: COMMANDS\n      ## builtin/history.c ##\n     @@\n      #include \"builtin.h\"\n    + #include \"branch.h\"\n     +#include \"commit.h\"\n     +#include \"commit-reach.h\"\n     +#include \"config.h\"\n    @@ builtin/history.c\n     +#include \"refs.h\"\n     +#include \"reset.h\"\n     +#include \"revision.h\"\n    -+#include \"sequencer.h\"\n    -+\n    + #include \"sequencer.h\"\n    + \n    + static int cmd_history_abort(int argc,\n    +@@ builtin/history.c: static int cmd_history_quit(int argc,\n    + \treturn ret;\n    + }\n    + \n     +static int collect_commits(struct repository *repo,\n     +\t\t\t   struct commit *old_commit,\n     +\t\t\t   struct commit *new_commit,\n    @@ builtin/history.c\n     +\tint ret;\n     +\n     +\t/*\n    -+\t * Check that the old actually is an ancestor of HEAD. If not\n    ++\t * Check that the old commit actually is an ancestor of HEAD. If not\n     +\t * the whole request becomes nonsensical.\n     +\t*/\n     +\tif (old_commit) {\n    @@ builtin/history.c\n     +\n     +\t/*\n     +\t * Revisions are in newest-order-first. We have to reverse the\n    -+\t * array though so that we pick the oldest commits first. Note\n    -+\t * that we keep the first string untouched, as it is the\n    -+\t * equivalent of `argv[0]` to `setup_revisions()`.\n    ++\t * array though so that we pick the oldest commits first.\n     +\t */\n     +\tfor (size_t i = 0, j = out->nr - 1; i < j; i++, j--)\n     +\t\tSWAP(out->v[i], out->v[j]);\n    @@ builtin/history.c\n     +\t * We have performed all safety checks, so we now prepare\n     +\t * replaying the commits.\n     +\t*/\n    -+\treplay_opts.action = REPLAY_PICK;\n    ++\treplay_opts.action = REPLAY_HISTORY_EDIT;\n     +\tsequencer_init_config(&replay_opts);\n     +\treplay_opts.quiet = 1;\n     +\treplay_opts.skip_commit_summary = 1;\n    @@ builtin/history.c\n     +\t * squash that root commit with the first commit we're picking\n     +\t * onto it.\n     +\t */\n    -+\tif (!base) {\n    ++\tif (!base->parents) {\n     +\t\tif (commit_tree(\"\", 0, repo->hash_algo->empty_tree, NULL,\n     +\t\t\t\t&root_commit, NULL, NULL) < 0) {\n     +\t\t\tret = error(_(\"Could not create new root commit\"));\n    @@ builtin/history.c\n     +\t\treplay_opts.have_squash_onto = 1;\n     +\t\treset_opts.oid = &root_commit;\n     +\t} else {\n    -+\t\treset_opts.oid = &base->object.oid;\n    ++\t\treset_opts.oid = &base->parents->item->object.oid;\n     +\t}\n     +\n     +\treplay_opts.restore_head_target =\n    @@ builtin/history.c\n     +\t\t\t    struct repository *repo)\n     +{\n     +\tconst char * const usage[] = {\n    -+\t\tN_(\"git history drop [<options>] <revision>\"),\n    ++\t\tN_(\"git history drop <commit>\"),\n     +\t\tNULL,\n     +\t};\n     +\tstruct option options[] = {\n    @@ builtin/history.c\n     +\t\tif (ret < 0)\n     +\t\t\tgoto out;\n     +\n    -+\t\tret = apply_commits(repo, &commits, head, commit_to_drop->parents ?\n    -+\t\t\t\t    commit_to_drop->parents->item : NULL, \"drop\");\n    ++\t\tret = apply_commits(repo, &commits, head, commit_to_drop, \"drop\");\n     +\t\tif (ret < 0)\n     +\t\t\tgoto out;\n     +\t}\n    @@ builtin/history.c\n     +\tstrbuf_release(&buf);\n     +\treturn ret;\n     +}\n    - \n    ++\n      int cmd_history(int argc,\n      \t\tconst char **argv,\n      \t\tconst char *prefix,\n    --\t\tstruct repository *repo UNUSED)\n    -+\t\tstruct repository *repo)\n    - {\n    - \tconst char * const usage[] = {\n    --\t\tN_(\"git history [<options>]\"),\n    -+\t\tN_(\"git history drop [<options>] <revision>\"),\n    +@@ builtin/history.c: int cmd_history(int argc,\n    + \t\tN_(\"git history abort\"),\n    + \t\tN_(\"git history continue\"),\n    + \t\tN_(\"git history quit\"),\n    ++\t\tN_(\"git history drop <commit>\"),\n      \t\tNULL,\n      \t};\n    -+\tparse_opt_subcommand_fn *fn = NULL;\n    - \tstruct option options[] = {\n    + \tparse_opt_subcommand_fn *fn = NULL;\n    +@@ builtin/history.c: int cmd_history(int argc,\n    + \t\tOPT_SUBCOMMAND(\"abort\", &fn, cmd_history_abort),\n    + \t\tOPT_SUBCOMMAND(\"continue\", &fn, cmd_history_continue),\n    + \t\tOPT_SUBCOMMAND(\"quit\", &fn, cmd_history_quit),\n     +\t\tOPT_SUBCOMMAND(\"drop\", &fn, cmd_history_drop),\n      \t\tOPT_END(),\n      \t};\n      \n    - \targc = parse_options(argc, argv, prefix, options, usage, 0);\n    --\treturn 0;\n    -+\treturn fn(argc, argv, prefix, repo);\n    - }\n     \n      ## t/meson.build ##\n     @@ t/meson.build: integration_tests = [\n    -   't3436-rebase-more-options.sh',\n        't3437-rebase-fixup-options.sh',\n        't3438-rebase-broken-files.sh',\n    -+  't3450-history-drop.sh',\n    +   't3450-history.sh',\n    ++  't3451-history-drop.sh',\n        't3500-cherry.sh',\n        't3501-revert-cherry-pick.sh',\n        't3502-cherry-pick-merge.sh',\n    -@@ t/meson.build: if perl.found() and time.found()\n    -       timeout: 0,\n    -     )\n    -   endforeach\n    --endif\n    - \\ No newline at end of file\n    -+endif\n     \n    - ## t/t3450-history-drop.sh (new) ##\n    + ## t/t3451-history-drop.sh (new) ##\n     @@\n     +#!/bin/sh\n     +\n    @@ t/t3450-history-drop.sh (new)\n     +\t)\n     +'\n     +\n    ++test_expect_success 'refuses to work with changes in the worktree or index' '\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_commit file file &&\n    ++\t\techo foo >file &&\n    ++\t\ttest_must_fail git history drop HEAD 2>err &&\n    ++\t\ttest_grep \"Your local changes to the following files would be overwritten\" err &&\n    ++\t\tgit add file &&\n    ++\t\ttest_must_fail git history drop HEAD 2>err &&\n    ++\t\ttest_grep \"Your local changes to the following files would be overwritten\" err\n    ++\t)\n    ++'\n    ++\n     +test_expect_success 'can drop tip of a branch' '\n     +\ttest_when_finished \"rm -rf repo\" &&\n     +\tgit init repo &&\n    @@ t/t3450-history-drop.sh (new)\n     +\t)\n     +'\n     +\n    ++test_expect_success 'conflicts are detected' '\n    ++\ttest_when_finished \"rm -rf repo\" &&\n    ++\tgit init repo &&\n    ++\t(\n    ++\t\tcd repo &&\n    ++\t\ttest_commit base &&\n    ++\t\techo original >file &&\n    ++\t\tgit add . &&\n    ++\t\tgit commit -m original &&\n    ++\t\techo modified >file &&\n    ++\t\tgit commit -am modified &&\n    ++\n    ++\t\ttest_must_fail git history drop HEAD~ >err 2>&1 &&\n    ++\t\ttest_grep CONFLICT err &&\n    ++\t\ttest_grep \"git history continue\" err &&\n    ++\t\techo resolved >file &&\n    ++\t\tgit add file &&\n    ++\t\tgit history continue &&\n    ++\n    ++\t\tcat >expect <<-EOF &&\n    ++\t\tmodified\n    ++\t\tbase\n    ++\t\tEOF\n    ++\t\tgit log --format=%s >actual &&\n    ++\t\ttest_cmp expect actual &&\n    ++\t\techo resolved >expect &&\n    ++\t\tgit cat-file -p HEAD:file >actual &&\n    ++\t\ttest_cmp expect actual\n    ++\t)\n    ++'\n    ++\n     +test_done\n 6:  5ba28ca5e5 !  9:  95ce67205d builtin/history: implement \"reorder\" subcommand\n    @@ Commit message\n         builtin/history: implement \"reorder\" subcommand\n     \n         When working in projects where having nice commits matters it's quite\n    -    common that developers end up reordering commits a lot. Tihs is\n    +    common that developers end up reordering commits a lot. This is\n         typically done via interactive rebases, where they can then rearrange\n         commits in the instruction sheet.\n     \n         Still, this operation is a frequent-enough operation to provide a more\n    -    direct of doing this imperatively. As such, introduce a new \"reorder\"\n    -    subcommand where users can reorder a commit A to come after or before\n    -    another commit B:\n    +    direct way of doing this imperatively. As such, introduce a new\n    +    \"reorder\" subcommand where users can reorder a commit A to come after or\n    +    before another commit B:\n     \n             $ git log --oneline\n             a978f73 fifth\n    @@ Commit message\n         Signed-off-by: Patrick Steinhardt <ps@pks.im>\n     \n      ## Documentation/git-history.adoc ##\n    -@@ Documentation/git-history.adoc: SYNOPSIS\n    - --------\n    - [synopsis]\n    - git history drop [<options>] <revision>\n    -+git history reorder [<options>] <revision> --(before|after)=<revision>\n    +@@ Documentation/git-history.adoc: git history abort\n    + git history continue\n    + git history quit\n    + git history drop <commit>\n    ++git history reorder <commit> (--before=<following-commit>|--after=<preceding-commit>)\n      \n      DESCRIPTION\n      -----------\n    -@@ Documentation/git-history.adoc: Dropping the root commit converts the child of that commit into the new\n    +@@ Documentation/git-history.adoc: rewrite history in different ways:\n    + `drop <commit>`::\n    + \tDrop a commit from the history and reapply all children of that\n    + \tcommit on top of the commit's parent. The commit that is to be\n    +-\tdropped must be reachable from the current `HEAD` commit.\n    ++\tdropped must be reachable from the currently checked-out commit.\n    + +\n    + Dropping the root commit converts the child of that commit into the new\n      root commit. It is invalid to drop a root commit that does not have any\n      child commits, as that would lead to an empty branch.\n      \n    -+reorder <revision> (--before=<revision>|--after=<revision>)::\n    -+\tReorder the commit so that it becomes either the parent\n    -+\t(`--before=`) or child (`--after=`) of the other specified\n    -+\tcommit. The commits must be related to one another and must be\n    -+\treachable from the current `HEAD` commit.\n    -+\n    - EXAMPLES\n    - --------\n    ++`reorder <commit> (--before=<following-commit>|--after=<preceding-commit>)`::\n    ++\tMove the commit so that it becomes either the parent of\n    ++\t<following-commit> or the child of <preceding-commit>. The commits must\n    ++\tbe related to one another and must be reachable from the current `HEAD`\n    ++\tcommit.\n    ++\n    + The following commands are used to manage an interrupted history-rewriting\n    + operation:\n      \n     @@ Documentation/git-history.adoc: b1bc1bd third\n      e098c27 first\n      ----------\n      \n    -+* Reorder a commit.\n    -++\n    ++Reorder a commit\n    ++~~~~~~~~~~~~~~~~\n    ++\n     +----------\n     +$ git log --oneline\n     +a978f73 fifth\n    @@ builtin/history.c: static int cmd_history_drop(int argc,\n     +\t\t\t       struct repository *repo)\n     +{\n     +\tconst char * const usage[] = {\n    -+\t\tN_(\"git history reorder [<options>] <revision> (--before=<commit>|--after=<commit>)\"),\n    ++\t\tN_(\"git history reorder <commit> (--before=<following-commit>|--after=<preceding-commit>)\"),\n     +\t\tNULL,\n     +\t};\n     +\tconst char *before = NULL, *after = NULL;\n    @@ builtin/history.c: static int cmd_history_drop(int argc,\n     +\treplace_commits(&commits, &commit_to_reorder->object.oid, NULL, 0);\n     +\treplace_commits(&commits, &anchor->object.oid, replacement, ARRAY_SIZE(replacement));\n     +\n    -+\t/*\n    -+\t * And now we pick commits in the new order on top of either the root\n    -+\t * commit or on top the old commit's parent.\n    -+\t */\n    -+\tret = apply_commits(repo, &commits, head,\n    -+\t\t\t    old->parents ? old->parents->item : NULL, \"reorder\");\n    ++\tret = apply_commits(repo, &commits, head, old, \"reorder\");\n     +\tif (ret < 0)\n     +\t\tgoto out;\n     +\n    @@ builtin/history.c: static int cmd_history_drop(int argc,\n      \t\tconst char **argv,\n      \t\tconst char *prefix,\n     @@ builtin/history.c: int cmd_history(int argc,\n    - {\n    - \tconst char * const usage[] = {\n    - \t\tN_(\"git history drop [<options>] <revision>\"),\n    -+\t\tN_(\"git history reorder [<options>] <revision> --(before|after)=<revision>\"),\n    + \t\tN_(\"git history continue\"),\n    + \t\tN_(\"git history quit\"),\n    + \t\tN_(\"git history drop <commit>\"),\n    ++\t\tN_(\"git history reorder <commit> (--before=<following-commit>|--after=<preceding-commit>)\"),\n      \t\tNULL,\n      \t};\n      \tparse_opt_subcommand_fn *fn = NULL;\n    - \tstruct option options[] = {\n    +@@ builtin/history.c: int cmd_history(int argc,\n    + \t\tOPT_SUBCOMMAND(\"continue\", &fn, cmd_history_continue),\n    + \t\tOPT_SUBCOMMAND(\"quit\", &fn, cmd_history_quit),\n      \t\tOPT_SUBCOMMAND(\"drop\", &fn, cmd_history_drop),\n     +\t\tOPT_SUBCOMMAND(\"reorder\", &fn, cmd_history_reorder),\n      \t\tOPT_END(),\n    @@ builtin/history.c: int cmd_history(int argc,\n     \n      ## t/meson.build ##\n     @@ t/meson.build: integration_tests = [\n    -   't3437-rebase-fixup-options.sh',\n        't3438-rebase-broken-files.sh',\n    -   't3450-history-drop.sh',\n    -+  't3451-history-reorder.sh',\n    +   't3450-history.sh',\n    +   't3451-history-drop.sh',\n    ++  't3452-history-reorder.sh',\n        't3500-cherry.sh',\n        't3501-revert-cherry-pick.sh',\n        't3502-cherry-pick-merge.sh',\n     \n    - ## t/t3451-history-reorder.sh (new) ##\n    + ## t/t3452-history-reorder.sh (new) ##\n     @@\n     +#!/bin/sh\n     +\n    @@ t/t3451-history-reorder.sh (new)\n     +\n     +. ./test-lib.sh\n     +\n    -+test_expect_success 'reorder refuses to work with merge commits' '\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/t3451-history-reorder.sh (new)\n     +\t)\n     +'\n     +\n    -+test_expect_success 'reorder requires exactly one of --before or --after' '\n    ++test_expect_success 'refuses to work with changes in the worktree or index' '\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_commit file file &&\n    ++\t\techo foo >file &&\n    ++\t\ttest_must_fail git history reorder HEAD --before=HEAD~ 2>err &&\n    ++\t\ttest_grep \"Your local changes to the following files would be overwritten\" err &&\n    ++\t\tgit add file &&\n    ++\t\ttest_must_fail git history reorder HEAD --before=HEAD~ 2>err &&\n    ++\t\ttest_grep \"Your local changes to the following files would be overwritten\" err\n    ++\t)\n    ++'\n    ++\n    ++test_expect_success 'requires exactly one of --before or --after' '\n     +\ttest_when_finished \"rm -rf repo\" &&\n     +\tgit init repo &&\n     +\t(\n    @@ t/t3451-history-reorder.sh (new)\n     +\t)\n     +'\n     +\n    -+test_expect_success 'reorder refuses to reorder commit with itself' '\n    ++test_expect_success 'refuses to reorder commit with itself' '\n     +\ttest_when_finished \"rm -rf repo\" &&\n     +\tgit init repo &&\n     +\t(\n    @@ t/t3451-history-reorder.sh (new)\n     +\t\ttest_must_fail git symbolic-ref HEAD &&\n     +\t\techo \"second edit\" >file &&\n     +\t\tgit add file &&\n    -+\t\ttest_must_fail git cherry-pick --continue &&\n    ++\t\ttest_must_fail git history continue &&\n     +\t\techo \"first edit\" >file &&\n     +\t\tgit add file &&\n    -+\t\tgit cherry-pick --continue &&\n    ++\t\tgit history continue &&\n     +\n     +\t\tcat >expect <<-EOF &&\n     +\t\tfirst edit\n 7:  91221a3883 = 10:  1bc1d4f06c add-patch: split out header from \"add-interactive.h\"\n 8:  018d7bd8ea = 11:  fed38713fa add-patch: split out `struct interactive_options`\n 9:  915376b78a = 12:  d8ba71c015 add-patch: remove dependency on \"add-interactive\" subsystem\n10:  3fa285e5b8 ! 13:  b56722b519 add-patch: add support for in-memory index patching\n    @@ add-patch.c: static int patch_update_file(struct add_p_state *s,\n      \t\t\t\terror(_(\"'git apply' failed\"));\n      \t\t}\n     -\t\tif (repo_read_index(s->r) >= 0)\n    -+\t\tread_index_from(s->index, s->index_file, s->r->gitdir);\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 -:  ---------- > 14:  5a6b18855e wt-status: provide function to expose status for trees\n11:  81e16d3c01 ! 15:  4701b4dff2 builtin/history: implement \"split\" subcommand\n    @@ Commit message\n         Signed-off-by: Patrick Steinhardt <ps@pks.im>\n     \n      ## Documentation/git-history.adoc ##\n    -@@ Documentation/git-history.adoc: SYNOPSIS\n    - [synopsis]\n    - git history drop [<options>] <revision>\n    - git history reorder [<options>] <revision> --(before|after)=<revision>\n    -+git history split [<options>] <revision> [--] [<pathspec>...]\n    +@@ Documentation/git-history.adoc: git history continue\n    + git history quit\n    + git history drop <commit>\n    + git history reorder <commit> (--before=<following-commit>|--after=<preceding-commit>)\n    ++git history split [<options>] <commit> [--] [<pathspec>...]\n      \n      DESCRIPTION\n      -----------\n    -@@ Documentation/git-history.adoc: reorder <revision> (--before=<revision>|--after=<revision>)::\n    - \tcommit. The commits must be related to one another and must be\n    - \treachable from the current `HEAD` commit.\n    +@@ Documentation/git-history.adoc: child commits, as that would lead to an empty branch.\n    + \tbe related to one another and must be reachable from the current `HEAD`\n    + \tcommit.\n      \n    -+split <revision> [--message=<message>] [--] [<pathspec>...]::\n    -+\tInteractively split up the commit into two commits by choosing\n    ++`split [--message=<message>] <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    @@ Documentation/git-history.adoc: reorder <revision> (--before=<revision>|--after=\n     +\tcommit.\n     ++\n     +The commit message of the new commit will be asked for by launching the\n    -+configured editor. Authorship of the commit will be the same as for the\n    -+original commit.\n    ++configured editor, unless it has been specified with the `-m` option.\n    ++Authorship of the commit will be the same as for the 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 about the _<pathspec>_ syntax,\n    -+see the 'pathspec' entry.\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    - EXAMPLES\n    - --------\n    + The following commands are used to manage an interrupted history-rewriting\n    + operation:\n      \n     @@ Documentation/git-history.adoc: f44a46e third\n      bf7438d first\n      ----------\n      \n    -+* Split a commit.\n    -++\n    ++Split a commit\n    ++~~~~~~~~~~~~~~\n    ++\n     +----------\n     +$ git log --stat --oneline\n     +3f81232 (HEAD -> main) original\n    @@ builtin/history.c\n     +#define USE_THE_REPOSITORY_VARIABLE\n     +\n      #include \"builtin.h\"\n    + #include \"branch.h\"\n     +#include \"cache-tree.h\"\n      #include \"commit.h\"\n      #include \"commit-reach.h\"\n    @@ builtin/history.c\n      #include \"sequencer.h\"\n     +#include \"sparse-index.h\"\n      \n    - static int collect_commits(struct repository *repo,\n    - \t\t\t   struct commit *old_commit,\n    + static int cmd_history_abort(int argc,\n    + \t\t\t     const char **argv,\n     @@ builtin/history.c: static int cmd_history_reorder(int argc,\n      \treturn ret;\n      }\n      \n    ++static void change_data_free(void *util, const char *str UNUSED)\n    ++{\n    ++\tstruct wt_status_change_data *d = util;\n    ++\tfree(d->rename_source);\n    ++\tfree(d);\n    ++}\n    ++\n    ++static int fill_commit_message(struct repository *repo,\n    ++\t\t\t       const struct object_id *old_tree,\n    ++\t\t\t       const struct object_id *new_tree,\n    ++\t\t\t       const char *default_message,\n    ++\t\t\t       const char *provided_message,\n    ++\t\t\t       const char *action,\n    ++\t\t\t       struct strbuf *out)\n    ++{\n    ++\tif (!provided_message) {\n    ++\t\tstruct wt_status s;\n    ++\t\tconst char *path = git_path_commit_editmsg();\n    ++\t\tconst char *hint =\n    ++\t\t\t_(\"Please enter the commit message for the %s changes. Lines starting\\n\"\n    ++\t\t\t  \"with '%s' will be kept; you may remove them yourself if you want to.\\n\");\n    ++\n    ++\t\tstrbuf_addstr(out, default_message);\n    ++\t\tstrbuf_addch(out, '\\n');\n    ++\t\tstrbuf_commented_addf(out, comment_line_str, hint, action, comment_line_str);\n    ++\t\twrite_file_buf(path, out->buf, out->len);\n    ++\n    ++\t\twt_status_prepare(repo, &s);\n    ++\t\tFREE_AND_NULL(s.branch);\n    ++\t\ts.ahead_behind_flags = AHEAD_BEHIND_QUICK;\n    ++\t\ts.commit_template = 1;\n    ++\t\ts.colopts = 0;\n    ++\t\ts.display_comment_prefix = 1;\n    ++\t\ts.hints = 0;\n    ++\t\ts.use_color = 0;\n    ++\t\ts.whence = FROM_COMMIT;\n    ++\t\ts.committable = 1;\n    ++\n    ++\t\ts.fp = fopen(git_path_commit_editmsg(), \"a\");\n    ++\t\tif (!s.fp)\n    ++\t\t\treturn error_errno(_(\"could not open '%s'\"), git_path_commit_editmsg());\n    ++\n    ++\t\twt_status_collect_changes_trees(&s, old_tree, new_tree);\n    ++\t\twt_status_print(&s);\n    ++\t\twt_status_collect_free_buffers(&s);\n    ++\t\tstring_list_clear_func(&s.change, change_data_free);\n    ++\n    ++\t\tstrbuf_reset(out);\n    ++\t\tif (launch_editor(path, out, NULL)) {\n    ++\t\t\tfprintf(stderr, _(\"Please supply the message using either -m or -F option.\\n\"));\n    ++\t\t\treturn -1;\n    ++\t\t}\n    ++\t\tstrbuf_stripspace(out, comment_line_str);\n    ++\n    ++\t} else {\n    ++\t\tstrbuf_addstr(out, provided_message);\n    ++\t}\n    ++\n    ++\tcleanup_message(out, COMMIT_MSG_CLEANUP_ALL, 0);\n    ++\n    ++\tif (!out->len) {\n    ++\t\tfprintf(stderr, _(\"Aborting commit due to empty commit message.\\n\"));\n    ++\t\treturn -1;\n    ++\t}\n    ++\n    ++\treturn 0;\n    ++}\n    ++\n     +static int split_commit(struct repository *repo,\n     +\t\t\tstruct commit *original_commit,\n     +\t\t\tstruct pathspec *pathspec,\n    @@ builtin/history.c: static int cmd_history_reorder(int argc,\n     +\tstruct object_id original_commit_tree_oid, parent_tree_oid;\n     +\tconst char *original_message, *original_body, *ptr;\n     +\tchar original_commit_oid[GIT_MAX_HEXSZ + 1];\n    -+\tchar *split_message_path = NULL, *original_author = NULL;\n    ++\tchar *original_author = NULL;\n     +\tstruct commit_list *parents = NULL;\n     +\tstruct commit *first_commit;\n     +\tstruct tree *split_tree;\n    @@ builtin/history.c: static int cmd_history_reorder(int argc,\n     +\tif (ptr)\n     +\t\toriginal_author = xmemdupz(ptr, len);\n     +\n    -+\t/*\n    -+\t * But we do ask the user for a new commit message. This is in contrast\n    -+\t * to the second commit, where we'll retain the original commit\n    -+\t * message.\n    -+\t */\n    -+\tif (!commit_message) {\n    -+\t\tsplit_message_path = repo_git_path(repo, \"SPLIT_MSG\");\n    -+\t\tstrbuf_addch(&split_message, '\\n');\n    -+\t\tstrbuf_commented_addf(&split_message, comment_line_str,\n    -+\t\t\t\t      _(\"Please enter a commit message for the split-out changes.\"));\n    -+\t\twrite_file_buf(split_message_path, split_message.buf, split_message.len);\n    -+\n    -+\t\tstrbuf_reset(&split_message);\n    -+\t\tif (launch_editor(split_message_path, &split_message, NULL)) {\n    -+\t\t\tfprintf(stderr, _(\"Please supply the message using either -m or -F option.\\n\"));\n    -+\t\t\tret = -1;\n    -+\t\t\tgoto out;\n    -+\t\t}\n    -+\t\tstrbuf_stripspace(&split_message, comment_line_str);\n    -+\t} else {\n    -+\t\tstrbuf_addstr(&split_message, commit_message);\n    -+\t}\n    -+\tcleanup_message(&split_message, COMMIT_MSG_CLEANUP_ALL, 0);\n    ++\tret = fill_commit_message(repo, &parent_tree_oid, &split_tree->object.oid,\n    ++\t\t\t\t  \"\", commit_message, \"split-out\", &split_message);\n    ++\tif (ret < 0)\n    ++\t\tgoto out;\n     +\n     +\tret = commit_tree(split_message.buf, split_message.len, &split_tree->object.oid,\n     +\t\t\t  original_commit->parents, &out[0], original_author, NULL);\n    @@ builtin/history.c: static int cmd_history_reorder(int argc,\n     +\tstrbuf_release(&split_message);\n     +\tstrbuf_release(&index_file);\n     +\tfree_commit_list(parents);\n    -+\tfree(split_message_path);\n     +\tfree(original_author);\n     +\trelease_index(&index);\n     +\treturn ret;\n    @@ builtin/history.c: static int cmd_history_reorder(int argc,\n     +\t\t\t     struct repository *repo)\n     +{\n     +\tconst char * const usage[] = {\n    -+\t\tN_(\"git history split [<options>] <revision>\"),\n    ++\t\tN_(\"git history split [<options>] <commit>\"),\n     +\t\tNULL,\n     +\t};\n     +\tconst char *commit_message = NULL;\n    @@ builtin/history.c: static int cmd_history_reorder(int argc,\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 revision\"));\n    ++\t\tgoto out;\n    ++\t}\n     +\trepo_config(repo, git_default_config, NULL);\n     +\n     +\toriginal_commit = lookup_commit_reference_by_name(argv[0]);\n    @@ builtin/history.c: static int cmd_history_reorder(int argc,\n     +\treplace_commits(&commits, &original_commit->object.oid,\n     +\t\t\tsplit_commits, ARRAY_SIZE(split_commits));\n     +\n    -+\t/*\n    -+\t * And now we pick commits in the new order on top of either the root\n    -+\t * commit or on top the old commit's parent.\n    -+\t */\n    -+\tret = apply_commits(repo, &commits, head,\n    -+\t\t\t    original_commit->parents ? original_commit->parents->item : NULL,\n    -+\t\t\t    \"split\");\n    ++\tret = apply_commits(repo, &commits, head, original_commit, \"split\");\n     +\tif (ret < 0)\n     +\t\tgoto out;\n     +\n    @@ builtin/history.c: static int cmd_history_reorder(int argc,\n      \t\tconst char **argv,\n      \t\tconst char *prefix,\n     @@ builtin/history.c: int cmd_history(int argc,\n    - \tconst char * const usage[] = {\n    - \t\tN_(\"git history drop [<options>] <revision>\"),\n    - \t\tN_(\"git history reorder [<options>] <revision> --(before|after)=<revision>\"),\n    -+\t\tN_(\"git history split [<options>] <revision> [--] [<pathspec>...]\"),\n    + \t\tN_(\"git history quit\"),\n    + \t\tN_(\"git history drop <commit>\"),\n    + \t\tN_(\"git history reorder <commit> (--before=<following-commit>|--after=<preceding-commit>)\"),\n    ++\t\tN_(\"git history split [<options>] <commit> [--] [<pathspec>...]\"),\n      \t\tNULL,\n      \t};\n      \tparse_opt_subcommand_fn *fn = NULL;\n    - \tstruct option options[] = {\n    +@@ builtin/history.c: int cmd_history(int argc,\n    + \t\tOPT_SUBCOMMAND(\"quit\", &fn, cmd_history_quit),\n      \t\tOPT_SUBCOMMAND(\"drop\", &fn, cmd_history_drop),\n      \t\tOPT_SUBCOMMAND(\"reorder\", &fn, cmd_history_reorder),\n     +\t\tOPT_SUBCOMMAND(\"split\", &fn, cmd_history_split),\n    @@ builtin/history.c: int cmd_history(int argc,\n     \n      ## t/meson.build ##\n     @@ t/meson.build: integration_tests = [\n    -   't3438-rebase-broken-files.sh',\n    -   't3450-history-drop.sh',\n    -   't3451-history-reorder.sh',\n    -+  't3452-history-split.sh',\n    +   't3450-history.sh',\n    +   't3451-history-drop.sh',\n    +   't3452-history-reorder.sh',\n    ++  't3453-history-split.sh',\n        't3500-cherry.sh',\n        't3501-revert-cherry-pick.sh',\n        't3502-cherry-pick-merge.sh',\n     \n    - ## t/t3452-history-split.sh (new) ##\n    + ## t/t3453-history-split.sh (new) ##\n     @@\n     +#!/bin/sh\n     +\n    @@ t/t3452-history-split.sh (new)\n     +\t)\n     +'\n     +\n    ++test_expect_success 'refuses to work with changes in the worktree or index' '\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\techo changed >bar &&\n    ++\t\ttest_must_fail git history split -m message HEAD 2>err <<-EOF &&\n    ++\t\ty\n    ++\t\tn\n    ++\t\tEOF\n    ++\t\ttest_grep \"Your local changes to the following files would be overwritten\" err &&\n    ++\n    ++\t\tgit add bar &&\n    ++\t\ttest_must_fail git history split -m message HEAD 2>err <<-EOF &&\n    ++\t\ty\n    ++\t\tn\n    ++\t\tEOF\n    ++\t\ttest_grep \"Your local changes to the following files would be overwritten\" err\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/t3452-history-split.sh (new)\n     +\t\tgit add . &&\n     +\t\tgit commit -m split-me &&\n     +\n    ++\t\tgit symbolic-ref HEAD >expect &&\n     +\t\tset_fake_editor &&\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\tsplit-me\n    @@ t/t3452-history-split.sh (new)\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\ttest_must_fail git history split HEAD -m \"\" <<-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 'can specify message via option' '\n     +\ttest_when_finished \"rm -rf repo\" &&\n     +\tgit init repo &&\n    @@ t/t3452-history-split.sh (new)\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\tcp \"$1\" . &&\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\tcat >expect <<-EOF &&\n    ++\n    ++\t\t# Please enter the commit message for the split-out changes. Lines starting\n    ++\t\t# with ${SQ}#${SQ} will be kept; you may remove them yourself if you want to.\n    ++\t\t# Changes to be committed:\n    ++\t\t#\tnew file:   bar\n    ++\t\t#\n    ++\t\tEOF\n    ++\t\ttest_cmp expect COMMIT_EDITMSG &&\n    ++\n    ++\t\texpect_log <<-EOF\n    ++\t\tsplit-me\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 -:  ---------- > 16:  3e3587d375 builtin/history: implement \"reword\" subcommand\n\n---\nbase-commit: c44beea485f0f2feaf460e2ac87fdd5608d63cf0\nchange-id: 20250819-b4-pks-history-builtin-83398f9a05f0\n\n"},{"id":"524826","messageId":"20250824-b4-pks-history-builtin-v2-1-964ac12f65bd@pks.im","threadId":"63986","inReplyTo":"20250824-b4-pks-history-builtin-v2-0-964ac12f65bd@pks.im","subject":"[PATCH RFC v2 01/16] sequencer: optionally skip printing commit summary","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-08-24T17:42:14Z","receivedAt":"2025-08-24T17:42:42Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"When picking commits by using for example git-cherry-pick(1) we end up\nprinting a commit summary that gives the reader information around what\nexactly we have been picking:\n\n```\n$ git cherry-pick main\n[other 76c8456] bar\n Date: Tue Aug 19 08:07:26 2025 +0200\n 1 file changed, 1 insertion(+)\n create mode 100644 bar\n```\n\nWhile useful for some commands, we're about to introduce a new command\nwhere this output will be less so. But right now there is no way to\ndisable printing this commit summary.\n\nIntroduce a new `skip_commit_summary` replay option that does so.\nPersist the option into the sequencer configuration so that it persists\nacross different processes, e.g. when we need to stop due to a merge\nconflict.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n sequencer.c | 12 +++++++++---\n sequencer.h |  1 +\n 2 files changed, 10 insertions(+), 3 deletions(-)\n\ndiff --git a/sequencer.c b/sequencer.c\nindex aaf2e4df64..7066cdc939 100644\n--- a/sequencer.c\n+++ b/sequencer.c\n@@ -1742,7 +1742,7 @@ static int do_commit(struct repository *r,\n \t\t\trefs_delete_ref(get_main_ref_store(r), \"\",\n \t\t\t\t\t\"CHERRY_PICK_HEAD\", NULL, REF_NO_DEREF);\n \t\t\tunlink(git_path_merge_msg(r));\n-\t\t\tif (!is_rebase_i(opts))\n+\t\t\tif (!is_rebase_i(opts) && !opts->skip_commit_summary)\n \t\t\t\tprint_commit_summary(r, NULL, &oid,\n \t\t\t\t\t\tSUMMARY_SHOW_AUTHOR_DATE);\n \t\t\treturn res;\n@@ -3139,8 +3139,12 @@ static int populate_opts_cb(const char *key, const char *value,\n \telse if (!strcmp(key, \"options.default-msg-cleanup\")) {\n \t\topts->explicit_cleanup = 1;\n \t\topts->default_msg_cleanup = get_cleanup_mode(value, 1);\n-\t} else\n+\t} else if (!strcmp(key, \"options.skip-commit-summary\")) {\n+\t\topts->skip_commit_summary =\n+\t\t\tgit_config_bool_or_int(key, value, ctx->kvi, &error_flag);\n+\t} else {\n \t\treturn error(_(\"invalid key: %s\"), key);\n+\t}\n \n \tif (!error_flag)\n \t\treturn error(_(\"invalid value for '%s': '%s'\"), key, value);\n@@ -3698,11 +3702,13 @@ static int save_opts(struct replay_opts *opts)\n \t\t\t\t\"options.allow-rerere-auto\", NULL,\n \t\t\t\topts->allow_rerere_auto == RERERE_AUTOUPDATE ?\n \t\t\t\t\"true\" : \"false\");\n-\n \tif (opts->explicit_cleanup)\n \t\tres |= repo_config_set_in_file_gently(the_repository, opts_file,\n \t\t\t\t\"options.default-msg-cleanup\", NULL,\n \t\t\t\tdescribe_cleanup_mode(opts->default_msg_cleanup));\n+\tif (opts->skip_commit_summary)\n+\t\tres |= repo_config_set_in_file_gently(the_repository, opts_file,\n+\t\t\t\t\t\"options.skip-commit-summary\", NULL, \"true\");\n \treturn res;\n }\n \ndiff --git a/sequencer.h b/sequencer.h\nindex 304ba4b4d3..1767fd737e 100644\n--- a/sequencer.h\n+++ b/sequencer.h\n@@ -52,6 +52,7 @@ struct replay_opts {\n \tint keep_redundant_commits;\n \tint verbose;\n \tint quiet;\n+\tint skip_commit_summary;\n \tint reschedule_failed_exec;\n \tint committer_date_is_author_date;\n \tint ignore_date;\n\n-- \n2.51.0.308.g032396e0da.dirty\n\n"},{"id":"524827","messageId":"20250824-b4-pks-history-builtin-v2-2-964ac12f65bd@pks.im","threadId":"63986","inReplyTo":"20250824-b4-pks-history-builtin-v2-0-964ac12f65bd@pks.im","subject":"[PATCH RFC v2 02/16] sequencer: add option to rewind HEAD after picking commits","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-08-24T17:42:15Z","receivedAt":"2025-08-24T17:42:48Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"While the sequencer infrastructure knows to rewind \"HEAD\" to whatever it\nwas pointing to before a rebase, it doesn't do the same for non-rebase\noperations like cherry-picks. This is because the expectation is that\nthe user directly picks commits on top of whatever \"HEAD\" points to, and\nwe advance the reference pointed to by \"HEAD\" instead of updating it\ndirectly.\n\nWe're about to introduce a new command though that needs to detach\n\"HEAD\" while being more similar to git-cherry-pick(1) rathen than to\ngit-rebase(1). As such, we'll want to restore \"HEAD\" to point to the\nbranch that we started on while not using the more heavy-weight rebase\nmachinery.\n\nIntroduce a new option `restore_head_target` to do so. Persist the\noption into the sequencer configuration so that it persists across\ndifferent processes, e.g. when we need to stop due to a merge conflict.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n sequencer.c | 27 ++++++++++++++++++++++++++-\n sequencer.h |  3 +++\n 2 files changed, 29 insertions(+), 1 deletion(-)\n\ndiff --git a/sequencer.c b/sequencer.c\nindex 7066cdc939..bff181df76 100644\n--- a/sequencer.c\n+++ b/sequencer.c\n@@ -413,6 +413,7 @@ void replay_opts_release(struct replay_opts *opts)\n \tstruct replay_ctx *ctx = opts->ctx;\n \n \tfree(opts->gpg_sign);\n+\tfree(opts->restore_head_target);\n \tfree(opts->reflog_action);\n \tfree(opts->default_strategy);\n \tfree(opts->strategy);\n@@ -3142,6 +3143,8 @@ static int populate_opts_cb(const char *key, const char *value,\n \t} else if (!strcmp(key, \"options.skip-commit-summary\")) {\n \t\topts->skip_commit_summary =\n \t\t\tgit_config_bool_or_int(key, value, ctx->kvi, &error_flag);\n+\t} else if (!strcmp(key, \"options.restore-head-target\")) {\n+\t\tgit_config_string_dup(&opts->restore_head_target, key, value);\n \t} else {\n \t\treturn error(_(\"invalid key: %s\"), key);\n \t}\n@@ -3709,6 +3712,10 @@ static int save_opts(struct replay_opts *opts)\n \tif (opts->skip_commit_summary)\n \t\tres |= repo_config_set_in_file_gently(the_repository, opts_file,\n \t\t\t\t\t\"options.skip-commit-summary\", NULL, \"true\");\n+\tif (opts->restore_head_target)\n+\t\tres |= repo_config_set_in_file_gently(the_repository, opts_file,\n+\t\t\t\t\"options.restore-head-target\", NULL, opts->restore_head_target);\n+\n \treturn res;\n }\n \n@@ -5177,6 +5184,23 @@ static int pick_commits(struct repository *r,\n \t\t\treturn -1;\n \t}\n \n+\tif (opts->restore_head_target) {\n+\t\tstruct reset_head_opts reset_opts = { 0 };\n+\t\tconst char *msg;\n+\n+\t\tmsg = reflog_message(opts, \"finish\", \"returning to %s\", opts->restore_head_target);\n+\n+\t\treset_opts.branch = opts->restore_head_target;\n+\t\treset_opts.flags = RESET_HEAD_REFS_ONLY;\n+\t\treset_opts.branch_msg = msg;\n+\t\treset_opts.head_msg = msg;\n+\n+\t\tif (reset_head(r, &reset_opts)) {\n+\t\t\terror(_(\"could not switch HEAD back to %s\"), opts->restore_head_target);\n+\t\t\treturn -1;\n+\t\t}\n+\t}\n+\n \t/*\n \t * Sequence of picks finished successfully; cleanup by\n \t * removing the .git/sequencer directory\n@@ -5533,7 +5557,8 @@ int sequencer_pick_revisions(struct repository *r,\n \tif (opts->revs->cmdline.nr == 1 &&\n \t    opts->revs->cmdline.rev->whence == REV_CMD_REV &&\n \t    opts->revs->no_walk &&\n-\t    !opts->revs->cmdline.rev->flags) {\n+\t    !opts->revs->cmdline.rev->flags &&\n+\t    !opts->restore_head_target) {\n \t\tstruct commit *cmit;\n \n \t\tif (prepare_revision_walk(opts->revs)) {\ndiff --git a/sequencer.h b/sequencer.h\nindex 1767fd737e..a905f6afc7 100644\n--- a/sequencer.h\n+++ b/sequencer.h\n@@ -72,6 +72,9 @@ struct replay_opts {\n \t/* Reflog */\n \tchar *reflog_action;\n \n+\t/* Reference to which HEAD shall be reset to after the operation. */\n+\tchar *restore_head_target;\n+\n \t/* placeholder commit for -i --root */\n \tstruct object_id squash_onto;\n \tint have_squash_onto;\n\n-- \n2.51.0.308.g032396e0da.dirty\n\n"},{"id":"524828","messageId":"20250824-b4-pks-history-builtin-v2-3-964ac12f65bd@pks.im","threadId":"63986","inReplyTo":"20250824-b4-pks-history-builtin-v2-0-964ac12f65bd@pks.im","subject":"[PATCH RFC v2 03/16] sequencer: introduce new history editing mode","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-08-24T17:42:16Z","receivedAt":"2025-08-24T17:42:54Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Introduce a new history editing mode into our sequencer machinery. This\nmode is basically the same as `REBASE_CHERRY`, but will be used by the\nnew git-history(1) command that is to be introduced in a subsequent\ncommit.\n\nNote that the advice already points towards the git-history(1) command.\nThis advice is bogus right now, but we'll introduce the relevant infra\nin a subsequent commit.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n sequencer.c        | 130 ++++++++++++++++++++++++++++++++++++++++++-----------\n sequencer.h        |   3 +-\n t/t3450-history.sh |  12 +++++\n 3 files changed, 119 insertions(+), 26 deletions(-)\n\ndiff --git a/sequencer.c b/sequencer.c\nindex bff181df76..898ac1a2a8 100644\n--- a/sequencer.c\n+++ b/sequencer.c\n@@ -465,6 +465,8 @@ static const char *action_name(const struct replay_opts *opts)\n \t\treturn N_(\"cherry-pick\");\n \tcase REPLAY_INTERACTIVE_REBASE:\n \t\treturn N_(\"rebase\");\n+\tcase REPLAY_HISTORY_EDIT:\n+\t\treturn N_(\"history edit\");\n \t}\n \tdie(_(\"unknown action: %d\"), opts->action);\n }\n@@ -557,6 +559,13 @@ static void print_advice(struct repository *r, int show_hint,\n \t\t\t\t\t    \"You can instead skip this commit with \\\"git revert --skip\\\".\\n\"\n \t\t\t\t\t    \"To abort and get back to the state before \\\"git revert\\\",\\n\"\n \t\t\t\t\t    \"run \\\"git revert --abort\\\".\"));\n+\t\telse if (opts->action == REPLAY_HISTORY_EDIT)\n+\t\t\tadvise_if_enabled(ADVICE_MERGE_CONFLICT,\n+\t\t\t\t\t  _(\"After resolving the conflicts, mark them with\\n\"\n+\t\t\t\t\t    \"\\\"git add/rm <pathspec>\\\", then run\\n\"\n+\t\t\t\t\t    \"\\\"git history continue\\\".\\n\"\n+\t\t\t\t\t    \"To abort and get back to the state before \\\"git history\\\",\\n\"\n+\t\t\t\t\t    \"run \\\"git history abort\\\".\"));\n \t\telse\n \t\t\tBUG(\"unexpected pick action in print_advice()\");\n \t}\n@@ -1742,6 +1751,8 @@ static int do_commit(struct repository *r,\n \t\tif (!res) {\n \t\t\trefs_delete_ref(get_main_ref_store(r), \"\",\n \t\t\t\t\t\"CHERRY_PICK_HEAD\", NULL, REF_NO_DEREF);\n+\t\t\trefs_delete_ref(get_main_ref_store(r), \"\",\n+\t\t\t\t\t\"HISTORY_EDIT_HEAD\", NULL, REF_NO_DEREF);\n \t\t\tunlink(git_path_merge_msg(r));\n \t\t\tif (!is_rebase_i(opts) && !opts->skip_commit_summary)\n \t\t\t\tprint_commit_summary(r, NULL, &oid,\n@@ -2491,16 +2502,24 @@ static int do_pick_commit(struct repository *r,\n \t * However, if the merge did not even start, then we don't want to\n \t * write it at all.\n \t */\n-\tif ((command == TODO_PICK || command == TODO_REWORD ||\n-\t     command == TODO_EDIT) && !opts->no_commit &&\n-\t    (res == 0 || res == 1) &&\n-\t    refs_update_ref(get_main_ref_store(the_repository), NULL, \"CHERRY_PICK_HEAD\", &commit->object.oid, NULL,\n-\t\t\t    REF_NO_DEREF, UPDATE_REFS_MSG_ON_ERR))\n-\t\tres = -1;\n-\tif (command == TODO_REVERT && ((opts->no_commit && res == 0) || res == 1) &&\n-\t    refs_update_ref(get_main_ref_store(the_repository), NULL, \"REVERT_HEAD\", &commit->object.oid, NULL,\n-\t\t\t    REF_NO_DEREF, UPDATE_REFS_MSG_ON_ERR))\n-\t\tres = -1;\n+\tif (opts->action == REPLAY_HISTORY_EDIT && command == TODO_PICK &&\n+\t    !opts->no_commit && (res == 0 || res == 1)) {\n+\t\tif (refs_update_ref(get_main_ref_store(the_repository), NULL,\n+\t\t\t\t    \"HISTORY_EDIT_HEAD\", &commit->object.oid, NULL,\n+\t\t\t\t    REF_NO_DEREF, UPDATE_REFS_MSG_ON_ERR))\n+\t\t\tres = -1;\n+\t} else if ((command == TODO_PICK || command == TODO_REWORD ||\n+\t\t   command == TODO_EDIT) && !opts->no_commit && (res == 0 || res == 1)) {\n+\t\tif (refs_update_ref(get_main_ref_store(the_repository), NULL,\n+\t\t\t\t    \"CHERRY_PICK_HEAD\", &commit->object.oid, NULL,\n+\t\t\t\t    REF_NO_DEREF, UPDATE_REFS_MSG_ON_ERR))\n+\t\t\tres = -1;\n+\t} else if (command == TODO_REVERT && ((opts->no_commit && res == 0) || res == 1)) {\n+\t\tif (refs_update_ref(get_main_ref_store(the_repository), NULL,\n+\t\t\t\t    \"REVERT_HEAD\", &commit->object.oid, NULL,\n+\t\t\t\t    REF_NO_DEREF, UPDATE_REFS_MSG_ON_ERR))\n+\t\t\tres = -1;\n+\t}\n \n \tif (res) {\n \t\terror(command == TODO_REVERT\n@@ -2526,6 +2545,8 @@ static int do_pick_commit(struct repository *r,\n \t\tunlink(git_path_merge_msg(r));\n \t\trefs_delete_ref(get_main_ref_store(r), \"\", \"AUTO_MERGE\",\n \t\t\t\tNULL, REF_NO_DEREF);\n+\t\trefs_delete_ref(get_main_ref_store(r), \"\", \"HISTORY_EDIT_HEAD\",\n+\t\t\t\tNULL, REF_NO_DEREF);\n \t\tfprintf(stderr,\n \t\t\t_(\"dropping %s %s -- patch contents already upstream\\n\"),\n \t\t\toid_to_hex(&commit->object.oid), msg.subject);\n@@ -2843,12 +2864,17 @@ static int parse_insn_line(struct repository *r, struct replay_opts *opts,\n \treturn 0;\n }\n \n-int sequencer_get_last_command(struct repository *r UNUSED, enum replay_action *action)\n+int sequencer_get_last_command(struct repository *r, enum replay_action *action)\n {\n \tconst char *todo_file, *bol;\n \tstruct strbuf buf = STRBUF_INIT;\n \tint ret = 0;\n \n+\tif (refs_ref_exists(get_main_ref_store(r), \"HISTORY_EDIT_HEAD\")) {\n+\t\t*action = REPLAY_HISTORY_EDIT;\n+\t\treturn 0;\n+\t}\n+\n \ttodo_file = git_path_todo_file();\n \tif (strbuf_read_file(&buf, todo_file, 0) < 0) {\n \t\tif (errno == ENOENT || errno == ENOTDIR)\n@@ -2995,6 +3021,15 @@ void sequencer_post_commit_cleanup(struct repository *r, int verbose)\n \t\tneed_cleanup = 1;\n \t}\n \n+\tif (refs_ref_exists(get_main_ref_store(r), \"HISTORY_EDIT_HEAD\")) {\n+\t\tif (!refs_delete_ref(get_main_ref_store(r), \"\",\n+\t\t\t\t     \"HISTORY_EDIT_HEAD\", NULL, REF_NO_DEREF) &&\n+\t\t    verbose)\n+\t\t\twarning(_(\"cancelling a history edit in progress\"));\n+\t\topts.action = REPLAY_HISTORY_EDIT;\n+\t\tneed_cleanup = 1;\n+\t}\n+\n \tif (refs_ref_exists(get_main_ref_store(r), \"REVERT_HEAD\")) {\n \t\tif (!refs_delete_ref(get_main_ref_store(r), \"\", \"REVERT_HEAD\",\n \t\t\t\t     NULL, REF_NO_DEREF) &&\n@@ -3052,17 +3087,29 @@ static int read_populate_todo(struct repository *r,\n \t\treturn error(_(\"no commits parsed.\"));\n \n \tif (!is_rebase_i(opts)) {\n-\t\tenum todo_command valid =\n-\t\t\topts->action == REPLAY_PICK ? TODO_PICK : TODO_REVERT;\n+\t\tenum todo_command valid;\n \t\tint i;\n \n-\t\tfor (i = 0; i < todo_list->nr; i++)\n+\t\tswitch (opts->action) {\n+\t\tcase REPLAY_PICK:\n+\t\tcase REPLAY_HISTORY_EDIT:\n+\t\t\tvalid = TODO_PICK;\n+\t\t\tbreak;\n+\t\tdefault:\n+\t\t\tvalid = TODO_REVERT;\n+\t\t\tbreak;\n+\t\t}\n+\n+\t\tfor (i = 0; i < todo_list->nr; i++) {\n \t\t\tif (valid == todo_list->items[i].command)\n \t\t\t\tcontinue;\n \t\t\telse if (valid == TODO_PICK)\n-\t\t\t\treturn error(_(\"cannot cherry-pick during a revert.\"));\n+\t\t\t\treturn error(_(\"cannot cherry-pick during a %s.\"),\n+\t\t\t\t\t     action_name(opts));\n \t\t\telse\n-\t\t\t\treturn error(_(\"cannot revert during a cherry-pick.\"));\n+\t\t\t\treturn error(_(\"cannot revert during a %s.\"),\n+\t\t\t\t\t     action_name(opts));\n+\t\t}\n \t}\n \n \tif (is_rebase_i(opts)) {\n@@ -3353,15 +3400,25 @@ int write_basic_state(struct replay_opts *opts, const char *head_name,\n static int walk_revs_populate_todo(struct todo_list *todo_list,\n \t\t\t\tstruct replay_opts *opts)\n {\n-\tenum todo_command command = opts->action == REPLAY_PICK ?\n-\t\tTODO_PICK : TODO_REVERT;\n-\tconst char *command_string = todo_command_info[command].str;\n+\tenum todo_command command;\n+\tconst char *command_string;\n \tconst char *encoding;\n \tstruct commit *commit;\n \n \tif (prepare_revs(opts))\n \t\treturn -1;\n \n+\tswitch (opts->action) {\n+\tcase REPLAY_PICK:\n+\tcase REPLAY_HISTORY_EDIT:\n+\t\tcommand = TODO_PICK;\n+\t\tbreak;\n+\tdefault:\n+\t\tcommand = TODO_REVERT;\n+\t\tbreak;\n+\t}\n+\n+\tcommand_string = todo_command_info[command].str;\n \tencoding = get_log_output_encoding();\n \n \twhile ((commit = get_revision(opts->revs))) {\n@@ -3412,6 +3469,11 @@ static int create_seq_dir(struct repository *r)\n \t\t\tin_progress_advice =\n \t\t\t_(\"try \\\"git cherry-pick (--continue | %s--abort | --quit)\\\"\");\n \t\t\tbreak;\n+\t\tcase REPLAY_HISTORY_EDIT:\n+\t\t\tin_progress_error = _(\"history edit is already in progress\");\n+\t\t\tin_progress_advice =\n+\t\t\t_(\"try \\\"git history (continue | abort | quit)\\\"\");\n+\t\t\tbreak;\n \t\tdefault:\n \t\t\tBUG(\"unexpected action in create_seq_dir\");\n \t\t}\n@@ -3472,13 +3534,14 @@ static int reset_merge(const struct object_id *oid)\n \treturn run_command(&cmd);\n }\n \n-static int rollback_single_pick(struct repository *r)\n+static int rollback_single_pick(struct repository *r, struct replay_opts *opts)\n {\n \tstruct object_id head_oid;\n \n \tif (!refs_ref_exists(get_main_ref_store(r), \"CHERRY_PICK_HEAD\") &&\n+\t    !refs_ref_exists(get_main_ref_store(r), \"HISTORY_EDIT_HEAD\") &&\n \t    !refs_ref_exists(get_main_ref_store(r), \"REVERT_HEAD\"))\n-\t\treturn error(_(\"no cherry-pick or revert in progress\"));\n+\t\treturn error(_(\"no %s in progress\"), action_name(opts));\n \tif (refs_read_ref_full(get_main_ref_store(the_repository), \"HEAD\", 0, &head_oid, NULL))\n \t\treturn error(_(\"cannot resolve HEAD\"));\n \tif (is_null_oid(&head_oid))\n@@ -3509,7 +3572,7 @@ int sequencer_rollback(struct repository *r, struct replay_opts *opts)\n \t\t * If CHERRY_PICK_HEAD or REVERT_HEAD indicates\n \t\t * a single-cherry-pick in progress, abort that.\n \t\t */\n-\t\treturn rollback_single_pick(r);\n+\t\treturn rollback_single_pick(r, opts);\n \t}\n \tif (!f)\n \t\treturn error_errno(_(\"cannot open '%s'\"), git_path_head_file());\n@@ -5213,8 +5276,9 @@ static int continue_single_pick(struct repository *r, struct replay_opts *opts)\n \tstruct child_process cmd = CHILD_PROCESS_INIT;\n \n \tif (!refs_ref_exists(get_main_ref_store(r), \"CHERRY_PICK_HEAD\") &&\n+\t    !refs_ref_exists(get_main_ref_store(r), \"HISTORY_EDIT_HEAD\") &&\n \t    !refs_ref_exists(get_main_ref_store(r), \"REVERT_HEAD\"))\n-\t\treturn error(_(\"no cherry-pick or revert in progress\"));\n+\t\treturn error(_(\"no %s in progress\"), action_name(opts));\n \n \tcmd.git_cmd = 1;\n \tstrvec_push(&cmd.args, \"commit\");\n@@ -5393,6 +5457,14 @@ static int commit_staged_changes(struct repository *r,\n \t\t\tgoto out;\n \t\t}\n \n+\t\tif (refs_ref_exists(get_main_ref_store(r),\n+\t\t\t\t    \"HISTORY_EDIT_HEAD\") &&\n+\t\t    refs_delete_ref(get_main_ref_store(r), \"\",\n+\t\t\t\t    \"HISTORY_EDIT_HEAD\", NULL, REF_NO_DEREF)) {\n+\t\t\tret = error(_(\"could not remove HISTORY_EDIT_HEAD\"));\n+\t\t\tgoto out;\n+\t\t}\n+\n \t\tif (unlink(git_path_merge_msg(r)) && errno != ENOENT) {\n \t\t\tret = error_errno(_(\"could not remove '%s'\"),\n \t\t\t\t\t  git_path_merge_msg(r));\n@@ -5471,6 +5543,7 @@ int sequencer_continue(struct repository *r, struct replay_opts *opts)\n \t\t/* Verify that the conflict has been resolved */\n \t\tif (refs_ref_exists(get_main_ref_store(r),\n \t\t\t\t    \"CHERRY_PICK_HEAD\") ||\n+\t\t    refs_ref_exists(get_main_ref_store(r), \"HISTORY_EDIT_HEAD\") ||\n \t\t    refs_ref_exists(get_main_ref_store(r), \"REVERT_HEAD\")) {\n \t\t\tres = continue_single_pick(r, opts);\n \t\t\tif (res)\n@@ -5505,8 +5578,15 @@ static int single_pick(struct repository *r,\n \tint check_todo;\n \tstruct todo_item item;\n \n-\titem.command = opts->action == REPLAY_PICK ?\n-\t\t\tTODO_PICK : TODO_REVERT;\n+\tswitch (opts->action) {\n+\tcase REPLAY_PICK:\n+\tcase REPLAY_HISTORY_EDIT:\n+\t\titem.command = TODO_PICK;\n+\t\tbreak;\n+\tdefault:\n+\t\titem.command = TODO_REVERT;\n+\t\tbreak;\n+\t}\n \titem.commit = cmit;\n \n \treturn do_pick_commit(r, &item, opts, 0, &check_todo);\ndiff --git a/sequencer.h b/sequencer.h\nindex a905f6afc7..082fbe3e35 100644\n--- a/sequencer.h\n+++ b/sequencer.h\n@@ -21,7 +21,8 @@ extern const char *rebase_resolvemsg;\n enum replay_action {\n \tREPLAY_REVERT,\n \tREPLAY_PICK,\n-\tREPLAY_INTERACTIVE_REBASE\n+\tREPLAY_INTERACTIVE_REBASE,\n+\tREPLAY_HISTORY_EDIT,\n };\n \n enum commit_msg_cleanup_mode {\ndiff --git a/t/t3450-history.sh b/t/t3450-history.sh\nnew file mode 100755\nindex 0000000000..9eb1ed6749\n--- /dev/null\n+++ b/t/t3450-history.sh\n@@ -0,0 +1,12 @@\n+#!/bin/sh\n+\n+test_description='tests for git-history command'\n+\n+. ./test-lib.sh\n+\n+test_expect_success 'refuses to do anything without subcommand' '\n+\ttest_must_fail git history 2>err &&\n+\ttest_grep foo err\n+'\n+\n+test_done\n\n-- \n2.51.0.308.g032396e0da.dirty\n\n"},{"id":"524829","messageId":"20250824-b4-pks-history-builtin-v2-4-964ac12f65bd@pks.im","threadId":"63986","inReplyTo":"20250824-b4-pks-history-builtin-v2-0-964ac12f65bd@pks.im","subject":"[PATCH RFC v2 04/16] sequencer: stop using `the_repository` in `sequencer_remove_state()`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-08-24T17:42:17Z","receivedAt":"2025-08-24T17:43:03Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Refactor `sequencer_remove_state()` to stop using `the_repository` in\nfavor of a passed-in repository.\n\nA lot of the other code in our sequencer infrastructure still uses\n`the_repository`, but this bigger refactoring is left for another day.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/rebase.c |  4 ++--\n builtin/revert.c |  2 +-\n sequencer.c      | 18 +++++++++---------\n sequencer.h      |  2 +-\n 4 files changed, 13 insertions(+), 13 deletions(-)\n\ndiff --git a/builtin/rebase.c b/builtin/rebase.c\nindex 3c85768d29..66824ae136 100644\n--- a/builtin/rebase.c\n+++ b/builtin/rebase.c\n@@ -568,7 +568,7 @@ static int finish_rebase(struct rebase_options *opts)\n \t\tstruct replay_opts replay = REPLAY_OPTS_INIT;\n \n \t\treplay.action = REPLAY_INTERACTIVE_REBASE;\n-\t\tret = sequencer_remove_state(&replay);\n+\t\tret = sequencer_remove_state(the_repository, &replay);\n \t\treplay_opts_release(&replay);\n \t} else {\n \t\tstrbuf_addstr(&dir, opts->state_dir);\n@@ -1405,7 +1405,7 @@ int cmd_rebase(int argc,\n \t\t\tstruct replay_opts replay = REPLAY_OPTS_INIT;\n \n \t\t\treplay.action = REPLAY_INTERACTIVE_REBASE;\n-\t\t\tret = sequencer_remove_state(&replay);\n+\t\t\tret = sequencer_remove_state(the_repository, &replay);\n \t\t\treplay_opts_release(&replay);\n \t\t} else {\n \t\t\tstrbuf_reset(&buf);\ndiff --git a/builtin/revert.c b/builtin/revert.c\nindex c3f92b585d..6456cf2171 100644\n--- a/builtin/revert.c\n+++ b/builtin/revert.c\n@@ -263,7 +263,7 @@ static int run_sequencer(int argc, const char **argv, const char *prefix,\n \tfree(options);\n \n \tif (cmd == 'q') {\n-\t\tint ret = sequencer_remove_state(opts);\n+\t\tint ret = sequencer_remove_state(the_repository, opts);\n \t\tif (!ret)\n \t\t\tremove_branch_state(the_repository, 0);\n \t\treturn ret;\ndiff --git a/sequencer.c b/sequencer.c\nindex 898ac1a2a8..749e30c2e6 100644\n--- a/sequencer.c\n+++ b/sequencer.c\n@@ -425,7 +425,7 @@ void replay_opts_release(struct replay_opts *opts)\n \tfree(opts->ctx);\n }\n \n-int sequencer_remove_state(struct replay_opts *opts)\n+int sequencer_remove_state(struct repository *repo, struct replay_opts *opts)\n {\n \tstruct strbuf buf = STRBUF_INIT;\n \tint ret = 0;\n@@ -437,7 +437,7 @@ int sequencer_remove_state(struct replay_opts *opts)\n \t\t\tchar *eol = strchr(p, '\\n');\n \t\t\tif (eol)\n \t\t\t\t*eol = '\\0';\n-\t\t\tif (refs_delete_ref(get_main_ref_store(the_repository), \"(rebase) cleanup\", p, NULL, 0) < 0) {\n+\t\t\tif (refs_delete_ref(get_main_ref_store(repo), \"(rebase) cleanup\", p, NULL, 0) < 0) {\n \t\t\t\twarning(_(\"could not delete '%s'\"), p);\n \t\t\t\tret = -1;\n \t\t\t}\n@@ -3048,7 +3048,7 @@ void sequencer_post_commit_cleanup(struct repository *r, int verbose)\n \tif (!have_finished_the_last_pick())\n \t\tgoto out;\n \n-\tsequencer_remove_state(&opts);\n+\tsequencer_remove_state(the_repository, &opts);\n out:\n \treplay_opts_release(&opts);\n }\n@@ -3601,7 +3601,7 @@ int sequencer_rollback(struct repository *r, struct replay_opts *opts)\n \tif (reset_merge(&oid))\n \t\tgoto fail;\n \tstrbuf_release(&buf);\n-\treturn sequencer_remove_state(opts);\n+\treturn sequencer_remove_state(the_repository, opts);\n fail:\n \tstrbuf_release(&buf);\n \treturn -1;\n@@ -4903,7 +4903,7 @@ static int checkout_onto(struct repository *r, struct replay_opts *opts,\n \t};\n \tif (reset_head(r, &ropts)) {\n \t\tapply_autostash(rebase_path_autostash());\n-\t\tsequencer_remove_state(opts);\n+\t\tsequencer_remove_state(the_repository, opts);\n \t\treturn error(_(\"could not detach HEAD\"));\n \t}\n \n@@ -5268,7 +5268,7 @@ static int pick_commits(struct repository *r,\n \t * Sequence of picks finished successfully; cleanup by\n \t * removing the .git/sequencer directory\n \t */\n-\treturn sequencer_remove_state(opts);\n+\treturn sequencer_remove_state(the_repository, opts);\n }\n \n static int continue_single_pick(struct repository *r, struct replay_opts *opts)\n@@ -6607,7 +6607,7 @@ int complete_action(struct repository *r, struct replay_opts *opts, unsigned fla\n \n \tif (count_commands(todo_list) == 0) {\n \t\tapply_autostash(rebase_path_autostash());\n-\t\tsequencer_remove_state(opts);\n+\t\tsequencer_remove_state(the_repository, opts);\n \n \t\treturn error(_(\"nothing to do\"));\n \t}\n@@ -6618,12 +6618,12 @@ int complete_action(struct repository *r, struct replay_opts *opts, unsigned fla\n \t\treturn -1;\n \telse if (res == -2) {\n \t\tapply_autostash(rebase_path_autostash());\n-\t\tsequencer_remove_state(opts);\n+\t\tsequencer_remove_state(the_repository, opts);\n \n \t\treturn -1;\n \t} else if (res == -3) {\n \t\tapply_autostash(rebase_path_autostash());\n-\t\tsequencer_remove_state(opts);\n+\t\tsequencer_remove_state(the_repository, opts);\n \t\ttodo_list_release(&new_todo);\n \n \t\treturn error(_(\"nothing to do\"));\ndiff --git a/sequencer.h b/sequencer.h\nindex 082fbe3e35..0e0e7301b8 100644\n--- a/sequencer.h\n+++ b/sequencer.h\n@@ -170,7 +170,7 @@ int sequencer_continue(struct repository *repo, struct replay_opts *opts);\n int sequencer_rollback(struct repository *repo, struct replay_opts *opts);\n int sequencer_skip(struct repository *repo, struct replay_opts *opts);\n void replay_opts_release(struct replay_opts *opts);\n-int sequencer_remove_state(struct replay_opts *opts);\n+int sequencer_remove_state(struct repository *repo, struct replay_opts *opts);\n \n #define TODO_LIST_KEEP_EMPTY (1U << 0)\n #define TODO_LIST_SHORTEN_IDS (1U << 1)\n\n-- \n2.51.0.308.g032396e0da.dirty\n\n"},{"id":"524830","messageId":"20250824-b4-pks-history-builtin-v2-5-964ac12f65bd@pks.im","threadId":"63986","inReplyTo":"20250824-b4-pks-history-builtin-v2-0-964ac12f65bd@pks.im","subject":"[PATCH RFC v2 05/16] cache-tree: allow writing in-memory index as tree","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-08-24T17:42:18Z","receivedAt":"2025-08-24T17:43:10Z","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       | 5 ++---\n cache-tree.h       | 3 ++-\n 3 files changed, 6 insertions(+), 5 deletions(-)\n\ndiff --git a/builtin/checkout.c b/builtin/checkout.c\nindex f9453473fe2..43583c8d1be 100644\n--- a/builtin/checkout.c\n+++ b/builtin/checkout.c\n@@ -902,7 +902,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 66ef2becbe0..029ec933abe 100644\n--- a/cache-tree.c\n+++ b/cache-tree.c\n@@ -699,11 +699,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 \n@@ -723,7 +723,6 @@ struct tree* write_in_core_index_as_tree(struct repository *repo) {\n \treturn lookup_tree(repo, &index_state->cache_tree->oid);\n }\n \n-\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 {\n \tint entries, was_valid;\ndiff --git a/cache-tree.h b/cache-tree.h\nindex b82c4963e7c..f8bddae5235 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.51.0.308.g032396e0da.dirty\n\n"},{"id":"524831","messageId":"20250824-b4-pks-history-builtin-v2-6-964ac12f65bd@pks.im","threadId":"63986","inReplyTo":"20250824-b4-pks-history-builtin-v2-0-964ac12f65bd@pks.im","subject":"[PATCH RFC v2 06/16] builtin: add new \"history\" command","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-08-24T17:42:19Z","receivedAt":"2025-08-24T17:43:14Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"When rewriting history via git-rebase(1) there are a couple of very\ncommon use cases:\n\n  - The ordering of two commits should be reversed.\n\n  - A commit should be split up into two commits.\n\n  - A commit should be dropped from the history completely.\n\n  - Multiple commits should be squashed into one.\n\nWhile these operations are all doable, it often feels needlessly cludgy\nto do so by doing an interactive rebase, using the editor to say what\none wants, and then perform the actions. Furthermore, some operations\nlike splitting up a commit into two are way more involved than that and\nrequire a whole series of commands.\n\nAdd a new \"history\" command to plug this gap. This command will have\nseveral different subcommands to imperatively rewrite history for common\nuse cases like the above. These commands will be implemented in\nsubsequent commits.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n .gitignore                     |  1 +\n Documentation/git-history.adoc | 45 ++++++++++++++++++++++++++++++++++++++++++\n Documentation/meson.build      |  1 +\n Makefile                       |  1 +\n builtin.h                      |  1 +\n builtin/history.c              | 20 +++++++++++++++++++\n command-list.txt               |  1 +\n git.c                          |  1 +\n meson.build                    |  1 +\n 9 files changed, 72 insertions(+)\n\ndiff --git a/.gitignore b/.gitignore\nindex 04c444404e..3932d4d618 100644\n--- a/.gitignore\n+++ b/.gitignore\n@@ -77,6 +77,7 @@\n /git-grep\n /git-hash-object\n /git-help\n+/git-history\n /git-hook\n /git-http-backend\n /git-http-fetch\ndiff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc\nnew file mode 100644\nindex 0000000000..1537960374\n--- /dev/null\n+++ b/Documentation/git-history.adoc\n@@ -0,0 +1,45 @@\n+git-history(1)\n+==============\n+\n+NAME\n+----\n+git-history - EXPERIMENTAL: Rewrite history of the current branch\n+\n+SYNOPSIS\n+--------\n+[synopsis]\n+git history [<options>]\n+\n+DESCRIPTION\n+-----------\n+\n+Rewrite history by rearranging or modifying specific commits in the\n+history.\n+\n+This command is similar to linkgit:git-rebase[1] and uses the same\n+underlying machinery. You should use rebases if you either want to\n+reapply a range of commits onto a different base, or interactive rebases\n+if you want to edit a range of commits.\n+\n+Note that this command does not (yet) work with histories that contain\n+merges. You should use linkgit:git-rebase[1] with the `--rebase-merges`\n+flag instead.\n+\n+THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.\n+\n+COMMANDS\n+--------\n+\n+This command requires a subcommand. Several subcommands are available to\n+rewrite history in different ways:\n+\n+CONFIGURATION\n+-------------\n+\n+include::includes/cmd-config-section-all.adoc[]\n+\n+include::config/sequencer.adoc[]\n+\n+GIT\n+---\n+Part of the linkgit:git[1] suite\ndiff --git a/Documentation/meson.build b/Documentation/meson.build\nindex 4404c623f0..a30b5307fd 100644\n--- a/Documentation/meson.build\n+++ b/Documentation/meson.build\n@@ -64,6 +64,7 @@ manpages = {\n   'git-gui.adoc' : 1,\n   'git-hash-object.adoc' : 1,\n   'git-help.adoc' : 1,\n+  'git-history.adoc' : 1,\n   'git-hook.adoc' : 1,\n   'git-http-backend.adoc' : 1,\n   'git-http-fetch.adoc' : 1,\ndiff --git a/Makefile b/Makefile\nindex e11340c1ae..bed6eda5e6 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -1261,6 +1261,7 @@ BUILTIN_OBJS += builtin/get-tar-commit-id.o\n BUILTIN_OBJS += builtin/grep.o\n BUILTIN_OBJS += builtin/hash-object.o\n BUILTIN_OBJS += builtin/help.o\n+BUILTIN_OBJS += builtin/history.o\n BUILTIN_OBJS += builtin/hook.o\n BUILTIN_OBJS += builtin/index-pack.o\n BUILTIN_OBJS += builtin/init-db.o\ndiff --git a/builtin.h b/builtin.h\nindex bff13e3069..2934f4479a 100644\n--- a/builtin.h\n+++ b/builtin.h\n@@ -172,6 +172,7 @@ int cmd_get_tar_commit_id(int argc, const char **argv, const char *prefix, struc\n int cmd_grep(int argc, const char **argv, const char *prefix, struct repository *repo);\n int cmd_hash_object(int argc, const char **argv, const char *prefix, struct repository *repo);\n int cmd_help(int argc, const char **argv, const char *prefix, struct repository *repo);\n+int cmd_history(int argc, const char **argv, const char *prefix, struct repository *repo);\n int cmd_hook(int argc, const char **argv, const char *prefix, struct repository *repo);\n int cmd_index_pack(int argc, const char **argv, const char *prefix, struct repository *repo);\n int cmd_init_db(int argc, const char **argv, const char *prefix, struct repository *repo);\ndiff --git a/builtin/history.c b/builtin/history.c\nnew file mode 100644\nindex 0000000000..d1a40368e0\n--- /dev/null\n+++ b/builtin/history.c\n@@ -0,0 +1,20 @@\n+#include \"builtin.h\"\n+#include \"gettext.h\"\n+#include \"parse-options.h\"\n+\n+int cmd_history(int argc,\n+\t\tconst char **argv,\n+\t\tconst char *prefix,\n+\t\tstruct repository *repo UNUSED)\n+{\n+\tconst char * const usage[] = {\n+\t\tN_(\"git history [<options>]\"),\n+\t\tNULL,\n+\t};\n+\tstruct option options[] = {\n+\t\tOPT_END(),\n+\t};\n+\n+\targc = parse_options(argc, argv, prefix, options, usage, 0);\n+\treturn 0;\n+}\ndiff --git a/command-list.txt b/command-list.txt\nindex b7ade3ab9f..f95f0ce926 100644\n--- a/command-list.txt\n+++ b/command-list.txt\n@@ -115,6 +115,7 @@ git-grep                                mainporcelain           info\n git-gui                                 mainporcelain\n git-hash-object                         plumbingmanipulators\n git-help                                ancillaryinterrogators          complete\n+git-history                             mainporcelain           history\n git-hook                                purehelpers\n git-http-backend                        synchingrepositories\n git-http-fetch                          synchelpers\ndiff --git a/git.c b/git.c\nindex 83eac0aeab..9d2cba2906 100644\n--- a/git.c\n+++ b/git.c\n@@ -560,6 +560,7 @@ static struct cmd_struct commands[] = {\n \t{ \"grep\", cmd_grep, RUN_SETUP_GENTLY },\n \t{ \"hash-object\", cmd_hash_object },\n \t{ \"help\", cmd_help },\n+\t{ \"history\", cmd_history, RUN_SETUP },\n \t{ \"hook\", cmd_hook, RUN_SETUP },\n \t{ \"index-pack\", cmd_index_pack, RUN_SETUP_GENTLY | NO_PARSEOPT },\n \t{ \"init\", cmd_init_db },\ndiff --git a/meson.build b/meson.build\nindex 5dd299b496..0e40778a23 100644\n--- a/meson.build\n+++ b/meson.build\n@@ -603,6 +603,7 @@ builtin_sources = [\n   'builtin/grep.c',\n   'builtin/hash-object.c',\n   'builtin/help.c',\n+  'builtin/history.c',\n   'builtin/hook.c',\n   'builtin/index-pack.c',\n   'builtin/init-db.c',\n\n-- \n2.51.0.308.g032396e0da.dirty\n\n"},{"id":"524832","messageId":"20250824-b4-pks-history-builtin-v2-7-964ac12f65bd@pks.im","threadId":"63986","inReplyTo":"20250824-b4-pks-history-builtin-v2-0-964ac12f65bd@pks.im","subject":"[PATCH RFC v2 07/16] builtin/history: introduce subcommands to manage interrupted rewrites","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-08-24T17:42:20Z","receivedAt":"2025-08-24T17:43:20Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Introduce subcommands to manage the sequencer state for git-history(1).\nThese aren't really useful yet, but will become useful in subsequent\ncommits where we will introduce git-history(1) subcommands that actually\nedit history.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/git-history.adoc |  20 +++++++-\n builtin/history.c              | 114 +++++++++++++++++++++++++++++++++++++++--\n t/meson.build                  |   3 +-\n t/t3450-history.sh             |  32 +++++++++++-\n 4 files changed, 163 insertions(+), 6 deletions(-)\n\ndiff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc\nindex 1537960374..3e9a789b83 100644\n--- a/Documentation/git-history.adoc\n+++ b/Documentation/git-history.adoc\n@@ -8,7 +8,9 @@ git-history - EXPERIMENTAL: Rewrite history of the current branch\n SYNOPSIS\n --------\n [synopsis]\n-git history [<options>]\n+git history abort\n+git history continue\n+git history quit\n \n DESCRIPTION\n -----------\n@@ -33,6 +35,22 @@ COMMANDS\n This command requires a subcommand. Several subcommands are available to\n rewrite history in different ways:\n \n+The following commands are used to manage an interrupted history-rewriting\n+operation:\n+\n+`abort`::\n+\tAbort the history-rewriting operation and reset HEAD to the original\n+\tbranch.\n+\n+`continue`::\n+\tRestart the history-rewriting process after having resolved a merge\n+\tconflict.\n+\n+`quit`::\n+\tAbort the history-rewriting operation but `HEAD` is not reset back to\n+\tthe original branch. The index and working tree are also left unchanged\n+\tas a result.\n+\n CONFIGURATION\n -------------\n \ndiff --git a/builtin/history.c b/builtin/history.c\nindex d1a40368e0..0ad45dbfef 100644\n--- a/builtin/history.c\n+++ b/builtin/history.c\n@@ -1,20 +1,128 @@\n #include \"builtin.h\"\n+#include \"branch.h\"\n #include \"gettext.h\"\n #include \"parse-options.h\"\n+#include \"sequencer.h\"\n+\n+static int cmd_history_abort(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\tN_(\"git history abort\"),\n+\t\tNULL,\n+\t};\n+\tstruct option options[] = {\n+\t\tOPT_END(),\n+\t};\n+\tstruct replay_opts opts = REPLAY_OPTS_INIT;\n+\tint ret;\n+\n+\targc = parse_options(argc, argv, prefix, options, usage, 0);\n+\tif (argc) {\n+\t\tret = error(_(\"command does not take arguments\"));\n+\t\tgoto out;\n+\t}\n+\n+\topts.action = REPLAY_HISTORY_EDIT;\n+\tret = sequencer_rollback(repo, &opts);\n+\tif (ret)\n+\t\tgoto out;\n+\n+\tret = 0;\n+\n+out:\n+\treplay_opts_release(&opts);\n+\treturn ret;\n+}\n+\n+static int cmd_history_continue(int argc,\n+\t\t\t\tconst char **argv,\n+\t\t\t\tconst char *prefix,\n+\t\t\t\tstruct repository *repo)\n+{\n+\tconst char * const usage[] = {\n+\t\tN_(\"git history continue\"),\n+\t\tNULL,\n+\t};\n+\tstruct option options[] = {\n+\t\tOPT_END(),\n+\t};\n+\tstruct replay_opts opts = REPLAY_OPTS_INIT;\n+\tint ret;\n+\n+\targc = parse_options(argc, argv, prefix, options, usage, 0);\n+\tif (argc) {\n+\t\tret = error(_(\"command does not take arguments\"));\n+\t\tgoto out;\n+\t}\n+\n+\topts.action = REPLAY_HISTORY_EDIT;\n+\tret = sequencer_continue(repo, &opts);\n+\tif (ret)\n+\t\tgoto out;\n+\n+\tret = 0;\n+\n+out:\n+\treplay_opts_release(&opts);\n+\treturn ret;\n+}\n+\n+static int cmd_history_quit(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\tN_(\"git history quit\"),\n+\t\tNULL,\n+\t};\n+\tstruct option options[] = {\n+\t\tOPT_END(),\n+\t};\n+\tstruct replay_opts opts = REPLAY_OPTS_INIT;\n+\tint ret;\n+\n+\targc = parse_options(argc, argv, prefix, options, usage, 0);\n+\tif (argc) {\n+\t\tret = error(_(\"command does not take arguments\"));\n+\t\tgoto out;\n+\t}\n+\n+\topts.action = REPLAY_HISTORY_EDIT;\n+\tret = sequencer_remove_state(repo, &opts);\n+\tif (ret)\n+\t\tgoto out;\n+\tremove_branch_state(repo, 0);\n+\n+\tret = 0;\n+\n+out:\n+\treplay_opts_release(&opts);\n+\treturn ret;\n+}\n \n int cmd_history(int argc,\n \t\tconst char **argv,\n \t\tconst char *prefix,\n-\t\tstruct repository *repo UNUSED)\n+\t\tstruct repository *repo)\n {\n \tconst char * const usage[] = {\n-\t\tN_(\"git history [<options>]\"),\n+\t\tN_(\"git history abort\"),\n+\t\tN_(\"git history continue\"),\n+\t\tN_(\"git history quit\"),\n \t\tNULL,\n \t};\n+\tparse_opt_subcommand_fn *fn = NULL;\n \tstruct option options[] = {\n+\t\tOPT_SUBCOMMAND(\"abort\", &fn, cmd_history_abort),\n+\t\tOPT_SUBCOMMAND(\"continue\", &fn, cmd_history_continue),\n+\t\tOPT_SUBCOMMAND(\"quit\", &fn, cmd_history_quit),\n \t\tOPT_END(),\n \t};\n \n \targc = parse_options(argc, argv, prefix, options, usage, 0);\n-\treturn 0;\n+\treturn fn(argc, argv, prefix, repo);\n }\ndiff --git a/t/meson.build b/t/meson.build\nindex bbeba1a8d5..966d7c14f4 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -376,6 +376,7 @@ integration_tests = [\n   't3436-rebase-more-options.sh',\n   't3437-rebase-fixup-options.sh',\n   't3438-rebase-broken-files.sh',\n+  't3450-history.sh',\n   't3500-cherry.sh',\n   't3501-revert-cherry-pick.sh',\n   't3502-cherry-pick-merge.sh',\n@@ -1214,4 +1215,4 @@ if perl.found() and time.found()\n       timeout: 0,\n     )\n   endforeach\n-endif\n\\ No newline at end of file\n+endif\ndiff --git a/t/t3450-history.sh b/t/t3450-history.sh\nindex 9eb1ed6749..aa9d44c03b 100755\n--- a/t/t3450-history.sh\n+++ b/t/t3450-history.sh\n@@ -6,7 +6,37 @@ test_description='tests for git-history command'\n \n test_expect_success 'refuses to do anything without subcommand' '\n \ttest_must_fail git history 2>err &&\n-\ttest_grep foo err\n+\ttest_grep \"need a subcommand\" err\n+'\n+\n+test_expect_success 'abort complains about arguments' '\n+\ttest_must_fail git history abort foo 2>err &&\n+\ttest_grep \"command does not take arguments\" err\n+'\n+\n+test_expect_success 'abort complains when no history edit is active' '\n+\ttest_must_fail git history abort 2>err &&\n+\ttest_grep \"no history edit in progress\" err\n+'\n+\n+test_expect_success 'continue complains about arguments' '\n+\ttest_must_fail git history continue foo 2>err &&\n+\ttest_grep \"command does not take arguments\" err\n+'\n+\n+test_expect_success 'continue complains when no history edit is active' '\n+\ttest_must_fail git history continue 2>err &&\n+\ttest_grep \"no history edit in progress\" err\n+'\n+\n+test_expect_success 'quit complains about arguments' '\n+\ttest_must_fail git history quit foo 2>err &&\n+\ttest_grep \"command does not take arguments\" err\n+'\n+\n+test_expect_success 'quit does not complain when no history edit is active' '\n+\tgit history quit 2>err &&\n+\ttest_must_be_empty err\n '\n \n test_done\n\n-- \n2.51.0.308.g032396e0da.dirty\n\n"},{"id":"524833","messageId":"20250824-b4-pks-history-builtin-v2-8-964ac12f65bd@pks.im","threadId":"63986","inReplyTo":"20250824-b4-pks-history-builtin-v2-0-964ac12f65bd@pks.im","subject":"[PATCH RFC v2 08/16] builtin/history: implement \"drop\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-08-24T17:42:21Z","receivedAt":"2025-08-24T17:43:27Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"It is a fairly common operation to perform an interactive rebase so that\none of the commits can be dropped from history. Doing this is not very\nhard in general, but still requires the user to perform multiple steps:\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. Edit the instruction sheet to drop that commit.\n\nThis is needlessly complex for such a supposedly-trivial operation.\nFurthermore, the second step doesn't account for certain edge cases like\nfor example dropping the root commit.\n\nIntroduce a new \"drop\" subcommand to make this use case significantly\nsimpler: all the user needs to do is to say `git history drop $COMMIT`\nand they're done.\n\nNote that for now, this command only allows users to drop a single\ncommit at once. It should be easy enough though to expand the command at\na later point in time to support dropping whole commit ranges.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/git-history.adoc |  27 ++++\n builtin/history.c              | 287 +++++++++++++++++++++++++++++++++++++++++\n t/meson.build                  |   1 +\n t/t3451-history-drop.sh        | 174 +++++++++++++++++++++++++\n 4 files changed, 489 insertions(+)\n\ndiff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc\nindex 3e9a789b83..db5b292994 100644\n--- a/Documentation/git-history.adoc\n+++ b/Documentation/git-history.adoc\n@@ -11,6 +11,7 @@ SYNOPSIS\n git history abort\n git history continue\n git history quit\n+git history drop <commit>\n \n DESCRIPTION\n -----------\n@@ -35,6 +36,15 @@ COMMANDS\n This command requires a subcommand. Several subcommands are available to\n rewrite history in different ways:\n \n+`drop <commit>`::\n+\tDrop a commit from the history and reapply all children of that\n+\tcommit on top of the commit's parent. The commit that is to be\n+\tdropped must be reachable from the current `HEAD` commit.\n++\n+Dropping the root commit converts the child of that commit into the new\n+root commit. It is invalid to drop a root commit that does not have any\n+child commits, as that would lead to an empty branch.\n+\n The following commands are used to manage an interrupted history-rewriting\n operation:\n \n@@ -51,6 +61,23 @@ operation:\n \tthe original branch. The index and working tree are also left unchanged\n \tas a result.\n \n+EXAMPLES\n+--------\n+\n+Drop a commit from history\n+~~~~~~~~~~~~~~~~~~~~~~~~~~\n+\n+----------\n+$ git log --oneline\n+2d4cd6d third\n+125a0f3 second\n+e098c27 first\n+$ git history drop HEAD~\n+$ git log\n+b1bc1bd third\n+e098c27 first\n+----------\n+\n CONFIGURATION\n -------------\n \ndiff --git a/builtin/history.c b/builtin/history.c\nindex 0ad45dbfef..2132b6a441 100644\n--- a/builtin/history.c\n+++ b/builtin/history.c\n@@ -1,7 +1,16 @@\n #include \"builtin.h\"\n #include \"branch.h\"\n+#include \"commit.h\"\n+#include \"commit-reach.h\"\n+#include \"config.h\"\n+#include \"environment.h\"\n #include \"gettext.h\"\n+#include \"hex.h\"\n+#include \"object-name.h\"\n #include \"parse-options.h\"\n+#include \"refs.h\"\n+#include \"reset.h\"\n+#include \"revision.h\"\n #include \"sequencer.h\"\n \n static int cmd_history_abort(int argc,\n@@ -104,6 +113,282 @@ static int cmd_history_quit(int argc,\n \treturn ret;\n }\n \n+static int collect_commits(struct repository *repo,\n+\t\t\t   struct commit *old_commit,\n+\t\t\t   struct commit *new_commit,\n+\t\t\t   struct strvec *out)\n+{\n+\tstruct setup_revision_opt revision_opts = {\n+\t\t.assume_dashdash = 1,\n+\t};\n+\tstruct strvec revisions = STRVEC_INIT;\n+\tstruct commit_list *from_list = NULL;\n+\tstruct commit *child;\n+\tstruct rev_info rev = { 0 };\n+\tint ret;\n+\n+\t/*\n+\t * Check that the old commit actually is an ancestor of HEAD. If not\n+\t * the whole request becomes nonsensical.\n+\t*/\n+\tif (old_commit) {\n+\t\tcommit_list_insert(old_commit, &from_list);\n+\t\tif (!repo_is_descendant_of(repo, new_commit, from_list)) {\n+\t\t\tret = error(_(\"commit must be reachable from current HEAD commit\"));\n+\t\t\tgoto out;\n+\t\t}\n+\t}\n+\n+\trepo_init_revisions(repo, &rev, NULL);\n+\tstrvec_push(&revisions, \"\");\n+\tstrvec_push(&revisions, oid_to_hex(&new_commit->object.oid));\n+\tif (old_commit)\n+\t\tstrvec_pushf(&revisions, \"^%s\", oid_to_hex(&old_commit->object.oid));\n+\tif (setup_revisions(revisions.nr, revisions.v, &rev, &revision_opts) != 1 ||\n+\t    prepare_revision_walk(&rev)) {\n+\t\tret = error(_(\"revision walk setup failed\"));\n+\t\tgoto out;\n+\t}\n+\n+\twhile ((child = get_revision(&rev))) {\n+\t\tif (old_commit && !child->parents)\n+\t\t\tBUG(\"revision walk did not find child commit\");\n+\t\tif (child->parents && child->parents->next) {\n+\t\t\tret = error(_(\"cannot rearrange commit history with merges\"));\n+\t\t\tgoto out;\n+\t\t}\n+\n+\t\tstrvec_push(out, oid_to_hex(&child->object.oid));\n+\n+\t\tif (child->parents && old_commit &&\n+\t\t    commit_list_contains(old_commit, child->parents))\n+\t\t\tbreak;\n+\t}\n+\n+\t/*\n+\t * Revisions are in newest-order-first. We have to reverse the\n+\t * array though so that we pick the oldest commits first.\n+\t */\n+\tfor (size_t i = 0, j = out->nr - 1; i < j; i++, j--)\n+\t\tSWAP(out->v[i], out->v[j]);\n+\n+\tret = 0;\n+\n+out:\n+\tfree_commit_list(from_list);\n+\tstrvec_clear(&revisions);\n+\trelease_revisions(&rev);\n+\treset_revision_walk();\n+\treturn ret;\n+}\n+\n+static int apply_commits(struct repository *repo,\n+\t\t\t const struct strvec *commits,\n+\t\t\t struct commit *head,\n+\t\t\t struct commit *base,\n+\t\t\t const char *action)\n+{\n+\tstruct setup_revision_opt revision_opts = {\n+\t\t.assume_dashdash = 1,\n+\t};\n+\tstruct replay_opts replay_opts = REPLAY_OPTS_INIT;\n+\tstruct reset_head_opts reset_opts = { 0 };\n+\tstruct object_id root_commit;\n+\tstruct strvec args = STRVEC_INIT;\n+\tstruct strbuf buf = STRBUF_INIT;\n+\tchar hex[GIT_MAX_HEXSZ + 1];\n+\tint ref_flags, ret;\n+\n+\t/*\n+\t * We have performed all safety checks, so we now prepare\n+\t * replaying the commits.\n+\t*/\n+\treplay_opts.action = REPLAY_HISTORY_EDIT;\n+\tsequencer_init_config(&replay_opts);\n+\treplay_opts.quiet = 1;\n+\treplay_opts.skip_commit_summary = 1;\n+\tif (!replay_opts.strategy && replay_opts.default_strategy) {\n+\t\treplay_opts.strategy = replay_opts.default_strategy;\n+\t\treplay_opts.default_strategy = NULL;\n+\t}\n+\n+\tstrvec_push(&args, \"\");\n+\tstrvec_pushv(&args, commits->v);\n+\n+\treplay_opts.revs = xmalloc(sizeof(*replay_opts.revs));\n+\trepo_init_revisions(repo, replay_opts.revs, NULL);\n+\treplay_opts.revs->no_walk = 1;\n+\treplay_opts.revs->unsorted_input = 1;\n+\tif (setup_revisions(args.nr, args.v, replay_opts.revs,\n+\t\t\t    &revision_opts) != 1) {\n+\t\tret = error(_(\"setting up revisions failed\"));\n+\t\tgoto out;\n+\t}\n+\n+\t/*\n+\t * If we're dropping the root commit we first need to create\n+\t * a new empty root. We then instruct the seqencer machinery to\n+\t * squash that root commit with the first commit we're picking\n+\t * onto it.\n+\t */\n+\tif (!base->parents) {\n+\t\tif (commit_tree(\"\", 0, repo->hash_algo->empty_tree, NULL,\n+\t\t\t\t&root_commit, NULL, NULL) < 0) {\n+\t\t\tret = error(_(\"Could not create new root commit\"));\n+\t\t\tgoto out;\n+\t\t}\n+\n+\t\treplay_opts.squash_onto = root_commit;\n+\t\treplay_opts.have_squash_onto = 1;\n+\t\treset_opts.oid = &root_commit;\n+\t} else {\n+\t\treset_opts.oid = &base->parents->item->object.oid;\n+\t}\n+\n+\treplay_opts.restore_head_target =\n+\t\txstrdup_or_null(refs_resolve_ref_unsafe(get_main_ref_store(repo),\n+\t\t\t\t\t\t\t\"HEAD\", 0, NULL, &ref_flags));\n+\tif (!(ref_flags & REF_ISSYMREF))\n+\t\tFREE_AND_NULL(replay_opts.restore_head_target);\n+\n+\t/*\n+\t * Perform a hard-reset to the parent of our commit that is to\n+\t * be dropped. This is the new base onto which we'll pick all\n+\t * the descendants.\n+\t */\n+\tstrbuf_addf(&buf, \"%s (start): checkout %s\", action,\n+\t\t    oid_to_hex_r(hex, reset_opts.oid));\n+\treset_opts.orig_head = &head->object.oid;\n+\treset_opts.flags = RESET_HEAD_DETACH | RESET_ORIG_HEAD;\n+\treset_opts.head_msg = buf.buf;\n+\treset_opts.default_reflog_action = action;\n+\tif (reset_head(repo, &reset_opts) < 0) {\n+\t\tret = error(_(\"could not switch to %s\"), oid_to_hex(reset_opts.oid));\n+\t\tgoto out;\n+\t}\n+\n+\tret = sequencer_pick_revisions(repo, &replay_opts);\n+\tif (ret < 0) {\n+\t\tret = error(_(\"could not pick commits\"));\n+\t\tgoto out;\n+\t} else if (ret > 0) {\n+\t\t/*\n+\t\t * A positive return value indicates we've got a merge\n+\t\t * conflict. Bail out, but don't print a message as\n+\t\t * `sequencer_pick_revisions()` already printed enough\n+\t\t * information.\n+\t\t */\n+\t\tret = -1;\n+\t\tgoto out;\n+\t}\n+\n+\tret = 0;\n+\n+out:\n+\treplay_opts_release(&replay_opts);\n+\tstrbuf_release(&buf);\n+\tstrvec_clear(&args);\n+\treturn ret;\n+}\n+\n+static int cmd_history_drop(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\tN_(\"git history drop <commit>\"),\n+\t\tNULL,\n+\t};\n+\tstruct option options[] = {\n+\t\tOPT_END(),\n+\t};\n+\tstruct commit *commit_to_drop, *head;\n+\tstruct strvec commits = STRVEC_INIT;\n+\tstruct strbuf buf = STRBUF_INIT;\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 single revision\"));\n+\t\tgoto out;\n+\t}\n+\trepo_config(repo, git_default_config, NULL);\n+\n+\tcommit_to_drop = lookup_commit_reference_by_name(argv[0]);\n+\tif (!commit_to_drop) {\n+\t\tret = error(_(\"commit to be dropped cannot be found: %s\"), argv[0]);\n+\t\tgoto out;\n+\t}\n+\tif (commit_to_drop->parents && commit_to_drop->parents->next) {\n+\t\tret = error(_(\"commit to be dropped must not be a merge commit\"));\n+\t\tgoto out;\n+\t}\n+\n+\thead = lookup_commit_reference_by_name(\"HEAD\");\n+\tif (!head) {\n+\t\tret = error(_(\"could not resolve HEAD to a commit\"));\n+\t\tgoto out;\n+\t}\n+\n+\tif (oideq(&commit_to_drop->object.oid, &head->object.oid)) {\n+\t\t/*\n+\t\t * If we want to drop the tip of the current branch we don't\n+\t\t * have to perform any rebase at all. Instead, we simply\n+\t\t * perform a hard reset to the parent commit.\n+\t\t */\n+\t\tstruct reset_head_opts reset_opts = {\n+\t\t\t.orig_head = &head->object.oid,\n+\t\t\t.flags = RESET_ORIG_HEAD,\n+\t\t\t.default_reflog_action = \"drop\",\n+\t\t};\n+\t\tchar hex[GIT_MAX_HEXSZ + 1];\n+\n+\t\tif (!commit_to_drop->parents) {\n+\t\t\tret = error(_(\"cannot drop the only commit on this branch\"));\n+\t\t\tgoto out;\n+\t\t}\n+\n+\t\toid_to_hex_r(hex, &commit_to_drop->parents->item->object.oid);\n+\t\tstrbuf_addf(&buf, \"drop (start): checkout %s\", hex);\n+\t\treset_opts.oid = &commit_to_drop->parents->item->object.oid;\n+\t\treset_opts.head_msg = buf.buf;\n+\n+\t\tif (reset_head(repo, &reset_opts) < 0) {\n+\t\t\tret = error(_(\"could not switch to %s\"), hex);\n+\t\t\tgoto out;\n+\t\t}\n+\t} else {\n+\t\t/*\n+\t\t * Prepare a revision walk from old commit to the commit that is\n+\t\t * about to be dropped. This serves three purposes:\n+\t\t *\n+\t\t *   - We verify that the history doesn't contain any merges.\n+\t\t *     For now, merges aren't yet handled by us.\n+\t\t *\n+\t\t *   - We need to find the child of the commit-to-be-dropped.\n+\t\t *     This child is what will be adopted by the parent of the\n+\t\t *     commit that we are about to drop.\n+\t\t *\n+\t\t *   - We compute the list of commits-to-be-picked.\n+\t\t */\n+\t\tret = collect_commits(repo, commit_to_drop, head, &commits);\n+\t\tif (ret < 0)\n+\t\t\tgoto out;\n+\n+\t\tret = apply_commits(repo, &commits, head, commit_to_drop, \"drop\");\n+\t\tif (ret < 0)\n+\t\t\tgoto out;\n+\t}\n+\n+\tret = 0;\n+\n+out:\n+\tstrvec_clear(&commits);\n+\tstrbuf_release(&buf);\n+\treturn ret;\n+}\n+\n int cmd_history(int argc,\n \t\tconst char **argv,\n \t\tconst char *prefix,\n@@ -113,6 +398,7 @@ int cmd_history(int argc,\n \t\tN_(\"git history abort\"),\n \t\tN_(\"git history continue\"),\n \t\tN_(\"git history quit\"),\n+\t\tN_(\"git history drop <commit>\"),\n \t\tNULL,\n \t};\n \tparse_opt_subcommand_fn *fn = NULL;\n@@ -120,6 +406,7 @@ int cmd_history(int argc,\n \t\tOPT_SUBCOMMAND(\"abort\", &fn, cmd_history_abort),\n \t\tOPT_SUBCOMMAND(\"continue\", &fn, cmd_history_continue),\n \t\tOPT_SUBCOMMAND(\"quit\", &fn, cmd_history_quit),\n+\t\tOPT_SUBCOMMAND(\"drop\", &fn, cmd_history_drop),\n \t\tOPT_END(),\n \t};\n \ndiff --git a/t/meson.build b/t/meson.build\nindex 966d7c14f4..8189c6c561 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -377,6 +377,7 @@ integration_tests = [\n   't3437-rebase-fixup-options.sh',\n   't3438-rebase-broken-files.sh',\n   't3450-history.sh',\n+  't3451-history-drop.sh',\n   't3500-cherry.sh',\n   't3501-revert-cherry-pick.sh',\n   't3502-cherry-pick-merge.sh',\ndiff --git a/t/t3451-history-drop.sh b/t/t3451-history-drop.sh\nnew file mode 100755\nindex 0000000000..325d353d73\n--- /dev/null\n+++ b/t/t3451-history-drop.sh\n@@ -0,0 +1,174 @@\n+#!/bin/sh\n+\n+test_description='tests for git-history drop subcommand'\n+\n+. ./test-lib.sh\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 drop HEAD 2>err &&\n+\t\ttest_grep \"commit to be dropped must not be a merge commit\" err &&\n+\t\ttest_must_fail git history drop HEAD~ 2>err &&\n+\t\ttest_grep \"cannot rearrange commit history with merges\" err\n+\t)\n+'\n+\n+test_expect_success 'refuses to work when history becomes empty' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit base &&\n+\t\ttest_must_fail git history drop HEAD 2>err &&\n+\t\ttest_grep \"cannot drop the only commit on this branch\" err\n+\t)\n+'\n+\n+test_expect_success 'refuses to work with changes in the worktree or index' '\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_commit file file &&\n+\t\techo foo >file &&\n+\t\ttest_must_fail git history drop HEAD 2>err &&\n+\t\ttest_grep \"Your local changes to the following files would be overwritten\" err &&\n+\t\tgit add file &&\n+\t\ttest_must_fail git history drop HEAD 2>err &&\n+\t\ttest_grep \"Your local changes to the following files would be overwritten\" err\n+\t)\n+'\n+\n+test_expect_success 'can drop tip of a branch' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit first &&\n+\t\ttest_commit second &&\n+\t\ttest_commit third &&\n+\n+\t\tgit symbolic-ref HEAD >expect &&\n+\t\tgit history drop HEAD &&\n+\t\tgit symbolic-ref HEAD >actual &&\n+\t\ttest_cmp expect actual &&\n+\n+\t\tcat >expect <<-EOF &&\n+\t\tsecond\n+\t\tfirst\n+\t\tEOF\n+\t\tgit log --format=%s >actual &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+test_expect_success 'can drop commit in the middle' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit first &&\n+\t\ttest_commit second &&\n+\t\ttest_commit third &&\n+\t\ttest_commit fourth &&\n+\t\ttest_commit fifth &&\n+\n+\t\tgit symbolic-ref HEAD >expect &&\n+\t\tgit history drop HEAD~2 &&\n+\t\tgit symbolic-ref HEAD >actual &&\n+\t\ttest_cmp expect actual &&\n+\n+\t\tcat >expect <<-EOF &&\n+\t\tfifth\n+\t\tfourth\n+\t\tsecond\n+\t\tfirst\n+\t\tEOF\n+\t\tgit log --format=%s >actual &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+test_expect_success 'correct order is retained' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit first &&\n+\t\ttest_commit second &&\n+\t\ttest_commit third &&\n+\t\ttest_commit fourth &&\n+\t\ttest_commit fifth &&\n+\t\tgit history drop HEAD~3 &&\n+\t\tcat >expect <<-EOF &&\n+\t\tfifth\n+\t\tfourth\n+\t\tthird\n+\t\tfirst\n+\t\tEOF\n+\t\tgit log --format=%s >actual &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+test_expect_success 'can drop root commit' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit first &&\n+\t\ttest_commit second &&\n+\t\ttest_commit third &&\n+\t\tgit history drop HEAD~2 &&\n+\t\tcat >expect <<-EOF &&\n+\t\tthird\n+\t\tsecond\n+\t\tEOF\n+\t\tgit log --format=%s >actual &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+test_expect_success 'conflicts are detected' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit base &&\n+\t\techo original >file &&\n+\t\tgit add . &&\n+\t\tgit commit -m original &&\n+\t\techo modified >file &&\n+\t\tgit commit -am modified &&\n+\n+\t\ttest_must_fail git history drop HEAD~ >err 2>&1 &&\n+\t\ttest_grep CONFLICT err &&\n+\t\ttest_grep \"git history continue\" err &&\n+\t\techo resolved >file &&\n+\t\tgit add file &&\n+\t\tgit history continue &&\n+\n+\t\tcat >expect <<-EOF &&\n+\t\tmodified\n+\t\tbase\n+\t\tEOF\n+\t\tgit log --format=%s >actual &&\n+\t\ttest_cmp expect actual &&\n+\t\techo resolved >expect &&\n+\t\tgit cat-file -p HEAD:file >actual &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+test_done\n\n-- \n2.51.0.308.g032396e0da.dirty\n\n"},{"id":"524834","messageId":"20250824-b4-pks-history-builtin-v2-9-964ac12f65bd@pks.im","threadId":"63986","inReplyTo":"20250824-b4-pks-history-builtin-v2-0-964ac12f65bd@pks.im","subject":"[PATCH RFC v2 09/16] builtin/history: implement \"reorder\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-08-24T17:42:22Z","receivedAt":"2025-08-24T17:43:44Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"When working in projects where having nice commits matters it's quite\ncommon that developers end up reordering commits a lot. This is\ntypically done via interactive rebases, where they can then rearrange\ncommits in the instruction sheet.\n\nStill, this operation is a frequent-enough operation to provide a more\ndirect way of doing this imperatively. As such, introduce a new\n\"reorder\" subcommand where users can reorder a commit A to come after or\nbefore another commit B:\n\n    $ git log --oneline\n    a978f73 fifth\n    57594ee fourth\n    04eb1c4 third\n    d535e30 second\n    bf7438d first\n\n    $ git history reorder :/fourth --before=:/second\n    $ git log --oneline\n    1610fe0 fifth\n    444f97d third\n    2f90797 second\n    b0ae659 fourth\n    bf7438d first\n\n    $ git history reorder :/fourth --after=:/second\n    $ git log --oneline\n    c48729d fifth\n    f44a46e third\n    26693b8 fourth\n    8cb4171 second\n    bf7438d first\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/git-history.adoc |  35 +++++-\n builtin/history.c              | 130 +++++++++++++++++++++++\n t/meson.build                  |   1 +\n t/t3452-history-reorder.sh     | 234 +++++++++++++++++++++++++++++++++++++++++\n 4 files changed, 399 insertions(+), 1 deletion(-)\n\ndiff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc\nindex db5b292994..b36cd925dd 100644\n--- a/Documentation/git-history.adoc\n+++ b/Documentation/git-history.adoc\n@@ -12,6 +12,7 @@ git history abort\n git history continue\n git history quit\n git history drop <commit>\n+git history reorder <commit> (--before=<following-commit>|--after=<preceding-commit>)\n \n DESCRIPTION\n -----------\n@@ -39,12 +40,18 @@ rewrite history in different ways:\n `drop <commit>`::\n \tDrop a commit from the history and reapply all children of that\n \tcommit on top of the commit's parent. The commit that is to be\n-\tdropped must be reachable from the current `HEAD` commit.\n+\tdropped must be reachable from the currently checked-out commit.\n +\n Dropping the root commit converts the child of that commit into the new\n root commit. It is invalid to drop a root commit that does not have any\n child commits, as that would lead to an empty branch.\n \n+`reorder <commit> (--before=<following-commit>|--after=<preceding-commit>)`::\n+\tMove the commit so that it becomes either the parent of\n+\t<following-commit> or the child of <preceding-commit>. The commits must\n+\tbe related to one another and must be reachable from the current `HEAD`\n+\tcommit.\n+\n The following commands are used to manage an interrupted history-rewriting\n operation:\n \n@@ -78,6 +85,32 @@ b1bc1bd third\n e098c27 first\n ----------\n \n+Reorder a commit\n+~~~~~~~~~~~~~~~~\n+\n+----------\n+$ git log --oneline\n+a978f73 fifth\n+57594ee fourth\n+04eb1c4 third\n+d535e30 second\n+bf7438d first\n+$ git history reorder :/fourth --before=:/second\n+$ git log --oneline\n+1610fe0 fifth\n+444f97d third\n+2f90797 second\n+b0ae659 fourth\n+bf7438d first\n+$ git history reorder :/fourth --after=:/second\n+$ git log --oneline\n+c48729d fifth\n+f44a46e third\n+26693b8 fourth\n+8cb4171 second\n+bf7438d first\n+----------\n+\n CONFIGURATION\n -------------\n \ndiff --git a/builtin/history.c b/builtin/history.c\nindex 2132b6a441..16b516856e 100644\n--- a/builtin/history.c\n+++ b/builtin/history.c\n@@ -182,6 +182,33 @@ static int collect_commits(struct repository *repo,\n \treturn ret;\n }\n \n+static void replace_commits(struct strvec *commits,\n+\t\t\t    const struct object_id *commit_to_replace,\n+\t\t\t    const struct object_id *replacements,\n+\t\t\t    size_t replacements_nr)\n+{\n+\tchar commit_to_replace_oid[GIT_MAX_HEXSZ + 1];\n+\tstruct strvec replacement_oids = STRVEC_INIT;\n+\tbool found = false;\n+\tsize_t i;\n+\n+\toid_to_hex_r(commit_to_replace_oid, commit_to_replace);\n+\tfor (i = 0; i < replacements_nr; i++)\n+\t\tstrvec_push(&replacement_oids, oid_to_hex(&replacements[i]));\n+\n+\tfor (i = 0; i < commits->nr; i++) {\n+\t\tif (strcmp(commits->v[i], commit_to_replace_oid))\n+\t\t\tcontinue;\n+\t\tstrvec_splice(commits, i, 1, replacement_oids.v, replacement_oids.nr);\n+\t\tfound = true;\n+\t\tbreak;\n+\t}\n+\tif (!found)\n+\t\tBUG(\"could not find commit to replace\");\n+\n+\tstrvec_clear(&replacement_oids);\n+}\n+\n static int apply_commits(struct repository *repo,\n \t\t\t const struct strvec *commits,\n \t\t\t struct commit *head,\n@@ -389,6 +416,107 @@ static int cmd_history_drop(int argc,\n \treturn ret;\n }\n \n+static int cmd_history_reorder(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\tN_(\"git history reorder <commit> (--before=<following-commit>|--after=<preceding-commit>)\"),\n+\t\tNULL,\n+\t};\n+\tconst char *before = NULL, *after = NULL;\n+\tstruct option options[] = {\n+\t\tOPT_STRING(0, \"before\", &before, N_(\"commit\"), N_(\"reorder before this commit\")),\n+\t\tOPT_STRING(0, \"after\", &after, N_(\"commit\"), N_(\"reorder after this commit\")),\n+\t\tOPT_END(),\n+\t};\n+\tstruct commit *commit_to_reorder, *head, *anchor, *old;\n+\tstruct strvec commits = STRVEC_INIT;\n+\tstruct object_id replacement[2];\n+\tstruct commit_list *list = NULL;\n+\tint ret;\n+\n+\targc = parse_options(argc, argv, prefix, options, usage, 0);\n+\tif (argc != 1)\n+\t\tdie(_(\"command expects a single revision\"));\n+\tif (!before && !after)\n+\t\tdie(_(\"exactly one option of 'before' or 'after' must be given\"));\n+\tdie_for_incompatible_opt2(!!before, \"before\", !!after, \"after\");\n+\n+\trepo_config(repo, git_default_config, NULL);\n+\n+\tcommit_to_reorder = lookup_commit_reference_by_name(argv[0]);\n+\tif (!commit_to_reorder)\n+\t\tdie(_(\"commit to be reordered cannot be found: %s\"), argv[0]);\n+\tif (commit_to_reorder->parents && commit_to_reorder->parents->next)\n+\t\tdie(_(\"commit to be reordered must not be a merge commit\"));\n+\n+\tanchor = lookup_commit_reference_by_name(before ? before : after);\n+\tif (!commit_to_reorder)\n+\t\tdie(_(\"anchor commit cannot be found: %s\"), before ? before : after);\n+\n+\tif (oideq(&commit_to_reorder->object.oid, &anchor->object.oid))\n+\t\tdie(_(\"commit to reorder and anchor must not be the same\"));\n+\n+\thead = lookup_commit_reference_by_name(\"HEAD\");\n+\tif (!head)\n+\t\tdie(_(\"could not resolve HEAD to a commit\"));\n+\n+\tcommit_list_append(commit_to_reorder, &list);\n+\tif (!repo_is_descendant_of(repo, commit_to_reorder, list))\n+\t\tdie(_(\"reordered commit must be reachable from current HEAD commit\"));\n+\n+\t/*\n+\t * There is no requirement for the user to have either one of the\n+\t * provided commits be the parent or child. We thus have to figure out\n+\t * ourselves which one is which.\n+\t*/\n+\tif (repo_is_descendant_of(repo, anchor, list))\n+\t\told = commit_to_reorder;\n+\telse\n+\t\told = anchor;\n+\n+\t/*\n+\t * Select the whole range of commits, including the boundary commit\n+\t * itself. In case the old commit is the root commit we simply pass no\n+\t * boundary.\n+\t*/\n+\tret = collect_commits(repo, old->parents ? old->parents->item : NULL,\n+\t\t\t      head, &commits);\n+\tif (ret < 0)\n+\t\tgoto out;\n+\n+\t/*\n+\t * Perform the reordering of commits in the strvec. This is done by:\n+\t *\n+\t *   - Deleting the to-be-reordered commit from the range of commits.\n+\t *\n+\t *   - Replacing the anchor commit with the anchor commit plus the\n+\t *     to-be-reordered commit.\n+\t */\n+\tif (before) {\n+\t\treplacement[0] = commit_to_reorder->object.oid;\n+\t\treplacement[1] = anchor->object.oid;\n+\t} else {\n+\t\treplacement[0] = anchor->object.oid;\n+\t\treplacement[1] = commit_to_reorder->object.oid;\n+\t}\n+\treplace_commits(&commits, &commit_to_reorder->object.oid, NULL, 0);\n+\treplace_commits(&commits, &anchor->object.oid, replacement, ARRAY_SIZE(replacement));\n+\n+\tret = apply_commits(repo, &commits, head, old, \"reorder\");\n+\tif (ret < 0)\n+\t\tgoto out;\n+\n+\tret = 0;\n+\n+out:\n+\tfree_commit_list(list);\n+\tstrvec_clear(&commits);\n+\treturn ret;\n+}\n+\n int cmd_history(int argc,\n \t\tconst char **argv,\n \t\tconst char *prefix,\n@@ -399,6 +527,7 @@ int cmd_history(int argc,\n \t\tN_(\"git history continue\"),\n \t\tN_(\"git history quit\"),\n \t\tN_(\"git history drop <commit>\"),\n+\t\tN_(\"git history reorder <commit> (--before=<following-commit>|--after=<preceding-commit>)\"),\n \t\tNULL,\n \t};\n \tparse_opt_subcommand_fn *fn = NULL;\n@@ -407,6 +536,7 @@ int cmd_history(int argc,\n \t\tOPT_SUBCOMMAND(\"continue\", &fn, cmd_history_continue),\n \t\tOPT_SUBCOMMAND(\"quit\", &fn, cmd_history_quit),\n \t\tOPT_SUBCOMMAND(\"drop\", &fn, cmd_history_drop),\n+\t\tOPT_SUBCOMMAND(\"reorder\", &fn, cmd_history_reorder),\n \t\tOPT_END(),\n \t};\n \ndiff --git a/t/meson.build b/t/meson.build\nindex 8189c6c561..2bf7bcab5a 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -378,6 +378,7 @@ integration_tests = [\n   't3438-rebase-broken-files.sh',\n   't3450-history.sh',\n   't3451-history-drop.sh',\n+  't3452-history-reorder.sh',\n   't3500-cherry.sh',\n   't3501-revert-cherry-pick.sh',\n   't3502-cherry-pick-merge.sh',\ndiff --git a/t/t3452-history-reorder.sh b/t/t3452-history-reorder.sh\nnew file mode 100755\nindex 0000000000..49a0784c29\n--- /dev/null\n+++ b/t/t3452-history-reorder.sh\n@@ -0,0 +1,234 @@\n+#!/bin/sh\n+\n+test_description='tests for git-history reorder subcommand'\n+\n+. ./test-lib.sh\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 reorder HEAD --before=HEAD~ 2>err &&\n+\t\ttest_grep \"commit to be reordered must not be a merge commit\" err &&\n+\t\ttest_must_fail git history reorder HEAD~ --after=HEAD 2>err &&\n+\t\ttest_grep \"cannot rearrange commit history with merges\" err\n+\t)\n+'\n+\n+test_expect_success 'refuses to work with changes in the worktree or index' '\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_commit file file &&\n+\t\techo foo >file &&\n+\t\ttest_must_fail git history reorder HEAD --before=HEAD~ 2>err &&\n+\t\ttest_grep \"Your local changes to the following files would be overwritten\" err &&\n+\t\tgit add file &&\n+\t\ttest_must_fail git history reorder HEAD --before=HEAD~ 2>err &&\n+\t\ttest_grep \"Your local changes to the following files would be overwritten\" err\n+\t)\n+'\n+\n+test_expect_success 'requires exactly one of --before or --after' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit first &&\n+\t\ttest_commit second &&\n+\t\ttest_must_fail git history reorder HEAD 2>err &&\n+\t\ttest_grep \"exactly one option of ${SQ}before${SQ} or ${SQ}after${SQ} must be given\" err &&\n+\t\ttest_must_fail git history reorder HEAD --before=a --after=b 2>err &&\n+\t\ttest_grep \"options ${SQ}before${SQ} and ${SQ}after${SQ} cannot be used together\" err\n+\t)\n+'\n+\n+test_expect_success 'refuses to reorder commit with itself' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit first &&\n+\t\ttest_commit second &&\n+\t\ttest_must_fail git history reorder HEAD --after=HEAD 2>err &&\n+\t\ttest_grep \"commit to reorder and anchor must not be the same\" err\n+\t)\n+'\n+\n+test_expect_success '--before can move commit back in history' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit first &&\n+\t\ttest_commit second &&\n+\t\ttest_commit third &&\n+\t\ttest_commit fourth &&\n+\t\ttest_commit fifth &&\n+\t\tgit history reorder :/fourth --before=:/second &&\n+\t\tcat >expect <<-EOF &&\n+\t\tfifth\n+\t\tthird\n+\t\tsecond\n+\t\tfourth\n+\t\tfirst\n+\t\tEOF\n+\t\tgit log --format=%s >actual &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+test_expect_success '--before can move commit forward in history' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit first &&\n+\t\ttest_commit second &&\n+\t\ttest_commit third &&\n+\t\ttest_commit fourth &&\n+\t\ttest_commit fifth &&\n+\t\tgit history reorder :/second --before=:/fourth &&\n+\t\tcat >expect <<-EOF &&\n+\t\tfifth\n+\t\tfourth\n+\t\tsecond\n+\t\tthird\n+\t\tfirst\n+\t\tEOF\n+\t\tgit log --format=%s >actual &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+test_expect_success '--before can make a commit a root commit' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit first &&\n+\t\ttest_commit second &&\n+\t\ttest_commit third &&\n+\t\tgit history reorder :/third --before=:/first &&\n+\t\tcat >expect <<-EOF &&\n+\t\tsecond\n+\t\tfirst\n+\t\tthird\n+\t\tEOF\n+\t\tgit log --format=%s >actual &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+test_expect_success '--after can move commit back in history' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit first &&\n+\t\ttest_commit second &&\n+\t\ttest_commit third &&\n+\t\ttest_commit fourth &&\n+\t\ttest_commit fifth &&\n+\t\tgit history reorder :/fourth --after=:/second &&\n+\t\tcat >expect <<-EOF &&\n+\t\tfifth\n+\t\tthird\n+\t\tfourth\n+\t\tsecond\n+\t\tfirst\n+\t\tEOF\n+\t\tgit log --format=%s >actual &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+test_expect_success '--after can move commit forward in history' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit first &&\n+\t\ttest_commit second &&\n+\t\ttest_commit third &&\n+\t\ttest_commit fourth &&\n+\t\ttest_commit fifth &&\n+\t\tgit history reorder :/second --after=:/fourth &&\n+\t\tcat >expect <<-EOF &&\n+\t\tfifth\n+\t\tsecond\n+\t\tfourth\n+\t\tthird\n+\t\tfirst\n+\t\tEOF\n+\t\tgit log --format=%s >actual &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+test_expect_success '--after can make commit the tip' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit first &&\n+\t\ttest_commit second &&\n+\t\ttest_commit third &&\n+\t\tgit history reorder :/first --after=:/third &&\n+\t\tcat >expect <<-EOF &&\n+\t\tfirst\n+\t\tthird\n+\t\tsecond\n+\t\tEOF\n+\t\tgit log --format=%s >actual &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+test_expect_success 'conflicts are detected' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\techo base >file &&\n+\t\tgit add file &&\n+\t\tgit commit -m base &&\n+\t\techo \"first edit\" >file &&\n+\t\tgit commit -am \"first edit\" &&\n+\t\techo \"second edit\" >file &&\n+\t\tgit commit -am \"second edit\" &&\n+\n+\t\tgit symbolic-ref HEAD >expect-head &&\n+\t\ttest_must_fail git history reorder HEAD --before=HEAD~ &&\n+\t\ttest_must_fail git symbolic-ref HEAD &&\n+\t\techo \"second edit\" >file &&\n+\t\tgit add file &&\n+\t\ttest_must_fail git history continue &&\n+\t\techo \"first edit\" >file &&\n+\t\tgit add file &&\n+\t\tgit history continue &&\n+\n+\t\tcat >expect <<-EOF &&\n+\t\tfirst edit\n+\t\tsecond edit\n+\t\tbase\n+\t\tEOF\n+\t\tgit log --format=%s >actual &&\n+\t\ttest_cmp expect actual &&\n+\n+\t\tgit symbolic-ref HEAD >actual-head &&\n+\t\ttest_cmp expect-head actual-head\n+\t)\n+'\n+\n+test_done\n\n-- \n2.51.0.308.g032396e0da.dirty\n\n"},{"id":"524835","messageId":"20250824-b4-pks-history-builtin-v2-10-964ac12f65bd@pks.im","threadId":"63986","inReplyTo":"20250824-b4-pks-history-builtin-v2-0-964ac12f65bd@pks.im","subject":"[PATCH RFC v2 10/16] add-patch: split out header from \"add-interactive.h\"","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-08-24T17:42:23Z","receivedAt":"2025-08-24T17:43:50Z","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 | 23 +++--------------------\n add-patch.c       |  1 +\n add-patch.h       | 26 ++++++++++++++++++++++++++\n 3 files changed, 30 insertions(+), 20 deletions(-)\n\ndiff --git a/add-interactive.h b/add-interactive.h\nindex 4213dcd67b..fb95b6ee05 100644\n--- a/add-interactive.h\n+++ b/add-interactive.h\n@@ -1,14 +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-};\n-\n-#define ADD_P_OPT_INIT { .context = -1, .interhunkcontext = -1 }\n+struct pathspec;\n+struct repository;\n \n struct add_i_state {\n \tstruct repository *r;\n@@ -32,21 +29,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 302e6ba7d9..e2b002fa73 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..4394c74107\n--- /dev/null\n+++ b/add-patch.h\n@@ -0,0 +1,26 @@\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+};\n+\n+#define ADD_P_OPT_INIT { .context = -1, .interhunkcontext = -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.51.0.308.g032396e0da.dirty\n\n"},{"id":"524836","messageId":"20250824-b4-pks-history-builtin-v2-11-964ac12f65bd@pks.im","threadId":"63986","inReplyTo":"20250824-b4-pks-history-builtin-v2-0-964ac12f65bd@pks.im","subject":"[PATCH RFC v2 11/16] add-patch: split out `struct interactive_options`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-08-24T17:42:24Z","receivedAt":"2025-08-24T17:43:56Z","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 the infra for \"git add -p\"\nand \"git add -i\". Users of `run_add_i()` for example are expected to\npass `struct add_p_opt`. This is somewhat confusing and raises the\nquestion 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  | 151 +++++++++++++----------------------------------------\n add-interactive.h  |  20 ++-----\n add-patch.c        | 145 +++++++++++++++++++++++++++++++++++++++++---------\n add-patch.h        |  33 ++++++++++--\n builtin/add.c      |  22 ++++----\n builtin/checkout.c |   4 +-\n builtin/commit.c   |  16 +++---\n builtin/reset.c    |  16 +++---\n builtin/stash.c    |  46 ++++++++--------\n commit.h           |   2 +-\n 10 files changed, 243 insertions(+), 212 deletions(-)\n\ndiff --git a/add-interactive.c b/add-interactive.c\nindex 3e692b47ec..3babc3e013 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,96 +19,18 @@\n #include \"prompt.h\"\n #include \"tree.h\"\n \n-static void init_color(struct repository *r, struct add_i_state *s,\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 (!s->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 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-\tconst char *value;\n-\n \ts->r = r;\n-\ts->context = -1;\n-\ts->interhunkcontext = -1;\n-\n-\tif (repo_config_get_value(r, \"color.interactive\", &value))\n-\t\ts->use_color = -1;\n-\telse\n-\t\ts->use_color =\n-\t\t\tgit_config_colorbool(\"color.interactive\", value);\n-\ts->use_color = want_color(s->use_color);\n-\n-\tinit_color(r, s, \"interactive.header\", s->header_color, GIT_COLOR_BOLD);\n-\tinit_color(r, s, \"interactive.help\", s->help_color, GIT_COLOR_BOLD_RED);\n-\tinit_color(r, s, \"interactive.prompt\", s->prompt_color,\n-\t\t   GIT_COLOR_BOLD_BLUE);\n-\tinit_color(r, s, \"interactive.error\", s->error_color,\n-\t\t   GIT_COLOR_BOLD_RED);\n-\n-\tinit_color(r, s, \"diff.frag\", s->fraginfo_color,\n-\t\t   diff_get_color(s->use_color, DIFF_FRAGINFO));\n-\tinit_color(r, s, \"diff.context\", s->context_color, \"fall back\");\n-\tif (!strcmp(s->context_color, \"fall back\"))\n-\t\tinit_color(r, s, \"diff.plain\", s->context_color,\n-\t\t\t   diff_get_color(s->use_color, DIFF_CONTEXT));\n-\tinit_color(r, s, \"diff.old\", s->file_old_color,\n-\t\tdiff_get_color(s->use_color, DIFF_FILE_OLD));\n-\tinit_color(r, s, \"diff.new\", s->file_new_color,\n-\t\tdiff_get_color(s->use_color, DIFF_FILE_NEW));\n-\n-\tstrlcpy(s->reset_color,\n-\t\ts->use_color ? 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 = -1;\n+\tinteractive_config_clear(&s->cfg);\n }\n \n /*\n@@ -262,7 +183,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@@ -330,7 +251,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@@ -408,7 +329,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@@ -968,7 +889,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@@ -990,9 +911,9 @@ 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\tstruct interactive_options opts = {\n+\t\t\t.context = s->cfg.context,\n+\t\t\t.interhunkcontext = s->cfg.interhunkcontext,\n \t\t};\n \t\tstruct strvec args = STRVEC_INIT;\n \t\tstruct pathspec ps_selected = { 0 };\n@@ -1004,7 +925,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@@ -1040,10 +961,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@@ -1061,17 +982,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@@ -1079,21 +1000,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@@ -1128,7 +1049,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@@ -1139,7 +1060,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@@ -1182,15 +1103,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 (s.use_color) {\n-\t\tdata.color = s.prompt_color;\n-\t\tdata.reset = s.reset_color;\n+\tif (s.cfg.use_color) {\n+\t\tdata.color = s.cfg.prompt_color;\n+\t\tdata.reset = s.cfg.reset_color;\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 fb95b6ee05..eefa2edc7c 100644\n--- a/add-interactive.h\n+++ b/add-interactive.h\n@@ -2,34 +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-\tint use_color;\n-\tchar header_color[COLOR_MAXLEN];\n-\tchar help_color[COLOR_MAXLEN];\n-\tchar prompt_color[COLOR_MAXLEN];\n-\tchar error_color[COLOR_MAXLEN];\n-\tchar reset_color[COLOR_MAXLEN];\n-\tchar fraginfo_color[COLOR_MAXLEN];\n-\tchar context_color[COLOR_MAXLEN];\n-\tchar file_old_color[COLOR_MAXLEN];\n-\tchar file_new_color[COLOR_MAXLEN];\n-\n-\tint use_single_key;\n-\tchar *interactive_diff_filter, *interactive_diff_algorithm;\n-\tint context, interhunkcontext;\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 e2b002fa73..45bc254e0c 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,99 @@ struct add_p_state {\n \tconst char *revision;\n };\n \n+static void init_color(struct repository *r,\n+\t\t       struct interactive_config *cfg,\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 (!cfg->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+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+\tconst char *value;\n+\n+\tcfg->context = -1;\n+\tcfg->interhunkcontext = -1;\n+\n+\tif (repo_config_get_value(r, \"color.interactive\", &value))\n+\t\tcfg->use_color = -1;\n+\telse\n+\t\tcfg->use_color =\n+\t\t\tgit_config_colorbool(\"color.interactive\", value);\n+\tcfg->use_color = want_color(cfg->use_color);\n+\n+\tinit_color(r, cfg, \"interactive.header\", cfg->header_color, GIT_COLOR_BOLD);\n+\tinit_color(r, cfg, \"interactive.help\", cfg->help_color, GIT_COLOR_BOLD_RED);\n+\tinit_color(r, cfg, \"interactive.prompt\", cfg->prompt_color,\n+\t\t   GIT_COLOR_BOLD_BLUE);\n+\tinit_color(r, cfg, \"interactive.error\", cfg->error_color,\n+\t\t   GIT_COLOR_BOLD_RED);\n+\n+\tinit_color(r, cfg, \"diff.frag\", cfg->fraginfo_color,\n+\t\t   diff_get_color(cfg->use_color, DIFF_FRAGINFO));\n+\tinit_color(r, cfg, \"diff.context\", cfg->context_color, \"fall back\");\n+\tif (!strcmp(cfg->context_color, \"fall back\"))\n+\t\tinit_color(r, cfg, \"diff.plain\", cfg->context_color,\n+\t\t\t   diff_get_color(cfg->use_color, DIFF_CONTEXT));\n+\tinit_color(r, cfg, \"diff.old\", cfg->file_old_color,\n+\t\tdiff_get_color(cfg->use_color, DIFF_FILE_OLD));\n+\tinit_color(r, cfg, \"diff.new\", cfg->file_new_color,\n+\t\tdiff_get_color(cfg->use_color, DIFF_FILE_NEW));\n+\n+\tstrlcpy(cfg->reset_color,\n+\t\tcfg->use_color ? 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 = -1;\n+}\n+\n static void add_p_state_clear(struct add_p_state *s)\n {\n \tsize_t i;\n@@ -299,9 +394,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);\n+\tputs(s->s.cfg.reset_color);\n \tva_end(args);\n }\n \n@@ -424,12 +519,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@@ -460,7 +555,7 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps)\n \n \tif (want_color_fd(1, -1)) {\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 +788,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 +810,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);\n+\t\t\tstrbuf_addf(out, \"%s\\n\", s->s.cfg.reset_color);\n \t\telse\n \t\t\tstrbuf_addch(out, '\\n');\n \t}\n@@ -1103,12 +1198,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);\n+\t\tstrbuf_addstr(&s->colored, s->s.cfg.reset_color);\n \t\tif (next > eol)\n \t\t\tstrbuf_add(&s->colored, plain + eol, next - eol);\n \t\tcurrent = next;\n@@ -1227,7 +1322,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@@ -1241,7 +1336,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@@ -1522,15 +1617,15 @@ static int patch_update_file(struct add_p_state *s,\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 \t\t\t\t\t\t: 1));\n \t\tprintf(_(s->mode->prompt_mode[prompt_mode_type]),\n \t\t       s->buf.buf);\n-\t\tif (*s->s.reset_color)\n-\t\t\tfputs(s->s.reset_color, stdout);\n+\t\tif (*s->s.cfg.reset_color)\n+\t\t\tfputs(s->s.cfg.reset_color, stdout);\n \t\tfflush(stdout);\n \t\tif (read_single_character(s) == EOF)\n \t\t\tbreak;\n@@ -1687,7 +1782,7 @@ static int patch_update_file(struct add_p_state *s,\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@@ -1705,7 +1800,7 @@ static int patch_update_file(struct add_p_state *s,\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@@ -1723,7 +1818,7 @@ static int patch_update_file(struct add_p_state *s,\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@@ -1764,7 +1859,7 @@ static int patch_update_file(struct add_p_state *s,\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@@ -1772,7 +1867,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;\ndiff --git a/add-patch.h b/add-patch.h\nindex 4394c74107..51c0d7bce9 100644\n--- a/add-patch.h\n+++ b/add-patch.h\n@@ -1,15 +1,42 @@\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 };\n \n-#define ADD_P_OPT_INIT { .context = -1, .interhunkcontext = -1 }\n+#define INTERACTIVE_OPTIONS_INIT { \\\n+\t.context = -1, \\\n+\t.interhunkcontext = -1, \\\n+}\n+\n+struct interactive_config {\n+\tint use_color;\n+\tchar header_color[COLOR_MAXLEN];\n+\tchar help_color[COLOR_MAXLEN];\n+\tchar prompt_color[COLOR_MAXLEN];\n+\tchar error_color[COLOR_MAXLEN];\n+\tchar reset_color[COLOR_MAXLEN];\n+\tchar fraginfo_color[COLOR_MAXLEN];\n+\tchar context_color[COLOR_MAXLEN];\n+\tchar file_old_color[COLOR_MAXLEN];\n+\tchar file_new_color[COLOR_MAXLEN];\n+\n+\tint use_single_key;\n+\tchar *interactive_diff_filter, *interactive_diff_algorithm;\n+\tint context, interhunkcontext;\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@@ -20,7 +47,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 0235854f80..a94c826c14 100644\n--- a/builtin/add.c\n+++ b/builtin/add.c\n@@ -30,7 +30,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@@ -159,7 +159,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@@ -171,9 +171,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@@ -255,8 +255,8 @@ 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_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@@ -398,9 +398,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@@ -410,11 +410,11 @@ 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}\n \ndiff --git a/builtin/checkout.c b/builtin/checkout.c\nindex 43583c8d1b..0b90f398fe 100644\n--- a/builtin/checkout.c\n+++ b/builtin/checkout.c\n@@ -546,7 +546,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};\n@@ -575,7 +575,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 b5b9608813..767351fd87 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@@ -356,9 +356,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@@ -407,7 +407,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@@ -432,9 +432,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@@ -1738,8 +1738,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 ed35802af1..088449e120 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,8 +371,8 @@ 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_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@@ -423,9 +423,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@@ -436,12 +436,12 @@ 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}\n \ndiff --git a/builtin/stash.c b/builtin/stash.c\nindex 1977e50df2..5070b8a88f 100644\n--- a/builtin/stash.c\n+++ b/builtin/stash.c\n@@ -1302,7 +1302,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@@ -1327,7 +1327,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@@ -1422,7 +1422,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@@ -1504,7 +1505,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@@ -1590,7 +1591,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@@ -1662,7 +1664,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@@ -1835,7 +1837,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@@ -1843,8 +1845,8 @@ 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_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@@ -1901,19 +1903,19 @@ 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}\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@@ -1938,7 +1940,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@@ -1946,8 +1948,8 @@ 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_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@@ -1967,20 +1969,20 @@ 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}\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 1d6e0c7518..7b6e59d6c1 100644\n--- a/commit.h\n+++ b/commit.h\n@@ -258,7 +258,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.51.0.308.g032396e0da.dirty\n\n"},{"id":"524837","messageId":"20250824-b4-pks-history-builtin-v2-12-964ac12f65bd@pks.im","threadId":"63986","inReplyTo":"20250824-b4-pks-history-builtin-v2-0-964ac12f65bd@pks.im","subject":"[PATCH RFC v2 12/16] add-patch: remove dependency on \"add-interactive\" subsystem","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-08-24T17:42:25Z","receivedAt":"2025-08-24T17:44:00Z","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 | 68 ++++++++++++++++++++++++++++++++-----------------------------\n 1 file changed, 36 insertions(+), 32 deletions(-)\n\ndiff --git a/add-patch.c b/add-patch.c\nindex 45bc254e0c..1bcbc91de9 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@@ -385,7 +385,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@@ -394,9 +394,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);\n+\tputs(s->cfg.reset_color);\n \tva_end(args);\n }\n \n@@ -414,7 +414,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@@ -519,12 +519,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@@ -555,7 +555,7 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps)\n \n \tif (want_color_fd(1, -1)) {\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@@ -788,7 +788,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@@ -810,7 +810,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);\n+\t\t\tstrbuf_addf(out, \"%s\\n\", s->cfg.reset_color);\n \t\telse\n \t\t\tstrbuf_addch(out, '\\n');\n \t}\n@@ -1198,12 +1198,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);\n+\t\tstrbuf_addstr(&s->colored, s->cfg.reset_color);\n \t\tif (next > eol)\n \t\t\tstrbuf_add(&s->colored, plain + eol, next - eol);\n \t\tcurrent = next;\n@@ -1322,7 +1322,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@@ -1336,7 +1336,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@@ -1617,15 +1617,15 @@ static int patch_update_file(struct add_p_state *s,\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 \t\t\t\t\t\t: 1));\n \t\tprintf(_(s->mode->prompt_mode[prompt_mode_type]),\n \t\t       s->buf.buf);\n-\t\tif (*s->s.cfg.reset_color)\n-\t\t\tfputs(s->s.cfg.reset_color, stdout);\n+\t\tif (*s->cfg.reset_color)\n+\t\t\tfputs(s->cfg.reset_color, stdout);\n \t\tfflush(stdout);\n \t\tif (read_single_character(s) == EOF)\n \t\t\tbreak;\n@@ -1782,7 +1782,7 @@ static int patch_update_file(struct add_p_state *s,\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@@ -1800,7 +1800,7 @@ static int patch_update_file(struct add_p_state *s,\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@@ -1818,7 +1818,7 @@ static int patch_update_file(struct add_p_state *s,\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@@ -1838,7 +1838,7 @@ static int patch_update_file(struct add_p_state *s,\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\t   s->mode->is_reverse);\n@@ -1849,8 +1849,8 @@ static int patch_update_file(struct add_p_state *s,\n \t\t\t\t\t NULL, 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\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@@ -1863,11 +1863,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\n-- \n2.51.0.308.g032396e0da.dirty\n\n"},{"id":"524838","messageId":"20250824-b4-pks-history-builtin-v2-13-964ac12f65bd@pks.im","threadId":"63986","inReplyTo":"20250824-b4-pks-history-builtin-v2-0-964ac12f65bd@pks.im","subject":"[PATCH RFC v2 13/16] add-patch: add support for in-memory index patching","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-08-24T17:42:26Z","receivedAt":"2025-08-24T17:44:06Z","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,\nworktree 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 | 110 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--\n add-patch.h |   8 +++++\n 2 files changed, 115 insertions(+), 3 deletions(-)\n\ndiff --git a/add-patch.c b/add-patch.c\nindex 1bcbc91de9..2a72c7b931 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@@ -414,7 +418,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@@ -1838,7 +1842,7 @@ static int patch_update_file(struct add_p_state *s,\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\t   s->mode->is_reverse);\n@@ -1849,9 +1853,11 @@ static int patch_update_file(struct add_p_state *s,\n \t\t\t\t\t NULL, 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 \tputchar('\\n');\n@@ -1864,6 +1870,8 @@ int run_add_p(struct repository *r, enum add_p_mode mode,\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@@ -1922,3 +1930,99 @@ int run_add_p(struct repository *r, enum add_p_mode mode,\n \tadd_p_state_clear(&s);\n \treturn 0;\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+\tstruct strbuf parent_revision = STRBUF_INIT;\n+\tchar parent_tree_oid[GIT_MAX_HEXSZ + 1];\n+\tsize_t binary_count = 0;\n+\tstruct commit *commit;\n+\tint ret;\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+\tstrbuf_addf(&parent_revision, \"%s~\", revision);\n+\tmode.diff_cmd[0] = \"diff-tree\";\n+\tmode.diff_cmd[1] = \"-r\";\n+\tmode.diff_cmd[2] = parent_tree_oid;\n+\n+\tinteractive_config_init(&s.cfg, r, opts);\n+\n+\tif (parse_diff(&s, ps) < 0) {\n+\t\tret = -1;\n+\t\tgoto out;\n+\t}\n+\n+\tfor (size_t i = 0; i < s.file_diff_nr; i++) {\n+\t\tif (s.file_diff[i].binary && !s.file_diff[i].hunk_nr)\n+\t\t\tbinary_count++;\n+\t\telse if (patch_update_file(&s, s.file_diff + i))\n+\t\t\tbreak;\n+\t}\n+\n+\tif (s.file_diff_nr == 0) {\n+\t\terr(&s, _(\"No changes.\"));\n+\t\tret = -1;\n+\t\tgoto out;\n+\t}\n+\n+\tif (binary_count == s.file_diff_nr) {\n+\t\terr(&s, _(\"Only binary files changed.\"));\n+\t\tret = -1;\n+\t\tgoto out;\n+\t}\n+\n+\tret = 0;\n+\n+out:\n+\tstrbuf_release(&parent_revision);\n+\tadd_p_state_clear(&s);\n+\treturn ret;\n+}\ndiff --git a/add-patch.h b/add-patch.h\nindex 51c0d7bce9..d0edfec936 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@@ -50,4 +51,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.51.0.308.g032396e0da.dirty\n\n"},{"id":"524839","messageId":"20250824-b4-pks-history-builtin-v2-14-964ac12f65bd@pks.im","threadId":"63986","inReplyTo":"20250824-b4-pks-history-builtin-v2-0-964ac12f65bd@pks.im","subject":"[PATCH RFC v2 14/16] wt-status: provide function to expose status for trees","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-08-24T17:42:27Z","receivedAt":"2025-08-24T17:44:12Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The \"wt-status\" subsystem is responsible for printing status information\naround the current state of the working tree. This most importantly\nincludes information around whether the working tree or the index have\nany changes.\n\nWe're about to introduce a new command though where the changes in\nneither of them are actually relevant to us. Instead, what we want is to\nformat the changes between two different trees. While it is a little bit\nof a stretch to add this as functionality to _working tree_ status, it\ndoesn't make any sense to open-code this functionality, either.\n\nImplement a new function `wt_status_collect_changes_trees()` that diffs\ntwo trees and formats the status accordingly. This function is not yet\nused, but will be in a subsequent commit.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n wt-status.c | 24 ++++++++++++++++++++++++\n wt-status.h |  3 +++\n 2 files changed, 27 insertions(+)\n\ndiff --git a/wt-status.c b/wt-status.c\nindex 454601afa15..f09309d12e3 100644\n--- a/wt-status.c\n+++ b/wt-status.c\n@@ -612,6 +612,30 @@ static void wt_status_collect_updated_cb(struct diff_queue_struct *q,\n \t}\n }\n \n+void wt_status_collect_changes_trees(struct wt_status *s,\n+\t\t\t\t     const struct object_id *old_treeish,\n+\t\t\t\t     const struct object_id *new_treeish)\n+{\n+\tstruct diff_options opts = { 0 };\n+\n+\trepo_diff_setup(s->repo, &opts);\n+\topts.output_format = DIFF_FORMAT_CALLBACK;\n+\topts.format_callback = wt_status_collect_updated_cb;\n+\topts.format_callback_data = s;\n+\topts.detect_rename = s->detect_rename >= 0 ? s->detect_rename : opts.detect_rename;\n+\topts.rename_limit = s->rename_limit >= 0 ? s->rename_limit : opts.rename_limit;\n+\topts.rename_score = s->rename_score >= 0 ? s->rename_score : opts.rename_score;\n+\topts.flags.recursive = 1;\n+\tdiff_setup_done(&opts);\n+\n+\tdiff_tree_oid(old_treeish, new_treeish, \"\", &opts);\n+\tdiffcore_std(&opts);\n+\tdiff_flush(&opts);\n+\twt_status_get_state(s->repo, &s->state, 0);\n+\n+\tdiff_free(&opts);\n+}\n+\n static void wt_status_collect_changes_worktree(struct wt_status *s)\n {\n \tstruct rev_info rev;\ndiff --git a/wt-status.h b/wt-status.h\nindex 4e377ce62b8..b262e345f79 100644\n--- a/wt-status.h\n+++ b/wt-status.h\n@@ -153,6 +153,9 @@ void wt_status_add_cut_line(struct wt_status *s);\n void wt_status_prepare(struct repository *r, struct wt_status *s);\n void wt_status_print(struct wt_status *s);\n void wt_status_collect(struct wt_status *s);\n+void wt_status_collect_changes_trees(struct wt_status *s,\n+\t\t\t\t     const struct object_id *old_treeish,\n+\t\t\t\t     const struct object_id *new_treeish);\n /*\n  * Frees the buffers allocated by wt_status_collect.\n  */\n\n-- \n2.51.0.308.g032396e0da.dirty\n\n"},{"id":"524840","messageId":"20250824-b4-pks-history-builtin-v2-15-964ac12f65bd@pks.im","threadId":"63986","inReplyTo":"20250824-b4-pks-history-builtin-v2-0-964ac12f65bd@pks.im","subject":"[PATCH RFC v2 15/16] builtin/history: implement \"split\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-08-24T17:42:28Z","receivedAt":"2025-08-24T17:44:17Z","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 of it into a separate commit. This is quite an involved operation\nthough:\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 |  60 +++++++\n builtin/history.c              | 291 +++++++++++++++++++++++++++++++\n t/meson.build                  |   1 +\n t/t3453-history-split.sh       | 387 +++++++++++++++++++++++++++++++++++++++++\n 4 files changed, 739 insertions(+)\n\ndiff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc\nindex b36cd925dd..6f0c64b90e 100644\n--- a/Documentation/git-history.adoc\n+++ b/Documentation/git-history.adoc\n@@ -13,6 +13,7 @@ git history continue\n git history quit\n git history drop <commit>\n git history reorder <commit> (--before=<following-commit>|--after=<preceding-commit>)\n+git history split [<options>] <commit> [--] [<pathspec>...]\n \n DESCRIPTION\n -----------\n@@ -52,6 +53,26 @@ child commits, as that would lead to an empty branch.\n \tbe related to one another and must be reachable from the current `HEAD`\n \tcommit.\n \n+`split [--message=<message>] <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 message of the new commit will be asked for by launching the\n+configured editor, unless it has been specified with the `-m` option.\n+Authorship of the commit will be the same as for the 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 The following commands are used to manage an interrupted history-rewriting\n operation:\n \n@@ -111,6 +132,45 @@ f44a46e third\n bf7438d first\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 --message=\"split-out commit\"\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,e,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,e,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+\n CONFIGURATION\n -------------\n \ndiff --git a/builtin/history.c b/builtin/history.c\nindex 16b516856e..6d3f44152c 100644\n--- a/builtin/history.c\n+++ b/builtin/history.c\n@@ -1,17 +1,27 @@\n+/* Required for `comment_line_str`. */\n+#define USE_THE_REPOSITORY_VARIABLE\n+\n #include \"builtin.h\"\n #include \"branch.h\"\n+#include \"cache-tree.h\"\n #include \"commit.h\"\n #include \"commit-reach.h\"\n #include \"config.h\"\n+#include \"editor.h\"\n #include \"environment.h\"\n #include \"gettext.h\"\n #include \"hex.h\"\n #include \"object-name.h\"\n #include \"parse-options.h\"\n+#include \"path.h\"\n+#include \"pathspec.h\"\n+#include \"read-cache-ll.h\"\n #include \"refs.h\"\n #include \"reset.h\"\n #include \"revision.h\"\n+#include \"run-command.h\"\n #include \"sequencer.h\"\n+#include \"sparse-index.h\"\n \n static int cmd_history_abort(int argc,\n \t\t\t     const char **argv,\n@@ -517,6 +527,285 @@ static int cmd_history_reorder(int argc,\n \treturn ret;\n }\n \n+static void change_data_free(void *util, const char *str UNUSED)\n+{\n+\tstruct wt_status_change_data *d = util;\n+\tfree(d->rename_source);\n+\tfree(d);\n+}\n+\n+static int fill_commit_message(struct repository *repo,\n+\t\t\t       const struct object_id *old_tree,\n+\t\t\t       const struct object_id *new_tree,\n+\t\t\t       const char *default_message,\n+\t\t\t       const char *provided_message,\n+\t\t\t       const char *action,\n+\t\t\t       struct strbuf *out)\n+{\n+\tif (!provided_message) {\n+\t\tstruct wt_status s;\n+\t\tconst char *path = git_path_commit_editmsg();\n+\t\tconst char *hint =\n+\t\t\t_(\"Please enter the commit message for the %s changes. Lines starting\\n\"\n+\t\t\t  \"with '%s' will be kept; you may remove them yourself if you want to.\\n\");\n+\n+\t\tstrbuf_addstr(out, default_message);\n+\t\tstrbuf_addch(out, '\\n');\n+\t\tstrbuf_commented_addf(out, comment_line_str, hint, action, comment_line_str);\n+\t\twrite_file_buf(path, out->buf, out->len);\n+\n+\t\twt_status_prepare(repo, &s);\n+\t\tFREE_AND_NULL(s.branch);\n+\t\ts.ahead_behind_flags = AHEAD_BEHIND_QUICK;\n+\t\ts.commit_template = 1;\n+\t\ts.colopts = 0;\n+\t\ts.display_comment_prefix = 1;\n+\t\ts.hints = 0;\n+\t\ts.use_color = 0;\n+\t\ts.whence = FROM_COMMIT;\n+\t\ts.committable = 1;\n+\n+\t\ts.fp = fopen(git_path_commit_editmsg(), \"a\");\n+\t\tif (!s.fp)\n+\t\t\treturn error_errno(_(\"could not open '%s'\"), git_path_commit_editmsg());\n+\n+\t\twt_status_collect_changes_trees(&s, old_tree, new_tree);\n+\t\twt_status_print(&s);\n+\t\twt_status_collect_free_buffers(&s);\n+\t\tstring_list_clear_func(&s.change, change_data_free);\n+\n+\t\tstrbuf_reset(out);\n+\t\tif (launch_editor(path, out, NULL)) {\n+\t\t\tfprintf(stderr, _(\"Please supply the message using either -m or -F option.\\n\"));\n+\t\t\treturn -1;\n+\t\t}\n+\t\tstrbuf_stripspace(out, comment_line_str);\n+\n+\t} else {\n+\t\tstrbuf_addstr(out, provided_message);\n+\t}\n+\n+\tcleanup_message(out, COMMIT_MSG_CLEANUP_ALL, 0);\n+\n+\tif (!out->len) {\n+\t\tfprintf(stderr, _(\"Aborting commit due to empty commit message.\\n\"));\n+\t\treturn -1;\n+\t}\n+\n+\treturn 0;\n+}\n+\n+static int split_commit(struct repository *repo,\n+\t\t\tstruct commit *original_commit,\n+\t\t\tstruct pathspec *pathspec,\n+\t\t\tconst char *commit_message,\n+\t\t\tstruct object_id *out)\n+{\n+\tstruct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;\n+\tstruct strbuf index_file = STRBUF_INIT, split_message = STRBUF_INIT;\n+\tstruct child_process read_tree_cmd = CHILD_PROCESS_INIT;\n+\tstruct index_state index = INDEX_STATE_INIT(repo);\n+\tstruct object_id original_commit_tree_oid, parent_tree_oid;\n+\tconst char *original_message, *original_body, *ptr;\n+\tchar original_commit_oid[GIT_MAX_HEXSZ + 1];\n+\tchar *original_author = NULL;\n+\tstruct commit_list *parents = NULL;\n+\tstruct commit *first_commit;\n+\tstruct tree *split_tree;\n+\tsize_t len;\n+\tint ret;\n+\n+\tif (original_commit->parents)\n+\t\tparent_tree_oid = *get_commit_tree_oid(original_commit->parents->item);\n+\telse\n+\t\toidcpy(&parent_tree_oid, repo->hash_algo->empty_tree);\n+\toriginal_commit_tree_oid = *get_commit_tree_oid(original_commit);\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+\tread_tree_cmd.git_cmd = 1;\n+\tstrvec_pushf(&read_tree_cmd.env, \"GIT_INDEX_FILE=%s\", index_file.buf);\n+\tstrvec_push(&read_tree_cmd.args, \"read-tree\");\n+\tstrvec_push(&read_tree_cmd.args, oid_to_hex(&parent_tree_oid));\n+\tret = run_command(&read_tree_cmd);\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_commit->object.oid);\n+\tret = run_add_p_index(repo, &index, index_file.buf, &interactive_opts,\n+\t\t\t      original_commit_oid, pathspec);\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+\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/* We retain authorship of the original commit. */\n+\toriginal_message = repo_logmsg_reencode(repo, original_commit, NULL, NULL);\n+\tptr = find_commit_header(original_message, \"author\", &len);\n+\tif (ptr)\n+\t\toriginal_author = xmemdupz(ptr, len);\n+\n+\tret = fill_commit_message(repo, &parent_tree_oid, &split_tree->object.oid,\n+\t\t\t\t  \"\", commit_message, \"split-out\", &split_message);\n+\tif (ret < 0)\n+\t\tgoto out;\n+\n+\tret = commit_tree(split_message.buf, split_message.len, &split_tree->object.oid,\n+\t\t\t  original_commit->parents, &out[0], original_author, NULL);\n+\tif (ret < 0) {\n+\t\tret = error(_(\"failed writing split-out commit\"));\n+\t\tgoto out;\n+\t}\n+\n+\t/*\n+\t * The second commit is much simpler to construct, as we can simply use\n+\t * the original commit details, except that we adjust its parent to be\n+\t * the newly split-out commit.\n+\t */\n+\tfind_commit_subject(original_message, &original_body);\n+\tfirst_commit = lookup_commit_reference(repo, &out[0]);\n+\tcommit_list_append(first_commit, &parents);\n+\n+\tret = commit_tree(original_body, strlen(original_body), &original_commit_tree_oid,\n+\t\t\t  parents, &out[1], original_author, NULL);\n+\tif (ret < 0) {\n+\t\tret = error(_(\"failed writing second commit\"));\n+\t\tgoto out;\n+\t}\n+\n+\tret = 0;\n+\n+out:\n+\tif (index_file.len)\n+\t\tunlink(index_file.buf);\n+\tstrbuf_release(&split_message);\n+\tstrbuf_release(&index_file);\n+\tfree_commit_list(parents);\n+\tfree(original_author);\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\tN_(\"git history split [<options>] <commit>\"),\n+\t\tNULL,\n+\t};\n+\tconst char *commit_message = NULL;\n+\tstruct option options[] = {\n+\t\tOPT_STRING('m', \"message\", &commit_message, N_(\"message\"), N_(\"commit message\")),\n+\t\tOPT_END(),\n+\t};\n+\tstruct commit *original_commit, *head;\n+\tstruct strvec commits = STRVEC_INIT;\n+\tstruct commit_list *list = NULL;\n+\tstruct object_id split_commits[2];\n+\tstruct pathspec pathspec = { 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 revision\"));\n+\t\tgoto out;\n+\t}\n+\trepo_config(repo, git_default_config, NULL);\n+\n+\toriginal_commit = lookup_commit_reference_by_name(argv[0]);\n+\tif (!original_commit) {\n+\t\tret = error(_(\"commit to be split cannot be found: %s\"), argv[0]);\n+\t\tgoto out;\n+\t}\n+\n+\tif (original_commit->parents && original_commit->parents->next) {\n+\t\tret = error(_(\"commit to be split must not be a merge commit\"));\n+\t\tgoto out;\n+\t}\n+\n+\thead = lookup_commit_reference_by_name(\"HEAD\");\n+\tif (!head) {\n+\t\tret = error(_(\"could not resolve HEAD to a commit\"));\n+\t\tgoto out;\n+\t}\n+\n+\tcommit_list_append(original_commit, &list);\n+\tif (!repo_is_descendant_of(repo, original_commit, list)) {\n+\t\tret = error (_(\"split commit must be reachable from current HEAD commit\"));\n+\t\tgoto out;\n+\t}\n+\n+\tparse_pathspec(&pathspec, 0,\n+\t\t       PATHSPEC_PREFER_FULL | PATHSPEC_SYMLINK_LEADING_PATH | PATHSPEC_PREFIX_ORIGIN,\n+\t\t       prefix, argv + 1);\n+\n+\t/*\n+\t * Collect the list of commits that we'll have to reapply now already.\n+\t * This ensures that we'll abort early on in case the range of commits\n+\t * contains merges, which we do not yet handle.\n+\t */\n+\tret = collect_commits(repo, original_commit->parents ? original_commit->parents->item : NULL,\n+\t\t\t      head, &commits);\n+\tif (ret < 0)\n+\t\tgoto out;\n+\n+\t/*\n+\t * Then we split up the commit and replace the original commit with the\n+\t * new new ones.\n+\t */\n+\tret = split_commit(repo, original_commit, &pathspec,\n+\t\t\t   commit_message, split_commits);\n+\tif (ret < 0)\n+\t\tgoto out;\n+\n+\treplace_commits(&commits, &original_commit->object.oid,\n+\t\t\tsplit_commits, ARRAY_SIZE(split_commits));\n+\n+\tret = apply_commits(repo, &commits, head, original_commit, \"split\");\n+\tif (ret < 0)\n+\t\tgoto out;\n+\n+\tret = 0;\n+\n+out:\n+\tclear_pathspec(&pathspec);\n+\tstrvec_clear(&commits);\n+\tfree_commit_list(list);\n+\treturn ret;\n+}\n+\n int cmd_history(int argc,\n \t\tconst char **argv,\n \t\tconst char *prefix,\n@@ -528,6 +817,7 @@ int cmd_history(int argc,\n \t\tN_(\"git history quit\"),\n \t\tN_(\"git history drop <commit>\"),\n \t\tN_(\"git history reorder <commit> (--before=<following-commit>|--after=<preceding-commit>)\"),\n+\t\tN_(\"git history split [<options>] <commit> [--] [<pathspec>...]\"),\n \t\tNULL,\n \t};\n \tparse_opt_subcommand_fn *fn = NULL;\n@@ -537,6 +827,7 @@ int cmd_history(int argc,\n \t\tOPT_SUBCOMMAND(\"quit\", &fn, cmd_history_quit),\n \t\tOPT_SUBCOMMAND(\"drop\", &fn, cmd_history_drop),\n \t\tOPT_SUBCOMMAND(\"reorder\", &fn, cmd_history_reorder),\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 2bf7bcab5a..b3d33c8588 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -379,6 +379,7 @@ integration_tests = [\n   't3450-history.sh',\n   't3451-history-drop.sh',\n   't3452-history-reorder.sh',\n+  't3453-history-split.sh',\n   't3500-cherry.sh',\n   't3501-revert-cherry-pick.sh',\n   't3502-cherry-pick-merge.sh',\ndiff --git a/t/t3453-history-split.sh b/t/t3453-history-split.sh\nnew file mode 100755\nindex 0000000000..b053fc2f29\n--- /dev/null\n+++ b/t/t3453-history-split.sh\n@@ -0,0 +1,387 @@\n+#!/bin/sh\n+\n+test_description='tests for git-history split subcommand'\n+\n+. ./test-lib.sh\n+\n+set_fake_editor () {\n+\twrite_script fake-editor.sh <<-\\EOF &&\n+\techo \"split-out commit\" >\"$1\"\n+\tEOF\n+\ttest_set_editor \"$(pwd)\"/fake-editor.sh\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 \"commit to be split must not be a merge commit\" err &&\n+\t\ttest_must_fail git history split HEAD~ 2>err &&\n+\t\ttest_grep \"cannot rearrange commit history with merges\" err\n+\t)\n+'\n+\n+test_expect_success 'refuses to work with changes in the worktree or index' '\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\techo changed >bar &&\n+\t\ttest_must_fail git history split -m message HEAD 2>err <<-EOF &&\n+\t\ty\n+\t\tn\n+\t\tEOF\n+\t\ttest_grep \"Your local changes to the following files would be overwritten\" err &&\n+\n+\t\tgit add bar &&\n+\t\ttest_must_fail git history split -m message HEAD 2>err <<-EOF &&\n+\t\ty\n+\t\tn\n+\t\tEOF\n+\t\ttest_grep \"Your local changes to the following files would be overwritten\" err\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 &&\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\tsplit-me\n+\t\tsplit-out commit\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+\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 &&\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\troot\n+\t\tsplit-out commit\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 &&\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\tsplit-me\n+\t\tsplit-out commit\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 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\tgit history split HEAD -m \"split-out commit\" <<-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+\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\tgit history split HEAD -m \"split-out commit\" <<-EOF &&\n+\t\tn\n+\t\ty\n+\t\tEOF\n+\n+\t\texpect_log <<-EOF &&\n+\t\tsplit-me\n+\t\tsplit-out commit\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 '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\ttest_must_fail git history split HEAD -m \"\" <<-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 'can specify message via option' '\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\tgit history split HEAD -m \"message option\" <<-EOF &&\n+\t\ty\n+\t\tn\n+\t\tEOF\n+\n+\t\texpect_log <<-EOF\n+\t\tsplit-me\n+\t\tmessage option\n+\t\tEOF\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\tcp \"$1\" . &&\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\tcat >expect <<-EOF &&\n+\n+\t\t# Please enter the commit message for the split-out changes. Lines starting\n+\t\t# with ${SQ}#${SQ} will be kept; you may remove them yourself if you want to.\n+\t\t# Changes to be committed:\n+\t\t#\tnew file:   bar\n+\t\t#\n+\t\tEOF\n+\t\ttest_cmp expect COMMIT_EDITMSG &&\n+\n+\t\texpect_log <<-EOF\n+\t\tsplit-me\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\tgit history split HEAD -m \"message option\" -- 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 '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 '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_done\n\n-- \n2.51.0.308.g032396e0da.dirty\n\n"},{"id":"524841","messageId":"20250824-b4-pks-history-builtin-v2-16-964ac12f65bd@pks.im","threadId":"63986","inReplyTo":"20250824-b4-pks-history-builtin-v2-0-964ac12f65bd@pks.im","subject":"[PATCH RFC v2 16/16] builtin/history: implement \"reword\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-08-24T17:42:29Z","receivedAt":"2025-08-24T17:44:22Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Implement a new \"reword\" subcommand for git-history(1). This subcommand\nis essentially the same as if a user performed an interactive rebase\nwith a single commit changed to use the \"reword\" verb.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/git-history.adoc |   5 ++\n builtin/history.c              |  96 +++++++++++++++++++++++++\n t/meson.build                  |   1 +\n t/t3454-history-reword.sh      | 158 +++++++++++++++++++++++++++++++++++++++++\n 4 files changed, 260 insertions(+)\n\ndiff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc\nindex 6f0c64b90e..cbbcef3582 100644\n--- a/Documentation/git-history.adoc\n+++ b/Documentation/git-history.adoc\n@@ -13,6 +13,7 @@ git history continue\n git history quit\n git history drop <commit>\n git history reorder <commit> (--before=<following-commit>|--after=<preceding-commit>)\n+git history reword [<options>] <commit>\n git history split [<options>] <commit> [--] [<pathspec>...]\n \n DESCRIPTION\n@@ -53,6 +54,10 @@ child commits, as that would lead to an empty branch.\n \tbe related to one another and must be reachable from the current `HEAD`\n \tcommit.\n \n+`reword <commit> [--message=<message>]`::\n+\tRewrite the commit message of the specified commit. All the other\n+\tdetails of this commit remain unchanged.\n+\n `split [--message=<message>] <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\ndiff --git a/builtin/history.c b/builtin/history.c\nindex 6d3f44152c..cdc93a1cbd 100644\n--- a/builtin/history.c\n+++ b/builtin/history.c\n@@ -716,6 +716,100 @@ static int split_commit(struct repository *repo,\n \treturn ret;\n }\n \n+static int cmd_history_reword(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\tN_(\"git history reword [<options>] <commit>\"),\n+\t\tNULL,\n+\t};\n+\tconst char *commit_message = NULL;\n+\tstruct option options[] = {\n+\t\tOPT_STRING('m', \"message\", &commit_message, N_(\"message\"), N_(\"commit message\")),\n+\t\tOPT_END(),\n+\t};\n+\tstruct strbuf final_message = STRBUF_INIT;\n+\tstruct commit *original_commit, *head;\n+\tstruct strvec commits = STRVEC_INIT;\n+\tstruct object_id parent_tree_oid, original_commit_tree_oid;\n+\tstruct object_id rewritten_commit;\n+\tconst char *original_message, *original_body, *ptr;\n+\tchar *original_author = NULL;\n+\tsize_t len;\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 single revision\"));\n+\t\tgoto out;\n+\t}\n+\trepo_config(repo, git_default_config, NULL);\n+\n+\toriginal_commit = lookup_commit_reference_by_name(argv[0]);\n+\tif (!original_commit) {\n+\t\tret = error(_(\"commit to be split cannot be found: %s\"), argv[0]);\n+\t\tgoto out;\n+\t}\n+\n+\thead = lookup_commit_reference_by_name(\"HEAD\");\n+\tif (!head) {\n+\t\tret = error(_(\"could not resolve HEAD to a commit\"));\n+\t\tgoto out;\n+\t}\n+\n+\t/*\n+\t * Collect the list of commits that we'll have to reapply now already.\n+\t * This ensures that we'll abort early on in case the range of commits\n+\t * contains merges, which we do not yet handle.\n+\t */\n+\tret = collect_commits(repo, original_commit->parents ? original_commit->parents->item : NULL,\n+\t\t\t      head, &commits);\n+\tif (ret < 0)\n+\t\tgoto out;\n+\n+\t/* We retain authorship of the original commit. */\n+\toriginal_message = repo_logmsg_reencode(repo, original_commit, 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+\tif (original_commit->parents)\n+\t\tparent_tree_oid = *get_commit_tree_oid(original_commit->parents->item);\n+\telse\n+\t\toidcpy(&parent_tree_oid, repo->hash_algo->empty_tree);\n+\toriginal_commit_tree_oid = *get_commit_tree_oid(original_commit);\n+\n+\tret = fill_commit_message(repo, &parent_tree_oid, &original_commit_tree_oid,\n+\t\t\t\t  original_body, commit_message, \"reworded\", &final_message);\n+\tif (ret < 0)\n+\t\tgoto out;\n+\n+\tret = commit_tree(final_message.buf, final_message.len,\n+\t\t\t  &repo_get_commit_tree(repo, original_commit)->object.oid,\n+\t\t\t  original_commit->parents, &rewritten_commit, original_author, NULL);\n+\tif (ret < 0) {\n+\t\tret = error(_(\"failed writing reworded commit\"));\n+\t\tgoto out;\n+\t}\n+\n+\treplace_commits(&commits, &original_commit->object.oid, &rewritten_commit, 1);\n+\n+\tret = apply_commits(repo, &commits, head, original_commit, \"reword\");\n+\tif (ret < 0)\n+\t\tgoto out;\n+\n+\tret = 0;\n+\n+out:\n+\tstrbuf_release(&final_message);\n+\tstrvec_clear(&commits);\n+\tfree(original_author);\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@@ -817,6 +911,7 @@ int cmd_history(int argc,\n \t\tN_(\"git history quit\"),\n \t\tN_(\"git history drop <commit>\"),\n \t\tN_(\"git history reorder <commit> (--before=<following-commit>|--after=<preceding-commit>)\"),\n+\t\tN_(\"git history reword [<options>] <commit>\"),\n \t\tN_(\"git history split [<options>] <commit> [--] [<pathspec>...]\"),\n \t\tNULL,\n \t};\n@@ -827,6 +922,7 @@ int cmd_history(int argc,\n \t\tOPT_SUBCOMMAND(\"quit\", &fn, cmd_history_quit),\n \t\tOPT_SUBCOMMAND(\"drop\", &fn, cmd_history_drop),\n \t\tOPT_SUBCOMMAND(\"reorder\", &fn, cmd_history_reorder),\n+\t\tOPT_SUBCOMMAND(\"reword\", &fn, cmd_history_reword),\n \t\tOPT_SUBCOMMAND(\"split\", &fn, cmd_history_split),\n \t\tOPT_END(),\n \t};\ndiff --git a/t/meson.build b/t/meson.build\nindex b3d33c8588..948223f453 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -380,6 +380,7 @@ integration_tests = [\n   't3451-history-drop.sh',\n   't3452-history-reorder.sh',\n   't3453-history-split.sh',\n+  't3454-history-reword.sh',\n   't3500-cherry.sh',\n   't3501-revert-cherry-pick.sh',\n   't3502-cherry-pick-merge.sh',\ndiff --git a/t/t3454-history-reword.sh b/t/t3454-history-reword.sh\nnew file mode 100755\nindex 0000000000..9822c0336a\n--- /dev/null\n+++ b/t/t3454-history-reword.sh\n@@ -0,0 +1,158 @@\n+#!/bin/sh\n+\n+test_description='tests for git-history reword subcommand'\n+\n+. ./test-lib.sh\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 reword HEAD~ 2>err &&\n+\t\ttest_grep \"cannot rearrange commit history with merges\" err &&\n+\t\ttest_must_fail git history reword HEAD 2>err &&\n+\t\ttest_grep \"cannot rearrange commit history with merges\" err\n+\t)\n+'\n+\n+test_expect_success 'refuses to work with changes in the worktree or index' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit base file &&\n+\t\techo foo >file &&\n+\t\ttest_must_fail git history reword HEAD 2>err &&\n+\t\ttest_grep \"Your local changes to the following files would be overwritten\" err &&\n+\t\tgit add file &&\n+\t\ttest_must_fail git history reword HEAD 2>err &&\n+\t\ttest_grep \"Your local changes to the following files would be overwritten\" err\n+\t)\n+'\n+\n+test_expect_success 'can reword tip of a branch' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit first &&\n+\t\ttest_commit second &&\n+\t\ttest_commit third &&\n+\n+\t\tgit symbolic-ref HEAD >expect &&\n+\t\tgit history reword -m \"third reworded\" HEAD &&\n+\t\tgit symbolic-ref HEAD >actual &&\n+\t\ttest_cmp expect actual &&\n+\n+\t\tcat >expect <<-EOF &&\n+\t\tthird reworded\n+\t\tsecond\n+\t\tfirst\n+\t\tEOF\n+\t\tgit log --format=%s >actual &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+test_expect_success 'can reword commit in the middle' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit first &&\n+\t\ttest_commit second &&\n+\t\ttest_commit third &&\n+\n+\t\tgit symbolic-ref HEAD >expect &&\n+\t\tgit history reword -m \"second reworded\" HEAD~ &&\n+\t\tgit symbolic-ref HEAD >actual &&\n+\t\ttest_cmp expect actual &&\n+\n+\t\tcat >expect <<-EOF &&\n+\t\tthird\n+\t\tsecond reworded\n+\t\tfirst\n+\t\tEOF\n+\t\tgit log --format=%s >actual &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+test_expect_success 'can reword root commit' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit first &&\n+\t\ttest_commit second &&\n+\t\ttest_commit third &&\n+\t\tgit history reword -m \"first reworded\" HEAD~2 &&\n+\n+\t\tcat >expect <<-EOF &&\n+\t\tthird\n+\t\tsecond\n+\t\tfirst reworded\n+\t\tEOF\n+\t\tgit log --format=%s >actual &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+test_expect_success 'can use editor to rewrite commit message' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit first &&\n+\n+\t\twrite_script fake-editor.sh <<-\\EOF &&\n+\t\tcp \"$1\" . &&\n+\t\tprintf \"\\namend a comment\\n\" >>\"$1\"\n+\t\tEOF\n+\t\ttest_set_editor \"$(pwd)\"/fake-editor.sh &&\n+\t\tgit history reword HEAD &&\n+\n+\t\tcat >expect <<-EOF &&\n+\t\tfirst\n+\n+\t\t# Please enter the commit message for the reworded changes. Lines starting\n+\t\t# with ${SQ}#${SQ} will be kept; you may remove them yourself if you want to.\n+\t\t# Changes to be committed:\n+\t\t#\tnew file:   first.t\n+\t\t#\n+\t\tEOF\n+\t\ttest_cmp expect COMMIT_EDITMSG &&\n+\n+\t\tcat >expect <<-EOF &&\n+\t\tfirst\n+\n+\t\tamend a comment\n+\n+\t\tEOF\n+\t\tgit log --format=%B >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\ttest_commit first &&\n+\n+\t\ttest_must_fail git history reword -m \"\" HEAD 2>err &&\n+\t\ttest_grep \"Aborting commit due to empty commit message.\" err\n+\t)\n+'\n+\n+\n+test_done\n\n-- \n2.51.0.308.g032396e0da.dirty\n\n"},{"id":"524842","messageId":"602e215c-be91-458e-9961-c78cf6aed2a9@app.fastmail.com","threadId":"63986","inReplyTo":"20250824-b4-pks-history-builtin-v2-15-964ac12f65bd@pks.im","subject":"Re: [PATCH RFC v2 15/16] builtin/history: implement \"split\" subcommand","fromName":"Kristoffer Haugsbakk","fromEmail":"kristofferhaugsbakk@fastmail.com","sentAt":"2025-08-24T18:03:18Z","receivedAt":"2025-08-24T18:03:52Z","isPatch":true,"sender":{"key":"kristofferhaugsbakk@fastmail.com","avatar":null},"body":"On Sun, Aug 24, 2025, at 19:42, Patrick Steinhardt wrote:\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 of it into a separate commit. This is quite an involved operation\n\ns/out of it into/out into/\n\n> 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\nExactly right and this fills a conspicuous hole (in the rewriting parts\nof git(1)).\n\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\nThe interactive mode here seems just-right.\n\n• Split in two, give the commit message for the new one\n• I can use `git history split :/'The second batch'` if I want to split\n  a single commit multiple times\n• I can use `git history reword :/'The second batch'` if I want\n  to change the original commit message as well\n\nBut it’s interactive-only, correct?  Would it make sense for a stateful\nsplit session so that other tools could be used to split the patch?\n\nOne nice thing about “the staging area” is that people can use whatever\ntools they want for things like selectively including changes in a\ncommit.  Likewise for a rebase session.\n"},{"id":"524843","messageId":"5079326d-497c-4335-88d0-a248e0b8c6d1@app.fastmail.com","threadId":"63986","inReplyTo":"20250824-b4-pks-history-builtin-v2-16-964ac12f65bd@pks.im","subject":"Re: [PATCH RFC v2 16/16] builtin/history: implement \"reword\" subcommand","fromName":"Kristoffer Haugsbakk","fromEmail":"kristofferhaugsbakk@fastmail.com","sentAt":"2025-08-24T18:08:07Z","receivedAt":"2025-08-24T18:08:34Z","isPatch":true,"sender":{"key":"kristofferhaugsbakk@fastmail.com","avatar":null},"body":"On Sun, Aug 24, 2025, at 19:42, Patrick Steinhardt wrote:\n> Implement a new \"reword\" subcommand for git-history(1). This subcommand\n> is essentially the same as if a user performed an interactive rebase\n> with a single commit changed to use the \"reword\" verb.\n>\n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n\nI get a “split” error when I typo the commit to reword:\n\n    $ ./git history reword ./s2\n    error: commit to be split cannot be found: ./s2\n"},{"id":"524871","messageId":"xmqqjz2rs6fk.fsf@gitster.g","threadId":"63986","inReplyTo":"20250824-b4-pks-history-builtin-v2-5-964ac12f65bd@pks.im","subject":"Re: [PATCH RFC v2 05/16] cache-tree: allow writing in-memory index as tree","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-08-25T16:38:07Z","receivedAt":"2025-08-25T16:38:10Z","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> The function `write_in_core_index_as_tree()` takes a repository and\n> writes its index into a tree object. What this function cannot do though\n> is to take an _arbitrary_ in-memory index.\n>\n> Introduce a new `struct index_state` parameter so that the caller can\n> pass a different index than the one belonging to the repository. This\n> will be used in a subsequent commit.\n\nNice.  \n\nI wonder if this would also allow us to simplify the code paths for\n\"git commit -o <pathspec>\", where we use a separate temporary index\nthat gets populated afresh from HEAD, grab the new snapshot for the\npaths that match the pathspec, and write it out as a tree to be\nwrapped in the new commit (and then the real index is also updated\nat these same paths).\n\nI guess the code paths need to expose what is in the temporary index\nto hooks, which means the index file needs to be written out to an\nactual on-disk file, so the picture would be a bit different?\n\n\n"},{"id":"524872","messageId":"xmqqfrdfs69y.fsf@gitster.g","threadId":"63986","inReplyTo":"20250824-b4-pks-history-builtin-v2-10-964ac12f65bd@pks.im","subject":"Re: [PATCH RFC v2 10/16] add-patch: split out header from \"add-interactive.h\"","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-08-25T16:41:29Z","receivedAt":"2025-08-25T16:41:31Z","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> 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\n> find relevant code and to identify clear boundaries between the two\n> subsystems.\n>\n> Split up concerns and move declarations that relate to \"add-patch.c\"\n> into a new \"add-patch.h\" header.\n>\n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n>  add-interactive.h | 23 +++--------------------\n>  add-patch.c       |  1 +\n>  add-patch.h       | 26 ++++++++++++++++++++++++++\n>  3 files changed, 30 insertions(+), 20 deletions(-)\n\nWhat is left in the interactive side is the things \"add -i\" can do\nother than \"add -p\" (aka \"add -i\" plus \"5: patch\"), which makes\nsense.\n\nIt is surprising that this step does not touch any of the clients of\nthe \"-p\" machinery (like \"git reset -p\"), though.  They surely do\nnot need the rest of \"add-interactive.h\", do they?\n"},{"id":"524873","messageId":"xmqqa53ns66a.fsf@gitster.g","threadId":"63986","inReplyTo":"20250824-b4-pks-history-builtin-v2-12-964ac12f65bd@pks.im","subject":"Re: [PATCH RFC v2 12/16] add-patch: remove dependency on \"add-interactive\" subsystem","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-08-25T16:43:41Z","receivedAt":"2025-08-25T16:43:43Z","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> With the preceding commit we have split out interactive configuration\n> that is used by both \"git add -p\" and \"git add -i\". But we still\n> initialize that configuration in the \"add -p\" subsystem by calling\n> `init_add_i_state()`, even though we only do so to initialize the\n> interactive configuration as well as a repository pointer.\n>\n> Stop doing so and instead store and initialize the interactive\n> configuration in `struct add_p_state` directly.\n>\n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n>  add-patch.c | 68 ++++++++++++++++++++++++++++++++-----------------------------\n>  1 file changed, 36 insertions(+), 32 deletions(-)\n\nAhh, with the two steps, this and the previous one, my question is\nanswered.  I like the shape of the code base at this step very much.\n"},{"id":"524941","messageId":"CALnO6CBgGNdD7eRsseXVmTCkfi9TU6S46MckC6her0FzJNa=Wg@mail.gmail.com","threadId":"63986","inReplyTo":"20250824-b4-pks-history-builtin-v2-3-964ac12f65bd@pks.im","subject":"Re: [PATCH RFC v2 03/16] sequencer: introduce new history editing mode","fromName":"D. Ben Knoble","fromEmail":"ben.knoble@gmail.com","sentAt":"2025-08-26T12:55:13Z","receivedAt":"2025-08-26T12:55:27Z","isPatch":true,"sender":{"key":"ben.knoble@gmail.com","avatar":"https://avatars.githubusercontent.com/u/22802209?v=4"},"body":"On Sun, Aug 24, 2025 at 1:42 PM Patrick Steinhardt <ps@pks.im> wrote:\n> diff --git a/sequencer.c b/sequencer.c\n> index bff181df76..898ac1a2a8 100644\n> --- a/sequencer.c\n> +++ b/sequencer.c\n> @@ -3052,17 +3087,29 @@ static int read_populate_todo(struct repository *r,\n>                 return error(_(\"no commits parsed.\"));\n>\n>         if (!is_rebase_i(opts)) {\n> -               enum todo_command valid =\n> -                       opts->action == REPLAY_PICK ? TODO_PICK : TODO_REVERT;\n> +               enum todo_command valid;\n>                 int i;\n>\n> -               for (i = 0; i < todo_list->nr; i++)\n> +               switch (opts->action) {\n> +               case REPLAY_PICK:\n> +               case REPLAY_HISTORY_EDIT:\n> +                       valid = TODO_PICK;\n> +                       break;\n> +               default:\n> +                       valid = TODO_REVERT;\n> +                       break;\n> +               }\n\nI think I see this hunk repeated in a few places—maybe some\nleftoverbits for a refactor?\n\n-- \nD. Ben Knoble\n"},{"id":"524942","messageId":"CALnO6CB=ZPO33gz0qyCGrC6E2_b2S1kGZkLd-q_CS37gupvPXA@mail.gmail.com","threadId":"63986","inReplyTo":"20250824-b4-pks-history-builtin-v2-9-964ac12f65bd@pks.im","subject":"Re: [PATCH RFC v2 09/16] builtin/history: implement \"reorder\" subcommand","fromName":"D. Ben Knoble","fromEmail":"ben.knoble@gmail.com","sentAt":"2025-08-26T13:03:18Z","receivedAt":"2025-08-26T13:03:33Z","isPatch":true,"sender":{"key":"ben.knoble@gmail.com","avatar":"https://avatars.githubusercontent.com/u/22802209?v=4"},"body":"On Sun, Aug 24, 2025 at 1:43 PM Patrick Steinhardt <ps@pks.im> wrote:\n> diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc\n> index db5b292994..b36cd925dd 100644\n> --- a/Documentation/git-history.adoc\n> +++ b/Documentation/git-history.adoc\n> @@ -12,6 +12,7 @@ git history abort\n>  git history continue\n>  git history quit\n>  git history drop <commit>\n> +git history reorder <commit> (--before=<following-commit>|--after=<preceding-commit>)\n>\n>  DESCRIPTION\n>  -----------\n> @@ -39,12 +40,18 @@ rewrite history in different ways:\n>  `drop <commit>`::\n>         Drop a commit from the history and reapply all children of that\n>         commit on top of the commit's parent. The commit that is to be\n> -       dropped must be reachable from the current `HEAD` commit.\n> +       dropped must be reachable from the currently checked-out commit.\n>  +\n>  Dropping the root commit converts the child of that commit into the new\n>  root commit. It is invalid to drop a root commit that does not have any\n>  child commits, as that would lead to an empty branch.\n\nFixup in the wrong commit, maybe?\n\n> +`reorder <commit> (--before=<following-commit>|--after=<preceding-commit>)`::\n> +       Move the commit so that it becomes either the parent of\n> +       <following-commit> or the child of <preceding-commit>. The commits must\n> +       be related to one another and must be reachable from the current `HEAD`\n> +       commit.\n> +\n>  The following commands are used to manage an interrupted history-rewriting\n>  operation:\n>\n\n-- \nD. Ben Knoble\n"},{"id":"524943","messageId":"CALnO6CBRv-Jvm1FsUAkDSyaNNRXtMukReKzefvCw5-vj=-pnCA@mail.gmail.com","threadId":"63986","inReplyTo":"20250824-b4-pks-history-builtin-v2-15-964ac12f65bd@pks.im","subject":"Re: [PATCH RFC v2 15/16] builtin/history: implement \"split\" subcommand","fromName":"D. Ben Knoble","fromEmail":"ben.knoble@gmail.com","sentAt":"2025-08-26T13:14:49Z","receivedAt":"2025-08-26T13:15:04Z","isPatch":true,"sender":{"key":"ben.knoble@gmail.com","avatar":"https://avatars.githubusercontent.com/u/22802209?v=4"},"body":"On Sun, Aug 24, 2025 at 1:44 PM Patrick Steinhardt <ps@pks.im> wrote:\n> diff --git a/builtin/history.c b/builtin/history.c\n> index 16b516856e..6d3f44152c 100644\n> --- a/builtin/history.c\n> +++ b/builtin/history.c\n> @@ -517,6 +527,285 @@ static int cmd_history_reorder(int argc,\n>         return ret;\n>  }\n>\n> +static void change_data_free(void *util, const char *str UNUSED)\n> +{\n> +       struct wt_status_change_data *d = util;\n> +       free(d->rename_source);\n> +       free(d);\n> +}\n> +\n> +static int fill_commit_message(struct repository *repo,\n> +                              const struct object_id *old_tree,\n> +                              const struct object_id *new_tree,\n> +                              const char *default_message,\n> +                              const char *provided_message,\n> +                              const char *action,\n> +                              struct strbuf *out)\n> +{\n> +       if (!provided_message) {\n> +               struct wt_status s;\n> +               const char *path = git_path_commit_editmsg();\n> +               const char *hint =\n> +                       _(\"Please enter the commit message for the %s changes. Lines starting\\n\"\n> +                         \"with '%s' will be kept; you may remove them yourself if you want to.\\n\");\n> +\n> +               strbuf_addstr(out, default_message);\n> +               strbuf_addch(out, '\\n');\n> +               strbuf_commented_addf(out, comment_line_str, hint, action, comment_line_str);\n> +               write_file_buf(path, out->buf, out->len);\n> +\n> +               wt_status_prepare(repo, &s);\n> +               FREE_AND_NULL(s.branch);\n> +               s.ahead_behind_flags = AHEAD_BEHIND_QUICK;\n> +               s.commit_template = 1;\n> +               s.colopts = 0;\n> +               s.display_comment_prefix = 1;\n> +               s.hints = 0;\n> +               s.use_color = 0;\n> +               s.whence = FROM_COMMIT;\n> +               s.committable = 1;\n> +\n> +               s.fp = fopen(git_path_commit_editmsg(), \"a\");\n> +               if (!s.fp)\n> +                       return error_errno(_(\"could not open '%s'\"), git_path_commit_editmsg());\n> +\n> +               wt_status_collect_changes_trees(&s, old_tree, new_tree);\n> +               wt_status_print(&s);\n> +               wt_status_collect_free_buffers(&s);\n> +               string_list_clear_func(&s.change, change_data_free);\n\nI think I'm supposed to see the changes between the old and new trees,\nright? Does this only happen if I use the interactive machinery to\nedit a hunk? When I try accepting some changes and leaving others for\nthe next commit I get no diff in the template.\n\nI did try to add new diff lines to a hunk, and nothing showed up…\nmaybe I'm holding it wrong? I'm pretty sure I compiled this version.\n\nIt doesn't look like it's triggered only on commit.verbose config, either.\n\n> +\n> +               strbuf_reset(out);\n> +               if (launch_editor(path, out, NULL)) {\n> +                       fprintf(stderr, _(\"Please supply the message using either -m or -F option.\\n\"));\n\nAccording to the usage, git history split only supports -m, not -F ;)\n\n> +                       return -1;\n> +               }\n> +               strbuf_stripspace(out, comment_line_str);\n> +\n> +       } else {\n> +               strbuf_addstr(out, provided_message);\n> +       }\n> +\n> +       cleanup_message(out, COMMIT_MSG_CLEANUP_ALL, 0);\n> +\n> +       if (!out->len) {\n> +               fprintf(stderr, _(\"Aborting commit due to empty commit message.\\n\"));\n\nIt _would_ be nice if this and similar errors left me able to \"try\nagain\" without losing staged changes—I think I mentioned this before,\nthough. And with the in-memory indices vs. actual working state,\npresenting a UI here could be very difficult. So it's an\nunderstandable choice.\n\n-- \nD. Ben Knoble\n"},{"id":"525422","messageId":"aLgyUw7wkxTYd1Dm@pks.im","threadId":"63986","inReplyTo":"CALnO6CBgGNdD7eRsseXVmTCkfi9TU6S46MckC6her0FzJNa=Wg@mail.gmail.com","subject":"Re: [PATCH RFC v2 03/16] sequencer: introduce new history editing mode","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-09-03T12:19:31Z","receivedAt":"2025-09-03T12:19:38Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Tue, Aug 26, 2025 at 08:55:13AM -0400, D. Ben Knoble wrote:\n> On Sun, Aug 24, 2025 at 1:42 PM Patrick Steinhardt <ps@pks.im> wrote:\n> > diff --git a/sequencer.c b/sequencer.c\n> > index bff181df76..898ac1a2a8 100644\n> > --- a/sequencer.c\n> > +++ b/sequencer.c\n> > @@ -3052,17 +3087,29 @@ static int read_populate_todo(struct repository *r,\n> >                 return error(_(\"no commits parsed.\"));\n> >\n> >         if (!is_rebase_i(opts)) {\n> > -               enum todo_command valid =\n> > -                       opts->action == REPLAY_PICK ? TODO_PICK : TODO_REVERT;\n> > +               enum todo_command valid;\n> >                 int i;\n> >\n> > -               for (i = 0; i < todo_list->nr; i++)\n> > +               switch (opts->action) {\n> > +               case REPLAY_PICK:\n> > +               case REPLAY_HISTORY_EDIT:\n> > +                       valid = TODO_PICK;\n> > +                       break;\n> > +               default:\n> > +                       valid = TODO_REVERT;\n> > +                       break;\n> > +               }\n> \n> I think I see this hunk repeated in a few places—maybe some\n> leftoverbits for a refactor?\n\nFair enough. I don't really see a strong reason why we shouldn't fix\nthis in the same patch though. We can for example do something like the\nbelow patch.\n\nPatrick\n\n-- >8 --\n\ndiff --git a/sequencer.c b/sequencer.c\nindex 898ac1a2a8..9a66e7d128 100644\n--- a/sequencer.c\n+++ b/sequencer.c\n@@ -3063,6 +3063,19 @@ static void todo_list_write_total_nr(struct todo_list *todo_list)\n \t}\n }\n \n+static enum todo_command action_to_command(enum replay_action action)\n+{\n+\tswitch (action) {\n+\tcase REPLAY_PICK:\n+\tcase REPLAY_HISTORY_EDIT:\n+\t\treturn TODO_PICK;\n+\tcase REPLAY_REVERT:\n+\t\treturn TODO_REVERT;\n+\tdefault:\n+\t\tBUG(\"unsupported action %d\", action);\n+\t}\n+}\n+\n static int read_populate_todo(struct repository *r,\n \t\t\t      struct todo_list *todo_list,\n \t\t\t      struct replay_opts *opts)\n@@ -3087,19 +3100,9 @@ static int read_populate_todo(struct repository *r,\n \t\treturn error(_(\"no commits parsed.\"));\n \n \tif (!is_rebase_i(opts)) {\n-\t\tenum todo_command valid;\n+\t\tenum todo_command valid = action_to_command(opts->action);\n \t\tint i;\n \n-\t\tswitch (opts->action) {\n-\t\tcase REPLAY_PICK:\n-\t\tcase REPLAY_HISTORY_EDIT:\n-\t\t\tvalid = TODO_PICK;\n-\t\t\tbreak;\n-\t\tdefault:\n-\t\t\tvalid = TODO_REVERT;\n-\t\t\tbreak;\n-\t\t}\n-\n \t\tfor (i = 0; i < todo_list->nr; i++) {\n \t\t\tif (valid == todo_list->items[i].command)\n \t\t\t\tcontinue;\n@@ -3408,16 +3411,7 @@ static int walk_revs_populate_todo(struct todo_list *todo_list,\n \tif (prepare_revs(opts))\n \t\treturn -1;\n \n-\tswitch (opts->action) {\n-\tcase REPLAY_PICK:\n-\tcase REPLAY_HISTORY_EDIT:\n-\t\tcommand = TODO_PICK;\n-\t\tbreak;\n-\tdefault:\n-\t\tcommand = TODO_REVERT;\n-\t\tbreak;\n-\t}\n-\n+\tcommand = action_to_command(opts->action);\n \tcommand_string = todo_command_info[command].str;\n \tencoding = get_log_output_encoding();\n \n@@ -5578,15 +5572,7 @@ static int single_pick(struct repository *r,\n \tint check_todo;\n \tstruct todo_item item;\n \n-\tswitch (opts->action) {\n-\tcase REPLAY_PICK:\n-\tcase REPLAY_HISTORY_EDIT:\n-\t\titem.command = TODO_PICK;\n-\t\tbreak;\n-\tdefault:\n-\t\titem.command = TODO_REVERT;\n-\t\tbreak;\n-\t}\n+\titem.command = action_to_command(opts->action);\n \titem.commit = cmit;\n \n \treturn do_pick_commit(r, &item, opts, 0, &check_todo);\n\n"},{"id":"525423","messageId":"aLgyZdXV7TUIx_y9@pks.im","threadId":"63986","inReplyTo":"xmqqjz2rs6fk.fsf@gitster.g","subject":"Re: [PATCH RFC v2 05/16] cache-tree: allow writing in-memory index as tree","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-09-03T12:19:49Z","receivedAt":"2025-09-03T12:19:55Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Mon, Aug 25, 2025 at 09:38:07AM -0700, Junio C Hamano wrote:\n> Patrick Steinhardt <ps@pks.im> writes:\n> \n> > The function `write_in_core_index_as_tree()` takes a repository and\n> > writes its index into a tree object. What this function cannot do though\n> > is to take an _arbitrary_ in-memory index.\n> >\n> > Introduce a new `struct index_state` parameter so that the caller can\n> > pass a different index than the one belonging to the repository. This\n> > will be used in a subsequent commit.\n> \n> Nice.  \n> \n> I wonder if this would also allow us to simplify the code paths for\n> \"git commit -o <pathspec>\", where we use a separate temporary index\n> that gets populated afresh from HEAD, grab the new snapshot for the\n> paths that match the pathspec, and write it out as a tree to be\n> wrapped in the new commit (and then the real index is also updated\n> at these same paths).\n> \n> I guess the code paths need to expose what is in the temporary index\n> to hooks, which means the index file needs to be written out to an\n> actual on-disk file, so the picture would be a bit different?\n\nWell, the sequencer itself is also writing out the temporary index to\ndisk. Took me quite a while to figure out why the index I wrote always\nturned out to only contain a subset of the changes I wanted. So I don't\nreally see a reason why we couldn't use the infra for other commands,\nbut cannot say whether or not it would end up improving the status quo.\n\nPatrick\n"},{"id":"525424","messageId":"aLgyai6muTkG21FM@pks.im","threadId":"63986","inReplyTo":"CALnO6CB=ZPO33gz0qyCGrC6E2_b2S1kGZkLd-q_CS37gupvPXA@mail.gmail.com","subject":"Re: [PATCH RFC v2 09/16] builtin/history: implement \"reorder\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-09-03T12:19:54Z","receivedAt":"2025-09-03T12:20:00Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Tue, Aug 26, 2025 at 09:03:18AM -0400, D. Ben Knoble wrote:\n> On Sun, Aug 24, 2025 at 1:43 PM Patrick Steinhardt <ps@pks.im> wrote:\n> > diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc\n> > index db5b292994..b36cd925dd 100644\n> > --- a/Documentation/git-history.adoc\n> > +++ b/Documentation/git-history.adoc\n> > @@ -39,12 +40,18 @@ rewrite history in different ways:\n> >  `drop <commit>`::\n> >         Drop a commit from the history and reapply all children of that\n> >         commit on top of the commit's parent. The commit that is to be\n> > -       dropped must be reachable from the current `HEAD` commit.\n> > +       dropped must be reachable from the currently checked-out commit.\n> >  +\n> >  Dropping the root commit converts the child of that commit into the new\n> >  root commit. It is invalid to drop a root commit that does not have any\n> >  child commits, as that would lead to an empty branch.\n> \n> Fixup in the wrong commit, maybe?\n\nIndeed, fixed now.\n\nPatrick\n"},{"id":"525425","messageId":"aLgycInNfe44RmgR@pks.im","threadId":"63986","inReplyTo":"602e215c-be91-458e-9961-c78cf6aed2a9@app.fastmail.com","subject":"Re: [PATCH RFC v2 15/16] builtin/history: implement \"split\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-09-03T12:20:00Z","receivedAt":"2025-09-03T12:20:06Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Sun, Aug 24, 2025 at 08:03:18PM +0200, Kristoffer Haugsbakk wrote:\n> On Sun, Aug 24, 2025, at 19:42, Patrick Steinhardt wrote:\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> The interactive mode here seems just-right.\n> \n> • Split in two, give the commit message for the new one\n> • I can use `git history split :/'The second batch'` if I want to split\n>   a single commit multiple times\n> • I can use `git history reword :/'The second batch'` if I want\n>   to change the original commit message as well\n> \n> But it’s interactive-only, correct?  Would it make sense for a stateful\n> split session so that other tools could be used to split the patch?\n\nYeah, it's interactive-only. I see git-history(1) as part of porcelain\nrather than plumbing, so this is kind of intentional. I also wouldn't\nquite know how this would look like to have a split session.\n\nBut if anyone has an idea how such a stateful split session might look\nlike I don't see a strong reason to not add this in the future. For now\nthough my focus is to get a baseline going that helps the user to\nperform common tasks easier as opposed to before.\n\nPatrick\n"},{"id":"525426","messageId":"aLgyfZEmjDAYewaV@pks.im","threadId":"63986","inReplyTo":"5079326d-497c-4335-88d0-a248e0b8c6d1@app.fastmail.com","subject":"Re: [PATCH RFC v2 16/16] builtin/history: implement \"reword\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-09-03T12:20:13Z","receivedAt":"2025-09-03T12:20:19Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Sun, Aug 24, 2025 at 08:08:07PM +0200, Kristoffer Haugsbakk wrote:\n> On Sun, Aug 24, 2025, at 19:42, Patrick Steinhardt wrote:\n> > Implement a new \"reword\" subcommand for git-history(1). This subcommand\n> > is essentially the same as if a user performed an interactive rebase\n> > with a single commit changed to use the \"reword\" verb.\n> >\n> > Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> \n> I get a “split” error when I typo the commit to reword:\n> \n>     $ ./git history reword ./s2\n>     error: commit to be split cannot be found: ./s2\n\nOh, indeed, this is a boring copy-paste error:\n\ndiff --git a/builtin/history.c b/builtin/history.c\nindex cdc93a1cbd..f03272bddd 100644\n--- a/builtin/history.c\n+++ b/builtin/history.c\n@@ -749,7 +749,7 @@ static int cmd_history_reword(int argc,\n \n \toriginal_commit = lookup_commit_reference_by_name(argv[0]);\n \tif (!original_commit) {\n-\t\tret = error(_(\"commit to be split cannot be found: %s\"), argv[0]);\n+\t\tret = error(_(\"commit to be reworded cannot be found: %s\"), argv[0]);\n \t\tgoto out;\n \t}\n\nThanks for noticing!\n\nPatrick\n"},{"id":"525427","messageId":"aLgygyUuQCmR25qU@pks.im","threadId":"63986","inReplyTo":"CALnO6CBRv-Jvm1FsUAkDSyaNNRXtMukReKzefvCw5-vj=-pnCA@mail.gmail.com","subject":"Re: [PATCH RFC v2 15/16] builtin/history: implement \"split\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-09-03T12:20:19Z","receivedAt":"2025-09-03T12:20:25Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Tue, Aug 26, 2025 at 09:14:49AM -0400, D. Ben Knoble wrote:\n> On Sun, Aug 24, 2025 at 1:44 PM Patrick Steinhardt <ps@pks.im> wrote:\n> > diff --git a/builtin/history.c b/builtin/history.c\n> > index 16b516856e..6d3f44152c 100644\n> > --- a/builtin/history.c\n> > +++ b/builtin/history.c\n> > @@ -517,6 +527,285 @@ static int cmd_history_reorder(int argc,\n[snip]\n> > +               wt_status_collect_changes_trees(&s, old_tree, new_tree);\n> > +               wt_status_print(&s);\n> > +               wt_status_collect_free_buffers(&s);\n> > +               string_list_clear_func(&s.change, change_data_free);\n> \n> I think I'm supposed to see the changes between the old and new trees,\n> right? Does this only happen if I use the interactive machinery to\n> edit a hunk? When I try accepting some changes and leaving others for\n> the next commit I get no diff in the template.\n\nYeah, it's supposed to show the diff between old and new tree indeed. So\nin theory you should see something.\n\n> I did try to add new diff lines to a hunk, and nothing showed up…\n> maybe I'm holding it wrong? I'm pretty sure I compiled this version.\n\nDo you maybe have a reproducer for this? It seems to work alright for\nme, but I wouldn't be surprised if there was a bug here. The wt-status\ninterfaces are quite something and I was tearing my hair while trying to\nfigure them out.\n\n> It doesn't look like it's triggered only on commit.verbose config, either.\n\nFixed now.\n\n> > +\n> > +               strbuf_reset(out);\n> > +               if (launch_editor(path, out, NULL)) {\n> > +                       fprintf(stderr, _(\"Please supply the message using either -m or -F option.\\n\"));\n> \n> According to the usage, git history split only supports -m, not -F ;)\n\nTrue. I didn't want to add too many options right from the start to keep\nthe series somewhat simple. We should eventually add it though.\n\n> > +                       return -1;\n> > +               }\n> > +               strbuf_stripspace(out, comment_line_str);\n> > +\n> > +       } else {\n> > +               strbuf_addstr(out, provided_message);\n> > +       }\n> > +\n> > +       cleanup_message(out, COMMIT_MSG_CLEANUP_ALL, 0);\n> > +\n> > +       if (!out->len) {\n> > +               fprintf(stderr, _(\"Aborting commit due to empty commit message.\\n\"));\n> \n> It _would_ be nice if this and similar errors left me able to \"try\n> again\" without losing staged changes—I think I mentioned this before,\n> though. And with the in-memory indices vs. actual working state,\n> presenting a UI here could be very difficult. So it's an\n> understandable choice.\n\nYeah, I don't dare touching this yet, but certainly see that this might\nbe a worthwhile addition as we iterate on this command.\n\nPatrick\n"},{"id":"525468","messageId":"CALnO6CAjv0=99AcBLU=z71hGrk0fXp9pWm7wiBF-a1Xd_efrdA@mail.gmail.com","threadId":"63986","inReplyTo":"aLgygyUuQCmR25qU@pks.im","subject":"Re: [PATCH RFC v2 15/16] builtin/history: implement \"split\" subcommand","fromName":"D. Ben Knoble","fromEmail":"ben.knoble@gmail.com","sentAt":"2025-09-03T21:55:28Z","receivedAt":"2025-09-03T21:55:41Z","isPatch":true,"sender":{"key":"ben.knoble@gmail.com","avatar":"https://avatars.githubusercontent.com/u/22802209?v=4"},"body":"On Wed, Sep 3, 2025 at 8:20 AM Patrick Steinhardt <ps@pks.im> wrote:\n>\n> On Tue, Aug 26, 2025 at 09:14:49AM -0400, D. Ben Knoble wrote:\n> > On Sun, Aug 24, 2025 at 1:44 PM Patrick Steinhardt <ps@pks.im> wrote:\n> > > diff --git a/builtin/history.c b/builtin/history.c\n> > > index 16b516856e..6d3f44152c 100644\n> > > --- a/builtin/history.c\n> > > +++ b/builtin/history.c\n> > > @@ -517,6 +527,285 @@ static int cmd_history_reorder(int argc,\n> [snip]\n> > > +               wt_status_collect_changes_trees(&s, old_tree, new_tree);\n> > > +               wt_status_print(&s);\n> > > +               wt_status_collect_free_buffers(&s);\n> > > +               string_list_clear_func(&s.change, change_data_free);\n> >\n> > I think I'm supposed to see the changes between the old and new trees,\n> > right? Does this only happen if I use the interactive machinery to\n> > edit a hunk? When I try accepting some changes and leaving others for\n> > the next commit I get no diff in the template.\n>\n> Yeah, it's supposed to show the diff between old and new tree indeed. So\n> in theory you should see something.\n>\n> > I did try to add new diff lines to a hunk, and nothing showed up…\n> > maybe I'm holding it wrong? I'm pretty sure I compiled this version.\n>\n> Do you maybe have a reproducer for this? It seems to work alright for\n> me, but I wouldn't be surprised if there was a bug here. The wt-status\n> interfaces are quite something and I was tearing my hair while trying to\n> figure them out.\n\nHm. I have a copy of these patches at\nhttps://github.com/benknoble/git/tree/ps-jj. After \"make DEVELOPER=1\n-j $(nproc)\" on that branch, I did\n\n    bin-wrappers/git history split @~3\n    <input y,q> # once I even used \"e\" and added new diffs to the patch\n    <type commit message> # no status info\n    <exit editor>\n\nThen it looks like the 2nd commit gets created automatically. Maybe\nI'm just missing how this should work? Thanks for looking at it.\n\n-- \nD. Ben Knoble\n"},{"id":"525474","messageId":"CALnO6CC5K81QRxJR3jvqyL+fO6Yec_Ee83EOeeiUEzF6oBV3UA@mail.gmail.com","threadId":"63986","inReplyTo":"20250824-b4-pks-history-builtin-v2-0-964ac12f65bd@pks.im","subject":"Re: [PATCH RFC v2 00/16] Introduce git-history(1) command for easy history editing","fromName":"D. Ben Knoble","fromEmail":"ben.knoble@gmail.com","sentAt":"2025-09-03T23:39:58Z","receivedAt":"2025-09-03T23:40:11Z","isPatch":true,"sender":{"key":"ben.knoble@gmail.com","avatar":"https://avatars.githubusercontent.com/u/22802209?v=4"},"body":"On Sun, Aug 24, 2025 at 1:42 PM Patrick Steinhardt <ps@pks.im> wrote:\n> Changes in v2:\n>   - Add a new \"reword\" subcommand.\n>   - List git-history(1) in \"command-list.txt\".\n>   - Add some missing error handling.\n>   - Simplify calling convention of `apply_commits()` to handle root\n>     commits internally instead of requiring every caller to do so.\n>   - Add tests to verify that git-history(1) refuses to work with changes\n>     in the worktree or index.\n>   - Mark git-history(1) as experimental.\n>   - Introduce commands to manage interrupted history edits.\n>   - A bunch of improvements to the manpage.\n>   - Link to v1: https://lore.kernel.org/r/20250819-b4-pks-history-builtin-v1-0-9b77c32688fe@pks.im\n\nHaving test-driven this a bit, I wanted to mention a feature from my\neditor that I use frequently and that \"reword\" reminded me of [*]. I\nwonder if this would be a good fit for git-history, but certainly\nwouldn't be _necessary_ for a v1.\n\nWith fugitive.vim [1], I can get a list of {staged,unstaged} changes\nand commits. It looks something like this\n\nHead: ps-jj\nRebase: origin/master\nPush: benknoble/ps-jj\nHelp: g?\n\nUnstaged (1)\nM README.md\n\nUnpushed to origin/master (16)\n5c65c405e6 builtin/history: implement \"reword\" subcommand\nb215e38d49 builtin/history: implement \"split\" subcommand\n93977ef8d2 wt-status: provide function to expose status for trees\nb09b4b9d48 add-patch: add support for in-memory index patching\n[…]\n\nIf I stage changes, then the following mappings are available when the\ncursor is on any commit line:\n\ncF                      Create a `fixup!` commit for the commit under the\n                        cursor and immediately rebase it.\n\ncS                      Create a `squash!` commit for the commit under the\n                        cursor and immediately rebase it.\n\nWhat they do is populate my command line with something like\n\n:Git commit --fixup=<commit>|Git -c sequence.editor=true rebase\n--interactive --autosquash <commit>^\n\n(The pipe is Vim's command separator, a bit like \";\" or \"&&\" in shell.)\n\nIf git-history had a squash or fixup mode, I imagine it would function\nsimilarly (and could be used as the backend for fugitive's cF/cS with\nnew enough Git).\n\n[*] The reason I thought of this from \"reword\" is the obvious tie-in\nto interactive rebase commands. Fugitive does have \"cw\" to reword the\nlast commit, \"ca\" to amend, and \"cW\" to \"amend!\" an arbitrary commit,\nbut no mappings that \"amend!\" + rebase immediately. It doesn't need\nthem because \"rw\" starts an interactive rebase with the commit under\ncursor set to \"reword\" ;) likewise \"rd\" for drop.\n\n[1]: https://github.com/tpope/vim-fugitive\n\n-- \nD. Ben Knoble\n"},{"id":"525503","messageId":"aLmMycWf-SMBw2bX@pks.im","threadId":"63986","inReplyTo":"CALnO6CAjv0=99AcBLU=z71hGrk0fXp9pWm7wiBF-a1Xd_efrdA@mail.gmail.com","subject":"Re: [PATCH RFC v2 15/16] builtin/history: implement \"split\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-09-04T12:57:45Z","receivedAt":"2025-09-04T12:57:51Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Wed, Sep 03, 2025 at 05:55:28PM -0400, D. Ben Knoble wrote:\n> On Wed, Sep 3, 2025 at 8:20 AM Patrick Steinhardt <ps@pks.im> wrote:\n> >\n> > On Tue, Aug 26, 2025 at 09:14:49AM -0400, D. Ben Knoble wrote:\n> > > On Sun, Aug 24, 2025 at 1:44 PM Patrick Steinhardt <ps@pks.im> wrote:\n> > > > diff --git a/builtin/history.c b/builtin/history.c\n> > > > index 16b516856e..6d3f44152c 100644\n> > > > --- a/builtin/history.c\n> > > > +++ b/builtin/history.c\n> > > > @@ -517,6 +527,285 @@ static int cmd_history_reorder(int argc,\n> > [snip]\n> > > > +               wt_status_collect_changes_trees(&s, old_tree, new_tree);\n> > > > +               wt_status_print(&s);\n> > > > +               wt_status_collect_free_buffers(&s);\n> > > > +               string_list_clear_func(&s.change, change_data_free);\n> > >\n> > > I think I'm supposed to see the changes between the old and new trees,\n> > > right? Does this only happen if I use the interactive machinery to\n> > > edit a hunk? When I try accepting some changes and leaving others for\n> > > the next commit I get no diff in the template.\n> >\n> > Yeah, it's supposed to show the diff between old and new tree indeed. So\n> > in theory you should see something.\n> >\n> > > I did try to add new diff lines to a hunk, and nothing showed up…\n> > > maybe I'm holding it wrong? I'm pretty sure I compiled this version.\n> >\n> > Do you maybe have a reproducer for this? It seems to work alright for\n> > me, but I wouldn't be surprised if there was a bug here. The wt-status\n> > interfaces are quite something and I was tearing my hair while trying to\n> > figure them out.\n> \n> Hm. I have a copy of these patches at\n> https://github.com/benknoble/git/tree/ps-jj. After \"make DEVELOPER=1\n> -j $(nproc)\" on that branch, I did\n> \n>     bin-wrappers/git history split @~3\n>     <input y,q> # once I even used \"e\" and added new diffs to the patch\n>     <type commit message> # no status info\n>     <exit editor>\n> \n> Then it looks like the 2nd commit gets created automatically. Maybe\n> I'm just missing how this should work? Thanks for looking at it.\n\nWeird. I used the exact same branch, command and input and did have\nstatus information in my editor. Note that for now the editor only asks\nfor the commit message of the first commit. The second commit is\nbasically retained from the original: both tree and commit message are\nthe exact same, only difference is its parent.\n\nPatrick\n"},{"id":"525504","messageId":"aLmOfV4WU6RdpdLO@pks.im","threadId":"63986","inReplyTo":"CALnO6CC5K81QRxJR3jvqyL+fO6Yec_Ee83EOeeiUEzF6oBV3UA@mail.gmail.com","subject":"Re: [PATCH RFC v2 00/16] Introduce git-history(1) command for easy history editing","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-09-04T13:05:01Z","receivedAt":"2025-09-04T13:05:08Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Wed, Sep 03, 2025 at 07:39:58PM -0400, D. Ben Knoble wrote:\n> If git-history had a squash or fixup mode, I imagine it would function\n> similarly (and could be used as the backend for fugitive's cF/cS with\n> new enough Git).\n\nYup! I definitely do want to introduce a squash command eventually.\n\nPatrick\n"},{"id":"525524","messageId":"20250904-b4-pks-history-builtin-v3-0-509053514755@pks.im","threadId":"63986","inReplyTo":"20250819-b4-pks-history-builtin-v1-0-9b77c32688fe@pks.im","subject":"[PATCH RFC v3 00/18] Introduce git-history(1) command for easy history editing","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-09-04T14:27:30Z","receivedAt":"2025-09-04T14:27:39Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Hi,\n\nover recent months I've been playing around with Jujutsu quite\nfrequently. While I still prefer using Git, there's been a couple\nfeatures in it that I really like and that I'd like to have in Git, as\nwell.\n\nA copule of these features relate to history editing. Most importantly,\nI really dig the following commands:\n\n  - jj-abandon(1) to drop a specific commit from your history.\n\n  - jj-absorb(1) to take some changes and automatically apply them to\n    commits in your history that last modified the respective hunks.\n\n  - jj-split(1) to split a commit into two.\n\n  - jj-new(1) to insert a new commit after or before a specific other\n    commit.\n\nNot all of these commands can be ported directly into Git. jj-new(1) for\nexample doesn't really make a ton of sense for us, I'd claim. But some\nof these commands _do_ make sense.\n\nI thus had a look at implementing some of these commands in Git itself,\nwhere the result is this patch series. Specifically, the following\ncommands are introduced by this patch series:\n\n  - `git history drop` to drop a specific commit. This is basically the\n    same as jj-abandon(1).\n\n  - `git history reorder` to reorder a specific commit before or after\n    another commit. This is inspired by jj-new(1).\n\n  - `git history split` takes a commit and splits it into two. This is\n    basically the same as jj-split(1).\n\nIf this is something we want to have I think it'd be just a starting\npoint. There's other commands that I think are quite common and that\nmight make sense to introduce eventually:\n\n  - An equivalent to jj-absorb(1) would be awesome to have.\n\n  - `git history reword` to change only the commit message of a specific\n    commit.\n\n  - `git history squash` to squash together multiple commits into one.\n\nIn the end, I'd like us to learn from what people like about Jujutsu and\napply those learnings to Git. We won't be able to apply all learnings\nfrom Jujutsu, as the workflow is quite different there due to the lack\nof the index. But other things we certainly can apply to Git directly.\n\nChanges in v2:\n  - Add a new \"reword\" subcommand.\n  - List git-history(1) in \"command-list.txt\".\n  - Add some missing error handling.\n  - Simplify calling convention of `apply_commits()` to handle root\n    commits internally instead of requiring every caller to do so.\n  - Add tests to verify that git-history(1) refuses to work with changes\n    in the worktree or index.\n  - Mark git-history(1) as experimental.\n  - Introduce commands to manage interrupted history edits.\n  - A bunch of improvements to the manpage.\n  - Link to v1: https://lore.kernel.org/r/20250819-b4-pks-history-builtin-v1-0-9b77c32688fe@pks.im\n\nChanges in v3:\n  - Add logic to drive the \"post-rewrite\" hook and add tests to verify\n    that all hooks are executed as expected.\n  - Deduplicate logic to turn a replay action into a todo command.\n  - Move the addition of tests for the top-level git-history(1) command\n    to the correct commit.\n  - Some smaller commit message fixes.\n  - Honor \"commit.verbose\".\n  - Fix copy-paste error with an error message.\n  - Link to v2: https://lore.kernel.org/r/20250824-b4-pks-history-builtin-v2-0-964ac12f65bd@pks.im\n\nNote: this patch series is growing quite large overall. I'll send one\nlast version of the complete series with the RFC tag, but after that\nI'll probably split the series into two and stop after introducing the\n\"reorder\" command.\n\nThanks!\n\nPatrick\n\n---\nPatrick Steinhardt (18):\n      sequencer: optionally skip printing commit summary\n      sequencer: add option to rewind HEAD after picking commits\n      sequencer: introduce new history editing mode\n      sequencer: stop using `the_repository` in `sequencer_remove_state()`\n      sequencer: wire up \"rewritten-hook\" for REPLAY_HISTORY_EDIT\n      cache-tree: allow writing in-memory index as tree\n      builtin: add new \"history\" command\n      builtin/history: introduce subcommands to manage interrupted rewrites\n      builtin/history: implement \"drop\" subcommand\n      builtin/history: implement \"reorder\" subcommand\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      wt-status: provide function to expose status for trees\n      sequencer: allow callers to provide mappings for the old commit\n      builtin/history: implement \"split\" subcommand\n      builtin/history: implement \"reword\" subcommand\n\n .gitignore                     |   1 +\n Documentation/git-history.adoc | 188 ++++++++\n Documentation/meson.build      |   1 +\n Makefile                       |   1 +\n add-interactive.c              | 151 ++-----\n add-interactive.h              |  43 +-\n add-patch.c                    | 270 ++++++++++--\n add-patch.h                    |  61 +++\n builtin.h                      |   1 +\n builtin/add.c                  |  22 +-\n builtin/checkout.c             |   7 +-\n builtin/commit.c               |  16 +-\n builtin/history.c              | 958 +++++++++++++++++++++++++++++++++++++++++\n builtin/rebase.c               |   4 +-\n builtin/reset.c                |  16 +-\n builtin/revert.c               |   2 +-\n builtin/stash.c                |  46 +-\n cache-tree.c                   |   5 +-\n cache-tree.h                   |   3 +-\n command-list.txt               |   1 +\n commit.h                       |   2 +-\n git.c                          |   1 +\n meson.build                    |   1 +\n sequencer.c                    | 259 ++++++++---\n sequencer.h                    |  23 +-\n t/meson.build                  |   7 +-\n t/t3450-history.sh             |  42 ++\n t/t3451-history-drop.sh        | 207 +++++++++\n t/t3452-history-reorder.sh     | 278 ++++++++++++\n t/t3453-history-split.sh       | 468 ++++++++++++++++++++\n t/t3454-history-reword.sh      | 202 +++++++++\n wt-status.c                    |  24 ++\n wt-status.h                    |   3 +\n 33 files changed, 3005 insertions(+), 309 deletions(-)\n\nRange-diff versus v2:\n\n 1:  ac3e60d11f =  1:  d397c84460 sequencer: optionally skip printing commit summary\n 2:  411212b581 =  2:  9f6444ab79 sequencer: add option to rewind HEAD after picking commits\n 3:  670a1879ea !  3:  6918f3fc1b sequencer: introduce new history editing mode\n    @@ sequencer.c: void sequencer_post_commit_cleanup(struct repository *r, int verbos\n      \tif (refs_ref_exists(get_main_ref_store(r), \"REVERT_HEAD\")) {\n      \t\tif (!refs_delete_ref(get_main_ref_store(r), \"\", \"REVERT_HEAD\",\n      \t\t\t\t     NULL, REF_NO_DEREF) &&\n    +@@ sequencer.c: static void todo_list_write_total_nr(struct todo_list *todo_list)\n    + \t}\n    + }\n    + \n    ++static enum todo_command action_to_command(enum replay_action action)\n    ++{\n    ++\tswitch (action) {\n    ++\tcase REPLAY_PICK:\n    ++\tcase REPLAY_HISTORY_EDIT:\n    ++\t\treturn TODO_PICK;\n    ++\tcase REPLAY_REVERT:\n    ++\t\treturn TODO_REVERT;\n    ++\tdefault:\n    ++\t\tBUG(\"unsupported action %d\", action);\n    ++\t}\n    ++}\n    ++\n    + static int read_populate_todo(struct repository *r,\n    + \t\t\t      struct todo_list *todo_list,\n    + \t\t\t      struct replay_opts *opts)\n     @@ sequencer.c: static int read_populate_todo(struct repository *r,\n      \t\treturn error(_(\"no commits parsed.\"));\n      \n      \tif (!is_rebase_i(opts)) {\n     -\t\tenum todo_command valid =\n     -\t\t\topts->action == REPLAY_PICK ? TODO_PICK : TODO_REVERT;\n    -+\t\tenum todo_command valid;\n    ++\t\tenum todo_command valid = action_to_command(opts->action);\n      \t\tint i;\n      \n     -\t\tfor (i = 0; i < todo_list->nr; i++)\n    -+\t\tswitch (opts->action) {\n    -+\t\tcase REPLAY_PICK:\n    -+\t\tcase REPLAY_HISTORY_EDIT:\n    -+\t\t\tvalid = TODO_PICK;\n    -+\t\t\tbreak;\n    -+\t\tdefault:\n    -+\t\t\tvalid = TODO_REVERT;\n    -+\t\t\tbreak;\n    -+\t\t}\n    -+\n     +\t\tfor (i = 0; i < todo_list->nr; i++) {\n      \t\t\tif (valid == todo_list->items[i].command)\n      \t\t\t\tcontinue;\n    @@ sequencer.c: int write_basic_state(struct replay_opts *opts, const char *head_na\n      \tif (prepare_revs(opts))\n      \t\treturn -1;\n      \n    -+\tswitch (opts->action) {\n    -+\tcase REPLAY_PICK:\n    -+\tcase REPLAY_HISTORY_EDIT:\n    -+\t\tcommand = TODO_PICK;\n    -+\t\tbreak;\n    -+\tdefault:\n    -+\t\tcommand = TODO_REVERT;\n    -+\t\tbreak;\n    -+\t}\n    -+\n    ++\tcommand = action_to_command(opts->action);\n     +\tcommand_string = todo_command_info[command].str;\n      \tencoding = get_log_output_encoding();\n      \n    @@ sequencer.c: static int single_pick(struct repository *r,\n      \n     -\titem.command = opts->action == REPLAY_PICK ?\n     -\t\t\tTODO_PICK : TODO_REVERT;\n    -+\tswitch (opts->action) {\n    -+\tcase REPLAY_PICK:\n    -+\tcase REPLAY_HISTORY_EDIT:\n    -+\t\titem.command = TODO_PICK;\n    -+\t\tbreak;\n    -+\tdefault:\n    -+\t\titem.command = TODO_REVERT;\n    -+\t\tbreak;\n    -+\t}\n    ++\titem.command = action_to_command(opts->action);\n      \titem.commit = cmit;\n      \n      \treturn do_pick_commit(r, &item, opts, 0, &check_todo);\n    @@ sequencer.h: extern const char *rebase_resolvemsg;\n      };\n      \n      enum commit_msg_cleanup_mode {\n    -\n    - ## t/t3450-history.sh (new) ##\n    -@@\n    -+#!/bin/sh\n    -+\n    -+test_description='tests for git-history command'\n    -+\n    -+. ./test-lib.sh\n    -+\n    -+test_expect_success 'refuses to do anything without subcommand' '\n    -+\ttest_must_fail git history 2>err &&\n    -+\ttest_grep foo err\n    -+'\n    -+\n    -+test_done\n 4:  6f2605f63d =  4:  e5317d44a3 sequencer: stop using `the_repository` in `sequencer_remove_state()`\n -:  ---------- >  5:  cc5108af10 sequencer: wire up \"rewritten-hook\" for REPLAY_HISTORY_EDIT\n 5:  727ec4955d =  6:  457042c591 cache-tree: allow writing in-memory index as tree\n 6:  eec4f74fd8 !  7:  1acf4de28a builtin: add new \"history\" command\n    @@ meson.build: builtin_sources = [\n        'builtin/hook.c',\n        'builtin/index-pack.c',\n        'builtin/init-db.c',\n    +\n    + ## t/meson.build ##\n    +@@ t/meson.build: integration_tests = [\n    +   't3436-rebase-more-options.sh',\n    +   't3437-rebase-fixup-options.sh',\n    +   't3438-rebase-broken-files.sh',\n    ++  't3450-history.sh',\n    +   't3500-cherry.sh',\n    +   't3501-revert-cherry-pick.sh',\n    +   't3502-cherry-pick-merge.sh',\n    +@@ t/meson.build: if perl.found() and time.found()\n    +       timeout: 0,\n    +     )\n    +   endforeach\n    +-endif\n    + \\ No newline at end of file\n    ++endif\n    +\n    + ## t/t3450-history.sh (new) ##\n    +@@\n    ++#!/bin/sh\n    ++\n    ++test_description='tests for git-history command'\n    ++\n    ++. ./test-lib.sh\n    ++\n    ++test_expect_success 'refuses to do anything without subcommand' '\n    ++\ttest_must_fail git history 2>err &&\n    ++\ttest_grep foo err\n    ++'\n    ++\n    ++test_done\n 7:  2820521f46 !  8:  07fff4c157 builtin/history: introduce subcommands to manage interrupted rewrites\n    @@ builtin/history.c\n     +\treturn fn(argc, argv, prefix, repo);\n      }\n     \n    - ## t/meson.build ##\n    -@@ t/meson.build: integration_tests = [\n    -   't3436-rebase-more-options.sh',\n    -   't3437-rebase-fixup-options.sh',\n    -   't3438-rebase-broken-files.sh',\n    -+  't3450-history.sh',\n    -   't3500-cherry.sh',\n    -   't3501-revert-cherry-pick.sh',\n    -   't3502-cherry-pick-merge.sh',\n    -@@ t/meson.build: if perl.found() and time.found()\n    -       timeout: 0,\n    -     )\n    -   endforeach\n    --endif\n    - \\ No newline at end of file\n    -+endif\n    -\n      ## t/t3450-history.sh ##\n     @@ t/t3450-history.sh: test_description='tests for git-history command'\n      \n 8:  3438f23f1b !  9:  a68cd72864 builtin/history: implement \"drop\" subcommand\n    @@ Documentation/git-history.adoc: COMMANDS\n     +`drop <commit>`::\n     +\tDrop a commit from the history and reapply all children of that\n     +\tcommit on top of the commit's parent. The commit that is to be\n    -+\tdropped must be reachable from the current `HEAD` commit.\n    ++\tdropped must be reachable from the currently checked-out commit.\n     ++\n     +Dropping the root commit converts the child of that commit into the new\n     +root commit. It is invalid to drop a root commit that does not have any\n    @@ t/t3451-history-drop.sh (new)\n     +\t)\n     +'\n     +\n    ++test_expect_success 'hooks are executed for rewritten commits' '\n    ++\ttest_when_finished \"rm -rf repo\" &&\n    ++\tgit init repo &&\n    ++\t(\n    ++\t\tcd repo &&\n    ++\t\ttest_commit first &&\n    ++\t\ttest_commit second &&\n    ++\t\ttest_commit third &&\n    ++\n    ++\t\twrite_script .git/hooks/prepare-commit-msg <<-EOF &&\n    ++\t\techo \"prepare-commit-msg: \\$@\" >>\"$(pwd)/hooks.log\"\n    ++\t\tEOF\n    ++\t\twrite_script .git/hooks/post-commit <<-EOF &&\n    ++\t\techo \"post-commit\" >>\"$(pwd)/hooks.log\"\n    ++\t\tEOF\n    ++\t\twrite_script .git/hooks/post-rewrite <<-EOF &&\n    ++\t\t{\n    ++\t\t\techo \"post-rewrite: \\$@\"\n    ++\t\t\tcat\n    ++\t\t} >>\"$(pwd)/hooks.log\"\n    ++\t\tEOF\n    ++\n    ++\t\tgit history drop HEAD~ &&\n    ++\t\tcat >expect <<-EOF &&\n    ++\t\tprepare-commit-msg: .git/COMMIT_EDITMSG message\n    ++\t\tpost-commit\n    ++\t\tpost-rewrite: history\n    ++\t\t$(git rev-parse third) $(git rev-parse HEAD)\n    ++\t\tEOF\n    ++\t\ttest_cmp expect hooks.log\n    ++\t)\n    ++'\n    ++\n     +test_expect_success 'can drop root commit' '\n     +\ttest_when_finished \"rm -rf repo\" &&\n     +\tgit init repo &&\n 9:  1af60a15fe ! 10:  3caf8f8e23 builtin/history: implement \"reorder\" subcommand\n    @@ Documentation/git-history.adoc: git history abort\n      \n      DESCRIPTION\n      -----------\n    -@@ Documentation/git-history.adoc: rewrite history in different ways:\n    - `drop <commit>`::\n    - \tDrop a commit from the history and reapply all children of that\n    - \tcommit on top of the commit's parent. The commit that is to be\n    --\tdropped must be reachable from the current `HEAD` commit.\n    -+\tdropped must be reachable from the currently checked-out commit.\n    - +\n    - Dropping the root commit converts the child of that commit into the new\n    +@@ Documentation/git-history.adoc: Dropping the root commit converts the child of that commit into the new\n      root commit. It is invalid to drop a root commit that does not have any\n      child commits, as that would lead to an empty branch.\n      \n    @@ t/t3452-history-reorder.sh (new)\n     +\t)\n     +'\n     +\n    ++test_expect_success 'hooks are executed for rewritten commits' '\n    ++\ttest_when_finished \"rm -rf repo\" &&\n    ++\tgit init repo &&\n    ++\t(\n    ++\t\tcd repo &&\n    ++\t\ttest_commit first &&\n    ++\t\ttest_commit second &&\n    ++\t\ttest_commit third &&\n    ++\n    ++\t\twrite_script .git/hooks/prepare-commit-msg <<-EOF &&\n    ++\t\techo \"prepare-commit-msg: \\$@\" >>\"$(pwd)/hooks.log\"\n    ++\t\tEOF\n    ++\t\twrite_script .git/hooks/post-commit <<-EOF &&\n    ++\t\techo \"post-commit\" >>\"$(pwd)/hooks.log\"\n    ++\t\tEOF\n    ++\t\twrite_script .git/hooks/post-rewrite <<-EOF &&\n    ++\t\t{\n    ++\t\t\techo \"post-rewrite: \\$@\"\n    ++\t\t\tcat\n    ++\t\t} >>\"$(pwd)/hooks.log\"\n    ++\t\tEOF\n    ++\n    ++\t\tgit history reorder :/third --before=:/second &&\n    ++\t\tcat >expect <<-EOF &&\n    ++\t\tsecond\n    ++\t\tthird\n    ++\t\tfirst\n    ++\t\tEOF\n    ++\t\tgit log --format=%s >actual &&\n    ++\t\ttest_cmp expect actual &&\n    ++\n    ++\t\tcat >expect <<-EOF &&\n    ++\t\tprepare-commit-msg: .git/COMMIT_EDITMSG message\n    ++\t\tpost-commit\n    ++\t\tprepare-commit-msg: .git/COMMIT_EDITMSG message\n    ++\t\tpost-commit\n    ++\t\tpost-rewrite: history\n    ++\t\t$(git rev-parse third) $(git rev-parse HEAD~)\n    ++\t\t$(git rev-parse second) $(git rev-parse HEAD)\n    ++\t\tEOF\n    ++\t\ttest_cmp expect hooks.log\n    ++\t)\n    ++'\n    ++\n     +test_expect_success 'conflicts are detected' '\n     +\ttest_when_finished \"rm -rf repo\" &&\n     +\tgit init repo &&\n10:  a5e90d5b2f = 11:  5f509b2edc add-patch: split out header from \"add-interactive.h\"\n11:  927f2d226b = 12:  cba60462f5 add-patch: split out `struct interactive_options`\n12:  1b232881f2 = 13:  316b830d2c add-patch: remove dependency on \"add-interactive\" subsystem\n13:  4f1fcc0f85 = 14:  4e964cdcf8 add-patch: add support for in-memory index patching\n14:  b88ac830f1 = 15:  9b33852290 wt-status: provide function to expose status for trees\n -:  ---------- > 16:  2a7fc74da1 sequencer: allow callers to provide mappings for the old commit\n15:  163e8d4d6e ! 17:  9b68e75de2 builtin/history: implement \"split\" subcommand\n    @@ Commit message\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 of it into a separate commit. This is quite an involved operation\n    -    though:\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    @@ builtin/history.c\n      \n      static int cmd_history_abort(int argc,\n      \t\t\t     const char **argv,\n    +@@ builtin/history.c: static int apply_commits(struct repository *repo,\n    + \t\t\t const struct strvec *commits,\n    + \t\t\t struct commit *head,\n    + \t\t\t struct commit *base,\n    ++\t\t\t const struct oidmap *rewritten_commits,\n    + \t\t\t const char *action)\n    + {\n    + \tstruct setup_revision_opt revision_opts = {\n    +@@ builtin/history.c: static int apply_commits(struct repository *repo,\n    + \t\treplay_opts.strategy = replay_opts.default_strategy;\n    + \t\treplay_opts.default_strategy = NULL;\n    + \t}\n    ++\treplay_opts.old_oid_mappings = rewritten_commits;\n    + \n    + \tstrvec_push(&args, \"\");\n    + \tstrvec_pushv(&args, commits->v);\n    +@@ builtin/history.c: static int cmd_history_drop(int argc,\n    + \t\tif (ret < 0)\n    + \t\t\tgoto out;\n    + \n    +-\t\tret = apply_commits(repo, &commits, head, commit_to_drop, \"drop\");\n    ++\t\tret = apply_commits(repo, &commits, head, commit_to_drop,\n    ++\t\t\t\t    NULL, \"drop\");\n    + \t\tif (ret < 0)\n    + \t\t\tgoto out;\n    + \t}\n    +@@ builtin/history.c: static int cmd_history_reorder(int argc,\n    + \treplace_commits(&commits, &commit_to_reorder->object.oid, NULL, 0);\n    + \treplace_commits(&commits, &anchor->object.oid, replacement, ARRAY_SIZE(replacement));\n    + \n    +-\tret = apply_commits(repo, &commits, head, old, \"reorder\");\n    ++\tret = apply_commits(repo, &commits, head, old, NULL, \"reorder\");\n    + \tif (ret < 0)\n    + \t\tgoto out;\n    + \n     @@ builtin/history.c: static int cmd_history_reorder(int argc,\n      \treturn ret;\n      }\n    @@ builtin/history.c: static int cmd_history_reorder(int argc,\n     +\t\t\t       struct strbuf *out)\n     +{\n     +\tif (!provided_message) {\n    -+\t\tstruct wt_status s;\n     +\t\tconst char *path = git_path_commit_editmsg();\n     +\t\tconst char *hint =\n     +\t\t\t_(\"Please enter the commit message for the %s changes. Lines starting\\n\"\n     +\t\t\t  \"with '%s' will be kept; you may remove them yourself if you want to.\\n\");\n    ++\t\tint verbose = 1;\n     +\n     +\t\tstrbuf_addstr(out, default_message);\n     +\t\tstrbuf_addch(out, '\\n');\n     +\t\tstrbuf_commented_addf(out, comment_line_str, hint, action, comment_line_str);\n     +\t\twrite_file_buf(path, out->buf, out->len);\n     +\n    -+\t\twt_status_prepare(repo, &s);\n    -+\t\tFREE_AND_NULL(s.branch);\n    -+\t\ts.ahead_behind_flags = AHEAD_BEHIND_QUICK;\n    -+\t\ts.commit_template = 1;\n    -+\t\ts.colopts = 0;\n    -+\t\ts.display_comment_prefix = 1;\n    -+\t\ts.hints = 0;\n    -+\t\ts.use_color = 0;\n    -+\t\ts.whence = FROM_COMMIT;\n    -+\t\ts.committable = 1;\n    -+\n    -+\t\ts.fp = fopen(git_path_commit_editmsg(), \"a\");\n    -+\t\tif (!s.fp)\n    -+\t\t\treturn error_errno(_(\"could not open '%s'\"), git_path_commit_editmsg());\n    -+\n    -+\t\twt_status_collect_changes_trees(&s, old_tree, new_tree);\n    -+\t\twt_status_print(&s);\n    -+\t\twt_status_collect_free_buffers(&s);\n    -+\t\tstring_list_clear_func(&s.change, change_data_free);\n    ++\t\trepo_config_get_bool(repo, \"commit.verbose\", &verbose);\n    ++\t\tif (verbose) {\n    ++\t\t\tstruct wt_status s;\n    ++\n    ++\t\t\twt_status_prepare(repo, &s);\n    ++\t\t\tFREE_AND_NULL(s.branch);\n    ++\t\t\ts.ahead_behind_flags = AHEAD_BEHIND_QUICK;\n    ++\t\t\ts.commit_template = 1;\n    ++\t\t\ts.colopts = 0;\n    ++\t\t\ts.display_comment_prefix = 1;\n    ++\t\t\ts.hints = 0;\n    ++\t\t\ts.use_color = 0;\n    ++\t\t\ts.whence = FROM_COMMIT;\n    ++\t\t\ts.committable = 1;\n    ++\n    ++\t\t\ts.fp = fopen(git_path_commit_editmsg(), \"a\");\n    ++\t\t\tif (!s.fp)\n    ++\t\t\t\treturn error_errno(_(\"could not open '%s'\"), git_path_commit_editmsg());\n    ++\n    ++\t\t\twt_status_collect_changes_trees(&s, old_tree, new_tree);\n    ++\t\t\twt_status_print(&s);\n    ++\t\t\twt_status_collect_free_buffers(&s);\n    ++\t\t\tstring_list_clear_func(&s.change, change_data_free);\n    ++\t\t}\n     +\n     +\t\tstrbuf_reset(out);\n     +\t\tif (launch_editor(path, out, NULL)) {\n    -+\t\t\tfprintf(stderr, _(\"Please supply the message using either -m or -F option.\\n\"));\n    ++\t\t\tfprintf(stderr, _(\"Please supply the message using the -m option.\\n\"));\n     +\t\t\treturn -1;\n     +\t\t}\n     +\t\tstrbuf_stripspace(out, comment_line_str);\n    -+\n     +\t} else {\n     +\t\tstrbuf_addstr(out, provided_message);\n     +\t}\n    @@ builtin/history.c: static int cmd_history_reorder(int argc,\n     +\t\tOPT_STRING('m', \"message\", &commit_message, N_(\"message\"), N_(\"commit message\")),\n     +\t\tOPT_END(),\n     +\t};\n    ++\tstruct oidmap rewritten_commits = OIDMAP_INIT;\n     +\tstruct commit *original_commit, *head;\n     +\tstruct strvec commits = STRVEC_INIT;\n     +\tstruct commit_list *list = NULL;\n     +\tstruct object_id split_commits[2];\n    ++\tstruct replay_oid_mapping mapping[2] = { 0 };\n     +\tstruct pathspec pathspec = { 0 };\n     +\tint ret;\n     +\n    @@ builtin/history.c: static int cmd_history_reorder(int argc,\n     +\tif (ret < 0)\n     +\t\tgoto out;\n     +\n    ++\tmapping[0].entry.oid = split_commits[0];\n    ++\tmapping[0].rewritten_oid = original_commit->object.oid;\n    ++\toidmap_put(&rewritten_commits, &mapping[0]);\n    ++\tmapping[1].entry.oid = split_commits[1];\n    ++\tmapping[1].rewritten_oid = original_commit->object.oid;\n    ++\toidmap_put(&rewritten_commits, &mapping[1]);\n    ++\n     +\treplace_commits(&commits, &original_commit->object.oid,\n     +\t\t\tsplit_commits, ARRAY_SIZE(split_commits));\n     +\n    -+\tret = apply_commits(repo, &commits, head, original_commit, \"split\");\n    ++\tret = apply_commits(repo, &commits, head, original_commit,\n    ++\t\t\t    &rewritten_commits, \"split\");\n     +\tif (ret < 0)\n     +\t\tgoto out;\n     +\n     +\tret = 0;\n     +\n     +out:\n    ++\toidmap_clear(&rewritten_commits, 0);\n     +\tclear_pathspec(&pathspec);\n     +\tstrvec_clear(&commits);\n     +\tfree_commit_list(list);\n    @@ t/t3453-history-split.sh (new)\n     +\t)\n     +'\n     +\n    ++test_expect_success 'skips change summary with commit.verbose=false' '\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\tcp \"$1\" . &&\n    ++\t\techo \"some commit message\" >>\"$1\"\n    ++\t\tEOF\n    ++\t\ttest_set_editor \"$(pwd)\"/fake-editor.sh &&\n    ++\n    ++\t\tgit -c commit.verbose=false history split HEAD <<-EOF &&\n    ++\t\ty\n    ++\t\tn\n    ++\t\tEOF\n    ++\n    ++\t\tcat >expect <<-EOF &&\n    ++\n    ++\t\t# Please enter the commit message for the split-out changes. Lines starting\n    ++\t\t# with ${SQ}#${SQ} will be kept; you may remove them yourself if you want to.\n    ++\t\tEOF\n    ++\t\ttest_cmp expect COMMIT_EDITMSG &&\n    ++\n    ++\t\texpect_log <<-EOF\n    ++\t\tsplit-me\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/t3453-history-split.sh (new)\n     +\t)\n     +'\n     +\n    ++test_expect_success 'hooks are 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\twrite_script .git/hooks/prepare-commit-msg <<-EOF &&\n    ++\t\techo \"prepare-commit-msg: \\$@\" >>\"$(pwd)/hooks.log\"\n    ++\t\tEOF\n    ++\t\twrite_script .git/hooks/post-commit <<-EOF &&\n    ++\t\techo \"post-commit\" >>\"$(pwd)/hooks.log\"\n    ++\t\tEOF\n    ++\t\twrite_script .git/hooks/post-rewrite <<-EOF &&\n    ++\t\t{\n    ++\t\t\techo \"post-rewrite: \\$@\"\n    ++\t\t\tcat\n    ++\t\t} >>\"$(pwd)/hooks.log\"\n    ++\t\tEOF\n    ++\n    ++\t\tset_fake_editor &&\n    ++\t\tgit history split HEAD <<-EOF &&\n    ++\t\ty\n    ++\t\tn\n    ++\t\tEOF\n    ++\n    ++\t\texpect_log <<-EOF &&\n    ++\t\tsplit-me\n    ++\t\tsplit-out commit\n    ++\t\tEOF\n    ++\n    ++\t\tcat >expect <<-EOF &&\n    ++\t\tprepare-commit-msg: .git/COMMIT_EDITMSG message\n    ++\t\tpost-commit\n    ++\t\tprepare-commit-msg: .git/COMMIT_EDITMSG message\n    ++\t\tpost-commit\n    ++\t\tpost-rewrite: history\n    ++\t\t$old_head $(git rev-parse HEAD~)\n    ++\t\t$old_head $(git rev-parse HEAD)\n    ++\t\tEOF\n    ++\t\ttest_cmp expect 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 &&\n16:  142e1e627b ! 18:  07eca0b266 builtin/history: implement \"reword\" subcommand\n    @@ builtin/history.c: static int split_commit(struct repository *repo,\n     +\tstruct object_id parent_tree_oid, original_commit_tree_oid;\n     +\tstruct object_id rewritten_commit;\n     +\tconst char *original_message, *original_body, *ptr;\n    ++\tstruct oidmap rewritten_commits = OIDMAP_INIT;\n    ++\tstruct replay_oid_mapping mapping = { 0 };\n     +\tchar *original_author = NULL;\n     +\tsize_t len;\n     +\tint ret;\n    @@ builtin/history.c: static int split_commit(struct repository *repo,\n     +\n     +\toriginal_commit = lookup_commit_reference_by_name(argv[0]);\n     +\tif (!original_commit) {\n    -+\t\tret = error(_(\"commit to be split cannot be found: %s\"), argv[0]);\n    ++\t\tret = error(_(\"commit to be reworded cannot be found: %s\"), argv[0]);\n     +\t\tgoto out;\n     +\t}\n     +\n    @@ builtin/history.c: static int split_commit(struct repository *repo,\n     +\n     +\treplace_commits(&commits, &original_commit->object.oid, &rewritten_commit, 1);\n     +\n    -+\tret = apply_commits(repo, &commits, head, original_commit, \"reword\");\n    ++\tmapping.entry.oid = rewritten_commit;\n    ++\tmapping.rewritten_oid = original_commit->object.oid;\n    ++\toidmap_put(&rewritten_commits, &mapping);\n    ++\n    ++\tret = apply_commits(repo, &commits, head, original_commit,\n    ++\t\t\t    &rewritten_commits, \"reword\");\n     +\tif (ret < 0)\n     +\t\tgoto out;\n     +\n     +\tret = 0;\n     +\n     +out:\n    ++\toidmap_clear(&rewritten_commits, 0);\n     +\tstrbuf_release(&final_message);\n     +\tstrvec_clear(&commits);\n     +\tfree(original_author);\n    @@ t/t3454-history-reword.sh (new)\n     +\t)\n     +'\n     +\n    ++test_expect_success 'hooks are executed for rewritten commits' '\n    ++\ttest_when_finished \"rm -rf repo\" &&\n    ++\tgit init repo &&\n    ++\t(\n    ++\t\tcd repo &&\n    ++\t\ttest_commit first &&\n    ++\t\ttest_commit second &&\n    ++\t\ttest_commit third &&\n    ++\n    ++\t\twrite_script .git/hooks/prepare-commit-msg <<-EOF &&\n    ++\t\techo \"prepare-commit-msg: \\$@\" >>\"$(pwd)/hooks.log\"\n    ++\t\tEOF\n    ++\t\twrite_script .git/hooks/post-commit <<-EOF &&\n    ++\t\techo \"post-commit\" >>\"$(pwd)/hooks.log\"\n    ++\t\tEOF\n    ++\t\twrite_script .git/hooks/post-rewrite <<-EOF &&\n    ++\t\t{\n    ++\t\t\techo \"post-rewrite: \\$@\"\n    ++\t\t\tcat\n    ++\t\t} >>\"$(pwd)/hooks.log\"\n    ++\t\tEOF\n    ++\n    ++\t\tgit history reword -m \"second reworded\" HEAD~ &&\n    ++\n    ++\t\tcat >expect <<-EOF &&\n    ++\t\tthird\n    ++\t\tsecond reworded\n    ++\t\tfirst\n    ++\t\tEOF\n    ++\t\tgit log --format=%s >actual &&\n    ++\t\ttest_cmp expect actual &&\n    ++\n    ++\t\tcat >expect <<-EOF &&\n    ++\t\tprepare-commit-msg: .git/COMMIT_EDITMSG message\n    ++\t\tpost-commit\n    ++\t\tprepare-commit-msg: .git/COMMIT_EDITMSG message\n    ++\t\tpost-commit\n    ++\t\tpost-rewrite: history\n    ++\t\t$(git rev-parse second) $(git rev-parse HEAD~)\n    ++\t\t$(git rev-parse third) $(git rev-parse HEAD)\n    ++\t\tEOF\n    ++\t\ttest_cmp expect hooks.log\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/t3454-history-reword.sh (new)\n     +\t)\n     +'\n     +\n    -+\n     +test_done\n\n---\nbase-commit: c44beea485f0f2feaf460e2ac87fdd5608d63cf0\nchange-id: 20250819-b4-pks-history-builtin-83398f9a05f0\n\n"},{"id":"525525","messageId":"20250904-b4-pks-history-builtin-v3-1-509053514755@pks.im","threadId":"63986","inReplyTo":"20250904-b4-pks-history-builtin-v3-0-509053514755@pks.im","subject":"[PATCH RFC v3 01/18] sequencer: optionally skip printing commit summary","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-09-04T14:27:31Z","receivedAt":"2025-09-04T14:27:42Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"When picking commits by using for example git-cherry-pick(1) we end up\nprinting a commit summary that gives the reader information around what\nexactly we have been picking:\n\n```\n$ git cherry-pick main\n[other 76c8456] bar\n Date: Tue Aug 19 08:07:26 2025 +0200\n 1 file changed, 1 insertion(+)\n create mode 100644 bar\n```\n\nWhile useful for some commands, we're about to introduce a new command\nwhere this output will be less so. But right now there is no way to\ndisable printing this commit summary.\n\nIntroduce a new `skip_commit_summary` replay option that does so.\nPersist the option into the sequencer configuration so that it persists\nacross different processes, e.g. when we need to stop due to a merge\nconflict.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n sequencer.c | 12 +++++++++---\n sequencer.h |  1 +\n 2 files changed, 10 insertions(+), 3 deletions(-)\n\ndiff --git a/sequencer.c b/sequencer.c\nindex aaf2e4df64..7066cdc939 100644\n--- a/sequencer.c\n+++ b/sequencer.c\n@@ -1742,7 +1742,7 @@ static int do_commit(struct repository *r,\n \t\t\trefs_delete_ref(get_main_ref_store(r), \"\",\n \t\t\t\t\t\"CHERRY_PICK_HEAD\", NULL, REF_NO_DEREF);\n \t\t\tunlink(git_path_merge_msg(r));\n-\t\t\tif (!is_rebase_i(opts))\n+\t\t\tif (!is_rebase_i(opts) && !opts->skip_commit_summary)\n \t\t\t\tprint_commit_summary(r, NULL, &oid,\n \t\t\t\t\t\tSUMMARY_SHOW_AUTHOR_DATE);\n \t\t\treturn res;\n@@ -3139,8 +3139,12 @@ static int populate_opts_cb(const char *key, const char *value,\n \telse if (!strcmp(key, \"options.default-msg-cleanup\")) {\n \t\topts->explicit_cleanup = 1;\n \t\topts->default_msg_cleanup = get_cleanup_mode(value, 1);\n-\t} else\n+\t} else if (!strcmp(key, \"options.skip-commit-summary\")) {\n+\t\topts->skip_commit_summary =\n+\t\t\tgit_config_bool_or_int(key, value, ctx->kvi, &error_flag);\n+\t} else {\n \t\treturn error(_(\"invalid key: %s\"), key);\n+\t}\n \n \tif (!error_flag)\n \t\treturn error(_(\"invalid value for '%s': '%s'\"), key, value);\n@@ -3698,11 +3702,13 @@ static int save_opts(struct replay_opts *opts)\n \t\t\t\t\"options.allow-rerere-auto\", NULL,\n \t\t\t\topts->allow_rerere_auto == RERERE_AUTOUPDATE ?\n \t\t\t\t\"true\" : \"false\");\n-\n \tif (opts->explicit_cleanup)\n \t\tres |= repo_config_set_in_file_gently(the_repository, opts_file,\n \t\t\t\t\"options.default-msg-cleanup\", NULL,\n \t\t\t\tdescribe_cleanup_mode(opts->default_msg_cleanup));\n+\tif (opts->skip_commit_summary)\n+\t\tres |= repo_config_set_in_file_gently(the_repository, opts_file,\n+\t\t\t\t\t\"options.skip-commit-summary\", NULL, \"true\");\n \treturn res;\n }\n \ndiff --git a/sequencer.h b/sequencer.h\nindex 304ba4b4d3..1767fd737e 100644\n--- a/sequencer.h\n+++ b/sequencer.h\n@@ -52,6 +52,7 @@ struct replay_opts {\n \tint keep_redundant_commits;\n \tint verbose;\n \tint quiet;\n+\tint skip_commit_summary;\n \tint reschedule_failed_exec;\n \tint committer_date_is_author_date;\n \tint ignore_date;\n\n-- \n2.51.0.417.g1ba7204a04.dirty\n\n"},{"id":"525526","messageId":"20250904-b4-pks-history-builtin-v3-2-509053514755@pks.im","threadId":"63986","inReplyTo":"20250904-b4-pks-history-builtin-v3-0-509053514755@pks.im","subject":"[PATCH RFC v3 02/18] sequencer: add option to rewind HEAD after picking commits","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-09-04T14:27:32Z","receivedAt":"2025-09-04T14:27:45Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"While the sequencer infrastructure knows to rewind \"HEAD\" to whatever it\nwas pointing to before a rebase, it doesn't do the same for non-rebase\noperations like cherry-picks. This is because the expectation is that\nthe user directly picks commits on top of whatever \"HEAD\" points to, and\nwe advance the reference pointed to by \"HEAD\" instead of updating it\ndirectly.\n\nWe're about to introduce a new command though that needs to detach\n\"HEAD\" while being more similar to git-cherry-pick(1) rathen than to\ngit-rebase(1). As such, we'll want to restore \"HEAD\" to point to the\nbranch that we started on while not using the more heavy-weight rebase\nmachinery.\n\nIntroduce a new option `restore_head_target` to do so. Persist the\noption into the sequencer configuration so that it persists across\ndifferent processes, e.g. when we need to stop due to a merge conflict.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n sequencer.c | 27 ++++++++++++++++++++++++++-\n sequencer.h |  3 +++\n 2 files changed, 29 insertions(+), 1 deletion(-)\n\ndiff --git a/sequencer.c b/sequencer.c\nindex 7066cdc939..bff181df76 100644\n--- a/sequencer.c\n+++ b/sequencer.c\n@@ -413,6 +413,7 @@ void replay_opts_release(struct replay_opts *opts)\n \tstruct replay_ctx *ctx = opts->ctx;\n \n \tfree(opts->gpg_sign);\n+\tfree(opts->restore_head_target);\n \tfree(opts->reflog_action);\n \tfree(opts->default_strategy);\n \tfree(opts->strategy);\n@@ -3142,6 +3143,8 @@ static int populate_opts_cb(const char *key, const char *value,\n \t} else if (!strcmp(key, \"options.skip-commit-summary\")) {\n \t\topts->skip_commit_summary =\n \t\t\tgit_config_bool_or_int(key, value, ctx->kvi, &error_flag);\n+\t} else if (!strcmp(key, \"options.restore-head-target\")) {\n+\t\tgit_config_string_dup(&opts->restore_head_target, key, value);\n \t} else {\n \t\treturn error(_(\"invalid key: %s\"), key);\n \t}\n@@ -3709,6 +3712,10 @@ static int save_opts(struct replay_opts *opts)\n \tif (opts->skip_commit_summary)\n \t\tres |= repo_config_set_in_file_gently(the_repository, opts_file,\n \t\t\t\t\t\"options.skip-commit-summary\", NULL, \"true\");\n+\tif (opts->restore_head_target)\n+\t\tres |= repo_config_set_in_file_gently(the_repository, opts_file,\n+\t\t\t\t\"options.restore-head-target\", NULL, opts->restore_head_target);\n+\n \treturn res;\n }\n \n@@ -5177,6 +5184,23 @@ static int pick_commits(struct repository *r,\n \t\t\treturn -1;\n \t}\n \n+\tif (opts->restore_head_target) {\n+\t\tstruct reset_head_opts reset_opts = { 0 };\n+\t\tconst char *msg;\n+\n+\t\tmsg = reflog_message(opts, \"finish\", \"returning to %s\", opts->restore_head_target);\n+\n+\t\treset_opts.branch = opts->restore_head_target;\n+\t\treset_opts.flags = RESET_HEAD_REFS_ONLY;\n+\t\treset_opts.branch_msg = msg;\n+\t\treset_opts.head_msg = msg;\n+\n+\t\tif (reset_head(r, &reset_opts)) {\n+\t\t\terror(_(\"could not switch HEAD back to %s\"), opts->restore_head_target);\n+\t\t\treturn -1;\n+\t\t}\n+\t}\n+\n \t/*\n \t * Sequence of picks finished successfully; cleanup by\n \t * removing the .git/sequencer directory\n@@ -5533,7 +5557,8 @@ int sequencer_pick_revisions(struct repository *r,\n \tif (opts->revs->cmdline.nr == 1 &&\n \t    opts->revs->cmdline.rev->whence == REV_CMD_REV &&\n \t    opts->revs->no_walk &&\n-\t    !opts->revs->cmdline.rev->flags) {\n+\t    !opts->revs->cmdline.rev->flags &&\n+\t    !opts->restore_head_target) {\n \t\tstruct commit *cmit;\n \n \t\tif (prepare_revision_walk(opts->revs)) {\ndiff --git a/sequencer.h b/sequencer.h\nindex 1767fd737e..a905f6afc7 100644\n--- a/sequencer.h\n+++ b/sequencer.h\n@@ -72,6 +72,9 @@ struct replay_opts {\n \t/* Reflog */\n \tchar *reflog_action;\n \n+\t/* Reference to which HEAD shall be reset to after the operation. */\n+\tchar *restore_head_target;\n+\n \t/* placeholder commit for -i --root */\n \tstruct object_id squash_onto;\n \tint have_squash_onto;\n\n-- \n2.51.0.417.g1ba7204a04.dirty\n\n"},{"id":"525527","messageId":"20250904-b4-pks-history-builtin-v3-3-509053514755@pks.im","threadId":"63986","inReplyTo":"20250904-b4-pks-history-builtin-v3-0-509053514755@pks.im","subject":"[PATCH RFC v3 03/18] sequencer: introduce new history editing mode","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-09-04T14:27:33Z","receivedAt":"2025-09-04T14:27:47Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Introduce a new history editing mode into our sequencer machinery. This\nmode is basically the same as `REBASE_CHERRY`, but will be used by the\nnew git-history(1) command that is to be introduced in a subsequent\ncommit.\n\nNote that the advice already points towards the git-history(1) command.\nThis advice is bogus right now, but we'll introduce the relevant infra\nin a subsequent commit.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n sequencer.c | 116 +++++++++++++++++++++++++++++++++++++++++++++++-------------\n sequencer.h |   3 +-\n 2 files changed, 93 insertions(+), 26 deletions(-)\n\ndiff --git a/sequencer.c b/sequencer.c\nindex bff181df76..9a66e7d128 100644\n--- a/sequencer.c\n+++ b/sequencer.c\n@@ -465,6 +465,8 @@ static const char *action_name(const struct replay_opts *opts)\n \t\treturn N_(\"cherry-pick\");\n \tcase REPLAY_INTERACTIVE_REBASE:\n \t\treturn N_(\"rebase\");\n+\tcase REPLAY_HISTORY_EDIT:\n+\t\treturn N_(\"history edit\");\n \t}\n \tdie(_(\"unknown action: %d\"), opts->action);\n }\n@@ -557,6 +559,13 @@ static void print_advice(struct repository *r, int show_hint,\n \t\t\t\t\t    \"You can instead skip this commit with \\\"git revert --skip\\\".\\n\"\n \t\t\t\t\t    \"To abort and get back to the state before \\\"git revert\\\",\\n\"\n \t\t\t\t\t    \"run \\\"git revert --abort\\\".\"));\n+\t\telse if (opts->action == REPLAY_HISTORY_EDIT)\n+\t\t\tadvise_if_enabled(ADVICE_MERGE_CONFLICT,\n+\t\t\t\t\t  _(\"After resolving the conflicts, mark them with\\n\"\n+\t\t\t\t\t    \"\\\"git add/rm <pathspec>\\\", then run\\n\"\n+\t\t\t\t\t    \"\\\"git history continue\\\".\\n\"\n+\t\t\t\t\t    \"To abort and get back to the state before \\\"git history\\\",\\n\"\n+\t\t\t\t\t    \"run \\\"git history abort\\\".\"));\n \t\telse\n \t\t\tBUG(\"unexpected pick action in print_advice()\");\n \t}\n@@ -1742,6 +1751,8 @@ static int do_commit(struct repository *r,\n \t\tif (!res) {\n \t\t\trefs_delete_ref(get_main_ref_store(r), \"\",\n \t\t\t\t\t\"CHERRY_PICK_HEAD\", NULL, REF_NO_DEREF);\n+\t\t\trefs_delete_ref(get_main_ref_store(r), \"\",\n+\t\t\t\t\t\"HISTORY_EDIT_HEAD\", NULL, REF_NO_DEREF);\n \t\t\tunlink(git_path_merge_msg(r));\n \t\t\tif (!is_rebase_i(opts) && !opts->skip_commit_summary)\n \t\t\t\tprint_commit_summary(r, NULL, &oid,\n@@ -2491,16 +2502,24 @@ static int do_pick_commit(struct repository *r,\n \t * However, if the merge did not even start, then we don't want to\n \t * write it at all.\n \t */\n-\tif ((command == TODO_PICK || command == TODO_REWORD ||\n-\t     command == TODO_EDIT) && !opts->no_commit &&\n-\t    (res == 0 || res == 1) &&\n-\t    refs_update_ref(get_main_ref_store(the_repository), NULL, \"CHERRY_PICK_HEAD\", &commit->object.oid, NULL,\n-\t\t\t    REF_NO_DEREF, UPDATE_REFS_MSG_ON_ERR))\n-\t\tres = -1;\n-\tif (command == TODO_REVERT && ((opts->no_commit && res == 0) || res == 1) &&\n-\t    refs_update_ref(get_main_ref_store(the_repository), NULL, \"REVERT_HEAD\", &commit->object.oid, NULL,\n-\t\t\t    REF_NO_DEREF, UPDATE_REFS_MSG_ON_ERR))\n-\t\tres = -1;\n+\tif (opts->action == REPLAY_HISTORY_EDIT && command == TODO_PICK &&\n+\t    !opts->no_commit && (res == 0 || res == 1)) {\n+\t\tif (refs_update_ref(get_main_ref_store(the_repository), NULL,\n+\t\t\t\t    \"HISTORY_EDIT_HEAD\", &commit->object.oid, NULL,\n+\t\t\t\t    REF_NO_DEREF, UPDATE_REFS_MSG_ON_ERR))\n+\t\t\tres = -1;\n+\t} else if ((command == TODO_PICK || command == TODO_REWORD ||\n+\t\t   command == TODO_EDIT) && !opts->no_commit && (res == 0 || res == 1)) {\n+\t\tif (refs_update_ref(get_main_ref_store(the_repository), NULL,\n+\t\t\t\t    \"CHERRY_PICK_HEAD\", &commit->object.oid, NULL,\n+\t\t\t\t    REF_NO_DEREF, UPDATE_REFS_MSG_ON_ERR))\n+\t\t\tres = -1;\n+\t} else if (command == TODO_REVERT && ((opts->no_commit && res == 0) || res == 1)) {\n+\t\tif (refs_update_ref(get_main_ref_store(the_repository), NULL,\n+\t\t\t\t    \"REVERT_HEAD\", &commit->object.oid, NULL,\n+\t\t\t\t    REF_NO_DEREF, UPDATE_REFS_MSG_ON_ERR))\n+\t\t\tres = -1;\n+\t}\n \n \tif (res) {\n \t\terror(command == TODO_REVERT\n@@ -2526,6 +2545,8 @@ static int do_pick_commit(struct repository *r,\n \t\tunlink(git_path_merge_msg(r));\n \t\trefs_delete_ref(get_main_ref_store(r), \"\", \"AUTO_MERGE\",\n \t\t\t\tNULL, REF_NO_DEREF);\n+\t\trefs_delete_ref(get_main_ref_store(r), \"\", \"HISTORY_EDIT_HEAD\",\n+\t\t\t\tNULL, REF_NO_DEREF);\n \t\tfprintf(stderr,\n \t\t\t_(\"dropping %s %s -- patch contents already upstream\\n\"),\n \t\t\toid_to_hex(&commit->object.oid), msg.subject);\n@@ -2843,12 +2864,17 @@ static int parse_insn_line(struct repository *r, struct replay_opts *opts,\n \treturn 0;\n }\n \n-int sequencer_get_last_command(struct repository *r UNUSED, enum replay_action *action)\n+int sequencer_get_last_command(struct repository *r, enum replay_action *action)\n {\n \tconst char *todo_file, *bol;\n \tstruct strbuf buf = STRBUF_INIT;\n \tint ret = 0;\n \n+\tif (refs_ref_exists(get_main_ref_store(r), \"HISTORY_EDIT_HEAD\")) {\n+\t\t*action = REPLAY_HISTORY_EDIT;\n+\t\treturn 0;\n+\t}\n+\n \ttodo_file = git_path_todo_file();\n \tif (strbuf_read_file(&buf, todo_file, 0) < 0) {\n \t\tif (errno == ENOENT || errno == ENOTDIR)\n@@ -2995,6 +3021,15 @@ void sequencer_post_commit_cleanup(struct repository *r, int verbose)\n \t\tneed_cleanup = 1;\n \t}\n \n+\tif (refs_ref_exists(get_main_ref_store(r), \"HISTORY_EDIT_HEAD\")) {\n+\t\tif (!refs_delete_ref(get_main_ref_store(r), \"\",\n+\t\t\t\t     \"HISTORY_EDIT_HEAD\", NULL, REF_NO_DEREF) &&\n+\t\t    verbose)\n+\t\t\twarning(_(\"cancelling a history edit in progress\"));\n+\t\topts.action = REPLAY_HISTORY_EDIT;\n+\t\tneed_cleanup = 1;\n+\t}\n+\n \tif (refs_ref_exists(get_main_ref_store(r), \"REVERT_HEAD\")) {\n \t\tif (!refs_delete_ref(get_main_ref_store(r), \"\", \"REVERT_HEAD\",\n \t\t\t\t     NULL, REF_NO_DEREF) &&\n@@ -3028,6 +3063,19 @@ static void todo_list_write_total_nr(struct todo_list *todo_list)\n \t}\n }\n \n+static enum todo_command action_to_command(enum replay_action action)\n+{\n+\tswitch (action) {\n+\tcase REPLAY_PICK:\n+\tcase REPLAY_HISTORY_EDIT:\n+\t\treturn TODO_PICK;\n+\tcase REPLAY_REVERT:\n+\t\treturn TODO_REVERT;\n+\tdefault:\n+\t\tBUG(\"unsupported action %d\", action);\n+\t}\n+}\n+\n static int read_populate_todo(struct repository *r,\n \t\t\t      struct todo_list *todo_list,\n \t\t\t      struct replay_opts *opts)\n@@ -3052,17 +3100,19 @@ static int read_populate_todo(struct repository *r,\n \t\treturn error(_(\"no commits parsed.\"));\n \n \tif (!is_rebase_i(opts)) {\n-\t\tenum todo_command valid =\n-\t\t\topts->action == REPLAY_PICK ? TODO_PICK : TODO_REVERT;\n+\t\tenum todo_command valid = action_to_command(opts->action);\n \t\tint i;\n \n-\t\tfor (i = 0; i < todo_list->nr; i++)\n+\t\tfor (i = 0; i < todo_list->nr; i++) {\n \t\t\tif (valid == todo_list->items[i].command)\n \t\t\t\tcontinue;\n \t\t\telse if (valid == TODO_PICK)\n-\t\t\t\treturn error(_(\"cannot cherry-pick during a revert.\"));\n+\t\t\t\treturn error(_(\"cannot cherry-pick during a %s.\"),\n+\t\t\t\t\t     action_name(opts));\n \t\t\telse\n-\t\t\t\treturn error(_(\"cannot revert during a cherry-pick.\"));\n+\t\t\t\treturn error(_(\"cannot revert during a %s.\"),\n+\t\t\t\t\t     action_name(opts));\n+\t\t}\n \t}\n \n \tif (is_rebase_i(opts)) {\n@@ -3353,15 +3403,16 @@ int write_basic_state(struct replay_opts *opts, const char *head_name,\n static int walk_revs_populate_todo(struct todo_list *todo_list,\n \t\t\t\tstruct replay_opts *opts)\n {\n-\tenum todo_command command = opts->action == REPLAY_PICK ?\n-\t\tTODO_PICK : TODO_REVERT;\n-\tconst char *command_string = todo_command_info[command].str;\n+\tenum todo_command command;\n+\tconst char *command_string;\n \tconst char *encoding;\n \tstruct commit *commit;\n \n \tif (prepare_revs(opts))\n \t\treturn -1;\n \n+\tcommand = action_to_command(opts->action);\n+\tcommand_string = todo_command_info[command].str;\n \tencoding = get_log_output_encoding();\n \n \twhile ((commit = get_revision(opts->revs))) {\n@@ -3412,6 +3463,11 @@ static int create_seq_dir(struct repository *r)\n \t\t\tin_progress_advice =\n \t\t\t_(\"try \\\"git cherry-pick (--continue | %s--abort | --quit)\\\"\");\n \t\t\tbreak;\n+\t\tcase REPLAY_HISTORY_EDIT:\n+\t\t\tin_progress_error = _(\"history edit is already in progress\");\n+\t\t\tin_progress_advice =\n+\t\t\t_(\"try \\\"git history (continue | abort | quit)\\\"\");\n+\t\t\tbreak;\n \t\tdefault:\n \t\t\tBUG(\"unexpected action in create_seq_dir\");\n \t\t}\n@@ -3472,13 +3528,14 @@ static int reset_merge(const struct object_id *oid)\n \treturn run_command(&cmd);\n }\n \n-static int rollback_single_pick(struct repository *r)\n+static int rollback_single_pick(struct repository *r, struct replay_opts *opts)\n {\n \tstruct object_id head_oid;\n \n \tif (!refs_ref_exists(get_main_ref_store(r), \"CHERRY_PICK_HEAD\") &&\n+\t    !refs_ref_exists(get_main_ref_store(r), \"HISTORY_EDIT_HEAD\") &&\n \t    !refs_ref_exists(get_main_ref_store(r), \"REVERT_HEAD\"))\n-\t\treturn error(_(\"no cherry-pick or revert in progress\"));\n+\t\treturn error(_(\"no %s in progress\"), action_name(opts));\n \tif (refs_read_ref_full(get_main_ref_store(the_repository), \"HEAD\", 0, &head_oid, NULL))\n \t\treturn error(_(\"cannot resolve HEAD\"));\n \tif (is_null_oid(&head_oid))\n@@ -3509,7 +3566,7 @@ int sequencer_rollback(struct repository *r, struct replay_opts *opts)\n \t\t * If CHERRY_PICK_HEAD or REVERT_HEAD indicates\n \t\t * a single-cherry-pick in progress, abort that.\n \t\t */\n-\t\treturn rollback_single_pick(r);\n+\t\treturn rollback_single_pick(r, opts);\n \t}\n \tif (!f)\n \t\treturn error_errno(_(\"cannot open '%s'\"), git_path_head_file());\n@@ -5213,8 +5270,9 @@ static int continue_single_pick(struct repository *r, struct replay_opts *opts)\n \tstruct child_process cmd = CHILD_PROCESS_INIT;\n \n \tif (!refs_ref_exists(get_main_ref_store(r), \"CHERRY_PICK_HEAD\") &&\n+\t    !refs_ref_exists(get_main_ref_store(r), \"HISTORY_EDIT_HEAD\") &&\n \t    !refs_ref_exists(get_main_ref_store(r), \"REVERT_HEAD\"))\n-\t\treturn error(_(\"no cherry-pick or revert in progress\"));\n+\t\treturn error(_(\"no %s in progress\"), action_name(opts));\n \n \tcmd.git_cmd = 1;\n \tstrvec_push(&cmd.args, \"commit\");\n@@ -5393,6 +5451,14 @@ static int commit_staged_changes(struct repository *r,\n \t\t\tgoto out;\n \t\t}\n \n+\t\tif (refs_ref_exists(get_main_ref_store(r),\n+\t\t\t\t    \"HISTORY_EDIT_HEAD\") &&\n+\t\t    refs_delete_ref(get_main_ref_store(r), \"\",\n+\t\t\t\t    \"HISTORY_EDIT_HEAD\", NULL, REF_NO_DEREF)) {\n+\t\t\tret = error(_(\"could not remove HISTORY_EDIT_HEAD\"));\n+\t\t\tgoto out;\n+\t\t}\n+\n \t\tif (unlink(git_path_merge_msg(r)) && errno != ENOENT) {\n \t\t\tret = error_errno(_(\"could not remove '%s'\"),\n \t\t\t\t\t  git_path_merge_msg(r));\n@@ -5471,6 +5537,7 @@ int sequencer_continue(struct repository *r, struct replay_opts *opts)\n \t\t/* Verify that the conflict has been resolved */\n \t\tif (refs_ref_exists(get_main_ref_store(r),\n \t\t\t\t    \"CHERRY_PICK_HEAD\") ||\n+\t\t    refs_ref_exists(get_main_ref_store(r), \"HISTORY_EDIT_HEAD\") ||\n \t\t    refs_ref_exists(get_main_ref_store(r), \"REVERT_HEAD\")) {\n \t\t\tres = continue_single_pick(r, opts);\n \t\t\tif (res)\n@@ -5505,8 +5572,7 @@ static int single_pick(struct repository *r,\n \tint check_todo;\n \tstruct todo_item item;\n \n-\titem.command = opts->action == REPLAY_PICK ?\n-\t\t\tTODO_PICK : TODO_REVERT;\n+\titem.command = action_to_command(opts->action);\n \titem.commit = cmit;\n \n \treturn do_pick_commit(r, &item, opts, 0, &check_todo);\ndiff --git a/sequencer.h b/sequencer.h\nindex a905f6afc7..082fbe3e35 100644\n--- a/sequencer.h\n+++ b/sequencer.h\n@@ -21,7 +21,8 @@ extern const char *rebase_resolvemsg;\n enum replay_action {\n \tREPLAY_REVERT,\n \tREPLAY_PICK,\n-\tREPLAY_INTERACTIVE_REBASE\n+\tREPLAY_INTERACTIVE_REBASE,\n+\tREPLAY_HISTORY_EDIT,\n };\n \n enum commit_msg_cleanup_mode {\n\n-- \n2.51.0.417.g1ba7204a04.dirty\n\n"},{"id":"525528","messageId":"20250904-b4-pks-history-builtin-v3-4-509053514755@pks.im","threadId":"63986","inReplyTo":"20250904-b4-pks-history-builtin-v3-0-509053514755@pks.im","subject":"[PATCH RFC v3 04/18] sequencer: stop using `the_repository` in `sequencer_remove_state()`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-09-04T14:27:34Z","receivedAt":"2025-09-04T14:27:51Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Refactor `sequencer_remove_state()` to stop using `the_repository` in\nfavor of a passed-in repository.\n\nA lot of the other code in our sequencer infrastructure still uses\n`the_repository`, but this bigger refactoring is left for another day.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/rebase.c |  4 ++--\n builtin/revert.c |  2 +-\n sequencer.c      | 18 +++++++++---------\n sequencer.h      |  2 +-\n 4 files changed, 13 insertions(+), 13 deletions(-)\n\ndiff --git a/builtin/rebase.c b/builtin/rebase.c\nindex 3c85768d29..66824ae136 100644\n--- a/builtin/rebase.c\n+++ b/builtin/rebase.c\n@@ -568,7 +568,7 @@ static int finish_rebase(struct rebase_options *opts)\n \t\tstruct replay_opts replay = REPLAY_OPTS_INIT;\n \n \t\treplay.action = REPLAY_INTERACTIVE_REBASE;\n-\t\tret = sequencer_remove_state(&replay);\n+\t\tret = sequencer_remove_state(the_repository, &replay);\n \t\treplay_opts_release(&replay);\n \t} else {\n \t\tstrbuf_addstr(&dir, opts->state_dir);\n@@ -1405,7 +1405,7 @@ int cmd_rebase(int argc,\n \t\t\tstruct replay_opts replay = REPLAY_OPTS_INIT;\n \n \t\t\treplay.action = REPLAY_INTERACTIVE_REBASE;\n-\t\t\tret = sequencer_remove_state(&replay);\n+\t\t\tret = sequencer_remove_state(the_repository, &replay);\n \t\t\treplay_opts_release(&replay);\n \t\t} else {\n \t\t\tstrbuf_reset(&buf);\ndiff --git a/builtin/revert.c b/builtin/revert.c\nindex c3f92b585d..6456cf2171 100644\n--- a/builtin/revert.c\n+++ b/builtin/revert.c\n@@ -263,7 +263,7 @@ static int run_sequencer(int argc, const char **argv, const char *prefix,\n \tfree(options);\n \n \tif (cmd == 'q') {\n-\t\tint ret = sequencer_remove_state(opts);\n+\t\tint ret = sequencer_remove_state(the_repository, opts);\n \t\tif (!ret)\n \t\t\tremove_branch_state(the_repository, 0);\n \t\treturn ret;\ndiff --git a/sequencer.c b/sequencer.c\nindex 9a66e7d128..36e4db8526 100644\n--- a/sequencer.c\n+++ b/sequencer.c\n@@ -425,7 +425,7 @@ void replay_opts_release(struct replay_opts *opts)\n \tfree(opts->ctx);\n }\n \n-int sequencer_remove_state(struct replay_opts *opts)\n+int sequencer_remove_state(struct repository *repo, struct replay_opts *opts)\n {\n \tstruct strbuf buf = STRBUF_INIT;\n \tint ret = 0;\n@@ -437,7 +437,7 @@ int sequencer_remove_state(struct replay_opts *opts)\n \t\t\tchar *eol = strchr(p, '\\n');\n \t\t\tif (eol)\n \t\t\t\t*eol = '\\0';\n-\t\t\tif (refs_delete_ref(get_main_ref_store(the_repository), \"(rebase) cleanup\", p, NULL, 0) < 0) {\n+\t\t\tif (refs_delete_ref(get_main_ref_store(repo), \"(rebase) cleanup\", p, NULL, 0) < 0) {\n \t\t\t\twarning(_(\"could not delete '%s'\"), p);\n \t\t\t\tret = -1;\n \t\t\t}\n@@ -3048,7 +3048,7 @@ void sequencer_post_commit_cleanup(struct repository *r, int verbose)\n \tif (!have_finished_the_last_pick())\n \t\tgoto out;\n \n-\tsequencer_remove_state(&opts);\n+\tsequencer_remove_state(the_repository, &opts);\n out:\n \treplay_opts_release(&opts);\n }\n@@ -3595,7 +3595,7 @@ int sequencer_rollback(struct repository *r, struct replay_opts *opts)\n \tif (reset_merge(&oid))\n \t\tgoto fail;\n \tstrbuf_release(&buf);\n-\treturn sequencer_remove_state(opts);\n+\treturn sequencer_remove_state(the_repository, opts);\n fail:\n \tstrbuf_release(&buf);\n \treturn -1;\n@@ -4897,7 +4897,7 @@ static int checkout_onto(struct repository *r, struct replay_opts *opts,\n \t};\n \tif (reset_head(r, &ropts)) {\n \t\tapply_autostash(rebase_path_autostash());\n-\t\tsequencer_remove_state(opts);\n+\t\tsequencer_remove_state(the_repository, opts);\n \t\treturn error(_(\"could not detach HEAD\"));\n \t}\n \n@@ -5262,7 +5262,7 @@ static int pick_commits(struct repository *r,\n \t * Sequence of picks finished successfully; cleanup by\n \t * removing the .git/sequencer directory\n \t */\n-\treturn sequencer_remove_state(opts);\n+\treturn sequencer_remove_state(the_repository, opts);\n }\n \n static int continue_single_pick(struct repository *r, struct replay_opts *opts)\n@@ -6593,7 +6593,7 @@ int complete_action(struct repository *r, struct replay_opts *opts, unsigned fla\n \n \tif (count_commands(todo_list) == 0) {\n \t\tapply_autostash(rebase_path_autostash());\n-\t\tsequencer_remove_state(opts);\n+\t\tsequencer_remove_state(the_repository, opts);\n \n \t\treturn error(_(\"nothing to do\"));\n \t}\n@@ -6604,12 +6604,12 @@ int complete_action(struct repository *r, struct replay_opts *opts, unsigned fla\n \t\treturn -1;\n \telse if (res == -2) {\n \t\tapply_autostash(rebase_path_autostash());\n-\t\tsequencer_remove_state(opts);\n+\t\tsequencer_remove_state(the_repository, opts);\n \n \t\treturn -1;\n \t} else if (res == -3) {\n \t\tapply_autostash(rebase_path_autostash());\n-\t\tsequencer_remove_state(opts);\n+\t\tsequencer_remove_state(the_repository, opts);\n \t\ttodo_list_release(&new_todo);\n \n \t\treturn error(_(\"nothing to do\"));\ndiff --git a/sequencer.h b/sequencer.h\nindex 082fbe3e35..0e0e7301b8 100644\n--- a/sequencer.h\n+++ b/sequencer.h\n@@ -170,7 +170,7 @@ int sequencer_continue(struct repository *repo, struct replay_opts *opts);\n int sequencer_rollback(struct repository *repo, struct replay_opts *opts);\n int sequencer_skip(struct repository *repo, struct replay_opts *opts);\n void replay_opts_release(struct replay_opts *opts);\n-int sequencer_remove_state(struct replay_opts *opts);\n+int sequencer_remove_state(struct repository *repo, struct replay_opts *opts);\n \n #define TODO_LIST_KEEP_EMPTY (1U << 0)\n #define TODO_LIST_SHORTEN_IDS (1U << 1)\n\n-- \n2.51.0.417.g1ba7204a04.dirty\n\n"},{"id":"525529","messageId":"20250904-b4-pks-history-builtin-v3-5-509053514755@pks.im","threadId":"63986","inReplyTo":"20250904-b4-pks-history-builtin-v3-0-509053514755@pks.im","subject":"[PATCH RFC v3 05/18] sequencer: wire up \"rewritten-hook\" for REPLAY_HISTORY_EDIT","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-09-04T14:27:35Z","receivedAt":"2025-09-04T14:27:54Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"While the sequencer already knows to drive REPLAY_HISTORY_EDIT, we\ncurrently skip the execution of the \"rewritten-hook\" as it is only\nspecific to interactive rebases. We do want to execute this hook though\nfor commits we're rewriting in the upcoming git-history(1) command.\n\nWire up the infrastructure so that we also execute this hook with\nREPLAY_HISTORY_EDIT.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n sequencer.c | 79 +++++++++++++++++++++++++++++++++++++++++++------------------\n 1 file changed, 56 insertions(+), 23 deletions(-)\n\ndiff --git a/sequencer.c b/sequencer.c\nindex 36e4db8526..61447e5ccf 100644\n--- a/sequencer.c\n+++ b/sequencer.c\n@@ -70,6 +70,8 @@ static GIT_PATH_FUNC(git_path_seq_dir, \"sequencer\")\n static GIT_PATH_FUNC(git_path_todo_file, \"sequencer/todo\")\n static GIT_PATH_FUNC(git_path_opts_file, \"sequencer/opts\")\n static GIT_PATH_FUNC(git_path_head_file, \"sequencer/head\")\n+static GIT_PATH_FUNC(git_path_rewritten_list_file, \"sequencer/rewritten-list\")\n+static GIT_PATH_FUNC(git_path_rewritten_pending_file, \"sequencer/rewritten-pending\")\n static GIT_PATH_FUNC(git_path_abort_safety_file, \"sequencer/abort-safety\")\n \n static GIT_PATH_FUNC(rebase_path, \"rebase-merge\")\n@@ -2170,15 +2172,25 @@ static int update_squash_messages(struct repository *r,\n \treturn res;\n }\n \n-static void flush_rewritten_pending(void)\n+static void flush_rewritten_pending(struct replay_opts *opts)\n {\n \tstruct strbuf buf = STRBUF_INIT;\n \tstruct object_id newoid;\n+\tconst char *pending_path;\n+\tconst char *list_path;\n \tFILE *out;\n \n-\tif (strbuf_read_file(&buf, rebase_path_rewritten_pending(), (GIT_MAX_HEXSZ + 1) * 2) > 0 &&\n+\tif (opts->action == REPLAY_HISTORY_EDIT) {\n+\t\tpending_path = git_path_rewritten_pending_file();\n+\t\tlist_path = git_path_rewritten_list_file();\n+\t} else {\n+\t\tpending_path = rebase_path_rewritten_pending();\n+\t\tlist_path = rebase_path_rewritten_list();\n+\t}\n+\n+\tif (strbuf_read_file(&buf, pending_path, (GIT_MAX_HEXSZ + 1) * 2) > 0 &&\n \t    !repo_get_oid(the_repository, \"HEAD\", &newoid) &&\n-\t    (out = fopen_or_warn(rebase_path_rewritten_list(), \"a\"))) {\n+\t    (out = fopen_or_warn(list_path, \"a\"))) {\n \t\tchar *bol = buf.buf, *eol;\n \n \t\twhile (*bol) {\n@@ -2190,16 +2202,24 @@ static void flush_rewritten_pending(void)\n \t\t\tbol = eol + 1;\n \t\t}\n \t\tfclose(out);\n-\t\tunlink(rebase_path_rewritten_pending());\n+\t\tunlink(pending_path);\n \t}\n \tstrbuf_release(&buf);\n }\n \n static void record_in_rewritten(struct object_id *oid,\n-\t\tenum todo_command next_command)\n+\t\t\t\tenum todo_command next_command,\n+\t\t\t\tstruct replay_opts *opts)\n {\n-\tFILE *out = fopen_or_warn(rebase_path_rewritten_pending(), \"a\");\n+\tconst char *path;\n+\tFILE *out;\n \n+\tif (opts->action == REPLAY_HISTORY_EDIT)\n+\t\tpath = git_path_rewritten_pending_file();\n+\telse\n+\t\tpath = rebase_path_rewritten_pending();\n+\n+\tout = fopen_or_warn(path, \"a\");\n \tif (!out)\n \t\treturn;\n \n@@ -2207,7 +2227,7 @@ static void record_in_rewritten(struct object_id *oid,\n \tfclose(out);\n \n \tif (!is_fixup(next_command))\n-\t\tflush_rewritten_pending();\n+\t\tflush_rewritten_pending(opts);\n }\n \n static int should_edit(struct replay_opts *opts) {\n@@ -4984,9 +5004,9 @@ static int pick_one_commit(struct repository *r,\n \t\treturn error_with_patch(r, commit,\n \t\t\t\t\targ, item->arg_len, opts, res, !res);\n \t}\n-\tif (is_rebase_i(opts) && !res)\n+\tif ((is_rebase_i(opts) || opts->action == REPLAY_HISTORY_EDIT) && !res)\n \t\trecord_in_rewritten(&item->commit->object.oid,\n-\t\t\t\t    peek_command(todo_list, 1));\n+\t\t\t\t    peek_command(todo_list, 1), opts);\n \tif (res && is_fixup(item->command)) {\n \t\tif (res == 1)\n \t\t\tintend_to_amend();\n@@ -5020,6 +5040,7 @@ static int pick_commits(struct repository *r,\n \t\t\tstruct todo_list *todo_list,\n \t\t\tstruct replay_opts *opts)\n {\n+\tstruct strbuf head_ref = STRBUF_INIT, buf = STRBUF_INIT;\n \tstruct replay_ctx *ctx = opts->ctx;\n \tint res = 0, reschedule = 0;\n \n@@ -5106,7 +5127,7 @@ static int pick_commits(struct repository *r,\n \t\t\t\treschedule = 1;\n \t\t\telse if (item->commit)\n \t\t\t\trecord_in_rewritten(&item->commit->object.oid,\n-\t\t\t\t\t\t    peek_command(todo_list, 1));\n+\t\t\t\t\t\t    peek_command(todo_list, 1), opts);\n \t\t\tif (res > 0)\n \t\t\t\t/* failed with merge conflicts */\n \t\t\t\treturn error_with_patch(r, item->commit,\n@@ -5142,9 +5163,6 @@ static int pick_commits(struct repository *r,\n \t}\n \n \tif (is_rebase_i(opts)) {\n-\t\tstruct strbuf head_ref = STRBUF_INIT, buf = STRBUF_INIT;\n-\t\tstruct stat st;\n-\n \t\tif (read_oneliner(&head_ref, rebase_path_head_name(), 0) &&\n \t\t\t\tstarts_with(head_ref.buf, \"refs/\")) {\n \t\t\tconst char *msg;\n@@ -5206,13 +5224,24 @@ static int pick_commits(struct repository *r,\n \t\t\t}\n \t\t\trelease_revisions(&log_tree_opt);\n \t\t}\n-\t\tflush_rewritten_pending();\n-\t\tif (!stat(rebase_path_rewritten_list(), &st) &&\n-\t\t\t\tst.st_size > 0) {\n+\t}\n+\n+\tif (is_rebase_i(opts) || opts->action == REPLAY_HISTORY_EDIT) {\n+\t\tconst char *rewritten_list_path;\n+\t\tstruct stat st;\n+\n+\t\tflush_rewritten_pending(opts);\n+\n+\t\tif (opts->action == REPLAY_HISTORY_EDIT)\n+\t\t\trewritten_list_path = git_path_rewritten_list_file();\n+\t\telse\n+\t\t\trewritten_list_path = rebase_path_rewritten_list();\n+\n+\t\tif (!stat(rewritten_list_path, &st) && st.st_size > 0) {\n \t\t\tstruct child_process child = CHILD_PROCESS_INIT;\n \t\t\tstruct run_hooks_opt hook_opt = RUN_HOOKS_OPT_INIT;\n \n-\t\t\tchild.in = open(rebase_path_rewritten_list(), O_RDONLY);\n+\t\t\tchild.in = open(rewritten_list_path, O_RDONLY);\n \t\t\tchild.git_cmd = 1;\n \t\t\tstrvec_push(&child.args, \"notes\");\n \t\t\tstrvec_push(&child.args, \"copy\");\n@@ -5220,10 +5249,13 @@ static int pick_commits(struct repository *r,\n \t\t\t/* we don't care if this copying failed */\n \t\t\trun_command(&child);\n \n-\t\t\thook_opt.path_to_stdin = rebase_path_rewritten_list();\n-\t\t\tstrvec_push(&hook_opt.args, \"rebase\");\n+\t\t\thook_opt.path_to_stdin = rewritten_list_path;\n+\t\t\tstrvec_push(&hook_opt.args, is_rebase_i(opts) ? \"rebase\" : \"history\");\n \t\t\trun_hooks_opt(r, \"post-rewrite\", &hook_opt);\n \t\t}\n+\t}\n+\n+\tif (is_rebase_i(opts)) {\n \t\tapply_autostash(rebase_path_autostash());\n \n \t\tif (!opts->quiet) {\n@@ -5555,7 +5587,7 @@ int sequencer_continue(struct repository *r, struct replay_opts *opts)\n \t\tif (read_oneliner(&buf, rebase_path_stopped_sha(),\n \t\t\t\t  READ_ONELINER_SKIP_IF_EMPTY) &&\n \t\t    !get_oid_hex(buf.buf, &oid))\n-\t\t\trecord_in_rewritten(&oid, peek_command(&todo_list, 0));\n+\t\t\trecord_in_rewritten(&oid, peek_command(&todo_list, 0), opts);\n \t\tstrbuf_release(&buf);\n \t}\n \n@@ -6396,7 +6428,8 @@ int todo_list_write_to_file(struct repository *r, struct todo_list *todo_list,\n /* skip picking commits whose parents are unchanged */\n static int skip_unnecessary_picks(struct repository *r,\n \t\t\t\t  struct todo_list *todo_list,\n-\t\t\t\t  struct object_id *base_oid)\n+\t\t\t\t  struct object_id *base_oid,\n+\t\t\t\t  struct replay_opts *opts)\n {\n \tstruct object_id *parent_oid;\n \tint i;\n@@ -6435,7 +6468,7 @@ static int skip_unnecessary_picks(struct repository *r,\n \t\ttodo_list->done_nr += i;\n \n \t\tif (is_fixup(peek_command(todo_list, 0)))\n-\t\t\trecord_in_rewritten(base_oid, peek_command(todo_list, 0));\n+\t\t\trecord_in_rewritten(base_oid, peek_command(todo_list, 0), opts);\n \t}\n \n \treturn 0;\n@@ -6630,7 +6663,7 @@ int complete_action(struct repository *r, struct replay_opts *opts, unsigned fla\n \t\tBUG(\"invalid todo list after expanding IDs:\\n%s\",\n \t\t    new_todo.buf.buf);\n \n-\tif (opts->allow_ff && skip_unnecessary_picks(r, &new_todo, &oid)) {\n+\tif (opts->allow_ff && skip_unnecessary_picks(r, &new_todo, &oid, opts)) {\n \t\ttodo_list_release(&new_todo);\n \t\treturn error(_(\"could not skip unnecessary pick commands\"));\n \t}\n\n-- \n2.51.0.417.g1ba7204a04.dirty\n\n"},{"id":"525530","messageId":"20250904-b4-pks-history-builtin-v3-6-509053514755@pks.im","threadId":"63986","inReplyTo":"20250904-b4-pks-history-builtin-v3-0-509053514755@pks.im","subject":"[PATCH RFC v3 06/18] cache-tree: allow writing in-memory index as tree","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-09-04T14:27:36Z","receivedAt":"2025-09-04T14:27:57Z","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       | 5 ++---\n cache-tree.h       | 3 ++-\n 3 files changed, 6 insertions(+), 5 deletions(-)\n\ndiff --git a/builtin/checkout.c b/builtin/checkout.c\nindex f9453473fe2..43583c8d1be 100644\n--- a/builtin/checkout.c\n+++ b/builtin/checkout.c\n@@ -902,7 +902,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 66ef2becbe0..029ec933abe 100644\n--- a/cache-tree.c\n+++ b/cache-tree.c\n@@ -699,11 +699,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 \n@@ -723,7 +723,6 @@ struct tree* write_in_core_index_as_tree(struct repository *repo) {\n \treturn lookup_tree(repo, &index_state->cache_tree->oid);\n }\n \n-\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 {\n \tint entries, was_valid;\ndiff --git a/cache-tree.h b/cache-tree.h\nindex b82c4963e7c..f8bddae5235 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.51.0.417.g1ba7204a04.dirty\n\n"},{"id":"525531","messageId":"20250904-b4-pks-history-builtin-v3-7-509053514755@pks.im","threadId":"63986","inReplyTo":"20250904-b4-pks-history-builtin-v3-0-509053514755@pks.im","subject":"[PATCH RFC v3 07/18] builtin: add new \"history\" command","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-09-04T14:27:37Z","receivedAt":"2025-09-04T14:28:01Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"When rewriting history via git-rebase(1) there are a couple of very\ncommon use cases:\n\n  - The ordering of two commits should be reversed.\n\n  - A commit should be split up into two commits.\n\n  - A commit should be dropped from the history completely.\n\n  - Multiple commits should be squashed into one.\n\nWhile these operations are all doable, it often feels needlessly cludgy\nto do so by doing an interactive rebase, using the editor to say what\none wants, and then perform the actions. Furthermore, some operations\nlike splitting up a commit into two are way more involved than that and\nrequire a whole series of commands.\n\nAdd a new \"history\" command to plug this gap. This command will have\nseveral different subcommands to imperatively rewrite history for common\nuse cases like the above. These commands will be implemented in\nsubsequent commits.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n .gitignore                     |  1 +\n Documentation/git-history.adoc | 45 ++++++++++++++++++++++++++++++++++++++++++\n Documentation/meson.build      |  1 +\n Makefile                       |  1 +\n builtin.h                      |  1 +\n builtin/history.c              | 20 +++++++++++++++++++\n command-list.txt               |  1 +\n git.c                          |  1 +\n meson.build                    |  1 +\n t/meson.build                  |  3 ++-\n t/t3450-history.sh             | 12 +++++++++++\n 11 files changed, 86 insertions(+), 1 deletion(-)\n\ndiff --git a/.gitignore b/.gitignore\nindex 04c444404e..3932d4d618 100644\n--- a/.gitignore\n+++ b/.gitignore\n@@ -77,6 +77,7 @@\n /git-grep\n /git-hash-object\n /git-help\n+/git-history\n /git-hook\n /git-http-backend\n /git-http-fetch\ndiff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc\nnew file mode 100644\nindex 0000000000..1537960374\n--- /dev/null\n+++ b/Documentation/git-history.adoc\n@@ -0,0 +1,45 @@\n+git-history(1)\n+==============\n+\n+NAME\n+----\n+git-history - EXPERIMENTAL: Rewrite history of the current branch\n+\n+SYNOPSIS\n+--------\n+[synopsis]\n+git history [<options>]\n+\n+DESCRIPTION\n+-----------\n+\n+Rewrite history by rearranging or modifying specific commits in the\n+history.\n+\n+This command is similar to linkgit:git-rebase[1] and uses the same\n+underlying machinery. You should use rebases if you either want to\n+reapply a range of commits onto a different base, or interactive rebases\n+if you want to edit a range of commits.\n+\n+Note that this command does not (yet) work with histories that contain\n+merges. You should use linkgit:git-rebase[1] with the `--rebase-merges`\n+flag instead.\n+\n+THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.\n+\n+COMMANDS\n+--------\n+\n+This command requires a subcommand. Several subcommands are available to\n+rewrite history in different ways:\n+\n+CONFIGURATION\n+-------------\n+\n+include::includes/cmd-config-section-all.adoc[]\n+\n+include::config/sequencer.adoc[]\n+\n+GIT\n+---\n+Part of the linkgit:git[1] suite\ndiff --git a/Documentation/meson.build b/Documentation/meson.build\nindex 4404c623f0..a30b5307fd 100644\n--- a/Documentation/meson.build\n+++ b/Documentation/meson.build\n@@ -64,6 +64,7 @@ manpages = {\n   'git-gui.adoc' : 1,\n   'git-hash-object.adoc' : 1,\n   'git-help.adoc' : 1,\n+  'git-history.adoc' : 1,\n   'git-hook.adoc' : 1,\n   'git-http-backend.adoc' : 1,\n   'git-http-fetch.adoc' : 1,\ndiff --git a/Makefile b/Makefile\nindex e11340c1ae..bed6eda5e6 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -1261,6 +1261,7 @@ BUILTIN_OBJS += builtin/get-tar-commit-id.o\n BUILTIN_OBJS += builtin/grep.o\n BUILTIN_OBJS += builtin/hash-object.o\n BUILTIN_OBJS += builtin/help.o\n+BUILTIN_OBJS += builtin/history.o\n BUILTIN_OBJS += builtin/hook.o\n BUILTIN_OBJS += builtin/index-pack.o\n BUILTIN_OBJS += builtin/init-db.o\ndiff --git a/builtin.h b/builtin.h\nindex bff13e3069..2934f4479a 100644\n--- a/builtin.h\n+++ b/builtin.h\n@@ -172,6 +172,7 @@ int cmd_get_tar_commit_id(int argc, const char **argv, const char *prefix, struc\n int cmd_grep(int argc, const char **argv, const char *prefix, struct repository *repo);\n int cmd_hash_object(int argc, const char **argv, const char *prefix, struct repository *repo);\n int cmd_help(int argc, const char **argv, const char *prefix, struct repository *repo);\n+int cmd_history(int argc, const char **argv, const char *prefix, struct repository *repo);\n int cmd_hook(int argc, const char **argv, const char *prefix, struct repository *repo);\n int cmd_index_pack(int argc, const char **argv, const char *prefix, struct repository *repo);\n int cmd_init_db(int argc, const char **argv, const char *prefix, struct repository *repo);\ndiff --git a/builtin/history.c b/builtin/history.c\nnew file mode 100644\nindex 0000000000..d1a40368e0\n--- /dev/null\n+++ b/builtin/history.c\n@@ -0,0 +1,20 @@\n+#include \"builtin.h\"\n+#include \"gettext.h\"\n+#include \"parse-options.h\"\n+\n+int cmd_history(int argc,\n+\t\tconst char **argv,\n+\t\tconst char *prefix,\n+\t\tstruct repository *repo UNUSED)\n+{\n+\tconst char * const usage[] = {\n+\t\tN_(\"git history [<options>]\"),\n+\t\tNULL,\n+\t};\n+\tstruct option options[] = {\n+\t\tOPT_END(),\n+\t};\n+\n+\targc = parse_options(argc, argv, prefix, options, usage, 0);\n+\treturn 0;\n+}\ndiff --git a/command-list.txt b/command-list.txt\nindex b7ade3ab9f..f95f0ce926 100644\n--- a/command-list.txt\n+++ b/command-list.txt\n@@ -115,6 +115,7 @@ git-grep                                mainporcelain           info\n git-gui                                 mainporcelain\n git-hash-object                         plumbingmanipulators\n git-help                                ancillaryinterrogators          complete\n+git-history                             mainporcelain           history\n git-hook                                purehelpers\n git-http-backend                        synchingrepositories\n git-http-fetch                          synchelpers\ndiff --git a/git.c b/git.c\nindex 83eac0aeab..9d2cba2906 100644\n--- a/git.c\n+++ b/git.c\n@@ -560,6 +560,7 @@ static struct cmd_struct commands[] = {\n \t{ \"grep\", cmd_grep, RUN_SETUP_GENTLY },\n \t{ \"hash-object\", cmd_hash_object },\n \t{ \"help\", cmd_help },\n+\t{ \"history\", cmd_history, RUN_SETUP },\n \t{ \"hook\", cmd_hook, RUN_SETUP },\n \t{ \"index-pack\", cmd_index_pack, RUN_SETUP_GENTLY | NO_PARSEOPT },\n \t{ \"init\", cmd_init_db },\ndiff --git a/meson.build b/meson.build\nindex 5dd299b496..0e40778a23 100644\n--- a/meson.build\n+++ b/meson.build\n@@ -603,6 +603,7 @@ builtin_sources = [\n   'builtin/grep.c',\n   'builtin/hash-object.c',\n   'builtin/help.c',\n+  'builtin/history.c',\n   'builtin/hook.c',\n   'builtin/index-pack.c',\n   'builtin/init-db.c',\ndiff --git a/t/meson.build b/t/meson.build\nindex bbeba1a8d5..966d7c14f4 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -376,6 +376,7 @@ integration_tests = [\n   't3436-rebase-more-options.sh',\n   't3437-rebase-fixup-options.sh',\n   't3438-rebase-broken-files.sh',\n+  't3450-history.sh',\n   't3500-cherry.sh',\n   't3501-revert-cherry-pick.sh',\n   't3502-cherry-pick-merge.sh',\n@@ -1214,4 +1215,4 @@ if perl.found() and time.found()\n       timeout: 0,\n     )\n   endforeach\n-endif\n\\ No newline at end of file\n+endif\ndiff --git a/t/t3450-history.sh b/t/t3450-history.sh\nnew file mode 100755\nindex 0000000000..9eb1ed6749\n--- /dev/null\n+++ b/t/t3450-history.sh\n@@ -0,0 +1,12 @@\n+#!/bin/sh\n+\n+test_description='tests for git-history command'\n+\n+. ./test-lib.sh\n+\n+test_expect_success 'refuses to do anything without subcommand' '\n+\ttest_must_fail git history 2>err &&\n+\ttest_grep foo err\n+'\n+\n+test_done\n\n-- \n2.51.0.417.g1ba7204a04.dirty\n\n"},{"id":"525532","messageId":"20250904-b4-pks-history-builtin-v3-8-509053514755@pks.im","threadId":"63986","inReplyTo":"20250904-b4-pks-history-builtin-v3-0-509053514755@pks.im","subject":"[PATCH RFC v3 08/18] builtin/history: introduce subcommands to manage interrupted rewrites","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-09-04T14:27:38Z","receivedAt":"2025-09-04T14:28:04Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Introduce subcommands to manage the sequencer state for git-history(1).\nThese aren't really useful yet, but will become useful in subsequent\ncommits where we will introduce git-history(1) subcommands that actually\nedit history.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/git-history.adoc |  20 +++++++-\n builtin/history.c              | 114 +++++++++++++++++++++++++++++++++++++++--\n t/t3450-history.sh             |  32 +++++++++++-\n 3 files changed, 161 insertions(+), 5 deletions(-)\n\ndiff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc\nindex 1537960374..3e9a789b83 100644\n--- a/Documentation/git-history.adoc\n+++ b/Documentation/git-history.adoc\n@@ -8,7 +8,9 @@ git-history - EXPERIMENTAL: Rewrite history of the current branch\n SYNOPSIS\n --------\n [synopsis]\n-git history [<options>]\n+git history abort\n+git history continue\n+git history quit\n \n DESCRIPTION\n -----------\n@@ -33,6 +35,22 @@ COMMANDS\n This command requires a subcommand. Several subcommands are available to\n rewrite history in different ways:\n \n+The following commands are used to manage an interrupted history-rewriting\n+operation:\n+\n+`abort`::\n+\tAbort the history-rewriting operation and reset HEAD to the original\n+\tbranch.\n+\n+`continue`::\n+\tRestart the history-rewriting process after having resolved a merge\n+\tconflict.\n+\n+`quit`::\n+\tAbort the history-rewriting operation but `HEAD` is not reset back to\n+\tthe original branch. The index and working tree are also left unchanged\n+\tas a result.\n+\n CONFIGURATION\n -------------\n \ndiff --git a/builtin/history.c b/builtin/history.c\nindex d1a40368e0..0ad45dbfef 100644\n--- a/builtin/history.c\n+++ b/builtin/history.c\n@@ -1,20 +1,128 @@\n #include \"builtin.h\"\n+#include \"branch.h\"\n #include \"gettext.h\"\n #include \"parse-options.h\"\n+#include \"sequencer.h\"\n+\n+static int cmd_history_abort(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\tN_(\"git history abort\"),\n+\t\tNULL,\n+\t};\n+\tstruct option options[] = {\n+\t\tOPT_END(),\n+\t};\n+\tstruct replay_opts opts = REPLAY_OPTS_INIT;\n+\tint ret;\n+\n+\targc = parse_options(argc, argv, prefix, options, usage, 0);\n+\tif (argc) {\n+\t\tret = error(_(\"command does not take arguments\"));\n+\t\tgoto out;\n+\t}\n+\n+\topts.action = REPLAY_HISTORY_EDIT;\n+\tret = sequencer_rollback(repo, &opts);\n+\tif (ret)\n+\t\tgoto out;\n+\n+\tret = 0;\n+\n+out:\n+\treplay_opts_release(&opts);\n+\treturn ret;\n+}\n+\n+static int cmd_history_continue(int argc,\n+\t\t\t\tconst char **argv,\n+\t\t\t\tconst char *prefix,\n+\t\t\t\tstruct repository *repo)\n+{\n+\tconst char * const usage[] = {\n+\t\tN_(\"git history continue\"),\n+\t\tNULL,\n+\t};\n+\tstruct option options[] = {\n+\t\tOPT_END(),\n+\t};\n+\tstruct replay_opts opts = REPLAY_OPTS_INIT;\n+\tint ret;\n+\n+\targc = parse_options(argc, argv, prefix, options, usage, 0);\n+\tif (argc) {\n+\t\tret = error(_(\"command does not take arguments\"));\n+\t\tgoto out;\n+\t}\n+\n+\topts.action = REPLAY_HISTORY_EDIT;\n+\tret = sequencer_continue(repo, &opts);\n+\tif (ret)\n+\t\tgoto out;\n+\n+\tret = 0;\n+\n+out:\n+\treplay_opts_release(&opts);\n+\treturn ret;\n+}\n+\n+static int cmd_history_quit(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\tN_(\"git history quit\"),\n+\t\tNULL,\n+\t};\n+\tstruct option options[] = {\n+\t\tOPT_END(),\n+\t};\n+\tstruct replay_opts opts = REPLAY_OPTS_INIT;\n+\tint ret;\n+\n+\targc = parse_options(argc, argv, prefix, options, usage, 0);\n+\tif (argc) {\n+\t\tret = error(_(\"command does not take arguments\"));\n+\t\tgoto out;\n+\t}\n+\n+\topts.action = REPLAY_HISTORY_EDIT;\n+\tret = sequencer_remove_state(repo, &opts);\n+\tif (ret)\n+\t\tgoto out;\n+\tremove_branch_state(repo, 0);\n+\n+\tret = 0;\n+\n+out:\n+\treplay_opts_release(&opts);\n+\treturn ret;\n+}\n \n int cmd_history(int argc,\n \t\tconst char **argv,\n \t\tconst char *prefix,\n-\t\tstruct repository *repo UNUSED)\n+\t\tstruct repository *repo)\n {\n \tconst char * const usage[] = {\n-\t\tN_(\"git history [<options>]\"),\n+\t\tN_(\"git history abort\"),\n+\t\tN_(\"git history continue\"),\n+\t\tN_(\"git history quit\"),\n \t\tNULL,\n \t};\n+\tparse_opt_subcommand_fn *fn = NULL;\n \tstruct option options[] = {\n+\t\tOPT_SUBCOMMAND(\"abort\", &fn, cmd_history_abort),\n+\t\tOPT_SUBCOMMAND(\"continue\", &fn, cmd_history_continue),\n+\t\tOPT_SUBCOMMAND(\"quit\", &fn, cmd_history_quit),\n \t\tOPT_END(),\n \t};\n \n \targc = parse_options(argc, argv, prefix, options, usage, 0);\n-\treturn 0;\n+\treturn fn(argc, argv, prefix, repo);\n }\ndiff --git a/t/t3450-history.sh b/t/t3450-history.sh\nindex 9eb1ed6749..aa9d44c03b 100755\n--- a/t/t3450-history.sh\n+++ b/t/t3450-history.sh\n@@ -6,7 +6,37 @@ test_description='tests for git-history command'\n \n test_expect_success 'refuses to do anything without subcommand' '\n \ttest_must_fail git history 2>err &&\n-\ttest_grep foo err\n+\ttest_grep \"need a subcommand\" err\n+'\n+\n+test_expect_success 'abort complains about arguments' '\n+\ttest_must_fail git history abort foo 2>err &&\n+\ttest_grep \"command does not take arguments\" err\n+'\n+\n+test_expect_success 'abort complains when no history edit is active' '\n+\ttest_must_fail git history abort 2>err &&\n+\ttest_grep \"no history edit in progress\" err\n+'\n+\n+test_expect_success 'continue complains about arguments' '\n+\ttest_must_fail git history continue foo 2>err &&\n+\ttest_grep \"command does not take arguments\" err\n+'\n+\n+test_expect_success 'continue complains when no history edit is active' '\n+\ttest_must_fail git history continue 2>err &&\n+\ttest_grep \"no history edit in progress\" err\n+'\n+\n+test_expect_success 'quit complains about arguments' '\n+\ttest_must_fail git history quit foo 2>err &&\n+\ttest_grep \"command does not take arguments\" err\n+'\n+\n+test_expect_success 'quit does not complain when no history edit is active' '\n+\tgit history quit 2>err &&\n+\ttest_must_be_empty err\n '\n \n test_done\n\n-- \n2.51.0.417.g1ba7204a04.dirty\n\n"},{"id":"525533","messageId":"20250904-b4-pks-history-builtin-v3-9-509053514755@pks.im","threadId":"63986","inReplyTo":"20250904-b4-pks-history-builtin-v3-0-509053514755@pks.im","subject":"[PATCH RFC v3 09/18] builtin/history: implement \"drop\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-09-04T14:27:39Z","receivedAt":"2025-09-04T14:28:06Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"It is a fairly common operation to perform an interactive rebase so that\none of the commits can be dropped from history. Doing this is not very\nhard in general, but still requires the user to perform multiple steps:\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. Edit the instruction sheet to drop that commit.\n\nThis is needlessly complex for such a supposedly-trivial operation.\nFurthermore, the second step doesn't account for certain edge cases like\nfor example dropping the root commit.\n\nIntroduce a new \"drop\" subcommand to make this use case significantly\nsimpler: all the user needs to do is to say `git history drop $COMMIT`\nand they're done.\n\nNote that for now, this command only allows users to drop a single\ncommit at once. It should be easy enough though to expand the command at\na later point in time to support dropping whole commit ranges.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/git-history.adoc |  27 ++++\n builtin/history.c              | 287 +++++++++++++++++++++++++++++++++++++++++\n t/meson.build                  |   1 +\n t/t3451-history-drop.sh        | 207 +++++++++++++++++++++++++++++\n 4 files changed, 522 insertions(+)\n\ndiff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc\nindex 3e9a789b83..39c9f1e25e 100644\n--- a/Documentation/git-history.adoc\n+++ b/Documentation/git-history.adoc\n@@ -11,6 +11,7 @@ SYNOPSIS\n git history abort\n git history continue\n git history quit\n+git history drop <commit>\n \n DESCRIPTION\n -----------\n@@ -35,6 +36,15 @@ COMMANDS\n This command requires a subcommand. Several subcommands are available to\n rewrite history in different ways:\n \n+`drop <commit>`::\n+\tDrop a commit from the history and reapply all children of that\n+\tcommit on top of the commit's parent. The commit that is to be\n+\tdropped must be reachable from the currently checked-out commit.\n++\n+Dropping the root commit converts the child of that commit into the new\n+root commit. It is invalid to drop a root commit that does not have any\n+child commits, as that would lead to an empty branch.\n+\n The following commands are used to manage an interrupted history-rewriting\n operation:\n \n@@ -51,6 +61,23 @@ operation:\n \tthe original branch. The index and working tree are also left unchanged\n \tas a result.\n \n+EXAMPLES\n+--------\n+\n+Drop a commit from history\n+~~~~~~~~~~~~~~~~~~~~~~~~~~\n+\n+----------\n+$ git log --oneline\n+2d4cd6d third\n+125a0f3 second\n+e098c27 first\n+$ git history drop HEAD~\n+$ git log\n+b1bc1bd third\n+e098c27 first\n+----------\n+\n CONFIGURATION\n -------------\n \ndiff --git a/builtin/history.c b/builtin/history.c\nindex 0ad45dbfef..2132b6a441 100644\n--- a/builtin/history.c\n+++ b/builtin/history.c\n@@ -1,7 +1,16 @@\n #include \"builtin.h\"\n #include \"branch.h\"\n+#include \"commit.h\"\n+#include \"commit-reach.h\"\n+#include \"config.h\"\n+#include \"environment.h\"\n #include \"gettext.h\"\n+#include \"hex.h\"\n+#include \"object-name.h\"\n #include \"parse-options.h\"\n+#include \"refs.h\"\n+#include \"reset.h\"\n+#include \"revision.h\"\n #include \"sequencer.h\"\n \n static int cmd_history_abort(int argc,\n@@ -104,6 +113,282 @@ static int cmd_history_quit(int argc,\n \treturn ret;\n }\n \n+static int collect_commits(struct repository *repo,\n+\t\t\t   struct commit *old_commit,\n+\t\t\t   struct commit *new_commit,\n+\t\t\t   struct strvec *out)\n+{\n+\tstruct setup_revision_opt revision_opts = {\n+\t\t.assume_dashdash = 1,\n+\t};\n+\tstruct strvec revisions = STRVEC_INIT;\n+\tstruct commit_list *from_list = NULL;\n+\tstruct commit *child;\n+\tstruct rev_info rev = { 0 };\n+\tint ret;\n+\n+\t/*\n+\t * Check that the old commit actually is an ancestor of HEAD. If not\n+\t * the whole request becomes nonsensical.\n+\t*/\n+\tif (old_commit) {\n+\t\tcommit_list_insert(old_commit, &from_list);\n+\t\tif (!repo_is_descendant_of(repo, new_commit, from_list)) {\n+\t\t\tret = error(_(\"commit must be reachable from current HEAD commit\"));\n+\t\t\tgoto out;\n+\t\t}\n+\t}\n+\n+\trepo_init_revisions(repo, &rev, NULL);\n+\tstrvec_push(&revisions, \"\");\n+\tstrvec_push(&revisions, oid_to_hex(&new_commit->object.oid));\n+\tif (old_commit)\n+\t\tstrvec_pushf(&revisions, \"^%s\", oid_to_hex(&old_commit->object.oid));\n+\tif (setup_revisions(revisions.nr, revisions.v, &rev, &revision_opts) != 1 ||\n+\t    prepare_revision_walk(&rev)) {\n+\t\tret = error(_(\"revision walk setup failed\"));\n+\t\tgoto out;\n+\t}\n+\n+\twhile ((child = get_revision(&rev))) {\n+\t\tif (old_commit && !child->parents)\n+\t\t\tBUG(\"revision walk did not find child commit\");\n+\t\tif (child->parents && child->parents->next) {\n+\t\t\tret = error(_(\"cannot rearrange commit history with merges\"));\n+\t\t\tgoto out;\n+\t\t}\n+\n+\t\tstrvec_push(out, oid_to_hex(&child->object.oid));\n+\n+\t\tif (child->parents && old_commit &&\n+\t\t    commit_list_contains(old_commit, child->parents))\n+\t\t\tbreak;\n+\t}\n+\n+\t/*\n+\t * Revisions are in newest-order-first. We have to reverse the\n+\t * array though so that we pick the oldest commits first.\n+\t */\n+\tfor (size_t i = 0, j = out->nr - 1; i < j; i++, j--)\n+\t\tSWAP(out->v[i], out->v[j]);\n+\n+\tret = 0;\n+\n+out:\n+\tfree_commit_list(from_list);\n+\tstrvec_clear(&revisions);\n+\trelease_revisions(&rev);\n+\treset_revision_walk();\n+\treturn ret;\n+}\n+\n+static int apply_commits(struct repository *repo,\n+\t\t\t const struct strvec *commits,\n+\t\t\t struct commit *head,\n+\t\t\t struct commit *base,\n+\t\t\t const char *action)\n+{\n+\tstruct setup_revision_opt revision_opts = {\n+\t\t.assume_dashdash = 1,\n+\t};\n+\tstruct replay_opts replay_opts = REPLAY_OPTS_INIT;\n+\tstruct reset_head_opts reset_opts = { 0 };\n+\tstruct object_id root_commit;\n+\tstruct strvec args = STRVEC_INIT;\n+\tstruct strbuf buf = STRBUF_INIT;\n+\tchar hex[GIT_MAX_HEXSZ + 1];\n+\tint ref_flags, ret;\n+\n+\t/*\n+\t * We have performed all safety checks, so we now prepare\n+\t * replaying the commits.\n+\t*/\n+\treplay_opts.action = REPLAY_HISTORY_EDIT;\n+\tsequencer_init_config(&replay_opts);\n+\treplay_opts.quiet = 1;\n+\treplay_opts.skip_commit_summary = 1;\n+\tif (!replay_opts.strategy && replay_opts.default_strategy) {\n+\t\treplay_opts.strategy = replay_opts.default_strategy;\n+\t\treplay_opts.default_strategy = NULL;\n+\t}\n+\n+\tstrvec_push(&args, \"\");\n+\tstrvec_pushv(&args, commits->v);\n+\n+\treplay_opts.revs = xmalloc(sizeof(*replay_opts.revs));\n+\trepo_init_revisions(repo, replay_opts.revs, NULL);\n+\treplay_opts.revs->no_walk = 1;\n+\treplay_opts.revs->unsorted_input = 1;\n+\tif (setup_revisions(args.nr, args.v, replay_opts.revs,\n+\t\t\t    &revision_opts) != 1) {\n+\t\tret = error(_(\"setting up revisions failed\"));\n+\t\tgoto out;\n+\t}\n+\n+\t/*\n+\t * If we're dropping the root commit we first need to create\n+\t * a new empty root. We then instruct the seqencer machinery to\n+\t * squash that root commit with the first commit we're picking\n+\t * onto it.\n+\t */\n+\tif (!base->parents) {\n+\t\tif (commit_tree(\"\", 0, repo->hash_algo->empty_tree, NULL,\n+\t\t\t\t&root_commit, NULL, NULL) < 0) {\n+\t\t\tret = error(_(\"Could not create new root commit\"));\n+\t\t\tgoto out;\n+\t\t}\n+\n+\t\treplay_opts.squash_onto = root_commit;\n+\t\treplay_opts.have_squash_onto = 1;\n+\t\treset_opts.oid = &root_commit;\n+\t} else {\n+\t\treset_opts.oid = &base->parents->item->object.oid;\n+\t}\n+\n+\treplay_opts.restore_head_target =\n+\t\txstrdup_or_null(refs_resolve_ref_unsafe(get_main_ref_store(repo),\n+\t\t\t\t\t\t\t\"HEAD\", 0, NULL, &ref_flags));\n+\tif (!(ref_flags & REF_ISSYMREF))\n+\t\tFREE_AND_NULL(replay_opts.restore_head_target);\n+\n+\t/*\n+\t * Perform a hard-reset to the parent of our commit that is to\n+\t * be dropped. This is the new base onto which we'll pick all\n+\t * the descendants.\n+\t */\n+\tstrbuf_addf(&buf, \"%s (start): checkout %s\", action,\n+\t\t    oid_to_hex_r(hex, reset_opts.oid));\n+\treset_opts.orig_head = &head->object.oid;\n+\treset_opts.flags = RESET_HEAD_DETACH | RESET_ORIG_HEAD;\n+\treset_opts.head_msg = buf.buf;\n+\treset_opts.default_reflog_action = action;\n+\tif (reset_head(repo, &reset_opts) < 0) {\n+\t\tret = error(_(\"could not switch to %s\"), oid_to_hex(reset_opts.oid));\n+\t\tgoto out;\n+\t}\n+\n+\tret = sequencer_pick_revisions(repo, &replay_opts);\n+\tif (ret < 0) {\n+\t\tret = error(_(\"could not pick commits\"));\n+\t\tgoto out;\n+\t} else if (ret > 0) {\n+\t\t/*\n+\t\t * A positive return value indicates we've got a merge\n+\t\t * conflict. Bail out, but don't print a message as\n+\t\t * `sequencer_pick_revisions()` already printed enough\n+\t\t * information.\n+\t\t */\n+\t\tret = -1;\n+\t\tgoto out;\n+\t}\n+\n+\tret = 0;\n+\n+out:\n+\treplay_opts_release(&replay_opts);\n+\tstrbuf_release(&buf);\n+\tstrvec_clear(&args);\n+\treturn ret;\n+}\n+\n+static int cmd_history_drop(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\tN_(\"git history drop <commit>\"),\n+\t\tNULL,\n+\t};\n+\tstruct option options[] = {\n+\t\tOPT_END(),\n+\t};\n+\tstruct commit *commit_to_drop, *head;\n+\tstruct strvec commits = STRVEC_INIT;\n+\tstruct strbuf buf = STRBUF_INIT;\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 single revision\"));\n+\t\tgoto out;\n+\t}\n+\trepo_config(repo, git_default_config, NULL);\n+\n+\tcommit_to_drop = lookup_commit_reference_by_name(argv[0]);\n+\tif (!commit_to_drop) {\n+\t\tret = error(_(\"commit to be dropped cannot be found: %s\"), argv[0]);\n+\t\tgoto out;\n+\t}\n+\tif (commit_to_drop->parents && commit_to_drop->parents->next) {\n+\t\tret = error(_(\"commit to be dropped must not be a merge commit\"));\n+\t\tgoto out;\n+\t}\n+\n+\thead = lookup_commit_reference_by_name(\"HEAD\");\n+\tif (!head) {\n+\t\tret = error(_(\"could not resolve HEAD to a commit\"));\n+\t\tgoto out;\n+\t}\n+\n+\tif (oideq(&commit_to_drop->object.oid, &head->object.oid)) {\n+\t\t/*\n+\t\t * If we want to drop the tip of the current branch we don't\n+\t\t * have to perform any rebase at all. Instead, we simply\n+\t\t * perform a hard reset to the parent commit.\n+\t\t */\n+\t\tstruct reset_head_opts reset_opts = {\n+\t\t\t.orig_head = &head->object.oid,\n+\t\t\t.flags = RESET_ORIG_HEAD,\n+\t\t\t.default_reflog_action = \"drop\",\n+\t\t};\n+\t\tchar hex[GIT_MAX_HEXSZ + 1];\n+\n+\t\tif (!commit_to_drop->parents) {\n+\t\t\tret = error(_(\"cannot drop the only commit on this branch\"));\n+\t\t\tgoto out;\n+\t\t}\n+\n+\t\toid_to_hex_r(hex, &commit_to_drop->parents->item->object.oid);\n+\t\tstrbuf_addf(&buf, \"drop (start): checkout %s\", hex);\n+\t\treset_opts.oid = &commit_to_drop->parents->item->object.oid;\n+\t\treset_opts.head_msg = buf.buf;\n+\n+\t\tif (reset_head(repo, &reset_opts) < 0) {\n+\t\t\tret = error(_(\"could not switch to %s\"), hex);\n+\t\t\tgoto out;\n+\t\t}\n+\t} else {\n+\t\t/*\n+\t\t * Prepare a revision walk from old commit to the commit that is\n+\t\t * about to be dropped. This serves three purposes:\n+\t\t *\n+\t\t *   - We verify that the history doesn't contain any merges.\n+\t\t *     For now, merges aren't yet handled by us.\n+\t\t *\n+\t\t *   - We need to find the child of the commit-to-be-dropped.\n+\t\t *     This child is what will be adopted by the parent of the\n+\t\t *     commit that we are about to drop.\n+\t\t *\n+\t\t *   - We compute the list of commits-to-be-picked.\n+\t\t */\n+\t\tret = collect_commits(repo, commit_to_drop, head, &commits);\n+\t\tif (ret < 0)\n+\t\t\tgoto out;\n+\n+\t\tret = apply_commits(repo, &commits, head, commit_to_drop, \"drop\");\n+\t\tif (ret < 0)\n+\t\t\tgoto out;\n+\t}\n+\n+\tret = 0;\n+\n+out:\n+\tstrvec_clear(&commits);\n+\tstrbuf_release(&buf);\n+\treturn ret;\n+}\n+\n int cmd_history(int argc,\n \t\tconst char **argv,\n \t\tconst char *prefix,\n@@ -113,6 +398,7 @@ int cmd_history(int argc,\n \t\tN_(\"git history abort\"),\n \t\tN_(\"git history continue\"),\n \t\tN_(\"git history quit\"),\n+\t\tN_(\"git history drop <commit>\"),\n \t\tNULL,\n \t};\n \tparse_opt_subcommand_fn *fn = NULL;\n@@ -120,6 +406,7 @@ int cmd_history(int argc,\n \t\tOPT_SUBCOMMAND(\"abort\", &fn, cmd_history_abort),\n \t\tOPT_SUBCOMMAND(\"continue\", &fn, cmd_history_continue),\n \t\tOPT_SUBCOMMAND(\"quit\", &fn, cmd_history_quit),\n+\t\tOPT_SUBCOMMAND(\"drop\", &fn, cmd_history_drop),\n \t\tOPT_END(),\n \t};\n \ndiff --git a/t/meson.build b/t/meson.build\nindex 966d7c14f4..8189c6c561 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -377,6 +377,7 @@ integration_tests = [\n   't3437-rebase-fixup-options.sh',\n   't3438-rebase-broken-files.sh',\n   't3450-history.sh',\n+  't3451-history-drop.sh',\n   't3500-cherry.sh',\n   't3501-revert-cherry-pick.sh',\n   't3502-cherry-pick-merge.sh',\ndiff --git a/t/t3451-history-drop.sh b/t/t3451-history-drop.sh\nnew file mode 100755\nindex 0000000000..4fc295ef21\n--- /dev/null\n+++ b/t/t3451-history-drop.sh\n@@ -0,0 +1,207 @@\n+#!/bin/sh\n+\n+test_description='tests for git-history drop subcommand'\n+\n+. ./test-lib.sh\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 drop HEAD 2>err &&\n+\t\ttest_grep \"commit to be dropped must not be a merge commit\" err &&\n+\t\ttest_must_fail git history drop HEAD~ 2>err &&\n+\t\ttest_grep \"cannot rearrange commit history with merges\" err\n+\t)\n+'\n+\n+test_expect_success 'refuses to work when history becomes empty' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit base &&\n+\t\ttest_must_fail git history drop HEAD 2>err &&\n+\t\ttest_grep \"cannot drop the only commit on this branch\" err\n+\t)\n+'\n+\n+test_expect_success 'refuses to work with changes in the worktree or index' '\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_commit file file &&\n+\t\techo foo >file &&\n+\t\ttest_must_fail git history drop HEAD 2>err &&\n+\t\ttest_grep \"Your local changes to the following files would be overwritten\" err &&\n+\t\tgit add file &&\n+\t\ttest_must_fail git history drop HEAD 2>err &&\n+\t\ttest_grep \"Your local changes to the following files would be overwritten\" err\n+\t)\n+'\n+\n+test_expect_success 'can drop tip of a branch' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit first &&\n+\t\ttest_commit second &&\n+\t\ttest_commit third &&\n+\n+\t\tgit symbolic-ref HEAD >expect &&\n+\t\tgit history drop HEAD &&\n+\t\tgit symbolic-ref HEAD >actual &&\n+\t\ttest_cmp expect actual &&\n+\n+\t\tcat >expect <<-EOF &&\n+\t\tsecond\n+\t\tfirst\n+\t\tEOF\n+\t\tgit log --format=%s >actual &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+test_expect_success 'can drop commit in the middle' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit first &&\n+\t\ttest_commit second &&\n+\t\ttest_commit third &&\n+\t\ttest_commit fourth &&\n+\t\ttest_commit fifth &&\n+\n+\t\tgit symbolic-ref HEAD >expect &&\n+\t\tgit history drop HEAD~2 &&\n+\t\tgit symbolic-ref HEAD >actual &&\n+\t\ttest_cmp expect actual &&\n+\n+\t\tcat >expect <<-EOF &&\n+\t\tfifth\n+\t\tfourth\n+\t\tsecond\n+\t\tfirst\n+\t\tEOF\n+\t\tgit log --format=%s >actual &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+test_expect_success 'correct order is retained' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit first &&\n+\t\ttest_commit second &&\n+\t\ttest_commit third &&\n+\t\ttest_commit fourth &&\n+\t\ttest_commit fifth &&\n+\t\tgit history drop HEAD~3 &&\n+\t\tcat >expect <<-EOF &&\n+\t\tfifth\n+\t\tfourth\n+\t\tthird\n+\t\tfirst\n+\t\tEOF\n+\t\tgit log --format=%s >actual &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+test_expect_success 'hooks are executed for rewritten commits' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit first &&\n+\t\ttest_commit second &&\n+\t\ttest_commit third &&\n+\n+\t\twrite_script .git/hooks/prepare-commit-msg <<-EOF &&\n+\t\techo \"prepare-commit-msg: \\$@\" >>\"$(pwd)/hooks.log\"\n+\t\tEOF\n+\t\twrite_script .git/hooks/post-commit <<-EOF &&\n+\t\techo \"post-commit\" >>\"$(pwd)/hooks.log\"\n+\t\tEOF\n+\t\twrite_script .git/hooks/post-rewrite <<-EOF &&\n+\t\t{\n+\t\t\techo \"post-rewrite: \\$@\"\n+\t\t\tcat\n+\t\t} >>\"$(pwd)/hooks.log\"\n+\t\tEOF\n+\n+\t\tgit history drop HEAD~ &&\n+\t\tcat >expect <<-EOF &&\n+\t\tprepare-commit-msg: .git/COMMIT_EDITMSG message\n+\t\tpost-commit\n+\t\tpost-rewrite: history\n+\t\t$(git rev-parse third) $(git rev-parse HEAD)\n+\t\tEOF\n+\t\ttest_cmp expect hooks.log\n+\t)\n+'\n+\n+test_expect_success 'can drop root commit' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit first &&\n+\t\ttest_commit second &&\n+\t\ttest_commit third &&\n+\t\tgit history drop HEAD~2 &&\n+\t\tcat >expect <<-EOF &&\n+\t\tthird\n+\t\tsecond\n+\t\tEOF\n+\t\tgit log --format=%s >actual &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+test_expect_success 'conflicts are detected' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit base &&\n+\t\techo original >file &&\n+\t\tgit add . &&\n+\t\tgit commit -m original &&\n+\t\techo modified >file &&\n+\t\tgit commit -am modified &&\n+\n+\t\ttest_must_fail git history drop HEAD~ >err 2>&1 &&\n+\t\ttest_grep CONFLICT err &&\n+\t\ttest_grep \"git history continue\" err &&\n+\t\techo resolved >file &&\n+\t\tgit add file &&\n+\t\tgit history continue &&\n+\n+\t\tcat >expect <<-EOF &&\n+\t\tmodified\n+\t\tbase\n+\t\tEOF\n+\t\tgit log --format=%s >actual &&\n+\t\ttest_cmp expect actual &&\n+\t\techo resolved >expect &&\n+\t\tgit cat-file -p HEAD:file >actual &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+test_done\n\n-- \n2.51.0.417.g1ba7204a04.dirty\n\n"},{"id":"525534","messageId":"20250904-b4-pks-history-builtin-v3-10-509053514755@pks.im","threadId":"63986","inReplyTo":"20250904-b4-pks-history-builtin-v3-0-509053514755@pks.im","subject":"[PATCH RFC v3 10/18] builtin/history: implement \"reorder\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-09-04T14:27:40Z","receivedAt":"2025-09-04T14:28:10Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"When working in projects where having nice commits matters it's quite\ncommon that developers end up reordering commits a lot. This is\ntypically done via interactive rebases, where they can then rearrange\ncommits in the instruction sheet.\n\nStill, this operation is a frequent-enough operation to provide a more\ndirect way of doing this imperatively. As such, introduce a new\n\"reorder\" subcommand where users can reorder a commit A to come after or\nbefore another commit B:\n\n    $ git log --oneline\n    a978f73 fifth\n    57594ee fourth\n    04eb1c4 third\n    d535e30 second\n    bf7438d first\n\n    $ git history reorder :/fourth --before=:/second\n    $ git log --oneline\n    1610fe0 fifth\n    444f97d third\n    2f90797 second\n    b0ae659 fourth\n    bf7438d first\n\n    $ git history reorder :/fourth --after=:/second\n    $ git log --oneline\n    c48729d fifth\n    f44a46e third\n    26693b8 fourth\n    8cb4171 second\n    bf7438d first\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/git-history.adoc |  33 +++++\n builtin/history.c              | 130 +++++++++++++++++++\n t/meson.build                  |   1 +\n t/t3452-history-reorder.sh     | 278 +++++++++++++++++++++++++++++++++++++++++\n 4 files changed, 442 insertions(+)\n\ndiff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc\nindex 39c9f1e25e..b36cd925dd 100644\n--- a/Documentation/git-history.adoc\n+++ b/Documentation/git-history.adoc\n@@ -12,6 +12,7 @@ git history abort\n git history continue\n git history quit\n git history drop <commit>\n+git history reorder <commit> (--before=<following-commit>|--after=<preceding-commit>)\n \n DESCRIPTION\n -----------\n@@ -45,6 +46,12 @@ Dropping the root commit converts the child of that commit into the new\n root commit. It is invalid to drop a root commit that does not have any\n child commits, as that would lead to an empty branch.\n \n+`reorder <commit> (--before=<following-commit>|--after=<preceding-commit>)`::\n+\tMove the commit so that it becomes either the parent of\n+\t<following-commit> or the child of <preceding-commit>. The commits must\n+\tbe related to one another and must be reachable from the current `HEAD`\n+\tcommit.\n+\n The following commands are used to manage an interrupted history-rewriting\n operation:\n \n@@ -78,6 +85,32 @@ b1bc1bd third\n e098c27 first\n ----------\n \n+Reorder a commit\n+~~~~~~~~~~~~~~~~\n+\n+----------\n+$ git log --oneline\n+a978f73 fifth\n+57594ee fourth\n+04eb1c4 third\n+d535e30 second\n+bf7438d first\n+$ git history reorder :/fourth --before=:/second\n+$ git log --oneline\n+1610fe0 fifth\n+444f97d third\n+2f90797 second\n+b0ae659 fourth\n+bf7438d first\n+$ git history reorder :/fourth --after=:/second\n+$ git log --oneline\n+c48729d fifth\n+f44a46e third\n+26693b8 fourth\n+8cb4171 second\n+bf7438d first\n+----------\n+\n CONFIGURATION\n -------------\n \ndiff --git a/builtin/history.c b/builtin/history.c\nindex 2132b6a441..16b516856e 100644\n--- a/builtin/history.c\n+++ b/builtin/history.c\n@@ -182,6 +182,33 @@ static int collect_commits(struct repository *repo,\n \treturn ret;\n }\n \n+static void replace_commits(struct strvec *commits,\n+\t\t\t    const struct object_id *commit_to_replace,\n+\t\t\t    const struct object_id *replacements,\n+\t\t\t    size_t replacements_nr)\n+{\n+\tchar commit_to_replace_oid[GIT_MAX_HEXSZ + 1];\n+\tstruct strvec replacement_oids = STRVEC_INIT;\n+\tbool found = false;\n+\tsize_t i;\n+\n+\toid_to_hex_r(commit_to_replace_oid, commit_to_replace);\n+\tfor (i = 0; i < replacements_nr; i++)\n+\t\tstrvec_push(&replacement_oids, oid_to_hex(&replacements[i]));\n+\n+\tfor (i = 0; i < commits->nr; i++) {\n+\t\tif (strcmp(commits->v[i], commit_to_replace_oid))\n+\t\t\tcontinue;\n+\t\tstrvec_splice(commits, i, 1, replacement_oids.v, replacement_oids.nr);\n+\t\tfound = true;\n+\t\tbreak;\n+\t}\n+\tif (!found)\n+\t\tBUG(\"could not find commit to replace\");\n+\n+\tstrvec_clear(&replacement_oids);\n+}\n+\n static int apply_commits(struct repository *repo,\n \t\t\t const struct strvec *commits,\n \t\t\t struct commit *head,\n@@ -389,6 +416,107 @@ static int cmd_history_drop(int argc,\n \treturn ret;\n }\n \n+static int cmd_history_reorder(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\tN_(\"git history reorder <commit> (--before=<following-commit>|--after=<preceding-commit>)\"),\n+\t\tNULL,\n+\t};\n+\tconst char *before = NULL, *after = NULL;\n+\tstruct option options[] = {\n+\t\tOPT_STRING(0, \"before\", &before, N_(\"commit\"), N_(\"reorder before this commit\")),\n+\t\tOPT_STRING(0, \"after\", &after, N_(\"commit\"), N_(\"reorder after this commit\")),\n+\t\tOPT_END(),\n+\t};\n+\tstruct commit *commit_to_reorder, *head, *anchor, *old;\n+\tstruct strvec commits = STRVEC_INIT;\n+\tstruct object_id replacement[2];\n+\tstruct commit_list *list = NULL;\n+\tint ret;\n+\n+\targc = parse_options(argc, argv, prefix, options, usage, 0);\n+\tif (argc != 1)\n+\t\tdie(_(\"command expects a single revision\"));\n+\tif (!before && !after)\n+\t\tdie(_(\"exactly one option of 'before' or 'after' must be given\"));\n+\tdie_for_incompatible_opt2(!!before, \"before\", !!after, \"after\");\n+\n+\trepo_config(repo, git_default_config, NULL);\n+\n+\tcommit_to_reorder = lookup_commit_reference_by_name(argv[0]);\n+\tif (!commit_to_reorder)\n+\t\tdie(_(\"commit to be reordered cannot be found: %s\"), argv[0]);\n+\tif (commit_to_reorder->parents && commit_to_reorder->parents->next)\n+\t\tdie(_(\"commit to be reordered must not be a merge commit\"));\n+\n+\tanchor = lookup_commit_reference_by_name(before ? before : after);\n+\tif (!commit_to_reorder)\n+\t\tdie(_(\"anchor commit cannot be found: %s\"), before ? before : after);\n+\n+\tif (oideq(&commit_to_reorder->object.oid, &anchor->object.oid))\n+\t\tdie(_(\"commit to reorder and anchor must not be the same\"));\n+\n+\thead = lookup_commit_reference_by_name(\"HEAD\");\n+\tif (!head)\n+\t\tdie(_(\"could not resolve HEAD to a commit\"));\n+\n+\tcommit_list_append(commit_to_reorder, &list);\n+\tif (!repo_is_descendant_of(repo, commit_to_reorder, list))\n+\t\tdie(_(\"reordered commit must be reachable from current HEAD commit\"));\n+\n+\t/*\n+\t * There is no requirement for the user to have either one of the\n+\t * provided commits be the parent or child. We thus have to figure out\n+\t * ourselves which one is which.\n+\t*/\n+\tif (repo_is_descendant_of(repo, anchor, list))\n+\t\told = commit_to_reorder;\n+\telse\n+\t\told = anchor;\n+\n+\t/*\n+\t * Select the whole range of commits, including the boundary commit\n+\t * itself. In case the old commit is the root commit we simply pass no\n+\t * boundary.\n+\t*/\n+\tret = collect_commits(repo, old->parents ? old->parents->item : NULL,\n+\t\t\t      head, &commits);\n+\tif (ret < 0)\n+\t\tgoto out;\n+\n+\t/*\n+\t * Perform the reordering of commits in the strvec. This is done by:\n+\t *\n+\t *   - Deleting the to-be-reordered commit from the range of commits.\n+\t *\n+\t *   - Replacing the anchor commit with the anchor commit plus the\n+\t *     to-be-reordered commit.\n+\t */\n+\tif (before) {\n+\t\treplacement[0] = commit_to_reorder->object.oid;\n+\t\treplacement[1] = anchor->object.oid;\n+\t} else {\n+\t\treplacement[0] = anchor->object.oid;\n+\t\treplacement[1] = commit_to_reorder->object.oid;\n+\t}\n+\treplace_commits(&commits, &commit_to_reorder->object.oid, NULL, 0);\n+\treplace_commits(&commits, &anchor->object.oid, replacement, ARRAY_SIZE(replacement));\n+\n+\tret = apply_commits(repo, &commits, head, old, \"reorder\");\n+\tif (ret < 0)\n+\t\tgoto out;\n+\n+\tret = 0;\n+\n+out:\n+\tfree_commit_list(list);\n+\tstrvec_clear(&commits);\n+\treturn ret;\n+}\n+\n int cmd_history(int argc,\n \t\tconst char **argv,\n \t\tconst char *prefix,\n@@ -399,6 +527,7 @@ int cmd_history(int argc,\n \t\tN_(\"git history continue\"),\n \t\tN_(\"git history quit\"),\n \t\tN_(\"git history drop <commit>\"),\n+\t\tN_(\"git history reorder <commit> (--before=<following-commit>|--after=<preceding-commit>)\"),\n \t\tNULL,\n \t};\n \tparse_opt_subcommand_fn *fn = NULL;\n@@ -407,6 +536,7 @@ int cmd_history(int argc,\n \t\tOPT_SUBCOMMAND(\"continue\", &fn, cmd_history_continue),\n \t\tOPT_SUBCOMMAND(\"quit\", &fn, cmd_history_quit),\n \t\tOPT_SUBCOMMAND(\"drop\", &fn, cmd_history_drop),\n+\t\tOPT_SUBCOMMAND(\"reorder\", &fn, cmd_history_reorder),\n \t\tOPT_END(),\n \t};\n \ndiff --git a/t/meson.build b/t/meson.build\nindex 8189c6c561..2bf7bcab5a 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -378,6 +378,7 @@ integration_tests = [\n   't3438-rebase-broken-files.sh',\n   't3450-history.sh',\n   't3451-history-drop.sh',\n+  't3452-history-reorder.sh',\n   't3500-cherry.sh',\n   't3501-revert-cherry-pick.sh',\n   't3502-cherry-pick-merge.sh',\ndiff --git a/t/t3452-history-reorder.sh b/t/t3452-history-reorder.sh\nnew file mode 100755\nindex 0000000000..2e9d64a9fd\n--- /dev/null\n+++ b/t/t3452-history-reorder.sh\n@@ -0,0 +1,278 @@\n+#!/bin/sh\n+\n+test_description='tests for git-history reorder subcommand'\n+\n+. ./test-lib.sh\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 reorder HEAD --before=HEAD~ 2>err &&\n+\t\ttest_grep \"commit to be reordered must not be a merge commit\" err &&\n+\t\ttest_must_fail git history reorder HEAD~ --after=HEAD 2>err &&\n+\t\ttest_grep \"cannot rearrange commit history with merges\" err\n+\t)\n+'\n+\n+test_expect_success 'refuses to work with changes in the worktree or index' '\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_commit file file &&\n+\t\techo foo >file &&\n+\t\ttest_must_fail git history reorder HEAD --before=HEAD~ 2>err &&\n+\t\ttest_grep \"Your local changes to the following files would be overwritten\" err &&\n+\t\tgit add file &&\n+\t\ttest_must_fail git history reorder HEAD --before=HEAD~ 2>err &&\n+\t\ttest_grep \"Your local changes to the following files would be overwritten\" err\n+\t)\n+'\n+\n+test_expect_success 'requires exactly one of --before or --after' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit first &&\n+\t\ttest_commit second &&\n+\t\ttest_must_fail git history reorder HEAD 2>err &&\n+\t\ttest_grep \"exactly one option of ${SQ}before${SQ} or ${SQ}after${SQ} must be given\" err &&\n+\t\ttest_must_fail git history reorder HEAD --before=a --after=b 2>err &&\n+\t\ttest_grep \"options ${SQ}before${SQ} and ${SQ}after${SQ} cannot be used together\" err\n+\t)\n+'\n+\n+test_expect_success 'refuses to reorder commit with itself' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit first &&\n+\t\ttest_commit second &&\n+\t\ttest_must_fail git history reorder HEAD --after=HEAD 2>err &&\n+\t\ttest_grep \"commit to reorder and anchor must not be the same\" err\n+\t)\n+'\n+\n+test_expect_success '--before can move commit back in history' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit first &&\n+\t\ttest_commit second &&\n+\t\ttest_commit third &&\n+\t\ttest_commit fourth &&\n+\t\ttest_commit fifth &&\n+\t\tgit history reorder :/fourth --before=:/second &&\n+\t\tcat >expect <<-EOF &&\n+\t\tfifth\n+\t\tthird\n+\t\tsecond\n+\t\tfourth\n+\t\tfirst\n+\t\tEOF\n+\t\tgit log --format=%s >actual &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+test_expect_success '--before can move commit forward in history' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit first &&\n+\t\ttest_commit second &&\n+\t\ttest_commit third &&\n+\t\ttest_commit fourth &&\n+\t\ttest_commit fifth &&\n+\t\tgit history reorder :/second --before=:/fourth &&\n+\t\tcat >expect <<-EOF &&\n+\t\tfifth\n+\t\tfourth\n+\t\tsecond\n+\t\tthird\n+\t\tfirst\n+\t\tEOF\n+\t\tgit log --format=%s >actual &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+test_expect_success '--before can make a commit a root commit' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit first &&\n+\t\ttest_commit second &&\n+\t\ttest_commit third &&\n+\t\tgit history reorder :/third --before=:/first &&\n+\t\tcat >expect <<-EOF &&\n+\t\tsecond\n+\t\tfirst\n+\t\tthird\n+\t\tEOF\n+\t\tgit log --format=%s >actual &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+test_expect_success '--after can move commit back in history' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit first &&\n+\t\ttest_commit second &&\n+\t\ttest_commit third &&\n+\t\ttest_commit fourth &&\n+\t\ttest_commit fifth &&\n+\t\tgit history reorder :/fourth --after=:/second &&\n+\t\tcat >expect <<-EOF &&\n+\t\tfifth\n+\t\tthird\n+\t\tfourth\n+\t\tsecond\n+\t\tfirst\n+\t\tEOF\n+\t\tgit log --format=%s >actual &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+test_expect_success '--after can move commit forward in history' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit first &&\n+\t\ttest_commit second &&\n+\t\ttest_commit third &&\n+\t\ttest_commit fourth &&\n+\t\ttest_commit fifth &&\n+\t\tgit history reorder :/second --after=:/fourth &&\n+\t\tcat >expect <<-EOF &&\n+\t\tfifth\n+\t\tsecond\n+\t\tfourth\n+\t\tthird\n+\t\tfirst\n+\t\tEOF\n+\t\tgit log --format=%s >actual &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+test_expect_success '--after can make commit the tip' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit first &&\n+\t\ttest_commit second &&\n+\t\ttest_commit third &&\n+\t\tgit history reorder :/first --after=:/third &&\n+\t\tcat >expect <<-EOF &&\n+\t\tfirst\n+\t\tthird\n+\t\tsecond\n+\t\tEOF\n+\t\tgit log --format=%s >actual &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+test_expect_success 'hooks are executed for rewritten commits' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit first &&\n+\t\ttest_commit second &&\n+\t\ttest_commit third &&\n+\n+\t\twrite_script .git/hooks/prepare-commit-msg <<-EOF &&\n+\t\techo \"prepare-commit-msg: \\$@\" >>\"$(pwd)/hooks.log\"\n+\t\tEOF\n+\t\twrite_script .git/hooks/post-commit <<-EOF &&\n+\t\techo \"post-commit\" >>\"$(pwd)/hooks.log\"\n+\t\tEOF\n+\t\twrite_script .git/hooks/post-rewrite <<-EOF &&\n+\t\t{\n+\t\t\techo \"post-rewrite: \\$@\"\n+\t\t\tcat\n+\t\t} >>\"$(pwd)/hooks.log\"\n+\t\tEOF\n+\n+\t\tgit history reorder :/third --before=:/second &&\n+\t\tcat >expect <<-EOF &&\n+\t\tsecond\n+\t\tthird\n+\t\tfirst\n+\t\tEOF\n+\t\tgit log --format=%s >actual &&\n+\t\ttest_cmp expect actual &&\n+\n+\t\tcat >expect <<-EOF &&\n+\t\tprepare-commit-msg: .git/COMMIT_EDITMSG message\n+\t\tpost-commit\n+\t\tprepare-commit-msg: .git/COMMIT_EDITMSG message\n+\t\tpost-commit\n+\t\tpost-rewrite: history\n+\t\t$(git rev-parse third) $(git rev-parse HEAD~)\n+\t\t$(git rev-parse second) $(git rev-parse HEAD)\n+\t\tEOF\n+\t\ttest_cmp expect hooks.log\n+\t)\n+'\n+\n+test_expect_success 'conflicts are detected' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\techo base >file &&\n+\t\tgit add file &&\n+\t\tgit commit -m base &&\n+\t\techo \"first edit\" >file &&\n+\t\tgit commit -am \"first edit\" &&\n+\t\techo \"second edit\" >file &&\n+\t\tgit commit -am \"second edit\" &&\n+\n+\t\tgit symbolic-ref HEAD >expect-head &&\n+\t\ttest_must_fail git history reorder HEAD --before=HEAD~ &&\n+\t\ttest_must_fail git symbolic-ref HEAD &&\n+\t\techo \"second edit\" >file &&\n+\t\tgit add file &&\n+\t\ttest_must_fail git history continue &&\n+\t\techo \"first edit\" >file &&\n+\t\tgit add file &&\n+\t\tgit history continue &&\n+\n+\t\tcat >expect <<-EOF &&\n+\t\tfirst edit\n+\t\tsecond edit\n+\t\tbase\n+\t\tEOF\n+\t\tgit log --format=%s >actual &&\n+\t\ttest_cmp expect actual &&\n+\n+\t\tgit symbolic-ref HEAD >actual-head &&\n+\t\ttest_cmp expect-head actual-head\n+\t)\n+'\n+\n+test_done\n\n-- \n2.51.0.417.g1ba7204a04.dirty\n\n"},{"id":"525535","messageId":"20250904-b4-pks-history-builtin-v3-11-509053514755@pks.im","threadId":"63986","inReplyTo":"20250904-b4-pks-history-builtin-v3-0-509053514755@pks.im","subject":"[PATCH RFC v3 11/18] add-patch: split out header from \"add-interactive.h\"","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-09-04T14:27:41Z","receivedAt":"2025-09-04T14:28:13Z","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 | 23 +++--------------------\n add-patch.c       |  1 +\n add-patch.h       | 26 ++++++++++++++++++++++++++\n 3 files changed, 30 insertions(+), 20 deletions(-)\n\ndiff --git a/add-interactive.h b/add-interactive.h\nindex 4213dcd67b..fb95b6ee05 100644\n--- a/add-interactive.h\n+++ b/add-interactive.h\n@@ -1,14 +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-};\n-\n-#define ADD_P_OPT_INIT { .context = -1, .interhunkcontext = -1 }\n+struct pathspec;\n+struct repository;\n \n struct add_i_state {\n \tstruct repository *r;\n@@ -32,21 +29,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 302e6ba7d9..e2b002fa73 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..4394c74107\n--- /dev/null\n+++ b/add-patch.h\n@@ -0,0 +1,26 @@\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+};\n+\n+#define ADD_P_OPT_INIT { .context = -1, .interhunkcontext = -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.51.0.417.g1ba7204a04.dirty\n\n"},{"id":"525536","messageId":"20250904-b4-pks-history-builtin-v3-12-509053514755@pks.im","threadId":"63986","inReplyTo":"20250904-b4-pks-history-builtin-v3-0-509053514755@pks.im","subject":"[PATCH RFC v3 12/18] add-patch: split out `struct interactive_options`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-09-04T14:27:42Z","receivedAt":"2025-09-04T14:28:16Z","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 the infra for \"git add -p\"\nand \"git add -i\". Users of `run_add_i()` for example are expected to\npass `struct add_p_opt`. This is somewhat confusing and raises the\nquestion 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  | 151 +++++++++++++----------------------------------------\n add-interactive.h  |  20 ++-----\n add-patch.c        | 145 +++++++++++++++++++++++++++++++++++++++++---------\n add-patch.h        |  33 ++++++++++--\n builtin/add.c      |  22 ++++----\n builtin/checkout.c |   4 +-\n builtin/commit.c   |  16 +++---\n builtin/reset.c    |  16 +++---\n builtin/stash.c    |  46 ++++++++--------\n commit.h           |   2 +-\n 10 files changed, 243 insertions(+), 212 deletions(-)\n\ndiff --git a/add-interactive.c b/add-interactive.c\nindex 3e692b47ec..3babc3e013 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,96 +19,18 @@\n #include \"prompt.h\"\n #include \"tree.h\"\n \n-static void init_color(struct repository *r, struct add_i_state *s,\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 (!s->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 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-\tconst char *value;\n-\n \ts->r = r;\n-\ts->context = -1;\n-\ts->interhunkcontext = -1;\n-\n-\tif (repo_config_get_value(r, \"color.interactive\", &value))\n-\t\ts->use_color = -1;\n-\telse\n-\t\ts->use_color =\n-\t\t\tgit_config_colorbool(\"color.interactive\", value);\n-\ts->use_color = want_color(s->use_color);\n-\n-\tinit_color(r, s, \"interactive.header\", s->header_color, GIT_COLOR_BOLD);\n-\tinit_color(r, s, \"interactive.help\", s->help_color, GIT_COLOR_BOLD_RED);\n-\tinit_color(r, s, \"interactive.prompt\", s->prompt_color,\n-\t\t   GIT_COLOR_BOLD_BLUE);\n-\tinit_color(r, s, \"interactive.error\", s->error_color,\n-\t\t   GIT_COLOR_BOLD_RED);\n-\n-\tinit_color(r, s, \"diff.frag\", s->fraginfo_color,\n-\t\t   diff_get_color(s->use_color, DIFF_FRAGINFO));\n-\tinit_color(r, s, \"diff.context\", s->context_color, \"fall back\");\n-\tif (!strcmp(s->context_color, \"fall back\"))\n-\t\tinit_color(r, s, \"diff.plain\", s->context_color,\n-\t\t\t   diff_get_color(s->use_color, DIFF_CONTEXT));\n-\tinit_color(r, s, \"diff.old\", s->file_old_color,\n-\t\tdiff_get_color(s->use_color, DIFF_FILE_OLD));\n-\tinit_color(r, s, \"diff.new\", s->file_new_color,\n-\t\tdiff_get_color(s->use_color, DIFF_FILE_NEW));\n-\n-\tstrlcpy(s->reset_color,\n-\t\ts->use_color ? 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 = -1;\n+\tinteractive_config_clear(&s->cfg);\n }\n \n /*\n@@ -262,7 +183,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@@ -330,7 +251,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@@ -408,7 +329,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@@ -968,7 +889,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@@ -990,9 +911,9 @@ 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\tstruct interactive_options opts = {\n+\t\t\t.context = s->cfg.context,\n+\t\t\t.interhunkcontext = s->cfg.interhunkcontext,\n \t\t};\n \t\tstruct strvec args = STRVEC_INIT;\n \t\tstruct pathspec ps_selected = { 0 };\n@@ -1004,7 +925,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@@ -1040,10 +961,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@@ -1061,17 +982,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@@ -1079,21 +1000,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@@ -1128,7 +1049,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@@ -1139,7 +1060,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@@ -1182,15 +1103,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 (s.use_color) {\n-\t\tdata.color = s.prompt_color;\n-\t\tdata.reset = s.reset_color;\n+\tif (s.cfg.use_color) {\n+\t\tdata.color = s.cfg.prompt_color;\n+\t\tdata.reset = s.cfg.reset_color;\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 fb95b6ee05..eefa2edc7c 100644\n--- a/add-interactive.h\n+++ b/add-interactive.h\n@@ -2,34 +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-\tint use_color;\n-\tchar header_color[COLOR_MAXLEN];\n-\tchar help_color[COLOR_MAXLEN];\n-\tchar prompt_color[COLOR_MAXLEN];\n-\tchar error_color[COLOR_MAXLEN];\n-\tchar reset_color[COLOR_MAXLEN];\n-\tchar fraginfo_color[COLOR_MAXLEN];\n-\tchar context_color[COLOR_MAXLEN];\n-\tchar file_old_color[COLOR_MAXLEN];\n-\tchar file_new_color[COLOR_MAXLEN];\n-\n-\tint use_single_key;\n-\tchar *interactive_diff_filter, *interactive_diff_algorithm;\n-\tint context, interhunkcontext;\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 e2b002fa73..45bc254e0c 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,99 @@ struct add_p_state {\n \tconst char *revision;\n };\n \n+static void init_color(struct repository *r,\n+\t\t       struct interactive_config *cfg,\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 (!cfg->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+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+\tconst char *value;\n+\n+\tcfg->context = -1;\n+\tcfg->interhunkcontext = -1;\n+\n+\tif (repo_config_get_value(r, \"color.interactive\", &value))\n+\t\tcfg->use_color = -1;\n+\telse\n+\t\tcfg->use_color =\n+\t\t\tgit_config_colorbool(\"color.interactive\", value);\n+\tcfg->use_color = want_color(cfg->use_color);\n+\n+\tinit_color(r, cfg, \"interactive.header\", cfg->header_color, GIT_COLOR_BOLD);\n+\tinit_color(r, cfg, \"interactive.help\", cfg->help_color, GIT_COLOR_BOLD_RED);\n+\tinit_color(r, cfg, \"interactive.prompt\", cfg->prompt_color,\n+\t\t   GIT_COLOR_BOLD_BLUE);\n+\tinit_color(r, cfg, \"interactive.error\", cfg->error_color,\n+\t\t   GIT_COLOR_BOLD_RED);\n+\n+\tinit_color(r, cfg, \"diff.frag\", cfg->fraginfo_color,\n+\t\t   diff_get_color(cfg->use_color, DIFF_FRAGINFO));\n+\tinit_color(r, cfg, \"diff.context\", cfg->context_color, \"fall back\");\n+\tif (!strcmp(cfg->context_color, \"fall back\"))\n+\t\tinit_color(r, cfg, \"diff.plain\", cfg->context_color,\n+\t\t\t   diff_get_color(cfg->use_color, DIFF_CONTEXT));\n+\tinit_color(r, cfg, \"diff.old\", cfg->file_old_color,\n+\t\tdiff_get_color(cfg->use_color, DIFF_FILE_OLD));\n+\tinit_color(r, cfg, \"diff.new\", cfg->file_new_color,\n+\t\tdiff_get_color(cfg->use_color, DIFF_FILE_NEW));\n+\n+\tstrlcpy(cfg->reset_color,\n+\t\tcfg->use_color ? 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 = -1;\n+}\n+\n static void add_p_state_clear(struct add_p_state *s)\n {\n \tsize_t i;\n@@ -299,9 +394,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);\n+\tputs(s->s.cfg.reset_color);\n \tva_end(args);\n }\n \n@@ -424,12 +519,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@@ -460,7 +555,7 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps)\n \n \tif (want_color_fd(1, -1)) {\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 +788,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 +810,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);\n+\t\t\tstrbuf_addf(out, \"%s\\n\", s->s.cfg.reset_color);\n \t\telse\n \t\t\tstrbuf_addch(out, '\\n');\n \t}\n@@ -1103,12 +1198,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);\n+\t\tstrbuf_addstr(&s->colored, s->s.cfg.reset_color);\n \t\tif (next > eol)\n \t\t\tstrbuf_add(&s->colored, plain + eol, next - eol);\n \t\tcurrent = next;\n@@ -1227,7 +1322,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@@ -1241,7 +1336,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@@ -1522,15 +1617,15 @@ static int patch_update_file(struct add_p_state *s,\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 \t\t\t\t\t\t: 1));\n \t\tprintf(_(s->mode->prompt_mode[prompt_mode_type]),\n \t\t       s->buf.buf);\n-\t\tif (*s->s.reset_color)\n-\t\t\tfputs(s->s.reset_color, stdout);\n+\t\tif (*s->s.cfg.reset_color)\n+\t\t\tfputs(s->s.cfg.reset_color, stdout);\n \t\tfflush(stdout);\n \t\tif (read_single_character(s) == EOF)\n \t\t\tbreak;\n@@ -1687,7 +1782,7 @@ static int patch_update_file(struct add_p_state *s,\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@@ -1705,7 +1800,7 @@ static int patch_update_file(struct add_p_state *s,\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@@ -1723,7 +1818,7 @@ static int patch_update_file(struct add_p_state *s,\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@@ -1764,7 +1859,7 @@ static int patch_update_file(struct add_p_state *s,\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@@ -1772,7 +1867,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;\ndiff --git a/add-patch.h b/add-patch.h\nindex 4394c74107..51c0d7bce9 100644\n--- a/add-patch.h\n+++ b/add-patch.h\n@@ -1,15 +1,42 @@\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 };\n \n-#define ADD_P_OPT_INIT { .context = -1, .interhunkcontext = -1 }\n+#define INTERACTIVE_OPTIONS_INIT { \\\n+\t.context = -1, \\\n+\t.interhunkcontext = -1, \\\n+}\n+\n+struct interactive_config {\n+\tint use_color;\n+\tchar header_color[COLOR_MAXLEN];\n+\tchar help_color[COLOR_MAXLEN];\n+\tchar prompt_color[COLOR_MAXLEN];\n+\tchar error_color[COLOR_MAXLEN];\n+\tchar reset_color[COLOR_MAXLEN];\n+\tchar fraginfo_color[COLOR_MAXLEN];\n+\tchar context_color[COLOR_MAXLEN];\n+\tchar file_old_color[COLOR_MAXLEN];\n+\tchar file_new_color[COLOR_MAXLEN];\n+\n+\tint use_single_key;\n+\tchar *interactive_diff_filter, *interactive_diff_algorithm;\n+\tint context, interhunkcontext;\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@@ -20,7 +47,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 0235854f80..a94c826c14 100644\n--- a/builtin/add.c\n+++ b/builtin/add.c\n@@ -30,7 +30,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@@ -159,7 +159,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@@ -171,9 +171,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@@ -255,8 +255,8 @@ 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_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@@ -398,9 +398,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@@ -410,11 +410,11 @@ 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}\n \ndiff --git a/builtin/checkout.c b/builtin/checkout.c\nindex 43583c8d1b..0b90f398fe 100644\n--- a/builtin/checkout.c\n+++ b/builtin/checkout.c\n@@ -546,7 +546,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};\n@@ -575,7 +575,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 b5b9608813..767351fd87 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@@ -356,9 +356,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@@ -407,7 +407,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@@ -432,9 +432,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@@ -1738,8 +1738,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 ed35802af1..088449e120 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,8 +371,8 @@ 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_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@@ -423,9 +423,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@@ -436,12 +436,12 @@ 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}\n \ndiff --git a/builtin/stash.c b/builtin/stash.c\nindex 1977e50df2..5070b8a88f 100644\n--- a/builtin/stash.c\n+++ b/builtin/stash.c\n@@ -1302,7 +1302,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@@ -1327,7 +1327,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@@ -1422,7 +1422,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@@ -1504,7 +1505,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@@ -1590,7 +1591,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@@ -1662,7 +1664,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@@ -1835,7 +1837,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@@ -1843,8 +1845,8 @@ 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_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@@ -1901,19 +1903,19 @@ 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}\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@@ -1938,7 +1940,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@@ -1946,8 +1948,8 @@ 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_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@@ -1967,20 +1969,20 @@ 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}\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 1d6e0c7518..7b6e59d6c1 100644\n--- a/commit.h\n+++ b/commit.h\n@@ -258,7 +258,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.51.0.417.g1ba7204a04.dirty\n\n"},{"id":"525537","messageId":"20250904-b4-pks-history-builtin-v3-13-509053514755@pks.im","threadId":"63986","inReplyTo":"20250904-b4-pks-history-builtin-v3-0-509053514755@pks.im","subject":"[PATCH RFC v3 13/18] add-patch: remove dependency on \"add-interactive\" subsystem","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-09-04T14:27:43Z","receivedAt":"2025-09-04T14:28:19Z","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 | 68 ++++++++++++++++++++++++++++++++-----------------------------\n 1 file changed, 36 insertions(+), 32 deletions(-)\n\ndiff --git a/add-patch.c b/add-patch.c\nindex 45bc254e0c..1bcbc91de9 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@@ -385,7 +385,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@@ -394,9 +394,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);\n+\tputs(s->cfg.reset_color);\n \tva_end(args);\n }\n \n@@ -414,7 +414,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@@ -519,12 +519,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@@ -555,7 +555,7 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps)\n \n \tif (want_color_fd(1, -1)) {\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@@ -788,7 +788,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@@ -810,7 +810,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);\n+\t\t\tstrbuf_addf(out, \"%s\\n\", s->cfg.reset_color);\n \t\telse\n \t\t\tstrbuf_addch(out, '\\n');\n \t}\n@@ -1198,12 +1198,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);\n+\t\tstrbuf_addstr(&s->colored, s->cfg.reset_color);\n \t\tif (next > eol)\n \t\t\tstrbuf_add(&s->colored, plain + eol, next - eol);\n \t\tcurrent = next;\n@@ -1322,7 +1322,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@@ -1336,7 +1336,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@@ -1617,15 +1617,15 @@ static int patch_update_file(struct add_p_state *s,\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 \t\t\t\t\t\t: 1));\n \t\tprintf(_(s->mode->prompt_mode[prompt_mode_type]),\n \t\t       s->buf.buf);\n-\t\tif (*s->s.cfg.reset_color)\n-\t\t\tfputs(s->s.cfg.reset_color, stdout);\n+\t\tif (*s->cfg.reset_color)\n+\t\t\tfputs(s->cfg.reset_color, stdout);\n \t\tfflush(stdout);\n \t\tif (read_single_character(s) == EOF)\n \t\t\tbreak;\n@@ -1782,7 +1782,7 @@ static int patch_update_file(struct add_p_state *s,\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@@ -1800,7 +1800,7 @@ static int patch_update_file(struct add_p_state *s,\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@@ -1818,7 +1818,7 @@ static int patch_update_file(struct add_p_state *s,\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@@ -1838,7 +1838,7 @@ static int patch_update_file(struct add_p_state *s,\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\t   s->mode->is_reverse);\n@@ -1849,8 +1849,8 @@ static int patch_update_file(struct add_p_state *s,\n \t\t\t\t\t NULL, 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\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@@ -1863,11 +1863,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\n-- \n2.51.0.417.g1ba7204a04.dirty\n\n"},{"id":"525538","messageId":"20250904-b4-pks-history-builtin-v3-14-509053514755@pks.im","threadId":"63986","inReplyTo":"20250904-b4-pks-history-builtin-v3-0-509053514755@pks.im","subject":"[PATCH RFC v3 14/18] add-patch: add support for in-memory index patching","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-09-04T14:27:44Z","receivedAt":"2025-09-04T14:28:23Z","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,\nworktree 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 | 110 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--\n add-patch.h |   8 +++++\n 2 files changed, 115 insertions(+), 3 deletions(-)\n\ndiff --git a/add-patch.c b/add-patch.c\nindex 1bcbc91de9..2a72c7b931 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@@ -414,7 +418,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@@ -1838,7 +1842,7 @@ static int patch_update_file(struct add_p_state *s,\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\t   s->mode->is_reverse);\n@@ -1849,9 +1853,11 @@ static int patch_update_file(struct add_p_state *s,\n \t\t\t\t\t NULL, 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 \tputchar('\\n');\n@@ -1864,6 +1870,8 @@ int run_add_p(struct repository *r, enum add_p_mode mode,\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@@ -1922,3 +1930,99 @@ int run_add_p(struct repository *r, enum add_p_mode mode,\n \tadd_p_state_clear(&s);\n \treturn 0;\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+\tstruct strbuf parent_revision = STRBUF_INIT;\n+\tchar parent_tree_oid[GIT_MAX_HEXSZ + 1];\n+\tsize_t binary_count = 0;\n+\tstruct commit *commit;\n+\tint ret;\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+\tstrbuf_addf(&parent_revision, \"%s~\", revision);\n+\tmode.diff_cmd[0] = \"diff-tree\";\n+\tmode.diff_cmd[1] = \"-r\";\n+\tmode.diff_cmd[2] = parent_tree_oid;\n+\n+\tinteractive_config_init(&s.cfg, r, opts);\n+\n+\tif (parse_diff(&s, ps) < 0) {\n+\t\tret = -1;\n+\t\tgoto out;\n+\t}\n+\n+\tfor (size_t i = 0; i < s.file_diff_nr; i++) {\n+\t\tif (s.file_diff[i].binary && !s.file_diff[i].hunk_nr)\n+\t\t\tbinary_count++;\n+\t\telse if (patch_update_file(&s, s.file_diff + i))\n+\t\t\tbreak;\n+\t}\n+\n+\tif (s.file_diff_nr == 0) {\n+\t\terr(&s, _(\"No changes.\"));\n+\t\tret = -1;\n+\t\tgoto out;\n+\t}\n+\n+\tif (binary_count == s.file_diff_nr) {\n+\t\terr(&s, _(\"Only binary files changed.\"));\n+\t\tret = -1;\n+\t\tgoto out;\n+\t}\n+\n+\tret = 0;\n+\n+out:\n+\tstrbuf_release(&parent_revision);\n+\tadd_p_state_clear(&s);\n+\treturn ret;\n+}\ndiff --git a/add-patch.h b/add-patch.h\nindex 51c0d7bce9..d0edfec936 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@@ -50,4 +51,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.51.0.417.g1ba7204a04.dirty\n\n"},{"id":"525539","messageId":"20250904-b4-pks-history-builtin-v3-15-509053514755@pks.im","threadId":"63986","inReplyTo":"20250904-b4-pks-history-builtin-v3-0-509053514755@pks.im","subject":"[PATCH RFC v3 15/18] wt-status: provide function to expose status for trees","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-09-04T14:27:45Z","receivedAt":"2025-09-04T14:28:26Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The \"wt-status\" subsystem is responsible for printing status information\naround the current state of the working tree. This most importantly\nincludes information around whether the working tree or the index have\nany changes.\n\nWe're about to introduce a new command though where the changes in\nneither of them are actually relevant to us. Instead, what we want is to\nformat the changes between two different trees. While it is a little bit\nof a stretch to add this as functionality to _working tree_ status, it\ndoesn't make any sense to open-code this functionality, either.\n\nImplement a new function `wt_status_collect_changes_trees()` that diffs\ntwo trees and formats the status accordingly. This function is not yet\nused, but will be in a subsequent commit.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n wt-status.c | 24 ++++++++++++++++++++++++\n wt-status.h |  3 +++\n 2 files changed, 27 insertions(+)\n\ndiff --git a/wt-status.c b/wt-status.c\nindex 454601afa15..f09309d12e3 100644\n--- a/wt-status.c\n+++ b/wt-status.c\n@@ -612,6 +612,30 @@ static void wt_status_collect_updated_cb(struct diff_queue_struct *q,\n \t}\n }\n \n+void wt_status_collect_changes_trees(struct wt_status *s,\n+\t\t\t\t     const struct object_id *old_treeish,\n+\t\t\t\t     const struct object_id *new_treeish)\n+{\n+\tstruct diff_options opts = { 0 };\n+\n+\trepo_diff_setup(s->repo, &opts);\n+\topts.output_format = DIFF_FORMAT_CALLBACK;\n+\topts.format_callback = wt_status_collect_updated_cb;\n+\topts.format_callback_data = s;\n+\topts.detect_rename = s->detect_rename >= 0 ? s->detect_rename : opts.detect_rename;\n+\topts.rename_limit = s->rename_limit >= 0 ? s->rename_limit : opts.rename_limit;\n+\topts.rename_score = s->rename_score >= 0 ? s->rename_score : opts.rename_score;\n+\topts.flags.recursive = 1;\n+\tdiff_setup_done(&opts);\n+\n+\tdiff_tree_oid(old_treeish, new_treeish, \"\", &opts);\n+\tdiffcore_std(&opts);\n+\tdiff_flush(&opts);\n+\twt_status_get_state(s->repo, &s->state, 0);\n+\n+\tdiff_free(&opts);\n+}\n+\n static void wt_status_collect_changes_worktree(struct wt_status *s)\n {\n \tstruct rev_info rev;\ndiff --git a/wt-status.h b/wt-status.h\nindex 4e377ce62b8..b262e345f79 100644\n--- a/wt-status.h\n+++ b/wt-status.h\n@@ -153,6 +153,9 @@ void wt_status_add_cut_line(struct wt_status *s);\n void wt_status_prepare(struct repository *r, struct wt_status *s);\n void wt_status_print(struct wt_status *s);\n void wt_status_collect(struct wt_status *s);\n+void wt_status_collect_changes_trees(struct wt_status *s,\n+\t\t\t\t     const struct object_id *old_treeish,\n+\t\t\t\t     const struct object_id *new_treeish);\n /*\n  * Frees the buffers allocated by wt_status_collect.\n  */\n\n-- \n2.51.0.417.g1ba7204a04.dirty\n\n"},{"id":"525540","messageId":"20250904-b4-pks-history-builtin-v3-16-509053514755@pks.im","threadId":"63986","inReplyTo":"20250904-b4-pks-history-builtin-v3-0-509053514755@pks.im","subject":"[PATCH RFC v3 16/18] sequencer: allow callers to provide mappings for the old commit","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-09-04T14:27:46Z","receivedAt":"2025-09-04T14:28:29Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"When executing the \"rewritten-hook\" we provide a list of commit mappings\nthat tell the hook the original commit ID as well as the commit ID that\nspecific commit was rewritten to. Typically, these should always be\ndifferent from one another, as otherwise there wouldn't have been a\nrewrite of those commits in the first place.\n\nWith two upcoming subcommands for git-history(1) that is not the case\nthough, as we have already written the new commits ahead of time. We\nonly use the sequencer infrastructure in that case to insert those\ncommits at the correct position in the graph. This has the consequence\nthat original and rewritten object IDs will be the exact same, which is\nquite unhelpful.\n\nIntroduce infrastructure so that the caller can tell us the original\nobject ID for such already-rewritten objects.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n sequencer.c |  7 +++++++\n sequencer.h | 14 ++++++++++++++\n 2 files changed, 21 insertions(+)\n\ndiff --git a/sequencer.c b/sequencer.c\nindex 61447e5ccf..72d26b0eef 100644\n--- a/sequencer.c\n+++ b/sequencer.c\n@@ -2214,6 +2214,13 @@ static void record_in_rewritten(struct object_id *oid,\n \tconst char *path;\n \tFILE *out;\n \n+\tif (opts->old_oid_mappings) {\n+\t\tstruct replay_oid_mapping *mapping =\n+\t\t\toidmap_get(opts->old_oid_mappings, oid);\n+\t\tif (mapping)\n+\t\t\toid = &mapping->rewritten_oid;\n+\t}\n+\n \tif (opts->action == REPLAY_HISTORY_EDIT)\n \t\tpath = git_path_rewritten_pending_file();\n \telse\ndiff --git a/sequencer.h b/sequencer.h\nindex 0e0e7301b8..e6cc8aeb5d 100644\n--- a/sequencer.h\n+++ b/sequencer.h\n@@ -1,6 +1,7 @@\n #ifndef SEQUENCER_H\n #define SEQUENCER_H\n \n+#include \"oidmap.h\"\n #include \"strbuf.h\"\n #include \"strvec.h\"\n #include \"wt-status.h\"\n@@ -35,6 +36,12 @@ enum commit_msg_cleanup_mode {\n struct replay_ctx;\n struct replay_ctx* replay_ctx_new(void);\n \n+/* Used as entry for the `original_oid_map`. */\n+struct replay_oid_mapping {\n+\tstruct oidmap_entry entry;\n+\tstruct object_id rewritten_oid;\n+};\n+\n struct replay_opts {\n \tenum replay_action action;\n \n@@ -83,6 +90,13 @@ struct replay_opts {\n \t/* Only used by REPLAY_NONE */\n \tstruct rev_info *revs;\n \n+\t/*\n+\t * Used by the post-rewrite hook to fix up old object IDs. This can be\n+\t * used to rewrite the old object ID to whatever is stored as value in\n+\t * this map. The map contains `struct replay_oid_mapping` entries.\n+\t */\n+\tconst struct oidmap *old_oid_mappings;\n+\n \t/* Private use */\n \tstruct replay_ctx *ctx;\n };\n\n-- \n2.51.0.417.g1ba7204a04.dirty\n\n"},{"id":"525541","messageId":"20250904-b4-pks-history-builtin-v3-17-509053514755@pks.im","threadId":"63986","inReplyTo":"20250904-b4-pks-history-builtin-v3-0-509053514755@pks.im","subject":"[PATCH RFC v3 17/18] builtin/history: implement \"split\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-09-04T14:27:47Z","receivedAt":"2025-09-04T14:28:32Z","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 |  60 ++++++\n builtin/history.c              | 313 ++++++++++++++++++++++++++-\n t/meson.build                  |   1 +\n t/t3453-history-split.sh       | 468 +++++++++++++++++++++++++++++++++++++++++\n 4 files changed, 840 insertions(+), 2 deletions(-)\n\ndiff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc\nindex b36cd925dd..6f0c64b90e 100644\n--- a/Documentation/git-history.adoc\n+++ b/Documentation/git-history.adoc\n@@ -13,6 +13,7 @@ git history continue\n git history quit\n git history drop <commit>\n git history reorder <commit> (--before=<following-commit>|--after=<preceding-commit>)\n+git history split [<options>] <commit> [--] [<pathspec>...]\n \n DESCRIPTION\n -----------\n@@ -52,6 +53,26 @@ child commits, as that would lead to an empty branch.\n \tbe related to one another and must be reachable from the current `HEAD`\n \tcommit.\n \n+`split [--message=<message>] <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 message of the new commit will be asked for by launching the\n+configured editor, unless it has been specified with the `-m` option.\n+Authorship of the commit will be the same as for the 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 The following commands are used to manage an interrupted history-rewriting\n operation:\n \n@@ -111,6 +132,45 @@ f44a46e third\n bf7438d first\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 --message=\"split-out commit\"\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,e,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,e,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+\n CONFIGURATION\n -------------\n \ndiff --git a/builtin/history.c b/builtin/history.c\nindex 16b516856e..df04b8dfc6 100644\n--- a/builtin/history.c\n+++ b/builtin/history.c\n@@ -1,17 +1,27 @@\n+/* Required for `comment_line_str`. */\n+#define USE_THE_REPOSITORY_VARIABLE\n+\n #include \"builtin.h\"\n #include \"branch.h\"\n+#include \"cache-tree.h\"\n #include \"commit.h\"\n #include \"commit-reach.h\"\n #include \"config.h\"\n+#include \"editor.h\"\n #include \"environment.h\"\n #include \"gettext.h\"\n #include \"hex.h\"\n #include \"object-name.h\"\n #include \"parse-options.h\"\n+#include \"path.h\"\n+#include \"pathspec.h\"\n+#include \"read-cache-ll.h\"\n #include \"refs.h\"\n #include \"reset.h\"\n #include \"revision.h\"\n+#include \"run-command.h\"\n #include \"sequencer.h\"\n+#include \"sparse-index.h\"\n \n static int cmd_history_abort(int argc,\n \t\t\t     const char **argv,\n@@ -213,6 +223,7 @@ static int apply_commits(struct repository *repo,\n \t\t\t const struct strvec *commits,\n \t\t\t struct commit *head,\n \t\t\t struct commit *base,\n+\t\t\t const struct oidmap *rewritten_commits,\n \t\t\t const char *action)\n {\n \tstruct setup_revision_opt revision_opts = {\n@@ -238,6 +249,7 @@ static int apply_commits(struct repository *repo,\n \t\treplay_opts.strategy = replay_opts.default_strategy;\n \t\treplay_opts.default_strategy = NULL;\n \t}\n+\treplay_opts.old_oid_mappings = rewritten_commits;\n \n \tstrvec_push(&args, \"\");\n \tstrvec_pushv(&args, commits->v);\n@@ -403,7 +415,8 @@ static int cmd_history_drop(int argc,\n \t\tif (ret < 0)\n \t\t\tgoto out;\n \n-\t\tret = apply_commits(repo, &commits, head, commit_to_drop, \"drop\");\n+\t\tret = apply_commits(repo, &commits, head, commit_to_drop,\n+\t\t\t\t    NULL, \"drop\");\n \t\tif (ret < 0)\n \t\t\tgoto out;\n \t}\n@@ -505,7 +518,7 @@ static int cmd_history_reorder(int argc,\n \treplace_commits(&commits, &commit_to_reorder->object.oid, NULL, 0);\n \treplace_commits(&commits, &anchor->object.oid, replacement, ARRAY_SIZE(replacement));\n \n-\tret = apply_commits(repo, &commits, head, old, \"reorder\");\n+\tret = apply_commits(repo, &commits, head, old, NULL, \"reorder\");\n \tif (ret < 0)\n \t\tgoto out;\n \n@@ -517,6 +530,300 @@ static int cmd_history_reorder(int argc,\n \treturn ret;\n }\n \n+static void change_data_free(void *util, const char *str UNUSED)\n+{\n+\tstruct wt_status_change_data *d = util;\n+\tfree(d->rename_source);\n+\tfree(d);\n+}\n+\n+static int fill_commit_message(struct repository *repo,\n+\t\t\t       const struct object_id *old_tree,\n+\t\t\t       const struct object_id *new_tree,\n+\t\t\t       const char *default_message,\n+\t\t\t       const char *provided_message,\n+\t\t\t       const char *action,\n+\t\t\t       struct strbuf *out)\n+{\n+\tif (!provided_message) {\n+\t\tconst char *path = git_path_commit_editmsg();\n+\t\tconst char *hint =\n+\t\t\t_(\"Please enter the commit message for the %s changes. Lines starting\\n\"\n+\t\t\t  \"with '%s' will be kept; you may remove them yourself if you want to.\\n\");\n+\t\tint verbose = 1;\n+\n+\t\tstrbuf_addstr(out, default_message);\n+\t\tstrbuf_addch(out, '\\n');\n+\t\tstrbuf_commented_addf(out, comment_line_str, hint, action, comment_line_str);\n+\t\twrite_file_buf(path, out->buf, out->len);\n+\n+\t\trepo_config_get_bool(repo, \"commit.verbose\", &verbose);\n+\t\tif (verbose) {\n+\t\t\tstruct wt_status s;\n+\n+\t\t\twt_status_prepare(repo, &s);\n+\t\t\tFREE_AND_NULL(s.branch);\n+\t\t\ts.ahead_behind_flags = AHEAD_BEHIND_QUICK;\n+\t\t\ts.commit_template = 1;\n+\t\t\ts.colopts = 0;\n+\t\t\ts.display_comment_prefix = 1;\n+\t\t\ts.hints = 0;\n+\t\t\ts.use_color = 0;\n+\t\t\ts.whence = FROM_COMMIT;\n+\t\t\ts.committable = 1;\n+\n+\t\t\ts.fp = fopen(git_path_commit_editmsg(), \"a\");\n+\t\t\tif (!s.fp)\n+\t\t\t\treturn error_errno(_(\"could not open '%s'\"), git_path_commit_editmsg());\n+\n+\t\t\twt_status_collect_changes_trees(&s, old_tree, new_tree);\n+\t\t\twt_status_print(&s);\n+\t\t\twt_status_collect_free_buffers(&s);\n+\t\t\tstring_list_clear_func(&s.change, change_data_free);\n+\t\t}\n+\n+\t\tstrbuf_reset(out);\n+\t\tif (launch_editor(path, out, NULL)) {\n+\t\t\tfprintf(stderr, _(\"Please supply the message using the -m option.\\n\"));\n+\t\t\treturn -1;\n+\t\t}\n+\t\tstrbuf_stripspace(out, comment_line_str);\n+\t} else {\n+\t\tstrbuf_addstr(out, provided_message);\n+\t}\n+\n+\tcleanup_message(out, COMMIT_MSG_CLEANUP_ALL, 0);\n+\n+\tif (!out->len) {\n+\t\tfprintf(stderr, _(\"Aborting commit due to empty commit message.\\n\"));\n+\t\treturn -1;\n+\t}\n+\n+\treturn 0;\n+}\n+\n+static int split_commit(struct repository *repo,\n+\t\t\tstruct commit *original_commit,\n+\t\t\tstruct pathspec *pathspec,\n+\t\t\tconst char *commit_message,\n+\t\t\tstruct object_id *out)\n+{\n+\tstruct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;\n+\tstruct strbuf index_file = STRBUF_INIT, split_message = STRBUF_INIT;\n+\tstruct child_process read_tree_cmd = CHILD_PROCESS_INIT;\n+\tstruct index_state index = INDEX_STATE_INIT(repo);\n+\tstruct object_id original_commit_tree_oid, parent_tree_oid;\n+\tconst char *original_message, *original_body, *ptr;\n+\tchar original_commit_oid[GIT_MAX_HEXSZ + 1];\n+\tchar *original_author = NULL;\n+\tstruct commit_list *parents = NULL;\n+\tstruct commit *first_commit;\n+\tstruct tree *split_tree;\n+\tsize_t len;\n+\tint ret;\n+\n+\tif (original_commit->parents)\n+\t\tparent_tree_oid = *get_commit_tree_oid(original_commit->parents->item);\n+\telse\n+\t\toidcpy(&parent_tree_oid, repo->hash_algo->empty_tree);\n+\toriginal_commit_tree_oid = *get_commit_tree_oid(original_commit);\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+\tread_tree_cmd.git_cmd = 1;\n+\tstrvec_pushf(&read_tree_cmd.env, \"GIT_INDEX_FILE=%s\", index_file.buf);\n+\tstrvec_push(&read_tree_cmd.args, \"read-tree\");\n+\tstrvec_push(&read_tree_cmd.args, oid_to_hex(&parent_tree_oid));\n+\tret = run_command(&read_tree_cmd);\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_commit->object.oid);\n+\tret = run_add_p_index(repo, &index, index_file.buf, &interactive_opts,\n+\t\t\t      original_commit_oid, pathspec);\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+\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/* We retain authorship of the original commit. */\n+\toriginal_message = repo_logmsg_reencode(repo, original_commit, NULL, NULL);\n+\tptr = find_commit_header(original_message, \"author\", &len);\n+\tif (ptr)\n+\t\toriginal_author = xmemdupz(ptr, len);\n+\n+\tret = fill_commit_message(repo, &parent_tree_oid, &split_tree->object.oid,\n+\t\t\t\t  \"\", commit_message, \"split-out\", &split_message);\n+\tif (ret < 0)\n+\t\tgoto out;\n+\n+\tret = commit_tree(split_message.buf, split_message.len, &split_tree->object.oid,\n+\t\t\t  original_commit->parents, &out[0], original_author, NULL);\n+\tif (ret < 0) {\n+\t\tret = error(_(\"failed writing split-out commit\"));\n+\t\tgoto out;\n+\t}\n+\n+\t/*\n+\t * The second commit is much simpler to construct, as we can simply use\n+\t * the original commit details, except that we adjust its parent to be\n+\t * the newly split-out commit.\n+\t */\n+\tfind_commit_subject(original_message, &original_body);\n+\tfirst_commit = lookup_commit_reference(repo, &out[0]);\n+\tcommit_list_append(first_commit, &parents);\n+\n+\tret = commit_tree(original_body, strlen(original_body), &original_commit_tree_oid,\n+\t\t\t  parents, &out[1], original_author, NULL);\n+\tif (ret < 0) {\n+\t\tret = error(_(\"failed writing second commit\"));\n+\t\tgoto out;\n+\t}\n+\n+\tret = 0;\n+\n+out:\n+\tif (index_file.len)\n+\t\tunlink(index_file.buf);\n+\tstrbuf_release(&split_message);\n+\tstrbuf_release(&index_file);\n+\tfree_commit_list(parents);\n+\tfree(original_author);\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\tN_(\"git history split [<options>] <commit>\"),\n+\t\tNULL,\n+\t};\n+\tconst char *commit_message = NULL;\n+\tstruct option options[] = {\n+\t\tOPT_STRING('m', \"message\", &commit_message, N_(\"message\"), N_(\"commit message\")),\n+\t\tOPT_END(),\n+\t};\n+\tstruct oidmap rewritten_commits = OIDMAP_INIT;\n+\tstruct commit *original_commit, *head;\n+\tstruct strvec commits = STRVEC_INIT;\n+\tstruct commit_list *list = NULL;\n+\tstruct object_id split_commits[2];\n+\tstruct replay_oid_mapping mapping[2] = { 0 };\n+\tstruct pathspec pathspec = { 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 revision\"));\n+\t\tgoto out;\n+\t}\n+\trepo_config(repo, git_default_config, NULL);\n+\n+\toriginal_commit = lookup_commit_reference_by_name(argv[0]);\n+\tif (!original_commit) {\n+\t\tret = error(_(\"commit to be split cannot be found: %s\"), argv[0]);\n+\t\tgoto out;\n+\t}\n+\n+\tif (original_commit->parents && original_commit->parents->next) {\n+\t\tret = error(_(\"commit to be split must not be a merge commit\"));\n+\t\tgoto out;\n+\t}\n+\n+\thead = lookup_commit_reference_by_name(\"HEAD\");\n+\tif (!head) {\n+\t\tret = error(_(\"could not resolve HEAD to a commit\"));\n+\t\tgoto out;\n+\t}\n+\n+\tcommit_list_append(original_commit, &list);\n+\tif (!repo_is_descendant_of(repo, original_commit, list)) {\n+\t\tret = error (_(\"split commit must be reachable from current HEAD commit\"));\n+\t\tgoto out;\n+\t}\n+\n+\tparse_pathspec(&pathspec, 0,\n+\t\t       PATHSPEC_PREFER_FULL | PATHSPEC_SYMLINK_LEADING_PATH | PATHSPEC_PREFIX_ORIGIN,\n+\t\t       prefix, argv + 1);\n+\n+\t/*\n+\t * Collect the list of commits that we'll have to reapply now already.\n+\t * This ensures that we'll abort early on in case the range of commits\n+\t * contains merges, which we do not yet handle.\n+\t */\n+\tret = collect_commits(repo, original_commit->parents ? original_commit->parents->item : NULL,\n+\t\t\t      head, &commits);\n+\tif (ret < 0)\n+\t\tgoto out;\n+\n+\t/*\n+\t * Then we split up the commit and replace the original commit with the\n+\t * new new ones.\n+\t */\n+\tret = split_commit(repo, original_commit, &pathspec,\n+\t\t\t   commit_message, split_commits);\n+\tif (ret < 0)\n+\t\tgoto out;\n+\n+\tmapping[0].entry.oid = split_commits[0];\n+\tmapping[0].rewritten_oid = original_commit->object.oid;\n+\toidmap_put(&rewritten_commits, &mapping[0]);\n+\tmapping[1].entry.oid = split_commits[1];\n+\tmapping[1].rewritten_oid = original_commit->object.oid;\n+\toidmap_put(&rewritten_commits, &mapping[1]);\n+\n+\treplace_commits(&commits, &original_commit->object.oid,\n+\t\t\tsplit_commits, ARRAY_SIZE(split_commits));\n+\n+\tret = apply_commits(repo, &commits, head, original_commit,\n+\t\t\t    &rewritten_commits, \"split\");\n+\tif (ret < 0)\n+\t\tgoto out;\n+\n+\tret = 0;\n+\n+out:\n+\toidmap_clear(&rewritten_commits, 0);\n+\tclear_pathspec(&pathspec);\n+\tstrvec_clear(&commits);\n+\tfree_commit_list(list);\n+\treturn ret;\n+}\n+\n int cmd_history(int argc,\n \t\tconst char **argv,\n \t\tconst char *prefix,\n@@ -528,6 +835,7 @@ int cmd_history(int argc,\n \t\tN_(\"git history quit\"),\n \t\tN_(\"git history drop <commit>\"),\n \t\tN_(\"git history reorder <commit> (--before=<following-commit>|--after=<preceding-commit>)\"),\n+\t\tN_(\"git history split [<options>] <commit> [--] [<pathspec>...]\"),\n \t\tNULL,\n \t};\n \tparse_opt_subcommand_fn *fn = NULL;\n@@ -537,6 +845,7 @@ int cmd_history(int argc,\n \t\tOPT_SUBCOMMAND(\"quit\", &fn, cmd_history_quit),\n \t\tOPT_SUBCOMMAND(\"drop\", &fn, cmd_history_drop),\n \t\tOPT_SUBCOMMAND(\"reorder\", &fn, cmd_history_reorder),\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 2bf7bcab5a..b3d33c8588 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -379,6 +379,7 @@ integration_tests = [\n   't3450-history.sh',\n   't3451-history-drop.sh',\n   't3452-history-reorder.sh',\n+  't3453-history-split.sh',\n   't3500-cherry.sh',\n   't3501-revert-cherry-pick.sh',\n   't3502-cherry-pick-merge.sh',\ndiff --git a/t/t3453-history-split.sh b/t/t3453-history-split.sh\nnew file mode 100755\nindex 0000000000..a6a652e7df\n--- /dev/null\n+++ b/t/t3453-history-split.sh\n@@ -0,0 +1,468 @@\n+#!/bin/sh\n+\n+test_description='tests for git-history split subcommand'\n+\n+. ./test-lib.sh\n+\n+set_fake_editor () {\n+\twrite_script fake-editor.sh <<-\\EOF &&\n+\techo \"split-out commit\" >\"$1\"\n+\tEOF\n+\ttest_set_editor \"$(pwd)\"/fake-editor.sh\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 \"commit to be split must not be a merge commit\" err &&\n+\t\ttest_must_fail git history split HEAD~ 2>err &&\n+\t\ttest_grep \"cannot rearrange commit history with merges\" err\n+\t)\n+'\n+\n+test_expect_success 'refuses to work with changes in the worktree or index' '\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\techo changed >bar &&\n+\t\ttest_must_fail git history split -m message HEAD 2>err <<-EOF &&\n+\t\ty\n+\t\tn\n+\t\tEOF\n+\t\ttest_grep \"Your local changes to the following files would be overwritten\" err &&\n+\n+\t\tgit add bar &&\n+\t\ttest_must_fail git history split -m message HEAD 2>err <<-EOF &&\n+\t\ty\n+\t\tn\n+\t\tEOF\n+\t\ttest_grep \"Your local changes to the following files would be overwritten\" err\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 &&\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\tsplit-me\n+\t\tsplit-out commit\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+\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 &&\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\troot\n+\t\tsplit-out commit\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 &&\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\tsplit-me\n+\t\tsplit-out commit\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 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\tgit history split HEAD -m \"split-out commit\" <<-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+\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\tgit history split HEAD -m \"split-out commit\" <<-EOF &&\n+\t\tn\n+\t\ty\n+\t\tEOF\n+\n+\t\texpect_log <<-EOF &&\n+\t\tsplit-me\n+\t\tsplit-out commit\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 '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\ttest_must_fail git history split HEAD -m \"\" <<-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 'can specify message via option' '\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\tgit history split HEAD -m \"message option\" <<-EOF &&\n+\t\ty\n+\t\tn\n+\t\tEOF\n+\n+\t\texpect_log <<-EOF\n+\t\tsplit-me\n+\t\tmessage option\n+\t\tEOF\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\tcp \"$1\" . &&\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\tcat >expect <<-EOF &&\n+\n+\t\t# Please enter the commit message for the split-out changes. Lines starting\n+\t\t# with ${SQ}#${SQ} will be kept; you may remove them yourself if you want to.\n+\t\t# Changes to be committed:\n+\t\t#\tnew file:   bar\n+\t\t#\n+\t\tEOF\n+\t\ttest_cmp expect COMMIT_EDITMSG &&\n+\n+\t\texpect_log <<-EOF\n+\t\tsplit-me\n+\t\tsome commit message\n+\t\tEOF\n+\t)\n+'\n+\n+test_expect_success 'skips change summary with commit.verbose=false' '\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\tcp \"$1\" . &&\n+\t\techo \"some commit message\" >>\"$1\"\n+\t\tEOF\n+\t\ttest_set_editor \"$(pwd)\"/fake-editor.sh &&\n+\n+\t\tgit -c commit.verbose=false history split HEAD <<-EOF &&\n+\t\ty\n+\t\tn\n+\t\tEOF\n+\n+\t\tcat >expect <<-EOF &&\n+\n+\t\t# Please enter the commit message for the split-out changes. Lines starting\n+\t\t# with ${SQ}#${SQ} will be kept; you may remove them yourself if you want to.\n+\t\tEOF\n+\t\ttest_cmp expect COMMIT_EDITMSG &&\n+\n+\t\texpect_log <<-EOF\n+\t\tsplit-me\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\tgit history split HEAD -m \"message option\" -- 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 '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 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\twrite_script .git/hooks/prepare-commit-msg <<-EOF &&\n+\t\techo \"prepare-commit-msg: \\$@\" >>\"$(pwd)/hooks.log\"\n+\t\tEOF\n+\t\twrite_script .git/hooks/post-commit <<-EOF &&\n+\t\techo \"post-commit\" >>\"$(pwd)/hooks.log\"\n+\t\tEOF\n+\t\twrite_script .git/hooks/post-rewrite <<-EOF &&\n+\t\t{\n+\t\t\techo \"post-rewrite: \\$@\"\n+\t\t\tcat\n+\t\t} >>\"$(pwd)/hooks.log\"\n+\t\tEOF\n+\n+\t\tset_fake_editor &&\n+\t\tgit history split HEAD <<-EOF &&\n+\t\ty\n+\t\tn\n+\t\tEOF\n+\n+\t\texpect_log <<-EOF &&\n+\t\tsplit-me\n+\t\tsplit-out commit\n+\t\tEOF\n+\n+\t\tcat >expect <<-EOF &&\n+\t\tprepare-commit-msg: .git/COMMIT_EDITMSG message\n+\t\tpost-commit\n+\t\tprepare-commit-msg: .git/COMMIT_EDITMSG message\n+\t\tpost-commit\n+\t\tpost-rewrite: history\n+\t\t$old_head $(git rev-parse HEAD~)\n+\t\t$old_head $(git rev-parse HEAD)\n+\t\tEOF\n+\t\ttest_cmp expect 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_done\n\n-- \n2.51.0.417.g1ba7204a04.dirty\n\n"},{"id":"525542","messageId":"20250904-b4-pks-history-builtin-v3-18-509053514755@pks.im","threadId":"63986","inReplyTo":"20250904-b4-pks-history-builtin-v3-0-509053514755@pks.im","subject":"[PATCH RFC v3 18/18] builtin/history: implement \"reword\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-09-04T14:27:48Z","receivedAt":"2025-09-04T14:28:35Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Implement a new \"reword\" subcommand for git-history(1). This subcommand\nis essentially the same as if a user performed an interactive rebase\nwith a single commit changed to use the \"reword\" verb.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/git-history.adoc |   5 +\n builtin/history.c              | 104 +++++++++++++++++++++\n t/meson.build                  |   1 +\n t/t3454-history-reword.sh      | 202 +++++++++++++++++++++++++++++++++++++++++\n 4 files changed, 312 insertions(+)\n\ndiff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc\nindex 6f0c64b90e..cbbcef3582 100644\n--- a/Documentation/git-history.adoc\n+++ b/Documentation/git-history.adoc\n@@ -13,6 +13,7 @@ git history continue\n git history quit\n git history drop <commit>\n git history reorder <commit> (--before=<following-commit>|--after=<preceding-commit>)\n+git history reword [<options>] <commit>\n git history split [<options>] <commit> [--] [<pathspec>...]\n \n DESCRIPTION\n@@ -53,6 +54,10 @@ child commits, as that would lead to an empty branch.\n \tbe related to one another and must be reachable from the current `HEAD`\n \tcommit.\n \n+`reword <commit> [--message=<message>]`::\n+\tRewrite the commit message of the specified commit. All the other\n+\tdetails of this commit remain unchanged.\n+\n `split [--message=<message>] <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\ndiff --git a/builtin/history.c b/builtin/history.c\nindex df04b8dfc6..39acf4df28 100644\n--- a/builtin/history.c\n+++ b/builtin/history.c\n@@ -723,6 +723,108 @@ static int split_commit(struct repository *repo,\n \treturn ret;\n }\n \n+static int cmd_history_reword(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\tN_(\"git history reword [<options>] <commit>\"),\n+\t\tNULL,\n+\t};\n+\tconst char *commit_message = NULL;\n+\tstruct option options[] = {\n+\t\tOPT_STRING('m', \"message\", &commit_message, N_(\"message\"), N_(\"commit message\")),\n+\t\tOPT_END(),\n+\t};\n+\tstruct strbuf final_message = STRBUF_INIT;\n+\tstruct commit *original_commit, *head;\n+\tstruct strvec commits = STRVEC_INIT;\n+\tstruct object_id parent_tree_oid, original_commit_tree_oid;\n+\tstruct object_id rewritten_commit;\n+\tconst char *original_message, *original_body, *ptr;\n+\tstruct oidmap rewritten_commits = OIDMAP_INIT;\n+\tstruct replay_oid_mapping mapping = { 0 };\n+\tchar *original_author = NULL;\n+\tsize_t len;\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 single revision\"));\n+\t\tgoto out;\n+\t}\n+\trepo_config(repo, git_default_config, NULL);\n+\n+\toriginal_commit = lookup_commit_reference_by_name(argv[0]);\n+\tif (!original_commit) {\n+\t\tret = error(_(\"commit to be reworded cannot be found: %s\"), argv[0]);\n+\t\tgoto out;\n+\t}\n+\n+\thead = lookup_commit_reference_by_name(\"HEAD\");\n+\tif (!head) {\n+\t\tret = error(_(\"could not resolve HEAD to a commit\"));\n+\t\tgoto out;\n+\t}\n+\n+\t/*\n+\t * Collect the list of commits that we'll have to reapply now already.\n+\t * This ensures that we'll abort early on in case the range of commits\n+\t * contains merges, which we do not yet handle.\n+\t */\n+\tret = collect_commits(repo, original_commit->parents ? original_commit->parents->item : NULL,\n+\t\t\t      head, &commits);\n+\tif (ret < 0)\n+\t\tgoto out;\n+\n+\t/* We retain authorship of the original commit. */\n+\toriginal_message = repo_logmsg_reencode(repo, original_commit, 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+\tif (original_commit->parents)\n+\t\tparent_tree_oid = *get_commit_tree_oid(original_commit->parents->item);\n+\telse\n+\t\toidcpy(&parent_tree_oid, repo->hash_algo->empty_tree);\n+\toriginal_commit_tree_oid = *get_commit_tree_oid(original_commit);\n+\n+\tret = fill_commit_message(repo, &parent_tree_oid, &original_commit_tree_oid,\n+\t\t\t\t  original_body, commit_message, \"reworded\", &final_message);\n+\tif (ret < 0)\n+\t\tgoto out;\n+\n+\tret = commit_tree(final_message.buf, final_message.len,\n+\t\t\t  &repo_get_commit_tree(repo, original_commit)->object.oid,\n+\t\t\t  original_commit->parents, &rewritten_commit, original_author, NULL);\n+\tif (ret < 0) {\n+\t\tret = error(_(\"failed writing reworded commit\"));\n+\t\tgoto out;\n+\t}\n+\n+\treplace_commits(&commits, &original_commit->object.oid, &rewritten_commit, 1);\n+\n+\tmapping.entry.oid = rewritten_commit;\n+\tmapping.rewritten_oid = original_commit->object.oid;\n+\toidmap_put(&rewritten_commits, &mapping);\n+\n+\tret = apply_commits(repo, &commits, head, original_commit,\n+\t\t\t    &rewritten_commits, \"reword\");\n+\tif (ret < 0)\n+\t\tgoto out;\n+\n+\tret = 0;\n+\n+out:\n+\toidmap_clear(&rewritten_commits, 0);\n+\tstrbuf_release(&final_message);\n+\tstrvec_clear(&commits);\n+\tfree(original_author);\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@@ -835,6 +937,7 @@ int cmd_history(int argc,\n \t\tN_(\"git history quit\"),\n \t\tN_(\"git history drop <commit>\"),\n \t\tN_(\"git history reorder <commit> (--before=<following-commit>|--after=<preceding-commit>)\"),\n+\t\tN_(\"git history reword [<options>] <commit>\"),\n \t\tN_(\"git history split [<options>] <commit> [--] [<pathspec>...]\"),\n \t\tNULL,\n \t};\n@@ -845,6 +948,7 @@ int cmd_history(int argc,\n \t\tOPT_SUBCOMMAND(\"quit\", &fn, cmd_history_quit),\n \t\tOPT_SUBCOMMAND(\"drop\", &fn, cmd_history_drop),\n \t\tOPT_SUBCOMMAND(\"reorder\", &fn, cmd_history_reorder),\n+\t\tOPT_SUBCOMMAND(\"reword\", &fn, cmd_history_reword),\n \t\tOPT_SUBCOMMAND(\"split\", &fn, cmd_history_split),\n \t\tOPT_END(),\n \t};\ndiff --git a/t/meson.build b/t/meson.build\nindex b3d33c8588..948223f453 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -380,6 +380,7 @@ integration_tests = [\n   't3451-history-drop.sh',\n   't3452-history-reorder.sh',\n   't3453-history-split.sh',\n+  't3454-history-reword.sh',\n   't3500-cherry.sh',\n   't3501-revert-cherry-pick.sh',\n   't3502-cherry-pick-merge.sh',\ndiff --git a/t/t3454-history-reword.sh b/t/t3454-history-reword.sh\nnew file mode 100755\nindex 0000000000..97bdd755fa\n--- /dev/null\n+++ b/t/t3454-history-reword.sh\n@@ -0,0 +1,202 @@\n+#!/bin/sh\n+\n+test_description='tests for git-history reword subcommand'\n+\n+. ./test-lib.sh\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 reword HEAD~ 2>err &&\n+\t\ttest_grep \"cannot rearrange commit history with merges\" err &&\n+\t\ttest_must_fail git history reword HEAD 2>err &&\n+\t\ttest_grep \"cannot rearrange commit history with merges\" err\n+\t)\n+'\n+\n+test_expect_success 'refuses to work with changes in the worktree or index' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit base file &&\n+\t\techo foo >file &&\n+\t\ttest_must_fail git history reword HEAD 2>err &&\n+\t\ttest_grep \"Your local changes to the following files would be overwritten\" err &&\n+\t\tgit add file &&\n+\t\ttest_must_fail git history reword HEAD 2>err &&\n+\t\ttest_grep \"Your local changes to the following files would be overwritten\" err\n+\t)\n+'\n+\n+test_expect_success 'can reword tip of a branch' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit first &&\n+\t\ttest_commit second &&\n+\t\ttest_commit third &&\n+\n+\t\tgit symbolic-ref HEAD >expect &&\n+\t\tgit history reword -m \"third reworded\" HEAD &&\n+\t\tgit symbolic-ref HEAD >actual &&\n+\t\ttest_cmp expect actual &&\n+\n+\t\tcat >expect <<-EOF &&\n+\t\tthird reworded\n+\t\tsecond\n+\t\tfirst\n+\t\tEOF\n+\t\tgit log --format=%s >actual &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+test_expect_success 'can reword commit in the middle' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit first &&\n+\t\ttest_commit second &&\n+\t\ttest_commit third &&\n+\n+\t\tgit symbolic-ref HEAD >expect &&\n+\t\tgit history reword -m \"second reworded\" HEAD~ &&\n+\t\tgit symbolic-ref HEAD >actual &&\n+\t\ttest_cmp expect actual &&\n+\n+\t\tcat >expect <<-EOF &&\n+\t\tthird\n+\t\tsecond reworded\n+\t\tfirst\n+\t\tEOF\n+\t\tgit log --format=%s >actual &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+test_expect_success 'can reword root commit' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit first &&\n+\t\ttest_commit second &&\n+\t\ttest_commit third &&\n+\t\tgit history reword -m \"first reworded\" HEAD~2 &&\n+\n+\t\tcat >expect <<-EOF &&\n+\t\tthird\n+\t\tsecond\n+\t\tfirst reworded\n+\t\tEOF\n+\t\tgit log --format=%s >actual &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+test_expect_success 'can use editor to rewrite commit message' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit first &&\n+\n+\t\twrite_script fake-editor.sh <<-\\EOF &&\n+\t\tcp \"$1\" . &&\n+\t\tprintf \"\\namend a comment\\n\" >>\"$1\"\n+\t\tEOF\n+\t\ttest_set_editor \"$(pwd)\"/fake-editor.sh &&\n+\t\tgit history reword HEAD &&\n+\n+\t\tcat >expect <<-EOF &&\n+\t\tfirst\n+\n+\t\t# Please enter the commit message for the reworded changes. Lines starting\n+\t\t# with ${SQ}#${SQ} will be kept; you may remove them yourself if you want to.\n+\t\t# Changes to be committed:\n+\t\t#\tnew file:   first.t\n+\t\t#\n+\t\tEOF\n+\t\ttest_cmp expect COMMIT_EDITMSG &&\n+\n+\t\tcat >expect <<-EOF &&\n+\t\tfirst\n+\n+\t\tamend a comment\n+\n+\t\tEOF\n+\t\tgit log --format=%B >actual &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+test_expect_success 'hooks are executed for rewritten commits' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit first &&\n+\t\ttest_commit second &&\n+\t\ttest_commit third &&\n+\n+\t\twrite_script .git/hooks/prepare-commit-msg <<-EOF &&\n+\t\techo \"prepare-commit-msg: \\$@\" >>\"$(pwd)/hooks.log\"\n+\t\tEOF\n+\t\twrite_script .git/hooks/post-commit <<-EOF &&\n+\t\techo \"post-commit\" >>\"$(pwd)/hooks.log\"\n+\t\tEOF\n+\t\twrite_script .git/hooks/post-rewrite <<-EOF &&\n+\t\t{\n+\t\t\techo \"post-rewrite: \\$@\"\n+\t\t\tcat\n+\t\t} >>\"$(pwd)/hooks.log\"\n+\t\tEOF\n+\n+\t\tgit history reword -m \"second reworded\" HEAD~ &&\n+\n+\t\tcat >expect <<-EOF &&\n+\t\tthird\n+\t\tsecond reworded\n+\t\tfirst\n+\t\tEOF\n+\t\tgit log --format=%s >actual &&\n+\t\ttest_cmp expect actual &&\n+\n+\t\tcat >expect <<-EOF &&\n+\t\tprepare-commit-msg: .git/COMMIT_EDITMSG message\n+\t\tpost-commit\n+\t\tprepare-commit-msg: .git/COMMIT_EDITMSG message\n+\t\tpost-commit\n+\t\tpost-rewrite: history\n+\t\t$(git rev-parse second) $(git rev-parse HEAD~)\n+\t\t$(git rev-parse third) $(git rev-parse HEAD)\n+\t\tEOF\n+\t\ttest_cmp expect hooks.log\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\ttest_commit first &&\n+\n+\t\ttest_must_fail git history reword -m \"\" HEAD 2>err &&\n+\t\ttest_grep \"Aborting commit due to empty commit message.\" err\n+\t)\n+'\n+\n+test_done\n\n-- \n2.51.0.417.g1ba7204a04.dirty\n\n"},{"id":"525605","messageId":"83d36c12-64c0-44e1-a5d3-dd7a5575ad65@app.fastmail.com","threadId":"63986","inReplyTo":"20250904-b4-pks-history-builtin-v3-0-509053514755@pks.im","subject":"Re: [PATCH RFC v3 00/18] Introduce git-history(1) command for easy history editing","fromName":"Kristoffer Haugsbakk","fromEmail":"kristofferhaugsbakk@fastmail.com","sentAt":"2025-09-05T10:29:55Z","receivedAt":"2025-09-05T10:30:18Z","isPatch":true,"sender":{"key":"kristofferhaugsbakk@fastmail.com","avatar":null},"body":"On Thu, Sep 4, 2025, at 16:27, Patrick Steinhardt wrote:\n> Hi,\n>[snip]\n> I thus had a look at implementing some of these commands in Git itself,\n> where the result is this patch series. Specifically, the following\n> commands are introduced by this patch series:\n>\n>   - `git history drop` to drop a specific commit. This is basically the\n>     same as jj-abandon(1).\n>\n>   - `git history reorder` to reorder a specific commit before or after\n>     another commit. This is inspired by jj-new(1).\n>\n>   - `git history split` takes a commit and splits it into two. This is\n>     basically the same as jj-split(1).\n>\n> If this is something we want to have I think it'd be just a starting\n> point. There's other commands that I think are quite common and that\n> might make sense to introduce eventually:\n>\n>   - An equivalent to jj-absorb(1) would be awesome to have.\n>\n>   - `git history reword` to change only the commit message of a specific\n>     commit.\n\nThe cover letter is a bit outdated. Reword has been here since v2.\n\n>\n>   - `git history squash` to squash together multiple commits into one.\n>\n>[snip]\n"},{"id":"525607","messageId":"aLrJs5ONF0Foi6er@pks.im","threadId":"63986","inReplyTo":"83d36c12-64c0-44e1-a5d3-dd7a5575ad65@app.fastmail.com","subject":"Re: [PATCH RFC v3 00/18] Introduce git-history(1) command for easy history editing","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-09-05T11:29:55Z","receivedAt":"2025-09-05T11:30:04Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Fri, Sep 05, 2025 at 12:29:55PM +0200, Kristoffer Haugsbakk wrote:\n> On Thu, Sep 4, 2025, at 16:27, Patrick Steinhardt wrote:\n> > Hi,\n> >[snip]\n> > I thus had a look at implementing some of these commands in Git itself,\n> > where the result is this patch series. Specifically, the following\n> > commands are introduced by this patch series:\n> >\n> >   - `git history drop` to drop a specific commit. This is basically the\n> >     same as jj-abandon(1).\n> >\n> >   - `git history reorder` to reorder a specific commit before or after\n> >     another commit. This is inspired by jj-new(1).\n> >\n> >   - `git history split` takes a commit and splits it into two. This is\n> >     basically the same as jj-split(1).\n> >\n> > If this is something we want to have I think it'd be just a starting\n> > point. There's other commands that I think are quite common and that\n> > might make sense to introduce eventually:\n> >\n> >   - An equivalent to jj-absorb(1) would be awesome to have.\n> >\n> >   - `git history reword` to change only the commit message of a specific\n> >     commit.\n> \n> The cover letter is a bit outdated. Reword has been here since v2.\n\nGood point, addressed locally.\n\nPatrick\n"},{"id":"525704","messageId":"CABPp-BHeXtJPq7BE1UZ+zH0C-9VzQcSYRo+t0P-KWth68Zd94Q@mail.gmail.com","threadId":"63986","inReplyTo":"20250904-b4-pks-history-builtin-v3-0-509053514755@pks.im","subject":"Re: [PATCH RFC v3 00/18] Introduce git-history(1) command for easy history editing","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2025-09-07T06:46:48Z","receivedAt":"2025-09-07T06:47:00Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Thu, Sep 4, 2025 at 11:43 PM Patrick Steinhardt <ps@pks.im> wrote:\n>\n> Hi,\n>\n> over recent months I've been playing around with Jujutsu quite\n> frequently. While I still prefer using Git, there's been a couple\n> features in it that I really like and that I'd like to have in Git, as\n> well.\n>\n> A copule of these features relate to history editing. Most importantly,\n> I really dig the following commands:\n>\n>   - jj-abandon(1) to drop a specific commit from your history.\n>\n>   - jj-absorb(1) to take some changes and automatically apply them to\n>     commits in your history that last modified the respective hunks.\n>\n>   - jj-split(1) to split a commit into two.\n>\n>   - jj-new(1) to insert a new commit after or before a specific other\n>     commit.\n\nCool, I had been thinking of adding some jj-like functionality as\nwell, to git-replay in my case, though I was more interested in fixing\nup the infrastructure to handle replaying merges sanely first.\n\n> Not all of these commands can be ported directly into Git. jj-new(1) for\n> example doesn't really make a ton of sense for us, I'd claim. But some\n> of these commands _do_ make sense.\n>\n> I thus had a look at implementing some of these commands in Git itself,\n> where the result is this patch series. Specifically, the following\n> commands are introduced by this patch series:\n>\n>   - `git history drop` to drop a specific commit. This is basically the\n>     same as jj-abandon(1).\n>\n>   - `git history reorder` to reorder a specific commit before or after\n>     another commit. This is inspired by jj-new(1).\n>\n>   - `git history split` takes a commit and splits it into two. This is\n>     basically the same as jj-split(1).\n>\n> If this is something we want to have I think it'd be just a starting\n> point. There's other commands that I think are quite common and that\n> might make sense to introduce eventually:\n>\n>   - An equivalent to jj-absorb(1) would be awesome to have.\n>\n>   - `git history reword` to change only the commit message of a specific\n>     commit.\n>\n>   - `git history squash` to squash together multiple commits into one.\n>\n> In the end, I'd like us to learn from what people like about Jujutsu and\n> apply those learnings to Git. We won't be able to apply all learnings\n> from Jujutsu, as the workflow is quite different there due to the lack\n> of the index. But other things we certainly can apply to Git directly.\n\nSo, this brings up a question.  Should we have git-rebase &\ngit-cherry-pick & git-replay & git-history, or should we consolidate?\nI had envisioned having git-replay consolidate both cherry-pick and\nrebase functionality into one (then got pulled away by combination of\nwork reassgniment & multiple life crises hitting at once taking my\nfocus away for quite some time).  But now we're going in the other\ndirection.  And further along that other direction is another extreme\n-- just having these be top-level commands, e.g. \"git reorder\", \"git\nsplit\", etc.\n\nIn a separate conversation we had (and I hope I'm paraphrasing\ncorrectly; if not please correct me), you mentioned you wanted\ngit-history to be the home of history rewriting, and viewed git-replay\nas just a server side thing (whereas I created git-replay specifically\nas a user-focusing thing and then Christian changed it into a\nserver-side thing since that part was complete and enough for his\npurposes).  But if git history is the home of history editing, how far\ndoes that go?  Do we have a \"git history reset\"?  \"git history\ncommit\"?  \"git history fast-export/fast-import\"  \"git history\nfilter-repo\"?  Or is it just the home for certain kinds of history\nrewriting operations?  If so, which ones?\n\nThat all said, I'm a big fan of the idea of incorporating more of jj\ncapabilities, and you clearly marked the command as experimental\n(thanks!), which leave us room to adjust later if we don't like this\npath.  So I don't want to serve as a roadblock, I just think it's a\nuseful conversation to have...\n"},{"id":"526030","messageId":"fcb3355d-c9df-408a-9e28-421a7a18e1fd@gmail.com","threadId":"63986","inReplyTo":"20250904-b4-pks-history-builtin-v3-1-509053514755@pks.im","subject":"Re: [PATCH RFC v3 01/18] sequencer: optionally skip printing commit summary","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2025-09-10T14:01:54Z","receivedAt":"2025-09-10T14:01:57Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"Hi Patrick\n\nOn 04/09/2025 15:27, Patrick Steinhardt wrote:\n> \n> diff --git a/sequencer.c b/sequencer.c\n> index aaf2e4df64..7066cdc939 100644\n> --- a/sequencer.c\n> +++ b/sequencer.c\n> @@ -1742,7 +1742,7 @@ static int do_commit(struct repository *r,\n>   \t\t\trefs_delete_ref(get_main_ref_store(r), \"\",\n>   \t\t\t\t\t\"CHERRY_PICK_HEAD\", NULL, REF_NO_DEREF);\n>   \t\t\tunlink(git_path_merge_msg(r));\n> -\t\t\tif (!is_rebase_i(opts))\n> +\t\t\tif (!is_rebase_i(opts) && !opts->skip_commit_summary)\n\nI think it would be cleaner to make rebase set the new option and remove \n!is_rebase_i(opts) here.\n\nThanks\n\nPhillip\n\n>   \t\t\t\tprint_commit_summary(r, NULL, &oid,\n>   \t\t\t\t\t\tSUMMARY_SHOW_AUTHOR_DATE);\n>   \t\t\treturn res;\n> @@ -3139,8 +3139,12 @@ static int populate_opts_cb(const char *key, const char *value,\n>   \telse if (!strcmp(key, \"options.default-msg-cleanup\")) {\n>   \t\topts->explicit_cleanup = 1;\n>   \t\topts->default_msg_cleanup = get_cleanup_mode(value, 1);\n> -\t} else\n> +\t} else if (!strcmp(key, \"options.skip-commit-summary\")) {\n> +\t\topts->skip_commit_summary =\n> +\t\t\tgit_config_bool_or_int(key, value, ctx->kvi, &error_flag);\n> +\t} else {\n>   \t\treturn error(_(\"invalid key: %s\"), key);\n> +\t}\n>   \n>   \tif (!error_flag)\n>   \t\treturn error(_(\"invalid value for '%s': '%s'\"), key, value);\n> @@ -3698,11 +3702,13 @@ static int save_opts(struct replay_opts *opts)\n>   \t\t\t\t\"options.allow-rerere-auto\", NULL,\n>   \t\t\t\topts->allow_rerere_auto == RERERE_AUTOUPDATE ?\n>   \t\t\t\t\"true\" : \"false\");\n> -\n>   \tif (opts->explicit_cleanup)\n>   \t\tres |= repo_config_set_in_file_gently(the_repository, opts_file,\n>   \t\t\t\t\"options.default-msg-cleanup\", NULL,\n>   \t\t\t\tdescribe_cleanup_mode(opts->default_msg_cleanup));\n> +\tif (opts->skip_commit_summary)\n> +\t\tres |= repo_config_set_in_file_gently(the_repository, opts_file,\n> +\t\t\t\t\t\"options.skip-commit-summary\", NULL, \"true\");\n>   \treturn res;\n>   }\n>   \n> diff --git a/sequencer.h b/sequencer.h\n> index 304ba4b4d3..1767fd737e 100644\n> --- a/sequencer.h\n> +++ b/sequencer.h\n> @@ -52,6 +52,7 @@ struct replay_opts {\n>   \tint keep_redundant_commits;\n>   \tint verbose;\n>   \tint quiet;\n> +\tint skip_commit_summary;\n>   \tint reschedule_failed_exec;\n>   \tint committer_date_is_author_date;\n>   \tint ignore_date;\n> \n\n"},{"id":"526031","messageId":"f74b9dfe-b05c-431e-8872-92e2bbb75b8c@gmail.com","threadId":"63986","inReplyTo":"20250904-b4-pks-history-builtin-v3-2-509053514755@pks.im","subject":"Re: [PATCH RFC v3 02/18] sequencer: add option to rewind HEAD after picking commits","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2025-09-10T14:04:00Z","receivedAt":"2025-09-10T14:04:03Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"Hi Patrick\n\nOn 04/09/2025 15:27, Patrick Steinhardt wrote:\n> While the sequencer infrastructure knows to rewind \"HEAD\" to whatever it\n> was pointing to before a rebase, it doesn't do the same for non-rebase\n> operations like cherry-picks. This is because the expectation is that\n> the user directly picks commits on top of whatever \"HEAD\" points to, and\n> we advance the reference pointed to by \"HEAD\" instead of updating it\n> directly.\n> \n> We're about to introduce a new command though that needs to detach\n> \"HEAD\" while being more similar to git-cherry-pick(1) rathen than to\n> git-rebase(1). As such, we'll want to restore \"HEAD\" to point to the\n> branch that we started on while not using the more heavy-weight rebase\n> machinery.\n> \n> Introduce a new option `restore_head_target` to do so. Persist the\n> option into the sequencer configuration so that it persists across\n> different processes, e.g. when we need to stop due to a merge conflict.\n\nAs with the last patch, can we use this new option in \"git rebase\"? The \nsequencer is already a nest of conditionals, it would be nice to \nminimize the number of new ones.\n\nThanks\n\nPhillip\n\n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n>   sequencer.c | 27 ++++++++++++++++++++++++++-\n>   sequencer.h |  3 +++\n>   2 files changed, 29 insertions(+), 1 deletion(-)\n> \n> diff --git a/sequencer.c b/sequencer.c\n> index 7066cdc939..bff181df76 100644\n> --- a/sequencer.c\n> +++ b/sequencer.c\n> @@ -413,6 +413,7 @@ void replay_opts_release(struct replay_opts *opts)\n>   \tstruct replay_ctx *ctx = opts->ctx;\n>   \n>   \tfree(opts->gpg_sign);\n> +\tfree(opts->restore_head_target);\n>   \tfree(opts->reflog_action);\n>   \tfree(opts->default_strategy);\n>   \tfree(opts->strategy);\n> @@ -3142,6 +3143,8 @@ static int populate_opts_cb(const char *key, const char *value,\n>   \t} else if (!strcmp(key, \"options.skip-commit-summary\")) {\n>   \t\topts->skip_commit_summary =\n>   \t\t\tgit_config_bool_or_int(key, value, ctx->kvi, &error_flag);\n> +\t} else if (!strcmp(key, \"options.restore-head-target\")) {\n> +\t\tgit_config_string_dup(&opts->restore_head_target, key, value);\n>   \t} else {\n>   \t\treturn error(_(\"invalid key: %s\"), key);\n>   \t}\n> @@ -3709,6 +3712,10 @@ static int save_opts(struct replay_opts *opts)\n>   \tif (opts->skip_commit_summary)\n>   \t\tres |= repo_config_set_in_file_gently(the_repository, opts_file,\n>   \t\t\t\t\t\"options.skip-commit-summary\", NULL, \"true\");\n> +\tif (opts->restore_head_target)\n> +\t\tres |= repo_config_set_in_file_gently(the_repository, opts_file,\n> +\t\t\t\t\"options.restore-head-target\", NULL, opts->restore_head_target);\n> +\n>   \treturn res;\n>   }\n>   \n> @@ -5177,6 +5184,23 @@ static int pick_commits(struct repository *r,\n>   \t\t\treturn -1;\n>   \t}\n>   \n> +\tif (opts->restore_head_target) {\n> +\t\tstruct reset_head_opts reset_opts = { 0 };\n> +\t\tconst char *msg;\n> +\n> +\t\tmsg = reflog_message(opts, \"finish\", \"returning to %s\", opts->restore_head_target);\n> +\n> +\t\treset_opts.branch = opts->restore_head_target;\n> +\t\treset_opts.flags = RESET_HEAD_REFS_ONLY;\n> +\t\treset_opts.branch_msg = msg;\n> +\t\treset_opts.head_msg = msg;\n> +\n> +\t\tif (reset_head(r, &reset_opts)) {\n> +\t\t\terror(_(\"could not switch HEAD back to %s\"), opts->restore_head_target);\n> +\t\t\treturn -1;\n> +\t\t}\n> +\t}\n> +\n>   \t/*\n>   \t * Sequence of picks finished successfully; cleanup by\n>   \t * removing the .git/sequencer directory\n> @@ -5533,7 +5557,8 @@ int sequencer_pick_revisions(struct repository *r,\n>   \tif (opts->revs->cmdline.nr == 1 &&\n>   \t    opts->revs->cmdline.rev->whence == REV_CMD_REV &&\n>   \t    opts->revs->no_walk &&\n> -\t    !opts->revs->cmdline.rev->flags) {\n> +\t    !opts->revs->cmdline.rev->flags &&\n> +\t    !opts->restore_head_target) {\n>   \t\tstruct commit *cmit;\n>   \n>   \t\tif (prepare_revision_walk(opts->revs)) {\n> diff --git a/sequencer.h b/sequencer.h\n> index 1767fd737e..a905f6afc7 100644\n> --- a/sequencer.h\n> +++ b/sequencer.h\n> @@ -72,6 +72,9 @@ struct replay_opts {\n>   \t/* Reflog */\n>   \tchar *reflog_action;\n>   \n> +\t/* Reference to which HEAD shall be reset to after the operation. */\n> +\tchar *restore_head_target;\n> +\n>   \t/* placeholder commit for -i --root */\n>   \tstruct object_id squash_onto;\n>   \tint have_squash_onto;\n> \n\n"},{"id":"526032","messageId":"51689153-3012-4d3c-995b-69af296ea608@gmail.com","threadId":"63986","inReplyTo":"20250904-b4-pks-history-builtin-v3-17-509053514755@pks.im","subject":"Re: [PATCH RFC v3 17/18] builtin/history: implement \"split\" subcommand","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2025-09-10T14:04:49Z","receivedAt":"2025-09-10T14:04:52Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"Hi Patrick\n\nOn 04/09/2025 15:27, Patrick Steinhardt wrote:\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\nI like the idea of this command, but I think it would be much better to \nprompt the user to edit the orginal message after creating each new \ncommit rather than asking them to write a new message for the first \ncommit that we create and then not letting them edit the message for the \nsecond commit. We've got no way of knowing how they are splitting the \ncommit - they could be keeping most of the canges from the orginial in \nthe first commit in which case they probably want something simiar to \nthe orginial commit message for that one, or, they could be spitting out \nsomething which means they need to edit the message when creating the \nsecond commit.\n\nIf this was implemented in the sequencer then we'd be able to reuse the \nexisting code for creating commits and editing commit messages. It would \nalso make the \"split\" command available to \"rebase -i\".\n\nThanks\n\nPhillip\n\n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n>   Documentation/git-history.adoc |  60 ++++++\n>   builtin/history.c              | 313 ++++++++++++++++++++++++++-\n>   t/meson.build                  |   1 +\n>   t/t3453-history-split.sh       | 468 +++++++++++++++++++++++++++++++++++++++++\n>   4 files changed, 840 insertions(+), 2 deletions(-)\n> \n> diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc\n> index b36cd925dd..6f0c64b90e 100644\n> --- a/Documentation/git-history.adoc\n> +++ b/Documentation/git-history.adoc\n> @@ -13,6 +13,7 @@ git history continue\n>   git history quit\n>   git history drop <commit>\n>   git history reorder <commit> (--before=<following-commit>|--after=<preceding-commit>)\n> +git history split [<options>] <commit> [--] [<pathspec>...]\n>   \n>   DESCRIPTION\n>   -----------\n> @@ -52,6 +53,26 @@ child commits, as that would lead to an empty branch.\n>   \tbe related to one another and must be reachable from the current `HEAD`\n>   \tcommit.\n>   \n> +`split [--message=<message>] <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 message of the new commit will be asked for by launching the\n> +configured editor, unless it has been specified with the `-m` option.\n> +Authorship of the commit will be the same as for the 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>   The following commands are used to manage an interrupted history-rewriting\n>   operation:\n>   \n> @@ -111,6 +132,45 @@ f44a46e third\n>   bf7438d first\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 --message=\"split-out commit\"\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,e,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,e,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> +\n>   CONFIGURATION\n>   -------------\n>   \n> diff --git a/builtin/history.c b/builtin/history.c\n> index 16b516856e..df04b8dfc6 100644\n> --- a/builtin/history.c\n> +++ b/builtin/history.c\n> @@ -1,17 +1,27 @@\n> +/* Required for `comment_line_str`. */\n> +#define USE_THE_REPOSITORY_VARIABLE\n> +\n>   #include \"builtin.h\"\n>   #include \"branch.h\"\n> +#include \"cache-tree.h\"\n>   #include \"commit.h\"\n>   #include \"commit-reach.h\"\n>   #include \"config.h\"\n> +#include \"editor.h\"\n>   #include \"environment.h\"\n>   #include \"gettext.h\"\n>   #include \"hex.h\"\n>   #include \"object-name.h\"\n>   #include \"parse-options.h\"\n> +#include \"path.h\"\n> +#include \"pathspec.h\"\n> +#include \"read-cache-ll.h\"\n>   #include \"refs.h\"\n>   #include \"reset.h\"\n>   #include \"revision.h\"\n> +#include \"run-command.h\"\n>   #include \"sequencer.h\"\n> +#include \"sparse-index.h\"\n>   \n>   static int cmd_history_abort(int argc,\n>   \t\t\t     const char **argv,\n> @@ -213,6 +223,7 @@ static int apply_commits(struct repository *repo,\n>   \t\t\t const struct strvec *commits,\n>   \t\t\t struct commit *head,\n>   \t\t\t struct commit *base,\n> +\t\t\t const struct oidmap *rewritten_commits,\n>   \t\t\t const char *action)\n>   {\n>   \tstruct setup_revision_opt revision_opts = {\n> @@ -238,6 +249,7 @@ static int apply_commits(struct repository *repo,\n>   \t\treplay_opts.strategy = replay_opts.default_strategy;\n>   \t\treplay_opts.default_strategy = NULL;\n>   \t}\n> +\treplay_opts.old_oid_mappings = rewritten_commits;\n>   \n>   \tstrvec_push(&args, \"\");\n>   \tstrvec_pushv(&args, commits->v);\n> @@ -403,7 +415,8 @@ static int cmd_history_drop(int argc,\n>   \t\tif (ret < 0)\n>   \t\t\tgoto out;\n>   \n> -\t\tret = apply_commits(repo, &commits, head, commit_to_drop, \"drop\");\n> +\t\tret = apply_commits(repo, &commits, head, commit_to_drop,\n> +\t\t\t\t    NULL, \"drop\");\n>   \t\tif (ret < 0)\n>   \t\t\tgoto out;\n>   \t}\n> @@ -505,7 +518,7 @@ static int cmd_history_reorder(int argc,\n>   \treplace_commits(&commits, &commit_to_reorder->object.oid, NULL, 0);\n>   \treplace_commits(&commits, &anchor->object.oid, replacement, ARRAY_SIZE(replacement));\n>   \n> -\tret = apply_commits(repo, &commits, head, old, \"reorder\");\n> +\tret = apply_commits(repo, &commits, head, old, NULL, \"reorder\");\n>   \tif (ret < 0)\n>   \t\tgoto out;\n>   \n> @@ -517,6 +530,300 @@ static int cmd_history_reorder(int argc,\n>   \treturn ret;\n>   }\n>   \n> +static void change_data_free(void *util, const char *str UNUSED)\n> +{\n> +\tstruct wt_status_change_data *d = util;\n> +\tfree(d->rename_source);\n> +\tfree(d);\n> +}\n> +\n> +static int fill_commit_message(struct repository *repo,\n> +\t\t\t       const struct object_id *old_tree,\n> +\t\t\t       const struct object_id *new_tree,\n> +\t\t\t       const char *default_message,\n> +\t\t\t       const char *provided_message,\n> +\t\t\t       const char *action,\n> +\t\t\t       struct strbuf *out)\n> +{\n> +\tif (!provided_message) {\n> +\t\tconst char *path = git_path_commit_editmsg();\n> +\t\tconst char *hint =\n> +\t\t\t_(\"Please enter the commit message for the %s changes. Lines starting\\n\"\n> +\t\t\t  \"with '%s' will be kept; you may remove them yourself if you want to.\\n\");\n> +\t\tint verbose = 1;\n> +\n> +\t\tstrbuf_addstr(out, default_message);\n> +\t\tstrbuf_addch(out, '\\n');\n> +\t\tstrbuf_commented_addf(out, comment_line_str, hint, action, comment_line_str);\n> +\t\twrite_file_buf(path, out->buf, out->len);\n> +\n> +\t\trepo_config_get_bool(repo, \"commit.verbose\", &verbose);\n> +\t\tif (verbose) {\n> +\t\t\tstruct wt_status s;\n> +\n> +\t\t\twt_status_prepare(repo, &s);\n> +\t\t\tFREE_AND_NULL(s.branch);\n> +\t\t\ts.ahead_behind_flags = AHEAD_BEHIND_QUICK;\n> +\t\t\ts.commit_template = 1;\n> +\t\t\ts.colopts = 0;\n> +\t\t\ts.display_comment_prefix = 1;\n> +\t\t\ts.hints = 0;\n> +\t\t\ts.use_color = 0;\n> +\t\t\ts.whence = FROM_COMMIT;\n> +\t\t\ts.committable = 1;\n> +\n> +\t\t\ts.fp = fopen(git_path_commit_editmsg(), \"a\");\n> +\t\t\tif (!s.fp)\n> +\t\t\t\treturn error_errno(_(\"could not open '%s'\"), git_path_commit_editmsg());\n> +\n> +\t\t\twt_status_collect_changes_trees(&s, old_tree, new_tree);\n> +\t\t\twt_status_print(&s);\n> +\t\t\twt_status_collect_free_buffers(&s);\n> +\t\t\tstring_list_clear_func(&s.change, change_data_free);\n> +\t\t}\n> +\n> +\t\tstrbuf_reset(out);\n> +\t\tif (launch_editor(path, out, NULL)) {\n> +\t\t\tfprintf(stderr, _(\"Please supply the message using the -m option.\\n\"));\n> +\t\t\treturn -1;\n> +\t\t}\n> +\t\tstrbuf_stripspace(out, comment_line_str);\n> +\t} else {\n> +\t\tstrbuf_addstr(out, provided_message);\n> +\t}\n> +\n> +\tcleanup_message(out, COMMIT_MSG_CLEANUP_ALL, 0);\n> +\n> +\tif (!out->len) {\n> +\t\tfprintf(stderr, _(\"Aborting commit due to empty commit message.\\n\"));\n> +\t\treturn -1;\n> +\t}\n> +\n> +\treturn 0;\n> +}\n> +\n> +static int split_commit(struct repository *repo,\n> +\t\t\tstruct commit *original_commit,\n> +\t\t\tstruct pathspec *pathspec,\n> +\t\t\tconst char *commit_message,\n> +\t\t\tstruct object_id *out)\n> +{\n> +\tstruct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;\n> +\tstruct strbuf index_file = STRBUF_INIT, split_message = STRBUF_INIT;\n> +\tstruct child_process read_tree_cmd = CHILD_PROCESS_INIT;\n> +\tstruct index_state index = INDEX_STATE_INIT(repo);\n> +\tstruct object_id original_commit_tree_oid, parent_tree_oid;\n> +\tconst char *original_message, *original_body, *ptr;\n> +\tchar original_commit_oid[GIT_MAX_HEXSZ + 1];\n> +\tchar *original_author = NULL;\n> +\tstruct commit_list *parents = NULL;\n> +\tstruct commit *first_commit;\n> +\tstruct tree *split_tree;\n> +\tsize_t len;\n> +\tint ret;\n> +\n> +\tif (original_commit->parents)\n> +\t\tparent_tree_oid = *get_commit_tree_oid(original_commit->parents->item);\n> +\telse\n> +\t\toidcpy(&parent_tree_oid, repo->hash_algo->empty_tree);\n> +\toriginal_commit_tree_oid = *get_commit_tree_oid(original_commit);\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> +\tread_tree_cmd.git_cmd = 1;\n> +\tstrvec_pushf(&read_tree_cmd.env, \"GIT_INDEX_FILE=%s\", index_file.buf);\n> +\tstrvec_push(&read_tree_cmd.args, \"read-tree\");\n> +\tstrvec_push(&read_tree_cmd.args, oid_to_hex(&parent_tree_oid));\n> +\tret = run_command(&read_tree_cmd);\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_commit->object.oid);\n> +\tret = run_add_p_index(repo, &index, index_file.buf, &interactive_opts,\n> +\t\t\t      original_commit_oid, pathspec);\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> +\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/* We retain authorship of the original commit. */\n> +\toriginal_message = repo_logmsg_reencode(repo, original_commit, NULL, NULL);\n> +\tptr = find_commit_header(original_message, \"author\", &len);\n> +\tif (ptr)\n> +\t\toriginal_author = xmemdupz(ptr, len);\n> +\n> +\tret = fill_commit_message(repo, &parent_tree_oid, &split_tree->object.oid,\n> +\t\t\t\t  \"\", commit_message, \"split-out\", &split_message);\n> +\tif (ret < 0)\n> +\t\tgoto out;\n> +\n> +\tret = commit_tree(split_message.buf, split_message.len, &split_tree->object.oid,\n> +\t\t\t  original_commit->parents, &out[0], original_author, NULL);\n> +\tif (ret < 0) {\n> +\t\tret = error(_(\"failed writing split-out commit\"));\n> +\t\tgoto out;\n> +\t}\n> +\n> +\t/*\n> +\t * The second commit is much simpler to construct, as we can simply use\n> +\t * the original commit details, except that we adjust its parent to be\n> +\t * the newly split-out commit.\n> +\t */\n> +\tfind_commit_subject(original_message, &original_body);\n> +\tfirst_commit = lookup_commit_reference(repo, &out[0]);\n> +\tcommit_list_append(first_commit, &parents);\n> +\n> +\tret = commit_tree(original_body, strlen(original_body), &original_commit_tree_oid,\n> +\t\t\t  parents, &out[1], original_author, NULL);\n> +\tif (ret < 0) {\n> +\t\tret = error(_(\"failed writing second commit\"));\n> +\t\tgoto out;\n> +\t}\n> +\n> +\tret = 0;\n> +\n> +out:\n> +\tif (index_file.len)\n> +\t\tunlink(index_file.buf);\n> +\tstrbuf_release(&split_message);\n> +\tstrbuf_release(&index_file);\n> +\tfree_commit_list(parents);\n> +\tfree(original_author);\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\tN_(\"git history split [<options>] <commit>\"),\n> +\t\tNULL,\n> +\t};\n> +\tconst char *commit_message = NULL;\n> +\tstruct option options[] = {\n> +\t\tOPT_STRING('m', \"message\", &commit_message, N_(\"message\"), N_(\"commit message\")),\n> +\t\tOPT_END(),\n> +\t};\n> +\tstruct oidmap rewritten_commits = OIDMAP_INIT;\n> +\tstruct commit *original_commit, *head;\n> +\tstruct strvec commits = STRVEC_INIT;\n> +\tstruct commit_list *list = NULL;\n> +\tstruct object_id split_commits[2];\n> +\tstruct replay_oid_mapping mapping[2] = { 0 };\n> +\tstruct pathspec pathspec = { 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 revision\"));\n> +\t\tgoto out;\n> +\t}\n> +\trepo_config(repo, git_default_config, NULL);\n> +\n> +\toriginal_commit = lookup_commit_reference_by_name(argv[0]);\n> +\tif (!original_commit) {\n> +\t\tret = error(_(\"commit to be split cannot be found: %s\"), argv[0]);\n> +\t\tgoto out;\n> +\t}\n> +\n> +\tif (original_commit->parents && original_commit->parents->next) {\n> +\t\tret = error(_(\"commit to be split must not be a merge commit\"));\n> +\t\tgoto out;\n> +\t}\n> +\n> +\thead = lookup_commit_reference_by_name(\"HEAD\");\n> +\tif (!head) {\n> +\t\tret = error(_(\"could not resolve HEAD to a commit\"));\n> +\t\tgoto out;\n> +\t}\n> +\n> +\tcommit_list_append(original_commit, &list);\n> +\tif (!repo_is_descendant_of(repo, original_commit, list)) {\n> +\t\tret = error (_(\"split commit must be reachable from current HEAD commit\"));\n> +\t\tgoto out;\n> +\t}\n> +\n> +\tparse_pathspec(&pathspec, 0,\n> +\t\t       PATHSPEC_PREFER_FULL | PATHSPEC_SYMLINK_LEADING_PATH | PATHSPEC_PREFIX_ORIGIN,\n> +\t\t       prefix, argv + 1);\n> +\n> +\t/*\n> +\t * Collect the list of commits that we'll have to reapply now already.\n> +\t * This ensures that we'll abort early on in case the range of commits\n> +\t * contains merges, which we do not yet handle.\n> +\t */\n> +\tret = collect_commits(repo, original_commit->parents ? original_commit->parents->item : NULL,\n> +\t\t\t      head, &commits);\n> +\tif (ret < 0)\n> +\t\tgoto out;\n> +\n> +\t/*\n> +\t * Then we split up the commit and replace the original commit with the\n> +\t * new new ones.\n> +\t */\n> +\tret = split_commit(repo, original_commit, &pathspec,\n> +\t\t\t   commit_message, split_commits);\n> +\tif (ret < 0)\n> +\t\tgoto out;\n> +\n> +\tmapping[0].entry.oid = split_commits[0];\n> +\tmapping[0].rewritten_oid = original_commit->object.oid;\n> +\toidmap_put(&rewritten_commits, &mapping[0]);\n> +\tmapping[1].entry.oid = split_commits[1];\n> +\tmapping[1].rewritten_oid = original_commit->object.oid;\n> +\toidmap_put(&rewritten_commits, &mapping[1]);\n> +\n> +\treplace_commits(&commits, &original_commit->object.oid,\n> +\t\t\tsplit_commits, ARRAY_SIZE(split_commits));\n> +\n> +\tret = apply_commits(repo, &commits, head, original_commit,\n> +\t\t\t    &rewritten_commits, \"split\");\n> +\tif (ret < 0)\n> +\t\tgoto out;\n> +\n> +\tret = 0;\n> +\n> +out:\n> +\toidmap_clear(&rewritten_commits, 0);\n> +\tclear_pathspec(&pathspec);\n> +\tstrvec_clear(&commits);\n> +\tfree_commit_list(list);\n> +\treturn ret;\n> +}\n> +\n>   int cmd_history(int argc,\n>   \t\tconst char **argv,\n>   \t\tconst char *prefix,\n> @@ -528,6 +835,7 @@ int cmd_history(int argc,\n>   \t\tN_(\"git history quit\"),\n>   \t\tN_(\"git history drop <commit>\"),\n>   \t\tN_(\"git history reorder <commit> (--before=<following-commit>|--after=<preceding-commit>)\"),\n> +\t\tN_(\"git history split [<options>] <commit> [--] [<pathspec>...]\"),\n>   \t\tNULL,\n>   \t};\n>   \tparse_opt_subcommand_fn *fn = NULL;\n> @@ -537,6 +845,7 @@ int cmd_history(int argc,\n>   \t\tOPT_SUBCOMMAND(\"quit\", &fn, cmd_history_quit),\n>   \t\tOPT_SUBCOMMAND(\"drop\", &fn, cmd_history_drop),\n>   \t\tOPT_SUBCOMMAND(\"reorder\", &fn, cmd_history_reorder),\n> +\t\tOPT_SUBCOMMAND(\"split\", &fn, cmd_history_split),\n>   \t\tOPT_END(),\n>   \t};\n>   \n> diff --git a/t/meson.build b/t/meson.build\n> index 2bf7bcab5a..b3d33c8588 100644\n> --- a/t/meson.build\n> +++ b/t/meson.build\n> @@ -379,6 +379,7 @@ integration_tests = [\n>     't3450-history.sh',\n>     't3451-history-drop.sh',\n>     't3452-history-reorder.sh',\n> +  't3453-history-split.sh',\n>     't3500-cherry.sh',\n>     't3501-revert-cherry-pick.sh',\n>     't3502-cherry-pick-merge.sh',\n> diff --git a/t/t3453-history-split.sh b/t/t3453-history-split.sh\n> new file mode 100755\n> index 0000000000..a6a652e7df\n> --- /dev/null\n> +++ b/t/t3453-history-split.sh\n> @@ -0,0 +1,468 @@\n> +#!/bin/sh\n> +\n> +test_description='tests for git-history split subcommand'\n> +\n> +. ./test-lib.sh\n> +\n> +set_fake_editor () {\n> +\twrite_script fake-editor.sh <<-\\EOF &&\n> +\techo \"split-out commit\" >\"$1\"\n> +\tEOF\n> +\ttest_set_editor \"$(pwd)\"/fake-editor.sh\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 \"commit to be split must not be a merge commit\" err &&\n> +\t\ttest_must_fail git history split HEAD~ 2>err &&\n> +\t\ttest_grep \"cannot rearrange commit history with merges\" err\n> +\t)\n> +'\n> +\n> +test_expect_success 'refuses to work with changes in the worktree or index' '\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\techo changed >bar &&\n> +\t\ttest_must_fail git history split -m message HEAD 2>err <<-EOF &&\n> +\t\ty\n> +\t\tn\n> +\t\tEOF\n> +\t\ttest_grep \"Your local changes to the following files would be overwritten\" err &&\n> +\n> +\t\tgit add bar &&\n> +\t\ttest_must_fail git history split -m message HEAD 2>err <<-EOF &&\n> +\t\ty\n> +\t\tn\n> +\t\tEOF\n> +\t\ttest_grep \"Your local changes to the following files would be overwritten\" err\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 &&\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\tsplit-me\n> +\t\tsplit-out commit\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> +\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 &&\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\troot\n> +\t\tsplit-out commit\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 &&\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\tsplit-me\n> +\t\tsplit-out commit\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 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\tgit history split HEAD -m \"split-out commit\" <<-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> +\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\tgit history split HEAD -m \"split-out commit\" <<-EOF &&\n> +\t\tn\n> +\t\ty\n> +\t\tEOF\n> +\n> +\t\texpect_log <<-EOF &&\n> +\t\tsplit-me\n> +\t\tsplit-out commit\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 '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\ttest_must_fail git history split HEAD -m \"\" <<-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 'can specify message via option' '\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\tgit history split HEAD -m \"message option\" <<-EOF &&\n> +\t\ty\n> +\t\tn\n> +\t\tEOF\n> +\n> +\t\texpect_log <<-EOF\n> +\t\tsplit-me\n> +\t\tmessage option\n> +\t\tEOF\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\tcp \"$1\" . &&\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\tcat >expect <<-EOF &&\n> +\n> +\t\t# Please enter the commit message for the split-out changes. Lines starting\n> +\t\t# with ${SQ}#${SQ} will be kept; you may remove them yourself if you want to.\n> +\t\t# Changes to be committed:\n> +\t\t#\tnew file:   bar\n> +\t\t#\n> +\t\tEOF\n> +\t\ttest_cmp expect COMMIT_EDITMSG &&\n> +\n> +\t\texpect_log <<-EOF\n> +\t\tsplit-me\n> +\t\tsome commit message\n> +\t\tEOF\n> +\t)\n> +'\n> +\n> +test_expect_success 'skips change summary with commit.verbose=false' '\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\tcp \"$1\" . &&\n> +\t\techo \"some commit message\" >>\"$1\"\n> +\t\tEOF\n> +\t\ttest_set_editor \"$(pwd)\"/fake-editor.sh &&\n> +\n> +\t\tgit -c commit.verbose=false history split HEAD <<-EOF &&\n> +\t\ty\n> +\t\tn\n> +\t\tEOF\n> +\n> +\t\tcat >expect <<-EOF &&\n> +\n> +\t\t# Please enter the commit message for the split-out changes. Lines starting\n> +\t\t# with ${SQ}#${SQ} will be kept; you may remove them yourself if you want to.\n> +\t\tEOF\n> +\t\ttest_cmp expect COMMIT_EDITMSG &&\n> +\n> +\t\texpect_log <<-EOF\n> +\t\tsplit-me\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\tgit history split HEAD -m \"message option\" -- 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 '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 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\twrite_script .git/hooks/prepare-commit-msg <<-EOF &&\n> +\t\techo \"prepare-commit-msg: \\$@\" >>\"$(pwd)/hooks.log\"\n> +\t\tEOF\n> +\t\twrite_script .git/hooks/post-commit <<-EOF &&\n> +\t\techo \"post-commit\" >>\"$(pwd)/hooks.log\"\n> +\t\tEOF\n> +\t\twrite_script .git/hooks/post-rewrite <<-EOF &&\n> +\t\t{\n> +\t\t\techo \"post-rewrite: \\$@\"\n> +\t\t\tcat\n> +\t\t} >>\"$(pwd)/hooks.log\"\n> +\t\tEOF\n> +\n> +\t\tset_fake_editor &&\n> +\t\tgit history split HEAD <<-EOF &&\n> +\t\ty\n> +\t\tn\n> +\t\tEOF\n> +\n> +\t\texpect_log <<-EOF &&\n> +\t\tsplit-me\n> +\t\tsplit-out commit\n> +\t\tEOF\n> +\n> +\t\tcat >expect <<-EOF &&\n> +\t\tprepare-commit-msg: .git/COMMIT_EDITMSG message\n> +\t\tpost-commit\n> +\t\tprepare-commit-msg: .git/COMMIT_EDITMSG message\n> +\t\tpost-commit\n> +\t\tpost-rewrite: history\n> +\t\t$old_head $(git rev-parse HEAD~)\n> +\t\t$old_head $(git rev-parse HEAD)\n> +\t\tEOF\n> +\t\ttest_cmp expect 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_done\n> \n\n"},{"id":"526033","messageId":"10696a16-3f2b-4844-9f9c-9815976b3e1e@gmail.com","threadId":"63986","inReplyTo":"20250904-b4-pks-history-builtin-v3-18-509053514755@pks.im","subject":"Re: [PATCH RFC v3 18/18] builtin/history: implement \"reword\" subcommand","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2025-09-10T14:05:04Z","receivedAt":"2025-09-10T14:05:07Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"Hi Patrick\n\nOn 04/09/2025 15:27, Patrick Steinhardt wrote:\n> Implement a new \"reword\" subcommand for git-history(1). This subcommand\n> is essentially the same as if a user performed an interactive rebase\n> with a single commit changed to use the \"reword\" verb.\n\nThe sequencer already knows how to reword a commit, it would be much \nsimpler to reuse that code.\n\nThanks\n\nPhillip\n\n> \n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n>   Documentation/git-history.adoc |   5 +\n>   builtin/history.c              | 104 +++++++++++++++++++++\n>   t/meson.build                  |   1 +\n>   t/t3454-history-reword.sh      | 202 +++++++++++++++++++++++++++++++++++++++++\n>   4 files changed, 312 insertions(+)\n> \n> diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc\n> index 6f0c64b90e..cbbcef3582 100644\n> --- a/Documentation/git-history.adoc\n> +++ b/Documentation/git-history.adoc\n> @@ -13,6 +13,7 @@ git history continue\n>   git history quit\n>   git history drop <commit>\n>   git history reorder <commit> (--before=<following-commit>|--after=<preceding-commit>)\n> +git history reword [<options>] <commit>\n>   git history split [<options>] <commit> [--] [<pathspec>...]\n>   \n>   DESCRIPTION\n> @@ -53,6 +54,10 @@ child commits, as that would lead to an empty branch.\n>   \tbe related to one another and must be reachable from the current `HEAD`\n>   \tcommit.\n>   \n> +`reword <commit> [--message=<message>]`::\n> +\tRewrite the commit message of the specified commit. All the other\n> +\tdetails of this commit remain unchanged.\n> +\n>   `split [--message=<message>] <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> diff --git a/builtin/history.c b/builtin/history.c\n> index df04b8dfc6..39acf4df28 100644\n> --- a/builtin/history.c\n> +++ b/builtin/history.c\n> @@ -723,6 +723,108 @@ static int split_commit(struct repository *repo,\n>   \treturn ret;\n>   }\n>   \n> +static int cmd_history_reword(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\tN_(\"git history reword [<options>] <commit>\"),\n> +\t\tNULL,\n> +\t};\n> +\tconst char *commit_message = NULL;\n> +\tstruct option options[] = {\n> +\t\tOPT_STRING('m', \"message\", &commit_message, N_(\"message\"), N_(\"commit message\")),\n> +\t\tOPT_END(),\n> +\t};\n> +\tstruct strbuf final_message = STRBUF_INIT;\n> +\tstruct commit *original_commit, *head;\n> +\tstruct strvec commits = STRVEC_INIT;\n> +\tstruct object_id parent_tree_oid, original_commit_tree_oid;\n> +\tstruct object_id rewritten_commit;\n> +\tconst char *original_message, *original_body, *ptr;\n> +\tstruct oidmap rewritten_commits = OIDMAP_INIT;\n> +\tstruct replay_oid_mapping mapping = { 0 };\n> +\tchar *original_author = NULL;\n> +\tsize_t len;\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 single revision\"));\n> +\t\tgoto out;\n> +\t}\n> +\trepo_config(repo, git_default_config, NULL);\n> +\n> +\toriginal_commit = lookup_commit_reference_by_name(argv[0]);\n> +\tif (!original_commit) {\n> +\t\tret = error(_(\"commit to be reworded cannot be found: %s\"), argv[0]);\n> +\t\tgoto out;\n> +\t}\n> +\n> +\thead = lookup_commit_reference_by_name(\"HEAD\");\n> +\tif (!head) {\n> +\t\tret = error(_(\"could not resolve HEAD to a commit\"));\n> +\t\tgoto out;\n> +\t}\n> +\n> +\t/*\n> +\t * Collect the list of commits that we'll have to reapply now already.\n> +\t * This ensures that we'll abort early on in case the range of commits\n> +\t * contains merges, which we do not yet handle.\n> +\t */\n> +\tret = collect_commits(repo, original_commit->parents ? original_commit->parents->item : NULL,\n> +\t\t\t      head, &commits);\n> +\tif (ret < 0)\n> +\t\tgoto out;\n> +\n> +\t/* We retain authorship of the original commit. */\n> +\toriginal_message = repo_logmsg_reencode(repo, original_commit, 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> +\tif (original_commit->parents)\n> +\t\tparent_tree_oid = *get_commit_tree_oid(original_commit->parents->item);\n> +\telse\n> +\t\toidcpy(&parent_tree_oid, repo->hash_algo->empty_tree);\n> +\toriginal_commit_tree_oid = *get_commit_tree_oid(original_commit);\n> +\n> +\tret = fill_commit_message(repo, &parent_tree_oid, &original_commit_tree_oid,\n> +\t\t\t\t  original_body, commit_message, \"reworded\", &final_message);\n> +\tif (ret < 0)\n> +\t\tgoto out;\n> +\n> +\tret = commit_tree(final_message.buf, final_message.len,\n> +\t\t\t  &repo_get_commit_tree(repo, original_commit)->object.oid,\n> +\t\t\t  original_commit->parents, &rewritten_commit, original_author, NULL);\n> +\tif (ret < 0) {\n> +\t\tret = error(_(\"failed writing reworded commit\"));\n> +\t\tgoto out;\n> +\t}\n> +\n> +\treplace_commits(&commits, &original_commit->object.oid, &rewritten_commit, 1);\n> +\n> +\tmapping.entry.oid = rewritten_commit;\n> +\tmapping.rewritten_oid = original_commit->object.oid;\n> +\toidmap_put(&rewritten_commits, &mapping);\n> +\n> +\tret = apply_commits(repo, &commits, head, original_commit,\n> +\t\t\t    &rewritten_commits, \"reword\");\n> +\tif (ret < 0)\n> +\t\tgoto out;\n> +\n> +\tret = 0;\n> +\n> +out:\n> +\toidmap_clear(&rewritten_commits, 0);\n> +\tstrbuf_release(&final_message);\n> +\tstrvec_clear(&commits);\n> +\tfree(original_author);\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> @@ -835,6 +937,7 @@ int cmd_history(int argc,\n>   \t\tN_(\"git history quit\"),\n>   \t\tN_(\"git history drop <commit>\"),\n>   \t\tN_(\"git history reorder <commit> (--before=<following-commit>|--after=<preceding-commit>)\"),\n> +\t\tN_(\"git history reword [<options>] <commit>\"),\n>   \t\tN_(\"git history split [<options>] <commit> [--] [<pathspec>...]\"),\n>   \t\tNULL,\n>   \t};\n> @@ -845,6 +948,7 @@ int cmd_history(int argc,\n>   \t\tOPT_SUBCOMMAND(\"quit\", &fn, cmd_history_quit),\n>   \t\tOPT_SUBCOMMAND(\"drop\", &fn, cmd_history_drop),\n>   \t\tOPT_SUBCOMMAND(\"reorder\", &fn, cmd_history_reorder),\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> diff --git a/t/meson.build b/t/meson.build\n> index b3d33c8588..948223f453 100644\n> --- a/t/meson.build\n> +++ b/t/meson.build\n> @@ -380,6 +380,7 @@ integration_tests = [\n>     't3451-history-drop.sh',\n>     't3452-history-reorder.sh',\n>     't3453-history-split.sh',\n> +  't3454-history-reword.sh',\n>     't3500-cherry.sh',\n>     't3501-revert-cherry-pick.sh',\n>     't3502-cherry-pick-merge.sh',\n> diff --git a/t/t3454-history-reword.sh b/t/t3454-history-reword.sh\n> new file mode 100755\n> index 0000000000..97bdd755fa\n> --- /dev/null\n> +++ b/t/t3454-history-reword.sh\n> @@ -0,0 +1,202 @@\n> +#!/bin/sh\n> +\n> +test_description='tests for git-history reword subcommand'\n> +\n> +. ./test-lib.sh\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 reword HEAD~ 2>err &&\n> +\t\ttest_grep \"cannot rearrange commit history with merges\" err &&\n> +\t\ttest_must_fail git history reword HEAD 2>err &&\n> +\t\ttest_grep \"cannot rearrange commit history with merges\" err\n> +\t)\n> +'\n> +\n> +test_expect_success 'refuses to work with changes in the worktree or index' '\n> +\ttest_when_finished \"rm -rf repo\" &&\n> +\tgit init repo &&\n> +\t(\n> +\t\tcd repo &&\n> +\t\ttest_commit base file &&\n> +\t\techo foo >file &&\n> +\t\ttest_must_fail git history reword HEAD 2>err &&\n> +\t\ttest_grep \"Your local changes to the following files would be overwritten\" err &&\n> +\t\tgit add file &&\n> +\t\ttest_must_fail git history reword HEAD 2>err &&\n> +\t\ttest_grep \"Your local changes to the following files would be overwritten\" err\n> +\t)\n> +'\n> +\n> +test_expect_success 'can reword tip of a branch' '\n> +\ttest_when_finished \"rm -rf repo\" &&\n> +\tgit init repo &&\n> +\t(\n> +\t\tcd repo &&\n> +\t\ttest_commit first &&\n> +\t\ttest_commit second &&\n> +\t\ttest_commit third &&\n> +\n> +\t\tgit symbolic-ref HEAD >expect &&\n> +\t\tgit history reword -m \"third reworded\" HEAD &&\n> +\t\tgit symbolic-ref HEAD >actual &&\n> +\t\ttest_cmp expect actual &&\n> +\n> +\t\tcat >expect <<-EOF &&\n> +\t\tthird reworded\n> +\t\tsecond\n> +\t\tfirst\n> +\t\tEOF\n> +\t\tgit log --format=%s >actual &&\n> +\t\ttest_cmp expect actual\n> +\t)\n> +'\n> +\n> +test_expect_success 'can reword commit in the middle' '\n> +\ttest_when_finished \"rm -rf repo\" &&\n> +\tgit init repo &&\n> +\t(\n> +\t\tcd repo &&\n> +\t\ttest_commit first &&\n> +\t\ttest_commit second &&\n> +\t\ttest_commit third &&\n> +\n> +\t\tgit symbolic-ref HEAD >expect &&\n> +\t\tgit history reword -m \"second reworded\" HEAD~ &&\n> +\t\tgit symbolic-ref HEAD >actual &&\n> +\t\ttest_cmp expect actual &&\n> +\n> +\t\tcat >expect <<-EOF &&\n> +\t\tthird\n> +\t\tsecond reworded\n> +\t\tfirst\n> +\t\tEOF\n> +\t\tgit log --format=%s >actual &&\n> +\t\ttest_cmp expect actual\n> +\t)\n> +'\n> +\n> +test_expect_success 'can reword root commit' '\n> +\ttest_when_finished \"rm -rf repo\" &&\n> +\tgit init repo &&\n> +\t(\n> +\t\tcd repo &&\n> +\t\ttest_commit first &&\n> +\t\ttest_commit second &&\n> +\t\ttest_commit third &&\n> +\t\tgit history reword -m \"first reworded\" HEAD~2 &&\n> +\n> +\t\tcat >expect <<-EOF &&\n> +\t\tthird\n> +\t\tsecond\n> +\t\tfirst reworded\n> +\t\tEOF\n> +\t\tgit log --format=%s >actual &&\n> +\t\ttest_cmp expect actual\n> +\t)\n> +'\n> +\n> +test_expect_success 'can use editor to rewrite commit message' '\n> +\ttest_when_finished \"rm -rf repo\" &&\n> +\tgit init repo &&\n> +\t(\n> +\t\tcd repo &&\n> +\t\ttest_commit first &&\n> +\n> +\t\twrite_script fake-editor.sh <<-\\EOF &&\n> +\t\tcp \"$1\" . &&\n> +\t\tprintf \"\\namend a comment\\n\" >>\"$1\"\n> +\t\tEOF\n> +\t\ttest_set_editor \"$(pwd)\"/fake-editor.sh &&\n> +\t\tgit history reword HEAD &&\n> +\n> +\t\tcat >expect <<-EOF &&\n> +\t\tfirst\n> +\n> +\t\t# Please enter the commit message for the reworded changes. Lines starting\n> +\t\t# with ${SQ}#${SQ} will be kept; you may remove them yourself if you want to.\n> +\t\t# Changes to be committed:\n> +\t\t#\tnew file:   first.t\n> +\t\t#\n> +\t\tEOF\n> +\t\ttest_cmp expect COMMIT_EDITMSG &&\n> +\n> +\t\tcat >expect <<-EOF &&\n> +\t\tfirst\n> +\n> +\t\tamend a comment\n> +\n> +\t\tEOF\n> +\t\tgit log --format=%B >actual &&\n> +\t\ttest_cmp expect actual\n> +\t)\n> +'\n> +\n> +test_expect_success 'hooks are executed for rewritten commits' '\n> +\ttest_when_finished \"rm -rf repo\" &&\n> +\tgit init repo &&\n> +\t(\n> +\t\tcd repo &&\n> +\t\ttest_commit first &&\n> +\t\ttest_commit second &&\n> +\t\ttest_commit third &&\n> +\n> +\t\twrite_script .git/hooks/prepare-commit-msg <<-EOF &&\n> +\t\techo \"prepare-commit-msg: \\$@\" >>\"$(pwd)/hooks.log\"\n> +\t\tEOF\n> +\t\twrite_script .git/hooks/post-commit <<-EOF &&\n> +\t\techo \"post-commit\" >>\"$(pwd)/hooks.log\"\n> +\t\tEOF\n> +\t\twrite_script .git/hooks/post-rewrite <<-EOF &&\n> +\t\t{\n> +\t\t\techo \"post-rewrite: \\$@\"\n> +\t\t\tcat\n> +\t\t} >>\"$(pwd)/hooks.log\"\n> +\t\tEOF\n> +\n> +\t\tgit history reword -m \"second reworded\" HEAD~ &&\n> +\n> +\t\tcat >expect <<-EOF &&\n> +\t\tthird\n> +\t\tsecond reworded\n> +\t\tfirst\n> +\t\tEOF\n> +\t\tgit log --format=%s >actual &&\n> +\t\ttest_cmp expect actual &&\n> +\n> +\t\tcat >expect <<-EOF &&\n> +\t\tprepare-commit-msg: .git/COMMIT_EDITMSG message\n> +\t\tpost-commit\n> +\t\tprepare-commit-msg: .git/COMMIT_EDITMSG message\n> +\t\tpost-commit\n> +\t\tpost-rewrite: history\n> +\t\t$(git rev-parse second) $(git rev-parse HEAD~)\n> +\t\t$(git rev-parse third) $(git rev-parse HEAD)\n> +\t\tEOF\n> +\t\ttest_cmp expect hooks.log\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\ttest_commit first &&\n> +\n> +\t\ttest_must_fail git history reword -m \"\" HEAD 2>err &&\n> +\t\ttest_grep \"Aborting commit due to empty commit message.\" err\n> +\t)\n> +'\n> +\n> +test_done\n> \n\n"},{"id":"526034","messageId":"978e6351-1c08-44eb-8a2e-4a39541b9337@gmail.com","threadId":"63986","inReplyTo":"CABPp-BHeXtJPq7BE1UZ+zH0C-9VzQcSYRo+t0P-KWth68Zd94Q@mail.gmail.com","subject":"Re: [PATCH RFC v3 00/18] Introduce git-history(1) command for easy history editing","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2025-09-10T14:05:20Z","receivedAt":"2025-09-10T14:05:23Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"On 07/09/2025 07:46, Elijah Newren wrote:\n> On Thu, Sep 4, 2025 at 11:43 PM Patrick Steinhardt <ps@pks.im> wrote:\n>>\n>> Hi,\n>>\n>> over recent months I've been playing around with Jujutsu quite\n>> frequently. While I still prefer using Git, there's been a couple\n>> features in it that I really like and that I'd like to have in Git, as\n>> well.\n\nFantastic, thanks for working on this\n\n>> A copule of these features relate to history editing. Most importantly,\n>> I really dig the following commands:\n>>\n>>    - jj-abandon(1) to drop a specific commit from your history.\n>>\n>>    - jj-absorb(1) to take some changes and automatically apply them to\n>>      commits in your history that last modified the respective hunks.\n>>\n>>    - jj-split(1) to split a commit into two.\n>>\n>>    - jj-new(1) to insert a new commit after or before a specific other\n>>      commit.\n> \n> Cool, I had been thinking of adding some jj-like functionality as\n> well, to git-replay in my case, though I was more interested in fixing\n> up the infrastructure to handle replaying merges sanely first.\n\nInteresting, one of the questions I have about this series is whether it \nmakes sense to use the sequencer or extend git-replay. I do like the \nidea and name \"history\" though. I've been using a wrapper around \ngit-rebase that I call \"git-rewrite\" that lets me amend, reword, or drop \ncommits and rewrite all the branches that contain the modified commit \nfor quite a while now. I find amending the commit directly rather than \nusing fixup commits and then squashing much more convenient and it \navoids the problem of the fixup having conflicts when you try and squash \nit (though not the conflicts caused by amending the commit of course). \nOne feature I particularly like, which we might want to add to \"git \nhistory\" in the future, is being able to specify a filename, line pair \ninstead of a commit name. The script then uses \"git diff\" to map the \nline number to the file in HEAD and \"git blame\" to find the relevant \ncommit. This makes it easy to start a rewrite directly from my editor \nwhen I see something that needs fixing up.\n\nIf we do want to use the sequencer then I think we need to decide \nexactly what behavior we want from the new command with regard to \nrunning hooks and copying commit headers and implement that behavior \nrather than just accepting the status quo which is largely a historical \naccident. I would suggest that for commits that we're not modifing we \nshould not be running any hooks. For commits that are reworded we should \nbe running the \"commit-msg\" hook and possibly \"prepare-commit-msg\" as \nwell but no others. Where we're amending the commit content or splitting \na commit then we should be running the \"pre-commit\" hook as well. \nCurrently \"git-replay\" copies any extra commit headers when it creates a \nnew commit whereas \"git-rebase\" does not. There is some discussion at \n[1] where people were pushing back against copying extra headers by default.\n\nI think it would make sense in the long run to update all the branches \nthat contain the modified commit. To do that we can use the sequencer's \n\"update-ref\" command. To that end I think we should add a new entry \npoint to the sequencer that takes a todo list rather than a list of \ncommits to pick. That would also allow us to implement the \"split\" \ncommand in the sequencer and reuse the infrastructure that already \nexists for rewording commits. The way this series is currently \nimplemented makes it hard to extend in the future because it is based \naround cherry-picking commits rather than creating a todo list for the \nsequencer to execute.\n\nThanks\n\nPhillip\n\n>> Not all of these commands can be ported directly into Git. jj-new(1) for\n>> example doesn't really make a ton of sense for us, I'd claim. But some\n>> of these commands _do_ make sense.\n>>\n>> I thus had a look at implementing some of these commands in Git itself,\n>> where the result is this patch series. Specifically, the following\n>> commands are introduced by this patch series:\n>>\n>>    - `git history drop` to drop a specific commit. This is basically the\n>>      same as jj-abandon(1).\n>>\n>>    - `git history reorder` to reorder a specific commit before or after\n>>      another commit. This is inspired by jj-new(1).\n>>\n>>    - `git history split` takes a commit and splits it into two. This is\n>>      basically the same as jj-split(1).\n >>>> If this is something we want to have I think it'd be just a starting\n>> point. There's other commands that I think are quite common and that\n>> might make sense to introduce eventually:\n>>\n>>    - An equivalent to jj-absorb(1) would be awesome to have.\n>>\n>>    - `git history reword` to change only the commit message of a specific\n>>      commit.\n>>\n>>    - `git history squash` to squash together multiple commits into one.\n>>\n>> In the end, I'd like us to learn from what people like about Jujutsu and\n>> apply those learnings to Git. We won't be able to apply all learnings\n>> from Jujutsu, as the workflow is quite different there due to the lack\n>> of the index. But other things we certainly can apply to Git directly.\n> \n> So, this brings up a question.  Should we have git-rebase &\n> git-cherry-pick & git-replay & git-history, or should we consolidate?\n> I had envisioned having git-replay consolidate both cherry-pick and\n> rebase functionality into one (then got pulled away by combination of\n> work reassgniment & multiple life crises hitting at once taking my\n> focus away for quite some time).  But now we're going in the other\n> direction.  And further along that other direction is another extreme\n> -- just having these be top-level commands, e.g. \"git reorder\", \"git\n> split\", etc.\n> \n> In a separate conversation we had (and I hope I'm paraphrasing\n> correctly; if not please correct me), you mentioned you wanted\n> git-history to be the home of history rewriting, and viewed git-replay\n> as just a server side thing (whereas I created git-replay specifically\n> as a user-focusing thing and then Christian changed it into a\n> server-side thing since that part was complete and enough for his\n> purposes).  But if git history is the home of history editing, how far\n> does that go?  Do we have a \"git history reset\"?  \"git history\n> commit\"?  \"git history fast-export/fast-import\"  \"git history\n> filter-repo\"?  Or is it just the home for certain kinds of history\n> rewriting operations?  If so, which ones?\n> \n> That all said, I'm a big fan of the idea of incorporating more of jj\n> capabilities, and you clearly marked the command as experimental\n> (thanks!), which leave us room to adjust later if we don't like this\n> path.  So I don't want to serve as a roadblock, I just think it's a\n> useful conversation to have...\n> \n\n"},{"id":"526035","messageId":"413ce210-09aa-460a-8a35-f7c0877dc23d@gmail.com","threadId":"63986","inReplyTo":"978e6351-1c08-44eb-8a2e-4a39541b9337@gmail.com","subject":"Re: [PATCH RFC v3 00/18] Introduce git-history(1) command for easy history editing","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2025-09-10T14:08:03Z","receivedAt":"2025-09-10T14:08:06Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"On 10/09/2025 15:05, Phillip Wood wrote:\n> \n> new commit whereas \"git-rebase\" does not. There is some discussion at \n> [1] where people were pushing back against copying extra headers by \n> default.\n\nSorry I forget to add the link before sending\n\n[1] \nhttps://lore.kernel.org/git/pull.1902.git.1744041163929.gitgitgadget@gmail.com\n\n"},{"id":"526077","messageId":"xmqqcy7yrs2i.fsf@gitster.g","threadId":"63986","inReplyTo":"20250904-b4-pks-history-builtin-v3-0-509053514755@pks.im","subject":"Re: [PATCH RFC v3 00/18] Introduce git-history(1) command for easy history editing","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-09-10T20:05:57Z","receivedAt":"2025-09-10T20:05:59Z","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> Note: this patch series is growing quite large overall. I'll send one\n> last version of the complete series with the RFC tag, but after that\n> I'll probably split the series into two and stop after introducing the\n> \"reorder\" command.\n\nI haven't merged this to 'seen', not because I do not like what it\ndoes, but simply because I do not have enough concentration to deal\nwith conflicts with some in-flight topics (IIRC it textually overlapped\nwith Peff's add-i color topic).\n\nThanks.\n"},{"id":"526219","messageId":"CALnO6CCG=8JeRxgVfM9ayaQ6oY1iZQe3277sh=FG2YRkbbUj=g@mail.gmail.com","threadId":"63986","inReplyTo":"aLmMycWf-SMBw2bX@pks.im","subject":"Re: [PATCH RFC v2 15/16] builtin/history: implement \"split\" subcommand","fromName":"D. Ben Knoble","fromEmail":"ben.knoble@gmail.com","sentAt":"2025-09-12T18:26:04Z","receivedAt":"2025-09-12T18:26:17Z","isPatch":true,"sender":{"key":"ben.knoble@gmail.com","avatar":"https://avatars.githubusercontent.com/u/22802209?v=4"},"body":"On Thu, Sep 4, 2025 at 8:57 AM Patrick Steinhardt <ps@pks.im> wrote:\n>\n> On Wed, Sep 03, 2025 at 05:55:28PM -0400, D. Ben Knoble wrote:\n> > On Wed, Sep 3, 2025 at 8:20 AM Patrick Steinhardt <ps@pks.im> wrote:\n> > >\n> > > On Tue, Aug 26, 2025 at 09:14:49AM -0400, D. Ben Knoble wrote:\n> > > > On Sun, Aug 24, 2025 at 1:44 PM Patrick Steinhardt <ps@pks.im> wrote:\n> > > > > diff --git a/builtin/history.c b/builtin/history.c\n> > > > > index 16b516856e..6d3f44152c 100644\n> > > > > --- a/builtin/history.c\n> > > > > +++ b/builtin/history.c\n> > > > > @@ -517,6 +527,285 @@ static int cmd_history_reorder(int argc,\n> > > [snip]\n> > > > > +               wt_status_collect_changes_trees(&s, old_tree, new_tree);\n> > > > > +               wt_status_print(&s);\n> > > > > +               wt_status_collect_free_buffers(&s);\n> > > > > +               string_list_clear_func(&s.change, change_data_free);\n> > > >\n> > > > I think I'm supposed to see the changes between the old and new trees,\n> > > > right? Does this only happen if I use the interactive machinery to\n> > > > edit a hunk? When I try accepting some changes and leaving others for\n> > > > the next commit I get no diff in the template.\n> > >\n> > > Yeah, it's supposed to show the diff between old and new tree indeed. So\n> > > in theory you should see something.\n> > >\n> > > > I did try to add new diff lines to a hunk, and nothing showed up…\n> > > > maybe I'm holding it wrong? I'm pretty sure I compiled this version.\n> > >\n> > > Do you maybe have a reproducer for this? It seems to work alright for\n> > > me, but I wouldn't be surprised if there was a bug here. The wt-status\n> > > interfaces are quite something and I was tearing my hair while trying to\n> > > figure them out.\n> >\n> > Hm. I have a copy of these patches at\n> > https://github.com/benknoble/git/tree/ps-jj. After \"make DEVELOPER=1\n> > -j $(nproc)\" on that branch, I did\n> >\n> >     bin-wrappers/git history split @~3\n> >     <input y,q> # once I even used \"e\" and added new diffs to the patch\n> >     <type commit message> # no status info\n> >     <exit editor>\n> >\n> > Then it looks like the 2nd commit gets created automatically. Maybe\n> > I'm just missing how this should work? Thanks for looking at it.\n>\n> Weird. I used the exact same branch, command and input and did have\n> status information in my editor.\n\nHm. I've pulled down v3, built it, and pushed to the same branch. The\ntip is e91e23546b (builtin/history: implement \"reword\" subcommand,\n2025-09-04).\n\nNow, a heavier-handed recipe:\n\n    GIT_CONFIG_NOSYSTEM=1 GIT_CONFIG_GLOBAL=/dev/null bin-wrappers/git\n-c commit.verbose=true history split @~5\n    <input y,q> # once I even used \"e\" and added new diffs to the patch\n\nI see the usual instructions:\n\n# Please enter the commit message for the split-out changes. Lines starting\n# with '#' will be kept; you may remove them yourself if you want to.\n# Modifications qui seront validées :\n# modifié :         add-patch.c\n#\n\nAnd finally I might know what happened, ha… if the \"modified files\" is\nthe status information, then it has been there all along! Meanwhile, I\nwas expecting a _diff_.\n\nI'm actually _still_ expecting a diff with v3 and commit.verbose set,\nbut I apologize if I've led you down a wild goose chase for the rest\n:)\n\n> Note that for now the editor only asks\n> for the commit message of the first commit. The second commit is\n> basically retained from the original: both tree and commit message are\n> the exact same, only difference is its parent.\n\nSensible, though I might agree that the 2nd message should be edited\n(mentioned elsewhere).\n\n-- \nD. Ben Knoble\n"},{"id":"526327","messageId":"aMfdGTQWT1ES1jbm@pks.im","threadId":"63986","inReplyTo":"CALnO6CCG=8JeRxgVfM9ayaQ6oY1iZQe3277sh=FG2YRkbbUj=g@mail.gmail.com","subject":"Re: [PATCH RFC v2 15/16] builtin/history: implement \"split\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-09-15T09:32:09Z","receivedAt":"2025-09-15T09:32:22Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Fri, Sep 12, 2025 at 02:26:04PM -0400, D. Ben Knoble wrote:\n> Hm. I've pulled down v3, built it, and pushed to the same branch. The\n> tip is e91e23546b (builtin/history: implement \"reword\" subcommand,\n> 2025-09-04).\n> \n> Now, a heavier-handed recipe:\n> \n>     GIT_CONFIG_NOSYSTEM=1 GIT_CONFIG_GLOBAL=/dev/null bin-wrappers/git\n> -c commit.verbose=true history split @~5\n>     <input y,q> # once I even used \"e\" and added new diffs to the patch\n> \n> I see the usual instructions:\n> \n> # Please enter the commit message for the split-out changes. Lines starting\n> # with '#' will be kept; you may remove them yourself if you want to.\n> # Modifications qui seront validées :\n> # modifié :         add-patch.c\n> #\n> \n> And finally I might know what happened, ha… if the \"modified files\" is\n> the status information, then it has been there all along! Meanwhile, I\n> was expecting a _diff_.\n> \n> I'm actually _still_ expecting a diff with v3 and commit.verbose set,\n> but I apologize if I've led you down a wild goose chase for the rest\n> :)\n\nOh! I didn't even know this was a thing Git supports, I've never seen it\nbefore. For the sake of simplicity I'd propose to keep this as-is for\nnow, but to amend that mode once the initial couple of patches have\nlanded.\n\nWould that be fine with you?\n\nPatrick\n"},{"id":"526328","messageId":"aMfdMe72WB3DR4gW@pks.im","threadId":"63986","inReplyTo":"10696a16-3f2b-4844-9f9c-9815976b3e1e@gmail.com","subject":"Re: [PATCH RFC v3 18/18] builtin/history: implement \"reword\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-09-15T09:32:33Z","receivedAt":"2025-09-15T09:32:39Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Wed, Sep 10, 2025 at 03:05:04PM +0100, Phillip Wood wrote:\n> On 04/09/2025 15:27, Patrick Steinhardt wrote:\n> > Implement a new \"reword\" subcommand for git-history(1). This subcommand\n> > is essentially the same as if a user performed an interactive rebase\n> > with a single commit changed to use the \"reword\" verb.\n> \n> The sequencer already knows how to reword a commit, it would be much simpler\n> to reuse that code.\n\nI'll drop the second half of this patch series for now to reduce the\nscope of this series a bit. But once I send the second half I'll have a\nlook at whether this can be simplified.\n\nPatrick\n"},{"id":"526329","messageId":"aMfdK9f8BjRPPJOG@pks.im","threadId":"63986","inReplyTo":"xmqqcy7yrs2i.fsf@gitster.g","subject":"Re: [PATCH RFC v3 00/18] Introduce git-history(1) command for easy history editing","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-09-15T09:32:27Z","receivedAt":"2025-09-15T09:33:54Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Wed, Sep 10, 2025 at 01:05:57PM -0700, Junio C Hamano wrote:\n> Patrick Steinhardt <ps@pks.im> writes:\n> \n> > Note: this patch series is growing quite large overall. I'll send one\n> > last version of the complete series with the RFC tag, but after that\n> > I'll probably split the series into two and stop after introducing the\n> > \"reorder\" command.\n> \n> I haven't merged this to 'seen', not because I do not like what it\n> does, but simply because I do not have enough concentration to deal\n> with conflicts with some in-flight topics (IIRC it textually overlapped\n> with Peff's add-i color topic).\n\nThat's fair, it's been in RFC state anyway. I'll trim down the size of\nthis series, resolve conflicts with 'seen' and then send a new version.\n\nPatrick\n"},{"id":"526331","messageId":"aMfdJZgERk9Hbjlq@pks.im","threadId":"63986","inReplyTo":"fcb3355d-c9df-408a-9e28-421a7a18e1fd@gmail.com","subject":"Re: [PATCH RFC v3 01/18] sequencer: optionally skip printing commit summary","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-09-15T09:32:21Z","receivedAt":"2025-09-15T09:33:54Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Wed, Sep 10, 2025 at 03:01:54PM +0100, Phillip Wood wrote:\n> On 04/09/2025 15:27, Patrick Steinhardt wrote:\n> > diff --git a/sequencer.c b/sequencer.c\n> > index aaf2e4df64..7066cdc939 100644\n> > --- a/sequencer.c\n> > +++ b/sequencer.c\n> > @@ -1742,7 +1742,7 @@ static int do_commit(struct repository *r,\n> >   \t\t\trefs_delete_ref(get_main_ref_store(r), \"\",\n> >   \t\t\t\t\t\"CHERRY_PICK_HEAD\", NULL, REF_NO_DEREF);\n> >   \t\t\tunlink(git_path_merge_msg(r));\n> > -\t\t\tif (!is_rebase_i(opts))\n> > +\t\t\tif (!is_rebase_i(opts) && !opts->skip_commit_summary)\n> \n> I think it would be cleaner to make rebase set the new option and remove\n> !is_rebase_i(opts) here.\n\nGood suggestion, will do.\n\nPatrick\n"},{"id":"526330","messageId":"aMfdQFYdL1xoHADp@pks.im","threadId":"63986","inReplyTo":"f74b9dfe-b05c-431e-8872-92e2bbb75b8c@gmail.com","subject":"Re: [PATCH RFC v3 02/18] sequencer: add option to rewind HEAD after picking commits","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-09-15T09:32:48Z","receivedAt":"2025-09-15T09:33:55Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Wed, Sep 10, 2025 at 03:04:00PM +0100, Phillip Wood wrote:\n> Hi Patrick\n> \n> On 04/09/2025 15:27, Patrick Steinhardt wrote:\n> > While the sequencer infrastructure knows to rewind \"HEAD\" to whatever it\n> > was pointing to before a rebase, it doesn't do the same for non-rebase\n> > operations like cherry-picks. This is because the expectation is that\n> > the user directly picks commits on top of whatever \"HEAD\" points to, and\n> > we advance the reference pointed to by \"HEAD\" instead of updating it\n> > directly.\n> > \n> > We're about to introduce a new command though that needs to detach\n> > \"HEAD\" while being more similar to git-cherry-pick(1) rathen than to\n> > git-rebase(1). As such, we'll want to restore \"HEAD\" to point to the\n> > branch that we started on while not using the more heavy-weight rebase\n> > machinery.\n> > \n> > Introduce a new option `restore_head_target` to do so. Persist the\n> > option into the sequencer configuration so that it persists across\n> > different processes, e.g. when we need to stop due to a merge conflict.\n> \n> As with the last patch, can we use this new option in \"git rebase\"? The\n> sequencer is already a nest of conditionals, it would be nice to minimize\n> the number of new ones.\n\nYou probably refer to the condition in `sequencer_pick_revisions()`\nhere? Everything else is basically new code.\n\nHonestly, I don't dare touching that condition -- it's already quite\ncomplex, and I wouldn't be surprised if changing it in any way would\ncause regressions.\n\nPatrick\n"},{"id":"526332","messageId":"aMfdR3JE4zq-2j9b@pks.im","threadId":"63986","inReplyTo":"51689153-3012-4d3c-995b-69af296ea608@gmail.com","subject":"Re: [PATCH RFC v3 17/18] builtin/history: implement \"split\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-09-15T09:32:55Z","receivedAt":"2025-09-15T09:33:55Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Wed, Sep 10, 2025 at 03:04:49PM +0100, Phillip Wood wrote:\n> On 04/09/2025 15:27, Patrick Steinhardt wrote:\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> I like the idea of this command, but I think it would be much better to\n> prompt the user to edit the orginal message after creating each new commit\n> rather than asking them to write a new message for the first commit that we\n> create and then not letting them edit the message for the second commit.\n> We've got no way of knowing how they are splitting the commit - they could\n> be keeping most of the canges from the orginial in the first commit in which\n> case they probably want something simiar to the orginial commit message for\n> that one, or, they could be spitting out something which means they need to\n> edit the message when creating the second commit.\n\nYeah, Junio already said the same. I can do that.\n\n> If this was implemented in the sequencer then we'd be able to reuse the\n> existing code for creating commits and editing commit messages. It would\n> also make the \"split\" command available to \"rebase -i\".\n\nTrue, having it available in \"rebase -i\" would be nice indeed.\n\nPatrick\n"},{"id":"526333","messageId":"aMfdh8NxUj1v89Uu@pks.im","threadId":"63986","inReplyTo":"CABPp-BHeXtJPq7BE1UZ+zH0C-9VzQcSYRo+t0P-KWth68Zd94Q@mail.gmail.com","subject":"Re: [PATCH RFC v3 00/18] Introduce git-history(1) command for easy history editing","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-09-15T09:33:59Z","receivedAt":"2025-09-15T09:34:06Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Sat, Sep 06, 2025 at 11:46:48PM -0700, Elijah Newren wrote:\n> So, this brings up a question.  Should we have git-rebase &\n> git-cherry-pick & git-replay & git-history, or should we consolidate?\n> I had envisioned having git-replay consolidate both cherry-pick and\n> rebase functionality into one (then got pulled away by combination of\n> work reassgniment & multiple life crises hitting at once taking my\n> focus away for quite some time).  But now we're going in the other\n> direction.  And further along that other direction is another extreme\n> -- just having these be top-level commands, e.g. \"git reorder\", \"git\n> split\", etc.\n\nYeah, we should consolidate from my point of view. With the current\nstatus quo I'd say that:\n\n  - git-replay(1) becomes the home for all plumbing-level functionality\n    used by scripts and on the server-side.\n\n  - git-history(1) becomes the home for history editing functionality\n    that is user-facing. Potentially, we could also move (or rather\n    alias) git-rebase(1) into git-history(1) to complete that vision at\n    one point in time.\n\nThe main reason why I propose to introduce a top-level command with\ndifferent subcommands is that it helps users discover related\nfunctionality. If we had \"git reorder\", \"git split\" et cetera as\nseparate subcommands it would be way harder for a user to find out \"what\ncommands do I have to modify history?\"\n\nIn the worst case, users can still create an alias for git-split(1).\n\n> In a separate conversation we had (and I hope I'm paraphrasing\n> correctly; if not please correct me), you mentioned you wanted\n> git-history to be the home of history rewriting, and viewed git-replay\n> as just a server side thing (whereas I created git-replay specifically\n> as a user-focusing thing and then Christian changed it into a\n> server-side thing since that part was complete and enough for his\n> purposes).  But if git history is the home of history editing, how far\n> does that go?  Do we have a \"git history reset\"?  \"git history\n> commit\"?  \"git history fast-export/fast-import\"  \"git history\n> filter-repo\"?  Or is it just the home for certain kinds of history\n> rewriting operations?  If so, which ones?\n\nMy take in once sentence: git-history(1) modifies a preexisting sequence\nof commits. With that definition we rule out:\n\n  - \"git history commit\" because this creates a new commit on top.\n\n  - \"git history fast-export\" because this doesn't edit the commit\n    sequence.\n\n  - \"git history fast-import\" because this imports nonexistent commits.\n\n  - \"git history reset\" (which I assume is an alias of git-reset(1))\n    because this command doesn't only care about commits, but it also\n    modifies the working tree depending on the mode.\n\nSomething like \"git history filter-repo\" would probably be in the\npicture though, as it matches the above definition.\n\nI was also wondering whether \"git history\" is too broad with that\ndefinition in mind. At one point in time I though about \"git histedit\"\ninstead, which may be a bit of a better fit?\n\n> That all said, I'm a big fan of the idea of incorporating more of jj\n> capabilities, and you clearly marked the command as experimental\n> (thanks!), which leave us room to adjust later if we don't like this\n> path.  So I don't want to serve as a roadblock, I just think it's a\n> useful conversation to have...\n\nDefinitely, thanks a lot for your thoughts!\n\nPatrick\n"},{"id":"526365","messageId":"492931C5-AC12-4C5D-9321-340C2A4842CB@gmail.com","threadId":"63986","inReplyTo":"aMfdGTQWT1ES1jbm@pks.im","subject":"Re: [PATCH RFC v2 15/16] builtin/history: implement \"split\" subcommand","fromName":"Ben Knoble","fromEmail":"ben.knoble@gmail.com","sentAt":"2025-09-15T13:04:11Z","receivedAt":"2025-09-15T13:04:23Z","isPatch":true,"sender":{"key":"ben.knoble@gmail.com","avatar":"https://avatars.githubusercontent.com/u/22802209?v=4"},"body":"\n> Le 15 sept. 2025 à 05:32, Patrick Steinhardt <ps@pks.im> a écrit :\n> \n> ﻿On Fri, Sep 12, 2025 at 02:26:04PM -0400, D. Ben Knoble wrote:\n>> Hm. I've pulled down v3, built it, and pushed to the same branch. The\n>> tip is e91e23546b (builtin/history: implement \"reword\" subcommand,\n>> 2025-09-04).\n>> \n>> Now, a heavier-handed recipe:\n>> \n>>    GIT_CONFIG_NOSYSTEM=1 GIT_CONFIG_GLOBAL=/dev/null bin-wrappers/git\n>> -c commit.verbose=true history split @~5\n>>    <input y,q> # once I even used \"e\" and added new diffs to the patch\n>> \n>> I see the usual instructions:\n>> \n>> # Please enter the commit message for the split-out changes. Lines starting\n>> # with '#' will be kept; you may remove them yourself if you want to.\n>> # Modifications qui seront validées :\n>> # modifié :         add-patch.c\n>> #\n>> \n>> And finally I might know what happened, ha… if the \"modified files\" is\n>> the status information, then it has been there all along! Meanwhile, I\n>> was expecting a _diff_.\n>> \n>> I'm actually _still_ expecting a diff with v3 and commit.verbose set,\n>> but I apologize if I've led you down a wild goose chase for the rest\n>> :)\n> \n> Oh! I didn't even know this was a thing Git supports, I've never seen it\n> before. For the sake of simplicity I'd propose to keep this as-is for\n> now, but to amend that mode once the initial couple of patches have\n> landed.\n> \n> Would that be fine with you?\n\nSure! My apologies again for the « commit.verbose » red herring. "},{"id":"526367","messageId":"66e9aa8d-762e-4f51-b049-347b76578bf9@gmail.com","threadId":"63986","inReplyTo":"aMfdQFYdL1xoHADp@pks.im","subject":"Re: [PATCH RFC v3 02/18] sequencer: add option to rewind HEAD after picking commits","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2025-09-15T14:10:50Z","receivedAt":"2025-09-15T14:10:53Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"On 15/09/2025 10:32, Patrick Steinhardt wrote:\n> On Wed, Sep 10, 2025 at 03:04:00PM +0100, Phillip Wood wrote:\n>> Hi Patrick\n>>\n>> On 04/09/2025 15:27, Patrick Steinhardt wrote:\n> \n> You probably refer to the condition in `sequencer_pick_revisions()`\n> here? Everything else is basically new code.\n\nI was thinking of the code that restores HEAD at the end of \npick_commits() as rebase does something similar already (it looks like \nyou're using the same reflog message). Though thinking about it again \nI'm not sure if \"git history\" detaches HEAD at the start like rebase \ndoes. Rewriting the history with a detached HEAD is probably a good idea \nas it will stop the branch reflog being polluted. One nice aspect of \nrebase detaching HEAD is that at the end you can check the result by running\n\n     git range-diff $branch@{1}...HEAD\n\nThanks\n\nPhillip\n\n"},{"id":"526368","messageId":"fe671dbf-5177-4e66-a89a-8f99b379ac75@gmail.com","threadId":"63986","inReplyTo":"aMfdMe72WB3DR4gW@pks.im","subject":"Re: [PATCH RFC v3 18/18] builtin/history: implement \"reword\" subcommand","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2025-09-15T14:10:56Z","receivedAt":"2025-09-15T14:10:59Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"On 15/09/2025 10:32, Patrick Steinhardt wrote:\n> On Wed, Sep 10, 2025 at 03:05:04PM +0100, Phillip Wood wrote:\n>> On 04/09/2025 15:27, Patrick Steinhardt wrote:\n>>> Implement a new \"reword\" subcommand for git-history(1). This subcommand\n>>> is essentially the same as if a user performed an interactive rebase\n>>> with a single commit changed to use the \"reword\" verb.\n>>\n>> The sequencer already knows how to reword a commit, it would be much simpler\n>> to reuse that code.\n> \n> I'll drop the second half of this patch series for now to reduce the\n> scope of this series a bit. But once I send the second half I'll have a\n> look at whether this can be simplified.\n\nIf we passed a todo-list rather than just a list of commits to the \nsequencer then it would be as simple as writing \"reword $oid\"[*] in the \ntodo-list.\n\nThanks\n\nPhillip\n\n[*] I'm probably simplifying slightly as we might want to tweak the \nhooks that the sequencer runs for \"git-history\" but it should be fairly \neasy.\n"},{"id":"526430","messageId":"aMkbOgLlDDRlqt7a@pks.im","threadId":"63986","inReplyTo":"fe671dbf-5177-4e66-a89a-8f99b379ac75@gmail.com","subject":"Re: [PATCH RFC v3 18/18] builtin/history: implement \"reword\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-09-16T08:09:30Z","receivedAt":"2025-09-16T08:09:38Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Mon, Sep 15, 2025 at 03:10:56PM +0100, Phillip Wood wrote:\n> On 15/09/2025 10:32, Patrick Steinhardt wrote:\n> > On Wed, Sep 10, 2025 at 03:05:04PM +0100, Phillip Wood wrote:\n> > > On 04/09/2025 15:27, Patrick Steinhardt wrote:\n> > > > Implement a new \"reword\" subcommand for git-history(1). This subcommand\n> > > > is essentially the same as if a user performed an interactive rebase\n> > > > with a single commit changed to use the \"reword\" verb.\n> > > \n> > > The sequencer already knows how to reword a commit, it would be much simpler\n> > > to reuse that code.\n> > \n> > I'll drop the second half of this patch series for now to reduce the\n> > scope of this series a bit. But once I send the second half I'll have a\n> > look at whether this can be simplified.\n> \n> If we passed a todo-list rather than just a list of commits to the sequencer\n> then it would be as simple as writing \"reword $oid\"[*] in the todo-list.\n\nOne downside though is that we'll now be in interactive-rebase mode\ninstead of in history-editing mode. We could of course introduce\nhistory-editing mode as somewhat of an alias for interactive-rebases.\nBut the required changes are non-trivial and all over the place in\n\"sequencer.c\", so I eventually stopped pursuing that route.\n\nI still think it should be possible to at least separate out the actual\noperations and share them across the sequencer and git-history(1) so\nthat we can avoid some of the duplication.\n\nPatrick\n"},{"id":"526431","messageId":"cfcfaa43-7a16-408c-8d8a-325549a7838d@gmail.com","threadId":"63986","inReplyTo":"aMkbOgLlDDRlqt7a@pks.im","subject":"Re: [PATCH RFC v3 18/18] builtin/history: implement \"reword\" subcommand","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2025-09-16T08:42:05Z","receivedAt":"2025-09-16T08:42:09Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"On 16/09/2025 09:09, Patrick Steinhardt wrote:\n> On Mon, Sep 15, 2025 at 03:10:56PM +0100, Phillip Wood wrote:\n>> On 15/09/2025 10:32, Patrick Steinhardt wrote:\n>>> On Wed, Sep 10, 2025 at 03:05:04PM +0100, Phillip Wood wrote:\n>>>> On 04/09/2025 15:27, Patrick Steinhardt wrote:\n>>>>> Implement a new \"reword\" subcommand for git-history(1). This subcommand\n>>>>> is essentially the same as if a user performed an interactive rebase\n>>>>> with a single commit changed to use the \"reword\" verb.\n>>>>\n>>>> The sequencer already knows how to reword a commit, it would be much simpler\n>>>> to reuse that code.\n>>>\n>>> I'll drop the second half of this patch series for now to reduce the\n>>> scope of this series a bit. But once I send the second half I'll have a\n>>> look at whether this can be simplified.\n>>\n>> If we passed a todo-list rather than just a list of commits to the sequencer\n>> then it would be as simple as writing \"reword $oid\"[*] in the todo-list.\n> \n> One downside though is that we'll now be in interactive-rebase mode\n> instead of in history-editing mode. We could of course introduce\n> history-editing mode as somewhat of an alias for interactive-rebases.\n> But the required changes are non-trivial and all over the place in\n> \"sequencer.c\", so I eventually stopped pursuing that route.\n\nI'm not sure I understand. At git-history uses the sequencer to \ncherry-pick commits by calling sequencer_pick_revisions()[1]. Both \ncherry-pick and rebase share the same todo-list format and the main loop \nin pick_commits() processes that list in the same way for both commands. \nAs I said in a previous mail I think we should add a new entry point to \nthe sequencer that takes a todo list rather than a list of revisions but \nthat should be simple enough and then we get most of the functionality \nwe want such as rewording commits and updating refs more or less for \nfree. So I'm not sure what you mean by \"we'll now be in \ninteractive-rebase\" mode.\n\nThere are good arguments for not using the sequencer at all so that we \ndon't update the worktree each time we pick a commit (that would be a \nlot more work though), but I cannot currently see a good reason for the \napproach of using the sequencer to cherry-pick commits but implementing \nall the other operations separately.\n\nThanks\n\nPhillip\n\n[1] At some point we should figure out how to teach \"git status\" to \ndistinguish between \"git cherry-pick\" and \"git history\" as I think at \nthe moment both probably look like a cherry-pick.\n\n\n> I still think it should be possible to at least separate out the actual\n> operations and share them across the sequencer and git-history(1) so\n> that we can avoid some of the duplication.\n> \n> Patrick\n> \n\n"},{"id":"526442","messageId":"aMlIseiqlfslrh9_@nubble.lan","threadId":"63986","inReplyTo":"aMfdh8NxUj1v89Uu@pks.im","subject":"Re: [PATCH RFC v3 00/18] Introduce git-history(1) command for easy history editing","fromName":"Oswald Buddenhagen","fromEmail":"oswald.buddenhagen@gmx.de","sentAt":"2025-09-16T11:23:29Z","receivedAt":"2025-09-16T11:23:40Z","isPatch":true,"sender":{"key":"oswald.buddenhagen@gmx.de","avatar":"https://avatars.githubusercontent.com/u/812380?v=4"},"body":"On Mon, Sep 15, 2025 at 11:33:59AM +0200, Patrick Steinhardt wrote:\n>On Sat, Sep 06, 2025 at 11:46:48PM -0700, Elijah Newren wrote:\n>> So, this brings up a question.  Should we have git-rebase &\n>> git-cherry-pick & git-replay & git-history, or should we consolidate?\n>> [...]\n>\n>The main reason why I propose to introduce a top-level command with\n>different subcommands is that it helps users discover related\n>functionality.\n>\ni think this is better addressed with proper documentation.\n\n>In the worst case, users can still create an alias for git-split(1).\n>\nit seems backwards to basically require aliases for efficient use. this\nwould also lead to a mismatch between what people write into how-tos and\nwhat is actually in use (aliases are individual and therefore\ninconsistent, so one needs to use the full form in writing).\n\n>My take in once sentence: git-history(1) modifies a preexisting\n>sequence of commits.\n>\nthe details of this definition are arbitrary, and you already noticed\nthat there are grey areas. we will find more when we start looking for\nthem.\n\nso, obviously, i'm in favor of atomizing.\n\n>I was also wondering whether \"git history\" is too broad with that\n>definition in mind. At one point in time I though about \"git histedit\"\n>instead, which may be a bit of a better fit?\n>\na natural name for this would be \"revise\", which, not coindicentally at\nall, is actually an existing 3rd-party tool with a quite similar scope.\nbut then, see above.\n"},{"id":"526617","messageId":"aMsWL2cSoytbmRjA@szeder.dev","threadId":"63986","inReplyTo":"aKs3tqjE510MF0T-@pks.im","subject":"Re: [PATCH RFC 00/11] Introduce git-history(1) command for easy history editing","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2025-09-17T20:12:31Z","receivedAt":"2025-09-17T20:12:45Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"On Sun, Aug 24, 2025 at 06:03:02PM +0200, Patrick Steinhardt wrote:\n> On Sat, Aug 23, 2025 at 06:25:06PM -0700, Martin von Zweigbergk wrote:\n> > On Tue, Aug 19, 2025 at 3:57 AM Patrick Steinhardt <ps@pks.im> wrote:\n> > >\n> > > Hi,\n> > >\n> > > over recent months I've been playing around with Jujutsu quite\n> > > frequently. While I still prefer using Git, there's been a couple\n> > > features in it that I really like and that I'd like to have in Git, as\n> > > well.\n> > >\n> > > A copule of these features relate to history editing. Most importantly,\n> > > I really dig the following commands:\n> > >\n> > >   - jj-abandon(1) to drop a specific commit from your history.\n> > \n> > It also rebases all descendants on top of the parent(s) of the\n> > abandoned commit(s). Branches pointing to the rebased commits are also\n> > repointed. So is the working copy if it points to a rebased commit\n> > (the closest equivalent in Git would be HEAD). Do you plan to make all\n> > the `git history` commands behave that way too?\n> \n> Yup.\n\nThat sounds scary...  What does \"all descendants\" mean in this\ncontext?\n\nLet's suppose I have this piece of history, I'm on 'branch2', and I\ndrop commit B.  Which commits will be rewritten and which branches\nwill be repointed?\n\n   A---B---C---D   branch1\n            \\   \\\n             \\   E---F   branch2\n              \\       \\\n               \\       G---H---I   branch3\n                \\\n                 J---K---L   branch4\n\n"},{"id":"527684","messageId":"20251001-b4-pks-history-builtin-v4-0-8e61ddb86317@pks.im","threadId":"63986","inReplyTo":"20250819-b4-pks-history-builtin-v1-0-9b77c32688fe@pks.im","subject":"[PATCH v4 00/12] Introduce git-history(1) command for easy history editing","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-01T15:57:26Z","receivedAt":"2025-10-01T15:57:48Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Hi,\n\nover recent months I've been playing around with Jujutsu quite\nfrequently. While I still prefer using Git, there's been a couple\nfeatures in it that I really like and that I'd like to have in Git, as\nwell.\n\nA copule of these features relate to history editing. Most importantly,\nI really dig the following commands:\n\n  - jj-abandon(1) to drop a specific commit from your history.\n\n  - jj-absorb(1) to take some changes and automatically apply them to\n    commits in your history that last modified the respective hunks.\n\n  - jj-split(1) to split a commit into two.\n\n  - jj-new(1) to insert a new commit after or before a specific other\n    commit.\n\nNot all of these commands can be ported directly into Git. jj-new(1) for\nexample doesn't really make a ton of sense for us, I'd claim. But some\nof these commands _do_ make sense.\n\nThis patch series is a starting point for such a command. I've\nsignificantly slimmed it down from the first couple revisions now\nfollowing the discussions at the Contributor's Summit yesterday. This\nwas my intent anyway, as I already mentioned on the last iteration.\n\nChanges in v2:\n  - Add a new \"reword\" subcommand.\n  - List git-history(1) in \"command-list.txt\".\n  - Add some missing error handling.\n  - Simplify calling convention of `apply_commits()` to handle root\n    commits internally instead of requiring every caller to do so.\n  - Add tests to verify that git-history(1) refuses to work with changes\n    in the worktree or index.\n  - Mark git-history(1) as experimental.\n  - Introduce commands to manage interrupted history edits.\n  - A bunch of improvements to the manpage.\n  - Link to v1: https://lore.kernel.org/r/20250819-b4-pks-history-builtin-v1-0-9b77c32688fe@pks.im\n\nChanges in v3:\n  - Add logic to drive the \"post-rewrite\" hook and add tests to verify\n    that all hooks are executed as expected.\n  - Deduplicate logic to turn a replay action into a todo command.\n  - Move the addition of tests for the top-level git-history(1) command\n    to the correct commit.\n  - Some smaller commit message fixes.\n  - Honor \"commit.verbose\".\n  - Fix copy-paste error with an error message.\n  - Link to v2: https://lore.kernel.org/r/20250824-b4-pks-history-builtin-v2-0-964ac12f65bd@pks.im\n\nChanges in v4:\n  - I've rebuilt the patch series. It is now based on 821f583da6 (The\n    thirteenth batcn, 2025-09-29) with sa/replay-atomic-ref-updates\n    at 665c66a743 (replay: make atomic ref updates the default behavior,\n    2025-09-27) merged into it. This should fix all conflicts with seen.\n  - I've reworked this patch series to use the same infra as\n    git-replay(1), as discussed during the Contributor's Summit.\n  - I've slimmed down the patch series to only tackle those commands\n    that cannot result in a conflict to keep it simple. I also learned\n    that Elijah has been working on a \"git replay edit\" command, so I\n    dropped that command so that we can instead use his version.\n  - During the Contributor's Summit we have agreed that for now, we\n    won't care about hook execution just yet. This may be backfilled at\n    a later point in time.\n  - I dropped \"commit.verbose\" handling for now, as my understanding of\n    it was wrong at first. This is something we should backfill.\n  - Link to v3: https://lore.kernel.org/r/20250904-b4-pks-history-builtin-v3-0-509053514755@pks.im\n\nThanks!\n\nPatrick\n\n---\nPatrick Steinhardt (12):\n      wt-status: provide function to expose status for trees\n      replay: extract logic to pick commits\n      replay: stop using `the_repository`\n      replay: parse commits before dereferencing them\n      builtin: add new \"history\" command\n      builtin/history: implement \"reword\" subcommand\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      cache-tree: allow writing in-memory index as tree\n      builtin/history: implement \"split\" subcommand\n\n .gitignore                     |   1 +\n Documentation/git-history.adoc | 114 ++++++++\n Documentation/meson.build      |   1 +\n Makefile                       |   2 +\n add-interactive.c              | 174 +++---------\n add-interactive.h              |  46 +--\n add-patch.c                    | 295 +++++++++++++++++---\n add-patch.h                    |  64 +++++\n builtin.h                      |   1 +\n builtin/add.c                  |  22 +-\n builtin/checkout.c             |   7 +-\n builtin/commit.c               |  16 +-\n builtin/history.c              | 614 +++++++++++++++++++++++++++++++++++++++++\n builtin/replay.c               | 110 +-------\n builtin/reset.c                |  16 +-\n builtin/stash.c                |  46 +--\n cache-tree.c                   |   5 +-\n cache-tree.h                   |   3 +-\n command-list.txt               |   1 +\n commit.h                       |   2 +-\n git.c                          |   1 +\n meson.build                    |   2 +\n replay.c                       | 118 ++++++++\n replay.h                       |  18 ++\n t/meson.build                  |   3 +\n t/t3450-history.sh             |  17 ++\n t/t3451-history-reword.sh      | 202 ++++++++++++++\n t/t3452-history-split.sh       | 432 +++++++++++++++++++++++++++++\n wt-status.c                    |  24 ++\n wt-status.h                    |   3 +\n 30 files changed, 1984 insertions(+), 376 deletions(-)\n\nRange-diff versus v3:\n\n 1:  166ba26bea <  -:  ---------- sequencer: optionally skip printing commit summary\n 2:  60173823a9 <  -:  ---------- sequencer: add option to rewind HEAD after picking commits\n 3:  74d21ea445 <  -:  ---------- sequencer: introduce new history editing mode\n 4:  b1c052afc7 <  -:  ---------- sequencer: stop using `the_repository` in `sequencer_remove_state()`\n 5:  0884c4fd96 <  -:  ---------- sequencer: wire up \"rewritten-hook\" for REPLAY_HISTORY_EDIT\n15:  b77e378615 =  1:  695fc789d5 wt-status: provide function to expose status for trees\n -:  ---------- >  2:  55707c8cb3 replay: extract logic to pick commits\n -:  ---------- >  3:  1308a0bbbb replay: stop using `the_repository`\n -:  ---------- >  4:  d153d366b5 replay: parse commits before dereferencing them\n 7:  243d36cb45 !  5:  820a2f4c17 builtin: add new \"history\" command\n    @@ builtin/history.c (new)\n     +\t};\n     +\n     +\targc = parse_options(argc, argv, prefix, options, usage, 0);\n    ++\tif (argc)\n    ++\t\tusagef(\"unrecognized argument: %s\", argv[0]);\n     +\treturn 0;\n     +}\n     \n    @@ t/meson.build: integration_tests = [\n        't3500-cherry.sh',\n        't3501-revert-cherry-pick.sh',\n        't3502-cherry-pick-merge.sh',\n    -@@ t/meson.build: if perl.found() and time.found()\n    -       timeout: 0,\n    -     )\n    -   endforeach\n    --endif\n    - \\ No newline at end of file\n    -+endif\n     \n      ## t/t3450-history.sh (new) ##\n     @@\n    @@ t/t3450-history.sh (new)\n     +\n     +. ./test-lib.sh\n     +\n    -+test_expect_success 'refuses to do anything without subcommand' '\n    -+\ttest_must_fail git history 2>err &&\n    -+\ttest_grep foo err\n    ++test_expect_success 'does nothing without any arguments' '\n    ++\tgit history >out 2>&1 &&\n    ++\ttest_must_be_empty out\n    ++'\n    ++\n    ++test_expect_success 'raises an error with unknown argument' '\n    ++\ttest_must_fail git history garbage 2>err &&\n    ++\ttest_grep \"unrecognized argument: garbage\" err\n     +'\n     +\n     +test_done\n18:  0a1ff48827 !  6:  43af6dc4dc builtin/history: implement \"reword\" subcommand\n    @@ Commit message\n         Signed-off-by: Patrick Steinhardt <ps@pks.im>\n     \n      ## Documentation/git-history.adoc ##\n    -@@ Documentation/git-history.adoc: git history continue\n    - git history quit\n    - git history drop <commit>\n    - git history reorder <commit> (--before=<following-commit>|--after=<preceding-commit>)\n    +@@ Documentation/git-history.adoc: SYNOPSIS\n    + --------\n    + [synopsis]\n    + git history [<options>]\n     +git history reword [<options>] <commit>\n    - git history split [<options>] <commit> [--] [<pathspec>...]\n      \n      DESCRIPTION\n    -@@ Documentation/git-history.adoc: child commits, as that would lead to an empty branch.\n    - \tbe related to one another and must be reachable from the current `HEAD`\n    - \tcommit.\n    + -----------\n    +@@ Documentation/git-history.adoc: COMMANDS\n    + This command requires a subcommand. Several subcommands are available to\n    + rewrite history in different ways:\n      \n     +`reword <commit> [--message=<message>]`::\n     +\tRewrite the commit message of the specified commit. All the other\n    -+\tdetails of this commit remain unchanged.\n    ++\tdetails of this commit remain unchanged. If no commit message is\n    ++\tprovided, then this command will spawn an editor with the current\n    ++\tmessage of that commit.\n     +\n    - `split [--message=<message>] <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    + CONFIGURATION\n    + -------------\n    + \n     \n      ## builtin/history.c ##\n    -@@ builtin/history.c: static int split_commit(struct repository *repo,\n    - \treturn ret;\n    - }\n    - \n    +@@\n    ++#define USE_THE_REPOSITORY_VARIABLE\n    ++\n    + #include \"builtin.h\"\n    ++#include \"commit-reach.h\"\n    ++#include \"commit.h\"\n    ++#include \"config.h\"\n    ++#include \"editor.h\"\n    ++#include \"environment.h\"\n    + #include \"gettext.h\"\n    ++#include \"hex.h\"\n    ++#include \"oidmap.h\"\n    + #include \"parse-options.h\"\n    ++#include \"refs.h\"\n    ++#include \"replay.h\"\n    ++#include \"reset.h\"\n    ++#include \"revision.h\"\n    ++#include \"sequencer.h\"\n    ++#include \"strvec.h\"\n    ++#include \"tree.h\"\n    ++#include \"wt-status.h\"\n    ++\n    ++static int collect_commits(struct repository *repo,\n    ++\t\t\t   struct commit *old_commit,\n    ++\t\t\t   struct commit *new_commit,\n    ++\t\t\t   struct strvec *out)\n    ++{\n    ++\tstruct setup_revision_opt revision_opts = {\n    ++\t\t.assume_dashdash = 1,\n    ++\t};\n    ++\tstruct strvec revisions = STRVEC_INIT;\n    ++\tstruct commit_list *from_list = NULL;\n    ++\tstruct commit *child;\n    ++\tstruct rev_info rev = { 0 };\n    ++\tint ret;\n    ++\n    ++\t/*\n    ++\t * Check that the old commit actually is an ancestor of HEAD. If not\n    ++\t * the whole request becomes nonsensical.\n    ++\t*/\n    ++\tif (old_commit) {\n    ++\t\tcommit_list_insert(old_commit, &from_list);\n    ++\t\tif (!repo_is_descendant_of(repo, new_commit, from_list)) {\n    ++\t\t\tret = error(_(\"commit must be reachable from current HEAD commit\"));\n    ++\t\t\tgoto out;\n    ++\t\t}\n    ++\t}\n    ++\n    ++\trepo_init_revisions(repo, &rev, NULL);\n    ++\tstrvec_push(&revisions, \"\");\n    ++\tstrvec_push(&revisions, oid_to_hex(&new_commit->object.oid));\n    ++\tif (old_commit)\n    ++\t\tstrvec_pushf(&revisions, \"^%s\", oid_to_hex(&old_commit->object.oid));\n    ++\n    ++\tsetup_revisions_from_strvec(&revisions, &rev, &revision_opts);\n    ++\tif (revisions.nr != 1 || prepare_revision_walk(&rev)) {\n    ++\t\tret = error(_(\"revision walk setup failed\"));\n    ++\t\tgoto out;\n    ++\t}\n    ++\n    ++\twhile ((child = get_revision(&rev))) {\n    ++\t\tif (old_commit && !child->parents)\n    ++\t\t\tBUG(\"revision walk did not find child commit\");\n    ++\t\tif (child->parents && child->parents->next) {\n    ++\t\t\tret = error(_(\"cannot rearrange commit history with merges\"));\n    ++\t\t\tgoto out;\n    ++\t\t}\n    ++\n    ++\t\tstrvec_push(out, oid_to_hex(&child->object.oid));\n    ++\n    ++\t\tif (child->parents && old_commit &&\n    ++\t\t    commit_list_contains(old_commit, child->parents))\n    ++\t\t\tbreak;\n    ++\t}\n    ++\n    ++\t/*\n    ++\t * Revisions are in newest-order-first. We have to reverse the\n    ++\t * array though so that we pick the oldest commits first.\n    ++\t */\n    ++\tfor (size_t i = 0, j = out->nr - 1; i < j; i++, j--)\n    ++\t\tSWAP(out->v[i], out->v[j]);\n    ++\n    ++\tret = 0;\n    ++\n    ++out:\n    ++\tfree_commit_list(from_list);\n    ++\tstrvec_clear(&revisions);\n    ++\trelease_revisions(&rev);\n    ++\treset_revision_walk();\n    ++\treturn ret;\n    ++}\n    ++\n    ++static void replace_commits(struct strvec *commits,\n    ++\t\t\t    const struct object_id *commit_to_replace,\n    ++\t\t\t    const struct object_id *replacements,\n    ++\t\t\t    size_t replacements_nr)\n    ++{\n    ++\tchar commit_to_replace_oid[GIT_MAX_HEXSZ + 1];\n    ++\tstruct strvec replacement_oids = STRVEC_INIT;\n    ++\tbool found = false;\n    ++\n    ++\toid_to_hex_r(commit_to_replace_oid, commit_to_replace);\n    ++\tfor (size_t i = 0; i < replacements_nr; i++)\n    ++\t\tstrvec_push(&replacement_oids, oid_to_hex(&replacements[i]));\n    ++\n    ++\tfor (size_t i = 0; i < commits->nr; i++) {\n    ++\t\tif (strcmp(commits->v[i], commit_to_replace_oid))\n    ++\t\t\tcontinue;\n    ++\t\tstrvec_splice(commits, i, 1, replacement_oids.v, replacement_oids.nr);\n    ++\t\tfound = true;\n    ++\t\tbreak;\n    ++\t}\n    ++\tif (!found)\n    ++\t\tBUG(\"could not find commit to replace\");\n    ++\n    ++\tstrvec_clear(&replacement_oids);\n    ++}\n    ++\n    ++static int apply_commits(struct repository *repo,\n    ++\t\t\t const struct strvec *commits,\n    ++\t\t\t struct commit *onto,\n    ++\t\t\t struct commit *orig_head,\n    ++\t\t\t const char *action)\n    ++{\n    ++\tstruct reset_head_opts reset_opts = { 0 };\n    ++\tstruct merge_options merge_opts = { 0 };\n    ++\tstruct merge_result result = { 0 };\n    ++\tstruct strbuf buf = STRBUF_INIT;\n    ++\tkh_oid_map_t *replayed_commits;\n    ++\tint ret;\n    ++\n    ++\treplayed_commits = kh_init_oid_map();\n    ++\n    ++\tinit_basic_merge_options(&merge_opts, repo);\n    ++\tmerge_opts.show_rename_progress = 0;\n    ++\n    ++\tfor (size_t i = 0; i < commits->nr; i++) {\n    ++\t\tstruct object_id commit_id;\n    ++\t\tstruct commit *commit;\n    ++\t\tconst char *end;\n    ++\t\tint hash_result;\n    ++\t\tkhint_t pos;\n    ++\n    ++\t\tif (parse_oid_hex_algop(commits->v[i], &commit_id, &end,\n    ++\t\t\t\t\trepo->hash_algo)) {\n    ++\t\t\tret = error(_(\"invalid object ID: %s\"), commits->v[i]);\n    ++\t\t\tgoto out;\n    ++\t\t}\n    ++\n    ++\t\tcommit = lookup_commit(repo, &commit_id);\n    ++\t\tif (!commit || repo_parse_commit(repo, commit)) {\n    ++\t\t\tret = error(_(\"failed to look up commit: %s\"), oid_to_hex(&commit_id));\n    ++\t\t\tgoto out;\n    ++\t\t}\n    ++\n    ++\t\tif (!onto) {\n    ++\t\t\tonto = commit;\n    ++\t\t\tresult.clean = 1;\n    ++\t\t\tresult.tree = repo_get_commit_tree(repo, commit);\n    ++\t\t} else {\n    ++\t\t\tonto = replay_pick_regular_commit(repo, commit, replayed_commits,\n    ++\t\t\t\t\t\t\t  onto, &merge_opts, &result);\n    ++\t\t\tif (!onto)\n    ++\t\t\t\tbreak;\n    ++\t\t}\n    ++\n    ++\t\tpos = kh_put_oid_map(replayed_commits, commit->object.oid, &hash_result);\n    ++\t\tif (hash_result == 0) {\n    ++\t\t\tret = error(_(\"duplicate rewritten commit: %s\\n\"),\n    ++\t\t\t\t    oid_to_hex(&commit->object.oid));\n    ++\t\t\tgoto out;\n    ++\t\t}\n    ++\t\tkh_value(replayed_commits, pos) = onto;\n    ++\t}\n    ++\n    ++\tif (!result.clean) {\n    ++\t\tret = error(_(\"could not merge\"));\n    ++\t\tgoto out;\n    ++\t}\n    ++\n    ++\treset_opts.oid = &onto->object.oid;\n    ++\tstrbuf_addf(&buf, \"%s: switch to rewritten %s\", action, oid_to_hex(reset_opts.oid));\n    ++\treset_opts.flags = RESET_HEAD_REFS_ONLY | RESET_ORIG_HEAD;\n    ++\treset_opts.orig_head = &orig_head->object.oid;\n    ++\treset_opts.default_reflog_action = action;\n    ++\tif (reset_head(repo, &reset_opts) < 0) {\n    ++\t\tret = error(_(\"could not switch to %s\"), oid_to_hex(reset_opts.oid));\n    ++\t\tgoto out;\n    ++\t}\n    ++\n    ++\tret = 0;\n    ++\n    ++out:\n    ++\tkh_destroy_oid_map(replayed_commits);\n    ++\tmerge_finalize(&merge_opts, &result);\n    ++\tstrbuf_release(&buf);\n    ++\treturn ret;\n    ++}\n    ++\n    ++static void change_data_free(void *util, const char *str UNUSED)\n    ++{\n    ++\tstruct wt_status_change_data *d = util;\n    ++\tfree(d->rename_source);\n    ++\tfree(d);\n    ++}\n    ++\n    ++static int fill_commit_message(struct repository *repo,\n    ++\t\t\t       const struct object_id *old_tree,\n    ++\t\t\t       const struct object_id *new_tree,\n    ++\t\t\t       const char *default_message,\n    ++\t\t\t       const char *provided_message,\n    ++\t\t\t       const char *action,\n    ++\t\t\t       struct strbuf *out)\n    ++{\n    ++\tif (!provided_message) {\n    ++\t\tconst char *path = git_path_commit_editmsg();\n    ++\t\tconst char *hint =\n    ++\t\t\t_(\"Please enter the commit message for the %s changes. Lines starting\\n\"\n    ++     \"with '%s' will be kept; you may remove them yourself if you want to.\\n\");\n    ++\t\tstruct wt_status s;\n    ++\n    ++\t\tstrbuf_addstr(out, default_message);\n    ++\t\tstrbuf_addch(out, '\\n');\n    ++\t\tstrbuf_commented_addf(out, comment_line_str, hint, action, comment_line_str);\n    ++\t\twrite_file_buf(path, out->buf, out->len);\n    ++\n    ++\t\twt_status_prepare(repo, &s);\n    ++\t\tFREE_AND_NULL(s.branch);\n    ++\t\ts.ahead_behind_flags = AHEAD_BEHIND_QUICK;\n    ++\t\ts.commit_template = 1;\n    ++\t\ts.colopts = 0;\n    ++\t\ts.display_comment_prefix = 1;\n    ++\t\ts.hints = 0;\n    ++\t\ts.use_color = 0;\n    ++\t\ts.whence = FROM_COMMIT;\n    ++\t\ts.committable = 1;\n    ++\n    ++\t\ts.fp = fopen(git_path_commit_editmsg(), \"a\");\n    ++\t\tif (!s.fp)\n    ++\t\t\treturn error_errno(_(\"could not open '%s'\"), git_path_commit_editmsg());\n    ++\n    ++\t\twt_status_collect_changes_trees(&s, old_tree, new_tree);\n    ++\t\twt_status_print(&s);\n    ++\t\twt_status_collect_free_buffers(&s);\n    ++\t\tstring_list_clear_func(&s.change, change_data_free);\n    ++\n    ++\t\tstrbuf_reset(out);\n    ++\t\tif (launch_editor(path, out, NULL)) {\n    ++\t\t\tfprintf(stderr, _(\"Please supply the message using the -m option.\\n\"));\n    ++\t\t\treturn -1;\n    ++\t\t}\n    ++\t\tstrbuf_stripspace(out, comment_line_str);\n    ++\t} else {\n    ++\t\tstrbuf_addstr(out, provided_message);\n    ++\t}\n    ++\n    ++\tcleanup_message(out, COMMIT_MSG_CLEANUP_ALL, 0);\n    ++\n    ++\tif (!out->len) {\n    ++\t\tfprintf(stderr, _(\"Aborting commit due to empty commit message.\\n\"));\n    ++\t\treturn -1;\n    ++\t}\n    ++\n    ++\treturn 0;\n    ++}\n    ++\n     +static int cmd_history_reword(int argc,\n     +\t\t\t      const char **argv,\n     +\t\t\t      const char *prefix,\n    @@ builtin/history.c: static int split_commit(struct repository *repo,\n     +\t\tOPT_END(),\n     +\t};\n     +\tstruct strbuf final_message = STRBUF_INIT;\n    -+\tstruct commit *original_commit, *head;\n    ++\tstruct commit *original_commit, *parent, *head;\n     +\tstruct strvec commits = STRVEC_INIT;\n     +\tstruct object_id parent_tree_oid, original_commit_tree_oid;\n     +\tstruct object_id rewritten_commit;\n     +\tconst char *original_message, *original_body, *ptr;\n    -+\tstruct oidmap rewritten_commits = OIDMAP_INIT;\n    -+\tstruct replay_oid_mapping mapping = { 0 };\n     +\tchar *original_author = NULL;\n     +\tsize_t len;\n     +\tint ret;\n    @@ builtin/history.c: static int split_commit(struct repository *repo,\n     +\t\tret = error(_(\"commit to be reworded cannot be found: %s\"), argv[0]);\n     +\t\tgoto out;\n     +\t}\n    ++\tif (repo_parse_commit(repo, original_commit)) {\n    ++\t\tret = error(_(\"unable to parse commit %s\"),\n    ++\t\t\t    oid_to_hex(&original_commit->object.oid));\n    ++\t\tgoto out;\n    ++\t}\n    ++\toriginal_commit_tree_oid = repo_get_commit_tree(repo, original_commit)->object.oid;\n    ++\n    ++\tparent = original_commit->parents ? original_commit->parents->item : NULL;\n    ++\tif (parent) {\n    ++\t\tif (repo_parse_commit(repo, parent)) {\n    ++\t\t\tret = error(_(\"unable to parse commit %s\"),\n    ++\t\t\t\t    oid_to_hex(&parent->object.oid));\n    ++\t\t\tgoto out;\n    ++\t\t}\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     +\thead = lookup_commit_reference_by_name(\"HEAD\");\n     +\tif (!head) {\n    @@ builtin/history.c: static int split_commit(struct repository *repo,\n     +\t * This ensures that we'll abort early on in case the range of commits\n     +\t * contains merges, which we do not yet handle.\n     +\t */\n    -+\tret = collect_commits(repo, original_commit->parents ? original_commit->parents->item : NULL,\n    -+\t\t\t      head, &commits);\n    ++\tret = collect_commits(repo, parent, head, &commits);\n     +\tif (ret < 0)\n     +\t\tgoto out;\n     +\n    @@ builtin/history.c: static int split_commit(struct repository *repo,\n     +\t\toriginal_author = xmemdupz(ptr, len);\n     +\tfind_commit_subject(original_message, &original_body);\n     +\n    -+\tif (original_commit->parents)\n    -+\t\tparent_tree_oid = *get_commit_tree_oid(original_commit->parents->item);\n    -+\telse\n    -+\t\toidcpy(&parent_tree_oid, repo->hash_algo->empty_tree);\n    -+\toriginal_commit_tree_oid = *get_commit_tree_oid(original_commit);\n    -+\n     +\tret = fill_commit_message(repo, &parent_tree_oid, &original_commit_tree_oid,\n     +\t\t\t\t  original_body, commit_message, \"reworded\", &final_message);\n     +\tif (ret < 0)\n    @@ builtin/history.c: static int split_commit(struct repository *repo,\n     +\n     +\treplace_commits(&commits, &original_commit->object.oid, &rewritten_commit, 1);\n     +\n    -+\tmapping.entry.oid = rewritten_commit;\n    -+\tmapping.rewritten_oid = original_commit->object.oid;\n    -+\toidmap_put(&rewritten_commits, &mapping);\n    -+\n    -+\tret = apply_commits(repo, &commits, head, original_commit,\n    -+\t\t\t    &rewritten_commits, \"reword\");\n    ++\tret = apply_commits(repo, &commits, parent, head, \"reword\");\n     +\tif (ret < 0)\n     +\t\tgoto out;\n     +\n     +\tret = 0;\n     +\n     +out:\n    -+\toidmap_clear(&rewritten_commits, 0);\n     +\tstrbuf_release(&final_message);\n     +\tstrvec_clear(&commits);\n     +\tfree(original_author);\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    -@@ builtin/history.c: int cmd_history(int argc,\n    - \t\tN_(\"git history quit\"),\n    - \t\tN_(\"git history drop <commit>\"),\n    - \t\tN_(\"git history reorder <commit> (--before=<following-commit>|--after=<preceding-commit>)\"),\n    + \n    + int cmd_history(int argc,\n    + \t\tconst char **argv,\n    + \t\tconst char *prefix,\n    +-\t\tstruct repository *repo UNUSED)\n    ++\t\tstruct repository *repo)\n    + {\n    + \tconst char * const usage[] = {\n    + \t\tN_(\"git history [<options>]\"),\n     +\t\tN_(\"git history reword [<options>] <commit>\"),\n    - \t\tN_(\"git history split [<options>] <commit> [--] [<pathspec>...]\"),\n      \t\tNULL,\n      \t};\n    -@@ builtin/history.c: int cmd_history(int argc,\n    - \t\tOPT_SUBCOMMAND(\"quit\", &fn, cmd_history_quit),\n    - \t\tOPT_SUBCOMMAND(\"drop\", &fn, cmd_history_drop),\n    - \t\tOPT_SUBCOMMAND(\"reorder\", &fn, cmd_history_reorder),\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    + \n    + \targc = parse_options(argc, argv, prefix, options, usage, 0);\n    +-\tif (argc)\n    +-\t\tusagef(\"unrecognized argument: %s\", argv[0]);\n    +-\treturn 0;\n    ++\treturn fn(argc, argv, prefix, repo);\n    + }\n     \n      ## t/meson.build ##\n     @@ t/meson.build: integration_tests = [\n    -   't3451-history-drop.sh',\n    -   't3452-history-reorder.sh',\n    -   't3453-history-split.sh',\n    -+  't3454-history-reword.sh',\n    +   't3437-rebase-fixup-options.sh',\n    +   't3438-rebase-broken-files.sh',\n    +   't3450-history.sh',\n    ++  't3451-history-reword.sh',\n        't3500-cherry.sh',\n        't3501-revert-cherry-pick.sh',\n        't3502-cherry-pick-merge.sh',\n     \n    - ## t/t3454-history-reword.sh (new) ##\n    + ## t/t3450-history.sh ##\n    +@@ t/t3450-history.sh: test_description='tests for git-history command'\n    + . ./test-lib.sh\n    + \n    + test_expect_success 'does nothing without any arguments' '\n    +-\tgit history >out 2>&1 &&\n    +-\ttest_must_be_empty out\n    ++\ttest_must_fail git history 2>err &&\n    ++\ttest_grep \"need a subcommand\" err\n    + '\n    + \n    + test_expect_success 'raises an error with unknown argument' '\n    + \ttest_must_fail git history garbage 2>err &&\n    +-\ttest_grep \"unrecognized argument: garbage\" err\n    ++\ttest_grep \"unknown subcommand: .garbage.\" err\n    + '\n    + \n    + test_done\n    +\n    + ## t/t3451-history-reword.sh (new) ##\n     @@\n     +#!/bin/sh\n     +\n    @@ t/t3454-history-reword.sh (new)\n     +\t)\n     +'\n     +\n    -+test_expect_success 'refuses to work with changes in the worktree or index' '\n    -+\ttest_when_finished \"rm -rf repo\" &&\n    -+\tgit init repo &&\n    -+\t(\n    -+\t\tcd repo &&\n    -+\t\ttest_commit base file &&\n    -+\t\techo foo >file &&\n    -+\t\ttest_must_fail git history reword HEAD 2>err &&\n    -+\t\ttest_grep \"Your local changes to the following files would be overwritten\" err &&\n    -+\t\tgit add file &&\n    -+\t\ttest_must_fail git history reword HEAD 2>err &&\n    -+\t\ttest_grep \"Your local changes to the following files would be overwritten\" err\n    -+\t)\n    -+'\n    -+\n     +test_expect_success 'can reword tip of a branch' '\n     +\ttest_when_finished \"rm -rf repo\" &&\n     +\tgit init repo &&\n    @@ t/t3454-history-reword.sh (new)\n     +\t)\n     +'\n     +\n    ++# For now, git-history(1) does not yet execute any hooks. This is subject to\n    ++# change in the future, and if it does this test here is expected to start\n    ++# failing. In other words, this test is not an endorsement of the current\n    ++# status quo.\n     +test_expect_success 'hooks are executed for rewritten commits' '\n     +\ttest_when_finished \"rm -rf repo\" &&\n     +\tgit init repo &&\n    @@ t/t3454-history-reword.sh (new)\n     +\t\ttest_commit third &&\n     +\n     +\t\twrite_script .git/hooks/prepare-commit-msg <<-EOF &&\n    -+\t\techo \"prepare-commit-msg: \\$@\" >>\"$(pwd)/hooks.log\"\n    ++\t\ttouch \"$(pwd)/hooks.log\n     +\t\tEOF\n     +\t\twrite_script .git/hooks/post-commit <<-EOF &&\n    -+\t\techo \"post-commit\" >>\"$(pwd)/hooks.log\"\n    ++\t\ttouch \"$(pwd)/hooks.log\n     +\t\tEOF\n     +\t\twrite_script .git/hooks/post-rewrite <<-EOF &&\n    -+\t\t{\n    -+\t\t\techo \"post-rewrite: \\$@\"\n    -+\t\t\tcat\n    -+\t\t} >>\"$(pwd)/hooks.log\"\n    ++\t\ttouch \"$(pwd)/hooks.log\n     +\t\tEOF\n     +\n     +\t\tgit history reword -m \"second reworded\" HEAD~ &&\n    @@ t/t3454-history-reword.sh (new)\n     +\t\tgit log --format=%s >actual &&\n     +\t\ttest_cmp expect actual &&\n     +\n    -+\t\tcat >expect <<-EOF &&\n    -+\t\tprepare-commit-msg: .git/COMMIT_EDITMSG message\n    -+\t\tpost-commit\n    -+\t\tprepare-commit-msg: .git/COMMIT_EDITMSG message\n    -+\t\tpost-commit\n    -+\t\tpost-rewrite: history\n    -+\t\t$(git rev-parse second) $(git rev-parse HEAD~)\n    -+\t\t$(git rev-parse third) $(git rev-parse HEAD)\n    -+\t\tEOF\n    -+\t\ttest_cmp expect hooks.log\n    ++\t\ttest_path_is_missing hooks.log\n     +\t)\n     +'\n     +\n    @@ t/t3454-history-reword.sh (new)\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\ttouch a b &&\n    ++\t\tgit add . &&\n    ++\t\tgit commit -m \"initial commit\" &&\n    ++\t\techo foo >a &&\n    ++\t\techo bar >b &&\n    ++\t\tgit add b &&\n    ++\t\tgit history reword HEAD -m message &&\n    ++\t\tcat >expect <<-\\EOF &&\n    ++\t\t M a\n    ++\t\tM  b\n    ++\t\t?? actual\n    ++\t\t?? expect\n    ++\t\tEOF\n    ++\t\tgit status --porcelain >actual &&\n    ++\t\ttest_cmp expect actual\n    ++\t)\n    ++'\n    ++\n     +test_done\n11:  01cab3d8a5 =  7:  62e11acdfe add-patch: split out header from \"add-interactive.h\"\n12:  6726e2ee02 !  8:  e0b65245fb add-patch: split out `struct interactive_options`\n    @@ add-interactive.c\n      #include \"prompt.h\"\n      #include \"tree.h\"\n      \n    --static void init_color(struct repository *r, struct add_i_state *s,\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 (!s->use_color)\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    @@ add-interactive.c\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    --\tconst char *value;\n    --\n      \ts->r = r;\n     -\ts->context = -1;\n     -\ts->interhunkcontext = -1;\n     -\n    --\tif (repo_config_get_value(r, \"color.interactive\", &value))\n    --\t\ts->use_color = -1;\n    --\telse\n    --\t\ts->use_color =\n    --\t\t\tgit_config_colorbool(\"color.interactive\", value);\n    --\ts->use_color = want_color(s->use_color);\n    +-\ts->use_color_interactive = check_color_config(r, \"color.interactive\");\n     -\n    --\tinit_color(r, s, \"interactive.header\", s->header_color, GIT_COLOR_BOLD);\n    --\tinit_color(r, s, \"interactive.help\", s->help_color, GIT_COLOR_BOLD_RED);\n    --\tinit_color(r, s, \"interactive.prompt\", s->prompt_color,\n    --\t\t   GIT_COLOR_BOLD_BLUE);\n    --\tinit_color(r, s, \"interactive.error\", s->error_color,\n    --\t\t   GIT_COLOR_BOLD_RED);\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    --\tinit_color(r, s, \"diff.frag\", s->fraginfo_color,\n    --\t\t   diff_get_color(s->use_color, DIFF_FRAGINFO));\n    --\tinit_color(r, s, \"diff.context\", s->context_color, \"fall back\");\n    --\tif (!strcmp(s->context_color, \"fall back\"))\n    --\t\tinit_color(r, s, \"diff.plain\", s->context_color,\n    --\t\t\t   diff_get_color(s->use_color, DIFF_CONTEXT));\n    --\tinit_color(r, s, \"diff.old\", s->file_old_color,\n    --\t\tdiff_get_color(s->use_color, DIFF_FILE_OLD));\n    --\tinit_color(r, s, \"diff.new\", s->file_new_color,\n    --\t\tdiff_get_color(s->use_color, DIFF_FILE_NEW));\n    +-\ts->use_color_diff = check_color_config(r, \"color.diff\");\n     -\n    --\tstrlcpy(s->reset_color,\n    --\t\ts->use_color ? GIT_COLOR_RESET : \"\", COLOR_MAXLEN);\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    @@ add-interactive.c\n     -\tFREE_AND_NULL(s->interactive_diff_algorithm);\n     +\tinteractive_config_clear(&s->cfg);\n      \tmemset(s, 0, sizeof(*s));\n    --\ts->use_color = -1;\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    @@ add-interactive.c: int run_add_i(struct repository *r, const struct pathspec *ps\n      \t * When color was asked for, use the prompt color for\n      \t * highlighting, otherwise use square brackets.\n      \t */\n    --\tif (s.use_color) {\n    +-\tif (want_color(s.use_color_interactive)) {\n     -\t\tdata.color = s.prompt_color;\n    --\t\tdata.reset = s.reset_color;\n    -+\tif (s.cfg.use_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;\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;\n    @@ add-interactive.h\n      \n      struct add_i_state {\n      \tstruct repository *r;\n    --\tint use_color;\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[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    @@ add-patch.c: struct add_p_state {\n      };\n      \n     +static void init_color(struct repository *r,\n    -+\t\t       struct interactive_config *cfg,\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 (!cfg->use_color)\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    @@ add-patch.c: struct add_p_state {\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    -+\tconst char *value;\n    -+\n     +\tcfg->context = -1;\n     +\tcfg->interhunkcontext = -1;\n     +\n    -+\tif (repo_config_get_value(r, \"color.interactive\", &value))\n    -+\t\tcfg->use_color = -1;\n    -+\telse\n    -+\t\tcfg->use_color =\n    -+\t\t\tgit_config_colorbool(\"color.interactive\", value);\n    -+\tcfg->use_color = want_color(cfg->use_color);\n    ++\tcfg->use_color_interactive = check_color_config(r, \"color.interactive\");\n     +\n    -+\tinit_color(r, cfg, \"interactive.header\", cfg->header_color, GIT_COLOR_BOLD);\n    -+\tinit_color(r, cfg, \"interactive.help\", cfg->help_color, GIT_COLOR_BOLD_RED);\n    -+\tinit_color(r, cfg, \"interactive.prompt\", cfg->prompt_color,\n    -+\t\t   GIT_COLOR_BOLD_BLUE);\n    -+\tinit_color(r, cfg, \"interactive.error\", cfg->error_color,\n    -+\t\t   GIT_COLOR_BOLD_RED);\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    -+\tinit_color(r, cfg, \"diff.frag\", cfg->fraginfo_color,\n    -+\t\t   diff_get_color(cfg->use_color, DIFF_FRAGINFO));\n    -+\tinit_color(r, cfg, \"diff.context\", cfg->context_color, \"fall back\");\n    -+\tif (!strcmp(cfg->context_color, \"fall back\"))\n    -+\t\tinit_color(r, cfg, \"diff.plain\", cfg->context_color,\n    -+\t\t\t   diff_get_color(cfg->use_color, DIFF_CONTEXT));\n    -+\tinit_color(r, cfg, \"diff.old\", cfg->file_old_color,\n    -+\t\tdiff_get_color(cfg->use_color, DIFF_FILE_OLD));\n    -+\tinit_color(r, cfg, \"diff.new\", cfg->file_new_color,\n    -+\t\tdiff_get_color(cfg->use_color, DIFF_FILE_NEW));\n    ++\tcfg->use_color_diff = check_color_config(r, \"color.diff\");\n     +\n    -+\tstrlcpy(cfg->reset_color,\n    -+\t\tcfg->use_color ? GIT_COLOR_RESET : \"\", COLOR_MAXLEN);\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    @@ add-patch.c: struct add_p_state {\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 = -1;\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    @@ add-patch.c: static void err(struct add_p_state *s, const char *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);\n    -+\tputs(s->s.cfg.reset_color);\n    +-\tputs(s->s.reset_color_interactive);\n    ++\tputs(s->s.cfg.reset_color_interactive);\n      \tva_end(args);\n      }\n      \n    @@ add-patch.c: static int parse_diff(struct add_p_state *s, const struct pathspec\n      \t\tstruct object_id oid;\n      \t\tstrvec_push(&args,\n     @@ add-patch.c: 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, -1)) {\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    @@ add-patch.c: 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);\n    -+\t\t\tstrbuf_addf(out, \"%s\\n\", s->s.cfg.reset_color);\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    @@ add-patch.c: static void recolor_hunk(struct add_p_state *s, struct hunk *hunk)\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);\n    -+\t\tstrbuf_addstr(&s->colored, s->s.cfg.reset_color);\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    @@ add-patch.c: static int patch_update_file(struct add_p_state *s,\n      \t\t\t\t\t\t: 1));\n      \t\tprintf(_(s->mode->prompt_mode[prompt_mode_type]),\n      \t\t       s->buf.buf);\n    --\t\tif (*s->s.reset_color)\n    --\t\t\tfputs(s->s.reset_color, stdout);\n    -+\t\tif (*s->s.cfg.reset_color)\n    -+\t\t\tfputs(s->s.cfg.reset_color, stdout);\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\tbreak;\n    @@ add-patch.h\n     +}\n     +\n     +struct interactive_config {\n    -+\tint use_color;\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[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;\n13:  fe853dff04 !  9:  3fef9306d3 add-patch: remove dependency on \"add-interactive\" subsystem\n    @@ add-patch.c: static void err(struct add_p_state *s, const char *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);\n    -+\tputs(s->cfg.reset_color);\n    +-\tputs(s->s.cfg.reset_color_interactive);\n    ++\tputs(s->cfg.reset_color_interactive);\n      \tva_end(args);\n      }\n      \n    @@ add-patch.c: static int parse_diff(struct add_p_state *s, const struct pathspec\n      \t\tstruct object_id oid;\n      \t\tstrvec_push(&args,\n     @@ add-patch.c: 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, -1)) {\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    @@ add-patch.c: 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);\n    -+\t\t\tstrbuf_addf(out, \"%s\\n\", s->cfg.reset_color);\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    @@ add-patch.c: static void recolor_hunk(struct add_p_state *s, struct hunk *hunk)\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);\n    -+\t\tstrbuf_addstr(&s->colored, s->cfg.reset_color);\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    @@ add-patch.c: static int patch_update_file(struct add_p_state *s,\n      \t\t\t\t\t\t: 1));\n      \t\tprintf(_(s->mode->prompt_mode[prompt_mode_type]),\n      \t\t       s->buf.buf);\n    --\t\tif (*s->s.cfg.reset_color)\n    --\t\t\tfputs(s->s.cfg.reset_color, stdout);\n    -+\t\tif (*s->cfg.reset_color)\n    -+\t\t\tfputs(s->cfg.reset_color, stdout);\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\tbreak;\n14:  37f6cc2a69 = 10:  9948b99601 add-patch: add support for in-memory index patching\n 6:  e18b8b86c0 = 11:  19dfc229c9 cache-tree: allow writing in-memory index as tree\n 8:  8c6179ef85 <  -:  ---------- builtin/history: introduce subcommands to manage interrupted rewrites\n 9:  f5c931a657 <  -:  ---------- builtin/history: implement \"drop\" subcommand\n10:  3ab680968c <  -:  ---------- builtin/history: implement \"reorder\" subcommand\n16:  a66825ed24 <  -:  ---------- sequencer: allow callers to provide mappings for the old commit\n17:  731b243497 ! 12:  f72e364f92 builtin/history: implement \"split\" subcommand\n    @@ Commit message\n         Signed-off-by: Patrick Steinhardt <ps@pks.im>\n     \n      ## Documentation/git-history.adoc ##\n    -@@ Documentation/git-history.adoc: git history continue\n    - git history quit\n    - git history drop <commit>\n    - git history reorder <commit> (--before=<following-commit>|--after=<preceding-commit>)\n    +@@ Documentation/git-history.adoc: SYNOPSIS\n    + [synopsis]\n    + git history [<options>]\n    + git history reword [<options>] <commit>\n     +git history split [<options>] <commit> [--] [<pathspec>...]\n      \n      DESCRIPTION\n      -----------\n    -@@ Documentation/git-history.adoc: child commits, as that would lead to an empty branch.\n    - \tbe related to one another and must be reachable from the current `HEAD`\n    - \tcommit.\n    +@@ Documentation/git-history.adoc: rewrite history in different ways:\n    + \tprovided, then this command will spawn an editor with the current\n    + \tmessage of that commit.\n      \n     +`split [--message=<message>] <commit> [--] [<pathspec>...]`::\n     +\tInteractively split up <commit> into two commits by choosing\n    @@ Documentation/git-history.adoc: child commits, as that would lead to an empty br\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    - The following commands are used to manage an interrupted history-rewriting\n    - operation:\n    + CONFIGURATION\n    + -------------\n      \n    -@@ Documentation/git-history.adoc: f44a46e third\n    - bf7438d first\n    - ----------\n    +@@ Documentation/git-history.adoc: include::includes/cmd-config-section-all.adoc[]\n      \n    + include::config/sequencer.adoc[]\n    + \n    ++EXAMPLES\n    ++--------\n    ++\n     +Split a commit\n     +~~~~~~~~~~~~~~\n     +\n    @@ -0,0 +1 @@\n     + 1 file changed, 1 insertion(+)\n     +----------\n     +\n    -+\n    - CONFIGURATION\n    - -------------\n    - \n    + GIT\n    + ---\n    + Part of the linkgit:git[1] suite\n     \n      ## builtin/history.c ##\n     @@\n    -+/* Required for `comment_line_str`. */\n    -+#define USE_THE_REPOSITORY_VARIABLE\n    -+\n    + #define USE_THE_REPOSITORY_VARIABLE\n    + \n      #include \"builtin.h\"\n    - #include \"branch.h\"\n     +#include \"cache-tree.h\"\n    - #include \"commit.h\"\n      #include \"commit-reach.h\"\n    + #include \"commit.h\"\n      #include \"config.h\"\n    -+#include \"editor.h\"\n    - #include \"environment.h\"\n    - #include \"gettext.h\"\n    +@@\n      #include \"hex.h\"\n    - #include \"object-name.h\"\n    + #include \"oidmap.h\"\n      #include \"parse-options.h\"\n     +#include \"path.h\"\n    -+#include \"pathspec.h\"\n    -+#include \"read-cache-ll.h\"\n    ++#include \"read-cache.h\"\n      #include \"refs.h\"\n    + #include \"replay.h\"\n      #include \"reset.h\"\n      #include \"revision.h\"\n     +#include \"run-command.h\"\n      #include \"sequencer.h\"\n    -+#include \"sparse-index.h\"\n    - \n    - static int cmd_history_abort(int argc,\n    - \t\t\t     const char **argv,\n    -@@ builtin/history.c: static int apply_commits(struct repository *repo,\n    - \t\t\t const struct strvec *commits,\n    - \t\t\t struct commit *head,\n    - \t\t\t struct commit *base,\n    -+\t\t\t const struct oidmap *rewritten_commits,\n    - \t\t\t const char *action)\n    - {\n    - \tstruct setup_revision_opt revision_opts = {\n    -@@ builtin/history.c: static int apply_commits(struct repository *repo,\n    - \t\treplay_opts.strategy = replay_opts.default_strategy;\n    - \t\treplay_opts.default_strategy = NULL;\n    - \t}\n    -+\treplay_opts.old_oid_mappings = rewritten_commits;\n    - \n    - \tstrvec_push(&args, \"\");\n    - \tstrvec_pushv(&args, commits->v);\n    -@@ builtin/history.c: static int cmd_history_drop(int argc,\n    - \t\tif (ret < 0)\n    - \t\t\tgoto out;\n    - \n    --\t\tret = apply_commits(repo, &commits, head, commit_to_drop, \"drop\");\n    -+\t\tret = apply_commits(repo, &commits, head, commit_to_drop,\n    -+\t\t\t\t    NULL, \"drop\");\n    - \t\tif (ret < 0)\n    - \t\t\tgoto out;\n    - \t}\n    -@@ builtin/history.c: static int cmd_history_reorder(int argc,\n    - \treplace_commits(&commits, &commit_to_reorder->object.oid, NULL, 0);\n    - \treplace_commits(&commits, &anchor->object.oid, replacement, ARRAY_SIZE(replacement));\n    - \n    --\tret = apply_commits(repo, &commits, head, old, \"reorder\");\n    -+\tret = apply_commits(repo, &commits, head, old, NULL, \"reorder\");\n    - \tif (ret < 0)\n    - \t\tgoto out;\n    - \n    -@@ builtin/history.c: static int cmd_history_reorder(int argc,\n    + #include \"strvec.h\"\n    + #include \"tree.h\"\n    +@@ builtin/history.c: static int cmd_history_reword(int argc,\n      \treturn ret;\n      }\n      \n    -+static void change_data_free(void *util, const char *str UNUSED)\n    -+{\n    -+\tstruct wt_status_change_data *d = util;\n    -+\tfree(d->rename_source);\n    -+\tfree(d);\n    -+}\n    -+\n    -+static int fill_commit_message(struct repository *repo,\n    -+\t\t\t       const struct object_id *old_tree,\n    -+\t\t\t       const struct object_id *new_tree,\n    -+\t\t\t       const char *default_message,\n    -+\t\t\t       const char *provided_message,\n    -+\t\t\t       const char *action,\n    -+\t\t\t       struct strbuf *out)\n    -+{\n    -+\tif (!provided_message) {\n    -+\t\tconst char *path = git_path_commit_editmsg();\n    -+\t\tconst char *hint =\n    -+\t\t\t_(\"Please enter the commit message for the %s changes. Lines starting\\n\"\n    -+\t\t\t  \"with '%s' will be kept; you may remove them yourself if you want to.\\n\");\n    -+\t\tint verbose = 1;\n    -+\n    -+\t\tstrbuf_addstr(out, default_message);\n    -+\t\tstrbuf_addch(out, '\\n');\n    -+\t\tstrbuf_commented_addf(out, comment_line_str, hint, action, comment_line_str);\n    -+\t\twrite_file_buf(path, out->buf, out->len);\n    -+\n    -+\t\trepo_config_get_bool(repo, \"commit.verbose\", &verbose);\n    -+\t\tif (verbose) {\n    -+\t\t\tstruct wt_status s;\n    -+\n    -+\t\t\twt_status_prepare(repo, &s);\n    -+\t\t\tFREE_AND_NULL(s.branch);\n    -+\t\t\ts.ahead_behind_flags = AHEAD_BEHIND_QUICK;\n    -+\t\t\ts.commit_template = 1;\n    -+\t\t\ts.colopts = 0;\n    -+\t\t\ts.display_comment_prefix = 1;\n    -+\t\t\ts.hints = 0;\n    -+\t\t\ts.use_color = 0;\n    -+\t\t\ts.whence = FROM_COMMIT;\n    -+\t\t\ts.committable = 1;\n    -+\n    -+\t\t\ts.fp = fopen(git_path_commit_editmsg(), \"a\");\n    -+\t\t\tif (!s.fp)\n    -+\t\t\t\treturn error_errno(_(\"could not open '%s'\"), git_path_commit_editmsg());\n    -+\n    -+\t\t\twt_status_collect_changes_trees(&s, old_tree, new_tree);\n    -+\t\t\twt_status_print(&s);\n    -+\t\t\twt_status_collect_free_buffers(&s);\n    -+\t\t\tstring_list_clear_func(&s.change, change_data_free);\n    -+\t\t}\n    -+\n    -+\t\tstrbuf_reset(out);\n    -+\t\tif (launch_editor(path, out, NULL)) {\n    -+\t\t\tfprintf(stderr, _(\"Please supply the message using the -m option.\\n\"));\n    -+\t\t\treturn -1;\n    -+\t\t}\n    -+\t\tstrbuf_stripspace(out, comment_line_str);\n    -+\t} else {\n    -+\t\tstrbuf_addstr(out, provided_message);\n    -+\t}\n    -+\n    -+\tcleanup_message(out, COMMIT_MSG_CLEANUP_ALL, 0);\n    -+\n    -+\tif (!out->len) {\n    -+\t\tfprintf(stderr, _(\"Aborting commit due to empty commit message.\\n\"));\n    -+\t\treturn -1;\n    -+\t}\n    -+\n    -+\treturn 0;\n    -+}\n    -+\n     +static int split_commit(struct repository *repo,\n     +\t\t\tstruct commit *original_commit,\n     +\t\t\tstruct pathspec *pathspec,\n    @@ builtin/history.c: static int cmd_history_reorder(int argc,\n     +\toriginal_commit_tree_oid = *get_commit_tree_oid(original_commit);\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    ++\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     +\tread_tree_cmd.git_cmd = 1;\n    @@ builtin/history.c: static int cmd_history_reorder(int argc,\n     +\tunlink(index_file.buf);\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    ++\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    @@ builtin/history.c: static int cmd_history_reorder(int argc,\n     +\t}\n     +\n     +\t/*\n    -+\t * The second commit is much simpler to construct, as we can simply use\n    -+\t * the original commit details, except that we adjust its parent to be\n    -+\t * the newly split-out commit.\n    -+\t */\n    ++\t* The second commit is much simpler to construct, as we can simply use\n    ++\t* the original commit details, except that we adjust its parent to be\n    ++\t* the newly split-out commit.\n    ++\t*/\n     +\tfind_commit_subject(original_message, &original_body);\n     +\tfirst_commit = lookup_commit_reference(repo, &out[0]);\n     +\tcommit_list_append(first_commit, &parents);\n    @@ builtin/history.c: static int cmd_history_reorder(int argc,\n     +\t\tOPT_END(),\n     +\t};\n     +\tstruct oidmap rewritten_commits = OIDMAP_INIT;\n    -+\tstruct commit *original_commit, *head;\n    ++\tstruct commit *original_commit, *parent, *head;\n     +\tstruct strvec commits = STRVEC_INIT;\n     +\tstruct commit_list *list = NULL;\n     +\tstruct object_id split_commits[2];\n    -+\tstruct replay_oid_mapping mapping[2] = { 0 };\n     +\tstruct pathspec pathspec = { 0 };\n     +\tint ret;\n     +\n    @@ builtin/history.c: static int cmd_history_reorder(int argc,\n     +\t\tgoto out;\n     +\t}\n     +\n    ++\tparent = original_commit->parents ? original_commit->parents->item : NULL;\n    ++\tif (parent && repo_parse_commit(repo, parent)) {\n    ++\t\tret = error(_(\"unable to parse commit %s\"),\n    ++\t\t\t    oid_to_hex(&parent->object.oid));\n    ++\t\tgoto out;\n    ++\t}\n    ++\n     +\thead = lookup_commit_reference_by_name(\"HEAD\");\n     +\tif (!head) {\n     +\t\tret = error(_(\"could not resolve HEAD to a commit\"));\n    @@ builtin/history.c: static int cmd_history_reorder(int argc,\n     +\t}\n     +\n     +\tparse_pathspec(&pathspec, 0,\n    -+\t\t       PATHSPEC_PREFER_FULL | PATHSPEC_SYMLINK_LEADING_PATH | PATHSPEC_PREFIX_ORIGIN,\n    -+\t\t       prefix, argv + 1);\n    ++\t\tPATHSPEC_PREFER_FULL | PATHSPEC_SYMLINK_LEADING_PATH | PATHSPEC_PREFIX_ORIGIN,\n    ++\t\tprefix, argv + 1);\n     +\n     +\t/*\n    -+\t * Collect the list of commits that we'll have to reapply now already.\n    -+\t * This ensures that we'll abort early on in case the range of commits\n    -+\t * contains merges, which we do not yet handle.\n    -+\t */\n    -+\tret = collect_commits(repo, original_commit->parents ? original_commit->parents->item : NULL,\n    -+\t\t\t      head, &commits);\n    ++\t* Collect the list of commits that we'll have to reapply now already.\n    ++\t* This ensures that we'll abort early on in case the range of commits\n    ++\t* contains merges, which we do not yet handle.\n    ++\t*/\n    ++\tret = collect_commits(repo, parent, head, &commits);\n     +\tif (ret < 0)\n     +\t\tgoto out;\n     +\n    @@ builtin/history.c: static int cmd_history_reorder(int argc,\n     +\tif (ret < 0)\n     +\t\tgoto out;\n     +\n    -+\tmapping[0].entry.oid = split_commits[0];\n    -+\tmapping[0].rewritten_oid = original_commit->object.oid;\n    -+\toidmap_put(&rewritten_commits, &mapping[0]);\n    -+\tmapping[1].entry.oid = split_commits[1];\n    -+\tmapping[1].rewritten_oid = original_commit->object.oid;\n    -+\toidmap_put(&rewritten_commits, &mapping[1]);\n    -+\n     +\treplace_commits(&commits, &original_commit->object.oid,\n     +\t\t\tsplit_commits, ARRAY_SIZE(split_commits));\n     +\n    -+\tret = apply_commits(repo, &commits, head, original_commit,\n    -+\t\t\t    &rewritten_commits, \"split\");\n    ++\tret = apply_commits(repo, &commits, parent, head, \"split\");\n     +\tif (ret < 0)\n     +\t\tgoto out;\n     +\n    @@ builtin/history.c: static int cmd_history_reorder(int argc,\n      \t\tconst char **argv,\n      \t\tconst char *prefix,\n     @@ builtin/history.c: int cmd_history(int argc,\n    - \t\tN_(\"git history quit\"),\n    - \t\tN_(\"git history drop <commit>\"),\n    - \t\tN_(\"git history reorder <commit> (--before=<following-commit>|--after=<preceding-commit>)\"),\n    + \tconst char * const usage[] = {\n    + \t\tN_(\"git history [<options>]\"),\n    + \t\tN_(\"git history reword [<options>] <commit>\"),\n     +\t\tN_(\"git history split [<options>] <commit> [--] [<pathspec>...]\"),\n      \t\tNULL,\n      \t};\n      \tparse_opt_subcommand_fn *fn = NULL;\n    -@@ builtin/history.c: int cmd_history(int argc,\n    - \t\tOPT_SUBCOMMAND(\"quit\", &fn, cmd_history_quit),\n    - \t\tOPT_SUBCOMMAND(\"drop\", &fn, cmd_history_drop),\n    - \t\tOPT_SUBCOMMAND(\"reorder\", &fn, cmd_history_reorder),\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    @@ builtin/history.c: int cmd_history(int argc,\n     \n      ## t/meson.build ##\n     @@ t/meson.build: integration_tests = [\n    +   't3438-rebase-broken-files.sh',\n        't3450-history.sh',\n    -   't3451-history-drop.sh',\n    -   't3452-history-reorder.sh',\n    -+  't3453-history-split.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     \n    - ## t/t3453-history-split.sh (new) ##\n    + ## t/t3452-history-split.sh (new) ##\n     @@\n     +#!/bin/sh\n     +\n    @@ t/t3453-history-split.sh (new)\n     +\t)\n     +'\n     +\n    -+test_expect_success 'refuses to work with changes in the worktree or index' '\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\techo changed >bar &&\n    -+\t\ttest_must_fail git history split -m message HEAD 2>err <<-EOF &&\n    -+\t\ty\n    -+\t\tn\n    -+\t\tEOF\n    -+\t\ttest_grep \"Your local changes to the following files would be overwritten\" err &&\n    -+\n    -+\t\tgit add bar &&\n    -+\t\ttest_must_fail git history split -m message HEAD 2>err <<-EOF &&\n    -+\t\ty\n    -+\t\tn\n    -+\t\tEOF\n    -+\t\ttest_grep \"Your local changes to the following files would be overwritten\" err\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/t3453-history-split.sh (new)\n     +\t)\n     +'\n     +\n    -+test_expect_success 'skips change summary with commit.verbose=false' '\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\tcp \"$1\" . &&\n    -+\t\techo \"some commit message\" >>\"$1\"\n    -+\t\tEOF\n    -+\t\ttest_set_editor \"$(pwd)\"/fake-editor.sh &&\n    -+\n    -+\t\tgit -c commit.verbose=false history split HEAD <<-EOF &&\n    -+\t\ty\n    -+\t\tn\n    -+\t\tEOF\n    -+\n    -+\t\tcat >expect <<-EOF &&\n    -+\n    -+\t\t# Please enter the commit message for the split-out changes. Lines starting\n    -+\t\t# with ${SQ}#${SQ} will be kept; you may remove them yourself if you want to.\n    -+\t\tEOF\n    -+\t\ttest_cmp expect COMMIT_EDITMSG &&\n    -+\n    -+\t\texpect_log <<-EOF\n    -+\t\tsplit-me\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/t3453-history-split.sh (new)\n     +\t\told_head=$(git rev-parse HEAD) &&\n     +\n     +\t\twrite_script .git/hooks/prepare-commit-msg <<-EOF &&\n    -+\t\techo \"prepare-commit-msg: \\$@\" >>\"$(pwd)/hooks.log\"\n    ++\t\ttouch \"$(pwd)/hooks.log\"\n     +\t\tEOF\n     +\t\twrite_script .git/hooks/post-commit <<-EOF &&\n    -+\t\techo \"post-commit\" >>\"$(pwd)/hooks.log\"\n    ++\t\ttouch \"$(pwd)/hooks.log\"\n     +\t\tEOF\n     +\t\twrite_script .git/hooks/post-rewrite <<-EOF &&\n    -+\t\t{\n    -+\t\t\techo \"post-rewrite: \\$@\"\n    -+\t\t\tcat\n    -+\t\t} >>\"$(pwd)/hooks.log\"\n    ++\t\ttouch \"$(pwd)/hooks.log\"\n     +\t\tEOF\n     +\n     +\t\tset_fake_editor &&\n    @@ t/t3453-history-split.sh (new)\n     +\t\tsplit-out commit\n     +\t\tEOF\n     +\n    -+\t\tcat >expect <<-EOF &&\n    -+\t\tprepare-commit-msg: .git/COMMIT_EDITMSG message\n    -+\t\tpost-commit\n    -+\t\tprepare-commit-msg: .git/COMMIT_EDITMSG message\n    -+\t\tpost-commit\n    -+\t\tpost-rewrite: history\n    -+\t\t$old_head $(git rev-parse HEAD~)\n    -+\t\t$old_head $(git rev-parse HEAD)\n    -+\t\tEOF\n    -+\t\ttest_cmp expect hooks.log\n    ++\t\ttest_path_is_missing hooks.log\n     +\t)\n     +'\n     +\n    @@ t/t3453-history-split.sh (new)\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\tgit history split HEAD -m a-only <<-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\tEOF\n    ++\t\tgit status --porcelain >actual &&\n    ++\t\ttest_cmp expect actual\n    ++\t)\n    ++'\n    ++\n     +test_done\n\n---\nbase-commit: 896ecf94ed941815749901ff6ad06d7141904cb2\nchange-id: 20250819-b4-pks-history-builtin-83398f9a05f0\n\n"},{"id":"527685","messageId":"20251001-b4-pks-history-builtin-v4-1-8e61ddb86317@pks.im","threadId":"63986","inReplyTo":"20251001-b4-pks-history-builtin-v4-0-8e61ddb86317@pks.im","subject":"[PATCH v4 01/12] wt-status: provide function to expose status for trees","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-01T15:57:27Z","receivedAt":"2025-10-01T15:57:50Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The \"wt-status\" subsystem is responsible for printing status information\naround the current state of the working tree. This most importantly\nincludes information around whether the working tree or the index have\nany changes.\n\nWe're about to introduce a new command though where the changes in\nneither of them are actually relevant to us. Instead, what we want is to\nformat the changes between two different trees. While it is a little bit\nof a stretch to add this as functionality to _working tree_ status, it\ndoesn't make any sense to open-code this functionality, either.\n\nImplement a new function `wt_status_collect_changes_trees()` that diffs\ntwo trees and formats the status accordingly. This function is not yet\nused, but will be in a subsequent commit.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n wt-status.c | 24 ++++++++++++++++++++++++\n wt-status.h |  3 +++\n 2 files changed, 27 insertions(+)\n\ndiff --git a/wt-status.c b/wt-status.c\nindex 8ffe6d3988..b66edbfca6 100644\n--- a/wt-status.c\n+++ b/wt-status.c\n@@ -612,6 +612,30 @@ static void wt_status_collect_updated_cb(struct diff_queue_struct *q,\n \t}\n }\n \n+void wt_status_collect_changes_trees(struct wt_status *s,\n+\t\t\t\t     const struct object_id *old_treeish,\n+\t\t\t\t     const struct object_id *new_treeish)\n+{\n+\tstruct diff_options opts = { 0 };\n+\n+\trepo_diff_setup(s->repo, &opts);\n+\topts.output_format = DIFF_FORMAT_CALLBACK;\n+\topts.format_callback = wt_status_collect_updated_cb;\n+\topts.format_callback_data = s;\n+\topts.detect_rename = s->detect_rename >= 0 ? s->detect_rename : opts.detect_rename;\n+\topts.rename_limit = s->rename_limit >= 0 ? s->rename_limit : opts.rename_limit;\n+\topts.rename_score = s->rename_score >= 0 ? s->rename_score : opts.rename_score;\n+\topts.flags.recursive = 1;\n+\tdiff_setup_done(&opts);\n+\n+\tdiff_tree_oid(old_treeish, new_treeish, \"\", &opts);\n+\tdiffcore_std(&opts);\n+\tdiff_flush(&opts);\n+\twt_status_get_state(s->repo, &s->state, 0);\n+\n+\tdiff_free(&opts);\n+}\n+\n static void wt_status_collect_changes_worktree(struct wt_status *s)\n {\n \tstruct rev_info rev;\ndiff --git a/wt-status.h b/wt-status.h\nindex e40a27214a..924d7a5fa9 100644\n--- a/wt-status.h\n+++ b/wt-status.h\n@@ -153,6 +153,9 @@ void wt_status_add_cut_line(struct wt_status *s);\n void wt_status_prepare(struct repository *r, struct wt_status *s);\n void wt_status_print(struct wt_status *s);\n void wt_status_collect(struct wt_status *s);\n+void wt_status_collect_changes_trees(struct wt_status *s,\n+\t\t\t\t     const struct object_id *old_treeish,\n+\t\t\t\t     const struct object_id *new_treeish);\n /*\n  * Frees the buffers allocated by wt_status_collect.\n  */\n\n-- \n2.51.0.700.g236ee7b076.dirty\n\n"},{"id":"527686","messageId":"20251001-b4-pks-history-builtin-v4-2-8e61ddb86317@pks.im","threadId":"63986","inReplyTo":"20251001-b4-pks-history-builtin-v4-0-8e61ddb86317@pks.im","subject":"[PATCH v4 02/12] replay: extract logic to pick commits","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-01T15:57:28Z","receivedAt":"2025-10-01T15:57:53Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"We're about to add a new git-history(1) command that will reuse some of\nthe same infrastructure as git-replay(1). To prepare for this, extract\nthe logic to pick a commit into a new \"replay.c\" file so that it can be\nshared between both commands.\n\nRename the function to have a \"replay_\" prefix to clearly indicate its\nsubsystem.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Makefile         |   1 +\n builtin/replay.c | 110 ++--------------------------------------------------\n meson.build      |   1 +\n replay.c         | 115 +++++++++++++++++++++++++++++++++++++++++++++++++++++++\n replay.h         |  18 +++++++++\n 5 files changed, 138 insertions(+), 107 deletions(-)\n\ndiff --git a/Makefile b/Makefile\nindex 4c95affadb..5960c80736 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -1137,6 +1137,7 @@ LIB_OBJS += refs/ref-cache.o\n LIB_OBJS += refspec.o\n LIB_OBJS += remote.o\n LIB_OBJS += replace-object.o\n+LIB_OBJS += replay.o\n LIB_OBJS += repo-settings.o\n LIB_OBJS += repository.o\n LIB_OBJS += rerere.o\ndiff --git a/builtin/replay.c b/builtin/replay.c\nindex b6f9d53560..e9d6559b47 100644\n--- a/builtin/replay.c\n+++ b/builtin/replay.c\n@@ -2,7 +2,6 @@\n  * \"git replay\" builtin command\n  */\n \n-#define USE_THE_REPOSITORY_VARIABLE\n #define DISABLE_SIGN_COMPARE_WARNINGS\n \n #include \"git-compat-util.h\"\n@@ -15,18 +14,12 @@\n #include \"object-name.h\"\n #include \"parse-options.h\"\n #include \"refs.h\"\n+#include \"replay.h\"\n #include \"revision.h\"\n #include \"strmap.h\"\n #include <oidset.h>\n #include <tree.h>\n \n-static const char *short_commit_name(struct repository *repo,\n-\t\t\t\t     struct commit *commit)\n-{\n-\treturn repo_find_unique_abbrev(repo, &commit->object.oid,\n-\t\t\t\t       DEFAULT_ABBREV);\n-}\n-\n static struct commit *peel_committish(struct repository *repo, const char *name)\n {\n \tstruct object *obj;\n@@ -39,59 +32,6 @@ static struct commit *peel_committish(struct repository *repo, const char *name)\n \t\t\t\t\t\t  OBJ_COMMIT);\n }\n \n-static char *get_author(const char *message)\n-{\n-\tsize_t len;\n-\tconst char *a;\n-\n-\ta = find_commit_header(message, \"author\", &len);\n-\tif (a)\n-\t\treturn xmemdupz(a, len);\n-\n-\treturn NULL;\n-}\n-\n-static struct commit *create_commit(struct repository *repo,\n-\t\t\t\t    struct tree *tree,\n-\t\t\t\t    struct commit *based_on,\n-\t\t\t\t    struct commit *parent)\n-{\n-\tstruct object_id ret;\n-\tstruct object *obj = NULL;\n-\tstruct commit_list *parents = NULL;\n-\tchar *author;\n-\tchar *sign_commit = NULL; /* FIXME: cli users might want to sign again */\n-\tstruct commit_extra_header *extra = NULL;\n-\tstruct strbuf msg = STRBUF_INIT;\n-\tconst char *out_enc = get_commit_output_encoding();\n-\tconst char *message = repo_logmsg_reencode(repo, based_on,\n-\t\t\t\t\t\t   NULL, out_enc);\n-\tconst char *orig_message = NULL;\n-\tconst char *exclude_gpgsig[] = { \"gpgsig\", NULL };\n-\n-\tcommit_list_insert(parent, &parents);\n-\textra = read_commit_extra_headers(based_on, exclude_gpgsig);\n-\tfind_commit_subject(message, &orig_message);\n-\tstrbuf_addstr(&msg, orig_message);\n-\tauthor = get_author(message);\n-\treset_ident_date();\n-\tif (commit_tree_extended(msg.buf, msg.len, &tree->object.oid, parents,\n-\t\t\t\t &ret, author, NULL, sign_commit, extra)) {\n-\t\terror(_(\"failed to write commit object\"));\n-\t\tgoto out;\n-\t}\n-\n-\tobj = parse_object(repo, &ret);\n-\n-out:\n-\trepo_unuse_commit_buffer(the_repository, based_on, message);\n-\tfree_commit_extra_headers(extra);\n-\tfree_commit_list(parents);\n-\tstrbuf_release(&msg);\n-\tfree(author);\n-\treturn (struct commit *)obj;\n-}\n-\n struct ref_info {\n \tstruct commit *onto;\n \tstruct strset positive_refs;\n@@ -240,50 +180,6 @@ static void determine_replay_mode(struct repository *repo,\n \tstrset_clear(&rinfo.positive_refs);\n }\n \n-static struct commit *mapped_commit(kh_oid_map_t *replayed_commits,\n-\t\t\t\t    struct commit *commit,\n-\t\t\t\t    struct commit *fallback)\n-{\n-\tkhint_t pos = kh_get_oid_map(replayed_commits, commit->object.oid);\n-\tif (pos == kh_end(replayed_commits))\n-\t\treturn fallback;\n-\treturn kh_value(replayed_commits, pos);\n-}\n-\n-static struct commit *pick_regular_commit(struct repository *repo,\n-\t\t\t\t\t  struct commit *pickme,\n-\t\t\t\t\t  kh_oid_map_t *replayed_commits,\n-\t\t\t\t\t  struct commit *onto,\n-\t\t\t\t\t  struct merge_options *merge_opt,\n-\t\t\t\t\t  struct merge_result *result)\n-{\n-\tstruct commit *base, *replayed_base;\n-\tstruct tree *pickme_tree, *base_tree;\n-\n-\tbase = pickme->parents->item;\n-\treplayed_base = mapped_commit(replayed_commits, base, onto);\n-\n-\tresult->tree = repo_get_commit_tree(repo, replayed_base);\n-\tpickme_tree = repo_get_commit_tree(repo, pickme);\n-\tbase_tree = repo_get_commit_tree(repo, base);\n-\n-\tmerge_opt->branch1 = short_commit_name(repo, replayed_base);\n-\tmerge_opt->branch2 = short_commit_name(repo, pickme);\n-\tmerge_opt->ancestor = xstrfmt(\"parent of %s\", merge_opt->branch2);\n-\n-\tmerge_incore_nonrecursive(merge_opt,\n-\t\t\t\t  base_tree,\n-\t\t\t\t  result->tree,\n-\t\t\t\t  pickme_tree,\n-\t\t\t\t  result);\n-\n-\tfree((char*)merge_opt->ancestor);\n-\tmerge_opt->ancestor = NULL;\n-\tif (!result->clean)\n-\t\treturn NULL;\n-\treturn create_commit(repo, result->tree, pickme, replayed_base);\n-}\n-\n static int add_ref_to_transaction(struct ref_transaction *transaction,\n \t\t\t\t  const char *refname,\n \t\t\t\t  const struct object_id *new_oid,\n@@ -459,8 +355,8 @@ int cmd_replay(int argc,\n \t\tif (commit->parents->next)\n \t\t\tdie(_(\"replaying merge commits is not supported yet!\"));\n \n-\t\tlast_commit = pick_regular_commit(repo, commit, replayed_commits,\n-\t\t\t\t\t\t  onto, &merge_opt, &result);\n+\t\tlast_commit = replay_pick_regular_commit(repo, commit, replayed_commits,\n+\t\t\t\t\t\t\t onto, &merge_opt, &result);\n \t\tif (!last_commit)\n \t\t\tbreak;\n \ndiff --git a/meson.build b/meson.build\nindex b3dfcc0497..c320bdba9f 100644\n--- a/meson.build\n+++ b/meson.build\n@@ -463,6 +463,7 @@ libgit_sources = [\n   'reftable/writer.c',\n   'remote.c',\n   'replace-object.c',\n+  'replay.c',\n   'repo-settings.c',\n   'repository.c',\n   'rerere.c',\ndiff --git a/replay.c b/replay.c\nnew file mode 100644\nindex 0000000000..e22ce39940\n--- /dev/null\n+++ b/replay.c\n@@ -0,0 +1,115 @@\n+#define USE_THE_REPOSITORY_VARIABLE\n+\n+#include \"git-compat-util.h\"\n+#include \"commit.h\"\n+#include \"environment.h\"\n+#include \"gettext.h\"\n+#include \"ident.h\"\n+#include \"object.h\"\n+#include \"object-name.h\"\n+#include \"replay.h\"\n+#include \"tree.h\"\n+\n+static const char *short_commit_name(struct repository *repo,\n+\t\t\t\t     struct commit *commit)\n+{\n+\treturn repo_find_unique_abbrev(repo, &commit->object.oid,\n+\t\t\t\t       DEFAULT_ABBREV);\n+}\n+\n+static char *get_author(const char *message)\n+{\n+\tsize_t len;\n+\tconst char *a;\n+\n+\ta = find_commit_header(message, \"author\", &len);\n+\tif (a)\n+\t\treturn xmemdupz(a, len);\n+\n+\treturn NULL;\n+}\n+\n+static struct commit *create_commit(struct repository *repo,\n+\t\t\t\t    struct tree *tree,\n+\t\t\t\t    struct commit *based_on,\n+\t\t\t\t    struct commit *parent)\n+{\n+\tstruct object_id ret;\n+\tstruct object *obj = NULL;\n+\tstruct commit_list *parents = NULL;\n+\tchar *author;\n+\tchar *sign_commit = NULL; /* FIXME: cli users might want to sign again */\n+\tstruct commit_extra_header *extra = NULL;\n+\tstruct strbuf msg = STRBUF_INIT;\n+\tconst char *out_enc = get_commit_output_encoding();\n+\tconst char *message = repo_logmsg_reencode(repo, based_on,\n+\t\t\t\t\t\t   NULL, out_enc);\n+\tconst char *orig_message = NULL;\n+\tconst char *exclude_gpgsig[] = { \"gpgsig\", NULL };\n+\n+\tcommit_list_insert(parent, &parents);\n+\textra = read_commit_extra_headers(based_on, exclude_gpgsig);\n+\tfind_commit_subject(message, &orig_message);\n+\tstrbuf_addstr(&msg, orig_message);\n+\tauthor = get_author(message);\n+\treset_ident_date();\n+\tif (commit_tree_extended(msg.buf, msg.len, &tree->object.oid, parents,\n+\t\t\t\t &ret, author, NULL, sign_commit, extra)) {\n+\t\terror(_(\"failed to write commit object\"));\n+\t\tgoto out;\n+\t}\n+\n+\tobj = parse_object(repo, &ret);\n+\n+out:\n+\trepo_unuse_commit_buffer(the_repository, based_on, message);\n+\tfree_commit_extra_headers(extra);\n+\tfree_commit_list(parents);\n+\tstrbuf_release(&msg);\n+\tfree(author);\n+\treturn (struct commit *)obj;\n+}\n+\n+static struct commit *mapped_commit(kh_oid_map_t *replayed_commits,\n+\t\t\t\t    struct commit *commit,\n+\t\t\t\t    struct commit *fallback)\n+{\n+\tkhint_t pos = kh_get_oid_map(replayed_commits, commit->object.oid);\n+\tif (pos == kh_end(replayed_commits))\n+\t\treturn fallback;\n+\treturn kh_value(replayed_commits, pos);\n+}\n+\n+struct commit *replay_pick_regular_commit(struct repository *repo,\n+\t\t\t\t\t  struct commit *pickme,\n+\t\t\t\t\t  kh_oid_map_t *replayed_commits,\n+\t\t\t\t\t  struct commit *onto,\n+\t\t\t\t\t  struct merge_options *merge_opt,\n+\t\t\t\t\t  struct merge_result *result)\n+{\n+\tstruct commit *base, *replayed_base;\n+\tstruct tree *pickme_tree, *base_tree;\n+\n+\tbase = pickme->parents->item;\n+\treplayed_base = mapped_commit(replayed_commits, base, onto);\n+\n+\tresult->tree = repo_get_commit_tree(repo, replayed_base);\n+\tpickme_tree = repo_get_commit_tree(repo, pickme);\n+\tbase_tree = repo_get_commit_tree(repo, base);\n+\n+\tmerge_opt->branch1 = short_commit_name(repo, replayed_base);\n+\tmerge_opt->branch2 = short_commit_name(repo, pickme);\n+\tmerge_opt->ancestor = xstrfmt(\"parent of %s\", merge_opt->branch2);\n+\n+\tmerge_incore_nonrecursive(merge_opt,\n+\t\t\t\t  base_tree,\n+\t\t\t\t  result->tree,\n+\t\t\t\t  pickme_tree,\n+\t\t\t\t  result);\n+\n+\tfree((char*)merge_opt->ancestor);\n+\tmerge_opt->ancestor = NULL;\n+\tif (!result->clean)\n+\t\treturn NULL;\n+\treturn create_commit(repo, result->tree, pickme, replayed_base);\n+}\ndiff --git a/replay.h b/replay.h\nnew file mode 100644\nindex 0000000000..a461b5c234\n--- /dev/null\n+++ b/replay.h\n@@ -0,0 +1,18 @@\n+#ifndef REPLAY_H\n+#define REPLAY_H\n+\n+#include \"khash.h\"\n+#include \"merge-ort.h\"\n+#include \"repository.h\"\n+\n+struct commit;\n+struct tree;\n+\n+struct commit *replay_pick_regular_commit(struct repository *repo,\n+\t\t\t\t\t  struct commit *pickme,\n+\t\t\t\t\t  kh_oid_map_t *replayed_commits,\n+\t\t\t\t\t  struct commit *onto,\n+\t\t\t\t\t  struct merge_options *merge_opt,\n+\t\t\t\t\t  struct merge_result *result);\n+\n+#endif\n\n-- \n2.51.0.700.g236ee7b076.dirty\n\n"},{"id":"527687","messageId":"20251001-b4-pks-history-builtin-v4-3-8e61ddb86317@pks.im","threadId":"63986","inReplyTo":"20251001-b4-pks-history-builtin-v4-0-8e61ddb86317@pks.im","subject":"[PATCH v4 03/12] replay: stop using `the_repository`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-01T15:57:29Z","receivedAt":"2025-10-01T15:57:56Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"In `create_commit()` we're using `the_repository` even though we already\nhave a repository passed to use as an argument. Fix this.\n\nNote that we still cannot get rid of `USE_THE_REPOSITORY_VARIABLE`. This\nis because we use `DEFAULT_ABBREV and `get_commit_output_encoding()`,\nboth of which are stored as global variables that can be modified via\nthe Git configuration.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n replay.c | 2 +-\n 1 file changed, 1 insertion(+), 1 deletion(-)\n\ndiff --git a/replay.c b/replay.c\nindex e22ce39940..13d75d8054 100644\n--- a/replay.c\n+++ b/replay.c\n@@ -62,7 +62,7 @@ static struct commit *create_commit(struct repository *repo,\n \tobj = parse_object(repo, &ret);\n \n out:\n-\trepo_unuse_commit_buffer(the_repository, based_on, message);\n+\trepo_unuse_commit_buffer(repo, based_on, message);\n \tfree_commit_extra_headers(extra);\n \tfree_commit_list(parents);\n \tstrbuf_release(&msg);\n\n-- \n2.51.0.700.g236ee7b076.dirty\n\n"},{"id":"527688","messageId":"20251001-b4-pks-history-builtin-v4-4-8e61ddb86317@pks.im","threadId":"63986","inReplyTo":"20251001-b4-pks-history-builtin-v4-0-8e61ddb86317@pks.im","subject":"[PATCH v4 04/12] replay: parse commits before dereferencing them","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-01T15:57:30Z","receivedAt":"2025-10-01T15:57:59Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"When looking up a commit it may not be parsed yet. Callers that wish to\naccess the fields of `struct commit` have to call `repo_parse_commit()`\nfirst so that it is guaranteed to be populated.\n\nWe didn't yet care about doing so, because code paths that lead to\n`pick_regular_commit()` in \"builtin/replay.c\" already implicitly parsed\nthe commits. But now that the function is exposed to outside callers\nit's quite easy to get this wrong.\n\nMake the function easier to use by calling `repo_parse_commit()`.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n replay.c | 3 +++\n 1 file changed, 3 insertions(+)\n\ndiff --git a/replay.c b/replay.c\nindex 13d75d8054..c3628d2488 100644\n--- a/replay.c\n+++ b/replay.c\n@@ -90,6 +90,9 @@ struct commit *replay_pick_regular_commit(struct repository *repo,\n \tstruct commit *base, *replayed_base;\n \tstruct tree *pickme_tree, *base_tree;\n \n+\tif (repo_parse_commit(repo, pickme))\n+\t\treturn NULL;\n+\n \tbase = pickme->parents->item;\n \treplayed_base = mapped_commit(replayed_commits, base, onto);\n \n\n-- \n2.51.0.700.g236ee7b076.dirty\n\n"},{"id":"527689","messageId":"20251001-b4-pks-history-builtin-v4-5-8e61ddb86317@pks.im","threadId":"63986","inReplyTo":"20251001-b4-pks-history-builtin-v4-0-8e61ddb86317@pks.im","subject":"[PATCH v4 05/12] builtin: add new \"history\" command","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-01T15:57:31Z","receivedAt":"2025-10-01T15:58:02Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"When rewriting history via git-rebase(1) there are a couple of very\ncommon use cases:\n\n  - The ordering of two commits should be reversed.\n\n  - A commit should be split up into two commits.\n\n  - A commit should be dropped from the history completely.\n\n  - Multiple commits should be squashed into one.\n\nWhile these operations are all doable, it often feels needlessly cludgy\nto do so by doing an interactive rebase, using the editor to say what\none wants, and then perform the actions. Furthermore, some operations\nlike splitting up a commit into two are way more involved than that and\nrequire a whole series of commands.\n\nAdd a new \"history\" command to plug this gap. This command will have\nseveral different subcommands to imperatively rewrite history for common\nuse cases like the above. These commands will be implemented in\nsubsequent commits.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n .gitignore                     |  1 +\n Documentation/git-history.adoc | 45 ++++++++++++++++++++++++++++++++++++++++++\n Documentation/meson.build      |  1 +\n Makefile                       |  1 +\n builtin.h                      |  1 +\n builtin/history.c              | 22 +++++++++++++++++++++\n command-list.txt               |  1 +\n git.c                          |  1 +\n meson.build                    |  1 +\n t/meson.build                  |  1 +\n t/t3450-history.sh             | 17 ++++++++++++++++\n 11 files changed, 92 insertions(+)\n\ndiff --git a/.gitignore b/.gitignore\nindex 802ce70e48..3de9f9f16f 100644\n--- a/.gitignore\n+++ b/.gitignore\n@@ -77,6 +77,7 @@\n /git-grep\n /git-hash-object\n /git-help\n+/git-history\n /git-hook\n /git-http-backend\n /git-http-fetch\ndiff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc\nnew file mode 100644\nindex 0000000000..1537960374\n--- /dev/null\n+++ b/Documentation/git-history.adoc\n@@ -0,0 +1,45 @@\n+git-history(1)\n+==============\n+\n+NAME\n+----\n+git-history - EXPERIMENTAL: Rewrite history of the current branch\n+\n+SYNOPSIS\n+--------\n+[synopsis]\n+git history [<options>]\n+\n+DESCRIPTION\n+-----------\n+\n+Rewrite history by rearranging or modifying specific commits in the\n+history.\n+\n+This command is similar to linkgit:git-rebase[1] and uses the same\n+underlying machinery. You should use rebases if you either want to\n+reapply a range of commits onto a different base, or interactive rebases\n+if you want to edit a range of commits.\n+\n+Note that this command does not (yet) work with histories that contain\n+merges. You should use linkgit:git-rebase[1] with the `--rebase-merges`\n+flag instead.\n+\n+THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.\n+\n+COMMANDS\n+--------\n+\n+This command requires a subcommand. Several subcommands are available to\n+rewrite history in different ways:\n+\n+CONFIGURATION\n+-------------\n+\n+include::includes/cmd-config-section-all.adoc[]\n+\n+include::config/sequencer.adoc[]\n+\n+GIT\n+---\n+Part of the linkgit:git[1] suite\ndiff --git a/Documentation/meson.build b/Documentation/meson.build\nindex e34965c5b0..36500879e4 100644\n--- a/Documentation/meson.build\n+++ b/Documentation/meson.build\n@@ -64,6 +64,7 @@ manpages = {\n   'git-gui.adoc' : 1,\n   'git-hash-object.adoc' : 1,\n   'git-help.adoc' : 1,\n+  'git-history.adoc' : 1,\n   'git-hook.adoc' : 1,\n   'git-http-backend.adoc' : 1,\n   'git-http-fetch.adoc' : 1,\ndiff --git a/Makefile b/Makefile\nindex 5960c80736..4e405509e9 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -1262,6 +1262,7 @@ BUILTIN_OBJS += builtin/get-tar-commit-id.o\n BUILTIN_OBJS += builtin/grep.o\n BUILTIN_OBJS += builtin/hash-object.o\n BUILTIN_OBJS += builtin/help.o\n+BUILTIN_OBJS += builtin/history.o\n BUILTIN_OBJS += builtin/hook.o\n BUILTIN_OBJS += builtin/index-pack.o\n BUILTIN_OBJS += builtin/init-db.o\ndiff --git a/builtin.h b/builtin.h\nindex 1b35565fbd..93c91d07d4 100644\n--- a/builtin.h\n+++ b/builtin.h\n@@ -172,6 +172,7 @@ int cmd_get_tar_commit_id(int argc, const char **argv, const char *prefix, struc\n int cmd_grep(int argc, const char **argv, const char *prefix, struct repository *repo);\n int cmd_hash_object(int argc, const char **argv, const char *prefix, struct repository *repo);\n int cmd_help(int argc, const char **argv, const char *prefix, struct repository *repo);\n+int cmd_history(int argc, const char **argv, const char *prefix, struct repository *repo);\n int cmd_hook(int argc, const char **argv, const char *prefix, struct repository *repo);\n int cmd_index_pack(int argc, const char **argv, const char *prefix, struct repository *repo);\n int cmd_init_db(int argc, const char **argv, const char *prefix, struct repository *repo);\ndiff --git a/builtin/history.c b/builtin/history.c\nnew file mode 100644\nindex 0000000000..f6fe32610b\n--- /dev/null\n+++ b/builtin/history.c\n@@ -0,0 +1,22 @@\n+#include \"builtin.h\"\n+#include \"gettext.h\"\n+#include \"parse-options.h\"\n+\n+int cmd_history(int argc,\n+\t\tconst char **argv,\n+\t\tconst char *prefix,\n+\t\tstruct repository *repo UNUSED)\n+{\n+\tconst char * const usage[] = {\n+\t\tN_(\"git history [<options>]\"),\n+\t\tNULL,\n+\t};\n+\tstruct option options[] = {\n+\t\tOPT_END(),\n+\t};\n+\n+\targc = parse_options(argc, argv, prefix, options, usage, 0);\n+\tif (argc)\n+\t\tusagef(\"unrecognized argument: %s\", argv[0]);\n+\treturn 0;\n+}\ndiff --git a/command-list.txt b/command-list.txt\nindex accd3d0c4b..f9005cf459 100644\n--- a/command-list.txt\n+++ b/command-list.txt\n@@ -115,6 +115,7 @@ git-grep                                mainporcelain           info\n git-gui                                 mainporcelain\n git-hash-object                         plumbingmanipulators\n git-help                                ancillaryinterrogators          complete\n+git-history                             mainporcelain           history\n git-hook                                purehelpers\n git-http-backend                        synchingrepositories\n git-http-fetch                          synchelpers\ndiff --git a/git.c b/git.c\nindex d020eef021..c7c13cea67 100644\n--- a/git.c\n+++ b/git.c\n@@ -560,6 +560,7 @@ static struct cmd_struct commands[] = {\n \t{ \"grep\", cmd_grep, RUN_SETUP_GENTLY },\n \t{ \"hash-object\", cmd_hash_object },\n \t{ \"help\", cmd_help },\n+\t{ \"history\", cmd_history, RUN_SETUP },\n \t{ \"hook\", cmd_hook, RUN_SETUP },\n \t{ \"index-pack\", cmd_index_pack, RUN_SETUP_GENTLY | NO_PARSEOPT },\n \t{ \"init\", cmd_init_db },\ndiff --git a/meson.build b/meson.build\nindex c320bdba9f..7630a8fb0a 100644\n--- a/meson.build\n+++ b/meson.build\n@@ -604,6 +604,7 @@ builtin_sources = [\n   'builtin/grep.c',\n   'builtin/hash-object.c',\n   'builtin/help.c',\n+  'builtin/history.c',\n   'builtin/hook.c',\n   'builtin/index-pack.c',\n   'builtin/init-db.c',\ndiff --git a/t/meson.build b/t/meson.build\nindex 7974795fe4..8b31eb0858 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -382,6 +382,7 @@ integration_tests = [\n   't3436-rebase-more-options.sh',\n   't3437-rebase-fixup-options.sh',\n   't3438-rebase-broken-files.sh',\n+  't3450-history.sh',\n   't3500-cherry.sh',\n   't3501-revert-cherry-pick.sh',\n   't3502-cherry-pick-merge.sh',\ndiff --git a/t/t3450-history.sh b/t/t3450-history.sh\nnew file mode 100755\nindex 0000000000..417c343d43\n--- /dev/null\n+++ b/t/t3450-history.sh\n@@ -0,0 +1,17 @@\n+#!/bin/sh\n+\n+test_description='tests for git-history command'\n+\n+. ./test-lib.sh\n+\n+test_expect_success 'does nothing without any arguments' '\n+\tgit history >out 2>&1 &&\n+\ttest_must_be_empty out\n+'\n+\n+test_expect_success 'raises an error with unknown argument' '\n+\ttest_must_fail git history garbage 2>err &&\n+\ttest_grep \"unrecognized argument: garbage\" err\n+'\n+\n+test_done\n\n-- \n2.51.0.700.g236ee7b076.dirty\n\n"},{"id":"527690","messageId":"20251001-b4-pks-history-builtin-v4-6-8e61ddb86317@pks.im","threadId":"63986","inReplyTo":"20251001-b4-pks-history-builtin-v4-0-8e61ddb86317@pks.im","subject":"[PATCH v4 06/12] builtin/history: implement \"reword\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-01T15:57:32Z","receivedAt":"2025-10-01T15:58:05Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Implement a new \"reword\" subcommand for git-history(1). This subcommand\nis essentially the same as if a user performed an interactive rebase\nwith a single commit changed to use the \"reword\" verb.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/git-history.adoc |   7 +\n builtin/history.c              | 375 ++++++++++++++++++++++++++++++++++++++++-\n t/meson.build                  |   1 +\n t/t3450-history.sh             |   6 +-\n t/t3451-history-reword.sh      | 202 ++++++++++++++++++++++\n 5 files changed, 584 insertions(+), 7 deletions(-)\n\ndiff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc\nindex 1537960374..b55babe206 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 [<options>]\n+git history reword [<options>] <commit>\n \n DESCRIPTION\n -----------\n@@ -33,6 +34,12 @@ COMMANDS\n This command requires a subcommand. Several subcommands are available to\n rewrite history in different ways:\n \n+`reword <commit> [--message=<message>]`::\n+\tRewrite the commit message of the specified commit. All the other\n+\tdetails of this commit remain unchanged. If no commit message is\n+\tprovided, then this command will spawn an editor with the current\n+\tmessage of that commit.\n+\n CONFIGURATION\n -------------\n \ndiff --git a/builtin/history.c b/builtin/history.c\nindex f6fe32610b..7b2a0023e8 100644\n--- a/builtin/history.c\n+++ b/builtin/history.c\n@@ -1,22 +1,389 @@\n+#define USE_THE_REPOSITORY_VARIABLE\n+\n #include \"builtin.h\"\n+#include \"commit-reach.h\"\n+#include \"commit.h\"\n+#include \"config.h\"\n+#include \"editor.h\"\n+#include \"environment.h\"\n #include \"gettext.h\"\n+#include \"hex.h\"\n+#include \"oidmap.h\"\n #include \"parse-options.h\"\n+#include \"refs.h\"\n+#include \"replay.h\"\n+#include \"reset.h\"\n+#include \"revision.h\"\n+#include \"sequencer.h\"\n+#include \"strvec.h\"\n+#include \"tree.h\"\n+#include \"wt-status.h\"\n+\n+static int collect_commits(struct repository *repo,\n+\t\t\t   struct commit *old_commit,\n+\t\t\t   struct commit *new_commit,\n+\t\t\t   struct strvec *out)\n+{\n+\tstruct setup_revision_opt revision_opts = {\n+\t\t.assume_dashdash = 1,\n+\t};\n+\tstruct strvec revisions = STRVEC_INIT;\n+\tstruct commit_list *from_list = NULL;\n+\tstruct commit *child;\n+\tstruct rev_info rev = { 0 };\n+\tint ret;\n+\n+\t/*\n+\t * Check that the old commit actually is an ancestor of HEAD. If not\n+\t * the whole request becomes nonsensical.\n+\t*/\n+\tif (old_commit) {\n+\t\tcommit_list_insert(old_commit, &from_list);\n+\t\tif (!repo_is_descendant_of(repo, new_commit, from_list)) {\n+\t\t\tret = error(_(\"commit must be reachable from current HEAD commit\"));\n+\t\t\tgoto out;\n+\t\t}\n+\t}\n+\n+\trepo_init_revisions(repo, &rev, NULL);\n+\tstrvec_push(&revisions, \"\");\n+\tstrvec_push(&revisions, oid_to_hex(&new_commit->object.oid));\n+\tif (old_commit)\n+\t\tstrvec_pushf(&revisions, \"^%s\", oid_to_hex(&old_commit->object.oid));\n+\n+\tsetup_revisions_from_strvec(&revisions, &rev, &revision_opts);\n+\tif (revisions.nr != 1 || prepare_revision_walk(&rev)) {\n+\t\tret = error(_(\"revision walk setup failed\"));\n+\t\tgoto out;\n+\t}\n+\n+\twhile ((child = get_revision(&rev))) {\n+\t\tif (old_commit && !child->parents)\n+\t\t\tBUG(\"revision walk did not find child commit\");\n+\t\tif (child->parents && child->parents->next) {\n+\t\t\tret = error(_(\"cannot rearrange commit history with merges\"));\n+\t\t\tgoto out;\n+\t\t}\n+\n+\t\tstrvec_push(out, oid_to_hex(&child->object.oid));\n+\n+\t\tif (child->parents && old_commit &&\n+\t\t    commit_list_contains(old_commit, child->parents))\n+\t\t\tbreak;\n+\t}\n+\n+\t/*\n+\t * Revisions are in newest-order-first. We have to reverse the\n+\t * array though so that we pick the oldest commits first.\n+\t */\n+\tfor (size_t i = 0, j = out->nr - 1; i < j; i++, j--)\n+\t\tSWAP(out->v[i], out->v[j]);\n+\n+\tret = 0;\n+\n+out:\n+\tfree_commit_list(from_list);\n+\tstrvec_clear(&revisions);\n+\trelease_revisions(&rev);\n+\treset_revision_walk();\n+\treturn ret;\n+}\n+\n+static void replace_commits(struct strvec *commits,\n+\t\t\t    const struct object_id *commit_to_replace,\n+\t\t\t    const struct object_id *replacements,\n+\t\t\t    size_t replacements_nr)\n+{\n+\tchar commit_to_replace_oid[GIT_MAX_HEXSZ + 1];\n+\tstruct strvec replacement_oids = STRVEC_INIT;\n+\tbool found = false;\n+\n+\toid_to_hex_r(commit_to_replace_oid, commit_to_replace);\n+\tfor (size_t i = 0; i < replacements_nr; i++)\n+\t\tstrvec_push(&replacement_oids, oid_to_hex(&replacements[i]));\n+\n+\tfor (size_t i = 0; i < commits->nr; i++) {\n+\t\tif (strcmp(commits->v[i], commit_to_replace_oid))\n+\t\t\tcontinue;\n+\t\tstrvec_splice(commits, i, 1, replacement_oids.v, replacement_oids.nr);\n+\t\tfound = true;\n+\t\tbreak;\n+\t}\n+\tif (!found)\n+\t\tBUG(\"could not find commit to replace\");\n+\n+\tstrvec_clear(&replacement_oids);\n+}\n+\n+static int apply_commits(struct repository *repo,\n+\t\t\t const struct strvec *commits,\n+\t\t\t struct commit *onto,\n+\t\t\t struct commit *orig_head,\n+\t\t\t const char *action)\n+{\n+\tstruct reset_head_opts reset_opts = { 0 };\n+\tstruct merge_options merge_opts = { 0 };\n+\tstruct merge_result result = { 0 };\n+\tstruct strbuf buf = STRBUF_INIT;\n+\tkh_oid_map_t *replayed_commits;\n+\tint ret;\n+\n+\treplayed_commits = kh_init_oid_map();\n+\n+\tinit_basic_merge_options(&merge_opts, repo);\n+\tmerge_opts.show_rename_progress = 0;\n+\n+\tfor (size_t i = 0; i < commits->nr; i++) {\n+\t\tstruct object_id commit_id;\n+\t\tstruct commit *commit;\n+\t\tconst char *end;\n+\t\tint hash_result;\n+\t\tkhint_t pos;\n+\n+\t\tif (parse_oid_hex_algop(commits->v[i], &commit_id, &end,\n+\t\t\t\t\trepo->hash_algo)) {\n+\t\t\tret = error(_(\"invalid object ID: %s\"), commits->v[i]);\n+\t\t\tgoto out;\n+\t\t}\n+\n+\t\tcommit = lookup_commit(repo, &commit_id);\n+\t\tif (!commit || repo_parse_commit(repo, commit)) {\n+\t\t\tret = error(_(\"failed to look up commit: %s\"), oid_to_hex(&commit_id));\n+\t\t\tgoto out;\n+\t\t}\n+\n+\t\tif (!onto) {\n+\t\t\tonto = commit;\n+\t\t\tresult.clean = 1;\n+\t\t\tresult.tree = repo_get_commit_tree(repo, commit);\n+\t\t} else {\n+\t\t\tonto = replay_pick_regular_commit(repo, commit, replayed_commits,\n+\t\t\t\t\t\t\t  onto, &merge_opts, &result);\n+\t\t\tif (!onto)\n+\t\t\t\tbreak;\n+\t\t}\n+\n+\t\tpos = kh_put_oid_map(replayed_commits, commit->object.oid, &hash_result);\n+\t\tif (hash_result == 0) {\n+\t\t\tret = error(_(\"duplicate rewritten commit: %s\\n\"),\n+\t\t\t\t    oid_to_hex(&commit->object.oid));\n+\t\t\tgoto out;\n+\t\t}\n+\t\tkh_value(replayed_commits, pos) = onto;\n+\t}\n+\n+\tif (!result.clean) {\n+\t\tret = error(_(\"could not merge\"));\n+\t\tgoto out;\n+\t}\n+\n+\treset_opts.oid = &onto->object.oid;\n+\tstrbuf_addf(&buf, \"%s: switch to rewritten %s\", action, oid_to_hex(reset_opts.oid));\n+\treset_opts.flags = RESET_HEAD_REFS_ONLY | RESET_ORIG_HEAD;\n+\treset_opts.orig_head = &orig_head->object.oid;\n+\treset_opts.default_reflog_action = action;\n+\tif (reset_head(repo, &reset_opts) < 0) {\n+\t\tret = error(_(\"could not switch to %s\"), oid_to_hex(reset_opts.oid));\n+\t\tgoto out;\n+\t}\n+\n+\tret = 0;\n+\n+out:\n+\tkh_destroy_oid_map(replayed_commits);\n+\tmerge_finalize(&merge_opts, &result);\n+\tstrbuf_release(&buf);\n+\treturn ret;\n+}\n+\n+static void change_data_free(void *util, const char *str UNUSED)\n+{\n+\tstruct wt_status_change_data *d = util;\n+\tfree(d->rename_source);\n+\tfree(d);\n+}\n+\n+static int fill_commit_message(struct repository *repo,\n+\t\t\t       const struct object_id *old_tree,\n+\t\t\t       const struct object_id *new_tree,\n+\t\t\t       const char *default_message,\n+\t\t\t       const char *provided_message,\n+\t\t\t       const char *action,\n+\t\t\t       struct strbuf *out)\n+{\n+\tif (!provided_message) {\n+\t\tconst char *path = git_path_commit_editmsg();\n+\t\tconst char *hint =\n+\t\t\t_(\"Please enter the commit message for the %s changes. Lines starting\\n\"\n+     \"with '%s' will be kept; you may remove them yourself if you want to.\\n\");\n+\t\tstruct wt_status s;\n+\n+\t\tstrbuf_addstr(out, default_message);\n+\t\tstrbuf_addch(out, '\\n');\n+\t\tstrbuf_commented_addf(out, comment_line_str, hint, action, comment_line_str);\n+\t\twrite_file_buf(path, out->buf, out->len);\n+\n+\t\twt_status_prepare(repo, &s);\n+\t\tFREE_AND_NULL(s.branch);\n+\t\ts.ahead_behind_flags = AHEAD_BEHIND_QUICK;\n+\t\ts.commit_template = 1;\n+\t\ts.colopts = 0;\n+\t\ts.display_comment_prefix = 1;\n+\t\ts.hints = 0;\n+\t\ts.use_color = 0;\n+\t\ts.whence = FROM_COMMIT;\n+\t\ts.committable = 1;\n+\n+\t\ts.fp = fopen(git_path_commit_editmsg(), \"a\");\n+\t\tif (!s.fp)\n+\t\t\treturn error_errno(_(\"could not open '%s'\"), git_path_commit_editmsg());\n+\n+\t\twt_status_collect_changes_trees(&s, old_tree, new_tree);\n+\t\twt_status_print(&s);\n+\t\twt_status_collect_free_buffers(&s);\n+\t\tstring_list_clear_func(&s.change, change_data_free);\n+\n+\t\tstrbuf_reset(out);\n+\t\tif (launch_editor(path, out, NULL)) {\n+\t\t\tfprintf(stderr, _(\"Please supply the message using the -m option.\\n\"));\n+\t\t\treturn -1;\n+\t\t}\n+\t\tstrbuf_stripspace(out, comment_line_str);\n+\t} else {\n+\t\tstrbuf_addstr(out, provided_message);\n+\t}\n+\n+\tcleanup_message(out, COMMIT_MSG_CLEANUP_ALL, 0);\n+\n+\tif (!out->len) {\n+\t\tfprintf(stderr, _(\"Aborting commit due to empty commit message.\\n\"));\n+\t\treturn -1;\n+\t}\n+\n+\treturn 0;\n+}\n+\n+static int cmd_history_reword(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\tN_(\"git history reword [<options>] <commit>\"),\n+\t\tNULL,\n+\t};\n+\tconst char *commit_message = NULL;\n+\tstruct option options[] = {\n+\t\tOPT_STRING('m', \"message\", &commit_message, N_(\"message\"), N_(\"commit message\")),\n+\t\tOPT_END(),\n+\t};\n+\tstruct strbuf final_message = STRBUF_INIT;\n+\tstruct commit *original_commit, *parent, *head;\n+\tstruct strvec commits = STRVEC_INIT;\n+\tstruct object_id parent_tree_oid, original_commit_tree_oid;\n+\tstruct object_id rewritten_commit;\n+\tconst char *original_message, *original_body, *ptr;\n+\tchar *original_author = NULL;\n+\tsize_t len;\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 single revision\"));\n+\t\tgoto out;\n+\t}\n+\trepo_config(repo, git_default_config, NULL);\n+\n+\toriginal_commit = lookup_commit_reference_by_name(argv[0]);\n+\tif (!original_commit) {\n+\t\tret = error(_(\"commit to be reworded cannot be found: %s\"), argv[0]);\n+\t\tgoto out;\n+\t}\n+\tif (repo_parse_commit(repo, original_commit)) {\n+\t\tret = error(_(\"unable to parse commit %s\"),\n+\t\t\t    oid_to_hex(&original_commit->object.oid));\n+\t\tgoto out;\n+\t}\n+\toriginal_commit_tree_oid = repo_get_commit_tree(repo, original_commit)->object.oid;\n+\n+\tparent = original_commit->parents ? original_commit->parents->item : NULL;\n+\tif (parent) {\n+\t\tif (repo_parse_commit(repo, parent)) {\n+\t\t\tret = error(_(\"unable to parse commit %s\"),\n+\t\t\t\t    oid_to_hex(&parent->object.oid));\n+\t\t\tgoto out;\n+\t\t}\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+\thead = lookup_commit_reference_by_name(\"HEAD\");\n+\tif (!head) {\n+\t\tret = error(_(\"could not resolve HEAD to a commit\"));\n+\t\tgoto out;\n+\t}\n+\n+\t/*\n+\t * Collect the list of commits that we'll have to reapply now already.\n+\t * This ensures that we'll abort early on in case the range of commits\n+\t * contains merges, which we do not yet handle.\n+\t */\n+\tret = collect_commits(repo, parent, head, &commits);\n+\tif (ret < 0)\n+\t\tgoto out;\n+\n+\t/* We retain authorship of the original commit. */\n+\toriginal_message = repo_logmsg_reencode(repo, original_commit, 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_commit_tree_oid,\n+\t\t\t\t  original_body, commit_message, \"reworded\", &final_message);\n+\tif (ret < 0)\n+\t\tgoto out;\n+\n+\tret = commit_tree(final_message.buf, final_message.len,\n+\t\t\t  &repo_get_commit_tree(repo, original_commit)->object.oid,\n+\t\t\t  original_commit->parents, &rewritten_commit, original_author, NULL);\n+\tif (ret < 0) {\n+\t\tret = error(_(\"failed writing reworded commit\"));\n+\t\tgoto out;\n+\t}\n+\n+\treplace_commits(&commits, &original_commit->object.oid, &rewritten_commit, 1);\n+\n+\tret = apply_commits(repo, &commits, parent, head, \"reword\");\n+\tif (ret < 0)\n+\t\tgoto out;\n+\n+\tret = 0;\n+\n+out:\n+\tstrbuf_release(&final_message);\n+\tstrvec_clear(&commits);\n+\tfree(original_author);\n+\treturn ret;\n+}\n \n int cmd_history(int argc,\n \t\tconst char **argv,\n \t\tconst char *prefix,\n-\t\tstruct repository *repo UNUSED)\n+\t\tstruct repository *repo)\n {\n \tconst char * const usage[] = {\n \t\tN_(\"git history [<options>]\"),\n+\t\tN_(\"git history reword [<options>] <commit>\"),\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_END(),\n \t};\n \n \targc = parse_options(argc, argv, prefix, options, usage, 0);\n-\tif (argc)\n-\t\tusagef(\"unrecognized argument: %s\", argv[0]);\n-\treturn 0;\n+\treturn fn(argc, argv, prefix, repo);\n }\ndiff --git a/t/meson.build b/t/meson.build\nindex 8b31eb0858..2a74243202 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -383,6 +383,7 @@ integration_tests = [\n   't3437-rebase-fixup-options.sh',\n   't3438-rebase-broken-files.sh',\n   't3450-history.sh',\n+  't3451-history-reword.sh',\n   't3500-cherry.sh',\n   't3501-revert-cherry-pick.sh',\n   't3502-cherry-pick-merge.sh',\ndiff --git a/t/t3450-history.sh b/t/t3450-history.sh\nindex 417c343d43..f513463b92 100755\n--- a/t/t3450-history.sh\n+++ b/t/t3450-history.sh\n@@ -5,13 +5,13 @@ test_description='tests for git-history command'\n . ./test-lib.sh\n \n test_expect_success 'does nothing without any arguments' '\n-\tgit history >out 2>&1 &&\n-\ttest_must_be_empty out\n+\ttest_must_fail git history 2>err &&\n+\ttest_grep \"need a subcommand\" err\n '\n \n test_expect_success 'raises an error with unknown argument' '\n \ttest_must_fail git history garbage 2>err &&\n-\ttest_grep \"unrecognized argument: garbage\" err\n+\ttest_grep \"unknown subcommand: .garbage.\" err\n '\n \n test_done\ndiff --git a/t/t3451-history-reword.sh b/t/t3451-history-reword.sh\nnew file mode 100755\nindex 0000000000..84d203643e\n--- /dev/null\n+++ b/t/t3451-history-reword.sh\n@@ -0,0 +1,202 @@\n+#!/bin/sh\n+\n+test_description='tests for git-history reword subcommand'\n+\n+. ./test-lib.sh\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 reword HEAD~ 2>err &&\n+\t\ttest_grep \"cannot rearrange commit history with merges\" err &&\n+\t\ttest_must_fail git history reword HEAD 2>err &&\n+\t\ttest_grep \"cannot rearrange commit history with merges\" err\n+\t)\n+'\n+\n+test_expect_success 'can reword tip of a branch' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit first &&\n+\t\ttest_commit second &&\n+\t\ttest_commit third &&\n+\n+\t\tgit symbolic-ref HEAD >expect &&\n+\t\tgit history reword -m \"third reworded\" HEAD &&\n+\t\tgit symbolic-ref HEAD >actual &&\n+\t\ttest_cmp expect actual &&\n+\n+\t\tcat >expect <<-EOF &&\n+\t\tthird reworded\n+\t\tsecond\n+\t\tfirst\n+\t\tEOF\n+\t\tgit log --format=%s >actual &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+test_expect_success 'can reword commit in the middle' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit first &&\n+\t\ttest_commit second &&\n+\t\ttest_commit third &&\n+\n+\t\tgit symbolic-ref HEAD >expect &&\n+\t\tgit history reword -m \"second reworded\" HEAD~ &&\n+\t\tgit symbolic-ref HEAD >actual &&\n+\t\ttest_cmp expect actual &&\n+\n+\t\tcat >expect <<-EOF &&\n+\t\tthird\n+\t\tsecond reworded\n+\t\tfirst\n+\t\tEOF\n+\t\tgit log --format=%s >actual &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+test_expect_success 'can reword root commit' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit first &&\n+\t\ttest_commit second &&\n+\t\ttest_commit third &&\n+\t\tgit history reword -m \"first reworded\" HEAD~2 &&\n+\n+\t\tcat >expect <<-EOF &&\n+\t\tthird\n+\t\tsecond\n+\t\tfirst reworded\n+\t\tEOF\n+\t\tgit log --format=%s >actual &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+test_expect_success 'can use editor to rewrite commit message' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit first &&\n+\n+\t\twrite_script fake-editor.sh <<-\\EOF &&\n+\t\tcp \"$1\" . &&\n+\t\tprintf \"\\namend a comment\\n\" >>\"$1\"\n+\t\tEOF\n+\t\ttest_set_editor \"$(pwd)\"/fake-editor.sh &&\n+\t\tgit history reword HEAD &&\n+\n+\t\tcat >expect <<-EOF &&\n+\t\tfirst\n+\n+\t\t# Please enter the commit message for the reworded changes. Lines starting\n+\t\t# with ${SQ}#${SQ} will be kept; you may remove them yourself if you want to.\n+\t\t# Changes to be committed:\n+\t\t#\tnew file:   first.t\n+\t\t#\n+\t\tEOF\n+\t\ttest_cmp expect COMMIT_EDITMSG &&\n+\n+\t\tcat >expect <<-EOF &&\n+\t\tfirst\n+\n+\t\tamend a comment\n+\n+\t\tEOF\n+\t\tgit log --format=%B >actual &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+# For now, git-history(1) does not yet execute any hooks. This is subject to\n+# change in the future, and if it does this test here is expected to start\n+# failing. In other words, this test is not an endorsement of the current\n+# status quo.\n+test_expect_success 'hooks are executed for rewritten commits' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit first &&\n+\t\ttest_commit second &&\n+\t\ttest_commit third &&\n+\n+\t\twrite_script .git/hooks/prepare-commit-msg <<-EOF &&\n+\t\ttouch \"$(pwd)/hooks.log\n+\t\tEOF\n+\t\twrite_script .git/hooks/post-commit <<-EOF &&\n+\t\ttouch \"$(pwd)/hooks.log\n+\t\tEOF\n+\t\twrite_script .git/hooks/post-rewrite <<-EOF &&\n+\t\ttouch \"$(pwd)/hooks.log\n+\t\tEOF\n+\n+\t\tgit history reword -m \"second reworded\" HEAD~ &&\n+\n+\t\tcat >expect <<-EOF &&\n+\t\tthird\n+\t\tsecond reworded\n+\t\tfirst\n+\t\tEOF\n+\t\tgit log --format=%s >actual &&\n+\t\ttest_cmp expect actual &&\n+\n+\t\ttest_path_is_missing hooks.log\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\ttest_commit first &&\n+\n+\t\ttest_must_fail git history reword -m \"\" HEAD 2>err &&\n+\t\ttest_grep \"Aborting commit due to empty commit message.\" 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\ttouch a b &&\n+\t\tgit add . &&\n+\t\tgit commit -m \"initial commit\" &&\n+\t\techo foo >a &&\n+\t\techo bar >b &&\n+\t\tgit add b &&\n+\t\tgit history reword HEAD -m message &&\n+\t\tcat >expect <<-\\EOF &&\n+\t\t M a\n+\t\tM  b\n+\t\t?? actual\n+\t\t?? expect\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.51.0.700.g236ee7b076.dirty\n\n"},{"id":"527691","messageId":"20251001-b4-pks-history-builtin-v4-7-8e61ddb86317@pks.im","threadId":"63986","inReplyTo":"20251001-b4-pks-history-builtin-v4-0-8e61ddb86317@pks.im","subject":"[PATCH v4 07/12] add-patch: split out header from \"add-interactive.h\"","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-01T15:57:33Z","receivedAt":"2025-10-01T15:58:08Z","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 | 23 +++--------------------\n add-patch.c       |  1 +\n add-patch.h       | 26 ++++++++++++++++++++++++++\n 3 files changed, 30 insertions(+), 20 deletions(-)\n\ndiff --git a/add-interactive.h b/add-interactive.h\nindex da49502b76..2e3d1d871d 100644\n--- a/add-interactive.h\n+++ b/add-interactive.h\n@@ -1,14 +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-};\n-\n-#define ADD_P_OPT_INIT { .context = -1, .interhunkcontext = -1 }\n+struct pathspec;\n+struct repository;\n \n struct add_i_state {\n \tstruct repository *r;\n@@ -35,21 +32,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 b0389c5d5b..9d0890fc49 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..4394c74107\n--- /dev/null\n+++ b/add-patch.h\n@@ -0,0 +1,26 @@\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+};\n+\n+#define ADD_P_OPT_INIT { .context = -1, .interhunkcontext = -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.51.0.700.g236ee7b076.dirty\n\n"},{"id":"527692","messageId":"20251001-b4-pks-history-builtin-v4-8-8e61ddb86317@pks.im","threadId":"63986","inReplyTo":"20251001-b4-pks-history-builtin-v4-0-8e61ddb86317@pks.im","subject":"[PATCH v4 08/12] add-patch: split out `struct interactive_options`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-01T15:57:34Z","receivedAt":"2025-10-01T15:58:13Z","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 the infra for \"git add -p\"\nand \"git add -i\". Users of `run_add_i()` for example are expected to\npass `struct add_p_opt`. This is somewhat confusing and raises the\nquestion 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  | 174 +++++++++++------------------------------------------\n add-interactive.h  |  23 +------\n add-patch.c        | 170 +++++++++++++++++++++++++++++++++++++++++++--------\n add-patch.h        |  36 ++++++++++-\n builtin/add.c      |  22 +++----\n builtin/checkout.c |   4 +-\n builtin/commit.c   |  16 ++---\n builtin/reset.c    |  16 ++---\n builtin/stash.c    |  46 +++++++-------\n commit.h           |   2 +-\n 10 files changed, 270 insertions(+), 239 deletions(-)\n\ndiff --git a/add-interactive.c b/add-interactive.c\nindex 6ffe64c38d..f2d971818e 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,119 +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-\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@@ -285,7 +183,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@@ -353,7 +251,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@@ -431,7 +329,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@@ -991,7 +889,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@@ -1013,9 +911,9 @@ 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\tstruct interactive_options opts = {\n+\t\t\t.context = s->cfg.context,\n+\t\t\t.interhunkcontext = s->cfg.interhunkcontext,\n \t\t};\n \t\tstruct strvec args = STRVEC_INIT;\n \t\tstruct pathspec ps_selected = { 0 };\n@@ -1027,7 +925,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@@ -1063,10 +961,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@@ -1084,17 +982,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@@ -1102,21 +1000,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@@ -1151,7 +1049,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@@ -1162,7 +1060,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@@ -1205,15 +1103,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 2e3d1d871d..eefa2edc7c 100644\n--- a/add-interactive.h\n+++ b/add-interactive.h\n@@ -2,37 +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+\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 9d0890fc49..29c15695dd 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,122 @@ 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+\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 +417,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 +542,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 +576,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 +811,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 +833,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@@ -1103,12 +1221,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@@ -1227,7 +1345,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@@ -1241,7 +1359,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@@ -1522,15 +1640,15 @@ static int patch_update_file(struct add_p_state *s,\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 \t\t\t\t\t\t: 1));\n \t\tprintf(_(s->mode->prompt_mode[prompt_mode_type]),\n \t\t       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\tbreak;\n@@ -1687,7 +1805,7 @@ static int patch_update_file(struct add_p_state *s,\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@@ -1705,7 +1823,7 @@ static int patch_update_file(struct add_p_state *s,\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@@ -1723,7 +1841,7 @@ static int patch_update_file(struct add_p_state *s,\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@@ -1764,7 +1882,7 @@ static int patch_update_file(struct add_p_state *s,\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@@ -1772,7 +1890,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;\ndiff --git a/add-patch.h b/add-patch.h\nindex 4394c74107..a4a05d9d14 100644\n--- a/add-patch.h\n+++ b/add-patch.h\n@@ -1,15 +1,45 @@\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 };\n \n-#define ADD_P_OPT_INIT { .context = -1, .interhunkcontext = -1 }\n+#define INTERACTIVE_OPTIONS_INIT { \\\n+\t.context = -1, \\\n+\t.interhunkcontext = -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+};\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@@ -20,7 +50,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 4cd3d183f9..08e2ef98b0 100644\n--- a/builtin/add.c\n+++ b/builtin/add.c\n@@ -30,7 +30,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@@ -159,7 +159,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@@ -171,9 +171,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@@ -255,8 +255,8 @@ 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_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@@ -399,9 +399,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@@ -411,11 +411,11 @@ 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}\n \ndiff --git a/builtin/checkout.c b/builtin/checkout.c\nindex f9453473fe..d230b1f899 100644\n--- a/builtin/checkout.c\n+++ b/builtin/checkout.c\n@@ -546,7 +546,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};\n@@ -575,7 +575,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 0243f17d53..640495cc57 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@@ -356,9 +356,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@@ -407,7 +407,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@@ -432,9 +432,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@@ -1742,8 +1742,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 ed35802af1..088449e120 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,8 +371,8 @@ 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_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@@ -423,9 +423,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@@ -436,12 +436,12 @@ 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}\n \ndiff --git a/builtin/stash.c b/builtin/stash.c\nindex 948eba06fb..3b50905233 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,8 +1851,8 @@ 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_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@@ -1907,19 +1909,19 @@ 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}\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@@ -1944,7 +1946,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@@ -1952,8 +1954,8 @@ 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_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@@ -1973,20 +1975,20 @@ 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}\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 1d6e0c7518..7b6e59d6c1 100644\n--- a/commit.h\n+++ b/commit.h\n@@ -258,7 +258,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.51.0.700.g236ee7b076.dirty\n\n"},{"id":"527693","messageId":"20251001-b4-pks-history-builtin-v4-9-8e61ddb86317@pks.im","threadId":"63986","inReplyTo":"20251001-b4-pks-history-builtin-v4-0-8e61ddb86317@pks.im","subject":"[PATCH v4 09/12] add-patch: remove dependency on \"add-interactive\" subsystem","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-01T15:57:35Z","receivedAt":"2025-10-01T15:58:15Z","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 | 70 ++++++++++++++++++++++++++++++++-----------------------------\n 1 file changed, 37 insertions(+), 33 deletions(-)\n\ndiff --git a/add-patch.c b/add-patch.c\nindex 29c15695dd..9c0f3b23ef 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@@ -408,7 +408,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@@ -417,9 +417,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@@ -437,7 +437,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@@ -542,12 +542,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@@ -576,9 +576,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@@ -811,7 +811,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@@ -833,7 +833,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@@ -1221,12 +1221,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@@ -1345,7 +1345,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@@ -1359,7 +1359,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@@ -1640,15 +1640,15 @@ static int patch_update_file(struct add_p_state *s,\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 \t\t\t\t\t\t: 1));\n \t\tprintf(_(s->mode->prompt_mode[prompt_mode_type]),\n \t\t       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\tbreak;\n@@ -1805,7 +1805,7 @@ static int patch_update_file(struct add_p_state *s,\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@@ -1823,7 +1823,7 @@ static int patch_update_file(struct add_p_state *s,\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@@ -1841,7 +1841,7 @@ static int patch_update_file(struct add_p_state *s,\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@@ -1861,7 +1861,7 @@ static int patch_update_file(struct add_p_state *s,\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\t   s->mode->is_reverse);\n@@ -1872,8 +1872,8 @@ static int patch_update_file(struct add_p_state *s,\n \t\t\t\t\t NULL, 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\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@@ -1886,11 +1886,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\n-- \n2.51.0.700.g236ee7b076.dirty\n\n"},{"id":"527694","messageId":"20251001-b4-pks-history-builtin-v4-10-8e61ddb86317@pks.im","threadId":"63986","inReplyTo":"20251001-b4-pks-history-builtin-v4-0-8e61ddb86317@pks.im","subject":"[PATCH v4 10/12] add-patch: add support for in-memory index patching","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-01T15:57:36Z","receivedAt":"2025-10-01T15:58:19Z","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,\nworktree 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 | 110 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--\n add-patch.h |   8 +++++\n 2 files changed, 115 insertions(+), 3 deletions(-)\n\ndiff --git a/add-patch.c b/add-patch.c\nindex 9c0f3b23ef..fac82c3886 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@@ -437,7 +441,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@@ -1861,7 +1865,7 @@ static int patch_update_file(struct add_p_state *s,\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\t   s->mode->is_reverse);\n@@ -1872,9 +1876,11 @@ static int patch_update_file(struct add_p_state *s,\n \t\t\t\t\t NULL, 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 \tputchar('\\n');\n@@ -1887,6 +1893,8 @@ int run_add_p(struct repository *r, enum add_p_mode mode,\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@@ -1945,3 +1953,99 @@ int run_add_p(struct repository *r, enum add_p_mode mode,\n \tadd_p_state_clear(&s);\n \treturn 0;\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+\tstruct strbuf parent_revision = STRBUF_INIT;\n+\tchar parent_tree_oid[GIT_MAX_HEXSZ + 1];\n+\tsize_t binary_count = 0;\n+\tstruct commit *commit;\n+\tint ret;\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+\tstrbuf_addf(&parent_revision, \"%s~\", revision);\n+\tmode.diff_cmd[0] = \"diff-tree\";\n+\tmode.diff_cmd[1] = \"-r\";\n+\tmode.diff_cmd[2] = parent_tree_oid;\n+\n+\tinteractive_config_init(&s.cfg, r, opts);\n+\n+\tif (parse_diff(&s, ps) < 0) {\n+\t\tret = -1;\n+\t\tgoto out;\n+\t}\n+\n+\tfor (size_t i = 0; i < s.file_diff_nr; i++) {\n+\t\tif (s.file_diff[i].binary && !s.file_diff[i].hunk_nr)\n+\t\t\tbinary_count++;\n+\t\telse if (patch_update_file(&s, s.file_diff + i))\n+\t\t\tbreak;\n+\t}\n+\n+\tif (s.file_diff_nr == 0) {\n+\t\terr(&s, _(\"No changes.\"));\n+\t\tret = -1;\n+\t\tgoto out;\n+\t}\n+\n+\tif (binary_count == s.file_diff_nr) {\n+\t\terr(&s, _(\"Only binary files changed.\"));\n+\t\tret = -1;\n+\t\tgoto out;\n+\t}\n+\n+\tret = 0;\n+\n+out:\n+\tstrbuf_release(&parent_revision);\n+\tadd_p_state_clear(&s);\n+\treturn ret;\n+}\ndiff --git a/add-patch.h b/add-patch.h\nindex a4a05d9d14..901c42fd7b 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@@ -53,4 +54,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.51.0.700.g236ee7b076.dirty\n\n"},{"id":"527695","messageId":"20251001-b4-pks-history-builtin-v4-11-8e61ddb86317@pks.im","threadId":"63986","inReplyTo":"20251001-b4-pks-history-builtin-v4-0-8e61ddb86317@pks.im","subject":"[PATCH v4 11/12] cache-tree: allow writing in-memory index as tree","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-01T15:57:37Z","receivedAt":"2025-10-01T15:58:22Z","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       | 5 ++---\n cache-tree.h       | 3 ++-\n 3 files changed, 6 insertions(+), 5 deletions(-)\n\ndiff --git a/builtin/checkout.c b/builtin/checkout.c\nindex d230b1f8995..0b90f398feb 100644\n--- a/builtin/checkout.c\n+++ b/builtin/checkout.c\n@@ -902,7 +902,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 d225554eedd..1fe03605225 100644\n--- a/cache-tree.c\n+++ b/cache-tree.c\n@@ -700,11 +700,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 \n@@ -724,7 +724,6 @@ struct tree* write_in_core_index_as_tree(struct repository *repo) {\n \treturn lookup_tree(repo, &index_state->cache_tree->oid);\n }\n \n-\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 {\n \tint entries, was_valid;\ndiff --git a/cache-tree.h b/cache-tree.h\nindex b82c4963e7c..f8bddae5235 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.51.0.700.g236ee7b076.dirty\n\n"},{"id":"527696","messageId":"20251001-b4-pks-history-builtin-v4-12-8e61ddb86317@pks.im","threadId":"63986","inReplyTo":"20251001-b4-pks-history-builtin-v4-0-8e61ddb86317@pks.im","subject":"[PATCH v4 12/12] builtin/history: implement \"split\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-01T15:57:38Z","receivedAt":"2025-10-01T15:58:25Z","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              | 225 +++++++++++++++++++++\n t/meson.build                  |   1 +\n t/t3452-history-split.sh       | 432 +++++++++++++++++++++++++++++++++++++++++\n 4 files changed, 720 insertions(+)\n\ndiff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc\nindex b55babe206..83d675afea 100644\n--- a/Documentation/git-history.adoc\n+++ b/Documentation/git-history.adoc\n@@ -10,6 +10,7 @@ SYNOPSIS\n [synopsis]\n git history [<options>]\n git history reword [<options>] <commit>\n+git history split [<options>] <commit> [--] [<pathspec>...]\n \n DESCRIPTION\n -----------\n@@ -40,6 +41,26 @@ rewrite history in different ways:\n \tprovided, then this command will spawn an editor with the current\n \tmessage of that commit.\n \n+`split [--message=<message>] <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 message of the new commit will be asked for by launching the\n+configured editor, unless it has been specified with the `-m` option.\n+Authorship of the commit will be the same as for the 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 CONFIGURATION\n -------------\n \n@@ -47,6 +68,47 @@ include::includes/cmd-config-section-all.adoc[]\n \n include::config/sequencer.adoc[]\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 --message=\"split-out commit\"\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,e,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,e,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 7b2a0023e8..8851a2945e 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-reach.h\"\n #include \"commit.h\"\n #include \"config.h\"\n@@ -10,10 +11,13 @@\n #include \"hex.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 \"reset.h\"\n #include \"revision.h\"\n+#include \"run-command.h\"\n #include \"sequencer.h\"\n #include \"strvec.h\"\n #include \"tree.h\"\n@@ -368,6 +372,225 @@ static int cmd_history_reword(int argc,\n \treturn ret;\n }\n \n+static int split_commit(struct repository *repo,\n+\t\t\tstruct commit *original_commit,\n+\t\t\tstruct pathspec *pathspec,\n+\t\t\tconst char *commit_message,\n+\t\t\tstruct object_id *out)\n+{\n+\tstruct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;\n+\tstruct strbuf index_file = STRBUF_INIT, split_message = STRBUF_INIT;\n+\tstruct child_process read_tree_cmd = CHILD_PROCESS_INIT;\n+\tstruct index_state index = INDEX_STATE_INIT(repo);\n+\tstruct object_id original_commit_tree_oid, parent_tree_oid;\n+\tconst char *original_message, *original_body, *ptr;\n+\tchar original_commit_oid[GIT_MAX_HEXSZ + 1];\n+\tchar *original_author = NULL;\n+\tstruct commit_list *parents = NULL;\n+\tstruct commit *first_commit;\n+\tstruct tree *split_tree;\n+\tsize_t len;\n+\tint ret;\n+\n+\tif (original_commit->parents)\n+\t\tparent_tree_oid = *get_commit_tree_oid(original_commit->parents->item);\n+\telse\n+\t\toidcpy(&parent_tree_oid, repo->hash_algo->empty_tree);\n+\toriginal_commit_tree_oid = *get_commit_tree_oid(original_commit);\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+\tread_tree_cmd.git_cmd = 1;\n+\tstrvec_pushf(&read_tree_cmd.env, \"GIT_INDEX_FILE=%s\", index_file.buf);\n+\tstrvec_push(&read_tree_cmd.args, \"read-tree\");\n+\tstrvec_push(&read_tree_cmd.args, oid_to_hex(&parent_tree_oid));\n+\tret = run_command(&read_tree_cmd);\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_commit->object.oid);\n+\tret = run_add_p_index(repo, &index, index_file.buf, &interactive_opts,\n+\t\t\t      original_commit_oid, pathspec);\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+\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/* We retain authorship of the original commit. */\n+\toriginal_message = repo_logmsg_reencode(repo, original_commit, NULL, NULL);\n+\tptr = find_commit_header(original_message, \"author\", &len);\n+\tif (ptr)\n+\t\toriginal_author = xmemdupz(ptr, len);\n+\n+\tret = fill_commit_message(repo, &parent_tree_oid, &split_tree->object.oid,\n+\t\t\t\t  \"\", commit_message, \"split-out\", &split_message);\n+\tif (ret < 0)\n+\t\tgoto out;\n+\n+\tret = commit_tree(split_message.buf, split_message.len, &split_tree->object.oid,\n+\t\t\t  original_commit->parents, &out[0], original_author, NULL);\n+\tif (ret < 0) {\n+\t\tret = error(_(\"failed writing split-out commit\"));\n+\t\tgoto out;\n+\t}\n+\n+\t/*\n+\t* The second commit is much simpler to construct, as we can simply use\n+\t* the original commit details, except that we adjust its parent to be\n+\t* the newly split-out commit.\n+\t*/\n+\tfind_commit_subject(original_message, &original_body);\n+\tfirst_commit = lookup_commit_reference(repo, &out[0]);\n+\tcommit_list_append(first_commit, &parents);\n+\n+\tret = commit_tree(original_body, strlen(original_body), &original_commit_tree_oid,\n+\t\t\t  parents, &out[1], original_author, NULL);\n+\tif (ret < 0) {\n+\t\tret = error(_(\"failed writing second commit\"));\n+\t\tgoto out;\n+\t}\n+\n+\tret = 0;\n+\n+out:\n+\tif (index_file.len)\n+\t\tunlink(index_file.buf);\n+\tstrbuf_release(&split_message);\n+\tstrbuf_release(&index_file);\n+\tfree_commit_list(parents);\n+\tfree(original_author);\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\tN_(\"git history split [<options>] <commit>\"),\n+\t\tNULL,\n+\t};\n+\tconst char *commit_message = NULL;\n+\tstruct option options[] = {\n+\t\tOPT_STRING('m', \"message\", &commit_message, N_(\"message\"), N_(\"commit message\")),\n+\t\tOPT_END(),\n+\t};\n+\tstruct oidmap rewritten_commits = OIDMAP_INIT;\n+\tstruct commit *original_commit, *parent, *head;\n+\tstruct strvec commits = STRVEC_INIT;\n+\tstruct commit_list *list = NULL;\n+\tstruct object_id split_commits[2];\n+\tstruct pathspec pathspec = { 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 revision\"));\n+\t\tgoto out;\n+\t}\n+\trepo_config(repo, git_default_config, NULL);\n+\n+\toriginal_commit = lookup_commit_reference_by_name(argv[0]);\n+\tif (!original_commit) {\n+\t\tret = error(_(\"commit to be split cannot be found: %s\"), argv[0]);\n+\t\tgoto out;\n+\t}\n+\n+\tif (original_commit->parents && original_commit->parents->next) {\n+\t\tret = error(_(\"commit to be split must not be a merge commit\"));\n+\t\tgoto out;\n+\t}\n+\n+\tparent = original_commit->parents ? original_commit->parents->item : NULL;\n+\tif (parent && repo_parse_commit(repo, parent)) {\n+\t\tret = error(_(\"unable to parse commit %s\"),\n+\t\t\t    oid_to_hex(&parent->object.oid));\n+\t\tgoto out;\n+\t}\n+\n+\thead = lookup_commit_reference_by_name(\"HEAD\");\n+\tif (!head) {\n+\t\tret = error(_(\"could not resolve HEAD to a commit\"));\n+\t\tgoto out;\n+\t}\n+\n+\tcommit_list_append(original_commit, &list);\n+\tif (!repo_is_descendant_of(repo, original_commit, list)) {\n+\t\tret = error (_(\"split commit must be reachable from current HEAD commit\"));\n+\t\tgoto out;\n+\t}\n+\n+\tparse_pathspec(&pathspec, 0,\n+\t\tPATHSPEC_PREFER_FULL | PATHSPEC_SYMLINK_LEADING_PATH | PATHSPEC_PREFIX_ORIGIN,\n+\t\tprefix, argv + 1);\n+\n+\t/*\n+\t* Collect the list of commits that we'll have to reapply now already.\n+\t* This ensures that we'll abort early on in case the range of commits\n+\t* contains merges, which we do not yet handle.\n+\t*/\n+\tret = collect_commits(repo, parent, head, &commits);\n+\tif (ret < 0)\n+\t\tgoto out;\n+\n+\t/*\n+\t * Then we split up the commit and replace the original commit with the\n+\t * new new ones.\n+\t */\n+\tret = split_commit(repo, original_commit, &pathspec,\n+\t\t\t   commit_message, split_commits);\n+\tif (ret < 0)\n+\t\tgoto out;\n+\n+\treplace_commits(&commits, &original_commit->object.oid,\n+\t\t\tsplit_commits, ARRAY_SIZE(split_commits));\n+\n+\tret = apply_commits(repo, &commits, parent, head, \"split\");\n+\tif (ret < 0)\n+\t\tgoto out;\n+\n+\tret = 0;\n+\n+out:\n+\toidmap_clear(&rewritten_commits, 0);\n+\tclear_pathspec(&pathspec);\n+\tstrvec_clear(&commits);\n+\tfree_commit_list(list);\n+\treturn ret;\n+}\n+\n int cmd_history(int argc,\n \t\tconst char **argv,\n \t\tconst char *prefix,\n@@ -376,11 +599,13 @@ int cmd_history(int argc,\n \tconst char * const usage[] = {\n \t\tN_(\"git history [<options>]\"),\n \t\tN_(\"git history reword [<options>] <commit>\"),\n+\t\tN_(\"git history split [<options>] <commit> [--] [<pathspec>...]\"),\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 2a74243202..fb05be16ae 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -384,6 +384,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..45d3b32ebf\n--- /dev/null\n+++ b/t/t3452-history-split.sh\n@@ -0,0 +1,432 @@\n+#!/bin/sh\n+\n+test_description='tests for git-history split subcommand'\n+\n+. ./test-lib.sh\n+\n+set_fake_editor () {\n+\twrite_script fake-editor.sh <<-\\EOF &&\n+\techo \"split-out commit\" >\"$1\"\n+\tEOF\n+\ttest_set_editor \"$(pwd)\"/fake-editor.sh\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 \"commit to be split must not be a merge commit\" err &&\n+\t\ttest_must_fail git history split HEAD~ 2>err &&\n+\t\ttest_grep \"cannot rearrange commit history with merges\" err\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 &&\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\tsplit-me\n+\t\tsplit-out commit\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+\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 &&\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\troot\n+\t\tsplit-out commit\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 &&\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\tsplit-me\n+\t\tsplit-out commit\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 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\tgit history split HEAD -m \"split-out commit\" <<-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+\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\tgit history split HEAD -m \"split-out commit\" <<-EOF &&\n+\t\tn\n+\t\ty\n+\t\tEOF\n+\n+\t\texpect_log <<-EOF &&\n+\t\tsplit-me\n+\t\tsplit-out commit\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 '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\ttest_must_fail git history split HEAD -m \"\" <<-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 'can specify message via option' '\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\tgit history split HEAD -m \"message option\" <<-EOF &&\n+\t\ty\n+\t\tn\n+\t\tEOF\n+\n+\t\texpect_log <<-EOF\n+\t\tsplit-me\n+\t\tmessage option\n+\t\tEOF\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\tcp \"$1\" . &&\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\tcat >expect <<-EOF &&\n+\n+\t\t# Please enter the commit message for the split-out changes. Lines starting\n+\t\t# with ${SQ}#${SQ} will be kept; you may remove them yourself if you want to.\n+\t\t# Changes to be committed:\n+\t\t#\tnew file:   bar\n+\t\t#\n+\t\tEOF\n+\t\ttest_cmp expect COMMIT_EDITMSG &&\n+\n+\t\texpect_log <<-EOF\n+\t\tsplit-me\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\tgit history split HEAD -m \"message option\" -- 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 '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 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\twrite_script .git/hooks/prepare-commit-msg <<-EOF &&\n+\t\ttouch \"$(pwd)/hooks.log\"\n+\t\tEOF\n+\t\twrite_script .git/hooks/post-commit <<-EOF &&\n+\t\ttouch \"$(pwd)/hooks.log\"\n+\t\tEOF\n+\t\twrite_script .git/hooks/post-rewrite <<-EOF &&\n+\t\ttouch \"$(pwd)/hooks.log\"\n+\t\tEOF\n+\n+\t\tset_fake_editor &&\n+\t\tgit history split HEAD <<-EOF &&\n+\t\ty\n+\t\tn\n+\t\tEOF\n+\n+\t\texpect_log <<-EOF &&\n+\t\tsplit-me\n+\t\tsplit-out commit\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\tgit history split HEAD -m a-only <<-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\tEOF\n+\t\tgit status --porcelain >actual &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+test_done\n\n-- \n2.51.0.700.g236ee7b076.dirty\n\n"},{"id":"527773","messageId":"e816989d-bb45-4696-bd2e-b1ac6c3fa312@app.fastmail.com","threadId":"63986","inReplyTo":"20251001-b4-pks-history-builtin-v4-8-8e61ddb86317@pks.im","subject":"Re: [PATCH v4 08/12] add-patch: split out `struct interactive_options`","fromName":"Kristoffer Haugsbakk","fromEmail":"kristofferhaugsbakk@fastmail.com","sentAt":"2025-10-02T09:25:33Z","receivedAt":"2025-10-02T09:26:38Z","isPatch":true,"sender":{"key":"kristofferhaugsbakk@fastmail.com","avatar":null},"body":"On Wed, Oct 1, 2025, at 17:57, Patrick Steinhardt wrote:\n> The `struct add_p_opt` is reused both by our the infra for \"git add -p\"\n\n“both by our the infra”\n\nI think an edit to this sentence got mixed up/jumbled with\nthe old version.\n\n> and \"git add -i\". Users of `run_add_i()` for example are expected to\n> pass `struct add_p_opt`. This is somewhat confusing and raises the\n> question which options apply to what part of the stack.\n\ns/question which/question of which/ ?\n\nOr: s/question which options apply to what/question of what options apply to what/\n\n>\n>[snip]\n"},{"id":"527774","messageId":"2607e71a-b7a7-405f-8eb5-e8cc756b0270@app.fastmail.com","threadId":"63986","inReplyTo":"20251001-b4-pks-history-builtin-v4-5-8e61ddb86317@pks.im","subject":"Re: [PATCH v4 05/12] builtin: add new \"history\" command","fromName":"Kristoffer Haugsbakk","fromEmail":"kristofferhaugsbakk@fastmail.com","sentAt":"2025-10-02T09:26:43Z","receivedAt":"2025-10-02T09:27:04Z","isPatch":true,"sender":{"key":"kristofferhaugsbakk@fastmail.com","avatar":null},"body":"On Wed, Oct 1, 2025, at 17:57, Patrick Steinhardt wrote:\n>[snip]\n>\n> While these operations are all doable, it often feels needlessly cludgy\n\ns/cludgy/kludgey/\n\n> to do so by doing an interactive rebase, using the editor to say what\n> one wants, and then perform the actions. Furthermore, some operations\n> like splitting up a commit into two are way more involved than that and\n> require a whole series of commands.\n>\n>[snip]\n"},{"id":"527775","messageId":"1a19d9c6-f0f2-41d6-9969-63f6425b7340@app.fastmail.com","threadId":"63986","inReplyTo":"20251001-b4-pks-history-builtin-v4-10-8e61ddb86317@pks.im","subject":"Re: [PATCH v4 10/12] add-patch: add support for in-memory index patching","fromName":"Kristoffer Haugsbakk","fromEmail":"kristofferhaugsbakk@fastmail.com","sentAt":"2025-10-02T09:28:23Z","receivedAt":"2025-10-02T09:28:47Z","isPatch":true,"sender":{"key":"kristofferhaugsbakk@fastmail.com","avatar":null},"body":"On Wed, Oct 1, 2025, at 17:57, Patrick Steinhardt wrote:\n> With `run_add_p()` callers have the ability to apply changes from a\n> specific revision to a repository's index. This infra supports several\n> different modes, like for example applying changes to the index,\n> worktree or both.\n\ns/worktree/working tree/ ?\n\n>\n>[snip]\n"},{"id":"527780","messageId":"aN5SwUn4beYvFJ4n@pks.im","threadId":"63986","inReplyTo":"1a19d9c6-f0f2-41d6-9969-63f6425b7340@app.fastmail.com","subject":"Re: [PATCH v4 10/12] add-patch: add support for in-memory index patching","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-02T10:24:01Z","receivedAt":"2025-10-02T10:24:09Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Thu, Oct 02, 2025 at 11:28:23AM +0200, Kristoffer Haugsbakk wrote:\n> On Wed, Oct 1, 2025, at 17:57, Patrick Steinhardt wrote:\n> > With `run_add_p()` callers have the ability to apply changes from a\n> > specific revision to a repository's index. This infra supports several\n> > different modes, like for example applying changes to the index,\n> > worktree or both.\n> \n> s/worktree/working tree/ ?\n\nYup. I've applied all of your feedback locally and will send it out with\nthe next version. Thanks!\n\nPatrick\n"},{"id":"528691","messageId":"CAOLa=ZTUDOYb1KVUEZY4KFyoaP-PdXjBvUto0vxOtYanEjoOFA@mail.gmail.com","threadId":"63986","inReplyTo":"20251001-b4-pks-history-builtin-v4-1-8e61ddb86317@pks.im","subject":"Re: [PATCH v4 01/12] wt-status: provide function to expose status for trees","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2025-10-14T08:49:14Z","receivedAt":"2025-10-14T08:49:16Z","isPatch":true,"sender":{"key":"karthik.188@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1786334?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> The \"wt-status\" subsystem is responsible for printing status information\n> around the current state of the working tree. This most importantly\n> includes information around whether the working tree or the index have\n> any changes.\n>\n> We're about to introduce a new command though where the changes in\n\nNit: s/though//\n\n> neither of them are actually relevant to us. Instead, what we want is to\n> format the changes between two different trees. While it is a little bit\n> of a stretch to add this as functionality to _working tree_ status, it\n> doesn't make any sense to open-code this functionality, either.\n>\n> Implement a new function `wt_status_collect_changes_trees()` that diffs\n> two trees and formats the status accordingly. This function is not yet\n> used, but will be in a subsequent commit.\n>\n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n>  wt-status.c | 24 ++++++++++++++++++++++++\n>  wt-status.h |  3 +++\n>  2 files changed, 27 insertions(+)\n>\n> diff --git a/wt-status.c b/wt-status.c\n> index 8ffe6d3988..b66edbfca6 100644\n> --- a/wt-status.c\n> +++ b/wt-status.c\n> @@ -612,6 +612,30 @@ static void wt_status_collect_updated_cb(struct diff_queue_struct *q,\n>  \t}\n>  }\n>\n> +void wt_status_collect_changes_trees(struct wt_status *s,\n> +\t\t\t\t     const struct object_id *old_treeish,\n> +\t\t\t\t     const struct object_id *new_treeish)\n> +{\n\nSo, my understanding here is that we want to diff two trees\n`old_treeish` and `new_treeish` and then finally store the status change\nin `wt_status`\n\n> +\tstruct diff_options opts = { 0 };\n> +\n> +\trepo_diff_setup(s->repo, &opts);\n> +\topts.output_format = DIFF_FORMAT_CALLBACK;\n> +\topts.format_callback = wt_status_collect_updated_cb;\n> +\topts.format_callback_data = s;\n> +\topts.detect_rename = s->detect_rename >= 0 ? s->detect_rename : opts.detect_rename;\n> +\topts.rename_limit = s->rename_limit >= 0 ? s->rename_limit : opts.rename_limit;\n> +\topts.rename_score = s->rename_score >= 0 ? s->rename_score : opts.rename_score;\n\nCurious, why do we need a '>= 0' check here?\n\n> +\topts.flags.recursive = 1;\n> +\tdiff_setup_done(&opts);\n> +\n>\n\nSo first we setup the diff options, with the right callbacks so that the\nrelevant information is added to the `wt_status`.\n\n> +\tdiff_tree_oid(old_treeish, new_treeish, \"\", &opts);\n> +\tdiffcore_std(&opts);\n> +\tdiff_flush(&opts);\n\nThis is the part which calls the callback function with the relevant\ninformation and callback data.\n\n> +\twt_status_get_state(s->repo, &s->state, 0);\n> +\n\nBased on the list of diff data in `s->change`, we add the status print\ninformation. Okay makes sense.\n\n> +\tdiff_free(&opts);\n> +}\n> +\n>  static void wt_status_collect_changes_worktree(struct wt_status *s)\n>  {\n>  \tstruct rev_info rev;\n> diff --git a/wt-status.h b/wt-status.h\n> index e40a27214a..924d7a5fa9 100644\n> --- a/wt-status.h\n> +++ b/wt-status.h\n> @@ -153,6 +153,9 @@ void wt_status_add_cut_line(struct wt_status *s);\n>  void wt_status_prepare(struct repository *r, struct wt_status *s);\n>  void wt_status_print(struct wt_status *s);\n>  void wt_status_collect(struct wt_status *s);\n> +void wt_status_collect_changes_trees(struct wt_status *s,\n> +\t\t\t\t     const struct object_id *old_treeish,\n> +\t\t\t\t     const struct object_id *new_treeish);\n>  /*\n>   * Frees the buffers allocated by wt_status_collect.\n>   */\n>\n> --\n> 2.51.0.700.g236ee7b076.dirty\n\nSo this function will be used in an upcoming patch, looks good.\n"},{"id":"528692","messageId":"CAOLa=ZQQXEjTPLOwdsPKd2U1xzq1MwjGDeDdCAbv_eg4NK-TNA@mail.gmail.com","threadId":"63986","inReplyTo":"20251001-b4-pks-history-builtin-v4-3-8e61ddb86317@pks.im","subject":"Re: [PATCH v4 03/12] replay: stop using `the_repository`","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2025-10-14T08:53:18Z","receivedAt":"2025-10-14T08:53:20Z","isPatch":true,"sender":{"key":"karthik.188@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1786334?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> In `create_commit()` we're using `the_repository` even though we already\n> have a repository passed to use as an argument. Fix this.\n>\n\nI was thinking about this as I read your previous patch. This makes\nsense.\n\n> Note that we still cannot get rid of `USE_THE_REPOSITORY_VARIABLE`. This\n> is because we use `DEFAULT_ABBREV and `get_commit_output_encoding()`,\n> both of which are stored as global variables that can be modified via\n> the Git configuration.\n>\n\nThat's a bummer. But one less step needed is a win.\n\n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n>  replay.c | 2 +-\n>  1 file changed, 1 insertion(+), 1 deletion(-)\n>\n> diff --git a/replay.c b/replay.c\n> index e22ce39940..13d75d8054 100644\n> --- a/replay.c\n> +++ b/replay.c\n> @@ -62,7 +62,7 @@ static struct commit *create_commit(struct repository *repo,\n>  \tobj = parse_object(repo, &ret);\n>\n>  out:\n> -\trepo_unuse_commit_buffer(the_repository, based_on, message);\n> +\trepo_unuse_commit_buffer(repo, based_on, message);\n>  \tfree_commit_extra_headers(extra);\n>  \tfree_commit_list(parents);\n>  \tstrbuf_release(&msg);\n>\n> --\n> 2.51.0.700.g236ee7b076.dirty\n"},{"id":"528694","messageId":"CAOLa=ZTU7JvqiDqDK0gHbR1KshZ8A_rZgguNZykcHp2i--GQAw@mail.gmail.com","threadId":"63986","inReplyTo":"20251001-b4-pks-history-builtin-v4-4-8e61ddb86317@pks.im","subject":"Re: [PATCH v4 04/12] replay: parse commits before dereferencing them","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2025-10-14T08:57:15Z","receivedAt":"2025-10-14T08:57:18Z","isPatch":true,"sender":{"key":"karthik.188@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1786334?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> When looking up a commit it may not be parsed yet. Callers that wish to\n> access the fields of `struct commit` have to call `repo_parse_commit()`\n> first so that it is guaranteed to be populated.\n>\n> We didn't yet care about doing so, because code paths that lead to\n> `pick_regular_commit()` in \"builtin/replay.c\" already implicitly parsed\n> the commits. But now that the function is exposed to outside callers\n> it's quite easy to get this wrong.\n>\n\nSo I was wondering, wouldn't this duplicate the call made to\n`pick_regular_commit()` and end up parsing the commit twice. But seems\nlike down the stack in `repo_parse_commit_internal()`, we check for\n`item->object.parsed` and only parse if it hasn't been already parsed.\nSo this change is welcome.\n\n> Make the function easier to use by calling `repo_parse_commit()`.\n>\n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n>  replay.c | 3 +++\n>  1 file changed, 3 insertions(+)\n>\n> diff --git a/replay.c b/replay.c\n> index 13d75d8054..c3628d2488 100644\n> --- a/replay.c\n> +++ b/replay.c\n> @@ -90,6 +90,9 @@ struct commit *replay_pick_regular_commit(struct repository *repo,\n>  \tstruct commit *base, *replayed_base;\n>  \tstruct tree *pickme_tree, *base_tree;\n>\n> +\tif (repo_parse_commit(repo, pickme))\n> +\t\treturn NULL;\n> +\n>  \tbase = pickme->parents->item;\n>  \treplayed_base = mapped_commit(replayed_commits, base, onto);\n>\n>\n> --\n> 2.51.0.700.g236ee7b076.dirty\n"},{"id":"528696","messageId":"CAOLa=ZQSc0jHv9gK5T06GA_mprknucwCj5H_M2djZ-UWfRMbJQ@mail.gmail.com","threadId":"63986","inReplyTo":"20251001-b4-pks-history-builtin-v4-5-8e61ddb86317@pks.im","subject":"Re: [PATCH v4 05/12] builtin: add new \"history\" command","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2025-10-14T09:07:03Z","receivedAt":"2025-10-14T09:07:05Z","isPatch":true,"sender":{"key":"karthik.188@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1786334?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> When rewriting history via git-rebase(1) there are a couple of very\n> common use cases:\n>\n>   - The ordering of two commits should be reversed.\n>\n>   - A commit should be split up into two commits.\n>\n>   - A commit should be dropped from the history completely.\n>\n>   - Multiple commits should be squashed into one.\n>\n> While these operations are all doable, it often feels needlessly cludgy\n> to do so by doing an interactive rebase, using the editor to say what\n> one wants, and then perform the actions. Furthermore, some operations\n> like splitting up a commit into two are way more involved than that and\n> require a whole series of commands.\n>\n> Add a new \"history\" command to plug this gap. This command will have\n> several different subcommands to imperatively rewrite history for common\n> use cases like the above. These commands will be implemented in\n\nNit: s/commands/subcommands\n\n> subsequent commits.\n>\n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n\n[snip]\n\n> new file mode 100644\n> index 0000000000..1537960374\n> --- /dev/null\n> +++ b/Documentation/git-history.adoc\n> @@ -0,0 +1,45 @@\n> +git-history(1)\n> +==============\n> +\n> +NAME\n> +----\n> +git-history - EXPERIMENTAL: Rewrite history of the current branch\n> +\n> +SYNOPSIS\n> +--------\n> +[synopsis]\n> +git history [<options>]\n> +\n> +DESCRIPTION\n> +-----------\n> +\n> +Rewrite history by rearranging or modifying specific commits in the\n> +history.\n> +\n> +This command is similar to linkgit:git-rebase[1] and uses the same\n> +underlying machinery. You should use rebases if you either want to\n> +reapply a range of commits onto a different base, or interactive rebases\n> +if you want to edit a range of commits.\n> +\n>\n\nThe either..or in the last sentence is a bit confusing; as it is not an\neither between 'want to reapply a range of commit onto a different base'\n& 'interactive rebases'.\n\nPerhaps we can simply s/either//\n\n> +Note that this command does not (yet) work with histories that contain\n> +merges. You should use linkgit:git-rebase[1] with the `--rebase-merges`\n> +flag instead.\n> +\n> +THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.\n> +\n> +COMMANDS\n> +--------\n> +\n> +This command requires a subcommand. Several subcommands are available to\n> +rewrite history in different ways:\n> +\n> +CONFIGURATION\n> +-------------\n> +\n> +include::includes/cmd-config-section-all.adoc[]\n> +\n> +include::config/sequencer.adoc[]\n> +\n> +GIT\n> +---\n> +Part of the linkgit:git[1] suite\n\n[snip]\n\n> diff --git a/builtin/history.c b/builtin/history.c\n> new file mode 100644\n> index 0000000000..f6fe32610b\n> --- /dev/null\n> +++ b/builtin/history.c\n> @@ -0,0 +1,22 @@\n> +#include \"builtin.h\"\n> +#include \"gettext.h\"\n> +#include \"parse-options.h\"\n> +\n> +int cmd_history(int argc,\n> +\t\tconst char **argv,\n> +\t\tconst char *prefix,\n> +\t\tstruct repository *repo UNUSED)\n> +{\n> +\tconst char * const usage[] = {\n> +\t\tN_(\"git history [<options>]\"),\n> +\t\tNULL,\n> +\t};\n\nNit: We have pointer alignment set to 'Right' in our styling guide and\nalso mentioned in our 'Documentation/CodingGuidelines'\n\n   When declaring pointers, the star sides with the variable\n   name, i.e. \"char *string\", not \"char* string\" or\n   \"char * string\".  This makes it easier to understand code\n   like \"char *string, c;\".\n\nThe rest of the patch looks good!\n\n[snip]\n"},{"id":"528700","messageId":"CAOLa=ZSU8yr9Gn0EZ7x705qPyVM-qiMjgMCNCb8p8SMGTToxqQ@mail.gmail.com","threadId":"63986","inReplyTo":"20251001-b4-pks-history-builtin-v4-6-8e61ddb86317@pks.im","subject":"Re: [PATCH v4 06/12] builtin/history: implement \"reword\" subcommand","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2025-10-14T11:04:06Z","receivedAt":"2025-10-14T11:04:08Z","isPatch":true,"sender":{"key":"karthik.188@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1786334?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> Implement a new \"reword\" subcommand for git-history(1). This subcommand\n> is essentially the same as if a user performed an interactive rebase\n> with a single commit changed to use the \"reword\" verb.\n>\n\n[snip]\n\n> diff --git a/builtin/history.c b/builtin/history.c\n> index f6fe32610b..7b2a0023e8 100644\n> --- a/builtin/history.c\n> +++ b/builtin/history.c\n> @@ -1,22 +1,389 @@\n> +#define USE_THE_REPOSITORY_VARIABLE\n> +\n>  #include \"builtin.h\"\n> +#include \"commit-reach.h\"\n> +#include \"commit.h\"\n> +#include \"config.h\"\n> +#include \"editor.h\"\n> +#include \"environment.h\"\n>  #include \"gettext.h\"\n> +#include \"hex.h\"\n> +#include \"oidmap.h\"\n\nNit: This can be dropped, perhaps needed in a future patch?\n\n>  #include \"parse-options.h\"\n> +#include \"refs.h\"\n> +#include \"replay.h\"\n> +#include \"reset.h\"\n> +#include \"revision.h\"\n> +#include \"sequencer.h\"\n> +#include \"strvec.h\"\n> +#include \"tree.h\"\n> +#include \"wt-status.h\"\n> +\n> +static int collect_commits(struct repository *repo,\n> +\t\t\t   struct commit *old_commit,\n> +\t\t\t   struct commit *new_commit,\n> +\t\t\t   struct strvec *out)\n> +{\n> +\tstruct setup_revision_opt revision_opts = {\n> +\t\t.assume_dashdash = 1,\n> +\t};\n> +\tstruct strvec revisions = STRVEC_INIT;\n> +\tstruct commit_list *from_list = NULL;\n> +\tstruct commit *child;\n> +\tstruct rev_info rev = { 0 };\n> +\tint ret;\n> +\n> +\t/*\n> +\t * Check that the old commit actually is an ancestor of HEAD. If not\n> +\t * the whole request becomes nonsensical.\n> +\t*/\n\nMissing space here\n\n> +\tif (old_commit) {\n> +\t\tcommit_list_insert(old_commit, &from_list);\n> +\t\tif (!repo_is_descendant_of(repo, new_commit, from_list)) {\n> +\t\t\tret = error(_(\"commit must be reachable from current HEAD commit\"));\n> +\t\t\tgoto out;\n> +\t\t}\n> +\t}\n\nMakes sense. There is an inherent assumption using the 'git history'\ncommand that you want to modify the history of the current reference.\n\nOne question, wouldn't it make sense to parse and check that the commit\nto be reworded should be checked to be a descendant of HEAD earlier on\nin `cmd_history_reword()`?\n\nThis would ensure this function `collect_commits()` doesn't worry about\nhow it is meant to be used, and simply worries about collecting commits.\n\n\n> +\trepo_init_revisions(repo, &rev, NULL);\n> +\tstrvec_push(&revisions, \"\");\n> +\tstrvec_push(&revisions, oid_to_hex(&new_commit->object.oid));\n> +\tif (old_commit)\n> +\t\tstrvec_pushf(&revisions, \"^%s\", oid_to_hex(&old_commit->object.oid));\n> +\n> +\tsetup_revisions_from_strvec(&revisions, &rev, &revision_opts);\n> +\tif (revisions.nr != 1 || prepare_revision_walk(&rev)) {\n> +\t\tret = error(_(\"revision walk setup failed\"));\n> +\t\tgoto out;\n> +\t}\n> +\n> +\twhile ((child = get_revision(&rev))) {\n> +\t\tif (old_commit && !child->parents)\n> +\t\t\tBUG(\"revision walk did not find child commit\");\n> +\t\tif (child->parents && child->parents->next) {\n> +\t\t\tret = error(_(\"cannot rearrange commit history with merges\"));\n> +\t\t\tgoto out;\n> +\t\t}\n> +\n> +\t\tstrvec_push(out, oid_to_hex(&child->object.oid));\n> +\n> +\t\tif (child->parents && old_commit &&\n> +\t\t    commit_list_contains(old_commit, child->parents))\n> +\t\t\tbreak;\n> +\t}\n> +\n\nOkay makes sense here, we collect all the commits we break as soon as we\nreach old_commit. Since we check for merges at the start of the loop,\nthe history should be linear.\n\n[snip]\n\n> +static void replace_commits(struct strvec *commits,\n> +\t\t\t    const struct object_id *commit_to_replace,\n> +\t\t\t    const struct object_id *replacements,\n> +\t\t\t    size_t replacements_nr)\n> +{\n> +\tchar commit_to_replace_oid[GIT_MAX_HEXSZ + 1];\n> +\tstruct strvec replacement_oids = STRVEC_INIT;\n> +\tbool found = false;\n> +\n> +\toid_to_hex_r(commit_to_replace_oid, commit_to_replace);\n> +\tfor (size_t i = 0; i < replacements_nr; i++)\n> +\t\tstrvec_push(&replacement_oids, oid_to_hex(&replacements[i]));\n> +\n> +\tfor (size_t i = 0; i < commits->nr; i++) {\n> +\t\tif (strcmp(commits->v[i], commit_to_replace_oid))\n> +\t\t\tcontinue;\n> +\t\tstrvec_splice(commits, i, 1, replacement_oids.v, replacement_oids.nr);\n> +\t\tfound = true;\n> +\t\tbreak;\n> +\t}\n> +\tif (!found)\n> +\t\tBUG(\"could not find commit to replace\");\n> +\n> +\tstrvec_clear(&replacement_oids);\n> +}\n\nSo this basically goes over the commits that we pass and replaces a\nsingle commit with a set of commits. In our case that would be\n\n  C0 C1 C2 ...  CN\n   └─(reword)    └─(HEAD)\n\n     ↓ (rewrites to)\n\n  R0 R1 R2 ... RN C1 C2 ... CN\n   └────────────┘            └─(HEAD)\n    (new commits)\n\nMakes sense. I assume we use a list of replacements here for future\ncommands.\n\n> +static int apply_commits(struct repository *repo,\n> +\t\t\t const struct strvec *commits,\n> +\t\t\t struct commit *onto,\n> +\t\t\t struct commit *orig_head,\n> +\t\t\t const char *action)\n> +{\n> +\tstruct reset_head_opts reset_opts = { 0 };\n> +\tstruct merge_options merge_opts = { 0 };\n> +\tstruct merge_result result = { 0 };\n> +\tstruct strbuf buf = STRBUF_INIT;\n> +\tkh_oid_map_t *replayed_commits;\n> +\tint ret;\n> +\n> +\treplayed_commits = kh_init_oid_map();\n> +\n\nSo this is used to the pass the replayed_commits list to the replay\nmechanism so that the appropriate base commit is selected.\n\n> +\tinit_basic_merge_options(&merge_opts, repo);\n> +\tmerge_opts.show_rename_progress = 0;\n> +\n> +\tfor (size_t i = 0; i < commits->nr; i++) {\n> +\t\tstruct object_id commit_id;\n> +\t\tstruct commit *commit;\n> +\t\tconst char *end;\n> +\t\tint hash_result;\n> +\t\tkhint_t pos;\n> +\n> +\t\tif (parse_oid_hex_algop(commits->v[i], &commit_id, &end,\n> +\t\t\t\t\trepo->hash_algo)) {\n> +\t\t\tret = error(_(\"invalid object ID: %s\"), commits->v[i]);\n> +\t\t\tgoto out;\n> +\t\t}\n> +\n> +\t\tcommit = lookup_commit(repo, &commit_id);\n> +\t\tif (!commit || repo_parse_commit(repo, commit)) {\n> +\t\t\tret = error(_(\"failed to look up commit: %s\"), oid_to_hex(&commit_id));\n> +\t\t\tgoto out;\n> +\t\t}\n> +\n> +\t\tif (!onto) {\n> +\t\t\tonto = commit;\n> +\t\t\tresult.clean = 1;\n> +\t\t\tresult.tree = repo_get_commit_tree(repo, commit);\n\nSo if there is no onto, we're starting at the root commit and expect a\nclean merge.\n\n> +\t\t} else {\n> +\t\t\tonto = replay_pick_regular_commit(repo, commit, replayed_commits,\n> +\t\t\t\t\t\t\t  onto, &merge_opts, &result);\n> +\t\t\tif (!onto)\n> +\t\t\t\tbreak;\n\nElse we will replay the current commit onto the prev 'onto' commit.\n\n> +\t\t}\n> +\n> +\t\tpos = kh_put_oid_map(replayed_commits, commit->object.oid, &hash_result);\n> +\t\tif (hash_result == 0) {\n> +\t\t\tret = error(_(\"duplicate rewritten commit: %s\\n\"),\n> +\t\t\t\t    oid_to_hex(&commit->object.oid));\n> +\t\t\tgoto out;\n> +\t\t}\n> +\t\tkh_value(replayed_commits, pos) = onto;\n> +\t}\n> +\n> +\tif (!result.clean) {\n> +\t\tret = error(_(\"could not merge\"));\n> +\t\tgoto out;\n> +\t}\n> +\n> +\treset_opts.oid = &onto->object.oid;\n> +\tstrbuf_addf(&buf, \"%s: switch to rewritten %s\", action, oid_to_hex(reset_opts.oid));\n> +\treset_opts.flags = RESET_HEAD_REFS_ONLY | RESET_ORIG_HEAD;\n> +\treset_opts.orig_head = &orig_head->object.oid;\n> +\treset_opts.default_reflog_action = action;\n> +\tif (reset_head(repo, &reset_opts) < 0) {\n> +\t\tret = error(_(\"could not switch to %s\"), oid_to_hex(reset_opts.oid));\n> +\t\tgoto out;\n> +\t}\n> +\n\nWe finally update the HEAD reference also.\n\n> +\tret = 0;\n> +\n> +out:\n> +\tkh_destroy_oid_map(replayed_commits);\n> +\tmerge_finalize(&merge_opts, &result);\n> +\tstrbuf_release(&buf);\n> +\treturn ret;\n> +}\n> +\n> +static void change_data_free(void *util, const char *str UNUSED)\n> +{\n> +\tstruct wt_status_change_data *d = util;\n> +\tfree(d->rename_source);\n> +\tfree(d);\n> +}\n> +\n> +static int fill_commit_message(struct repository *repo,\n> +\t\t\t       const struct object_id *old_tree,\n> +\t\t\t       const struct object_id *new_tree,\n> +\t\t\t       const char *default_message,\n> +\t\t\t       const char *provided_message,\n> +\t\t\t       const char *action,\n> +\t\t\t       struct strbuf *out)\n> +{\n> +\tif (!provided_message) {\n> +\t\tconst char *path = git_path_commit_editmsg();\n> +\t\tconst char *hint =\n> +\t\t\t_(\"Please enter the commit message for the %s changes. Lines starting\\n\"\n> +     \"with '%s' will be kept; you may remove them yourself if you want to.\\n\");\n\nShouldn't this be s/kept/removed? Also this line needs to be aligned.\n\n> +\t\tstruct wt_status s;\n> +\n> +\t\tstrbuf_addstr(out, default_message);\n> +\t\tstrbuf_addch(out, '\\n');\n> +\t\tstrbuf_commented_addf(out, comment_line_str, hint, action, comment_line_str);\n> +\t\twrite_file_buf(path, out->buf, out->len);\n> +\n> +\t\twt_status_prepare(repo, &s);\n> +\t\tFREE_AND_NULL(s.branch);\n> +\t\ts.ahead_behind_flags = AHEAD_BEHIND_QUICK;\n> +\t\ts.commit_template = 1;\n> +\t\ts.colopts = 0;\n> +\t\ts.display_comment_prefix = 1;\n> +\t\ts.hints = 0;\n> +\t\ts.use_color = 0;\n> +\t\ts.whence = FROM_COMMIT;\n> +\t\ts.committable = 1;\n> +\n> +\t\ts.fp = fopen(git_path_commit_editmsg(), \"a\");\n> +\t\tif (!s.fp)\n> +\t\t\treturn error_errno(_(\"could not open '%s'\"), git_path_commit_editmsg());\n> +\n> +\t\twt_status_collect_changes_trees(&s, old_tree, new_tree);\n> +\t\twt_status_print(&s);\n> +\t\twt_status_collect_free_buffers(&s);\n> +\t\tstring_list_clear_func(&s.change, change_data_free);\n> +\n\nNice, so we show the user the diff tree and then ask them to modify the\nexisting message as they want.\n\n> +\t\tstrbuf_reset(out);\n> +\t\tif (launch_editor(path, out, NULL)) {\n> +\t\t\tfprintf(stderr, _(\"Please supply the message using the -m option.\\n\"));\n> +\t\t\treturn -1;\n> +\t\t}\n> +\t\tstrbuf_stripspace(out, comment_line_str);\n> +\t} else {\n> +\t\tstrbuf_addstr(out, provided_message);\n> +\t}\n> +\n> +\tcleanup_message(out, COMMIT_MSG_CLEANUP_ALL, 0);\n> +\n> +\tif (!out->len) {\n> +\t\tfprintf(stderr, _(\"Aborting commit due to empty commit message.\\n\"));\n> +\t\treturn -1;\n> +\t}\n> +\n> +\treturn 0;\n> +}\n> +\n> +static int cmd_history_reword(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\nSame as the prev commit, this should be '*const'.\n\n> +\t\tN_(\"git history reword [<options>] <commit>\"),\n> +\t\tNULL,\n> +\t};\n> +\tconst char *commit_message = NULL;\n> +\tstruct option options[] = {\n> +\t\tOPT_STRING('m', \"message\", &commit_message, N_(\"message\"), N_(\"commit message\")),\n> +\t\tOPT_END(),\n> +\t};\n> +\tstruct strbuf final_message = STRBUF_INIT;\n> +\tstruct commit *original_commit, *parent, *head;\n> +\tstruct strvec commits = STRVEC_INIT;\n> +\tstruct object_id parent_tree_oid, original_commit_tree_oid;\n> +\tstruct object_id rewritten_commit;\n> +\tconst char *original_message, *original_body, *ptr;\n> +\tchar *original_author = NULL;\n> +\tsize_t len;\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 single revision\"));\n> +\t\tgoto out;\n> +\t}\n> +\trepo_config(repo, git_default_config, NULL);\n> +\n\nRight. We only expect something like 'git history reword @~10', so we\nallow only one commit to be re-worded at a given time.\n\n> +\toriginal_commit = lookup_commit_reference_by_name(argv[0]);\n> +\tif (!original_commit) {\n> +\t\tret = error(_(\"commit to be reworded cannot be found: %s\"), argv[0]);\n> +\t\tgoto out;\n> +\t}\n\nSo here we want to parse the commit-ish name to find the commit.\n\n> +\tif (repo_parse_commit(repo, original_commit)) {\n> +\t\tret = error(_(\"unable to parse commit %s\"),\n> +\t\t\t    oid_to_hex(&original_commit->object.oid));\n> +\t\tgoto out;\n> +\t}\n\nIsn't this already done as part of\n`lookup_commit_reference_by_name_gently()` which is called by\n`lookup_commit_reference_by_name()` ?\n\n> +\toriginal_commit_tree_oid = repo_get_commit_tree(repo, original_commit)->object.oid;\n> +\n\nOkay we get the tree of the commit, I assume this is used to create the\nnew commit and replay the children on top of it.\n\n> +\tparent = original_commit->parents ? original_commit->parents->item : NULL;\n> +\tif (parent) {\n> +\t\tif (repo_parse_commit(repo, parent)) {\n> +\t\t\tret = error(_(\"unable to parse commit %s\"),\n> +\t\t\t\t    oid_to_hex(&parent->object.oid));\n> +\t\t\tgoto out;\n> +\t\t}\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> +\thead = lookup_commit_reference_by_name(\"HEAD\");\n> +\tif (!head) {\n> +\t\tret = error(_(\"could not resolve HEAD to a commit\"));\n> +\t\tgoto out;\n> +\t}\n> +\n> +\t/*\n> +\t * Collect the list of commits that we'll have to reapply now already.\n> +\t * This ensures that we'll abort early on in case the range of commits\n> +\t * contains merges, which we do not yet handle.\n> +\t */\n> +\tret = collect_commits(repo, parent, head, &commits);\n> +\tif (ret < 0)\n> +\t\tgoto out;\n> +\n\nThe user is currently at HEAD and wishes to reword a commit.\n\n  PN ... P2 P1 C0 C1 ... CN\n               └─(reword) └─(HEAD)\n\nSo we want to re-word C0, so we need to collect P1..CN.\n\n> +\t/* We retain authorship of the original commit. */\n> +\toriginal_message = repo_logmsg_reencode(repo, original_commit, 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_commit_tree_oid,\n> +\t\t\t\t  original_body, commit_message, \"reworded\", &final_message);\n> +\tif (ret < 0)\n> +\t\tgoto out;\n\nWe obtain the new message from the user.\n\n> +\n> +\tret = commit_tree(final_message.buf, final_message.len,\n> +\t\t\t  &repo_get_commit_tree(repo, original_commit)->object.oid,\n\nCan't we use original_commit_tree_oid here?\n\n> +\t\t\t  original_commit->parents, &rewritten_commit, original_author, NULL);\n> +\tif (ret < 0) {\n> +\t\tret = error(_(\"failed writing reworded commit\"));\n> +\t\tgoto out;\n> +\t}\n> +\n> +\treplace_commits(&commits, &original_commit->object.oid, &rewritten_commit, 1);\n> +\n\nOkay we replace the commits we obtained with the new rewritten commit.\n\n> +\tret = apply_commits(repo, &commits, parent, head, \"reword\");\n> +\tif (ret < 0)\n> +\t\tgoto out;\n> +\n> +\tret = 0;\n> +\n\nSo we're now asking to replay the commits onto parent and update HEAD.\nLooks good.\n\n> +out:\n> +\tstrbuf_release(&final_message);\n> +\tstrvec_clear(&commits);\n> +\tfree(original_author);\n> +\treturn ret;\n> +}\n>\n>  int cmd_history(int argc,\n>  \t\tconst char **argv,\n>  \t\tconst char *prefix,\n> -\t\tstruct repository *repo UNUSED)\n> +\t\tstruct repository *repo)\n>  {\n>  \tconst char * const usage[] = {\n>  \t\tN_(\"git history [<options>]\"),\n> +\t\tN_(\"git history reword [<options>] <commit>\"),\n\nThis string is used twice, perhaps we move it to a macro?\n\n[snip]\n\nThe tests looked good too!\n"},{"id":"528733","messageId":"CAOLa=ZRh8LDu=-PAxiAV9QxFtjuQtC8sOojZm-4=CgN6t4vJFg@mail.gmail.com","threadId":"63986","inReplyTo":"20251001-b4-pks-history-builtin-v4-8-8e61ddb86317@pks.im","subject":"Re: [PATCH v4 08/12] add-patch: split out `struct interactive_options`","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2025-10-14T12:35:39Z","receivedAt":"2025-10-14T12:35:41Z","isPatch":true,"sender":{"key":"karthik.188@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1786334?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> The `struct add_p_opt` is reused both by our the infra for \"git add -p\"\n> and \"git add -i\". Users of `run_add_i()` for example are expected to\n> pass `struct add_p_opt`. This is somewhat confusing and raises the\n> question which options apply to what part of the stack.\n>\n\nOkay. So seems like `struct add_p_opt` is defined in 'add-patch.h' and\n`struct add_i_state` in 'add-interactive.h'.\n\n> But things are even more confusing than that: while callers are expected\n> to pass in `struct add_p_opt`, these options ultimately get used to\n> initialize a `struct add_i_state` that is used by both subsystems. So we\n> are basically going full circle here.\n>\n> Refactor the code and split out a new `struct interactive_options` that\n> hosts common options used by both. These options are then applied to a\n> `struct interactive_config` that hosts common configuration.\n>\n> This refactoring doesn't yet fully detangle the two subsystems from one\n> another, 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>\n\n[snip]\n\n> diff --git a/add-patch.h b/add-patch.h\n> index 4394c74107..a4a05d9d14 100644\n> --- a/add-patch.h\n> +++ b/add-patch.h\n> @@ -1,15 +1,45 @@\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>  };\n>\n> -#define ADD_P_OPT_INIT { .context = -1, .interhunkcontext = -1 }\n> +#define INTERACTIVE_OPTIONS_INIT { \\\n> +\t.context = -1, \\\n> +\t.interhunkcontext = -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> +};\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\nIt feels a little odd that the `interactive_*` code lies in the\n'add-patch.h' and not in the 'add-interactive.h'.\n\nShould we also consider moving this or renaming the structs?\n\nNit: might be nice to make add the 'add_' prefix to them while we're\nhere.\n\n>  enum add_p_mode {\n>  \tADD_P_ADD,\n> @@ -20,7 +50,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\n\n[snip]\n"},{"id":"528734","messageId":"CAOLa=ZQvTxG2Phnitg_=KAs4RxkmKLBbJwKfw_qM8TV9Az3hjw@mail.gmail.com","threadId":"63986","inReplyTo":"20251001-b4-pks-history-builtin-v4-10-8e61ddb86317@pks.im","subject":"Re: [PATCH v4 10/12] add-patch: add support for in-memory index patching","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2025-10-14T13:08:18Z","receivedAt":"2025-10-14T13:08:21Z","isPatch":true,"sender":{"key":"karthik.188@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1786334?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n\n[snip]\n\n> @@ -1945,3 +1953,99 @@ int run_add_p(struct repository *r, enum add_p_mode mode,\n>  \tadd_p_state_clear(&s);\n>  \treturn 0;\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\nMissing trailing comma.\n\nRest of the patch looks good.\n\n[snip]\n"},{"id":"528737","messageId":"CAOLa=ZTnsHxK2+rRsAErY_zc1Rg5WNrmcDBiPgDk38zb9TNfkQ@mail.gmail.com","threadId":"63986","inReplyTo":"20251001-b4-pks-history-builtin-v4-12-8e61ddb86317@pks.im","subject":"Re: [PATCH v4 12/12] builtin/history: implement \"split\" subcommand","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2025-10-14T13:38:51Z","receivedAt":"2025-10-14T13:38:54Z","isPatch":true,"sender":{"key":"karthik.188@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1786334?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\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              | 225 +++++++++++++++++++++\n>  t/meson.build                  |   1 +\n>  t/t3452-history-split.sh       | 432 +++++++++++++++++++++++++++++++++++++++++\n>  4 files changed, 720 insertions(+)\n>\n> diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc\n> index b55babe206..83d675afea 100644\n> --- a/Documentation/git-history.adoc\n> +++ b/Documentation/git-history.adoc\n> @@ -10,6 +10,7 @@ SYNOPSIS\n>  [synopsis]\n>  git history [<options>]\n>  git history reword [<options>] <commit>\n> +git history split [<options>] <commit> [--] [<pathspec>...]\n>\n>  DESCRIPTION\n>  -----------\n> @@ -40,6 +41,26 @@ rewrite history in different ways:\n>  \tprovided, then this command will spawn an editor with the current\n>  \tmessage of that commit.\n>\n> +`split [--message=<message>] <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\nSo in essence we do this:\n\n  Before split:\n  P1 ── C0 ── C1 ── ... ── CN\n        └─(target)         └─(HEAD)\n\n  After split:\n  P1 ── S0 ── C0' ── C1 ── ...... ── CN\n        │     └─(modified original)  └─(HEAD)\n        └─(split-out hunks)\n\nI do wonder if S0 should contain the existing message and the new\nmessage should go to C0'. So perhaps more like\n\n  After split:\n  P1 ── C0' ── S0 ── C1 ── ..... ── CN\n        │      └─(split-out hunks)  └─(HEAD)\n        └─(modified original)\n\nMostly because when you say split, I would assume we keep the original\nas is and add on top of it. I don't really have a strong argument though\n:)\n\n[snip]\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 --message=\"split-out commit\"\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,e,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,e,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\nIt's really nice to have examples.\n\n[snip]\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\tN_(\"git history split [<options>] <commit>\"),\n> +\t\tNULL,\n> +\t};\n\nshould be '*const' here.\n\n> +\tconst char *commit_message = NULL;\n> +\tstruct option options[] = {\n> +\t\tOPT_STRING('m', \"message\", &commit_message, N_(\"message\"), N_(\"commit message\")),\n> +\t\tOPT_END(),\n> +\t};\n> +\tstruct oidmap rewritten_commits = OIDMAP_INIT;\n> +\tstruct commit *original_commit, *parent, *head;\n> +\tstruct strvec commits = STRVEC_INIT;\n> +\tstruct commit_list *list = NULL;\n> +\tstruct object_id split_commits[2];\n> +\tstruct pathspec pathspec = { 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 revision\"));\n> +\t\tgoto out;\n> +\t}\n> +\trepo_config(repo, git_default_config, NULL);\n> +\n> +\toriginal_commit = lookup_commit_reference_by_name(argv[0]);\n> +\tif (!original_commit) {\n> +\t\tret = error(_(\"commit to be split cannot be found: %s\"), argv[0]);\n> +\t\tgoto out;\n> +\t}\n> +\n> +\tif (original_commit->parents && original_commit->parents->next) {\n> +\t\tret = error(_(\"commit to be split must not be a merge commit\"));\n> +\t\tgoto out;\n> +\t}\n\nDo we need this? Since we also check for merges in `collect_commits()` below.\n\n> +\tparent = original_commit->parents ? original_commit->parents->item : NULL;\n> +\tif (parent && repo_parse_commit(repo, parent)) {\n> +\t\tret = error(_(\"unable to parse commit %s\"),\n> +\t\t\t    oid_to_hex(&parent->object.oid));\n> +\t\tgoto out;\n> +\t}\n> +\n> +\thead = lookup_commit_reference_by_name(\"HEAD\");\n> +\tif (!head) {\n> +\t\tret = error(_(\"could not resolve HEAD to a commit\"));\n> +\t\tgoto out;\n> +\t}\n> +\n> +\tcommit_list_append(original_commit, &list);\n> +\tif (!repo_is_descendant_of(repo, original_commit, list)) {\n> +\t\tret = error (_(\"split commit must be reachable from current HEAD commit\"));\n\ns/error /error/\n\n> +\t\tgoto out;\n> +\t}\n> +\n\nThis is also checked within collect_commits(), no?\n\n> +\tparse_pathspec(&pathspec, 0,\n> +\t\tPATHSPEC_PREFER_FULL | PATHSPEC_SYMLINK_LEADING_PATH | PATHSPEC_PREFIX_ORIGIN,\n> +\t\tprefix, argv + 1);\n> +\n> +\t/*\n> +\t* Collect the list of commits that we'll have to reapply now already.\n> +\t* This ensures that we'll abort early on in case the range of commits\n> +\t* contains merges, which we do not yet handle.\n> +\t*/\n\nComment spacing is off here.\n\n> +\tret = collect_commits(repo, parent, head, &commits);\n> +\tif (ret < 0)\n> +\t\tgoto out;\n> +\n> +\t/*\n> +\t * Then we split up the commit and replace the original commit with the\n> +\t * new new ones.\n> +\t */\n\ns/new/new\n\n> +\tret = split_commit(repo, original_commit, &pathspec,\n> +\t\t\t   commit_message, split_commits);\n> +\tif (ret < 0)\n> +\t\tgoto out;\n> +\n\nThis one was straight forward, it handles adding the hunks to the index\ncreating the two commits and linking them.\n\n> +\treplace_commits(&commits, &original_commit->object.oid,\n> +\t\t\tsplit_commits, ARRAY_SIZE(split_commits));\n> +\n\nNice. We use the function introduced earlier to replace the replace the\ncommits with the new two commits.\n\n> +\tret = apply_commits(repo, &commits, parent, head, \"split\");\n> +\tif (ret < 0)\n> +\t\tgoto out;\n> +\n> +\tret = 0;\n> +\n> +out:\n> +\toidmap_clear(&rewritten_commits, 0);\n> +\tclear_pathspec(&pathspec);\n> +\tstrvec_clear(&commits);\n> +\tfree_commit_list(list);\n> +\treturn ret;\n> +}\n\n[snip]\n\nThe tests look good. Nothing to add there.\n"},{"id":"528739","messageId":"CAOLa=ZRa6s1CMq19Z4GDz2HGsw6TCUQ4eu-nC26P2=qP2Jb6ug@mail.gmail.com","threadId":"63986","inReplyTo":"20251001-b4-pks-history-builtin-v4-0-8e61ddb86317@pks.im","subject":"Re: [PATCH v4 00/12] Introduce git-history(1) command for easy history editing","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2025-10-14T13:41:11Z","receivedAt":"2025-10-14T13:41:13Z","isPatch":true,"sender":{"key":"karthik.188@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1786334?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> Hi,\n>\n> over recent months I've been playing around with Jujutsu quite\n> frequently. While I still prefer using Git, there's been a couple\n> features in it that I really like and that I'd like to have in Git, as\n> well.\n>\n> A copule of these features relate to history editing. Most importantly,\n> I really dig the following commands:\n>\n>   - jj-abandon(1) to drop a specific commit from your history.\n>\n>   - jj-absorb(1) to take some changes and automatically apply them to\n>     commits in your history that last modified the respective hunks.\n>\n>   - jj-split(1) to split a commit into two.\n>\n>   - jj-new(1) to insert a new commit after or before a specific other\n>     commit.\n>\n> Not all of these commands can be ported directly into Git. jj-new(1) for\n> example doesn't really make a ton of sense for us, I'd claim. But some\n> of these commands _do_ make sense.\n>\n> This patch series is a starting point for such a command. I've\n> significantly slimmed it down from the first couple revisions now\n> following the discussions at the Contributor's Summit yesterday. This\n> was my intent anyway, as I already mentioned on the last iteration.\n>\n\nHello,\n\nI'm jumping in directly to review the fourth version of the patch\nseries. As such I might have missed discussions in the prev versions.\n\nApart from small comments and questions, it looks to be in a good shape.\nI think the addition of changes would perhaps warrant a re-roll but we\nshould be close :)\n\nThanks,\nKarthik\n\n[snip]\n"},{"id":"528745","messageId":"xmqq347lzaz9.fsf@gitster.g","threadId":"63986","inReplyTo":"20251001-b4-pks-history-builtin-v4-0-8e61ddb86317@pks.im","subject":"Re: [PATCH v4 00/12] Introduce git-history(1) command for easy history editing","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-10-14T16:47:38Z","receivedAt":"2025-10-14T16:47:41Z","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> over recent months I've been playing around with Jujutsu quite\n> frequently. While I still prefer using Git, there's been a couple\n> features in it that I really like and that I'd like to have in Git, as\n> well.\n> ...\n> Changes in v4:\n>   - I've rebuilt the patch series. It is now based on 821f583da6 (The\n>     thirteenth batcn, 2025-09-29) with sa/replay-atomic-ref-updates\n>     at 665c66a743 (replay: make atomic ref updates the default behavior,\n>     2025-09-27) merged into it. This should fix all conflicts with seen.\n>   - I've reworked this patch series to use the same infra as\n>     git-replay(1), as discussed during the Contributor's Summit.\n>   - I've slimmed down the patch series to only tackle those commands\n>     that cannot result in a conflict to keep it simple. I also learned\n>     that Elijah has been working on a \"git replay edit\" command, so I\n>     dropped that command so that we can instead use his version.\n>   - During the Contributor's Summit we have agreed that for now, we\n>     won't care about hook execution just yet. This may be backfilled at\n>     a later point in time.\n>   - I dropped \"commit.verbose\" handling for now, as my understanding of\n>     it was wrong at first. This is something we should backfill.\n>   - Link to v3: https://lore.kernel.org/r/20250904-b4-pks-history-builtin-v3-0-509053514755@pks.im\n\nWhat is queued near the tip of 'seen' is v4 but rebased on the\nupdated version of sa/replay-atomic-ref-updates that came from\n<20251013183311.33329-1-siddharthasthana31@gmail.com>.  The rebase\nonly had a slight conflict at [PATCH v4 02/12].\n\nThe result based on the same 821f583da6 (The thirteenth batcn,\n2025-09-29) with sa/replay-atomic-ref-updates at a07d37b3 (replay:\nadd replay.defaultAction config option, 2025-10-14).\n\n"},{"id":"529254","messageId":"aPdx5heE7blfnX3e@pks.im","threadId":"63986","inReplyTo":"CAOLa=ZTUDOYb1KVUEZY4KFyoaP-PdXjBvUto0vxOtYanEjoOFA@mail.gmail.com","subject":"Re: [PATCH v4 01/12] wt-status: provide function to expose status for trees","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-21T11:43:34Z","receivedAt":"2025-10-21T11:43:41Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Tue, Oct 14, 2025 at 04:49:14AM -0400, Karthik Nayak wrote:\n> Patrick Steinhardt <ps@pks.im> writes:\n> > diff --git a/wt-status.c b/wt-status.c\n> > index 8ffe6d3988..b66edbfca6 100644\n> > --- a/wt-status.c\n> > +++ b/wt-status.c\n> > @@ -612,6 +612,30 @@ static void wt_status_collect_updated_cb(struct diff_queue_struct *q,\n> >  \t}\n> >  }\n> >\n> > +void wt_status_collect_changes_trees(struct wt_status *s,\n> > +\t\t\t\t     const struct object_id *old_treeish,\n> > +\t\t\t\t     const struct object_id *new_treeish)\n> > +{\n> \n> So, my understanding here is that we want to diff two trees\n> `old_treeish` and `new_treeish` and then finally store the status change\n> in `wt_status`\n\nExactly.\n\n> > +\tstruct diff_options opts = { 0 };\n> > +\n> > +\trepo_diff_setup(s->repo, &opts);\n> > +\topts.output_format = DIFF_FORMAT_CALLBACK;\n> > +\topts.format_callback = wt_status_collect_updated_cb;\n> > +\topts.format_callback_data = s;\n> > +\topts.detect_rename = s->detect_rename >= 0 ? s->detect_rename : opts.detect_rename;\n> > +\topts.rename_limit = s->rename_limit >= 0 ? s->rename_limit : opts.rename_limit;\n> > +\topts.rename_score = s->rename_score >= 0 ? s->rename_score : opts.rename_score;\n> \n> Curious, why do we need a '>= 0' check here?\n\nI'm mostly just mirroring the same behaviour that we already have in\n`wt_status_collect_chanegs_worktree()`.\n\nPatrick\n"},{"id":"529255","messageId":"aPdx68-hxXBBKOc6@pks.im","threadId":"63986","inReplyTo":"CAOLa=ZQSc0jHv9gK5T06GA_mprknucwCj5H_M2djZ-UWfRMbJQ@mail.gmail.com","subject":"Re: [PATCH v4 05/12] builtin: add new \"history\" command","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-21T11:43:39Z","receivedAt":"2025-10-21T11:43:45Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Tue, Oct 14, 2025 at 05:07:03AM -0400, Karthik Nayak wrote:\n> Patrick Steinhardt <ps@pks.im> writes:\n> > new file mode 100644\n> > index 0000000000..1537960374\n> > --- /dev/null\n> > +++ b/Documentation/git-history.adoc\n> > @@ -0,0 +1,45 @@\n> > +git-history(1)\n> > +==============\n> > +\n> > +NAME\n> > +----\n> > +git-history - EXPERIMENTAL: Rewrite history of the current branch\n> > +\n> > +SYNOPSIS\n> > +--------\n> > +[synopsis]\n> > +git history [<options>]\n> > +\n> > +DESCRIPTION\n> > +-----------\n> > +\n> > +Rewrite history by rearranging or modifying specific commits in the\n> > +history.\n> > +\n> > +This command is similar to linkgit:git-rebase[1] and uses the same\n> > +underlying machinery. You should use rebases if you either want to\n> > +reapply a range of commits onto a different base, or interactive rebases\n> > +if you want to edit a range of commits.\n> > +\n> >\n> \n> The either..or in the last sentence is a bit confusing; as it is not an\n> either between 'want to reapply a range of commit onto a different base'\n> & 'interactive rebases'.\n> \n> Perhaps we can simply s/either//\n\nFair.\n\n> > diff --git a/builtin/history.c b/builtin/history.c\n> > new file mode 100644\n> > index 0000000000..f6fe32610b\n> > --- /dev/null\n> > +++ b/builtin/history.c\n> > @@ -0,0 +1,22 @@\n> > +#include \"builtin.h\"\n> > +#include \"gettext.h\"\n> > +#include \"parse-options.h\"\n> > +\n> > +int cmd_history(int argc,\n> > +\t\tconst char **argv,\n> > +\t\tconst char *prefix,\n> > +\t\tstruct repository *repo UNUSED)\n> > +{\n> > +\tconst char * const usage[] = {\n> > +\t\tN_(\"git history [<options>]\"),\n> > +\t\tNULL,\n> > +\t};\n> \n> Nit: We have pointer alignment set to 'Right' in our styling guide and\n> also mentioned in our 'Documentation/CodingGuidelines'\n> \n>    When declaring pointers, the star sides with the variable\n>    name, i.e. \"char *string\", not \"char* string\" or\n>    \"char * string\".  This makes it easier to understand code\n>    like \"char *string, c;\".\n> \n> The rest of the patch looks good!\n\nThis is one of the common exceptions though:\n\n    $ git grep 'const char \\* const' | wc -l\n    186\n    $ git grep 'const char \\*const' | wc -l\n    108\n\nSo when there is another keyword following the asterisk we tend to have\nan additional space inbetween. We tend to only drop the space when the\nnext token is the variable name.\n\nPatrick\n"},{"id":"529256","messageId":"aPdx8Pv-9UQDpe7I@pks.im","threadId":"63986","inReplyTo":"CAOLa=ZSU8yr9Gn0EZ7x705qPyVM-qiMjgMCNCb8p8SMGTToxqQ@mail.gmail.com","subject":"Re: [PATCH v4 06/12] builtin/history: implement \"reword\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-21T11:43:44Z","receivedAt":"2025-10-21T11:43:50Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Tue, Oct 14, 2025 at 07:04:06AM -0400, Karthik Nayak wrote:\n> Patrick Steinhardt <ps@pks.im> writes:\n> > diff --git a/builtin/history.c b/builtin/history.c\n> > index f6fe32610b..7b2a0023e8 100644\n> > --- a/builtin/history.c\n> > +++ b/builtin/history.c\n> > @@ -1,22 +1,389 @@\n> > +#define USE_THE_REPOSITORY_VARIABLE\n> > +\n> >  #include \"builtin.h\"\n> > +#include \"commit-reach.h\"\n> > +#include \"commit.h\"\n> > +#include \"config.h\"\n> > +#include \"editor.h\"\n> > +#include \"environment.h\"\n> >  #include \"gettext.h\"\n> > +#include \"hex.h\"\n> > +#include \"oidmap.h\"\n> \n> Nit: This can be dropped, perhaps needed in a future patch?\n\nYeah, it's indeed needed in a subsequent patch. Let me move the import\naround.\n\n> >  #include \"parse-options.h\"\n> > +#include \"refs.h\"\n> > +#include \"replay.h\"\n> > +#include \"reset.h\"\n> > +#include \"revision.h\"\n> > +#include \"sequencer.h\"\n> > +#include \"strvec.h\"\n> > +#include \"tree.h\"\n> > +#include \"wt-status.h\"\n> > +\n> > +static int collect_commits(struct repository *repo,\n> > +\t\t\t   struct commit *old_commit,\n> > +\t\t\t   struct commit *new_commit,\n> > +\t\t\t   struct strvec *out)\n> > +{\n> > +\tstruct setup_revision_opt revision_opts = {\n> > +\t\t.assume_dashdash = 1,\n> > +\t};\n> > +\tstruct strvec revisions = STRVEC_INIT;\n> > +\tstruct commit_list *from_list = NULL;\n> > +\tstruct commit *child;\n> > +\tstruct rev_info rev = { 0 };\n> > +\tint ret;\n> > +\n> > +\t/*\n> > +\t * Check that the old commit actually is an ancestor of HEAD. If not\n> > +\t * the whole request becomes nonsensical.\n> > +\t*/\n> \n> Missing space here\n\nGood eyes.\n\n> > +\tif (old_commit) {\n> > +\t\tcommit_list_insert(old_commit, &from_list);\n> > +\t\tif (!repo_is_descendant_of(repo, new_commit, from_list)) {\n> > +\t\t\tret = error(_(\"commit must be reachable from current HEAD commit\"));\n> > +\t\t\tgoto out;\n> > +\t\t}\n> > +\t}\n> \n> Makes sense. There is an inherent assumption using the 'git history'\n> command that you want to modify the history of the current reference.\n> \n> One question, wouldn't it make sense to parse and check that the commit\n> to be reworded should be checked to be a descendant of HEAD earlier on\n> in `cmd_history_reword()`?\n> \n> This would ensure this function `collect_commits()` doesn't worry about\n> how it is meant to be used, and simply worries about collecting commits.\n\nThe reason why I opted to move this into `collect_commits()` is so that\nwe don't have to reimplement that check for every single subcommand, as\nthey also have the same restriction.\n\n[snip]\n> > +static int fill_commit_message(struct repository *repo,\n> > +\t\t\t       const struct object_id *old_tree,\n> > +\t\t\t       const struct object_id *new_tree,\n> > +\t\t\t       const char *default_message,\n> > +\t\t\t       const char *provided_message,\n> > +\t\t\t       const char *action,\n> > +\t\t\t       struct strbuf *out)\n> > +{\n> > +\tif (!provided_message) {\n> > +\t\tconst char *path = git_path_commit_editmsg();\n> > +\t\tconst char *hint =\n> > +\t\t\t_(\"Please enter the commit message for the %s changes. Lines starting\\n\"\n> > +     \"with '%s' will be kept; you may remove them yourself if you want to.\\n\");\n> \n> Shouldn't this be s/kept/removed? Also this line needs to be aligned.\n\nHuh, yes, indeed.\n\n[snip]\n> > +\tif (repo_parse_commit(repo, original_commit)) {\n> > +\t\tret = error(_(\"unable to parse commit %s\"),\n> > +\t\t\t    oid_to_hex(&original_commit->object.oid));\n> > +\t\tgoto out;\n> > +\t}\n> \n> Isn't this already done as part of\n> `lookup_commit_reference_by_name_gently()` which is called by\n> `lookup_commit_reference_by_name()` ?\n\nYes, you're right.\n\n[snip]\n> > +\tret = commit_tree(final_message.buf, final_message.len,\n> > +\t\t\t  &repo_get_commit_tree(repo, original_commit)->object.oid,\n> \n> Can't we use original_commit_tree_oid here?\n\nYup, indeed.\n\n> >  \t\tconst char **argv,\n> >  \t\tconst char *prefix,\n> > -\t\tstruct repository *repo UNUSED)\n> > +\t\tstruct repository *repo)\n> >  {\n> >  \tconst char * const usage[] = {\n> >  \t\tN_(\"git history [<options>]\"),\n> > +\t\tN_(\"git history reword [<options>] <commit>\"),\n> \n> This string is used twice, perhaps we move it to a macro?\n\nYeah, why not.\n\nPatrick\n"},{"id":"529257","messageId":"aPdyAOhVjSMB9Csb@pks.im","threadId":"63986","inReplyTo":"CAOLa=ZRh8LDu=-PAxiAV9QxFtjuQtC8sOojZm-4=CgN6t4vJFg@mail.gmail.com","subject":"Re: [PATCH v4 08/12] add-patch: split out `struct interactive_options`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-21T11:44:00Z","receivedAt":"2025-10-21T11:44:06Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Tue, Oct 14, 2025 at 08:35:39AM -0400, Karthik Nayak wrote:\n> Patrick Steinhardt <ps@pks.im> writes:\n> > diff --git a/add-patch.h b/add-patch.h\n> > index 4394c74107..a4a05d9d14 100644\n> > --- a/add-patch.h\n> > +++ b/add-patch.h\n> > @@ -1,15 +1,45 @@\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> >  };\n> >\n> > -#define ADD_P_OPT_INIT { .context = -1, .interhunkcontext = -1 }\n> > +#define INTERACTIVE_OPTIONS_INIT { \\\n> > +\t.context = -1, \\\n> > +\t.interhunkcontext = -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> > +};\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> \n> It feels a little odd that the `interactive_*` code lies in the\n> 'add-patch.h' and not in the 'add-interactive.h'.\n> \n> Should we also consider moving this or renaming the structs?\n> \n> Nit: might be nice to make add the 'add_' prefix to them while we're\n> here.\n\nThe proper name for this struct would be `add_patch_interactive_config`,\nbut I decided against that name for now as it feels like a mouthful. I\nthink these two patches improve the status quo regardless of that, so\nI'd prefer to just keep those as-is if you don't mind?\n\nPatrick\n"},{"id":"529258","messageId":"aPdyBpuLfuYOLe7q@pks.im","threadId":"63986","inReplyTo":"CAOLa=ZTnsHxK2+rRsAErY_zc1Rg5WNrmcDBiPgDk38zb9TNfkQ@mail.gmail.com","subject":"Re: [PATCH v4 12/12] builtin/history: implement \"split\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-21T11:44:06Z","receivedAt":"2025-10-21T11:44:11Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Tue, Oct 14, 2025 at 09:38:51AM -0400, Karthik Nayak wrote:\n> Patrick Steinhardt <ps@pks.im> writes:\n> > diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc\n> > index b55babe206..83d675afea 100644\n> > --- a/Documentation/git-history.adoc\n> > +++ b/Documentation/git-history.adoc\n> > @@ -40,6 +41,26 @@ rewrite history in different ways:\n> >  \tprovided, then this command will spawn an editor with the current\n> >  \tmessage of that commit.\n> >\n> > +`split [--message=<message>] <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> \n> So in essence we do this:\n> \n>   Before split:\n>   P1 ── C0 ── C1 ── ... ── CN\n>         └─(target)         └─(HEAD)\n> \n>   After split:\n>   P1 ── S0 ── C0' ── C1 ── ...... ── CN\n>         │     └─(modified original)  └─(HEAD)\n>         └─(split-out hunks)\n> \n> I do wonder if S0 should contain the existing message and the new\n> message should go to C0'. So perhaps more like\n> \n>   After split:\n>   P1 ── C0' ── S0 ── C1 ── ..... ── CN\n>         │      └─(split-out hunks)  └─(HEAD)\n>         └─(modified original)\n> \n> Mostly because when you say split, I would assume we keep the original\n> as is and add on top of it. I don't really have a strong argument though\n> :)\n\nYeah, this has already caused some discussion beforehand. I guess you\ncan argue either way, and the suggestion from others was to simply allow\nthe user to edit both commit messages.\n\nI don't at all mind going into that direction, but I wonder how to call\nthe \"--message\" switch in that case. We could of course just call these\n\"--first-message\" and \"--second-message\", but that feels somewhat\nawkward.\n\nAlso, I already have it in my mind that it would be cool to extend this\ncommand so that you can split into arbitrary many commits. That is,\nafter you have split out the first commit we simply go back into\ninteractive mode to create a second commit tree. Rinse and repeat until\nwe have no chunks left anymore. But if we had such a mode though, then\nnumbered parameters don't make much sense anymore.\n\nAn alternative could be to just accept multiple \"-m\" arguments, and we\nthen apply the messages to the respective commits? Dunno.\n\n> > +\tconst char *commit_message = NULL;\n> > +\tstruct option options[] = {\n> > +\t\tOPT_STRING('m', \"message\", &commit_message, N_(\"message\"), N_(\"commit message\")),\n> > +\t\tOPT_END(),\n> > +\t};\n> > +\tstruct oidmap rewritten_commits = OIDMAP_INIT;\n> > +\tstruct commit *original_commit, *parent, *head;\n> > +\tstruct strvec commits = STRVEC_INIT;\n> > +\tstruct commit_list *list = NULL;\n> > +\tstruct object_id split_commits[2];\n> > +\tstruct pathspec pathspec = { 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 revision\"));\n> > +\t\tgoto out;\n> > +\t}\n> > +\trepo_config(repo, git_default_config, NULL);\n> > +\n> > +\toriginal_commit = lookup_commit_reference_by_name(argv[0]);\n> > +\tif (!original_commit) {\n> > +\t\tret = error(_(\"commit to be split cannot be found: %s\"), argv[0]);\n> > +\t\tgoto out;\n> > +\t}\n> > +\n> > +\tif (original_commit->parents && original_commit->parents->next) {\n> > +\t\tret = error(_(\"commit to be split must not be a merge commit\"));\n> > +\t\tgoto out;\n> > +\t}\n> \n> Do we need this? Since we also check for merges in `collect_commits()` below.\n\nYeah, we don't indeed.\n\n> > +\tparent = original_commit->parents ? original_commit->parents->item : NULL;\n> > +\tif (parent && repo_parse_commit(repo, parent)) {\n> > +\t\tret = error(_(\"unable to parse commit %s\"),\n> > +\t\t\t    oid_to_hex(&parent->object.oid));\n> > +\t\tgoto out;\n> > +\t}\n> > +\n> > +\thead = lookup_commit_reference_by_name(\"HEAD\");\n> > +\tif (!head) {\n> > +\t\tret = error(_(\"could not resolve HEAD to a commit\"));\n> > +\t\tgoto out;\n> > +\t}\n> > +\n> > +\tcommit_list_append(original_commit, &list);\n> > +\tif (!repo_is_descendant_of(repo, original_commit, list)) {\n> > +\t\tret = error (_(\"split commit must be reachable from current HEAD commit\"));\n> \n> s/error /error/\n> \n> > +\t\tgoto out;\n> > +\t}\n> > +\n> \n> This is also checked within collect_commits(), no?\n\nThe problem is rather that the check is wrong: we check whether the\noriginal commit is a descendent of itself, which is always true. What we\nactually want to check though is whether HEAD is a descendant of the\noriginal commit.\n\nThe check in `collect_commits()` is slightly different, as we verify\nthat the range provided to us is actually the same. But that check\nshouldn't ever hit if the above check actually triggers.\n\nI'll fix the check here and drop the one in `collect_commits()`.\n\nPatrick\n"},{"id":"529281","messageId":"20251021-b4-pks-history-builtin-v5-0-78d23f578fe6@pks.im","threadId":"63986","inReplyTo":"20250819-b4-pks-history-builtin-v1-0-9b77c32688fe@pks.im","subject":"[PATCH v5 00/12] Introduce git-history(1) command for easy history editing","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-21T14:15:48Z","receivedAt":"2025-10-21T14:15:59Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Hi,\n\nover recent months I've been playing around with Jujutsu quite\nfrequently. While I still prefer using Git, there's been a couple\nfeatures in it that I really like and that I'd like to have in Git, as\nwell.\n\nA copule of these features relate to history editing. Most importantly,\nI really dig the following commands:\n\n  - jj-abandon(1) to drop a specific commit from your history.\n\n  - jj-absorb(1) to take some changes and automatically apply them to\n    commits in your history that last modified the respective hunks.\n\n  - jj-split(1) to split a commit into two.\n\n  - jj-new(1) to insert a new commit after or before a specific other\n    commit.\n\nNot all of these commands can be ported directly into Git. jj-new(1) for\nexample doesn't really make a ton of sense for us, I'd claim. But some\nof these commands _do_ make sense.\n\nThis patch series is a starting point for such a command. I've\nsignificantly slimmed it down from the first couple revisions now\nfollowing the discussions at the Contributor's Summit yesterday. This\nwas my intent anyway, as I already mentioned on the last iteration.\n\nChanges in v5:\n  - I've changed the patch series to be based on top of 133d151831 (The\n    twenty-first batch, 2025-10-20) with sa/replay-atomic-ref-updates at\n    a1c22e627e (SQAUASH??? t0450 band-aid, 2025-10-14) merged into it.\n    This is one the one hand to fix a conflict, but also to get some of\n    the CI updates to make GitLab CI work again.\n  - Some slight commit message improvements.\n  - Deduplicate subcommand usage strings by using defines.\n  - Fix the desendancy checks to properly verify that HEAD is a\n    descendant of the commit to be rewritten. Also add some tests for\n    this.\n  - Fix the hint that mentions that lines starting with the comment\n    character will be tripped after having written the commit message.\n  - Move an include to the correct commit.\n  - Link to v4: https://lore.kernel.org/r/20251001-b4-pks-history-builtin-v4-0-8e61ddb86317@pks.im\n\nChanges in v4:\n  - I've rebuilt the patch series. It is now based on 821f583da6 (The\n    thirteenth batcn, 2025-09-29) with sa/replay-atomic-ref-updates\n    at 665c66a743 (replay: make atomic ref updates the default behavior,\n    2025-09-27) merged into it. This should fix all conflicts with seen.\n  - I've reworked this patch series to use the same infra as\n    git-replay(1), as discussed during the Contributor's Summit.\n  - I've slimmed down the patch series to only tackle those commands\n    that cannot result in a conflict to keep it simple. I also learned\n    that Elijah has been working on a \"git replay edit\" command, so I\n    dropped that command so that we can instead use his version.\n  - During the Contributor's Summit we have agreed that for now, we\n    won't care about hook execution just yet. This may be backfilled at\n    a later point in time.\n  - I dropped \"commit.verbose\" handling for now, as my understanding of\n    it was wrong at first. This is something we should backfill.\n  - Link to v3: https://lore.kernel.org/r/20250904-b4-pks-history-builtin-v3-0-509053514755@pks.im\n\nChanges in v3:\n  - Add logic to drive the \"post-rewrite\" hook and add tests to verify\n    that all hooks are executed as expected.\n  - Deduplicate logic to turn a replay action into a todo command.\n  - Move the addition of tests for the top-level git-history(1) command\n    to the correct commit.\n  - Some smaller commit message fixes.\n  - Honor \"commit.verbose\".\n  - Fix copy-paste error with an error message.\n  - Link to v2: https://lore.kernel.org/r/20250824-b4-pks-history-builtin-v2-0-964ac12f65bd@pks.im\n\nChanges in v2:\n  - Add a new \"reword\" subcommand.\n  - List git-history(1) in \"command-list.txt\".\n  - Add some missing error handling.\n  - Simplify calling convention of `apply_commits()` to handle root\n    commits internally instead of requiring every caller to do so.\n  - Add tests to verify that git-history(1) refuses to work with changes\n    in the worktree or index.\n  - Mark git-history(1) as experimental.\n  - Introduce commands to manage interrupted history edits.\n  - A bunch of improvements to the manpage.\n  - Link to v1: https://lore.kernel.org/r/20250819-b4-pks-history-builtin-v1-0-9b77c32688fe@pks.im\n\nThanks!\n\nPatrick\n\n---\nPatrick Steinhardt (12):\n      wt-status: provide function to expose status for trees\n      replay: extract logic to pick commits\n      replay: stop using `the_repository`\n      replay: parse commits before dereferencing them\n      builtin: add new \"history\" command\n      builtin/history: implement \"reword\" subcommand\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      cache-tree: allow writing in-memory index as tree\n      builtin/history: implement \"split\" subcommand\n\n .gitignore                     |   1 +\n Documentation/git-history.adoc | 114 ++++++++\n Documentation/meson.build      |   1 +\n Makefile                       |   2 +\n add-interactive.c              | 174 +++---------\n add-interactive.h              |  46 +---\n add-patch.c                    | 297 +++++++++++++++++---\n add-patch.h                    |  64 +++++\n builtin.h                      |   1 +\n builtin/add.c                  |  22 +-\n builtin/checkout.c             |   7 +-\n builtin/commit.c               |  16 +-\n builtin/history.c              | 600 +++++++++++++++++++++++++++++++++++++++++\n builtin/replay.c               | 110 +-------\n builtin/reset.c                |  16 +-\n builtin/stash.c                |  46 ++--\n cache-tree.c                   |   5 +-\n cache-tree.h                   |   3 +-\n command-list.txt               |   1 +\n commit.h                       |   2 +-\n git.c                          |   1 +\n meson.build                    |   2 +\n replay.c                       | 118 ++++++++\n replay.h                       |  18 ++\n t/meson.build                  |   3 +\n t/t3450-history.sh             |  17 ++\n t/t3451-history-reword.sh      | 217 +++++++++++++++\n t/t3452-history-split.sh       | 447 ++++++++++++++++++++++++++++++\n wt-status.c                    |  24 ++\n wt-status.h                    |   3 +\n 30 files changed, 2001 insertions(+), 377 deletions(-)\n\nRange-diff versus v4:\n\n 1:  5073f973555 !  1:  74adad5ad51 wt-status: provide function to expose status for trees\n    @@ Commit message\n         includes information around whether the working tree or the index have\n         any changes.\n     \n    -    We're about to introduce a new command though where the changes in\n    -    neither of them are actually relevant to us. Instead, what we want is to\n    -    format the changes between two different trees. While it is a little bit\n    -    of a stretch to add this as functionality to _working tree_ status, it\n    +    We're about to introduce a new command where the changes in neither of\n    +    them are actually relevant to us. Instead, what we want is to format the\n    +    changes between two different trees. While it is a little bit of a\n    +    stretch to add this as functionality to _working tree_ status, it\n         doesn't make any sense to open-code this functionality, either.\n     \n         Implement a new function `wt_status_collect_changes_trees()` that diffs\n 2:  b21049120fa !  2:  e92d28e9033 replay: extract logic to pick commits\n    @@ Commit message\n         Signed-off-by: Patrick Steinhardt <ps@pks.im>\n     \n      ## Makefile ##\n    -@@ Makefile: LIB_OBJS += refs/ref-cache.o\n    - LIB_OBJS += refspec.o\n    +@@ Makefile: LIB_OBJS += reftable/tree.o\n    + LIB_OBJS += reftable/writer.o\n      LIB_OBJS += remote.o\n      LIB_OBJS += replace-object.o\n     +LIB_OBJS += replay.o\n    @@ builtin/replay.c: static void determine_replay_mode(struct repository *repo,\n     -\treturn create_commit(repo, result->tree, pickme, replayed_base);\n     -}\n     -\n    - static int add_ref_to_transaction(struct ref_transaction *transaction,\n    - \t\t\t\t  const char *refname,\n    - \t\t\t\t  const struct object_id *new_oid,\n    + static int handle_ref_update(const char *mode,\n    + \t\t\t     struct ref_transaction *transaction,\n    + \t\t\t     const char *refname,\n     @@ builtin/replay.c: int cmd_replay(int argc,\n      \t\tif (commit->parents->next)\n      \t\t\tdie(_(\"replaying merge commits is not supported yet!\"));\n 3:  be13cd322df =  3:  652fbddc10e replay: stop using `the_repository`\n 4:  7af626c0fbc =  4:  0c9a409eea7 replay: parse commits before dereferencing them\n 5:  ac152bca162 !  5:  d568ffc1f65 builtin: add new \"history\" command\n    @@ Commit message\n     \n           - Multiple commits should be squashed into one.\n     \n    -    While these operations are all doable, it often feels needlessly cludgy\n    +    While these operations are all doable, it often feels needlessly kludgey\n         to do so by doing an interactive rebase, using the editor to say what\n         one wants, and then perform the actions. Furthermore, some operations\n         like splitting up a commit into two are way more involved than that and\n    @@ Commit message\n     \n         Add a new \"history\" command to plug this gap. This command will have\n         several different subcommands to imperatively rewrite history for common\n    -    use cases like the above. These commands will be implemented in\n    +    use cases like the above. These subcommands will be implemented in\n         subsequent commits.\n     \n         Signed-off-by: Patrick Steinhardt <ps@pks.im>\n    @@ Documentation/git-history.adoc (new)\n     +history.\n     +\n     +This command is similar to linkgit:git-rebase[1] and uses the same\n    -+underlying machinery. You should use rebases if you either want to\n    -+reapply a range of commits onto a different base, or interactive rebases\n    -+if you want to edit a range of commits.\n    ++underlying machinery. You should use rebases if you want to reapply a range of\n    ++commits onto a different base, or interactive rebases if you want to edit a\n    ++range of commits.\n     +\n     +Note that this command does not (yet) work with histories that contain\n     +merges. You should use linkgit:git-rebase[1] with the `--rebase-merges`\n 6:  8519861bfb0 !  6:  39acd4734c3 builtin/history: implement \"reword\" subcommand\n    @@ builtin/history.c\n     +#include \"environment.h\"\n      #include \"gettext.h\"\n     +#include \"hex.h\"\n    -+#include \"oidmap.h\"\n      #include \"parse-options.h\"\n     +#include \"refs.h\"\n     +#include \"replay.h\"\n    @@ builtin/history.c\n     +#include \"tree.h\"\n     +#include \"wt-status.h\"\n     +\n    ++#define GIT_HISTORY_REWORD_USAGE N_(\"git history reword [<options>] <commit>\")\n    ++\n     +static int collect_commits(struct repository *repo,\n     +\t\t\t   struct commit *old_commit,\n     +\t\t\t   struct commit *new_commit,\n    @@ builtin/history.c\n     +\t\t.assume_dashdash = 1,\n     +\t};\n     +\tstruct strvec revisions = STRVEC_INIT;\n    -+\tstruct commit_list *from_list = NULL;\n     +\tstruct commit *child;\n     +\tstruct rev_info rev = { 0 };\n     +\tint ret;\n     +\n    -+\t/*\n    -+\t * Check that the old commit actually is an ancestor of HEAD. If not\n    -+\t * the whole request becomes nonsensical.\n    -+\t*/\n    -+\tif (old_commit) {\n    -+\t\tcommit_list_insert(old_commit, &from_list);\n    -+\t\tif (!repo_is_descendant_of(repo, new_commit, from_list)) {\n    -+\t\t\tret = error(_(\"commit must be reachable from current HEAD commit\"));\n    -+\t\t\tgoto out;\n    -+\t\t}\n    -+\t}\n    -+\n     +\trepo_init_revisions(repo, &rev, NULL);\n     +\tstrvec_push(&revisions, \"\");\n     +\tstrvec_push(&revisions, oid_to_hex(&new_commit->object.oid));\n    @@ builtin/history.c\n     +\tret = 0;\n     +\n     +out:\n    -+\tfree_commit_list(from_list);\n     +\tstrvec_clear(&revisions);\n     +\trelease_revisions(&rev);\n     +\treset_revision_walk();\n    @@ builtin/history.c\n     +\tif (!provided_message) {\n     +\t\tconst char *path = git_path_commit_editmsg();\n     +\t\tconst char *hint =\n    -+\t\t\t_(\"Please enter the commit message for the %s changes. Lines starting\\n\"\n    -+     \"with '%s' will be kept; you may remove them yourself if you want to.\\n\");\n    ++\t\t\t_(\"Please enter the commit message for the %s changes.\"\n    ++\t\t\t  \" Lines starting\\nwith '%s' will be ignored.\\n\");\n     +\t\tstruct wt_status s;\n     +\n     +\t\tstrbuf_addstr(out, default_message);\n    @@ builtin/history.c\n     +\t\t\t      struct repository *repo)\n     +{\n     +\tconst char * const usage[] = {\n    -+\t\tN_(\"git history reword [<options>] <commit>\"),\n    ++\t\tGIT_HISTORY_REWORD_USAGE,\n     +\t\tNULL,\n     +\t};\n     +\tconst char *commit_message = NULL;\n    @@ builtin/history.c\n     +\tstruct strvec commits = STRVEC_INIT;\n     +\tstruct object_id parent_tree_oid, original_commit_tree_oid;\n     +\tstruct object_id rewritten_commit;\n    ++\tstruct commit_list *from_list = NULL;\n     +\tconst char *original_message, *original_body, *ptr;\n     +\tchar *original_author = NULL;\n     +\tsize_t len;\n    @@ builtin/history.c\n     +\t\tret = error(_(\"commit to be reworded cannot be found: %s\"), argv[0]);\n     +\t\tgoto out;\n     +\t}\n    -+\tif (repo_parse_commit(repo, original_commit)) {\n    -+\t\tret = error(_(\"unable to parse commit %s\"),\n    -+\t\t\t    oid_to_hex(&original_commit->object.oid));\n    -+\t\tgoto out;\n    -+\t}\n     +\toriginal_commit_tree_oid = repo_get_commit_tree(repo, original_commit)->object.oid;\n     +\n     +\tparent = original_commit->parents ? original_commit->parents->item : NULL;\n    @@ builtin/history.c\n     +\t\tgoto out;\n     +\t}\n     +\n    ++\tcommit_list_append(original_commit, &from_list);\n    ++\tif (!repo_is_descendant_of(repo, head, from_list)) {\n    ++\t\tret = error (_(\"split commit must be reachable from current HEAD commit\"));\n    ++\t\tgoto out;\n    ++\t}\n    ++\n     +\t/*\n     +\t * Collect the list of commits that we'll have to reapply now already.\n     +\t * This ensures that we'll abort early on in case the range of commits\n    @@ builtin/history.c\n     +\tif (ret < 0)\n     +\t\tgoto out;\n     +\n    -+\tret = commit_tree(final_message.buf, final_message.len,\n    -+\t\t\t  &repo_get_commit_tree(repo, original_commit)->object.oid,\n    ++\tret = commit_tree(final_message.buf, final_message.len, &original_commit_tree_oid,\n     +\t\t\t  original_commit->parents, &rewritten_commit, original_author, NULL);\n     +\tif (ret < 0) {\n     +\t\tret = error(_(\"failed writing reworded commit\"));\n    @@ builtin/history.c\n     +\n     +out:\n     +\tstrbuf_release(&final_message);\n    ++\tfree_commit_list(from_list);\n     +\tstrvec_clear(&commits);\n     +\tfree(original_author);\n     +\treturn ret;\n    @@ builtin/history.c\n      {\n      \tconst char * const usage[] = {\n      \t\tN_(\"git history [<options>]\"),\n    -+\t\tN_(\"git history reword [<options>] <commit>\"),\n    ++\t\tGIT_HISTORY_REWORD_USAGE,\n      \t\tNULL,\n      \t};\n     +\tparse_opt_subcommand_fn *fn = NULL;\n    @@ t/t3451-history-reword.sh (new)\n     +\t)\n     +'\n     +\n    ++test_expect_success 'refuses to work with unrelated 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\ttest_must_fail git history reword ours 2>err &&\n    ++\t\ttest_grep \"split commit must be reachable from current HEAD commit\" err\n    ++\t)\n    ++'\n    ++\n     +test_expect_success 'can reword tip of a branch' '\n     +\ttest_when_finished \"rm -rf repo\" &&\n     +\tgit init repo &&\n    @@ t/t3451-history-reword.sh (new)\n     +\t\tfirst\n     +\n     +\t\t# Please enter the commit message for the reworded changes. Lines starting\n    -+\t\t# with ${SQ}#${SQ} will be kept; you may remove them yourself if you want to.\n    ++\t\t# with ${SQ}#${SQ} will be ignored.\n     +\t\t# Changes to be committed:\n     +\t\t#\tnew file:   first.t\n     +\t\t#\n 7:  89883c85fa3 =  7:  2e4291a3b87 add-patch: split out header from \"add-interactive.h\"\n 8:  f1602b8513c !  8:  715f3a32b82 add-patch: split out `struct interactive_options`\n    @@ Metadata\n      ## Commit message ##\n         add-patch: split out `struct interactive_options`\n     \n    -    The `struct add_p_opt` is reused both by our the infra for \"git add -p\"\n    -    and \"git add -i\". Users of `run_add_i()` for example are expected to\n    -    pass `struct add_p_opt`. This is somewhat confusing and raises the\n    -    question which options apply to what part of the stack.\n    +    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\n    +    of which options apply to what part of the stack.\n     \n         But things are even more confusing than that: while callers are expected\n         to pass in `struct add_p_opt`, these options ultimately get used to\n 9:  8ef83293ae5 =  9:  94ea67db00c add-patch: remove dependency on \"add-interactive\" subsystem\n10:  385c39356dc ! 10:  19c02eb16de add-patch: add support for in-memory index patching\n    @@ Commit message\n         With `run_add_p()` callers have the ability to apply changes from a\n         specific revision to a repository's index. This infra supports several\n         different modes, like for example applying changes to the index,\n    -    worktree or both.\n    +    working tree or both.\n     \n         One feature that is missing though is the ability to apply changes to an\n         in-memory index different from the repository's index. Add a new\n    @@ add-patch.c\n      #include \"object-name.h\"\n      #include \"pager.h\"\n      #include \"read-cache-ll.h\"\n    +@@ add-patch.c: static struct patch_mode patch_mode_add = {\n    + \t\tN_(\"Stage mode change [y,n,q,a,d%s,?]? \"),\n    + \t\tN_(\"Stage deletion [y,n,q,a,d%s,?]? \"),\n    + \t\tN_(\"Stage addition [y,n,q,a,d%s,?]? \"),\n    +-\t\tN_(\"Stage this hunk [y,n,q,a,d%s,?]? \")\n    ++\t\tN_(\"Stage this hunk [y,n,q,a,d%s,?]? \"),\n    + \t},\n    + \t.edit_hunk_hint = N_(\"If the patch applies cleanly, the edited hunk \"\n    + \t\t\t     \"will immediately be marked for staging.\"),\n     @@ add-patch.c: struct hunk {\n      \n      struct add_p_state {\n11:  d26e225c854 = 11:  3161d4bad3f cache-tree: allow writing in-memory index as tree\n12:  fa878dc362c ! 12:  e7a6537ecfe builtin/history: implement \"split\" subcommand\n    @@ builtin/history.c\n      #include \"commit.h\"\n      #include \"config.h\"\n     @@\n    + #include \"environment.h\"\n    + #include \"gettext.h\"\n      #include \"hex.h\"\n    - #include \"oidmap.h\"\n    ++#include \"oidmap.h\"\n      #include \"parse-options.h\"\n     +#include \"path.h\"\n     +#include \"read-cache.h\"\n    @@ builtin/history.c\n      #include \"sequencer.h\"\n      #include \"strvec.h\"\n      #include \"tree.h\"\n    + #include \"wt-status.h\"\n    + \n    + #define GIT_HISTORY_REWORD_USAGE N_(\"git history reword [<options>] <commit>\")\n    ++#define GIT_HISTORY_SPLIT_USAGE  N_(\"git history split [<options>] <commit> [--] [<pathspec>...]\")\n    + \n    + static int collect_commits(struct repository *repo,\n    + \t\t\t   struct commit *old_commit,\n     @@ builtin/history.c: static int cmd_history_reword(int argc,\n      \treturn ret;\n      }\n    @@ builtin/history.c: static int cmd_history_reword(int argc,\n     +\t\t\t     struct repository *repo)\n     +{\n     +\tconst char * const usage[] = {\n    -+\t\tN_(\"git history split [<options>] <commit>\"),\n    ++\t\tGIT_HISTORY_SPLIT_USAGE,\n     +\t\tNULL,\n     +\t};\n     +\tconst char *commit_message = NULL;\n    @@ builtin/history.c: static int cmd_history_reword(int argc,\n     +\tstruct oidmap rewritten_commits = OIDMAP_INIT;\n     +\tstruct commit *original_commit, *parent, *head;\n     +\tstruct strvec commits = STRVEC_INIT;\n    -+\tstruct commit_list *list = NULL;\n    ++\tstruct commit_list *from_list = NULL;\n     +\tstruct object_id split_commits[2];\n     +\tstruct pathspec pathspec = { 0 };\n     +\tint ret;\n    @@ builtin/history.c: static int cmd_history_reword(int argc,\n     +\t\tgoto out;\n     +\t}\n     +\n    -+\tif (original_commit->parents && original_commit->parents->next) {\n    -+\t\tret = error(_(\"commit to be split must not be a merge commit\"));\n    -+\t\tgoto out;\n    -+\t}\n    -+\n     +\tparent = original_commit->parents ? original_commit->parents->item : NULL;\n     +\tif (parent && repo_parse_commit(repo, parent)) {\n     +\t\tret = error(_(\"unable to parse commit %s\"),\n    @@ builtin/history.c: static int cmd_history_reword(int argc,\n     +\t\tgoto out;\n     +\t}\n     +\n    -+\tcommit_list_append(original_commit, &list);\n    -+\tif (!repo_is_descendant_of(repo, original_commit, list)) {\n    -+\t\tret = error (_(\"split commit must be reachable from current HEAD commit\"));\n    ++\tcommit_list_append(original_commit, &from_list);\n    ++\tif (!repo_is_descendant_of(repo, head, from_list)) {\n    ++\t\tret = error(_(\"split commit must be reachable from current HEAD commit\"));\n     +\t\tgoto out;\n     +\t}\n     +\n    @@ builtin/history.c: static int cmd_history_reword(int argc,\n     +\t\tprefix, argv + 1);\n     +\n     +\t/*\n    -+\t* Collect the list of commits that we'll have to reapply now already.\n    -+\t* This ensures that we'll abort early on in case the range of commits\n    -+\t* contains merges, which we do not yet handle.\n    -+\t*/\n    ++\t * Collect the list of commits that we'll have to reapply now already.\n    ++\t * This ensures that we'll abort early on in case the range of commits\n    ++\t * contains merges, which we do not yet handle.\n    ++\t */\n     +\tret = collect_commits(repo, parent, head, &commits);\n     +\tif (ret < 0)\n     +\t\tgoto out;\n     +\n     +\t/*\n     +\t * Then we split up the commit and replace the original commit with the\n    -+\t * new new ones.\n    ++\t * new ones.\n     +\t */\n     +\tret = split_commit(repo, original_commit, &pathspec,\n     +\t\t\t   commit_message, split_commits);\n    @@ builtin/history.c: static int cmd_history_reword(int argc,\n     +\n     +out:\n     +\toidmap_clear(&rewritten_commits, 0);\n    ++\tfree_commit_list(from_list);\n     +\tclear_pathspec(&pathspec);\n     +\tstrvec_clear(&commits);\n    -+\tfree_commit_list(list);\n     +\treturn ret;\n     +}\n     +\n    @@ builtin/history.c: static int cmd_history_reword(int argc,\n     @@ builtin/history.c: int cmd_history(int argc,\n      \tconst char * const usage[] = {\n      \t\tN_(\"git history [<options>]\"),\n    - \t\tN_(\"git history reword [<options>] <commit>\"),\n    -+\t\tN_(\"git history split [<options>] <commit> [--] [<pathspec>...]\"),\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    @@ t/t3452-history-split.sh (new)\n     +\t\tgit switch - &&\n     +\t\tgit merge theirs &&\n     +\t\ttest_must_fail git history split HEAD 2>err &&\n    -+\t\ttest_grep \"commit to be split must not be a merge commit\" err &&\n    ++\t\ttest_grep \"cannot rearrange commit history with merges\" err &&\n     +\t\ttest_must_fail git history split HEAD~ 2>err &&\n     +\t\ttest_grep \"cannot rearrange commit history with merges\" err\n     +\t)\n     +'\n     +\n    ++test_expect_success 'refuses to work with unrelated 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\ttest_must_fail git history split ours 2>err &&\n    ++\t\ttest_grep \"split commit must be reachable from current HEAD commit\" err\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/t3452-history-split.sh (new)\n     +\t\tcat >expect <<-EOF &&\n     +\n     +\t\t# Please enter the commit message for the split-out changes. Lines starting\n    -+\t\t# with ${SQ}#${SQ} will be kept; you may remove them yourself if you want to.\n    ++\t\t# with ${SQ}#${SQ} will be ignored.\n     +\t\t# Changes to be committed:\n     +\t\t#\tnew file:   bar\n     +\t\t#\n\n---\nbase-commit: eef20e55db9c6590670e245245e207271daea61f\nchange-id: 20250819-b4-pks-history-builtin-83398f9a05f0\n\n"},{"id":"529282","messageId":"20251021-b4-pks-history-builtin-v5-1-78d23f578fe6@pks.im","threadId":"63986","inReplyTo":"20251021-b4-pks-history-builtin-v5-0-78d23f578fe6@pks.im","subject":"[PATCH v5 01/12] wt-status: provide function to expose status for trees","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-21T14:15:49Z","receivedAt":"2025-10-21T14:16:01Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The \"wt-status\" subsystem is responsible for printing status information\naround the current state of the working tree. This most importantly\nincludes information around whether the working tree or the index have\nany changes.\n\nWe're about to introduce a new command where the changes in neither of\nthem are actually relevant to us. Instead, what we want is to format the\nchanges between two different trees. While it is a little bit of a\nstretch to add this as functionality to _working tree_ status, it\ndoesn't make any sense to open-code this functionality, either.\n\nImplement a new function `wt_status_collect_changes_trees()` that diffs\ntwo trees and formats the status accordingly. This function is not yet\nused, but will be in a subsequent commit.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n wt-status.c | 24 ++++++++++++++++++++++++\n wt-status.h |  3 +++\n 2 files changed, 27 insertions(+)\n\ndiff --git a/wt-status.c b/wt-status.c\nindex 8ffe6d3988f..b66edbfca6c 100644\n--- a/wt-status.c\n+++ b/wt-status.c\n@@ -612,6 +612,30 @@ static void wt_status_collect_updated_cb(struct diff_queue_struct *q,\n \t}\n }\n \n+void wt_status_collect_changes_trees(struct wt_status *s,\n+\t\t\t\t     const struct object_id *old_treeish,\n+\t\t\t\t     const struct object_id *new_treeish)\n+{\n+\tstruct diff_options opts = { 0 };\n+\n+\trepo_diff_setup(s->repo, &opts);\n+\topts.output_format = DIFF_FORMAT_CALLBACK;\n+\topts.format_callback = wt_status_collect_updated_cb;\n+\topts.format_callback_data = s;\n+\topts.detect_rename = s->detect_rename >= 0 ? s->detect_rename : opts.detect_rename;\n+\topts.rename_limit = s->rename_limit >= 0 ? s->rename_limit : opts.rename_limit;\n+\topts.rename_score = s->rename_score >= 0 ? s->rename_score : opts.rename_score;\n+\topts.flags.recursive = 1;\n+\tdiff_setup_done(&opts);\n+\n+\tdiff_tree_oid(old_treeish, new_treeish, \"\", &opts);\n+\tdiffcore_std(&opts);\n+\tdiff_flush(&opts);\n+\twt_status_get_state(s->repo, &s->state, 0);\n+\n+\tdiff_free(&opts);\n+}\n+\n static void wt_status_collect_changes_worktree(struct wt_status *s)\n {\n \tstruct rev_info rev;\ndiff --git a/wt-status.h b/wt-status.h\nindex e40a27214a7..924d7a5fa99 100644\n--- a/wt-status.h\n+++ b/wt-status.h\n@@ -153,6 +153,9 @@ void wt_status_add_cut_line(struct wt_status *s);\n void wt_status_prepare(struct repository *r, struct wt_status *s);\n void wt_status_print(struct wt_status *s);\n void wt_status_collect(struct wt_status *s);\n+void wt_status_collect_changes_trees(struct wt_status *s,\n+\t\t\t\t     const struct object_id *old_treeish,\n+\t\t\t\t     const struct object_id *new_treeish);\n /*\n  * Frees the buffers allocated by wt_status_collect.\n  */\n\n-- \n2.51.1.851.g4ebd6896fd.dirty\n\n"},{"id":"529283","messageId":"20251021-b4-pks-history-builtin-v5-2-78d23f578fe6@pks.im","threadId":"63986","inReplyTo":"20251021-b4-pks-history-builtin-v5-0-78d23f578fe6@pks.im","subject":"[PATCH v5 02/12] replay: extract logic to pick commits","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-21T14:15:50Z","receivedAt":"2025-10-21T14:16:03Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"We're about to add a new git-history(1) command that will reuse some of\nthe same infrastructure as git-replay(1). To prepare for this, extract\nthe logic to pick a commit into a new \"replay.c\" file so that it can be\nshared between both commands.\n\nRename the function to have a \"replay_\" prefix to clearly indicate its\nsubsystem.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Makefile         |   1 +\n builtin/replay.c | 110 ++--------------------------------------------------\n meson.build      |   1 +\n replay.c         | 115 +++++++++++++++++++++++++++++++++++++++++++++++++++++++\n replay.h         |  18 +++++++++\n 5 files changed, 138 insertions(+), 107 deletions(-)\n\ndiff --git a/Makefile b/Makefile\nindex 1919d35bf3f..01c171b4f03 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -1261,6 +1261,7 @@ LIB_OBJS += reftable/tree.o\n LIB_OBJS += reftable/writer.o\n LIB_OBJS += remote.o\n LIB_OBJS += replace-object.o\n+LIB_OBJS += replay.o\n LIB_OBJS += repo-settings.o\n LIB_OBJS += repository.o\n LIB_OBJS += rerere.o\ndiff --git a/builtin/replay.c b/builtin/replay.c\nindex d0f04927908..aeb7cc88fe5 100644\n--- a/builtin/replay.c\n+++ b/builtin/replay.c\n@@ -2,7 +2,6 @@\n  * \"git replay\" builtin command\n  */\n \n-#define USE_THE_REPOSITORY_VARIABLE\n #define DISABLE_SIGN_COMPARE_WARNINGS\n \n #include \"git-compat-util.h\"\n@@ -16,18 +15,12 @@\n #include \"object-name.h\"\n #include \"parse-options.h\"\n #include \"refs.h\"\n+#include \"replay.h\"\n #include \"revision.h\"\n #include \"strmap.h\"\n #include <oidset.h>\n #include <tree.h>\n \n-static const char *short_commit_name(struct repository *repo,\n-\t\t\t\t     struct commit *commit)\n-{\n-\treturn repo_find_unique_abbrev(repo, &commit->object.oid,\n-\t\t\t\t       DEFAULT_ABBREV);\n-}\n-\n static struct commit *peel_committish(struct repository *repo, const char *name)\n {\n \tstruct object *obj;\n@@ -40,59 +33,6 @@ static struct commit *peel_committish(struct repository *repo, const char *name)\n \t\t\t\t\t\t  OBJ_COMMIT);\n }\n \n-static char *get_author(const char *message)\n-{\n-\tsize_t len;\n-\tconst char *a;\n-\n-\ta = find_commit_header(message, \"author\", &len);\n-\tif (a)\n-\t\treturn xmemdupz(a, len);\n-\n-\treturn NULL;\n-}\n-\n-static struct commit *create_commit(struct repository *repo,\n-\t\t\t\t    struct tree *tree,\n-\t\t\t\t    struct commit *based_on,\n-\t\t\t\t    struct commit *parent)\n-{\n-\tstruct object_id ret;\n-\tstruct object *obj = NULL;\n-\tstruct commit_list *parents = NULL;\n-\tchar *author;\n-\tchar *sign_commit = NULL; /* FIXME: cli users might want to sign again */\n-\tstruct commit_extra_header *extra = NULL;\n-\tstruct strbuf msg = STRBUF_INIT;\n-\tconst char *out_enc = get_commit_output_encoding();\n-\tconst char *message = repo_logmsg_reencode(repo, based_on,\n-\t\t\t\t\t\t   NULL, out_enc);\n-\tconst char *orig_message = NULL;\n-\tconst char *exclude_gpgsig[] = { \"gpgsig\", NULL };\n-\n-\tcommit_list_insert(parent, &parents);\n-\textra = read_commit_extra_headers(based_on, exclude_gpgsig);\n-\tfind_commit_subject(message, &orig_message);\n-\tstrbuf_addstr(&msg, orig_message);\n-\tauthor = get_author(message);\n-\treset_ident_date();\n-\tif (commit_tree_extended(msg.buf, msg.len, &tree->object.oid, parents,\n-\t\t\t\t &ret, author, NULL, sign_commit, extra)) {\n-\t\terror(_(\"failed to write commit object\"));\n-\t\tgoto out;\n-\t}\n-\n-\tobj = parse_object(repo, &ret);\n-\n-out:\n-\trepo_unuse_commit_buffer(the_repository, based_on, message);\n-\tfree_commit_extra_headers(extra);\n-\tfree_commit_list(parents);\n-\tstrbuf_release(&msg);\n-\tfree(author);\n-\treturn (struct commit *)obj;\n-}\n-\n struct ref_info {\n \tstruct commit *onto;\n \tstruct strset positive_refs;\n@@ -241,50 +181,6 @@ static void determine_replay_mode(struct repository *repo,\n \tstrset_clear(&rinfo.positive_refs);\n }\n \n-static struct commit *mapped_commit(kh_oid_map_t *replayed_commits,\n-\t\t\t\t    struct commit *commit,\n-\t\t\t\t    struct commit *fallback)\n-{\n-\tkhint_t pos = kh_get_oid_map(replayed_commits, commit->object.oid);\n-\tif (pos == kh_end(replayed_commits))\n-\t\treturn fallback;\n-\treturn kh_value(replayed_commits, pos);\n-}\n-\n-static struct commit *pick_regular_commit(struct repository *repo,\n-\t\t\t\t\t  struct commit *pickme,\n-\t\t\t\t\t  kh_oid_map_t *replayed_commits,\n-\t\t\t\t\t  struct commit *onto,\n-\t\t\t\t\t  struct merge_options *merge_opt,\n-\t\t\t\t\t  struct merge_result *result)\n-{\n-\tstruct commit *base, *replayed_base;\n-\tstruct tree *pickme_tree, *base_tree;\n-\n-\tbase = pickme->parents->item;\n-\treplayed_base = mapped_commit(replayed_commits, base, onto);\n-\n-\tresult->tree = repo_get_commit_tree(repo, replayed_base);\n-\tpickme_tree = repo_get_commit_tree(repo, pickme);\n-\tbase_tree = repo_get_commit_tree(repo, base);\n-\n-\tmerge_opt->branch1 = short_commit_name(repo, replayed_base);\n-\tmerge_opt->branch2 = short_commit_name(repo, pickme);\n-\tmerge_opt->ancestor = xstrfmt(\"parent of %s\", merge_opt->branch2);\n-\n-\tmerge_incore_nonrecursive(merge_opt,\n-\t\t\t\t  base_tree,\n-\t\t\t\t  result->tree,\n-\t\t\t\t  pickme_tree,\n-\t\t\t\t  result);\n-\n-\tfree((char*)merge_opt->ancestor);\n-\tmerge_opt->ancestor = NULL;\n-\tif (!result->clean)\n-\t\treturn NULL;\n-\treturn create_commit(repo, result->tree, pickme, replayed_base);\n-}\n-\n static int handle_ref_update(const char *mode,\n \t\t\t     struct ref_transaction *transaction,\n \t\t\t     const char *refname,\n@@ -472,8 +368,8 @@ int cmd_replay(int argc,\n \t\tif (commit->parents->next)\n \t\t\tdie(_(\"replaying merge commits is not supported yet!\"));\n \n-\t\tlast_commit = pick_regular_commit(repo, commit, replayed_commits,\n-\t\t\t\t\t\t  onto, &merge_opt, &result);\n+\t\tlast_commit = replay_pick_regular_commit(repo, commit, replayed_commits,\n+\t\t\t\t\t\t\t onto, &merge_opt, &result);\n \t\tif (!last_commit)\n \t\t\tbreak;\n \ndiff --git a/meson.build b/meson.build\nindex cee94244759..ae8d4fef059 100644\n--- a/meson.build\n+++ b/meson.build\n@@ -464,6 +464,7 @@ libgit_sources = [\n   'reftable/writer.c',\n   'remote.c',\n   'replace-object.c',\n+  'replay.c',\n   'repo-settings.c',\n   'repository.c',\n   'rerere.c',\ndiff --git a/replay.c b/replay.c\nnew file mode 100644\nindex 00000000000..e22ce399406\n--- /dev/null\n+++ b/replay.c\n@@ -0,0 +1,115 @@\n+#define USE_THE_REPOSITORY_VARIABLE\n+\n+#include \"git-compat-util.h\"\n+#include \"commit.h\"\n+#include \"environment.h\"\n+#include \"gettext.h\"\n+#include \"ident.h\"\n+#include \"object.h\"\n+#include \"object-name.h\"\n+#include \"replay.h\"\n+#include \"tree.h\"\n+\n+static const char *short_commit_name(struct repository *repo,\n+\t\t\t\t     struct commit *commit)\n+{\n+\treturn repo_find_unique_abbrev(repo, &commit->object.oid,\n+\t\t\t\t       DEFAULT_ABBREV);\n+}\n+\n+static char *get_author(const char *message)\n+{\n+\tsize_t len;\n+\tconst char *a;\n+\n+\ta = find_commit_header(message, \"author\", &len);\n+\tif (a)\n+\t\treturn xmemdupz(a, len);\n+\n+\treturn NULL;\n+}\n+\n+static struct commit *create_commit(struct repository *repo,\n+\t\t\t\t    struct tree *tree,\n+\t\t\t\t    struct commit *based_on,\n+\t\t\t\t    struct commit *parent)\n+{\n+\tstruct object_id ret;\n+\tstruct object *obj = NULL;\n+\tstruct commit_list *parents = NULL;\n+\tchar *author;\n+\tchar *sign_commit = NULL; /* FIXME: cli users might want to sign again */\n+\tstruct commit_extra_header *extra = NULL;\n+\tstruct strbuf msg = STRBUF_INIT;\n+\tconst char *out_enc = get_commit_output_encoding();\n+\tconst char *message = repo_logmsg_reencode(repo, based_on,\n+\t\t\t\t\t\t   NULL, out_enc);\n+\tconst char *orig_message = NULL;\n+\tconst char *exclude_gpgsig[] = { \"gpgsig\", NULL };\n+\n+\tcommit_list_insert(parent, &parents);\n+\textra = read_commit_extra_headers(based_on, exclude_gpgsig);\n+\tfind_commit_subject(message, &orig_message);\n+\tstrbuf_addstr(&msg, orig_message);\n+\tauthor = get_author(message);\n+\treset_ident_date();\n+\tif (commit_tree_extended(msg.buf, msg.len, &tree->object.oid, parents,\n+\t\t\t\t &ret, author, NULL, sign_commit, extra)) {\n+\t\terror(_(\"failed to write commit object\"));\n+\t\tgoto out;\n+\t}\n+\n+\tobj = parse_object(repo, &ret);\n+\n+out:\n+\trepo_unuse_commit_buffer(the_repository, based_on, message);\n+\tfree_commit_extra_headers(extra);\n+\tfree_commit_list(parents);\n+\tstrbuf_release(&msg);\n+\tfree(author);\n+\treturn (struct commit *)obj;\n+}\n+\n+static struct commit *mapped_commit(kh_oid_map_t *replayed_commits,\n+\t\t\t\t    struct commit *commit,\n+\t\t\t\t    struct commit *fallback)\n+{\n+\tkhint_t pos = kh_get_oid_map(replayed_commits, commit->object.oid);\n+\tif (pos == kh_end(replayed_commits))\n+\t\treturn fallback;\n+\treturn kh_value(replayed_commits, pos);\n+}\n+\n+struct commit *replay_pick_regular_commit(struct repository *repo,\n+\t\t\t\t\t  struct commit *pickme,\n+\t\t\t\t\t  kh_oid_map_t *replayed_commits,\n+\t\t\t\t\t  struct commit *onto,\n+\t\t\t\t\t  struct merge_options *merge_opt,\n+\t\t\t\t\t  struct merge_result *result)\n+{\n+\tstruct commit *base, *replayed_base;\n+\tstruct tree *pickme_tree, *base_tree;\n+\n+\tbase = pickme->parents->item;\n+\treplayed_base = mapped_commit(replayed_commits, base, onto);\n+\n+\tresult->tree = repo_get_commit_tree(repo, replayed_base);\n+\tpickme_tree = repo_get_commit_tree(repo, pickme);\n+\tbase_tree = repo_get_commit_tree(repo, base);\n+\n+\tmerge_opt->branch1 = short_commit_name(repo, replayed_base);\n+\tmerge_opt->branch2 = short_commit_name(repo, pickme);\n+\tmerge_opt->ancestor = xstrfmt(\"parent of %s\", merge_opt->branch2);\n+\n+\tmerge_incore_nonrecursive(merge_opt,\n+\t\t\t\t  base_tree,\n+\t\t\t\t  result->tree,\n+\t\t\t\t  pickme_tree,\n+\t\t\t\t  result);\n+\n+\tfree((char*)merge_opt->ancestor);\n+\tmerge_opt->ancestor = NULL;\n+\tif (!result->clean)\n+\t\treturn NULL;\n+\treturn create_commit(repo, result->tree, pickme, replayed_base);\n+}\ndiff --git a/replay.h b/replay.h\nnew file mode 100644\nindex 00000000000..a461b5c2341\n--- /dev/null\n+++ b/replay.h\n@@ -0,0 +1,18 @@\n+#ifndef REPLAY_H\n+#define REPLAY_H\n+\n+#include \"khash.h\"\n+#include \"merge-ort.h\"\n+#include \"repository.h\"\n+\n+struct commit;\n+struct tree;\n+\n+struct commit *replay_pick_regular_commit(struct repository *repo,\n+\t\t\t\t\t  struct commit *pickme,\n+\t\t\t\t\t  kh_oid_map_t *replayed_commits,\n+\t\t\t\t\t  struct commit *onto,\n+\t\t\t\t\t  struct merge_options *merge_opt,\n+\t\t\t\t\t  struct merge_result *result);\n+\n+#endif\n\n-- \n2.51.1.851.g4ebd6896fd.dirty\n\n"},{"id":"529284","messageId":"20251021-b4-pks-history-builtin-v5-3-78d23f578fe6@pks.im","threadId":"63986","inReplyTo":"20251021-b4-pks-history-builtin-v5-0-78d23f578fe6@pks.im","subject":"[PATCH v5 03/12] replay: stop using `the_repository`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-21T14:15:51Z","receivedAt":"2025-10-21T14:16:06Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"In `create_commit()` we're using `the_repository` even though we already\nhave a repository passed to use as an argument. Fix this.\n\nNote that we still cannot get rid of `USE_THE_REPOSITORY_VARIABLE`. This\nis because we use `DEFAULT_ABBREV and `get_commit_output_encoding()`,\nboth of which are stored as global variables that can be modified via\nthe Git configuration.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n replay.c | 2 +-\n 1 file changed, 1 insertion(+), 1 deletion(-)\n\ndiff --git a/replay.c b/replay.c\nindex e22ce399406..13d75d80543 100644\n--- a/replay.c\n+++ b/replay.c\n@@ -62,7 +62,7 @@ static struct commit *create_commit(struct repository *repo,\n \tobj = parse_object(repo, &ret);\n \n out:\n-\trepo_unuse_commit_buffer(the_repository, based_on, message);\n+\trepo_unuse_commit_buffer(repo, based_on, message);\n \tfree_commit_extra_headers(extra);\n \tfree_commit_list(parents);\n \tstrbuf_release(&msg);\n\n-- \n2.51.1.851.g4ebd6896fd.dirty\n\n"},{"id":"529285","messageId":"20251021-b4-pks-history-builtin-v5-4-78d23f578fe6@pks.im","threadId":"63986","inReplyTo":"20251021-b4-pks-history-builtin-v5-0-78d23f578fe6@pks.im","subject":"[PATCH v5 04/12] replay: parse commits before dereferencing them","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-21T14:15:52Z","receivedAt":"2025-10-21T14:16:10Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"When looking up a commit it may not be parsed yet. Callers that wish to\naccess the fields of `struct commit` have to call `repo_parse_commit()`\nfirst so that it is guaranteed to be populated.\n\nWe didn't yet care about doing so, because code paths that lead to\n`pick_regular_commit()` in \"builtin/replay.c\" already implicitly parsed\nthe commits. But now that the function is exposed to outside callers\nit's quite easy to get this wrong.\n\nMake the function easier to use by calling `repo_parse_commit()`.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n replay.c | 3 +++\n 1 file changed, 3 insertions(+)\n\ndiff --git a/replay.c b/replay.c\nindex 13d75d80543..c3628d2488b 100644\n--- a/replay.c\n+++ b/replay.c\n@@ -90,6 +90,9 @@ struct commit *replay_pick_regular_commit(struct repository *repo,\n \tstruct commit *base, *replayed_base;\n \tstruct tree *pickme_tree, *base_tree;\n \n+\tif (repo_parse_commit(repo, pickme))\n+\t\treturn NULL;\n+\n \tbase = pickme->parents->item;\n \treplayed_base = mapped_commit(replayed_commits, base, onto);\n \n\n-- \n2.51.1.851.g4ebd6896fd.dirty\n\n"},{"id":"529286","messageId":"20251021-b4-pks-history-builtin-v5-5-78d23f578fe6@pks.im","threadId":"63986","inReplyTo":"20251021-b4-pks-history-builtin-v5-0-78d23f578fe6@pks.im","subject":"[PATCH v5 05/12] builtin: add new \"history\" command","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-21T14:15:53Z","receivedAt":"2025-10-21T14:16:12Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"When rewriting history via git-rebase(1) there are a couple of very\ncommon use cases:\n\n  - The ordering of two commits should be reversed.\n\n  - A commit should be split up into two commits.\n\n  - A commit should be dropped from the history completely.\n\n  - Multiple commits should be squashed into one.\n\nWhile these operations are all doable, it often feels needlessly kludgey\nto do so by doing an interactive rebase, using the editor to say what\none wants, and then perform the actions. Furthermore, some operations\nlike splitting up a commit into two are way more involved than that and\nrequire a whole series of commands.\n\nAdd a new \"history\" command to plug this gap. This command will have\nseveral different subcommands to imperatively rewrite history for common\nuse cases like the above. These subcommands will be implemented in\nsubsequent commits.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n .gitignore                     |  1 +\n Documentation/git-history.adoc | 45 ++++++++++++++++++++++++++++++++++++++++++\n Documentation/meson.build      |  1 +\n Makefile                       |  1 +\n builtin.h                      |  1 +\n builtin/history.c              | 22 +++++++++++++++++++++\n command-list.txt               |  1 +\n git.c                          |  1 +\n meson.build                    |  1 +\n t/meson.build                  |  1 +\n t/t3450-history.sh             | 17 ++++++++++++++++\n 11 files changed, 92 insertions(+)\n\ndiff --git a/.gitignore b/.gitignore\nindex 78a45cb5bec..24635cf2d6f 100644\n--- a/.gitignore\n+++ b/.gitignore\n@@ -79,6 +79,7 @@\n /git-grep\n /git-hash-object\n /git-help\n+/git-history\n /git-hook\n /git-http-backend\n /git-http-fetch\ndiff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc\nnew file mode 100644\nindex 00000000000..57560525a70\n--- /dev/null\n+++ b/Documentation/git-history.adoc\n@@ -0,0 +1,45 @@\n+git-history(1)\n+==============\n+\n+NAME\n+----\n+git-history - EXPERIMENTAL: Rewrite history of the current branch\n+\n+SYNOPSIS\n+--------\n+[synopsis]\n+git history [<options>]\n+\n+DESCRIPTION\n+-----------\n+\n+Rewrite history by rearranging or modifying specific commits in the\n+history.\n+\n+This command is similar to linkgit:git-rebase[1] and uses the same\n+underlying machinery. You should use rebases if you want to reapply a range of\n+commits onto a different base, or interactive rebases if you want to edit a\n+range of commits.\n+\n+Note that this command does not (yet) work with histories that contain\n+merges. You should use linkgit:git-rebase[1] with the `--rebase-merges`\n+flag instead.\n+\n+THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.\n+\n+COMMANDS\n+--------\n+\n+This command requires a subcommand. Several subcommands are available to\n+rewrite history in different ways:\n+\n+CONFIGURATION\n+-------------\n+\n+include::includes/cmd-config-section-all.adoc[]\n+\n+include::config/sequencer.adoc[]\n+\n+GIT\n+---\n+Part of the linkgit:git[1] suite\ndiff --git a/Documentation/meson.build b/Documentation/meson.build\nindex 44f94cdb7ba..cf551a28ae7 100644\n--- a/Documentation/meson.build\n+++ b/Documentation/meson.build\n@@ -64,6 +64,7 @@ manpages = {\n   'git-gui.adoc' : 1,\n   'git-hash-object.adoc' : 1,\n   'git-help.adoc' : 1,\n+  'git-history.adoc' : 1,\n   'git-hook.adoc' : 1,\n   'git-http-backend.adoc' : 1,\n   'git-http-fetch.adoc' : 1,\ndiff --git a/Makefile b/Makefile\nindex 01c171b4f03..1380ee1e196 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -1395,6 +1395,7 @@ BUILTIN_OBJS += builtin/get-tar-commit-id.o\n BUILTIN_OBJS += builtin/grep.o\n BUILTIN_OBJS += builtin/hash-object.o\n BUILTIN_OBJS += builtin/help.o\n+BUILTIN_OBJS += builtin/history.o\n BUILTIN_OBJS += builtin/hook.o\n BUILTIN_OBJS += builtin/index-pack.o\n BUILTIN_OBJS += builtin/init-db.o\ndiff --git a/builtin.h b/builtin.h\nindex 1b35565fbd9..93c91d07d4b 100644\n--- a/builtin.h\n+++ b/builtin.h\n@@ -172,6 +172,7 @@ int cmd_get_tar_commit_id(int argc, const char **argv, const char *prefix, struc\n int cmd_grep(int argc, const char **argv, const char *prefix, struct repository *repo);\n int cmd_hash_object(int argc, const char **argv, const char *prefix, struct repository *repo);\n int cmd_help(int argc, const char **argv, const char *prefix, struct repository *repo);\n+int cmd_history(int argc, const char **argv, const char *prefix, struct repository *repo);\n int cmd_hook(int argc, const char **argv, const char *prefix, struct repository *repo);\n int cmd_index_pack(int argc, const char **argv, const char *prefix, struct repository *repo);\n int cmd_init_db(int argc, const char **argv, const char *prefix, struct repository *repo);\ndiff --git a/builtin/history.c b/builtin/history.c\nnew file mode 100644\nindex 00000000000..f6fe32610b0\n--- /dev/null\n+++ b/builtin/history.c\n@@ -0,0 +1,22 @@\n+#include \"builtin.h\"\n+#include \"gettext.h\"\n+#include \"parse-options.h\"\n+\n+int cmd_history(int argc,\n+\t\tconst char **argv,\n+\t\tconst char *prefix,\n+\t\tstruct repository *repo UNUSED)\n+{\n+\tconst char * const usage[] = {\n+\t\tN_(\"git history [<options>]\"),\n+\t\tNULL,\n+\t};\n+\tstruct option options[] = {\n+\t\tOPT_END(),\n+\t};\n+\n+\targc = parse_options(argc, argv, prefix, options, usage, 0);\n+\tif (argc)\n+\t\tusagef(\"unrecognized argument: %s\", argv[0]);\n+\treturn 0;\n+}\ndiff --git a/command-list.txt b/command-list.txt\nindex accd3d0c4b5..f9005cf4597 100644\n--- a/command-list.txt\n+++ b/command-list.txt\n@@ -115,6 +115,7 @@ git-grep                                mainporcelain           info\n git-gui                                 mainporcelain\n git-hash-object                         plumbingmanipulators\n git-help                                ancillaryinterrogators          complete\n+git-history                             mainporcelain           history\n git-hook                                purehelpers\n git-http-backend                        synchingrepositories\n git-http-fetch                          synchelpers\ndiff --git a/git.c b/git.c\nindex c5fad56813f..744cb6527e0 100644\n--- a/git.c\n+++ b/git.c\n@@ -586,6 +586,7 @@ static struct cmd_struct commands[] = {\n \t{ \"grep\", cmd_grep, RUN_SETUP_GENTLY },\n \t{ \"hash-object\", cmd_hash_object },\n \t{ \"help\", cmd_help },\n+\t{ \"history\", cmd_history, RUN_SETUP },\n \t{ \"hook\", cmd_hook, RUN_SETUP },\n \t{ \"index-pack\", cmd_index_pack, RUN_SETUP_GENTLY | NO_PARSEOPT },\n \t{ \"init\", cmd_init_db },\ndiff --git a/meson.build b/meson.build\nindex ae8d4fef059..2d789612a01 100644\n--- a/meson.build\n+++ b/meson.build\n@@ -604,6 +604,7 @@ builtin_sources = [\n   'builtin/grep.c',\n   'builtin/hash-object.c',\n   'builtin/help.c',\n+  'builtin/history.c',\n   'builtin/hook.c',\n   'builtin/index-pack.c',\n   'builtin/init-db.c',\ndiff --git a/t/meson.build b/t/meson.build\nindex 401b24e50e0..019435918fa 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -384,6 +384,7 @@ integration_tests = [\n   't3436-rebase-more-options.sh',\n   't3437-rebase-fixup-options.sh',\n   't3438-rebase-broken-files.sh',\n+  't3450-history.sh',\n   't3500-cherry.sh',\n   't3501-revert-cherry-pick.sh',\n   't3502-cherry-pick-merge.sh',\ndiff --git a/t/t3450-history.sh b/t/t3450-history.sh\nnew file mode 100755\nindex 00000000000..417c343d43b\n--- /dev/null\n+++ b/t/t3450-history.sh\n@@ -0,0 +1,17 @@\n+#!/bin/sh\n+\n+test_description='tests for git-history command'\n+\n+. ./test-lib.sh\n+\n+test_expect_success 'does nothing without any arguments' '\n+\tgit history >out 2>&1 &&\n+\ttest_must_be_empty out\n+'\n+\n+test_expect_success 'raises an error with unknown argument' '\n+\ttest_must_fail git history garbage 2>err &&\n+\ttest_grep \"unrecognized argument: garbage\" err\n+'\n+\n+test_done\n\n-- \n2.51.1.851.g4ebd6896fd.dirty\n\n"},{"id":"529287","messageId":"20251021-b4-pks-history-builtin-v5-6-78d23f578fe6@pks.im","threadId":"63986","inReplyTo":"20251021-b4-pks-history-builtin-v5-0-78d23f578fe6@pks.im","subject":"[PATCH v5 06/12] builtin/history: implement \"reword\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-21T14:15:54Z","receivedAt":"2025-10-21T14:16:15Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Implement a new \"reword\" subcommand for git-history(1). This subcommand\nis essentially the same as if a user performed an interactive rebase\nwith a single commit changed to use the \"reword\" verb.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/git-history.adoc |   7 +\n builtin/history.c              | 364 ++++++++++++++++++++++++++++++++++++++++-\n t/meson.build                  |   1 +\n t/t3450-history.sh             |   6 +-\n t/t3451-history-reword.sh      | 217 ++++++++++++++++++++++++\n 5 files changed, 588 insertions(+), 7 deletions(-)\n\ndiff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc\nindex 57560525a70..6f016801936 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 [<options>]\n+git history reword [<options>] <commit>\n \n DESCRIPTION\n -----------\n@@ -33,6 +34,12 @@ COMMANDS\n This command requires a subcommand. Several subcommands are available to\n rewrite history in different ways:\n \n+`reword <commit> [--message=<message>]`::\n+\tRewrite the commit message of the specified commit. All the other\n+\tdetails of this commit remain unchanged. If no commit message is\n+\tprovided, then this command will spawn an editor with the current\n+\tmessage of that commit.\n+\n CONFIGURATION\n -------------\n \ndiff --git a/builtin/history.c b/builtin/history.c\nindex f6fe32610b0..f14e88c9bf4 100644\n--- a/builtin/history.c\n+++ b/builtin/history.c\n@@ -1,22 +1,378 @@\n+#define USE_THE_REPOSITORY_VARIABLE\n+\n #include \"builtin.h\"\n+#include \"commit-reach.h\"\n+#include \"commit.h\"\n+#include \"config.h\"\n+#include \"editor.h\"\n+#include \"environment.h\"\n #include \"gettext.h\"\n+#include \"hex.h\"\n #include \"parse-options.h\"\n+#include \"refs.h\"\n+#include \"replay.h\"\n+#include \"reset.h\"\n+#include \"revision.h\"\n+#include \"sequencer.h\"\n+#include \"strvec.h\"\n+#include \"tree.h\"\n+#include \"wt-status.h\"\n+\n+#define GIT_HISTORY_REWORD_USAGE N_(\"git history reword [<options>] <commit>\")\n+\n+static int collect_commits(struct repository *repo,\n+\t\t\t   struct commit *old_commit,\n+\t\t\t   struct commit *new_commit,\n+\t\t\t   struct strvec *out)\n+{\n+\tstruct setup_revision_opt revision_opts = {\n+\t\t.assume_dashdash = 1,\n+\t};\n+\tstruct strvec revisions = STRVEC_INIT;\n+\tstruct commit *child;\n+\tstruct rev_info rev = { 0 };\n+\tint ret;\n+\n+\trepo_init_revisions(repo, &rev, NULL);\n+\tstrvec_push(&revisions, \"\");\n+\tstrvec_push(&revisions, oid_to_hex(&new_commit->object.oid));\n+\tif (old_commit)\n+\t\tstrvec_pushf(&revisions, \"^%s\", oid_to_hex(&old_commit->object.oid));\n+\n+\tsetup_revisions_from_strvec(&revisions, &rev, &revision_opts);\n+\tif (revisions.nr != 1 || prepare_revision_walk(&rev)) {\n+\t\tret = error(_(\"revision walk setup failed\"));\n+\t\tgoto out;\n+\t}\n+\n+\twhile ((child = get_revision(&rev))) {\n+\t\tif (old_commit && !child->parents)\n+\t\t\tBUG(\"revision walk did not find child commit\");\n+\t\tif (child->parents && child->parents->next) {\n+\t\t\tret = error(_(\"cannot rearrange commit history with merges\"));\n+\t\t\tgoto out;\n+\t\t}\n+\n+\t\tstrvec_push(out, oid_to_hex(&child->object.oid));\n+\n+\t\tif (child->parents && old_commit &&\n+\t\t    commit_list_contains(old_commit, child->parents))\n+\t\t\tbreak;\n+\t}\n+\n+\t/*\n+\t * Revisions are in newest-order-first. We have to reverse the\n+\t * array though so that we pick the oldest commits first.\n+\t */\n+\tfor (size_t i = 0, j = out->nr - 1; i < j; i++, j--)\n+\t\tSWAP(out->v[i], out->v[j]);\n+\n+\tret = 0;\n+\n+out:\n+\tstrvec_clear(&revisions);\n+\trelease_revisions(&rev);\n+\treset_revision_walk();\n+\treturn ret;\n+}\n+\n+static void replace_commits(struct strvec *commits,\n+\t\t\t    const struct object_id *commit_to_replace,\n+\t\t\t    const struct object_id *replacements,\n+\t\t\t    size_t replacements_nr)\n+{\n+\tchar commit_to_replace_oid[GIT_MAX_HEXSZ + 1];\n+\tstruct strvec replacement_oids = STRVEC_INIT;\n+\tbool found = false;\n+\n+\toid_to_hex_r(commit_to_replace_oid, commit_to_replace);\n+\tfor (size_t i = 0; i < replacements_nr; i++)\n+\t\tstrvec_push(&replacement_oids, oid_to_hex(&replacements[i]));\n+\n+\tfor (size_t i = 0; i < commits->nr; i++) {\n+\t\tif (strcmp(commits->v[i], commit_to_replace_oid))\n+\t\t\tcontinue;\n+\t\tstrvec_splice(commits, i, 1, replacement_oids.v, replacement_oids.nr);\n+\t\tfound = true;\n+\t\tbreak;\n+\t}\n+\tif (!found)\n+\t\tBUG(\"could not find commit to replace\");\n+\n+\tstrvec_clear(&replacement_oids);\n+}\n+\n+static int apply_commits(struct repository *repo,\n+\t\t\t const struct strvec *commits,\n+\t\t\t struct commit *onto,\n+\t\t\t struct commit *orig_head,\n+\t\t\t const char *action)\n+{\n+\tstruct reset_head_opts reset_opts = { 0 };\n+\tstruct merge_options merge_opts = { 0 };\n+\tstruct merge_result result = { 0 };\n+\tstruct strbuf buf = STRBUF_INIT;\n+\tkh_oid_map_t *replayed_commits;\n+\tint ret;\n+\n+\treplayed_commits = kh_init_oid_map();\n+\n+\tinit_basic_merge_options(&merge_opts, repo);\n+\tmerge_opts.show_rename_progress = 0;\n+\n+\tfor (size_t i = 0; i < commits->nr; i++) {\n+\t\tstruct object_id commit_id;\n+\t\tstruct commit *commit;\n+\t\tconst char *end;\n+\t\tint hash_result;\n+\t\tkhint_t pos;\n+\n+\t\tif (parse_oid_hex_algop(commits->v[i], &commit_id, &end,\n+\t\t\t\t\trepo->hash_algo)) {\n+\t\t\tret = error(_(\"invalid object ID: %s\"), commits->v[i]);\n+\t\t\tgoto out;\n+\t\t}\n+\n+\t\tcommit = lookup_commit(repo, &commit_id);\n+\t\tif (!commit || repo_parse_commit(repo, commit)) {\n+\t\t\tret = error(_(\"failed to look up commit: %s\"), oid_to_hex(&commit_id));\n+\t\t\tgoto out;\n+\t\t}\n+\n+\t\tif (!onto) {\n+\t\t\tonto = commit;\n+\t\t\tresult.clean = 1;\n+\t\t\tresult.tree = repo_get_commit_tree(repo, commit);\n+\t\t} else {\n+\t\t\tonto = replay_pick_regular_commit(repo, commit, replayed_commits,\n+\t\t\t\t\t\t\t  onto, &merge_opts, &result);\n+\t\t\tif (!onto)\n+\t\t\t\tbreak;\n+\t\t}\n+\n+\t\tpos = kh_put_oid_map(replayed_commits, commit->object.oid, &hash_result);\n+\t\tif (hash_result == 0) {\n+\t\t\tret = error(_(\"duplicate rewritten commit: %s\\n\"),\n+\t\t\t\t    oid_to_hex(&commit->object.oid));\n+\t\t\tgoto out;\n+\t\t}\n+\t\tkh_value(replayed_commits, pos) = onto;\n+\t}\n+\n+\tif (!result.clean) {\n+\t\tret = error(_(\"could not merge\"));\n+\t\tgoto out;\n+\t}\n+\n+\treset_opts.oid = &onto->object.oid;\n+\tstrbuf_addf(&buf, \"%s: switch to rewritten %s\", action, oid_to_hex(reset_opts.oid));\n+\treset_opts.flags = RESET_HEAD_REFS_ONLY | RESET_ORIG_HEAD;\n+\treset_opts.orig_head = &orig_head->object.oid;\n+\treset_opts.default_reflog_action = action;\n+\tif (reset_head(repo, &reset_opts) < 0) {\n+\t\tret = error(_(\"could not switch to %s\"), oid_to_hex(reset_opts.oid));\n+\t\tgoto out;\n+\t}\n+\n+\tret = 0;\n+\n+out:\n+\tkh_destroy_oid_map(replayed_commits);\n+\tmerge_finalize(&merge_opts, &result);\n+\tstrbuf_release(&buf);\n+\treturn ret;\n+}\n+\n+static void change_data_free(void *util, const char *str UNUSED)\n+{\n+\tstruct wt_status_change_data *d = util;\n+\tfree(d->rename_source);\n+\tfree(d);\n+}\n+\n+static int fill_commit_message(struct repository *repo,\n+\t\t\t       const struct object_id *old_tree,\n+\t\t\t       const struct object_id *new_tree,\n+\t\t\t       const char *default_message,\n+\t\t\t       const char *provided_message,\n+\t\t\t       const char *action,\n+\t\t\t       struct strbuf *out)\n+{\n+\tif (!provided_message) {\n+\t\tconst char *path = git_path_commit_editmsg();\n+\t\tconst char *hint =\n+\t\t\t_(\"Please enter the commit message for the %s changes.\"\n+\t\t\t  \" Lines starting\\nwith '%s' will be ignored.\\n\");\n+\t\tstruct wt_status s;\n+\n+\t\tstrbuf_addstr(out, default_message);\n+\t\tstrbuf_addch(out, '\\n');\n+\t\tstrbuf_commented_addf(out, comment_line_str, hint, action, comment_line_str);\n+\t\twrite_file_buf(path, out->buf, out->len);\n+\n+\t\twt_status_prepare(repo, &s);\n+\t\tFREE_AND_NULL(s.branch);\n+\t\ts.ahead_behind_flags = AHEAD_BEHIND_QUICK;\n+\t\ts.commit_template = 1;\n+\t\ts.colopts = 0;\n+\t\ts.display_comment_prefix = 1;\n+\t\ts.hints = 0;\n+\t\ts.use_color = 0;\n+\t\ts.whence = FROM_COMMIT;\n+\t\ts.committable = 1;\n+\n+\t\ts.fp = fopen(git_path_commit_editmsg(), \"a\");\n+\t\tif (!s.fp)\n+\t\t\treturn error_errno(_(\"could not open '%s'\"), git_path_commit_editmsg());\n+\n+\t\twt_status_collect_changes_trees(&s, old_tree, new_tree);\n+\t\twt_status_print(&s);\n+\t\twt_status_collect_free_buffers(&s);\n+\t\tstring_list_clear_func(&s.change, change_data_free);\n+\n+\t\tstrbuf_reset(out);\n+\t\tif (launch_editor(path, out, NULL)) {\n+\t\t\tfprintf(stderr, _(\"Please supply the message using the -m option.\\n\"));\n+\t\t\treturn -1;\n+\t\t}\n+\t\tstrbuf_stripspace(out, comment_line_str);\n+\t} else {\n+\t\tstrbuf_addstr(out, provided_message);\n+\t}\n+\n+\tcleanup_message(out, COMMIT_MSG_CLEANUP_ALL, 0);\n+\n+\tif (!out->len) {\n+\t\tfprintf(stderr, _(\"Aborting commit due to empty commit message.\\n\"));\n+\t\treturn -1;\n+\t}\n+\n+\treturn 0;\n+}\n+\n+static int cmd_history_reword(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_REWORD_USAGE,\n+\t\tNULL,\n+\t};\n+\tconst char *commit_message = NULL;\n+\tstruct option options[] = {\n+\t\tOPT_STRING('m', \"message\", &commit_message, N_(\"message\"), N_(\"commit message\")),\n+\t\tOPT_END(),\n+\t};\n+\tstruct strbuf final_message = STRBUF_INIT;\n+\tstruct commit *original_commit, *parent, *head;\n+\tstruct strvec commits = STRVEC_INIT;\n+\tstruct object_id parent_tree_oid, original_commit_tree_oid;\n+\tstruct object_id rewritten_commit;\n+\tstruct commit_list *from_list = NULL;\n+\tconst char *original_message, *original_body, *ptr;\n+\tchar *original_author = NULL;\n+\tsize_t len;\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 single revision\"));\n+\t\tgoto out;\n+\t}\n+\trepo_config(repo, git_default_config, NULL);\n+\n+\toriginal_commit = lookup_commit_reference_by_name(argv[0]);\n+\tif (!original_commit) {\n+\t\tret = error(_(\"commit to be reworded cannot be found: %s\"), argv[0]);\n+\t\tgoto out;\n+\t}\n+\toriginal_commit_tree_oid = repo_get_commit_tree(repo, original_commit)->object.oid;\n+\n+\tparent = original_commit->parents ? original_commit->parents->item : NULL;\n+\tif (parent) {\n+\t\tif (repo_parse_commit(repo, parent)) {\n+\t\t\tret = error(_(\"unable to parse commit %s\"),\n+\t\t\t\t    oid_to_hex(&parent->object.oid));\n+\t\t\tgoto out;\n+\t\t}\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+\thead = lookup_commit_reference_by_name(\"HEAD\");\n+\tif (!head) {\n+\t\tret = error(_(\"could not resolve HEAD to a commit\"));\n+\t\tgoto out;\n+\t}\n+\n+\tcommit_list_append(original_commit, &from_list);\n+\tif (!repo_is_descendant_of(repo, head, from_list)) {\n+\t\tret = error (_(\"split commit must be reachable from current HEAD commit\"));\n+\t\tgoto out;\n+\t}\n+\n+\t/*\n+\t * Collect the list of commits that we'll have to reapply now already.\n+\t * This ensures that we'll abort early on in case the range of commits\n+\t * contains merges, which we do not yet handle.\n+\t */\n+\tret = collect_commits(repo, parent, head, &commits);\n+\tif (ret < 0)\n+\t\tgoto out;\n+\n+\t/* We retain authorship of the original commit. */\n+\toriginal_message = repo_logmsg_reencode(repo, original_commit, 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_commit_tree_oid,\n+\t\t\t\t  original_body, commit_message, \"reworded\", &final_message);\n+\tif (ret < 0)\n+\t\tgoto out;\n+\n+\tret = commit_tree(final_message.buf, final_message.len, &original_commit_tree_oid,\n+\t\t\t  original_commit->parents, &rewritten_commit, original_author, NULL);\n+\tif (ret < 0) {\n+\t\tret = error(_(\"failed writing reworded commit\"));\n+\t\tgoto out;\n+\t}\n+\n+\treplace_commits(&commits, &original_commit->object.oid, &rewritten_commit, 1);\n+\n+\tret = apply_commits(repo, &commits, parent, head, \"reword\");\n+\tif (ret < 0)\n+\t\tgoto out;\n+\n+\tret = 0;\n+\n+out:\n+\tstrbuf_release(&final_message);\n+\tfree_commit_list(from_list);\n+\tstrvec_clear(&commits);\n+\tfree(original_author);\n+\treturn ret;\n+}\n \n int cmd_history(int argc,\n \t\tconst char **argv,\n \t\tconst char *prefix,\n-\t\tstruct repository *repo UNUSED)\n+\t\tstruct repository *repo)\n {\n \tconst char * const usage[] = {\n \t\tN_(\"git history [<options>]\"),\n+\t\tGIT_HISTORY_REWORD_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_END(),\n \t};\n \n \targc = parse_options(argc, argv, prefix, options, usage, 0);\n-\tif (argc)\n-\t\tusagef(\"unrecognized argument: %s\", argv[0]);\n-\treturn 0;\n+\treturn fn(argc, argv, prefix, repo);\n }\ndiff --git a/t/meson.build b/t/meson.build\nindex 019435918fa..a3ec9199947 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -385,6 +385,7 @@ integration_tests = [\n   't3437-rebase-fixup-options.sh',\n   't3438-rebase-broken-files.sh',\n   't3450-history.sh',\n+  't3451-history-reword.sh',\n   't3500-cherry.sh',\n   't3501-revert-cherry-pick.sh',\n   't3502-cherry-pick-merge.sh',\ndiff --git a/t/t3450-history.sh b/t/t3450-history.sh\nindex 417c343d43b..f513463b92b 100755\n--- a/t/t3450-history.sh\n+++ b/t/t3450-history.sh\n@@ -5,13 +5,13 @@ test_description='tests for git-history command'\n . ./test-lib.sh\n \n test_expect_success 'does nothing without any arguments' '\n-\tgit history >out 2>&1 &&\n-\ttest_must_be_empty out\n+\ttest_must_fail git history 2>err &&\n+\ttest_grep \"need a subcommand\" err\n '\n \n test_expect_success 'raises an error with unknown argument' '\n \ttest_must_fail git history garbage 2>err &&\n-\ttest_grep \"unrecognized argument: garbage\" err\n+\ttest_grep \"unknown subcommand: .garbage.\" err\n '\n \n test_done\ndiff --git a/t/t3451-history-reword.sh b/t/t3451-history-reword.sh\nnew file mode 100755\nindex 00000000000..b73371fb51c\n--- /dev/null\n+++ b/t/t3451-history-reword.sh\n@@ -0,0 +1,217 @@\n+#!/bin/sh\n+\n+test_description='tests for git-history reword subcommand'\n+\n+. ./test-lib.sh\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 reword HEAD~ 2>err &&\n+\t\ttest_grep \"cannot rearrange commit history with merges\" err &&\n+\t\ttest_must_fail git history reword HEAD 2>err &&\n+\t\ttest_grep \"cannot rearrange commit history with merges\" err\n+\t)\n+'\n+\n+test_expect_success 'refuses to work with unrelated 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\ttest_must_fail git history reword ours 2>err &&\n+\t\ttest_grep \"split commit must be reachable from current HEAD commit\" err\n+\t)\n+'\n+\n+test_expect_success 'can reword tip of a branch' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit first &&\n+\t\ttest_commit second &&\n+\t\ttest_commit third &&\n+\n+\t\tgit symbolic-ref HEAD >expect &&\n+\t\tgit history reword -m \"third reworded\" HEAD &&\n+\t\tgit symbolic-ref HEAD >actual &&\n+\t\ttest_cmp expect actual &&\n+\n+\t\tcat >expect <<-EOF &&\n+\t\tthird reworded\n+\t\tsecond\n+\t\tfirst\n+\t\tEOF\n+\t\tgit log --format=%s >actual &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+test_expect_success 'can reword commit in the middle' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit first &&\n+\t\ttest_commit second &&\n+\t\ttest_commit third &&\n+\n+\t\tgit symbolic-ref HEAD >expect &&\n+\t\tgit history reword -m \"second reworded\" HEAD~ &&\n+\t\tgit symbolic-ref HEAD >actual &&\n+\t\ttest_cmp expect actual &&\n+\n+\t\tcat >expect <<-EOF &&\n+\t\tthird\n+\t\tsecond reworded\n+\t\tfirst\n+\t\tEOF\n+\t\tgit log --format=%s >actual &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+test_expect_success 'can reword root commit' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit first &&\n+\t\ttest_commit second &&\n+\t\ttest_commit third &&\n+\t\tgit history reword -m \"first reworded\" HEAD~2 &&\n+\n+\t\tcat >expect <<-EOF &&\n+\t\tthird\n+\t\tsecond\n+\t\tfirst reworded\n+\t\tEOF\n+\t\tgit log --format=%s >actual &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+test_expect_success 'can use editor to rewrite commit message' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit first &&\n+\n+\t\twrite_script fake-editor.sh <<-\\EOF &&\n+\t\tcp \"$1\" . &&\n+\t\tprintf \"\\namend a comment\\n\" >>\"$1\"\n+\t\tEOF\n+\t\ttest_set_editor \"$(pwd)\"/fake-editor.sh &&\n+\t\tgit history reword HEAD &&\n+\n+\t\tcat >expect <<-EOF &&\n+\t\tfirst\n+\n+\t\t# Please enter the commit message for the reworded changes. Lines starting\n+\t\t# with ${SQ}#${SQ} will be ignored.\n+\t\t# Changes to be committed:\n+\t\t#\tnew file:   first.t\n+\t\t#\n+\t\tEOF\n+\t\ttest_cmp expect COMMIT_EDITMSG &&\n+\n+\t\tcat >expect <<-EOF &&\n+\t\tfirst\n+\n+\t\tamend a comment\n+\n+\t\tEOF\n+\t\tgit log --format=%B >actual &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+# For now, git-history(1) does not yet execute any hooks. This is subject to\n+# change in the future, and if it does this test here is expected to start\n+# failing. In other words, this test is not an endorsement of the current\n+# status quo.\n+test_expect_success 'hooks are executed for rewritten commits' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit first &&\n+\t\ttest_commit second &&\n+\t\ttest_commit third &&\n+\n+\t\twrite_script .git/hooks/prepare-commit-msg <<-EOF &&\n+\t\ttouch \"$(pwd)/hooks.log\n+\t\tEOF\n+\t\twrite_script .git/hooks/post-commit <<-EOF &&\n+\t\ttouch \"$(pwd)/hooks.log\n+\t\tEOF\n+\t\twrite_script .git/hooks/post-rewrite <<-EOF &&\n+\t\ttouch \"$(pwd)/hooks.log\n+\t\tEOF\n+\n+\t\tgit history reword -m \"second reworded\" HEAD~ &&\n+\n+\t\tcat >expect <<-EOF &&\n+\t\tthird\n+\t\tsecond reworded\n+\t\tfirst\n+\t\tEOF\n+\t\tgit log --format=%s >actual &&\n+\t\ttest_cmp expect actual &&\n+\n+\t\ttest_path_is_missing hooks.log\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\ttest_commit first &&\n+\n+\t\ttest_must_fail git history reword -m \"\" HEAD 2>err &&\n+\t\ttest_grep \"Aborting commit due to empty commit message.\" 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\ttouch a b &&\n+\t\tgit add . &&\n+\t\tgit commit -m \"initial commit\" &&\n+\t\techo foo >a &&\n+\t\techo bar >b &&\n+\t\tgit add b &&\n+\t\tgit history reword HEAD -m message &&\n+\t\tcat >expect <<-\\EOF &&\n+\t\t M a\n+\t\tM  b\n+\t\t?? actual\n+\t\t?? expect\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.51.1.851.g4ebd6896fd.dirty\n\n"},{"id":"529288","messageId":"20251021-b4-pks-history-builtin-v5-7-78d23f578fe6@pks.im","threadId":"63986","inReplyTo":"20251021-b4-pks-history-builtin-v5-0-78d23f578fe6@pks.im","subject":"[PATCH v5 07/12] add-patch: split out header from \"add-interactive.h\"","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-21T14:15:55Z","receivedAt":"2025-10-21T14:16:18Z","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 | 23 +++--------------------\n add-patch.c       |  1 +\n add-patch.h       | 26 ++++++++++++++++++++++++++\n 3 files changed, 30 insertions(+), 20 deletions(-)\n\ndiff --git a/add-interactive.h b/add-interactive.h\nindex da49502b765..2e3d1d871d2 100644\n--- a/add-interactive.h\n+++ b/add-interactive.h\n@@ -1,14 +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-};\n-\n-#define ADD_P_OPT_INIT { .context = -1, .interhunkcontext = -1 }\n+struct pathspec;\n+struct repository;\n \n struct add_i_state {\n \tstruct repository *r;\n@@ -35,21 +32,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 9402dc71bc6..3bb7bcf3d26 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 00000000000..4394c741076\n--- /dev/null\n+++ b/add-patch.h\n@@ -0,0 +1,26 @@\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+};\n+\n+#define ADD_P_OPT_INIT { .context = -1, .interhunkcontext = -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.51.1.851.g4ebd6896fd.dirty\n\n"},{"id":"529289","messageId":"20251021-b4-pks-history-builtin-v5-8-78d23f578fe6@pks.im","threadId":"63986","inReplyTo":"20251021-b4-pks-history-builtin-v5-0-78d23f578fe6@pks.im","subject":"[PATCH v5 08/12] add-patch: split out `struct interactive_options`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-21T14:15:56Z","receivedAt":"2025-10-21T14:16:21Z","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  | 174 +++++++++++------------------------------------------\n add-interactive.h  |  23 +------\n add-patch.c        | 170 +++++++++++++++++++++++++++++++++++++++++++--------\n add-patch.h        |  36 ++++++++++-\n builtin/add.c      |  22 +++----\n builtin/checkout.c |   4 +-\n builtin/commit.c   |  16 ++---\n builtin/reset.c    |  16 ++---\n builtin/stash.c    |  46 +++++++-------\n commit.h           |   2 +-\n 10 files changed, 270 insertions(+), 239 deletions(-)\n\ndiff --git a/add-interactive.c b/add-interactive.c\nindex 68fc09547dd..05d2e7eefe3 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,119 +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-\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@@ -286,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@@ -354,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@@ -432,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@@ -992,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@@ -1014,9 +912,9 @@ 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\tstruct interactive_options opts = {\n+\t\t\t.context = s->cfg.context,\n+\t\t\t.interhunkcontext = s->cfg.interhunkcontext,\n \t\t};\n \t\tstruct strvec args = STRVEC_INIT;\n \t\tstruct pathspec ps_selected = { 0 };\n@@ -1028,7 +926,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@@ -1064,10 +962,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@@ -1085,17 +983,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@@ -1103,21 +1001,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@@ -1152,7 +1050,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@@ -1163,7 +1061,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@@ -1206,15 +1104,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 2e3d1d871d2..eefa2edc7c1 100644\n--- a/add-interactive.h\n+++ b/add-interactive.h\n@@ -2,37 +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+\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 3bb7bcf3d26..1c625a7d7a7 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,122 @@ 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+\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 +417,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 +542,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 +576,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 +811,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 +833,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 +1222,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 +1356,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 +1370,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@@ -1559,15 +1677,15 @@ static int patch_update_file(struct add_p_state *s,\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 \t\t\t\t\t\t: 1));\n \t\tprintf(_(s->mode->prompt_mode[prompt_mode_type]),\n \t\t       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\tbreak;\n@@ -1729,7 +1847,7 @@ static int patch_update_file(struct add_p_state *s,\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@@ -1747,7 +1865,7 @@ static int patch_update_file(struct add_p_state *s,\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@@ -1765,7 +1883,7 @@ static int patch_update_file(struct add_p_state *s,\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@@ -1806,7 +1924,7 @@ static int patch_update_file(struct add_p_state *s,\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@@ -1814,7 +1932,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;\ndiff --git a/add-patch.h b/add-patch.h\nindex 4394c741076..a4a05d9d145 100644\n--- a/add-patch.h\n+++ b/add-patch.h\n@@ -1,15 +1,45 @@\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 };\n \n-#define ADD_P_OPT_INIT { .context = -1, .interhunkcontext = -1 }\n+#define INTERACTIVE_OPTIONS_INIT { \\\n+\t.context = -1, \\\n+\t.interhunkcontext = -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+};\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@@ -20,7 +50,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 32709794b38..6f1e2130528 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,8 +256,8 @@ 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_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@@ -400,9 +400,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@@ -412,11 +412,11 @@ 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}\n \ndiff --git a/builtin/checkout.c b/builtin/checkout.c\nindex f9453473fe2..d230b1f8995 100644\n--- a/builtin/checkout.c\n+++ b/builtin/checkout.c\n@@ -546,7 +546,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};\n@@ -575,7 +575,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 0243f17d53c..640495cc57e 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@@ -356,9 +356,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@@ -407,7 +407,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@@ -432,9 +432,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@@ -1742,8 +1742,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 ed35802af15..088449e1209 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,8 +371,8 @@ 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_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@@ -423,9 +423,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@@ -436,12 +436,12 @@ 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}\n \ndiff --git a/builtin/stash.c b/builtin/stash.c\nindex 948eba06fbc..3b509052338 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,8 +1851,8 @@ 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_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@@ -1907,19 +1909,19 @@ 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}\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@@ -1944,7 +1946,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@@ -1952,8 +1954,8 @@ 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_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@@ -1973,20 +1975,20 @@ 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}\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 1d6e0c7518b..7b6e59d6c19 100644\n--- a/commit.h\n+++ b/commit.h\n@@ -258,7 +258,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.51.1.851.g4ebd6896fd.dirty\n\n"},{"id":"529290","messageId":"20251021-b4-pks-history-builtin-v5-9-78d23f578fe6@pks.im","threadId":"63986","inReplyTo":"20251021-b4-pks-history-builtin-v5-0-78d23f578fe6@pks.im","subject":"[PATCH v5 09/12] add-patch: remove dependency on \"add-interactive\" subsystem","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-21T14:15:57Z","receivedAt":"2025-10-21T14:16:24Z","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 | 70 ++++++++++++++++++++++++++++++++-----------------------------\n 1 file changed, 37 insertions(+), 33 deletions(-)\n\ndiff --git a/add-patch.c b/add-patch.c\nindex 1c625a7d7a7..362726c962d 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@@ -408,7 +408,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@@ -417,9 +417,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@@ -437,7 +437,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@@ -542,12 +542,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@@ -576,9 +576,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@@ -811,7 +811,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@@ -833,7 +833,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@@ -1222,12 +1222,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@@ -1356,7 +1356,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@@ -1370,7 +1370,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@@ -1677,15 +1677,15 @@ static int patch_update_file(struct add_p_state *s,\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 \t\t\t\t\t\t: 1));\n \t\tprintf(_(s->mode->prompt_mode[prompt_mode_type]),\n \t\t       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\tbreak;\n@@ -1847,7 +1847,7 @@ static int patch_update_file(struct add_p_state *s,\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@@ -1865,7 +1865,7 @@ static int patch_update_file(struct add_p_state *s,\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@@ -1883,7 +1883,7 @@ static int patch_update_file(struct add_p_state *s,\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@@ -1903,7 +1903,7 @@ static int patch_update_file(struct add_p_state *s,\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\t   s->mode->is_reverse);\n@@ -1914,8 +1914,8 @@ static int patch_update_file(struct add_p_state *s,\n \t\t\t\t\t NULL, 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\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@@ -1928,11 +1928,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\n-- \n2.51.1.851.g4ebd6896fd.dirty\n\n"},{"id":"529291","messageId":"20251021-b4-pks-history-builtin-v5-10-78d23f578fe6@pks.im","threadId":"63986","inReplyTo":"20251021-b4-pks-history-builtin-v5-0-78d23f578fe6@pks.im","subject":"[PATCH v5 10/12] add-patch: add support for in-memory index patching","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-21T14:15:58Z","receivedAt":"2025-10-21T14:16:26Z","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 | 112 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++---\n add-patch.h |   8 +++++\n 2 files changed, 116 insertions(+), 4 deletions(-)\n\ndiff --git a/add-patch.c b/add-patch.c\nindex 362726c962d..b8d46d54a2e 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@@ -47,7 +49,7 @@ static struct patch_mode patch_mode_add = {\n \t\tN_(\"Stage mode change [y,n,q,a,d%s,?]? \"),\n \t\tN_(\"Stage deletion [y,n,q,a,d%s,?]? \"),\n \t\tN_(\"Stage addition [y,n,q,a,d%s,?]? \"),\n-\t\tN_(\"Stage this hunk [y,n,q,a,d%s,?]? \")\n+\t\tN_(\"Stage this hunk [y,n,q,a,d%s,?]? \"),\n \t},\n \t.edit_hunk_hint = N_(\"If the patch applies cleanly, the edited hunk \"\n \t\t\t     \"will immediately be marked for staging.\"),\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@@ -437,7 +441,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@@ -1903,7 +1907,7 @@ static int patch_update_file(struct add_p_state *s,\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\t   s->mode->is_reverse);\n@@ -1914,9 +1918,11 @@ static int patch_update_file(struct add_p_state *s,\n \t\t\t\t\t NULL, 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 \tputchar('\\n');\n@@ -1929,6 +1935,8 @@ int run_add_p(struct repository *r, enum add_p_mode mode,\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@@ -1987,3 +1995,99 @@ int run_add_p(struct repository *r, enum add_p_mode mode,\n \tadd_p_state_clear(&s);\n \treturn 0;\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+\tstruct strbuf parent_revision = STRBUF_INIT;\n+\tchar parent_tree_oid[GIT_MAX_HEXSZ + 1];\n+\tsize_t binary_count = 0;\n+\tstruct commit *commit;\n+\tint ret;\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+\tstrbuf_addf(&parent_revision, \"%s~\", revision);\n+\tmode.diff_cmd[0] = \"diff-tree\";\n+\tmode.diff_cmd[1] = \"-r\";\n+\tmode.diff_cmd[2] = parent_tree_oid;\n+\n+\tinteractive_config_init(&s.cfg, r, opts);\n+\n+\tif (parse_diff(&s, ps) < 0) {\n+\t\tret = -1;\n+\t\tgoto out;\n+\t}\n+\n+\tfor (size_t i = 0; i < s.file_diff_nr; i++) {\n+\t\tif (s.file_diff[i].binary && !s.file_diff[i].hunk_nr)\n+\t\t\tbinary_count++;\n+\t\telse if (patch_update_file(&s, s.file_diff + i))\n+\t\t\tbreak;\n+\t}\n+\n+\tif (s.file_diff_nr == 0) {\n+\t\terr(&s, _(\"No changes.\"));\n+\t\tret = -1;\n+\t\tgoto out;\n+\t}\n+\n+\tif (binary_count == s.file_diff_nr) {\n+\t\terr(&s, _(\"Only binary files changed.\"));\n+\t\tret = -1;\n+\t\tgoto out;\n+\t}\n+\n+\tret = 0;\n+\n+out:\n+\tstrbuf_release(&parent_revision);\n+\tadd_p_state_clear(&s);\n+\treturn ret;\n+}\ndiff --git a/add-patch.h b/add-patch.h\nindex a4a05d9d145..901c42fd7b6 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@@ -53,4 +54,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.51.1.851.g4ebd6896fd.dirty\n\n"},{"id":"529292","messageId":"20251021-b4-pks-history-builtin-v5-11-78d23f578fe6@pks.im","threadId":"63986","inReplyTo":"20251021-b4-pks-history-builtin-v5-0-78d23f578fe6@pks.im","subject":"[PATCH v5 11/12] cache-tree: allow writing in-memory index as tree","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-21T14:15:59Z","receivedAt":"2025-10-21T14:16:29Z","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       | 5 ++---\n cache-tree.h       | 3 ++-\n 3 files changed, 6 insertions(+), 5 deletions(-)\n\ndiff --git a/builtin/checkout.c b/builtin/checkout.c\nindex d230b1f8995..0b90f398feb 100644\n--- a/builtin/checkout.c\n+++ b/builtin/checkout.c\n@@ -902,7 +902,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 2aba47060e9..b67d0d703d2 100644\n--- a/cache-tree.c\n+++ b/cache-tree.c\n@@ -699,11 +699,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 \n@@ -723,7 +723,6 @@ struct tree* write_in_core_index_as_tree(struct repository *repo) {\n \treturn lookup_tree(repo, &index_state->cache_tree->oid);\n }\n \n-\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 {\n \tint entries, was_valid;\ndiff --git a/cache-tree.h b/cache-tree.h\nindex b82c4963e7c..f8bddae5235 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.51.1.851.g4ebd6896fd.dirty\n\n"},{"id":"529293","messageId":"20251021-b4-pks-history-builtin-v5-12-78d23f578fe6@pks.im","threadId":"63986","inReplyTo":"20251021-b4-pks-history-builtin-v5-0-78d23f578fe6@pks.im","subject":"[PATCH v5 12/12] builtin/history: implement \"split\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-21T14:16:00Z","receivedAt":"2025-10-21T14:16:32Z","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              | 222 ++++++++++++++++++++\n t/meson.build                  |   1 +\n t/t3452-history-split.sh       | 447 +++++++++++++++++++++++++++++++++++++++++\n 4 files changed, 732 insertions(+)\n\ndiff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc\nindex 6f016801936..d741caf8347 100644\n--- a/Documentation/git-history.adoc\n+++ b/Documentation/git-history.adoc\n@@ -10,6 +10,7 @@ SYNOPSIS\n [synopsis]\n git history [<options>]\n git history reword [<options>] <commit>\n+git history split [<options>] <commit> [--] [<pathspec>...]\n \n DESCRIPTION\n -----------\n@@ -40,6 +41,26 @@ rewrite history in different ways:\n \tprovided, then this command will spawn an editor with the current\n \tmessage of that commit.\n \n+`split [--message=<message>] <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 message of the new commit will be asked for by launching the\n+configured editor, unless it has been specified with the `-m` option.\n+Authorship of the commit will be the same as for the 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 CONFIGURATION\n -------------\n \n@@ -47,6 +68,47 @@ include::includes/cmd-config-section-all.adoc[]\n \n include::config/sequencer.adoc[]\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 --message=\"split-out commit\"\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,e,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,e,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 f14e88c9bf4..6ca4382c8e2 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-reach.h\"\n #include \"commit.h\"\n #include \"config.h\"\n@@ -8,17 +9,22 @@\n #include \"environment.h\"\n #include \"gettext.h\"\n #include \"hex.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 \"reset.h\"\n #include \"revision.h\"\n+#include \"run-command.h\"\n #include \"sequencer.h\"\n #include \"strvec.h\"\n #include \"tree.h\"\n #include \"wt-status.h\"\n \n #define GIT_HISTORY_REWORD_USAGE N_(\"git history reword [<options>] <commit>\")\n+#define GIT_HISTORY_SPLIT_USAGE  N_(\"git history split [<options>] <commit> [--] [<pathspec>...]\")\n \n static int collect_commits(struct repository *repo,\n \t\t\t   struct commit *old_commit,\n@@ -357,6 +363,220 @@ static int cmd_history_reword(int argc,\n \treturn ret;\n }\n \n+static int split_commit(struct repository *repo,\n+\t\t\tstruct commit *original_commit,\n+\t\t\tstruct pathspec *pathspec,\n+\t\t\tconst char *commit_message,\n+\t\t\tstruct object_id *out)\n+{\n+\tstruct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;\n+\tstruct strbuf index_file = STRBUF_INIT, split_message = STRBUF_INIT;\n+\tstruct child_process read_tree_cmd = CHILD_PROCESS_INIT;\n+\tstruct index_state index = INDEX_STATE_INIT(repo);\n+\tstruct object_id original_commit_tree_oid, parent_tree_oid;\n+\tconst char *original_message, *original_body, *ptr;\n+\tchar original_commit_oid[GIT_MAX_HEXSZ + 1];\n+\tchar *original_author = NULL;\n+\tstruct commit_list *parents = NULL;\n+\tstruct commit *first_commit;\n+\tstruct tree *split_tree;\n+\tsize_t len;\n+\tint ret;\n+\n+\tif (original_commit->parents)\n+\t\tparent_tree_oid = *get_commit_tree_oid(original_commit->parents->item);\n+\telse\n+\t\toidcpy(&parent_tree_oid, repo->hash_algo->empty_tree);\n+\toriginal_commit_tree_oid = *get_commit_tree_oid(original_commit);\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+\tread_tree_cmd.git_cmd = 1;\n+\tstrvec_pushf(&read_tree_cmd.env, \"GIT_INDEX_FILE=%s\", index_file.buf);\n+\tstrvec_push(&read_tree_cmd.args, \"read-tree\");\n+\tstrvec_push(&read_tree_cmd.args, oid_to_hex(&parent_tree_oid));\n+\tret = run_command(&read_tree_cmd);\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_commit->object.oid);\n+\tret = run_add_p_index(repo, &index, index_file.buf, &interactive_opts,\n+\t\t\t      original_commit_oid, pathspec);\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+\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/* We retain authorship of the original commit. */\n+\toriginal_message = repo_logmsg_reencode(repo, original_commit, NULL, NULL);\n+\tptr = find_commit_header(original_message, \"author\", &len);\n+\tif (ptr)\n+\t\toriginal_author = xmemdupz(ptr, len);\n+\n+\tret = fill_commit_message(repo, &parent_tree_oid, &split_tree->object.oid,\n+\t\t\t\t  \"\", commit_message, \"split-out\", &split_message);\n+\tif (ret < 0)\n+\t\tgoto out;\n+\n+\tret = commit_tree(split_message.buf, split_message.len, &split_tree->object.oid,\n+\t\t\t  original_commit->parents, &out[0], original_author, NULL);\n+\tif (ret < 0) {\n+\t\tret = error(_(\"failed writing split-out commit\"));\n+\t\tgoto out;\n+\t}\n+\n+\t/*\n+\t* The second commit is much simpler to construct, as we can simply use\n+\t* the original commit details, except that we adjust its parent to be\n+\t* the newly split-out commit.\n+\t*/\n+\tfind_commit_subject(original_message, &original_body);\n+\tfirst_commit = lookup_commit_reference(repo, &out[0]);\n+\tcommit_list_append(first_commit, &parents);\n+\n+\tret = commit_tree(original_body, strlen(original_body), &original_commit_tree_oid,\n+\t\t\t  parents, &out[1], original_author, NULL);\n+\tif (ret < 0) {\n+\t\tret = error(_(\"failed writing second commit\"));\n+\t\tgoto out;\n+\t}\n+\n+\tret = 0;\n+\n+out:\n+\tif (index_file.len)\n+\t\tunlink(index_file.buf);\n+\tstrbuf_release(&split_message);\n+\tstrbuf_release(&index_file);\n+\tfree_commit_list(parents);\n+\tfree(original_author);\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+\tconst char *commit_message = NULL;\n+\tstruct option options[] = {\n+\t\tOPT_STRING('m', \"message\", &commit_message, N_(\"message\"), N_(\"commit message\")),\n+\t\tOPT_END(),\n+\t};\n+\tstruct oidmap rewritten_commits = OIDMAP_INIT;\n+\tstruct commit *original_commit, *parent, *head;\n+\tstruct strvec commits = STRVEC_INIT;\n+\tstruct commit_list *from_list = NULL;\n+\tstruct object_id split_commits[2];\n+\tstruct pathspec pathspec = { 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 revision\"));\n+\t\tgoto out;\n+\t}\n+\trepo_config(repo, git_default_config, NULL);\n+\n+\toriginal_commit = lookup_commit_reference_by_name(argv[0]);\n+\tif (!original_commit) {\n+\t\tret = error(_(\"commit to be split cannot be found: %s\"), argv[0]);\n+\t\tgoto out;\n+\t}\n+\n+\tparent = original_commit->parents ? original_commit->parents->item : NULL;\n+\tif (parent && repo_parse_commit(repo, parent)) {\n+\t\tret = error(_(\"unable to parse commit %s\"),\n+\t\t\t    oid_to_hex(&parent->object.oid));\n+\t\tgoto out;\n+\t}\n+\n+\thead = lookup_commit_reference_by_name(\"HEAD\");\n+\tif (!head) {\n+\t\tret = error(_(\"could not resolve HEAD to a commit\"));\n+\t\tgoto out;\n+\t}\n+\n+\tcommit_list_append(original_commit, &from_list);\n+\tif (!repo_is_descendant_of(repo, head, from_list)) {\n+\t\tret = error(_(\"split commit must be reachable from current HEAD commit\"));\n+\t\tgoto out;\n+\t}\n+\n+\tparse_pathspec(&pathspec, 0,\n+\t\tPATHSPEC_PREFER_FULL | PATHSPEC_SYMLINK_LEADING_PATH | PATHSPEC_PREFIX_ORIGIN,\n+\t\tprefix, argv + 1);\n+\n+\t/*\n+\t * Collect the list of commits that we'll have to reapply now already.\n+\t * This ensures that we'll abort early on in case the range of commits\n+\t * contains merges, which we do not yet handle.\n+\t */\n+\tret = collect_commits(repo, parent, head, &commits);\n+\tif (ret < 0)\n+\t\tgoto out;\n+\n+\t/*\n+\t * Then we split up the commit and replace the original commit with the\n+\t * new ones.\n+\t */\n+\tret = split_commit(repo, original_commit, &pathspec,\n+\t\t\t   commit_message, split_commits);\n+\tif (ret < 0)\n+\t\tgoto out;\n+\n+\treplace_commits(&commits, &original_commit->object.oid,\n+\t\t\tsplit_commits, ARRAY_SIZE(split_commits));\n+\n+\tret = apply_commits(repo, &commits, parent, head, \"split\");\n+\tif (ret < 0)\n+\t\tgoto out;\n+\n+\tret = 0;\n+\n+out:\n+\toidmap_clear(&rewritten_commits, 0);\n+\tfree_commit_list(from_list);\n+\tclear_pathspec(&pathspec);\n+\tstrvec_clear(&commits);\n+\treturn ret;\n+}\n+\n int cmd_history(int argc,\n \t\tconst char **argv,\n \t\tconst char *prefix,\n@@ -365,11 +585,13 @@ int cmd_history(int argc,\n \tconst char * const usage[] = {\n \t\tN_(\"git history [<options>]\"),\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 a3ec9199947..5d3014a768f 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -386,6 +386,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 00000000000..d9874c61f62\n--- /dev/null\n+++ b/t/t3452-history-split.sh\n@@ -0,0 +1,447 @@\n+#!/bin/sh\n+\n+test_description='tests for git-history split subcommand'\n+\n+. ./test-lib.sh\n+\n+set_fake_editor () {\n+\twrite_script fake-editor.sh <<-\\EOF &&\n+\techo \"split-out commit\" >\"$1\"\n+\tEOF\n+\ttest_set_editor \"$(pwd)\"/fake-editor.sh\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 rearrange commit history with merges\" err &&\n+\t\ttest_must_fail git history split HEAD~ 2>err &&\n+\t\ttest_grep \"cannot rearrange commit history with merges\" err\n+\t)\n+'\n+\n+test_expect_success 'refuses to work with unrelated 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\ttest_must_fail git history split ours 2>err &&\n+\t\ttest_grep \"split commit must be reachable from current HEAD commit\" err\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 &&\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\tsplit-me\n+\t\tsplit-out commit\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+\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 &&\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\troot\n+\t\tsplit-out commit\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 &&\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\tsplit-me\n+\t\tsplit-out commit\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 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\tgit history split HEAD -m \"split-out commit\" <<-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+\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\tgit history split HEAD -m \"split-out commit\" <<-EOF &&\n+\t\tn\n+\t\ty\n+\t\tEOF\n+\n+\t\texpect_log <<-EOF &&\n+\t\tsplit-me\n+\t\tsplit-out commit\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 '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\ttest_must_fail git history split HEAD -m \"\" <<-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 'can specify message via option' '\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\tgit history split HEAD -m \"message option\" <<-EOF &&\n+\t\ty\n+\t\tn\n+\t\tEOF\n+\n+\t\texpect_log <<-EOF\n+\t\tsplit-me\n+\t\tmessage option\n+\t\tEOF\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\tcp \"$1\" . &&\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\tcat >expect <<-EOF &&\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.\n+\t\t# Changes to be committed:\n+\t\t#\tnew file:   bar\n+\t\t#\n+\t\tEOF\n+\t\ttest_cmp expect COMMIT_EDITMSG &&\n+\n+\t\texpect_log <<-EOF\n+\t\tsplit-me\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\tgit history split HEAD -m \"message option\" -- 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 '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 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\twrite_script .git/hooks/prepare-commit-msg <<-EOF &&\n+\t\ttouch \"$(pwd)/hooks.log\"\n+\t\tEOF\n+\t\twrite_script .git/hooks/post-commit <<-EOF &&\n+\t\ttouch \"$(pwd)/hooks.log\"\n+\t\tEOF\n+\t\twrite_script .git/hooks/post-rewrite <<-EOF &&\n+\t\ttouch \"$(pwd)/hooks.log\"\n+\t\tEOF\n+\n+\t\tset_fake_editor &&\n+\t\tgit history split HEAD <<-EOF &&\n+\t\ty\n+\t\tn\n+\t\tEOF\n+\n+\t\texpect_log <<-EOF &&\n+\t\tsplit-me\n+\t\tsplit-out commit\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\tgit history split HEAD -m a-only <<-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\tEOF\n+\t\tgit status --porcelain >actual &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+test_done\n\n-- \n2.51.1.851.g4ebd6896fd.dirty\n\n"},{"id":"529319","messageId":"xmqqh5vsulwa.fsf@gitster.g","threadId":"63986","inReplyTo":"20251021-b4-pks-history-builtin-v5-0-78d23f578fe6@pks.im","subject":"Re: [PATCH v5 00/12] Introduce git-history(1) command for easy history editing","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-10-21T18:53:25Z","receivedAt":"2025-10-21T18:53:28Z","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> A copule of these features relate to history editing. Most importantly,\n> I really dig the following commands:\n>\n>   - jj-abandon(1) to drop a specific commit from your history.\n>\n>   - jj-absorb(1) to take some changes and automatically apply them to\n>     commits in your history that last modified the respective hunks.\n>\n>   - jj-split(1) to split a commit into two.\n>\n>   - jj-new(1) to insert a new commit after or before a specific other\n>     commit.\n>\n> Not all of these commands can be ported directly into Git. jj-new(1) for\n> example doesn't really make a ton of sense for us, I'd claim. But some\n> of these commands _do_ make sense.\n>\n> This patch series is a starting point for such a command. I've\n> significantly slimmed it down from the first couple revisions now\n> following the discussions at the Contributor's Summit yesterday. This\n> was my intent anyway, as I already mentioned on the last iteration.\n\nWill replace.  The other topic this depends on seems to be almost\nready, hopefully, and the part this series depends on should not\nchange much, even though what is remaining may be a bit more than\nfinding where to squash the tip \"SQUASH\" fixup in.\n\nThanks.\n\n"},{"id":"529322","messageId":"xmqq8qh4uh1u.fsf@gitster.g","threadId":"63986","inReplyTo":"20251021-b4-pks-history-builtin-v5-1-78d23f578fe6@pks.im","subject":"Re: [PATCH v5 01/12] wt-status: provide function to expose status for trees","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-10-21T20:38:05Z","receivedAt":"2025-10-21T20:38:08Z","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> diff --git a/wt-status.c b/wt-status.c\n> index 8ffe6d3988f..b66edbfca6c 100644\n> --- a/wt-status.c\n> +++ b/wt-status.c\n> @@ -612,6 +612,30 @@ static void wt_status_collect_updated_cb(struct diff_queue_struct *q,\n>  \t}\n>  }\n>  \n> +void wt_status_collect_changes_trees(struct wt_status *s,\n> +\t\t\t\t     const struct object_id *old_treeish,\n> +\t\t\t\t     const struct object_id *new_treeish)\n> +{\n> +\tstruct diff_options opts = { 0 };\n> +\n> +\trepo_diff_setup(s->repo, &opts);\n> +\topts.output_format = DIFF_FORMAT_CALLBACK;\n> +\topts.format_callback = wt_status_collect_updated_cb;\n> +\topts.format_callback_data = s;\n> +\topts.detect_rename = s->detect_rename >= 0 ? s->detect_rename : opts.detect_rename;\n> +\topts.rename_limit = s->rename_limit >= 0 ? s->rename_limit : opts.rename_limit;\n> +\topts.rename_score = s->rename_score >= 0 ? s->rename_score : opts.rename_score;\n> +\topts.flags.recursive = 1;\n> +\tdiff_setup_done(&opts);\n\nThis is obviously modelled after collect_changes_index(), whose\ncallback this function reuses, except that the set up to prepare for\nrunning a diff is different from their way to compare between HEAD\n(or void, if root) with the index.  We also do not have to worry\nabout sparse checkout.  We do not have to worry about\nignore-submodule argument, either.\n\n> +\tdiff_tree_oid(old_treeish, new_treeish, \"\", &opts);\n> +\tdiffcore_std(&opts);\n> +\tdiff_flush(&opts);\n> +\twt_status_get_state(s->repo, &s->state, 0);\n> +\n> +\tdiff_free(&opts);\n> +}\n> +\n>  static void wt_status_collect_changes_worktree(struct wt_status *s)\n>  {\n>  \tstruct rev_info rev;\n> diff --git a/wt-status.h b/wt-status.h\n> index e40a27214a7..924d7a5fa99 100644\n> --- a/wt-status.h\n> +++ b/wt-status.h\n> @@ -153,6 +153,9 @@ void wt_status_add_cut_line(struct wt_status *s);\n> ...\n\nLet's add a comment to help callers of this function that the\nchanges going from \"old\" to \"new\" are recorded as if they are\n\"staged\" changes and recorded on the \"index\" side.  It can only be\ninferred from the use of collect_updated_cb, which was written to be\nused by wt_status_collect_changes_index(), and probably it is a bit\nbrutal to expect for casual readers to realize on their own.\n\n> +void wt_status_collect_changes_trees(struct wt_status *s,\n> +\t\t\t\t     const struct object_id *old_treeish,\n> +\t\t\t\t     const struct object_id *new_treeish);\n"},{"id":"529323","messageId":"xmqq4irsugvx.fsf@gitster.g","threadId":"63986","inReplyTo":"20251021-b4-pks-history-builtin-v5-2-78d23f578fe6@pks.im","subject":"Re: [PATCH v5 02/12] replay: extract logic to pick commits","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-10-21T20:41:38Z","receivedAt":"2025-10-21T20:41:41Z","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> We're about to add a new git-history(1) command that will reuse some of\n> the same infrastructure as git-replay(1). To prepare for this, extract\n> the logic to pick a commit into a new \"replay.c\" file so that it can be\n> shared between both commands.\n>\n> Rename the function to have a \"replay_\" prefix to clearly indicate its\n> subsystem.\n>\n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n>  Makefile         |   1 +\n>  builtin/replay.c | 110 ++--------------------------------------------------\n>  meson.build      |   1 +\n>  replay.c         | 115 +++++++++++++++++++++++++++++++++++++++++++++++++++++++\n>  replay.h         |  18 +++++++++\n>  5 files changed, 138 insertions(+), 107 deletions(-)\n\nA clean refactoring that --color-moved helps vastly in reading.  I\nwish all our patches are this easy ;-)\n"},{"id":"529324","messageId":"xmqqzf9kt1zo.fsf@gitster.g","threadId":"63986","inReplyTo":"20251021-b4-pks-history-builtin-v5-3-78d23f578fe6@pks.im","subject":"Re: [PATCH v5 03/12] replay: stop using `the_repository`","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-10-21T20:48:43Z","receivedAt":"2025-10-21T20:48:46Z","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> In `create_commit()` we're using `the_repository` even though we already\n> have a repository passed to use as an argument. Fix this.\n>\n> Note that we still cannot get rid of `USE_THE_REPOSITORY_VARIABLE`. This\n> is because we use `DEFAULT_ABBREV and `get_commit_output_encoding()`,\n> both of which are stored as global variables that can be modified via\n> the Git configuration.\n>\n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n>  replay.c | 2 +-\n>  1 file changed, 1 insertion(+), 1 deletion(-)\n\nObviously correct.\n\n>\n> diff --git a/replay.c b/replay.c\n> index e22ce399406..13d75d80543 100644\n> --- a/replay.c\n> +++ b/replay.c\n> @@ -62,7 +62,7 @@ static struct commit *create_commit(struct repository *repo,\n>  \tobj = parse_object(repo, &ret);\n>  \n>  out:\n> -\trepo_unuse_commit_buffer(the_repository, based_on, message);\n> +\trepo_unuse_commit_buffer(repo, based_on, message);\n>  \tfree_commit_extra_headers(extra);\n>  \tfree_commit_list(parents);\n>  \tstrbuf_release(&msg);\n"},{"id":"529325","messageId":"xmqqv7k8t1tt.fsf@gitster.g","threadId":"63986","inReplyTo":"20251021-b4-pks-history-builtin-v5-3-78d23f578fe6@pks.im","subject":"Re: [PATCH v5 03/12] replay: stop using `the_repository`","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-10-21T20:52:14Z","receivedAt":"2025-10-21T20:52:17Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":""},{"id":"529326","messageId":"xmqqo6q0t1kd.fsf@gitster.g","threadId":"63986","inReplyTo":"20251021-b4-pks-history-builtin-v5-4-78d23f578fe6@pks.im","subject":"Re: [PATCH v5 04/12] replay: parse commits before dereferencing them","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-10-21T20:57:54Z","receivedAt":"2025-10-21T20:57:56Z","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> When looking up a commit it may not be parsed yet. Callers that wish to\n> access the fields of `struct commit` have to call `repo_parse_commit()`\n> first so that it is guaranteed to be populated.\n>\n> We didn't yet care about doing so, because code paths that lead to\n> `pick_regular_commit()` in \"builtin/replay.c\" already implicitly parsed\n> the commits. But now that the function is exposed to outside callers\n> it's quite easy to get this wrong.\n>\n> Make the function easier to use by calling `repo_parse_commit()`.\n\nTwo-and-half obvious questions.\n\n * With this change, can we lose the parse-commit call(s) from\n   existing callers, or do the need to look at the in-core commit\n   object themselves before calling this function so they need to\n   have their parse-commit call(s) anyway?\n\n * Can new callers you plan to add decide without having an already\n   parsed \"pickme\" commit object if they want to call this function,\n   iow, can they decide to call or not to call this function without\n   looking at the members of the commit structure?\n\n * Are existing callers prepared to see NULL returned from this\n   function to signal an error?\n\n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n>  replay.c | 3 +++\n>  1 file changed, 3 insertions(+)\n>\n> diff --git a/replay.c b/replay.c\n> index 13d75d80543..c3628d2488b 100644\n> --- a/replay.c\n> +++ b/replay.c\n> @@ -90,6 +90,9 @@ struct commit *replay_pick_regular_commit(struct repository *repo,\n>  \tstruct commit *base, *replayed_base;\n>  \tstruct tree *pickme_tree, *base_tree;\n>  \n> +\tif (repo_parse_commit(repo, pickme))\n> +\t\treturn NULL;\n> +\n>  \tbase = pickme->parents->item;\n>  \treplayed_base = mapped_commit(replayed_commits, base, onto);\n"},{"id":"529328","messageId":"xmqqikg8t0rl.fsf@gitster.g","threadId":"63986","inReplyTo":"20251021-b4-pks-history-builtin-v5-5-78d23f578fe6@pks.im","subject":"Re: [PATCH v5 05/12] builtin: add new \"history\" command","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-10-21T21:15:10Z","receivedAt":"2025-10-21T21:15:13Z","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> diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc\n> new file mode 100644\n> index 00000000000..57560525a70\n> --- /dev/null\n> +++ b/Documentation/git-history.adoc\n> @@ -0,0 +1,45 @@\n> +git-history(1)\n> +==============\n> +\n> +NAME\n> +----\n> +git-history - EXPERIMENTAL: Rewrite history of the current branch\n>\n>\n> +SYNOPSIS\n> +--------\n> +[synopsis]\n> +git history [<options>]\n> +\n> +DESCRIPTION\n> +-----------\n\nWe would want to make sure that all experimental things identify\nthemselves in a similar way.\n\nThe way how replay identifies itself as experimental, which this\npatch is modeled after, is somewhat different from what is done by\nbackfill, for-each-repo, last-modified, and sparse-checkout\ncommands.\n\n> +Rewrite history by rearranging or modifying specific commits in the\n> +history.\n> +\n> +This command is similar to linkgit:git-rebase[1] and uses the same\n> +underlying machinery. You should use rebases if you want to reapply a range of\n> +commits onto a different base, or interactive rebases if you want to edit a\n> +range of commits.\n> +\n> +Note that this command does not (yet) work with histories that contain\n> +merges. You should use linkgit:git-rebase[1] with the `--rebase-merges`\n> +flag instead.\n> +\n> +THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.\n> +\n> +COMMANDS\n> +--------\n> +\n> +This command requires a subcommand. Several subcommands are available to\n> +rewrite history in different ways:\n\nLooking at \"refs\", \"repo\" and \"sparse-checkout\", none of them say\n\"requires a subcommand\", even though they do.  It would probably be\nobvious from the syntax, so drop the first sentence, perhaps?\n\nAnd \"subcommand\" -> \"command\" to match the section title.\n\nThe remainder of this step seems a bog standard \"here is how you add\nan empty shell for a new command\" and I didn't see anything fishy in\nit.\n\n\n"},{"id":"529329","messageId":"CALnO6CAj2Jynun7Ns5222FFevqEF3O7ACVEU-GzT6DqUSxQNjw@mail.gmail.com","threadId":"63986","inReplyTo":"aPdyBpuLfuYOLe7q@pks.im","subject":"Re: [PATCH v4 12/12] builtin/history: implement \"split\" subcommand","fromName":"D. Ben Knoble","fromEmail":"ben.knoble@gmail.com","sentAt":"2025-10-21T21:19:19Z","receivedAt":"2025-10-21T21:19:32Z","isPatch":true,"sender":{"key":"ben.knoble@gmail.com","avatar":"https://avatars.githubusercontent.com/u/22802209?v=4"},"body":"On Tue, Oct 21, 2025 at 7:44 AM Patrick Steinhardt <ps@pks.im> wrote:\n>\n> On Tue, Oct 14, 2025 at 09:38:51AM -0400, Karthik Nayak wrote:\n> > Patrick Steinhardt <ps@pks.im> writes:\n> > > diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc\n> > > index b55babe206..83d675afea 100644\n> > > --- a/Documentation/git-history.adoc\n> > > +++ b/Documentation/git-history.adoc\n> > > @@ -40,6 +41,26 @@ rewrite history in different ways:\n> > >     provided, then this command will spawn an editor with the current\n> > >     message of that commit.\n> > >\n> > > +`split [--message=<message>] <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> >\n> > So in essence we do this:\n> >\n> >   Before split:\n> >   P1 ── C0 ── C1 ── ... ── CN\n> >         └─(target)         └─(HEAD)\n> >\n> >   After split:\n> >   P1 ── S0 ── C0' ── C1 ── ...... ── CN\n> >         │     └─(modified original)  └─(HEAD)\n> >         └─(split-out hunks)\n> >\n> > I do wonder if S0 should contain the existing message and the new\n> > message should go to C0'. So perhaps more like\n> >\n> >   After split:\n> >   P1 ── C0' ── S0 ── C1 ── ..... ── CN\n> >         │      └─(split-out hunks)  └─(HEAD)\n> >         └─(modified original)\n> >\n> > Mostly because when you say split, I would assume we keep the original\n> > as is and add on top of it. I don't really have a strong argument though\n> > :)\n>\n> Yeah, this has already caused some discussion beforehand. I guess you\n> can argue either way, and the suggestion from others was to simply allow\n> the user to edit both commit messages.\n>\n> I don't at all mind going into that direction, but I wonder how to call\n> the \"--message\" switch in that case. We could of course just call these\n> \"--first-message\" and \"--second-message\", but that feels somewhat\n> awkward.\n>\n> Also, I already have it in my mind that it would be cool to extend this\n> command so that you can split into arbitrary many commits. That is,\n> after you have split out the first commit we simply go back into\n> interactive mode to create a second commit tree. Rinse and repeat until\n> we have no chunks left anymore. But if we had such a mode though, then\n> numbered parameters don't make much sense anymore.\n>\n> An alternative could be to just accept multiple \"-m\" arguments, and we\n> then apply the messages to the respective commits? Dunno.\n\nOr *gasp* not support \"-m\" at all, and require the user to put\n_something_ in an editor? 🤔\n\n-- \nD. Ben Knoble\n"},{"id":"529332","messageId":"xmqqa51kszvc.fsf@gitster.g","threadId":"63986","inReplyTo":"20251021-b4-pks-history-builtin-v5-6-78d23f578fe6@pks.im","subject":"Re: [PATCH v5 06/12] builtin/history: implement \"reword\" subcommand","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-10-21T21:34:31Z","receivedAt":"2025-10-21T21:34:34Z","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> Implement a new \"reword\" subcommand for git-history(1). This subcommand\n> is essentially the same as if a user performed an interactive rebase\n> with a single commit changed to use the \"reword\" verb.\n\nOy.  I've always wanted something like this in \"rebase -i\" myself.\n\nIt is a bit sad that I have to learn a new command to do something\nobvious and trivial like this, but that's life ;-)\n\nMaybe \"git history\" becomes powerful enough and can replace my\nevery-day use of \"rebase -i\".\n\n> @@ -9,6 +9,7 @@ SYNOPSIS\n>  --------\n>  [synopsis]\n>  git history [<options>]\n> +git history reword [<options>] <commit>\n\nIt is curious that a command-less form is still listed here,\nespecially since this command \"requires\" a subcommand.  I would have\nexpected that there will be a single line here after implementing a\nsingle subcommand.\n\n> +`reword <commit> [--message=<message>]`::\n\nThat should be `reword [--message=<message>] <commit>` no?\n\n> +\tRewrite the commit message of the specified commit. All the other\n> +\tdetails of this commit remain unchanged. If no commit message is\n> +\tprovided, then this command will spawn an editor with the current\n> +\tmessage of that commit.\n\nAs long as it takes more than one -m and concatenates them just like\n\"git commit -m <message1> -m <message2>\" does, I would not complain\ntoo much that a command line option to give message encourages sloppy\nlog messages.\n\n> +\t\tif (!onto) {\n> +\t\t\tonto = commit;\n> +\t\t\tresult.clean = 1;\n> +\t\t\tresult.tree = repo_get_commit_tree(repo, commit);\n> +\t\t} else {\n> +\t\t\tonto = replay_pick_regular_commit(repo, commit, replayed_commits,\n> +\t\t\t\t\t\t\t  onto, &merge_opts, &result);\n> +\t\t\tif (!onto)\n> +\t\t\t\tbreak;\n> +\t\t}\n\nHmph, I would have expected that the overall flow of this command\nwould be\n\n * find the commits above and including the <commit> in question,\n   making sure there is no merge.\n\n * read metadata of <commit> like the parent (as we do not allow\n   merges), tree, author ident & time.\n\n * create a new commit object that has the same metadata as <commit>\n   on top of the parent of <commit>, but with the updated message\n   and new committer ident & time.\n\n * initialize a variable Current to point at the rewritten <commit>\n\n * loop for each commit C in <commit>..HEAD range in reverse order\n   (we know we have a single strand of pearls):\n\n   - read metadata of C\n\n   - create a new commit object C' that has the same metadata and\n     message as C on top of the Current commit, with new committer\n     ident & time.\n\n   - make Current point at the resulting C'\n\n * Point the Current with HEAD.\n\nwithout having to touch any \"pick\" machinery.  Why do we need to go\ndown to the merge machinery for a mere \"reword\" operation?\n"},{"id":"529336","messageId":"CALnO6CBT+5i==AtF-_xEgp9nEUEZY2G4DSAsSL9dysxr8A-WfA@mail.gmail.com","threadId":"63986","inReplyTo":"xmqqa51kszvc.fsf@gitster.g","subject":"Re: [PATCH v5 06/12] builtin/history: implement \"reword\" subcommand","fromName":"D. Ben Knoble","fromEmail":"ben.knoble@gmail.com","sentAt":"2025-10-21T21:43:16Z","receivedAt":"2025-10-21T21:43:29Z","isPatch":true,"sender":{"key":"ben.knoble@gmail.com","avatar":"https://avatars.githubusercontent.com/u/22802209?v=4"},"body":"On Tue, Oct 21, 2025 at 5:34 PM Junio C Hamano <gitster@pobox.com> wrote:\n>\n> Patrick Steinhardt <ps@pks.im> writes:\n>\n> > Implement a new \"reword\" subcommand for git-history(1). This subcommand\n> > is essentially the same as if a user performed an interactive rebase\n> > with a single commit changed to use the \"reword\" verb.\n>\n> Oy.  I've always wanted something like this in \"rebase -i\" myself.\n>\n> It is a bit sad that I have to learn a new command to do something\n> obvious and trivial like this, but that's life ;-)\n>\n> Maybe \"git history\" becomes powerful enough and can replace my\n> every-day use of \"rebase -i\".\n>\n> > @@ -9,6 +9,7 @@ SYNOPSIS\n> >  --------\n> >  [synopsis]\n> >  git history [<options>]\n> > +git history reword [<options>] <commit>\n>\n> It is curious that a command-less form is still listed here,\n> especially since this command \"requires\" a subcommand.  I would have\n> expected that there will be a single line here after implementing a\n> single subcommand.\n>\n> > +`reword <commit> [--message=<message>]`::\n>\n> That should be `reword [--message=<message>] <commit>` no?\n>\n> > +     Rewrite the commit message of the specified commit. All the other\n> > +     details of this commit remain unchanged. If no commit message is\n> > +     provided, then this command will spawn an editor with the current\n> > +     message of that commit.\n>\n> As long as it takes more than one -m and concatenates them just like\n> \"git commit -m <message1> -m <message2>\" does, I would not complain\n> too much that a command line option to give message encourages sloppy\n> log messages.\n>\n> > +             if (!onto) {\n> > +                     onto = commit;\n> > +                     result.clean = 1;\n> > +                     result.tree = repo_get_commit_tree(repo, commit);\n> > +             } else {\n> > +                     onto = replay_pick_regular_commit(repo, commit, replayed_commits,\n> > +                                                       onto, &merge_opts, &result);\n> > +                     if (!onto)\n> > +                             break;\n> > +             }\n>\n> Hmph, I would have expected that the overall flow of this command\n> would be\n>\n>  * find the commits above and including the <commit> in question,\n>    making sure there is no merge.\n\nI don't remember offhand if the implementation supports merges, so\nthis might not answer the question…\n\n> without having to touch any \"pick\" machinery.  Why do we need to go\n> down to the merge machinery for a mere \"reword\" operation?\n\n…but it would be nice to not overly restrict the commits that can be\nreworded (IIRC, jj permits the equivalent).\n\n-- \nD. Ben Knoble\n"},{"id":"529346","messageId":"xmqqldl31uhq.fsf@gitster.g","threadId":"63986","inReplyTo":"CAOLa=ZQSc0jHv9gK5T06GA_mprknucwCj5H_M2djZ-UWfRMbJQ@mail.gmail.com","subject":"Re: [PATCH v4 05/12] builtin: add new \"history\" command","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-10-22T03:32:49Z","receivedAt":"2025-10-22T03:32:52Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Karthik Nayak <karthik.188@gmail.com> writes:\n\n>> +\tconst char * const usage[] = {\n>> +\t\tN_(\"git history [<options>]\"),\n>> +\t\tNULL,\n>> +\t};\n>\n> Nit: We have pointer alignment set to 'Right' in our styling guide and\n> also mentioned in our 'Documentation/CodingGuidelines'\n>\n>    When declaring pointers, the star sides with the variable\n>    name, i.e. \"char *string\", not \"char* string\" or\n>    \"char * string\".  This makes it easier to understand code\n>    like \"char *string, c;\".\n\nBut there is nothing specified for an asterisk that cannot side with\nvariable name, like the one we see above.  I _think_ the \"space on\nboth sides\" is the prevalent style, but I do not know (although I\nsuspect you do---as the person with most changes in it) what (y)our\nclang format configuration wants to do.  Can you make sure the tool\nsuggests the style that matches the prevailing style?\n\nThanks.\n"},{"id":"529409","messageId":"CAOLa=ZTHxw8uCbo=oHq=LF=qX=sufnWKJTC6F2YLsrZ8EyxsYw@mail.gmail.com","threadId":"63986","inReplyTo":"xmqqldl31uhq.fsf@gitster.g","subject":"Re: [PATCH v4 05/12] builtin: add new \"history\" command","fromName":"Karthik Nayak","fromEmail":"karthik.188@gmail.com","sentAt":"2025-10-22T12:12:32Z","receivedAt":"2025-10-22T12:12:34Z","isPatch":true,"sender":{"key":"karthik.188@gmail.com","avatar":"https://avatars.githubusercontent.com/u/1786334?v=4"},"body":"Junio C Hamano <gitster@pobox.com> writes:\n\n> Karthik Nayak <karthik.188@gmail.com> writes:\n>\n>>> +\tconst char * const usage[] = {\n>>> +\t\tN_(\"git history [<options>]\"),\n>>> +\t\tNULL,\n>>> +\t};\n>>\n>> Nit: We have pointer alignment set to 'Right' in our styling guide and\n>> also mentioned in our 'Documentation/CodingGuidelines'\n>>\n>>    When declaring pointers, the star sides with the variable\n>>    name, i.e. \"char *string\", not \"char* string\" or\n>>    \"char * string\".  This makes it easier to understand code\n>>    like \"char *string, c;\".\n>\n> But there is nothing specified for an asterisk that cannot side with\n> variable name, like the one we see above.  I _think_ the \"space on\n> both sides\" is the prevalent style, but I do not know (although I\n> suspect you do---as the person with most changes in it) what (y)our\n> clang format configuration wants to do.  Can you make sure the tool\n> suggests the style that matches the prevailing style?\n>\n> Thanks.\n\nI looked into this, and unfortunately it [1] doesn't support such\ngranularity.\n\nSo for something like `const char * const usage`, it only cares about\nthe alignment of the pointer with respect to the tokens surrounding it.\n\nWith our current setting of `PointerAlignment: Right`, this means it\nwould expect to have `const char *const usage` which is not the\nprevalent style.\n\n[1]: https://clang.llvm.org/docs/ClangFormatStyleOptions.html#pointeralignment\n"},{"id":"529708","messageId":"aP9B8yd0JJL9nue8@pks.im","threadId":"63986","inReplyTo":"xmqqikg8t0rl.fsf@gitster.g","subject":"Re: [PATCH v5 05/12] builtin: add new \"history\" command","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-27T09:57:07Z","receivedAt":"2025-10-27T09:57:14Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Tue, Oct 21, 2025 at 02:15:10PM -0700, Junio C Hamano wrote:\n> Patrick Steinhardt <ps@pks.im> writes:\n> \n> > diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc\n> > new file mode 100644\n> > index 00000000000..57560525a70\n> > --- /dev/null\n> > +++ b/Documentation/git-history.adoc\n> > @@ -0,0 +1,45 @@\n> > +git-history(1)\n> > +==============\n> > +\n> > +NAME\n> > +----\n> > +git-history - EXPERIMENTAL: Rewrite history of the current branch\n> >\n> >\n> > +SYNOPSIS\n> > +--------\n> > +[synopsis]\n> > +git history [<options>]\n> > +\n> > +DESCRIPTION\n> > +-----------\n> \n> We would want to make sure that all experimental things identify\n> themselves in a similar way.\n> \n> The way how replay identifies itself as experimental, which this\n> patch is modeled after, is somewhat different from what is done by\n> backfill, for-each-repo, last-modified, and sparse-checkout\n> commands.\n\nI guess the only thing that's different with git-replay(1) is that we\nalso have the `(EXPERIMENTAL!)` tag in the synopsis. No other man page\ndoes that as far as I can see.\n\nBut yeah, I agree that things should be consistent here. I think the\nmost sensible thing to do is to:\n\n  - Have the \"EXPERIMENTAL:\" tag in the NAME section.\n\n  - Have \"THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.\" in the\n    DESCRIPTION section, ideally after the first summarizing sentence.\n\n> > +Rewrite history by rearranging or modifying specific commits in the\n> > +history.\n> > +\n> > +This command is similar to linkgit:git-rebase[1] and uses the same\n> > +underlying machinery. You should use rebases if you want to reapply a range of\n> > +commits onto a different base, or interactive rebases if you want to edit a\n> > +range of commits.\n> > +\n> > +Note that this command does not (yet) work with histories that contain\n> > +merges. You should use linkgit:git-rebase[1] with the `--rebase-merges`\n> > +flag instead.\n> > +\n> > +THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.\n> > +\n> > +COMMANDS\n> > +--------\n> > +\n> > +This command requires a subcommand. Several subcommands are available to\n> > +rewrite history in different ways:\n> \n> Looking at \"refs\", \"repo\" and \"sparse-checkout\", none of them say\n> \"requires a subcommand\", even though they do.  It would probably be\n> obvious from the syntax, so drop the first sentence, perhaps?\n> \n> And \"subcommand\" -> \"command\" to match the section title.\n\nMakes sense.\n\nThanks!\n\nPatrick\n"},{"id":"529709","messageId":"aP9CGN39LSaFU-Ru@pks.im","threadId":"63986","inReplyTo":"xmqqo6q0t1kd.fsf@gitster.g","subject":"Re: [PATCH v5 04/12] replay: parse commits before dereferencing them","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-27T09:57:44Z","receivedAt":"2025-10-27T09:57:50Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Tue, Oct 21, 2025 at 01:57:54PM -0700, Junio C Hamano wrote:\n> Patrick Steinhardt <ps@pks.im> writes:\n> \n> > When looking up a commit it may not be parsed yet. Callers that wish to\n> > access the fields of `struct commit` have to call `repo_parse_commit()`\n> > first so that it is guaranteed to be populated.\n> >\n> > We didn't yet care about doing so, because code paths that lead to\n> > `pick_regular_commit()` in \"builtin/replay.c\" already implicitly parsed\n> > the commits. But now that the function is exposed to outside callers\n> > it's quite easy to get this wrong.\n> >\n> > Make the function easier to use by calling `repo_parse_commit()`.\n> \n> Two-and-half obvious questions.\n> \n>  * With this change, can we lose the parse-commit call(s) from\n>    existing callers, or do the need to look at the in-core commit\n>    object themselves before calling this function so they need to\n>    have their parse-commit call(s) anyway?\n\nThere's only a single caller in \"builtin/replay.c\", and that caller\nparses commits deep inside the callstack via `prepare_revision_walk()`.\nSo we cannot easily get rid of any calls.\n\n>  * Can new callers you plan to add decide without having an already\n>    parsed \"pickme\" commit object if they want to call this function,\n>    iow, can they decide to call or not to call this function without\n>    looking at the members of the commit structure?\n\nNot quite sure I understand this question. In any case, I was hitting\nsegfaults in tests when I didn't have this call. But your questions made\nme double-check this now, and I cannot see any of these failures\nanymore. And we do use the same infra to pick commits as\n\"builtin/replay.c\" does, so things should work alright.\n\nLet me drop this commit for now. Things work without it, and in theory\nthey should. And now that I'm revamping the infra to not use the merge\nmachinery in the first place we don't even hit this code path anymore.\n\nThanks!\n\nPatrick\n"},{"id":"529710","messageId":"aP9CMa3dDuV_RGnG@pks.im","threadId":"63986","inReplyTo":"xmqqa51kszvc.fsf@gitster.g","subject":"Re: [PATCH v5 06/12] builtin/history: implement \"reword\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-27T09:58:09Z","receivedAt":"2025-10-27T09:58:16Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Tue, Oct 21, 2025 at 02:34:31PM -0700, Junio C Hamano wrote:\n> Patrick Steinhardt <ps@pks.im> writes:\n> > @@ -9,6 +9,7 @@ SYNOPSIS\n> >  --------\n> >  [synopsis]\n> >  git history [<options>]\n> > +git history reword [<options>] <commit>\n> \n> It is curious that a command-less form is still listed here,\n> especially since this command \"requires\" a subcommand.  I would have\n> expected that there will be a single line here after implementing a\n> single subcommand.\n\nTrue. You can execute `git history` alone, but it doesn't do anything\nuseful except for giving you an error and all the potential subcommands.\nThat's in contrast to e.g. git-reflog(1), which supports a command-less\nmode that does something useful.\n\n> > +`reword <commit> [--message=<message>]`::\n> \n> That should be `reword [--message=<message>] <commit>` no?\n\nIndeed.\n\n> > +\tRewrite the commit message of the specified commit. All the other\n> > +\tdetails of this commit remain unchanged. If no commit message is\n> > +\tprovided, then this command will spawn an editor with the current\n> > +\tmessage of that commit.\n> \n> As long as it takes more than one -m and concatenates them just like\n> \"git commit -m <message1> -m <message2>\" does, I would not complain\n> too much that a command line option to give message encourages sloppy\n> log messages.\n\nI'll for now defer the discussion around \"-m\" completely, as the design\nisn't entirely clear yet in the first place. So I'll just drop the\noption in the next iteration.\n\n> > +\t\tif (!onto) {\n> > +\t\t\tonto = commit;\n> > +\t\t\tresult.clean = 1;\n> > +\t\t\tresult.tree = repo_get_commit_tree(repo, commit);\n> > +\t\t} else {\n> > +\t\t\tonto = replay_pick_regular_commit(repo, commit, replayed_commits,\n> > +\t\t\t\t\t\t\t  onto, &merge_opts, &result);\n> > +\t\t\tif (!onto)\n> > +\t\t\t\tbreak;\n> > +\t\t}\n> \n> Hmph, I would have expected that the overall flow of this command\n> would be\n> \n>  * find the commits above and including the <commit> in question,\n>    making sure there is no merge.\n> \n>  * read metadata of <commit> like the parent (as we do not allow\n>    merges), tree, author ident & time.\n> \n>  * create a new commit object that has the same metadata as <commit>\n>    on top of the parent of <commit>, but with the updated message\n>    and new committer ident & time.\n> \n>  * initialize a variable Current to point at the rewritten <commit>\n> \n>  * loop for each commit C in <commit>..HEAD range in reverse order\n>    (we know we have a single strand of pearls):\n> \n>    - read metadata of C\n> \n>    - create a new commit object C' that has the same metadata and\n>      message as C on top of the Current commit, with new committer\n>      ident & time.\n> \n>    - make Current point at the resulting C'\n> \n>  * Point the Current with HEAD.\n> \n> without having to touch any \"pick\" machinery.  Why do we need to go\n> down to the merge machinery for a mere \"reword\" operation?\n\nYou're exactly right, we don't need the \"pick\" machinery at all right\nnow. I think this is still a leftover from previous iterations, where I\nwas also driving things like an \"edit\" command that _do_ require merges.\nBut neither splitting a commit nor rewording it does require a merge at\nall.\n\nWill simplify. We can reintroduce the heavier machinery at a later point\nin time as needed.\n\nThanks!\n\nPatrick\n"},{"id":"529711","messageId":"aP9COS96zn3yCRlp@pks.im","threadId":"63986","inReplyTo":"CALnO6CAj2Jynun7Ns5222FFevqEF3O7ACVEU-GzT6DqUSxQNjw@mail.gmail.com","subject":"Re: [PATCH v4 12/12] builtin/history: implement \"split\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-27T09:58:17Z","receivedAt":"2025-10-27T09:58:23Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Tue, Oct 21, 2025 at 05:19:19PM -0400, D. Ben Knoble wrote:\n> On Tue, Oct 21, 2025 at 7:44 AM Patrick Steinhardt <ps@pks.im> wrote:\n> > On Tue, Oct 14, 2025 at 09:38:51AM -0400, Karthik Nayak wrote:\n> > > Patrick Steinhardt <ps@pks.im> writes:\n> > > > diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc\n> > > > index b55babe206..83d675afea 100644\n> > > > --- a/Documentation/git-history.adoc\n> > > > +++ b/Documentation/git-history.adoc\n> > > > @@ -40,6 +41,26 @@ rewrite history in different ways:\n> > > >     provided, then this command will spawn an editor with the current\n> > > >     message of that commit.\n> > > >\n> > > > +`split [--message=<message>] <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> > >\n> > > So in essence we do this:\n> > >\n> > >   Before split:\n> > >   P1 ── C0 ── C1 ── ... ── CN\n> > >         └─(target)         └─(HEAD)\n> > >\n> > >   After split:\n> > >   P1 ── S0 ── C0' ── C1 ── ...... ── CN\n> > >         │     └─(modified original)  └─(HEAD)\n> > >         └─(split-out hunks)\n> > >\n> > > I do wonder if S0 should contain the existing message and the new\n> > > message should go to C0'. So perhaps more like\n> > >\n> > >   After split:\n> > >   P1 ── C0' ── S0 ── C1 ── ..... ── CN\n> > >         │      └─(split-out hunks)  └─(HEAD)\n> > >         └─(modified original)\n> > >\n> > > Mostly because when you say split, I would assume we keep the original\n> > > as is and add on top of it. I don't really have a strong argument though\n> > > :)\n> >\n> > Yeah, this has already caused some discussion beforehand. I guess you\n> > can argue either way, and the suggestion from others was to simply allow\n> > the user to edit both commit messages.\n> >\n> > I don't at all mind going into that direction, but I wonder how to call\n> > the \"--message\" switch in that case. We could of course just call these\n> > \"--first-message\" and \"--second-message\", but that feels somewhat\n> > awkward.\n> >\n> > Also, I already have it in my mind that it would be cool to extend this\n> > command so that you can split into arbitrary many commits. That is,\n> > after you have split out the first commit we simply go back into\n> > interactive mode to create a second commit tree. Rinse and repeat until\n> > we have no chunks left anymore. But if we had such a mode though, then\n> > numbered parameters don't make much sense anymore.\n> >\n> > An alternative could be to just accept multiple \"-m\" arguments, and we\n> > then apply the messages to the respective commits? Dunno.\n> \n> Or *gasp* not support \"-m\" at all, and require the user to put\n> _something_ in an editor? 🤔\n\nLet's do that for now and discuss in more detail in follow-up patch\nseries.\n\nThanks!\n\nPatrick\n"},{"id":"529712","messageId":"aP9CQyyWnyFGx3-Q@pks.im","threadId":"63986","inReplyTo":"CALnO6CBT+5i==AtF-_xEgp9nEUEZY2G4DSAsSL9dysxr8A-WfA@mail.gmail.com","subject":"Re: [PATCH v5 06/12] builtin/history: implement \"reword\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-27T09:58:27Z","receivedAt":"2025-10-27T09:58:33Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Tue, Oct 21, 2025 at 05:43:16PM -0400, D. Ben Knoble wrote:\n> On Tue, Oct 21, 2025 at 5:34 PM Junio C Hamano <gitster@pobox.com> wrote:\n> > Hmph, I would have expected that the overall flow of this command\n> > would be\n> >\n> >  * find the commits above and including the <commit> in question,\n> >    making sure there is no merge.\n> \n> I don't remember offhand if the implementation supports merges, so\n> this might not answer the question…\n> \n> > without having to touch any \"pick\" machinery.  Why do we need to go\n> > down to the merge machinery for a mere \"reword\" operation?\n> \n> …but it would be nice to not overly restrict the commits that can be\n> reworded (IIRC, jj permits the equivalent).\n\nI agree that it would be nice, but I'd defer that to the future. Let's\nfocus on the easy cases for now and then extend going forward.\n\nPatrick\n"},{"id":"529716","messageId":"20251027-b4-pks-history-builtin-v6-0-407dd3f57ad3@pks.im","threadId":"63986","inReplyTo":"20250819-b4-pks-history-builtin-v1-0-9b77c32688fe@pks.im","subject":"[PATCH v6 00/11] Introduce git-history(1) command for easy history editing","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-27T11:33:48Z","receivedAt":"2025-10-27T11:34:02Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Hi,\n\nover recent months I've been playing around with Jujutsu quite\nfrequently. While I still prefer using Git, there's been a couple\nfeatures in it that I really like and that I'd like to have in Git, as\nwell.\n\nA copule of these features relate to history editing. Most importantly,\nI really dig the following commands:\n\n  - jj-abandon(1) to drop a specific commit from your history.\n\n  - jj-absorb(1) to take some changes and automatically apply them to\n    commits in your history that last modified the respective hunks.\n\n  - jj-split(1) to split a commit into two.\n\n  - jj-new(1) to insert a new commit after or before a specific other\n    commit.\n\nNot all of these commands can be ported directly into Git. jj-new(1) for\nexample doesn't really make a ton of sense for us, I'd claim. But some\nof these commands _do_ make sense.\n\nThis patch series is a starting point for such a command. I've\nsignificantly slimmed it down from the first couple revisions now\nfollowing the discussions at the Contributor's Summit yesterday. This\nwas my intent anyway, as I already mentioned on the last iteration.\n\nChanges in v6:\n  - I've rebased the patch series again to pull in the latest updates\n    from sa/replay-atomic-ref-updates and fix conflicts. It is now based\n    on 4e98b730f1 (The twenty-fourth batch, 2025-10-24) with ab661bb1bb\n    (replay: add replay.refAction config option, 2025-10-23) merged into\n    it.\n  - I've dropped the \"-m\" options for now, so commit messages are always\n    asked for via the editor. These can be introduced in a subsequent\n    patch series once discussion around them has settled.\n  - We don't use the merge machinery anymore to pick the commits.\n  - Drop the commit to parse commits in the replay machinery. It didn't\n    seem to be necessary in v5 anymore, and now that we don't use the\n    merge machinery at all we don't ever take that code path in the\n    first place.\n  - Link to v5:\n    https://lore.kernel.org/r/20251021-b4-pks-history-builtin-v5-0-78d23f578fe6@pks.im\n\nChanges in v5:\n  - I've changed the patch series to be based on top of 133d151831 (The\n    twenty-first batch, 2025-10-20) with sa/replay-atomic-ref-updates at\n    a1c22e627e (SQAUASH??? t0450 band-aid, 2025-10-14) merged into it.\n    This is one the one hand to fix a conflict, but also to get some of\n    the CI updates to make GitLab CI work again.\n  - Some slight commit message improvements.\n  - Deduplicate subcommand usage strings by using defines.\n  - Fix the desendancy checks to properly verify that HEAD is a\n    descendant of the commit to be rewritten. Also add some tests for\n    this.\n  - Fix the hint that mentions that lines starting with the comment\n    character will be tripped after having written the commit message.\n  - Move an include to the correct commit.\n  - Link to v4: https://lore.kernel.org/r/20251001-b4-pks-history-builtin-v4-0-8e61ddb86317@pks.im\n\nChanges in v4:\n  - I've rebuilt the patch series. It is now based on 821f583da6 (The\n    thirteenth batcn, 2025-09-29) with sa/replay-atomic-ref-updates\n    at 665c66a743 (replay: make atomic ref updates the default behavior,\n    2025-09-27) merged into it. This should fix all conflicts with seen.\n  - I've reworked this patch series to use the same infra as\n    git-replay(1), as discussed during the Contributor's Summit.\n  - I've slimmed down the patch series to only tackle those commands\n    that cannot result in a conflict to keep it simple. I also learned\n    that Elijah has been working on a \"git replay edit\" command, so I\n    dropped that command so that we can instead use his version.\n  - During the Contributor's Summit we have agreed that for now, we\n    won't care about hook execution just yet. This may be backfilled at\n    a later point in time.\n  - I dropped \"commit.verbose\" handling for now, as my understanding of\n    it was wrong at first. This is something we should backfill.\n  - Link to v3: https://lore.kernel.org/r/20250904-b4-pks-history-builtin-v3-0-509053514755@pks.im\n\nChanges in v3:\n  - Add logic to drive the \"post-rewrite\" hook and add tests to verify\n    that all hooks are executed as expected.\n  - Deduplicate logic to turn a replay action into a todo command.\n  - Move the addition of tests for the top-level git-history(1) command\n    to the correct commit.\n  - Some smaller commit message fixes.\n  - Honor \"commit.verbose\".\n  - Fix copy-paste error with an error message.\n  - Link to v2: https://lore.kernel.org/r/20250824-b4-pks-history-builtin-v2-0-964ac12f65bd@pks.im\n\nChanges in v2:\n  - Add a new \"reword\" subcommand.\n  - List git-history(1) in \"command-list.txt\".\n  - Add some missing error handling.\n  - Simplify calling convention of `apply_commits()` to handle root\n    commits internally instead of requiring every caller to do so.\n  - Add tests to verify that git-history(1) refuses to work with changes\n    in the worktree or index.\n  - Mark git-history(1) as experimental.\n  - Introduce commands to manage interrupted history edits.\n  - A bunch of improvements to the manpage.\n  - Link to v1: https://lore.kernel.org/r/20250819-b4-pks-history-builtin-v1-0-9b77c32688fe@pks.im\n\nThanks!\n\nPatrick\n\n---\nPatrick Steinhardt (11):\n      wt-status: provide function to expose status for trees\n      replay: extract logic to pick commits\n      replay: stop using `the_repository`\n      builtin: add new \"history\" command\n      builtin/history: implement \"reword\" subcommand\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      cache-tree: allow writing in-memory index as tree\n      builtin/history: implement \"split\" subcommand\n\n .gitignore                     |   1 +\n Documentation/git-history.adoc | 111 ++++++++\n Documentation/meson.build      |   1 +\n Makefile                       |   2 +\n add-interactive.c              | 174 +++----------\n add-interactive.h              |  46 +---\n add-patch.c                    | 297 +++++++++++++++++++---\n add-patch.h                    |  64 +++++\n builtin.h                      |   1 +\n builtin/add.c                  |  22 +-\n builtin/checkout.c             |   7 +-\n builtin/commit.c               |  16 +-\n builtin/history.c              | 561 +++++++++++++++++++++++++++++++++++++++++\n builtin/replay.c               | 110 +-------\n builtin/reset.c                |  16 +-\n builtin/stash.c                |  46 ++--\n cache-tree.c                   |   5 +-\n cache-tree.h                   |   3 +-\n command-list.txt               |   1 +\n commit.h                       |   2 +-\n git.c                          |   1 +\n meson.build                    |   2 +\n replay.c                       | 115 +++++++++\n replay.h                       |  23 ++\n t/meson.build                  |   3 +\n t/t3450-history.sh             |  17 ++\n t/t3451-history-reword.sh      | 237 +++++++++++++++++\n t/t3452-history-split.sh       | 432 +++++++++++++++++++++++++++++++\n wt-status.c                    |  24 ++\n wt-status.h                    |   9 +\n 30 files changed, 1972 insertions(+), 377 deletions(-)\n\nRange-diff versus v5:\n\n 1:  c351cd9fb34 !  1:  7e32877a47c wt-status: provide function to expose status for trees\n    @@ wt-status.h: void wt_status_add_cut_line(struct wt_status *s);\n      void wt_status_prepare(struct repository *r, struct wt_status *s);\n      void wt_status_print(struct wt_status *s);\n      void wt_status_collect(struct wt_status *s);\n    ++\n    ++/*\n    ++ * Collect all changes between the two trees. Changes will be displayed as if\n    ++ * they were staged into the index.\n    ++ */\n     +void wt_status_collect_changes_trees(struct wt_status *s,\n     +\t\t\t\t     const struct object_id *old_treeish,\n     +\t\t\t\t     const struct object_id *new_treeish);\n    ++\n      /*\n       * Frees the buffers allocated by wt_status_collect.\n       */\n 2:  71266f8ed78 !  2:  3688a10ec38 replay: extract logic to pick commits\n    @@ builtin/replay.c\n      #include \"revision.h\"\n      #include \"strmap.h\"\n      #include <oidset.h>\n    - #include <tree.h>\n    +@@ builtin/replay.c: enum ref_action_mode {\n    + \tREF_ACTION_PRINT\n    + };\n      \n     -static const char *short_commit_name(struct repository *repo,\n     -\t\t\t\t     struct commit *commit)\n    @@ builtin/replay.c: static void determine_replay_mode(struct repository *repo,\n     -\treturn create_commit(repo, result->tree, pickme, replayed_base);\n     -}\n     -\n    - static int handle_ref_update(const char *mode,\n    + static int handle_ref_update(enum ref_action_mode mode,\n      \t\t\t     struct ref_transaction *transaction,\n      \t\t\t     const char *refname,\n     @@ builtin/replay.c: int cmd_replay(int argc,\n    @@ replay.c (new)\n     +\treturn NULL;\n     +}\n     +\n    -+static struct commit *create_commit(struct repository *repo,\n    ++struct commit *replay_create_commit(struct repository *repo,\n     +\t\t\t\t    struct tree *tree,\n     +\t\t\t\t    struct commit *based_on,\n     +\t\t\t\t    struct commit *parent)\n    @@ replay.c (new)\n     +\tmerge_opt->ancestor = NULL;\n     +\tif (!result->clean)\n     +\t\treturn NULL;\n    -+\treturn create_commit(repo, result->tree, pickme, replayed_base);\n    ++\treturn replay_create_commit(repo, result->tree, pickme, replayed_base);\n     +}\n     \n      ## replay.h (new) ##\n    @@ replay.h (new)\n     +struct commit;\n     +struct tree;\n     +\n    ++struct commit *replay_create_commit(struct repository *repo,\n    ++\t\t\t\t    struct tree *tree,\n    ++\t\t\t\t    struct commit *based_on,\n    ++\t\t\t\t    struct commit *parent);\n    ++\n     +struct commit *replay_pick_regular_commit(struct repository *repo,\n     +\t\t\t\t\t  struct commit *pickme,\n     +\t\t\t\t\t  kh_oid_map_t *replayed_commits,\n 3:  703fc59d4e1 !  3:  7a5217797a0 replay: stop using `the_repository`\n    @@ Commit message\n         Signed-off-by: Patrick Steinhardt <ps@pks.im>\n     \n      ## replay.c ##\n    -@@ replay.c: static struct commit *create_commit(struct repository *repo,\n    +@@ replay.c: struct commit *replay_create_commit(struct repository *repo,\n      \tobj = parse_object(repo, &ret);\n      \n      out:\n 4:  f18a8ac67e0 <  -:  ----------- replay: parse commits before dereferencing them\n 5:  65892f7da77 !  4:  bc435f62d87 builtin: add new \"history\" command\n    @@ Documentation/git-history.adoc (new)\n     +Rewrite history by rearranging or modifying specific commits in the\n     +history.\n     +\n    ++THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.\n    ++\n     +This command is similar to linkgit:git-rebase[1] and uses the same\n     +underlying machinery. You should use rebases if you want to reapply a range of\n     +commits onto a different base, or interactive rebases if you want to edit a\n    @@ Documentation/git-history.adoc (new)\n     +merges. You should use linkgit:git-rebase[1] with the `--rebase-merges`\n     +flag instead.\n     +\n    -+THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.\n    -+\n     +COMMANDS\n     +--------\n     +\n    -+This command requires a subcommand. Several subcommands are available to\n    -+rewrite history in different ways:\n    ++Several commands are available to rewrite history in different ways:\n     +\n     +CONFIGURATION\n     +-------------\n 6:  cefe1dbb847 !  5:  39a21dc2bbd builtin/history: implement \"reword\" subcommand\n    @@ Commit message\n         Signed-off-by: Patrick Steinhardt <ps@pks.im>\n     \n      ## Documentation/git-history.adoc ##\n    -@@ Documentation/git-history.adoc: SYNOPSIS\n    +@@ Documentation/git-history.adoc: git-history - EXPERIMENTAL: Rewrite history of the current branch\n    + SYNOPSIS\n      --------\n      [synopsis]\n    - git history [<options>]\n    -+git history reword [<options>] <commit>\n    +-git history [<options>]\n    ++git history reword <commit>\n      \n      DESCRIPTION\n      -----------\n     @@ Documentation/git-history.adoc: COMMANDS\n    - This command requires a subcommand. Several subcommands are available to\n    - rewrite history in different ways:\n      \n    -+`reword <commit> [--message=<message>]`::\n    + Several commands are available to rewrite history in different ways:\n    + \n    ++`reword <commit>`::\n     +\tRewrite the commit message of the specified commit. All the other\n    -+\tdetails of this commit remain unchanged. If no commit message is\n    -+\tprovided, then this command will spawn an editor with the current\n    -+\tmessage of that commit.\n    ++\tdetails of this commit remain unchanged. This command will spawn an\n    ++\teditor with the current message of that commit.\n     +\n      CONFIGURATION\n      -------------\n    @@ builtin/history.c\n     +#include \"tree.h\"\n     +#include \"wt-status.h\"\n     +\n    -+#define GIT_HISTORY_REWORD_USAGE N_(\"git history reword [<options>] <commit>\")\n    ++#define GIT_HISTORY_REWORD_USAGE N_(\"git history reword <commit>\")\n     +\n     +static int collect_commits(struct repository *repo,\n     +\t\t\t   struct commit *old_commit,\n    @@ builtin/history.c\n     +\t\t\t const char *action)\n     +{\n     +\tstruct reset_head_opts reset_opts = { 0 };\n    -+\tstruct merge_options merge_opts = { 0 };\n    -+\tstruct merge_result result = { 0 };\n     +\tstruct strbuf buf = STRBUF_INIT;\n    -+\tkh_oid_map_t *replayed_commits;\n     +\tint ret;\n     +\n    -+\treplayed_commits = kh_init_oid_map();\n    -+\n    -+\tinit_basic_merge_options(&merge_opts, repo);\n    -+\tmerge_opts.show_rename_progress = 0;\n    -+\n     +\tfor (size_t i = 0; i < commits->nr; i++) {\n     +\t\tstruct object_id commit_id;\n     +\t\tstruct commit *commit;\n     +\t\tconst char *end;\n    -+\t\tint hash_result;\n    -+\t\tkhint_t pos;\n     +\n     +\t\tif (parse_oid_hex_algop(commits->v[i], &commit_id, &end,\n     +\t\t\t\t\trepo->hash_algo)) {\n    @@ builtin/history.c\n     +\n     +\t\tif (!onto) {\n     +\t\t\tonto = commit;\n    -+\t\t\tresult.clean = 1;\n    -+\t\t\tresult.tree = repo_get_commit_tree(repo, commit);\n     +\t\t} else {\n    -+\t\t\tonto = replay_pick_regular_commit(repo, commit, replayed_commits,\n    -+\t\t\t\t\t\t\t  onto, &merge_opts, &result);\n    ++\t\t\tstruct tree *tree = repo_get_commit_tree(repo, commit);\n    ++\t\t\tonto = replay_create_commit(repo, tree, commit, onto);\n     +\t\t\tif (!onto)\n     +\t\t\t\tbreak;\n     +\t\t}\n    -+\n    -+\t\tpos = kh_put_oid_map(replayed_commits, commit->object.oid, &hash_result);\n    -+\t\tif (hash_result == 0) {\n    -+\t\t\tret = error(_(\"duplicate rewritten commit: %s\\n\"),\n    -+\t\t\t\t    oid_to_hex(&commit->object.oid));\n    -+\t\t\tgoto out;\n    -+\t\t}\n    -+\t\tkh_value(replayed_commits, pos) = onto;\n    -+\t}\n    -+\n    -+\tif (!result.clean) {\n    -+\t\tret = error(_(\"could not merge\"));\n    -+\t\tgoto out;\n     +\t}\n     +\n     +\treset_opts.oid = &onto->object.oid;\n    @@ builtin/history.c\n     +\tret = 0;\n     +\n     +out:\n    -+\tkh_destroy_oid_map(replayed_commits);\n    -+\tmerge_finalize(&merge_opts, &result);\n     +\tstrbuf_release(&buf);\n     +\treturn ret;\n     +}\n    @@ builtin/history.c\n     +\t\t\t       const struct object_id *old_tree,\n     +\t\t\t       const struct object_id *new_tree,\n     +\t\t\t       const char *default_message,\n    -+\t\t\t       const char *provided_message,\n     +\t\t\t       const char *action,\n     +\t\t\t       struct strbuf *out)\n     +{\n    -+\tif (!provided_message) {\n    -+\t\tconst char *path = git_path_commit_editmsg();\n    -+\t\tconst char *hint =\n    -+\t\t\t_(\"Please enter the commit message for the %s changes.\"\n    -+\t\t\t  \" Lines starting\\nwith '%s' will be ignored.\\n\");\n    -+\t\tstruct wt_status s;\n    -+\n    -+\t\tstrbuf_addstr(out, default_message);\n    -+\t\tstrbuf_addch(out, '\\n');\n    -+\t\tstrbuf_commented_addf(out, comment_line_str, hint, action, comment_line_str);\n    -+\t\twrite_file_buf(path, out->buf, out->len);\n    -+\n    -+\t\twt_status_prepare(repo, &s);\n    -+\t\tFREE_AND_NULL(s.branch);\n    -+\t\ts.ahead_behind_flags = AHEAD_BEHIND_QUICK;\n    -+\t\ts.commit_template = 1;\n    -+\t\ts.colopts = 0;\n    -+\t\ts.display_comment_prefix = 1;\n    -+\t\ts.hints = 0;\n    -+\t\ts.use_color = 0;\n    -+\t\ts.whence = FROM_COMMIT;\n    -+\t\ts.committable = 1;\n    -+\n    -+\t\ts.fp = fopen(git_path_commit_editmsg(), \"a\");\n    -+\t\tif (!s.fp)\n    -+\t\t\treturn error_errno(_(\"could not open '%s'\"), git_path_commit_editmsg());\n    -+\n    -+\t\twt_status_collect_changes_trees(&s, old_tree, new_tree);\n    -+\t\twt_status_print(&s);\n    -+\t\twt_status_collect_free_buffers(&s);\n    -+\t\tstring_list_clear_func(&s.change, change_data_free);\n    -+\n    -+\t\tstrbuf_reset(out);\n    -+\t\tif (launch_editor(path, out, NULL)) {\n    -+\t\t\tfprintf(stderr, _(\"Please supply the message using the -m option.\\n\"));\n    -+\t\t\treturn -1;\n    -+\t\t}\n    -+\t\tstrbuf_stripspace(out, comment_line_str);\n    -+\t} else {\n    -+\t\tstrbuf_addstr(out, provided_message);\n    ++\tconst char *path = git_path_commit_editmsg();\n    ++\tconst char *hint =\n    ++\t\t_(\"Please enter the commit message for the %s changes.\"\n    ++\t\t  \" Lines starting\\nwith '%s' will be ignored.\\n\");\n    ++\tstruct wt_status s;\n    ++\n    ++\tstrbuf_addstr(out, default_message);\n    ++\tstrbuf_addch(out, '\\n');\n    ++\tstrbuf_commented_addf(out, comment_line_str, hint, action, comment_line_str);\n    ++\twrite_file_buf(path, out->buf, out->len);\n    ++\n    ++\twt_status_prepare(repo, &s);\n    ++\tFREE_AND_NULL(s.branch);\n    ++\ts.ahead_behind_flags = AHEAD_BEHIND_QUICK;\n    ++\ts.commit_template = 1;\n    ++\ts.colopts = 0;\n    ++\ts.display_comment_prefix = 1;\n    ++\ts.hints = 0;\n    ++\ts.use_color = 0;\n    ++\ts.whence = FROM_COMMIT;\n    ++\ts.committable = 1;\n    ++\n    ++\ts.fp = fopen(git_path_commit_editmsg(), \"a\");\n    ++\tif (!s.fp)\n    ++\t\treturn error_errno(_(\"could not open '%s'\"), git_path_commit_editmsg());\n    ++\n    ++\twt_status_collect_changes_trees(&s, old_tree, new_tree);\n    ++\twt_status_print(&s);\n    ++\twt_status_collect_free_buffers(&s);\n    ++\tstring_list_clear_func(&s.change, change_data_free);\n    ++\n    ++\tstrbuf_reset(out);\n    ++\tif (launch_editor(path, out, NULL)) {\n    ++\t\tfprintf(stderr, _(\"Please supply the message using the -m option.\\n\"));\n    ++\t\treturn -1;\n     +\t}\n    ++\tstrbuf_stripspace(out, comment_line_str);\n     +\n     +\tcleanup_message(out, COMMIT_MSG_CLEANUP_ALL, 0);\n     +\n    @@ builtin/history.c\n     +\t\tGIT_HISTORY_REWORD_USAGE,\n     +\t\tNULL,\n     +\t};\n    -+\tconst char *commit_message = NULL;\n     +\tstruct option options[] = {\n    -+\t\tOPT_STRING('m', \"message\", &commit_message, N_(\"message\"), N_(\"commit message\")),\n     +\t\tOPT_END(),\n     +\t};\n     +\tstruct strbuf final_message = STRBUF_INIT;\n    @@ builtin/history.c\n     +\tfind_commit_subject(original_message, &original_body);\n     +\n     +\tret = fill_commit_message(repo, &parent_tree_oid, &original_commit_tree_oid,\n    -+\t\t\t\t  original_body, commit_message, \"reworded\", &final_message);\n    ++\t\t\t\t  original_body, \"reworded\", &final_message);\n     +\tif (ret < 0)\n     +\t\tgoto out;\n     +\n    @@ builtin/history.c\n     +\t\tstruct repository *repo)\n      {\n      \tconst char * const usage[] = {\n    - \t\tN_(\"git history [<options>]\"),\n    +-\t\tN_(\"git history [<options>]\"),\n     +\t\tGIT_HISTORY_REWORD_USAGE,\n      \t\tNULL,\n      \t};\n    @@ t/t3451-history-reword.sh (new)\n     +\n     +. ./test-lib.sh\n     +\n    ++reword_with_message () {\n    ++\tcat >message &&\n    ++\twrite_script fake-editor.sh <<-EOF &&\n    ++\tcp \"$(pwd)/message\" \"\\$1\"\n    ++\tEOF\n    ++\ttest_set_editor \"$(pwd)\"/fake-editor.sh &&\n    ++\tgit history reword \"$@\" &&\n    ++\trm fake-editor.sh message\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/t3451-history-reword.sh (new)\n     +\t\ttest_commit third &&\n     +\n     +\t\tgit symbolic-ref HEAD >expect &&\n    -+\t\tgit history reword -m \"third reworded\" HEAD &&\n    ++\t\treword_with_message HEAD <<-EOF &&\n    ++\t\tthird reworded\n    ++\t\tEOF\n     +\t\tgit symbolic-ref HEAD >actual &&\n     +\t\ttest_cmp expect actual &&\n     +\n    @@ t/t3451-history-reword.sh (new)\n     +\t\ttest_commit third &&\n     +\n     +\t\tgit symbolic-ref HEAD >expect &&\n    -+\t\tgit history reword -m \"second reworded\" HEAD~ &&\n    ++\t\treword_with_message HEAD~ <<-EOF &&\n    ++\t\tsecond reworded\n    ++\t\tEOF\n     +\t\tgit symbolic-ref HEAD >actual &&\n     +\t\ttest_cmp expect actual &&\n     +\n    @@ t/t3451-history-reword.sh (new)\n     +\t\ttest_commit first &&\n     +\t\ttest_commit second &&\n     +\t\ttest_commit third &&\n    -+\t\tgit history reword -m \"first reworded\" HEAD~2 &&\n    ++\t\treword_with_message HEAD~2 <<-EOF &&\n    ++\t\tfirst reworded\n    ++\t\tEOF\n     +\n     +\t\tcat >expect <<-EOF &&\n     +\t\tthird\n    @@ t/t3451-history-reword.sh (new)\n     +# change in the future, and if it does this test here is expected to start\n     +# failing. In other words, this test is not an endorsement of the current\n     +# status quo.\n    -+test_expect_success 'hooks are executed for rewritten commits' '\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/t3451-history-reword.sh (new)\n     +\t\ttouch \"$(pwd)/hooks.log\n     +\t\tEOF\n     +\n    -+\t\tgit history reword -m \"second reworded\" HEAD~ &&\n    ++\t\treword_with_message HEAD~ <<-EOF &&\n    ++\t\tsecond reworded\n    ++\t\tEOF\n     +\n     +\t\tcat >expect <<-EOF &&\n     +\t\tthird\n    @@ t/t3451-history-reword.sh (new)\n     +\t\tcd repo &&\n     +\t\ttest_commit first &&\n     +\n    -+\t\ttest_must_fail git history reword -m \"\" HEAD 2>err &&\n    ++\t\t! reword_with_message HEAD 2>err </dev/null &&\n     +\t\ttest_grep \"Aborting commit due to empty commit message.\" err\n     +\t)\n     +'\n    @@ t/t3451-history-reword.sh (new)\n     +\t\techo foo >a &&\n     +\t\techo bar >b &&\n     +\t\tgit add b &&\n    -+\t\tgit history reword HEAD -m message &&\n    ++\t\treword_with_message HEAD <<-EOF &&\n    ++\t\tmessage\n    ++\t\tEOF\n     +\t\tcat >expect <<-\\EOF &&\n     +\t\t M a\n     +\t\tM  b\n 7:  a064a8c7eca =  6:  a1b1e5877bc add-patch: split out header from \"add-interactive.h\"\n 8:  39c231e726f =  7:  4262663aa50 add-patch: split out `struct interactive_options`\n 9:  104a42a552f =  8:  c285918281f add-patch: remove dependency on \"add-interactive\" subsystem\n10:  2e60b2b6b48 =  9:  3f6e32241df add-patch: add support for in-memory index patching\n11:  52bdbb2ac6a = 10:  c25564e1d16 cache-tree: allow writing in-memory index as tree\n12:  052ee5bd58b ! 11:  221dd490af1 builtin/history: implement \"split\" subcommand\n    @@ Commit message\n     \n      ## Documentation/git-history.adoc ##\n     @@ Documentation/git-history.adoc: SYNOPSIS\n    + --------\n      [synopsis]\n    - git history [<options>]\n    - git history reword [<options>] <commit>\n    -+git history split [<options>] <commit> [--] [<pathspec>...]\n    + git history reword <commit>\n    ++git history split <commit> [--] [<pathspec>...]\n      \n      DESCRIPTION\n      -----------\n    -@@ Documentation/git-history.adoc: rewrite history in different ways:\n    - \tprovided, then this command will spawn an editor with the current\n    - \tmessage of that commit.\n    +@@ Documentation/git-history.adoc: Several 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 [--message=<message>] <commit> [--] [<pathspec>...]`::\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    @@ Documentation/git-history.adoc: rewrite history in different ways:\n     +\tcommit.\n     ++\n     +The commit message of the new commit will be asked for by launching the\n    -+configured editor, unless it has been specified with the `-m` option.\n    -+Authorship of the commit will be the same as for the original commit.\n    ++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    @@ Documentation/git-history.adoc: include::includes/cmd-config-section-all.adoc[]\n     + foo | 1 +\n     + 2 files changed, 2 insertions(+)\n     +\n    -+$ git history split HEAD --message=\"split-out commit\"\n    ++$ git history split HEAD\n     +diff --git a/bar b/bar\n     +new file mode 100644\n     +index 0000000..5716ca5\n    @@ builtin/history.c\n      #include \"tree.h\"\n      #include \"wt-status.h\"\n      \n    - #define GIT_HISTORY_REWORD_USAGE N_(\"git history reword [<options>] <commit>\")\n    -+#define GIT_HISTORY_SPLIT_USAGE  N_(\"git history split [<options>] <commit> [--] [<pathspec>...]\")\n    + #define GIT_HISTORY_REWORD_USAGE N_(\"git history reword <commit>\")\n    ++#define GIT_HISTORY_SPLIT_USAGE  N_(\"git history split <commit> [--] [<pathspec>...]\")\n      \n      static int collect_commits(struct repository *repo,\n      \t\t\t   struct commit *old_commit,\n    @@ builtin/history.c: static int cmd_history_reword(int argc,\n     +static int split_commit(struct repository *repo,\n     +\t\t\tstruct commit *original_commit,\n     +\t\t\tstruct pathspec *pathspec,\n    -+\t\t\tconst char *commit_message,\n     +\t\t\tstruct object_id *out)\n     +{\n     +\tstruct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;\n    @@ builtin/history.c: static int cmd_history_reword(int argc,\n     +\t\toriginal_author = xmemdupz(ptr, len);\n     +\n     +\tret = fill_commit_message(repo, &parent_tree_oid, &split_tree->object.oid,\n    -+\t\t\t\t  \"\", commit_message, \"split-out\", &split_message);\n    ++\t\t\t\t  \"\", \"split-out\", &split_message);\n     +\tif (ret < 0)\n     +\t\tgoto out;\n     +\n    @@ builtin/history.c: static int cmd_history_reword(int argc,\n     +\t\tGIT_HISTORY_SPLIT_USAGE,\n     +\t\tNULL,\n     +\t};\n    -+\tconst char *commit_message = NULL;\n     +\tstruct option options[] = {\n    -+\t\tOPT_STRING('m', \"message\", &commit_message, N_(\"message\"), N_(\"commit message\")),\n     +\t\tOPT_END(),\n     +\t};\n     +\tstruct oidmap rewritten_commits = OIDMAP_INIT;\n    @@ builtin/history.c: static int cmd_history_reword(int argc,\n     +\t * Then we split up the commit and replace the original commit with the\n     +\t * new ones.\n     +\t */\n    -+\tret = split_commit(repo, original_commit, &pathspec,\n    -+\t\t\t   commit_message, split_commits);\n    ++\tret = split_commit(repo, original_commit, &pathspec, split_commits);\n     +\tif (ret < 0)\n     +\t\tgoto out;\n     +\n    @@ builtin/history.c: static int cmd_history_reword(int argc,\n      \t\tconst char **argv,\n      \t\tconst char *prefix,\n     @@ builtin/history.c: int cmd_history(int argc,\n    + {\n      \tconst char * const usage[] = {\n    - \t\tN_(\"git history [<options>]\"),\n      \t\tGIT_HISTORY_REWORD_USAGE,\n     +\t\tGIT_HISTORY_SPLIT_USAGE,\n      \t\tNULL,\n    @@ t/t3452-history-split.sh (new)\n     +. ./test-lib.sh\n     +\n     +set_fake_editor () {\n    -+\twrite_script fake-editor.sh <<-\\EOF &&\n    -+\techo \"split-out commit\" >\"$1\"\n    ++\twrite_script fake-editor.sh <<-EOF &&\n    ++\techo \"$@\" >\"\\$1\"\n     +\tEOF\n     +\ttest_set_editor \"$(pwd)\"/fake-editor.sh\n     +}\n    @@ t/t3452-history-split.sh (new)\n     +\t\tgit commit -m split-me &&\n     +\n     +\t\tgit symbolic-ref HEAD >expect &&\n    -+\t\tset_fake_editor &&\n    ++\t\tset_fake_editor \"split-out commit\" &&\n     +\t\tgit history split HEAD <<-EOF &&\n     +\t\ty\n     +\t\tn\n    @@ t/t3452-history-split.sh (new)\n     +\t\tgit commit -m root &&\n     +\t\ttest_commit tip &&\n     +\n    -+\t\tset_fake_editor &&\n    ++\t\tset_fake_editor \"split-out commit\" &&\n     +\t\tgit history split HEAD~ <<-EOF &&\n     +\t\ty\n     +\t\tn\n    @@ t/t3452-history-split.sh (new)\n     +\t\tgit commit -m split-me &&\n     +\t\ttest_commit tip &&\n     +\n    -+\t\tset_fake_editor &&\n    ++\t\tset_fake_editor \"split-out commit\" &&\n     +\t\tgit history split HEAD~ <<-EOF &&\n     +\t\ty\n     +\t\tn\n    @@ t/t3452-history-split.sh (new)\n     +\t\tgit add . &&\n     +\t\tgit commit -m split-me &&\n     +\n    -+\t\tgit history split HEAD -m \"split-out commit\" <<-EOF &&\n    ++\t\tset_fake_editor \"split-out-commit\" &&\n    ++\t\tgit history split HEAD <<-EOF &&\n     +\t\ty\n     +\t\tn\n     +\t\ty\n    @@ t/t3452-history-split.sh (new)\n     +\t\tgit add . &&\n     +\t\tgit commit -m split-me &&\n     +\n    -+\t\tgit history split HEAD -m \"split-out commit\" <<-EOF &&\n    ++\t\tset_fake_editor \"split-out commit\" &&\n    ++\t\tgit history split HEAD <<-EOF &&\n     +\t\tn\n     +\t\ty\n     +\t\tEOF\n    @@ t/t3452-history-split.sh (new)\n     +\t\tgit add . &&\n     +\t\tgit commit -m split-me &&\n     +\n    -+\t\ttest_must_fail git history split HEAD -m \"\" <<-EOF 2>err &&\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/t3452-history-split.sh (new)\n     +\t)\n     +'\n     +\n    -+test_expect_success 'can specify message via option' '\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\tgit history split HEAD -m \"message option\" <<-EOF &&\n    -+\t\ty\n    -+\t\tn\n    -+\t\tEOF\n    -+\n    -+\t\texpect_log <<-EOF\n    -+\t\tsplit-me\n    -+\t\tmessage option\n    -+\t\tEOF\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/t3452-history-split.sh (new)\n     +\t\tgit add . &&\n     +\t\tgit commit -m split-me &&\n     +\n    -+\t\tgit history split HEAD -m \"message option\" -- foo <<-EOF &&\n    ++\t\tset_fake_editor \"split-out commit\" &&\n    ++\t\tgit history split HEAD -- foo <<-EOF &&\n     +\t\ty\n     +\t\tEOF\n     +\n    @@ t/t3452-history-split.sh (new)\n     +\t\ttouch \"$(pwd)/hooks.log\"\n     +\t\tEOF\n     +\n    -+\t\tset_fake_editor &&\n    ++\t\tset_fake_editor \"split-out commit\" &&\n     +\t\tgit history split HEAD <<-EOF &&\n     +\t\ty\n     +\t\tn\n    @@ t/t3452-history-split.sh (new)\n     +\t\techo a-modified >a &&\n     +\t\techo b-modified >b &&\n     +\t\tgit add b &&\n    -+\t\tgit history split HEAD -m a-only <<-EOF &&\n    ++\t\tset_fake_editor \"a-only\" &&\n    ++\t\tgit history split HEAD <<-EOF &&\n     +\t\ty\n     +\t\tn\n     +\t\tEOF\n    @@ t/t3452-history-split.sh (new)\n     +\t\tM  b\n     +\t\t?? actual\n     +\t\t?? expect\n    ++\t\t?? fake-editor.sh\n     +\t\tEOF\n     +\t\tgit status --porcelain >actual &&\n     +\t\ttest_cmp expect actual\n\n---\nbase-commit: a912335e972768e607159eb74a4c7b62f86ee38e\nchange-id: 20250819-b4-pks-history-builtin-83398f9a05f0\n\n"},{"id":"529717","messageId":"20251027-b4-pks-history-builtin-v6-1-407dd3f57ad3@pks.im","threadId":"63986","inReplyTo":"20251027-b4-pks-history-builtin-v6-0-407dd3f57ad3@pks.im","subject":"[PATCH v6 01/11] wt-status: provide function to expose status for trees","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-27T11:33:49Z","receivedAt":"2025-10-27T11:34:04Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The \"wt-status\" subsystem is responsible for printing status information\naround the current state of the working tree. This most importantly\nincludes information around whether the working tree or the index have\nany changes.\n\nWe're about to introduce a new command where the changes in neither of\nthem are actually relevant to us. Instead, what we want is to format the\nchanges between two different trees. While it is a little bit of a\nstretch to add this as functionality to _working tree_ status, it\ndoesn't make any sense to open-code this functionality, either.\n\nImplement a new function `wt_status_collect_changes_trees()` that diffs\ntwo trees and formats the status accordingly. This function is not yet\nused, but will be in a subsequent commit.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n wt-status.c | 24 ++++++++++++++++++++++++\n wt-status.h |  9 +++++++++\n 2 files changed, 33 insertions(+)\n\ndiff --git a/wt-status.c b/wt-status.c\nindex e12adb26b9f..95942399f8c 100644\n--- a/wt-status.c\n+++ b/wt-status.c\n@@ -612,6 +612,30 @@ static void wt_status_collect_updated_cb(struct diff_queue_struct *q,\n \t}\n }\n \n+void wt_status_collect_changes_trees(struct wt_status *s,\n+\t\t\t\t     const struct object_id *old_treeish,\n+\t\t\t\t     const struct object_id *new_treeish)\n+{\n+\tstruct diff_options opts = { 0 };\n+\n+\trepo_diff_setup(s->repo, &opts);\n+\topts.output_format = DIFF_FORMAT_CALLBACK;\n+\topts.format_callback = wt_status_collect_updated_cb;\n+\topts.format_callback_data = s;\n+\topts.detect_rename = s->detect_rename >= 0 ? s->detect_rename : opts.detect_rename;\n+\topts.rename_limit = s->rename_limit >= 0 ? s->rename_limit : opts.rename_limit;\n+\topts.rename_score = s->rename_score >= 0 ? s->rename_score : opts.rename_score;\n+\topts.flags.recursive = 1;\n+\tdiff_setup_done(&opts);\n+\n+\tdiff_tree_oid(old_treeish, new_treeish, \"\", &opts);\n+\tdiffcore_std(&opts);\n+\tdiff_flush(&opts);\n+\twt_status_get_state(s->repo, &s->state, 0);\n+\n+\tdiff_free(&opts);\n+}\n+\n static void wt_status_collect_changes_worktree(struct wt_status *s)\n {\n \tstruct rev_info rev;\ndiff --git a/wt-status.h b/wt-status.h\nindex e40a27214a7..e9fe32e98cc 100644\n--- a/wt-status.h\n+++ b/wt-status.h\n@@ -153,6 +153,15 @@ void wt_status_add_cut_line(struct wt_status *s);\n void wt_status_prepare(struct repository *r, struct wt_status *s);\n void wt_status_print(struct wt_status *s);\n void wt_status_collect(struct wt_status *s);\n+\n+/*\n+ * Collect all changes between the two trees. Changes will be displayed as if\n+ * they were staged into the index.\n+ */\n+void wt_status_collect_changes_trees(struct wt_status *s,\n+\t\t\t\t     const struct object_id *old_treeish,\n+\t\t\t\t     const struct object_id *new_treeish);\n+\n /*\n  * Frees the buffers allocated by wt_status_collect.\n  */\n\n-- \n2.51.1.930.gacf6e81ea2.dirty\n\n"},{"id":"529718","messageId":"20251027-b4-pks-history-builtin-v6-2-407dd3f57ad3@pks.im","threadId":"63986","inReplyTo":"20251027-b4-pks-history-builtin-v6-0-407dd3f57ad3@pks.im","subject":"[PATCH v6 02/11] replay: extract logic to pick commits","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-27T11:33:50Z","receivedAt":"2025-10-27T11:34:08Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"We're about to add a new git-history(1) command that will reuse some of\nthe same infrastructure as git-replay(1). To prepare for this, extract\nthe logic to pick a commit into a new \"replay.c\" file so that it can be\nshared between both commands.\n\nRename the function to have a \"replay_\" prefix to clearly indicate its\nsubsystem.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Makefile         |   1 +\n builtin/replay.c | 110 ++--------------------------------------------------\n meson.build      |   1 +\n replay.c         | 115 +++++++++++++++++++++++++++++++++++++++++++++++++++++++\n replay.h         |  23 +++++++++++\n 5 files changed, 143 insertions(+), 107 deletions(-)\n\ndiff --git a/Makefile b/Makefile\nindex 1919d35bf3f..01c171b4f03 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -1261,6 +1261,7 @@ LIB_OBJS += reftable/tree.o\n LIB_OBJS += reftable/writer.o\n LIB_OBJS += remote.o\n LIB_OBJS += replace-object.o\n+LIB_OBJS += replay.o\n LIB_OBJS += repo-settings.o\n LIB_OBJS += repository.o\n LIB_OBJS += rerere.o\ndiff --git a/builtin/replay.c b/builtin/replay.c\nindex bb0420dc992..e39824912cd 100644\n--- a/builtin/replay.c\n+++ b/builtin/replay.c\n@@ -2,7 +2,6 @@\n  * \"git replay\" builtin command\n  */\n \n-#define USE_THE_REPOSITORY_VARIABLE\n #define DISABLE_SIGN_COMPARE_WARNINGS\n \n #include \"git-compat-util.h\"\n@@ -16,6 +15,7 @@\n #include \"object-name.h\"\n #include \"parse-options.h\"\n #include \"refs.h\"\n+#include \"replay.h\"\n #include \"revision.h\"\n #include \"strmap.h\"\n #include <oidset.h>\n@@ -26,13 +26,6 @@ enum ref_action_mode {\n \tREF_ACTION_PRINT\n };\n \n-static const char *short_commit_name(struct repository *repo,\n-\t\t\t\t     struct commit *commit)\n-{\n-\treturn repo_find_unique_abbrev(repo, &commit->object.oid,\n-\t\t\t\t       DEFAULT_ABBREV);\n-}\n-\n static struct commit *peel_committish(struct repository *repo, const char *name)\n {\n \tstruct object *obj;\n@@ -45,59 +38,6 @@ static struct commit *peel_committish(struct repository *repo, const char *name)\n \t\t\t\t\t\t  OBJ_COMMIT);\n }\n \n-static char *get_author(const char *message)\n-{\n-\tsize_t len;\n-\tconst char *a;\n-\n-\ta = find_commit_header(message, \"author\", &len);\n-\tif (a)\n-\t\treturn xmemdupz(a, len);\n-\n-\treturn NULL;\n-}\n-\n-static struct commit *create_commit(struct repository *repo,\n-\t\t\t\t    struct tree *tree,\n-\t\t\t\t    struct commit *based_on,\n-\t\t\t\t    struct commit *parent)\n-{\n-\tstruct object_id ret;\n-\tstruct object *obj = NULL;\n-\tstruct commit_list *parents = NULL;\n-\tchar *author;\n-\tchar *sign_commit = NULL; /* FIXME: cli users might want to sign again */\n-\tstruct commit_extra_header *extra = NULL;\n-\tstruct strbuf msg = STRBUF_INIT;\n-\tconst char *out_enc = get_commit_output_encoding();\n-\tconst char *message = repo_logmsg_reencode(repo, based_on,\n-\t\t\t\t\t\t   NULL, out_enc);\n-\tconst char *orig_message = NULL;\n-\tconst char *exclude_gpgsig[] = { \"gpgsig\", NULL };\n-\n-\tcommit_list_insert(parent, &parents);\n-\textra = read_commit_extra_headers(based_on, exclude_gpgsig);\n-\tfind_commit_subject(message, &orig_message);\n-\tstrbuf_addstr(&msg, orig_message);\n-\tauthor = get_author(message);\n-\treset_ident_date();\n-\tif (commit_tree_extended(msg.buf, msg.len, &tree->object.oid, parents,\n-\t\t\t\t &ret, author, NULL, sign_commit, extra)) {\n-\t\terror(_(\"failed to write commit object\"));\n-\t\tgoto out;\n-\t}\n-\n-\tobj = parse_object(repo, &ret);\n-\n-out:\n-\trepo_unuse_commit_buffer(the_repository, based_on, message);\n-\tfree_commit_extra_headers(extra);\n-\tfree_commit_list(parents);\n-\tstrbuf_release(&msg);\n-\tfree(author);\n-\treturn (struct commit *)obj;\n-}\n-\n struct ref_info {\n \tstruct commit *onto;\n \tstruct strset positive_refs;\n@@ -246,50 +186,6 @@ static void determine_replay_mode(struct repository *repo,\n \tstrset_clear(&rinfo.positive_refs);\n }\n \n-static struct commit *mapped_commit(kh_oid_map_t *replayed_commits,\n-\t\t\t\t    struct commit *commit,\n-\t\t\t\t    struct commit *fallback)\n-{\n-\tkhint_t pos = kh_get_oid_map(replayed_commits, commit->object.oid);\n-\tif (pos == kh_end(replayed_commits))\n-\t\treturn fallback;\n-\treturn kh_value(replayed_commits, pos);\n-}\n-\n-static struct commit *pick_regular_commit(struct repository *repo,\n-\t\t\t\t\t  struct commit *pickme,\n-\t\t\t\t\t  kh_oid_map_t *replayed_commits,\n-\t\t\t\t\t  struct commit *onto,\n-\t\t\t\t\t  struct merge_options *merge_opt,\n-\t\t\t\t\t  struct merge_result *result)\n-{\n-\tstruct commit *base, *replayed_base;\n-\tstruct tree *pickme_tree, *base_tree;\n-\n-\tbase = pickme->parents->item;\n-\treplayed_base = mapped_commit(replayed_commits, base, onto);\n-\n-\tresult->tree = repo_get_commit_tree(repo, replayed_base);\n-\tpickme_tree = repo_get_commit_tree(repo, pickme);\n-\tbase_tree = repo_get_commit_tree(repo, base);\n-\n-\tmerge_opt->branch1 = short_commit_name(repo, replayed_base);\n-\tmerge_opt->branch2 = short_commit_name(repo, pickme);\n-\tmerge_opt->ancestor = xstrfmt(\"parent of %s\", merge_opt->branch2);\n-\n-\tmerge_incore_nonrecursive(merge_opt,\n-\t\t\t\t  base_tree,\n-\t\t\t\t  result->tree,\n-\t\t\t\t  pickme_tree,\n-\t\t\t\t  result);\n-\n-\tfree((char*)merge_opt->ancestor);\n-\tmerge_opt->ancestor = NULL;\n-\tif (!result->clean)\n-\t\treturn NULL;\n-\treturn create_commit(repo, result->tree, pickme, replayed_base);\n-}\n-\n static int handle_ref_update(enum ref_action_mode mode,\n \t\t\t     struct ref_transaction *transaction,\n \t\t\t     const char *refname,\n@@ -483,8 +379,8 @@ int cmd_replay(int argc,\n \t\tif (commit->parents->next)\n \t\t\tdie(_(\"replaying merge commits is not supported yet!\"));\n \n-\t\tlast_commit = pick_regular_commit(repo, commit, replayed_commits,\n-\t\t\t\t\t\t  onto, &merge_opt, &result);\n+\t\tlast_commit = replay_pick_regular_commit(repo, commit, replayed_commits,\n+\t\t\t\t\t\t\t onto, &merge_opt, &result);\n \t\tif (!last_commit)\n \t\t\tbreak;\n \ndiff --git a/meson.build b/meson.build\nindex cee94244759..ae8d4fef059 100644\n--- a/meson.build\n+++ b/meson.build\n@@ -464,6 +464,7 @@ libgit_sources = [\n   'reftable/writer.c',\n   'remote.c',\n   'replace-object.c',\n+  'replay.c',\n   'repo-settings.c',\n   'repository.c',\n   'rerere.c',\ndiff --git a/replay.c b/replay.c\nnew file mode 100644\nindex 00000000000..98be33b8545\n--- /dev/null\n+++ b/replay.c\n@@ -0,0 +1,115 @@\n+#define USE_THE_REPOSITORY_VARIABLE\n+\n+#include \"git-compat-util.h\"\n+#include \"commit.h\"\n+#include \"environment.h\"\n+#include \"gettext.h\"\n+#include \"ident.h\"\n+#include \"object.h\"\n+#include \"object-name.h\"\n+#include \"replay.h\"\n+#include \"tree.h\"\n+\n+static const char *short_commit_name(struct repository *repo,\n+\t\t\t\t     struct commit *commit)\n+{\n+\treturn repo_find_unique_abbrev(repo, &commit->object.oid,\n+\t\t\t\t       DEFAULT_ABBREV);\n+}\n+\n+static char *get_author(const char *message)\n+{\n+\tsize_t len;\n+\tconst char *a;\n+\n+\ta = find_commit_header(message, \"author\", &len);\n+\tif (a)\n+\t\treturn xmemdupz(a, len);\n+\n+\treturn NULL;\n+}\n+\n+struct commit *replay_create_commit(struct repository *repo,\n+\t\t\t\t    struct tree *tree,\n+\t\t\t\t    struct commit *based_on,\n+\t\t\t\t    struct commit *parent)\n+{\n+\tstruct object_id ret;\n+\tstruct object *obj = NULL;\n+\tstruct commit_list *parents = NULL;\n+\tchar *author;\n+\tchar *sign_commit = NULL; /* FIXME: cli users might want to sign again */\n+\tstruct commit_extra_header *extra = NULL;\n+\tstruct strbuf msg = STRBUF_INIT;\n+\tconst char *out_enc = get_commit_output_encoding();\n+\tconst char *message = repo_logmsg_reencode(repo, based_on,\n+\t\t\t\t\t\t   NULL, out_enc);\n+\tconst char *orig_message = NULL;\n+\tconst char *exclude_gpgsig[] = { \"gpgsig\", NULL };\n+\n+\tcommit_list_insert(parent, &parents);\n+\textra = read_commit_extra_headers(based_on, exclude_gpgsig);\n+\tfind_commit_subject(message, &orig_message);\n+\tstrbuf_addstr(&msg, orig_message);\n+\tauthor = get_author(message);\n+\treset_ident_date();\n+\tif (commit_tree_extended(msg.buf, msg.len, &tree->object.oid, parents,\n+\t\t\t\t &ret, author, NULL, sign_commit, extra)) {\n+\t\terror(_(\"failed to write commit object\"));\n+\t\tgoto out;\n+\t}\n+\n+\tobj = parse_object(repo, &ret);\n+\n+out:\n+\trepo_unuse_commit_buffer(the_repository, based_on, message);\n+\tfree_commit_extra_headers(extra);\n+\tfree_commit_list(parents);\n+\tstrbuf_release(&msg);\n+\tfree(author);\n+\treturn (struct commit *)obj;\n+}\n+\n+static struct commit *mapped_commit(kh_oid_map_t *replayed_commits,\n+\t\t\t\t    struct commit *commit,\n+\t\t\t\t    struct commit *fallback)\n+{\n+\tkhint_t pos = kh_get_oid_map(replayed_commits, commit->object.oid);\n+\tif (pos == kh_end(replayed_commits))\n+\t\treturn fallback;\n+\treturn kh_value(replayed_commits, pos);\n+}\n+\n+struct commit *replay_pick_regular_commit(struct repository *repo,\n+\t\t\t\t\t  struct commit *pickme,\n+\t\t\t\t\t  kh_oid_map_t *replayed_commits,\n+\t\t\t\t\t  struct commit *onto,\n+\t\t\t\t\t  struct merge_options *merge_opt,\n+\t\t\t\t\t  struct merge_result *result)\n+{\n+\tstruct commit *base, *replayed_base;\n+\tstruct tree *pickme_tree, *base_tree;\n+\n+\tbase = pickme->parents->item;\n+\treplayed_base = mapped_commit(replayed_commits, base, onto);\n+\n+\tresult->tree = repo_get_commit_tree(repo, replayed_base);\n+\tpickme_tree = repo_get_commit_tree(repo, pickme);\n+\tbase_tree = repo_get_commit_tree(repo, base);\n+\n+\tmerge_opt->branch1 = short_commit_name(repo, replayed_base);\n+\tmerge_opt->branch2 = short_commit_name(repo, pickme);\n+\tmerge_opt->ancestor = xstrfmt(\"parent of %s\", merge_opt->branch2);\n+\n+\tmerge_incore_nonrecursive(merge_opt,\n+\t\t\t\t  base_tree,\n+\t\t\t\t  result->tree,\n+\t\t\t\t  pickme_tree,\n+\t\t\t\t  result);\n+\n+\tfree((char*)merge_opt->ancestor);\n+\tmerge_opt->ancestor = NULL;\n+\tif (!result->clean)\n+\t\treturn NULL;\n+\treturn replay_create_commit(repo, result->tree, pickme, replayed_base);\n+}\ndiff --git a/replay.h b/replay.h\nnew file mode 100644\nindex 00000000000..d6535ee56c9\n--- /dev/null\n+++ b/replay.h\n@@ -0,0 +1,23 @@\n+#ifndef REPLAY_H\n+#define REPLAY_H\n+\n+#include \"khash.h\"\n+#include \"merge-ort.h\"\n+#include \"repository.h\"\n+\n+struct commit;\n+struct tree;\n+\n+struct commit *replay_create_commit(struct repository *repo,\n+\t\t\t\t    struct tree *tree,\n+\t\t\t\t    struct commit *based_on,\n+\t\t\t\t    struct commit *parent);\n+\n+struct commit *replay_pick_regular_commit(struct repository *repo,\n+\t\t\t\t\t  struct commit *pickme,\n+\t\t\t\t\t  kh_oid_map_t *replayed_commits,\n+\t\t\t\t\t  struct commit *onto,\n+\t\t\t\t\t  struct merge_options *merge_opt,\n+\t\t\t\t\t  struct merge_result *result);\n+\n+#endif\n\n-- \n2.51.1.930.gacf6e81ea2.dirty\n\n"},{"id":"529719","messageId":"20251027-b4-pks-history-builtin-v6-3-407dd3f57ad3@pks.im","threadId":"63986","inReplyTo":"20251027-b4-pks-history-builtin-v6-0-407dd3f57ad3@pks.im","subject":"[PATCH v6 03/11] replay: stop using `the_repository`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-27T11:33:51Z","receivedAt":"2025-10-27T11:34:11Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"In `create_commit()` we're using `the_repository` even though we already\nhave a repository passed to use as an argument. Fix this.\n\nNote that we still cannot get rid of `USE_THE_REPOSITORY_VARIABLE`. This\nis because we use `DEFAULT_ABBREV and `get_commit_output_encoding()`,\nboth of which are stored as global variables that can be modified via\nthe Git configuration.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n replay.c | 2 +-\n 1 file changed, 1 insertion(+), 1 deletion(-)\n\ndiff --git a/replay.c b/replay.c\nindex 98be33b854..58fdc20140 100644\n--- a/replay.c\n+++ b/replay.c\n@@ -62,7 +62,7 @@ struct commit *replay_create_commit(struct repository *repo,\n \tobj = parse_object(repo, &ret);\n \n out:\n-\trepo_unuse_commit_buffer(the_repository, based_on, message);\n+\trepo_unuse_commit_buffer(repo, based_on, message);\n \tfree_commit_extra_headers(extra);\n \tfree_commit_list(parents);\n \tstrbuf_release(&msg);\n\n-- \n2.51.1.930.gacf6e81ea2.dirty\n\n"},{"id":"529720","messageId":"20251027-b4-pks-history-builtin-v6-4-407dd3f57ad3@pks.im","threadId":"63986","inReplyTo":"20251027-b4-pks-history-builtin-v6-0-407dd3f57ad3@pks.im","subject":"[PATCH v6 04/11] builtin: add new \"history\" command","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-27T11:33:52Z","receivedAt":"2025-10-27T11:34:15Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"When rewriting history via git-rebase(1) there are a couple of very\ncommon use cases:\n\n  - The ordering of two commits should be reversed.\n\n  - A commit should be split up into two commits.\n\n  - A commit should be dropped from the history completely.\n\n  - Multiple commits should be squashed into one.\n\nWhile these operations are all doable, it often feels needlessly kludgey\nto do so by doing an interactive rebase, using the editor to say what\none wants, and then perform the actions. Furthermore, some operations\nlike splitting up a commit into two are way more involved than that and\nrequire a whole series of commands.\n\nAdd a new \"history\" command to plug this gap. This command will have\nseveral different subcommands to imperatively rewrite history for common\nuse cases like the above. These subcommands will be implemented in\nsubsequent commits.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n .gitignore                     |  1 +\n Documentation/git-history.adoc | 44 ++++++++++++++++++++++++++++++++++++++++++\n Documentation/meson.build      |  1 +\n Makefile                       |  1 +\n builtin.h                      |  1 +\n builtin/history.c              | 22 +++++++++++++++++++++\n command-list.txt               |  1 +\n git.c                          |  1 +\n meson.build                    |  1 +\n t/meson.build                  |  1 +\n t/t3450-history.sh             | 17 ++++++++++++++++\n 11 files changed, 91 insertions(+)\n\ndiff --git a/.gitignore b/.gitignore\nindex 78a45cb5bec..24635cf2d6f 100644\n--- a/.gitignore\n+++ b/.gitignore\n@@ -79,6 +79,7 @@\n /git-grep\n /git-hash-object\n /git-help\n+/git-history\n /git-hook\n /git-http-backend\n /git-http-fetch\ndiff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc\nnew file mode 100644\nindex 00000000000..6bdfeb50e8b\n--- /dev/null\n+++ b/Documentation/git-history.adoc\n@@ -0,0 +1,44 @@\n+git-history(1)\n+==============\n+\n+NAME\n+----\n+git-history - EXPERIMENTAL: Rewrite history of the current branch\n+\n+SYNOPSIS\n+--------\n+[synopsis]\n+git history [<options>]\n+\n+DESCRIPTION\n+-----------\n+\n+Rewrite history by rearranging or modifying specific commits in the\n+history.\n+\n+THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.\n+\n+This command is similar to linkgit:git-rebase[1] and uses the same\n+underlying machinery. You should use rebases if you want to reapply a range of\n+commits onto a different base, or interactive rebases if you want to edit a\n+range of commits.\n+\n+Note that this command does not (yet) work with histories that contain\n+merges. You should use linkgit:git-rebase[1] with the `--rebase-merges`\n+flag instead.\n+\n+COMMANDS\n+--------\n+\n+Several commands are available to rewrite history in different ways:\n+\n+CONFIGURATION\n+-------------\n+\n+include::includes/cmd-config-section-all.adoc[]\n+\n+include::config/sequencer.adoc[]\n+\n+GIT\n+---\n+Part of the linkgit:git[1] suite\ndiff --git a/Documentation/meson.build b/Documentation/meson.build\nindex 9d24f2da544..d1f6bde7c16 100644\n--- a/Documentation/meson.build\n+++ b/Documentation/meson.build\n@@ -64,6 +64,7 @@ manpages = {\n   'git-gui.adoc' : 1,\n   'git-hash-object.adoc' : 1,\n   'git-help.adoc' : 1,\n+  'git-history.adoc' : 1,\n   'git-hook.adoc' : 1,\n   'git-http-backend.adoc' : 1,\n   'git-http-fetch.adoc' : 1,\ndiff --git a/Makefile b/Makefile\nindex 01c171b4f03..1380ee1e196 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -1395,6 +1395,7 @@ BUILTIN_OBJS += builtin/get-tar-commit-id.o\n BUILTIN_OBJS += builtin/grep.o\n BUILTIN_OBJS += builtin/hash-object.o\n BUILTIN_OBJS += builtin/help.o\n+BUILTIN_OBJS += builtin/history.o\n BUILTIN_OBJS += builtin/hook.o\n BUILTIN_OBJS += builtin/index-pack.o\n BUILTIN_OBJS += builtin/init-db.o\ndiff --git a/builtin.h b/builtin.h\nindex 1b35565fbd9..93c91d07d4b 100644\n--- a/builtin.h\n+++ b/builtin.h\n@@ -172,6 +172,7 @@ int cmd_get_tar_commit_id(int argc, const char **argv, const char *prefix, struc\n int cmd_grep(int argc, const char **argv, const char *prefix, struct repository *repo);\n int cmd_hash_object(int argc, const char **argv, const char *prefix, struct repository *repo);\n int cmd_help(int argc, const char **argv, const char *prefix, struct repository *repo);\n+int cmd_history(int argc, const char **argv, const char *prefix, struct repository *repo);\n int cmd_hook(int argc, const char **argv, const char *prefix, struct repository *repo);\n int cmd_index_pack(int argc, const char **argv, const char *prefix, struct repository *repo);\n int cmd_init_db(int argc, const char **argv, const char *prefix, struct repository *repo);\ndiff --git a/builtin/history.c b/builtin/history.c\nnew file mode 100644\nindex 00000000000..f6fe32610b0\n--- /dev/null\n+++ b/builtin/history.c\n@@ -0,0 +1,22 @@\n+#include \"builtin.h\"\n+#include \"gettext.h\"\n+#include \"parse-options.h\"\n+\n+int cmd_history(int argc,\n+\t\tconst char **argv,\n+\t\tconst char *prefix,\n+\t\tstruct repository *repo UNUSED)\n+{\n+\tconst char * const usage[] = {\n+\t\tN_(\"git history [<options>]\"),\n+\t\tNULL,\n+\t};\n+\tstruct option options[] = {\n+\t\tOPT_END(),\n+\t};\n+\n+\targc = parse_options(argc, argv, prefix, options, usage, 0);\n+\tif (argc)\n+\t\tusagef(\"unrecognized argument: %s\", argv[0]);\n+\treturn 0;\n+}\ndiff --git a/command-list.txt b/command-list.txt\nindex accd3d0c4b5..f9005cf4597 100644\n--- a/command-list.txt\n+++ b/command-list.txt\n@@ -115,6 +115,7 @@ git-grep                                mainporcelain           info\n git-gui                                 mainporcelain\n git-hash-object                         plumbingmanipulators\n git-help                                ancillaryinterrogators          complete\n+git-history                             mainporcelain           history\n git-hook                                purehelpers\n git-http-backend                        synchingrepositories\n git-http-fetch                          synchelpers\ndiff --git a/git.c b/git.c\nindex c5fad56813f..744cb6527e0 100644\n--- a/git.c\n+++ b/git.c\n@@ -586,6 +586,7 @@ static struct cmd_struct commands[] = {\n \t{ \"grep\", cmd_grep, RUN_SETUP_GENTLY },\n \t{ \"hash-object\", cmd_hash_object },\n \t{ \"help\", cmd_help },\n+\t{ \"history\", cmd_history, RUN_SETUP },\n \t{ \"hook\", cmd_hook, RUN_SETUP },\n \t{ \"index-pack\", cmd_index_pack, RUN_SETUP_GENTLY | NO_PARSEOPT },\n \t{ \"init\", cmd_init_db },\ndiff --git a/meson.build b/meson.build\nindex ae8d4fef059..2d789612a01 100644\n--- a/meson.build\n+++ b/meson.build\n@@ -604,6 +604,7 @@ builtin_sources = [\n   'builtin/grep.c',\n   'builtin/hash-object.c',\n   'builtin/help.c',\n+  'builtin/history.c',\n   'builtin/hook.c',\n   'builtin/index-pack.c',\n   'builtin/init-db.c',\ndiff --git a/t/meson.build b/t/meson.build\nindex 401b24e50e0..019435918fa 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -384,6 +384,7 @@ integration_tests = [\n   't3436-rebase-more-options.sh',\n   't3437-rebase-fixup-options.sh',\n   't3438-rebase-broken-files.sh',\n+  't3450-history.sh',\n   't3500-cherry.sh',\n   't3501-revert-cherry-pick.sh',\n   't3502-cherry-pick-merge.sh',\ndiff --git a/t/t3450-history.sh b/t/t3450-history.sh\nnew file mode 100755\nindex 00000000000..417c343d43b\n--- /dev/null\n+++ b/t/t3450-history.sh\n@@ -0,0 +1,17 @@\n+#!/bin/sh\n+\n+test_description='tests for git-history command'\n+\n+. ./test-lib.sh\n+\n+test_expect_success 'does nothing without any arguments' '\n+\tgit history >out 2>&1 &&\n+\ttest_must_be_empty out\n+'\n+\n+test_expect_success 'raises an error with unknown argument' '\n+\ttest_must_fail git history garbage 2>err &&\n+\ttest_grep \"unrecognized argument: garbage\" err\n+'\n+\n+test_done\n\n-- \n2.51.1.930.gacf6e81ea2.dirty\n\n"},{"id":"529721","messageId":"20251027-b4-pks-history-builtin-v6-5-407dd3f57ad3@pks.im","threadId":"63986","inReplyTo":"20251027-b4-pks-history-builtin-v6-0-407dd3f57ad3@pks.im","subject":"[PATCH v6 05/11] builtin/history: implement \"reword\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-27T11:33:53Z","receivedAt":"2025-10-27T11:34:18Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Implement a new \"reword\" subcommand for git-history(1). This subcommand\nis essentially the same as if a user performed an interactive rebase\nwith a single commit changed to use the \"reword\" verb.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/git-history.adoc |   7 +-\n builtin/history.c              | 331 ++++++++++++++++++++++++++++++++++++++++-\n t/meson.build                  |   1 +\n t/t3450-history.sh             |   6 +-\n t/t3451-history-reword.sh      | 237 +++++++++++++++++++++++++++++\n 5 files changed, 573 insertions(+), 9 deletions(-)\n\ndiff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc\nindex 6bdfeb50e8b..bd903875120 100644\n--- a/Documentation/git-history.adoc\n+++ b/Documentation/git-history.adoc\n@@ -8,7 +8,7 @@ git-history - EXPERIMENTAL: Rewrite history of the current branch\n SYNOPSIS\n --------\n [synopsis]\n-git history [<options>]\n+git history reword <commit>\n \n DESCRIPTION\n -----------\n@@ -32,6 +32,11 @@ COMMANDS\n \n Several commands are available to rewrite history in different ways:\n \n+`reword <commit>`::\n+\tRewrite the commit message of the specified commit. All the other\n+\tdetails of this commit remain unchanged. This command will spawn an\n+\teditor with the current message of that commit.\n+\n CONFIGURATION\n -------------\n \ndiff --git a/builtin/history.c b/builtin/history.c\nindex f6fe32610b0..cb251ae2e01 100644\n--- a/builtin/history.c\n+++ b/builtin/history.c\n@@ -1,22 +1,343 @@\n+#define USE_THE_REPOSITORY_VARIABLE\n+\n #include \"builtin.h\"\n+#include \"commit-reach.h\"\n+#include \"commit.h\"\n+#include \"config.h\"\n+#include \"editor.h\"\n+#include \"environment.h\"\n #include \"gettext.h\"\n+#include \"hex.h\"\n #include \"parse-options.h\"\n+#include \"refs.h\"\n+#include \"replay.h\"\n+#include \"reset.h\"\n+#include \"revision.h\"\n+#include \"sequencer.h\"\n+#include \"strvec.h\"\n+#include \"tree.h\"\n+#include \"wt-status.h\"\n+\n+#define GIT_HISTORY_REWORD_USAGE N_(\"git history reword <commit>\")\n+\n+static int collect_commits(struct repository *repo,\n+\t\t\t   struct commit *old_commit,\n+\t\t\t   struct commit *new_commit,\n+\t\t\t   struct strvec *out)\n+{\n+\tstruct setup_revision_opt revision_opts = {\n+\t\t.assume_dashdash = 1,\n+\t};\n+\tstruct strvec revisions = STRVEC_INIT;\n+\tstruct commit *child;\n+\tstruct rev_info rev = { 0 };\n+\tint ret;\n+\n+\trepo_init_revisions(repo, &rev, NULL);\n+\tstrvec_push(&revisions, \"\");\n+\tstrvec_push(&revisions, oid_to_hex(&new_commit->object.oid));\n+\tif (old_commit)\n+\t\tstrvec_pushf(&revisions, \"^%s\", oid_to_hex(&old_commit->object.oid));\n+\n+\tsetup_revisions_from_strvec(&revisions, &rev, &revision_opts);\n+\tif (revisions.nr != 1 || prepare_revision_walk(&rev)) {\n+\t\tret = error(_(\"revision walk setup failed\"));\n+\t\tgoto out;\n+\t}\n+\n+\twhile ((child = get_revision(&rev))) {\n+\t\tif (old_commit && !child->parents)\n+\t\t\tBUG(\"revision walk did not find child commit\");\n+\t\tif (child->parents && child->parents->next) {\n+\t\t\tret = error(_(\"cannot rearrange commit history with merges\"));\n+\t\t\tgoto out;\n+\t\t}\n+\n+\t\tstrvec_push(out, oid_to_hex(&child->object.oid));\n+\n+\t\tif (child->parents && old_commit &&\n+\t\t    commit_list_contains(old_commit, child->parents))\n+\t\t\tbreak;\n+\t}\n+\n+\t/*\n+\t * Revisions are in newest-order-first. We have to reverse the\n+\t * array though so that we pick the oldest commits first.\n+\t */\n+\tfor (size_t i = 0, j = out->nr - 1; i < j; i++, j--)\n+\t\tSWAP(out->v[i], out->v[j]);\n+\n+\tret = 0;\n+\n+out:\n+\tstrvec_clear(&revisions);\n+\trelease_revisions(&rev);\n+\treset_revision_walk();\n+\treturn ret;\n+}\n+\n+static void replace_commits(struct strvec *commits,\n+\t\t\t    const struct object_id *commit_to_replace,\n+\t\t\t    const struct object_id *replacements,\n+\t\t\t    size_t replacements_nr)\n+{\n+\tchar commit_to_replace_oid[GIT_MAX_HEXSZ + 1];\n+\tstruct strvec replacement_oids = STRVEC_INIT;\n+\tbool found = false;\n+\n+\toid_to_hex_r(commit_to_replace_oid, commit_to_replace);\n+\tfor (size_t i = 0; i < replacements_nr; i++)\n+\t\tstrvec_push(&replacement_oids, oid_to_hex(&replacements[i]));\n+\n+\tfor (size_t i = 0; i < commits->nr; i++) {\n+\t\tif (strcmp(commits->v[i], commit_to_replace_oid))\n+\t\t\tcontinue;\n+\t\tstrvec_splice(commits, i, 1, replacement_oids.v, replacement_oids.nr);\n+\t\tfound = true;\n+\t\tbreak;\n+\t}\n+\tif (!found)\n+\t\tBUG(\"could not find commit to replace\");\n+\n+\tstrvec_clear(&replacement_oids);\n+}\n+\n+static int apply_commits(struct repository *repo,\n+\t\t\t const struct strvec *commits,\n+\t\t\t struct commit *onto,\n+\t\t\t struct commit *orig_head,\n+\t\t\t const char *action)\n+{\n+\tstruct reset_head_opts reset_opts = { 0 };\n+\tstruct strbuf buf = STRBUF_INIT;\n+\tint ret;\n+\n+\tfor (size_t i = 0; i < commits->nr; i++) {\n+\t\tstruct object_id commit_id;\n+\t\tstruct commit *commit;\n+\t\tconst char *end;\n+\n+\t\tif (parse_oid_hex_algop(commits->v[i], &commit_id, &end,\n+\t\t\t\t\trepo->hash_algo)) {\n+\t\t\tret = error(_(\"invalid object ID: %s\"), commits->v[i]);\n+\t\t\tgoto out;\n+\t\t}\n+\n+\t\tcommit = lookup_commit(repo, &commit_id);\n+\t\tif (!commit || repo_parse_commit(repo, commit)) {\n+\t\t\tret = error(_(\"failed to look up commit: %s\"), oid_to_hex(&commit_id));\n+\t\t\tgoto out;\n+\t\t}\n+\n+\t\tif (!onto) {\n+\t\t\tonto = commit;\n+\t\t} else {\n+\t\t\tstruct tree *tree = repo_get_commit_tree(repo, commit);\n+\t\t\tonto = replay_create_commit(repo, tree, commit, onto);\n+\t\t\tif (!onto)\n+\t\t\t\tbreak;\n+\t\t}\n+\t}\n+\n+\treset_opts.oid = &onto->object.oid;\n+\tstrbuf_addf(&buf, \"%s: switch to rewritten %s\", action, oid_to_hex(reset_opts.oid));\n+\treset_opts.flags = RESET_HEAD_REFS_ONLY | RESET_ORIG_HEAD;\n+\treset_opts.orig_head = &orig_head->object.oid;\n+\treset_opts.default_reflog_action = action;\n+\tif (reset_head(repo, &reset_opts) < 0) {\n+\t\tret = error(_(\"could not switch to %s\"), oid_to_hex(reset_opts.oid));\n+\t\tgoto out;\n+\t}\n+\n+\tret = 0;\n+\n+out:\n+\tstrbuf_release(&buf);\n+\treturn ret;\n+}\n+\n+static void change_data_free(void *util, const char *str UNUSED)\n+{\n+\tstruct wt_status_change_data *d = util;\n+\tfree(d->rename_source);\n+\tfree(d);\n+}\n+\n+static int fill_commit_message(struct repository *repo,\n+\t\t\t       const struct object_id *old_tree,\n+\t\t\t       const struct object_id *new_tree,\n+\t\t\t       const char *default_message,\n+\t\t\t       const char *action,\n+\t\t\t       struct strbuf *out)\n+{\n+\tconst char *path = git_path_commit_editmsg();\n+\tconst char *hint =\n+\t\t_(\"Please enter the commit message for the %s changes.\"\n+\t\t  \" Lines starting\\nwith '%s' will be ignored.\\n\");\n+\tstruct wt_status s;\n+\n+\tstrbuf_addstr(out, default_message);\n+\tstrbuf_addch(out, '\\n');\n+\tstrbuf_commented_addf(out, comment_line_str, hint, action, comment_line_str);\n+\twrite_file_buf(path, out->buf, out->len);\n+\n+\twt_status_prepare(repo, &s);\n+\tFREE_AND_NULL(s.branch);\n+\ts.ahead_behind_flags = AHEAD_BEHIND_QUICK;\n+\ts.commit_template = 1;\n+\ts.colopts = 0;\n+\ts.display_comment_prefix = 1;\n+\ts.hints = 0;\n+\ts.use_color = 0;\n+\ts.whence = FROM_COMMIT;\n+\ts.committable = 1;\n+\n+\ts.fp = fopen(git_path_commit_editmsg(), \"a\");\n+\tif (!s.fp)\n+\t\treturn error_errno(_(\"could not open '%s'\"), git_path_commit_editmsg());\n+\n+\twt_status_collect_changes_trees(&s, old_tree, new_tree);\n+\twt_status_print(&s);\n+\twt_status_collect_free_buffers(&s);\n+\tstring_list_clear_func(&s.change, change_data_free);\n+\n+\tstrbuf_reset(out);\n+\tif (launch_editor(path, out, NULL)) {\n+\t\tfprintf(stderr, _(\"Please supply the message using the -m option.\\n\"));\n+\t\treturn -1;\n+\t}\n+\tstrbuf_stripspace(out, comment_line_str);\n+\n+\tcleanup_message(out, COMMIT_MSG_CLEANUP_ALL, 0);\n+\n+\tif (!out->len) {\n+\t\tfprintf(stderr, _(\"Aborting commit due to empty commit message.\\n\"));\n+\t\treturn -1;\n+\t}\n+\n+\treturn 0;\n+}\n+\n+static int cmd_history_reword(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_REWORD_USAGE,\n+\t\tNULL,\n+\t};\n+\tstruct option options[] = {\n+\t\tOPT_END(),\n+\t};\n+\tstruct strbuf final_message = STRBUF_INIT;\n+\tstruct commit *original_commit, *parent, *head;\n+\tstruct strvec commits = STRVEC_INIT;\n+\tstruct object_id parent_tree_oid, original_commit_tree_oid;\n+\tstruct object_id rewritten_commit;\n+\tstruct commit_list *from_list = NULL;\n+\tconst char *original_message, *original_body, *ptr;\n+\tchar *original_author = NULL;\n+\tsize_t len;\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 single revision\"));\n+\t\tgoto out;\n+\t}\n+\trepo_config(repo, git_default_config, NULL);\n+\n+\toriginal_commit = lookup_commit_reference_by_name(argv[0]);\n+\tif (!original_commit) {\n+\t\tret = error(_(\"commit to be reworded cannot be found: %s\"), argv[0]);\n+\t\tgoto out;\n+\t}\n+\toriginal_commit_tree_oid = repo_get_commit_tree(repo, original_commit)->object.oid;\n+\n+\tparent = original_commit->parents ? original_commit->parents->item : NULL;\n+\tif (parent) {\n+\t\tif (repo_parse_commit(repo, parent)) {\n+\t\t\tret = error(_(\"unable to parse commit %s\"),\n+\t\t\t\t    oid_to_hex(&parent->object.oid));\n+\t\t\tgoto out;\n+\t\t}\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+\thead = lookup_commit_reference_by_name(\"HEAD\");\n+\tif (!head) {\n+\t\tret = error(_(\"could not resolve HEAD to a commit\"));\n+\t\tgoto out;\n+\t}\n+\n+\tcommit_list_append(original_commit, &from_list);\n+\tif (!repo_is_descendant_of(repo, head, from_list)) {\n+\t\tret = error (_(\"split commit must be reachable from current HEAD commit\"));\n+\t\tgoto out;\n+\t}\n+\n+\t/*\n+\t * Collect the list of commits that we'll have to reapply now already.\n+\t * This ensures that we'll abort early on in case the range of commits\n+\t * contains merges, which we do not yet handle.\n+\t */\n+\tret = collect_commits(repo, parent, head, &commits);\n+\tif (ret < 0)\n+\t\tgoto out;\n+\n+\t/* We retain authorship of the original commit. */\n+\toriginal_message = repo_logmsg_reencode(repo, original_commit, 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_commit_tree_oid,\n+\t\t\t\t  original_body, \"reworded\", &final_message);\n+\tif (ret < 0)\n+\t\tgoto out;\n+\n+\tret = commit_tree(final_message.buf, final_message.len, &original_commit_tree_oid,\n+\t\t\t  original_commit->parents, &rewritten_commit, original_author, NULL);\n+\tif (ret < 0) {\n+\t\tret = error(_(\"failed writing reworded commit\"));\n+\t\tgoto out;\n+\t}\n+\n+\treplace_commits(&commits, &original_commit->object.oid, &rewritten_commit, 1);\n+\n+\tret = apply_commits(repo, &commits, parent, head, \"reword\");\n+\tif (ret < 0)\n+\t\tgoto out;\n+\n+\tret = 0;\n+\n+out:\n+\tstrbuf_release(&final_message);\n+\tfree_commit_list(from_list);\n+\tstrvec_clear(&commits);\n+\tfree(original_author);\n+\treturn ret;\n+}\n \n int cmd_history(int argc,\n \t\tconst char **argv,\n \t\tconst char *prefix,\n-\t\tstruct repository *repo UNUSED)\n+\t\tstruct repository *repo)\n {\n \tconst char * const usage[] = {\n-\t\tN_(\"git history [<options>]\"),\n+\t\tGIT_HISTORY_REWORD_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_END(),\n \t};\n \n \targc = parse_options(argc, argv, prefix, options, usage, 0);\n-\tif (argc)\n-\t\tusagef(\"unrecognized argument: %s\", argv[0]);\n-\treturn 0;\n+\treturn fn(argc, argv, prefix, repo);\n }\ndiff --git a/t/meson.build b/t/meson.build\nindex 019435918fa..a3ec9199947 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -385,6 +385,7 @@ integration_tests = [\n   't3437-rebase-fixup-options.sh',\n   't3438-rebase-broken-files.sh',\n   't3450-history.sh',\n+  't3451-history-reword.sh',\n   't3500-cherry.sh',\n   't3501-revert-cherry-pick.sh',\n   't3502-cherry-pick-merge.sh',\ndiff --git a/t/t3450-history.sh b/t/t3450-history.sh\nindex 417c343d43b..f513463b92b 100755\n--- a/t/t3450-history.sh\n+++ b/t/t3450-history.sh\n@@ -5,13 +5,13 @@ test_description='tests for git-history command'\n . ./test-lib.sh\n \n test_expect_success 'does nothing without any arguments' '\n-\tgit history >out 2>&1 &&\n-\ttest_must_be_empty out\n+\ttest_must_fail git history 2>err &&\n+\ttest_grep \"need a subcommand\" err\n '\n \n test_expect_success 'raises an error with unknown argument' '\n \ttest_must_fail git history garbage 2>err &&\n-\ttest_grep \"unrecognized argument: garbage\" err\n+\ttest_grep \"unknown subcommand: .garbage.\" err\n '\n \n test_done\ndiff --git a/t/t3451-history-reword.sh b/t/t3451-history-reword.sh\nnew file mode 100755\nindex 00000000000..09dbc463c59\n--- /dev/null\n+++ b/t/t3451-history-reword.sh\n@@ -0,0 +1,237 @@\n+#!/bin/sh\n+\n+test_description='tests for git-history reword subcommand'\n+\n+. ./test-lib.sh\n+\n+reword_with_message () {\n+\tcat >message &&\n+\twrite_script fake-editor.sh <<-EOF &&\n+\tcp \"$(pwd)/message\" \"\\$1\"\n+\tEOF\n+\ttest_set_editor \"$(pwd)\"/fake-editor.sh &&\n+\tgit history reword \"$@\" &&\n+\trm fake-editor.sh message\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 reword HEAD~ 2>err &&\n+\t\ttest_grep \"cannot rearrange commit history with merges\" err &&\n+\t\ttest_must_fail git history reword HEAD 2>err &&\n+\t\ttest_grep \"cannot rearrange commit history with merges\" err\n+\t)\n+'\n+\n+test_expect_success 'refuses to work with unrelated 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\ttest_must_fail git history reword ours 2>err &&\n+\t\ttest_grep \"split commit must be reachable from current HEAD commit\" err\n+\t)\n+'\n+\n+test_expect_success 'can reword tip of a branch' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit first &&\n+\t\ttest_commit second &&\n+\t\ttest_commit third &&\n+\n+\t\tgit symbolic-ref HEAD >expect &&\n+\t\treword_with_message HEAD <<-EOF &&\n+\t\tthird reworded\n+\t\tEOF\n+\t\tgit symbolic-ref HEAD >actual &&\n+\t\ttest_cmp expect actual &&\n+\n+\t\tcat >expect <<-EOF &&\n+\t\tthird reworded\n+\t\tsecond\n+\t\tfirst\n+\t\tEOF\n+\t\tgit log --format=%s >actual &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+test_expect_success 'can reword commit in the middle' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit first &&\n+\t\ttest_commit second &&\n+\t\ttest_commit third &&\n+\n+\t\tgit symbolic-ref HEAD >expect &&\n+\t\treword_with_message HEAD~ <<-EOF &&\n+\t\tsecond reworded\n+\t\tEOF\n+\t\tgit symbolic-ref HEAD >actual &&\n+\t\ttest_cmp expect actual &&\n+\n+\t\tcat >expect <<-EOF &&\n+\t\tthird\n+\t\tsecond reworded\n+\t\tfirst\n+\t\tEOF\n+\t\tgit log --format=%s >actual &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+test_expect_success 'can reword root commit' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit first &&\n+\t\ttest_commit second &&\n+\t\ttest_commit third &&\n+\t\treword_with_message HEAD~2 <<-EOF &&\n+\t\tfirst reworded\n+\t\tEOF\n+\n+\t\tcat >expect <<-EOF &&\n+\t\tthird\n+\t\tsecond\n+\t\tfirst reworded\n+\t\tEOF\n+\t\tgit log --format=%s >actual &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+test_expect_success 'can use editor to rewrite commit message' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit first &&\n+\n+\t\twrite_script fake-editor.sh <<-\\EOF &&\n+\t\tcp \"$1\" . &&\n+\t\tprintf \"\\namend a comment\\n\" >>\"$1\"\n+\t\tEOF\n+\t\ttest_set_editor \"$(pwd)\"/fake-editor.sh &&\n+\t\tgit history reword HEAD &&\n+\n+\t\tcat >expect <<-EOF &&\n+\t\tfirst\n+\n+\t\t# Please enter the commit message for the reworded changes. Lines starting\n+\t\t# with ${SQ}#${SQ} will be ignored.\n+\t\t# Changes to be committed:\n+\t\t#\tnew file:   first.t\n+\t\t#\n+\t\tEOF\n+\t\ttest_cmp expect COMMIT_EDITMSG &&\n+\n+\t\tcat >expect <<-EOF &&\n+\t\tfirst\n+\n+\t\tamend a comment\n+\n+\t\tEOF\n+\t\tgit log --format=%B >actual &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+# For now, git-history(1) does not yet execute any hooks. This is subject to\n+# change in the future, and if it does this test here is expected to start\n+# failing. In other words, this test is not an endorsement of the current\n+# status quo.\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\ttest_commit first &&\n+\t\ttest_commit second &&\n+\t\ttest_commit third &&\n+\n+\t\twrite_script .git/hooks/prepare-commit-msg <<-EOF &&\n+\t\ttouch \"$(pwd)/hooks.log\n+\t\tEOF\n+\t\twrite_script .git/hooks/post-commit <<-EOF &&\n+\t\ttouch \"$(pwd)/hooks.log\n+\t\tEOF\n+\t\twrite_script .git/hooks/post-rewrite <<-EOF &&\n+\t\ttouch \"$(pwd)/hooks.log\n+\t\tEOF\n+\n+\t\treword_with_message HEAD~ <<-EOF &&\n+\t\tsecond reworded\n+\t\tEOF\n+\n+\t\tcat >expect <<-EOF &&\n+\t\tthird\n+\t\tsecond reworded\n+\t\tfirst\n+\t\tEOF\n+\t\tgit log --format=%s >actual &&\n+\t\ttest_cmp expect actual &&\n+\n+\t\ttest_path_is_missing hooks.log\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\ttest_commit first &&\n+\n+\t\t! reword_with_message HEAD 2>err </dev/null &&\n+\t\ttest_grep \"Aborting commit due to empty commit message.\" 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\ttouch a b &&\n+\t\tgit add . &&\n+\t\tgit commit -m \"initial commit\" &&\n+\t\techo foo >a &&\n+\t\techo bar >b &&\n+\t\tgit add b &&\n+\t\treword_with_message HEAD <<-EOF &&\n+\t\tmessage\n+\t\tEOF\n+\t\tcat >expect <<-\\EOF &&\n+\t\t M a\n+\t\tM  b\n+\t\t?? actual\n+\t\t?? expect\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.51.1.930.gacf6e81ea2.dirty\n\n"},{"id":"529722","messageId":"20251027-b4-pks-history-builtin-v6-6-407dd3f57ad3@pks.im","threadId":"63986","inReplyTo":"20251027-b4-pks-history-builtin-v6-0-407dd3f57ad3@pks.im","subject":"[PATCH v6 06/11] add-patch: split out header from \"add-interactive.h\"","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-27T11:33:54Z","receivedAt":"2025-10-27T11:34:21Z","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 | 23 +++--------------------\n add-patch.c       |  1 +\n add-patch.h       | 26 ++++++++++++++++++++++++++\n 3 files changed, 30 insertions(+), 20 deletions(-)\n\ndiff --git a/add-interactive.h b/add-interactive.h\nindex da49502b765..2e3d1d871d2 100644\n--- a/add-interactive.h\n+++ b/add-interactive.h\n@@ -1,14 +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-};\n-\n-#define ADD_P_OPT_INIT { .context = -1, .interhunkcontext = -1 }\n+struct pathspec;\n+struct repository;\n \n struct add_i_state {\n \tstruct repository *r;\n@@ -35,21 +32,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 ae9a20d8f23..3594dd22534 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 00000000000..4394c741076\n--- /dev/null\n+++ b/add-patch.h\n@@ -0,0 +1,26 @@\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+};\n+\n+#define ADD_P_OPT_INIT { .context = -1, .interhunkcontext = -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.51.1.930.gacf6e81ea2.dirty\n\n"},{"id":"529724","messageId":"20251027-b4-pks-history-builtin-v6-7-407dd3f57ad3@pks.im","threadId":"63986","inReplyTo":"20251027-b4-pks-history-builtin-v6-0-407dd3f57ad3@pks.im","subject":"[PATCH v6 07/11] add-patch: split out `struct interactive_options`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-27T11:33:55Z","receivedAt":"2025-10-27T11:34:24Z","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  | 174 +++++++++++------------------------------------------\n add-interactive.h  |  23 +------\n add-patch.c        | 170 +++++++++++++++++++++++++++++++++++++++++++--------\n add-patch.h        |  36 ++++++++++-\n builtin/add.c      |  22 +++----\n builtin/checkout.c |   4 +-\n builtin/commit.c   |  16 ++---\n builtin/reset.c    |  16 ++---\n builtin/stash.c    |  46 +++++++-------\n commit.h           |   2 +-\n 10 files changed, 270 insertions(+), 239 deletions(-)\n\ndiff --git a/add-interactive.c b/add-interactive.c\nindex 68fc09547dd..05d2e7eefe3 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,119 +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-\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@@ -286,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@@ -354,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@@ -432,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@@ -992,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@@ -1014,9 +912,9 @@ 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\tstruct interactive_options opts = {\n+\t\t\t.context = s->cfg.context,\n+\t\t\t.interhunkcontext = s->cfg.interhunkcontext,\n \t\t};\n \t\tstruct strvec args = STRVEC_INIT;\n \t\tstruct pathspec ps_selected = { 0 };\n@@ -1028,7 +926,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@@ -1064,10 +962,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@@ -1085,17 +983,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@@ -1103,21 +1001,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@@ -1152,7 +1050,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@@ -1163,7 +1061,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@@ -1206,15 +1104,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 2e3d1d871d2..eefa2edc7c1 100644\n--- a/add-interactive.h\n+++ b/add-interactive.h\n@@ -2,37 +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+\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 3594dd22534..5c6969927ac 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,122 @@ 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+\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 +417,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 +542,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 +576,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 +811,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 +833,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 +1222,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 +1356,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 +1370,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@@ -1560,15 +1678,15 @@ static int patch_update_file(struct add_p_state *s,\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 \t\t\t\t\t\t: 1));\n \t\tprintf(_(s->mode->prompt_mode[prompt_mode_type]),\n \t\t       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\tbreak;\n@@ -1730,7 +1848,7 @@ static int patch_update_file(struct add_p_state *s,\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@@ -1748,7 +1866,7 @@ static int patch_update_file(struct add_p_state *s,\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@@ -1766,7 +1884,7 @@ static int patch_update_file(struct add_p_state *s,\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@@ -1807,7 +1925,7 @@ static int patch_update_file(struct add_p_state *s,\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@@ -1815,7 +1933,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;\ndiff --git a/add-patch.h b/add-patch.h\nindex 4394c741076..a4a05d9d145 100644\n--- a/add-patch.h\n+++ b/add-patch.h\n@@ -1,15 +1,45 @@\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 };\n \n-#define ADD_P_OPT_INIT { .context = -1, .interhunkcontext = -1 }\n+#define INTERACTIVE_OPTIONS_INIT { \\\n+\t.context = -1, \\\n+\t.interhunkcontext = -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+};\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@@ -20,7 +50,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 32709794b38..6f1e2130528 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,8 +256,8 @@ 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_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@@ -400,9 +400,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@@ -412,11 +412,11 @@ 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}\n \ndiff --git a/builtin/checkout.c b/builtin/checkout.c\nindex f9453473fe2..d230b1f8995 100644\n--- a/builtin/checkout.c\n+++ b/builtin/checkout.c\n@@ -546,7 +546,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};\n@@ -575,7 +575,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 0243f17d53c..640495cc57e 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@@ -356,9 +356,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@@ -407,7 +407,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@@ -432,9 +432,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@@ -1742,8 +1742,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 ed35802af15..088449e1209 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,8 +371,8 @@ 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_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@@ -423,9 +423,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@@ -436,12 +436,12 @@ 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}\n \ndiff --git a/builtin/stash.c b/builtin/stash.c\nindex 948eba06fbc..3b509052338 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,8 +1851,8 @@ 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_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@@ -1907,19 +1909,19 @@ 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}\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@@ -1944,7 +1946,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@@ -1952,8 +1954,8 @@ 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_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@@ -1973,20 +1975,20 @@ 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}\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 1d6e0c7518b..7b6e59d6c19 100644\n--- a/commit.h\n+++ b/commit.h\n@@ -258,7 +258,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.51.1.930.gacf6e81ea2.dirty\n\n"},{"id":"529723","messageId":"20251027-b4-pks-history-builtin-v6-8-407dd3f57ad3@pks.im","threadId":"63986","inReplyTo":"20251027-b4-pks-history-builtin-v6-0-407dd3f57ad3@pks.im","subject":"[PATCH v6 08/11] add-patch: remove dependency on \"add-interactive\" subsystem","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-27T11:33:56Z","receivedAt":"2025-10-27T11:34:27Z","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 | 70 ++++++++++++++++++++++++++++++++-----------------------------\n 1 file changed, 37 insertions(+), 33 deletions(-)\n\ndiff --git a/add-patch.c b/add-patch.c\nindex 5c6969927a..790c848e79 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@@ -408,7 +408,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@@ -417,9 +417,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@@ -437,7 +437,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@@ -542,12 +542,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@@ -576,9 +576,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@@ -811,7 +811,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@@ -833,7 +833,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@@ -1222,12 +1222,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@@ -1356,7 +1356,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@@ -1370,7 +1370,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@@ -1678,15 +1678,15 @@ static int patch_update_file(struct add_p_state *s,\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 \t\t\t\t\t\t: 1));\n \t\tprintf(_(s->mode->prompt_mode[prompt_mode_type]),\n \t\t       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\tbreak;\n@@ -1848,7 +1848,7 @@ static int patch_update_file(struct add_p_state *s,\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@@ -1866,7 +1866,7 @@ static int patch_update_file(struct add_p_state *s,\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@@ -1884,7 +1884,7 @@ static int patch_update_file(struct add_p_state *s,\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@@ -1904,7 +1904,7 @@ static int patch_update_file(struct add_p_state *s,\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\t   s->mode->is_reverse);\n@@ -1915,8 +1915,8 @@ static int patch_update_file(struct add_p_state *s,\n \t\t\t\t\t NULL, 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\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@@ -1929,11 +1929,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\n-- \n2.51.1.930.gacf6e81ea2.dirty\n\n"},{"id":"529725","messageId":"20251027-b4-pks-history-builtin-v6-9-407dd3f57ad3@pks.im","threadId":"63986","inReplyTo":"20251027-b4-pks-history-builtin-v6-0-407dd3f57ad3@pks.im","subject":"[PATCH v6 09/11] add-patch: add support for in-memory index patching","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-27T11:33:57Z","receivedAt":"2025-10-27T11:34:31Z","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 | 112 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++---\n add-patch.h |   8 +++++\n 2 files changed, 116 insertions(+), 4 deletions(-)\n\ndiff --git a/add-patch.c b/add-patch.c\nindex 790c848e799..9c1688bd5a0 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@@ -47,7 +49,7 @@ static struct patch_mode patch_mode_add = {\n \t\tN_(\"Stage mode change [y,n,q,a,d%s,?]? \"),\n \t\tN_(\"Stage deletion [y,n,q,a,d%s,?]? \"),\n \t\tN_(\"Stage addition [y,n,q,a,d%s,?]? \"),\n-\t\tN_(\"Stage this hunk [y,n,q,a,d%s,?]? \")\n+\t\tN_(\"Stage this hunk [y,n,q,a,d%s,?]? \"),\n \t},\n \t.edit_hunk_hint = N_(\"If the patch applies cleanly, the edited hunk \"\n \t\t\t     \"will immediately be marked for staging.\"),\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@@ -437,7 +441,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@@ -1904,7 +1908,7 @@ static int patch_update_file(struct add_p_state *s,\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\t   s->mode->is_reverse);\n@@ -1915,9 +1919,11 @@ static int patch_update_file(struct add_p_state *s,\n \t\t\t\t\t NULL, 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 \tputchar('\\n');\n@@ -1930,6 +1936,8 @@ int run_add_p(struct repository *r, enum add_p_mode mode,\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@@ -1988,3 +1996,99 @@ int run_add_p(struct repository *r, enum add_p_mode mode,\n \tadd_p_state_clear(&s);\n \treturn 0;\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+\tstruct strbuf parent_revision = STRBUF_INIT;\n+\tchar parent_tree_oid[GIT_MAX_HEXSZ + 1];\n+\tsize_t binary_count = 0;\n+\tstruct commit *commit;\n+\tint ret;\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+\tstrbuf_addf(&parent_revision, \"%s~\", revision);\n+\tmode.diff_cmd[0] = \"diff-tree\";\n+\tmode.diff_cmd[1] = \"-r\";\n+\tmode.diff_cmd[2] = parent_tree_oid;\n+\n+\tinteractive_config_init(&s.cfg, r, opts);\n+\n+\tif (parse_diff(&s, ps) < 0) {\n+\t\tret = -1;\n+\t\tgoto out;\n+\t}\n+\n+\tfor (size_t i = 0; i < s.file_diff_nr; i++) {\n+\t\tif (s.file_diff[i].binary && !s.file_diff[i].hunk_nr)\n+\t\t\tbinary_count++;\n+\t\telse if (patch_update_file(&s, s.file_diff + i))\n+\t\t\tbreak;\n+\t}\n+\n+\tif (s.file_diff_nr == 0) {\n+\t\terr(&s, _(\"No changes.\"));\n+\t\tret = -1;\n+\t\tgoto out;\n+\t}\n+\n+\tif (binary_count == s.file_diff_nr) {\n+\t\terr(&s, _(\"Only binary files changed.\"));\n+\t\tret = -1;\n+\t\tgoto out;\n+\t}\n+\n+\tret = 0;\n+\n+out:\n+\tstrbuf_release(&parent_revision);\n+\tadd_p_state_clear(&s);\n+\treturn ret;\n+}\ndiff --git a/add-patch.h b/add-patch.h\nindex a4a05d9d145..901c42fd7b6 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@@ -53,4 +54,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.51.1.930.gacf6e81ea2.dirty\n\n"},{"id":"529726","messageId":"20251027-b4-pks-history-builtin-v6-10-407dd3f57ad3@pks.im","threadId":"63986","inReplyTo":"20251027-b4-pks-history-builtin-v6-0-407dd3f57ad3@pks.im","subject":"[PATCH v6 10/11] cache-tree: allow writing in-memory index as tree","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-27T11:33:58Z","receivedAt":"2025-10-27T11:34:33Z","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       | 5 ++---\n cache-tree.h       | 3 ++-\n 3 files changed, 6 insertions(+), 5 deletions(-)\n\ndiff --git a/builtin/checkout.c b/builtin/checkout.c\nindex d230b1f8995..0b90f398feb 100644\n--- a/builtin/checkout.c\n+++ b/builtin/checkout.c\n@@ -902,7 +902,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 2aba47060e9..b67d0d703d2 100644\n--- a/cache-tree.c\n+++ b/cache-tree.c\n@@ -699,11 +699,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 \n@@ -723,7 +723,6 @@ struct tree* write_in_core_index_as_tree(struct repository *repo) {\n \treturn lookup_tree(repo, &index_state->cache_tree->oid);\n }\n \n-\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 {\n \tint entries, was_valid;\ndiff --git a/cache-tree.h b/cache-tree.h\nindex b82c4963e7c..f8bddae5235 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.51.1.930.gacf6e81ea2.dirty\n\n"},{"id":"529727","messageId":"20251027-b4-pks-history-builtin-v6-11-407dd3f57ad3@pks.im","threadId":"63986","inReplyTo":"20251027-b4-pks-history-builtin-v6-0-407dd3f57ad3@pks.im","subject":"[PATCH v6 11/11] builtin/history: implement \"split\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-10-27T11:33:59Z","receivedAt":"2025-10-27T11:34:37Z","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              | 218 +++++++++++++++++++++\n t/meson.build                  |   1 +\n t/t3452-history-split.sh       | 432 +++++++++++++++++++++++++++++++++++++++++\n 4 files changed, 713 insertions(+)\n\ndiff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc\nindex bd903875120..3d6b2665f8d 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>\n+git history split <commit> [--] [<pathspec>...]\n \n DESCRIPTION\n -----------\n@@ -37,6 +38,26 @@ Several 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 message of the new commit will be asked for by launching the\n+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 CONFIGURATION\n -------------\n \n@@ -44,6 +65,47 @@ include::includes/cmd-config-section-all.adoc[]\n \n include::config/sequencer.adoc[]\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,e,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,e,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 cb251ae2e01..cae841707d0 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-reach.h\"\n #include \"commit.h\"\n #include \"config.h\"\n@@ -8,17 +9,22 @@\n #include \"environment.h\"\n #include \"gettext.h\"\n #include \"hex.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 \"reset.h\"\n #include \"revision.h\"\n+#include \"run-command.h\"\n #include \"sequencer.h\"\n #include \"strvec.h\"\n #include \"tree.h\"\n #include \"wt-status.h\"\n \n #define GIT_HISTORY_REWORD_USAGE N_(\"git history reword <commit>\")\n+#define GIT_HISTORY_SPLIT_USAGE  N_(\"git history split <commit> [--] [<pathspec>...]\")\n \n static int collect_commits(struct repository *repo,\n \t\t\t   struct commit *old_commit,\n@@ -323,6 +329,216 @@ static int cmd_history_reword(int argc,\n \treturn ret;\n }\n \n+static int split_commit(struct repository *repo,\n+\t\t\tstruct commit *original_commit,\n+\t\t\tstruct pathspec *pathspec,\n+\t\t\tstruct object_id *out)\n+{\n+\tstruct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;\n+\tstruct strbuf index_file = STRBUF_INIT, split_message = STRBUF_INIT;\n+\tstruct child_process read_tree_cmd = CHILD_PROCESS_INIT;\n+\tstruct index_state index = INDEX_STATE_INIT(repo);\n+\tstruct object_id original_commit_tree_oid, parent_tree_oid;\n+\tconst char *original_message, *original_body, *ptr;\n+\tchar original_commit_oid[GIT_MAX_HEXSZ + 1];\n+\tchar *original_author = NULL;\n+\tstruct commit_list *parents = NULL;\n+\tstruct commit *first_commit;\n+\tstruct tree *split_tree;\n+\tsize_t len;\n+\tint ret;\n+\n+\tif (original_commit->parents)\n+\t\tparent_tree_oid = *get_commit_tree_oid(original_commit->parents->item);\n+\telse\n+\t\toidcpy(&parent_tree_oid, repo->hash_algo->empty_tree);\n+\toriginal_commit_tree_oid = *get_commit_tree_oid(original_commit);\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+\tread_tree_cmd.git_cmd = 1;\n+\tstrvec_pushf(&read_tree_cmd.env, \"GIT_INDEX_FILE=%s\", index_file.buf);\n+\tstrvec_push(&read_tree_cmd.args, \"read-tree\");\n+\tstrvec_push(&read_tree_cmd.args, oid_to_hex(&parent_tree_oid));\n+\tret = run_command(&read_tree_cmd);\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_commit->object.oid);\n+\tret = run_add_p_index(repo, &index, index_file.buf, &interactive_opts,\n+\t\t\t      original_commit_oid, pathspec);\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+\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/* We retain authorship of the original commit. */\n+\toriginal_message = repo_logmsg_reencode(repo, original_commit, NULL, NULL);\n+\tptr = find_commit_header(original_message, \"author\", &len);\n+\tif (ptr)\n+\t\toriginal_author = xmemdupz(ptr, len);\n+\n+\tret = fill_commit_message(repo, &parent_tree_oid, &split_tree->object.oid,\n+\t\t\t\t  \"\", \"split-out\", &split_message);\n+\tif (ret < 0)\n+\t\tgoto out;\n+\n+\tret = commit_tree(split_message.buf, split_message.len, &split_tree->object.oid,\n+\t\t\t  original_commit->parents, &out[0], original_author, NULL);\n+\tif (ret < 0) {\n+\t\tret = error(_(\"failed writing split-out commit\"));\n+\t\tgoto out;\n+\t}\n+\n+\t/*\n+\t* The second commit is much simpler to construct, as we can simply use\n+\t* the original commit details, except that we adjust its parent to be\n+\t* the newly split-out commit.\n+\t*/\n+\tfind_commit_subject(original_message, &original_body);\n+\tfirst_commit = lookup_commit_reference(repo, &out[0]);\n+\tcommit_list_append(first_commit, &parents);\n+\n+\tret = commit_tree(original_body, strlen(original_body), &original_commit_tree_oid,\n+\t\t\t  parents, &out[1], original_author, NULL);\n+\tif (ret < 0) {\n+\t\tret = error(_(\"failed writing second commit\"));\n+\t\tgoto out;\n+\t}\n+\n+\tret = 0;\n+\n+out:\n+\tif (index_file.len)\n+\t\tunlink(index_file.buf);\n+\tstrbuf_release(&split_message);\n+\tstrbuf_release(&index_file);\n+\tfree_commit_list(parents);\n+\tfree(original_author);\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+\tstruct option options[] = {\n+\t\tOPT_END(),\n+\t};\n+\tstruct oidmap rewritten_commits = OIDMAP_INIT;\n+\tstruct commit *original_commit, *parent, *head;\n+\tstruct strvec commits = STRVEC_INIT;\n+\tstruct commit_list *from_list = NULL;\n+\tstruct object_id split_commits[2];\n+\tstruct pathspec pathspec = { 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 revision\"));\n+\t\tgoto out;\n+\t}\n+\trepo_config(repo, git_default_config, NULL);\n+\n+\toriginal_commit = lookup_commit_reference_by_name(argv[0]);\n+\tif (!original_commit) {\n+\t\tret = error(_(\"commit to be split cannot be found: %s\"), argv[0]);\n+\t\tgoto out;\n+\t}\n+\n+\tparent = original_commit->parents ? original_commit->parents->item : NULL;\n+\tif (parent && repo_parse_commit(repo, parent)) {\n+\t\tret = error(_(\"unable to parse commit %s\"),\n+\t\t\t    oid_to_hex(&parent->object.oid));\n+\t\tgoto out;\n+\t}\n+\n+\thead = lookup_commit_reference_by_name(\"HEAD\");\n+\tif (!head) {\n+\t\tret = error(_(\"could not resolve HEAD to a commit\"));\n+\t\tgoto out;\n+\t}\n+\n+\tcommit_list_append(original_commit, &from_list);\n+\tif (!repo_is_descendant_of(repo, head, from_list)) {\n+\t\tret = error(_(\"split commit must be reachable from current HEAD commit\"));\n+\t\tgoto out;\n+\t}\n+\n+\tparse_pathspec(&pathspec, 0,\n+\t\tPATHSPEC_PREFER_FULL | PATHSPEC_SYMLINK_LEADING_PATH | PATHSPEC_PREFIX_ORIGIN,\n+\t\tprefix, argv + 1);\n+\n+\t/*\n+\t * Collect the list of commits that we'll have to reapply now already.\n+\t * This ensures that we'll abort early on in case the range of commits\n+\t * contains merges, which we do not yet handle.\n+\t */\n+\tret = collect_commits(repo, parent, head, &commits);\n+\tif (ret < 0)\n+\t\tgoto out;\n+\n+\t/*\n+\t * Then we split up the commit and replace the original commit with the\n+\t * new ones.\n+\t */\n+\tret = split_commit(repo, original_commit, &pathspec, split_commits);\n+\tif (ret < 0)\n+\t\tgoto out;\n+\n+\treplace_commits(&commits, &original_commit->object.oid,\n+\t\t\tsplit_commits, ARRAY_SIZE(split_commits));\n+\n+\tret = apply_commits(repo, &commits, parent, head, \"split\");\n+\tif (ret < 0)\n+\t\tgoto out;\n+\n+\tret = 0;\n+\n+out:\n+\toidmap_clear(&rewritten_commits, 0);\n+\tfree_commit_list(from_list);\n+\tclear_pathspec(&pathspec);\n+\tstrvec_clear(&commits);\n+\treturn ret;\n+}\n+\n int cmd_history(int argc,\n \t\tconst char **argv,\n \t\tconst char *prefix,\n@@ -330,11 +546,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 a3ec9199947..5d3014a768f 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -386,6 +386,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 00000000000..2aac28afdf0\n--- /dev/null\n+++ b/t/t3452-history-split.sh\n@@ -0,0 +1,432 @@\n+#!/bin/sh\n+\n+test_description='tests for git-history split subcommand'\n+\n+. ./test-lib.sh\n+\n+set_fake_editor () {\n+\twrite_script fake-editor.sh <<-EOF &&\n+\techo \"$@\" >\"\\$1\"\n+\tEOF\n+\ttest_set_editor \"$(pwd)\"/fake-editor.sh\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 rearrange commit history with merges\" err &&\n+\t\ttest_must_fail git history split HEAD~ 2>err &&\n+\t\ttest_grep \"cannot rearrange commit history with merges\" err\n+\t)\n+'\n+\n+test_expect_success 'refuses to work with unrelated 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\ttest_must_fail git history split ours 2>err &&\n+\t\ttest_grep \"split commit must be reachable from current HEAD commit\" err\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 \"split-out commit\" &&\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\tsplit-me\n+\t\tsplit-out commit\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+\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 \"split-out commit\" &&\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\troot\n+\t\tsplit-out commit\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 \"split-out commit\" &&\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\tsplit-me\n+\t\tsplit-out commit\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 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 \"split-out-commit\" &&\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+\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 \"split-out commit\" &&\n+\t\tgit history split HEAD <<-EOF &&\n+\t\tn\n+\t\ty\n+\t\tEOF\n+\n+\t\texpect_log <<-EOF &&\n+\t\tsplit-me\n+\t\tsplit-out commit\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 '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\tcp \"$1\" . &&\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\tcat >expect <<-EOF &&\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.\n+\t\t# Changes to be committed:\n+\t\t#\tnew file:   bar\n+\t\t#\n+\t\tEOF\n+\t\ttest_cmp expect COMMIT_EDITMSG &&\n+\n+\t\texpect_log <<-EOF\n+\t\tsplit-me\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 \"split-out commit\" &&\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 '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 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\twrite_script .git/hooks/prepare-commit-msg <<-EOF &&\n+\t\ttouch \"$(pwd)/hooks.log\"\n+\t\tEOF\n+\t\twrite_script .git/hooks/post-commit <<-EOF &&\n+\t\ttouch \"$(pwd)/hooks.log\"\n+\t\tEOF\n+\t\twrite_script .git/hooks/post-rewrite <<-EOF &&\n+\t\ttouch \"$(pwd)/hooks.log\"\n+\t\tEOF\n+\n+\t\tset_fake_editor \"split-out commit\" &&\n+\t\tgit history split HEAD <<-EOF &&\n+\t\ty\n+\t\tn\n+\t\tEOF\n+\n+\t\texpect_log <<-EOF &&\n+\t\tsplit-me\n+\t\tsplit-out commit\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\" &&\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\tEOF\n+\t\tgit status --porcelain >actual &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+test_done\n\n-- \n2.51.1.930.gacf6e81ea2.dirty\n\n"},{"id":"530611","messageId":"87tsyzm5cp.fsf@osv.gnss.ru","threadId":"63986","inReplyTo":"20251027-b4-pks-history-builtin-v6-0-407dd3f57ad3@pks.im","subject":"Re: [PATCH v6 00/11] Introduce git-history(1) command for easy history editing","fromName":"Sergey Organov","fromEmail":"sorganov@gmail.com","sentAt":"2025-11-12T19:13:42Z","receivedAt":"2025-11-12T19:13:46Z","isPatch":true,"sender":{"key":"sorganov@gmail.com","avatar":"https://avatars.githubusercontent.com/u/8501568?v=4"},"body":"Hi Patrick,\n\nPatrick Steinhardt <ps@pks.im> writes:\n\n> Hi,\n>\n> over recent months I've been playing around with Jujutsu quite\n> frequently. While I still prefer using Git, there's been a couple\n> features in it that I really like and that I'd like to have in Git, as\n> well.\n\n[disclaimer: I didn't follow all the discussions closely enough, so\nexcuse me if these things below have been already addressed]\n\nAre you aware of 'git revise', I wonder?\n\nhttps://github.com/mystor/git-revise\n\nIn particular, its \n\n git revise --interactive --edit\n\nfeature helps a lot in massive editing of commit messages. It creates\nsingle file with all the commit messages (similar to todo list), and\nthen applies all the changes you make to the file as it revises\nparticular commits. Very handy.\n\nIt'd be nice to have something like this in the mainstream Git.\n\nBTW, it has commit split feature as well, though I didn't use it myself.\n\nThanks,\n-- Sergey Organov\n"},{"id":"530818","messageId":"073e9a8c-3f6f-4e5c-862a-da59043eb16f@gmail.com","threadId":"63986","inReplyTo":"20251027-b4-pks-history-builtin-v6-2-407dd3f57ad3@pks.im","subject":"Re: [PATCH v6 02/11] replay: extract logic to pick commits","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2025-11-17T16:27:40Z","receivedAt":"2025-11-17T16:27:44Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"Hi Patrick\n\nOn 27/10/2025 11:33, Patrick Steinhardt wrote:\n> We're about to add a new git-history(1) command that will reuse some of\n> the same infrastructure as git-replay(1). To prepare for this, extract\n> the logic to pick a commit into a new \"replay.c\" file so that it can be\n> shared between both commands.\n> \n> Rename the function to have a \"replay_\" prefix to clearly indicate its\n> subsystem.\n\nI'm sorry it has taken me so long to get round to looking at this, I've \nbeen intending to read through this series ever since you re-rolled \nafter the contributor summit.\n\nThis patch looks good, the only changes to the moved code are to \nnamespace the function which become public. I'm very pleased to see us \nswitching to using the replay machinery.\n\nThanks\n\nPhillip\n\n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n>   Makefile         |   1 +\n>   builtin/replay.c | 110 ++--------------------------------------------------\n>   meson.build      |   1 +\n>   replay.c         | 115 +++++++++++++++++++++++++++++++++++++++++++++++++++++++\n>   replay.h         |  23 +++++++++++\n>   5 files changed, 143 insertions(+), 107 deletions(-)\n> \n> diff --git a/Makefile b/Makefile\n> index 1919d35bf3f..01c171b4f03 100644\n> --- a/Makefile\n> +++ b/Makefile\n> @@ -1261,6 +1261,7 @@ LIB_OBJS += reftable/tree.o\n>   LIB_OBJS += reftable/writer.o\n>   LIB_OBJS += remote.o\n>   LIB_OBJS += replace-object.o\n> +LIB_OBJS += replay.o\n>   LIB_OBJS += repo-settings.o\n>   LIB_OBJS += repository.o\n>   LIB_OBJS += rerere.o\n> diff --git a/builtin/replay.c b/builtin/replay.c\n> index bb0420dc992..e39824912cd 100644\n> --- a/builtin/replay.c\n> +++ b/builtin/replay.c\n> @@ -2,7 +2,6 @@\n>    * \"git replay\" builtin command\n>    */\n>   \n> -#define USE_THE_REPOSITORY_VARIABLE\n>   #define DISABLE_SIGN_COMPARE_WARNINGS\n>   \n>   #include \"git-compat-util.h\"\n> @@ -16,6 +15,7 @@\n>   #include \"object-name.h\"\n>   #include \"parse-options.h\"\n>   #include \"refs.h\"\n> +#include \"replay.h\"\n>   #include \"revision.h\"\n>   #include \"strmap.h\"\n>   #include <oidset.h>\n> @@ -26,13 +26,6 @@ enum ref_action_mode {\n>   \tREF_ACTION_PRINT\n>   };\n>   \n> -static const char *short_commit_name(struct repository *repo,\n> -\t\t\t\t     struct commit *commit)\n> -{\n> -\treturn repo_find_unique_abbrev(repo, &commit->object.oid,\n> -\t\t\t\t       DEFAULT_ABBREV);\n> -}\n> -\n>   static struct commit *peel_committish(struct repository *repo, const char *name)\n>   {\n>   \tstruct object *obj;\n> @@ -45,59 +38,6 @@ static struct commit *peel_committish(struct repository *repo, const char *name)\n>   \t\t\t\t\t\t  OBJ_COMMIT);\n>   }\n>   \n> -static char *get_author(const char *message)\n> -{\n> -\tsize_t len;\n> -\tconst char *a;\n> -\n> -\ta = find_commit_header(message, \"author\", &len);\n> -\tif (a)\n> -\t\treturn xmemdupz(a, len);\n> -\n> -\treturn NULL;\n> -}\n> -\n> -static struct commit *create_commit(struct repository *repo,\n> -\t\t\t\t    struct tree *tree,\n> -\t\t\t\t    struct commit *based_on,\n> -\t\t\t\t    struct commit *parent)\n> -{\n> -\tstruct object_id ret;\n> -\tstruct object *obj = NULL;\n> -\tstruct commit_list *parents = NULL;\n> -\tchar *author;\n> -\tchar *sign_commit = NULL; /* FIXME: cli users might want to sign again */\n> -\tstruct commit_extra_header *extra = NULL;\n> -\tstruct strbuf msg = STRBUF_INIT;\n> -\tconst char *out_enc = get_commit_output_encoding();\n> -\tconst char *message = repo_logmsg_reencode(repo, based_on,\n> -\t\t\t\t\t\t   NULL, out_enc);\n> -\tconst char *orig_message = NULL;\n> -\tconst char *exclude_gpgsig[] = { \"gpgsig\", NULL };\n> -\n> -\tcommit_list_insert(parent, &parents);\n> -\textra = read_commit_extra_headers(based_on, exclude_gpgsig);\n> -\tfind_commit_subject(message, &orig_message);\n> -\tstrbuf_addstr(&msg, orig_message);\n> -\tauthor = get_author(message);\n> -\treset_ident_date();\n> -\tif (commit_tree_extended(msg.buf, msg.len, &tree->object.oid, parents,\n> -\t\t\t\t &ret, author, NULL, sign_commit, extra)) {\n> -\t\terror(_(\"failed to write commit object\"));\n> -\t\tgoto out;\n> -\t}\n> -\n> -\tobj = parse_object(repo, &ret);\n> -\n> -out:\n> -\trepo_unuse_commit_buffer(the_repository, based_on, message);\n> -\tfree_commit_extra_headers(extra);\n> -\tfree_commit_list(parents);\n> -\tstrbuf_release(&msg);\n> -\tfree(author);\n> -\treturn (struct commit *)obj;\n> -}\n> -\n>   struct ref_info {\n>   \tstruct commit *onto;\n>   \tstruct strset positive_refs;\n> @@ -246,50 +186,6 @@ static void determine_replay_mode(struct repository *repo,\n>   \tstrset_clear(&rinfo.positive_refs);\n>   }\n>   \n> -static struct commit *mapped_commit(kh_oid_map_t *replayed_commits,\n> -\t\t\t\t    struct commit *commit,\n> -\t\t\t\t    struct commit *fallback)\n> -{\n> -\tkhint_t pos = kh_get_oid_map(replayed_commits, commit->object.oid);\n> -\tif (pos == kh_end(replayed_commits))\n> -\t\treturn fallback;\n> -\treturn kh_value(replayed_commits, pos);\n> -}\n> -\n> -static struct commit *pick_regular_commit(struct repository *repo,\n> -\t\t\t\t\t  struct commit *pickme,\n> -\t\t\t\t\t  kh_oid_map_t *replayed_commits,\n> -\t\t\t\t\t  struct commit *onto,\n> -\t\t\t\t\t  struct merge_options *merge_opt,\n> -\t\t\t\t\t  struct merge_result *result)\n> -{\n> -\tstruct commit *base, *replayed_base;\n> -\tstruct tree *pickme_tree, *base_tree;\n> -\n> -\tbase = pickme->parents->item;\n> -\treplayed_base = mapped_commit(replayed_commits, base, onto);\n> -\n> -\tresult->tree = repo_get_commit_tree(repo, replayed_base);\n> -\tpickme_tree = repo_get_commit_tree(repo, pickme);\n> -\tbase_tree = repo_get_commit_tree(repo, base);\n> -\n> -\tmerge_opt->branch1 = short_commit_name(repo, replayed_base);\n> -\tmerge_opt->branch2 = short_commit_name(repo, pickme);\n> -\tmerge_opt->ancestor = xstrfmt(\"parent of %s\", merge_opt->branch2);\n> -\n> -\tmerge_incore_nonrecursive(merge_opt,\n> -\t\t\t\t  base_tree,\n> -\t\t\t\t  result->tree,\n> -\t\t\t\t  pickme_tree,\n> -\t\t\t\t  result);\n> -\n> -\tfree((char*)merge_opt->ancestor);\n> -\tmerge_opt->ancestor = NULL;\n> -\tif (!result->clean)\n> -\t\treturn NULL;\n> -\treturn create_commit(repo, result->tree, pickme, replayed_base);\n> -}\n> -\n>   static int handle_ref_update(enum ref_action_mode mode,\n>   \t\t\t     struct ref_transaction *transaction,\n>   \t\t\t     const char *refname,\n> @@ -483,8 +379,8 @@ int cmd_replay(int argc,\n>   \t\tif (commit->parents->next)\n>   \t\t\tdie(_(\"replaying merge commits is not supported yet!\"));\n>   \n> -\t\tlast_commit = pick_regular_commit(repo, commit, replayed_commits,\n> -\t\t\t\t\t\t  onto, &merge_opt, &result);\n> +\t\tlast_commit = replay_pick_regular_commit(repo, commit, replayed_commits,\n> +\t\t\t\t\t\t\t onto, &merge_opt, &result);\n>   \t\tif (!last_commit)\n>   \t\t\tbreak;\n>   \n> diff --git a/meson.build b/meson.build\n> index cee94244759..ae8d4fef059 100644\n> --- a/meson.build\n> +++ b/meson.build\n> @@ -464,6 +464,7 @@ libgit_sources = [\n>     'reftable/writer.c',\n>     'remote.c',\n>     'replace-object.c',\n> +  'replay.c',\n>     'repo-settings.c',\n>     'repository.c',\n>     'rerere.c',\n> diff --git a/replay.c b/replay.c\n> new file mode 100644\n> index 00000000000..98be33b8545\n> --- /dev/null\n> +++ b/replay.c\n> @@ -0,0 +1,115 @@\n> +#define USE_THE_REPOSITORY_VARIABLE\n> +\n> +#include \"git-compat-util.h\"\n> +#include \"commit.h\"\n> +#include \"environment.h\"\n> +#include \"gettext.h\"\n> +#include \"ident.h\"\n> +#include \"object.h\"\n> +#include \"object-name.h\"\n> +#include \"replay.h\"\n> +#include \"tree.h\"\n> +\n> +static const char *short_commit_name(struct repository *repo,\n> +\t\t\t\t     struct commit *commit)\n> +{\n> +\treturn repo_find_unique_abbrev(repo, &commit->object.oid,\n> +\t\t\t\t       DEFAULT_ABBREV);\n> +}\n> +\n> +static char *get_author(const char *message)\n> +{\n> +\tsize_t len;\n> +\tconst char *a;\n> +\n> +\ta = find_commit_header(message, \"author\", &len);\n> +\tif (a)\n> +\t\treturn xmemdupz(a, len);\n> +\n> +\treturn NULL;\n> +}\n> +\n> +struct commit *replay_create_commit(struct repository *repo,\n> +\t\t\t\t    struct tree *tree,\n> +\t\t\t\t    struct commit *based_on,\n> +\t\t\t\t    struct commit *parent)\n> +{\n> +\tstruct object_id ret;\n> +\tstruct object *obj = NULL;\n> +\tstruct commit_list *parents = NULL;\n> +\tchar *author;\n> +\tchar *sign_commit = NULL; /* FIXME: cli users might want to sign again */\n> +\tstruct commit_extra_header *extra = NULL;\n> +\tstruct strbuf msg = STRBUF_INIT;\n> +\tconst char *out_enc = get_commit_output_encoding();\n> +\tconst char *message = repo_logmsg_reencode(repo, based_on,\n> +\t\t\t\t\t\t   NULL, out_enc);\n> +\tconst char *orig_message = NULL;\n> +\tconst char *exclude_gpgsig[] = { \"gpgsig\", NULL };\n> +\n> +\tcommit_list_insert(parent, &parents);\n> +\textra = read_commit_extra_headers(based_on, exclude_gpgsig);\n> +\tfind_commit_subject(message, &orig_message);\n> +\tstrbuf_addstr(&msg, orig_message);\n> +\tauthor = get_author(message);\n> +\treset_ident_date();\n> +\tif (commit_tree_extended(msg.buf, msg.len, &tree->object.oid, parents,\n> +\t\t\t\t &ret, author, NULL, sign_commit, extra)) {\n> +\t\terror(_(\"failed to write commit object\"));\n> +\t\tgoto out;\n> +\t}\n> +\n> +\tobj = parse_object(repo, &ret);\n> +\n> +out:\n> +\trepo_unuse_commit_buffer(the_repository, based_on, message);\n> +\tfree_commit_extra_headers(extra);\n> +\tfree_commit_list(parents);\n> +\tstrbuf_release(&msg);\n> +\tfree(author);\n> +\treturn (struct commit *)obj;\n> +}\n> +\n> +static struct commit *mapped_commit(kh_oid_map_t *replayed_commits,\n> +\t\t\t\t    struct commit *commit,\n> +\t\t\t\t    struct commit *fallback)\n> +{\n> +\tkhint_t pos = kh_get_oid_map(replayed_commits, commit->object.oid);\n> +\tif (pos == kh_end(replayed_commits))\n> +\t\treturn fallback;\n> +\treturn kh_value(replayed_commits, pos);\n> +}\n> +\n> +struct commit *replay_pick_regular_commit(struct repository *repo,\n> +\t\t\t\t\t  struct commit *pickme,\n> +\t\t\t\t\t  kh_oid_map_t *replayed_commits,\n> +\t\t\t\t\t  struct commit *onto,\n> +\t\t\t\t\t  struct merge_options *merge_opt,\n> +\t\t\t\t\t  struct merge_result *result)\n> +{\n> +\tstruct commit *base, *replayed_base;\n> +\tstruct tree *pickme_tree, *base_tree;\n> +\n> +\tbase = pickme->parents->item;\n> +\treplayed_base = mapped_commit(replayed_commits, base, onto);\n> +\n> +\tresult->tree = repo_get_commit_tree(repo, replayed_base);\n> +\tpickme_tree = repo_get_commit_tree(repo, pickme);\n> +\tbase_tree = repo_get_commit_tree(repo, base);\n> +\n> +\tmerge_opt->branch1 = short_commit_name(repo, replayed_base);\n> +\tmerge_opt->branch2 = short_commit_name(repo, pickme);\n> +\tmerge_opt->ancestor = xstrfmt(\"parent of %s\", merge_opt->branch2);\n> +\n> +\tmerge_incore_nonrecursive(merge_opt,\n> +\t\t\t\t  base_tree,\n> +\t\t\t\t  result->tree,\n> +\t\t\t\t  pickme_tree,\n> +\t\t\t\t  result);\n> +\n> +\tfree((char*)merge_opt->ancestor);\n> +\tmerge_opt->ancestor = NULL;\n> +\tif (!result->clean)\n> +\t\treturn NULL;\n> +\treturn replay_create_commit(repo, result->tree, pickme, replayed_base);\n> +}\n> diff --git a/replay.h b/replay.h\n> new file mode 100644\n> index 00000000000..d6535ee56c9\n> --- /dev/null\n> +++ b/replay.h\n> @@ -0,0 +1,23 @@\n> +#ifndef REPLAY_H\n> +#define REPLAY_H\n> +\n> +#include \"khash.h\"\n> +#include \"merge-ort.h\"\n> +#include \"repository.h\"\n> +\n> +struct commit;\n> +struct tree;\n> +\n> +struct commit *replay_create_commit(struct repository *repo,\n> +\t\t\t\t    struct tree *tree,\n> +\t\t\t\t    struct commit *based_on,\n> +\t\t\t\t    struct commit *parent);\n> +\n> +struct commit *replay_pick_regular_commit(struct repository *repo,\n> +\t\t\t\t\t  struct commit *pickme,\n> +\t\t\t\t\t  kh_oid_map_t *replayed_commits,\n> +\t\t\t\t\t  struct commit *onto,\n> +\t\t\t\t\t  struct merge_options *merge_opt,\n> +\t\t\t\t\t  struct merge_result *result);\n> +\n> +#endif\n> \n\n"},{"id":"530819","messageId":"b36a644e-2d8d-41cf-b59b-e72ce0ca855c@gmail.com","threadId":"63986","inReplyTo":"20251027-b4-pks-history-builtin-v6-5-407dd3f57ad3@pks.im","subject":"Re: [PATCH v6 05/11] builtin/history: implement \"reword\" subcommand","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2025-11-17T16:27:59Z","receivedAt":"2025-11-17T16:28:02Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"Hi Patrick\nOn 27/10/2025 11:33, Patrick Steinhardt wrote:\n\n> +static int collect_commits(struct repository *repo,\n> +\t\t\t   struct commit *old_commit,\n> +\t\t\t   struct commit *new_commit,\n> +\t\t\t   struct strvec *out)\n\nNow that we're not using the sequencer it would be nice to stop messing \nabout converting object ids to and from strings and return an array of \n\"struct commit\" instead of \"struct strvec\"\n  > +{\n> +\tstruct setup_revision_opt revision_opts = {\n> +\t\t.assume_dashdash = 1,\n> +\t};\n> +\tstruct strvec revisions = STRVEC_INIT;\n> +\tstruct commit *child;\n> +\tstruct rev_info rev = { 0 };\n> +\tint ret;\n> +\n> +\trepo_init_revisions(repo, &rev, NULL);\n> +\tstrvec_push(&revisions, \"\");\n> +\tstrvec_push(&revisions, oid_to_hex(&new_commit->object.oid));\n> +\tif (old_commit)\n> +\t\tstrvec_pushf(&revisions, \"^%s\", oid_to_hex(&old_commit->object.oid));\n> +\n> +\tsetup_revisions_from_strvec(&revisions, &rev, &revision_opts);\n> +\tif (revisions.nr != 1 || prepare_revision_walk(&rev)) {\n\nI'm not that familiar with the revision walking api, what 'revisions.nr \n!= 1' check for here?\n\n> +static int apply_commits(struct repository *repo,\n> +\t\t\t const struct strvec *commits,\n> +\t\t\t struct commit *onto,\n> +\t\t\t struct commit *orig_head,\n> +\t\t\t const char *action)\n> +{\n> +\tstruct reset_head_opts reset_opts = { 0 };\n> +\tstruct strbuf buf = STRBUF_INIT;\n> +\tint ret;\n> +\n> +\tfor (size_t i = 0; i < commits->nr; i++) {\n> +\t\tstruct object_id commit_id;\n> +\t\tstruct commit *commit;\n> +\t\tconst char *end;\n> +\n> +\t\tif (parse_oid_hex_algop(commits->v[i], &commit_id, &end,\n> +\t\t\t\t\trepo->hash_algo)) {\n> +\t\t\tret = error(_(\"invalid object ID: %s\"), commits->v[i]);\n> +\t\t\tgoto out;\n> +\t\t}\n> +\t\tcommit = lookup_commit(repo, &commit_id);\n> +\t\tif (!commit || repo_parse_commit(repo, commit)) {\n> +\t\t\tret = error(_(\"failed to look up commit: %s\"), oid_to_hex(&commit_id));\n> +\t\t\tgoto out;\n> +\t\t}\n\n\nUsing an array of \"struct commit\" rather than \"struct strvec\" would mean \nwe could delete everything in the loop body up to here.\n\n> +\t\tif (!onto) {\n> +\t\t\tonto = commit;\n> +\t\t} else {\n> +\t\t\tstruct tree *tree = repo_get_commit_tree(repo, commit);\n> +\t\t\tonto = replay_create_commit(repo, tree, commit, onto);\n> +\t\t\tif (!onto)\n> +\t\t\t\tbreak;\n\nDon't we want to avoid updating HEAD if replay_create_commit() fails?\n\n> +\t\t}\n> +\t}\n> +\n> +\treset_opts.oid = &onto->object.oid;\n> +\tstrbuf_addf(&buf, \"%s: switch to rewritten %s\", action, oid_to_hex(reset_opts.oid));\n\nWe're not switching branches so I wonder if saying \"history: <action> \n<oid> <commit subject>\" might be a more useful reflog entry\n\n> +static int fill_commit_message(struct repository *repo,\n> +\t\t\t       const struct object_id *old_tree,\n> +\t\t\t       const struct object_id *new_tree,\n> +\t\t\t       const char *default_message,\n> +\t\t\t       const char *action,\n> +\t\t\t       struct strbuf *out)\n> +{\n> +\tconst char *path = git_path_commit_editmsg();\n> +\tconst char *hint =\n> +\t\t_(\"Please enter the commit message for the %s changes.\"\n\nMaybe \"Please edit the commit message\"? Also do we want to tell the user \nthey can abort by clearing the commit message?\n\n> +\t\t  \" Lines starting\\nwith '%s' will be ignored.\\n\");\n> +\tstruct wt_status s;\n> +\n> +\tstrbuf_addstr(out, default_message);\n> +\tstrbuf_addch(out, '\\n');\n> +\tstrbuf_commented_addf(out, comment_line_str, hint, action, comment_line_str);\n> +\twrite_file_buf(path, out->buf, out->len);\n> +\n> +\twt_status_prepare(repo, &s);\n> +\tFREE_AND_NULL(s.branch);\n> +\ts.ahead_behind_flags = AHEAD_BEHIND_QUICK;\n> +\ts.commit_template = 1;\n> +\ts.colopts = 0;\n> +\ts.display_comment_prefix = 1;\n> +\ts.hints = 0;\n> +\ts.use_color = 0;\n> +\ts.whence = FROM_COMMIT;\n> +\ts.committable = 1;\n\n\"git commit\" reads a load of status related config settings, is any of \nthat relevant here?\n\n> +\ts.fp = fopen(git_path_commit_editmsg(), \"a\");\n> +\tif (!s.fp)\n> +\t\treturn error_errno(_(\"could not open '%s'\"), git_path_commit_editmsg());\n> +\n> +\twt_status_collect_changes_trees(&s, old_tree, new_tree);\n> +\twt_status_print(&s);\n> +\twt_status_collect_free_buffers(&s);\n> +\tstring_list_clear_func(&s.change, change_data_free);\n> +\n> +\tstrbuf_reset(out);\n> +\tif (launch_editor(path, out, NULL)) {\n> +\t\tfprintf(stderr, _(\"Please supply the message using the -m option.\\n\"));\n\nI'm not sure that's a very helpful suggestion as we don't support \"-m\" \n(it's not very helpful when \"git commit --amend\" suggests it either). We \nshould just give up if the editor fails.\n\n> +static int cmd_history_reword(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_REWORD_USAGE,\n> +\t\tNULL,\n> +\t};\n> +\tstruct option options[] = {\n> +\t\tOPT_END(),\n> +\t};\n> +\tstruct strbuf final_message = STRBUF_INIT;\n> +\tstruct commit *original_commit, *parent, *head;\n> +\tstruct strvec commits = STRVEC_INIT;\n> +\tstruct object_id parent_tree_oid, original_commit_tree_oid;\n> +\tstruct object_id rewritten_commit;\n> +\tstruct commit_list *from_list = NULL;\n> +\tconst char *original_message, *original_body, *ptr;\n> +\tchar *original_author = NULL;\n> +\tsize_t len;\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 single revision\"));\n> +\t\tgoto out;\n> +\t}\n> +\trepo_config(repo, git_default_config, NULL);\n> +\n> +\toriginal_commit = lookup_commit_reference_by_name(argv[0]);\n> +\tif (!original_commit) {\n> +\t\tret = error(_(\"commit to be reworded cannot be found: %s\"), argv[0]);\n> +\t\tgoto out;\n> +\t}\n> +\toriginal_commit_tree_oid = repo_get_commit_tree(repo, original_commit)->object.oid;\n\nLooking at the implementation of repo_get_commit_tree() it can return NULL\n\n> +\n> +\tparent = original_commit->parents ? original_commit->parents->item : NULL;\n> +\tif (parent) {\n> +\t\tif (repo_parse_commit(repo, parent)) {\n> +\t\t\tret = error(_(\"unable to parse commit %s\"),\n> +\t\t\t\t    oid_to_hex(&parent->object.oid));\n> +\t\t\tgoto out;\n> +\t\t}\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> +\thead = lookup_commit_reference_by_name(\"HEAD\");\n> +\tif (!head) {\n> +\t\tret = error(_(\"could not resolve HEAD to a commit\"));\n> +\t\tgoto out;\n> +\t}\n> +\n> +\tcommit_list_append(original_commit, &from_list);\n> +\tif (!repo_is_descendant_of(repo, head, from_list)) {\n> +\t\tret = error (_(\"split commit must be reachable from current HEAD commit\"));\n\ns/split/reword/\n\n> +\t\tgoto out;\n> +\t}\n> +\n> +\t/*\n> +\t * Collect the list of commits that we'll have to reapply now already.\n> +\t * This ensures that we'll abort early on in case the range of commits\n> +\t * contains merges, which we do not yet handle.\n> +\t */\n\nAh, I wondered if we should be checking that the reworded commit wasn't \na merge when we look at the parents above but we don't need to because \nwe will error out here if it is.\n\n> diff --git a/t/t3450-history.sh b/t/t3450-history.sh\n> index 417c343d43b..f513463b92b 100755\n> --- a/t/t3450-history.sh\n> +++ b/t/t3450-history.sh\n> @@ -5,13 +5,13 @@ test_description='tests for git-history command'\n>   . ./test-lib.sh\n>   \n>   test_expect_success 'does nothing without any arguments' '\n> -\tgit history >out 2>&1 &&\n> -\ttest_must_be_empty out\n> +\ttest_must_fail git history 2>err &&\n> +\ttest_grep \"need a subcommand\" err\n>   '\n>   \n>   test_expect_success 'raises an error with unknown argument' '\n>   \ttest_must_fail git history garbage 2>err &&\n> -\ttest_grep \"unrecognized argument: garbage\" err\n> +\ttest_grep \"unknown subcommand: .garbage.\" err\n>   '\n>   \n>   test_done\n\nDo we really need a separate test file just for a couple of tests. I can \nsee that having a separate test file for each subcommand makes sense but \ncan't we just add these two tests to one of those?\n\n> diff --git a/t/t3451-history-reword.sh b/t/t3451-history-reword.sh\n> new file mode 100755\n> index 00000000000..09dbc463c59\n> --- /dev/null\n> +++ b/t/t3451-history-reword.sh\n> @@ -0,0 +1,237 @@\n> +#!/bin/sh\n> +\n> +test_description='tests for git-history reword subcommand'\n> +\n> +. ./test-lib.sh\n> +\n> +reword_with_message () {\n> +\tcat >message &&\n> +\twrite_script fake-editor.sh <<-EOF &&\n> +\tcp \"$(pwd)/message\" \"\\$1\"\n\nLet's hope $(pwd) doesn't contain any dollar signs, backticks, \nbackslashes or double quotes. Doing\n\n\texport MSG_PATH=\"$(pwd)/message\"\n\twrite_script fake-editor.sh <<-\\EOF &&\n\tcp \"$MSG_PATH\" \"$1\"\n\tEOF\n\nwould be safer\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\nDo we really need to set up a separate repo for each test? The test \nsuite is slow enough already without running \"git init\" followed by a \nbunch calls to test_commit() in each test. Can we instead run \"git reset \n--hard <known-starting-point> at the beginning of each test? That \nremoves any interdependence between tests but saves a bunch of processes.\n> +test_expect_success 'can reword root commit' '\n\nIt's nice that unlike \"git rebase\" this works without much effort from \nthe implementation.\n\n> +test_expect_success 'can use editor to rewrite commit message' '\n\nDon't all the other tests check that? This test is checking what's \npresented to the user which is a good idea but I wouldn't have guessed \nthat from the test name.\n\n> +\ttest_when_finished \"rm -rf repo\" &&\n> +\tgit init repo &&\n> +\t(\n> +\t\tcd repo &&\n> +\t\ttest_commit first &&\n> +\n> +\t\twrite_script fake-editor.sh <<-\\EOF &&\n> +\t\tcp \"$1\" . &&\n> +\t\tprintf \"\\namend a comment\\n\" >>\"$1\"\n> +\t\tEOF\n> +\t\ttest_set_editor \"$(pwd)\"/fake-editor.sh &&\n> +\t\tgit history reword HEAD &&\n> +\n> +\t\tcat >expect <<-EOF &&\n> +\t\tfirst\n> +\n> +\t\t# Please enter the commit message for the reworded changes. Lines starting\n> +\t\t# with ${SQ}#${SQ} will be ignored.\n> +\t\t# Changes to be committed:\n> +\t\t#\tnew file:   first.t\n> +\t\t#\n> +\t\tEOF\n> +\t\ttest_cmp expect COMMIT_EDITMSG &&\n> +\n> +\t\tcat >expect <<-EOF &&\n> +\t\tfirst\n> +\n> +\t\tamend a comment\n> +\n> +\t\tEOF\n> +\t\tgit log --format=%B >actual &&\n> +\t\ttest_cmp expect actual\n\nWe have test_commit_message() to do this which will accept the expected \nmessage on stdin.\n\n> +\t)\n> +'\n> +\n> +# For now, git-history(1) does not yet execute any hooks. This is subject to\n> +# change in the future, and if it does this test here is expected to start\n> +# failing. In other words, this test is not an endorsement of the current\n> +# status quo.\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\ttest_commit first &&\n> +\t\ttest_commit second &&\n> +\t\ttest_commit third &&\n> +\n> +\t\twrite_script .git/hooks/prepare-commit-msg <<-EOF &&\n> +\t\ttouch \"$(pwd)/hooks.log\n\nThis has the same problem of expanding $(pwd) as fake-editor.sh. For \ndebugging it would be nicer if the hook scripts did\n\n\techo \"$hook_name\" >>hooks.log\n\nso we can easily see which hooks are causing the test to fail.\n\n> +\t\tEOF\n> +\t\twrite_script .git/hooks/post-commit <<-EOF &&\n> +\t\ttouch \"$(pwd)/hooks.log\n> +\t\tEOF\n> +\t\twrite_script .git/hooks/post-rewrite <<-EOF &&\n> +\t\ttouch \"$(pwd)/hooks.log\n> +\t\tEOF\n\nThis is good idea. We should add tests for the \"pre-commit\" and \n\"commit-msg\" hooks as well.\n\nOverall the test coverage looks good, the only thing we might want to \nadd is a check for the reflog message. Thanks for working on this, I'll \ntry and look at the rest of the patches sometime this week.\n\nPhillip\n\n"},{"id":"530820","messageId":"cca1e594-6685-4f9e-a57d-8419ccc6fa68@gmail.com","threadId":"63986","inReplyTo":"20251027-b4-pks-history-builtin-v6-4-407dd3f57ad3@pks.im","subject":"Re: [PATCH v6 04/11] builtin: add new \"history\" command","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2025-11-17T16:28:06Z","receivedAt":"2025-11-17T16:28:09Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"Hi Patrick\n\nOn 27/10/2025 11:33, Patrick Steinhardt wrote:\n> When rewriting history via git-rebase(1) there are a couple of very\n\nThere's more than a couple of items in this list, s/couple of/few/?\n\n> common use cases:\n> \n>    - The ordering of two commits should be reversed.\n> \n>    - A commit should be split up into two commits.\n> \n>    - A commit should be dropped from the history completely.\n> \n>    - Multiple commits should be squashed into one.\n\nI'd add editing an existing commit to this list, even if we don't \nimplement it initially\n\n> diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc\n> new file mode 100644\n> index 00000000000..6bdfeb50e8b\n> --- /dev/null\n> +++ b/Documentation/git-history.adoc\n> @@ -0,0 +1,44 @@\n> +git-history(1)\n> +==============\n> +\n> +NAME\n> +----\n> +git-history - EXPERIMENTAL: Rewrite history of the current branch\n> +\n> +SYNOPSIS\n> +--------\n> +[synopsis]\n> +git history [<options>]\n> +\n> +DESCRIPTION\n> +-----------\n> +\n> +Rewrite history by rearranging or modifying specific commits in the\n> +history.\n> +\n> +THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.\n\nExcellent - keeping our options open is a very good idea\n\n> +This command is similar to linkgit:git-rebase[1] and uses the same\n> +underlying machinery.\n\nThis isn't strictly true now that we're baisg \"git history\" on the \nreplay machinery.\n\n> You should use rebases if you want to reapply a range of\n> +commits onto a different base, or interactive rebases if you want to edit a\n> +range of commits.\n> +\n> +Note that this command does not (yet) work with histories that contain\n> +merges. You should use linkgit:git-rebase[1] with the `--rebase-merges`\n> +flag instead.\n> +\n> +COMMANDS\n> +--------\n> +\n> +Several commands are available to rewrite history in different ways:\n> +\n> +CONFIGURATION\n> +-------------\n> +\n> +include::includes/cmd-config-section-all.adoc[]\n> +\n> +include::config/sequencer.adoc[]\n\nThis probably isn't relevant now we're not using the sequencer.\n\nThanks\n\nPhillip\n\n> +\n> +GIT\n> +---\n> +Part of the linkgit:git[1] suite\n> diff --git a/Documentation/meson.build b/Documentation/meson.build\n> index 9d24f2da544..d1f6bde7c16 100644\n> --- a/Documentation/meson.build\n> +++ b/Documentation/meson.build\n> @@ -64,6 +64,7 @@ manpages = {\n>     'git-gui.adoc' : 1,\n>     'git-hash-object.adoc' : 1,\n>     'git-help.adoc' : 1,\n> +  'git-history.adoc' : 1,\n>     'git-hook.adoc' : 1,\n>     'git-http-backend.adoc' : 1,\n>     'git-http-fetch.adoc' : 1,\n> diff --git a/Makefile b/Makefile\n> index 01c171b4f03..1380ee1e196 100644\n> --- a/Makefile\n> +++ b/Makefile\n> @@ -1395,6 +1395,7 @@ BUILTIN_OBJS += builtin/get-tar-commit-id.o\n>   BUILTIN_OBJS += builtin/grep.o\n>   BUILTIN_OBJS += builtin/hash-object.o\n>   BUILTIN_OBJS += builtin/help.o\n> +BUILTIN_OBJS += builtin/history.o\n>   BUILTIN_OBJS += builtin/hook.o\n>   BUILTIN_OBJS += builtin/index-pack.o\n>   BUILTIN_OBJS += builtin/init-db.o\n> diff --git a/builtin.h b/builtin.h\n> index 1b35565fbd9..93c91d07d4b 100644\n> --- a/builtin.h\n> +++ b/builtin.h\n> @@ -172,6 +172,7 @@ int cmd_get_tar_commit_id(int argc, const char **argv, const char *prefix, struc\n>   int cmd_grep(int argc, const char **argv, const char *prefix, struct repository *repo);\n>   int cmd_hash_object(int argc, const char **argv, const char *prefix, struct repository *repo);\n>   int cmd_help(int argc, const char **argv, const char *prefix, struct repository *repo);\n> +int cmd_history(int argc, const char **argv, const char *prefix, struct repository *repo);\n>   int cmd_hook(int argc, const char **argv, const char *prefix, struct repository *repo);\n>   int cmd_index_pack(int argc, const char **argv, const char *prefix, struct repository *repo);\n>   int cmd_init_db(int argc, const char **argv, const char *prefix, struct repository *repo);\n> diff --git a/builtin/history.c b/builtin/history.c\n> new file mode 100644\n> index 00000000000..f6fe32610b0\n> --- /dev/null\n> +++ b/builtin/history.c\n> @@ -0,0 +1,22 @@\n> +#include \"builtin.h\"\n> +#include \"gettext.h\"\n> +#include \"parse-options.h\"\n> +\n> +int cmd_history(int argc,\n> +\t\tconst char **argv,\n> +\t\tconst char *prefix,\n> +\t\tstruct repository *repo UNUSED)\n> +{\n> +\tconst char * const usage[] = {\n> +\t\tN_(\"git history [<options>]\"),\n> +\t\tNULL,\n> +\t};\n> +\tstruct option options[] = {\n> +\t\tOPT_END(),\n> +\t};\n> +\n> +\targc = parse_options(argc, argv, prefix, options, usage, 0);\n> +\tif (argc)\n> +\t\tusagef(\"unrecognized argument: %s\", argv[0]);\n> +\treturn 0;\n> +}\n> diff --git a/command-list.txt b/command-list.txt\n> index accd3d0c4b5..f9005cf4597 100644\n> --- a/command-list.txt\n> +++ b/command-list.txt\n> @@ -115,6 +115,7 @@ git-grep                                mainporcelain           info\n>   git-gui                                 mainporcelain\n>   git-hash-object                         plumbingmanipulators\n>   git-help                                ancillaryinterrogators          complete\n> +git-history                             mainporcelain           history\n>   git-hook                                purehelpers\n>   git-http-backend                        synchingrepositories\n>   git-http-fetch                          synchelpers\n> diff --git a/git.c b/git.c\n> index c5fad56813f..744cb6527e0 100644\n> --- a/git.c\n> +++ b/git.c\n> @@ -586,6 +586,7 @@ static struct cmd_struct commands[] = {\n>   \t{ \"grep\", cmd_grep, RUN_SETUP_GENTLY },\n>   \t{ \"hash-object\", cmd_hash_object },\n>   \t{ \"help\", cmd_help },\n> +\t{ \"history\", cmd_history, RUN_SETUP },\n>   \t{ \"hook\", cmd_hook, RUN_SETUP },\n>   \t{ \"index-pack\", cmd_index_pack, RUN_SETUP_GENTLY | NO_PARSEOPT },\n>   \t{ \"init\", cmd_init_db },\n> diff --git a/meson.build b/meson.build\n> index ae8d4fef059..2d789612a01 100644\n> --- a/meson.build\n> +++ b/meson.build\n> @@ -604,6 +604,7 @@ builtin_sources = [\n>     'builtin/grep.c',\n>     'builtin/hash-object.c',\n>     'builtin/help.c',\n> +  'builtin/history.c',\n>     'builtin/hook.c',\n>     'builtin/index-pack.c',\n>     'builtin/init-db.c',\n> diff --git a/t/meson.build b/t/meson.build\n> index 401b24e50e0..019435918fa 100644\n> --- a/t/meson.build\n> +++ b/t/meson.build\n> @@ -384,6 +384,7 @@ integration_tests = [\n>     't3436-rebase-more-options.sh',\n>     't3437-rebase-fixup-options.sh',\n>     't3438-rebase-broken-files.sh',\n> +  't3450-history.sh',\n>     't3500-cherry.sh',\n>     't3501-revert-cherry-pick.sh',\n>     't3502-cherry-pick-merge.sh',\n> diff --git a/t/t3450-history.sh b/t/t3450-history.sh\n> new file mode 100755\n> index 00000000000..417c343d43b\n> --- /dev/null\n> +++ b/t/t3450-history.sh\n> @@ -0,0 +1,17 @@\n> +#!/bin/sh\n> +\n> +test_description='tests for git-history command'\n> +\n> +. ./test-lib.sh\n> +\n> +test_expect_success 'does nothing without any arguments' '\n> +\tgit history >out 2>&1 &&\n> +\ttest_must_be_empty out\n> +'\n> +\n> +test_expect_success 'raises an error with unknown argument' '\n> +\ttest_must_fail git history garbage 2>err &&\n> +\ttest_grep \"unrecognized argument: garbage\" err\n> +'\n> +\n> +test_done\n> \n\n"},{"id":"531039","messageId":"CABPp-BGBWaXOtt1_TvkJDAuLxVNC2UAvM+g89avPRBK9Wrb5RA@mail.gmail.com","threadId":"63986","inReplyTo":"073e9a8c-3f6f-4e5c-862a-da59043eb16f@gmail.com","subject":"Re: [PATCH v6 02/11] replay: extract logic to pick commits","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2025-11-20T07:01:13Z","receivedAt":"2025-11-20T07:01:26Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Mon, Nov 17, 2025 at 8:27 AM Phillip Wood <phillip.wood123@gmail.com> wrote:\n>\n> Hi Patrick\n>\n> On 27/10/2025 11:33, Patrick Steinhardt wrote:\n> > We're about to add a new git-history(1) command that will reuse some of\n> > the same infrastructure as git-replay(1). To prepare for this, extract\n> > the logic to pick a commit into a new \"replay.c\" file so that it can be\n> > shared between both commands.\n> >\n> > Rename the function to have a \"replay_\" prefix to clearly indicate its\n> > subsystem.\n>\n> I'm sorry it has taken me so long to get round to looking at this, I've\n> been intending to read through this series ever since you re-rolled\n> after the contributor summit.\n>\n> This patch looks good, the only changes to the moved code are to\n> namespace the function which become public. I'm very pleased to see us\n> switching to using the replay machinery.\n\nThe _two_ functions which become public; otherwise, agreed with\nPhillip's review comments.\n"},{"id":"531040","messageId":"CABPp-BH7E1Bh2g0vR3T4NEsv34DvFQPzMuJSsqtOAaWY-fFCxg@mail.gmail.com","threadId":"63986","inReplyTo":"20251027-b4-pks-history-builtin-v6-3-407dd3f57ad3@pks.im","subject":"Re: [PATCH v6 03/11] replay: stop using `the_repository`","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2025-11-20T07:01:29Z","receivedAt":"2025-11-20T07:01:41Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Mon, Oct 27, 2025 at 4:34 AM Patrick Steinhardt <ps@pks.im> wrote:\n>\n> In `create_commit()` we're using `the_repository` even though we already\n> have a repository passed to use as an argument. Fix this.\n\nI feel like I've fixed this multiple times, but it keeps coming back.\nIn fact, I have this same fix locally in my replay-edit work.  Thanks\nfor fixing it.\n\n>\n> Note that we still cannot get rid of `USE_THE_REPOSITORY_VARIABLE`. This\n> is because we use `DEFAULT_ABBREV and `get_commit_output_encoding()`,\n> both of which are stored as global variables that can be modified via\n> the Git configuration.\n\nIndeed.\n\nGoing on a tangent for a second...I feel like I've had to remove\n\"the_repository\" from builtin/replay.c multiple times.  In my local\nreplay-edit work, I actually added a \"#define the_repository\nDO_NOT_USE_THE_REPOSITORY\" in builtin/replay.c, after all the header\nincludes, because the_repository isn't what builtin/replay.c is using,\nit's these other two things that are also only included if\nUSE_THE_REPOSITORY_VARIABALE is defined.  That obviously doesn't need\nto be part of your series, but what would you think if I were to\nsubmit that?  Is it too ugly/weird of a way to avoid the_repository\nbeing added back to builtin/replay.c so we can stop having to remove\nit again?\n\n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n>  replay.c | 2 +-\n>  1 file changed, 1 insertion(+), 1 deletion(-)\n>\n> diff --git a/replay.c b/replay.c\n> index 98be33b854..58fdc20140 100644\n> --- a/replay.c\n> +++ b/replay.c\n> @@ -62,7 +62,7 @@ struct commit *replay_create_commit(struct repository *repo,\n>         obj = parse_object(repo, &ret);\n>\n>  out:\n> -       repo_unuse_commit_buffer(the_repository, based_on, message);\n> +       repo_unuse_commit_buffer(repo, based_on, message);\n>         free_commit_extra_headers(extra);\n>         free_commit_list(parents);\n>         strbuf_release(&msg);\n>\n> --\n> 2.51.1.930.gacf6e81ea2.dirty\n\nPatch looks good.\n"},{"id":"531041","messageId":"CABPp-BGSYKPB7met4U_4ECNgA9vb5y_AWnMWfpiisOHnd_OyhQ@mail.gmail.com","threadId":"63986","inReplyTo":"20251027-b4-pks-history-builtin-v6-4-407dd3f57ad3@pks.im","subject":"Re: [PATCH v6 04/11] builtin: add new \"history\" command","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2025-11-20T07:02:20Z","receivedAt":"2025-11-20T07:02:32Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"In addition to what Phillip commented on...\n\nOn Mon, Oct 27, 2025 at 4:34 AM Patrick Steinhardt <ps@pks.im> wrote:\n>\n> When rewriting history via git-rebase(1) there are a couple of very\n> common use cases:\n>\n>   - The ordering of two commits should be reversed.\n>\n>   - A commit should be split up into two commits.\n>\n>   - A commit should be dropped from the history completely.\n>\n>   - Multiple commits should be squashed into one.\n>\n> While these operations are all doable, it often feels needlessly kludgey\n> to do so by doing an interactive rebase, using the editor to say what\n> one wants, and then perform the actions. Furthermore, some operations\n> like splitting up a commit into two are way more involved than that and\n> require a whole series of commands.\n>\n> Add a new \"history\" command to plug this gap. This command will have\n> several different subcommands to imperatively rewrite history for common\n> use cases like the above. These subcommands will be implemented in\n> subsequent commits.\n\n\"...*Some of* these subcommands will be implemented...\", right?  You\nonly implement two of them in this series, not all of them, or am I\nreading wrong?\n"},{"id":"531042","messageId":"CABPp-BEm1QBP+CuSOn5FaE3XJVFg+Qbfzdp560u00ZERbNm6qQ@mail.gmail.com","threadId":"63986","inReplyTo":"20251027-b4-pks-history-builtin-v6-5-407dd3f57ad3@pks.im","subject":"Re: [PATCH v6 05/11] builtin/history: implement \"reword\" subcommand","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2025-11-20T07:03:20Z","receivedAt":"2025-11-20T07:03:33Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"Phillip responded in good detail, but I wanted to comment on a few\nadditional things...\n\nOn Mon, Oct 27, 2025 at 4:34 AM Patrick Steinhardt <ps@pks.im> wrote:\n\n> +static int collect_commits(struct repository *repo,\n> +                          struct commit *old_commit,\n> +                          struct commit *new_commit,\n> +                          struct strvec *out)\n> +{\n> +       struct setup_revision_opt revision_opts = {\n> +               .assume_dashdash = 1,\n> +       };\n> +       struct strvec revisions = STRVEC_INIT;\n> +       struct commit *child;\n> +       struct rev_info rev = { 0 };\n> +       int ret;\n> +\n> +       repo_init_revisions(repo, &rev, NULL);\n> +       strvec_push(&revisions, \"\");\n> +       strvec_push(&revisions, oid_to_hex(&new_commit->object.oid));\n> +       if (old_commit)\n> +               strvec_pushf(&revisions, \"^%s\", oid_to_hex(&old_commit->object.oid));\n> +\n> +       setup_revisions_from_strvec(&revisions, &rev, &revision_opts);\n> +       if (revisions.nr != 1 || prepare_revision_walk(&rev)) {\n> +               ret = error(_(\"revision walk setup failed\"));\n> +               goto out;\n> +       }\n\nDon't we want to restrict the revision walk to descendants of\nold_commit (which can be done with `--ancestry-path`)?\n\n> +\n> +       while ((child = get_revision(&rev))) {\n> +               if (old_commit && !child->parents)\n> +                       BUG(\"revision walk did not find child commit\");\n> +               if (child->parents && child->parents->next) {\n> +                       ret = error(_(\"cannot rearrange commit history with merges\"));\n> +                       goto out;\n> +               }\n> +\n> +               strvec_push(out, oid_to_hex(&child->object.oid));\n> +\n> +               if (child->parents && old_commit &&\n> +                   commit_list_contains(old_commit, child->parents))\n> +                       break;\n\nIs this last if-check basically a workaround to not providing\n--ancestry-path to the revision walk?  And won't it sometimes still\nget non-descendants of old_commit before reaching old_commit?  Or, I\nguess that's not an issue since you error out when you hit merges, but\nonce replay supports merges, there's more logic that needs changing\nthan one expects with the way this is coded.\n\n> +       }\n> +\n> +       /*\n> +        * Revisions are in newest-order-first. We have to reverse the\n> +        * array though so that we pick the oldest commits first.\n> +        */\n> +       for (size_t i = 0, j = out->nr - 1; i < j; i++, j--)\n> +               SWAP(out->v[i], out->v[j]);\n\nSetting rev.reverse would obviate the need for this...\n\n> +\n> +       ret = 0;\n> +\n> +out:\n> +       strvec_clear(&revisions);\n> +       release_revisions(&rev);\n> +       reset_revision_walk();\n> +       return ret;\n> +}\n\nYou've pulled out some functions from builtin/replay, but you've\ndecided to hand re-roll all the revision walking.  Is that because you\nfirst implemented on top of sequencer, and then transliterated to\nreplay?  If so, I think we could restructure this; I think what you\nneed is:\n  * Create a new commit with an altered commit message.\n  * Invoking whatever function(s) would be invoked by \"git replay\n--onto ${NEW_COMMIT_ID} --ancestry-path ^${OLD_COMMIT_ID} --branches\"\n(or as a first cut, even shelling out to that subprocess).\n\nThe first bullet point would be your fill_commit_message().\n\nThe second bullet point would allow you to perhaps drop your\ncollect_commits(), replace_commits(), and apply_commits(), which feel\nlike they are just re-implementing replay logic, and replace them with\nsomething like:\n\nvoid replay_descendants(struct repository *repo,\n                        const struct object_id *prev_head,\n                        const struct object_id *new_head)\n{\n        struct strvec args = STRVEC_INIT;\n\n        strvec_pushl(&args, \"replay\", \"--onto\", NULL);\n        strvec_push(&args, oid_to_hex(new_head));\n        strvec_push(&args, \"--ancestry-path\");\n        strvec_pushf(&args, \"^%s\", oid_to_hex(prev_head));\n        strvec_push(&args, \"--branches\");\n\n        reset_revision_walk();\n        cmd_replay(args.nr, args.v, NULL, repo);\n}\n\n...although maybe it's a little ugly to invoke cmd_replay() this way\nand maybe we want to restructure that out.\n\nBut, I am really late in providing my review, so if you want to go\nforward with your existing three functions and then perhaps we\nrestructure later, that's fine too.  The command is experimental,\nafter all.\n\n> +       head = lookup_commit_reference_by_name(\"HEAD\");\n> +       if (!head) {\n> +               ret = error(_(\"could not resolve HEAD to a commit\"));\n> +               goto out;\n> +       }\n> +\n> +       commit_list_append(original_commit, &from_list);\n> +       if (!repo_is_descendant_of(repo, head, from_list)) {\n> +               ret = error (_(\"split commit must be reachable from current HEAD commit\"));\n> +               goto out;\n> +       }\n\nWhy should it be required to be reachable from HEAD?  Shouldn't it be\npossible to reword a commit from another branch?\n\nAlso, what about when a commit is reachable from both HEAD and other\nbranches?  I know you started by basing on git-rebase, and git-rebase\nrestricts things to just one branch, but that was perhaps its biggest\ndesign flaw that couldn't be backward compatibly fixed without\ncreating a new command.  I'd rather avoid copying that flaw.  (Maybe\nthe user needs an error by default if more than one branch is\naffected, or they need to provide an additional flag to rewrite\nmultiple branches, but only rewriting one branch when more than one is\naffected is just wrong to me unless the user explicitly specifies\nthat's what they want.)\n\n> +       /* We retain authorship of the original commit. */\n> +       original_message = repo_logmsg_reencode(repo, original_commit, 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_commit_tree_oid,\n> +                                 original_body, \"reworded\", &final_message);\n> +       if (ret < 0)\n> +               goto out;\n> +\n> +       ret = commit_tree(final_message.buf, final_message.len, &original_commit_tree_oid,\n> +                         original_commit->parents, &rewritten_commit, original_author, NULL);\n\nDoes the use of commit_tree() instead of commit_tree_extended() mean\nyou discard additional headers on the reworded commit, such as\nencoding?\n"},{"id":"531043","messageId":"CABPp-BFx-8bxSmrzO-2i81e7XP4pbXNV6Y+Ot=RkeuhU-N0VHg@mail.gmail.com","threadId":"63986","inReplyTo":"20251027-b4-pks-history-builtin-v6-6-407dd3f57ad3@pks.im","subject":"Re: [PATCH v6 06/11] add-patch: split out header from \"add-interactive.h\"","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2025-11-20T07:03:30Z","receivedAt":"2025-11-20T07:03:42Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Mon, Oct 27, 2025 at 4:34 AM Patrick Steinhardt <ps@pks.im> wrote:\n>\n> 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\n> find relevant code and to identify clear boundaries between the two\n> subsystems.\n>\n> Split up concerns and move declarations that relate to \"add-patch.c\"\n> into a new \"add-patch.h\" header.\n>\n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n>  add-interactive.h | 23 +++--------------------\n>  add-patch.c       |  1 +\n>  add-patch.h       | 26 ++++++++++++++++++++++++++\n>  3 files changed, 30 insertions(+), 20 deletions(-)\n>\n> diff --git a/add-interactive.h b/add-interactive.h\n> index da49502b765..2e3d1d871d2 100644\n> --- a/add-interactive.h\n> +++ b/add-interactive.h\n> @@ -1,14 +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> -       int context;\n> -       int interhunkcontext;\n> -};\n> -\n> -#define ADD_P_OPT_INIT { .context = -1, .interhunkcontext = -1 }\n> +struct pathspec;\n> +struct repository;\n>\n>  struct add_i_state {\n>         struct repository *r;\n> @@ -35,21 +32,7 @@ void init_add_i_state(struct add_i_state *s, struct repository *r,\n>                       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>               struct add_p_opt *add_p_opt);\n>\n> -enum add_p_mode {\n> -       ADD_P_ADD,\n> -       ADD_P_STASH,\n> -       ADD_P_RESET,\n> -       ADD_P_CHECKOUT,\n> -       ADD_P_WORKTREE,\n> -};\n> -\n> -int run_add_p(struct repository *r, enum add_p_mode mode,\n> -             struct add_p_opt *o, const char *revision,\n> -             const struct pathspec *ps);\n> -\n>  #endif\n> diff --git a/add-patch.c b/add-patch.c\n> index ae9a20d8f23..3594dd22534 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\"\n> diff --git a/add-patch.h b/add-patch.h\n> new file mode 100644\n> index 00000000000..4394c741076\n> --- /dev/null\n> +++ b/add-patch.h\n> @@ -0,0 +1,26 @@\n> +#ifndef ADD_PATCH_H\n> +#define ADD_PATCH_H\n> +\n> +struct pathspec;\n> +struct repository;\n> +\n> +struct add_p_opt {\n> +       int context;\n> +       int interhunkcontext;\n> +};\n> +\n> +#define ADD_P_OPT_INIT { .context = -1, .interhunkcontext = -1 }\n> +\n> +enum add_p_mode {\n> +       ADD_P_ADD,\n> +       ADD_P_STASH,\n> +       ADD_P_RESET,\n> +       ADD_P_CHECKOUT,\n> +       ADD_P_WORKTREE,\n> +};\n> +\n> +int run_add_p(struct repository *r, enum add_p_mode mode,\n> +             struct add_p_opt *o, const char *revision,\n> +             const struct pathspec *ps);\n> +\n> +#endif\n>\n> --\n> 2.51.1.930.gacf6e81ea2.dirty\n\nSimple enough.\n"},{"id":"531044","messageId":"CABPp-BFLEFvQ_ZpFtEB0twtx6J5VcrerCa6aGsUjrctBQVyDiQ@mail.gmail.com","threadId":"63986","inReplyTo":"20251027-b4-pks-history-builtin-v6-7-407dd3f57ad3@pks.im","subject":"Re: [PATCH v6 07/11] add-patch: split out `struct interactive_options`","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2025-11-20T07:03:36Z","receivedAt":"2025-11-20T07:03:48Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Mon, Oct 27, 2025 at 4:34 AM Patrick Steinhardt <ps@pks.im> wrote:\n>\n> 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\n> of which options apply to what part of the stack.\n>\n> But things are even more confusing than that: while callers are expected\n> to pass in `struct add_p_opt`, these options ultimately get used to\n> initialize a `struct add_i_state` that is used by both subsystems. So we\n> are basically going full circle here.\n>\n> Refactor the code and split out a new `struct interactive_options` that\n> hosts common options used by both. These options are then applied to a\n> `struct interactive_config` that hosts common configuration.\n\nMakes sense.\n\n--color-moved helped me view part of the patch, and --color-words=. in\nsome places showed just repeated additions of \".cfg\".  Still a pretty\nlong patch; I'm almost curious if it could be split up into more steps\nto make the review easier (I admit to skimming), but scanning over it\nlooks reasonable.\n"},{"id":"531045","messageId":"CABPp-BGZZ_+EL_cQgNbmAP_WLiF9TYc2h14rzpLWc0MQNtKOhw@mail.gmail.com","threadId":"63986","inReplyTo":"20251027-b4-pks-history-builtin-v6-8-407dd3f57ad3@pks.im","subject":"Re: [PATCH v6 08/11] add-patch: remove dependency on \"add-interactive\" subsystem","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2025-11-20T07:03:53Z","receivedAt":"2025-11-20T07:04:05Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Mon, Oct 27, 2025 at 4:34 AM Patrick Steinhardt <ps@pks.im> wrote:\n>\n> With the preceding commit we have split out interactive configuration\n> that is used by both \"git add -p\" and \"git add -i\". But we still\n> initialize that configuration in the \"add -p\" subsystem by calling\n> `init_add_i_state()`, even though we only do so to initialize the\n> interactive configuration as well as a repository pointer.\n>\n> Stop doing so and instead store and initialize the interactive\n> configuration in `struct add_p_state` directly.\n>\n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n>  add-patch.c | 70 ++++++++++++++++++++++++++++++++-----------------------------\n>  1 file changed, 37 insertions(+), 33 deletions(-)\n>\n> diff --git a/add-patch.c b/add-patch.c\n> index 5c6969927a..790c848e79 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> -       struct add_i_state s;\n> +       struct repository *r;\n> +       struct interactive_config cfg;\n>         struct strbuf answer, buf;\n>\n>         /* parsed diff */\n> @@ -408,7 +408,7 @@ static void add_p_state_clear(struct add_p_state *s)\n>         for (i = 0; i < s->file_diff_nr; i++)\n>                 free(s->file_diff[i].hunk);\n>         free(s->file_diff);\n> -       clear_add_i_state(&s->s);\n> +       interactive_config_clear(&s->cfg);\n>  }\n>\n>  __attribute__((format (printf, 2, 3)))\n> @@ -417,9 +417,9 @@ static void err(struct add_p_state *s, const char *fmt, ...)\n>         va_list args;\n>\n>         va_start(args, fmt);\n> -       fputs(s->s.cfg.error_color, stdout);\n> +       fputs(s->cfg.error_color, stdout);\n>         vprintf(fmt, args);\n> -       puts(s->s.cfg.reset_color_interactive);\n> +       puts(s->cfg.reset_color_interactive);\n>         va_end(args);\n>  }\n>\n> @@ -437,7 +437,7 @@ static void setup_child_process(struct add_p_state *s,\n>\n>         cp->git_cmd = 1;\n>         strvec_pushf(&cp->env,\n> -                    INDEX_ENVIRONMENT \"=%s\", s->s.r->index_file);\n> +                    INDEX_ENVIRONMENT \"=%s\", s->r->index_file);\n>  }\n>\n>  static int parse_range(const char **p,\n> @@ -542,12 +542,12 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps)\n>         int res;\n>\n>         strvec_pushv(&args, s->mode->diff_cmd);\n> -       if (s->s.cfg.context != -1)\n> -               strvec_pushf(&args, \"--unified=%i\", s->s.cfg.context);\n> -       if (s->s.cfg.interhunkcontext != -1)\n> -               strvec_pushf(&args, \"--inter-hunk-context=%i\", s->s.cfg.interhunkcontext);\n> -       if (s->s.cfg.interactive_diff_algorithm)\n> -               strvec_pushf(&args, \"--diff-algorithm=%s\", s->s.cfg.interactive_diff_algorithm);\n> +       if (s->cfg.context != -1)\n> +               strvec_pushf(&args, \"--unified=%i\", s->cfg.context);\n> +       if (s->cfg.interhunkcontext != -1)\n> +               strvec_pushf(&args, \"--inter-hunk-context=%i\", s->cfg.interhunkcontext);\n> +       if (s->cfg.interactive_diff_algorithm)\n> +               strvec_pushf(&args, \"--diff-algorithm=%s\", s->cfg.interactive_diff_algorithm);\n>         if (s->revision) {\n>                 struct object_id oid;\n>                 strvec_push(&args,\n> @@ -576,9 +576,9 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps)\n>         }\n>         strbuf_complete_line(plain);\n>\n> -       if (want_color_fd(1, s->s.cfg.use_color_diff)) {\n> +       if (want_color_fd(1, s->cfg.use_color_diff)) {\n>                 struct child_process colored_cp = CHILD_PROCESS_INIT;\n> -               const char *diff_filter = s->s.cfg.interactive_diff_filter;\n> +               const char *diff_filter = s->cfg.interactive_diff_filter;\n>\n>                 setup_child_process(s, &colored_cp, NULL);\n>                 xsnprintf((char *)args.v[color_arg_index], 8, \"--color\");\n> @@ -811,7 +811,7 @@ static void render_hunk(struct add_p_state *s, struct hunk *hunk,\n>                                    hunk->colored_end - hunk->colored_start);\n>                         return;\n>                 } else {\n> -                       strbuf_addstr(out, s->s.cfg.fraginfo_color);\n> +                       strbuf_addstr(out, s->cfg.fraginfo_color);\n>                         p = s->colored.buf + header->colored_extra_start;\n>                         len = header->colored_extra_end\n>                                 - header->colored_extra_start;\n> @@ -833,7 +833,7 @@ static void render_hunk(struct add_p_state *s, struct hunk *hunk,\n>                 if (len)\n>                         strbuf_add(out, p, len);\n>                 else if (colored)\n> -                       strbuf_addf(out, \"%s\\n\", s->s.cfg.reset_color_diff);\n> +                       strbuf_addf(out, \"%s\\n\", s->cfg.reset_color_diff);\n>                 else\n>                         strbuf_addch(out, '\\n');\n>         }\n> @@ -1222,12 +1222,12 @@ static void recolor_hunk(struct add_p_state *s, struct hunk *hunk)\n>\n>                 strbuf_addstr(&s->colored,\n>                               plain[current] == '-' ?\n> -                             s->s.cfg.file_old_color :\n> +                             s->cfg.file_old_color :\n>                               plain[current] == '+' ?\n> -                             s->s.cfg.file_new_color :\n> -                             s->s.cfg.context_color);\n> +                             s->cfg.file_new_color :\n> +                             s->cfg.context_color);\n>                 strbuf_add(&s->colored, plain + current, eol - current);\n> -               strbuf_addstr(&s->colored, s->s.cfg.reset_color_diff);\n> +               strbuf_addstr(&s->colored, s->cfg.reset_color_diff);\n>                 if (next > eol)\n>                         strbuf_add(&s->colored, plain + eol, next - eol);\n>                 current = next;\n> @@ -1356,7 +1356,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> -       if (s->s.cfg.use_single_key) {\n> +       if (s->cfg.use_single_key) {\n>                 int res = read_key_without_echo(&s->answer);\n>                 printf(\"%s\\n\", res == EOF ? \"\" : s->answer.buf);\n>                 return res;\n> @@ -1370,7 +1370,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>         for (;;) {\n> -               color_fprintf(stdout, s->s.cfg.prompt_color, \"%s\", _(prompt));\n> +               color_fprintf(stdout, s->cfg.prompt_color, \"%s\", _(prompt));\n>                 fflush(stdout);\n>                 if (read_single_character(s) == EOF)\n>                         return -1;\n> @@ -1678,15 +1678,15 @@ static int patch_update_file(struct add_p_state *s,\n>                 else\n>                         prompt_mode_type = PROMPT_HUNK;\n>\n> -               printf(\"%s(%\"PRIuMAX\"/%\"PRIuMAX\") \", s->s.cfg.prompt_color,\n> +               printf(\"%s(%\"PRIuMAX\"/%\"PRIuMAX\") \", s->cfg.prompt_color,\n>                               (uintmax_t)hunk_index + 1,\n>                               (uintmax_t)(file_diff->hunk_nr\n>                                                 ? file_diff->hunk_nr\n>                                                 : 1));\n>                 printf(_(s->mode->prompt_mode[prompt_mode_type]),\n>                        s->buf.buf);\n> -               if (*s->s.cfg.reset_color_interactive)\n> -                       fputs(s->s.cfg.reset_color_interactive, stdout);\n> +               if (*s->cfg.reset_color_interactive)\n> +                       fputs(s->cfg.reset_color_interactive, stdout);\n>                 fflush(stdout);\n>                 if (read_single_character(s) == EOF)\n>                         break;\n> @@ -1848,7 +1848,7 @@ static int patch_update_file(struct add_p_state *s,\n>                                 err(s, _(\"Sorry, cannot split this hunk\"));\n>                         } else if (!split_hunk(s, file_diff,\n>                                              hunk - file_diff->hunk)) {\n> -                               color_fprintf_ln(stdout, s->s.cfg.header_color,\n> +                               color_fprintf_ln(stdout, s->cfg.header_color,\n>                                                  _(\"Split into %d hunks.\"),\n>                                                  (int)splittable_into);\n>                                 rendered_hunk_index = -1;\n> @@ -1866,7 +1866,7 @@ static int patch_update_file(struct add_p_state *s,\n>                 } else if (s->answer.buf[0] == '?') {\n>                         const char *p = _(help_patch_remainder), *eol = p;\n>\n> -                       color_fprintf(stdout, s->s.cfg.help_color, \"%s\",\n> +                       color_fprintf(stdout, s->cfg.help_color, \"%s\",\n>                                       _(s->mode->help_patch_text));\n>\n>                         /*\n> @@ -1884,7 +1884,7 @@ static int patch_update_file(struct add_p_state *s,\n>                                 if (*p != '?' && !strchr(s->buf.buf, *p))\n>                                         continue;\n>\n> -                               color_fprintf_ln(stdout, s->s.cfg.help_color,\n> +                               color_fprintf_ln(stdout, s->cfg.help_color,\n>                                                  \"%.*s\", (int)(eol - p), p);\n>                         }\n>                 } else {\n> @@ -1904,7 +1904,7 @@ static int patch_update_file(struct add_p_state *s,\n>                 strbuf_reset(&s->buf);\n>                 reassemble_patch(s, file_diff, 0, &s->buf);\n>\n> -               discard_index(s->s.r->index);\n> +               discard_index(s->r->index);\n>                 if (s->mode->apply_for_checkout)\n>                         apply_for_checkout(s, &s->buf,\n>                                            s->mode->is_reverse);\n> @@ -1915,8 +1915,8 @@ static int patch_update_file(struct add_p_state *s,\n>                                          NULL, 0, NULL, 0))\n>                                 error(_(\"'git apply' failed\"));\n>                 }\n> -               if (repo_read_index(s->s.r) >= 0)\n> -                       repo_refresh_and_write_index(s->s.r, REFRESH_QUIET, 0,\n> +               if (repo_read_index(s->r) >= 0)\n> +                       repo_refresh_and_write_index(s->r, REFRESH_QUIET, 0,\n>                                                      1, NULL, NULL, NULL);\n>         }\n>\n> @@ -1929,11 +1929,15 @@ int run_add_p(struct repository *r, enum add_p_mode mode,\n>               const struct pathspec *ps)\n>  {\n>         struct add_p_state s = {\n> -               { r }, STRBUF_INIT, STRBUF_INIT, STRBUF_INIT, STRBUF_INIT\n> +               .r = r,\n> +               .answer = STRBUF_INIT,\n> +               .buf = STRBUF_INIT,\n> +               .plain = STRBUF_INIT,\n> +               .colored = STRBUF_INIT,\n>         };\n>         size_t i, binary_count = 0;\n>\n> -       init_add_i_state(&s.s, r, opts);\n> +       interactive_config_init(&s.cfg, r, opts);\n>\n>         if (mode == ADD_P_STASH)\n>                 s.mode = &patch_mode_stash;\n>\n> --\n> 2.51.1.930.gacf6e81ea2.dirty\n\nViewing the patch under --color-words=. makes it clear that in most\nplaces in the patch you were just removing \"s.\" for accessing fields.\nJust the beginning and end of the patch are better viewed without that\nfield.  Anyway, all looks reasonable to me.\n"},{"id":"531046","messageId":"CABPp-BGRnx7+qvFcDeWCZEZm1aRn=kRezZ2KZA0E=8hji9Vjiw@mail.gmail.com","threadId":"63986","inReplyTo":"20251027-b4-pks-history-builtin-v6-9-407dd3f57ad3@pks.im","subject":"Re: [PATCH v6 09/11] add-patch: add support for in-memory index patching","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2025-11-20T07:04:21Z","receivedAt":"2025-11-20T07:04:33Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Mon, Oct 27, 2025 at 4:34 AM Patrick Steinhardt <ps@pks.im> wrote:\n\n> +int run_add_p_index(struct repository *r,\n> +                   struct index_state *index,\n> +                   const char *index_file,\n> +                   struct interactive_options *opts,\n> +                   const char *revision,\n> +                   const struct pathspec *ps)\n> +{\n> +       struct patch_mode mode = {\n> +               .apply_args = { \"--cached\", NULL },\n> +               .apply_check_args = { \"--cached\", NULL },\n> +               .prompt_mode = {\n> +                       N_(\"Stage mode change [y,n,q,a,d%s,?]? \"),\n> +                       N_(\"Stage deletion [y,n,q,a,d%s,?]? \"),\n> +                       N_(\"Stage addition [y,n,q,a,d%s,?]? \"),\n> +                       N_(\"Stage this hunk [y,n,q,a,d%s,?]? \")\n> +               },\n> +               .edit_hunk_hint = N_(\"If the patch applies cleanly, the edited hunk \"\n> +                                    \"will immediately be marked for staging.\"),\n> +               .help_patch_text =\n> +                       N_(\"y - stage this hunk\\n\"\n> +                          \"n - do not stage this hunk\\n\"\n> +                          \"q - quit; do not stage this hunk or any of the remaining \"\n> +                               \"ones\\n\"\n> +                          \"a - stage this hunk and all later hunks in the file\\n\"\n> +                          \"d - do not stage this hunk or any of the later hunks in \"\n> +                               \"the file\\n\"),\n> +               .index_only = 1,\n> +       };\n> +       struct add_p_state s = {\n> +               .r = r,\n> +               .index = index,\n> +               .index_file = index_file,\n> +               .answer = STRBUF_INIT,\n> +               .buf = STRBUF_INIT,\n> +               .plain = STRBUF_INIT,\n> +               .colored = STRBUF_INIT,\n> +               .mode = &mode,\n> +               .revision = revision,\n> +       };\n> +       struct strbuf parent_revision = STRBUF_INIT;\n> +       char parent_tree_oid[GIT_MAX_HEXSZ + 1];\n> +       size_t binary_count = 0;\n> +       struct commit *commit;\n> +       int ret;\n> +\n> +       commit = lookup_commit_reference_by_name(revision);\n> +       if (!commit) {\n> +               err(&s, _(\"Revision does not refer to a commit\"));\n> +               ret = -1;\n> +               goto out;\n> +       }\n> +\n> +       if (commit->parents)\n> +               oid_to_hex_r(parent_tree_oid, get_commit_tree_oid(commit->parents->item));\n> +       else\n> +               oid_to_hex_r(parent_tree_oid, r->hash_algo->empty_tree);\n> +\n> +       strbuf_addf(&parent_revision, \"%s~\", revision);\n> +       mode.diff_cmd[0] = \"diff-tree\";\n> +       mode.diff_cmd[1] = \"-r\";\n> +       mode.diff_cmd[2] = parent_tree_oid;\n> +\n> +       interactive_config_init(&s.cfg, r, opts);\n> +\n> +       if (parse_diff(&s, ps) < 0) {\n> +               ret = -1;\n> +               goto out;\n> +       }\n> +\n> +       for (size_t i = 0; i < s.file_diff_nr; i++) {\n> +               if (s.file_diff[i].binary && !s.file_diff[i].hunk_nr)\n> +                       binary_count++;\n> +               else if (patch_update_file(&s, s.file_diff + i))\n> +                       break;\n> +       }\n> +\n> +       if (s.file_diff_nr == 0) {\n> +               err(&s, _(\"No changes.\"));\n> +               ret = -1;\n> +               goto out;\n> +       }\n> +\n> +       if (binary_count == s.file_diff_nr) {\n> +               err(&s, _(\"Only binary files changed.\"));\n> +               ret = -1;\n> +               goto out;\n> +       }\n> +\n> +       ret = 0;\n> +\n> +out:\n> +       strbuf_release(&parent_revision);\n> +       add_p_state_clear(&s);\n> +       return ret;\n> +}\n\nI'm totally unfamiliar with add-patch.[ch] beyond what I've been\nreviewing in this series, so this may be a dumb/naive question, but\nwhy add a sibling run_add_p_index() to run_add_p() via\ncopy+paste+modify?  (Or is it not copy+paste+modify in some\ninteresting way?)  I'm worried the two will drift, and I'm curious\nwhether run_add_p() should just be calling run_add_p_index() and just\npassing r->index for the index field.  Is there a reason that doesn't\nwork?\n"},{"id":"531047","messageId":"CABPp-BGLbOiT6X=U7KK2nusdmvYmWgfrN1W2Df8uNqB3+hg7mQ@mail.gmail.com","threadId":"63986","inReplyTo":"20251027-b4-pks-history-builtin-v6-10-407dd3f57ad3@pks.im","subject":"Re: [PATCH v6 10/11] cache-tree: allow writing in-memory index as tree","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2025-11-20T07:04:31Z","receivedAt":"2025-11-20T07:04:43Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Mon, Oct 27, 2025 at 4:34 AM Patrick Steinhardt <ps@pks.im> wrote:\n>\n> The function `write_in_core_index_as_tree()` takes a repository and\n> writes its index into a tree object. What this function cannot do though\n> is to take an _arbitrary_ in-memory index.\n>\n> Introduce a new `struct index_state` parameter so that the caller can\n> pass a different index than the one belonging to the repository. This\n> will be used in a subsequent commit.\n\nMake sense.\n\n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n>  builtin/checkout.c | 3 ++-\n>  cache-tree.c       | 5 ++---\n>  cache-tree.h       | 3 ++-\n>  3 files changed, 6 insertions(+), 5 deletions(-)\n>\n> diff --git a/builtin/checkout.c b/builtin/checkout.c\n> index d230b1f8995..0b90f398feb 100644\n> --- a/builtin/checkout.c\n> +++ b/builtin/checkout.c\n> @@ -902,7 +902,8 @@ static int merge_working_tree(const struct checkout_opts *opts,\n>                                            0);\n>                         init_ui_merge_options(&o, the_repository);\n>                         o.verbosity = 0;\n> -                       work = write_in_core_index_as_tree(the_repository);\n> +                       work = write_in_core_index_as_tree(the_repository,\n> +                                                          the_repository->index);\n>\n>                         ret = reset_tree(new_tree,\n>                                          opts, 1,\n> diff --git a/cache-tree.c b/cache-tree.c\n> index 2aba47060e9..b67d0d703d2 100644\n> --- a/cache-tree.c\n> +++ b/cache-tree.c\n> @@ -699,11 +699,11 @@ static int write_index_as_tree_internal(struct object_id *oid,\n>         return 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> +                                        struct index_state *index_state) {\n>         struct object_id o;\n>         int was_valid, ret;\n>\n> -       struct index_state *index_state = repo->index;\n>         was_valid = index_state->cache_tree &&\n>                     cache_tree_fully_valid(index_state->cache_tree);\n>\n> @@ -723,7 +723,6 @@ struct tree* write_in_core_index_as_tree(struct repository *repo) {\n>         return lookup_tree(repo, &index_state->cache_tree->oid);\n>  }\n>\n> -\n\nWhy the random whitespace change?\n\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>  {\n>         int entries, was_valid;\n> diff --git a/cache-tree.h b/cache-tree.h\n> index b82c4963e7c..f8bddae5235 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> +                                        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> --\n> 2.51.1.930.gacf6e81ea2.dirty\n\nLooks good other than the random whitespace change.\n"},{"id":"531048","messageId":"CABPp-BGP_BecoGf093MTK-FaXTc-B0SGRkH8SHkB39nAT5fb3Q@mail.gmail.com","threadId":"63986","inReplyTo":"20251027-b4-pks-history-builtin-v6-11-407dd3f57ad3@pks.im","subject":"Re: [PATCH v6 11/11] builtin/history: implement \"split\" subcommand","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2025-11-20T07:05:37Z","receivedAt":"2025-11-20T07:05:49Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Mon, Oct 27, 2025 at 4:34 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              | 218 +++++++++++++++++++++\n>  t/meson.build                  |   1 +\n>  t/t3452-history-split.sh       | 432 +++++++++++++++++++++++++++++++++++++++++\n>  4 files changed, 713 insertions(+)\n>\n> diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc\n> index bd903875120..3d6b2665f8d 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>\n> +git history split <commit> [--] [<pathspec>...]\n>\n>  DESCRIPTION\n>  -----------\n> @@ -37,6 +38,26 @@ Several 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\nAlways two?  Should we allow someone to split into three or four?\n\n> ++\n> +The commit message of the new commit will be asked for by launching the\n> +configured editor. Authorship of the commit will be the same as for the\n> +original commit.\n\nWhich one is the new one?  Aren't they both?\n\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\nIf the user edits a hunk, what happens then?  Is this function\nprepared to deal with that?\n\n> +\n>  CONFIGURATION\n>  -------------\n>\n> @@ -44,6 +65,47 @@ include::includes/cmd-config-section-all.adoc[]\n>\n>  include::config/sequencer.adoc[]\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,e,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,e,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 cb251ae2e01..cae841707d0 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-reach.h\"\n>  #include \"commit.h\"\n>  #include \"config.h\"\n> @@ -8,17 +9,22 @@\n>  #include \"environment.h\"\n>  #include \"gettext.h\"\n>  #include \"hex.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 \"reset.h\"\n>  #include \"revision.h\"\n> +#include \"run-command.h\"\n>  #include \"sequencer.h\"\n>  #include \"strvec.h\"\n>  #include \"tree.h\"\n>  #include \"wt-status.h\"\n>\n>  #define GIT_HISTORY_REWORD_USAGE N_(\"git history reword <commit>\")\n> +#define GIT_HISTORY_SPLIT_USAGE  N_(\"git history split <commit> [--] [<pathspec>...]\")\n>\n>  static int collect_commits(struct repository *repo,\n>                            struct commit *old_commit,\n> @@ -323,6 +329,216 @@ static int cmd_history_reword(int argc,\n>         return ret;\n>  }\n>\n> +static int split_commit(struct repository *repo,\n> +                       struct commit *original_commit,\n> +                       struct pathspec *pathspec,\n> +                       struct object_id *out)\n> +{\n> +       struct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;\n> +       struct strbuf index_file = STRBUF_INIT, split_message = STRBUF_INIT;\n> +       struct child_process read_tree_cmd = CHILD_PROCESS_INIT;\n> +       struct index_state index = INDEX_STATE_INIT(repo);\n> +       struct object_id original_commit_tree_oid, parent_tree_oid;\n> +       const char *original_message, *original_body, *ptr;\n> +       char original_commit_oid[GIT_MAX_HEXSZ + 1];\n> +       char *original_author = NULL;\n> +       struct commit_list *parents = NULL;\n> +       struct commit *first_commit;\n> +       struct tree *split_tree;\n> +       size_t len;\n> +       int ret;\n> +\n> +       if (original_commit->parents)\n> +               parent_tree_oid = *get_commit_tree_oid(original_commit->parents->item);\n> +       else\n> +               oidcpy(&parent_tree_oid, repo->hash_algo->empty_tree);\n> +       original_commit_tree_oid = *get_commit_tree_oid(original_commit);\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> +       read_tree_cmd.git_cmd = 1;\n> +       strvec_pushf(&read_tree_cmd.env, \"GIT_INDEX_FILE=%s\", index_file.buf);\n> +       strvec_push(&read_tree_cmd.args, \"read-tree\");\n> +       strvec_push(&read_tree_cmd.args, oid_to_hex(&parent_tree_oid));\n> +       ret = run_command(&read_tree_cmd);\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_commit->object.oid);\n> +       ret = run_add_p_index(repo, &index, index_file.buf, &interactive_opts,\n> +                             original_commit_oid, pathspec);\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> +\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> +       /* We retain authorship of the original commit. */\n> +       original_message = repo_logmsg_reencode(repo, original_commit, NULL, NULL);\n> +       ptr = find_commit_header(original_message, \"author\", &len);\n> +       if (ptr)\n> +               original_author = xmemdupz(ptr, len);\n> +\n> +       ret = fill_commit_message(repo, &parent_tree_oid, &split_tree->object.oid,\n> +                                 \"\", \"split-out\", &split_message);\n> +       if (ret < 0)\n> +               goto out;\n> +\n> +       ret = commit_tree(split_message.buf, split_message.len, &split_tree->object.oid,\n> +                         original_commit->parents, &out[0], original_author, NULL);\n\nAs with reword, you are discarding all extended headers?\n\n> +       if (ret < 0) {\n> +               ret = error(_(\"failed writing split-out commit\"));\n> +               goto out;\n> +       }\n> +\n> +       /*\n> +       * The second commit is much simpler to construct, as we can simply use\n> +       * the original commit details, except that we adjust its parent to be\n> +       * the newly split-out commit.\n> +       */\n> +       find_commit_subject(original_message, &original_body);\n> +       first_commit = lookup_commit_reference(repo, &out[0]);\n> +       commit_list_append(first_commit, &parents);\n> +\n> +       ret = commit_tree(original_body, strlen(original_body), &original_commit_tree_oid,\n> +                         parents, &out[1], original_author, NULL);\n\nI don't understand why the second commit is the one that is to retain\nthe commit message.  I can see that users would sometimes want that,\nbut I don't see why it'd be hardcoded.\n\n> +       if (ret < 0) {\n> +               ret = error(_(\"failed writing second commit\"));\n> +               goto out;\n> +       }\n> +\n> +       ret = 0;\n> +\n> +out:\n> +       if (index_file.len)\n> +               unlink(index_file.buf);\n> +       strbuf_release(&split_message);\n> +       strbuf_release(&index_file);\n> +       free_commit_list(parents);\n> +       free(original_author);\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> +       struct option options[] = {\n> +               OPT_END(),\n> +       };\n> +       struct oidmap rewritten_commits = OIDMAP_INIT;\n> +       struct commit *original_commit, *parent, *head;\n> +       struct strvec commits = STRVEC_INIT;\n> +       struct commit_list *from_list = NULL;\n> +       struct object_id split_commits[2];\n> +       struct pathspec pathspec = { 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 revision\"));\n> +               goto out;\n> +       }\n> +       repo_config(repo, git_default_config, NULL);\n> +\n> +       original_commit = lookup_commit_reference_by_name(argv[0]);\n> +       if (!original_commit) {\n> +               ret = error(_(\"commit to be split cannot be found: %s\"), argv[0]);\n> +               goto out;\n> +       }\n> +\n> +       parent = original_commit->parents ? original_commit->parents->item : NULL;\n> +       if (parent && repo_parse_commit(repo, parent)) {\n> +               ret = error(_(\"unable to parse commit %s\"),\n> +                           oid_to_hex(&parent->object.oid));\n> +               goto out;\n> +       }\n> +\n> +       head = lookup_commit_reference_by_name(\"HEAD\");\n> +       if (!head) {\n> +               ret = error(_(\"could not resolve HEAD to a commit\"));\n> +               goto out;\n> +       }\n> +\n> +       commit_list_append(original_commit, &from_list);\n> +       if (!repo_is_descendant_of(repo, head, from_list)) {\n> +               ret = error(_(\"split commit must be reachable from current HEAD commit\"));\n> +               goto out;\n> +       }\n\nAgain, I don't see why the commit to be split needs to be an ancestor\nof HEAD; seems like an arbitrary requirement.\n\n> +\n> +       parse_pathspec(&pathspec, 0,\n> +               PATHSPEC_PREFER_FULL | PATHSPEC_SYMLINK_LEADING_PATH | PATHSPEC_PREFIX_ORIGIN,\n> +               prefix, argv + 1);\n> +\n> +       /*\n> +        * Collect the list of commits that we'll have to reapply now already.\n> +        * This ensures that we'll abort early on in case the range of commits\n> +        * contains merges, which we do not yet handle.\n> +        */\n> +       ret = collect_commits(repo, parent, head, &commits);\n> +       if (ret < 0)\n> +               goto out;\n> +\n> +       /*\n> +        * Then we split up the commit and replace the original commit with the\n> +        * new ones.\n> +        */\n> +       ret = split_commit(repo, original_commit, &pathspec, split_commits);\n> +       if (ret < 0)\n> +               goto out;\n> +\n> +       replace_commits(&commits, &original_commit->object.oid,\n> +                       split_commits, ARRAY_SIZE(split_commits));\n> +\n> +       ret = apply_commits(repo, &commits, parent, head, \"split\");\n> +       if (ret < 0)\n> +               goto out;\n\nMuch like with \"reword\", I think we could drop your auxiliary\nfunctions (collect_commits(), replace_commits(), apply_commits()),\nsince replay already handles revision walking.\n\nHowever, unlike with \"reword\" you've got a slight mess here.  If the\nuser edits the hunk to be applied, then (1) the rest of the replayed\ncommits may have conflicts (which replay doesn't handle yet), and (2)\nafter replaying you'll need to reset your working tree and index to\nmatch the rebased result, which will be tricky if you had either\nstaged or unstaged modifications.\n\n> +\n> +       ret = 0;\n> +\n> +out:\n> +       oidmap_clear(&rewritten_commits, 0);\n> +       free_commit_list(from_list);\n> +       clear_pathspec(&pathspec);\n> +       strvec_clear(&commits);\n> +       return ret;\n> +}\n> +\n>  int cmd_history(int argc,\n>                 const char **argv,\n>                 const char *prefix,\n> @@ -330,11 +546,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 a3ec9199947..5d3014a768f 100644\n> --- a/t/meson.build\n> +++ b/t/meson.build\n> @@ -386,6 +386,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 00000000000..2aac28afdf0\n> --- /dev/null\n> +++ b/t/t3452-history-split.sh\n> @@ -0,0 +1,432 @@\n> +#!/bin/sh\n> +\n> +test_description='tests for git-history split subcommand'\n> +\n> +. ./test-lib.sh\n> +\n> +set_fake_editor () {\n> +       write_script fake-editor.sh <<-EOF &&\n> +       echo \"$@\" >\"\\$1\"\n> +       EOF\n> +       test_set_editor \"$(pwd)\"/fake-editor.sh\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 rearrange commit history with merges\" err &&\n> +               test_must_fail git history split HEAD~ 2>err &&\n> +               test_grep \"cannot rearrange commit history with merges\" err\n> +       )\n> +'\n> +\n> +test_expect_success 'refuses to work with unrelated 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> +               test_must_fail git history split ours 2>err &&\n> +               test_grep \"split commit must be reachable from current HEAD commit\" err\n> +       )\n> +'\n\nI don't understand why this test is desirable.  I guess that's not\nsurprising given that I called into question why you'd want this to be\nan error in the relevant code.\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 \"split-out commit\" &&\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> +               split-me\n> +               split-out commit\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> +'\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 \"split-out commit\" &&\n> +               git history split HEAD~ <<-EOF &&\n> +               y\n> +               n\n> +               EOF\n> +\n> +               expect_log <<-EOF &&\n> +               tip\n> +               root\n> +               split-out commit\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 \"split-out commit\" &&\n> +               git history split HEAD~ <<-EOF &&\n> +               y\n> +               n\n> +               EOF\n> +\n> +               expect_log <<-EOF &&\n> +               tip\n> +               split-me\n> +               split-out commit\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 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 \"split-out-commit\" &&\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> +\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 \"split-out commit\" &&\n> +               git history split HEAD <<-EOF &&\n> +               n\n> +               y\n> +               EOF\n> +\n> +               expect_log <<-EOF &&\n> +               split-me\n> +               split-out commit\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 '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> +               cp \"$1\" . &&\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> +               cat >expect <<-EOF &&\n> +\n> +               # Please enter the commit message for the split-out changes. Lines starting\n> +               # with ${SQ}#${SQ} will be ignored.\n> +               # Changes to be committed:\n> +               #       new file:   bar\n> +               #\n> +               EOF\n> +               test_cmp expect COMMIT_EDITMSG &&\n> +\n> +               expect_log <<-EOF\n> +               split-me\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 \"split-out commit\" &&\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 '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 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> +               write_script .git/hooks/prepare-commit-msg <<-EOF &&\n> +               touch \"$(pwd)/hooks.log\"\n> +               EOF\n> +               write_script .git/hooks/post-commit <<-EOF &&\n> +               touch \"$(pwd)/hooks.log\"\n> +               EOF\n> +               write_script .git/hooks/post-rewrite <<-EOF &&\n> +               touch \"$(pwd)/hooks.log\"\n> +               EOF\n> +\n> +               set_fake_editor \"split-out commit\" &&\n> +               git history split HEAD <<-EOF &&\n> +               y\n> +               n\n> +               EOF\n> +\n> +               expect_log <<-EOF &&\n> +               split-me\n> +               split-out commit\n> +               EOF\n> +\n> +               test_path_is_missing hooks.log\n> +       )\n> +'\n\n`test_path_is_missing hooks.log` suggests the hooks are NOT executed\nfor rewritten commits; your cover letter and documentation said hooks\nwouldn't run either, so I'm guessing the test description is the bug\nhere left over from an earlier round?\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\" &&\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> +               EOF\n> +               git status --porcelain >actual &&\n> +               test_cmp expect actual\n> +       )\n> +'\n\n...but does this test mean we need to prevent users from editing hunks\nwhen splitting commits?  If we don't, how can we retain changes in the\nworktree and index?\n"},{"id":"531049","messageId":"CABPp-BGLrVv=maEqhs=j9MmST-F=K=XN6gGqmd9Hox5QRDMiHg@mail.gmail.com","threadId":"63986","inReplyTo":"20251027-b4-pks-history-builtin-v6-0-407dd3f57ad3@pks.im","subject":"Re: [PATCH v6 00/11] Introduce git-history(1) command for easy history editing","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2025-11-20T07:07:34Z","receivedAt":"2025-11-20T07:07:47Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Mon, Oct 27, 2025 at 4:34 AM Patrick Steinhardt <ps@pks.im> wrote:\n>\n> Hi,\n>\n> over recent months I've been playing around with Jujutsu quite\n> frequently. While I still prefer using Git, there's been a couple\n> features in it that I really like and that I'd like to have in Git, as\n> well.\n>\n> A copule of these features relate to history editing. Most importantly,\n> I really dig the following commands:\n>\n>   - jj-abandon(1) to drop a specific commit from your history.\n>\n>   - jj-absorb(1) to take some changes and automatically apply them to\n>     commits in your history that last modified the respective hunks.\n>\n>   - jj-split(1) to split a commit into two.\n>\n>   - jj-new(1) to insert a new commit after or before a specific other\n>     commit.\n>\n> Not all of these commands can be ported directly into Git. jj-new(1) for\n> example doesn't really make a ton of sense for us, I'd claim. But some\n> of these commands _do_ make sense.\n>\n> This patch series is a starting point for such a command. I've\n> significantly slimmed it down from the first couple revisions now\n> following the discussions at the Contributor's Summit yesterday. This\n> was my intent anyway, as I already mentioned on the last iteration.\n\nSorry for taking so long to review the series now that it's based on\nreplay.  Thanks for working on this!\n"},{"id":"531058","messageId":"3872545c-6738-4852-ab2c-8c08525ff795@gmail.com","threadId":"63986","inReplyTo":"20251027-b4-pks-history-builtin-v6-7-407dd3f57ad3@pks.im","subject":"Re: [PATCH v6 07/11] add-patch: split out `struct interactive_options`","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2025-11-20T15:05:17Z","receivedAt":"2025-11-20T15:05:23Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"On 27/10/2025 11:33, Patrick Steinhardt wrote:\n> 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\n> of which options apply to what part of the stack.\n\nTo some extent the config setting are intertwined because run_add_i() is \na superset of run_add_p(). Callers of run_add_i() needs to initalize an \ninstance of \"struct add_p_opt\" because they are indirectly calling \nrun_add_p().\n\n> But things are even more confusing than that: while callers are expected\n> to pass in `struct add_p_opt`, these options ultimately get used to\n> initialize a `struct add_i_state` that is used by both subsystems. So we\n> are basically going full circle here.\n\nIt is certainly confusing that we have to initalize a \"struct \nadd_i_state\" in run_add_p(). struct add_p_opt is only consumed in \nadd-patch.c, the reason it apperas in add-interactive.c is that \nrun_add_i() needs to pass it along to run_add_p().\n\n> Refactor the code and split out a new `struct interactive_options` that\n> hosts common options used by both. These options are then applied to a\n> `struct interactive_config` that hosts common configuration.\n\nI'm a little skeptical about renaming \"sturct add_p_opt\" as it only \nholds members that are relavent to run_add_p(). Also if we're trying to \ndraw clear boundaries between the two subsystems hosting \"struct \ninteractive_options\" and \"struct interactive_config\" in add-patch.c \nrather than add-interactive.c is potentially confusing.\n\n> This refactoring doesn't yet fully detangle the two subsystems from one\n> another, 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\nI think the ultimate aim of not having to initalize a \"struct \nadd_i_state\" in run_add_p() is a good idea. I'm not sure though that \nhaving to pass a \"struct interactive_options\" to run_add_p() is any less \nconfusing than having to pass a \"struct add_p_opt\" to run_add_i().\n\nThanks\n\nPhillip\n\n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n>   add-interactive.c  | 174 +++++++++++------------------------------------------\n>   add-interactive.h  |  23 +------\n>   add-patch.c        | 170 +++++++++++++++++++++++++++++++++++++++++++--------\n>   add-patch.h        |  36 ++++++++++-\n>   builtin/add.c      |  22 +++----\n>   builtin/checkout.c |   4 +-\n>   builtin/commit.c   |  16 ++---\n>   builtin/reset.c    |  16 ++---\n>   builtin/stash.c    |  46 +++++++-------\n>   commit.h           |   2 +-\n>   10 files changed, 270 insertions(+), 239 deletions(-)\n> \n> diff --git a/add-interactive.c b/add-interactive.c\n> index 68fc09547dd..05d2e7eefe3 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,119 +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> -\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> @@ -286,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> @@ -354,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> @@ -432,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> @@ -992,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> @@ -1014,9 +912,9 @@ 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\tstruct interactive_options opts = {\n> +\t\t\t.context = s->cfg.context,\n> +\t\t\t.interhunkcontext = s->cfg.interhunkcontext,\n>   \t\t};\n>   \t\tstruct strvec args = STRVEC_INIT;\n>   \t\tstruct pathspec ps_selected = { 0 };\n> @@ -1028,7 +926,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> @@ -1064,10 +962,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> @@ -1085,17 +983,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> @@ -1103,21 +1001,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> @@ -1152,7 +1050,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> @@ -1163,7 +1061,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> @@ -1206,15 +1104,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;\n> diff --git a/add-interactive.h b/add-interactive.h\n> index 2e3d1d871d2..eefa2edc7c1 100644\n> --- a/add-interactive.h\n> +++ b/add-interactive.h\n> @@ -2,37 +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> +\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\n> diff --git a/add-patch.c b/add-patch.c\n> index 3594dd22534..5c6969927ac 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,122 @@ 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> +\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 +417,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 +542,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 +576,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 +811,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 +833,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 +1222,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 +1356,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 +1370,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> @@ -1560,15 +1678,15 @@ static int patch_update_file(struct add_p_state *s,\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>   \t\t\t\t\t\t: 1));\n>   \t\tprintf(_(s->mode->prompt_mode[prompt_mode_type]),\n>   \t\t       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\tbreak;\n> @@ -1730,7 +1848,7 @@ static int patch_update_file(struct add_p_state *s,\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> @@ -1748,7 +1866,7 @@ static int patch_update_file(struct add_p_state *s,\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> @@ -1766,7 +1884,7 @@ static int patch_update_file(struct add_p_state *s,\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> @@ -1807,7 +1925,7 @@ static int patch_update_file(struct add_p_state *s,\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> @@ -1815,7 +1933,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> diff --git a/add-patch.h b/add-patch.h\n> index 4394c741076..a4a05d9d145 100644\n> --- a/add-patch.h\n> +++ b/add-patch.h\n> @@ -1,15 +1,45 @@\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>   };\n>   \n> -#define ADD_P_OPT_INIT { .context = -1, .interhunkcontext = -1 }\n> +#define INTERACTIVE_OPTIONS_INIT { \\\n> +\t.context = -1, \\\n> +\t.interhunkcontext = -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> +};\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> @@ -20,7 +50,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\n> diff --git a/builtin/add.c b/builtin/add.c\n> index 32709794b38..6f1e2130528 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,8 +256,8 @@ 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_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> @@ -400,9 +400,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> @@ -412,11 +412,11 @@ 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}\n>   \n> diff --git a/builtin/checkout.c b/builtin/checkout.c\n> index f9453473fe2..d230b1f8995 100644\n> --- a/builtin/checkout.c\n> +++ b/builtin/checkout.c\n> @@ -546,7 +546,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};\n> @@ -575,7 +575,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>   \n> diff --git a/builtin/commit.c b/builtin/commit.c\n> index 0243f17d53c..640495cc57e 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> @@ -356,9 +356,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> @@ -407,7 +407,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> @@ -432,9 +432,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> @@ -1742,8 +1742,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\")),\n> diff --git a/builtin/reset.c b/builtin/reset.c\n> index ed35802af15..088449e1209 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,8 +371,8 @@ 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_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> @@ -423,9 +423,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> @@ -436,12 +436,12 @@ 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}\n>   \n> diff --git a/builtin/stash.c b/builtin/stash.c\n> index 948eba06fbc..3b509052338 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,8 +1851,8 @@ 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_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> @@ -1907,19 +1909,19 @@ 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}\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> @@ -1944,7 +1946,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> @@ -1952,8 +1954,8 @@ 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_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> @@ -1973,20 +1975,20 @@ 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}\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);\n> diff --git a/commit.h b/commit.h\n> index 1d6e0c7518b..7b6e59d6c19 100644\n> --- a/commit.h\n> +++ b/commit.h\n> @@ -258,7 +258,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\n"},{"id":"531059","messageId":"44f8f04f-825a-474c-8c01-35d0cfbd8073@gmail.com","threadId":"63986","inReplyTo":"20251027-b4-pks-history-builtin-v6-8-407dd3f57ad3@pks.im","subject":"Re: [PATCH v6 08/11] add-patch: remove dependency on \"add-interactive\" subsystem","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2025-11-20T15:05:35Z","receivedAt":"2025-11-20T15:05:39Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"On 27/10/2025 11:33, Patrick Steinhardt wrote:\n> With the preceding commit we have split out interactive configuration\n> that is used by both \"git add -p\" and \"git add -i\". But we still\n> initialize that configuration in the \"add -p\" subsystem by calling\n> `init_add_i_state()`, even though we only do so to initialize the\n> interactive configuration as well as a repository pointer.\n> \n> Stop doing so and instead store and initialize the interactive\n> configuration in `struct add_p_state` directly.\n\nMakes sense\n\nThanks\n\nPhillip\n\n> \n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n>   add-patch.c | 70 ++++++++++++++++++++++++++++++++-----------------------------\n>   1 file changed, 37 insertions(+), 33 deletions(-)\n> \n> diff --git a/add-patch.c b/add-patch.c\n> index 5c6969927a..790c848e79 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> @@ -408,7 +408,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> @@ -417,9 +417,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> @@ -437,7 +437,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> @@ -542,12 +542,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> @@ -576,9 +576,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> @@ -811,7 +811,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> @@ -833,7 +833,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> @@ -1222,12 +1222,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> @@ -1356,7 +1356,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> @@ -1370,7 +1370,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> @@ -1678,15 +1678,15 @@ static int patch_update_file(struct add_p_state *s,\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>   \t\t\t\t\t\t: 1));\n>   \t\tprintf(_(s->mode->prompt_mode[prompt_mode_type]),\n>   \t\t       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\tbreak;\n> @@ -1848,7 +1848,7 @@ static int patch_update_file(struct add_p_state *s,\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> @@ -1866,7 +1866,7 @@ static int patch_update_file(struct add_p_state *s,\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> @@ -1884,7 +1884,7 @@ static int patch_update_file(struct add_p_state *s,\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> @@ -1904,7 +1904,7 @@ static int patch_update_file(struct add_p_state *s,\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\t   s->mode->is_reverse);\n> @@ -1915,8 +1915,8 @@ static int patch_update_file(struct add_p_state *s,\n>   \t\t\t\t\t NULL, 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\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> @@ -1929,11 +1929,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> \n\n"},{"id":"531060","messageId":"40695513-d16b-4fe5-b98a-c6f789e3a44c@gmail.com","threadId":"63986","inReplyTo":"CABPp-BGRnx7+qvFcDeWCZEZm1aRn=kRezZ2KZA0E=8hji9Vjiw@mail.gmail.com","subject":"Re: [PATCH v6 09/11] add-patch: add support for in-memory index patching","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2025-11-20T15:05:43Z","receivedAt":"2025-11-20T15:05:48Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"On 20/11/2025 07:04, Elijah Newren wrote:\n> On Mon, Oct 27, 2025 at 4:34 AM Patrick Steinhardt <ps@pks.im> wrote:\n> \n> I'm totally unfamiliar with add-patch.[ch] beyond what I've been\n> reviewing in this series, so this may be a dumb/naive question, but\n> why add a sibling run_add_p_index() to run_add_p() via\n> copy+paste+modify?  (Or is it not copy+paste+modify in some\n> interesting way?)  I'm worried the two will drift, and I'm curious\n> whether run_add_p() should just be calling run_add_p_index() and just\n> passing r->index for the index field.  Is there a reason that doesn't\n> work?\n\nThat's a very good question. I also wondered if there is a way to \nprevent any future changes from accidentally using \"s->r->index\" instead \nof the \"s->index\" but I don't think there is.\n\nThanks\n\nPhillip\n\n"},{"id":"531078","messageId":"xmqqbjkw78jj.fsf@gitster.g","threadId":"63986","inReplyTo":"CABPp-BGLrVv=maEqhs=j9MmST-F=K=XN6gGqmd9Hox5QRDMiHg@mail.gmail.com","subject":"Re: [PATCH v6 00/11] Introduce git-history(1) command for easy history editing","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-11-20T20:28:48Z","receivedAt":"2025-11-20T20:28:51Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Elijah Newren <newren@gmail.com> writes:\n\n>> This patch series is a starting point for such a command. I've\n>> significantly slimmed it down from the first couple revisions now\n>> following the discussions at the Contributor's Summit yesterday. This\n>> was my intent anyway, as I already mentioned on the last iteration.\n>\n> Sorry for taking so long to review the series now that it's based on\n> replay.  Thanks for working on this!\n\nWith your comments and Phillip's, it seems that we are very close to\na good stopping point.  Let me mark the topic as expecting a\nhopefully small and final reroll before getting ready for 'next'.\n\nThanks, all.\n"},{"id":"531079","messageId":"CABPp-BG+2+vanKAS-cwAD2XZxwx=PPcu5OG58ys+8O7rfZEhug@mail.gmail.com","threadId":"63986","inReplyTo":"xmqqbjkw78jj.fsf@gitster.g","subject":"Re: [PATCH v6 00/11] Introduce git-history(1) command for easy history editing","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2025-11-20T20:40:12Z","receivedAt":"2025-11-20T20:40:26Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Thu, Nov 20, 2025 at 12:28 PM Junio C Hamano <gitster@pobox.com> wrote:\n>\n> Elijah Newren <newren@gmail.com> writes:\n>\n> >> This patch series is a starting point for such a command. I've\n> >> significantly slimmed it down from the first couple revisions now\n> >> following the discussions at the Contributor's Summit yesterday. This\n> >> was my intent anyway, as I already mentioned on the last iteration.\n> >\n> > Sorry for taking so long to review the series now that it's based on\n> > replay.  Thanks for working on this!\n>\n> With your comments and Phillip's, it seems that we are very close to\n> a good stopping point.  Let me mark the topic as expecting a\n> hopefully small and final reroll before getting ready for 'next'.\n>\n> Thanks, all.\n\nI'm a little unsure if it'll be small or just one reroll.  Some of the\nchanges for patches 5 & 9 might be big (but straightforward), there's\nalso a couple design related questions (single branch, HEAD-centric)\nthat might bring up bigger usability issues to address (if a commit\nbeing edited is part of multiple branches, do we just rewrite all of\nthem by default, or error out unless the user specifies how they want\nit handled)?, and a potential gotcha on patch 11 (how can you preserve\nthe index and working tree if the user edits the patch while splitting\na commit?) that may require rethinking or restricting that feature.\n"},{"id":"531080","messageId":"xmqq7bvk77lr.fsf@gitster.g","threadId":"63986","inReplyTo":"CABPp-BG+2+vanKAS-cwAD2XZxwx=PPcu5OG58ys+8O7rfZEhug@mail.gmail.com","subject":"Re: [PATCH v6 00/11] Introduce git-history(1) command for easy history editing","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-11-20T20:49:04Z","receivedAt":"2025-11-20T20:49:07Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Elijah Newren <newren@gmail.com> writes:\n\n> On Thu, Nov 20, 2025 at 12:28 PM Junio C Hamano <gitster@pobox.com> wrote:\n>>\n>> Elijah Newren <newren@gmail.com> writes:\n>>\n>> >> This patch series is a starting point for such a command. I've\n>> >> significantly slimmed it down from the first couple revisions now\n>> >> following the discussions at the Contributor's Summit yesterday. This\n>> >> was my intent anyway, as I already mentioned on the last iteration.\n>> >\n>> > Sorry for taking so long to review the series now that it's based on\n>> > replay.  Thanks for working on this!\n>>\n>> With your comments and Phillip's, it seems that we are very close to\n>> a good stopping point.  Let me mark the topic as expecting a\n>> hopefully small and final reroll before getting ready for 'next'.\n>>\n>> Thanks, all.\n>\n> I'm a little unsure if it'll be small or just one reroll.  Some of the\n> changes for patches 5 & 9 might be big (but straightforward), there's\n> also a couple design related questions (single branch, HEAD-centric)\n> that might bring up bigger usability issues to address (if a commit\n> being edited is part of multiple branches, do we just rewrite all of\n> them by default, or error out unless the user specifies how they want\n> it handled)?, and a potential gotcha on patch 11 (how can you preserve\n> the index and working tree if the user edits the patch while splitting\n> a commit?) that may require rethinking or restricting that feature.\n\nPerhaps.  But I thought the existing patches limited its initial\nscope small and manageable that by operating only on a single strand\nof pearls, with an intention to extend to cover more cases later.  I\nwas hoping that we can start small and simple, initially limiting it\nto single branch, etc., in other areas that require design\ndecisions.\n"},{"id":"531084","messageId":"CABPp-BEyMFiRdHoseTaYG9rUFO6Ta=dBG88CGRb3CfNf8aSAkg@mail.gmail.com","threadId":"63986","inReplyTo":"xmqq7bvk77lr.fsf@gitster.g","subject":"Re: [PATCH v6 00/11] Introduce git-history(1) command for easy history editing","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2025-11-20T22:02:27Z","receivedAt":"2025-11-20T22:02:41Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Thu, Nov 20, 2025 at 12:49 PM Junio C Hamano <gitster@pobox.com> wrote:\n>\n> Elijah Newren <newren@gmail.com> writes:\n>\n> > On Thu, Nov 20, 2025 at 12:28 PM Junio C Hamano <gitster@pobox.com> wrote:\n> >>\n> >> Elijah Newren <newren@gmail.com> writes:\n> >>\n> >> >> This patch series is a starting point for such a command. I've\n> >> >> significantly slimmed it down from the first couple revisions now\n> >> >> following the discussions at the Contributor's Summit yesterday. This\n> >> >> was my intent anyway, as I already mentioned on the last iteration.\n> >> >\n> >> > Sorry for taking so long to review the series now that it's based on\n> >> > replay.  Thanks for working on this!\n> >>\n> >> With your comments and Phillip's, it seems that we are very close to\n> >> a good stopping point.  Let me mark the topic as expecting a\n> >> hopefully small and final reroll before getting ready for 'next'.\n> >>\n> >> Thanks, all.\n> >\n> > I'm a little unsure if it'll be small or just one reroll.  Some of the\n> > changes for patches 5 & 9 might be big (but straightforward), there's\n> > also a couple design related questions (single branch, HEAD-centric)\n> > that might bring up bigger usability issues to address (if a commit\n> > being edited is part of multiple branches, do we just rewrite all of\n> > them by default, or error out unless the user specifies how they want\n> > it handled)?, and a potential gotcha on patch 11 (how can you preserve\n> > the index and working tree if the user edits the patch while splitting\n> > a commit?) that may require rethinking or restricting that feature.\n>\n> Perhaps.  But I thought the existing patches limited its initial\n> scope small and manageable that by operating only on a single strand\n> of pearls, with an intention to extend to cover more cases later.  I\n> was hoping that we can start small and simple, initially limiting it\n> to single branch, etc., in other areas that require design\n> decisions.\n\nSo, you are referring to the single branch, HEAD-centric piece of the\nfeedback.  The funny thing there is that operating on a more limited\ncase, without checking and verifying that you are indeed in the more\nlimited case (and erroring out if not), risks painting us into a\ncorner or providing some really buggy behavior when we aren't actually\nin that case.  To me, it opens a can of worms and makes the problem\nscope bigger instead of smaller.  Funnily enough, the single branch\nthing is also the one piece of this that I think could be solved by a\nfairly small change in the reroll (and I pointed out how in the\ncomments), so the limited view really didn't buy anything here IMO.\n\nThe other problems are independent of whether you try to limit the\nscope initially in such a manner:\n\nAre the testcases and the code requiring something for the feature\n(ensuring the index and worktree are preserved) doing something that\nis incompatible with the capabilities given to the user (allowing them\nto edit the patch while splitting, so that they stage stuff that\nwasn't part of the original commit)?  Or...is it assumed that the\nsplit commits always \"sum\" to the changes in the original commit,\nmeaning the \"other\" patch immediately undoes those extra changes?\n(Perhaps it's the latter, which I didn't think of until now, so maybe\nwe are closer to a solution than I realized.  In fact, re-reading the\ncode that looks like it does do that and I just missed it.  But,\nperhaps having users edit the patch when splitting commits is a\nspecial case that should be called out in the docs, since that might\nsurprise users who try it?)\n\nI'm also worried about extended header handling for the edited\n(reworded or split) commits.  That seems to have been overlooked in\nthis series, despite the fact that in early versions extended headers\nwere explicitly called out for the remainder of the commits being\nreplayed/rebased, so it seems interesting that they weren't considered\nfor the commits explicitly being edited.\n\nAnd I'm a bit surprised that the original commit message for a split\ncommit is automatically associated with the second commit; if I had\nbeen forced to choose, I would have assumed it should be associated\nwith the first.\n\nGranted, I think good progress is being made and perhaps the changes\nneeded for the rest aren't that huge (and maybe there's more pieces\nI'm not quite understanding yet similar to the\ntwo-split-patches-always-summing-to-the-original), I was just a little\nsurprised that my comments are summarized by \"expecting a small and\nfinal reroll\".  :-)\n"},{"id":"531143","messageId":"a27fce64-0d4c-4280-93e5-00cce812d1b2@gmail.com","threadId":"63986","inReplyTo":"20251027-b4-pks-history-builtin-v6-11-407dd3f57ad3@pks.im","subject":"Re: [PATCH v6 11/11] builtin/history: implement \"split\" subcommand","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2025-11-21T14:31:14Z","receivedAt":"2025-11-21T14:31:19Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"Hi Patrick\n\nOn 27/10/2025 11:33, Patrick Steinhardt wrote:\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\nIf they want to test the split commit it's even more complicated because \nthey need to stash the unstaged changes. We should think about how we \ncan add support for testing split commits to this command in the future.\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\nAs others have said (and I thought we'd agreed c.f. \n<aMfdR3JE4zq-2j9b@pks.im>) I think it would be better to prompt the user \nto edit the existing commit message when creating both commits. \nElsewhere Elijah mention being able to split a commit into more than two \ncommits. I wonder if we could loop running run_add_p_index() and \ncommitting the result until there are no more changes left. It does mean \nthat the user has to actively select changes for the final commit though \nwhich might be annoying. We can always play with that later.\n\nLooking below I do wonder if we can share more code between subcommands \nwhen it comes to checking the commit we're given on the command line and \nre-creating a commit and having the user edit the message.\n\n> +static int split_commit(struct repository *repo,\n> +\t\t\tstruct commit *original_commit,\n> +\t\t\tstruct pathspec *pathspec,\n> +\t\t\tstruct object_id *out)\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> +\tread_tree_cmd.git_cmd = 1;\n> +\tstrvec_pushf(&read_tree_cmd.env, \"GIT_INDEX_FILE=%s\", index_file.buf);\n> +\tstrvec_push(&read_tree_cmd.args, \"read-tree\");\n> +\tstrvec_push(&read_tree_cmd.args, oid_to_hex(&parent_tree_oid));\n> +\tret = run_command(&read_tree_cmd);\n\nWhy do we need to fork \"read-tree\" here rather than call unpack_trees() \nourselves?\n\n> [...]\n> +\t/* We retain authorship of the original commit. */\n> +\toriginal_message = repo_logmsg_reencode(repo, original_commit, NULL, NULL);\n> +\tptr = find_commit_header(original_message, \"author\", &len);\n> +\tif (ptr)\n> +\t\toriginal_author = xmemdupz(ptr, len);\n> +\n> +\tret = fill_commit_message(repo, &parent_tree_oid, &split_tree->object.oid,\n> +\t\t\t\t  \"\", \"split-out\", &split_message);\n> +\tif (ret < 0)\n> +\t\tgoto out;\n> +\n> +\tret = commit_tree(split_message.buf, split_message.len, &split_tree->object.oid,\n> +\t\t\t  original_commit->parents, &out[0], original_author, NULL);\n> +\tif (ret < 0) {\n> +\t\tret = error(_(\"failed writing split-out commit\"));\n> +\t\tgoto out;\n> +\t}\n\nDon't we have the same code for rewording a commit, maybe we should \npackage this up into a shared helper function.\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> +\tstruct option options[] = {\n> +\t\tOPT_END(),\n> +\t};\n> +\tstruct oidmap rewritten_commits = OIDMAP_INIT;\n> +\tstruct commit *original_commit, *parent, *head;\n> +\tstruct strvec commits = STRVEC_INIT;\n> +\tstruct commit_list *from_list = NULL;\n> +\tstruct object_id split_commits[2];\n> +\tstruct pathspec pathspec = { 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 revision\"));\n> +\t\tgoto out;\n> +\t}\n> +\trepo_config(repo, git_default_config, NULL);\n> +\n> +\toriginal_commit = lookup_commit_reference_by_name(argv[0]);\n> +\tif (!original_commit) {\n> +\t\tret = error(_(\"commit to be split cannot be found: %s\"), argv[0]);\n> +\t\tgoto out;\n> +\t}\n> +\n> +\tparent = original_commit->parents ? original_commit->parents->item : NULL;\n> +\tif (parent && repo_parse_commit(repo, parent)) {\n> +\t\tret = error(_(\"unable to parse commit %s\"),\n> +\t\t\t    oid_to_hex(&parent->object.oid));\n> +\t\tgoto out;\n> +\t}\n> +\n> +\thead = lookup_commit_reference_by_name(\"HEAD\");\n> +\tif (!head) {\n> +\t\tret = error(_(\"could not resolve HEAD to a commit\"));\n> +\t\tgoto out;\n> +\t}\n> +\n> +\tcommit_list_append(original_commit, &from_list);\n> +\tif (!repo_is_descendant_of(repo, head, from_list)) {\n> +\t\tret = error(_(\"split commit must be reachable from current HEAD commit\"));\n> +\t\tgoto out;\n> +\t}\n\nThis is very similar to cmd_history_reword() up to this point. When we \nadd the \"drop\" and \"amend\" subcommands they're going to want to do the \nsame checks.\n\n> +\tparse_pathspec(&pathspec, 0,\n> +\t\tPATHSPEC_PREFER_FULL | PATHSPEC_SYMLINK_LEADING_PATH | PATHSPEC_PREFIX_ORIGIN,\n> +\t\tprefix, argv + 1);\n\nThis and calling split_commit() below are the only real differences with \ncmd_history_reword(), is it worth trying to share some more code between \nthe two?\n\n> diff --git a/t/t3452-history-split.sh b/t/t3452-history-split.sh\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 rearrange commit history with merges\" err &&\n> +\t\ttest_must_fail git history split HEAD~ 2>err &&\n> +\t\ttest_grep \"cannot rearrange commit history with merges\" err\n> +\t)\n> +'\n\nMy comments from the reword tests apply here as well.\n\nThanks\n\nPhillip\n\n> +\n> +test_expect_success 'refuses to work with unrelated 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\ttest_must_fail git history split ours 2>err &&\n> +\t\ttest_grep \"split commit must be reachable from current HEAD commit\" err\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 \"split-out commit\" &&\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\tsplit-me\n> +\t\tsplit-out commit\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> +\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 \"split-out commit\" &&\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\troot\n> +\t\tsplit-out commit\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 \"split-out commit\" &&\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\tsplit-me\n> +\t\tsplit-out commit\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 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 \"split-out-commit\" &&\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> +\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 \"split-out commit\" &&\n> +\t\tgit history split HEAD <<-EOF &&\n> +\t\tn\n> +\t\ty\n> +\t\tEOF\n> +\n> +\t\texpect_log <<-EOF &&\n> +\t\tsplit-me\n> +\t\tsplit-out commit\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 '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\tcp \"$1\" . &&\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\tcat >expect <<-EOF &&\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.\n> +\t\t# Changes to be committed:\n> +\t\t#\tnew file:   bar\n> +\t\t#\n> +\t\tEOF\n> +\t\ttest_cmp expect COMMIT_EDITMSG &&\n> +\n> +\t\texpect_log <<-EOF\n> +\t\tsplit-me\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 \"split-out commit\" &&\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 '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 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\twrite_script .git/hooks/prepare-commit-msg <<-EOF &&\n> +\t\ttouch \"$(pwd)/hooks.log\"\n> +\t\tEOF\n> +\t\twrite_script .git/hooks/post-commit <<-EOF &&\n> +\t\ttouch \"$(pwd)/hooks.log\"\n> +\t\tEOF\n> +\t\twrite_script .git/hooks/post-rewrite <<-EOF &&\n> +\t\ttouch \"$(pwd)/hooks.log\"\n> +\t\tEOF\n> +\n> +\t\tset_fake_editor \"split-out commit\" &&\n> +\t\tgit history split HEAD <<-EOF &&\n> +\t\ty\n> +\t\tn\n> +\t\tEOF\n> +\n> +\t\texpect_log <<-EOF &&\n> +\t\tsplit-me\n> +\t\tsplit-out commit\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\" &&\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\tEOF\n> +\t\tgit status --porcelain >actual &&\n> +\t\ttest_cmp expect actual\n> +\t)\n> +'\n> +\n> +test_done\n> \n\n"},{"id":"531144","messageId":"3fb47b15-ed43-4137-95f8-cee97ab5e44c@gmail.com","threadId":"63986","inReplyTo":"CABPp-BEyMFiRdHoseTaYG9rUFO6Ta=dBG88CGRb3CfNf8aSAkg@mail.gmail.com","subject":"Re: [PATCH v6 00/11] Introduce git-history(1) command for easy history editing","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2025-11-21T14:31:38Z","receivedAt":"2025-11-21T14:31:42Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"On 20/11/2025 22:02, Elijah Newren wrote:\n> On Thu, Nov 20, 2025 at 12:49 PM Junio C Hamano <gitster@pobox.com> wrote:\n>> Elijah Newren <newren@gmail.com> writes:\n>>> \n>>> I'm a little unsure if it'll be small or just one reroll.  Some of the\n>>> changes for patches 5 & 9 might be big (but straightforward), there's\n>>> also a couple design related questions (single branch, HEAD-centric)\n>>> that might bring up bigger usability issues to address (if a commit\n>>> being edited is part of multiple branches, do we just rewrite all of\n>>> them by default, or error out unless the user specifies how they want\n>>> it handled)?, and a potential gotcha on patch 11 (how can you preserve\n>>> the index and working tree if the user edits the patch while splitting\n>>> a commit?) that may require rethinking or restricting that feature.\n>>\n>> Perhaps.  But I thought the existing patches limited its initial\n>> scope small and manageable that by operating only on a single strand\n>> of pearls, with an intention to extend to cover more cases later.  I\n>> was hoping that we can start small and simple, initially limiting it\n>> to single branch, etc., in other areas that require design\n>> decisions.\n> \n> So, you are referring to the single branch, HEAD-centric piece of the\n> feedback.  The funny thing there is that operating on a more limited\n> case, without checking and verifying that you are indeed in the more\n> limited case (and erroring out if not), risks painting us into a\n> corner or providing some really buggy behavior when we aren't actually\n> in that case.  To me, it opens a can of worms and makes the problem\n> scope bigger instead of smaller.  Funnily enough, the single branch\n> thing is also the one piece of this that I think could be solved by a\n> fairly small change in the reroll (and I pointed out how in the\n> comments), so the limited view really didn't buy anything here IMO.\n\nI can't find that comment. Are you referring to reusing more of the \nreplay machinery? If so we have the problem that the user gives a single \ncommit to \"git history\" so we don't have a handy revision range to pass \nto the replay machinery unless we assume we're rewriting an ancestor of \nHEAD or we go and find all the branches descended from the commit the \nuser gave us. Long term we should certainly do the latter but depending \non how much work it is to implement that we may want to go with the \nsingle branch case at first\n\n> The other problems are independent of whether you try to limit the\n> scope initially in such a manner:\n> \n> Are the testcases and the code requiring something for the feature\n> (ensuring the index and worktree are preserved) doing something that\n> is incompatible with the capabilities given to the user (allowing them\n> to edit the patch while splitting, so that they stage stuff that\n> wasn't part of the original commit)?  Or...is it assumed that the\n> split commits always \"sum\" to the changes in the original commit,\n> meaning the \"other\" patch immediately undoes those extra changes?\n\nYes that's what's implemented. I think that makes sense for the \"split\" \ncommand. Often when splitting a commit one needs to make small changes \nto the diff in order for the result to compile but you still want the \nsame end state from the sum of the split commits.\n\n> I'm also worried about extended header handling for the edited\n> (reworded or split) commits.  That seems to have been overlooked in\n> this series, despite the fact that in early versions extended headers\n> were explicitly called out for the remainder of the commits being\n> replayed/rebased, so it seems interesting that they weren't considered\n> for the commits explicitly being edited.\n\nWhat headers does it make sense to copy when splitting a commit? When \nrewording it is more likely that copying the extended headers is what \nthe user wants but the example of the \"encoding\" header you gave does \nnot make sense to me as we re-encode the commit message and author data \nwhen the user edit's the message so we're not preserving the original \nencoding.\n\n> And I'm a bit surprised that the original commit message for a split\n> commit is automatically associated with the second commit; if I had\n> been forced to choose, I would have assumed it should be associated\n> with the first.\n\nI don't think it is safe to assume either - we should prompt the user to \nedit the message when creating both commits and seed the editor with the \noriginal message.\n\n> Granted, I think good progress is being made and perhaps the changes\n> needed for the rest aren't that huge (and maybe there's more pieces\n> I'm not quite understanding yet similar to the\n> two-split-patches-always-summing-to-the-original), I was just a little\n> surprised that my comments are summarized by \"expecting a small and\n> final reroll\".  :-)\n\nYes I'm not expecting any new functionality but I am expecting a bit \nmore than tiny cleanup.\n\nThanks\n\nPhillip\n\n"},{"id":"531146","messageId":"xmqq7bvj5q8m.fsf@gitster.g","threadId":"63986","inReplyTo":"3fb47b15-ed43-4137-95f8-cee97ab5e44c@gmail.com","subject":"Re: [PATCH v6 00/11] Introduce git-history(1) command for easy history editing","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-11-21T16:01:45Z","receivedAt":"2025-11-21T16:01:48Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Phillip Wood <phillip.wood123@gmail.com> writes:\n\n>> Granted, I think good progress is being made and perhaps the changes\n>> needed for the rest aren't that huge (and maybe there's more pieces\n>> I'm not quite understanding yet similar to the\n>> two-split-patches-always-summing-to-the-original), I was just a little\n>> surprised that my comments are summarized by \"expecting a small and\n>> final reroll\".  :-)\n\nIt was because I didn't think (and I still do not think) your\ncomments are something for the immediate future, for a tool that\nwants to present its minimum-serviceable experimental version to\nusers so that the users can experiment, extend it more and fix its\nbehaviour incrementally.  I may have been probably wrong, but I was\ngetting an impression from the reviews that it is getting to there,\nnot the feature-perfect version that needs only maintenance from now\non, but a minimum-serviceable one.\n\nWe could instead of collect all the I want moon and I want cheeze\ncomments and iterate until the tool has all of them before it hits\n'next', but I do not think it is often what we do to a new feature.\n\n> Yes I'm not expecting any new functionality but I am expecting a bit \n> more than tiny cleanup.\n\nOK.  Then we'd need a non-trivial amount of work before we get\nthere.\n\nThanks.\n"},{"id":"531166","messageId":"CABPp-BG-tLuydXkctsitFzozEq-=A+qTf2qOe1vYm+NeRnWsOA@mail.gmail.com","threadId":"63986","inReplyTo":"3fb47b15-ed43-4137-95f8-cee97ab5e44c@gmail.com","subject":"Re: [PATCH v6 00/11] Introduce git-history(1) command for easy history editing","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2025-11-23T02:30:39Z","receivedAt":"2025-11-23T02:30:52Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Fri, Nov 21, 2025 at 6:31 AM Phillip Wood <phillip.wood123@gmail.com> wrote:\n>\n[...]\n> > So, you are referring to the single branch, HEAD-centric piece of the\n> > feedback.  The funny thing there is that operating on a more limited\n> > case, without checking and verifying that you are indeed in the more\n> > limited case (and erroring out if not), risks painting us into a\n> > corner or providing some really buggy behavior when we aren't actually\n> > in that case.  To me, it opens a can of worms and makes the problem\n> > scope bigger instead of smaller.  Funnily enough, the single branch\n> > thing is also the one piece of this that I think could be solved by a\n> > fairly small change in the reroll (and I pointed out how in the\n> > comments), so the limited view really didn't buy anything here IMO.\n>\n> I can't find that comment. Are you referring to reusing more of the\n> replay machinery?\n\nYeah, what's needed is the equivalent of running \"git replay --onto\n${NEW_COMMIT_ID} --ancestry-path ^${OLD_COMMIT_ID} --branches\", as\nnoted in more detail over at\nhttps://lore.kernel.org/git/CABPp-BEm1QBP+CuSOn5FaE3XJVFg+Qbfzdp560u00ZERbNm6qQ@mail.gmail.com/\n.\n\n> If so we have the problem that the user gives a single\n> commit to \"git history\" so we don't have a handy revision range to pass\n> to the replay machinery unless we assume we're rewriting an ancestor of\n> HEAD or we go and find all the branches descended from the commit the\n> user gave us.\n\nThe range is included in the command above: \"--ancestry-path\n^${OLD_COMMIT_ID} --branches\"\n\nAnd because of this, we don't even really need to \"find\" all the\nbranches as a separate step, it's just part of the same revision walk\nfor rewriting commits.\n\nWhereas if we do want to only handle a single branch as the current\nimplementation does, then we *need* to do an extra revision walk to\nensure that the commit is not also part of any other branch and error\nout if it is, because disconnecting the histories would be very\ncounterintuitive in most cases.  If users really do want to disconnect\nhistories of two branches sharing a commit, we should require the user\nto provide some flag to explicitly specify such to signal that it is\nokay for us to bypass such a check and just rewrite one branch.  Such\na check is missing from the current code.\n\n> Long term we should certainly do the latter but depending\n> on how much work it is to implement that we may want to go with the\n> single branch case at first\n\nI showed the implementation of the latter, and it's actually (much)\nless code than what's already in this series; see the\nreplay_descendants() function I posted at the same link above.\n\nMy replay-edit work used a just slightly modified form of that\nfunction, because editing a commit and replaying all commits from all\nbranches that reached the OLD_COMMIT_ID, to now be replayed on top of\nNEW_COMMIT_ID, is exactly what was needed there too.  (If you're\ncurious about the modifications: I had an extra --brief-stats option\nbecause I found it nice to provide some user feedback about what was\nupdated, and I pulled the \"--branches\" portion of the command from a\n${GIT_DIR}/REPLAY_EDIT file, because that allowed me to give users the\nopportunity to disconnect histories via some mechanism that would put\na single branch name in that file instead of \"--branches\".)\n\n> > The other problems are independent of whether you try to limit the\n> > scope initially in such a manner:\n> >\n> > Are the testcases and the code requiring something for the feature\n> > (ensuring the index and worktree are preserved) doing something that\n> > is incompatible with the capabilities given to the user (allowing them\n> > to edit the patch while splitting, so that they stage stuff that\n> > wasn't part of the original commit)?  Or...is it assumed that the\n> > split commits always \"sum\" to the changes in the original commit,\n> > meaning the \"other\" patch immediately undoes those extra changes?\n>\n> Yes that's what's implemented. I think that makes sense for the \"split\"\n> command. Often when splitting a commit one needs to make small changes\n> to the diff in order for the result to compile but you still want the\n> same end state from the sum of the split commits.\n\nMakes sense; thanks for confirming.  I just didn't realize this was\nthe case while reviewing the patches until my response above.\n\n> > I'm also worried about extended header handling for the edited\n> > (reworded or split) commits.  That seems to have been overlooked in\n> > this series, despite the fact that in early versions extended headers\n> > were explicitly called out for the remainder of the commits being\n> > replayed/rebased, so it seems interesting that they weren't considered\n> > for the commits explicitly being edited.\n>\n> What headers does it make sense to copy when splitting a commit? When\n> rewording it is more likely that copying the extended headers is what\n> the user wants but the example of the \"encoding\" header you gave does\n> not make sense to me as we re-encode the commit message and author data\n> when the user edit's the message so we're not preserving the original\n> encoding.\n\nI agree that when rewording we probably want to copy most extended\nheaders, but you make a good point about encoding.  For splitting, I\nagree it's less clear, and I'm not sure I know the answer.  But I\nexpected the topic to at least be discussed and mentioned in the\nrelevant commit messages.  It appears to have been silently\noverlooked, and I'm worried it's the kind of topic that doesn't come\nup often, meaning that if we don't discuss now and just pick whatever\nbehavior we get from implementation side-effects, then people will\ncome back in a year or two and point out we got it buggy but it's too\nlate to change it.\n\n> > And I'm a bit surprised that the original commit message for a split\n> > commit is automatically associated with the second commit; if I had\n> > been forced to choose, I would have assumed it should be associated\n> > with the first.\n>\n> I don't think it is safe to assume either - we should prompt the user to\n> edit the message when creating both commits and seed the editor with the\n> original message.\n\nThat sounds like a better solution to me for that particular issue,\nand probably wouldn't be hard to implement.\n"},{"id":"531170","messageId":"CABPp-BEVX85xZ7_1fSfW4-xJod13p2-HvQ-e5ga+m9-Sq7mbdw@mail.gmail.com","threadId":"63986","inReplyTo":"xmqq7bvj5q8m.fsf@gitster.g","subject":"Re: [PATCH v6 00/11] Introduce git-history(1) command for easy history editing","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2025-11-23T02:54:00Z","receivedAt":"2025-11-23T02:54:15Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Fri, Nov 21, 2025 at 8:01 AM Junio C Hamano <gitster@pobox.com> wrote:\n>\n> Phillip Wood <phillip.wood123@gmail.com> writes:\n>\n> >> Granted, I think good progress is being made and perhaps the changes\n> >> needed for the rest aren't that huge (and maybe there's more pieces\n> >> I'm not quite understanding yet similar to the\n> >> two-split-patches-always-summing-to-the-original), I was just a little\n> >> surprised that my comments are summarized by \"expecting a small and\n> >> final reroll\".  :-)\n>\n> It was because I didn't think (and I still do not think) your\n> comments are something for the immediate future, for a tool that\n> wants to present its minimum-serviceable experimental version to\n> users so that the users can experiment, extend it more and fix its\n> behaviour incrementally.  I may have been probably wrong, but I was\n> getting an impression from the reviews that it is getting to there,\n> not the feature-perfect version that needs only maintenance from now\n> on, but a minimum-serviceable one.\n>\n> We could instead of collect all the I want moon and I want cheeze\n> comments and iterate until the tool has all of them before it hits\n> 'next', but I do not think it is often what we do to a new feature.\n\nYou're reading my feedback as feature requests rather than as bugs\nand/or possible paint-ourselves-in-a-corner situations in the\npresented implementation?  I must have described things rather poorly;\nif they were just feature requests, I'd agree we could just implement\nthem later.\n\nBut maybe I see where the confusion arises, since you were focusing\nsolely on the single branch thing; that's the one issue where it's\nperhaps not as clear whether I was discussing a bug or a new feature.\nLet me try to explain that case another way:\n\n\nThe early cover letters said they focused on a case where just a\nsingle branch was involved, yet they don't check whether there really\nis only one branch involved for safe operation.\n\nIf a user tries to reword or split a commit that is in the history of\nmultiple branches, the current implementation does not check and makes\nthe branches diverge.  Some users may want that, though I suspect most\nwould be negatively surprised.  The commit messages and code do not\neven discuss this case.  If we merge the code as-is and then later\nnotice and fix this problem soon enough, maybe we'd be fine, but I\nalways worry a bit about a git-switch/git-restore kind of case where\nit sits long enough and people depend on side-effects in a way that\nprevents us from fixing it.  Besides, since the bug has been\nidentified and there are multiple not-too-hard ways to fix, I think we\nshould do something.  Some possibilities:\n\n  * Document this case and warn users to check on their own (not that\nfriendly, but might be good enough for the first cut).\n  * Check if the user-specified commit is part of multiple branches\nand error out, unless the user provides a flag verifying that they\nwant histories to diverge.\n  * Just rewrite all relevant branches\n\nThe third of those could sound like a feature request in isolation,\nbut I raised it primarily because it's a potential fix to this\noverlooked bug.  I mentioned all three possible fixes, but assumed\nothers didn't realize how simple that third option was, so I pointed\nout how easy it was with some code (~12 lines, which also replace many\nmore existing lines of code).  I personally think the third option is\n*less work* than the second option, and that the focus on trying to\nlimit to a single branch is creating more work rather than simplifying\nthe problem.  But if folks really do want to limit to a single branch\ndespite the code existing to handle the more general case, then we can\nimplement one of the other solutions.  (If we do so, I still think\nchoice three is more friendly to users, to cpus, and to future\nextension of these features while also simultaneously simplifying the\nexisting code; so I'll volunteer to investigate and post patches on\ntop of this series if others decide to go with one of the other\nchoices for the initial version of the feature.)  My main point here\nis just that there is a clear, un-discussed bug, and it should be\naddressed somehow in the initial version of the feature.\n"},{"id":"531220","messageId":"d9194329-266e-43df-ba11-933ecf83b8f8@gmail.com","threadId":"63986","inReplyTo":"CABPp-BG-tLuydXkctsitFzozEq-=A+qTf2qOe1vYm+NeRnWsOA@mail.gmail.com","subject":"Re: [PATCH v6 00/11] Introduce git-history(1) command for easy history editing","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2025-11-24T16:31:44Z","receivedAt":"2025-11-24T16:31:54Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"On 23/11/2025 02:30, Elijah Newren wrote:\n> On Fri, Nov 21, 2025 at 6:31 AM Phillip Wood <phillip.wood123@gmail.com> wrote:\n>>\n> [...]\n>>> So, you are referring to the single branch, HEAD-centric piece of the\n>>> feedback.  The funny thing there is that operating on a more limited\n>>> case, without checking and verifying that you are indeed in the more\n>>> limited case (and erroring out if not), risks painting us into a\n>>> corner or providing some really buggy behavior when we aren't actually\n>>> in that case.  To me, it opens a can of worms and makes the problem\n>>> scope bigger instead of smaller.  Funnily enough, the single branch\n>>> thing is also the one piece of this that I think could be solved by a\n>>> fairly small change in the reroll (and I pointed out how in the\n>>> comments), so the limited view really didn't buy anything here IMO.\n>>\n>> I can't find that comment. Are you referring to reusing more of the\n>> replay machinery?\n> \n> Yeah, what's needed is the equivalent of running \"git replay --onto\n> ${NEW_COMMIT_ID} --ancestry-path ^${OLD_COMMIT_ID} --branches\", as\n> noted in more detail over at\n> https://lore.kernel.org/git/CABPp-BEm1QBP+CuSOn5FaE3XJVFg+Qbfzdp560u00ZERbNm6qQ@mail.gmail.com/\n\nThanks, I'd somehow missed that when I read that message the first time\n\n>> If so we have the problem that the user gives a single\n>> commit to \"git history\" so we don't have a handy revision range to pass\n>> to the replay machinery unless we assume we're rewriting an ancestor of\n>> HEAD or we go and find all the branches descended from the commit the\n>> user gave us.\n> \n> The range is included in the command above: \"--ancestry-path\n> ^${OLD_COMMIT_ID} --branches\"\n> \n> And because of this, we don't even really need to \"find\" all the\n> branches as a separate step, it's just part of the same revision walk\n> for rewriting commits.\n\nOh, so --branches means we consider all the branches and --ancestry-path \nexcludes the those that are not descended from the commit we're \nrewriting - nice. We'd need to be careful about modifying the commit at \nthe tip of a branch though as in that case we'd exclude the branch from \nthe set of commits with ^{OLD_COMMIT_ID} and so \"git replay\" would not \nupdate that branch.\n\nIn general rewriting multiple branches can be confusing if those \nbranches are checked out elsewhere and the HEAD of that worktree \nsuddenly changes but for rewording and splitting commits as implemented \nhere the final tree is the same after the rewrite so it should be fine. \nThe other potential problem with rewriting multiple branches is that we \nneed to ensure two separate \"git history\" processes running at the same \ntime in two different worktrees don't try to update the same branch. \n\"git rebase --update-refs\" has some logic to prevent that.\n\n> Whereas if we do want to only handle a single branch as the current\n> implementation does, then we *need* to do an extra revision walk to\n> ensure that the commit is not also part of any other branch and error\n> out if it is, because disconnecting the histories would be very\n> counterintuitive in most cases. \n\nOh - I had not understood what the \"extra work\" you were talking about \nbefore was - that makes it clear.\n\n> If users really do want to disconnect\n> histories of two branches sharing a commit, we should require the user\n> to provide some flag to explicitly specify such to signal that it is\n> okay for us to bypass such a check and just rewrite one branch.  Such\n> a check is missing from the current code.\n> \n>> Long term we should certainly do the latter but depending\n>> on how much work it is to implement that we may want to go with the\n>> single branch case at first\n> \n> I showed the implementation of the latter, and it's actually (much)\n> less code than what's already in this series; see the\n> replay_descendants() function I posted at the same link above.\n> \n> My replay-edit work used a just slightly modified form of that\n> function, because editing a commit and replaying all commits from all\n> branches that reached the OLD_COMMIT_ID, to now be replayed on top of\n> NEW_COMMIT_ID, is exactly what was needed there too.  (If you're\n> curious about the modifications: I had an extra --brief-stats option\n> because I found it nice to provide some user feedback about what was\n> updated, and I pulled the \"--branches\" portion of the command from a\n> ${GIT_DIR}/REPLAY_EDIT file, because that allowed me to give users the\n> opportunity to disconnect histories via some mechanism that would put\n> a single branch name in that file instead of \"--branches\".)\n\nInteresting - I watched you're git merge talk about it recently and it \nlooked quite impressive.\n\n>>> I'm also worried about extended header handling for the edited\n>>> (reworded or split) commits.  That seems to have been overlooked in\n>>> this series, despite the fact that in early versions extended headers\n>>> were explicitly called out for the remainder of the commits being\n>>> replayed/rebased, so it seems interesting that they weren't considered\n>>> for the commits explicitly being edited.\n>>\n>> What headers does it make sense to copy when splitting a commit? When\n>> rewording it is more likely that copying the extended headers is what\n>> the user wants but the example of the \"encoding\" header you gave does\n>> not make sense to me as we re-encode the commit message and author data\n>> when the user edit's the message so we're not preserving the original\n>> encoding.\n> \n> I agree that when rewording we probably want to copy most extended\n> headers, but you make a good point about encoding.  For splitting, I\n> agree it's less clear, and I'm not sure I know the answer.  But I\n> expected the topic to at least be discussed and mentioned in the\n> relevant commit messages.  It appears to have been silently\n> overlooked, and I'm worried it's the kind of topic that doesn't come\n> up often, meaning that if we don't discuss now and just pick whatever\n> behavior we get from implementation side-effects, then people will\n> come back in a year or two and point out we got it buggy but it's too\n> late to change it.\n\nIt would certainly be worth adding a comment about commit headers in the \ncommit message.\n\nThanks\n\nPhillip\n\n>>> And I'm a bit surprised that the original commit message for a split\n>>> commit is automatically associated with the second commit; if I had\n>>> been forced to choose, I would have assumed it should be associated\n>>> with the first.\n>>\n>> I don't think it is safe to assume either - we should prompt the user to\n>> edit the message when creating both commits and seed the editor with the\n>> original message.\n> \n> That sounds like a better solution to me for that particular issue,\n> and probably wouldn't be hard to implement.\n\n"},{"id":"531250","messageId":"CABPp-BHTYQOooGcwYTQFPXnDG1TL-d=97a7YyBDQgn9QgO1M_Q@mail.gmail.com","threadId":"63986","inReplyTo":"d9194329-266e-43df-ba11-933ecf83b8f8@gmail.com","subject":"Re: [PATCH v6 00/11] Introduce git-history(1) command for easy history editing","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2025-11-25T03:39:36Z","receivedAt":"2025-11-25T03:39:48Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Mon, Nov 24, 2025 at 8:31 AM Phillip Wood <phillip.wood123@gmail.com> wrote:\n>\n> On 23/11/2025 02:30, Elijah Newren wrote:\n> > On Fri, Nov 21, 2025 at 6:31 AM Phillip Wood <phillip.wood123@gmail.com> wrote:\n> >>\n[...]\n> > The range is included in the command above: \"--ancestry-path\n> > ^${OLD_COMMIT_ID} --branches\"\n> >\n> > And because of this, we don't even really need to \"find\" all the\n> > branches as a separate step, it's just part of the same revision walk\n> > for rewriting commits.\n>\n> Oh, so --branches means we consider all the branches and --ancestry-path\n> excludes the those that are not descended from the commit we're\n> rewriting - nice. We'd need to be careful about modifying the commit at\n> the tip of a branch though as in that case we'd exclude the branch from\n> the set of commits with ^{OLD_COMMIT_ID} and so \"git replay\" would not\n> update that branch.\n\nAh, indeed, that's a good callout.\n\n[...]\n> > My replay-edit work used a just slightly modified form of that\n> > function,\n[...]\n>\n> Interesting - I watched you're git merge talk about it recently and it\n> looked quite impressive.\n\nThanks.  :-)\n"},{"id":"531256","messageId":"aSVpXPtrqa0cBsEm@szeder.dev","threadId":"63986","inReplyTo":"20251027-b4-pks-history-builtin-v6-5-407dd3f57ad3@pks.im","subject":"Re: [PATCH v6 05/11] builtin/history: implement \"reword\" subcommand","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2025-11-25T08:31:24Z","receivedAt":"2025-11-25T08:31:43Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"On Mon, Oct 27, 2025 at 12:33:53PM +0100, Patrick Steinhardt wrote:\n> Implement a new \"reword\" subcommand for git-history(1). This subcommand\n> is essentially the same as if a user performed an interactive rebase\n> with a single commit changed to use the \"reword\" verb.\n\ns/verb/instruction/\n\nThe behavior is not nearly \"essentially the same\", because 'git\nhistory reword' doesn't check out the reworded commit.\n\nThis is a substantial drawback when writing a commit message for\nanything non-trivial.  I, for one, often like to take a look at the\n\"big picture\", i.e. the actual file content in the reworded commit, in\ncase the diff included in the commit message template doesn't show\nenough context, or even run Git commands built from that particular\nrevision to be able to accurately describe its behavior.\n\nOTOH, I understand that this might be deemed desirable in some cases,\nlike when rewording a commit to correct a simple typo, because source\nfile mtimes stay intact, or when the worktree contains modified files.\n\nIt would be great if this new command could somehow support both use\ncases.\n\nIn any case, this significant behavior difference (and its drawbacks)\nis not mentioned let alone justified in the commit message, it is not\ndocumented anywhere, and it is not really tested, either.\n\n\n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n>  Documentation/git-history.adoc |   7 +-\n>  builtin/history.c              | 331 ++++++++++++++++++++++++++++++++++++++++-\n>  t/meson.build                  |   1 +\n>  t/t3450-history.sh             |   6 +-\n>  t/t3451-history-reword.sh      | 237 +++++++++++++++++++++++++++++\n>  5 files changed, 573 insertions(+), 9 deletions(-)\n> \n> diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc\n> index 6bdfeb50e8b..bd903875120 100644\n> --- a/Documentation/git-history.adoc\n> +++ b/Documentation/git-history.adoc\n> @@ -8,7 +8,7 @@ git-history - EXPERIMENTAL: Rewrite history of the current branch\n>  SYNOPSIS\n>  --------\n>  [synopsis]\n> -git history [<options>]\n> +git history reword <commit>\n\nI'm not sure I like this interface, because I have to know in advance\nhow to specify the revision I want to reword, and I usually don't know\nthat.  Choosing the commit from the rebase instruction sheet seems to\nbe much simpler, more intuitive and less error prone.\n\n"},{"id":"531568","messageId":"aS8zvg1WquqJiC4H@pks.im","threadId":"63986","inReplyTo":"CABPp-BH7E1Bh2g0vR3T4NEsv34DvFQPzMuJSsqtOAaWY-fFCxg@mail.gmail.com","subject":"Re: [PATCH v6 03/11] replay: stop using `the_repository`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-12-02T18:47:43Z","receivedAt":"2025-12-02T18:48:00Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Wed, Nov 19, 2025 at 11:01:29PM -0800, Elijah Newren wrote:\n> On Mon, Oct 27, 2025 at 4:34 AM Patrick Steinhardt <ps@pks.im> wrote:\n> >\n> > In `create_commit()` we're using `the_repository` even though we already\n> > have a repository passed to use as an argument. Fix this.\n> \n> I feel like I've fixed this multiple times, but it keeps coming back.\n> In fact, I have this same fix locally in my replay-edit work.  Thanks\n> for fixing it.\n> \n> >\n> > Note that we still cannot get rid of `USE_THE_REPOSITORY_VARIABLE`. This\n> > is because we use `DEFAULT_ABBREV and `get_commit_output_encoding()`,\n> > both of which are stored as global variables that can be modified via\n> > the Git configuration.\n> \n> Indeed.\n> \n> Going on a tangent for a second...I feel like I've had to remove\n> \"the_repository\" from builtin/replay.c multiple times.  In my local\n> replay-edit work, I actually added a \"#define the_repository\n> DO_NOT_USE_THE_REPOSITORY\" in builtin/replay.c, after all the header\n> includes, because the_repository isn't what builtin/replay.c is using,\n> it's these other two things that are also only included if\n> USE_THE_REPOSITORY_VARIABALE is defined.  That obviously doesn't need\n> to be part of your series, but what would you think if I were to\n> submit that?  Is it too ugly/weird of a way to avoid the_repository\n> being added back to builtin/replay.c so we can stop having to remove\n> it again?\n\nIt does feel somewhat ugly, and the better solution would of course be\nto refactor both `DEFAULT_ABBREV` and `get_commit_output_encoding()` to\naccept a repository as input. But if I remember correctly that was a\nnontrivial endeavour, so your proposed hack might be the next-best\nsolution.\n\nPatrick\n"},{"id":"531569","messageId":"aS80Zjr8OwVMPpNk@pks.im","threadId":"63986","inReplyTo":"cca1e594-6685-4f9e-a57d-8419ccc6fa68@gmail.com","subject":"Re: [PATCH v6 04/11] builtin: add new \"history\" command","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-12-02T18:48:06Z","receivedAt":"2025-12-02T18:48:16Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Mon, Nov 17, 2025 at 04:28:06PM +0000, Phillip Wood wrote:\n> Hi Patrick\n> \n> On 27/10/2025 11:33, Patrick Steinhardt wrote:\n> > When rewriting history via git-rebase(1) there are a couple of very\n> \n> There's more than a couple of items in this list, s/couple of/few/?\n> \n> > common use cases:\n> > \n> >    - The ordering of two commits should be reversed.\n> > \n> >    - A commit should be split up into two commits.\n> > \n> >    - A commit should be dropped from the history completely.\n> > \n> >    - Multiple commits should be squashed into one.\n> \n> I'd add editing an existing commit to this list, even if we don't implement\n> it initially\n\nFair indeed.\n\n> > diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc\n> > new file mode 100644\n> > index 00000000000..6bdfeb50e8b\n> > --- /dev/null\n> > +++ b/Documentation/git-history.adoc\n> > @@ -0,0 +1,44 @@\n> > +git-history(1)\n> > +==============\n> > +\n> > +NAME\n> > +----\n> > +git-history - EXPERIMENTAL: Rewrite history of the current branch\n> > +\n> > +SYNOPSIS\n> > +--------\n> > +[synopsis]\n> > +git history [<options>]\n> > +\n> > +DESCRIPTION\n> > +-----------\n> > +\n> > +Rewrite history by rearranging or modifying specific commits in the\n> > +history.\n> > +\n> > +THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.\n> \n> Excellent - keeping our options open is a very good idea\n> \n> > +This command is similar to linkgit:git-rebase[1] and uses the same\n> > +underlying machinery.\n> \n> This isn't strictly true now that we're baisg \"git history\" on the replay\n> machinery.\n\nTrue, this is a historic leftover.\n\n> > You should use rebases if you want to reapply a range of\n> > +commits onto a different base, or interactive rebases if you want to edit a\n> > +range of commits.\n> > +\n> > +Note that this command does not (yet) work with histories that contain\n> > +merges. You should use linkgit:git-rebase[1] with the `--rebase-merges`\n> > +flag instead.\n> > +\n> > +COMMANDS\n> > +--------\n> > +\n> > +Several commands are available to rewrite history in different ways:\n> > +\n> > +CONFIGURATION\n> > +-------------\n> > +\n> > +include::includes/cmd-config-section-all.adoc[]\n> > +\n> > +include::config/sequencer.adoc[]\n> \n> This probably isn't relevant now we're not using the sequencer.\n\nAnd this, too.\n\nPatrick\n"},{"id":"531570","messageId":"aS80co7VTABD6nXs@pks.im","threadId":"63986","inReplyTo":"CABPp-BGSYKPB7met4U_4ECNgA9vb5y_AWnMWfpiisOHnd_OyhQ@mail.gmail.com","subject":"Re: [PATCH v6 04/11] builtin: add new \"history\" command","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-12-02T18:48:18Z","receivedAt":"2025-12-02T18:48:27Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Wed, Nov 19, 2025 at 11:02:20PM -0800, Elijah Newren wrote:\n> In addition to what Phillip commented on...\n> \n> On Mon, Oct 27, 2025 at 4:34 AM Patrick Steinhardt <ps@pks.im> wrote:\n> >\n> > When rewriting history via git-rebase(1) there are a couple of very\n> > common use cases:\n> >\n> >   - The ordering of two commits should be reversed.\n> >\n> >   - A commit should be split up into two commits.\n> >\n> >   - A commit should be dropped from the history completely.\n> >\n> >   - Multiple commits should be squashed into one.\n> >\n> > While these operations are all doable, it often feels needlessly kludgey\n> > to do so by doing an interactive rebase, using the editor to say what\n> > one wants, and then perform the actions. Furthermore, some operations\n> > like splitting up a commit into two are way more involved than that and\n> > require a whole series of commands.\n> >\n> > Add a new \"history\" command to plug this gap. This command will have\n> > several different subcommands to imperatively rewrite history for common\n> > use cases like the above. These subcommands will be implemented in\n> > subsequent commits.\n> \n> \"...*Some of* these subcommands will be implemented...\", right?  You\n> only implement two of them in this series, not all of them, or am I\n> reading wrong?\n\nNo, you're right. The initial versions of this series implemented more\nof the above commands, but at no point in time did we actually implement\nall of them.\n\nPatrick\n"},{"id":"531571","messageId":"aS80gHQOfKWtSy5n@pks.im","threadId":"63986","inReplyTo":"3872545c-6738-4852-ab2c-8c08525ff795@gmail.com","subject":"Re: [PATCH v6 07/11] add-patch: split out `struct interactive_options`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-12-02T18:48:32Z","receivedAt":"2025-12-02T18:48:42Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Thu, Nov 20, 2025 at 03:05:17PM +0000, Phillip Wood wrote:\n> On 27/10/2025 11:33, Patrick Steinhardt wrote:\n> > Refactor the code and split out a new `struct interactive_options` that\n> > hosts common options used by both. These options are then applied to a\n> > `struct interactive_config` that hosts common configuration.\n> \n> I'm a little skeptical about renaming \"sturct add_p_opt\" as it only holds\n> members that are relavent to run_add_p(). Also if we're trying to draw clear\n> boundaries between the two subsystems hosting \"struct interactive_options\"\n> and \"struct interactive_config\" in add-patch.c rather than add-interactive.c\n> is potentially confusing.\n\nI didn't want to add it to \"add-interactive.c\" though because of the\ndirection of the dependency: \"add-interactive.c\" will depend on\n\"add-patch.c\", not the other way round.\n\nWe could of course split out the new options into a separate file\naltogether. But that felt a bit heavy-handed to me.\n\n> > This refactoring doesn't yet fully detangle the two subsystems from one\n> > another, 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> \n> I think the ultimate aim of not having to initalize a \"struct add_i_state\"\n> in run_add_p() is a good idea. I'm not sure though that having to pass a\n> \"struct interactive_options\" to run_add_p() is any less confusing than\n> having to pass a \"struct add_p_opt\" to run_add_i().\n\nI agree that the end result is still a bit confusing. But what I wanted\nto achieve is that at least the two subsystems are clearly separated\nfrom so that one doesn't have to wonder anymore which parts interact\nwith one another. It was extremely puzzling to me at first, and the end\nresult here is significantly easier to understand from my point of view.\n\nPatrick\n"},{"id":"531572","messageId":"aS80oc61kHICD0zZ@pks.im","threadId":"63986","inReplyTo":"CABPp-BGRnx7+qvFcDeWCZEZm1aRn=kRezZ2KZA0E=8hji9Vjiw@mail.gmail.com","subject":"Re: [PATCH v6 09/11] add-patch: add support for in-memory index patching","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-12-02T18:49:05Z","receivedAt":"2025-12-02T18:49:15Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Wed, Nov 19, 2025 at 11:04:21PM -0800, Elijah Newren wrote:\n> On Mon, Oct 27, 2025 at 4:34 AM Patrick Steinhardt <ps@pks.im> wrote:\n> \n> > +int run_add_p_index(struct repository *r,\n> > +                   struct index_state *index,\n> > +                   const char *index_file,\n> > +                   struct interactive_options *opts,\n> > +                   const char *revision,\n> > +                   const struct pathspec *ps)\n> > +{\n> > +       struct patch_mode mode = {\n> > +               .apply_args = { \"--cached\", NULL },\n> > +               .apply_check_args = { \"--cached\", NULL },\n> > +               .prompt_mode = {\n> > +                       N_(\"Stage mode change [y,n,q,a,d%s,?]? \"),\n> > +                       N_(\"Stage deletion [y,n,q,a,d%s,?]? \"),\n> > +                       N_(\"Stage addition [y,n,q,a,d%s,?]? \"),\n> > +                       N_(\"Stage this hunk [y,n,q,a,d%s,?]? \")\n> > +               },\n> > +               .edit_hunk_hint = N_(\"If the patch applies cleanly, the edited hunk \"\n> > +                                    \"will immediately be marked for staging.\"),\n> > +               .help_patch_text =\n> > +                       N_(\"y - stage this hunk\\n\"\n> > +                          \"n - do not stage this hunk\\n\"\n> > +                          \"q - quit; do not stage this hunk or any of the remaining \"\n> > +                               \"ones\\n\"\n> > +                          \"a - stage this hunk and all later hunks in the file\\n\"\n> > +                          \"d - do not stage this hunk or any of the later hunks in \"\n> > +                               \"the file\\n\"),\n> > +               .index_only = 1,\n> > +       };\n> > +       struct add_p_state s = {\n> > +               .r = r,\n> > +               .index = index,\n> > +               .index_file = index_file,\n> > +               .answer = STRBUF_INIT,\n> > +               .buf = STRBUF_INIT,\n> > +               .plain = STRBUF_INIT,\n> > +               .colored = STRBUF_INIT,\n> > +               .mode = &mode,\n> > +               .revision = revision,\n> > +       };\n> > +       struct strbuf parent_revision = STRBUF_INIT;\n> > +       char parent_tree_oid[GIT_MAX_HEXSZ + 1];\n> > +       size_t binary_count = 0;\n> > +       struct commit *commit;\n> > +       int ret;\n> > +\n> > +       commit = lookup_commit_reference_by_name(revision);\n> > +       if (!commit) {\n> > +               err(&s, _(\"Revision does not refer to a commit\"));\n> > +               ret = -1;\n> > +               goto out;\n> > +       }\n> > +\n> > +       if (commit->parents)\n> > +               oid_to_hex_r(parent_tree_oid, get_commit_tree_oid(commit->parents->item));\n> > +       else\n> > +               oid_to_hex_r(parent_tree_oid, r->hash_algo->empty_tree);\n> > +\n> > +       strbuf_addf(&parent_revision, \"%s~\", revision);\n> > +       mode.diff_cmd[0] = \"diff-tree\";\n> > +       mode.diff_cmd[1] = \"-r\";\n> > +       mode.diff_cmd[2] = parent_tree_oid;\n> > +\n> > +       interactive_config_init(&s.cfg, r, opts);\n> > +\n> > +       if (parse_diff(&s, ps) < 0) {\n> > +               ret = -1;\n> > +               goto out;\n> > +       }\n> > +\n> > +       for (size_t i = 0; i < s.file_diff_nr; i++) {\n> > +               if (s.file_diff[i].binary && !s.file_diff[i].hunk_nr)\n> > +                       binary_count++;\n> > +               else if (patch_update_file(&s, s.file_diff + i))\n> > +                       break;\n> > +       }\n> > +\n> > +       if (s.file_diff_nr == 0) {\n> > +               err(&s, _(\"No changes.\"));\n> > +               ret = -1;\n> > +               goto out;\n> > +       }\n> > +\n> > +       if (binary_count == s.file_diff_nr) {\n> > +               err(&s, _(\"Only binary files changed.\"));\n> > +               ret = -1;\n> > +               goto out;\n> > +       }\n> > +\n> > +       ret = 0;\n> > +\n> > +out:\n> > +       strbuf_release(&parent_revision);\n> > +       add_p_state_clear(&s);\n> > +       return ret;\n> > +}\n> \n> I'm totally unfamiliar with add-patch.[ch] beyond what I've been\n> reviewing in this series, so this may be a dumb/naive question, but\n> why add a sibling run_add_p_index() to run_add_p() via\n> copy+paste+modify?  (Or is it not copy+paste+modify in some\n> interesting way?)  I'm worried the two will drift, and I'm curious\n> whether run_add_p() should just be calling run_add_p_index() and just\n> passing r->index for the index field.  Is there a reason that doesn't\n> work?\n\nMost of the function isn't actually copy-paste-modify. Out of the ~90\nlines of code of the new function only ~20 are the same. We could of\ncourse introduce a function to share those lines, but it doesn't save us\n_that_ much.\n\nI think the logic is non-trivial enough though to warrant it being\nduplicated regardless of that.\n\nPatrick\n"},{"id":"531573","messageId":"aS80wzWB57VPGxIq@pks.im","threadId":"63986","inReplyTo":"CABPp-BGP_BecoGf093MTK-FaXTc-B0SGRkH8SHkB39nAT5fb3Q@mail.gmail.com","subject":"Re: [PATCH v6 11/11] builtin/history: implement \"split\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-12-02T18:49:39Z","receivedAt":"2025-12-02T18:49:49Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Wed, Nov 19, 2025 at 11:05:37PM -0800, Elijah Newren wrote:\n> On Mon, Oct 27, 2025 at 4:34 AM Patrick Steinhardt <ps@pks.im> wrote:\n> > diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc\n> > index bd903875120..3d6b2665f8d 100644\n> > --- a/Documentation/git-history.adoc\n> > +++ b/Documentation/git-history.adoc\n> > @@ -37,6 +38,26 @@ Several 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> Always two?  Should we allow someone to split into three or four?\n\nFor now it's always two, yes. This is mostly modeled after `jj split`,\nwhich also does the same. For the sake of simplicity I'd suggest to keep\nit like this by default, but I could certainly see that we introduce a\nnew option in the future that allows the user to split into multiple\ncommits. In that case, we would simply loop around the interactive\nprompt until all remaining hunks have been selected.\n\n> > ++\n> > +The commit message of the new commit will be asked for by launching the\n> > +configured editor. Authorship of the commit will be the same as for the\n> > +original commit.\n> \n> Which one is the new one?  Aren't they both?\n\nI'll change the behaviour to ask for a message for both commits, so I'll\nadapt this.\n\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> If the user edits a hunk, what happens then?  Is this function\n> prepared to deal with that?\n\nThe second commit will retain the original tree, so there wouldn't be a\nconflict even in that case. It's a good question though -- maybe we\nshould disable editing hunks for now?\n\n> > +diff --git a/foo b/foo\n> > +new file mode 100644\n> > +index 0000000..257cc56\n> > +--- /dev/null\n> > ++++ b/foo\n[snip]\n> > +       /* We retain authorship of the original commit. */\n> > +       original_message = repo_logmsg_reencode(repo, original_commit, NULL, NULL);\n> > +       ptr = find_commit_header(original_message, \"author\", &len);\n> > +       if (ptr)\n> > +               original_author = xmemdupz(ptr, len);\n> > +\n> > +       ret = fill_commit_message(repo, &parent_tree_oid, &split_tree->object.oid,\n> > +                                 \"\", \"split-out\", &split_message);\n> > +       if (ret < 0)\n> > +               goto out;\n> > +\n> > +       ret = commit_tree(split_message.buf, split_message.len, &split_tree->object.oid,\n> > +                         original_commit->parents, &out[0], original_author, NULL);\n> \n> As with reword, you are discarding all extended headers?\n\nGood catch, will fix!\n\n> > +       if (ret < 0) {\n> > +               ret = error(_(\"failed writing split-out commit\"));\n> > +               goto out;\n> > +       }\n> > +\n> > +       /*\n> > +       * The second commit is much simpler to construct, as we can simply use\n> > +       * the original commit details, except that we adjust its parent to be\n> > +       * the newly split-out commit.\n> > +       */\n> > +       find_commit_subject(original_message, &original_body);\n> > +       first_commit = lookup_commit_reference(repo, &out[0]);\n> > +       commit_list_append(first_commit, &parents);\n> > +\n> > +       ret = commit_tree(original_body, strlen(original_body), &original_commit_tree_oid,\n> > +                         parents, &out[1], original_author, NULL);\n> \n> I don't understand why the second commit is the one that is to retain\n> the commit message.  I can see that users would sometimes want that,\n> but I don't see why it'd be hardcoded.\n\nMy thinking here is that \"I am splitting changes out of a specific\ncommit\", so that commit still continues to exist.\n\n[snip]\n> > +       commit_list_append(original_commit, &from_list);\n> > +       if (!repo_is_descendant_of(repo, head, from_list)) {\n> > +               ret = error(_(\"split commit must be reachable from current HEAD commit\"));\n> > +               goto out;\n> > +       }\n> \n> Again, I don't see why the commit to be split needs to be an ancestor\n> of HEAD; seems like an arbitrary requirement.\n\nThis is done for the sake of simplicity: I'd rather want to be as\nrestrictive as possible initially and then extend git-history(1) to\nhandle more cases as we go forward.\n\n[snip]\n> > +       /*\n> > +        * Then we split up the commit and replace the original commit with the\n> > +        * new ones.\n> > +        */\n> > +       ret = split_commit(repo, original_commit, &pathspec, split_commits);\n> > +       if (ret < 0)\n> > +               goto out;\n> > +\n> > +       replace_commits(&commits, &original_commit->object.oid,\n> > +                       split_commits, ARRAY_SIZE(split_commits));\n> > +\n> > +       ret = apply_commits(repo, &commits, parent, head, \"split\");\n> > +       if (ret < 0)\n> > +               goto out;\n> \n> Much like with \"reword\", I think we could drop your auxiliary\n> functions (collect_commits(), replace_commits(), apply_commits()),\n> since replay already handles revision walking.\n\nI mostly introduced these functions because I want to extend\ngit-history(1) going forward to also handle cases that git-replay(1)\ndoesn't currently handle. This includes for example also reordering of\ncommits, which I think isn't easily possible with the replay subsystem?\n\n> However, unlike with \"reword\" you've got a slight mess here.  If the\n> user edits the hunk to be applied, then (1) the rest of the replayed\n> commits may have conflicts (which replay doesn't handle yet), and (2)\n> after replaying you'll need to reset your working tree and index to\n> match the rebased result, which will be tricky if you had either\n> staged or unstaged modifications.\n\nYeah, the \"editing\" part is an actual oversight on my part. I'll\nrestrict this for now.\n\n> > diff --git a/t/t3452-history-split.sh b/t/t3452-history-split.sh\n> > new file mode 100755\n> > index 00000000000..2aac28afdf0\n> > --- /dev/null\n> > +++ b/t/t3452-history-split.sh\n> > @@ -0,0 +1,432 @@\n[snip]\n> > +test_expect_success 'refuses to work with unrelated 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> > +               test_must_fail git history split ours 2>err &&\n> > +               test_grep \"split commit must be reachable from current HEAD commit\" err\n> > +       )\n> > +'\n> \n> I don't understand why this test is desirable.  I guess that's not\n> surprising given that I called into question why you'd want this to be\n> an error in the relevant code.\n\nThe question is what happens in the case where you edit a commit that is\nunrelated to the current history. Which branches would be updated? Do we\nupdate any at all? If not, what would the user-visible result be? We're\ngetting into territory where semantics are not immediately obvious, and\nI want git-history(1) to be a command that makes history editing easy\nfor the most common use cases.\n\nIf we find good semantics in the future for how to perform such an\noperation I'm very much in favor of adding that behaviour. But I think\nthat this is outside the scope of this series right now, as I rather\ncare about making the easy and obvious parts work first.\n\nAnd meanwhile, while we still have restrictions like these in place, I\nwant to ensure that we don't accidentally do the wrong thing in cases\nthat we don't yet support. Hence the test.\n\n> > +test_expect_success 'hooks are 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> > +               write_script .git/hooks/prepare-commit-msg <<-EOF &&\n> > +               touch \"$(pwd)/hooks.log\"\n> > +               EOF\n> > +               write_script .git/hooks/post-commit <<-EOF &&\n> > +               touch \"$(pwd)/hooks.log\"\n> > +               EOF\n> > +               write_script .git/hooks/post-rewrite <<-EOF &&\n> > +               touch \"$(pwd)/hooks.log\"\n> > +               EOF\n> > +\n> > +               set_fake_editor \"split-out commit\" &&\n> > +               git history split HEAD <<-EOF &&\n> > +               y\n> > +               n\n> > +               EOF\n> > +\n> > +               expect_log <<-EOF &&\n> > +               split-me\n> > +               split-out commit\n> > +               EOF\n> > +\n> > +               test_path_is_missing hooks.log\n> > +       )\n> > +'\n> \n> `test_path_is_missing hooks.log` suggests the hooks are NOT executed\n> for rewritten commits; your cover letter and documentation said hooks\n> wouldn't run either, so I'm guessing the test description is the bug\n> here left over from an earlier round?\n\nIt is indeed, thanks!\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\" &&\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> > +               EOF\n> > +               git status --porcelain >actual &&\n> > +               test_cmp expect actual\n> > +       )\n> > +'\n> \n> ...but does this test mean we need to prevent users from editing hunks\n> when splitting commits?  If we don't, how can we retain changes in the\n> worktree and index?\n\nI'll disallow editing hunks for now. We may reintroduce this ability in\nthe future, but let's focus on the easy parts for now :)\n\nPatrick\n"},{"id":"531574","messageId":"aS80z0DxAEBLba-M@pks.im","threadId":"63986","inReplyTo":"CABPp-BEVX85xZ7_1fSfW4-xJod13p2-HvQ-e5ga+m9-Sq7mbdw@mail.gmail.com","subject":"Re: [PATCH v6 00/11] Introduce git-history(1) command for easy history editing","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-12-02T18:49:51Z","receivedAt":"2025-12-02T18:50:11Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Sat, Nov 22, 2025 at 06:54:00PM -0800, Elijah Newren wrote:\n> On Fri, Nov 21, 2025 at 8:01 AM Junio C Hamano <gitster@pobox.com> wrote:\n> >\n> > Phillip Wood <phillip.wood123@gmail.com> writes:\n> >\n> > >> Granted, I think good progress is being made and perhaps the changes\n> > >> needed for the rest aren't that huge (and maybe there's more pieces\n> > >> I'm not quite understanding yet similar to the\n> > >> two-split-patches-always-summing-to-the-original), I was just a little\n> > >> surprised that my comments are summarized by \"expecting a small and\n> > >> final reroll\".  :-)\n> >\n> > It was because I didn't think (and I still do not think) your\n> > comments are something for the immediate future, for a tool that\n> > wants to present its minimum-serviceable experimental version to\n> > users so that the users can experiment, extend it more and fix its\n> > behaviour incrementally.  I may have been probably wrong, but I was\n> > getting an impression from the reviews that it is getting to there,\n> > not the feature-perfect version that needs only maintenance from now\n> > on, but a minimum-serviceable one.\n> >\n> > We could instead of collect all the I want moon and I want cheeze\n> > comments and iterate until the tool has all of them before it hits\n> > 'next', but I do not think it is often what we do to a new feature.\n> \n> You're reading my feedback as feature requests rather than as bugs\n> and/or possible paint-ourselves-in-a-corner situations in the\n> presented implementation?  I must have described things rather poorly;\n> if they were just feature requests, I'd agree we could just implement\n> them later.\n\nThe command is explicitly marked as experimental so that we can iterate\non its behaviour as needed. So I don't think we're painting us into a\ncorner yet.\n\n> But maybe I see where the confusion arises, since you were focusing\n> solely on the single branch thing; that's the one issue where it's\n> perhaps not as clear whether I was discussing a bug or a new feature.\n> Let me try to explain that case another way:\n> \n> \n> The early cover letters said they focused on a case where just a\n> single branch was involved, yet they don't check whether there really\n> is only one branch involved for safe operation.\n\nI think this depends on the definition. We _do_ verify that the commit\nthat is to be edited is part of the current branch. What we _don't_\ncheck is that the commit is _only_ part of that current branch.\n\nI'm not sure whether that is something we want. I myself have the same\ncommit on multiple branches quite regularly, as I tend to queue up\nmultiple dependent patch series. But I wouldn't ever want a history edit\nto affect all of these branches myself, I really only want it to modify\nthe branch I'm currently on.\n\n> If a user tries to reword or split a commit that is in the history of\n> multiple branches, the current implementation does not check and makes\n> the branches diverge.  Some users may want that, though I suspect most\n> would be negatively surprised.  The commit messages and code do not\n> even discuss this case.  If we merge the code as-is and then later\n> notice and fix this problem soon enough, maybe we'd be fine, but I\n> always worry a bit about a git-switch/git-restore kind of case where\n> it sits long enough and people depend on side-effects in a way that\n> prevents us from fixing it.  Besides, since the bug has been\n> identified and there are multiple not-too-hard ways to fix, I think we\n> should do something.  Some possibilities:\n> \n>   * Document this case and warn users to check on their own (not that\n> friendly, but might be good enough for the first cut).\n>   * Check if the user-specified commit is part of multiple branches\n> and error out, unless the user provides a flag verifying that they\n> want histories to diverge.\n>   * Just rewrite all relevant branches\n> \n> The third of those could sound like a feature request in isolation,\n> but I raised it primarily because it's a potential fix to this\n> overlooked bug.  I mentioned all three possible fixes, but assumed\n> others didn't realize how simple that third option was, so I pointed\n> out how easy it was with some code (~12 lines, which also replace many\n> more existing lines of code).  I personally think the third option is\n> *less work* than the second option, and that the focus on trying to\n> limit to a single branch is creating more work rather than simplifying\n> the problem.  But if folks really do want to limit to a single branch\n> despite the code existing to handle the more general case, then we can\n> implement one of the other solutions.  (If we do so, I still think\n> choice three is more friendly to users, to cpus, and to future\n> extension of these features while also simultaneously simplifying the\n> existing code; so I'll volunteer to investigate and post patches on\n> top of this series if others decide to go with one of the other\n> choices for the initial version of the feature.)  My main point here\n> is just that there is a clear, un-discussed bug, and it should be\n> addressed somehow in the initial version of the feature.\n\nSo with the above clarification I wouldn't call any of this a bug, but\nrather working as designed. We could of course still print a warning in\nthat case to protect the user, but one problem I see is that generating\nthis warning could be quite expensive as we'd now have to walk all\nreferences. That might be cheap in case the user only has short-lived\nfeature branchs. But it may very well not be cheap in case they for\nexample have old release branches checked out, as we'd now have to\npotentially walk a significant portiion of history.\n\nConsequently I'm leaning more into the direction of doing nothing. It's\nnot really clear to me that this is a bug, and we still can introduce a\nflag in the future that opts into the behaviour of rewriting relevant\nbranches. That behaviour certainly can be useful, but I'd claim that\nit would be rather surprising to the user if that was the default.\n\nPatrick\n"},{"id":"531575","messageId":"aS805xbWBJMy4tBk@pks.im","threadId":"63986","inReplyTo":"CABPp-BEm1QBP+CuSOn5FaE3XJVFg+Qbfzdp560u00ZERbNm6qQ@mail.gmail.com","subject":"Re: [PATCH v6 05/11] builtin/history: implement \"reword\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-12-02T18:50:15Z","receivedAt":"2025-12-02T18:50:37Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Wed, Nov 19, 2025 at 11:03:20PM -0800, Elijah Newren wrote:\n> Phillip responded in good detail, but I wanted to comment on a few\n> additional things...\n> \n> On Mon, Oct 27, 2025 at 4:34 AM Patrick Steinhardt <ps@pks.im> wrote:\n> \n> > +static int collect_commits(struct repository *repo,\n> > +                          struct commit *old_commit,\n> > +                          struct commit *new_commit,\n> > +                          struct strvec *out)\n> > +{\n> > +       struct setup_revision_opt revision_opts = {\n> > +               .assume_dashdash = 1,\n> > +       };\n> > +       struct strvec revisions = STRVEC_INIT;\n> > +       struct commit *child;\n> > +       struct rev_info rev = { 0 };\n> > +       int ret;\n> > +\n> > +       repo_init_revisions(repo, &rev, NULL);\n> > +       strvec_push(&revisions, \"\");\n> > +       strvec_push(&revisions, oid_to_hex(&new_commit->object.oid));\n> > +       if (old_commit)\n> > +               strvec_pushf(&revisions, \"^%s\", oid_to_hex(&old_commit->object.oid));\n> > +\n> > +       setup_revisions_from_strvec(&revisions, &rev, &revision_opts);\n> > +       if (revisions.nr != 1 || prepare_revision_walk(&rev)) {\n> > +               ret = error(_(\"revision walk setup failed\"));\n> > +               goto out;\n> > +       }\n> \n> Don't we want to restrict the revision walk to descendants of\n> old_commit (which can be done with `--ancestry-path`)?\n\nWe verify that both commits have direct ancestry and that there are no\nmerge commits in the history, so this shouldn't be needed. But indeed,\nthis makes the logic a bit easier to reason about.\n\n> > +\n> > +       while ((child = get_revision(&rev))) {\n> > +               if (old_commit && !child->parents)\n> > +                       BUG(\"revision walk did not find child commit\");\n> > +               if (child->parents && child->parents->next) {\n> > +                       ret = error(_(\"cannot rearrange commit history with merges\"));\n> > +                       goto out;\n> > +               }\n> > +\n> > +               strvec_push(out, oid_to_hex(&child->object.oid));\n> > +\n> > +               if (child->parents && old_commit &&\n> > +                   commit_list_contains(old_commit, child->parents))\n> > +                       break;\n> \n> Is this last if-check basically a workaround to not providing\n> --ancestry-path to the revision walk?  And won't it sometimes still\n> get non-descendants of old_commit before reaching old_commit?  Or, I\n> guess that's not an issue since you error out when you hit merges, but\n> once replay supports merges, there's more logic that needs changing\n> than one expects with the way this is coded.\n\nYeah, this is not currently an issue as we explicitly rule out merges.\nAnyway, I'm using the flag now, so this isn't needed anymore.\n\n> > +       }\n> > +\n> > +       /*\n> > +        * Revisions are in newest-order-first. We have to reverse the\n> > +        * array though so that we pick the oldest commits first.\n> > +        */\n> > +       for (size_t i = 0, j = out->nr - 1; i < j; i++, j--)\n> > +               SWAP(out->v[i], out->v[j]);\n> \n> Setting rev.reverse would obviate the need for this...\n\nYup, true. I couldn't use 'reverse' before due to the way the loop was\nhandled.\n\n> > +\n> > +       ret = 0;\n> > +\n> > +out:\n> > +       strvec_clear(&revisions);\n> > +       release_revisions(&rev);\n> > +       reset_revision_walk();\n> > +       return ret;\n> > +}\n> \n> You've pulled out some functions from builtin/replay, but you've\n> decided to hand re-roll all the revision walking.  Is that because you\n> first implemented on top of sequencer, and then transliterated to\n> replay?  If so, I think we could restructure this; I think what you\n> need is:\n>   * Create a new commit with an altered commit message.\n>   * Invoking whatever function(s) would be invoked by \"git replay\n> --onto ${NEW_COMMIT_ID} --ancestry-path ^${OLD_COMMIT_ID} --branches\"\n> (or as a first cut, even shelling out to that subprocess).\n> \n> The first bullet point would be your fill_commit_message().\n> \n> The second bullet point would allow you to perhaps drop your\n> collect_commits(), replace_commits(), and apply_commits(), which feel\n> like they are just re-implementing replay logic, and replace them with\n> something like:\n> \n> void replay_descendants(struct repository *repo,\n>                         const struct object_id *prev_head,\n>                         const struct object_id *new_head)\n> {\n>         struct strvec args = STRVEC_INIT;\n> \n>         strvec_pushl(&args, \"replay\", \"--onto\", NULL);\n>         strvec_push(&args, oid_to_hex(new_head));\n>         strvec_push(&args, \"--ancestry-path\");\n>         strvec_pushf(&args, \"^%s\", oid_to_hex(prev_head));\n>         strvec_push(&args, \"--branches\");\n> \n>         reset_revision_walk();\n>         cmd_replay(args.nr, args.v, NULL, repo);\n> }\n> \n> ...although maybe it's a little ugly to invoke cmd_replay() this way\n> and maybe we want to restructure that out.\n> \n> But, I am really late in providing my review, so if you want to go\n> forward with your existing three functions and then perhaps we\n> restructure later, that's fine too.  The command is experimental,\n> after all.\n\nI guess it's a combination of the transliteration and that I couldn't\nfigure out how to easily do some things without shelling out. I plan on\nintroducing features eventually that also allow for example to reorder\ncommits, and I'm not clear that this is easy to do with the replay\ninfra.\n\nSo for now I think I'd like to retain the current infra. But I certainly\nagree that we should revisit and see whether we can further refactor the\ninterfaces provided by \"replay.c\" to cover more cases without shelling\nout.\n\n> > +       head = lookup_commit_reference_by_name(\"HEAD\");\n> > +       if (!head) {\n> > +               ret = error(_(\"could not resolve HEAD to a commit\"));\n> > +               goto out;\n> > +       }\n> > +\n> > +       commit_list_append(original_commit, &from_list);\n> > +       if (!repo_is_descendant_of(repo, head, from_list)) {\n> > +               ret = error (_(\"split commit must be reachable from current HEAD commit\"));\n> > +               goto out;\n> > +       }\n> \n> Why should it be required to be reachable from HEAD?  Shouldn't it be\n> possible to reword a commit from another branch?\n> \n> Also, what about when a commit is reachable from both HEAD and other\n> branches?  I know you started by basing on git-rebase, and git-rebase\n> restricts things to just one branch, but that was perhaps its biggest\n> design flaw that couldn't be backward compatibly fixed without\n> creating a new command.  I'd rather avoid copying that flaw.  (Maybe\n> the user needs an error by default if more than one branch is\n> affected, or they need to provide an additional flag to rewrite\n> multiple branches, but only rewriting one branch when more than one is\n> affected is just wrong to me unless the user explicitly specifies\n> that's what they want.)\n\nFor now we the commands really only care about a single branch, the case\nwhere a commit exists on multiple branches is not considered. I'm not\nreally sure whether I'd call this a flaw -- I think it's as easy way to\nthink about the command for the user.\n\nThat being said, I certainly think that we can eventually introduce a\nflag to alter the behaviour so that it considers multiple branches in\ncase the commit exists on more than one branch.\n\n> > +       /* We retain authorship of the original commit. */\n> > +       original_message = repo_logmsg_reencode(repo, original_commit, 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_commit_tree_oid,\n> > +                                 original_body, \"reworded\", &final_message);\n> > +       if (ret < 0)\n> > +               goto out;\n> > +\n> > +       ret = commit_tree(final_message.buf, final_message.len, &original_commit_tree_oid,\n> > +                         original_commit->parents, &rewritten_commit, original_author, NULL);\n> \n> Does the use of commit_tree() instead of commit_tree_extended() mean\n> you discard additional headers on the reworded commit, such as\n> encoding?\n\nGood point, let me fix this.\n\nPatrick\n"},{"id":"531576","messageId":"aS80_rTVHP44JKhx@pks.im","threadId":"63986","inReplyTo":"b36a644e-2d8d-41cf-b59b-e72ce0ca855c@gmail.com","subject":"Re: [PATCH v6 05/11] builtin/history: implement \"reword\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-12-02T18:50:38Z","receivedAt":"2025-12-02T18:50:48Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Mon, Nov 17, 2025 at 04:27:59PM +0000, Phillip Wood wrote:\n> Hi Patrick\n> On 27/10/2025 11:33, Patrick Steinhardt wrote:\n> \n> > +static int collect_commits(struct repository *repo,\n> > +\t\t\t   struct commit *old_commit,\n> > +\t\t\t   struct commit *new_commit,\n> > +\t\t\t   struct strvec *out)\n> \n> Now that we're not using the sequencer it would be nice to stop messing\n> about converting object ids to and from strings and return an array of\n> \"struct commit\" instead of \"struct strvec\"\n\nI was trying to avoid using a strvec, but honestly that turned out to be\nmore pain than it is worth. We don't have functions like\n`strvec_splice()` for simple arrays, and there is no commit array struct\nthat provides similar wrappers, either.\n\n>  > +{\n> > +\tstruct setup_revision_opt revision_opts = {\n> > +\t\t.assume_dashdash = 1,\n> > +\t};\n> > +\tstruct strvec revisions = STRVEC_INIT;\n> > +\tstruct commit *child;\n> > +\tstruct rev_info rev = { 0 };\n> > +\tint ret;\n> > +\n> > +\trepo_init_revisions(repo, &rev, NULL);\n> > +\tstrvec_push(&revisions, \"\");\n> > +\tstrvec_push(&revisions, oid_to_hex(&new_commit->object.oid));\n> > +\tif (old_commit)\n> > +\t\tstrvec_pushf(&revisions, \"^%s\", oid_to_hex(&old_commit->object.oid));\n> > +\n> > +\tsetup_revisions_from_strvec(&revisions, &rev, &revision_opts);\n> > +\tif (revisions.nr != 1 || prepare_revision_walk(&rev)) {\n> \n> I'm not that familiar with the revision walking api, what 'revisions.nr !=\n> 1' check for here?\n\nIt's basically a check that the revision arguments have all been\nconsumed, except for the initial empty argument. The interface is a bit\nweird.\n\n[snip]\n> > +\t\tif (!onto) {\n> > +\t\t\tonto = commit;\n> > +\t\t} else {\n> > +\t\t\tstruct tree *tree = repo_get_commit_tree(repo, commit);\n> > +\t\t\tonto = replay_create_commit(repo, tree, commit, onto);\n> > +\t\t\tif (!onto)\n> > +\t\t\t\tbreak;\n> \n> Don't we want to avoid updating HEAD if replay_create_commit() fails?\n\nGood point, yes.\n\n> > +\t\t}\n> > +\t}\n> > +\n> > +\treset_opts.oid = &onto->object.oid;\n> > +\tstrbuf_addf(&buf, \"%s: switch to rewritten %s\", action, oid_to_hex(reset_opts.oid));\n> \n> We're not switching branches so I wonder if saying \"history: <action> <oid>\n> <commit subject>\" might be a more useful reflog entry\n\nWe're not switching branches, true, but we do switch to the rewritten\ncommit. Also I'm not sure that printing the commit subject here would\nmake sense, as the question becomes which subject to print: the one\nwe're moving to, which is the new tip of the branch but may not be the\ncommit we have rewritten? Or do we print the subject of the rewritten\ncommit?\n\n> > +static int fill_commit_message(struct repository *repo,\n> > +\t\t\t       const struct object_id *old_tree,\n> > +\t\t\t       const struct object_id *new_tree,\n> > +\t\t\t       const char *default_message,\n> > +\t\t\t       const char *action,\n> > +\t\t\t       struct strbuf *out)\n> > +{\n> > +\tconst char *path = git_path_commit_editmsg();\n> > +\tconst char *hint =\n> > +\t\t_(\"Please enter the commit message for the %s changes.\"\n> \n> Maybe \"Please edit the commit message\"? Also do we want to tell the user\n> they can abort by clearing the commit message?\n\nThe \"Please edit the commit message\" thing is taken from other commands\nthat phrase it similarly. But it certainly does make sense to note that\nclearing the commit message aborts, will add.\n\n> > +\t\t  \" Lines starting\\nwith '%s' will be ignored.\\n\");\n> > +\tstruct wt_status s;\n> > +\n> > +\tstrbuf_addstr(out, default_message);\n> > +\tstrbuf_addch(out, '\\n');\n> > +\tstrbuf_commented_addf(out, comment_line_str, hint, action, comment_line_str);\n> > +\twrite_file_buf(path, out->buf, out->len);\n> > +\n> > +\twt_status_prepare(repo, &s);\n> > +\tFREE_AND_NULL(s.branch);\n> > +\ts.ahead_behind_flags = AHEAD_BEHIND_QUICK;\n> > +\ts.commit_template = 1;\n> > +\ts.colopts = 0;\n> > +\ts.display_comment_prefix = 1;\n> > +\ts.hints = 0;\n> > +\ts.use_color = 0;\n> > +\ts.whence = FROM_COMMIT;\n> > +\ts.committable = 1;\n> \n> \"git commit\" reads a load of status related config settings, is any of that\n> relevant here?\n\nYeah, some of it is. We don't handle them all yet, but this will be\nbackfilled in the future.\n\n> > +\ts.fp = fopen(git_path_commit_editmsg(), \"a\");\n> > +\tif (!s.fp)\n> > +\t\treturn error_errno(_(\"could not open '%s'\"), git_path_commit_editmsg());\n> > +\n> > +\twt_status_collect_changes_trees(&s, old_tree, new_tree);\n> > +\twt_status_print(&s);\n> > +\twt_status_collect_free_buffers(&s);\n> > +\tstring_list_clear_func(&s.change, change_data_free);\n> > +\n> > +\tstrbuf_reset(out);\n> > +\tif (launch_editor(path, out, NULL)) {\n> > +\t\tfprintf(stderr, _(\"Please supply the message using the -m option.\\n\"));\n> \n> I'm not sure that's a very helpful suggestion as we don't support \"-m\" (it's\n> not very helpful when \"git commit --amend\" suggests it either). We should\n> just give up if the editor fails.\n\nAh, this is a leftover error message from previous iterations.\n\n> > +static int cmd_history_reword(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_REWORD_USAGE,\n> > +\t\tNULL,\n> > +\t};\n> > +\tstruct option options[] = {\n> > +\t\tOPT_END(),\n> > +\t};\n> > +\tstruct strbuf final_message = STRBUF_INIT;\n> > +\tstruct commit *original_commit, *parent, *head;\n> > +\tstruct strvec commits = STRVEC_INIT;\n> > +\tstruct object_id parent_tree_oid, original_commit_tree_oid;\n> > +\tstruct object_id rewritten_commit;\n> > +\tstruct commit_list *from_list = NULL;\n> > +\tconst char *original_message, *original_body, *ptr;\n> > +\tchar *original_author = NULL;\n> > +\tsize_t len;\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 single revision\"));\n> > +\t\tgoto out;\n> > +\t}\n> > +\trepo_config(repo, git_default_config, NULL);\n> > +\n> > +\toriginal_commit = lookup_commit_reference_by_name(argv[0]);\n> > +\tif (!original_commit) {\n> > +\t\tret = error(_(\"commit to be reworded cannot be found: %s\"), argv[0]);\n> > +\t\tgoto out;\n> > +\t}\n> > +\toriginal_commit_tree_oid = repo_get_commit_tree(repo, original_commit)->object.oid;\n> \n> Looking at the implementation of repo_get_commit_tree() it can return NULL\n\n> > diff --git a/t/t3450-history.sh b/t/t3450-history.sh\n> > index 417c343d43b..f513463b92b 100755\n> > --- a/t/t3450-history.sh\n> > +++ b/t/t3450-history.sh\n> > @@ -5,13 +5,13 @@ test_description='tests for git-history command'\n> >   . ./test-lib.sh\n> >   test_expect_success 'does nothing without any arguments' '\n> > -\tgit history >out 2>&1 &&\n> > -\ttest_must_be_empty out\n> > +\ttest_must_fail git history 2>err &&\n> > +\ttest_grep \"need a subcommand\" err\n> >   '\n> >   test_expect_success 'raises an error with unknown argument' '\n> >   \ttest_must_fail git history garbage 2>err &&\n> > -\ttest_grep \"unrecognized argument: garbage\" err\n> > +\ttest_grep \"unknown subcommand: .garbage.\" err\n> >   '\n> >   test_done\n> \n> Do we really need a separate test file just for a couple of tests. I can see\n> that having a separate test file for each subcommand makes sense but can't\n> we just add these two tests to one of those?\n\nI felt it was dirty to randomly add it to any of the other test suites,\nso I decided to instead have it in its own standalone file. It may also\nbecome relevant in the future if we ever needed commands like for\nexample `git history --continue`, same as the sequencer-based commands\nhave.\n\n> > diff --git a/t/t3451-history-reword.sh b/t/t3451-history-reword.sh\n> > new file mode 100755\n> > index 00000000000..09dbc463c59\n> > --- /dev/null\n> > +++ b/t/t3451-history-reword.sh\n> > @@ -0,0 +1,237 @@\n> > +#!/bin/sh\n> > +\n> > +test_description='tests for git-history reword subcommand'\n> > +\n> > +. ./test-lib.sh\n> > +\n> > +reword_with_message () {\n> > +\tcat >message &&\n> > +\twrite_script fake-editor.sh <<-EOF &&\n> > +\tcp \"$(pwd)/message\" \"\\$1\"\n> \n> Let's hope $(pwd) doesn't contain any dollar signs, backticks, backslashes\n> or double quotes. Doing\n> \n> \texport MSG_PATH=\"$(pwd)/message\"\n> \twrite_script fake-editor.sh <<-\\EOF &&\n> \tcp \"$MSG_PATH\" \"$1\"\n> \tEOF\n> \n> would be safer\n\nTrue. Will use this:\n\ndiff --git a/t/t3451-history-reword.sh b/t/t3451-history-reword.sh\nindex 8b353e74dc..4c87953176 100755\n--- a/t/t3451-history-reword.sh\n+++ b/t/t3451-history-reword.sh\n@@ -6,11 +6,11 @@ test_description='tests for git-history reword subcommand'\n \n reword_with_message () {\n \tcat >message &&\n-\twrite_script fake-editor.sh <<-EOF &&\n-\tcp \"$(pwd)/message\" \"\\$1\"\n+\twrite_script fake-editor.sh <<-\\EOF &&\n+\tcp \"$ORIG_PATH/message\" \"$1\"\n \tEOF\n \ttest_set_editor \"$(pwd)\"/fake-editor.sh &&\n-\tgit history reword \"$@\" &&\n+\tORIG_PATH=\"$(pwd)\" git history reword \"$@\" &&\n \trm fake-editor.sh message\n }\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> \n> Do we really need to set up a separate repo for each test? The test suite is\n> slow enough already without running \"git init\" followed by a bunch calls to\n> test_commit() in each test. Can we instead run \"git reset --hard\n> <known-starting-point> at the beginning of each test? That removes any\n> interdependence between tests but saves a bunch of processes.\n\nI prefer that style as it is extremely hard to reason about tests that\nhave interdependencies, and not all the state may be removed by a hard\nreset.\n\n> > +\ttest_when_finished \"rm -rf repo\" &&\n> > +\tgit init repo &&\n> > +\t(\n> > +\t\tcd repo &&\n> > +\t\ttest_commit first &&\n> > +\n> > +\t\twrite_script fake-editor.sh <<-\\EOF &&\n> > +\t\tcp \"$1\" . &&\n> > +\t\tprintf \"\\namend a comment\\n\" >>\"$1\"\n> > +\t\tEOF\n> > +\t\ttest_set_editor \"$(pwd)\"/fake-editor.sh &&\n> > +\t\tgit history reword HEAD &&\n> > +\n> > +\t\tcat >expect <<-EOF &&\n> > +\t\tfirst\n> > +\n> > +\t\t# Please enter the commit message for the reworded changes. Lines starting\n> > +\t\t# with ${SQ}#${SQ} will be ignored.\n> > +\t\t# Changes to be committed:\n> > +\t\t#\tnew file:   first.t\n> > +\t\t#\n> > +\t\tEOF\n> > +\t\ttest_cmp expect COMMIT_EDITMSG &&\n> > +\n> > +\t\tcat >expect <<-EOF &&\n> > +\t\tfirst\n> > +\n> > +\t\tamend a comment\n> > +\n> > +\t\tEOF\n> > +\t\tgit log --format=%B >actual &&\n> > +\t\ttest_cmp expect actual\n> \n> We have test_commit_message() to do this which will accept the expected\n> message on stdin.\n\nAh, indeed.\n\n> > +\t)\n> > +'\n> > +\n> > +# For now, git-history(1) does not yet execute any hooks. This is subject to\n> > +# change in the future, and if it does this test here is expected to start\n> > +# failing. In other words, this test is not an endorsement of the current\n> > +# status quo.\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\ttest_commit first &&\n> > +\t\ttest_commit second &&\n> > +\t\ttest_commit third &&\n> > +\n> > +\t\twrite_script .git/hooks/prepare-commit-msg <<-EOF &&\n> > +\t\ttouch \"$(pwd)/hooks.log\n> \n> This has the same problem of expanding $(pwd) as fake-editor.sh. For\n> debugging it would be nicer if the hook scripts did\n> \n> \techo \"$hook_name\" >>hooks.log\n> \n> so we can easily see which hooks are causing the test to fail.\n\nI'll rephrain from doing this as it would require `<<-EOF` instead of\n`<<-\\EOF`.\n\n> > +\t\tEOF\n> > +\t\twrite_script .git/hooks/post-commit <<-EOF &&\n> > +\t\ttouch \"$(pwd)/hooks.log\n> > +\t\tEOF\n> > +\t\twrite_script .git/hooks/post-rewrite <<-EOF &&\n> > +\t\ttouch \"$(pwd)/hooks.log\n> > +\t\tEOF\n> \n> This is good idea. We should add tests for the \"pre-commit\" and \"commit-msg\"\n> hooks as well.\n> \n> Overall the test coverage looks good, the only thing we might want to add is\n> a check for the reflog message. Thanks for working on this, I'll try and\n> look at the rest of the patches sometime this week.\n\nThanks!\n\nPatrick\n"},{"id":"531577","messageId":"aS81CgKzerDkHlS1@pks.im","threadId":"63986","inReplyTo":"aSVpXPtrqa0cBsEm@szeder.dev","subject":"Re: [PATCH v6 05/11] builtin/history: implement \"reword\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-12-02T18:50:50Z","receivedAt":"2025-12-02T18:51:01Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Tue, Nov 25, 2025 at 09:31:24AM +0100, SZEDER Gábor wrote:\n> On Mon, Oct 27, 2025 at 12:33:53PM +0100, Patrick Steinhardt wrote:\n> > Implement a new \"reword\" subcommand for git-history(1). This subcommand\n> > is essentially the same as if a user performed an interactive rebase\n> > with a single commit changed to use the \"reword\" verb.\n> \n> s/verb/instruction/\n> \n> The behavior is not nearly \"essentially the same\", because 'git\n> history reword' doesn't check out the reworded commit.\n\nTrue, this is a leftover from before. I'll reword this.\n\n> This is a substantial drawback when writing a commit message for\n> anything non-trivial.  I, for one, often like to take a look at the\n> \"big picture\", i.e. the actual file content in the reworded commit, in\n> case the diff included in the commit message template doesn't show\n> enough context, or even run Git commands built from that particular\n> revision to be able to accurately describe its behavior.\n> \n> OTOH, I understand that this might be deemed desirable in some cases,\n> like when rewording a commit to correct a simple typo, because source\n> file mtimes stay intact, or when the worktree contains modified files.\n> \n> It would be great if this new command could somehow support both use\n> cases.\n> \n> In any case, this significant behavior difference (and its drawbacks)\n> is not mentioned let alone justified in the commit message, it is not\n> documented anywhere, and it is not really tested, either.\n\nI guess it's a mixed bag. The big benefit of not having to check out the\ncommit is that it's as fast as it gets. All we need to do is to rewrite\ncommit history, and we don't need to check anything out. This has the\nobvious benefit of being fast, but also the less-obvious benefit of\nbeing able to deal with changes that exist in the working tree, only.\n\n> > Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> > ---\n> >  Documentation/git-history.adoc |   7 +-\n> >  builtin/history.c              | 331 ++++++++++++++++++++++++++++++++++++++++-\n> >  t/meson.build                  |   1 +\n> >  t/t3450-history.sh             |   6 +-\n> >  t/t3451-history-reword.sh      | 237 +++++++++++++++++++++++++++++\n> >  5 files changed, 573 insertions(+), 9 deletions(-)\n> > \n> > diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc\n> > index 6bdfeb50e8b..bd903875120 100644\n> > --- a/Documentation/git-history.adoc\n> > +++ b/Documentation/git-history.adoc\n> > @@ -8,7 +8,7 @@ git-history - EXPERIMENTAL: Rewrite history of the current branch\n> >  SYNOPSIS\n> >  --------\n> >  [synopsis]\n> > -git history [<options>]\n> > +git history reword <commit>\n> \n> I'm not sure I like this interface, because I have to know in advance\n> how to specify the revision I want to reword, and I usually don't know\n> that.  Choosing the commit from the rebase instruction sheet seems to\n> be much simpler, more intuitive and less error prone.\n\nFair. For me I would very much prefer this new interface, but it's\ncertainly a matter of taste.\n\nPatrick\n"},{"id":"531578","messageId":"aS81GAuKV79x_V2K@pks.im","threadId":"63986","inReplyTo":"a27fce64-0d4c-4280-93e5-00cce812d1b2@gmail.com","subject":"Re: [PATCH v6 11/11] builtin/history: implement \"split\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-12-02T18:51:04Z","receivedAt":"2025-12-02T18:51:16Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Fri, Nov 21, 2025 at 02:31:14PM +0000, Phillip Wood wrote:\n> Hi Patrick\n> \n> On 27/10/2025 11:33, Patrick Steinhardt wrote:\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> If they want to test the split commit it's even more complicated because\n> they need to stash the unstaged changes. We should think about how we can\n> add support for testing split commits to this command in the future.\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> As others have said (and I thought we'd agreed c.f.\n> <aMfdR3JE4zq-2j9b@pks.im>) I think it would be better to prompt the user to\n> edit the existing commit message when creating both commits. Elsewhere\n> Elijah mention being able to split a commit into more than two commits. I\n> wonder if we could loop running run_add_p_index() and committing the result\n> until there are no more changes left. It does mean that the user has to\n> actively select changes for the final commit though which might be annoying.\n> We can always play with that later.\n\nAh, right. Changing this now to prompt for both commit messages.\n\n> Looking below I do wonder if we can share more code between subcommands when\n> it comes to checking the commit we're given on the command line and\n> re-creating a commit and having the user edit the message.\n> \n> > +static int split_commit(struct repository *repo,\n> > +\t\t\tstruct commit *original_commit,\n> > +\t\t\tstruct pathspec *pathspec,\n> > +\t\t\tstruct object_id *out)\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> > +\tread_tree_cmd.git_cmd = 1;\n> > +\tstrvec_pushf(&read_tree_cmd.env, \"GIT_INDEX_FILE=%s\", index_file.buf);\n> > +\tstrvec_push(&read_tree_cmd.args, \"read-tree\");\n> > +\tstrvec_push(&read_tree_cmd.args, oid_to_hex(&parent_tree_oid));\n> > +\tret = run_command(&read_tree_cmd);\n> \n> Why do we need to fork \"read-tree\" here rather than call unpack_trees()\n> ourselves?\n\nThis is an artifact of how the `run_add_p()` interfaces work. They\nunfortunately do not work on top of an in-memory index, but they work on\nan on-disk index.\n\n> > [...]\n> > +\t/* We retain authorship of the original commit. */\n> > +\toriginal_message = repo_logmsg_reencode(repo, original_commit, NULL, NULL);\n> > +\tptr = find_commit_header(original_message, \"author\", &len);\n> > +\tif (ptr)\n> > +\t\toriginal_author = xmemdupz(ptr, len);\n> > +\n> > +\tret = fill_commit_message(repo, &parent_tree_oid, &split_tree->object.oid,\n> > +\t\t\t\t  \"\", \"split-out\", &split_message);\n> > +\tif (ret < 0)\n> > +\t\tgoto out;\n> > +\n> > +\tret = commit_tree(split_message.buf, split_message.len, &split_tree->object.oid,\n> > +\t\t\t  original_commit->parents, &out[0], original_author, NULL);\n> > +\tif (ret < 0) {\n> > +\t\tret = error(_(\"failed writing split-out commit\"));\n> > +\t\tgoto out;\n> > +\t}\n> \n> Don't we have the same code for rewording a commit, maybe we should package\n> this up into a shared helper function.\n\nHm, indeed, there's a bit of non-trivial logic here. I'll refactor this.\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> > +\tstruct option options[] = {\n> > +\t\tOPT_END(),\n> > +\t};\n> > +\tstruct oidmap rewritten_commits = OIDMAP_INIT;\n> > +\tstruct commit *original_commit, *parent, *head;\n> > +\tstruct strvec commits = STRVEC_INIT;\n> > +\tstruct commit_list *from_list = NULL;\n> > +\tstruct object_id split_commits[2];\n> > +\tstruct pathspec pathspec = { 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 revision\"));\n> > +\t\tgoto out;\n> > +\t}\n> > +\trepo_config(repo, git_default_config, NULL);\n> > +\n> > +\toriginal_commit = lookup_commit_reference_by_name(argv[0]);\n> > +\tif (!original_commit) {\n> > +\t\tret = error(_(\"commit to be split cannot be found: %s\"), argv[0]);\n> > +\t\tgoto out;\n> > +\t}\n> > +\n> > +\tparent = original_commit->parents ? original_commit->parents->item : NULL;\n> > +\tif (parent && repo_parse_commit(repo, parent)) {\n> > +\t\tret = error(_(\"unable to parse commit %s\"),\n> > +\t\t\t    oid_to_hex(&parent->object.oid));\n> > +\t\tgoto out;\n> > +\t}\n> > +\n> > +\thead = lookup_commit_reference_by_name(\"HEAD\");\n> > +\tif (!head) {\n> > +\t\tret = error(_(\"could not resolve HEAD to a commit\"));\n> > +\t\tgoto out;\n> > +\t}\n> > +\n> > +\tcommit_list_append(original_commit, &from_list);\n> > +\tif (!repo_is_descendant_of(repo, head, from_list)) {\n> > +\t\tret = error(_(\"split commit must be reachable from current HEAD commit\"));\n> > +\t\tgoto out;\n> > +\t}\n> \n> This is very similar to cmd_history_reword() up to this point. When we add\n> the \"drop\" and \"amend\" subcommands they're going to want to do the same\n> checks.\n> \n> > +\tparse_pathspec(&pathspec, 0,\n> > +\t\tPATHSPEC_PREFER_FULL | PATHSPEC_SYMLINK_LEADING_PATH | PATHSPEC_PREFIX_ORIGIN,\n> > +\t\tprefix, argv + 1);\n> \n> This and calling split_commit() below are the only real differences with\n> cmd_history_reword(), is it worth trying to share some more code between the\n> two?\n\nYup, done.\n\nThanks!\n\nPatrick\n"},{"id":"531594","messageId":"CALnO6CDG5EMha8k4DPy=p0FT1hO_SeU0wxm6qn2+Q92xNY6hqQ@mail.gmail.com","threadId":"63986","inReplyTo":"aS80co7VTABD6nXs@pks.im","subject":"Re: [PATCH v6 04/11] builtin: add new \"history\" command","fromName":"D. Ben Knoble","fromEmail":"ben.knoble@gmail.com","sentAt":"2025-12-02T22:44:15Z","receivedAt":"2025-12-02T22:44:27Z","isPatch":true,"sender":{"key":"ben.knoble@gmail.com","avatar":"https://avatars.githubusercontent.com/u/22802209?v=4"},"body":"On Tue, Dec 2, 2025 at 1:48 PM Patrick Steinhardt <ps@pks.im> wrote:\n>\n> On Wed, Nov 19, 2025 at 11:02:20PM -0800, Elijah Newren wrote:\n> > In addition to what Phillip commented on...\n> >\n> > On Mon, Oct 27, 2025 at 4:34 AM Patrick Steinhardt <ps@pks.im> wrote:\n> > >\n> > > When rewriting history via git-rebase(1) there are a couple of very\n> > > common use cases:\n> > >\n> > >   - The ordering of two commits should be reversed.\n> > >\n> > >   - A commit should be split up into two commits.\n> > >\n> > >   - A commit should be dropped from the history completely.\n> > >\n> > >   - Multiple commits should be squashed into one.\n> > >\n> > > While these operations are all doable, it often feels needlessly kludgey\n> > > to do so by doing an interactive rebase, using the editor to say what\n> > > one wants, and then perform the actions. Furthermore, some operations\n> > > like splitting up a commit into two are way more involved than that and\n> > > require a whole series of commands.\n> > >\n> > > Add a new \"history\" command to plug this gap. This command will have\n> > > several different subcommands to imperatively rewrite history for common\n> > > use cases like the above. These subcommands will be implemented in\n> > > subsequent commits.\n> >\n> > \"...*Some of* these subcommands will be implemented...\", right?  You\n> > only implement two of them in this series, not all of them, or am I\n> > reading wrong?\n>\n> No, you're right. The initial versions of this series implemented more\n> of the above commands, but at no point in time did we actually implement\n> all of them.\n>\n> Patrick\n\nWhile I'm thinking of it, at work today I had occasion to use \"drop\"\nand \"reorder\" (from an old version of this series whose binary I\nhappened to still have laying around), and it was very convenient.\nLooking forward to it ;)\n\n- drop: I had made some changes on my tree that needed to be in a\nseparate branch. I didn't want to mess with stashes for some reason,\nso I did \"commit; switch -c …; cherry-pick @@{1}\" (or something\nsimilar). Then when coming back (switch -), I could just do \"drop @\".\nI'm sure there's a better way to do this slicing that wouldn't have\nneeded drop, but I couldn't think of it at the time.\n\n- reorder: I have a series of mostly logical, separate wip commits\nthat need some more explanation and might need further tweaks as I go;\nI'm working on uncommitted changes that logically belong to the tip,\nbut spot something else that will be a separate commit. I make that\ncommit, then \"reorder @ --before-commit=@~\", and voilà, I'm ready to\namend again when I get around to committing the rest of what I have.\n\n-- \nD. Ben Knoble\n"},{"id":"531603","messageId":"aTAVb337XsGuRUTa@pks.im","threadId":"63986","inReplyTo":"CALnO6CDG5EMha8k4DPy=p0FT1hO_SeU0wxm6qn2+Q92xNY6hqQ@mail.gmail.com","subject":"Re: [PATCH v6 04/11] builtin: add new \"history\" command","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-12-03T10:48:15Z","receivedAt":"2025-12-03T10:48:24Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Tue, Dec 02, 2025 at 05:44:15PM -0500, D. Ben Knoble wrote:\n> On Tue, Dec 2, 2025 at 1:48 PM Patrick Steinhardt <ps@pks.im> wrote:\n> >\n> > On Wed, Nov 19, 2025 at 11:02:20PM -0800, Elijah Newren wrote:\n> > > In addition to what Phillip commented on...\n> > >\n> > > On Mon, Oct 27, 2025 at 4:34 AM Patrick Steinhardt <ps@pks.im> wrote:\n> > > >\n> > > > When rewriting history via git-rebase(1) there are a couple of very\n> > > > common use cases:\n> > > >\n> > > >   - The ordering of two commits should be reversed.\n> > > >\n> > > >   - A commit should be split up into two commits.\n> > > >\n> > > >   - A commit should be dropped from the history completely.\n> > > >\n> > > >   - Multiple commits should be squashed into one.\n> > > >\n> > > > While these operations are all doable, it often feels needlessly kludgey\n> > > > to do so by doing an interactive rebase, using the editor to say what\n> > > > one wants, and then perform the actions. Furthermore, some operations\n> > > > like splitting up a commit into two are way more involved than that and\n> > > > require a whole series of commands.\n> > > >\n> > > > Add a new \"history\" command to plug this gap. This command will have\n> > > > several different subcommands to imperatively rewrite history for common\n> > > > use cases like the above. These subcommands will be implemented in\n> > > > subsequent commits.\n> > >\n> > > \"...*Some of* these subcommands will be implemented...\", right?  You\n> > > only implement two of them in this series, not all of them, or am I\n> > > reading wrong?\n> >\n> > No, you're right. The initial versions of this series implemented more\n> > of the above commands, but at no point in time did we actually implement\n> > all of them.\n> >\n> > Patrick\n> \n> While I'm thinking of it, at work today I had occasion to use \"drop\"\n> and \"reorder\" (from an old version of this series whose binary I\n> happened to still have laying around), and it was very convenient.\n> Looking forward to it ;)\n> \n> - drop: I had made some changes on my tree that needed to be in a\n> separate branch. I didn't want to mess with stashes for some reason,\n> so I did \"commit; switch -c …; cherry-pick @@{1}\" (or something\n> similar). Then when coming back (switch -), I could just do \"drop @\".\n> I'm sure there's a better way to do this slicing that wouldn't have\n> needed drop, but I couldn't think of it at the time.\n> \n> - reorder: I have a series of mostly logical, separate wip commits\n> that need some more explanation and might need further tweaks as I go;\n> I'm working on uncommitted changes that logically belong to the tip,\n> but spot something else that will be a separate commit. I make that\n> commit, then \"reorder @ --before-commit=@~\", and voilà, I'm ready to\n> amend again when I get around to committing the rest of what I have.\n\nYeah, I definitely do wish to also upstream these two going forward.\nIt's going to be a bit more complicated though now that we are building\non top of the replay infrastructure, because it doesn't know to handle\nconflicts.\n\nMaybe for an initial version we can get away with just bailing out on a\nconflict and then iterate.\n\nPatrick\n"},{"id":"531604","messageId":"20251203-b4-pks-history-builtin-v7-0-9e9f849bfd0e@pks.im","threadId":"63986","inReplyTo":"20250819-b4-pks-history-builtin-v1-0-9b77c32688fe@pks.im","subject":"[PATCH v7 00/12] Introduce git-history(1) command for easy history editing","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-12-03T10:48:27Z","receivedAt":"2025-12-03T10:48:39Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Hi,\n\nover recent months I've been playing around with Jujutsu quite\nfrequently. While I still prefer using Git, there's been a couple\nfeatures in it that I really like and that I'd like to have in Git, as\nwell.\n\nA copule of these features relate to history editing. Most importantly,\nI really dig the following commands:\n\n  - jj-abandon(1) to drop a specific commit from your history.\n\n  - jj-absorb(1) to take some changes and automatically apply them to\n    commits in your history that last modified the respective hunks.\n\n  - jj-split(1) to split a commit into two.\n\n  - jj-new(1) to insert a new commit after or before a specific other\n    commit.\n\nNot all of these commands can be ported directly into Git. jj-new(1) for\nexample doesn't really make a ton of sense for us, I'd claim. But some\nof these commands _do_ make sense.\n\nThis patch series is a starting point for such a command. I've\nsignificantly slimmed it down from the first couple revisions now\nfollowing the discussions at the Contributor's Summit yesterday. This\nwas my intent anyway, as I already mentioned on the last iteration.\n\nChanges in v7:\n  - I've rebased the patch series on top of f0ef5b6d9b (The fifth batch,\n    2025-11-30) with pw/replay-exclude-gpgsig-fix at 9f3a115087 (replay:\n    do not copy \"gpgsign-sha256\" header, 2025-11-26) merged into it to\n    fix some conflicts.\n  - I refactored a bunch of code to be shared between split and reword.\n  - I am now using \"--ancestry-path\" and \"--reverse\" to enumerate\n    commits.\n  - `git history split` now asks for commit messages for both commits.\n  - `git history split` no longer allows the user to edit hunks so that\n    conflicts become impossible. This may be reintroduced at a later\n    point in time.\n  - A bunch of test improvements.\n  - Several commit message and documentation improvements.\n  - Link to v6: https://lore.kernel.org/r/20251027-b4-pks-history-builtin-v6-0-407dd3f57ad3@pks.im\n\nChanges in v6:\n  - I've rebased the patch series again to pull in the latest updates\n    from sa/replay-atomic-ref-updates and fix conflicts. It is now based\n    on 4e98b730f1 (The twenty-fourth batch, 2025-10-24) with ab661bb1bb\n    (replay: add replay.refAction config option, 2025-10-23) merged into\n    it.\n  - I've dropped the \"-m\" options for now, so commit messages are always\n    asked for via the editor. These can be introduced in a subsequent\n    patch series once discussion around them has settled.\n  - We don't use the merge machinery anymore to pick the commits.\n  - Drop the commit to parse commits in the replay machinery. It didn't\n    seem to be necessary in v5 anymore, and now that we don't use the\n    merge machinery at all we don't ever take that code path in the\n    first place.\n  - Link to v5:\n    https://lore.kernel.org/r/20251021-b4-pks-history-builtin-v5-0-78d23f578fe6@pks.im\n\nChanges in v5:\n  - I've changed the patch series to be based on top of 133d151831 (The\n    twenty-first batch, 2025-10-20) with sa/replay-atomic-ref-updates at\n    a1c22e627e (SQAUASH??? t0450 band-aid, 2025-10-14) merged into it.\n    This is one the one hand to fix a conflict, but also to get some of\n    the CI updates to make GitLab CI work again.\n  - Some slight commit message improvements.\n  - Deduplicate subcommand usage strings by using defines.\n  - Fix the desendancy checks to properly verify that HEAD is a\n    descendant of the commit to be rewritten. Also add some tests for\n    this.\n  - Fix the hint that mentions that lines starting with the comment\n    character will be tripped after having written the commit message.\n  - Move an include to the correct commit.\n  - Link to v4: https://lore.kernel.org/r/20251001-b4-pks-history-builtin-v4-0-8e61ddb86317@pks.im\n\nChanges in v4:\n  - I've rebuilt the patch series. It is now based on 821f583da6 (The\n    thirteenth batcn, 2025-09-29) with sa/replay-atomic-ref-updates\n    at 665c66a743 (replay: make atomic ref updates the default behavior,\n    2025-09-27) merged into it. This should fix all conflicts with seen.\n  - I've reworked this patch series to use the same infra as\n    git-replay(1), as discussed during the Contributor's Summit.\n  - I've slimmed down the patch series to only tackle those commands\n    that cannot result in a conflict to keep it simple. I also learned\n    that Elijah has been working on a \"git replay edit\" command, so I\n    dropped that command so that we can instead use his version.\n  - During the Contributor's Summit we have agreed that for now, we\n    won't care about hook execution just yet. This may be backfilled at\n    a later point in time.\n  - I dropped \"commit.verbose\" handling for now, as my understanding of\n    it was wrong at first. This is something we should backfill.\n  - Link to v3: https://lore.kernel.org/r/20250904-b4-pks-history-builtin-v3-0-509053514755@pks.im\n\nChanges in v3:\n  - Add logic to drive the \"post-rewrite\" hook and add tests to verify\n    that all hooks are executed as expected.\n  - Deduplicate logic to turn a replay action into a todo command.\n  - Move the addition of tests for the top-level git-history(1) command\n    to the correct commit.\n  - Some smaller commit message fixes.\n  - Honor \"commit.verbose\".\n  - Fix copy-paste error with an error message.\n  - Link to v2: https://lore.kernel.org/r/20250824-b4-pks-history-builtin-v2-0-964ac12f65bd@pks.im\n\nChanges in v2:\n  - Add a new \"reword\" subcommand.\n  - List git-history(1) in \"command-list.txt\".\n  - Add some missing error handling.\n  - Simplify calling convention of `apply_commits()` to handle root\n    commits internally instead of requiring every caller to do so.\n  - Add tests to verify that git-history(1) refuses to work with changes\n    in the worktree or index.\n  - Mark git-history(1) as experimental.\n  - Introduce commands to manage interrupted history edits.\n  - A bunch of improvements to the manpage.\n  - Link to v1: https://lore.kernel.org/r/20250819-b4-pks-history-builtin-v1-0-9b77c32688fe@pks.im\n\nThanks!\n\nPatrick\n\n---\nPatrick Steinhardt (12):\n      wt-status: provide function to expose status for trees\n      replay: extract logic to pick commits\n      replay: stop using `the_repository`\n      builtin: add new \"history\" command\n      builtin/history: implement \"reword\" subcommand\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: implement \"split\" subcommand\n\n .gitignore                     |   1 +\n Documentation/git-history.adoc | 109 ++++++++\n Documentation/meson.build      |   1 +\n Makefile                       |   2 +\n add-interactive.c              | 174 +++----------\n add-interactive.h              |  46 +---\n add-patch.c                    | 335 +++++++++++++++++++++----\n add-patch.h                    |  71 ++++++\n builtin.h                      |   1 +\n builtin/add.c                  |  22 +-\n builtin/checkout.c             |   9 +-\n builtin/commit.c               |  16 +-\n builtin/history.c              | 558 +++++++++++++++++++++++++++++++++++++++++\n builtin/replay.c               | 110 +-------\n builtin/reset.c                |  16 +-\n builtin/stash.c                |  46 ++--\n cache-tree.c                   |   4 +-\n cache-tree.h                   |   3 +-\n command-list.txt               |   1 +\n commit.h                       |   2 +-\n git.c                          |   1 +\n meson.build                    |   2 +\n replay.c                       | 115 +++++++++\n replay.h                       |  23 ++\n t/meson.build                  |   3 +\n t/t3450-history.sh             |  17 ++\n t/t3451-history-reword.sh      | 236 +++++++++++++++++\n t/t3452-history-split.sh       | 452 +++++++++++++++++++++++++++++++++\n wt-status.c                    |  24 ++\n wt-status.h                    |   9 +\n 30 files changed, 2014 insertions(+), 395 deletions(-)\n\nRange-diff versus v6:\n\n 1:  d59b2ff389 =  1:  ccf19f9067 wt-status: provide function to expose status for trees\n 2:  7b80b4d482 !  2:  39c5a05ee1 replay: extract logic to pick commits\n    @@ Commit message\n         Signed-off-by: Patrick Steinhardt <ps@pks.im>\n     \n      ## Makefile ##\n    -@@ Makefile: LIB_OBJS += reftable/tree.o\n    - LIB_OBJS += reftable/writer.o\n    - LIB_OBJS += remote.o\n    +@@ Makefile: LIB_OBJS += repack-geometry.o\n    + LIB_OBJS += repack-midx.o\n    + LIB_OBJS += repack-promisor.o\n      LIB_OBJS += replace-object.o\n     +LIB_OBJS += replay.o\n      LIB_OBJS += repo-settings.o\n    @@ builtin/replay.c\n      #include \"strmap.h\"\n      #include <oidset.h>\n     @@ builtin/replay.c: enum ref_action_mode {\n    - \tREF_ACTION_PRINT\n    + \tREF_ACTION_PRINT,\n      };\n      \n     -static const char *short_commit_name(struct repository *repo,\n    @@ builtin/replay.c: static struct commit *peel_committish(struct repository *repo,\n     -\tconst char *message = repo_logmsg_reencode(repo, based_on,\n     -\t\t\t\t\t\t   NULL, out_enc);\n     -\tconst char *orig_message = NULL;\n    --\tconst char *exclude_gpgsig[] = { \"gpgsig\", NULL };\n    +-\tconst char *exclude_gpgsig[] = { \"gpgsig\", \"gpgsig-sha256\", NULL };\n     -\n     -\tcommit_list_insert(parent, &parents);\n     -\textra = read_commit_extra_headers(based_on, exclude_gpgsig);\n    @@ builtin/replay.c: static void determine_replay_mode(struct repository *repo,\n     -\treturn create_commit(repo, result->tree, pickme, replayed_base);\n     -}\n     -\n    - static int handle_ref_update(enum ref_action_mode mode,\n    - \t\t\t     struct ref_transaction *transaction,\n    - \t\t\t     const char *refname,\n    + static enum ref_action_mode parse_ref_action_mode(const char *ref_action, const char *source)\n    + {\n    + \tif (!ref_action || !strcmp(ref_action, \"update\"))\n     @@ builtin/replay.c: int cmd_replay(int argc,\n      \t\tif (commit->parents->next)\n      \t\t\tdie(_(\"replaying merge commits is not supported yet!\"));\n    @@ builtin/replay.c: int cmd_replay(int argc,\n     \n      ## meson.build ##\n     @@ meson.build: libgit_sources = [\n    -   'reftable/writer.c',\n    -   'remote.c',\n    +   'repack-midx.c',\n    +   'repack-promisor.c',\n        'replace-object.c',\n     +  'replay.c',\n        'repo-settings.c',\n    @@ replay.c (new)\n     +\tconst char *message = repo_logmsg_reencode(repo, based_on,\n     +\t\t\t\t\t\t   NULL, out_enc);\n     +\tconst char *orig_message = NULL;\n    -+\tconst char *exclude_gpgsig[] = { \"gpgsig\", NULL };\n    ++\tconst char *exclude_gpgsig[] = { \"gpgsig\", \"gpgsig-sha256\", NULL };\n     +\n     +\tcommit_list_insert(parent, &parents);\n     +\textra = read_commit_extra_headers(based_on, exclude_gpgsig);\n 3:  3f66e8423d =  3:  fe73ba4059 replay: stop using `the_repository`\n 4:  f64dba0b08 !  4:  c162f407db builtin: add new \"history\" command\n    @@ Metadata\n      ## Commit message ##\n         builtin: add new \"history\" command\n     \n    -    When rewriting history via git-rebase(1) there are a couple of very\n    -    common use cases:\n    +    When rewriting history via git-rebase(1) there are a few very common use\n    +    cases:\n     \n           - The ordering of two commits should be reversed.\n     \n    @@ Commit message\n     \n           - Multiple commits should be squashed into one.\n     \n    +      - Editing an existing commit that is not the tip of the current\n    +        branch.\n    +\n         While these operations are all doable, it often feels needlessly kludgey\n         to do so by doing an interactive rebase, using the editor to say what\n         one wants, and then perform the actions. Furthermore, some operations\n    @@ Commit message\n     \n         Add a new \"history\" command to plug this gap. This command will have\n         several different subcommands to imperatively rewrite history for common\n    -    use cases like the above. These subcommands will be implemented in\n    -    subsequent commits.\n    +    use cases like the above. Some of these subcommands will be implemented\n    +    in subsequent commits.\n     \n         Signed-off-by: Patrick Steinhardt <ps@pks.im>\n     \n    @@ Documentation/git-history.adoc (new)\n     +\n     +THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.\n     +\n    -+This command is similar to linkgit:git-rebase[1] and uses the same\n    -+underlying machinery. You should use rebases if you want to reapply a range of\n    -+commits onto a different base, or interactive rebases if you want to edit a\n    ++This command is related to linkgit:git-rebase[1] in that both commands can be\n    ++used to rewrite history. You should use rebases if you want to reapply a range\n    ++of commits onto a different base, or interactive rebases if you want to edit a\n     +range of commits.\n     +\n     +Note that this command does not (yet) work with histories that contain\n    @@ Documentation/git-history.adoc (new)\n     +\n     +include::includes/cmd-config-section-all.adoc[]\n     +\n    -+include::config/sequencer.adoc[]\n    -+\n     +GIT\n     +---\n     +Part of the linkgit:git[1] suite\n 5:  3c1c599e9f !  5:  3ecbe6ec83 builtin/history: implement \"reword\" subcommand\n    @@ Commit message\n         builtin/history: implement \"reword\" subcommand\n     \n         Implement a new \"reword\" subcommand for git-history(1). This subcommand\n    -    is essentially the same as if a user performed an interactive rebase\n    -    with a single commit changed to use the \"reword\" verb.\n    +    is similar to the user performing an interactive rebase with a single\n    +    commit changed to use the \"reword\" instruction.\n    +\n    +    The major difference is that we do not check out the commit that is to\n    +    be reworded. This has the obvious benefit of being significantly faster\n    +    compared to git-rebase(1), but even more importantly it allows the user\n    +    to rewrite history even if there are local changes in the working tree\n    +    or in the index.\n     \n         Signed-off-by: Patrick Steinhardt <ps@pks.im>\n     \n    @@ builtin/history.c\n     +\tint ret;\n     +\n     +\trepo_init_revisions(repo, &rev, NULL);\n    ++\trev.reverse = 1;\n     +\tstrvec_push(&revisions, \"\");\n     +\tstrvec_push(&revisions, oid_to_hex(&new_commit->object.oid));\n    -+\tif (old_commit)\n    ++\tif (old_commit) {\n     +\t\tstrvec_pushf(&revisions, \"^%s\", oid_to_hex(&old_commit->object.oid));\n    ++\t\tstrvec_pushf(&revisions, \"--ancestry-path=%s\", oid_to_hex(&old_commit->object.oid));\n    ++\t}\n     +\n     +\tsetup_revisions_from_strvec(&revisions, &rev, &revision_opts);\n     +\tif (revisions.nr != 1 || prepare_revision_walk(&rev)) {\n    @@ builtin/history.c\n     +\t\t}\n     +\n     +\t\tstrvec_push(out, oid_to_hex(&child->object.oid));\n    -+\n    -+\t\tif (child->parents && old_commit &&\n    -+\t\t    commit_list_contains(old_commit, child->parents))\n    -+\t\t\tbreak;\n     +\t}\n     +\n    -+\t/*\n    -+\t * Revisions are in newest-order-first. We have to reverse the\n    -+\t * array though so that we pick the oldest commits first.\n    -+\t */\n    -+\tfor (size_t i = 0, j = out->nr - 1; i < j; i++, j--)\n    -+\t\tSWAP(out->v[i], out->v[j]);\n    -+\n     +\tret = 0;\n     +\n     +out:\n    @@ builtin/history.c\n     +\treturn ret;\n     +}\n     +\n    ++static int gather_commits_between_head_and_revision(struct repository *repo,\n    ++\t\t\t\t\t\t    const char *revision,\n    ++\t\t\t\t\t\t    struct commit **original_commit,\n    ++\t\t\t\t\t\t    struct commit **parent_commit,\n    ++\t\t\t\t\t\t    struct commit **head,\n    ++\t\t\t\t\t\t    struct strvec *commits)\n    ++{\n    ++\tstruct commit_list *from_list = NULL;\n    ++\tint ret;\n    ++\n    ++\t*original_commit = lookup_commit_reference_by_name(revision);\n    ++\tif (!*original_commit) {\n    ++\t\tret = error(_(\"commit cannot be found: %s\"), revision);\n    ++\t\tgoto out;\n    ++\t}\n    ++\n    ++\t*parent_commit = (*original_commit)->parents ? (*original_commit)->parents->item : NULL;\n    ++\tif (*parent_commit && repo_parse_commit(repo, *parent_commit)) {\n    ++\t\tret = error(_(\"unable to parse commit %s\"),\n    ++\t\t\t    oid_to_hex(&(*parent_commit)->object.oid));\n    ++\t\tgoto out;\n    ++\t}\n    ++\n    ++\t*head = lookup_commit_reference_by_name(\"HEAD\");\n    ++\tif (!(*head)) {\n    ++\t\tret = error(_(\"could not resolve HEAD to a commit\"));\n    ++\t\tgoto out;\n    ++\t}\n    ++\n    ++\tcommit_list_append(*original_commit, &from_list);\n    ++\tif (!repo_is_descendant_of(repo, *head, from_list)) {\n    ++\t\tret = error(_(\"commit must be reachable from current HEAD commit\"));\n    ++\t\tgoto out;\n    ++\t}\n    ++\n    ++\t/*\n    ++\t * Collect the list of commits that we'll have to reapply now already.\n    ++\t * This ensures that we'll abort early on in case the range of commits\n    ++\t * contains merges, which we do not yet handle.\n    ++\t */\n    ++\tret = collect_commits(repo, *parent_commit, *head, commits);\n    ++\tif (ret < 0)\n    ++\t\tgoto out;\n    ++\n    ++out:\n    ++\tfree_commit_list(from_list);\n    ++\treturn ret;\n    ++}\n    ++\n     +static void replace_commits(struct strvec *commits,\n     +\t\t\t    const struct object_id *commit_to_replace,\n     +\t\t\t    const struct object_id *replacements,\n    @@ builtin/history.c\n     +\t\t} else {\n     +\t\t\tstruct tree *tree = repo_get_commit_tree(repo, commit);\n     +\t\t\tonto = replay_create_commit(repo, tree, commit, onto);\n    -+\t\t\tif (!onto)\n    -+\t\t\t\tbreak;\n    ++\t\t\tif (!onto) {\n    ++\t\t\t\tret = -1;\n    ++\t\t\t\tgoto out;\n    ++\t\t\t}\n     +\t\t}\n     +\t}\n     +\n    @@ builtin/history.c\n     +\tconst char *path = git_path_commit_editmsg();\n     +\tconst char *hint =\n     +\t\t_(\"Please enter the commit message for the %s changes.\"\n    -+\t\t  \" Lines starting\\nwith '%s' will be ignored.\\n\");\n    ++\t\t  \" Lines starting\\nwith '%s' will be ignored, and an\"\n    ++\t\t  \" empty message aborts the commit.\\n\");\n     +\tstruct wt_status s;\n     +\n     +\tstrbuf_addstr(out, default_message);\n    @@ builtin/history.c\n     +\n     +\tstrbuf_reset(out);\n     +\tif (launch_editor(path, out, NULL)) {\n    -+\t\tfprintf(stderr, _(\"Please supply the message using the -m option.\\n\"));\n    ++\t\tfprintf(stderr, _(\"Aborting commit as launching the editor failed.\\n\"));\n     +\t\treturn -1;\n     +\t}\n     +\tstrbuf_stripspace(out, comment_line_str);\n    @@ builtin/history.c\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_commit,\n    ++\t\t\t\t\t   const struct object_id *new_tree_oid,\n    ++\t\t\t\t\t   const struct commit_list *parents,\n    ++\t\t\t\t\t   const struct object_id *parent_tree_oid,\n    ++\t\t\t\t\t   struct object_id *out)\n    ++{\n    ++\tconst char *exclude_gpgsig[] = { \"gpgsig\", \"gpgsig-sha256\", NULL };\n    ++\tconst char *original_message, *original_body, *ptr;\n    ++\tstruct commit_extra_header *original_extra_headers = NULL;\n    ++\tstruct strbuf commit_message = STRBUF_INIT;\n    ++\tchar *original_author = NULL;\n    ++\tsize_t len;\n    ++\tint ret;\n    ++\n    ++\t/* We retain authorship of the original commit. */\n    ++\toriginal_message = repo_logmsg_reencode(repo, original_commit, 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, new_tree_oid,\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_commit, exclude_gpgsig);\n    ++\n    ++\tret = commit_tree_extended(commit_message.buf, commit_message.len, new_tree_oid,\n    ++\t\t\t\t   parents, out, original_author, NULL, NULL,\n    ++\t\t\t\t   original_extra_headers);\n    ++\tif (ret < 0)\n    ++\t\tgoto out;\n    ++\n    ++out:\n    ++\tfree_commit_extra_headers(original_extra_headers);\n    ++\tstrbuf_release(&commit_message);\n    ++\tfree(original_author);\n    ++\treturn ret;\n    ++}\n    ++\n     +static int cmd_history_reword(int argc,\n     +\t\t\t      const char **argv,\n     +\t\t\t      const char *prefix,\n    @@ builtin/history.c\n     +\tstruct option options[] = {\n     +\t\tOPT_END(),\n     +\t};\n    -+\tstruct strbuf final_message = STRBUF_INIT;\n     +\tstruct commit *original_commit, *parent, *head;\n     +\tstruct strvec commits = STRVEC_INIT;\n     +\tstruct object_id parent_tree_oid, original_commit_tree_oid;\n     +\tstruct object_id rewritten_commit;\n    -+\tstruct commit_list *from_list = NULL;\n    -+\tconst char *original_message, *original_body, *ptr;\n    -+\tchar *original_author = NULL;\n    -+\tsize_t len;\n     +\tint ret;\n     +\n     +\targc = parse_options(argc, argv, prefix, options, usage, 0);\n    @@ builtin/history.c\n     +\t}\n     +\trepo_config(repo, git_default_config, NULL);\n     +\n    -+\toriginal_commit = lookup_commit_reference_by_name(argv[0]);\n    -+\tif (!original_commit) {\n    -+\t\tret = error(_(\"commit to be reworded cannot be found: %s\"), argv[0]);\n    ++\tret = gather_commits_between_head_and_revision(repo, argv[0], &original_commit,\n    ++\t\t\t\t\t\t       &parent, &head, &commits);\n    ++\tif (ret < 0)\n     +\t\tgoto out;\n    -+\t}\n    -+\toriginal_commit_tree_oid = repo_get_commit_tree(repo, original_commit)->object.oid;\n     +\n    -+\tparent = original_commit->parents ? original_commit->parents->item : NULL;\n    -+\tif (parent) {\n    -+\t\tif (repo_parse_commit(repo, parent)) {\n    -+\t\t\tret = error(_(\"unable to parse commit %s\"),\n    -+\t\t\t\t    oid_to_hex(&parent->object.oid));\n    -+\t\t\tgoto out;\n    -+\t\t}\n    ++\toriginal_commit_tree_oid = repo_get_commit_tree(repo, original_commit)->object.oid;\n    ++\tif (parent)\n     +\t\tparent_tree_oid = repo_get_commit_tree(repo, parent)->object.oid;\n    -+\t} else {\n    ++\telse\n     +\t\toidcpy(&parent_tree_oid, repo->hash_algo->empty_tree);\n    -+\t}\n    -+\n    -+\thead = lookup_commit_reference_by_name(\"HEAD\");\n    -+\tif (!head) {\n    -+\t\tret = error(_(\"could not resolve HEAD to a commit\"));\n    -+\t\tgoto out;\n    -+\t}\n    -+\n    -+\tcommit_list_append(original_commit, &from_list);\n    -+\tif (!repo_is_descendant_of(repo, head, from_list)) {\n    -+\t\tret = error (_(\"split commit must be reachable from current HEAD commit\"));\n    -+\t\tgoto out;\n    -+\t}\n    -+\n    -+\t/*\n    -+\t * Collect the list of commits that we'll have to reapply now already.\n    -+\t * This ensures that we'll abort early on in case the range of commits\n    -+\t * contains merges, which we do not yet handle.\n    -+\t */\n    -+\tret = collect_commits(repo, parent, head, &commits);\n    -+\tif (ret < 0)\n    -+\t\tgoto out;\n     +\n     +\t/* We retain authorship of the original commit. */\n    -+\toriginal_message = repo_logmsg_reencode(repo, original_commit, 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_commit_tree_oid,\n    -+\t\t\t\t  original_body, \"reworded\", &final_message);\n    -+\tif (ret < 0)\n    -+\t\tgoto out;\n    -+\n    -+\tret = commit_tree(final_message.buf, final_message.len, &original_commit_tree_oid,\n    -+\t\t\t  original_commit->parents, &rewritten_commit, original_author, NULL);\n    ++\tret = commit_tree_with_edited_message(repo, \"reworded\", original_commit,\n    ++\t\t\t\t\t      &original_commit_tree_oid,\n    ++\t\t\t\t\t      original_commit->parents, &parent_tree_oid,\n    ++\t\t\t\t\t      &rewritten_commit);\n     +\tif (ret < 0) {\n     +\t\tret = error(_(\"failed writing reworded commit\"));\n     +\t\tgoto out;\n    @@ builtin/history.c\n     +\tret = 0;\n     +\n     +out:\n    -+\tstrbuf_release(&final_message);\n    -+\tfree_commit_list(from_list);\n     +\tstrvec_clear(&commits);\n    -+\tfree(original_author);\n     +\treturn ret;\n     +}\n      \n    @@ t/t3451-history-reword.sh (new)\n     +\n     +reword_with_message () {\n     +\tcat >message &&\n    -+\twrite_script fake-editor.sh <<-EOF &&\n    -+\tcp \"$(pwd)/message\" \"\\$1\"\n    ++\twrite_script fake-editor.sh <<-\\EOF &&\n    ++\tcp message \"$1\"\n     +\tEOF\n     +\ttest_set_editor \"$(pwd)\"/fake-editor.sh &&\n     +\tgit history reword \"$@\" &&\n    @@ t/t3451-history-reword.sh (new)\n     +\t\tgit switch branch &&\n     +\t\ttest_commit theirs &&\n     +\t\ttest_must_fail git history reword ours 2>err &&\n    -+\t\ttest_grep \"split commit must be reachable from current HEAD commit\" err\n    ++\t\ttest_grep \"commit must be reachable from current HEAD commit\" err\n     +\t)\n     +'\n     +\n    @@ t/t3451-history-reword.sh (new)\n     +\t\tfirst\n     +\t\tEOF\n     +\t\tgit log --format=%s >actual &&\n    -+\t\ttest_cmp expect actual\n    ++\t\ttest_cmp expect actual &&\n    ++\n    ++\t\tgit reflog >reflog &&\n    ++\t\ttest_grep \"reword: updating HEAD\" reflog\n     +\t)\n     +'\n     +\n    @@ t/t3451-history-reword.sh (new)\n     +\t)\n     +'\n     +\n    -+test_expect_success 'can use editor to rewrite commit message' '\n    ++test_expect_success 'editor shows proper status' '\n     +\ttest_when_finished \"rm -rf repo\" &&\n     +\tgit init repo &&\n     +\t(\n    @@ t/t3451-history-reword.sh (new)\n     +\t\tfirst\n     +\n     +\t\t# Please enter the commit message for the reworded changes. Lines starting\n    -+\t\t# with ${SQ}#${SQ} will be ignored.\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:   first.t\n     +\t\t#\n     +\t\tEOF\n     +\t\ttest_cmp expect COMMIT_EDITMSG &&\n     +\n    -+\t\tcat >expect <<-EOF &&\n    ++\t\ttest_commit_message HEAD <<-\\EOF\n     +\t\tfirst\n     +\n     +\t\tamend a comment\n    -+\n     +\t\tEOF\n    -+\t\tgit log --format=%B >actual &&\n    -+\t\ttest_cmp expect actual\n     +\t)\n     +'\n     +\n    @@ t/t3451-history-reword.sh (new)\n     +\t\ttest_commit second &&\n     +\t\ttest_commit third &&\n     +\n    -+\t\twrite_script .git/hooks/prepare-commit-msg <<-EOF &&\n    -+\t\ttouch \"$(pwd)/hooks.log\n    -+\t\tEOF\n    -+\t\twrite_script .git/hooks/post-commit <<-EOF &&\n    -+\t\ttouch \"$(pwd)/hooks.log\n    -+\t\tEOF\n    -+\t\twrite_script .git/hooks/post-rewrite <<-EOF &&\n    -+\t\ttouch \"$(pwd)/hooks.log\n    -+\t\tEOF\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\treword_with_message HEAD~ <<-EOF &&\n     +\t\tsecond reworded\n 6:  7d736c2f8d =  6:  a1c6ad546d add-patch: split out header from \"add-interactive.h\"\n 7:  da50162d06 !  7:  eea2ac7a18 add-patch: split out `struct interactive_options`\n    @@ add-patch.c: static int patch_update_file(struct add_p_state *s,\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\tbreak;\n    + \t\tif (read_single_character(s) == EOF) {\n    + \t\t\tquit = 1;\n     @@ add-patch.c: static int patch_update_file(struct add_p_state *s,\n      \t\t\t\terr(s, _(\"Sorry, cannot split this hunk\"));\n      \t\t\t} else if (!split_hunk(s, file_diff,\n 8:  a888ae8a29 !  8:  95be396a5a add-patch: remove dependency on \"add-interactive\" subsystem\n    @@ add-patch.c: static int patch_update_file(struct add_p_state *s,\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\tbreak;\n    + \t\tif (read_single_character(s) == EOF) {\n    + \t\t\tquit = 1;\n     @@ add-patch.c: static int patch_update_file(struct add_p_state *s,\n      \t\t\t\terr(s, _(\"Sorry, cannot split this hunk\"));\n      \t\t\t} else if (!split_hunk(s, file_diff,\n 9:  e84e2e265d !  9:  d114c622a8 add-patch: add support for in-memory index patching\n    @@ add-patch.c: static int patch_update_file(struct add_p_state *s,\n      \t}\n      \n      \tputchar('\\n');\n    -@@ add-patch.c: int run_add_p(struct repository *r, enum add_p_mode mode,\n    + \treturn quit;\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    ++\n    ++\tif (parse_diff(state, ps) < 0)\n    ++\t\treturn -1;\n    ++\n    ++\tfor (size_t i = 0; i < state->file_diff_nr; i++) {\n    ++\t\tif (state->file_diff[i].binary && !state->file_diff[i].hunk_nr)\n    ++\t\t\tbinary_count++;\n    ++\t\telse if (patch_update_file(state, state->file_diff + i))\n    ++\t\t\tbreak;\n    ++\t}\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    @@ add-patch.c: int run_add_p(struct repository *r, enum add_p_mode mode,\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     @@ add-patch.c: 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; i++)\n    +-\t\tif (s.file_diff[i].binary && !s.file_diff[i].hunk_nr)\n    +-\t\t\tbinary_count++;\n    +-\t\telse if (patch_update_file(&s, s.file_diff + i))\n    +-\t\t\tbreak;\n    ++\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    - }\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    @@ add-patch.c: int run_add_p(struct repository *r, enum add_p_mode mode,\n     +\t};\n     +\tstruct strbuf parent_revision = STRBUF_INIT;\n     +\tchar parent_tree_oid[GIT_MAX_HEXSZ + 1];\n    -+\tsize_t binary_count = 0;\n     +\tstruct commit *commit;\n     +\tint ret;\n     +\n    @@ add-patch.c: int run_add_p(struct repository *r, enum add_p_mode mode,\n     +\n     +\tinteractive_config_init(&s.cfg, r, opts);\n     +\n    -+\tif (parse_diff(&s, ps) < 0) {\n    -+\t\tret = -1;\n    ++\tret = run_add_p_common(&s, ps);\n    ++\tif (ret < 0)\n     +\t\tgoto out;\n    -+\t}\n    -+\n    -+\tfor (size_t i = 0; i < s.file_diff_nr; i++) {\n    -+\t\tif (s.file_diff[i].binary && !s.file_diff[i].hunk_nr)\n    -+\t\t\tbinary_count++;\n    -+\t\telse if (patch_update_file(&s, s.file_diff + i))\n    -+\t\t\tbreak;\n    -+\t}\n    -+\n    -+\tif (s.file_diff_nr == 0) {\n    -+\t\terr(&s, _(\"No changes.\"));\n    -+\t\tret = -1;\n    -+\t\tgoto out;\n    -+\t}\n    -+\n    -+\tif (binary_count == s.file_diff_nr) {\n    -+\t\terr(&s, _(\"Only binary files changed.\"));\n    -+\t\tret = -1;\n    -+\t\tgoto out;\n    -+\t}\n     +\n     +\tret = 0;\n     +\n    @@ add-patch.c: int run_add_p(struct repository *r, enum add_p_mode mode,\n     +\tstrbuf_release(&parent_revision);\n     +\tadd_p_state_clear(&s);\n     +\treturn ret;\n    -+}\n    + }\n     \n      ## add-patch.h ##\n     @@\n -:  ---------- > 10:  6f8c443002 add-patch: allow disabling editing of hunks\n10:  2a43a306de ! 11:  f5f4ab21bb cache-tree: allow writing in-memory index as tree\n    @@ cache-tree.c: static int write_index_as_tree_internal(struct object_id *oid,\n      \twas_valid = index_state->cache_tree &&\n      \t\t    cache_tree_fully_valid(index_state->cache_tree);\n      \n    -@@ cache-tree.c: struct tree* write_in_core_index_as_tree(struct repository *repo) {\n    - \treturn lookup_tree(repo, &index_state->cache_tree->oid);\n    - }\n    - \n    --\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    - {\n    - \tint entries, was_valid;\n     \n      ## cache-tree.h ##\n     @@ cache-tree.h: int cache_tree_verify(struct repository *, struct index_state *);\n11:  bbb837e8d6 ! 12:  0d294bf943 builtin/history: implement \"split\" subcommand\n    @@ Documentation/git-history.adoc: Several commands are available to rewrite histor\n     +\tstays intact, except that its parent will be the newly split-out\n     +\tcommit.\n     ++\n    -+The commit message of the new commit will be asked for by launching the\n    -+configured editor. Authorship of the commit will be the same as for the\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    @@ Documentation/git-history.adoc: Several commands are available to rewrite histor\n      CONFIGURATION\n      -------------\n      \n    -@@ Documentation/git-history.adoc: include::includes/cmd-config-section-all.adoc[]\n    - \n    - include::config/sequencer.adoc[]\n    + include::includes/cmd-config-section-all.adoc[]\n      \n     +EXAMPLES\n     +--------\n    @@ builtin/history.c: static int cmd_history_reword(int argc,\n     +\t\t\tstruct object_id *out)\n     +{\n     +\tstruct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;\n    -+\tstruct strbuf index_file = STRBUF_INIT, split_message = STRBUF_INIT;\n    ++\tstruct strbuf index_file = STRBUF_INIT;\n     +\tstruct child_process read_tree_cmd = CHILD_PROCESS_INIT;\n     +\tstruct index_state index = INDEX_STATE_INIT(repo);\n     +\tstruct object_id original_commit_tree_oid, parent_tree_oid;\n    -+\tconst char *original_message, *original_body, *ptr;\n     +\tchar original_commit_oid[GIT_MAX_HEXSZ + 1];\n    -+\tchar *original_author = NULL;\n     +\tstruct commit_list *parents = NULL;\n     +\tstruct commit *first_commit;\n     +\tstruct tree *split_tree;\n    -+\tsize_t len;\n     +\tint ret;\n     +\n     +\tif (original_commit->parents)\n    @@ builtin/history.c: static int cmd_history_reword(int argc,\n     +\n     +\toid_to_hex_r(original_commit_oid, &original_commit->object.oid);\n     +\tret = run_add_p_index(repo, &index, index_file.buf, &interactive_opts,\n    -+\t\t\t      original_commit_oid, pathspec);\n    ++\t\t\t      original_commit_oid, pathspec, ADD_P_DISALLOW_EDIT);\n     +\tif (ret < 0)\n     +\t\tgoto out;\n     +\n    @@ builtin/history.c: static int cmd_history_reword(int argc,\n     +\t\tgoto out;\n     +\t}\n     +\n    -+\t/* We retain authorship of the original commit. */\n    -+\toriginal_message = repo_logmsg_reencode(repo, original_commit, NULL, NULL);\n    -+\tptr = find_commit_header(original_message, \"author\", &len);\n    -+\tif (ptr)\n    -+\t\toriginal_author = xmemdupz(ptr, len);\n    -+\n    -+\tret = fill_commit_message(repo, &parent_tree_oid, &split_tree->object.oid,\n    -+\t\t\t\t  \"\", \"split-out\", &split_message);\n    -+\tif (ret < 0)\n    -+\t\tgoto out;\n    -+\n    -+\tret = commit_tree(split_message.buf, split_message.len, &split_tree->object.oid,\n    -+\t\t\t  original_commit->parents, &out[0], original_author, NULL);\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(repo, \"split-out\", original_commit,\n    ++\t\t\t\t\t      &split_tree->object.oid,\n    ++\t\t\t\t\t      original_commit->parents, &parent_tree_oid, &out[0]);\n     +\tif (ret < 0) {\n     +\t\tret = error(_(\"failed writing split-out commit\"));\n     +\t\tgoto out;\n     +\t}\n     +\n     +\t/*\n    -+\t* The second commit is much simpler to construct, as we can simply use\n    -+\t* the original commit details, except that we adjust its parent to be\n    -+\t* the newly split-out commit.\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    -+\tfind_commit_subject(original_message, &original_body);\n     +\tfirst_commit = lookup_commit_reference(repo, &out[0]);\n     +\tcommit_list_append(first_commit, &parents);\n     +\n    -+\tret = commit_tree(original_body, strlen(original_body), &original_commit_tree_oid,\n    -+\t\t\t  parents, &out[1], original_author, NULL);\n    ++\tret = commit_tree_with_edited_message(repo, \"split-out\", original_commit,\n    ++\t\t\t\t\t      &original_commit_tree_oid,\n    ++\t\t\t\t\t      parents, get_commit_tree_oid(first_commit), &out[1]);\n     +\tif (ret < 0) {\n    -+\t\tret = error(_(\"failed writing second commit\"));\n    ++\t\tret = error(_(\"failed writing split-out commit\"));\n     +\t\tgoto out;\n     +\t}\n     +\n    @@ builtin/history.c: static int cmd_history_reword(int argc,\n     +out:\n     +\tif (index_file.len)\n     +\t\tunlink(index_file.buf);\n    -+\tstrbuf_release(&split_message);\n     +\tstrbuf_release(&index_file);\n     +\tfree_commit_list(parents);\n    -+\tfree(original_author);\n     +\trelease_index(&index);\n     +\treturn ret;\n     +}\n    @@ builtin/history.c: static int cmd_history_reword(int argc,\n     +\tstruct option options[] = {\n     +\t\tOPT_END(),\n     +\t};\n    -+\tstruct oidmap rewritten_commits = OIDMAP_INIT;\n     +\tstruct commit *original_commit, *parent, *head;\n     +\tstruct strvec commits = STRVEC_INIT;\n    -+\tstruct commit_list *from_list = NULL;\n     +\tstruct object_id split_commits[2];\n     +\tstruct pathspec pathspec = { 0 };\n     +\tint ret;\n    @@ builtin/history.c: static int cmd_history_reword(int argc,\n     +\t}\n     +\trepo_config(repo, git_default_config, NULL);\n     +\n    -+\toriginal_commit = lookup_commit_reference_by_name(argv[0]);\n    -+\tif (!original_commit) {\n    -+\t\tret = error(_(\"commit to be split cannot be found: %s\"), argv[0]);\n    -+\t\tgoto out;\n    -+\t}\n    -+\n    -+\tparent = original_commit->parents ? original_commit->parents->item : NULL;\n    -+\tif (parent && repo_parse_commit(repo, parent)) {\n    -+\t\tret = error(_(\"unable to parse commit %s\"),\n    -+\t\t\t    oid_to_hex(&parent->object.oid));\n    -+\t\tgoto out;\n    -+\t}\n    -+\n    -+\thead = lookup_commit_reference_by_name(\"HEAD\");\n    -+\tif (!head) {\n    -+\t\tret = error(_(\"could not resolve HEAD to a commit\"));\n    -+\t\tgoto out;\n    -+\t}\n    -+\n    -+\tcommit_list_append(original_commit, &from_list);\n    -+\tif (!repo_is_descendant_of(repo, head, from_list)) {\n    -+\t\tret = error(_(\"split commit must be reachable from current HEAD commit\"));\n    -+\t\tgoto out;\n    -+\t}\n    -+\n     +\tparse_pathspec(&pathspec, 0,\n     +\t\tPATHSPEC_PREFER_FULL | PATHSPEC_SYMLINK_LEADING_PATH | PATHSPEC_PREFIX_ORIGIN,\n     +\t\tprefix, argv + 1);\n     +\n    -+\t/*\n    -+\t * Collect the list of commits that we'll have to reapply now already.\n    -+\t * This ensures that we'll abort early on in case the range of commits\n    -+\t * contains merges, which we do not yet handle.\n    -+\t */\n    -+\tret = collect_commits(repo, parent, head, &commits);\n    ++\tret = gather_commits_between_head_and_revision(repo, argv[0], &original_commit,\n    ++\t\t\t\t\t\t       &parent, &head, &commits);\n     +\tif (ret < 0)\n     +\t\tgoto out;\n     +\n    @@ builtin/history.c: static int cmd_history_reword(int argc,\n     +\tret = 0;\n     +\n     +out:\n    -+\toidmap_clear(&rewritten_commits, 0);\n    -+\tfree_commit_list(from_list);\n     +\tclear_pathspec(&pathspec);\n     +\tstrvec_clear(&commits);\n     +\treturn ret;\n    @@ t/t3452-history-split.sh (new)\n     +\n     +. ./test-lib.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    -+\twrite_script fake-editor.sh <<-EOF &&\n    -+\techo \"$@\" >\"\\$1\"\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    @@ t/t3452-history-split.sh (new)\n     +\t\tgit switch branch &&\n     +\t\ttest_commit theirs &&\n     +\t\ttest_must_fail git history split ours 2>err &&\n    -+\t\ttest_grep \"split commit must be reachable from current HEAD commit\" err\n    ++\t\ttest_grep \"commit must be reachable from current HEAD commit\" err\n     +\t)\n     +'\n     +\n    @@ t/t3452-history-split.sh (new)\n     +\t\tgit commit -m split-me &&\n     +\n     +\t\tgit symbolic-ref HEAD >expect &&\n    -+\t\tset_fake_editor \"split-out commit\" &&\n    ++\t\tset_fake_editor \"first\" \"second\" &&\n     +\t\tgit history split HEAD <<-EOF &&\n     +\t\ty\n     +\t\tn\n    @@ t/t3452-history-split.sh (new)\n     +\t\ttest_cmp expect actual &&\n     +\n     +\t\texpect_log <<-EOF &&\n    -+\t\tsplit-me\n    -+\t\tsplit-out commit\n    ++\t\tsecond\n    ++\t\tfirst\n     +\t\tinitial\n     +\t\tEOF\n     +\n    @@ t/t3452-history-split.sh (new)\n     +\t\tinitial.t\n     +\t\tEOF\n     +\n    -+\t\texpect_tree_entries HEAD <<-EOF\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    @@ t/t3452-history-split.sh (new)\n     +\t\tgit commit -m root &&\n     +\t\ttest_commit tip &&\n     +\n    -+\t\tset_fake_editor \"split-out commit\" &&\n    ++\t\tset_fake_editor \"first\" \"second\" &&\n     +\t\tgit history split HEAD~ <<-EOF &&\n     +\t\ty\n     +\t\tn\n    @@ t/t3452-history-split.sh (new)\n     +\n     +\t\texpect_log <<-EOF &&\n     +\t\ttip\n    -+\t\troot\n    -+\t\tsplit-out commit\n    ++\t\tsecond\n    ++\t\tfirst\n     +\t\tEOF\n     +\n     +\t\texpect_tree_entries HEAD~2 <<-EOF &&\n    @@ t/t3452-history-split.sh (new)\n     +\t\tgit commit -m split-me &&\n     +\t\ttest_commit tip &&\n     +\n    -+\t\tset_fake_editor \"split-out commit\" &&\n    ++\t\tset_fake_editor \"first\" \"second\" &&\n     +\t\tgit history split HEAD~ <<-EOF &&\n     +\t\ty\n     +\t\tn\n    @@ t/t3452-history-split.sh (new)\n     +\n     +\t\texpect_log <<-EOF &&\n     +\t\ttip\n    -+\t\tsplit-me\n    -+\t\tsplit-out commit\n    ++\t\tsecond\n    ++\t\tfirst\n     +\t\tinitial\n     +\t\tEOF\n     +\n    @@ t/t3452-history-split.sh (new)\n     +\t\tgit add . &&\n     +\t\tgit commit -m split-me &&\n     +\n    -+\t\tset_fake_editor \"split-out-commit\" &&\n    ++\t\tset_fake_editor \"first\" \"second\" &&\n     +\t\tgit history split HEAD <<-EOF &&\n     +\t\ty\n     +\t\tn\n    @@ t/t3452-history-split.sh (new)\n     +\t\tgit add . &&\n     +\t\tgit commit -m split-me &&\n     +\n    -+\t\tset_fake_editor \"split-out commit\" &&\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\tsplit-me\n    -+\t\tsplit-out commit\n    ++\t\tsecond\n    ++\t\tfirst\n     +\t\tEOF\n     +\n     +\t\texpect_tree_entries HEAD~ <<-EOF &&\n    @@ t/t3452-history-split.sh (new)\n     +\t\tgit commit -m split-me &&\n     +\n     +\t\twrite_script fake-editor.sh <<-\\EOF &&\n    -+\t\tcp \"$1\" . &&\n    -+\t\techo \"some commit message\" >>\"$1\"\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/t3452-history-split.sh (new)\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.\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 COMMIT_EDITMSG &&\n    ++\t\ttest_cmp expect MESSAGES &&\n     +\n     +\t\texpect_log <<-EOF\n    -+\t\tsplit-me\n    ++\t\tsome commit message\n     +\t\tsome commit message\n     +\t\tEOF\n     +\t)\n    @@ t/t3452-history-split.sh (new)\n     +\t\tgit add . &&\n     +\t\tgit commit -m split-me &&\n     +\n    -+\t\tset_fake_editor \"split-out commit\" &&\n    ++\t\tset_fake_editor \"first\" \"second\" &&\n     +\t\tgit history split HEAD -- foo <<-EOF &&\n     +\t\ty\n     +\t\tEOF\n    @@ t/t3452-history-split.sh (new)\n     +\t)\n     +'\n     +\n    -+test_expect_success 'hooks are executed for rewritten commits' '\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/t3452-history-split.sh (new)\n     +\t\tgit commit -m split-me &&\n     +\t\told_head=$(git rev-parse HEAD) &&\n     +\n    -+\t\twrite_script .git/hooks/prepare-commit-msg <<-EOF &&\n    -+\t\ttouch \"$(pwd)/hooks.log\"\n    -+\t\tEOF\n    -+\t\twrite_script .git/hooks/post-commit <<-EOF &&\n    -+\t\ttouch \"$(pwd)/hooks.log\"\n    -+\t\tEOF\n    -+\t\twrite_script .git/hooks/post-rewrite <<-EOF &&\n    -+\t\ttouch \"$(pwd)/hooks.log\"\n    -+\t\tEOF\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 \"split-out commit\" &&\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\tsplit-me\n    -+\t\tsplit-out commit\n    ++\t\tsecond\n    ++\t\tfirst\n     +\t\tEOF\n     +\n     +\t\ttest_path_is_missing hooks.log\n    @@ t/t3452-history-split.sh (new)\n     +\t\techo a-modified >a &&\n     +\t\techo b-modified >b &&\n     +\t\tgit add b &&\n    -+\t\tset_fake_editor \"a-only\" &&\n    ++\t\tset_fake_editor \"a-only\" \"remainder\" &&\n     +\t\tgit history split HEAD <<-EOF &&\n     +\t\ty\n     +\t\tn\n    @@ t/t3452-history-split.sh (new)\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\n---\nbase-commit: 0d4583b45432bff1cdc7689c648fb51be3d7b321\nchange-id: 20250819-b4-pks-history-builtin-83398f9a05f0\n\n"},{"id":"531605","messageId":"20251203-b4-pks-history-builtin-v7-1-9e9f849bfd0e@pks.im","threadId":"63986","inReplyTo":"20251203-b4-pks-history-builtin-v7-0-9e9f849bfd0e@pks.im","subject":"[PATCH v7 01/12] wt-status: provide function to expose status for trees","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-12-03T10:48:28Z","receivedAt":"2025-12-03T10:48:42Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The \"wt-status\" subsystem is responsible for printing status information\naround the current state of the working tree. This most importantly\nincludes information around whether the working tree or the index have\nany changes.\n\nWe're about to introduce a new command where the changes in neither of\nthem are actually relevant to us. Instead, what we want is to format the\nchanges between two different trees. While it is a little bit of a\nstretch to add this as functionality to _working tree_ status, it\ndoesn't make any sense to open-code this functionality, either.\n\nImplement a new function `wt_status_collect_changes_trees()` that diffs\ntwo trees and formats the status accordingly. This function is not yet\nused, but will be in a subsequent commit.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n wt-status.c | 24 ++++++++++++++++++++++++\n wt-status.h |  9 +++++++++\n 2 files changed, 33 insertions(+)\n\ndiff --git a/wt-status.c b/wt-status.c\nindex e12adb26b9..95942399f8 100644\n--- a/wt-status.c\n+++ b/wt-status.c\n@@ -612,6 +612,30 @@ static void wt_status_collect_updated_cb(struct diff_queue_struct *q,\n \t}\n }\n \n+void wt_status_collect_changes_trees(struct wt_status *s,\n+\t\t\t\t     const struct object_id *old_treeish,\n+\t\t\t\t     const struct object_id *new_treeish)\n+{\n+\tstruct diff_options opts = { 0 };\n+\n+\trepo_diff_setup(s->repo, &opts);\n+\topts.output_format = DIFF_FORMAT_CALLBACK;\n+\topts.format_callback = wt_status_collect_updated_cb;\n+\topts.format_callback_data = s;\n+\topts.detect_rename = s->detect_rename >= 0 ? s->detect_rename : opts.detect_rename;\n+\topts.rename_limit = s->rename_limit >= 0 ? s->rename_limit : opts.rename_limit;\n+\topts.rename_score = s->rename_score >= 0 ? s->rename_score : opts.rename_score;\n+\topts.flags.recursive = 1;\n+\tdiff_setup_done(&opts);\n+\n+\tdiff_tree_oid(old_treeish, new_treeish, \"\", &opts);\n+\tdiffcore_std(&opts);\n+\tdiff_flush(&opts);\n+\twt_status_get_state(s->repo, &s->state, 0);\n+\n+\tdiff_free(&opts);\n+}\n+\n static void wt_status_collect_changes_worktree(struct wt_status *s)\n {\n \tstruct rev_info rev;\ndiff --git a/wt-status.h b/wt-status.h\nindex e40a27214a..e9fe32e98c 100644\n--- a/wt-status.h\n+++ b/wt-status.h\n@@ -153,6 +153,15 @@ void wt_status_add_cut_line(struct wt_status *s);\n void wt_status_prepare(struct repository *r, struct wt_status *s);\n void wt_status_print(struct wt_status *s);\n void wt_status_collect(struct wt_status *s);\n+\n+/*\n+ * Collect all changes between the two trees. Changes will be displayed as if\n+ * they were staged into the index.\n+ */\n+void wt_status_collect_changes_trees(struct wt_status *s,\n+\t\t\t\t     const struct object_id *old_treeish,\n+\t\t\t\t     const struct object_id *new_treeish);\n+\n /*\n  * Frees the buffers allocated by wt_status_collect.\n  */\n\n-- \n2.52.0.239.gd5f0c6e74e.dirty\n\n"},{"id":"531606","messageId":"20251203-b4-pks-history-builtin-v7-2-9e9f849bfd0e@pks.im","threadId":"63986","inReplyTo":"20251203-b4-pks-history-builtin-v7-0-9e9f849bfd0e@pks.im","subject":"[PATCH v7 02/12] replay: extract logic to pick commits","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-12-03T10:48:29Z","receivedAt":"2025-12-03T10:48:45Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"We're about to add a new git-history(1) command that will reuse some of\nthe same infrastructure as git-replay(1). To prepare for this, extract\nthe logic to pick a commit into a new \"replay.c\" file so that it can be\nshared between both commands.\n\nRename the function to have a \"replay_\" prefix to clearly indicate its\nsubsystem.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Makefile         |   1 +\n builtin/replay.c | 110 ++--------------------------------------------------\n meson.build      |   1 +\n replay.c         | 115 +++++++++++++++++++++++++++++++++++++++++++++++++++++++\n replay.h         |  23 +++++++++++\n 5 files changed, 143 insertions(+), 107 deletions(-)\n\ndiff --git a/Makefile b/Makefile\nindex 237b56fc9d..809b094595 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -1274,6 +1274,7 @@ LIB_OBJS += repack-geometry.o\n LIB_OBJS += repack-midx.o\n LIB_OBJS += repack-promisor.o\n LIB_OBJS += replace-object.o\n+LIB_OBJS += replay.o\n LIB_OBJS += repo-settings.o\n LIB_OBJS += repository.o\n LIB_OBJS += rerere.o\ndiff --git a/builtin/replay.c b/builtin/replay.c\nindex 1f9ffd2b3e..f974a8c963 100644\n--- a/builtin/replay.c\n+++ b/builtin/replay.c\n@@ -2,7 +2,6 @@\n  * \"git replay\" builtin command\n  */\n \n-#define USE_THE_REPOSITORY_VARIABLE\n #define DISABLE_SIGN_COMPARE_WARNINGS\n \n #include \"git-compat-util.h\"\n@@ -16,6 +15,7 @@\n #include \"object-name.h\"\n #include \"parse-options.h\"\n #include \"refs.h\"\n+#include \"replay.h\"\n #include \"revision.h\"\n #include \"strmap.h\"\n #include <oidset.h>\n@@ -26,13 +26,6 @@ enum ref_action_mode {\n \tREF_ACTION_PRINT,\n };\n \n-static const char *short_commit_name(struct repository *repo,\n-\t\t\t\t     struct commit *commit)\n-{\n-\treturn repo_find_unique_abbrev(repo, &commit->object.oid,\n-\t\t\t\t       DEFAULT_ABBREV);\n-}\n-\n static struct commit *peel_committish(struct repository *repo, const char *name)\n {\n \tstruct object *obj;\n@@ -45,59 +38,6 @@ static struct commit *peel_committish(struct repository *repo, const char *name)\n \t\t\t\t\t\t  OBJ_COMMIT);\n }\n \n-static char *get_author(const char *message)\n-{\n-\tsize_t len;\n-\tconst char *a;\n-\n-\ta = find_commit_header(message, \"author\", &len);\n-\tif (a)\n-\t\treturn xmemdupz(a, len);\n-\n-\treturn NULL;\n-}\n-\n-static struct commit *create_commit(struct repository *repo,\n-\t\t\t\t    struct tree *tree,\n-\t\t\t\t    struct commit *based_on,\n-\t\t\t\t    struct commit *parent)\n-{\n-\tstruct object_id ret;\n-\tstruct object *obj = NULL;\n-\tstruct commit_list *parents = NULL;\n-\tchar *author;\n-\tchar *sign_commit = NULL; /* FIXME: cli users might want to sign again */\n-\tstruct commit_extra_header *extra = NULL;\n-\tstruct strbuf msg = STRBUF_INIT;\n-\tconst char *out_enc = get_commit_output_encoding();\n-\tconst char *message = repo_logmsg_reencode(repo, based_on,\n-\t\t\t\t\t\t   NULL, out_enc);\n-\tconst char *orig_message = NULL;\n-\tconst char *exclude_gpgsig[] = { \"gpgsig\", \"gpgsig-sha256\", NULL };\n-\n-\tcommit_list_insert(parent, &parents);\n-\textra = read_commit_extra_headers(based_on, exclude_gpgsig);\n-\tfind_commit_subject(message, &orig_message);\n-\tstrbuf_addstr(&msg, orig_message);\n-\tauthor = get_author(message);\n-\treset_ident_date();\n-\tif (commit_tree_extended(msg.buf, msg.len, &tree->object.oid, parents,\n-\t\t\t\t &ret, author, NULL, sign_commit, extra)) {\n-\t\terror(_(\"failed to write commit object\"));\n-\t\tgoto out;\n-\t}\n-\n-\tobj = parse_object(repo, &ret);\n-\n-out:\n-\trepo_unuse_commit_buffer(the_repository, based_on, message);\n-\tfree_commit_extra_headers(extra);\n-\tfree_commit_list(parents);\n-\tstrbuf_release(&msg);\n-\tfree(author);\n-\treturn (struct commit *)obj;\n-}\n-\n struct ref_info {\n \tstruct commit *onto;\n \tstruct strset positive_refs;\n@@ -246,50 +186,6 @@ static void determine_replay_mode(struct repository *repo,\n \tstrset_clear(&rinfo.positive_refs);\n }\n \n-static struct commit *mapped_commit(kh_oid_map_t *replayed_commits,\n-\t\t\t\t    struct commit *commit,\n-\t\t\t\t    struct commit *fallback)\n-{\n-\tkhint_t pos = kh_get_oid_map(replayed_commits, commit->object.oid);\n-\tif (pos == kh_end(replayed_commits))\n-\t\treturn fallback;\n-\treturn kh_value(replayed_commits, pos);\n-}\n-\n-static struct commit *pick_regular_commit(struct repository *repo,\n-\t\t\t\t\t  struct commit *pickme,\n-\t\t\t\t\t  kh_oid_map_t *replayed_commits,\n-\t\t\t\t\t  struct commit *onto,\n-\t\t\t\t\t  struct merge_options *merge_opt,\n-\t\t\t\t\t  struct merge_result *result)\n-{\n-\tstruct commit *base, *replayed_base;\n-\tstruct tree *pickme_tree, *base_tree;\n-\n-\tbase = pickme->parents->item;\n-\treplayed_base = mapped_commit(replayed_commits, base, onto);\n-\n-\tresult->tree = repo_get_commit_tree(repo, replayed_base);\n-\tpickme_tree = repo_get_commit_tree(repo, pickme);\n-\tbase_tree = repo_get_commit_tree(repo, base);\n-\n-\tmerge_opt->branch1 = short_commit_name(repo, replayed_base);\n-\tmerge_opt->branch2 = short_commit_name(repo, pickme);\n-\tmerge_opt->ancestor = xstrfmt(\"parent of %s\", merge_opt->branch2);\n-\n-\tmerge_incore_nonrecursive(merge_opt,\n-\t\t\t\t  base_tree,\n-\t\t\t\t  result->tree,\n-\t\t\t\t  pickme_tree,\n-\t\t\t\t  result);\n-\n-\tfree((char*)merge_opt->ancestor);\n-\tmerge_opt->ancestor = NULL;\n-\tif (!result->clean)\n-\t\treturn NULL;\n-\treturn create_commit(repo, result->tree, pickme, replayed_base);\n-}\n-\n static enum ref_action_mode parse_ref_action_mode(const char *ref_action, const char *source)\n {\n \tif (!ref_action || !strcmp(ref_action, \"update\"))\n@@ -495,8 +391,8 @@ int cmd_replay(int argc,\n \t\tif (commit->parents->next)\n \t\t\tdie(_(\"replaying merge commits is not supported yet!\"));\n \n-\t\tlast_commit = pick_regular_commit(repo, commit, replayed_commits,\n-\t\t\t\t\t\t  onto, &merge_opt, &result);\n+\t\tlast_commit = replay_pick_regular_commit(repo, commit, replayed_commits,\n+\t\t\t\t\t\t\t onto, &merge_opt, &result);\n \t\tif (!last_commit)\n \t\t\tbreak;\n \ndiff --git a/meson.build b/meson.build\nindex f1b3615659..1878dff2b1 100644\n--- a/meson.build\n+++ b/meson.build\n@@ -470,6 +470,7 @@ libgit_sources = [\n   'repack-midx.c',\n   'repack-promisor.c',\n   'replace-object.c',\n+  'replay.c',\n   'repo-settings.c',\n   'repository.c',\n   'rerere.c',\ndiff --git a/replay.c b/replay.c\nnew file mode 100644\nindex 0000000000..fb906e9b51\n--- /dev/null\n+++ b/replay.c\n@@ -0,0 +1,115 @@\n+#define USE_THE_REPOSITORY_VARIABLE\n+\n+#include \"git-compat-util.h\"\n+#include \"commit.h\"\n+#include \"environment.h\"\n+#include \"gettext.h\"\n+#include \"ident.h\"\n+#include \"object.h\"\n+#include \"object-name.h\"\n+#include \"replay.h\"\n+#include \"tree.h\"\n+\n+static const char *short_commit_name(struct repository *repo,\n+\t\t\t\t     struct commit *commit)\n+{\n+\treturn repo_find_unique_abbrev(repo, &commit->object.oid,\n+\t\t\t\t       DEFAULT_ABBREV);\n+}\n+\n+static char *get_author(const char *message)\n+{\n+\tsize_t len;\n+\tconst char *a;\n+\n+\ta = find_commit_header(message, \"author\", &len);\n+\tif (a)\n+\t\treturn xmemdupz(a, len);\n+\n+\treturn NULL;\n+}\n+\n+struct commit *replay_create_commit(struct repository *repo,\n+\t\t\t\t    struct tree *tree,\n+\t\t\t\t    struct commit *based_on,\n+\t\t\t\t    struct commit *parent)\n+{\n+\tstruct object_id ret;\n+\tstruct object *obj = NULL;\n+\tstruct commit_list *parents = NULL;\n+\tchar *author;\n+\tchar *sign_commit = NULL; /* FIXME: cli users might want to sign again */\n+\tstruct commit_extra_header *extra = NULL;\n+\tstruct strbuf msg = STRBUF_INIT;\n+\tconst char *out_enc = get_commit_output_encoding();\n+\tconst char *message = repo_logmsg_reencode(repo, based_on,\n+\t\t\t\t\t\t   NULL, out_enc);\n+\tconst char *orig_message = NULL;\n+\tconst char *exclude_gpgsig[] = { \"gpgsig\", \"gpgsig-sha256\", NULL };\n+\n+\tcommit_list_insert(parent, &parents);\n+\textra = read_commit_extra_headers(based_on, exclude_gpgsig);\n+\tfind_commit_subject(message, &orig_message);\n+\tstrbuf_addstr(&msg, orig_message);\n+\tauthor = get_author(message);\n+\treset_ident_date();\n+\tif (commit_tree_extended(msg.buf, msg.len, &tree->object.oid, parents,\n+\t\t\t\t &ret, author, NULL, sign_commit, extra)) {\n+\t\terror(_(\"failed to write commit object\"));\n+\t\tgoto out;\n+\t}\n+\n+\tobj = parse_object(repo, &ret);\n+\n+out:\n+\trepo_unuse_commit_buffer(the_repository, based_on, message);\n+\tfree_commit_extra_headers(extra);\n+\tfree_commit_list(parents);\n+\tstrbuf_release(&msg);\n+\tfree(author);\n+\treturn (struct commit *)obj;\n+}\n+\n+static struct commit *mapped_commit(kh_oid_map_t *replayed_commits,\n+\t\t\t\t    struct commit *commit,\n+\t\t\t\t    struct commit *fallback)\n+{\n+\tkhint_t pos = kh_get_oid_map(replayed_commits, commit->object.oid);\n+\tif (pos == kh_end(replayed_commits))\n+\t\treturn fallback;\n+\treturn kh_value(replayed_commits, pos);\n+}\n+\n+struct commit *replay_pick_regular_commit(struct repository *repo,\n+\t\t\t\t\t  struct commit *pickme,\n+\t\t\t\t\t  kh_oid_map_t *replayed_commits,\n+\t\t\t\t\t  struct commit *onto,\n+\t\t\t\t\t  struct merge_options *merge_opt,\n+\t\t\t\t\t  struct merge_result *result)\n+{\n+\tstruct commit *base, *replayed_base;\n+\tstruct tree *pickme_tree, *base_tree;\n+\n+\tbase = pickme->parents->item;\n+\treplayed_base = mapped_commit(replayed_commits, base, onto);\n+\n+\tresult->tree = repo_get_commit_tree(repo, replayed_base);\n+\tpickme_tree = repo_get_commit_tree(repo, pickme);\n+\tbase_tree = repo_get_commit_tree(repo, base);\n+\n+\tmerge_opt->branch1 = short_commit_name(repo, replayed_base);\n+\tmerge_opt->branch2 = short_commit_name(repo, pickme);\n+\tmerge_opt->ancestor = xstrfmt(\"parent of %s\", merge_opt->branch2);\n+\n+\tmerge_incore_nonrecursive(merge_opt,\n+\t\t\t\t  base_tree,\n+\t\t\t\t  result->tree,\n+\t\t\t\t  pickme_tree,\n+\t\t\t\t  result);\n+\n+\tfree((char*)merge_opt->ancestor);\n+\tmerge_opt->ancestor = NULL;\n+\tif (!result->clean)\n+\t\treturn NULL;\n+\treturn replay_create_commit(repo, result->tree, pickme, replayed_base);\n+}\ndiff --git a/replay.h b/replay.h\nnew file mode 100644\nindex 0000000000..d6535ee56c\n--- /dev/null\n+++ b/replay.h\n@@ -0,0 +1,23 @@\n+#ifndef REPLAY_H\n+#define REPLAY_H\n+\n+#include \"khash.h\"\n+#include \"merge-ort.h\"\n+#include \"repository.h\"\n+\n+struct commit;\n+struct tree;\n+\n+struct commit *replay_create_commit(struct repository *repo,\n+\t\t\t\t    struct tree *tree,\n+\t\t\t\t    struct commit *based_on,\n+\t\t\t\t    struct commit *parent);\n+\n+struct commit *replay_pick_regular_commit(struct repository *repo,\n+\t\t\t\t\t  struct commit *pickme,\n+\t\t\t\t\t  kh_oid_map_t *replayed_commits,\n+\t\t\t\t\t  struct commit *onto,\n+\t\t\t\t\t  struct merge_options *merge_opt,\n+\t\t\t\t\t  struct merge_result *result);\n+\n+#endif\n\n-- \n2.52.0.239.gd5f0c6e74e.dirty\n\n"},{"id":"531607","messageId":"20251203-b4-pks-history-builtin-v7-3-9e9f849bfd0e@pks.im","threadId":"63986","inReplyTo":"20251203-b4-pks-history-builtin-v7-0-9e9f849bfd0e@pks.im","subject":"[PATCH v7 03/12] replay: stop using `the_repository`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-12-03T10:48:30Z","receivedAt":"2025-12-03T10:48:48Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"In `create_commit()` we're using `the_repository` even though we already\nhave a repository passed to use as an argument. Fix this.\n\nNote that we still cannot get rid of `USE_THE_REPOSITORY_VARIABLE`. This\nis because we use `DEFAULT_ABBREV and `get_commit_output_encoding()`,\nboth of which are stored as global variables that can be modified via\nthe Git configuration.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n replay.c | 2 +-\n 1 file changed, 1 insertion(+), 1 deletion(-)\n\ndiff --git a/replay.c b/replay.c\nindex fb906e9b51..13983dbc56 100644\n--- a/replay.c\n+++ b/replay.c\n@@ -62,7 +62,7 @@ struct commit *replay_create_commit(struct repository *repo,\n \tobj = parse_object(repo, &ret);\n \n out:\n-\trepo_unuse_commit_buffer(the_repository, based_on, message);\n+\trepo_unuse_commit_buffer(repo, based_on, message);\n \tfree_commit_extra_headers(extra);\n \tfree_commit_list(parents);\n \tstrbuf_release(&msg);\n\n-- \n2.52.0.239.gd5f0c6e74e.dirty\n\n"},{"id":"531608","messageId":"20251203-b4-pks-history-builtin-v7-4-9e9f849bfd0e@pks.im","threadId":"63986","inReplyTo":"20251203-b4-pks-history-builtin-v7-0-9e9f849bfd0e@pks.im","subject":"[PATCH v7 04/12] builtin: add new \"history\" command","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-12-03T10:48:31Z","receivedAt":"2025-12-03T10:48:51Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"When rewriting history via git-rebase(1) there are a few very common use\ncases:\n\n  - The ordering of two commits should be reversed.\n\n  - A commit should be split up into two commits.\n\n  - A commit should be dropped from the history completely.\n\n  - Multiple commits should be squashed into one.\n\n  - Editing an existing commit that is not the tip of the current\n    branch.\n\nWhile these operations are all doable, it often feels needlessly kludgey\nto do so by doing an interactive rebase, using the editor to say what\none wants, and then perform the actions. Furthermore, some operations\nlike splitting up a commit into two are way more involved than that and\nrequire a whole series of commands.\n\nAdd a new \"history\" command to plug this gap. This command will have\nseveral different subcommands to imperatively rewrite history for common\nuse cases like the above. Some of these subcommands will be implemented\nin subsequent commits.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n .gitignore                     |  1 +\n Documentation/git-history.adoc | 42 ++++++++++++++++++++++++++++++++++++++++++\n Documentation/meson.build      |  1 +\n Makefile                       |  1 +\n builtin.h                      |  1 +\n builtin/history.c              | 22 ++++++++++++++++++++++\n command-list.txt               |  1 +\n git.c                          |  1 +\n meson.build                    |  1 +\n t/meson.build                  |  1 +\n t/t3450-history.sh             | 17 +++++++++++++++++\n 11 files changed, 89 insertions(+)\n\ndiff --git a/.gitignore b/.gitignore\nindex 78a45cb5be..24635cf2d6 100644\n--- a/.gitignore\n+++ b/.gitignore\n@@ -79,6 +79,7 @@\n /git-grep\n /git-hash-object\n /git-help\n+/git-history\n /git-hook\n /git-http-backend\n /git-http-fetch\ndiff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc\nnew file mode 100644\nindex 0000000000..67b8ce2040\n--- /dev/null\n+++ b/Documentation/git-history.adoc\n@@ -0,0 +1,42 @@\n+git-history(1)\n+==============\n+\n+NAME\n+----\n+git-history - EXPERIMENTAL: Rewrite history of the current branch\n+\n+SYNOPSIS\n+--------\n+[synopsis]\n+git history [<options>]\n+\n+DESCRIPTION\n+-----------\n+\n+Rewrite history by rearranging or modifying specific commits in the\n+history.\n+\n+THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.\n+\n+This command is related to linkgit:git-rebase[1] in that both commands can be\n+used to rewrite history. You should use rebases if you want to reapply a range\n+of commits onto a different base, or interactive rebases if you want to edit a\n+range of commits.\n+\n+Note that this command does not (yet) work with histories that contain\n+merges. You should use linkgit:git-rebase[1] with the `--rebase-merges`\n+flag instead.\n+\n+COMMANDS\n+--------\n+\n+Several commands are available to rewrite history in different ways:\n+\n+CONFIGURATION\n+-------------\n+\n+include::includes/cmd-config-section-all.adoc[]\n+\n+GIT\n+---\n+Part of the linkgit:git[1] suite\ndiff --git a/Documentation/meson.build b/Documentation/meson.build\nindex f02dbc20cb..fd2e8cc02d 100644\n--- a/Documentation/meson.build\n+++ b/Documentation/meson.build\n@@ -64,6 +64,7 @@ manpages = {\n   'git-gui.adoc' : 1,\n   'git-hash-object.adoc' : 1,\n   'git-help.adoc' : 1,\n+  'git-history.adoc' : 1,\n   'git-hook.adoc' : 1,\n   'git-http-backend.adoc' : 1,\n   'git-http-fetch.adoc' : 1,\ndiff --git a/Makefile b/Makefile\nindex 809b094595..458841178b 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -1408,6 +1408,7 @@ BUILTIN_OBJS += builtin/get-tar-commit-id.o\n BUILTIN_OBJS += builtin/grep.o\n BUILTIN_OBJS += builtin/hash-object.o\n BUILTIN_OBJS += builtin/help.o\n+BUILTIN_OBJS += builtin/history.o\n BUILTIN_OBJS += builtin/hook.o\n BUILTIN_OBJS += builtin/index-pack.o\n BUILTIN_OBJS += builtin/init-db.o\ndiff --git a/builtin.h b/builtin.h\nindex 1b35565fbd..93c91d07d4 100644\n--- a/builtin.h\n+++ b/builtin.h\n@@ -172,6 +172,7 @@ int cmd_get_tar_commit_id(int argc, const char **argv, const char *prefix, struc\n int cmd_grep(int argc, const char **argv, const char *prefix, struct repository *repo);\n int cmd_hash_object(int argc, const char **argv, const char *prefix, struct repository *repo);\n int cmd_help(int argc, const char **argv, const char *prefix, struct repository *repo);\n+int cmd_history(int argc, const char **argv, const char *prefix, struct repository *repo);\n int cmd_hook(int argc, const char **argv, const char *prefix, struct repository *repo);\n int cmd_index_pack(int argc, const char **argv, const char *prefix, struct repository *repo);\n int cmd_init_db(int argc, const char **argv, const char *prefix, struct repository *repo);\ndiff --git a/builtin/history.c b/builtin/history.c\nnew file mode 100644\nindex 0000000000..f6fe32610b\n--- /dev/null\n+++ b/builtin/history.c\n@@ -0,0 +1,22 @@\n+#include \"builtin.h\"\n+#include \"gettext.h\"\n+#include \"parse-options.h\"\n+\n+int cmd_history(int argc,\n+\t\tconst char **argv,\n+\t\tconst char *prefix,\n+\t\tstruct repository *repo UNUSED)\n+{\n+\tconst char * const usage[] = {\n+\t\tN_(\"git history [<options>]\"),\n+\t\tNULL,\n+\t};\n+\tstruct option options[] = {\n+\t\tOPT_END(),\n+\t};\n+\n+\targc = parse_options(argc, argv, prefix, options, usage, 0);\n+\tif (argc)\n+\t\tusagef(\"unrecognized argument: %s\", argv[0]);\n+\treturn 0;\n+}\ndiff --git a/command-list.txt b/command-list.txt\nindex accd3d0c4b..f9005cf459 100644\n--- a/command-list.txt\n+++ b/command-list.txt\n@@ -115,6 +115,7 @@ git-grep                                mainporcelain           info\n git-gui                                 mainporcelain\n git-hash-object                         plumbingmanipulators\n git-help                                ancillaryinterrogators          complete\n+git-history                             mainporcelain           history\n git-hook                                purehelpers\n git-http-backend                        synchingrepositories\n git-http-fetch                          synchelpers\ndiff --git a/git.c b/git.c\nindex c5fad56813..744cb6527e 100644\n--- a/git.c\n+++ b/git.c\n@@ -586,6 +586,7 @@ static struct cmd_struct commands[] = {\n \t{ \"grep\", cmd_grep, RUN_SETUP_GENTLY },\n \t{ \"hash-object\", cmd_hash_object },\n \t{ \"help\", cmd_help },\n+\t{ \"history\", cmd_history, RUN_SETUP },\n \t{ \"hook\", cmd_hook, RUN_SETUP },\n \t{ \"index-pack\", cmd_index_pack, RUN_SETUP_GENTLY | NO_PARSEOPT },\n \t{ \"init\", cmd_init_db },\ndiff --git a/meson.build b/meson.build\nindex 1878dff2b1..f07a308fb8 100644\n--- a/meson.build\n+++ b/meson.build\n@@ -610,6 +610,7 @@ builtin_sources = [\n   'builtin/grep.c',\n   'builtin/hash-object.c',\n   'builtin/help.c',\n+  'builtin/history.c',\n   'builtin/hook.c',\n   'builtin/index-pack.c',\n   'builtin/init-db.c',\ndiff --git a/t/meson.build b/t/meson.build\nindex 7c994d4643..62f5dca098 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -386,6 +386,7 @@ integration_tests = [\n   't3436-rebase-more-options.sh',\n   't3437-rebase-fixup-options.sh',\n   't3438-rebase-broken-files.sh',\n+  't3450-history.sh',\n   't3500-cherry.sh',\n   't3501-revert-cherry-pick.sh',\n   't3502-cherry-pick-merge.sh',\ndiff --git a/t/t3450-history.sh b/t/t3450-history.sh\nnew file mode 100755\nindex 0000000000..417c343d43\n--- /dev/null\n+++ b/t/t3450-history.sh\n@@ -0,0 +1,17 @@\n+#!/bin/sh\n+\n+test_description='tests for git-history command'\n+\n+. ./test-lib.sh\n+\n+test_expect_success 'does nothing without any arguments' '\n+\tgit history >out 2>&1 &&\n+\ttest_must_be_empty out\n+'\n+\n+test_expect_success 'raises an error with unknown argument' '\n+\ttest_must_fail git history garbage 2>err &&\n+\ttest_grep \"unrecognized argument: garbage\" err\n+'\n+\n+test_done\n\n-- \n2.52.0.239.gd5f0c6e74e.dirty\n\n"},{"id":"531609","messageId":"20251203-b4-pks-history-builtin-v7-5-9e9f849bfd0e@pks.im","threadId":"63986","inReplyTo":"20251203-b4-pks-history-builtin-v7-0-9e9f849bfd0e@pks.im","subject":"[PATCH v7 05/12] builtin/history: implement \"reword\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-12-03T10:48:32Z","receivedAt":"2025-12-03T10:48:53Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Implement a new \"reword\" subcommand for git-history(1). This subcommand\nis similar to the user performing an interactive rebase with a single\ncommit changed to use the \"reword\" instruction.\n\nThe major difference is that we do not check out the commit that is to\nbe reworded. This has the obvious benefit of being significantly faster\ncompared to git-rebase(1), but even more importantly it allows the user\nto rewrite history even if there are local changes in the working tree\nor in the index.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/git-history.adoc |   7 +-\n builtin/history.c              | 372 ++++++++++++++++++++++++++++++++++++++++-\n t/meson.build                  |   1 +\n t/t3450-history.sh             |   6 +-\n t/t3451-history-reword.sh      | 236 ++++++++++++++++++++++++++\n 5 files changed, 613 insertions(+), 9 deletions(-)\n\ndiff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc\nindex 67b8ce2040..160bf5d4d2 100644\n--- a/Documentation/git-history.adoc\n+++ b/Documentation/git-history.adoc\n@@ -8,7 +8,7 @@ git-history - EXPERIMENTAL: Rewrite history of the current branch\n SYNOPSIS\n --------\n [synopsis]\n-git history [<options>]\n+git history reword <commit>\n \n DESCRIPTION\n -----------\n@@ -32,6 +32,11 @@ COMMANDS\n \n Several commands are available to rewrite history in different ways:\n \n+`reword <commit>`::\n+\tRewrite the commit message of the specified commit. All the other\n+\tdetails of this commit remain unchanged. This command will spawn an\n+\teditor with the current message of that commit.\n+\n CONFIGURATION\n -------------\n \ndiff --git a/builtin/history.c b/builtin/history.c\nindex f6fe32610b..17bb150b95 100644\n--- a/builtin/history.c\n+++ b/builtin/history.c\n@@ -1,22 +1,384 @@\n+#define USE_THE_REPOSITORY_VARIABLE\n+\n #include \"builtin.h\"\n+#include \"commit-reach.h\"\n+#include \"commit.h\"\n+#include \"config.h\"\n+#include \"editor.h\"\n+#include \"environment.h\"\n #include \"gettext.h\"\n+#include \"hex.h\"\n #include \"parse-options.h\"\n+#include \"refs.h\"\n+#include \"replay.h\"\n+#include \"reset.h\"\n+#include \"revision.h\"\n+#include \"sequencer.h\"\n+#include \"strvec.h\"\n+#include \"tree.h\"\n+#include \"wt-status.h\"\n+\n+#define GIT_HISTORY_REWORD_USAGE N_(\"git history reword <commit>\")\n+\n+static int collect_commits(struct repository *repo,\n+\t\t\t   struct commit *old_commit,\n+\t\t\t   struct commit *new_commit,\n+\t\t\t   struct strvec *out)\n+{\n+\tstruct setup_revision_opt revision_opts = {\n+\t\t.assume_dashdash = 1,\n+\t};\n+\tstruct strvec revisions = STRVEC_INIT;\n+\tstruct commit *child;\n+\tstruct rev_info rev = { 0 };\n+\tint ret;\n+\n+\trepo_init_revisions(repo, &rev, NULL);\n+\trev.reverse = 1;\n+\tstrvec_push(&revisions, \"\");\n+\tstrvec_push(&revisions, oid_to_hex(&new_commit->object.oid));\n+\tif (old_commit) {\n+\t\tstrvec_pushf(&revisions, \"^%s\", oid_to_hex(&old_commit->object.oid));\n+\t\tstrvec_pushf(&revisions, \"--ancestry-path=%s\", oid_to_hex(&old_commit->object.oid));\n+\t}\n+\n+\tsetup_revisions_from_strvec(&revisions, &rev, &revision_opts);\n+\tif (revisions.nr != 1 || prepare_revision_walk(&rev)) {\n+\t\tret = error(_(\"revision walk setup failed\"));\n+\t\tgoto out;\n+\t}\n+\n+\twhile ((child = get_revision(&rev))) {\n+\t\tif (old_commit && !child->parents)\n+\t\t\tBUG(\"revision walk did not find child commit\");\n+\t\tif (child->parents && child->parents->next) {\n+\t\t\tret = error(_(\"cannot rearrange commit history with merges\"));\n+\t\t\tgoto out;\n+\t\t}\n+\n+\t\tstrvec_push(out, oid_to_hex(&child->object.oid));\n+\t}\n+\n+\tret = 0;\n+\n+out:\n+\tstrvec_clear(&revisions);\n+\trelease_revisions(&rev);\n+\treset_revision_walk();\n+\treturn ret;\n+}\n+\n+static int gather_commits_between_head_and_revision(struct repository *repo,\n+\t\t\t\t\t\t    const char *revision,\n+\t\t\t\t\t\t    struct commit **original_commit,\n+\t\t\t\t\t\t    struct commit **parent_commit,\n+\t\t\t\t\t\t    struct commit **head,\n+\t\t\t\t\t\t    struct strvec *commits)\n+{\n+\tstruct commit_list *from_list = NULL;\n+\tint ret;\n+\n+\t*original_commit = lookup_commit_reference_by_name(revision);\n+\tif (!*original_commit) {\n+\t\tret = error(_(\"commit cannot be found: %s\"), revision);\n+\t\tgoto out;\n+\t}\n+\n+\t*parent_commit = (*original_commit)->parents ? (*original_commit)->parents->item : NULL;\n+\tif (*parent_commit && repo_parse_commit(repo, *parent_commit)) {\n+\t\tret = error(_(\"unable to parse commit %s\"),\n+\t\t\t    oid_to_hex(&(*parent_commit)->object.oid));\n+\t\tgoto out;\n+\t}\n+\n+\t*head = lookup_commit_reference_by_name(\"HEAD\");\n+\tif (!(*head)) {\n+\t\tret = error(_(\"could not resolve HEAD to a commit\"));\n+\t\tgoto out;\n+\t}\n+\n+\tcommit_list_append(*original_commit, &from_list);\n+\tif (!repo_is_descendant_of(repo, *head, from_list)) {\n+\t\tret = error(_(\"commit must be reachable from current HEAD commit\"));\n+\t\tgoto out;\n+\t}\n+\n+\t/*\n+\t * Collect the list of commits that we'll have to reapply now already.\n+\t * This ensures that we'll abort early on in case the range of commits\n+\t * contains merges, which we do not yet handle.\n+\t */\n+\tret = collect_commits(repo, *parent_commit, *head, commits);\n+\tif (ret < 0)\n+\t\tgoto out;\n+\n+out:\n+\tfree_commit_list(from_list);\n+\treturn ret;\n+}\n+\n+static void replace_commits(struct strvec *commits,\n+\t\t\t    const struct object_id *commit_to_replace,\n+\t\t\t    const struct object_id *replacements,\n+\t\t\t    size_t replacements_nr)\n+{\n+\tchar commit_to_replace_oid[GIT_MAX_HEXSZ + 1];\n+\tstruct strvec replacement_oids = STRVEC_INIT;\n+\tbool found = false;\n+\n+\toid_to_hex_r(commit_to_replace_oid, commit_to_replace);\n+\tfor (size_t i = 0; i < replacements_nr; i++)\n+\t\tstrvec_push(&replacement_oids, oid_to_hex(&replacements[i]));\n+\n+\tfor (size_t i = 0; i < commits->nr; i++) {\n+\t\tif (strcmp(commits->v[i], commit_to_replace_oid))\n+\t\t\tcontinue;\n+\t\tstrvec_splice(commits, i, 1, replacement_oids.v, replacement_oids.nr);\n+\t\tfound = true;\n+\t\tbreak;\n+\t}\n+\tif (!found)\n+\t\tBUG(\"could not find commit to replace\");\n+\n+\tstrvec_clear(&replacement_oids);\n+}\n+\n+static int apply_commits(struct repository *repo,\n+\t\t\t const struct strvec *commits,\n+\t\t\t struct commit *onto,\n+\t\t\t struct commit *orig_head,\n+\t\t\t const char *action)\n+{\n+\tstruct reset_head_opts reset_opts = { 0 };\n+\tstruct strbuf buf = STRBUF_INIT;\n+\tint ret;\n+\n+\tfor (size_t i = 0; i < commits->nr; i++) {\n+\t\tstruct object_id commit_id;\n+\t\tstruct commit *commit;\n+\t\tconst char *end;\n+\n+\t\tif (parse_oid_hex_algop(commits->v[i], &commit_id, &end,\n+\t\t\t\t\trepo->hash_algo)) {\n+\t\t\tret = error(_(\"invalid object ID: %s\"), commits->v[i]);\n+\t\t\tgoto out;\n+\t\t}\n+\n+\t\tcommit = lookup_commit(repo, &commit_id);\n+\t\tif (!commit || repo_parse_commit(repo, commit)) {\n+\t\t\tret = error(_(\"failed to look up commit: %s\"), oid_to_hex(&commit_id));\n+\t\t\tgoto out;\n+\t\t}\n+\n+\t\tif (!onto) {\n+\t\t\tonto = commit;\n+\t\t} else {\n+\t\t\tstruct tree *tree = repo_get_commit_tree(repo, commit);\n+\t\t\tonto = replay_create_commit(repo, tree, commit, onto);\n+\t\t\tif (!onto) {\n+\t\t\t\tret = -1;\n+\t\t\t\tgoto out;\n+\t\t\t}\n+\t\t}\n+\t}\n+\n+\treset_opts.oid = &onto->object.oid;\n+\tstrbuf_addf(&buf, \"%s: switch to rewritten %s\", action, oid_to_hex(reset_opts.oid));\n+\treset_opts.flags = RESET_HEAD_REFS_ONLY | RESET_ORIG_HEAD;\n+\treset_opts.orig_head = &orig_head->object.oid;\n+\treset_opts.default_reflog_action = action;\n+\tif (reset_head(repo, &reset_opts) < 0) {\n+\t\tret = error(_(\"could not switch to %s\"), oid_to_hex(reset_opts.oid));\n+\t\tgoto out;\n+\t}\n+\n+\tret = 0;\n+\n+out:\n+\tstrbuf_release(&buf);\n+\treturn ret;\n+}\n+\n+static void change_data_free(void *util, const char *str UNUSED)\n+{\n+\tstruct wt_status_change_data *d = util;\n+\tfree(d->rename_source);\n+\tfree(d);\n+}\n+\n+static int fill_commit_message(struct repository *repo,\n+\t\t\t       const struct object_id *old_tree,\n+\t\t\t       const struct object_id *new_tree,\n+\t\t\t       const char *default_message,\n+\t\t\t       const char *action,\n+\t\t\t       struct strbuf *out)\n+{\n+\tconst char *path = git_path_commit_editmsg();\n+\tconst char *hint =\n+\t\t_(\"Please enter the commit message for the %s changes.\"\n+\t\t  \" Lines starting\\nwith '%s' will be ignored, and an\"\n+\t\t  \" empty message aborts the commit.\\n\");\n+\tstruct wt_status s;\n+\n+\tstrbuf_addstr(out, default_message);\n+\tstrbuf_addch(out, '\\n');\n+\tstrbuf_commented_addf(out, comment_line_str, hint, action, comment_line_str);\n+\twrite_file_buf(path, out->buf, out->len);\n+\n+\twt_status_prepare(repo, &s);\n+\tFREE_AND_NULL(s.branch);\n+\ts.ahead_behind_flags = AHEAD_BEHIND_QUICK;\n+\ts.commit_template = 1;\n+\ts.colopts = 0;\n+\ts.display_comment_prefix = 1;\n+\ts.hints = 0;\n+\ts.use_color = 0;\n+\ts.whence = FROM_COMMIT;\n+\ts.committable = 1;\n+\n+\ts.fp = fopen(git_path_commit_editmsg(), \"a\");\n+\tif (!s.fp)\n+\t\treturn error_errno(_(\"could not open '%s'\"), git_path_commit_editmsg());\n+\n+\twt_status_collect_changes_trees(&s, old_tree, new_tree);\n+\twt_status_print(&s);\n+\twt_status_collect_free_buffers(&s);\n+\tstring_list_clear_func(&s.change, change_data_free);\n+\n+\tstrbuf_reset(out);\n+\tif (launch_editor(path, out, NULL)) {\n+\t\tfprintf(stderr, _(\"Aborting commit as launching the editor failed.\\n\"));\n+\t\treturn -1;\n+\t}\n+\tstrbuf_stripspace(out, comment_line_str);\n+\n+\tcleanup_message(out, COMMIT_MSG_CLEANUP_ALL, 0);\n+\n+\tif (!out->len) {\n+\t\tfprintf(stderr, _(\"Aborting commit due to empty commit message.\\n\"));\n+\t\treturn -1;\n+\t}\n+\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_commit,\n+\t\t\t\t\t   const struct object_id *new_tree_oid,\n+\t\t\t\t\t   const struct commit_list *parents,\n+\t\t\t\t\t   const struct object_id *parent_tree_oid,\n+\t\t\t\t\t   struct object_id *out)\n+{\n+\tconst char *exclude_gpgsig[] = { \"gpgsig\", \"gpgsig-sha256\", NULL };\n+\tconst char *original_message, *original_body, *ptr;\n+\tstruct commit_extra_header *original_extra_headers = NULL;\n+\tstruct strbuf commit_message = STRBUF_INIT;\n+\tchar *original_author = NULL;\n+\tsize_t len;\n+\tint ret;\n+\n+\t/* We retain authorship of the original commit. */\n+\toriginal_message = repo_logmsg_reencode(repo, original_commit, 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, new_tree_oid,\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_commit, exclude_gpgsig);\n+\n+\tret = commit_tree_extended(commit_message.buf, commit_message.len, new_tree_oid,\n+\t\t\t\t   parents, out, original_author, NULL, NULL,\n+\t\t\t\t   original_extra_headers);\n+\tif (ret < 0)\n+\t\tgoto out;\n+\n+out:\n+\tfree_commit_extra_headers(original_extra_headers);\n+\tstrbuf_release(&commit_message);\n+\tfree(original_author);\n+\treturn ret;\n+}\n+\n+static int cmd_history_reword(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_REWORD_USAGE,\n+\t\tNULL,\n+\t};\n+\tstruct option options[] = {\n+\t\tOPT_END(),\n+\t};\n+\tstruct commit *original_commit, *parent, *head;\n+\tstruct strvec commits = STRVEC_INIT;\n+\tstruct object_id parent_tree_oid, original_commit_tree_oid;\n+\tstruct object_id rewritten_commit;\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 single revision\"));\n+\t\tgoto out;\n+\t}\n+\trepo_config(repo, git_default_config, NULL);\n+\n+\tret = gather_commits_between_head_and_revision(repo, argv[0], &original_commit,\n+\t\t\t\t\t\t       &parent, &head, &commits);\n+\tif (ret < 0)\n+\t\tgoto out;\n+\n+\toriginal_commit_tree_oid = repo_get_commit_tree(repo, original_commit)->object.oid;\n+\tif (parent)\n+\t\tparent_tree_oid = repo_get_commit_tree(repo, parent)->object.oid;\n+\telse\n+\t\toidcpy(&parent_tree_oid, repo->hash_algo->empty_tree);\n+\n+\t/* We retain authorship of the original commit. */\n+\tret = commit_tree_with_edited_message(repo, \"reworded\", original_commit,\n+\t\t\t\t\t      &original_commit_tree_oid,\n+\t\t\t\t\t      original_commit->parents, &parent_tree_oid,\n+\t\t\t\t\t      &rewritten_commit);\n+\tif (ret < 0) {\n+\t\tret = error(_(\"failed writing reworded commit\"));\n+\t\tgoto out;\n+\t}\n+\n+\treplace_commits(&commits, &original_commit->object.oid, &rewritten_commit, 1);\n+\n+\tret = apply_commits(repo, &commits, parent, head, \"reword\");\n+\tif (ret < 0)\n+\t\tgoto out;\n+\n+\tret = 0;\n+\n+out:\n+\tstrvec_clear(&commits);\n+\treturn ret;\n+}\n \n int cmd_history(int argc,\n \t\tconst char **argv,\n \t\tconst char *prefix,\n-\t\tstruct repository *repo UNUSED)\n+\t\tstruct repository *repo)\n {\n \tconst char * const usage[] = {\n-\t\tN_(\"git history [<options>]\"),\n+\t\tGIT_HISTORY_REWORD_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_END(),\n \t};\n \n \targc = parse_options(argc, argv, prefix, options, usage, 0);\n-\tif (argc)\n-\t\tusagef(\"unrecognized argument: %s\", argv[0]);\n-\treturn 0;\n+\treturn fn(argc, argv, prefix, repo);\n }\ndiff --git a/t/meson.build b/t/meson.build\nindex 62f5dca098..e187aef31e 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -387,6 +387,7 @@ integration_tests = [\n   't3437-rebase-fixup-options.sh',\n   't3438-rebase-broken-files.sh',\n   't3450-history.sh',\n+  't3451-history-reword.sh',\n   't3500-cherry.sh',\n   't3501-revert-cherry-pick.sh',\n   't3502-cherry-pick-merge.sh',\ndiff --git a/t/t3450-history.sh b/t/t3450-history.sh\nindex 417c343d43..f513463b92 100755\n--- a/t/t3450-history.sh\n+++ b/t/t3450-history.sh\n@@ -5,13 +5,13 @@ test_description='tests for git-history command'\n . ./test-lib.sh\n \n test_expect_success 'does nothing without any arguments' '\n-\tgit history >out 2>&1 &&\n-\ttest_must_be_empty out\n+\ttest_must_fail git history 2>err &&\n+\ttest_grep \"need a subcommand\" err\n '\n \n test_expect_success 'raises an error with unknown argument' '\n \ttest_must_fail git history garbage 2>err &&\n-\ttest_grep \"unrecognized argument: garbage\" err\n+\ttest_grep \"unknown subcommand: .garbage.\" err\n '\n \n test_done\ndiff --git a/t/t3451-history-reword.sh b/t/t3451-history-reword.sh\nnew file mode 100755\nindex 0000000000..2a638d2378\n--- /dev/null\n+++ b/t/t3451-history-reword.sh\n@@ -0,0 +1,236 @@\n+#!/bin/sh\n+\n+test_description='tests for git-history reword subcommand'\n+\n+. ./test-lib.sh\n+\n+reword_with_message () {\n+\tcat >message &&\n+\twrite_script fake-editor.sh <<-\\EOF &&\n+\tcp message \"$1\"\n+\tEOF\n+\ttest_set_editor \"$(pwd)\"/fake-editor.sh &&\n+\tgit history reword \"$@\" &&\n+\trm fake-editor.sh message\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 reword HEAD~ 2>err &&\n+\t\ttest_grep \"cannot rearrange commit history with merges\" err &&\n+\t\ttest_must_fail git history reword HEAD 2>err &&\n+\t\ttest_grep \"cannot rearrange commit history with merges\" err\n+\t)\n+'\n+\n+test_expect_success 'refuses to work with unrelated 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\ttest_must_fail git history reword ours 2>err &&\n+\t\ttest_grep \"commit must be reachable from current HEAD commit\" err\n+\t)\n+'\n+\n+test_expect_success 'can reword tip of a branch' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit first &&\n+\t\ttest_commit second &&\n+\t\ttest_commit third &&\n+\n+\t\tgit symbolic-ref HEAD >expect &&\n+\t\treword_with_message HEAD <<-EOF &&\n+\t\tthird reworded\n+\t\tEOF\n+\t\tgit symbolic-ref HEAD >actual &&\n+\t\ttest_cmp expect actual &&\n+\n+\t\tcat >expect <<-EOF &&\n+\t\tthird reworded\n+\t\tsecond\n+\t\tfirst\n+\t\tEOF\n+\t\tgit log --format=%s >actual &&\n+\t\ttest_cmp expect actual &&\n+\n+\t\tgit reflog >reflog &&\n+\t\ttest_grep \"reword: updating HEAD\" reflog\n+\t)\n+'\n+\n+test_expect_success 'can reword commit in the middle' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit first &&\n+\t\ttest_commit second &&\n+\t\ttest_commit third &&\n+\n+\t\tgit symbolic-ref HEAD >expect &&\n+\t\treword_with_message HEAD~ <<-EOF &&\n+\t\tsecond reworded\n+\t\tEOF\n+\t\tgit symbolic-ref HEAD >actual &&\n+\t\ttest_cmp expect actual &&\n+\n+\t\tcat >expect <<-EOF &&\n+\t\tthird\n+\t\tsecond reworded\n+\t\tfirst\n+\t\tEOF\n+\t\tgit log --format=%s >actual &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+test_expect_success 'can reword root commit' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit first &&\n+\t\ttest_commit second &&\n+\t\ttest_commit third &&\n+\t\treword_with_message HEAD~2 <<-EOF &&\n+\t\tfirst reworded\n+\t\tEOF\n+\n+\t\tcat >expect <<-EOF &&\n+\t\tthird\n+\t\tsecond\n+\t\tfirst reworded\n+\t\tEOF\n+\t\tgit log --format=%s >actual &&\n+\t\ttest_cmp expect actual\n+\t)\n+'\n+\n+test_expect_success 'editor shows proper status' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit first &&\n+\n+\t\twrite_script fake-editor.sh <<-\\EOF &&\n+\t\tcp \"$1\" . &&\n+\t\tprintf \"\\namend a comment\\n\" >>\"$1\"\n+\t\tEOF\n+\t\ttest_set_editor \"$(pwd)\"/fake-editor.sh &&\n+\t\tgit history reword HEAD &&\n+\n+\t\tcat >expect <<-EOF &&\n+\t\tfirst\n+\n+\t\t# Please enter the commit message for the reworded 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:   first.t\n+\t\t#\n+\t\tEOF\n+\t\ttest_cmp expect COMMIT_EDITMSG &&\n+\n+\t\ttest_commit_message HEAD <<-\\EOF\n+\t\tfirst\n+\n+\t\tamend a comment\n+\t\tEOF\n+\t)\n+'\n+\n+# For now, git-history(1) does not yet execute any hooks. This is subject to\n+# change in the future, and if it does this test here is expected to start\n+# failing. In other words, this test is not an endorsement of the current\n+# status quo.\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\ttest_commit first &&\n+\t\ttest_commit second &&\n+\t\ttest_commit third &&\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\treword_with_message HEAD~ <<-EOF &&\n+\t\tsecond reworded\n+\t\tEOF\n+\n+\t\tcat >expect <<-EOF &&\n+\t\tthird\n+\t\tsecond reworded\n+\t\tfirst\n+\t\tEOF\n+\t\tgit log --format=%s >actual &&\n+\t\ttest_cmp expect actual &&\n+\n+\t\ttest_path_is_missing hooks.log\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\ttest_commit first &&\n+\n+\t\t! reword_with_message HEAD 2>err </dev/null &&\n+\t\ttest_grep \"Aborting commit due to empty commit message.\" 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\ttouch a b &&\n+\t\tgit add . &&\n+\t\tgit commit -m \"initial commit\" &&\n+\t\techo foo >a &&\n+\t\techo bar >b &&\n+\t\tgit add b &&\n+\t\treword_with_message HEAD <<-EOF &&\n+\t\tmessage\n+\t\tEOF\n+\t\tcat >expect <<-\\EOF &&\n+\t\t M a\n+\t\tM  b\n+\t\t?? actual\n+\t\t?? expect\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.52.0.239.gd5f0c6e74e.dirty\n\n"},{"id":"531610","messageId":"20251203-b4-pks-history-builtin-v7-6-9e9f849bfd0e@pks.im","threadId":"63986","inReplyTo":"20251203-b4-pks-history-builtin-v7-0-9e9f849bfd0e@pks.im","subject":"[PATCH v7 06/12] add-patch: split out header from \"add-interactive.h\"","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-12-03T10:48:33Z","receivedAt":"2025-12-03T10:48:57Z","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 | 23 +++--------------------\n add-patch.c       |  1 +\n add-patch.h       | 26 ++++++++++++++++++++++++++\n 3 files changed, 30 insertions(+), 20 deletions(-)\n\ndiff --git a/add-interactive.h b/add-interactive.h\nindex da49502b76..2e3d1d871d 100644\n--- a/add-interactive.h\n+++ b/add-interactive.h\n@@ -1,14 +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-};\n-\n-#define ADD_P_OPT_INIT { .context = -1, .interhunkcontext = -1 }\n+struct pathspec;\n+struct repository;\n \n struct add_i_state {\n \tstruct repository *r;\n@@ -35,21 +32,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 173a53241e..5e3481083b 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..4394c74107\n--- /dev/null\n+++ b/add-patch.h\n@@ -0,0 +1,26 @@\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+};\n+\n+#define ADD_P_OPT_INIT { .context = -1, .interhunkcontext = -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.52.0.239.gd5f0c6e74e.dirty\n\n"},{"id":"531611","messageId":"20251203-b4-pks-history-builtin-v7-7-9e9f849bfd0e@pks.im","threadId":"63986","inReplyTo":"20251203-b4-pks-history-builtin-v7-0-9e9f849bfd0e@pks.im","subject":"[PATCH v7 07/12] add-patch: split out `struct interactive_options`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-12-03T10:48:34Z","receivedAt":"2025-12-03T10:49:01Z","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  | 174 +++++++++++------------------------------------------\n add-interactive.h  |  23 +------\n add-patch.c        | 170 +++++++++++++++++++++++++++++++++++++++++++--------\n add-patch.h        |  36 ++++++++++-\n builtin/add.c      |  22 +++----\n builtin/checkout.c |   4 +-\n builtin/commit.c   |  16 ++---\n builtin/reset.c    |  16 ++---\n builtin/stash.c    |  46 +++++++-------\n commit.h           |   2 +-\n 10 files changed, 270 insertions(+), 239 deletions(-)\n\ndiff --git a/add-interactive.c b/add-interactive.c\nindex 68fc09547d..05d2e7eefe 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,119 +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-\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@@ -286,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@@ -354,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@@ -432,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@@ -992,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@@ -1014,9 +912,9 @@ 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\tstruct interactive_options opts = {\n+\t\t\t.context = s->cfg.context,\n+\t\t\t.interhunkcontext = s->cfg.interhunkcontext,\n \t\t};\n \t\tstruct strvec args = STRVEC_INIT;\n \t\tstruct pathspec ps_selected = { 0 };\n@@ -1028,7 +926,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@@ -1064,10 +962,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@@ -1085,17 +983,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@@ -1103,21 +1001,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@@ -1152,7 +1050,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@@ -1163,7 +1061,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@@ -1206,15 +1104,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 2e3d1d871d..eefa2edc7c 100644\n--- a/add-interactive.h\n+++ b/add-interactive.h\n@@ -2,37 +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+\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 5e3481083b..6797d2f690 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,122 @@ 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+\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 +417,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 +542,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 +576,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 +811,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 +833,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 +1222,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 +1356,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 +1370,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@@ -1560,15 +1678,15 @@ static int patch_update_file(struct add_p_state *s,\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 \t\t\t\t\t\t: 1));\n \t\tprintf(_(s->mode->prompt_mode[prompt_mode_type]),\n \t\t       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\tquit = 1;\n@@ -1731,7 +1849,7 @@ static int patch_update_file(struct add_p_state *s,\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@@ -1749,7 +1867,7 @@ static int patch_update_file(struct add_p_state *s,\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@@ -1767,7 +1885,7 @@ static int patch_update_file(struct add_p_state *s,\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@@ -1808,7 +1926,7 @@ static int patch_update_file(struct add_p_state *s,\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@@ -1816,7 +1934,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;\ndiff --git a/add-patch.h b/add-patch.h\nindex 4394c74107..a4a05d9d14 100644\n--- a/add-patch.h\n+++ b/add-patch.h\n@@ -1,15 +1,45 @@\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 };\n \n-#define ADD_P_OPT_INIT { .context = -1, .interhunkcontext = -1 }\n+#define INTERACTIVE_OPTIONS_INIT { \\\n+\t.context = -1, \\\n+\t.interhunkcontext = -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+};\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@@ -20,7 +50,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 32709794b3..6f1e213052 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,8 +256,8 @@ 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_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@@ -400,9 +400,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@@ -412,11 +412,11 @@ 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}\n \ndiff --git a/builtin/checkout.c b/builtin/checkout.c\nindex 66b69df6e6..530ae956ad 100644\n--- a/builtin/checkout.c\n+++ b/builtin/checkout.c\n@@ -546,7 +546,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};\n@@ -575,7 +575,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 0243f17d53..640495cc57 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@@ -356,9 +356,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@@ -407,7 +407,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@@ -432,9 +432,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@@ -1742,8 +1742,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 ed35802af1..088449e120 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,8 +371,8 @@ 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_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@@ -423,9 +423,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@@ -436,12 +436,12 @@ 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}\n \ndiff --git a/builtin/stash.c b/builtin/stash.c\nindex 948eba06fb..3b50905233 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,8 +1851,8 @@ 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_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@@ -1907,19 +1909,19 @@ 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}\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@@ -1944,7 +1946,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@@ -1952,8 +1954,8 @@ 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_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@@ -1973,20 +1975,20 @@ 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}\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 1d6e0c7518..7b6e59d6c1 100644\n--- a/commit.h\n+++ b/commit.h\n@@ -258,7 +258,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.52.0.239.gd5f0c6e74e.dirty\n\n"},{"id":"531612","messageId":"20251203-b4-pks-history-builtin-v7-8-9e9f849bfd0e@pks.im","threadId":"63986","inReplyTo":"20251203-b4-pks-history-builtin-v7-0-9e9f849bfd0e@pks.im","subject":"[PATCH v7 08/12] add-patch: remove dependency on \"add-interactive\" subsystem","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-12-03T10:48:35Z","receivedAt":"2025-12-03T10:49:03Z","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 | 70 ++++++++++++++++++++++++++++++++-----------------------------\n 1 file changed, 37 insertions(+), 33 deletions(-)\n\ndiff --git a/add-patch.c b/add-patch.c\nindex 6797d2f690..2780738153 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@@ -408,7 +408,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@@ -417,9 +417,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@@ -437,7 +437,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@@ -542,12 +542,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@@ -576,9 +576,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@@ -811,7 +811,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@@ -833,7 +833,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@@ -1222,12 +1222,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@@ -1356,7 +1356,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@@ -1370,7 +1370,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@@ -1678,15 +1678,15 @@ static int patch_update_file(struct add_p_state *s,\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 \t\t\t\t\t\t: 1));\n \t\tprintf(_(s->mode->prompt_mode[prompt_mode_type]),\n \t\t       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\tquit = 1;\n@@ -1849,7 +1849,7 @@ static int patch_update_file(struct add_p_state *s,\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@@ -1867,7 +1867,7 @@ static int patch_update_file(struct add_p_state *s,\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@@ -1885,7 +1885,7 @@ static int patch_update_file(struct add_p_state *s,\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@@ -1905,7 +1905,7 @@ static int patch_update_file(struct add_p_state *s,\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\t   s->mode->is_reverse);\n@@ -1916,8 +1916,8 @@ static int patch_update_file(struct add_p_state *s,\n \t\t\t\t\t NULL, 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\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@@ -1930,11 +1930,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\n-- \n2.52.0.239.gd5f0c6e74e.dirty\n\n"},{"id":"531613","messageId":"20251203-b4-pks-history-builtin-v7-9-9e9f849bfd0e@pks.im","threadId":"63986","inReplyTo":"20251203-b4-pks-history-builtin-v7-0-9e9f849bfd0e@pks.im","subject":"[PATCH v7 09/12] add-patch: add support for in-memory index patching","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-12-03T10:48:36Z","receivedAt":"2025-12-03T10:49:06Z","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 | 139 +++++++++++++++++++++++++++++++++++++++++++++++++++---------\n add-patch.h |   8 ++++\n 2 files changed, 128 insertions(+), 19 deletions(-)\n\ndiff --git a/add-patch.c b/add-patch.c\nindex 2780738153..31d82a3e22 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@@ -47,7 +49,7 @@ static struct patch_mode patch_mode_add = {\n \t\tN_(\"Stage mode change [y,n,q,a,d%s,?]? \"),\n \t\tN_(\"Stage deletion [y,n,q,a,d%s,?]? \"),\n \t\tN_(\"Stage addition [y,n,q,a,d%s,?]? \"),\n-\t\tN_(\"Stage this hunk [y,n,q,a,d%s,?]? \")\n+\t\tN_(\"Stage this hunk [y,n,q,a,d%s,?]? \"),\n \t},\n \t.edit_hunk_hint = N_(\"If the patch applies cleanly, the edited hunk \"\n \t\t\t     \"will immediately be marked for staging.\"),\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@@ -437,7 +441,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@@ -1905,7 +1909,7 @@ static int patch_update_file(struct add_p_state *s,\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\t   s->mode->is_reverse);\n@@ -1916,27 +1920,54 @@ static int patch_update_file(struct add_p_state *s,\n \t\t\t\t\t NULL, 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 \tputchar('\\n');\n \treturn quit;\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+\n+\tif (parse_diff(state, ps) < 0)\n+\t\treturn -1;\n+\n+\tfor (size_t i = 0; i < state->file_diff_nr; i++) {\n+\t\tif (state->file_diff[i].binary && !state->file_diff[i].hunk_nr)\n+\t\t\tbinary_count++;\n+\t\telse if (patch_update_file(state, state->file_diff + i))\n+\t\t\tbreak;\n+\t}\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@@ -1969,23 +2000,93 @@ 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; i++)\n-\t\tif (s.file_diff[i].binary && !s.file_diff[i].hunk_nr)\n-\t\t\tbinary_count++;\n-\t\telse if (patch_update_file(&s, s.file_diff + i))\n-\t\t\tbreak;\n+\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+\tstruct strbuf parent_revision = STRBUF_INIT;\n+\tchar parent_tree_oid[GIT_MAX_HEXSZ + 1];\n+\tstruct commit *commit;\n+\tint ret;\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+\tstrbuf_addf(&parent_revision, \"%s~\", revision);\n+\tmode.diff_cmd[0] = \"diff-tree\";\n+\tmode.diff_cmd[1] = \"-r\";\n+\tmode.diff_cmd[2] = parent_tree_oid;\n+\n+\tinteractive_config_init(&s.cfg, r, opts);\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+\tstrbuf_release(&parent_revision);\n+\tadd_p_state_clear(&s);\n+\treturn ret;\n }\ndiff --git a/add-patch.h b/add-patch.h\nindex a4a05d9d14..901c42fd7b 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@@ -53,4 +54,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.52.0.239.gd5f0c6e74e.dirty\n\n"},{"id":"531614","messageId":"20251203-b4-pks-history-builtin-v7-10-9e9f849bfd0e@pks.im","threadId":"63986","inReplyTo":"20251203-b4-pks-history-builtin-v7-0-9e9f849bfd0e@pks.im","subject":"[PATCH v7 10/12] add-patch: allow disabling editing of hunks","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-12-03T10:48:37Z","receivedAt":"2025-12-03T10:49:09Z","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        | 21 +++++++++++++--------\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, 27 insertions(+), 15 deletions(-)\n\ndiff --git a/add-interactive.c b/add-interactive.c\nindex 05d2e7eefe..d41e039bc3 100644\n--- a/add-interactive.c\n+++ b/add-interactive.c\n@@ -926,7 +926,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 31d82a3e22..51a7b371a2 100644\n--- a/add-patch.c\n+++ b/add-patch.c\n@@ -1565,7 +1565,8 @@ static bool get_first_undecided(const struct file_diff *file_diff, size_t *idx)\n }\n \n static int patch_update_file(struct add_p_state *s,\n-\t\t\t     struct file_diff *file_diff)\n+\t\t\t     struct file_diff *file_diff,\n+\t\t\t     unsigned flags)\n {\n \tsize_t hunk_index = 0;\n \tssize_t i, undecided_previous, undecided_next, rendered_hunk_index = -1;\n@@ -1666,7 +1667,8 @@ static int patch_update_file(struct add_p_state *s,\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@@ -1932,7 +1934,8 @@ static int patch_update_file(struct add_p_state *s,\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 \n@@ -1942,7 +1945,7 @@ static int run_add_p_common(struct add_p_state *state,\n \tfor (size_t i = 0; i < state->file_diff_nr; i++) {\n \t\tif (state->file_diff[i].binary && !state->file_diff[i].hunk_nr)\n \t\t\tbinary_count++;\n-\t\telse if (patch_update_file(state, state->file_diff + i))\n+\t\telse if (patch_update_file(state, state->file_diff + i, flags))\n \t\t\tbreak;\n \t}\n \n@@ -1956,7 +1959,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@@ -2005,7 +2009,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@@ -2021,7 +2025,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@@ -2079,7 +2084,7 @@ int run_add_p_index(struct repository *r,\n \n \tinteractive_config_init(&s.cfg, r, opts);\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 901c42fd7b..1facf19f96 100644\n--- a/add-patch.h\n+++ b/add-patch.h\n@@ -50,15 +50,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 6f1e213052..dfe9636079 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 530ae956ad..d325083ff3 100644\n--- a/builtin/checkout.c\n+++ b/builtin/checkout.c\n@@ -576,7 +576,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 088449e120..008929bc7c 100644\n--- a/builtin/reset.c\n+++ b/builtin/reset.c\n@@ -436,7 +436,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 3b50905233..eb8142565e 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.52.0.239.gd5f0c6e74e.dirty\n\n"},{"id":"531615","messageId":"20251203-b4-pks-history-builtin-v7-11-9e9f849bfd0e@pks.im","threadId":"63986","inReplyTo":"20251203-b4-pks-history-builtin-v7-0-9e9f849bfd0e@pks.im","subject":"[PATCH v7 11/12] cache-tree: allow writing in-memory index as tree","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-12-03T10:48:38Z","receivedAt":"2025-12-03T10:49:13Z","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 d325083ff3..aa00f0f483 100644\n--- a/builtin/checkout.c\n+++ b/builtin/checkout.c\n@@ -902,7 +902,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 2d8947b518..2976092270 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.52.0.239.gd5f0c6e74e.dirty\n\n"},{"id":"531616","messageId":"20251203-b4-pks-history-builtin-v7-12-9e9f849bfd0e@pks.im","threadId":"63986","inReplyTo":"20251203-b4-pks-history-builtin-v7-0-9e9f849bfd0e@pks.im","subject":"[PATCH v7 12/12] builtin/history: implement \"split\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-12-03T10:48:39Z","receivedAt":"2025-12-03T10:49:16Z","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              | 174 ++++++++++++++++\n t/meson.build                  |   1 +\n t/t3452-history-split.sh       | 452 +++++++++++++++++++++++++++++++++++++++++\n 4 files changed, 689 insertions(+)\n\ndiff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc\nindex 160bf5d4d2..f1252baa2f 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>\n+git history split <commit> [--] [<pathspec>...]\n \n DESCRIPTION\n -----------\n@@ -37,11 +38,72 @@ Several 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 CONFIGURATION\n -------------\n \n include::includes/cmd-config-section-all.adoc[]\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,e,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,e,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 17bb150b95..c5262d964a 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-reach.h\"\n #include \"commit.h\"\n #include \"config.h\"\n@@ -8,17 +9,22 @@\n #include \"environment.h\"\n #include \"gettext.h\"\n #include \"hex.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 \"reset.h\"\n #include \"revision.h\"\n+#include \"run-command.h\"\n #include \"sequencer.h\"\n #include \"strvec.h\"\n #include \"tree.h\"\n #include \"wt-status.h\"\n \n #define GIT_HISTORY_REWORD_USAGE N_(\"git history reword <commit>\")\n+#define GIT_HISTORY_SPLIT_USAGE  N_(\"git history split <commit> [--] [<pathspec>...]\")\n \n static int collect_commits(struct repository *repo,\n \t\t\t   struct commit *old_commit,\n@@ -364,6 +370,172 @@ static int cmd_history_reword(int argc,\n \treturn ret;\n }\n \n+static int split_commit(struct repository *repo,\n+\t\t\tstruct commit *original_commit,\n+\t\t\tstruct pathspec *pathspec,\n+\t\t\tstruct object_id *out)\n+{\n+\tstruct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;\n+\tstruct strbuf index_file = STRBUF_INIT;\n+\tstruct child_process read_tree_cmd = CHILD_PROCESS_INIT;\n+\tstruct index_state index = INDEX_STATE_INIT(repo);\n+\tstruct object_id original_commit_tree_oid, parent_tree_oid;\n+\tchar original_commit_oid[GIT_MAX_HEXSZ + 1];\n+\tstruct commit_list *parents = NULL;\n+\tstruct commit *first_commit;\n+\tstruct tree *split_tree;\n+\tint ret;\n+\n+\tif (original_commit->parents)\n+\t\tparent_tree_oid = *get_commit_tree_oid(original_commit->parents->item);\n+\telse\n+\t\toidcpy(&parent_tree_oid, repo->hash_algo->empty_tree);\n+\toriginal_commit_tree_oid = *get_commit_tree_oid(original_commit);\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+\tread_tree_cmd.git_cmd = 1;\n+\tstrvec_pushf(&read_tree_cmd.env, \"GIT_INDEX_FILE=%s\", index_file.buf);\n+\tstrvec_push(&read_tree_cmd.args, \"read-tree\");\n+\tstrvec_push(&read_tree_cmd.args, oid_to_hex(&parent_tree_oid));\n+\tret = run_command(&read_tree_cmd);\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_commit->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+\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(repo, \"split-out\", original_commit,\n+\t\t\t\t\t      &split_tree->object.oid,\n+\t\t\t\t\t      original_commit->parents, &parent_tree_oid, &out[0]);\n+\tif (ret < 0) {\n+\t\tret = error(_(\"failed writing split-out 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+\tfirst_commit = lookup_commit_reference(repo, &out[0]);\n+\tcommit_list_append(first_commit, &parents);\n+\n+\tret = commit_tree_with_edited_message(repo, \"split-out\", original_commit,\n+\t\t\t\t\t      &original_commit_tree_oid,\n+\t\t\t\t\t      parents, get_commit_tree_oid(first_commit), &out[1]);\n+\tif (ret < 0) {\n+\t\tret = error(_(\"failed writing split-out commit\"));\n+\t\tgoto out;\n+\t}\n+\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+\tstruct option options[] = {\n+\t\tOPT_END(),\n+\t};\n+\tstruct commit *original_commit, *parent, *head;\n+\tstruct strvec commits = STRVEC_INIT;\n+\tstruct object_id split_commits[2];\n+\tstruct pathspec pathspec = { 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 revision\"));\n+\t\tgoto out;\n+\t}\n+\trepo_config(repo, git_default_config, NULL);\n+\n+\tparse_pathspec(&pathspec, 0,\n+\t\tPATHSPEC_PREFER_FULL | PATHSPEC_SYMLINK_LEADING_PATH | PATHSPEC_PREFIX_ORIGIN,\n+\t\tprefix, argv + 1);\n+\n+\tret = gather_commits_between_head_and_revision(repo, argv[0], &original_commit,\n+\t\t\t\t\t\t       &parent, &head, &commits);\n+\tif (ret < 0)\n+\t\tgoto out;\n+\n+\t/*\n+\t * Then we split up the commit and replace the original commit with the\n+\t * new ones.\n+\t */\n+\tret = split_commit(repo, original_commit, &pathspec, split_commits);\n+\tif (ret < 0)\n+\t\tgoto out;\n+\n+\treplace_commits(&commits, &original_commit->object.oid,\n+\t\t\tsplit_commits, ARRAY_SIZE(split_commits));\n+\n+\tret = apply_commits(repo, &commits, parent, head, \"split\");\n+\tif (ret < 0)\n+\t\tgoto out;\n+\n+\tret = 0;\n+\n+out:\n+\tclear_pathspec(&pathspec);\n+\tstrvec_clear(&commits);\n+\treturn ret;\n+}\n+\n int cmd_history(int argc,\n \t\tconst char **argv,\n \t\tconst char *prefix,\n@@ -371,11 +543,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 e187aef31e..1eef433207 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -388,6 +388,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..02ae86a7e6\n--- /dev/null\n+++ b/t/t3452-history-split.sh\n@@ -0,0 +1,452 @@\n+#!/bin/sh\n+\n+test_description='tests for git-history split subcommand'\n+\n+. ./test-lib.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_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 rearrange commit history with merges\" err &&\n+\t\ttest_must_fail git history split HEAD~ 2>err &&\n+\t\ttest_grep \"cannot rearrange commit history with merges\" err\n+\t)\n+'\n+\n+test_expect_success 'refuses to work with unrelated 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\ttest_must_fail git history split ours 2>err &&\n+\t\ttest_grep \"commit must be reachable from current HEAD commit\" err\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 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+\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 '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 '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.52.0.239.gd5f0c6e74e.dirty\n\n"},{"id":"531632","messageId":"4m6rmefbv4hftclimitz5rp6yapswjtnjsxymrsdkuan4jbg3u@dm5jzdiq5cxz","threadId":"63986","inReplyTo":"aMsWL2cSoytbmRjA@szeder.dev","subject":"Re: [PATCH RFC 00/11] Introduce git-history(1) command for easy history editing","fromName":"Matthias Beyer","fromEmail":"mail@beyermatthias.de","sentAt":"2025-12-03T18:18:10Z","receivedAt":"2025-12-03T18:18:19Z","isPatch":true,"sender":{"key":"mail@beyermatthias.de","avatar":null},"body":"Hi,\n\nAm Wed, Sep 17, 2025 at 10:12:31PM +0200, schrieb SZEDER Gábor:\n> On Sun, Aug 24, 2025 at 06:03:02PM +0200, Patrick Steinhardt wrote:\n> > On Sat, Aug 23, 2025 at 06:25:06PM -0700, Martin von Zweigbergk wrote:\n> > > On Tue, Aug 19, 2025 at 3:57 AM Patrick Steinhardt <ps@pks.im> wrote:\n> > > >\n> > > > Hi,\n> > > >\n> > > > over recent months I've been playing around with Jujutsu quite\n> > > > frequently. While I still prefer using Git, there's been a couple\n> > > > features in it that I really like and that I'd like to have in Git, as\n> > > > well.\n> > > >\n> > > > A copule of these features relate to history editing. Most importantly,\n> > > > I really dig the following commands:\n> > > >\n> > > >   - jj-abandon(1) to drop a specific commit from your history.\n> > > \n> > > It also rebases all descendants on top of the parent(s) of the\n> > > abandoned commit(s). Branches pointing to the rebased commits are also\n> > > repointed. So is the working copy if it points to a rebased commit\n> > > (the closest equivalent in Git would be HEAD). Do you plan to make all\n> > > the `git history` commands behave that way too?\n> > \n> > Yup.\n> \n> That sounds scary...  What does \"all descendants\" mean in this\n> context?\n> \n> Let's suppose I have this piece of history, I'm on 'branch2', and I\n> drop commit B.  Which commits will be rewritten and which branches\n> will be repointed?\n> \n>    A---B---C---D   branch1\n>             \\   \\\n>              \\   E---F   branch2\n>               \\       \\\n>                \\       G---H---I   branch3\n>                 \\\n>                  J---K---L   branch4\n> \n\nJust speaking as a user here, but my expectation in this scenario would\nbe that rewriting B would be denied by default here, as branch{1..4}\nwould be rewritten although I am at branch2.\n\nIn the scenario at hand, I would expect that I can only rewrite G, H, I\nwhile on branch 3 and J, K, L while on branch4 (without passing some\nextra flags for \"yes, please also rewrite the other branches\").\n\nJust my 2cts\n"},{"id":"531686","messageId":"CABPp-BFtx7-vLFbVqbHar=UZb1CGX5=ufMA4hrJRkSYuB14_Tw@mail.gmail.com","threadId":"63986","inReplyTo":"aS80z0DxAEBLba-M@pks.im","subject":"Re: [PATCH v6 00/11] Introduce git-history(1) command for easy history editing","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2025-12-05T08:49:04Z","receivedAt":"2025-12-05T08:49:22Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Tue, Dec 2, 2025 at 10:50 AM Patrick Steinhardt <ps@pks.im> wrote:\n>\n> On Sat, Nov 22, 2025 at 06:54:00PM -0800, Elijah Newren wrote:\n> > On Fri, Nov 21, 2025 at 8:01 AM Junio C Hamano <gitster@pobox.com> wrote:\n> > >\n> > > Phillip Wood <phillip.wood123@gmail.com> writes:\n> > >\n> > > >> Granted, I think good progress is being made and perhaps the changes\n> > > >> needed for the rest aren't that huge (and maybe there's more pieces\n> > > >> I'm not quite understanding yet similar to the\n> > > >> two-split-patches-always-summing-to-the-original), I was just a little\n> > > >> surprised that my comments are summarized by \"expecting a small and\n> > > >> final reroll\".  :-)\n> > >\n> > > It was because I didn't think (and I still do not think) your\n> > > comments are something for the immediate future, for a tool that\n> > > wants to present its minimum-serviceable experimental version to\n> > > users so that the users can experiment, extend it more and fix its\n> > > behaviour incrementally.  I may have been probably wrong, but I was\n> > > getting an impression from the reviews that it is getting to there,\n> > > not the feature-perfect version that needs only maintenance from now\n> > > on, but a minimum-serviceable one.\n> > >\n> > > We could instead of collect all the I want moon and I want cheeze\n> > > comments and iterate until the tool has all of them before it hits\n> > > 'next', but I do not think it is often what we do to a new feature.\n> >\n> > You're reading my feedback as feature requests rather than as bugs\n> > and/or possible paint-ourselves-in-a-corner situations in the\n> > presented implementation?  I must have described things rather poorly;\n> > if they were just feature requests, I'd agree we could just implement\n> > them later.\n>\n> The command is explicitly marked as experimental so that we can iterate\n> on its behaviour as needed. So I don't think we're painting us into a\n> corner yet.\n\nI'm aware it's marked as experimental and even commented on that at\nleast once in this series.  I'm not sure that's sufficient _in this\nkind of case_ for the following reasons:\n\n* Users tend to not have multiple branches sharing commits and then\ntry to operate on those, it will only come up rarely.  (I still think\nit's a very important usecase, just that it's not common, in part\nbecause git has trained people away from this kind of usecase.)\n* Since the command is marked as experimental, it might extend the\ntimeline until we see such uses.\n* The implemented behavior is not always a bug, some users will want\nit (it's the kind of thing that makes sense as an option).\n\nCombining the above, it might be a long time before folks really hit\nthis behavior and start pointing out its problems, rather than hitting\nthem early on.  By then, we don't know how many users we have, just\nthat it's been a long time, and we are further faced with the fact\nthat some subset of users have begun to depend upon the existing\nbehavior.  The combination may make it hard to change at that point.\n\nTo me, it feels very much like risking painting ourselves into a\ncorner.  If it were an issue I felt people would likely hit right away\nOR it was an issue which users would always view as a bug, then I'd\nagree with you that there's no (or very little) risk with just\nproceeding as-is.\n\n> > But maybe I see where the confusion arises, since you were focusing\n> > solely on the single branch thing; that's the one issue where it's\n> > perhaps not as clear whether I was discussing a bug or a new feature.\n> > Let me try to explain that case another way:\n> >\n> >\n> > The early cover letters said they focused on a case where just a\n> > single branch was involved, yet they don't check whether there really\n> > is only one branch involved for safe operation.\n>\n> I think this depends on the definition. We _do_ verify that the commit\n> that is to be edited is part of the current branch. What we _don't_\n> check is that the commit is _only_ part of that current branch.\n>\n> I'm not sure whether that is something we want. I myself have the same\n> commit on multiple branches quite regularly, as I tend to queue up\n> multiple dependent patch series. But I wouldn't ever want a history edit\n> to affect all of these branches myself, I really only want it to modify\n> the branch I'm currently on.\n\nTo me at least, that feels crazy.  Rebase was broken for multiple\ninterconnected or dependent branches precisely because you could only\nupdate one branch, then needed to find the subset of the next branch\nnot contained in the first as well as finding where to graft that next\nset onto in order to manually rebase it, then repeat for the third\nbranch, and so on.  That's precisely the design mistake that made me\ngive up on rebase and write something else...and you want to\nexplicitly copy it?\n\n(--update-refs did come along later to help in the common case that\neach new branch fully contained the previous ones, but it didn't help\nwhen you had multiple branches that built on some common base, or had\nother interesting topologies, and thus didn't really fix the\nunderlying problem.)\n\n> > If a user tries to reword or split a commit that is in the history of\n> > multiple branches, the current implementation does not check and makes\n> > the branches diverge.  Some users may want that, though I suspect most\n> > would be negatively surprised.  The commit messages and code do not\n> > even discuss this case.  If we merge the code as-is and then later\n> > notice and fix this problem soon enough, maybe we'd be fine, but I\n> > always worry a bit about a git-switch/git-restore kind of case where\n> > it sits long enough and people depend on side-effects in a way that\n> > prevents us from fixing it.  Besides, since the bug has been\n> > identified and there are multiple not-too-hard ways to fix, I think we\n> > should do something.  Some possibilities:\n> >\n> >   * Document this case and warn users to check on their own (not that\n> > friendly, but might be good enough for the first cut).\n> >   * Check if the user-specified commit is part of multiple branches\n> > and error out, unless the user provides a flag verifying that they\n> > want histories to diverge.\n> >   * Just rewrite all relevant branches\n> >\n[...]\n> So with the above clarification I wouldn't call any of this a bug, but\n> rather working as designed. We could of course still print a warning in\n> that case to protect the user, but one problem I see is that generating\n> this warning could be quite expensive as we'd now have to walk all\n> references. That might be cheap in case the user only has short-lived\n> feature branchs. But it may very well not be cheap in case they for\n> example have old release branches checked out, as we'd now have to\n> potentially walk a significant portiion of history.\n\nMunging user's histories in unexpected ways and depending on them to\nfigure out on their own that such has happened may well be much more\nexpensive.  Granted, that's human time rather than cpu time, so it's\nnot directly comparable, but this feels like a big foot-gun, and I\nthink big foot-guns deserve good checks and warnings.  I agree there's\na cost here, but I think the cost is warranted, especially since such\nan error message would be the thing that frees us to iterate and\ndefine the multi-branch behavior later.\n\n> Consequently I'm leaning more into the direction of doing nothing. It's\n> not really clear to me that this is a bug, and we still can introduce a\n> flag in the future that opts into the behaviour of rewriting relevant\n> branches. That behaviour certainly can be useful, but I'd claim that\n> it would be rather surprising to the user if that was the default.\n\nWell, as I stated above, this is basically copying what I view as the\nfundamental design mistake of git-rebase.  The many other points of\nfeedback I had on this series (e.g. extended headers, reusing replay's\nwalking, etc.) are things I could easily negotiate on; this one\nbothers me much, much more.  To me, it ruins the command and makes me\nfeel it is unsuitable for inclusion in git; this is, after all, the\nkind of thing that made me decide to write yet another command to\nworkaround such a flaw.  If the series is merged with this behavior,\nI'm going to be in the awkward position of feeling I need to actively\nrecommend against its usage unless _and until_ we either\n\n(a) check that a commit is only part of one branch before proceeding,\n(b) always require the user to specify with a flag how to handle\ncommits that happen to be part of multiple branches (even when a\ncommit only happens to be part of one branch, in order to allow us to\nnot bother checking whether it's part of more),\nor\n(c) rewrite all branches that contain the given commit by default\n(with an option to only rewrite the current one).\n\nThat said, obviously the choice of whether the series is merged isn't\nup to me.  And maybe I'm in the minority, and others don't care about\nthis issue at all.  But it's how I feel about it.\n"},{"id":"531891","messageId":"aTfVfenbwY685fDZ@pks.im","threadId":"63986","inReplyTo":"CABPp-BFtx7-vLFbVqbHar=UZb1CGX5=ufMA4hrJRkSYuB14_Tw@mail.gmail.com","subject":"Re: [PATCH v6 00/11] Introduce git-history(1) command for easy history editing","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-12-09T07:53:33Z","receivedAt":"2025-12-09T07:53:40Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Fri, Dec 05, 2025 at 12:49:04AM -0800, Elijah Newren wrote:\n> On Tue, Dec 2, 2025 at 10:50 AM Patrick Steinhardt <ps@pks.im> wrote:\n> > Consequently I'm leaning more into the direction of doing nothing. It's\n> > not really clear to me that this is a bug, and we still can introduce a\n> > flag in the future that opts into the behaviour of rewriting relevant\n> > branches. That behaviour certainly can be useful, but I'd claim that\n> > it would be rather surprising to the user if that was the default.\n> \n> Well, as I stated above, this is basically copying what I view as the\n> fundamental design mistake of git-rebase.  The many other points of\n> feedback I had on this series (e.g. extended headers, reusing replay's\n> walking, etc.) are things I could easily negotiate on; this one\n> bothers me much, much more.  To me, it ruins the command and makes me\n> feel it is unsuitable for inclusion in git; this is, after all, the\n> kind of thing that made me decide to write yet another command to\n> workaround such a flaw.  If the series is merged with this behavior,\n> I'm going to be in the awkward position of feeling I need to actively\n> recommend against its usage unless _and until_ we either\n> \n> (a) check that a commit is only part of one branch before proceeding,\n> (b) always require the user to specify with a flag how to handle\n> commits that happen to be part of multiple branches (even when a\n> commit only happens to be part of one branch, in order to allow us to\n> not bother checking whether it's part of more),\n> or\n> (c) rewrite all branches that contain the given commit by default\n> (with an option to only rewrite the current one).\n> \n> That said, obviously the choice of whether the series is merged isn't\n> up to me.  And maybe I'm in the minority, and others don't care about\n> this issue at all.  But it's how I feel about it.\n\nI guess it's a matter of workflows and tastes, and there's never going\nto be the one correct way of doing things. I don't think (b) is a good\noption as it makes things more complex even for the simplest cases. But\nI wouldn't be opposed to a combination of (a) and (b) if we can\nimplement (a) efficiently.\n\nDo we already have logic like this in git-replay(1)?\n\nPatrick\n"},{"id":"531902","messageId":"CANiSa6hxjghKQMhURx8qC2t=+1gEE7p8YaHbWkg3rYOYa=poVg@mail.gmail.com","threadId":"63986","inReplyTo":"aTfVfenbwY685fDZ@pks.im","subject":"Re: [PATCH v6 00/11] Introduce git-history(1) command for easy history editing","fromName":"Martin von Zweigbergk","fromEmail":"martinvonz@gmail.com","sentAt":"2025-12-09T17:43:41Z","receivedAt":"2025-12-09T17:43:54Z","isPatch":true,"sender":{"key":"martinvonz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/891642?v=4"},"body":"On Mon, Dec 8, 2025 at 11:53 PM Patrick Steinhardt <ps@pks.im> wrote:\n>\n> On Fri, Dec 05, 2025 at 12:49:04AM -0800, Elijah Newren wrote:\n> > On Tue, Dec 2, 2025 at 10:50 AM Patrick Steinhardt <ps@pks.im> wrote:\n> > > Consequently I'm leaning more into the direction of doing nothing. It's\n> > > not really clear to me that this is a bug, and we still can introduce a\n> > > flag in the future that opts into the behaviour of rewriting relevant\n> > > branches. That behaviour certainly can be useful, but I'd claim that\n> > > it would be rather surprising to the user if that was the default.\n> >\n> > Well, as I stated above, this is basically copying what I view as the\n> > fundamental design mistake of git-rebase.  The many other points of\n> > feedback I had on this series (e.g. extended headers, reusing replay's\n> > walking, etc.) are things I could easily negotiate on; this one\n> > bothers me much, much more.  To me, it ruins the command and makes me\n> > feel it is unsuitable for inclusion in git; this is, after all, the\n> > kind of thing that made me decide to write yet another command to\n> > workaround such a flaw.  If the series is merged with this behavior,\n> > I'm going to be in the awkward position of feeling I need to actively\n> > recommend against its usage unless _and until_ we either\n> >\n> > (a) check that a commit is only part of one branch before proceeding,\n> > (b) always require the user to specify with a flag how to handle\n> > commits that happen to be part of multiple branches (even when a\n> > commit only happens to be part of one branch, in order to allow us to\n> > not bother checking whether it's part of more),\n> > or\n> > (c) rewrite all branches that contain the given commit by default\n> > (with an option to only rewrite the current one).\n> >\n> > That said, obviously the choice of whether the series is merged isn't\n> > up to me.  And maybe I'm in the minority, and others don't care about\n> > this issue at all.  But it's how I feel about it.\n>\n> I guess it's a matter of workflows and tastes, and there's never going\n> to be the one correct way of doing things. I don't think (b) is a good\n> option as it makes things more complex even for the simplest cases. But\n> I wouldn't be opposed to a combination of (a) and (b) if we can\n> implement (a) efficiently.\n\nFWIW, I agree with Elijah that (c) is the right end state. That's\nperhaps not surprising given that that's what jj has been doing for\nmany years (as many of you know already, I'm one of the jj\nmaintainers). I think it's very rare that we hear from users that they\nwant to rewrite a commit and its descendants while leaving some of the\ndescendants in place. We have a `jj duplicate` command they can use,\nbut that won't move any bookmarks (branches) over. So if you have\nbookmark `foo` with descendant bookmarks `bar` and `baz` and you want\nto amend `foo` while moving `bar` over and keeping `baz` in place, the\nsimplest way is probably to duplicate all the necessary commit\n(something like `jj duplicate -r main..bar`) and then manually move\nover `foo` and `bar`. That seems like such a rare use case that we\nhaven't had a reason to make it simpler so far.\n\nRegarding performance of (a), I would think that walking all\nbranches/tags until you reach the to-be-modified commit (or reach an\nolder generation) can usually be done quickly enough, but obviously\nthere are many others on this list who know that better than me :) But\nI have also heard that some Git repos have tens (hundreds?) of\nthousands of branches. Hopefully those refs are just on some server\nwhere users won't run `git history`.\n\n>\n> Do we already have logic like this in git-replay(1)?\n>\n> Patrick\n"},{"id":"531910","messageId":"b3ddfaa4-526b-41e3-b12a-0fec846ac7bc@app.fastmail.com","threadId":"63986","inReplyTo":"aS80z0DxAEBLba-M@pks.im","subject":"Re: [PATCH v6 00/11] Introduce git-history(1) command for easy history editing","fromName":"Kristoffer Haugsbakk","fromEmail":"kristofferhaugsbakk@fastmail.com","sentAt":"2025-12-09T18:29:27Z","receivedAt":"2025-12-09T18:29:48Z","isPatch":true,"sender":{"key":"kristofferhaugsbakk@fastmail.com","avatar":null},"body":"On Tue, Dec 2, 2025, at 19:49, Patrick Steinhardt wrote:\n>>[snip]\n>\n> I'm not sure whether that is something we want. I myself have the same\n> commit on multiple branches quite regularly, as I tend to queue up\n> multiple dependent patch series. But I wouldn't ever want a history edit\n> to affect all of these branches myself, I really only want it to modify\n> the branch I'm currently on.\n\nLet’s say with the current subcommands I want to\n\n• Split a commit because it contains unrelated formatting fixups\n• I want to fix typos in a commit message and also add a paragraph\n\nThen let’s say that there are three branches that contain these commits.\nI can’t think of a case where I intend to only make these changes for\none of the branches.\n\nA default of update-all-branches-in-range seems like the best default.\n\nI also agree with everything Elijah wrote here:\n\nhttps://lore.kernel.org/git/CABPp-BFtx7-vLFbVqbHar=UZb1CGX5=ufMA4hrJRkSYuB14_Tw@mail.gmail.com/\n\nI haven’t used Jujutsu yet. But apparently it is much less\nbranch-focused. But for us who use branches and sometimes need\n“dependent branches” or “stacked branches” the default equivalent of\n`--update-refs` seems like a win to me.\n\nOkay, maybe let’s say that I am really making intentionally-divergent\nhistories and one commit needs to be reworded for that divergent\ncontext. That could happen. But I can’t imageine where I would do that.\n\n>[snip]\n> Consequently I'm leaning more into the direction of doing nothing. It's\n> not really clear to me that this is a bug, and we still can introduce a\n> flag in the future that opts into the behaviour of rewriting relevant\n> branches. That behaviour certainly can be useful, but I'd claim that\n> it would be rather surprising to the user if that was the default.\n\nMaybe it’s surprising with the current defaults of other commands. But\nfor a new-and-better (for some circumstances or all) commands defaulting\nto updating all branches sounds great.\n\nI have read or heard about the “mega merge” strategy in Jujutsu.[1]\nBeing able to (this is how I imagine it could work) make a temporary\nintegration branch where N branches can be edited by making edits to\nthem and having all the branches be updated sounds amazing.[2][3] I have\nfound myself doing temporary integration branches where I make fixes on\ntop and manually cherry-picking them to the correct target afterwards.\n\n🔗 1: https://news.ycombinator.com/item?id=44650248\n† 2: Maybe the commits are “edited” and then the integration merges are\n     re-done which sounds simple in the case of splitting commits and\n     rewording commit messages. Well, “simple”, that’s easy for me to\n     say from the peanut gallery. :) (I am aware that git-history(1)\n     does not support merges in this current iteration)\n† 3: I am thinking of the “common case” of `-update-refs` but what\n     Elijah pointed out about “other interesting topologies” sounds even\n     better. :) In the link from above:\n\n     https://lore.kernel.org/git/CABPp-BFtx7-vLFbVqbHar=UZb1CGX5=ufMA4hrJRkSYuB14_Tw@mail.gmail.com/\n"},{"id":"531948","messageId":"CABPp-BE+Tu=TCjoNOo7aMAauGi6KAJRc_FPswdxgSU6-zPR+ww@mail.gmail.com","threadId":"63986","inReplyTo":"aTfVfenbwY685fDZ@pks.im","subject":"Re: [PATCH v6 00/11] Introduce git-history(1) command for easy history editing","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2025-12-10T06:55:02Z","receivedAt":"2025-12-10T06:55:15Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Mon, Dec 8, 2025 at 11:53 PM Patrick Steinhardt <ps@pks.im> wrote:\n>\n> On Fri, Dec 05, 2025 at 12:49:04AM -0800, Elijah Newren wrote:\n> > On Tue, Dec 2, 2025 at 10:50 AM Patrick Steinhardt <ps@pks.im> wrote:\n> > > Consequently I'm leaning more into the direction of doing nothing. It's\n> > > not really clear to me that this is a bug, and we still can introduce a\n> > > flag in the future that opts into the behaviour of rewriting relevant\n> > > branches. That behaviour certainly can be useful, but I'd claim that\n> > > it would be rather surprising to the user if that was the default.\n> >\n> > Well, as I stated above, this is basically copying what I view as the\n> > fundamental design mistake of git-rebase.  The many other points of\n> > feedback I had on this series (e.g. extended headers, reusing replay's\n> > walking, etc.) are things I could easily negotiate on; this one\n> > bothers me much, much more.  To me, it ruins the command and makes me\n> > feel it is unsuitable for inclusion in git; this is, after all, the\n> > kind of thing that made me decide to write yet another command to\n> > workaround such a flaw.  If the series is merged with this behavior,\n> > I'm going to be in the awkward position of feeling I need to actively\n> > recommend against its usage unless _and until_ we either\n> >\n> > (a) check that a commit is only part of one branch before proceeding,\n> > (b) always require the user to specify with a flag how to handle\n> > commits that happen to be part of multiple branches (even when a\n> > commit only happens to be part of one branch, in order to allow us to\n> > not bother checking whether it's part of more),\n> > or\n> > (c) rewrite all branches that contain the given commit by default\n> > (with an option to only rewrite the current one).\n> >\n> > That said, obviously the choice of whether the series is merged isn't\n> > up to me.  And maybe I'm in the minority, and others don't care about\n> > this issue at all.  But it's how I feel about it.\n>\n> I guess it's a matter of workflows and tastes, and there's never going\n> to be the one correct way of doing things. I don't think (b) is a good\n> option as it makes things more complex even for the simplest cases. But\n> I wouldn't be opposed to a combination of (a) and (b) if we can\n> implement (a) efficiently.\n>\n> Do we already have logic like this in git-replay(1)?\n\nNo, git-replay was written from the beginning with the idea in mind of\nhandling multiple branches (e.g. letting Junio edit a single commit in\nsomeone's topic and updating all the subsequent commits and merges\nwithout having to individually fuss with them all, or similarly for\nthe Git For Windows or Microsoft Git forks, or at a smaller level if I\nhave multiple topics that share a few commits and I want to update one\nof those), so the idea was always (c) by default, with options for\nalternate behavior, which is kind of the opposite angle you are\napproaching from.  Anyway, because of that view, nothing like (a) was\never implemented.\n"},{"id":"531954","messageId":"48ba9303-45f4-43bf-a257-10d58474096c@gmail.com","threadId":"63986","inReplyTo":"aS81GAuKV79x_V2K@pks.im","subject":"Re: [PATCH v6 11/11] builtin/history: implement \"split\" subcommand","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2025-12-10T09:51:33Z","receivedAt":"2025-12-10T09:51:40Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"On 02/12/2025 18:51, Patrick Steinhardt wrote:\n> On Fri, Nov 21, 2025 at 02:31:14PM +0000, Phillip Wood wrote:\n>> On 27/10/2025 11:33, Patrick Steinhardt wrote:\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>>> +\tread_tree_cmd.git_cmd = 1;\n>>> +\tstrvec_pushf(&read_tree_cmd.env, \"GIT_INDEX_FILE=%s\", index_file.buf);\n>>> +\tstrvec_push(&read_tree_cmd.args, \"read-tree\");\n>>> +\tstrvec_push(&read_tree_cmd.args, oid_to_hex(&parent_tree_oid));\n>>> +\tret = run_command(&read_tree_cmd);\n>>\n>> Why do we need to fork \"read-tree\" here rather than call unpack_trees()\n>> ourselves?\n> \n> This is an artifact of how the `run_add_p()` interfaces work. They\n> unfortunately do not work on top of an in-memory index, but they work on\n> an on-disk index.\n\nOh I see, but why does that mean we need to fork a subprocess rather \nthan writing the index to disc in this process?\n\nThanks\n\nPhillip\n\n>>> [...]\n>>> +\t/* We retain authorship of the original commit. */\n>>> +\toriginal_message = repo_logmsg_reencode(repo, original_commit, NULL, NULL);\n>>> +\tptr = find_commit_header(original_message, \"author\", &len);\n>>> +\tif (ptr)\n>>> +\t\toriginal_author = xmemdupz(ptr, len);\n>>> +\n>>> +\tret = fill_commit_message(repo, &parent_tree_oid, &split_tree->object.oid,\n>>> +\t\t\t\t  \"\", \"split-out\", &split_message);\n>>> +\tif (ret < 0)\n>>> +\t\tgoto out;\n>>> +\n>>> +\tret = commit_tree(split_message.buf, split_message.len, &split_tree->object.oid,\n>>> +\t\t\t  original_commit->parents, &out[0], original_author, NULL);\n>>> +\tif (ret < 0) {\n>>> +\t\tret = error(_(\"failed writing split-out commit\"));\n>>> +\t\tgoto out;\n>>> +\t}\n>>\n>> Don't we have the same code for rewording a commit, maybe we should package\n>> this up into a shared helper function.\n> \n> Hm, indeed, there's a bit of non-trivial logic here. I'll refactor this.\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>>> +\tstruct option options[] = {\n>>> +\t\tOPT_END(),\n>>> +\t};\n>>> +\tstruct oidmap rewritten_commits = OIDMAP_INIT;\n>>> +\tstruct commit *original_commit, *parent, *head;\n>>> +\tstruct strvec commits = STRVEC_INIT;\n>>> +\tstruct commit_list *from_list = NULL;\n>>> +\tstruct object_id split_commits[2];\n>>> +\tstruct pathspec pathspec = { 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 revision\"));\n>>> +\t\tgoto out;\n>>> +\t}\n>>> +\trepo_config(repo, git_default_config, NULL);\n>>> +\n>>> +\toriginal_commit = lookup_commit_reference_by_name(argv[0]);\n>>> +\tif (!original_commit) {\n>>> +\t\tret = error(_(\"commit to be split cannot be found: %s\"), argv[0]);\n>>> +\t\tgoto out;\n>>> +\t}\n>>> +\n>>> +\tparent = original_commit->parents ? original_commit->parents->item : NULL;\n>>> +\tif (parent && repo_parse_commit(repo, parent)) {\n>>> +\t\tret = error(_(\"unable to parse commit %s\"),\n>>> +\t\t\t    oid_to_hex(&parent->object.oid));\n>>> +\t\tgoto out;\n>>> +\t}\n>>> +\n>>> +\thead = lookup_commit_reference_by_name(\"HEAD\");\n>>> +\tif (!head) {\n>>> +\t\tret = error(_(\"could not resolve HEAD to a commit\"));\n>>> +\t\tgoto out;\n>>> +\t}\n>>> +\n>>> +\tcommit_list_append(original_commit, &from_list);\n>>> +\tif (!repo_is_descendant_of(repo, head, from_list)) {\n>>> +\t\tret = error(_(\"split commit must be reachable from current HEAD commit\"));\n>>> +\t\tgoto out;\n>>> +\t}\n>>\n>> This is very similar to cmd_history_reword() up to this point. When we add\n>> the \"drop\" and \"amend\" subcommands they're going to want to do the same\n>> checks.\n>>\n>>> +\tparse_pathspec(&pathspec, 0,\n>>> +\t\tPATHSPEC_PREFER_FULL | PATHSPEC_SYMLINK_LEADING_PATH | PATHSPEC_PREFIX_ORIGIN,\n>>> +\t\tprefix, argv + 1);\n>>\n>> This and calling split_commit() below are the only real differences with\n>> cmd_history_reword(), is it worth trying to share some more code between the\n>> two?\n> \n> Yup, done.\n> \n> Thanks!\n> \n> Patrick\n> \n\n"},{"id":"531955","messageId":"266a9cfc-142f-49c0-a2a0-1d67426479b9@gmail.com","threadId":"63986","inReplyTo":"aS80_rTVHP44JKhx@pks.im","subject":"Re: [PATCH v6 05/11] builtin/history: implement \"reword\" subcommand","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2025-12-10T09:52:52Z","receivedAt":"2025-12-10T09:52:59Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"On 02/12/2025 18:50, Patrick Steinhardt wrote:\n> On Mon, Nov 17, 2025 at 04:27:59PM +0000, Phillip Wood wrote:\n>> Hi Patrick\n>> On 27/10/2025 11:33, Patrick Steinhardt wrote:\n>>\n>>> +static int collect_commits(struct repository *repo,\n>>> +\t\t\t   struct commit *old_commit,\n>>> +\t\t\t   struct commit *new_commit,\n>>> +\t\t\t   struct strvec *out)\n>>\n>> Now that we're not using the sequencer it would be nice to stop messing\n>> about converting object ids to and from strings and return an array of\n>> \"struct commit\" instead of \"struct strvec\"\n> \n> I was trying to avoid using a strvec, but honestly that turned out to be\n> more pain than it is worth. We don't have functions like\n> `strvec_splice()` for simple arrays, and there is no commit array struct\n> that provides similar wrappers, either.\n\nI'm surprised it was such a pain compared to the cost to using a strvec. \nWe're forever converting from a string to a struct commit and back again \nwhich bloats the code and obscures the interesting and important parts. \nThat cost will be paid each time we add a new subcommand. An \nimplementation of 'struct commit_vec' that implements commit_vec_push(), \ncommit_vec_splice() and commit_vec_clear() is only going to be 30 or 40 \nlines of code and gives us a solid foundation for this series. Open \ncoding the array and adding a SPLICE_ARRAY macro would also be pretty \nsimple.\n\nThanks\n\nPhillip\n\n>>   > +{\n>>> +\tstruct setup_revision_opt revision_opts = {\n>>> +\t\t.assume_dashdash = 1,\n>>> +\t};\n>>> +\tstruct strvec revisions = STRVEC_INIT;\n>>> +\tstruct commit *child;\n>>> +\tstruct rev_info rev = { 0 };\n>>> +\tint ret;\n>>> +\n>>> +\trepo_init_revisions(repo, &rev, NULL);\n>>> +\tstrvec_push(&revisions, \"\");\n>>> +\tstrvec_push(&revisions, oid_to_hex(&new_commit->object.oid));\n>>> +\tif (old_commit)\n>>> +\t\tstrvec_pushf(&revisions, \"^%s\", oid_to_hex(&old_commit->object.oid));\n>>> +\n>>> +\tsetup_revisions_from_strvec(&revisions, &rev, &revision_opts);\n>>> +\tif (revisions.nr != 1 || prepare_revision_walk(&rev)) {\n>>\n>> I'm not that familiar with the revision walking api, what 'revisions.nr !=\n>> 1' check for here?\n> \n> It's basically a check that the revision arguments have all been\n> consumed, except for the initial empty argument. The interface is a bit\n> weird.\n> \n> [snip]\n>>> +\t\tif (!onto) {\n>>> +\t\t\tonto = commit;\n>>> +\t\t} else {\n>>> +\t\t\tstruct tree *tree = repo_get_commit_tree(repo, commit);\n>>> +\t\t\tonto = replay_create_commit(repo, tree, commit, onto);\n>>> +\t\t\tif (!onto)\n>>> +\t\t\t\tbreak;\n>>\n>> Don't we want to avoid updating HEAD if replay_create_commit() fails?\n> \n> Good point, yes.\n> \n>>> +\t\t}\n>>> +\t}\n>>> +\n>>> +\treset_opts.oid = &onto->object.oid;\n>>> +\tstrbuf_addf(&buf, \"%s: switch to rewritten %s\", action, oid_to_hex(reset_opts.oid));\n>>\n>> We're not switching branches so I wonder if saying \"history: <action> <oid>\n>> <commit subject>\" might be a more useful reflog entry\n> \n> We're not switching branches, true, but we do switch to the rewritten\n> commit. Also I'm not sure that printing the commit subject here would\n> make sense, as the question becomes which subject to print: the one\n> we're moving to, which is the new tip of the branch but may not be the\n> commit we have rewritten? Or do we print the subject of the rewritten\n> commit?\n> \n>>> +static int fill_commit_message(struct repository *repo,\n>>> +\t\t\t       const struct object_id *old_tree,\n>>> +\t\t\t       const struct object_id *new_tree,\n>>> +\t\t\t       const char *default_message,\n>>> +\t\t\t       const char *action,\n>>> +\t\t\t       struct strbuf *out)\n>>> +{\n>>> +\tconst char *path = git_path_commit_editmsg();\n>>> +\tconst char *hint =\n>>> +\t\t_(\"Please enter the commit message for the %s changes.\"\n>>\n>> Maybe \"Please edit the commit message\"? Also do we want to tell the user\n>> they can abort by clearing the commit message?\n> \n> The \"Please edit the commit message\" thing is taken from other commands\n> that phrase it similarly. But it certainly does make sense to note that\n> clearing the commit message aborts, will add.\n> \n>>> +\t\t  \" Lines starting\\nwith '%s' will be ignored.\\n\");\n>>> +\tstruct wt_status s;\n>>> +\n>>> +\tstrbuf_addstr(out, default_message);\n>>> +\tstrbuf_addch(out, '\\n');\n>>> +\tstrbuf_commented_addf(out, comment_line_str, hint, action, comment_line_str);\n>>> +\twrite_file_buf(path, out->buf, out->len);\n>>> +\n>>> +\twt_status_prepare(repo, &s);\n>>> +\tFREE_AND_NULL(s.branch);\n>>> +\ts.ahead_behind_flags = AHEAD_BEHIND_QUICK;\n>>> +\ts.commit_template = 1;\n>>> +\ts.colopts = 0;\n>>> +\ts.display_comment_prefix = 1;\n>>> +\ts.hints = 0;\n>>> +\ts.use_color = 0;\n>>> +\ts.whence = FROM_COMMIT;\n>>> +\ts.committable = 1;\n>>\n>> \"git commit\" reads a load of status related config settings, is any of that\n>> relevant here?\n> \n> Yeah, some of it is. We don't handle them all yet, but this will be\n> backfilled in the future.\n> \n>>> +\ts.fp = fopen(git_path_commit_editmsg(), \"a\");\n>>> +\tif (!s.fp)\n>>> +\t\treturn error_errno(_(\"could not open '%s'\"), git_path_commit_editmsg());\n>>> +\n>>> +\twt_status_collect_changes_trees(&s, old_tree, new_tree);\n>>> +\twt_status_print(&s);\n>>> +\twt_status_collect_free_buffers(&s);\n>>> +\tstring_list_clear_func(&s.change, change_data_free);\n>>> +\n>>> +\tstrbuf_reset(out);\n>>> +\tif (launch_editor(path, out, NULL)) {\n>>> +\t\tfprintf(stderr, _(\"Please supply the message using the -m option.\\n\"));\n>>\n>> I'm not sure that's a very helpful suggestion as we don't support \"-m\" (it's\n>> not very helpful when \"git commit --amend\" suggests it either). We should\n>> just give up if the editor fails.\n> \n> Ah, this is a leftover error message from previous iterations.\n> \n>>> +static int cmd_history_reword(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_REWORD_USAGE,\n>>> +\t\tNULL,\n>>> +\t};\n>>> +\tstruct option options[] = {\n>>> +\t\tOPT_END(),\n>>> +\t};\n>>> +\tstruct strbuf final_message = STRBUF_INIT;\n>>> +\tstruct commit *original_commit, *parent, *head;\n>>> +\tstruct strvec commits = STRVEC_INIT;\n>>> +\tstruct object_id parent_tree_oid, original_commit_tree_oid;\n>>> +\tstruct object_id rewritten_commit;\n>>> +\tstruct commit_list *from_list = NULL;\n>>> +\tconst char *original_message, *original_body, *ptr;\n>>> +\tchar *original_author = NULL;\n>>> +\tsize_t len;\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 single revision\"));\n>>> +\t\tgoto out;\n>>> +\t}\n>>> +\trepo_config(repo, git_default_config, NULL);\n>>> +\n>>> +\toriginal_commit = lookup_commit_reference_by_name(argv[0]);\n>>> +\tif (!original_commit) {\n>>> +\t\tret = error(_(\"commit to be reworded cannot be found: %s\"), argv[0]);\n>>> +\t\tgoto out;\n>>> +\t}\n>>> +\toriginal_commit_tree_oid = repo_get_commit_tree(repo, original_commit)->object.oid;\n>>\n>> Looking at the implementation of repo_get_commit_tree() it can return NULL\n> \n>>> diff --git a/t/t3450-history.sh b/t/t3450-history.sh\n>>> index 417c343d43b..f513463b92b 100755\n>>> --- a/t/t3450-history.sh\n>>> +++ b/t/t3450-history.sh\n>>> @@ -5,13 +5,13 @@ test_description='tests for git-history command'\n>>>    . ./test-lib.sh\n>>>    test_expect_success 'does nothing without any arguments' '\n>>> -\tgit history >out 2>&1 &&\n>>> -\ttest_must_be_empty out\n>>> +\ttest_must_fail git history 2>err &&\n>>> +\ttest_grep \"need a subcommand\" err\n>>>    '\n>>>    test_expect_success 'raises an error with unknown argument' '\n>>>    \ttest_must_fail git history garbage 2>err &&\n>>> -\ttest_grep \"unrecognized argument: garbage\" err\n>>> +\ttest_grep \"unknown subcommand: .garbage.\" err\n>>>    '\n>>>    test_done\n>>\n>> Do we really need a separate test file just for a couple of tests. I can see\n>> that having a separate test file for each subcommand makes sense but can't\n>> we just add these two tests to one of those?\n> \n> I felt it was dirty to randomly add it to any of the other test suites,\n> so I decided to instead have it in its own standalone file. It may also\n> become relevant in the future if we ever needed commands like for\n> example `git history --continue`, same as the sequencer-based commands\n> have.\n> \n>>> diff --git a/t/t3451-history-reword.sh b/t/t3451-history-reword.sh\n>>> new file mode 100755\n>>> index 00000000000..09dbc463c59\n>>> --- /dev/null\n>>> +++ b/t/t3451-history-reword.sh\n>>> @@ -0,0 +1,237 @@\n>>> +#!/bin/sh\n>>> +\n>>> +test_description='tests for git-history reword subcommand'\n>>> +\n>>> +. ./test-lib.sh\n>>> +\n>>> +reword_with_message () {\n>>> +\tcat >message &&\n>>> +\twrite_script fake-editor.sh <<-EOF &&\n>>> +\tcp \"$(pwd)/message\" \"\\$1\"\n>>\n>> Let's hope $(pwd) doesn't contain any dollar signs, backticks, backslashes\n>> or double quotes. Doing\n>>\n>> \texport MSG_PATH=\"$(pwd)/message\"\n>> \twrite_script fake-editor.sh <<-\\EOF &&\n>> \tcp \"$MSG_PATH\" \"$1\"\n>> \tEOF\n>>\n>> would be safer\n> \n> True. Will use this:\n> \n> diff --git a/t/t3451-history-reword.sh b/t/t3451-history-reword.sh\n> index 8b353e74dc..4c87953176 100755\n> --- a/t/t3451-history-reword.sh\n> +++ b/t/t3451-history-reword.sh\n> @@ -6,11 +6,11 @@ test_description='tests for git-history reword subcommand'\n>   \n>   reword_with_message () {\n>   \tcat >message &&\n> -\twrite_script fake-editor.sh <<-EOF &&\n> -\tcp \"$(pwd)/message\" \"\\$1\"\n> +\twrite_script fake-editor.sh <<-\\EOF &&\n> +\tcp \"$ORIG_PATH/message\" \"$1\"\n>   \tEOF\n>   \ttest_set_editor \"$(pwd)\"/fake-editor.sh &&\n> -\tgit history reword \"$@\" &&\n> +\tORIG_PATH=\"$(pwd)\" git history reword \"$@\" &&\n>   \trm fake-editor.sh message\n>   }\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>>\n>> Do we really need to set up a separate repo for each test? The test suite is\n>> slow enough already without running \"git init\" followed by a bunch calls to\n>> test_commit() in each test. Can we instead run \"git reset --hard\n>> <known-starting-point> at the beginning of each test? That removes any\n>> interdependence between tests but saves a bunch of processes.\n> \n> I prefer that style as it is extremely hard to reason about tests that\n> have interdependencies, and not all the state may be removed by a hard\n> reset.\n> \n>>> +\ttest_when_finished \"rm -rf repo\" &&\n>>> +\tgit init repo &&\n>>> +\t(\n>>> +\t\tcd repo &&\n>>> +\t\ttest_commit first &&\n>>> +\n>>> +\t\twrite_script fake-editor.sh <<-\\EOF &&\n>>> +\t\tcp \"$1\" . &&\n>>> +\t\tprintf \"\\namend a comment\\n\" >>\"$1\"\n>>> +\t\tEOF\n>>> +\t\ttest_set_editor \"$(pwd)\"/fake-editor.sh &&\n>>> +\t\tgit history reword HEAD &&\n>>> +\n>>> +\t\tcat >expect <<-EOF &&\n>>> +\t\tfirst\n>>> +\n>>> +\t\t# Please enter the commit message for the reworded changes. Lines starting\n>>> +\t\t# with ${SQ}#${SQ} will be ignored.\n>>> +\t\t# Changes to be committed:\n>>> +\t\t#\tnew file:   first.t\n>>> +\t\t#\n>>> +\t\tEOF\n>>> +\t\ttest_cmp expect COMMIT_EDITMSG &&\n>>> +\n>>> +\t\tcat >expect <<-EOF &&\n>>> +\t\tfirst\n>>> +\n>>> +\t\tamend a comment\n>>> +\n>>> +\t\tEOF\n>>> +\t\tgit log --format=%B >actual &&\n>>> +\t\ttest_cmp expect actual\n>>\n>> We have test_commit_message() to do this which will accept the expected\n>> message on stdin.\n> \n> Ah, indeed.\n> \n>>> +\t)\n>>> +'\n>>> +\n>>> +# For now, git-history(1) does not yet execute any hooks. This is subject to\n>>> +# change in the future, and if it does this test here is expected to start\n>>> +# failing. In other words, this test is not an endorsement of the current\n>>> +# status quo.\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\ttest_commit first &&\n>>> +\t\ttest_commit second &&\n>>> +\t\ttest_commit third &&\n>>> +\n>>> +\t\twrite_script .git/hooks/prepare-commit-msg <<-EOF &&\n>>> +\t\ttouch \"$(pwd)/hooks.log\n>>\n>> This has the same problem of expanding $(pwd) as fake-editor.sh. For\n>> debugging it would be nicer if the hook scripts did\n>>\n>> \techo \"$hook_name\" >>hooks.log\n>>\n>> so we can easily see which hooks are causing the test to fail.\n> \n> I'll rephrain from doing this as it would require `<<-EOF` instead of\n> `<<-\\EOF`.\n> \n>>> +\t\tEOF\n>>> +\t\twrite_script .git/hooks/post-commit <<-EOF &&\n>>> +\t\ttouch \"$(pwd)/hooks.log\n>>> +\t\tEOF\n>>> +\t\twrite_script .git/hooks/post-rewrite <<-EOF &&\n>>> +\t\ttouch \"$(pwd)/hooks.log\n>>> +\t\tEOF\n>>\n>> This is good idea. We should add tests for the \"pre-commit\" and \"commit-msg\"\n>> hooks as well.\n>>\n>> Overall the test coverage looks good, the only thing we might want to add is\n>> a check for the reflog message. Thanks for working on this, I'll try and\n>> look at the rest of the patches sometime this week.\n> \n> Thanks!\n> \n> Patrick\n> \n\n"},{"id":"531956","messageId":"bc01706e-b9cc-4cd4-953a-971a3ccd6c79@gmail.com","threadId":"63986","inReplyTo":"4m6rmefbv4hftclimitz5rp6yapswjtnjsxymrsdkuan4jbg3u@dm5jzdiq5cxz","subject":"Re: [PATCH RFC 00/11] Introduce git-history(1) command for easy history editing","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2025-12-10T09:58:13Z","receivedAt":"2025-12-10T09:58:20Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"Hi Matthias\n\nOn 03/12/2025 18:18, Matthias Beyer wrote:\n> Am Wed, Sep 17, 2025 at 10:12:31PM +0200, schrieb SZEDER Gábor:\n> \n>> Let's suppose I have this piece of history, I'm on 'branch2', and I\n>> drop commit B.  Which commits will be rewritten and which branches\n>> will be repointed?\n>>\n>>     A---B---C---D   branch1\n>>              \\   \\\n>>               \\   E---F   branch2\n>>                \\       \\\n>>                 \\       G---H---I   branch3\n>>                  \\\n>>                   J---K---L   branch4\n>>\n> \n> Just speaking as a user here, but my expectation in this scenario would\n> be that rewriting B would be denied by default here, as branch{1..4}\n> would be rewritten although I am at branch2.\n> \n> In the scenario at hand, I would expect that I can only rewrite G, H, I\n> while on branch 3 and J, K, L while on branch4 (without passing some\n> extra flags for \"yes, please also rewrite the other branches\").\n\nIs that because you have branches that you don't want to rewrite because \nthey've been merged upstream or is there another reason? If we start \nrewriting multiple branches we should probably check that we're not \nrewriting something that has been merged upstream but if I rewrite a \ncommits that's an ancestor of several branches it would be very helpful \nto rewrite them all at the same time to keep them in sync.\n\nThanks\n\nPhillip\n\n"},{"id":"531958","messageId":"paqf2ko6kcm5qdcqxqz57qu6gjw3vf6boabjsryeugfnlzzb7z@4dzqo6jug6l2","threadId":"63986","inReplyTo":"bc01706e-b9cc-4cd4-953a-971a3ccd6c79@gmail.com","subject":"Re: [PATCH RFC 00/11] Introduce git-history(1) command for easy history editing","fromName":"Matthias Beyer","fromEmail":"mail@beyermatthias.de","sentAt":"2025-12-10T10:37:58Z","receivedAt":"2025-12-10T10:38:09Z","isPatch":true,"sender":{"key":"mail@beyermatthias.de","avatar":null},"body":"Am Wed, Dec 10, 2025 at 09:58:13AM +0000, schrieb Phillip Wood:\n> Hi Matthias\n> \n> On 03/12/2025 18:18, Matthias Beyer wrote:\n> > Am Wed, Sep 17, 2025 at 10:12:31PM +0200, schrieb SZEDER Gábor:\n> > \n> > > Let's suppose I have this piece of history, I'm on 'branch2', and I\n> > > drop commit B.  Which commits will be rewritten and which branches\n> > > will be repointed?\n> > > \n> > >     A---B---C---D   branch1\n> > >              \\   \\\n> > >               \\   E---F   branch2\n> > >                \\       \\\n> > >                 \\       G---H---I   branch3\n> > >                  \\\n> > >                   J---K---L   branch4\n> > > \n> > \n> > Just speaking as a user here, but my expectation in this scenario would\n> > be that rewriting B would be denied by default here, as branch{1..4}\n> > would be rewritten although I am at branch2.\n> > \n> > In the scenario at hand, I would expect that I can only rewrite G, H, I\n> > while on branch 3 and J, K, L while on branch4 (without passing some\n> > extra flags for \"yes, please also rewrite the other branches\").\n> \n> Is that because you have branches that you don't want to rewrite because\n> they've been merged upstream or is there another reason? If we start\n> rewriting multiple branches we should probably check that we're not\n> rewriting something that has been merged upstream but if I rewrite a commits\n> that's an ancestor of several branches it would be very helpful to rewrite\n> them all at the same time to keep them in sync.\n\nIts mostly because I don't like too much magic and because I think being\nexplicit is always better than not.\n\nSo from my POV, I would expect \"the simple case\" to be \"the simple CLI\ncall\" and if I want the tool to do magic and \"rewrite all the\nthings\"^tm, that I would need to specify a flag for that.\n\nBest,\nMatthias\n"},{"id":"531960","messageId":"0ce5a2c6-cdeb-4897-9b64-39292d1b734f@gmail.com","threadId":"63986","inReplyTo":"CANiSa6hxjghKQMhURx8qC2t=+1gEE7p8YaHbWkg3rYOYa=poVg@mail.gmail.com","subject":"Re: [PATCH v6 00/11] Introduce git-history(1) command for easy history editing","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2025-12-10T11:32:09Z","receivedAt":"2025-12-10T11:32:16Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"On 09/12/2025 17:43, Martin von Zweigbergk wrote:\n> On Mon, Dec 8, 2025 at 11:53 PM Patrick Steinhardt <ps@pks.im> wrote:\n>> On Fri, Dec 05, 2025 at 12:49:04AM -0800, Elijah Newren wrote:\n>>>\n>>> (a) check that a commit is only part of one branch before proceeding,\n>>> (b) always require the user to specify with a flag how to handle\n>>> commits that happen to be part of multiple branches (even when a\n>>> commit only happens to be part of one branch, in order to allow us to\n>>> not bother checking whether it's part of more),\n>>> or\n>>> (c) rewrite all branches that contain the given commit by default\n>>> (with an option to only rewrite the current one).\n>>>\n>>> That said, obviously the choice of whether the series is merged isn't\n>>> up to me.  And maybe I'm in the minority, and others don't care about\n>>> this issue at all.  But it's how I feel about it.\n>>\n>> I guess it's a matter of workflows and tastes, and there's never going\n>> to be the one correct way of doing things. I don't think (b) is a good\n>> option as it makes things more complex even for the simplest cases. But\n>> I wouldn't be opposed to a combination of (a) and (b) if we can\n>> implement (a) efficiently.\n> \n> FWIW, I agree with Elijah that (c) is the right end state.\n\nFWIW I think so too, but there does seem to be a significant number of \npeople who find that behavior surprising so maybe we need a flag to \ncontrol what gets rewritten.\n\nThanks\n\nPhillip\n\n> That's\n> perhaps not surprising given that that's what jj has been doing for\n> many years (as many of you know already, I'm one of the jj\n> maintainers). I think it's very rare that we hear from users that they\n> want to rewrite a commit and its descendants while leaving some of the\n> descendants in place. We have a `jj duplicate` command they can use,\n> but that won't move any bookmarks (branches) over. So if you have\n> bookmark `foo` with descendant bookmarks `bar` and `baz` and you want\n> to amend `foo` while moving `bar` over and keeping `baz` in place, the\n> simplest way is probably to duplicate all the necessary commit\n> (something like `jj duplicate -r main..bar`) and then manually move\n> over `foo` and `bar`. That seems like such a rare use case that we\n> haven't had a reason to make it simpler so far.\n> \n> Regarding performance of (a), I would think that walking all\n> branches/tags until you reach the to-be-modified commit (or reach an\n> older generation) can usually be done quickly enough, but obviously\n> there are many others on this list who know that better than me :) But\n> I have also heard that some Git repos have tens (hundreds?) of\n> thousands of branches. Hopefully those refs are just on some server\n> where users won't run `git history`.\n> \n>>\n>> Do we already have logic like this in git-replay(1)?\n>>\n>> Patrick\n> \n\n"},{"id":"531961","messageId":"a6efc710-4062-4955-8fe8-bb6d428a44c1@gmail.com","threadId":"63986","inReplyTo":"paqf2ko6kcm5qdcqxqz57qu6gjw3vf6boabjsryeugfnlzzb7z@4dzqo6jug6l2","subject":"Re: [PATCH RFC 00/11] Introduce git-history(1) command for easy history editing","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2025-12-10T11:34:20Z","receivedAt":"2025-12-10T11:34:27Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"On 10/12/2025 10:37, Matthias Beyer wrote:\n> Am Wed, Dec 10, 2025 at 09:58:13AM +0000, schrieb Phillip Wood:\n>> On 03/12/2025 18:18, Matthias Beyer wrote:\n>>> Am Wed, Sep 17, 2025 at 10:12:31PM +0200, schrieb SZEDER Gábor:\n>>>\n>>>> Let's suppose I have this piece of history, I'm on 'branch2', and I\n>>>> drop commit B.  Which commits will be rewritten and which branches\n>>>> will be repointed?\n>>>>\n>>>>      A---B---C---D   branch1\n>>>>               \\   \\\n>>>>                \\   E---F   branch2\n>>>>                 \\       \\\n>>>>                  \\       G---H---I   branch3\n>>>>                   \\\n>>>>                    J---K---L   branch4\n>>>>\n>>>\n>>> Just speaking as a user here, but my expectation in this scenario would\n>>> be that rewriting B would be denied by default here, as branch{1..4}\n>>> would be rewritten although I am at branch2.\n>>>\n>>> In the scenario at hand, I would expect that I can only rewrite G, H, I\n>>> while on branch 3 and J, K, L while on branch4 (without passing some\n>>> extra flags for \"yes, please also rewrite the other branches\").\n>>\n>> Is that because you have branches that you don't want to rewrite because\n>> they've been merged upstream or is there another reason? If we start\n>> rewriting multiple branches we should probably check that we're not\n>> rewriting something that has been merged upstream but if I rewrite a commits\n>> that's an ancestor of several branches it would be very helpful to rewrite\n>> them all at the same time to keep them in sync.\n> \n> Its mostly because I don't like too much magic and because I think being\n> explicit is always better than not.\n> \n> So from my POV, I would expect \"the simple case\" to be \"the simple CLI\n> call\" and if I want the tool to do magic and \"rewrite all the\n> things\"^tm, that I would need to specify a flag for that.\n\nThanks, that's useful to know. I'd assumed rewriting all the branches \ndescended from the rewritten commit was the natural thing do do but \nclearly not everyone thinks it is.\n\nPhillip\n\n"},{"id":"531975","messageId":"xmqqms3qh13e.fsf@gitster.g","threadId":"63986","inReplyTo":"a6efc710-4062-4955-8fe8-bb6d428a44c1@gmail.com","subject":"Re: [PATCH RFC 00/11] Introduce git-history(1) command for easy history editing","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-12-10T14:18:29Z","receivedAt":"2025-12-10T14:18:32Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Phillip Wood <phillip.wood123@gmail.com> writes:\n\n>> Its mostly because I don't like too much magic and because I think being\n>> explicit is always better than not.\n>> \n>> So from my POV, I would expect \"the simple case\" to be \"the simple CLI\n>> call\" and if I want the tool to do magic and \"rewrite all the\n>> things\"^tm, that I would need to specify a flag for that.\n>\n> Thanks, that's useful to know. I'd assumed rewriting all the branches \n> descended from the rewritten commit was the natural thing do do but \n> clearly not everyone thinks it is.\n\nIt probably depends on the way one looks at the tool, as a building\nblock (in which case less magic may be preferrable) or a complete\nsolution for one part of workflow.  I probably fall into former camp\nmore often than other people, but for this particular one, I tend to\nthink it is less confusing if we moved all branch refs away from the\ncommits that are obsoleted by rewriting/replaying.\n"},{"id":"531994","messageId":"CANiSa6hwaQ2zLsvw=uiJNgfVYAVp2RyQtgVeTevZ5NO5p2Xmgg@mail.gmail.com","threadId":"63986","inReplyTo":"paqf2ko6kcm5qdcqxqz57qu6gjw3vf6boabjsryeugfnlzzb7z@4dzqo6jug6l2","subject":"Re: [PATCH RFC 00/11] Introduce git-history(1) command for easy history editing","fromName":"Martin von Zweigbergk","fromEmail":"martinvonz@gmail.com","sentAt":"2025-12-10T16:49:18Z","receivedAt":"2025-12-10T16:49:31Z","isPatch":true,"sender":{"key":"martinvonz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/891642?v=4"},"body":"On Wed, Dec 10, 2025 at 2:38 AM Matthias Beyer <mail@beyermatthias.de> wrote:\n>\n> Am Wed, Dec 10, 2025 at 09:58:13AM +0000, schrieb Phillip Wood:\n> > Hi Matthias\n> >\n> > On 03/12/2025 18:18, Matthias Beyer wrote:\n> > > Am Wed, Sep 17, 2025 at 10:12:31PM +0200, schrieb SZEDER Gábor:\n> > >\n> > > > Let's suppose I have this piece of history, I'm on 'branch2', and I\n> > > > drop commit B.  Which commits will be rewritten and which branches\n> > > > will be repointed?\n> > > >\n> > > >     A---B---C---D   branch1\n> > > >              \\   \\\n> > > >               \\   E---F   branch2\n> > > >                \\       \\\n> > > >                 \\       G---H---I   branch3\n> > > >                  \\\n> > > >                   J---K---L   branch4\n> > > >\n> > >\n> > > Just speaking as a user here, but my expectation in this scenario would\n> > > be that rewriting B would be denied by default here, as branch{1..4}\n> > > would be rewritten although I am at branch2.\n> > >\n> > > In the scenario at hand, I would expect that I can only rewrite G, H, I\n> > > while on branch 3 and J, K, L while on branch4 (without passing some\n> > > extra flags for \"yes, please also rewrite the other branches\").\n> >\n> > Is that because you have branches that you don't want to rewrite because\n> > they've been merged upstream or is there another reason?\n\nI think that's a common reason even if it's not Matthias's reason.\nPerhaps one way of doing it would be to have a configurable set of ref\npatterns that are considered immutable. That's similar to what jj\ndoes, though we use a more general language for selecting revisions\nfor it (https://docs.jj-vcs.dev/latest/config/#set-of-immutable-commits).\nI think that has been well received. As you might expect, the set of\nimmutable revisions are respected by all commands.\n"},{"id":"531998","messageId":"CABPp-BEs_Q5eGYugogm=Msu-acS3uTj5Oo0xTUnWay9OXBKqXg@mail.gmail.com","threadId":"63986","inReplyTo":"CANiSa6hwaQ2zLsvw=uiJNgfVYAVp2RyQtgVeTevZ5NO5p2Xmgg@mail.gmail.com","subject":"Re: [PATCH RFC 00/11] Introduce git-history(1) command for easy history editing","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2025-12-10T18:27:36Z","receivedAt":"2025-12-10T18:27:48Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Wed, Dec 10, 2025 at 8:52 AM Martin von Zweigbergk\n<martinvonz@gmail.com> wrote:\n>\n> On Wed, Dec 10, 2025 at 2:38 AM Matthias Beyer <mail@beyermatthias.de> wrote:\n> >\n> > Am Wed, Dec 10, 2025 at 09:58:13AM +0000, schrieb Phillip Wood:\n> > > Hi Matthias\n> > >\n> > > On 03/12/2025 18:18, Matthias Beyer wrote:\n> > > > Am Wed, Sep 17, 2025 at 10:12:31PM +0200, schrieb SZEDER Gábor:\n> > > >\n> > > > > Let's suppose I have this piece of history, I'm on 'branch2', and I\n> > > > > drop commit B.  Which commits will be rewritten and which branches\n> > > > > will be repointed?\n> > > > >\n> > > > >     A---B---C---D   branch1\n> > > > >              \\   \\\n> > > > >               \\   E---F   branch2\n> > > > >                \\       \\\n> > > > >                 \\       G---H---I   branch3\n> > > > >                  \\\n> > > > >                   J---K---L   branch4\n> > > > >\n> > > >\n> > > > Just speaking as a user here, but my expectation in this scenario would\n> > > > be that rewriting B would be denied by default here, as branch{1..4}\n> > > > would be rewritten although I am at branch2.\n> > > >\n> > > > In the scenario at hand, I would expect that I can only rewrite G, H, I\n> > > > while on branch 3 and J, K, L while on branch4 (without passing some\n> > > > extra flags for \"yes, please also rewrite the other branches\").\n> > >\n> > > Is that because you have branches that you don't want to rewrite because\n> > > they've been merged upstream or is there another reason?\n>\n> I think that's a common reason even if it's not Matthias's reason.\n> Perhaps one way of doing it would be to have a configurable set of ref\n> patterns that are considered immutable. That's similar to what jj\n> does, though we use a more general language for selecting revisions\n> for it (https://docs.jj-vcs.dev/latest/config/#set-of-immutable-commits).\n> I think that has been well received. As you might expect, the set of\n> immutable revisions are respected by all commands.\n\nI like the idea of a set of immutable revisions...but wouldn't that\nresult in the request to drop commit B in the graph above being met\nwith an error rather than with a single branch being rewritten?\n"},{"id":"531999","messageId":"CANiSa6gxA5SVBALvkYzpMJUrHTW8OJ+fFLkAmM53fJ1GdbUsbQ@mail.gmail.com","threadId":"63986","inReplyTo":"CABPp-BEs_Q5eGYugogm=Msu-acS3uTj5Oo0xTUnWay9OXBKqXg@mail.gmail.com","subject":"Re: [PATCH RFC 00/11] Introduce git-history(1) command for easy history editing","fromName":"Martin von Zweigbergk","fromEmail":"martinvonz@gmail.com","sentAt":"2025-12-10T18:45:11Z","receivedAt":"2025-12-10T18:45:23Z","isPatch":true,"sender":{"key":"martinvonz@gmail.com","avatar":"https://avatars.githubusercontent.com/u/891642?v=4"},"body":"On Wed, Dec 10, 2025 at 10:27 AM Elijah Newren <newren@gmail.com> wrote:\n>\n> On Wed, Dec 10, 2025 at 8:52 AM Martin von Zweigbergk\n> <martinvonz@gmail.com> wrote:\n> >\n> > On Wed, Dec 10, 2025 at 2:38 AM Matthias Beyer <mail@beyermatthias.de> wrote:\n> > >\n> > > Am Wed, Dec 10, 2025 at 09:58:13AM +0000, schrieb Phillip Wood:\n> > > > Hi Matthias\n> > > >\n> > > > On 03/12/2025 18:18, Matthias Beyer wrote:\n> > > > > Am Wed, Sep 17, 2025 at 10:12:31PM +0200, schrieb SZEDER Gábor:\n> > > > >\n> > > > > > Let's suppose I have this piece of history, I'm on 'branch2', and I\n> > > > > > drop commit B.  Which commits will be rewritten and which branches\n> > > > > > will be repointed?\n> > > > > >\n> > > > > >     A---B---C---D   branch1\n> > > > > >              \\   \\\n> > > > > >               \\   E---F   branch2\n> > > > > >                \\       \\\n> > > > > >                 \\       G---H---I   branch3\n> > > > > >                  \\\n> > > > > >                   J---K---L   branch4\n> > > > > >\n> > > > >\n> > > > > Just speaking as a user here, but my expectation in this scenario would\n> > > > > be that rewriting B would be denied by default here, as branch{1..4}\n> > > > > would be rewritten although I am at branch2.\n> > > > >\n> > > > > In the scenario at hand, I would expect that I can only rewrite G, H, I\n> > > > > while on branch 3 and J, K, L while on branch4 (without passing some\n> > > > > extra flags for \"yes, please also rewrite the other branches\").\n> > > >\n> > > > Is that because you have branches that you don't want to rewrite because\n> > > > they've been merged upstream or is there another reason?\n> >\n> > I think that's a common reason even if it's not Matthias's reason.\n> > Perhaps one way of doing it would be to have a configurable set of ref\n> > patterns that are considered immutable. That's similar to what jj\n> > does, though we use a more general language for selecting revisions\n> > for it (https://docs.jj-vcs.dev/latest/config/#set-of-immutable-commits).\n> > I think that has been well received. As you might expect, the set of\n> > immutable revisions are respected by all commands.\n>\n> I like the idea of a set of immutable revisions...but wouldn't that\n> result in the request to drop commit B in the graph above being met\n> with an error rather than with a single branch being rewritten?\n\nIf branch1 (or any of those branches, really) is configured as\nimmutable, then yes. But it's desirable to prevent rewriting or\ndropping B in that case, right?\n"},{"id":"532003","messageId":"CABPp-BGT=wvvg0OYZkMwv+a2QEXrBhRqjDVSJH_nYjEyiPPkaQ@mail.gmail.com","threadId":"63986","inReplyTo":"CANiSa6gxA5SVBALvkYzpMJUrHTW8OJ+fFLkAmM53fJ1GdbUsbQ@mail.gmail.com","subject":"Re: [PATCH RFC 00/11] Introduce git-history(1) command for easy history editing","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2025-12-10T19:55:40Z","receivedAt":"2025-12-10T19:55:53Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Wed, Dec 10, 2025 at 10:45 AM Martin von Zweigbergk\n<martinvonz@gmail.com> wrote:\n>\n> On Wed, Dec 10, 2025 at 10:27 AM Elijah Newren <newren@gmail.com> wrote:\n> >\n> > On Wed, Dec 10, 2025 at 8:52 AM Martin von Zweigbergk\n> > <martinvonz@gmail.com> wrote:\n> > >\n> > > On Wed, Dec 10, 2025 at 2:38 AM Matthias Beyer <mail@beyermatthias.de> wrote:\n> > > >\n> > > > Am Wed, Dec 10, 2025 at 09:58:13AM +0000, schrieb Phillip Wood:\n> > > > > Hi Matthias\n> > > > >\n> > > > > On 03/12/2025 18:18, Matthias Beyer wrote:\n> > > > > > Am Wed, Sep 17, 2025 at 10:12:31PM +0200, schrieb SZEDER Gábor:\n> > > > > >\n> > > > > > > Let's suppose I have this piece of history, I'm on 'branch2', and I\n> > > > > > > drop commit B.  Which commits will be rewritten and which branches\n> > > > > > > will be repointed?\n> > > > > > >\n> > > > > > >     A---B---C---D   branch1\n> > > > > > >              \\   \\\n> > > > > > >               \\   E---F   branch2\n> > > > > > >                \\       \\\n> > > > > > >                 \\       G---H---I   branch3\n> > > > > > >                  \\\n> > > > > > >                   J---K---L   branch4\n> > > > > > >\n> > > > > >\n> > > > > > Just speaking as a user here, but my expectation in this scenario would\n> > > > > > be that rewriting B would be denied by default here, as branch{1..4}\n> > > > > > would be rewritten although I am at branch2.\n> > > > > >\n> > > > > > In the scenario at hand, I would expect that I can only rewrite G, H, I\n> > > > > > while on branch 3 and J, K, L while on branch4 (without passing some\n> > > > > > extra flags for \"yes, please also rewrite the other branches\").\n> > > > >\n> > > > > Is that because you have branches that you don't want to rewrite because\n> > > > > they've been merged upstream or is there another reason?\n> > >\n> > > I think that's a common reason even if it's not Matthias's reason.\n> > > Perhaps one way of doing it would be to have a configurable set of ref\n> > > patterns that are considered immutable. That's similar to what jj\n> > > does, though we use a more general language for selecting revisions\n> > > for it (https://docs.jj-vcs.dev/latest/config/#set-of-immutable-commits).\n> > > I think that has been well received. As you might expect, the set of\n> > > immutable revisions are respected by all commands.\n> >\n> > I like the idea of a set of immutable revisions...but wouldn't that\n> > result in the request to drop commit B in the graph above being met\n> > with an error rather than with a single branch being rewritten?\n>\n> If branch1 (or any of those branches, really) is configured as\n> immutable, then yes. But it's desirable to prevent rewriting or\n> dropping B in that case, right?\n\nYes, I definitely think so.  I was mixing up in my head the different\nrequests and thought that Matthias had asked to rewrite just one\nbranch, which sounded like it wasn't something you'd get through\nimmutable revisions and had me confused why you were suggesting that.\nBut, it was all just me mis-remembering and not re-reading.  Sorry for\nthe mix-up.\n"},{"id":"532093","messageId":"CALnO6CD8stoeUg97bq+PvRdErCoeHPg9PUMMq73RPo-8kdVt8A@mail.gmail.com","threadId":"63986","inReplyTo":"b3ddfaa4-526b-41e3-b12a-0fec846ac7bc@app.fastmail.com","subject":"Working on top of mega merges","fromName":"D. Ben Knoble","fromEmail":"ben.knoble@gmail.com","sentAt":"2025-12-12T22:00:57Z","receivedAt":"2025-12-12T22:01:14Z","isPatch":false,"sender":{"key":"ben.knoble@gmail.com","avatar":"https://avatars.githubusercontent.com/u/22802209?v=4"},"body":"On Tue, Dec 9, 2025 at 1:29 PM Kristoffer Haugsbakk\n<kristofferhaugsbakk@fastmail.com> wrote:\n>\n> On Tue, Dec 2, 2025, at 19:49, Patrick Steinhardt wrote:\n> >>[snip]\n>\n> I haven’t used Jujutsu yet.\n\nSame, however:\n\n> I have read or heard about the “mega merge” strategy in Jujutsu.[1]\n> Being able to (this is how I imagine it could work) make a temporary\n> integration branch where N branches can be edited by making edits to\n> them and having all the branches be updated sounds amazing.[2][3] I have\n> found myself doing temporary integration branches where I make fixes on\n> top and manually cherry-picking them to the correct target afterwards.\n\nI have done something like this once. Created a few independent\nbranches, wrangled an octopus merge together, and then worked on top\nof that. When I wanted to commit, I did \"commit --fixup\" with the\nappropriate branch/commit, then eventually \"rebase --autosquash\n--rebase-merges\" or something. In that particular case, I started with\nempty commits on each branch, I think.\n\nYou could probably shuffle commits (like to add a new one to a sub\nbranch) using \"rebase --interactive…\" there, too, but I don't think I\ntried that.\n\nAnyway, the workflow did its job, but I didn't end up with something I\nthink I'd try in practice very often, at least not yet. It did make me\nwonder what from jj we'd need to make things smoother, though.\n\n-- \nD. Ben Knoble\n"},{"id":"532219","messageId":"efe4cbc5-bad5-4ded-9f65-1326cc102543@app.fastmail.com","threadId":"63986","inReplyTo":"paqf2ko6kcm5qdcqxqz57qu6gjw3vf6boabjsryeugfnlzzb7z@4dzqo6jug6l2","subject":"Re: [PATCH RFC 00/11] Introduce git-history(1) command for easy history editing","fromName":"Kristoffer Haugsbakk","fromEmail":"kristofferhaugsbakk@fastmail.com","sentAt":"2025-12-15T23:50:12Z","receivedAt":"2025-12-15T23:50:34Z","isPatch":true,"sender":{"key":"kristofferhaugsbakk@fastmail.com","avatar":null},"body":"On Wed, Dec 10, 2025, at 11:37, Matthias Beyer wrote:\n>>[snip]\n>>\n>> Is that because you have branches that you don't want to rewrite because\n>> they've been merged upstream or is there another reason? If we start\n>> rewriting multiple branches we should probably check that we're not\n>> rewriting something that has been merged upstream but if I rewrite a commits\n>> that's an ancestor of several branches it would be very helpful to rewrite\n>> them all at the same time to keep them in sync.\n>\n> Its mostly because I don't like too much magic and because I think being\n> explicit is always better than not.\n\nThat the first thing here is not magic but the other thing is seems\narbitrary:\n\n1. Change these commits, i.e. make new commits and all their descendants\n2. Update all branches that point at the commits that have now been\n   replaced\n\nThese two feel conceptually similar to me in terms of complexity, and\nneither of them are magical.\n\n>\n> So from my POV, I would expect \"the simple case\" to be \"the simple CLI\n> call\" and if I want the tool to do magic and \"rewrite all the\n> things\"^tm, that I would need to specify a flag for that.\n"},{"id":"532541","messageId":"aUVDax0PbkaXGB61@pks.im","threadId":"63986","inReplyTo":"xmqqms3qh13e.fsf@gitster.g","subject":"Re: [PATCH RFC 00/11] Introduce git-history(1) command for easy history editing","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-12-19T12:22:03Z","receivedAt":"2025-12-19T12:22:14Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Wed, Dec 10, 2025 at 11:18:29PM +0900, Junio C Hamano wrote:\n> Phillip Wood <phillip.wood123@gmail.com> writes:\n> \n> >> Its mostly because I don't like too much magic and because I think being\n> >> explicit is always better than not.\n> >> \n> >> So from my POV, I would expect \"the simple case\" to be \"the simple CLI\n> >> call\" and if I want the tool to do magic and \"rewrite all the\n> >> things\"^tm, that I would need to specify a flag for that.\n> >\n> > Thanks, that's useful to know. I'd assumed rewriting all the branches \n> > descended from the rewritten commit was the natural thing do do but \n> > clearly not everyone thinks it is.\n> \n> It probably depends on the way one looks at the tool, as a building\n> block (in which case less magic may be preferrable) or a complete\n> solution for one part of workflow.  I probably fall into former camp\n> more often than other people, but for this particular one, I tend to\n> think it is less confusing if we moved all branch refs away from the\n> commits that are obsoleted by rewriting/replaying.\n\nOkay, so the majority of folks here seem to favor rewriting all\ndependent branches, which is also the default that JJ uses here, and\ngit-replay(1) does it, too.\n\nThere is one major difference between git-replay(1) and git-history(1)\nthough: the former works with revision ranges, whereas the latter does\nnot. By using revision ranges we avoid the problem I have mentioned in a\ndifferent branch of this discussion, which is that we have no easy way\nto figure out which branches we'd have to touch in the first place. This\nis because we simply walk the revision range there and then look at\nwhich of our references point into that range. That's simple enough.\n\nBut in our case we're not working with ranges, we are working with a\nsingular commit. In my head this meant that we'd have to basically do a\nrevision walk that starts from all of our branches so that we can figure\nout which of them would eventually reach the commit that we are about to\nrewrite. And that of course doesn't scale.\n\nNow we could of course also introduce ranges into git-history(1). That\nwould indeed solve the issue, as we can reuse the same architecture as\nwe already have in git-replay(1). But I don't really want to go there as\nit is leaking complexity to the user: they want to rewrite a single\ncommit, why should they have to think about ranges?\n\nBut now that I've thought about the problem a bit I think we can avoid\nthat issue by implicitly identifying the range: it's all the commits\nbetween the commit we're about to rewrite and HEAD. So, same as with\ngit-replay(1), the set of branches that we'd need to rewrite is any one\nbranch that points into that range. It keeps the UI simple as the user\nstill only has to think about a singular commit, should be sufficiently\nfast to compute in most cases, and it allows mega-merge workflows like\nJJ supports.\n\nDoes that make sense to everyone? If so, I'll revise my stance and will\nadapt the current implementation to do exactly that.\n\nThanks for the discussion!\n\nPatrick\n"},{"id":"532545","messageId":"aUVMXKT0sqiE8Qx2@pks.im","threadId":"63986","inReplyTo":"48ba9303-45f4-43bf-a257-10d58474096c@gmail.com","subject":"Re: [PATCH v6 11/11] builtin/history: implement \"split\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-12-19T13:00:12Z","receivedAt":"2025-12-19T13:00:20Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Wed, Dec 10, 2025 at 09:51:33AM +0000, Phillip Wood wrote:\n> On 02/12/2025 18:51, Patrick Steinhardt wrote:\n> > On Fri, Nov 21, 2025 at 02:31:14PM +0000, Phillip Wood wrote:\n> > > On 27/10/2025 11:33, Patrick Steinhardt wrote:\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> > > > +\tread_tree_cmd.git_cmd = 1;\n> > > > +\tstrvec_pushf(&read_tree_cmd.env, \"GIT_INDEX_FILE=%s\", index_file.buf);\n> > > > +\tstrvec_push(&read_tree_cmd.args, \"read-tree\");\n> > > > +\tstrvec_push(&read_tree_cmd.args, oid_to_hex(&parent_tree_oid));\n> > > > +\tret = run_command(&read_tree_cmd);\n> > > \n> > > Why do we need to fork \"read-tree\" here rather than call unpack_trees()\n> > > ourselves?\n> > \n> > This is an artifact of how the `run_add_p()` interfaces work. They\n> > unfortunately do not work on top of an in-memory index, but they work on\n> > an on-disk index.\n> \n> Oh I see, but why does that mean we need to fork a subprocess rather than\n> writing the index to disc in this process?\n\nFair indeed. I guess it was laziness because other parts of Git did it\nthe same way. Anyway, let me change this now.\n\nPatrick\n"},{"id":"532548","messageId":"aUVaEPGoOkATQGl3@szeder.dev","threadId":"63986","inReplyTo":"aUVDax0PbkaXGB61@pks.im","subject":"Re: [PATCH RFC 00/11] Introduce git-history(1) command for easy history editing","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2025-12-19T13:58:40Z","receivedAt":"2025-12-19T13:58:43Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"On Fri, Dec 19, 2025 at 01:22:03PM +0100, Patrick Steinhardt wrote:\n> On Wed, Dec 10, 2025 at 11:18:29PM +0900, Junio C Hamano wrote:\n> > Phillip Wood <phillip.wood123@gmail.com> writes:\n> > \n> > >> Its mostly because I don't like too much magic and because I think being\n> > >> explicit is always better than not.\n> > >> \n> > >> So from my POV, I would expect \"the simple case\" to be \"the simple CLI\n> > >> call\" and if I want the tool to do magic and \"rewrite all the\n> > >> things\"^tm, that I would need to specify a flag for that.\n> > >\n> > > Thanks, that's useful to know. I'd assumed rewriting all the branches \n> > > descended from the rewritten commit was the natural thing do do but \n> > > clearly not everyone thinks it is.\n> > \n> > It probably depends on the way one looks at the tool, as a building\n> > block (in which case less magic may be preferrable) or a complete\n> > solution for one part of workflow.  I probably fall into former camp\n> > more often than other people, but for this particular one, I tend to\n> > think it is less confusing if we moved all branch refs away from the\n> > commits that are obsoleted by rewriting/replaying.\n> \n> Okay, so the majority of folks here seem to favor rewriting all\n> dependent branches, which is also the default that JJ uses here, and\n> git-replay(1) does it, too.\n\nI can't find the word \"conflict\" in this subthread, so let me bring\nback that little history snippet from around the beginning of the\nsubthread:\n\n  Let's suppose I have this piece of history, I'm on 'branch2', and I\n  drop commit B.  Which commits will be rewritten and which branches\n  will be repointed?\n  \n     A---B---C---D   branch1\n              \\   \\\n               \\   E---F   branch2\n                \\       \\\n                 \\       G---H---I   branch3\n                  \\\n                   J---K---L   branch4\n\nIf we were to rewrite all dependent branches after dropping commit B,\nthen besides 'branch2' we would rewrite 'branch1', 'branch3' and\n'branch4' as well, right?\n\nNow, let's suppose that dropping B would cause conflicts when\nrewriting commits G, H, I, J, K and L.\n\nWhen does the user have to resolve these conflicts?\n\nIf not right now, then how exactly will those dependent branches be\nrewritten?  (I understand jj can store conflicts and they can be\nresolved later...  But are we there yet?)\n\nIf right now, when 'git history' applies each of those commits, then\nrewriting all dependent branches seems to be a horrible idea.\n\n\n> But now that I've thought about the problem a bit I think we can avoid\n> that issue by implicitly identifying the range: it's all the commits\n> between the commit we're about to rewrite and HEAD. So, same as with\n> git-replay(1),\n\nI think 'git rebase --update-refs' does the same as well.\n\n> the set of branches that we'd need to rewrite is any one\n> branch that points into that range. It keeps the UI simple as the user\n> still only has to think about a singular commit, should be sufficiently\n> fast to compute in most cases, and it allows mega-merge workflows like\n> JJ supports.\n> \n> Does that make sense to everyone? If so, I'll revise my stance and will\n> adapt the current implementation to do exactly that.\n\nI would very much prefer that the tool would only rewrite branches\nthat I explicitly allowed to be rewritten, with an '--all' option that\nwould allow the rewrite of all dependent branches.\n\nOr at the very least there must be an escape hatch.\n\n"},{"id":"532549","messageId":"aUVcowmpU7VMLpE4@pks.im","threadId":"63986","inReplyTo":"aUVaEPGoOkATQGl3@szeder.dev","subject":"Re: [PATCH RFC 00/11] Introduce git-history(1) command for easy history editing","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-12-19T14:09:39Z","receivedAt":"2025-12-19T14:09:47Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Fri, Dec 19, 2025 at 02:58:40PM +0100, SZEDER Gábor wrote:\n> On Fri, Dec 19, 2025 at 01:22:03PM +0100, Patrick Steinhardt wrote:\n> > On Wed, Dec 10, 2025 at 11:18:29PM +0900, Junio C Hamano wrote:\n> > > Phillip Wood <phillip.wood123@gmail.com> writes:\n> > > \n> > > >> Its mostly because I don't like too much magic and because I think being\n> > > >> explicit is always better than not.\n> > > >> \n> > > >> So from my POV, I would expect \"the simple case\" to be \"the simple CLI\n> > > >> call\" and if I want the tool to do magic and \"rewrite all the\n> > > >> things\"^tm, that I would need to specify a flag for that.\n> > > >\n> > > > Thanks, that's useful to know. I'd assumed rewriting all the branches \n> > > > descended from the rewritten commit was the natural thing do do but \n> > > > clearly not everyone thinks it is.\n> > > \n> > > It probably depends on the way one looks at the tool, as a building\n> > > block (in which case less magic may be preferrable) or a complete\n> > > solution for one part of workflow.  I probably fall into former camp\n> > > more often than other people, but for this particular one, I tend to\n> > > think it is less confusing if we moved all branch refs away from the\n> > > commits that are obsoleted by rewriting/replaying.\n> > \n> > Okay, so the majority of folks here seem to favor rewriting all\n> > dependent branches, which is also the default that JJ uses here, and\n> > git-replay(1) does it, too.\n> \n> I can't find the word \"conflict\" in this subthread, so let me bring\n> back that little history snippet from around the beginning of the\n> subthread:\n> \n>   Let's suppose I have this piece of history, I'm on 'branch2', and I\n>   drop commit B.  Which commits will be rewritten and which branches\n>   will be repointed?\n>   \n>      A---B---C---D   branch1\n>               \\   \\\n>                \\   E---F   branch2\n>                 \\       \\\n>                  \\       G---H---I   branch3\n>                   \\\n>                    J---K---L   branch4\n> \n> If we were to rewrite all dependent branches after dropping commit B,\n> then besides 'branch2' we would rewrite 'branch1', 'branch3' and\n> 'branch4' as well, right?\n> \n> Now, let's suppose that dropping B would cause conflicts when\n> rewriting commits G, H, I, J, K and L.\n> \n> When does the user have to resolve these conflicts?\n\nIn the current proposed subcommands there cannot be any conflicts, as\nboth reword and split do not change the resulting trees.\n\n> If not right now, then how exactly will those dependent branches be\n> rewritten?  (I understand jj can store conflicts and they can be\n> resolved later...  But are we there yet?)\n\nNo, we aren't. I think Elijah wants to work on that, and I agree that\nfor a subset of commands it might be a required feature.\n\n> > the set of branches that we'd need to rewrite is any one\n> > branch that points into that range. It keeps the UI simple as the user\n> > still only has to think about a singular commit, should be sufficiently\n> > fast to compute in most cases, and it allows mega-merge workflows like\n> > JJ supports.\n> > \n> > Does that make sense to everyone? If so, I'll revise my stance and will\n> > adapt the current implementation to do exactly that.\n> \n> I would very much prefer that the tool would only rewrite branches\n> that I explicitly allowed to be rewritten, with an '--all' option that\n> would allow the rewrite of all dependent branches.\n> \n> Or at the very least there must be an escape hatch.\n\nI agree a 100% about there being an escape hatch. You really don't want\nto rewrite dependent branches in all cases. I'll include an option that\nallows users to pick what to either:\n\n  - Rewrite all dependent branches.\n\n  - Rewrite only the currently checked-out branch or reference.\n\n  - Only give us what _would_ happen, so basically git-replay(1)'s\n    \"--ref-action=print\" mode.\n\nThanks!\n\nPatrick\n"},{"id":"532559","messageId":"CABPp-BGE1PC0RhpkfABUL74Yade6HkMQd35bv0my9A2+1VY6AA@mail.gmail.com","threadId":"63986","inReplyTo":"aUVDax0PbkaXGB61@pks.im","subject":"Re: [PATCH RFC 00/11] Introduce git-history(1) command for easy history editing","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2025-12-19T16:30:10Z","receivedAt":"2025-12-19T16:30:23Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Fri, Dec 19, 2025 at 4:22 AM Patrick Steinhardt <ps@pks.im> wrote:\n>\n[...]\n> Okay, so the majority of folks here seem to favor rewriting all\n> dependent branches, which is also the default that JJ uses here, and\n> git-replay(1) does it, too.\n>\n> There is one major difference between git-replay(1) and git-history(1)\n> though: the former works with revision ranges, whereas the latter does\n> not. By using revision ranges we avoid the problem I have mentioned in a\n> different branch of this discussion, which is that we have no easy way\n> to figure out which branches we'd have to touch in the first place. This\n> is because we simply walk the revision range there and then look at\n> which of our references point into that range. That's simple enough.\n>\n> But in our case we're not working with ranges, we are working with a\n> singular commit.\n\nI don't understand the distinction at all.  `git replay edit` also\ntook a single commit, and then implemented the obvious (and jj-like)\nbehavior of rewriting all branches that descended from that commit.\n\n> In my head this meant that we'd have to basically do a\n> revision walk that starts from all of our branches so that we can figure\n> out which of them would eventually reach the commit that we are about to\n> rewrite.\n\nYes, and it's only a few lines of code, as I showed earlier.\n\n> And that of course doesn't scale.\n\nThat's quite an assumption about scaling; I don't believe it.  Under\nwhat conditions would this be slow enough for users to notice and be\nbothered?  commit-graphs not enabled + weird local clone with\nthousands of local branches?  Also, isn't jj specifically designed for\nlarge repositories and with scaling in mind, and yet this is their\ndefault behavior?\n\nMore importantly, this is being used to justify a large principle of\nleast astonishment violation (disconnecting branches with shared\nhistory), so we'd not only need to show that walking all branches was\nslower enough for users to notice, but slower enough that the negative\nuser performance experience offsets the negative user experience from\nthe astonishing behavior.  Typically, spending extra cycles to provide\nusers with good warnings/errors is a good use of time, especially when\nit'll take them far longer to discover and recover from negative\nsurprises.\n\n> Now we could of course also introduce ranges into git-history(1). That\n> would indeed solve the issue,\n\nI actually don't follow; how would this help?  I'm not even sure how\nit would make sense; am I missing something?\n\n> as we can reuse the same architecture as\n> we already have in git-replay(1). But I don't really want to go there as\n> it is leaking complexity to the user: they want to rewrite a single\n> commit, why should they have to think about ranges?\n\nI totally agree that they shouldn't have to think about ranges.  They\nrewrite a commit and every branch that descends from it is rewritten\nfor them.  If the commit the user tried to edit was part of an\nimmutable branch (to be implemented later), by default you throw an\nerror.  For the special cases where users do want to disconnect\nconnected histories, you can provide an option for users to specify\nthat they only want the current branch (or only the branches which the\ncurrent branch contains).\n\n> But now that I've thought about the problem a bit I think we can avoid\n> that issue by implicitly identifying the range\n\nYes!  As I've been saying, anything that descends from the commit\nbeing rewritten.\n\n> it's all the commits\n> between the commit we're about to rewrite and HEAD.\n\nHuh?\n\n> So, same as with\n> git-replay(1), the set of branches that we'd need to rewrite is any one\n> branch that points into that range. It keeps the UI simple as the user\n> still only has to think about a singular commit, should be sufficiently\n> fast to compute in most cases, and it allows mega-merge workflows like\n> JJ supports.\n>\n> Does that make sense to everyone? If so, I'll revise my stance and will\n> adapt the current implementation to do exactly that.\n\nNo, it doesn't make any sense to me at all.  It'll avoid the principle\nof least astonishment violation in some cases, but leave it present\nfor others (e.g. other branches which contain this one, or other\nbranches which share the specified commit even if they diverge\nafterwards).  I think we shouldn't have a principle of least\nastonishment violation.  There are three ways to avoid such a\nviolation:\n\n  (1) rewrite all branches (refs/heads/*) that descend from the commit\n(code for this already previously provided and is shorter than the\nexisting code in this series)\n  (2) walk all branches that descend from the commit so we can give\nthe user a warning/error when multiple branches are affected\n  (3) force the user to be explicit about what they want.  Provide a\nset of mutually exclusive flags, and error if none are provided.  One\nflag would be for rewriting all branches, one would be for rewriting\nonly the current branch, and we could add others (e.g. rewriting all\nbranches contained in the current branch).\n\nI don't think retaining this POLA violation makes sense as a starting point.\n"},{"id":"532596","messageId":"CABPp-BG9h1xxp9b2UfE00EaKozk5kGAUELe-Od139amZdAa47g@mail.gmail.com","threadId":"63986","inReplyTo":"CABPp-BGE1PC0RhpkfABUL74Yade6HkMQd35bv0my9A2+1VY6AA@mail.gmail.com","subject":"Re: [PATCH RFC 00/11] Introduce git-history(1) command for easy history editing","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2025-12-20T16:51:23Z","receivedAt":"2025-12-20T16:51:36Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Fri, Dec 19, 2025 at 8:30 AM Elijah Newren <newren@gmail.com> wrote:\n>\n> On Fri, Dec 19, 2025 at 4:22 AM Patrick Steinhardt <ps@pks.im> wrote:\n> >\n> [...]\n> > Okay, so the majority of folks here seem to favor rewriting all\n> > dependent branches, which is also the default that JJ uses here, and\n> > git-replay(1) does it, too.\n> >\n> > There is one major difference between git-replay(1) and git-history(1)\n> > though: the former works with revision ranges, whereas the latter does\n> > not. By using revision ranges we avoid the problem I have mentioned in a\n> > different branch of this discussion, which is that we have no easy way\n> > to figure out which branches we'd have to touch in the first place. This\n> > is because we simply walk the revision range there and then look at\n> > which of our references point into that range. That's simple enough.\n> >\n> > But in our case we're not working with ranges, we are working with a\n> > singular commit.\n>\n> I don't understand the distinction at all.  `git replay edit` also\n> took a single commit, and then implemented the obvious (and jj-like)\n> behavior of rewriting all branches that descended from that commit.\n>\n> > In my head this meant that we'd have to basically do a\n> > revision walk that starts from all of our branches so that we can figure\n> > out which of them would eventually reach the commit that we are about to\n> > rewrite.\n>\n> Yes, and it's only a few lines of code, as I showed earlier.\n>\n> > And that of course doesn't scale.\n>\n> That's quite an assumption about scaling; I don't believe it.  Under\n> what conditions would this be slow enough for users to notice and be\n> bothered?  commit-graphs not enabled + weird local clone with\n> thousands of local branches?  Also, isn't jj specifically designed for\n> large repositories and with scaling in mind, and yet this is their\n> default behavior?\n>\n> More importantly, this is being used to justify a large principle of\n> least astonishment violation (disconnecting branches with shared\n> history), so we'd not only need to show that walking all branches was\n> slower enough for users to notice, but slower enough that the negative\n> user performance experience offsets the negative user experience from\n> the astonishing behavior.  Typically, spending extra cycles to provide\n> users with good warnings/errors is a good use of time, especially when\n> it'll take them far longer to discover and recover from negative\n> surprises.\n\nA quick clarification in case I'm misunderstood above:\n\nWhen I talk about rewriting branches descended from the commit, I am\nspecifically talking about refs/heads/*, not refs/remotes/origin/* or\nrefs/tags/* or anything else.  Because:\n  * I suspect we'll soon implement an \"immutable branches\" concept, so\nthat e.g. a request to modify a commit in the history of \"main\" would\nresult in an error (by default).  This leads to the idea that we're\njust rewriting the user's local stuff they have on top of the\nimmutable history, i.e. their local branches.\n  * refs/tags/* are designed to be immutable, obviously.  In fact, we\nmight want to automatically include tags in the set of \"immutable\nbranches\" by default.\n  * refs/remotes/* are designed to match what the corresponding remote\nhad, not to be independently rewritten.  We'd really mess people up if\nwe changed that.\n  * The fact that users created local branches means they are marking\nthose parts of history as a relevant area of interest\n  * While it could be that some of refs/remotes/origin/* have shared\nhistory with whatever commit is being reworded/split/edited, so too\ncould branches that were never pushed.  I think focusing on the\nbranches of interest to the user (i.e. their local branches) makes\nsense.\n"},{"id":"532615","messageId":"91bd9241-96c1-4b34-98a9-af3bad345c4d@gmail.com","threadId":"63986","inReplyTo":"aUVDax0PbkaXGB61@pks.im","subject":"Re: [PATCH RFC 00/11] Introduce git-history(1) command for easy history editing","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2025-12-22T10:46:12Z","receivedAt":"2025-12-22T10:46:20Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"On 19/12/2025 12:22, Patrick Steinhardt wrote:\n> \n> But in our case we're not working with ranges, we are working with a\n> singular commit. In my head this meant that we'd have to basically do a\n> revision walk that starts from all of our branches so that we can figure\n> out which of them would eventually reach the commit that we are about to\n> rewrite. And that of course doesn't scale.\n\nI'm not so sure about that. In repositories with lots of refs most of \nthem are likely to be tags or remote tracking branches rather than local \nbranches so I'd hope that the number of refs we have to walk was \nmanageable. I'd also expect the commit we're rewriting to be relatively \nrecent so the revision walk should quickly prune any branches that point \nto commits older than the one we're rewriting which should further \nreduce the number of commits we need to walk.\n\n> But now that I've thought about the problem a bit I think we can avoid\n> that issue by implicitly identifying the range: it's all the commits\n> between the commit we're about to rewrite and HEAD. So, same as with\n> git-replay(1), the set of branches that we'd need to rewrite is any one\n> branch that points into that range. It keeps the UI simple as the user\n> still only has to think about a singular commit, should be sufficiently\n> fast to compute in most cases, and it allows mega-merge workflows like\n> JJ supports.\n\nI agree that users should not have to think about commit ranges, but \nusing an implicit range between the commit we're about to rewrite and \nHEAD will not rewrite all the branches descended from that commit, \ninstead it will behave like \"git rebase --update-refs\".\n\n> Does that make sense to everyone? If so, I'll revise my stance and will\n> adapt the current implementation to do exactly that.\n\nI'd much rather rewrite all the branches descended from the commit we're \nabout the rewrite rather than those that happen to point into the \nrevision range between that commit and HEAD as I think that ends up \nbeing confusing.\n\nThanks\n\nPhillip\n"},{"id":"532618","messageId":"CALnO6CDCRgpgsU1W38NDXe=Gzk9qpTfdTSHXf3TXVH95CmxrtQ@mail.gmail.com","threadId":"63986","inReplyTo":"aUVDax0PbkaXGB61@pks.im","subject":"Re: [PATCH RFC 00/11] Introduce git-history(1) command for easy history editing","fromName":"D. Ben Knoble","fromEmail":"ben.knoble@gmail.com","sentAt":"2025-12-22T13:47:38Z","receivedAt":"2025-12-22T13:47:50Z","isPatch":true,"sender":{"key":"ben.knoble@gmail.com","avatar":"https://avatars.githubusercontent.com/u/22802209?v=4"},"body":"[resend] I originally wrote this before some of the fruitful\nconversation replying to this message, so grain of salt. I think my\nquestions have been answered in terms of how --update-refs behaves,\netc.\n\nI still don't think we lose anything by not deviating from other\ncommands now, but I also agree (perhaps to come later, actually using\nthe experimental status to break things?) that I don't want to have to\nremember to rebase descendants myself.\n\nOn Fri, Dec 19, 2025 at 7:48 AM Patrick Steinhardt <ps@pks.im> wrote:\n>\n> On Wed, Dec 10, 2025 at 11:18:29PM +0900, Junio C Hamano wrote:\n> > Phillip Wood <phillip.wood123@gmail.com> writes:\n> >\n> > >> Its mostly because I don't like too much magic and because I think being\n> > >> explicit is always better than not.\n> > >>\n> > >> So from my POV, I would expect \"the simple case\" to be \"the simple CLI\n> > >> call\" and if I want the tool to do magic and \"rewrite all the\n> > >> things\"^tm, that I would need to specify a flag for that.\n> > >\n> > > Thanks, that's useful to know. I'd assumed rewriting all the branches\n> > > descended from the rewritten commit was the natural thing do do but\n> > > clearly not everyone thinks it is.\n> >\n> > It probably depends on the way one looks at the tool, as a building\n> > block (in which case less magic may be preferrable) or a complete\n> > solution for one part of workflow.  I probably fall into former camp\n> > more often than other people, but for this particular one, I tend to\n> > think it is less confusing if we moved all branch refs away from the\n> > commits that are obsoleted by rewriting/replaying.\n>\n> Okay, so the majority of folks here seem to favor rewriting all\n> dependent branches, which is also the default that JJ uses here, and\n> git-replay(1) does it, too.\n>\n> There is one major difference between git-replay(1) and git-history(1)\n> though: the former works with revision ranges, whereas the latter does\n> not. By using revision ranges we avoid the problem I have mentioned in a\n> different branch of this discussion, which is that we have no easy way\n> to figure out which branches we'd have to touch in the first place. This\n> is because we simply walk the revision range there and then look at\n> which of our references point into that range. That's simple enough.\n>\n> But in our case we're not working with ranges, we are working with a\n> singular commit. In my head this meant that we'd have to basically do a\n> revision walk that starts from all of our branches so that we can figure\n> out which of them would eventually reach the commit that we are about to\n> rewrite. And that of course doesn't scale.\n>\n> Now we could of course also introduce ranges into git-history(1). That\n> would indeed solve the issue, as we can reuse the same architecture as\n> we already have in git-replay(1). But I don't really want to go there as\n> it is leaking complexity to the user: they want to rewrite a single\n> commit, why should they have to think about ranges?\n>\n> But now that I've thought about the problem a bit I think we can avoid\n> that issue by implicitly identifying the range: it's all the commits\n> between the commit we're about to rewrite and HEAD. So, same as with\n> git-replay(1), the set of branches that we'd need to rewrite is any one\n> branch that points into that range. It keeps the UI simple as the user\n> still only has to think about a singular commit, should be sufficiently\n> fast to compute in most cases, and it allows mega-merge workflows like\n> JJ supports.\n>\n> Does that make sense to everyone? If so, I'll revise my stance and will\n> adapt the current implementation to do exactly that.\n>\n> Thanks for the discussion!\n>\n> Patrick\n>\n\nMakes sense to me, and is easily explainable.\n\nOne thing that I think JJ handles and which it sounds like replay,\nhistory do not (I’m not sure about rebase with update-refs): stacked\nbranches that point to a chain of commits reaching into the range, but\nwhose reference is still outside it. For example:\n\nA <- B <- C\n\nIf branchB points at B and similar for branchC, and branchB is HEAD,\nthen “replay <stuff> A..B“ and “history <cmd> <A|B>” sound like they\nwould leave branchC alone? That means I have to remember to do\nsomething like “rebase --onto=branchB branchB@{1} branchC”, and if I\nforget I usually have to later replace branchB@{1} with branchC~<n>.\n\nOTOH, it means branchC serves as an additional backup of the original\nbranchB pre-rewrite :)\n\nAnyway. If the other commands don’t support it yet, I don’t think we\nlose anything by not rewriting descendants. But something to consider\nin terms of workflow.\n\n-- \nD. Ben Knoble\n"},{"id":"532624","messageId":"247aa9d7-07f4-4163-bb20-0dad2852ce86@app.fastmail.com","threadId":"63986","inReplyTo":"20251203-b4-pks-history-builtin-v7-4-9e9f849bfd0e@pks.im","subject":"Re: [PATCH v7 04/12] builtin: add new \"history\" command","fromName":"Kristoffer Haugsbakk","fromEmail":"kristofferhaugsbakk@fastmail.com","sentAt":"2025-12-22T17:11:38Z","receivedAt":"2025-12-22T17:12:00Z","isPatch":true,"sender":{"key":"kristofferhaugsbakk@fastmail.com","avatar":null},"body":"On Wed, Dec 3, 2025, at 11:48, Patrick Steinhardt wrote:\n> When rewriting history via git-rebase(1) there are a few very common use\n> cases:\n>[snip]\n> +CONFIGURATION\n> +-------------\n> +\n> +include::includes/cmd-config-section-all.adoc[]\n\nNow there are no config variables listed after\n`include::config/sequencer.adoc[]` was removed.\n\n> +\n> +GIT\n> +---\n> +Part of the linkgit:git[1] suite\n>[snip]\n"},{"id":"533147","messageId":"aV0svlRpEZPoJmkO@pks.im","threadId":"63986","inReplyTo":"CABPp-BG9h1xxp9b2UfE00EaKozk5kGAUELe-Od139amZdAa47g@mail.gmail.com","subject":"Re: [PATCH RFC 00/11] Introduce git-history(1) command for easy history editing","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-01-06T15:39:42Z","receivedAt":"2026-01-06T15:39:54Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Sat, Dec 20, 2025 at 08:51:23AM -0800, Elijah Newren wrote:\n> On Fri, Dec 19, 2025 at 8:30 AM Elijah Newren <newren@gmail.com> wrote:\n> >\n> > On Fri, Dec 19, 2025 at 4:22 AM Patrick Steinhardt <ps@pks.im> wrote:\n> > >\n> > [...]\n> > > Okay, so the majority of folks here seem to favor rewriting all\n> > > dependent branches, which is also the default that JJ uses here, and\n> > > git-replay(1) does it, too.\n> > >\n> > > There is one major difference between git-replay(1) and git-history(1)\n> > > though: the former works with revision ranges, whereas the latter does\n> > > not. By using revision ranges we avoid the problem I have mentioned in a\n> > > different branch of this discussion, which is that we have no easy way\n> > > to figure out which branches we'd have to touch in the first place. This\n> > > is because we simply walk the revision range there and then look at\n> > > which of our references point into that range. That's simple enough.\n> > >\n> > > But in our case we're not working with ranges, we are working with a\n> > > singular commit.\n> >\n> > I don't understand the distinction at all.  `git replay edit` also\n> > took a single commit, and then implemented the obvious (and jj-like)\n> > behavior of rewriting all branches that descended from that commit.\n> >\n> > > In my head this meant that we'd have to basically do a\n> > > revision walk that starts from all of our branches so that we can figure\n> > > out which of them would eventually reach the commit that we are about to\n> > > rewrite.\n> >\n> > Yes, and it's only a few lines of code, as I showed earlier.\n> >\n> > > And that of course doesn't scale.\n> >\n> > That's quite an assumption about scaling; I don't believe it.  Under\n> > what conditions would this be slow enough for users to notice and be\n> > bothered?  commit-graphs not enabled + weird local clone with\n> > thousands of local branches?  Also, isn't jj specifically designed for\n> > large repositories and with scaling in mind, and yet this is their\n> > default behavior?\n> >\n> > More importantly, this is being used to justify a large principle of\n> > least astonishment violation (disconnecting branches with shared\n> > history), so we'd not only need to show that walking all branches was\n> > slower enough for users to notice, but slower enough that the negative\n> > user performance experience offsets the negative user experience from\n> > the astonishing behavior.  Typically, spending extra cycles to provide\n> > users with good warnings/errors is a good use of time, especially when\n> > it'll take them far longer to discover and recover from negative\n> > surprises.\n> \n> A quick clarification in case I'm misunderstood above:\n> \n> When I talk about rewriting branches descended from the commit, I am\n> specifically talking about refs/heads/*, not refs/remotes/origin/* or\n> refs/tags/* or anything else.  Because:\n>   * I suspect we'll soon implement an \"immutable branches\" concept, so\n> that e.g. a request to modify a commit in the history of \"main\" would\n> result in an error (by default).  This leads to the idea that we're\n> just rewriting the user's local stuff they have on top of the\n> immutable history, i.e. their local branches.\n>   * refs/tags/* are designed to be immutable, obviously.  In fact, we\n> might want to automatically include tags in the set of \"immutable\n> branches\" by default.\n>   * refs/remotes/* are designed to match what the corresponding remote\n> had, not to be independently rewritten.  We'd really mess people up if\n> we changed that.\n>   * The fact that users created local branches means they are marking\n> those parts of history as a relevant area of interest\n>   * While it could be that some of refs/remotes/origin/* have shared\n> history with whatever commit is being reworded/split/edited, so too\n> could branches that were never pushed.  I think focusing on the\n> branches of interest to the user (i.e. their local branches) makes\n> sense.\n\nFair. I'll send a new revision tomorrow that does this. Thanks!\n\nPatrick\n"},{"id":"533148","messageId":"aV0s6cFwPFQ-Pmhj@pks.im","threadId":"63986","inReplyTo":"91bd9241-96c1-4b34-98a9-af3bad345c4d@gmail.com","subject":"Re: [PATCH RFC 00/11] Introduce git-history(1) command for easy history editing","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-01-06T15:40:25Z","receivedAt":"2026-01-06T15:40:31Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Mon, Dec 22, 2025 at 10:46:12AM +0000, Phillip Wood wrote:\n> On 19/12/2025 12:22, Patrick Steinhardt wrote:\n> > \n> > But in our case we're not working with ranges, we are working with a\n> > singular commit. In my head this meant that we'd have to basically do a\n> > revision walk that starts from all of our branches so that we can figure\n> > out which of them would eventually reach the commit that we are about to\n> > rewrite. And that of course doesn't scale.\n> \n> I'm not so sure about that. In repositories with lots of refs most of them\n> are likely to be tags or remote tracking branches rather than local branches\n> so I'd hope that the number of refs we have to walk was manageable. I'd also\n> expect the commit we're rewriting to be relatively recent so the revision\n> walk should quickly prune any branches that point to commits older than the\n> one we're rewriting which should further reduce the number of commits we\n> need to walk.\n> \n> > But now that I've thought about the problem a bit I think we can avoid\n> > that issue by implicitly identifying the range: it's all the commits\n> > between the commit we're about to rewrite and HEAD. So, same as with\n> > git-replay(1), the set of branches that we'd need to rewrite is any one\n> > branch that points into that range. It keeps the UI simple as the user\n> > still only has to think about a singular commit, should be sufficiently\n> > fast to compute in most cases, and it allows mega-merge workflows like\n> > JJ supports.\n> \n> I agree that users should not have to think about commit ranges, but using\n> an implicit range between the commit we're about to rewrite and HEAD will\n> not rewrite all the branches descended from that commit, instead it will\n> behave like \"git rebase --update-refs\".\n> \n> > Does that make sense to everyone? If so, I'll revise my stance and will\n> > adapt the current implementation to do exactly that.\n> \n> I'd much rather rewrite all the branches descended from the commit we're\n> about the rewrite rather than those that happen to point into the revision\n> range between that commit and HEAD as I think that ends up being confusing.\n\nYeah, my next version of this patch series will do that now. I'll\nprobably send it out tomorrow.\n\nThanks for your thoughts!\n\nPatrick\n"},{"id":"533149","messageId":"aV0tIc_s_oKwFx7t@pks.im","threadId":"63986","inReplyTo":"247aa9d7-07f4-4163-bb20-0dad2852ce86@app.fastmail.com","subject":"Re: [PATCH v7 04/12] builtin: add new \"history\" command","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-01-06T15:41:21Z","receivedAt":"2026-01-06T15:41:28Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Mon, Dec 22, 2025 at 06:11:38PM +0100, Kristoffer Haugsbakk wrote:\n> On Wed, Dec 3, 2025, at 11:48, Patrick Steinhardt wrote:\n> > When rewriting history via git-rebase(1) there are a few very common use\n> > cases:\n> >[snip]\n> > +CONFIGURATION\n> > +-------------\n> > +\n> > +include::includes/cmd-config-section-all.adoc[]\n> \n> Now there are no config variables listed after\n> `include::config/sequencer.adoc[]` was removed.\n\nOh, indeed, good catch! Will fix.\n\nPatrick\n"},{"id":"533327","messageId":"20260109-b4-pks-history-builtin-v9-0-8766101814c6@pks.im","threadId":"63986","inReplyTo":"20250819-b4-pks-history-builtin-v1-0-9b77c32688fe@pks.im","subject":"[PATCH v9 0/7] Introduce git-history(1) command for easy history editing","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-01-09T08:35:35Z","receivedAt":"2026-01-09T08:35:45Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Hi,\n\nover recent months I've been playing around with Jujutsu quite\nfrequently. While I still prefer using Git, there's been a couple\nfeatures in it that I really like and that I'd like to have in Git, as\nwell.\n\nA copule of these features relate to history editing. Most importantly,\nI really dig the following commands:\n\n  - jj-abandon(1) to drop a specific commit from your history.\n\n  - jj-absorb(1) to take some changes and automatically apply them to\n    commits in your history that last modified the respective hunks.\n\n  - jj-split(1) to split a commit into two.\n\n  - jj-new(1) to insert a new commit after or before a specific other\n    commit.\n\nNot all of these commands can be ported directly into Git. jj-new(1) for\nexample doesn't really make a ton of sense for us, I'd claim. But some\nof these commands _do_ make sense.\n\nThis patch series is a starting point for such a command. For now, it\nonly implements rewording of commits. I'll continue introducing new\ncommands once this patch series has been merged though.\n\nChanges in v9:\n  - Rename `struct replay_ref_updates` to `struct replay_result` to make\n    its semantics less focussed on ref updates, only.\n  - Clarify and fix return codes of git-replay(1) so that we return 1 on\n    conflict, 128 on an error and 0 on success.\n  - The usual small improvements to commit messages.\n  - Link to v8: https://lore.kernel.org/r/20260107-b4-pks-history-builtin-v8-0-18e9779e3a26@pks.im\n\nChanges in v8:\n  - The series is now built on top of e0bfec3dfc (The 15th batch,\n    2026-01-06) with kh/replay-invalid-onto-advance at 56b77a687e\n    (t3650: add more regression tests for failure conditions,\n    2026-01-05) merged into it. The latter is expected to hit 'next'\n    soonish.\n  - Another biggish revamp to build almost completely on top of the\n    git-replay(1) logic.\n  - We now advance all branches descending from the rewritten commit by\n    default.\n  - I've dropped the patches introducing `git history split` and will\n    send this as a follow-up patch series once this once has been\n    merged. This was done to focus attention on the underlying mechanics\n    as much as possible (and to keep my own sanity with the frequent\n    revamps).\n  - Link to v7: https://lore.kernel.org/r/20251203-b4-pks-history-builtin-v7-0-9e9f849bfd0e@pks.im\n\nChanges in v7:\n  - I've rebased the patch series on top of f0ef5b6d9b (The fifth batch,\n    2025-11-30) with pw/replay-exclude-gpgsig-fix at 9f3a115087 (replay:\n    do not copy \"gpgsign-sha256\" header, 2025-11-26) merged into it to\n    fix some conflicts.\n  - I refactored a bunch of code to be shared between split and reword.\n  - I am now using \"--ancestry-path\" and \"--reverse\" to enumerate\n    commits.\n  - `git history split` now asks for commit messages for both commits.\n  - `git history split` no longer allows the user to edit hunks so that\n    conflicts become impossible. This may be reintroduced at a later\n    point in time.\n  - A bunch of test improvements.\n  - Several commit message and documentation improvements.\n  - Link to v6: https://lore.kernel.org/r/20251027-b4-pks-history-builtin-v6-0-407dd3f57ad3@pks.im\n\nChanges in v6:\n  - I've rebased the patch series again to pull in the latest updates\n    from sa/replay-atomic-ref-updates and fix conflicts. It is now based\n    on 4e98b730f1 (The twenty-fourth batch, 2025-10-24) with ab661bb1bb\n    (replay: add replay.refAction config option, 2025-10-23) merged into\n    it.\n  - I've dropped the \"-m\" options for now, so commit messages are always\n    asked for via the editor. These can be introduced in a subsequent\n    patch series once discussion around them has settled.\n  - We don't use the merge machinery anymore to pick the commits.\n  - Drop the commit to parse commits in the replay machinery. It didn't\n    seem to be necessary in v5 anymore, and now that we don't use the\n    merge machinery at all we don't ever take that code path in the\n    first place.\n  - Link to v5:\n    https://lore.kernel.org/r/20251021-b4-pks-history-builtin-v5-0-78d23f578fe6@pks.im\n\nChanges in v5:\n  - I've changed the patch series to be based on top of 133d151831 (The\n    twenty-first batch, 2025-10-20) with sa/replay-atomic-ref-updates at\n    a1c22e627e (SQAUASH??? t0450 band-aid, 2025-10-14) merged into it.\n    This is one the one hand to fix a conflict, but also to get some of\n    the CI updates to make GitLab CI work again.\n  - Some slight commit message improvements.\n  - Deduplicate subcommand usage strings by using defines.\n  - Fix the desendancy checks to properly verify that HEAD is a\n    descendant of the commit to be rewritten. Also add some tests for\n    this.\n  - Fix the hint that mentions that lines starting with the comment\n    character will be tripped after having written the commit message.\n  - Move an include to the correct commit.\n  - Link to v4: https://lore.kernel.org/r/20251001-b4-pks-history-builtin-v4-0-8e61ddb86317@pks.im\n\nChanges in v4:\n  - I've rebuilt the patch series. It is now based on 821f583da6 (The\n    thirteenth batcn, 2025-09-29) with sa/replay-atomic-ref-updates\n    at 665c66a743 (replay: make atomic ref updates the default behavior,\n    2025-09-27) merged into it. This should fix all conflicts with seen.\n  - I've reworked this patch series to use the same infra as\n    git-replay(1), as discussed during the Contributor's Summit.\n  - I've slimmed down the patch series to only tackle those commands\n    that cannot result in a conflict to keep it simple. I also learned\n    that Elijah has been working on a \"git replay edit\" command, so I\n    dropped that command so that we can instead use his version.\n  - During the Contributor's Summit we have agreed that for now, we\n    won't care about hook execution just yet. This may be backfilled at\n    a later point in time.\n  - I dropped \"commit.verbose\" handling for now, as my understanding of\n    it was wrong at first. This is something we should backfill.\n  - Link to v3: https://lore.kernel.org/r/20250904-b4-pks-history-builtin-v3-0-509053514755@pks.im\n\nChanges in v3:\n  - Add logic to drive the \"post-rewrite\" hook and add tests to verify\n    that all hooks are executed as expected.\n  - Deduplicate logic to turn a replay action into a todo command.\n  - Move the addition of tests for the top-level git-history(1) command\n    to the correct commit.\n  - Some smaller commit message fixes.\n  - Honor \"commit.verbose\".\n  - Fix copy-paste error with an error message.\n  - Link to v2: https://lore.kernel.org/r/20250824-b4-pks-history-builtin-v2-0-964ac12f65bd@pks.im\n\nChanges in v2:\n  - Add a new \"reword\" subcommand.\n  - List git-history(1) in \"command-list.txt\".\n  - Add some missing error handling.\n  - Simplify calling convention of `apply_commits()` to handle root\n    commits internally instead of requiring every caller to do so.\n  - Add tests to verify that git-history(1) refuses to work with changes\n    in the worktree or index.\n  - Mark git-history(1) as experimental.\n  - Introduce commands to manage interrupted history edits.\n  - A bunch of improvements to the manpage.\n  - Link to v1: https://lore.kernel.org/r/20250819-b4-pks-history-builtin-v1-0-9b77c32688fe@pks.im\n\nThanks!\n\nPatrick\n\n---\nPatrick Steinhardt (7):\n      builtin/replay: extract core logic to replay revisions\n      builtin/replay: move core logic into \"libgit.a\"\n      replay: small set of cleanups\n      replay: yield the object ID of the final rewritten commit\n      wt-status: provide function to expose status for trees\n      builtin: add new \"history\" command\n      builtin/history: implement \"reword\" subcommand\n\n .gitignore                     |   1 +\n Documentation/git-history.adoc |  73 ++++++++\n Documentation/meson.build      |   1 +\n Makefile                       |   2 +\n builtin.h                      |   1 +\n builtin/history.c              | 404 +++++++++++++++++++++++++++++++++++++++++\n builtin/replay.c               | 380 ++++----------------------------------\n command-list.txt               |   1 +\n git.c                          |   1 +\n meson.build                    |   2 +\n replay.c                       | 355 ++++++++++++++++++++++++++++++++++++\n replay.h                       |  80 ++++++++\n t/meson.build                  |   2 +\n t/t3450-history.sh             |  17 ++\n t/t3451-history-reword.sh      | 344 +++++++++++++++++++++++++++++++++++\n wt-status.c                    |  24 +++\n wt-status.h                    |   9 +\n 17 files changed, 1352 insertions(+), 345 deletions(-)\n\nRange-diff versus v8:\n\n1:  4b58ceb517 ! 1:  5f3123028a builtin/replay: extract core logic to replay revisions\n    @@ builtin/replay.c: static enum ref_action_mode get_ref_action_mode(struct reposit\n     +\tint contained;\n     +};\n     +\n    -+struct replay_ref_updates {\n    ++struct replay_result {\n     +\tstruct replay_ref_update {\n     +\t\tchar *refname;\n     +\t\tstruct object_id old_oid;\n     +\t\tstruct object_id new_oid;\n    -+\t} *items;\n    -+\tsize_t nr, alloc;\n    ++\t} *updates;\n    ++\tsize_t updates_nr, updates_alloc;\n    ++\n    ++\tbool merge_conflict;\n     +};\n     +\n    -+static void replay_ref_updates_release(struct replay_ref_updates *updates)\n    ++static void replay_result_release(struct replay_result *result)\n    ++{\n    ++\tfor (size_t i = 0; i < result->updates_nr; i++)\n    ++\t\tfree(result->updates[i].refname);\n    ++\tfree(result->updates);\n    ++}\n    ++\n    ++static void replay_result_queue_update(struct replay_result *result,\n    ++\t\t\t\t       const char *refname,\n    ++\t\t\t\t       const struct object_id *old_oid,\n    ++\t\t\t\t       const struct object_id *new_oid)\n     +{\n    -+\tfor (size_t i = 0; i < updates->nr; i++)\n    -+\t\tfree(updates->items[i].refname);\n    -+\tfree(updates->items);\n    ++\tALLOC_GROW(result->updates, result->updates_nr + 1, result->updates_alloc);\n    ++\tresult->updates[result->updates_nr].refname = xstrdup(refname);\n    ++\tresult->updates[result->updates_nr].old_oid = *old_oid;\n    ++\tresult->updates[result->updates_nr].new_oid = *new_oid;\n    ++\tresult->updates_nr++;\n     +}\n     +\n     +static int replay_revisions(struct repository *repo, struct rev_info *revs,\n     +\t\t\t    struct replay_revisions_options *opts,\n    -+\t\t\t    struct replay_ref_updates *updates)\n    ++\t\t\t    struct replay_result *out)\n     +{\n     +\tkh_oid_map_t *replayed_commits = NULL;\n     +\tstruct strset *update_refs = NULL;\n    @@ builtin/replay.c: static enum ref_action_mode get_ref_action_mode(struct reposit\n     +\t\t\tif (decoration->type == DECORATION_REF_LOCAL &&\n     +\t\t\t    (opts->contained || strset_contains(update_refs,\n     +\t\t\t\t\t\t\t\tdecoration->name))) {\n    -+\t\t\t\tALLOC_GROW(updates->items, updates->nr + 1, updates->alloc);\n    -+\t\t\t\tupdates->items[updates->nr].refname = xstrdup(decoration->name);\n    -+\t\t\t\tupdates->items[updates->nr].old_oid = commit->object.oid;\n    -+\t\t\t\tupdates->items[updates->nr].new_oid = last_commit->object.oid;\n    -+\t\t\t\tupdates->nr++;\n    ++\t\t\t\treplay_result_queue_update(out, decoration->name,\n    ++\t\t\t\t\t\t\t   &commit->object.oid,\n    ++\t\t\t\t\t\t\t   &last_commit->object.oid);\n     +\t\t\t}\n     +\t\t\tdecoration = decoration->next;\n     +\t\t}\n     +\t}\n     +\n     +\tif (!result.clean) {\n    ++\t\tout->merge_conflict = true;\n     +\t\tret = -1;\n     +\t\tgoto out;\n     +\t}\n     +\n     +\t/* In --advance mode, advance the target ref */\n    -+\tif (advance) {\n    -+\t\tALLOC_GROW(updates->items, updates->nr + 1, updates->alloc);\n    -+\t\tupdates->items[updates->nr].refname = xstrdup(advance);\n    -+\t\tupdates->items[updates->nr].old_oid = onto->object.oid;\n    -+\t\tupdates->items[updates->nr].new_oid = last_commit->object.oid;\n    -+\t\tupdates->nr++;\n    -+\t}\n    ++\tif (advance)\n    ++\t\treplay_result_queue_update(out, advance,\n    ++\t\t\t\t\t   &onto->object.oid,\n    ++\t\t\t\t\t   &last_commit->object.oid);\n     +\n     +\tret = 0;\n     +\n    @@ builtin/replay.c: int cmd_replay(int argc,\n     -\tconst char *onto_name = NULL;\n     -\tint contained = 0;\n     +\tstruct replay_revisions_options opts = { 0 };\n    -+\tstruct replay_ref_updates updates = { 0 };\n    ++\tstruct replay_result result = { 0 };\n      \tconst char *ref_action = NULL;\n      \tenum ref_action_mode ref_mode;\n     -\n    @@ builtin/replay.c: int cmd_replay(int argc,\n     -\t\t\t   &onto, &update_refs);\n     -\n     -\t/* FIXME: Should allow replaying commits with the first as a root commit */\n    -+\tret = replay_revisions(repo, &revs, &opts, &updates);\n    ++\tret = replay_revisions(repo, &revs, &opts, &result);\n     +\tif (ret)\n     +\t\tgoto cleanup;\n      \n    @@ builtin/replay.c: int cmd_replay(int argc,\n     -\t\t\t\t      &onto->object.oid,\n     -\t\t\t\t      reflog_msg.buf,\n     -\t\t\t\t      &transaction_err) < 0) {\n    -+\tfor (size_t i = 0; i < updates.nr; i++) {\n    -+\t\tret = handle_ref_update(ref_mode, transaction, updates.items[i].refname,\n    -+\t\t\t\t\t&updates.items[i].new_oid, &updates.items[i].old_oid,\n    ++\tfor (size_t i = 0; i < result.updates_nr; i++) {\n    ++\t\tret = handle_ref_update(ref_mode, transaction, result.updates[i].refname,\n    ++\t\t\t\t\t&result.updates[i].new_oid, &result.updates[i].old_oid,\n     +\t\t\t\t\treflog_msg.buf, &transaction_err);\n     +\t\tif (ret) {\n      \t\t\tret = error(_(\"failed to update ref '%s': %s\"),\n     -\t\t\t\t    advance_name, transaction_err.buf);\n    -+\t\t\t\t    updates.items[i].refname, transaction_err.buf);\n    ++\t\t\t\t    result.updates[i].refname, transaction_err.buf);\n      \t\t\tgoto cleanup;\n      \t\t}\n      \t}\n    @@ builtin/replay.c: int cmd_replay(int argc,\n      cleanup:\n      \tif (transaction)\n      \t\tref_transaction_free(transaction);\n    -+\treplay_ref_updates_release(&updates);\n    ++\treplay_result_release(&result);\n      \tstrbuf_release(&transaction_err);\n      \tstrbuf_release(&reflog_msg);\n      \trelease_revisions(&revs);\n    @@ builtin/replay.c: int cmd_replay(int argc,\n     -\tif (ret < 0)\n     -\t\texit(128);\n     -\treturn ret ? 0 : 1;\n    -+\treturn ret ? 1 : 0;\n    ++\tif (ret) {\n    ++\t\tif (result.merge_conflict)\n    ++\t\t\treturn 1;\n    ++\t\treturn 128;\n    ++\t}\n    ++\n    ++\treturn 0;\n      }\n2:  2b46a91628 ! 2:  fd63cca82f builtin/replay: move core logic into \"libgit.a\"\n    @@ builtin/replay.c: static enum ref_action_mode get_ref_action_mode(struct reposit\n     -\tint contained;\n     -};\n     -\n    --struct replay_ref_updates {\n    +-struct replay_result {\n     -\tstruct replay_ref_update {\n     -\t\tchar *refname;\n     -\t\tstruct object_id old_oid;\n     -\t\tstruct object_id new_oid;\n    --\t} *items;\n    --\tsize_t nr, alloc;\n    +-\t} *updates;\n    +-\tsize_t updates_nr, updates_alloc;\n    +-\n    +-\tbool merge_conflict;\n     -};\n     -\n    --static void replay_ref_updates_release(struct replay_ref_updates *updates)\n    +-static void replay_result_release(struct replay_result *result)\n     -{\n    --\tfor (size_t i = 0; i < updates->nr; i++)\n    --\t\tfree(updates->items[i].refname);\n    --\tfree(updates->items);\n    +-\tfor (size_t i = 0; i < result->updates_nr; i++)\n    +-\t\tfree(result->updates[i].refname);\n    +-\tfree(result->updates);\n    +-}\n    +-\n    +-static void replay_result_queue_update(struct replay_result *result,\n    +-\t\t\t\t       const char *refname,\n    +-\t\t\t\t       const struct object_id *old_oid,\n    +-\t\t\t\t       const struct object_id *new_oid)\n    +-{\n    +-\tALLOC_GROW(result->updates, result->updates_nr + 1, result->updates_alloc);\n    +-\tresult->updates[result->updates_nr].refname = xstrdup(refname);\n    +-\tresult->updates[result->updates_nr].old_oid = *old_oid;\n    +-\tresult->updates[result->updates_nr].new_oid = *new_oid;\n    +-\tresult->updates_nr++;\n     -}\n     -\n     -static int replay_revisions(struct repository *repo, struct rev_info *revs,\n     -\t\t\t    struct replay_revisions_options *opts,\n    --\t\t\t    struct replay_ref_updates *updates)\n    +-\t\t\t    struct replay_result *out)\n     -{\n     -\tkh_oid_map_t *replayed_commits = NULL;\n     -\tstruct strset *update_refs = NULL;\n    @@ builtin/replay.c: static enum ref_action_mode get_ref_action_mode(struct reposit\n     -\t\t\tif (decoration->type == DECORATION_REF_LOCAL &&\n     -\t\t\t    (opts->contained || strset_contains(update_refs,\n     -\t\t\t\t\t\t\t\tdecoration->name))) {\n    --\t\t\t\tALLOC_GROW(updates->items, updates->nr + 1, updates->alloc);\n    --\t\t\t\tupdates->items[updates->nr].refname = xstrdup(decoration->name);\n    --\t\t\t\tupdates->items[updates->nr].old_oid = commit->object.oid;\n    --\t\t\t\tupdates->items[updates->nr].new_oid = last_commit->object.oid;\n    --\t\t\t\tupdates->nr++;\n    +-\t\t\t\treplay_result_queue_update(out, decoration->name,\n    +-\t\t\t\t\t\t\t   &commit->object.oid,\n    +-\t\t\t\t\t\t\t   &last_commit->object.oid);\n     -\t\t\t}\n     -\t\t\tdecoration = decoration->next;\n     -\t\t}\n     -\t}\n     -\n     -\tif (!result.clean) {\n    +-\t\tout->merge_conflict = true;\n     -\t\tret = -1;\n     -\t\tgoto out;\n     -\t}\n     -\n     -\t/* In --advance mode, advance the target ref */\n    --\tif (advance) {\n    --\t\tALLOC_GROW(updates->items, updates->nr + 1, updates->alloc);\n    --\t\tupdates->items[updates->nr].refname = xstrdup(advance);\n    --\t\tupdates->items[updates->nr].old_oid = onto->object.oid;\n    --\t\tupdates->items[updates->nr].new_oid = last_commit->object.oid;\n    --\t\tupdates->nr++;\n    --\t}\n    +-\tif (advance)\n    +-\t\treplay_result_queue_update(out, advance,\n    +-\t\t\t\t\t   &onto->object.oid,\n    +-\t\t\t\t\t   &last_commit->object.oid);\n     -\n     -\tret = 0;\n     -\n    @@ replay.c (new)\n     +\treturn create_commit(repo, result->tree, pickme, replayed_base);\n     +}\n     +\n    -+void replay_ref_updates_release(struct replay_ref_updates *updates)\n    ++void replay_result_release(struct replay_result *result)\n     +{\n    -+\tfor (size_t i = 0; i < updates->nr; i++)\n    -+\t\tfree(updates->items[i].refname);\n    -+\tfree(updates->items);\n    ++\tfor (size_t i = 0; i < result->updates_nr; i++)\n    ++\t\tfree(result->updates[i].refname);\n    ++\tfree(result->updates);\n    ++}\n    ++\n    ++static void replay_result_queue_update(struct replay_result *result,\n    ++\t\t\t\t       const char *refname,\n    ++\t\t\t\t       const struct object_id *old_oid,\n    ++\t\t\t\t       const struct object_id *new_oid)\n    ++{\n    ++\tALLOC_GROW(result->updates, result->updates_nr + 1, result->updates_alloc);\n    ++\tresult->updates[result->updates_nr].refname = xstrdup(refname);\n    ++\tresult->updates[result->updates_nr].old_oid = *old_oid;\n    ++\tresult->updates[result->updates_nr].new_oid = *new_oid;\n    ++\tresult->updates_nr++;\n     +}\n     +\n     +int replay_revisions(struct repository *repo, struct rev_info *revs,\n     +\t\t     struct replay_revisions_options *opts,\n    -+\t\t     struct replay_ref_updates *updates)\n    ++\t\t     struct replay_result *out)\n     +{\n     +\tkh_oid_map_t *replayed_commits = NULL;\n     +\tstruct strset *update_refs = NULL;\n    @@ replay.c (new)\n     +\t\t\tif (decoration->type == DECORATION_REF_LOCAL &&\n     +\t\t\t    (opts->contained || strset_contains(update_refs,\n     +\t\t\t\t\t\t\t\tdecoration->name))) {\n    -+\t\t\t\tALLOC_GROW(updates->items, updates->nr + 1, updates->alloc);\n    -+\t\t\t\tupdates->items[updates->nr].refname = xstrdup(decoration->name);\n    -+\t\t\t\tupdates->items[updates->nr].old_oid = commit->object.oid;\n    -+\t\t\t\tupdates->items[updates->nr].new_oid = last_commit->object.oid;\n    -+\t\t\t\tupdates->nr++;\n    ++\t\t\t\treplay_result_queue_update(out, decoration->name,\n    ++\t\t\t\t\t\t\t   &commit->object.oid,\n    ++\t\t\t\t\t\t\t   &last_commit->object.oid);\n     +\t\t\t}\n     +\t\t\tdecoration = decoration->next;\n     +\t\t}\n     +\t}\n     +\n     +\tif (!result.clean) {\n    ++\t\tout->merge_conflict = true;\n     +\t\tret = -1;\n     +\t\tgoto out;\n     +\t}\n     +\n     +\t/* In --advance mode, advance the target ref */\n    -+\tif (advance) {\n    -+\t\tALLOC_GROW(updates->items, updates->nr + 1, updates->alloc);\n    -+\t\tupdates->items[updates->nr].refname = xstrdup(advance);\n    -+\t\tupdates->items[updates->nr].old_oid = onto->object.oid;\n    -+\t\tupdates->items[updates->nr].new_oid = last_commit->object.oid;\n    -+\t\tupdates->nr++;\n    -+\t}\n    ++\tif (advance)\n    ++\t\treplay_result_queue_update(out, advance,\n    ++\t\t\t\t\t   &onto->object.oid,\n    ++\t\t\t\t\t   &last_commit->object.oid);\n     +\n     +\tret = 0;\n     +\n    @@ replay.h (new)\n     +};\n     +\n     +/* This struct is used as an out-parameter by `replay_revisions()`. */\n    -+struct replay_ref_updates {\n    ++struct replay_result {\n     +\t/*\n     +\t * The set of reference updates that are caused by replaying the\n     +\t * commits.\n    @@ replay.h (new)\n     +\t\tchar *refname;\n     +\t\tstruct object_id old_oid;\n     +\t\tstruct object_id new_oid;\n    -+\t} *items;\n    -+\tsize_t nr, alloc;\n    ++\t} *updates;\n    ++\tsize_t updates_nr, updates_alloc;\n    ++\n    ++\t/* Set to true in case the replay failed with a merge conflict. */\n    ++\tbool merge_conflict;\n     +};\n     +\n    -+void replay_ref_updates_release(struct replay_ref_updates *updates);\n    ++void replay_result_release(struct replay_result *result);\n     +\n     +/*\n     + * Replay a set of commits onto a new location. Leaves both the working tree,\n    @@ replay.h (new)\n     + */\n     +int replay_revisions(struct repository *repo, struct rev_info *revs,\n     +\t\t     struct replay_revisions_options *opts,\n    -+\t\t     struct replay_ref_updates *updates);\n    ++\t\t     struct replay_result *out);\n     +\n     +#endif\n3:  881988bcfa = 3:  eb455cade1 replay: small set of cleanups\n4:  197fa292c4 ! 4:  ccd323f6db replay: yield the object ID of the final rewritten commit\n    @@ Commit message\n     \n      ## replay.c ##\n     @@ replay.c: int replay_revisions(struct repository *repo, struct rev_info *revs,\n    - \t\tupdates->nr++;\n    - \t}\n    + \t\t\t\t\t   &onto->object.oid,\n    + \t\t\t\t\t   &last_commit->object.oid);\n      \n    -+\tupdates->final_oid = last_commit->object.oid;\n    ++\tout->final_oid = last_commit->object.oid;\n     +\n      \tret = 0;\n      \n      out:\n     \n      ## replay.h ##\n    -@@ replay.h: struct replay_ref_updates {\n    - \t\tstruct object_id new_oid;\n    - \t} *items;\n    - \tsize_t nr, alloc;\n    +@@ replay.h: struct replay_result {\n    + \n    + \t/* Set to true in case the replay failed with a merge conflict. */\n    + \tbool merge_conflict;\n     +\n     +\t/*\n     +\t * The final object ID that was rewritten. Note that this field has\n    @@ replay.h: struct replay_ref_updates {\n     +\tstruct object_id final_oid;\n      };\n      \n    - void replay_ref_updates_release(struct replay_ref_updates *updates);\n    + void replay_result_release(struct replay_result *result);\n5:  91df8f3813 = 5:  a40034bcc1 wt-status: provide function to expose status for trees\n6:  025e13874e ! 6:  4d566d6c43 builtin: add new \"history\" command\n    @@ Commit message\n         splitting up a commit into two are way more involved than that and\n         require a whole series of commands.\n     \n    -    Another problem that rebases have is that dependent branches are not\n    -    being updated. The use of stacked branches has grown quite common with\n    -    competiting version control systems like Jujutsu though, so it clearly\n    -    is a need that users have. While rebases _can_ serve this use case if\n    -    one always works on the latest stacked branch, it is somewhat awkward\n    -    and very easy to get wrong.\n    +    Rebases also do not update dependent branches. The use of stacked\n    +    branches has grown quite common with competing version control systems\n    +    like Jujutsu though, so it clearly is a need that users have. While\n    +    rebases _can_ serve this use case if one always works on the latest\n    +    stacked branch, it is somewhat awkward and very easy to get wrong.\n     \n         Add a new \"history\" command to plug these gaps. This command will have\n         several different subcommands to imperatively rewrite history for common\n7:  b5cfe7da4e ! 7:  666fec826c builtin/history: implement \"reword\" subcommand\n    @@ builtin/history.c\n     +{\n     +\tconst struct name_decoration *decoration;\n     +\tstruct replay_revisions_options opts = { 0 };\n    -+\tstruct replay_ref_updates updates = {\n    ++\tstruct replay_result result = {\n     +\t\t.final_oid = rewritten->object.oid,\n     +\t};\n     +\tstruct ref_transaction *transaction = NULL;\n    @@ builtin/history.c\n     +\n     +\topts.onto = oid_to_hex_r(hex, &rewritten->object.oid);\n     +\n    -+\tret = replay_revisions(repo, &revs, &opts, &updates);\n    ++\tret = replay_revisions(repo, &revs, &opts, &result);\n     +\tif (ret)\n     +\t\tgoto out;\n     +\n    @@ builtin/history.c\n     +\t\t\tgoto out;\n     +\t\t}\n     +\n    -+\t\tfor (size_t i = 0; i < updates.nr; i++) {\n    ++\t\tfor (size_t i = 0; i < result.updates_nr; i++) {\n     +\t\t\tret = ref_transaction_update(transaction,\n    -+\t\t\t\t\t\t     updates.items[i].refname,\n    -+\t\t\t\t\t\t     &updates.items[i].new_oid,\n    -+\t\t\t\t\t\t     &updates.items[i].old_oid,\n    ++\t\t\t\t\t\t     result.updates[i].refname,\n    ++\t\t\t\t\t\t     &result.updates[i].new_oid,\n    ++\t\t\t\t\t\t     &result.updates[i].old_oid,\n     +\t\t\t\t\t\t     NULL, NULL, 0, reflog_msg, &err);\n     +\t\t\tif (ret) {\n     +\t\t\t\tret = error(_(\"failed to update ref '%s': %s\"),\n    -+\t\t\t\t\t    updates.items[i].refname, err.buf);\n    ++\t\t\t\t\t    result.updates[i].refname, err.buf);\n     +\t\t\t\tgoto out;\n     +\t\t\t}\n     +\t\t}\n    @@ builtin/history.c\n     +\t\tbreak;\n     +\tcase REF_ACTION_HEAD:\n     +\t\tret = refs_update_ref(get_main_ref_store(repo), reflog_msg, \"HEAD\",\n    -+\t\t\t\t      &updates.final_oid, &head->object.oid, 0,\n    ++\t\t\t\t      &result.final_oid, &head->object.oid, 0,\n     +\t\t\t\t      UPDATE_REFS_MSG_ON_ERR);\n     +\t\tif (ret)\n     +\t\t\tgoto out;\n     +\t\tbreak;\n     +\tcase REF_ACTION_PRINT:\n    -+\t\tfor (size_t i = 0; i < updates.nr; i++)\n    ++\t\tfor (size_t i = 0; i < result.updates_nr; i++)\n     +\t\t\tprintf(\"update %s %s %s\\n\",\n    -+\t\t\t       updates.items[i].refname,\n    -+\t\t\t       oid_to_hex(&updates.items[i].new_oid),\n    -+\t\t\t       oid_to_hex(&updates.items[i].old_oid));\n    ++\t\t\t       result.updates[i].refname,\n    ++\t\t\t       oid_to_hex(&result.updates[i].new_oid),\n    ++\t\t\t       oid_to_hex(&result.updates[i].old_oid));\n     +\t\tbreak;\n     +\tdefault:\n     +\t\tBUG(\"unsupported ref action %d\", action);\n    @@ builtin/history.c\n     +\tret = 0;\n     +\n     +out:\n    -+\treplay_ref_updates_release(&updates);\n     +\tref_transaction_free(transaction);\n    ++\treplay_result_release(&result);\n     +\trelease_revisions(&revs);\n     +\tstrbuf_release(&err);\n     +\tstrvec_clear(&args);\n\n---\nbase-commit: 02bdc12118c43e0a65258eb742492965859bffb6\nchange-id: 20250819-b4-pks-history-builtin-83398f9a05f0\n\n"},{"id":"533328","messageId":"20260109-b4-pks-history-builtin-v9-1-8766101814c6@pks.im","threadId":"63986","inReplyTo":"20260109-b4-pks-history-builtin-v9-0-8766101814c6@pks.im","subject":"[PATCH v9 1/7] builtin/replay: extract core logic to replay revisions","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-01-09T08:35:36Z","receivedAt":"2026-01-09T08:35:47Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"We're about to move the core logic used to replay revisions onto a new\nbase into the \"libgit.a\" library. Prepare for this by pulling out the\nlogic into a new function `replay_revisions()` that:\n\n  1. Takes a set of revisions to replay and some options that tell it how\n     it ought to replay the revisions.\n\n  2. Replays the commits.\n\n  3. Records any reference updates that would be caused by replaying the\n     commits in a structure that is owned by the caller.\n\nThe logic itself will be moved into a separate file in the next commit.\nThis change is not expected to cause user-visible change in behaviour.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/replay.c | 274 +++++++++++++++++++++++++++++++++----------------------\n 1 file changed, 165 insertions(+), 109 deletions(-)\n\ndiff --git a/builtin/replay.c b/builtin/replay.c\nindex 1960bbbee8..df3b32a52d 100644\n--- a/builtin/replay.c\n+++ b/builtin/replay.c\n@@ -278,6 +278,137 @@ static enum ref_action_mode get_ref_action_mode(struct repository *repo, const c\n \treturn REF_ACTION_UPDATE;\n }\n \n+struct replay_revisions_options {\n+\tconst char *advance;\n+\tconst char *onto;\n+\tint contained;\n+};\n+\n+struct replay_result {\n+\tstruct replay_ref_update {\n+\t\tchar *refname;\n+\t\tstruct object_id old_oid;\n+\t\tstruct object_id new_oid;\n+\t} *updates;\n+\tsize_t updates_nr, updates_alloc;\n+\n+\tbool merge_conflict;\n+};\n+\n+static void replay_result_release(struct replay_result *result)\n+{\n+\tfor (size_t i = 0; i < result->updates_nr; i++)\n+\t\tfree(result->updates[i].refname);\n+\tfree(result->updates);\n+}\n+\n+static void replay_result_queue_update(struct replay_result *result,\n+\t\t\t\t       const char *refname,\n+\t\t\t\t       const struct object_id *old_oid,\n+\t\t\t\t       const struct object_id *new_oid)\n+{\n+\tALLOC_GROW(result->updates, result->updates_nr + 1, result->updates_alloc);\n+\tresult->updates[result->updates_nr].refname = xstrdup(refname);\n+\tresult->updates[result->updates_nr].old_oid = *old_oid;\n+\tresult->updates[result->updates_nr].new_oid = *new_oid;\n+\tresult->updates_nr++;\n+}\n+\n+static int replay_revisions(struct repository *repo, struct rev_info *revs,\n+\t\t\t    struct replay_revisions_options *opts,\n+\t\t\t    struct replay_result *out)\n+{\n+\tkh_oid_map_t *replayed_commits = NULL;\n+\tstruct strset *update_refs = NULL;\n+\tstruct commit *last_commit = NULL;\n+\tstruct commit *commit;\n+\tstruct commit *onto = NULL;\n+\tstruct merge_options merge_opt;\n+\tstruct merge_result result;\n+\tchar *advance;\n+\tint ret;\n+\n+\tadvance = xstrdup_or_null(opts->advance);\n+\tset_up_replay_mode(repo, &revs->cmdline, opts->onto, &advance,\n+\t\t\t   &onto, &update_refs);\n+\n+\t/* FIXME: Should allow replaying commits with the first as a root commit */\n+\n+\tif (prepare_revision_walk(revs) < 0) {\n+\t\tret = error(_(\"error preparing revisions\"));\n+\t\tgoto out;\n+\t}\n+\n+\tinit_basic_merge_options(&merge_opt, repo);\n+\tmemset(&result, 0, sizeof(result));\n+\tmerge_opt.show_rename_progress = 0;\n+\tlast_commit = onto;\n+\treplayed_commits = kh_init_oid_map();\n+\twhile ((commit = get_revision(revs))) {\n+\t\tconst struct name_decoration *decoration;\n+\t\tkhint_t pos;\n+\t\tint hr;\n+\n+\t\tif (!commit->parents)\n+\t\t\tdie(_(\"replaying down from root commit is not supported yet!\"));\n+\t\tif (commit->parents->next)\n+\t\t\tdie(_(\"replaying merge commits is not supported yet!\"));\n+\n+\t\tlast_commit = pick_regular_commit(repo, commit, replayed_commits,\n+\t\t\t\t\t\t  onto, &merge_opt, &result);\n+\t\tif (!last_commit)\n+\t\t\tbreak;\n+\n+\t\t/* Record commit -> last_commit mapping */\n+\t\tpos = kh_put_oid_map(replayed_commits, commit->object.oid, &hr);\n+\t\tif (hr == 0)\n+\t\t\tBUG(\"Duplicate rewritten commit: %s\\n\",\n+\t\t\t    oid_to_hex(&commit->object.oid));\n+\t\tkh_value(replayed_commits, pos) = last_commit;\n+\n+\t\t/* Update any necessary branches */\n+\t\tif (advance)\n+\t\t\tcontinue;\n+\t\tdecoration = get_name_decoration(&commit->object);\n+\t\tif (!decoration)\n+\t\t\tcontinue;\n+\t\twhile (decoration) {\n+\t\t\tif (decoration->type == DECORATION_REF_LOCAL &&\n+\t\t\t    (opts->contained || strset_contains(update_refs,\n+\t\t\t\t\t\t\t\tdecoration->name))) {\n+\t\t\t\treplay_result_queue_update(out, decoration->name,\n+\t\t\t\t\t\t\t   &commit->object.oid,\n+\t\t\t\t\t\t\t   &last_commit->object.oid);\n+\t\t\t}\n+\t\t\tdecoration = decoration->next;\n+\t\t}\n+\t}\n+\n+\tif (!result.clean) {\n+\t\tout->merge_conflict = true;\n+\t\tret = -1;\n+\t\tgoto out;\n+\t}\n+\n+\t/* In --advance mode, advance the target ref */\n+\tif (advance)\n+\t\treplay_result_queue_update(out, advance,\n+\t\t\t\t\t   &onto->object.oid,\n+\t\t\t\t\t   &last_commit->object.oid);\n+\n+\tret = 0;\n+\n+out:\n+\tif (update_refs) {\n+\t\tstrset_clear(update_refs);\n+\t\tfree(update_refs);\n+\t}\n+\tkh_destroy_oid_map(replayed_commits);\n+\tmerge_finalize(&merge_opt, &result);\n+\tfree(advance);\n+\treturn ret;\n+}\n+\n static int handle_ref_update(enum ref_action_mode mode,\n \t\t\t     struct ref_transaction *transaction,\n \t\t\t     const char *refname,\n@@ -306,21 +437,11 @@ int cmd_replay(int argc,\n \t       const char *prefix,\n \t       struct repository *repo)\n {\n-\tconst char *advance_name_opt = NULL;\n-\tchar *advance_name = NULL;\n-\tstruct commit *onto = NULL;\n-\tconst char *onto_name = NULL;\n-\tint contained = 0;\n+\tstruct replay_revisions_options opts = { 0 };\n+\tstruct replay_result result = { 0 };\n \tconst char *ref_action = NULL;\n \tenum ref_action_mode ref_mode;\n-\n \tstruct rev_info revs;\n-\tstruct commit *last_commit = NULL;\n-\tstruct commit *commit;\n-\tstruct merge_options merge_opt;\n-\tstruct merge_result result;\n-\tstruct strset *update_refs = NULL;\n-\tkh_oid_map_t *replayed_commits;\n \tstruct ref_transaction *transaction = NULL;\n \tstruct strbuf transaction_err = STRBUF_INIT;\n \tstruct strbuf reflog_msg = STRBUF_INIT;\n@@ -333,13 +454,13 @@ int cmd_replay(int argc,\n \t\tNULL\n \t};\n \tstruct option replay_options[] = {\n-\t\tOPT_STRING(0, \"advance\", &advance_name_opt,\n+\t\tOPT_STRING(0, \"advance\", &opts.advance,\n \t\t\t   N_(\"branch\"),\n \t\t\t   N_(\"make replay advance given branch\")),\n-\t\tOPT_STRING(0, \"onto\", &onto_name,\n+\t\tOPT_STRING(0, \"onto\", &opts.onto,\n \t\t\t   N_(\"revision\"),\n \t\t\t   N_(\"replay onto given commit\")),\n-\t\tOPT_BOOL(0, \"contained\", &contained,\n+\t\tOPT_BOOL(0, \"contained\", &opts.contained,\n \t\t\t N_(\"update all branches that point at commits in <revision-range>\")),\n \t\tOPT_STRING(0, \"ref-action\", &ref_action,\n \t\t\t   N_(\"mode\"),\n@@ -350,19 +471,17 @@ int cmd_replay(int argc,\n \targc = parse_options(argc, argv, prefix, replay_options, replay_usage,\n \t\t\t     PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT);\n \n-\tif (!onto_name && !advance_name_opt) {\n+\tif (!opts.onto && !opts.advance) {\n \t\terror(_(\"option --onto or --advance is mandatory\"));\n \t\tusage_with_options(replay_usage, replay_options);\n \t}\n \n-\tdie_for_incompatible_opt2(!!advance_name_opt, \"--advance\",\n-\t\t\t\t  contained, \"--contained\");\n+\tdie_for_incompatible_opt2(!!opts.advance, \"--advance\",\n+\t\t\t\t  opts.contained, \"--contained\");\n \n \t/* Parse ref action mode from command line or config */\n \tref_mode = get_ref_action_mode(repo, ref_action);\n \n-\tadvance_name = xstrdup_or_null(advance_name_opt);\n-\n \trepo_init_revisions(repo, &revs, prefix);\n \n \t/*\n@@ -414,18 +533,19 @@ int cmd_replay(int argc,\n \t\trevs.simplify_history = 0;\n \t}\n \n-\tset_up_replay_mode(repo, &revs.cmdline,\n-\t\t\t   onto_name, &advance_name,\n-\t\t\t   &onto, &update_refs);\n-\n-\t/* FIXME: Should allow replaying commits with the first as a root commit */\n+\tret = replay_revisions(repo, &revs, &opts, &result);\n+\tif (ret)\n+\t\tgoto cleanup;\n \n \t/* Build reflog message */\n-\tif (advance_name_opt)\n-\t\tstrbuf_addf(&reflog_msg, \"replay --advance %s\", advance_name_opt);\n-\telse\n-\t\tstrbuf_addf(&reflog_msg, \"replay --onto %s\",\n-\t\t\t    oid_to_hex(&onto->object.oid));\n+\tif (opts.advance) {\n+\t\tstrbuf_addf(&reflog_msg, \"replay --advance %s\", opts.advance);\n+\t} else {\n+\t\tstruct object_id oid;\n+\t\tif (repo_get_oid_committish(repo, opts.onto, &oid))\n+\t\t\tBUG(\"--onto commit should have been resolved beforehand already\");\n+\t\tstrbuf_addf(&reflog_msg, \"replay --onto %s\", oid_to_hex(&oid));\n+\t}\n \n \t/* Initialize ref transaction if using update mode */\n \tif (ref_mode == REF_ACTION_UPDATE) {\n@@ -438,78 +558,19 @@ int cmd_replay(int argc,\n \t\t}\n \t}\n \n-\tif (prepare_revision_walk(&revs) < 0) {\n-\t\tret = error(_(\"error preparing revisions\"));\n-\t\tgoto cleanup;\n-\t}\n-\n-\tinit_basic_merge_options(&merge_opt, repo);\n-\tmemset(&result, 0, sizeof(result));\n-\tmerge_opt.show_rename_progress = 0;\n-\tlast_commit = onto;\n-\treplayed_commits = kh_init_oid_map();\n-\twhile ((commit = get_revision(&revs))) {\n-\t\tconst struct name_decoration *decoration;\n-\t\tkhint_t pos;\n-\t\tint hr;\n-\n-\t\tif (!commit->parents)\n-\t\t\tdie(_(\"replaying down from root commit is not supported yet!\"));\n-\t\tif (commit->parents->next)\n-\t\t\tdie(_(\"replaying merge commits is not supported yet!\"));\n-\n-\t\tlast_commit = pick_regular_commit(repo, commit, replayed_commits,\n-\t\t\t\t\t\t  onto, &merge_opt, &result);\n-\t\tif (!last_commit)\n-\t\t\tbreak;\n-\n-\t\t/* Record commit -> last_commit mapping */\n-\t\tpos = kh_put_oid_map(replayed_commits, commit->object.oid, &hr);\n-\t\tif (hr == 0)\n-\t\t\tBUG(\"Duplicate rewritten commit: %s\\n\",\n-\t\t\t    oid_to_hex(&commit->object.oid));\n-\t\tkh_value(replayed_commits, pos) = last_commit;\n-\n-\t\t/* Update any necessary branches */\n-\t\tif (advance_name)\n-\t\t\tcontinue;\n-\t\tdecoration = get_name_decoration(&commit->object);\n-\t\tif (!decoration)\n-\t\t\tcontinue;\n-\t\twhile (decoration) {\n-\t\t\tif (decoration->type == DECORATION_REF_LOCAL &&\n-\t\t\t    (contained || strset_contains(update_refs,\n-\t\t\t\t\t\t\t  decoration->name))) {\n-\t\t\t\tif (handle_ref_update(ref_mode, transaction,\n-\t\t\t\t\t\t      decoration->name,\n-\t\t\t\t\t\t      &last_commit->object.oid,\n-\t\t\t\t\t\t      &commit->object.oid,\n-\t\t\t\t\t\t      reflog_msg.buf,\n-\t\t\t\t\t\t      &transaction_err) < 0) {\n-\t\t\t\t\tret = error(_(\"failed to update ref '%s': %s\"),\n-\t\t\t\t\t\t    decoration->name, transaction_err.buf);\n-\t\t\t\t\tgoto cleanup;\n-\t\t\t\t}\n-\t\t\t}\n-\t\t\tdecoration = decoration->next;\n-\t\t}\n-\t}\n-\n-\t/* In --advance mode, advance the target ref */\n-\tif (result.clean == 1 && advance_name) {\n-\t\tif (handle_ref_update(ref_mode, transaction, advance_name,\n-\t\t\t\t      &last_commit->object.oid,\n-\t\t\t\t      &onto->object.oid,\n-\t\t\t\t      reflog_msg.buf,\n-\t\t\t\t      &transaction_err) < 0) {\n+\tfor (size_t i = 0; i < result.updates_nr; i++) {\n+\t\tret = handle_ref_update(ref_mode, transaction, result.updates[i].refname,\n+\t\t\t\t\t&result.updates[i].new_oid, &result.updates[i].old_oid,\n+\t\t\t\t\treflog_msg.buf, &transaction_err);\n+\t\tif (ret) {\n \t\t\tret = error(_(\"failed to update ref '%s': %s\"),\n-\t\t\t\t    advance_name, transaction_err.buf);\n+\t\t\t\t    result.updates[i].refname, transaction_err.buf);\n \t\t\tgoto cleanup;\n \t\t}\n \t}\n \n \t/* Commit the ref transaction if we have one */\n-\tif (transaction && result.clean == 1) {\n+\tif (transaction) {\n \t\tif (ref_transaction_commit(transaction, &transaction_err)) {\n \t\t\tret = error(_(\"failed to commit ref transaction: %s\"),\n \t\t\t\t    transaction_err.buf);\n@@ -517,24 +578,19 @@ int cmd_replay(int argc,\n \t\t}\n \t}\n \n-\tmerge_finalize(&merge_opt, &result);\n-\tkh_destroy_oid_map(replayed_commits);\n-\tif (update_refs) {\n-\t\tstrset_clear(update_refs);\n-\t\tfree(update_refs);\n-\t}\n-\tret = result.clean;\n-\n cleanup:\n \tif (transaction)\n \t\tref_transaction_free(transaction);\n+\treplay_result_release(&result);\n \tstrbuf_release(&transaction_err);\n \tstrbuf_release(&reflog_msg);\n \trelease_revisions(&revs);\n-\tfree(advance_name);\n \n-\t/* Return */\n-\tif (ret < 0)\n-\t\texit(128);\n-\treturn ret ? 0 : 1;\n+\tif (ret) {\n+\t\tif (result.merge_conflict)\n+\t\t\treturn 1;\n+\t\treturn 128;\n+\t}\n+\n+\treturn 0;\n }\n\n-- \n2.52.0.542.g9473a8513b.dirty\n\n"},{"id":"533329","messageId":"20260109-b4-pks-history-builtin-v9-2-8766101814c6@pks.im","threadId":"63986","inReplyTo":"20260109-b4-pks-history-builtin-v9-0-8766101814c6@pks.im","subject":"[PATCH v9 2/7] builtin/replay: move core logic into \"libgit.a\"","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-01-09T08:35:37Z","receivedAt":"2026-01-09T08:35:49Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Move the core logic used to replay commits into \"libgit.a\" so that it\ncan be easily reused by other commands. It will be used in a subsequent\ncommit where we're about to introduce a new git-history(1) command.\n\nNote that with this change we have no sign-comparison warnings anymore,\nand neither do we depend on `the_repository`.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Makefile         |   1 +\n builtin/replay.c | 368 +------------------------------------------------------\n meson.build      |   1 +\n replay.c         | 355 +++++++++++++++++++++++++++++++++++++++++++++++++++++\n replay.h         |  64 ++++++++++\n 5 files changed, 422 insertions(+), 367 deletions(-)\n\ndiff --git a/Makefile b/Makefile\nindex b7eba509c6..1c64a5d2ae 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -1285,6 +1285,7 @@ LIB_OBJS += repack-geometry.o\n LIB_OBJS += repack-midx.o\n LIB_OBJS += repack-promisor.o\n LIB_OBJS += replace-object.o\n+LIB_OBJS += replay.o\n LIB_OBJS += repo-settings.o\n LIB_OBJS += repository.o\n LIB_OBJS += rerere.o\ndiff --git a/builtin/replay.c b/builtin/replay.c\nindex df3b32a52d..da8b7202f6 100644\n--- a/builtin/replay.c\n+++ b/builtin/replay.c\n@@ -2,257 +2,22 @@\n  * \"git replay\" builtin command\n  */\n \n-#define USE_THE_REPOSITORY_VARIABLE\n-#define DISABLE_SIGN_COMPARE_WARNINGS\n-\n #include \"git-compat-util.h\"\n \n #include \"builtin.h\"\n #include \"config.h\"\n-#include \"environment.h\"\n #include \"hex.h\"\n-#include \"lockfile.h\"\n-#include \"merge-ort.h\"\n #include \"object-name.h\"\n #include \"parse-options.h\"\n #include \"refs.h\"\n+#include \"replay.h\"\n #include \"revision.h\"\n-#include \"strmap.h\"\n-#include <oidset.h>\n-#include <tree.h>\n \n enum ref_action_mode {\n \tREF_ACTION_UPDATE,\n \tREF_ACTION_PRINT,\n };\n \n-static const char *short_commit_name(struct repository *repo,\n-\t\t\t\t     struct commit *commit)\n-{\n-\treturn repo_find_unique_abbrev(repo, &commit->object.oid,\n-\t\t\t\t       DEFAULT_ABBREV);\n-}\n-\n-static struct commit *peel_committish(struct repository *repo,\n-\t\t\t\t      const char *name,\n-\t\t\t\t      const char *mode)\n-{\n-\tstruct object *obj;\n-\tstruct object_id oid;\n-\n-\tif (repo_get_oid(repo, name, &oid))\n-\t\tdie(_(\"'%s' is not a valid commit-ish for %s\"), name, mode);\n-\tobj = parse_object_or_die(repo, &oid, name);\n-\treturn (struct commit *)repo_peel_to_type(repo, name, 0, obj,\n-\t\t\t\t\t\t  OBJ_COMMIT);\n-}\n-\n-static char *get_author(const char *message)\n-{\n-\tsize_t len;\n-\tconst char *a;\n-\n-\ta = find_commit_header(message, \"author\", &len);\n-\tif (a)\n-\t\treturn xmemdupz(a, len);\n-\n-\treturn NULL;\n-}\n-\n-static struct commit *create_commit(struct repository *repo,\n-\t\t\t\t    struct tree *tree,\n-\t\t\t\t    struct commit *based_on,\n-\t\t\t\t    struct commit *parent)\n-{\n-\tstruct object_id ret;\n-\tstruct object *obj = NULL;\n-\tstruct commit_list *parents = NULL;\n-\tchar *author;\n-\tchar *sign_commit = NULL; /* FIXME: cli users might want to sign again */\n-\tstruct commit_extra_header *extra = NULL;\n-\tstruct strbuf msg = STRBUF_INIT;\n-\tconst char *out_enc = get_commit_output_encoding();\n-\tconst char *message = repo_logmsg_reencode(repo, based_on,\n-\t\t\t\t\t\t   NULL, out_enc);\n-\tconst char *orig_message = NULL;\n-\tconst char *exclude_gpgsig[] = { \"gpgsig\", \"gpgsig-sha256\", NULL };\n-\n-\tcommit_list_insert(parent, &parents);\n-\textra = read_commit_extra_headers(based_on, exclude_gpgsig);\n-\tfind_commit_subject(message, &orig_message);\n-\tstrbuf_addstr(&msg, orig_message);\n-\tauthor = get_author(message);\n-\treset_ident_date();\n-\tif (commit_tree_extended(msg.buf, msg.len, &tree->object.oid, parents,\n-\t\t\t\t &ret, author, NULL, sign_commit, extra)) {\n-\t\terror(_(\"failed to write commit object\"));\n-\t\tgoto out;\n-\t}\n-\n-\tobj = parse_object(repo, &ret);\n-\n-out:\n-\trepo_unuse_commit_buffer(the_repository, based_on, message);\n-\tfree_commit_extra_headers(extra);\n-\tfree_commit_list(parents);\n-\tstrbuf_release(&msg);\n-\tfree(author);\n-\treturn (struct commit *)obj;\n-}\n-\n-struct ref_info {\n-\tstruct commit *onto;\n-\tstruct strset positive_refs;\n-\tstruct strset negative_refs;\n-\tint positive_refexprs;\n-\tint negative_refexprs;\n-};\n-\n-static void get_ref_information(struct repository *repo,\n-\t\t\t\tstruct rev_cmdline_info *cmd_info,\n-\t\t\t\tstruct ref_info *ref_info)\n-{\n-\tint i;\n-\n-\tref_info->onto = NULL;\n-\tstrset_init(&ref_info->positive_refs);\n-\tstrset_init(&ref_info->negative_refs);\n-\tref_info->positive_refexprs = 0;\n-\tref_info->negative_refexprs = 0;\n-\n-\t/*\n-\t * When the user specifies e.g.\n-\t *   git replay origin/main..mybranch\n-\t *   git replay ^origin/next mybranch1 mybranch2\n-\t * we want to be able to determine where to replay the commits.  In\n-\t * these examples, the branches are probably based on an old version\n-\t * of either origin/main or origin/next, so we want to replay on the\n-\t * newest version of that branch.  In contrast we would want to error\n-\t * out if they ran\n-\t *   git replay ^origin/master ^origin/next mybranch\n-\t *   git replay mybranch~2..mybranch\n-\t * the first of those because there's no unique base to choose, and\n-\t * the second because they'd likely just be replaying commits on top\n-\t * of the same commit and not making any difference.\n-\t */\n-\tfor (i = 0; i < cmd_info->nr; i++) {\n-\t\tstruct rev_cmdline_entry *e = cmd_info->rev + i;\n-\t\tstruct object_id oid;\n-\t\tconst char *refexpr = e->name;\n-\t\tchar *fullname = NULL;\n-\t\tint can_uniquely_dwim = 1;\n-\n-\t\tif (*refexpr == '^')\n-\t\t\trefexpr++;\n-\t\tif (repo_dwim_ref(repo, refexpr, strlen(refexpr), &oid, &fullname, 0) != 1)\n-\t\t\tcan_uniquely_dwim = 0;\n-\n-\t\tif (e->flags & BOTTOM) {\n-\t\t\tif (can_uniquely_dwim)\n-\t\t\t\tstrset_add(&ref_info->negative_refs, fullname);\n-\t\t\tif (!ref_info->negative_refexprs)\n-\t\t\t\tref_info->onto = lookup_commit_reference_gently(repo,\n-\t\t\t\t\t\t\t\t\t\t&e->item->oid, 1);\n-\t\t\tref_info->negative_refexprs++;\n-\t\t} else {\n-\t\t\tif (can_uniquely_dwim)\n-\t\t\t\tstrset_add(&ref_info->positive_refs, fullname);\n-\t\t\tref_info->positive_refexprs++;\n-\t\t}\n-\n-\t\tfree(fullname);\n-\t}\n-}\n-\n-static void set_up_replay_mode(struct repository *repo,\n-\t\t\t       struct rev_cmdline_info *cmd_info,\n-\t\t\t       const char *onto_name,\n-\t\t\t       char **advance_name,\n-\t\t\t       struct commit **onto,\n-\t\t\t       struct strset **update_refs)\n-{\n-\tstruct ref_info rinfo;\n-\n-\tget_ref_information(repo, cmd_info, &rinfo);\n-\tif (!rinfo.positive_refexprs)\n-\t\tdie(_(\"need some commits to replay\"));\n-\n-\tdie_for_incompatible_opt2(!!onto_name, \"--onto\",\n-\t\t\t\t  !!*advance_name, \"--advance\");\n-\tif (onto_name) {\n-\t\t*onto = peel_committish(repo, onto_name, \"--onto\");\n-\t\tif (rinfo.positive_refexprs <\n-\t\t    strset_get_size(&rinfo.positive_refs))\n-\t\t\tdie(_(\"all positive revisions given must be references\"));\n-\t\t*update_refs = xcalloc(1, sizeof(**update_refs));\n-\t\t**update_refs = rinfo.positive_refs;\n-\t\tmemset(&rinfo.positive_refs, 0, sizeof(**update_refs));\n-\t} else {\n-\t\tstruct object_id oid;\n-\t\tchar *fullname = NULL;\n-\n-\t\tif (!*advance_name)\n-\t\t\tBUG(\"expected either onto_name or *advance_name in this function\");\n-\n-\t\tif (repo_dwim_ref(repo, *advance_name, strlen(*advance_name),\n-\t\t\t     &oid, &fullname, 0) == 1) {\n-\t\t\tfree(*advance_name);\n-\t\t\t*advance_name = fullname;\n-\t\t} else {\n-\t\t\tdie(_(\"argument to --advance must be a reference\"));\n-\t\t}\n-\t\t*onto = peel_committish(repo, *advance_name, \"--advance\");\n-\t\tif (rinfo.positive_refexprs > 1)\n-\t\t\tdie(_(\"cannot advance target with multiple sources because ordering would be ill-defined\"));\n-\t}\n-\tstrset_clear(&rinfo.negative_refs);\n-\tstrset_clear(&rinfo.positive_refs);\n-}\n-\n-static struct commit *mapped_commit(kh_oid_map_t *replayed_commits,\n-\t\t\t\t    struct commit *commit,\n-\t\t\t\t    struct commit *fallback)\n-{\n-\tkhint_t pos = kh_get_oid_map(replayed_commits, commit->object.oid);\n-\tif (pos == kh_end(replayed_commits))\n-\t\treturn fallback;\n-\treturn kh_value(replayed_commits, pos);\n-}\n-\n-static struct commit *pick_regular_commit(struct repository *repo,\n-\t\t\t\t\t  struct commit *pickme,\n-\t\t\t\t\t  kh_oid_map_t *replayed_commits,\n-\t\t\t\t\t  struct commit *onto,\n-\t\t\t\t\t  struct merge_options *merge_opt,\n-\t\t\t\t\t  struct merge_result *result)\n-{\n-\tstruct commit *base, *replayed_base;\n-\tstruct tree *pickme_tree, *base_tree;\n-\n-\tbase = pickme->parents->item;\n-\treplayed_base = mapped_commit(replayed_commits, base, onto);\n-\n-\tresult->tree = repo_get_commit_tree(repo, replayed_base);\n-\tpickme_tree = repo_get_commit_tree(repo, pickme);\n-\tbase_tree = repo_get_commit_tree(repo, base);\n-\n-\tmerge_opt->branch1 = short_commit_name(repo, replayed_base);\n-\tmerge_opt->branch2 = short_commit_name(repo, pickme);\n-\tmerge_opt->ancestor = xstrfmt(\"parent of %s\", merge_opt->branch2);\n-\n-\tmerge_incore_nonrecursive(merge_opt,\n-\t\t\t\t  base_tree,\n-\t\t\t\t  result->tree,\n-\t\t\t\t  pickme_tree,\n-\t\t\t\t  result);\n-\n-\tfree((char*)merge_opt->ancestor);\n-\tmerge_opt->ancestor = NULL;\n-\tif (!result->clean)\n-\t\treturn NULL;\n-\treturn create_commit(repo, result->tree, pickme, replayed_base);\n-}\n-\n static enum ref_action_mode parse_ref_action_mode(const char *ref_action, const char *source)\n {\n \tif (!ref_action || !strcmp(ref_action, \"update\"))\n@@ -278,137 +43,6 @@ static enum ref_action_mode get_ref_action_mode(struct repository *repo, const c\n \treturn REF_ACTION_UPDATE;\n }\n \n-struct replay_revisions_options {\n-\tconst char *advance;\n-\tconst char *onto;\n-\tint contained;\n-};\n-\n-struct replay_result {\n-\tstruct replay_ref_update {\n-\t\tchar *refname;\n-\t\tstruct object_id old_oid;\n-\t\tstruct object_id new_oid;\n-\t} *updates;\n-\tsize_t updates_nr, updates_alloc;\n-\n-\tbool merge_conflict;\n-};\n-\n-static void replay_result_release(struct replay_result *result)\n-{\n-\tfor (size_t i = 0; i < result->updates_nr; i++)\n-\t\tfree(result->updates[i].refname);\n-\tfree(result->updates);\n-}\n-\n-static void replay_result_queue_update(struct replay_result *result,\n-\t\t\t\t       const char *refname,\n-\t\t\t\t       const struct object_id *old_oid,\n-\t\t\t\t       const struct object_id *new_oid)\n-{\n-\tALLOC_GROW(result->updates, result->updates_nr + 1, result->updates_alloc);\n-\tresult->updates[result->updates_nr].refname = xstrdup(refname);\n-\tresult->updates[result->updates_nr].old_oid = *old_oid;\n-\tresult->updates[result->updates_nr].new_oid = *new_oid;\n-\tresult->updates_nr++;\n-}\n-\n-static int replay_revisions(struct repository *repo, struct rev_info *revs,\n-\t\t\t    struct replay_revisions_options *opts,\n-\t\t\t    struct replay_result *out)\n-{\n-\tkh_oid_map_t *replayed_commits = NULL;\n-\tstruct strset *update_refs = NULL;\n-\tstruct commit *last_commit = NULL;\n-\tstruct commit *commit;\n-\tstruct commit *onto = NULL;\n-\tstruct merge_options merge_opt;\n-\tstruct merge_result result;\n-\tchar *advance;\n-\tint ret;\n-\n-\tadvance = xstrdup_or_null(opts->advance);\n-\tset_up_replay_mode(repo, &revs->cmdline, opts->onto, &advance,\n-\t\t\t   &onto, &update_refs);\n-\n-\t/* FIXME: Should allow replaying commits with the first as a root commit */\n-\n-\tif (prepare_revision_walk(revs) < 0) {\n-\t\tret = error(_(\"error preparing revisions\"));\n-\t\tgoto out;\n-\t}\n-\n-\tinit_basic_merge_options(&merge_opt, repo);\n-\tmemset(&result, 0, sizeof(result));\n-\tmerge_opt.show_rename_progress = 0;\n-\tlast_commit = onto;\n-\treplayed_commits = kh_init_oid_map();\n-\twhile ((commit = get_revision(revs))) {\n-\t\tconst struct name_decoration *decoration;\n-\t\tkhint_t pos;\n-\t\tint hr;\n-\n-\t\tif (!commit->parents)\n-\t\t\tdie(_(\"replaying down from root commit is not supported yet!\"));\n-\t\tif (commit->parents->next)\n-\t\t\tdie(_(\"replaying merge commits is not supported yet!\"));\n-\n-\t\tlast_commit = pick_regular_commit(repo, commit, replayed_commits,\n-\t\t\t\t\t\t  onto, &merge_opt, &result);\n-\t\tif (!last_commit)\n-\t\t\tbreak;\n-\n-\t\t/* Record commit -> last_commit mapping */\n-\t\tpos = kh_put_oid_map(replayed_commits, commit->object.oid, &hr);\n-\t\tif (hr == 0)\n-\t\t\tBUG(\"Duplicate rewritten commit: %s\\n\",\n-\t\t\t    oid_to_hex(&commit->object.oid));\n-\t\tkh_value(replayed_commits, pos) = last_commit;\n-\n-\t\t/* Update any necessary branches */\n-\t\tif (advance)\n-\t\t\tcontinue;\n-\t\tdecoration = get_name_decoration(&commit->object);\n-\t\tif (!decoration)\n-\t\t\tcontinue;\n-\t\twhile (decoration) {\n-\t\t\tif (decoration->type == DECORATION_REF_LOCAL &&\n-\t\t\t    (opts->contained || strset_contains(update_refs,\n-\t\t\t\t\t\t\t\tdecoration->name))) {\n-\t\t\t\treplay_result_queue_update(out, decoration->name,\n-\t\t\t\t\t\t\t   &commit->object.oid,\n-\t\t\t\t\t\t\t   &last_commit->object.oid);\n-\t\t\t}\n-\t\t\tdecoration = decoration->next;\n-\t\t}\n-\t}\n-\n-\tif (!result.clean) {\n-\t\tout->merge_conflict = true;\n-\t\tret = -1;\n-\t\tgoto out;\n-\t}\n-\n-\t/* In --advance mode, advance the target ref */\n-\tif (advance)\n-\t\treplay_result_queue_update(out, advance,\n-\t\t\t\t\t   &onto->object.oid,\n-\t\t\t\t\t   &last_commit->object.oid);\n-\n-\tret = 0;\n-\n-out:\n-\tif (update_refs) {\n-\t\tstrset_clear(update_refs);\n-\t\tfree(update_refs);\n-\t}\n-\tkh_destroy_oid_map(replayed_commits);\n-\tmerge_finalize(&merge_opt, &result);\n-\tfree(advance);\n-\treturn ret;\n-}\n-\n static int handle_ref_update(enum ref_action_mode mode,\n \t\t\t     struct ref_transaction *transaction,\n \t\t\t     const char *refname,\ndiff --git a/meson.build b/meson.build\nindex dd52efd1c8..a5a4e99b25 100644\n--- a/meson.build\n+++ b/meson.build\n@@ -471,6 +471,7 @@ libgit_sources = [\n   'repack-midx.c',\n   'repack-promisor.c',\n   'replace-object.c',\n+  'replay.c',\n   'repo-settings.c',\n   'repository.c',\n   'rerere.c',\ndiff --git a/replay.c b/replay.c\nnew file mode 100644\nindex 0000000000..fc7186ef09\n--- /dev/null\n+++ b/replay.c\n@@ -0,0 +1,355 @@\n+#define USE_THE_REPOSITORY_VARIABLE\n+#define DISABLE_SIGN_COMPARE_WARNINGS\n+\n+#include \"git-compat-util.h\"\n+#include \"environment.h\"\n+#include \"hex.h\"\n+#include \"merge-ort.h\"\n+#include \"object-name.h\"\n+#include \"oidset.h\"\n+#include \"parse-options.h\"\n+#include \"refs.h\"\n+#include \"replay.h\"\n+#include \"revision.h\"\n+#include \"tree.h\"\n+\n+static const char *short_commit_name(struct repository *repo,\n+\t\t\t\t     struct commit *commit)\n+{\n+\treturn repo_find_unique_abbrev(repo, &commit->object.oid,\n+\t\t\t\t       DEFAULT_ABBREV);\n+}\n+\n+static struct commit *peel_committish(struct repository *repo,\n+\t\t\t\t      const char *name,\n+\t\t\t\t      const char *mode)\n+{\n+\tstruct object *obj;\n+\tstruct object_id oid;\n+\n+\tif (repo_get_oid(repo, name, &oid))\n+\t\tdie(_(\"'%s' is not a valid commit-ish for %s\"), name, mode);\n+\tobj = parse_object_or_die(repo, &oid, name);\n+\treturn (struct commit *)repo_peel_to_type(repo, name, 0, obj,\n+\t\t\t\t\t\t  OBJ_COMMIT);\n+}\n+\n+static char *get_author(const char *message)\n+{\n+\tsize_t len;\n+\tconst char *a;\n+\n+\ta = find_commit_header(message, \"author\", &len);\n+\tif (a)\n+\t\treturn xmemdupz(a, len);\n+\n+\treturn NULL;\n+}\n+\n+static struct commit *create_commit(struct repository *repo,\n+\t\t\t\t    struct tree *tree,\n+\t\t\t\t    struct commit *based_on,\n+\t\t\t\t    struct commit *parent)\n+{\n+\tstruct object_id ret;\n+\tstruct object *obj = NULL;\n+\tstruct commit_list *parents = NULL;\n+\tchar *author;\n+\tchar *sign_commit = NULL; /* FIXME: cli users might want to sign again */\n+\tstruct commit_extra_header *extra = NULL;\n+\tstruct strbuf msg = STRBUF_INIT;\n+\tconst char *out_enc = get_commit_output_encoding();\n+\tconst char *message = repo_logmsg_reencode(repo, based_on,\n+\t\t\t\t\t\t   NULL, out_enc);\n+\tconst char *orig_message = NULL;\n+\tconst char *exclude_gpgsig[] = { \"gpgsig\", \"gpgsig-sha256\", NULL };\n+\n+\tcommit_list_insert(parent, &parents);\n+\textra = read_commit_extra_headers(based_on, exclude_gpgsig);\n+\tfind_commit_subject(message, &orig_message);\n+\tstrbuf_addstr(&msg, orig_message);\n+\tauthor = get_author(message);\n+\treset_ident_date();\n+\tif (commit_tree_extended(msg.buf, msg.len, &tree->object.oid, parents,\n+\t\t\t\t &ret, author, NULL, sign_commit, extra)) {\n+\t\terror(_(\"failed to write commit object\"));\n+\t\tgoto out;\n+\t}\n+\n+\tobj = parse_object(repo, &ret);\n+\n+out:\n+\trepo_unuse_commit_buffer(the_repository, based_on, message);\n+\tfree_commit_extra_headers(extra);\n+\tfree_commit_list(parents);\n+\tstrbuf_release(&msg);\n+\tfree(author);\n+\treturn (struct commit *)obj;\n+}\n+\n+struct ref_info {\n+\tstruct commit *onto;\n+\tstruct strset positive_refs;\n+\tstruct strset negative_refs;\n+\tint positive_refexprs;\n+\tint negative_refexprs;\n+};\n+\n+static void get_ref_information(struct repository *repo,\n+\t\t\t\tstruct rev_cmdline_info *cmd_info,\n+\t\t\t\tstruct ref_info *ref_info)\n+{\n+\tint i;\n+\n+\tref_info->onto = NULL;\n+\tstrset_init(&ref_info->positive_refs);\n+\tstrset_init(&ref_info->negative_refs);\n+\tref_info->positive_refexprs = 0;\n+\tref_info->negative_refexprs = 0;\n+\n+\t/*\n+\t * When the user specifies e.g.\n+\t *   git replay origin/main..mybranch\n+\t *   git replay ^origin/next mybranch1 mybranch2\n+\t * we want to be able to determine where to replay the commits.  In\n+\t * these examples, the branches are probably based on an old version\n+\t * of either origin/main or origin/next, so we want to replay on the\n+\t * newest version of that branch.  In contrast we would want to error\n+\t * out if they ran\n+\t *   git replay ^origin/master ^origin/next mybranch\n+\t *   git replay mybranch~2..mybranch\n+\t * the first of those because there's no unique base to choose, and\n+\t * the second because they'd likely just be replaying commits on top\n+\t * of the same commit and not making any difference.\n+\t */\n+\tfor (i = 0; i < cmd_info->nr; i++) {\n+\t\tstruct rev_cmdline_entry *e = cmd_info->rev + i;\n+\t\tstruct object_id oid;\n+\t\tconst char *refexpr = e->name;\n+\t\tchar *fullname = NULL;\n+\t\tint can_uniquely_dwim = 1;\n+\n+\t\tif (*refexpr == '^')\n+\t\t\trefexpr++;\n+\t\tif (repo_dwim_ref(repo, refexpr, strlen(refexpr), &oid, &fullname, 0) != 1)\n+\t\t\tcan_uniquely_dwim = 0;\n+\n+\t\tif (e->flags & BOTTOM) {\n+\t\t\tif (can_uniquely_dwim)\n+\t\t\t\tstrset_add(&ref_info->negative_refs, fullname);\n+\t\t\tif (!ref_info->negative_refexprs)\n+\t\t\t\tref_info->onto = lookup_commit_reference_gently(repo,\n+\t\t\t\t\t\t\t\t\t\t&e->item->oid, 1);\n+\t\t\tref_info->negative_refexprs++;\n+\t\t} else {\n+\t\t\tif (can_uniquely_dwim)\n+\t\t\t\tstrset_add(&ref_info->positive_refs, fullname);\n+\t\t\tref_info->positive_refexprs++;\n+\t\t}\n+\n+\t\tfree(fullname);\n+\t}\n+}\n+\n+static void set_up_replay_mode(struct repository *repo,\n+\t\t\t       struct rev_cmdline_info *cmd_info,\n+\t\t\t       const char *onto_name,\n+\t\t\t       char **advance_name,\n+\t\t\t       struct commit **onto,\n+\t\t\t       struct strset **update_refs)\n+{\n+\tstruct ref_info rinfo;\n+\n+\tget_ref_information(repo, cmd_info, &rinfo);\n+\tif (!rinfo.positive_refexprs)\n+\t\tdie(_(\"need some commits to replay\"));\n+\n+\tdie_for_incompatible_opt2(!!onto_name, \"--onto\",\n+\t\t\t\t  !!*advance_name, \"--advance\");\n+\tif (onto_name) {\n+\t\t*onto = peel_committish(repo, onto_name, \"--onto\");\n+\t\tif (rinfo.positive_refexprs <\n+\t\t    strset_get_size(&rinfo.positive_refs))\n+\t\t\tdie(_(\"all positive revisions given must be references\"));\n+\t\t*update_refs = xcalloc(1, sizeof(**update_refs));\n+\t\t**update_refs = rinfo.positive_refs;\n+\t\tmemset(&rinfo.positive_refs, 0, sizeof(**update_refs));\n+\t} else {\n+\t\tstruct object_id oid;\n+\t\tchar *fullname = NULL;\n+\n+\t\tif (!*advance_name)\n+\t\t\tBUG(\"expected either onto_name or *advance_name in this function\");\n+\n+\t\tif (repo_dwim_ref(repo, *advance_name, strlen(*advance_name),\n+\t\t\t     &oid, &fullname, 0) == 1) {\n+\t\t\tfree(*advance_name);\n+\t\t\t*advance_name = fullname;\n+\t\t} else {\n+\t\t\tdie(_(\"argument to --advance must be a reference\"));\n+\t\t}\n+\t\t*onto = peel_committish(repo, *advance_name, \"--advance\");\n+\t\tif (rinfo.positive_refexprs > 1)\n+\t\t\tdie(_(\"cannot advance target with multiple sources because ordering would be ill-defined\"));\n+\t}\n+\tstrset_clear(&rinfo.negative_refs);\n+\tstrset_clear(&rinfo.positive_refs);\n+}\n+\n+static struct commit *mapped_commit(kh_oid_map_t *replayed_commits,\n+\t\t\t\t    struct commit *commit,\n+\t\t\t\t    struct commit *fallback)\n+{\n+\tkhint_t pos = kh_get_oid_map(replayed_commits, commit->object.oid);\n+\tif (pos == kh_end(replayed_commits))\n+\t\treturn fallback;\n+\treturn kh_value(replayed_commits, pos);\n+}\n+\n+static struct commit *pick_regular_commit(struct repository *repo,\n+\t\t\t\t\t  struct commit *pickme,\n+\t\t\t\t\t  kh_oid_map_t *replayed_commits,\n+\t\t\t\t\t  struct commit *onto,\n+\t\t\t\t\t  struct merge_options *merge_opt,\n+\t\t\t\t\t  struct merge_result *result)\n+{\n+\tstruct commit *base, *replayed_base;\n+\tstruct tree *pickme_tree, *base_tree;\n+\n+\tbase = pickme->parents->item;\n+\treplayed_base = mapped_commit(replayed_commits, base, onto);\n+\n+\tresult->tree = repo_get_commit_tree(repo, replayed_base);\n+\tpickme_tree = repo_get_commit_tree(repo, pickme);\n+\tbase_tree = repo_get_commit_tree(repo, base);\n+\n+\tmerge_opt->branch1 = short_commit_name(repo, replayed_base);\n+\tmerge_opt->branch2 = short_commit_name(repo, pickme);\n+\tmerge_opt->ancestor = xstrfmt(\"parent of %s\", merge_opt->branch2);\n+\n+\tmerge_incore_nonrecursive(merge_opt,\n+\t\t\t\t  base_tree,\n+\t\t\t\t  result->tree,\n+\t\t\t\t  pickme_tree,\n+\t\t\t\t  result);\n+\n+\tfree((char*)merge_opt->ancestor);\n+\tmerge_opt->ancestor = NULL;\n+\tif (!result->clean)\n+\t\treturn NULL;\n+\treturn create_commit(repo, result->tree, pickme, replayed_base);\n+}\n+\n+void replay_result_release(struct replay_result *result)\n+{\n+\tfor (size_t i = 0; i < result->updates_nr; i++)\n+\t\tfree(result->updates[i].refname);\n+\tfree(result->updates);\n+}\n+\n+static void replay_result_queue_update(struct replay_result *result,\n+\t\t\t\t       const char *refname,\n+\t\t\t\t       const struct object_id *old_oid,\n+\t\t\t\t       const struct object_id *new_oid)\n+{\n+\tALLOC_GROW(result->updates, result->updates_nr + 1, result->updates_alloc);\n+\tresult->updates[result->updates_nr].refname = xstrdup(refname);\n+\tresult->updates[result->updates_nr].old_oid = *old_oid;\n+\tresult->updates[result->updates_nr].new_oid = *new_oid;\n+\tresult->updates_nr++;\n+}\n+\n+int replay_revisions(struct repository *repo, struct rev_info *revs,\n+\t\t     struct replay_revisions_options *opts,\n+\t\t     struct replay_result *out)\n+{\n+\tkh_oid_map_t *replayed_commits = NULL;\n+\tstruct strset *update_refs = NULL;\n+\tstruct commit *last_commit = NULL;\n+\tstruct commit *commit;\n+\tstruct commit *onto = NULL;\n+\tstruct merge_options merge_opt;\n+\tstruct merge_result result;\n+\tchar *advance;\n+\tint ret;\n+\n+\tadvance = xstrdup_or_null(opts->advance);\n+\tset_up_replay_mode(repo, &revs->cmdline, opts->onto, &advance,\n+\t\t\t   &onto, &update_refs);\n+\n+\t/* FIXME: Should allow replaying commits with the first as a root commit */\n+\n+\tif (prepare_revision_walk(revs) < 0) {\n+\t\tret = error(_(\"error preparing revisions\"));\n+\t\tgoto out;\n+\t}\n+\n+\tinit_basic_merge_options(&merge_opt, repo);\n+\tmemset(&result, 0, sizeof(result));\n+\tmerge_opt.show_rename_progress = 0;\n+\tlast_commit = onto;\n+\treplayed_commits = kh_init_oid_map();\n+\twhile ((commit = get_revision(revs))) {\n+\t\tconst struct name_decoration *decoration;\n+\t\tkhint_t pos;\n+\t\tint hr;\n+\n+\t\tif (!commit->parents)\n+\t\t\tdie(_(\"replaying down from root commit is not supported yet!\"));\n+\t\tif (commit->parents->next)\n+\t\t\tdie(_(\"replaying merge commits is not supported yet!\"));\n+\n+\t\tlast_commit = pick_regular_commit(repo, commit, replayed_commits,\n+\t\t\t\t\t\t  onto, &merge_opt, &result);\n+\t\tif (!last_commit)\n+\t\t\tbreak;\n+\n+\t\t/* Record commit -> last_commit mapping */\n+\t\tpos = kh_put_oid_map(replayed_commits, commit->object.oid, &hr);\n+\t\tif (hr == 0)\n+\t\t\tBUG(\"Duplicate rewritten commit: %s\\n\",\n+\t\t\t    oid_to_hex(&commit->object.oid));\n+\t\tkh_value(replayed_commits, pos) = last_commit;\n+\n+\t\t/* Update any necessary branches */\n+\t\tif (advance)\n+\t\t\tcontinue;\n+\t\tdecoration = get_name_decoration(&commit->object);\n+\t\tif (!decoration)\n+\t\t\tcontinue;\n+\t\twhile (decoration) {\n+\t\t\tif (decoration->type == DECORATION_REF_LOCAL &&\n+\t\t\t    (opts->contained || strset_contains(update_refs,\n+\t\t\t\t\t\t\t\tdecoration->name))) {\n+\t\t\t\treplay_result_queue_update(out, decoration->name,\n+\t\t\t\t\t\t\t   &commit->object.oid,\n+\t\t\t\t\t\t\t   &last_commit->object.oid);\n+\t\t\t}\n+\t\t\tdecoration = decoration->next;\n+\t\t}\n+\t}\n+\n+\tif (!result.clean) {\n+\t\tout->merge_conflict = true;\n+\t\tret = -1;\n+\t\tgoto out;\n+\t}\n+\n+\t/* In --advance mode, advance the target ref */\n+\tif (advance)\n+\t\treplay_result_queue_update(out, advance,\n+\t\t\t\t\t   &onto->object.oid,\n+\t\t\t\t\t   &last_commit->object.oid);\n+\n+\tret = 0;\n+\n+out:\n+\tif (update_refs) {\n+\t\tstrset_clear(update_refs);\n+\t\tfree(update_refs);\n+\t}\n+\tkh_destroy_oid_map(replayed_commits);\n+\tmerge_finalize(&merge_opt, &result);\n+\tfree(advance);\n+\treturn ret;\n+}\ndiff --git a/replay.h b/replay.h\nnew file mode 100644\nindex 0000000000..84bc8a7a5b\n--- /dev/null\n+++ b/replay.h\n@@ -0,0 +1,64 @@\n+#ifndef REPLAY_H\n+#define REPLAY_H\n+\n+#include \"hash.h\"\n+\n+struct repository;\n+struct rev_info;\n+\n+/*\n+ * A set of options that can be passed to `replay_revisions()`.\n+ */\n+struct replay_revisions_options {\n+\t/*\n+\t * Starting point at which to create the new commits; must be a branch\n+\t * name. The branch will be updated to point to the rewritten commits.\n+\t * This option is mutually exclusive with `onto`.\n+\t */\n+\tconst char *advance;\n+\n+\t/*\n+\t * Starting point at which to create the new commits; must be a\n+\t * committish. References pointing at decendants of `onto` will be\n+\t * updated to point to the new commits.\n+\t */\n+\t const char *onto;\n+\n+\t/*\n+\t * Update branches that point at commits in the given revision range.\n+\t * Requires `onto` to be set.\n+\t */\n+\tint contained;\n+};\n+\n+/* This struct is used as an out-parameter by `replay_revisions()`. */\n+struct replay_result {\n+\t/*\n+\t * The set of reference updates that are caused by replaying the\n+\t * commits.\n+\t */\n+\tstruct replay_ref_update {\n+\t\tchar *refname;\n+\t\tstruct object_id old_oid;\n+\t\tstruct object_id new_oid;\n+\t} *updates;\n+\tsize_t updates_nr, updates_alloc;\n+\n+\t/* Set to true in case the replay failed with a merge conflict. */\n+\tbool merge_conflict;\n+};\n+\n+void replay_result_release(struct replay_result *result);\n+\n+/*\n+ * Replay a set of commits onto a new location. Leaves both the working tree,\n+ * index and references untouched. Reference updates caused by the replay will\n+ * be recorded in the `updates` out pointer.\n+ *\n+ * Returns 0 on success, a negative error code otherwise.\n+ */\n+int replay_revisions(struct repository *repo, struct rev_info *revs,\n+\t\t     struct replay_revisions_options *opts,\n+\t\t     struct replay_result *out);\n+\n+#endif\n\n-- \n2.52.0.542.g9473a8513b.dirty\n\n"},{"id":"533330","messageId":"20260109-b4-pks-history-builtin-v9-3-8766101814c6@pks.im","threadId":"63986","inReplyTo":"20260109-b4-pks-history-builtin-v9-0-8766101814c6@pks.im","subject":"[PATCH v9 3/7] replay: small set of cleanups","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-01-09T08:35:38Z","receivedAt":"2026-01-09T08:35:52Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Perform a small set of cleanups so that the \"replay\" logic compiles with\n\"-Wsign-compare\" and doesn't use `the_repository` anymore. Note that\nthere are still some implicit dependencies on `the_repository`, e.g.\nbecause we use `get_commit_output_encoding()`.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n replay.c | 11 ++++-------\n 1 file changed, 4 insertions(+), 7 deletions(-)\n\ndiff --git a/replay.c b/replay.c\nindex fc7186ef09..313fb2a768 100644\n--- a/replay.c\n+++ b/replay.c\n@@ -1,5 +1,4 @@\n #define USE_THE_REPOSITORY_VARIABLE\n-#define DISABLE_SIGN_COMPARE_WARNINGS\n \n #include \"git-compat-util.h\"\n #include \"environment.h\"\n@@ -79,7 +78,7 @@ static struct commit *create_commit(struct repository *repo,\n \tobj = parse_object(repo, &ret);\n \n out:\n-\trepo_unuse_commit_buffer(the_repository, based_on, message);\n+\trepo_unuse_commit_buffer(repo, based_on, message);\n \tfree_commit_extra_headers(extra);\n \tfree_commit_list(parents);\n \tstrbuf_release(&msg);\n@@ -91,16 +90,14 @@ struct ref_info {\n \tstruct commit *onto;\n \tstruct strset positive_refs;\n \tstruct strset negative_refs;\n-\tint positive_refexprs;\n-\tint negative_refexprs;\n+\tsize_t positive_refexprs;\n+\tsize_t negative_refexprs;\n };\n \n static void get_ref_information(struct repository *repo,\n \t\t\t\tstruct rev_cmdline_info *cmd_info,\n \t\t\t\tstruct ref_info *ref_info)\n {\n-\tint i;\n-\n \tref_info->onto = NULL;\n \tstrset_init(&ref_info->positive_refs);\n \tstrset_init(&ref_info->negative_refs);\n@@ -122,7 +119,7 @@ static void get_ref_information(struct repository *repo,\n \t * the second because they'd likely just be replaying commits on top\n \t * of the same commit and not making any difference.\n \t */\n-\tfor (i = 0; i < cmd_info->nr; i++) {\n+\tfor (size_t i = 0; i < cmd_info->nr; i++) {\n \t\tstruct rev_cmdline_entry *e = cmd_info->rev + i;\n \t\tstruct object_id oid;\n \t\tconst char *refexpr = e->name;\n\n-- \n2.52.0.542.g9473a8513b.dirty\n\n"},{"id":"533331","messageId":"20260109-b4-pks-history-builtin-v9-4-8766101814c6@pks.im","threadId":"63986","inReplyTo":"20260109-b4-pks-history-builtin-v9-0-8766101814c6@pks.im","subject":"[PATCH v9 4/7] replay: yield the object ID of the final rewritten commit","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-01-09T08:35:39Z","receivedAt":"2026-01-09T08:35:54Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"In a subsequent commit we'll introduce a new git-history(1) command that\nuses the replay machinery to rewrite commits. One of its supported modes\nwill only want to update the \"HEAD\" reference, but that is not currently\nsupported by the replay machinery.\n\nAllow implementing this use case by exposing a `final_oid` field for the\nreference updates. This field will be set to the last commit that was\nrewritten, which is sufficient information for us to implement this mode\nin git-history(1).\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n replay.c |  2 ++\n replay.h | 16 ++++++++++++++++\n 2 files changed, 18 insertions(+)\n\ndiff --git a/replay.c b/replay.c\nindex 313fb2a768..20560e792b 100644\n--- a/replay.c\n+++ b/replay.c\n@@ -338,6 +338,8 @@ int replay_revisions(struct repository *repo, struct rev_info *revs,\n \t\t\t\t\t   &onto->object.oid,\n \t\t\t\t\t   &last_commit->object.oid);\n \n+\tout->final_oid = last_commit->object.oid;\n+\n \tret = 0;\n \n out:\ndiff --git a/replay.h b/replay.h\nindex 84bc8a7a5b..f8f9889112 100644\n--- a/replay.h\n+++ b/replay.h\n@@ -46,6 +46,22 @@ struct replay_result {\n \n \t/* Set to true in case the replay failed with a merge conflict. */\n \tbool merge_conflict;\n+\n+\t/*\n+\t * The final object ID that was rewritten. Note that this field has\n+\t * somewhat special semantics and may or may not be what you want:\n+\t *\n+\t *   - If no commits were rewritten it will remain uninitialized.\n+\t *\n+\t *   - If a thicket of branches is rewritten it is undefined in which\n+\t *     order those branches will be rewritten, and thus the final object\n+\t *     ID may point to a different commit than you'd expect.\n+\t *\n+\t * That being said, this field can still be useful when you know that\n+\t * you only replay a single strand of commits. In that case, the final\n+\t * commit will point to the tip of the rewritten strand of commits.\n+\t */\n+\tstruct object_id final_oid;\n };\n \n void replay_result_release(struct replay_result *result);\n\n-- \n2.52.0.542.g9473a8513b.dirty\n\n"},{"id":"533332","messageId":"20260109-b4-pks-history-builtin-v9-5-8766101814c6@pks.im","threadId":"63986","inReplyTo":"20260109-b4-pks-history-builtin-v9-0-8766101814c6@pks.im","subject":"[PATCH v9 5/7] wt-status: provide function to expose status for trees","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-01-09T08:35:40Z","receivedAt":"2026-01-09T08:35:57Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The \"wt-status\" subsystem is responsible for printing status information\naround the current state of the working tree. This most importantly\nincludes information around whether the working tree or the index have\nany changes.\n\nWe're about to introduce a new command where the changes in neither of\nthem are actually relevant to us. Instead, what we want is to format the\nchanges between two different trees. While it is a little bit of a\nstretch to add this as functionality to _working tree_ status, it\ndoesn't make any sense to open-code this functionality, either.\n\nImplement a new function `wt_status_collect_changes_trees()` that diffs\ntwo trees and formats the status accordingly. This function is not yet\nused, but will be in a subsequent commit.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n wt-status.c | 24 ++++++++++++++++++++++++\n wt-status.h |  9 +++++++++\n 2 files changed, 33 insertions(+)\n\ndiff --git a/wt-status.c b/wt-status.c\nindex e12adb26b9..95942399f8 100644\n--- a/wt-status.c\n+++ b/wt-status.c\n@@ -612,6 +612,30 @@ static void wt_status_collect_updated_cb(struct diff_queue_struct *q,\n \t}\n }\n \n+void wt_status_collect_changes_trees(struct wt_status *s,\n+\t\t\t\t     const struct object_id *old_treeish,\n+\t\t\t\t     const struct object_id *new_treeish)\n+{\n+\tstruct diff_options opts = { 0 };\n+\n+\trepo_diff_setup(s->repo, &opts);\n+\topts.output_format = DIFF_FORMAT_CALLBACK;\n+\topts.format_callback = wt_status_collect_updated_cb;\n+\topts.format_callback_data = s;\n+\topts.detect_rename = s->detect_rename >= 0 ? s->detect_rename : opts.detect_rename;\n+\topts.rename_limit = s->rename_limit >= 0 ? s->rename_limit : opts.rename_limit;\n+\topts.rename_score = s->rename_score >= 0 ? s->rename_score : opts.rename_score;\n+\topts.flags.recursive = 1;\n+\tdiff_setup_done(&opts);\n+\n+\tdiff_tree_oid(old_treeish, new_treeish, \"\", &opts);\n+\tdiffcore_std(&opts);\n+\tdiff_flush(&opts);\n+\twt_status_get_state(s->repo, &s->state, 0);\n+\n+\tdiff_free(&opts);\n+}\n+\n static void wt_status_collect_changes_worktree(struct wt_status *s)\n {\n \tstruct rev_info rev;\ndiff --git a/wt-status.h b/wt-status.h\nindex e40a27214a..e9fe32e98c 100644\n--- a/wt-status.h\n+++ b/wt-status.h\n@@ -153,6 +153,15 @@ void wt_status_add_cut_line(struct wt_status *s);\n void wt_status_prepare(struct repository *r, struct wt_status *s);\n void wt_status_print(struct wt_status *s);\n void wt_status_collect(struct wt_status *s);\n+\n+/*\n+ * Collect all changes between the two trees. Changes will be displayed as if\n+ * they were staged into the index.\n+ */\n+void wt_status_collect_changes_trees(struct wt_status *s,\n+\t\t\t\t     const struct object_id *old_treeish,\n+\t\t\t\t     const struct object_id *new_treeish);\n+\n /*\n  * Frees the buffers allocated by wt_status_collect.\n  */\n\n-- \n2.52.0.542.g9473a8513b.dirty\n\n"},{"id":"533333","messageId":"20260109-b4-pks-history-builtin-v9-6-8766101814c6@pks.im","threadId":"63986","inReplyTo":"20260109-b4-pks-history-builtin-v9-0-8766101814c6@pks.im","subject":"[PATCH v9 6/7] builtin: add new \"history\" command","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-01-09T08:35:41Z","receivedAt":"2026-01-09T08:36:00Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"When rewriting history via git-rebase(1) there are a few very common use\ncases:\n\n  - The ordering of two commits should be reversed.\n\n  - A commit should be split up into two commits.\n\n  - A commit should be dropped from the history completely.\n\n  - Multiple commits should be squashed into one.\n\n  - Editing an existing commit that is not the tip of the current\n    branch.\n\nWhile these operations are all doable, it often feels needlessly kludgey\nto do so by doing an interactive rebase, using the editor to say what\none wants, and then perform the actions. Also, some operations like\nsplitting up a commit into two are way more involved than that and\nrequire a whole series of commands.\n\nRebases also do not update dependent branches. The use of stacked\nbranches has grown quite common with competing version control systems\nlike Jujutsu though, so it clearly is a need that users have. While\nrebases _can_ serve this use case if one always works on the latest\nstacked branch, it is somewhat awkward and very easy to get wrong.\n\nAdd a new \"history\" command to plug these gaps. This command will have\nseveral different subcommands to imperatively rewrite history for common\nuse cases like the above.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n .gitignore                     |  1 +\n Documentation/git-history.adoc | 56 ++++++++++++++++++++++++++++++++++++++++++\n Documentation/meson.build      |  1 +\n Makefile                       |  1 +\n builtin.h                      |  1 +\n builtin/history.c              | 22 +++++++++++++++++\n command-list.txt               |  1 +\n git.c                          |  1 +\n meson.build                    |  1 +\n t/meson.build                  |  1 +\n t/t3450-history.sh             | 17 +++++++++++++\n 11 files changed, 103 insertions(+)\n\ndiff --git a/.gitignore b/.gitignore\nindex 78a45cb5be..24635cf2d6 100644\n--- a/.gitignore\n+++ b/.gitignore\n@@ -79,6 +79,7 @@\n /git-grep\n /git-hash-object\n /git-help\n+/git-history\n /git-hook\n /git-http-backend\n /git-http-fetch\ndiff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc\nnew file mode 100644\nindex 0000000000..5a9d931efc\n--- /dev/null\n+++ b/Documentation/git-history.adoc\n@@ -0,0 +1,56 @@\n+git-history(1)\n+==============\n+\n+NAME\n+----\n+git-history - EXPERIMENTAL: Rewrite history\n+\n+SYNOPSIS\n+--------\n+[synopsis]\n+git history [<options>]\n+\n+DESCRIPTION\n+-----------\n+\n+Rewrite history by rearranging or modifying specific commits in the\n+history.\n+\n+THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.\n+\n+This command is related to linkgit:git-rebase[1] in that both commands can be\n+used to rewrite history. There are a couple of major differences though:\n+\n+* linkgit:git-history[1] can work in a bare repository as it does not need to\n+  touch either the index or the worktree.\n+* linkgit:git-history[1] does not execute any linkgit:githooks[5] at the\n+  current point in time. This may change in the future.\n+* linkgit:git-history[1] by default updates all branches that are descendants\n+  of the original commit to point to the rewritten commit.\n+\n+Overall, linkgit:git-history[1] aims to provide a more opinionated way to modify\n+your commit history that is simpler to use compared to linkgit:git-rebase[1] in\n+general.\n+\n+If you want to reapply a range of commits onto a different base, or interactive\n+rebases if you want to edit a range of commits.\n+\n+LIMITATIONS\n+-----------\n+\n+This command does not (yet) work with histories that contain merges. You\n+should use linkgit:git-rebase[1] with the `--rebase-merges` flag instead.\n+\n+Furthermore, the command does not support operations that can result in merge\n+conflicts. This limitation is by design as history rewrites are not intended to\n+be stateful operations. The limitation can be lifted once (if) Git learns about\n+first-class conflicts.\n+\n+COMMANDS\n+--------\n+\n+Several commands are available to rewrite history in different ways:\n+\n+GIT\n+---\n+Part of the linkgit:git[1] suite\ndiff --git a/Documentation/meson.build b/Documentation/meson.build\nindex f02dbc20cb..fd2e8cc02d 100644\n--- a/Documentation/meson.build\n+++ b/Documentation/meson.build\n@@ -64,6 +64,7 @@ manpages = {\n   'git-gui.adoc' : 1,\n   'git-hash-object.adoc' : 1,\n   'git-help.adoc' : 1,\n+  'git-history.adoc' : 1,\n   'git-hook.adoc' : 1,\n   'git-http-backend.adoc' : 1,\n   'git-http-fetch.adoc' : 1,\ndiff --git a/Makefile b/Makefile\nindex 1c64a5d2ae..c0569ed8e4 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -1418,6 +1418,7 @@ BUILTIN_OBJS += builtin/get-tar-commit-id.o\n BUILTIN_OBJS += builtin/grep.o\n BUILTIN_OBJS += builtin/hash-object.o\n BUILTIN_OBJS += builtin/help.o\n+BUILTIN_OBJS += builtin/history.o\n BUILTIN_OBJS += builtin/hook.o\n BUILTIN_OBJS += builtin/index-pack.o\n BUILTIN_OBJS += builtin/init-db.o\ndiff --git a/builtin.h b/builtin.h\nindex 1b35565fbd..93c91d07d4 100644\n--- a/builtin.h\n+++ b/builtin.h\n@@ -172,6 +172,7 @@ int cmd_get_tar_commit_id(int argc, const char **argv, const char *prefix, struc\n int cmd_grep(int argc, const char **argv, const char *prefix, struct repository *repo);\n int cmd_hash_object(int argc, const char **argv, const char *prefix, struct repository *repo);\n int cmd_help(int argc, const char **argv, const char *prefix, struct repository *repo);\n+int cmd_history(int argc, const char **argv, const char *prefix, struct repository *repo);\n int cmd_hook(int argc, const char **argv, const char *prefix, struct repository *repo);\n int cmd_index_pack(int argc, const char **argv, const char *prefix, struct repository *repo);\n int cmd_init_db(int argc, const char **argv, const char *prefix, struct repository *repo);\ndiff --git a/builtin/history.c b/builtin/history.c\nnew file mode 100644\nindex 0000000000..f6fe32610b\n--- /dev/null\n+++ b/builtin/history.c\n@@ -0,0 +1,22 @@\n+#include \"builtin.h\"\n+#include \"gettext.h\"\n+#include \"parse-options.h\"\n+\n+int cmd_history(int argc,\n+\t\tconst char **argv,\n+\t\tconst char *prefix,\n+\t\tstruct repository *repo UNUSED)\n+{\n+\tconst char * const usage[] = {\n+\t\tN_(\"git history [<options>]\"),\n+\t\tNULL,\n+\t};\n+\tstruct option options[] = {\n+\t\tOPT_END(),\n+\t};\n+\n+\targc = parse_options(argc, argv, prefix, options, usage, 0);\n+\tif (argc)\n+\t\tusagef(\"unrecognized argument: %s\", argv[0]);\n+\treturn 0;\n+}\ndiff --git a/command-list.txt b/command-list.txt\nindex accd3d0c4b..f9005cf459 100644\n--- a/command-list.txt\n+++ b/command-list.txt\n@@ -115,6 +115,7 @@ git-grep                                mainporcelain           info\n git-gui                                 mainporcelain\n git-hash-object                         plumbingmanipulators\n git-help                                ancillaryinterrogators          complete\n+git-history                             mainporcelain           history\n git-hook                                purehelpers\n git-http-backend                        synchingrepositories\n git-http-fetch                          synchelpers\ndiff --git a/git.c b/git.c\nindex c5fad56813..744cb6527e 100644\n--- a/git.c\n+++ b/git.c\n@@ -586,6 +586,7 @@ static struct cmd_struct commands[] = {\n \t{ \"grep\", cmd_grep, RUN_SETUP_GENTLY },\n \t{ \"hash-object\", cmd_hash_object },\n \t{ \"help\", cmd_help },\n+\t{ \"history\", cmd_history, RUN_SETUP },\n \t{ \"hook\", cmd_hook, RUN_SETUP },\n \t{ \"index-pack\", cmd_index_pack, RUN_SETUP_GENTLY | NO_PARSEOPT },\n \t{ \"init\", cmd_init_db },\ndiff --git a/meson.build b/meson.build\nindex a5a4e99b25..3a1d12caa4 100644\n--- a/meson.build\n+++ b/meson.build\n@@ -610,6 +610,7 @@ builtin_sources = [\n   'builtin/grep.c',\n   'builtin/hash-object.c',\n   'builtin/help.c',\n+  'builtin/history.c',\n   'builtin/hook.c',\n   'builtin/index-pack.c',\n   'builtin/init-db.c',\ndiff --git a/t/meson.build b/t/meson.build\nindex 459c52a489..73006b095a 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -387,6 +387,7 @@ integration_tests = [\n   't3436-rebase-more-options.sh',\n   't3437-rebase-fixup-options.sh',\n   't3438-rebase-broken-files.sh',\n+  't3450-history.sh',\n   't3500-cherry.sh',\n   't3501-revert-cherry-pick.sh',\n   't3502-cherry-pick-merge.sh',\ndiff --git a/t/t3450-history.sh b/t/t3450-history.sh\nnew file mode 100755\nindex 0000000000..417c343d43\n--- /dev/null\n+++ b/t/t3450-history.sh\n@@ -0,0 +1,17 @@\n+#!/bin/sh\n+\n+test_description='tests for git-history command'\n+\n+. ./test-lib.sh\n+\n+test_expect_success 'does nothing without any arguments' '\n+\tgit history >out 2>&1 &&\n+\ttest_must_be_empty out\n+'\n+\n+test_expect_success 'raises an error with unknown argument' '\n+\ttest_must_fail git history garbage 2>err &&\n+\ttest_grep \"unrecognized argument: garbage\" err\n+'\n+\n+test_done\n\n-- \n2.52.0.542.g9473a8513b.dirty\n\n"},{"id":"533334","messageId":"20260109-b4-pks-history-builtin-v9-7-8766101814c6@pks.im","threadId":"63986","inReplyTo":"20260109-b4-pks-history-builtin-v9-0-8766101814c6@pks.im","subject":"[PATCH v9 7/7] builtin/history: implement \"reword\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-01-09T08:35:42Z","receivedAt":"2026-01-09T08:36:03Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Implement a new \"reword\" subcommand for git-history(1). This subcommand\nis similar to the user performing an interactive rebase with a single\ncommit changed to use the \"reword\" instruction.\n\nThe \"reword\" subcommand is built on top of the replay subsystem\ninstead of the sequencer. This leads to some major differences compared\nto git-rebase(1):\n\n  - We do not check out the commit that is to be reworded and instead\n    perform the operation in-memory. This has the obvious benefit of\n    being significantly faster compared to git-rebase(1), but even more\n    importantly it allows the user to rewrite history even if there are\n    local changes in the working tree or in the index.\n\n  - We do not execute any hooks, even though we leave some room for\n    changing this in the future.\n\n  - By default, all local branches that contain the commit will be\n    rewritten. This especially helps with workflows that use stacked\n    branches.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/git-history.adoc |  23 ++-\n builtin/history.c              | 392 ++++++++++++++++++++++++++++++++++++++++-\n replay.c                       |   5 +-\n t/meson.build                  |   1 +\n t/t3450-history.sh             |   6 +-\n t/t3451-history-reword.sh      | 344 ++++++++++++++++++++++++++++++++++++\n 6 files changed, 758 insertions(+), 13 deletions(-)\n\ndiff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc\nindex 5a9d931efc..4eea317e5c 100644\n--- a/Documentation/git-history.adoc\n+++ b/Documentation/git-history.adoc\n@@ -8,7 +8,7 @@ git-history - EXPERIMENTAL: Rewrite history\n SYNOPSIS\n --------\n [synopsis]\n-git history [<options>]\n+git history reword <commit> [--ref-action=(branches|head|print)]\n \n DESCRIPTION\n -----------\n@@ -32,8 +32,9 @@ Overall, linkgit:git-history[1] aims to provide a more opinionated way to modify\n your commit history that is simpler to use compared to linkgit:git-rebase[1] in\n general.\n \n-If you want to reapply a range of commits onto a different base, or interactive\n-rebases if you want to edit a range of commits.\n+Use linkgit:git-rebase[1] if you want to reapply a range of commits onto a\n+different base, or interactive rebases if you want to edit a range of commits\n+at once.\n \n LIMITATIONS\n -----------\n@@ -51,6 +52,22 @@ COMMANDS\n \n Several commands are available to rewrite history in different ways:\n \n+`reword <commit>`::\n+\tRewrite the commit message of the specified commit. All the other\n+\tdetails of this commit remain unchanged. This command will spawn an\n+\teditor with the current message of that commit.\n+\n+OPTIONS\n+-------\n+\n+`--ref-action=(branches|head|print)`::\n+\tControl which references will be updated by the command, if any. With\n+\t`branches`, all local branches that point to commits which are\n+\tdecendants of the original commit will be rewritten. With `head`, only\n+\tthe current `HEAD` reference will be rewritten. With `print`, all\n+\tupdates as they would be performed with `branches` are printed in a\n+\tformat that can be consumed by linkgit:git-update-ref[1].\n+\n GIT\n ---\n Part of the linkgit:git[1] suite\ndiff --git a/builtin/history.c b/builtin/history.c\nindex f6fe32610b..28db6fc5b9 100644\n--- a/builtin/history.c\n+++ b/builtin/history.c\n@@ -1,22 +1,404 @@\n+#define USE_THE_REPOSITORY_VARIABLE\n+\n #include \"builtin.h\"\n+#include \"commit.h\"\n+#include \"commit-reach.h\"\n+#include \"config.h\"\n+#include \"editor.h\"\n+#include \"environment.h\"\n #include \"gettext.h\"\n+#include \"hex.h\"\n #include \"parse-options.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 \"wt-status.h\"\n+\n+#define GIT_HISTORY_REWORD_USAGE \\\n+\tN_(\"git history reword <commit> [--ref-action=(branches|head|print)]\")\n+\n+static void change_data_free(void *util, const char *str UNUSED)\n+{\n+\tstruct wt_status_change_data *d = util;\n+\tfree(d->rename_source);\n+\tfree(d);\n+}\n+\n+static int fill_commit_message(struct repository *repo,\n+\t\t\t       const struct object_id *old_tree,\n+\t\t\t       const struct object_id *new_tree,\n+\t\t\t       const char *default_message,\n+\t\t\t       const char *action,\n+\t\t\t       struct strbuf *out)\n+{\n+\tconst char *path = git_path_commit_editmsg();\n+\tconst char *hint =\n+\t\t_(\"Please enter the commit message for the %s changes.\"\n+\t\t  \" Lines starting\\nwith '%s' will be ignored, and an\"\n+\t\t  \" empty message aborts the commit.\\n\");\n+\tstruct wt_status s;\n+\n+\tstrbuf_addstr(out, default_message);\n+\tstrbuf_addch(out, '\\n');\n+\tstrbuf_commented_addf(out, comment_line_str, hint, action, comment_line_str);\n+\twrite_file_buf(path, out->buf, out->len);\n+\n+\twt_status_prepare(repo, &s);\n+\tFREE_AND_NULL(s.branch);\n+\ts.ahead_behind_flags = AHEAD_BEHIND_QUICK;\n+\ts.commit_template = 1;\n+\ts.colopts = 0;\n+\ts.display_comment_prefix = 1;\n+\ts.hints = 0;\n+\ts.use_color = 0;\n+\ts.whence = FROM_COMMIT;\n+\ts.committable = 1;\n+\n+\ts.fp = fopen(git_path_commit_editmsg(), \"a\");\n+\tif (!s.fp)\n+\t\treturn error_errno(_(\"could not open '%s'\"), git_path_commit_editmsg());\n+\n+\twt_status_collect_changes_trees(&s, old_tree, new_tree);\n+\twt_status_print(&s);\n+\twt_status_collect_free_buffers(&s);\n+\tstring_list_clear_func(&s.change, change_data_free);\n+\n+\tstrbuf_reset(out);\n+\tif (launch_editor(path, out, NULL)) {\n+\t\tfprintf(stderr, _(\"Aborting commit as launching the editor failed.\\n\"));\n+\t\treturn -1;\n+\t}\n+\tstrbuf_stripspace(out, comment_line_str);\n+\n+\tcleanup_message(out, COMMIT_MSG_CLEANUP_ALL, 0);\n+\n+\tif (!out->len) {\n+\t\tfprintf(stderr, _(\"Aborting commit due to empty commit message.\\n\"));\n+\t\treturn -1;\n+\t}\n+\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+{\n+\tconst char *exclude_gpgsig[] = { \"gpgsig\", \"gpgsig-sha256\", NULL };\n+\tconst char *original_message, *original_body, *ptr;\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+\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+\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+\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+\t\t\t\t   NULL, NULL, original_extra_headers);\n+\tif (ret < 0)\n+\t\tgoto out;\n+\n+\t*out = lookup_commit_or_die(&rewritten_commit_oid, \"rewritten commit\");\n+\n+out:\n+\tfree_commit_extra_headers(original_extra_headers);\n+\tstrbuf_release(&commit_message);\n+\tfree(original_author);\n+\treturn ret;\n+}\n+\n+enum ref_action {\n+\tREF_ACTION_DEFAULT,\n+\tREF_ACTION_BRANCHES,\n+\tREF_ACTION_HEAD,\n+\tREF_ACTION_PRINT,\n+};\n+\n+static int parse_ref_action(const struct option *opt, const char *value, int unset)\n+{\n+\tenum ref_action *action = opt->value;\n+\n+\tBUG_ON_OPT_NEG_NOARG(unset, value);\n+\tif (!strcmp(value, \"branches\")) {\n+\t\t*action = REF_ACTION_BRANCHES;\n+\t} else if (!strcmp(value, \"head\")) {\n+\t\t*action = REF_ACTION_HEAD;\n+\t} else if (!strcmp(value, \"print\")) {\n+\t\t*action = REF_ACTION_PRINT;\n+\t} else {\n+\t\treturn error(_(\"%s expects one of 'branches', 'head' or 'print'\"),\n+\t\t\t     opt->long_name);\n+\t}\n+\n+\treturn 0;\n+}\n+\n+static int handle_reference_updates(enum ref_action action,\n+\t\t\t\t    struct repository *repo,\n+\t\t\t\t    struct commit *original,\n+\t\t\t\t    struct commit *rewritten,\n+\t\t\t\t    const char *reflog_msg)\n+{\n+\tconst struct name_decoration *decoration;\n+\tstruct replay_revisions_options opts = { 0 };\n+\tstruct replay_result result = {\n+\t\t.final_oid = rewritten->object.oid,\n+\t};\n+\tstruct ref_transaction *transaction = NULL;\n+\tstruct strvec args = STRVEC_INIT;\n+\tstruct strbuf err = STRBUF_INIT;\n+\tstruct commit *head = NULL;\n+\tstruct rev_info revs;\n+\tchar hex[GIT_MAX_HEXSZ + 1];\n+\tint ret;\n+\n+\trepo_init_revisions(repo, &revs, NULL);\n+\tstrvec_push(&args, \"ignored\");\n+\tstrvec_push(&args, \"--reverse\");\n+\tstrvec_push(&args, \"--topo-order\");\n+\tstrvec_push(&args, \"--full-history\");\n+\n+\t/* We only want to see commits that are descendants of the old commit. */\n+\tstrvec_pushf(&args, \"--ancestry-path=%s\",\n+\t\t     oid_to_hex(&original->object.oid));\n+\n+\t/*\n+\t * Ancestry path may also show ancestors of the old commit, but we\n+\t * don't want to see those, either.\n+\t */\n+\tstrvec_pushf(&args, \"^%s\", oid_to_hex(&original->object.oid));\n+\n+\t/*\n+\t * When we're asked to update HEAD we need to verify that the commit\n+\t * that we want to rewrite is actually an ancestor of it and, if so,\n+\t * update it. Otherwise we'll update (or print) all descendant\n+\t * branches.\n+\t */\n+\tif (action == REF_ACTION_HEAD) {\n+\t\tstruct commit_list *from_list = NULL;\n+\n+\t\thead = lookup_commit_reference_by_name(\"HEAD\");\n+\t\tif (!head) {\n+\t\t\tret = error(_(\"cannot look up HEAD\"));\n+\t\t\tgoto out;\n+\t\t}\n+\n+\t\tcommit_list_insert(original, &from_list);\n+\t\tret = repo_is_descendant_of(repo, head, from_list);\n+\t\tfree_commit_list(from_list);\n+\n+\t\tif (ret < 0) {\n+\t\t\tret = error(_(\"cannot determine descendance\"));\n+\t\t\tgoto out;\n+\t\t} else if (!ret) {\n+\t\t\tret = error(_(\"rewritten commit must be an ancestor \"\n+\t\t\t\t      \"of HEAD when using --ref-action=head\"));\n+\t\t\tgoto out;\n+\t\t}\n+\n+\t\tstrvec_push(&args, oid_to_hex(&head->object.oid));\n+\t} else {\n+\t\tstrvec_push(&args, \"--branches\");\n+\t}\n+\n+\tsetup_revisions_from_strvec(&args, &revs, NULL);\n+\tif (revs.nr)\n+\t\tBUG(\"revisions were set up with invalid argument '%s'\", args.v[0]);\n+\n+\topts.onto = oid_to_hex_r(hex, &rewritten->object.oid);\n+\n+\tret = replay_revisions(repo, &revs, &opts, &result);\n+\tif (ret)\n+\t\tgoto out;\n+\n+\tswitch (action) {\n+\tcase REF_ACTION_DEFAULT:\n+\tcase REF_ACTION_BRANCHES:\n+\t\ttransaction = ref_store_transaction_begin(get_main_ref_store(repo), 0, &err);\n+\t\tif (!transaction) {\n+\t\t\tret = error(_(\"failed to begin ref transaction: %s\"), err.buf);\n+\t\t\tgoto out;\n+\t\t}\n+\n+\t\tfor (size_t i = 0; i < result.updates_nr; i++) {\n+\t\t\tret = ref_transaction_update(transaction,\n+\t\t\t\t\t\t     result.updates[i].refname,\n+\t\t\t\t\t\t     &result.updates[i].new_oid,\n+\t\t\t\t\t\t     &result.updates[i].old_oid,\n+\t\t\t\t\t\t     NULL, NULL, 0, reflog_msg, &err);\n+\t\t\tif (ret) {\n+\t\t\t\tret = error(_(\"failed to update ref '%s': %s\"),\n+\t\t\t\t\t    result.updates[i].refname, err.buf);\n+\t\t\t\tgoto out;\n+\t\t\t}\n+\t\t}\n+\n+\t\t/*\n+\t\t * `replay_revisions()` only updates references that are\n+\t\t * ancestors of `rewritten`, so we need to manually\n+\t\t * handle updating references that point to `original`.\n+\t\t */\n+\t\tfor (decoration = get_name_decoration(&original->object);\n+\t\t     decoration;\n+\t\t     decoration = decoration->next)\n+\t\t{\n+\t\t\tif (decoration->type != DECORATION_REF_LOCAL)\n+\t\t\t\tcontinue;\n+\n+\t\t\tret = ref_transaction_update(transaction,\n+\t\t\t\t\t\t     decoration->name,\n+\t\t\t\t\t\t     &rewritten->object.oid,\n+\t\t\t\t\t\t     &original->object.oid,\n+\t\t\t\t\t\t     NULL, NULL, 0, reflog_msg, &err);\n+\t\t\tif (ret) {\n+\t\t\t\tret = error(_(\"failed to update ref '%s': %s\"),\n+\t\t\t\t\t    decoration->name, err.buf);\n+\t\t\t\tgoto out;\n+\t\t\t}\n+\t\t}\n+\n+\t\tif (ref_transaction_commit(transaction, &err)) {\n+\t\t\tret = error(_(\"failed to commit ref transaction: %s\"), err.buf);\n+\t\t\tgoto out;\n+\t\t}\n+\n+\t\tbreak;\n+\tcase REF_ACTION_HEAD:\n+\t\tret = refs_update_ref(get_main_ref_store(repo), reflog_msg, \"HEAD\",\n+\t\t\t\t      &result.final_oid, &head->object.oid, 0,\n+\t\t\t\t      UPDATE_REFS_MSG_ON_ERR);\n+\t\tif (ret)\n+\t\t\tgoto out;\n+\t\tbreak;\n+\tcase REF_ACTION_PRINT:\n+\t\tfor (size_t i = 0; i < result.updates_nr; i++)\n+\t\t\tprintf(\"update %s %s %s\\n\",\n+\t\t\t       result.updates[i].refname,\n+\t\t\t       oid_to_hex(&result.updates[i].new_oid),\n+\t\t\t       oid_to_hex(&result.updates[i].old_oid));\n+\t\tbreak;\n+\tdefault:\n+\t\tBUG(\"unsupported ref action %d\", action);\n+\t}\n+\n+\tret = 0;\n+\n+out:\n+\tref_transaction_free(transaction);\n+\treplay_result_release(&result);\n+\trelease_revisions(&revs);\n+\tstrbuf_release(&err);\n+\tstrvec_clear(&args);\n+\treturn ret;\n+}\n+\n+static int cmd_history_reword(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_REWORD_USAGE,\n+\t\tNULL,\n+\t};\n+\tenum ref_action action = REF_ACTION_DEFAULT;\n+\tstruct option options[] = {\n+\t\tOPT_CALLBACK_F(0, \"ref-action\", &action, N_(\"<action>\"),\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_END(),\n+\t};\n+\tstruct strbuf reflog_msg = STRBUF_INIT;\n+\tstruct commit *original, *rewritten;\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 single revision\"));\n+\t\tgoto out;\n+\t}\n+\trepo_config(repo, git_default_config, NULL);\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 = commit_tree_with_edited_message(repo, \"reworded\", original, &rewritten);\n+\tif (ret < 0) {\n+\t\tret = error(_(\"failed writing reworded commit\"));\n+\t\tgoto out;\n+\t}\n+\n+\tstrbuf_addf(&reflog_msg, \"reword: updating %s\", argv[0]);\n+\n+\tret = handle_reference_updates(action, repo, original, rewritten,\n+\t\t\t\t       reflog_msg.buf);\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+\treturn ret;\n+}\n \n int cmd_history(int argc,\n \t\tconst char **argv,\n \t\tconst char *prefix,\n-\t\tstruct repository *repo UNUSED)\n+\t\tstruct repository *repo)\n {\n \tconst char * const usage[] = {\n-\t\tN_(\"git history [<options>]\"),\n+\t\tGIT_HISTORY_REWORD_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_END(),\n \t};\n \n \targc = parse_options(argc, argv, prefix, options, usage, 0);\n-\tif (argc)\n-\t\tusagef(\"unrecognized argument: %s\", argv[0]);\n-\treturn 0;\n+\treturn fn(argc, argv, prefix, repo);\n }\ndiff --git a/replay.c b/replay.c\nindex 20560e792b..74e45ed27a 100644\n--- a/replay.c\n+++ b/replay.c\n@@ -266,7 +266,9 @@ int replay_revisions(struct repository *repo, struct rev_info *revs,\n \tstruct commit *commit;\n \tstruct commit *onto = NULL;\n \tstruct merge_options merge_opt;\n-\tstruct merge_result result;\n+\tstruct merge_result result = {\n+\t\t.clean = 1,\n+\t};\n \tchar *advance;\n \tint ret;\n \n@@ -282,7 +284,6 @@ int replay_revisions(struct repository *repo, struct rev_info *revs,\n \t}\n \n \tinit_basic_merge_options(&merge_opt, repo);\n-\tmemset(&result, 0, sizeof(result));\n \tmerge_opt.show_rename_progress = 0;\n \tlast_commit = onto;\n \treplayed_commits = kh_init_oid_map();\ndiff --git a/t/meson.build b/t/meson.build\nindex 73006b095a..c9f92450dc 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -388,6 +388,7 @@ integration_tests = [\n   't3437-rebase-fixup-options.sh',\n   't3438-rebase-broken-files.sh',\n   't3450-history.sh',\n+  't3451-history-reword.sh',\n   't3500-cherry.sh',\n   't3501-revert-cherry-pick.sh',\n   't3502-cherry-pick-merge.sh',\ndiff --git a/t/t3450-history.sh b/t/t3450-history.sh\nindex 417c343d43..f513463b92 100755\n--- a/t/t3450-history.sh\n+++ b/t/t3450-history.sh\n@@ -5,13 +5,13 @@ test_description='tests for git-history command'\n . ./test-lib.sh\n \n test_expect_success 'does nothing without any arguments' '\n-\tgit history >out 2>&1 &&\n-\ttest_must_be_empty out\n+\ttest_must_fail git history 2>err &&\n+\ttest_grep \"need a subcommand\" err\n '\n \n test_expect_success 'raises an error with unknown argument' '\n \ttest_must_fail git history garbage 2>err &&\n-\ttest_grep \"unrecognized argument: garbage\" err\n+\ttest_grep \"unknown subcommand: .garbage.\" err\n '\n \n test_done\ndiff --git a/t/t3451-history-reword.sh b/t/t3451-history-reword.sh\nnew file mode 100755\nindex 0000000000..cd5883051d\n--- /dev/null\n+++ b/t/t3451-history-reword.sh\n@@ -0,0 +1,344 @@\n+#!/bin/sh\n+\n+test_description='tests for git-history reword subcommand'\n+\n+. ./test-lib.sh\n+. \"$TEST_DIRECTORY/lib-log-graph.sh\"\n+\n+reword_with_message () {\n+\tcat >message &&\n+\twrite_script fake-editor.sh <<-\\EOF &&\n+\tcp message \"$1\"\n+\tEOF\n+\ttest_set_editor \"$(pwd)\"/fake-editor.sh &&\n+\tgit history reword \"$@\" &&\n+\trm fake-editor.sh message\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+test_expect_success 'can reword tip of a branch' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit first &&\n+\t\ttest_commit second &&\n+\t\ttest_commit third &&\n+\n+\t\tgit symbolic-ref HEAD >expect &&\n+\t\treword_with_message HEAD <<-EOF &&\n+\t\tthird reworded\n+\t\tEOF\n+\t\tgit symbolic-ref HEAD >actual &&\n+\t\ttest_cmp expect actual &&\n+\n+\t\texpect_log <<-\\EOF &&\n+\t\tthird reworded\n+\t\tsecond\n+\t\tfirst\n+\t\tEOF\n+\n+\t\tgit reflog >reflog &&\n+\t\ttest_grep \"reword: updating HEAD\" reflog\n+\t)\n+'\n+\n+test_expect_success 'can reword commit in the middle' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit first &&\n+\t\ttest_commit second &&\n+\t\ttest_commit third &&\n+\n+\t\tgit symbolic-ref HEAD >expect &&\n+\t\treword_with_message HEAD~ <<-EOF &&\n+\t\tsecond reworded\n+\t\tEOF\n+\t\tgit symbolic-ref HEAD >actual &&\n+\t\ttest_cmp expect actual &&\n+\n+\t\texpect_log <<-\\EOF\n+\t\tthird\n+\t\tsecond reworded\n+\t\tfirst\n+\t\tEOF\n+\t)\n+'\n+\n+test_expect_success 'can reword root commit' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit first &&\n+\t\ttest_commit second &&\n+\t\ttest_commit third &&\n+\t\treword_with_message HEAD~2 <<-EOF &&\n+\t\tfirst reworded\n+\t\tEOF\n+\n+\t\texpect_log <<-\\EOF\n+\t\tthird\n+\t\tsecond\n+\t\tfirst reworded\n+\t\tEOF\n+\t)\n+'\n+\n+test_expect_success 'can reword in a bare repo' '\n+\ttest_when_finished \"rm -rf repo repo.git\" &&\n+\tgit init repo &&\n+\ttest_commit -C repo first &&\n+\tgit clone --bare repo repo.git &&\n+\t(\n+\t\tcd repo.git &&\n+\t\treword_with_message HEAD <<-EOF &&\n+\t\treworded\n+\t\tEOF\n+\n+\t\texpect_log <<-\\EOF\n+\t\treworded\n+\t\tEOF\n+\t)\n+'\n+\n+test_expect_success 'can reword a commit on a different 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 theirs &&\n+\t\ttest_commit ours &&\n+\t\tgit switch theirs &&\n+\t\ttest_commit theirs &&\n+\n+\t\tgit rev-parse ours >ours-before &&\n+\t\treword_with_message theirs <<-EOF &&\n+\t\tReworded theirs\n+\t\tEOF\n+\t\tgit rev-parse ours >ours-after &&\n+\t\ttest_cmp ours-before ours-after &&\n+\n+\t\texpect_graph --branches <<-\\EOF\n+\t\t* Reworded theirs\n+\t\t| * ours\n+\t\t|/\n+\t\t* base\n+\t\tEOF\n+\t)\n+'\n+\n+test_expect_success 'can reword a merge 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\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+\n+\t\t# It is not possible to replay merge commits embedded in the\n+\t\t# history (yet).\n+\t\ttest_must_fail git history reword HEAD~ 2>err &&\n+\t\ttest_grep \"replaying merge commits is not supported yet\" err &&\n+\n+\t\t# But it is possible to reword a merge commit directly.\n+\t\treword_with_message HEAD <<-EOF &&\n+\t\tReworded merge commit\n+\t\tEOF\n+\t\texpect_graph <<-\\EOF\n+\t\t*   Reworded merge commit\n+\t\t|\\\n+\t\t| * theirs\n+\t\t* | ours\n+\t\t|/\n+\t\t* base\n+\t\tEOF\n+\t)\n+'\n+\n+test_expect_success '--ref-action=print prints ref updates without modifying repo' '\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\tgit branch branch &&\n+\t\ttest_commit ours &&\n+\t\tgit switch branch &&\n+\t\ttest_commit theirs &&\n+\n+\t\tgit refs list >refs-expect &&\n+\t\treword_with_message --ref-action=print base >updates <<-\\EOF &&\n+\t\treworded commit\n+\t\tEOF\n+\t\tgit refs list >refs-actual &&\n+\t\ttest_cmp refs-expect refs-actual &&\n+\n+\t\ttest_grep \"update refs/heads/branch\" updates &&\n+\t\ttest_grep \"update refs/heads/main\" updates &&\n+\t\tgit update-ref --stdin <updates &&\n+\t\texpect_log --branches <<-\\EOF\n+\t\ttheirs\n+\t\tours\n+\t\treworded commit\n+\t\tEOF\n+\t)\n+'\n+\n+test_expect_success '--ref-action=head updates only HEAD' '\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\tgit branch branch &&\n+\t\ttest_commit theirs &&\n+\t\tgit switch branch &&\n+\t\ttest_commit ours &&\n+\n+\t\t# When told to update HEAD, only, the command will refuse to\n+\t\t# rewrite commits that are not an ancestor of HEAD.\n+\t\ttest_must_fail git history reword --ref-action=head theirs 2>err &&\n+\t\ttest_grep \"rewritten commit must be an ancestor of HEAD\" err &&\n+\n+\t\treword_with_message --ref-action=head base >updates <<-\\EOF &&\n+\t\treworded base\n+\t\tEOF\n+\t\texpect_log HEAD <<-\\EOF &&\n+\t\tours\n+\t\treworded base\n+\t\tEOF\n+\t\texpect_log main <<-\\EOF\n+\t\ttheirs\n+\t\tbase\n+\t\tEOF\n+\t)\n+'\n+\n+test_expect_success 'editor shows proper status' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit first &&\n+\n+\t\twrite_script fake-editor.sh <<-\\EOF &&\n+\t\tcp \"$1\" . &&\n+\t\tprintf \"\\namend a comment\\n\" >>\"$1\"\n+\t\tEOF\n+\t\ttest_set_editor \"$(pwd)\"/fake-editor.sh &&\n+\t\tgit history reword HEAD &&\n+\n+\t\tcat >expect <<-EOF &&\n+\t\tfirst\n+\n+\t\t# Please enter the commit message for the reworded 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:   first.t\n+\t\t#\n+\t\tEOF\n+\t\ttest_cmp expect COMMIT_EDITMSG &&\n+\n+\t\ttest_commit_message HEAD <<-\\EOF\n+\t\tfirst\n+\n+\t\tamend a comment\n+\t\tEOF\n+\t)\n+'\n+\n+# For now, git-history(1) does not yet execute any hooks. This is subject to\n+# change in the future, and if it does this test here is expected to start\n+# failing. In other words, this test is not an endorsement of the current\n+# status quo.\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\ttest_commit first &&\n+\t\ttest_commit second &&\n+\t\ttest_commit third &&\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\treword_with_message HEAD~ <<-EOF &&\n+\t\tsecond reworded\n+\t\tEOF\n+\n+\t\tcat >expect <<-EOF &&\n+\t\tthird\n+\t\tsecond reworded\n+\t\tfirst\n+\t\tEOF\n+\t\tgit log --format=%s >actual &&\n+\t\ttest_cmp expect actual &&\n+\n+\t\ttest_path_is_missing hooks.log\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\ttest_commit first &&\n+\n+\t\t! reword_with_message HEAD 2>err </dev/null &&\n+\t\ttest_grep \"Aborting commit due to empty commit message.\" 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\ttouch a b &&\n+\t\tgit add . &&\n+\t\tgit commit -m \"initial commit\" &&\n+\t\techo foo >a &&\n+\t\techo bar >b &&\n+\t\tgit add b &&\n+\t\treword_with_message HEAD <<-EOF &&\n+\t\tmessage\n+\t\tEOF\n+\t\tcat >expect <<-\\EOF &&\n+\t\t M a\n+\t\tM  b\n+\t\t?? actual\n+\t\t?? expect\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.52.0.542.g9473a8513b.dirty\n\n"},{"id":"533456","messageId":"CABPp-BFmQHjyeT0pXYV5eE5cnG5H6XXHeT_RsjWxq83ck3781w@mail.gmail.com","threadId":"63986","inReplyTo":"20260109-b4-pks-history-builtin-v9-1-8766101814c6@pks.im","subject":"Re: [PATCH v9 1/7] builtin/replay: extract core logic to replay revisions","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2026-01-10T01:14:48Z","receivedAt":"2026-01-10T01:15:01Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Fri, Jan 9, 2026 at 12:35 AM Patrick Steinhardt <ps@pks.im> wrote:\n>\n> We're about to move the core logic used to replay revisions onto a new\n> base into the \"libgit.a\" library. Prepare for this by pulling out the\n> logic into a new function `replay_revisions()` that:\n>\n>   1. Takes a set of revisions to replay and some options that tell it how\n>      it ought to replay the revisions.\n>\n>   2. Replays the commits.\n>\n>   3. Records any reference updates that would be caused by replaying the\n>      commits in a structure that is owned by the caller.\n>\n> The logic itself will be moved into a separate file in the next commit.\n> This change is not expected to cause user-visible change in behaviour.\n>\n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n>  builtin/replay.c | 274 +++++++++++++++++++++++++++++++++----------------------\n>  1 file changed, 165 insertions(+), 109 deletions(-)\n>\n> diff --git a/builtin/replay.c b/builtin/replay.c\n> index 1960bbbee8..df3b32a52d 100644\n> --- a/builtin/replay.c\n> +++ b/builtin/replay.c\n> @@ -278,6 +278,137 @@ static enum ref_action_mode get_ref_action_mode(struct repository *repo, const c\n>         return REF_ACTION_UPDATE;\n>  }\n>\n> +struct replay_revisions_options {\n> +       const char *advance;\n> +       const char *onto;\n> +       int contained;\n> +};\n> +\n> +struct replay_result {\n> +       struct replay_ref_update {\n> +               char *refname;\n> +               struct object_id old_oid;\n> +               struct object_id new_oid;\n> +       } *updates;\n> +       size_t updates_nr, updates_alloc;\n> +\n> +       bool merge_conflict;\n> +};\n> +\n> +static void replay_result_release(struct replay_result *result)\n> +{\n> +       for (size_t i = 0; i < result->updates_nr; i++)\n> +               free(result->updates[i].refname);\n> +       free(result->updates);\n> +}\n> +\n> +static void replay_result_queue_update(struct replay_result *result,\n> +                                      const char *refname,\n> +                                      const struct object_id *old_oid,\n> +                                      const struct object_id *new_oid)\n> +{\n> +       ALLOC_GROW(result->updates, result->updates_nr + 1, result->updates_alloc);\n> +       result->updates[result->updates_nr].refname = xstrdup(refname);\n> +       result->updates[result->updates_nr].old_oid = *old_oid;\n> +       result->updates[result->updates_nr].new_oid = *new_oid;\n> +       result->updates_nr++;\n> +}\n> +\n> +static int replay_revisions(struct repository *repo, struct rev_info *revs,\n> +                           struct replay_revisions_options *opts,\n> +                           struct replay_result *out)\n\nWhy have both repo & revs?  Can't we get repo from revs->repo?\n\n> +{\n> +       kh_oid_map_t *replayed_commits = NULL;\n> +       struct strset *update_refs = NULL;\n> +       struct commit *last_commit = NULL;\n> +       struct commit *commit;\n> +       struct commit *onto = NULL;\n> +       struct merge_options merge_opt;\n> +       struct merge_result result;\n> +       char *advance;\n> +       int ret;\n> +\n> +       advance = xstrdup_or_null(opts->advance);\n> +       set_up_replay_mode(repo, &revs->cmdline, opts->onto, &advance,\n> +                          &onto, &update_refs);\n> +\n> +       /* FIXME: Should allow replaying commits with the first as a root commit */\n> +\n> +       if (prepare_revision_walk(revs) < 0) {\n> +               ret = error(_(\"error preparing revisions\"));\n> +               goto out;\n> +       }\n> +\n> +       init_basic_merge_options(&merge_opt, repo);\n> +       memset(&result, 0, sizeof(result));\n> +       merge_opt.show_rename_progress = 0;\n> +       last_commit = onto;\n> +       replayed_commits = kh_init_oid_map();\n> +       while ((commit = get_revision(revs))) {\n> +               const struct name_decoration *decoration;\n> +               khint_t pos;\n> +               int hr;\n> +\n> +               if (!commit->parents)\n> +                       die(_(\"replaying down from root commit is not supported yet!\"));\n> +               if (commit->parents->next)\n> +                       die(_(\"replaying merge commits is not supported yet!\"));\n> +\n> +               last_commit = pick_regular_commit(repo, commit, replayed_commits,\n> +                                                 onto, &merge_opt, &result);\n> +               if (!last_commit)\n> +                       break;\n> +\n> +               /* Record commit -> last_commit mapping */\n> +               pos = kh_put_oid_map(replayed_commits, commit->object.oid, &hr);\n> +               if (hr == 0)\n> +                       BUG(\"Duplicate rewritten commit: %s\\n\",\n> +                           oid_to_hex(&commit->object.oid));\n> +               kh_value(replayed_commits, pos) = last_commit;\n> +\n> +               /* Update any necessary branches */\n> +               if (advance)\n> +                       continue;\n> +               decoration = get_name_decoration(&commit->object);\n> +               if (!decoration)\n> +                       continue;\n> +               while (decoration) {\n> +                       if (decoration->type == DECORATION_REF_LOCAL &&\n> +                           (opts->contained || strset_contains(update_refs,\n> +                                                               decoration->name))) {\n> +                               replay_result_queue_update(out, decoration->name,\n> +                                                          &commit->object.oid,\n> +                                                          &last_commit->object.oid);\n> +                       }\n> +                       decoration = decoration->next;\n> +               }\n> +       }\n> +\n> +       if (!result.clean) {\n> +               out->merge_conflict = true;\n> +               ret = -1;\n> +               goto out;\n\nWhat?  In the replay code before this series:\n\n        ret = result.clean;\n        [...]\n        /* Return */\n        if (ret < 0)\n                exit(128);\n        return ret ? 0 : 1;\n\nSo, !result.clean should result in a return code of 1, not -1.\n\n> +       }\n> +\n> +       /* In --advance mode, advance the target ref */\n> +       if (advance)\n> +               replay_result_queue_update(out, advance,\n> +                                          &onto->object.oid,\n> +                                          &last_commit->object.oid);\n> +\n> +       ret = 0;\n> +\n> +out:\n> +       if (update_refs) {\n> +               strset_clear(update_refs);\n> +               free(update_refs);\n> +       }\n> +       kh_destroy_oid_map(replayed_commits);\n> +       merge_finalize(&merge_opt, &result);\n> +       free(advance);\n> +       return ret;\n> +}\n> +\n>  static int handle_ref_update(enum ref_action_mode mode,\n>                              struct ref_transaction *transaction,\n>                              const char *refname,\n> @@ -306,21 +437,11 @@ int cmd_replay(int argc,\n>                const char *prefix,\n>                struct repository *repo)\n>  {\n> -       const char *advance_name_opt = NULL;\n> -       char *advance_name = NULL;\n> -       struct commit *onto = NULL;\n> -       const char *onto_name = NULL;\n> -       int contained = 0;\n> +       struct replay_revisions_options opts = { 0 };\n> +       struct replay_result result = { 0 };\n>         const char *ref_action = NULL;\n>         enum ref_action_mode ref_mode;\n> -\n>         struct rev_info revs;\n> -       struct commit *last_commit = NULL;\n> -       struct commit *commit;\n> -       struct merge_options merge_opt;\n> -       struct merge_result result;\n> -       struct strset *update_refs = NULL;\n> -       kh_oid_map_t *replayed_commits;\n>         struct ref_transaction *transaction = NULL;\n>         struct strbuf transaction_err = STRBUF_INIT;\n>         struct strbuf reflog_msg = STRBUF_INIT;\n> @@ -333,13 +454,13 @@ int cmd_replay(int argc,\n>                 NULL\n>         };\n>         struct option replay_options[] = {\n> -               OPT_STRING(0, \"advance\", &advance_name_opt,\n> +               OPT_STRING(0, \"advance\", &opts.advance,\n>                            N_(\"branch\"),\n>                            N_(\"make replay advance given branch\")),\n> -               OPT_STRING(0, \"onto\", &onto_name,\n> +               OPT_STRING(0, \"onto\", &opts.onto,\n>                            N_(\"revision\"),\n>                            N_(\"replay onto given commit\")),\n> -               OPT_BOOL(0, \"contained\", &contained,\n> +               OPT_BOOL(0, \"contained\", &opts.contained,\n>                          N_(\"update all branches that point at commits in <revision-range>\")),\n>                 OPT_STRING(0, \"ref-action\", &ref_action,\n>                            N_(\"mode\"),\n> @@ -350,19 +471,17 @@ int cmd_replay(int argc,\n>         argc = parse_options(argc, argv, prefix, replay_options, replay_usage,\n>                              PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT);\n>\n> -       if (!onto_name && !advance_name_opt) {\n> +       if (!opts.onto && !opts.advance) {\n>                 error(_(\"option --onto or --advance is mandatory\"));\n>                 usage_with_options(replay_usage, replay_options);\n>         }\n>\n> -       die_for_incompatible_opt2(!!advance_name_opt, \"--advance\",\n> -                                 contained, \"--contained\");\n> +       die_for_incompatible_opt2(!!opts.advance, \"--advance\",\n> +                                 opts.contained, \"--contained\");\n>\n>         /* Parse ref action mode from command line or config */\n>         ref_mode = get_ref_action_mode(repo, ref_action);\n>\n> -       advance_name = xstrdup_or_null(advance_name_opt);\n> -\n>         repo_init_revisions(repo, &revs, prefix);\n>\n>         /*\n> @@ -414,18 +533,19 @@ int cmd_replay(int argc,\n>                 revs.simplify_history = 0;\n>         }\n>\n> -       set_up_replay_mode(repo, &revs.cmdline,\n> -                          onto_name, &advance_name,\n> -                          &onto, &update_refs);\n> -\n> -       /* FIXME: Should allow replaying commits with the first as a root commit */\n> +       ret = replay_revisions(repo, &revs, &opts, &result);\n> +       if (ret)\n> +               goto cleanup;\n>\n>         /* Build reflog message */\n> -       if (advance_name_opt)\n> -               strbuf_addf(&reflog_msg, \"replay --advance %s\", advance_name_opt);\n> -       else\n> -               strbuf_addf(&reflog_msg, \"replay --onto %s\",\n> -                           oid_to_hex(&onto->object.oid));\n> +       if (opts.advance) {\n> +               strbuf_addf(&reflog_msg, \"replay --advance %s\", opts.advance);\n> +       } else {\n> +               struct object_id oid;\n> +               if (repo_get_oid_committish(repo, opts.onto, &oid))\n> +                       BUG(\"--onto commit should have been resolved beforehand already\");\n> +               strbuf_addf(&reflog_msg, \"replay --onto %s\", oid_to_hex(&oid));\n> +       }\n>\n>         /* Initialize ref transaction if using update mode */\n>         if (ref_mode == REF_ACTION_UPDATE) {\n> @@ -438,78 +558,19 @@ int cmd_replay(int argc,\n>                 }\n>         }\n>\n> -       if (prepare_revision_walk(&revs) < 0) {\n> -               ret = error(_(\"error preparing revisions\"));\n> -               goto cleanup;\n> -       }\n> -\n> -       init_basic_merge_options(&merge_opt, repo);\n> -       memset(&result, 0, sizeof(result));\n> -       merge_opt.show_rename_progress = 0;\n> -       last_commit = onto;\n> -       replayed_commits = kh_init_oid_map();\n> -       while ((commit = get_revision(&revs))) {\n> -               const struct name_decoration *decoration;\n> -               khint_t pos;\n> -               int hr;\n> -\n> -               if (!commit->parents)\n> -                       die(_(\"replaying down from root commit is not supported yet!\"));\n> -               if (commit->parents->next)\n> -                       die(_(\"replaying merge commits is not supported yet!\"));\n> -\n> -               last_commit = pick_regular_commit(repo, commit, replayed_commits,\n> -                                                 onto, &merge_opt, &result);\n> -               if (!last_commit)\n> -                       break;\n> -\n> -               /* Record commit -> last_commit mapping */\n> -               pos = kh_put_oid_map(replayed_commits, commit->object.oid, &hr);\n> -               if (hr == 0)\n> -                       BUG(\"Duplicate rewritten commit: %s\\n\",\n> -                           oid_to_hex(&commit->object.oid));\n> -               kh_value(replayed_commits, pos) = last_commit;\n> -\n> -               /* Update any necessary branches */\n> -               if (advance_name)\n> -                       continue;\n> -               decoration = get_name_decoration(&commit->object);\n> -               if (!decoration)\n> -                       continue;\n> -               while (decoration) {\n> -                       if (decoration->type == DECORATION_REF_LOCAL &&\n> -                           (contained || strset_contains(update_refs,\n> -                                                         decoration->name))) {\n> -                               if (handle_ref_update(ref_mode, transaction,\n> -                                                     decoration->name,\n> -                                                     &last_commit->object.oid,\n> -                                                     &commit->object.oid,\n> -                                                     reflog_msg.buf,\n> -                                                     &transaction_err) < 0) {\n> -                                       ret = error(_(\"failed to update ref '%s': %s\"),\n> -                                                   decoration->name, transaction_err.buf);\n> -                                       goto cleanup;\n> -                               }\n> -                       }\n> -                       decoration = decoration->next;\n> -               }\n> -       }\n> -\n> -       /* In --advance mode, advance the target ref */\n> -       if (result.clean == 1 && advance_name) {\n> -               if (handle_ref_update(ref_mode, transaction, advance_name,\n> -                                     &last_commit->object.oid,\n> -                                     &onto->object.oid,\n> -                                     reflog_msg.buf,\n> -                                     &transaction_err) < 0) {\n> +       for (size_t i = 0; i < result.updates_nr; i++) {\n> +               ret = handle_ref_update(ref_mode, transaction, result.updates[i].refname,\n> +                                       &result.updates[i].new_oid, &result.updates[i].old_oid,\n> +                                       reflog_msg.buf, &transaction_err);\n> +               if (ret) {\n>                         ret = error(_(\"failed to update ref '%s': %s\"),\n> -                                   advance_name, transaction_err.buf);\n> +                                   result.updates[i].refname, transaction_err.buf);\n>                         goto cleanup;\n>                 }\n>         }\n>\n>         /* Commit the ref transaction if we have one */\n> -       if (transaction && result.clean == 1) {\n> +       if (transaction) {\n>                 if (ref_transaction_commit(transaction, &transaction_err)) {\n>                         ret = error(_(\"failed to commit ref transaction: %s\"),\n>                                     transaction_err.buf);\n> @@ -517,24 +578,19 @@ int cmd_replay(int argc,\n>                 }\n>         }\n>\n> -       merge_finalize(&merge_opt, &result);\n> -       kh_destroy_oid_map(replayed_commits);\n> -       if (update_refs) {\n> -               strset_clear(update_refs);\n> -               free(update_refs);\n> -       }\n> -       ret = result.clean;\n> -\n>  cleanup:\n>         if (transaction)\n>                 ref_transaction_free(transaction);\n> +       replay_result_release(&result);\n>         strbuf_release(&transaction_err);\n>         strbuf_release(&reflog_msg);\n>         release_revisions(&revs);\n> -       free(advance_name);\n>\n> -       /* Return */\n> -       if (ret < 0)\n> -               exit(128);\n> -       return ret ? 0 : 1;\n> +       if (ret) {\n> +               if (result.merge_conflict)\n> +                       return 1;\n\nThis feels like a rather Rube-Goldberg way to get this.  Above where I\nhighlighted where you set ret to -1 incorrectly, can we just set it to\n1 there and then lose all this special logic in favor of \"return ret;\"\nhere?  Or am I overlooking something?\n\n\n> +               return 128;\n> +       }\n> +\n> +       return 0;\n>  }\n>\n> --\n> 2.52.0.542.g9473a8513b.dirty\n"},{"id":"533457","messageId":"CABPp-BEo5jGBgJBkCdu_GHstsbEm4mCpKO3NWvNfcjDVC+SbLQ@mail.gmail.com","threadId":"63986","inReplyTo":"20260109-b4-pks-history-builtin-v9-2-8766101814c6@pks.im","subject":"Re: [PATCH v9 2/7] builtin/replay: move core logic into \"libgit.a\"","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2026-01-10T01:16:41Z","receivedAt":"2026-01-10T01:16:53Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Fri, Jan 9, 2026 at 12:35 AM Patrick Steinhardt <ps@pks.im> wrote:\n>\n> Move the core logic used to replay commits into \"libgit.a\" so that it\n> can be easily reused by other commands. It will be used in a subsequent\n> commit where we're about to introduce a new git-history(1) command.\n>\n> Note that with this change we have no sign-comparison warnings anymore,\n> and neither do we depend on `the_repository`.\n>\n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n>  Makefile         |   1 +\n>  builtin/replay.c | 368 +------------------------------------------------------\n>  meson.build      |   1 +\n>  replay.c         | 355 +++++++++++++++++++++++++++++++++++++++++++++++++++++\n>  replay.h         |  64 ++++++++++\n>  5 files changed, 422 insertions(+), 367 deletions(-)\n>\n> diff --git a/Makefile b/Makefile\n> index b7eba509c6..1c64a5d2ae 100644\n> --- a/Makefile\n> +++ b/Makefile\n> @@ -1285,6 +1285,7 @@ LIB_OBJS += repack-geometry.o\n>  LIB_OBJS += repack-midx.o\n>  LIB_OBJS += repack-promisor.o\n>  LIB_OBJS += replace-object.o\n> +LIB_OBJS += replay.o\n>  LIB_OBJS += repo-settings.o\n>  LIB_OBJS += repository.o\n>  LIB_OBJS += rerere.o\n> diff --git a/builtin/replay.c b/builtin/replay.c\n> index df3b32a52d..da8b7202f6 100644\n> --- a/builtin/replay.c\n> +++ b/builtin/replay.c\n> @@ -2,257 +2,22 @@\n>   * \"git replay\" builtin command\n>   */\n>\n> -#define USE_THE_REPOSITORY_VARIABLE\n> -#define DISABLE_SIGN_COMPARE_WARNINGS\n> -\n>  #include \"git-compat-util.h\"\n>\n>  #include \"builtin.h\"\n>  #include \"config.h\"\n> -#include \"environment.h\"\n>  #include \"hex.h\"\n> -#include \"lockfile.h\"\n> -#include \"merge-ort.h\"\n>  #include \"object-name.h\"\n>  #include \"parse-options.h\"\n>  #include \"refs.h\"\n> +#include \"replay.h\"\n>  #include \"revision.h\"\n> -#include \"strmap.h\"\n> -#include <oidset.h>\n> -#include <tree.h>\n>\n>  enum ref_action_mode {\n>         REF_ACTION_UPDATE,\n>         REF_ACTION_PRINT,\n>  };\n>\n> -static const char *short_commit_name(struct repository *repo,\n> -                                    struct commit *commit)\n> -{\n> -       return repo_find_unique_abbrev(repo, &commit->object.oid,\n> -                                      DEFAULT_ABBREV);\n> -}\n> -\n> -static struct commit *peel_committish(struct repository *repo,\n> -                                     const char *name,\n> -                                     const char *mode)\n> -{\n> -       struct object *obj;\n> -       struct object_id oid;\n> -\n> -       if (repo_get_oid(repo, name, &oid))\n> -               die(_(\"'%s' is not a valid commit-ish for %s\"), name, mode);\n> -       obj = parse_object_or_die(repo, &oid, name);\n> -       return (struct commit *)repo_peel_to_type(repo, name, 0, obj,\n> -                                                 OBJ_COMMIT);\n> -}\n> -\n> -static char *get_author(const char *message)\n> -{\n> -       size_t len;\n> -       const char *a;\n> -\n> -       a = find_commit_header(message, \"author\", &len);\n> -       if (a)\n> -               return xmemdupz(a, len);\n> -\n> -       return NULL;\n> -}\n> -\n> -static struct commit *create_commit(struct repository *repo,\n> -                                   struct tree *tree,\n> -                                   struct commit *based_on,\n> -                                   struct commit *parent)\n> -{\n> -       struct object_id ret;\n> -       struct object *obj = NULL;\n> -       struct commit_list *parents = NULL;\n> -       char *author;\n> -       char *sign_commit = NULL; /* FIXME: cli users might want to sign again */\n> -       struct commit_extra_header *extra = NULL;\n> -       struct strbuf msg = STRBUF_INIT;\n> -       const char *out_enc = get_commit_output_encoding();\n> -       const char *message = repo_logmsg_reencode(repo, based_on,\n> -                                                  NULL, out_enc);\n> -       const char *orig_message = NULL;\n> -       const char *exclude_gpgsig[] = { \"gpgsig\", \"gpgsig-sha256\", NULL };\n> -\n> -       commit_list_insert(parent, &parents);\n> -       extra = read_commit_extra_headers(based_on, exclude_gpgsig);\n> -       find_commit_subject(message, &orig_message);\n> -       strbuf_addstr(&msg, orig_message);\n> -       author = get_author(message);\n> -       reset_ident_date();\n> -       if (commit_tree_extended(msg.buf, msg.len, &tree->object.oid, parents,\n> -                                &ret, author, NULL, sign_commit, extra)) {\n> -               error(_(\"failed to write commit object\"));\n> -               goto out;\n> -       }\n> -\n> -       obj = parse_object(repo, &ret);\n> -\n> -out:\n> -       repo_unuse_commit_buffer(the_repository, based_on, message);\n> -       free_commit_extra_headers(extra);\n> -       free_commit_list(parents);\n> -       strbuf_release(&msg);\n> -       free(author);\n> -       return (struct commit *)obj;\n> -}\n> -\n> -struct ref_info {\n> -       struct commit *onto;\n> -       struct strset positive_refs;\n> -       struct strset negative_refs;\n> -       int positive_refexprs;\n> -       int negative_refexprs;\n> -};\n> -\n> -static void get_ref_information(struct repository *repo,\n> -                               struct rev_cmdline_info *cmd_info,\n> -                               struct ref_info *ref_info)\n> -{\n> -       int i;\n> -\n> -       ref_info->onto = NULL;\n> -       strset_init(&ref_info->positive_refs);\n> -       strset_init(&ref_info->negative_refs);\n> -       ref_info->positive_refexprs = 0;\n> -       ref_info->negative_refexprs = 0;\n> -\n> -       /*\n> -        * When the user specifies e.g.\n> -        *   git replay origin/main..mybranch\n> -        *   git replay ^origin/next mybranch1 mybranch2\n> -        * we want to be able to determine where to replay the commits.  In\n> -        * these examples, the branches are probably based on an old version\n> -        * of either origin/main or origin/next, so we want to replay on the\n> -        * newest version of that branch.  In contrast we would want to error\n> -        * out if they ran\n> -        *   git replay ^origin/master ^origin/next mybranch\n> -        *   git replay mybranch~2..mybranch\n> -        * the first of those because there's no unique base to choose, and\n> -        * the second because they'd likely just be replaying commits on top\n> -        * of the same commit and not making any difference.\n> -        */\n> -       for (i = 0; i < cmd_info->nr; i++) {\n> -               struct rev_cmdline_entry *e = cmd_info->rev + i;\n> -               struct object_id oid;\n> -               const char *refexpr = e->name;\n> -               char *fullname = NULL;\n> -               int can_uniquely_dwim = 1;\n> -\n> -               if (*refexpr == '^')\n> -                       refexpr++;\n> -               if (repo_dwim_ref(repo, refexpr, strlen(refexpr), &oid, &fullname, 0) != 1)\n> -                       can_uniquely_dwim = 0;\n> -\n> -               if (e->flags & BOTTOM) {\n> -                       if (can_uniquely_dwim)\n> -                               strset_add(&ref_info->negative_refs, fullname);\n> -                       if (!ref_info->negative_refexprs)\n> -                               ref_info->onto = lookup_commit_reference_gently(repo,\n> -                                                                               &e->item->oid, 1);\n> -                       ref_info->negative_refexprs++;\n> -               } else {\n> -                       if (can_uniquely_dwim)\n> -                               strset_add(&ref_info->positive_refs, fullname);\n> -                       ref_info->positive_refexprs++;\n> -               }\n> -\n> -               free(fullname);\n> -       }\n> -}\n> -\n> -static void set_up_replay_mode(struct repository *repo,\n> -                              struct rev_cmdline_info *cmd_info,\n> -                              const char *onto_name,\n> -                              char **advance_name,\n> -                              struct commit **onto,\n> -                              struct strset **update_refs)\n> -{\n> -       struct ref_info rinfo;\n> -\n> -       get_ref_information(repo, cmd_info, &rinfo);\n> -       if (!rinfo.positive_refexprs)\n> -               die(_(\"need some commits to replay\"));\n> -\n> -       die_for_incompatible_opt2(!!onto_name, \"--onto\",\n> -                                 !!*advance_name, \"--advance\");\n> -       if (onto_name) {\n> -               *onto = peel_committish(repo, onto_name, \"--onto\");\n> -               if (rinfo.positive_refexprs <\n> -                   strset_get_size(&rinfo.positive_refs))\n> -                       die(_(\"all positive revisions given must be references\"));\n> -               *update_refs = xcalloc(1, sizeof(**update_refs));\n> -               **update_refs = rinfo.positive_refs;\n> -               memset(&rinfo.positive_refs, 0, sizeof(**update_refs));\n> -       } else {\n> -               struct object_id oid;\n> -               char *fullname = NULL;\n> -\n> -               if (!*advance_name)\n> -                       BUG(\"expected either onto_name or *advance_name in this function\");\n> -\n> -               if (repo_dwim_ref(repo, *advance_name, strlen(*advance_name),\n> -                            &oid, &fullname, 0) == 1) {\n> -                       free(*advance_name);\n> -                       *advance_name = fullname;\n> -               } else {\n> -                       die(_(\"argument to --advance must be a reference\"));\n> -               }\n> -               *onto = peel_committish(repo, *advance_name, \"--advance\");\n> -               if (rinfo.positive_refexprs > 1)\n> -                       die(_(\"cannot advance target with multiple sources because ordering would be ill-defined\"));\n> -       }\n> -       strset_clear(&rinfo.negative_refs);\n> -       strset_clear(&rinfo.positive_refs);\n> -}\n> -\n> -static struct commit *mapped_commit(kh_oid_map_t *replayed_commits,\n> -                                   struct commit *commit,\n> -                                   struct commit *fallback)\n> -{\n> -       khint_t pos = kh_get_oid_map(replayed_commits, commit->object.oid);\n> -       if (pos == kh_end(replayed_commits))\n> -               return fallback;\n> -       return kh_value(replayed_commits, pos);\n> -}\n> -\n> -static struct commit *pick_regular_commit(struct repository *repo,\n> -                                         struct commit *pickme,\n> -                                         kh_oid_map_t *replayed_commits,\n> -                                         struct commit *onto,\n> -                                         struct merge_options *merge_opt,\n> -                                         struct merge_result *result)\n> -{\n> -       struct commit *base, *replayed_base;\n> -       struct tree *pickme_tree, *base_tree;\n> -\n> -       base = pickme->parents->item;\n> -       replayed_base = mapped_commit(replayed_commits, base, onto);\n> -\n> -       result->tree = repo_get_commit_tree(repo, replayed_base);\n> -       pickme_tree = repo_get_commit_tree(repo, pickme);\n> -       base_tree = repo_get_commit_tree(repo, base);\n> -\n> -       merge_opt->branch1 = short_commit_name(repo, replayed_base);\n> -       merge_opt->branch2 = short_commit_name(repo, pickme);\n> -       merge_opt->ancestor = xstrfmt(\"parent of %s\", merge_opt->branch2);\n> -\n> -       merge_incore_nonrecursive(merge_opt,\n> -                                 base_tree,\n> -                                 result->tree,\n> -                                 pickme_tree,\n> -                                 result);\n> -\n> -       free((char*)merge_opt->ancestor);\n> -       merge_opt->ancestor = NULL;\n> -       if (!result->clean)\n> -               return NULL;\n> -       return create_commit(repo, result->tree, pickme, replayed_base);\n> -}\n> -\n>  static enum ref_action_mode parse_ref_action_mode(const char *ref_action, const char *source)\n>  {\n>         if (!ref_action || !strcmp(ref_action, \"update\"))\n> @@ -278,137 +43,6 @@ static enum ref_action_mode get_ref_action_mode(struct repository *repo, const c\n>         return REF_ACTION_UPDATE;\n>  }\n>\n> -struct replay_revisions_options {\n> -       const char *advance;\n> -       const char *onto;\n> -       int contained;\n> -};\n> -\n> -struct replay_result {\n> -       struct replay_ref_update {\n> -               char *refname;\n> -               struct object_id old_oid;\n> -               struct object_id new_oid;\n> -       } *updates;\n> -       size_t updates_nr, updates_alloc;\n> -\n> -       bool merge_conflict;\n> -};\n> -\n> -static void replay_result_release(struct replay_result *result)\n> -{\n> -       for (size_t i = 0; i < result->updates_nr; i++)\n> -               free(result->updates[i].refname);\n> -       free(result->updates);\n> -}\n> -\n> -static void replay_result_queue_update(struct replay_result *result,\n> -                                      const char *refname,\n> -                                      const struct object_id *old_oid,\n> -                                      const struct object_id *new_oid)\n> -{\n> -       ALLOC_GROW(result->updates, result->updates_nr + 1, result->updates_alloc);\n> -       result->updates[result->updates_nr].refname = xstrdup(refname);\n> -       result->updates[result->updates_nr].old_oid = *old_oid;\n> -       result->updates[result->updates_nr].new_oid = *new_oid;\n> -       result->updates_nr++;\n> -}\n> -\n> -static int replay_revisions(struct repository *repo, struct rev_info *revs,\n> -                           struct replay_revisions_options *opts,\n> -                           struct replay_result *out)\n> -{\n> -       kh_oid_map_t *replayed_commits = NULL;\n> -       struct strset *update_refs = NULL;\n> -       struct commit *last_commit = NULL;\n> -       struct commit *commit;\n> -       struct commit *onto = NULL;\n> -       struct merge_options merge_opt;\n> -       struct merge_result result;\n> -       char *advance;\n> -       int ret;\n> -\n> -       advance = xstrdup_or_null(opts->advance);\n> -       set_up_replay_mode(repo, &revs->cmdline, opts->onto, &advance,\n> -                          &onto, &update_refs);\n> -\n> -       /* FIXME: Should allow replaying commits with the first as a root commit */\n> -\n> -       if (prepare_revision_walk(revs) < 0) {\n> -               ret = error(_(\"error preparing revisions\"));\n> -               goto out;\n> -       }\n> -\n> -       init_basic_merge_options(&merge_opt, repo);\n> -       memset(&result, 0, sizeof(result));\n> -       merge_opt.show_rename_progress = 0;\n> -       last_commit = onto;\n> -       replayed_commits = kh_init_oid_map();\n> -       while ((commit = get_revision(revs))) {\n> -               const struct name_decoration *decoration;\n> -               khint_t pos;\n> -               int hr;\n> -\n> -               if (!commit->parents)\n> -                       die(_(\"replaying down from root commit is not supported yet!\"));\n> -               if (commit->parents->next)\n> -                       die(_(\"replaying merge commits is not supported yet!\"));\n> -\n> -               last_commit = pick_regular_commit(repo, commit, replayed_commits,\n> -                                                 onto, &merge_opt, &result);\n> -               if (!last_commit)\n> -                       break;\n> -\n> -               /* Record commit -> last_commit mapping */\n> -               pos = kh_put_oid_map(replayed_commits, commit->object.oid, &hr);\n> -               if (hr == 0)\n> -                       BUG(\"Duplicate rewritten commit: %s\\n\",\n> -                           oid_to_hex(&commit->object.oid));\n> -               kh_value(replayed_commits, pos) = last_commit;\n> -\n> -               /* Update any necessary branches */\n> -               if (advance)\n> -                       continue;\n> -               decoration = get_name_decoration(&commit->object);\n> -               if (!decoration)\n> -                       continue;\n> -               while (decoration) {\n> -                       if (decoration->type == DECORATION_REF_LOCAL &&\n> -                           (opts->contained || strset_contains(update_refs,\n> -                                                               decoration->name))) {\n> -                               replay_result_queue_update(out, decoration->name,\n> -                                                          &commit->object.oid,\n> -                                                          &last_commit->object.oid);\n> -                       }\n> -                       decoration = decoration->next;\n> -               }\n> -       }\n> -\n> -       if (!result.clean) {\n> -               out->merge_conflict = true;\n> -               ret = -1;\n> -               goto out;\n> -       }\n> -\n> -       /* In --advance mode, advance the target ref */\n> -       if (advance)\n> -               replay_result_queue_update(out, advance,\n> -                                          &onto->object.oid,\n> -                                          &last_commit->object.oid);\n> -\n> -       ret = 0;\n> -\n> -out:\n> -       if (update_refs) {\n> -               strset_clear(update_refs);\n> -               free(update_refs);\n> -       }\n> -       kh_destroy_oid_map(replayed_commits);\n> -       merge_finalize(&merge_opt, &result);\n> -       free(advance);\n> -       return ret;\n> -}\n> -\n>  static int handle_ref_update(enum ref_action_mode mode,\n>                              struct ref_transaction *transaction,\n>                              const char *refname,\n> diff --git a/meson.build b/meson.build\n> index dd52efd1c8..a5a4e99b25 100644\n> --- a/meson.build\n> +++ b/meson.build\n> @@ -471,6 +471,7 @@ libgit_sources = [\n>    'repack-midx.c',\n>    'repack-promisor.c',\n>    'replace-object.c',\n> +  'replay.c',\n>    'repo-settings.c',\n>    'repository.c',\n>    'rerere.c',\n> diff --git a/replay.c b/replay.c\n> new file mode 100644\n> index 0000000000..fc7186ef09\n> --- /dev/null\n> +++ b/replay.c\n> @@ -0,0 +1,355 @@\n> +#define USE_THE_REPOSITORY_VARIABLE\n> +#define DISABLE_SIGN_COMPARE_WARNINGS\n> +\n> +#include \"git-compat-util.h\"\n> +#include \"environment.h\"\n> +#include \"hex.h\"\n> +#include \"merge-ort.h\"\n> +#include \"object-name.h\"\n> +#include \"oidset.h\"\n\nI don't think oidset is used here?   Although, come to think of it,\nI'm not sure it was used in replay to begin with.  Looks like I added\nthis include during the switch from t/helper/test-fast-rebase, but\nnever used it even then.  I must have thought I was going to use it,\nand added it, but never did.  Anyway, maybe this is a good time to get\nrid of it?\n\n> +#include \"parse-options.h\"\n\nWhy would parse-options be included here?  Oh,\ndie_for_incompatible_opt2()?  Feels a little weird that we have option\nparsing logic outside of builtin/, but...maybe it's all fine?\n\n> +#include \"refs.h\"\n> +#include \"replay.h\"\n> +#include \"revision.h\"\n\nShouldn't strmap.h (for strset) also be included?  I think we get it\nas a side-effect of something else, but since we use it directly, it'd\nmake sense to include directly.\n\n> +#include \"tree.h\"\n> +\n> +static const char *short_commit_name(struct repository *repo,\n> +                                    struct commit *commit)\n> +{\n> +       return repo_find_unique_abbrev(repo, &commit->object.oid,\n> +                                      DEFAULT_ABBREV);\n> +}\n> +\n> +static struct commit *peel_committish(struct repository *repo,\n> +                                     const char *name,\n> +                                     const char *mode)\n> +{\n> +       struct object *obj;\n> +       struct object_id oid;\n> +\n> +       if (repo_get_oid(repo, name, &oid))\n> +               die(_(\"'%s' is not a valid commit-ish for %s\"), name, mode);\n> +       obj = parse_object_or_die(repo, &oid, name);\n> +       return (struct commit *)repo_peel_to_type(repo, name, 0, obj,\n> +                                                 OBJ_COMMIT);\n> +}\n> +\n> +static char *get_author(const char *message)\n> +{\n> +       size_t len;\n> +       const char *a;\n> +\n> +       a = find_commit_header(message, \"author\", &len);\n> +       if (a)\n> +               return xmemdupz(a, len);\n> +\n> +       return NULL;\n> +}\n> +\n> +static struct commit *create_commit(struct repository *repo,\n> +                                   struct tree *tree,\n> +                                   struct commit *based_on,\n> +                                   struct commit *parent)\n> +{\n> +       struct object_id ret;\n> +       struct object *obj = NULL;\n> +       struct commit_list *parents = NULL;\n> +       char *author;\n> +       char *sign_commit = NULL; /* FIXME: cli users might want to sign again */\n> +       struct commit_extra_header *extra = NULL;\n> +       struct strbuf msg = STRBUF_INIT;\n> +       const char *out_enc = get_commit_output_encoding();\n> +       const char *message = repo_logmsg_reencode(repo, based_on,\n> +                                                  NULL, out_enc);\n> +       const char *orig_message = NULL;\n> +       const char *exclude_gpgsig[] = { \"gpgsig\", \"gpgsig-sha256\", NULL };\n> +\n> +       commit_list_insert(parent, &parents);\n> +       extra = read_commit_extra_headers(based_on, exclude_gpgsig);\n> +       find_commit_subject(message, &orig_message);\n> +       strbuf_addstr(&msg, orig_message);\n> +       author = get_author(message);\n> +       reset_ident_date();\n> +       if (commit_tree_extended(msg.buf, msg.len, &tree->object.oid, parents,\n> +                                &ret, author, NULL, sign_commit, extra)) {\n> +               error(_(\"failed to write commit object\"));\n> +               goto out;\n> +       }\n> +\n> +       obj = parse_object(repo, &ret);\n> +\n> +out:\n> +       repo_unuse_commit_buffer(the_repository, based_on, message);\n> +       free_commit_extra_headers(extra);\n> +       free_commit_list(parents);\n> +       strbuf_release(&msg);\n> +       free(author);\n> +       return (struct commit *)obj;\n> +}\n> +\n> +struct ref_info {\n> +       struct commit *onto;\n> +       struct strset positive_refs;\n> +       struct strset negative_refs;\n> +       int positive_refexprs;\n> +       int negative_refexprs;\n> +};\n> +\n> +static void get_ref_information(struct repository *repo,\n> +                               struct rev_cmdline_info *cmd_info,\n> +                               struct ref_info *ref_info)\n> +{\n> +       int i;\n> +\n> +       ref_info->onto = NULL;\n> +       strset_init(&ref_info->positive_refs);\n> +       strset_init(&ref_info->negative_refs);\n> +       ref_info->positive_refexprs = 0;\n> +       ref_info->negative_refexprs = 0;\n> +\n> +       /*\n> +        * When the user specifies e.g.\n> +        *   git replay origin/main..mybranch\n> +        *   git replay ^origin/next mybranch1 mybranch2\n> +        * we want to be able to determine where to replay the commits.  In\n> +        * these examples, the branches are probably based on an old version\n> +        * of either origin/main or origin/next, so we want to replay on the\n> +        * newest version of that branch.  In contrast we would want to error\n> +        * out if they ran\n> +        *   git replay ^origin/master ^origin/next mybranch\n> +        *   git replay mybranch~2..mybranch\n> +        * the first of those because there's no unique base to choose, and\n> +        * the second because they'd likely just be replaying commits on top\n> +        * of the same commit and not making any difference.\n> +        */\n> +       for (i = 0; i < cmd_info->nr; i++) {\n> +               struct rev_cmdline_entry *e = cmd_info->rev + i;\n> +               struct object_id oid;\n> +               const char *refexpr = e->name;\n> +               char *fullname = NULL;\n> +               int can_uniquely_dwim = 1;\n> +\n> +               if (*refexpr == '^')\n> +                       refexpr++;\n> +               if (repo_dwim_ref(repo, refexpr, strlen(refexpr), &oid, &fullname, 0) != 1)\n> +                       can_uniquely_dwim = 0;\n> +\n> +               if (e->flags & BOTTOM) {\n> +                       if (can_uniquely_dwim)\n> +                               strset_add(&ref_info->negative_refs, fullname);\n> +                       if (!ref_info->negative_refexprs)\n> +                               ref_info->onto = lookup_commit_reference_gently(repo,\n> +                                                                               &e->item->oid, 1);\n> +                       ref_info->negative_refexprs++;\n> +               } else {\n> +                       if (can_uniquely_dwim)\n> +                               strset_add(&ref_info->positive_refs, fullname);\n> +                       ref_info->positive_refexprs++;\n> +               }\n> +\n> +               free(fullname);\n> +       }\n> +}\n> +\n> +static void set_up_replay_mode(struct repository *repo,\n> +                              struct rev_cmdline_info *cmd_info,\n> +                              const char *onto_name,\n> +                              char **advance_name,\n> +                              struct commit **onto,\n> +                              struct strset **update_refs)\n> +{\n> +       struct ref_info rinfo;\n> +\n> +       get_ref_information(repo, cmd_info, &rinfo);\n> +       if (!rinfo.positive_refexprs)\n> +               die(_(\"need some commits to replay\"));\n> +\n> +       die_for_incompatible_opt2(!!onto_name, \"--onto\",\n> +                                 !!*advance_name, \"--advance\");\n> +       if (onto_name) {\n> +               *onto = peel_committish(repo, onto_name, \"--onto\");\n> +               if (rinfo.positive_refexprs <\n> +                   strset_get_size(&rinfo.positive_refs))\n> +                       die(_(\"all positive revisions given must be references\"));\n> +               *update_refs = xcalloc(1, sizeof(**update_refs));\n> +               **update_refs = rinfo.positive_refs;\n> +               memset(&rinfo.positive_refs, 0, sizeof(**update_refs));\n> +       } else {\n> +               struct object_id oid;\n> +               char *fullname = NULL;\n> +\n> +               if (!*advance_name)\n> +                       BUG(\"expected either onto_name or *advance_name in this function\");\n> +\n> +               if (repo_dwim_ref(repo, *advance_name, strlen(*advance_name),\n> +                            &oid, &fullname, 0) == 1) {\n> +                       free(*advance_name);\n> +                       *advance_name = fullname;\n> +               } else {\n> +                       die(_(\"argument to --advance must be a reference\"));\n> +               }\n> +               *onto = peel_committish(repo, *advance_name, \"--advance\");\n> +               if (rinfo.positive_refexprs > 1)\n> +                       die(_(\"cannot advance target with multiple sources because ordering would be ill-defined\"));\n> +       }\n> +       strset_clear(&rinfo.negative_refs);\n> +       strset_clear(&rinfo.positive_refs);\n> +}\n> +\n> +static struct commit *mapped_commit(kh_oid_map_t *replayed_commits,\n> +                                   struct commit *commit,\n> +                                   struct commit *fallback)\n> +{\n> +       khint_t pos = kh_get_oid_map(replayed_commits, commit->object.oid);\n> +       if (pos == kh_end(replayed_commits))\n> +               return fallback;\n> +       return kh_value(replayed_commits, pos);\n> +}\n> +\n> +static struct commit *pick_regular_commit(struct repository *repo,\n> +                                         struct commit *pickme,\n> +                                         kh_oid_map_t *replayed_commits,\n> +                                         struct commit *onto,\n> +                                         struct merge_options *merge_opt,\n> +                                         struct merge_result *result)\n> +{\n> +       struct commit *base, *replayed_base;\n> +       struct tree *pickme_tree, *base_tree;\n> +\n> +       base = pickme->parents->item;\n> +       replayed_base = mapped_commit(replayed_commits, base, onto);\n> +\n> +       result->tree = repo_get_commit_tree(repo, replayed_base);\n> +       pickme_tree = repo_get_commit_tree(repo, pickme);\n> +       base_tree = repo_get_commit_tree(repo, base);\n> +\n> +       merge_opt->branch1 = short_commit_name(repo, replayed_base);\n> +       merge_opt->branch2 = short_commit_name(repo, pickme);\n> +       merge_opt->ancestor = xstrfmt(\"parent of %s\", merge_opt->branch2);\n> +\n> +       merge_incore_nonrecursive(merge_opt,\n> +                                 base_tree,\n> +                                 result->tree,\n> +                                 pickme_tree,\n> +                                 result);\n> +\n> +       free((char*)merge_opt->ancestor);\n> +       merge_opt->ancestor = NULL;\n> +       if (!result->clean)\n> +               return NULL;\n> +       return create_commit(repo, result->tree, pickme, replayed_base);\n> +}\n> +\n> +void replay_result_release(struct replay_result *result)\n> +{\n> +       for (size_t i = 0; i < result->updates_nr; i++)\n> +               free(result->updates[i].refname);\n> +       free(result->updates);\n> +}\n> +\n> +static void replay_result_queue_update(struct replay_result *result,\n> +                                      const char *refname,\n> +                                      const struct object_id *old_oid,\n> +                                      const struct object_id *new_oid)\n> +{\n> +       ALLOC_GROW(result->updates, result->updates_nr + 1, result->updates_alloc);\n> +       result->updates[result->updates_nr].refname = xstrdup(refname);\n> +       result->updates[result->updates_nr].old_oid = *old_oid;\n> +       result->updates[result->updates_nr].new_oid = *new_oid;\n> +       result->updates_nr++;\n> +}\n> +\n> +int replay_revisions(struct repository *repo, struct rev_info *revs,\n> +                    struct replay_revisions_options *opts,\n> +                    struct replay_result *out)\n> +{\n> +       kh_oid_map_t *replayed_commits = NULL;\n> +       struct strset *update_refs = NULL;\n> +       struct commit *last_commit = NULL;\n> +       struct commit *commit;\n> +       struct commit *onto = NULL;\n> +       struct merge_options merge_opt;\n> +       struct merge_result result;\n> +       char *advance;\n> +       int ret;\n> +\n> +       advance = xstrdup_or_null(opts->advance);\n> +       set_up_replay_mode(repo, &revs->cmdline, opts->onto, &advance,\n> +                          &onto, &update_refs);\n> +\n> +       /* FIXME: Should allow replaying commits with the first as a root commit */\n> +\n> +       if (prepare_revision_walk(revs) < 0) {\n> +               ret = error(_(\"error preparing revisions\"));\n> +               goto out;\n> +       }\n> +\n> +       init_basic_merge_options(&merge_opt, repo);\n> +       memset(&result, 0, sizeof(result));\n> +       merge_opt.show_rename_progress = 0;\n> +       last_commit = onto;\n> +       replayed_commits = kh_init_oid_map();\n> +       while ((commit = get_revision(revs))) {\n> +               const struct name_decoration *decoration;\n> +               khint_t pos;\n> +               int hr;\n> +\n> +               if (!commit->parents)\n> +                       die(_(\"replaying down from root commit is not supported yet!\"));\n> +               if (commit->parents->next)\n> +                       die(_(\"replaying merge commits is not supported yet!\"));\n> +\n> +               last_commit = pick_regular_commit(repo, commit, replayed_commits,\n> +                                                 onto, &merge_opt, &result);\n> +               if (!last_commit)\n> +                       break;\n> +\n> +               /* Record commit -> last_commit mapping */\n> +               pos = kh_put_oid_map(replayed_commits, commit->object.oid, &hr);\n> +               if (hr == 0)\n> +                       BUG(\"Duplicate rewritten commit: %s\\n\",\n> +                           oid_to_hex(&commit->object.oid));\n> +               kh_value(replayed_commits, pos) = last_commit;\n> +\n> +               /* Update any necessary branches */\n> +               if (advance)\n> +                       continue;\n> +               decoration = get_name_decoration(&commit->object);\n> +               if (!decoration)\n> +                       continue;\n> +               while (decoration) {\n> +                       if (decoration->type == DECORATION_REF_LOCAL &&\n> +                           (opts->contained || strset_contains(update_refs,\n> +                                                               decoration->name))) {\n> +                               replay_result_queue_update(out, decoration->name,\n> +                                                          &commit->object.oid,\n> +                                                          &last_commit->object.oid);\n> +                       }\n> +                       decoration = decoration->next;\n> +               }\n> +       }\n> +\n> +       if (!result.clean) {\n> +               out->merge_conflict = true;\n> +               ret = -1;\n> +               goto out;\n> +       }\n> +\n> +       /* In --advance mode, advance the target ref */\n> +       if (advance)\n> +               replay_result_queue_update(out, advance,\n> +                                          &onto->object.oid,\n> +                                          &last_commit->object.oid);\n> +\n> +       ret = 0;\n> +\n> +out:\n> +       if (update_refs) {\n> +               strset_clear(update_refs);\n> +               free(update_refs);\n> +       }\n> +       kh_destroy_oid_map(replayed_commits);\n> +       merge_finalize(&merge_opt, &result);\n> +       free(advance);\n> +       return ret;\n> +}\n> diff --git a/replay.h b/replay.h\n> new file mode 100644\n> index 0000000000..84bc8a7a5b\n> --- /dev/null\n> +++ b/replay.h\n> @@ -0,0 +1,64 @@\n> +#ifndef REPLAY_H\n> +#define REPLAY_H\n> +\n> +#include \"hash.h\"\n> +\n> +struct repository;\n> +struct rev_info;\n> +\n> +/*\n> + * A set of options that can be passed to `replay_revisions()`.\n> + */\n> +struct replay_revisions_options {\n> +       /*\n> +        * Starting point at which to create the new commits; must be a branch\n> +        * name. The branch will be updated to point to the rewritten commits.\n> +        * This option is mutually exclusive with `onto`.\n> +        */\n> +       const char *advance;\n> +\n> +       /*\n> +        * Starting point at which to create the new commits; must be a\n> +        * committish. References pointing at decendants of `onto` will be\n> +        * updated to point to the new commits.\n> +        */\n> +        const char *onto;\n> +\n> +       /*\n> +        * Update branches that point at commits in the given revision range.\n> +        * Requires `onto` to be set.\n> +        */\n> +       int contained;\n> +};\n> +\n> +/* This struct is used as an out-parameter by `replay_revisions()`. */\n> +struct replay_result {\n> +       /*\n> +        * The set of reference updates that are caused by replaying the\n> +        * commits.\n> +        */\n> +       struct replay_ref_update {\n> +               char *refname;\n> +               struct object_id old_oid;\n> +               struct object_id new_oid;\n> +       } *updates;\n> +       size_t updates_nr, updates_alloc;\n> +\n> +       /* Set to true in case the replay failed with a merge conflict. */\n> +       bool merge_conflict;\n> +};\n> +\n> +void replay_result_release(struct replay_result *result);\n> +\n> +/*\n> + * Replay a set of commits onto a new location. Leaves both the working tree,\n> + * index and references untouched. Reference updates caused by the replay will\n> + * be recorded in the `updates` out pointer.\n> + *\n> + * Returns 0 on success, a negative error code otherwise.\n> + */\n> +int replay_revisions(struct repository *repo, struct rev_info *revs,\n> +                    struct replay_revisions_options *opts,\n> +                    struct replay_result *out);\n> +\n\nstray extra line?\n\n> +#endif\n\nIt feels duplicative to have replay_result include a merge_conflict\nfield and to have replay_revisions() return an int which signifies\nwhether there's a conflict.  Can we remove one of the two?  (Perhaps\nthe merge_conflict field was only a workaround to the weird ret\nsetting from the previous patch?)\n"},{"id":"533458","messageId":"CABPp-BFXsZe5k-2qbkTfMaU7xxpViiHACOG+vwiRnf9xemd0QA@mail.gmail.com","threadId":"63986","inReplyTo":"20260109-b4-pks-history-builtin-v9-4-8766101814c6@pks.im","subject":"Re: [PATCH v9 4/7] replay: yield the object ID of the final rewritten commit","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2026-01-10T01:17:02Z","receivedAt":"2026-01-10T01:17:14Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Fri, Jan 9, 2026 at 12:35 AM Patrick Steinhardt <ps@pks.im> wrote:\n>\n> In a subsequent commit we'll introduce a new git-history(1) command that\n> uses the replay machinery to rewrite commits. One of its supported modes\n> will only want to update the \"HEAD\" reference, but that is not currently\n> supported by the replay machinery.\n\nI'm a little surprised.  In that case replay_result->updates_nr would\nbe 1 and you'd just use replay_result->updates[0], right?\n\n> Allow implementing this use case by exposing a `final_oid` field for the\n> reference updates. This field will be set to the last commit that was\n> rewritten, which is sufficient information for us to implement this mode\n> in git-history(1).\n>\n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n>  replay.c |  2 ++\n>  replay.h | 16 ++++++++++++++++\n>  2 files changed, 18 insertions(+)\n>\n> diff --git a/replay.c b/replay.c\n> index 313fb2a768..20560e792b 100644\n> --- a/replay.c\n> +++ b/replay.c\n> @@ -338,6 +338,8 @@ int replay_revisions(struct repository *repo, struct rev_info *revs,\n>                                            &onto->object.oid,\n>                                            &last_commit->object.oid);\n>\n> +       out->final_oid = last_commit->object.oid;\n> +\n>         ret = 0;\n>\n>  out:\n> diff --git a/replay.h b/replay.h\n> index 84bc8a7a5b..f8f9889112 100644\n> --- a/replay.h\n> +++ b/replay.h\n> @@ -46,6 +46,22 @@ struct replay_result {\n>\n>         /* Set to true in case the replay failed with a merge conflict. */\n>         bool merge_conflict;\n> +\n> +       /*\n> +        * The final object ID that was rewritten. Note that this field has\n> +        * somewhat special semantics and may or may not be what you want:\n> +        *\n> +        *   - If no commits were rewritten it will remain uninitialized.\n> +        *\n> +        *   - If a thicket of branches is rewritten it is undefined in which\n> +        *     order those branches will be rewritten, and thus the final object\n> +        *     ID may point to a different commit than you'd expect.\n> +        *\n> +        * That being said, this field can still be useful when you know that\n> +        * you only replay a single strand of commits. In that case, the final\n> +        * commit will point to the tip of the rewritten strand of commits.\n> +        */\n> +       struct object_id final_oid;\n>  };\n\nI don't understand why this is needed for the usecase you provide.\nAre you perhaps trying to rewrite a set of commits whose tip is not a\nbranch or something (directly contradicting your first paragraph of\nthe commit message)?  That's the only case I can think of where this\nwould be useful, unless I'm missing something?\n"},{"id":"533459","messageId":"CABPp-BF3jat7zStaydVa=fGHd7b=k6_KgDHt41XkOz+fMF+gbQ@mail.gmail.com","threadId":"63986","inReplyTo":"20260109-b4-pks-history-builtin-v9-6-8766101814c6@pks.im","subject":"Re: [PATCH v9 6/7] builtin: add new \"history\" command","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2026-01-10T01:17:25Z","receivedAt":"2026-01-10T01:17:37Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Fri, Jan 9, 2026 at 12:35 AM Patrick Steinhardt <ps@pks.im> wrote:\n>\n> When rewriting history via git-rebase(1) there are a few very common use\n> cases:\n>\n>   - The ordering of two commits should be reversed.\n>\n>   - A commit should be split up into two commits.\n>\n>   - A commit should be dropped from the history completely.\n>\n>   - Multiple commits should be squashed into one.\n>\n>   - Editing an existing commit that is not the tip of the current\n>     branch.\n>\n> While these operations are all doable, it often feels needlessly kludgey\n> to do so by doing an interactive rebase, using the editor to say what\n> one wants, and then perform the actions. Also, some operations like\n> splitting up a commit into two are way more involved than that and\n> require a whole series of commands.\n>\n> Rebases also do not update dependent branches. The use of stacked\n> branches has grown quite common with competing version control systems\n> like Jujutsu though, so it clearly is a need that users have. While\n> rebases _can_ serve this use case if one always works on the latest\n> stacked branch, it is somewhat awkward and very easy to get wrong.\n>\n> Add a new \"history\" command to plug these gaps. This command will have\n> several different subcommands to imperatively rewrite history for common\n> use cases like the above.\n>\n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n>  .gitignore                     |  1 +\n>  Documentation/git-history.adoc | 56 ++++++++++++++++++++++++++++++++++++++++++\n>  Documentation/meson.build      |  1 +\n>  Makefile                       |  1 +\n>  builtin.h                      |  1 +\n>  builtin/history.c              | 22 +++++++++++++++++\n>  command-list.txt               |  1 +\n>  git.c                          |  1 +\n>  meson.build                    |  1 +\n>  t/meson.build                  |  1 +\n>  t/t3450-history.sh             | 17 +++++++++++++\n>  11 files changed, 103 insertions(+)\n>\n> diff --git a/.gitignore b/.gitignore\n> index 78a45cb5be..24635cf2d6 100644\n> --- a/.gitignore\n> +++ b/.gitignore\n> @@ -79,6 +79,7 @@\n>  /git-grep\n>  /git-hash-object\n>  /git-help\n> +/git-history\n>  /git-hook\n>  /git-http-backend\n>  /git-http-fetch\n> diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc\n> new file mode 100644\n> index 0000000000..5a9d931efc\n> --- /dev/null\n> +++ b/Documentation/git-history.adoc\n> @@ -0,0 +1,56 @@\n> +git-history(1)\n> +==============\n> +\n> +NAME\n> +----\n> +git-history - EXPERIMENTAL: Rewrite history\n> +\n> +SYNOPSIS\n> +--------\n> +[synopsis]\n> +git history [<options>]\n> +\n> +DESCRIPTION\n> +-----------\n> +\n> +Rewrite history by rearranging or modifying specific commits in the\n> +history.\n> +\n> +THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.\n> +\n> +This command is related to linkgit:git-rebase[1] in that both commands can be\n> +used to rewrite history. There are a couple of major differences though:\n> +\n> +* linkgit:git-history[1] can work in a bare repository as it does not need to\n> +  touch either the index or the worktree.\n> +* linkgit:git-history[1] does not execute any linkgit:githooks[5] at the\n> +  current point in time. This may change in the future.\n> +* linkgit:git-history[1] by default updates all branches that are descendants\n> +  of the original commit to point to the rewritten commit.\n> +\n> +Overall, linkgit:git-history[1] aims to provide a more opinionated way to modify\n> +your commit history that is simpler to use compared to linkgit:git-rebase[1] in\n> +general.\n> +\n> +If you want to reapply a range of commits onto a different base, or interactive\n> +rebases if you want to edit a range of commits.\n\n\"If ...\" with no \"then ...\" ?  This sentence isn't complete.\n\n> +LIMITATIONS\n> +-----------\n> +\n> +This command does not (yet) work with histories that contain merges. You\n> +should use linkgit:git-rebase[1] with the `--rebase-merges` flag instead.\n> +\n> +Furthermore, the command does not support operations that can result in merge\n> +conflicts. This limitation is by design as history rewrites are not intended to\n> +be stateful operations. The limitation can be lifted once (if) Git learns about\n> +first-class conflicts.\n> +\n> +COMMANDS\n> +--------\n> +\n> +Several commands are available to rewrite history in different ways:\n\nUm, what are they?\n\n> +\n> +GIT\n> +---\n> +Part of the linkgit:git[1] suite\n> diff --git a/Documentation/meson.build b/Documentation/meson.build\n> index f02dbc20cb..fd2e8cc02d 100644\n> --- a/Documentation/meson.build\n> +++ b/Documentation/meson.build\n> @@ -64,6 +64,7 @@ manpages = {\n>    'git-gui.adoc' : 1,\n>    'git-hash-object.adoc' : 1,\n>    'git-help.adoc' : 1,\n> +  'git-history.adoc' : 1,\n>    'git-hook.adoc' : 1,\n>    'git-http-backend.adoc' : 1,\n>    'git-http-fetch.adoc' : 1,\n> diff --git a/Makefile b/Makefile\n> index 1c64a5d2ae..c0569ed8e4 100644\n> --- a/Makefile\n> +++ b/Makefile\n> @@ -1418,6 +1418,7 @@ BUILTIN_OBJS += builtin/get-tar-commit-id.o\n>  BUILTIN_OBJS += builtin/grep.o\n>  BUILTIN_OBJS += builtin/hash-object.o\n>  BUILTIN_OBJS += builtin/help.o\n> +BUILTIN_OBJS += builtin/history.o\n>  BUILTIN_OBJS += builtin/hook.o\n>  BUILTIN_OBJS += builtin/index-pack.o\n>  BUILTIN_OBJS += builtin/init-db.o\n> diff --git a/builtin.h b/builtin.h\n> index 1b35565fbd..93c91d07d4 100644\n> --- a/builtin.h\n> +++ b/builtin.h\n> @@ -172,6 +172,7 @@ int cmd_get_tar_commit_id(int argc, const char **argv, const char *prefix, struc\n>  int cmd_grep(int argc, const char **argv, const char *prefix, struct repository *repo);\n>  int cmd_hash_object(int argc, const char **argv, const char *prefix, struct repository *repo);\n>  int cmd_help(int argc, const char **argv, const char *prefix, struct repository *repo);\n> +int cmd_history(int argc, const char **argv, const char *prefix, struct repository *repo);\n>  int cmd_hook(int argc, const char **argv, const char *prefix, struct repository *repo);\n>  int cmd_index_pack(int argc, const char **argv, const char *prefix, struct repository *repo);\n>  int cmd_init_db(int argc, const char **argv, const char *prefix, struct repository *repo);\n> diff --git a/builtin/history.c b/builtin/history.c\n> new file mode 100644\n> index 0000000000..f6fe32610b\n> --- /dev/null\n> +++ b/builtin/history.c\n> @@ -0,0 +1,22 @@\n> +#include \"builtin.h\"\n> +#include \"gettext.h\"\n> +#include \"parse-options.h\"\n> +\n> +int cmd_history(int argc,\n> +               const char **argv,\n> +               const char *prefix,\n> +               struct repository *repo UNUSED)\n> +{\n> +       const char * const usage[] = {\n> +               N_(\"git history [<options>]\"),\n> +               NULL,\n> +       };\n> +       struct option options[] = {\n> +               OPT_END(),\n> +       };\n> +\n> +       argc = parse_options(argc, argv, prefix, options, usage, 0);\n> +       if (argc)\n> +               usagef(\"unrecognized argument: %s\", argv[0]);\n\nOh, the manual is just a placeholder that you'll extend later?  That\nmakes sense, but it would be less confusing if it started out with \"No\nsubcommands are yet supported, but will be added later\", with that\nsentence removed in the next commit when you add a subcommand.\n\n\n> +       return 0;\n> +}\n> diff --git a/command-list.txt b/command-list.txt\n> index accd3d0c4b..f9005cf459 100644\n> --- a/command-list.txt\n> +++ b/command-list.txt\n> @@ -115,6 +115,7 @@ git-grep                                mainporcelain           info\n>  git-gui                                 mainporcelain\n>  git-hash-object                         plumbingmanipulators\n>  git-help                                ancillaryinterrogators          complete\n> +git-history                             mainporcelain           history\n>  git-hook                                purehelpers\n>  git-http-backend                        synchingrepositories\n>  git-http-fetch                          synchelpers\n> diff --git a/git.c b/git.c\n> index c5fad56813..744cb6527e 100644\n> --- a/git.c\n> +++ b/git.c\n> @@ -586,6 +586,7 @@ static struct cmd_struct commands[] = {\n>         { \"grep\", cmd_grep, RUN_SETUP_GENTLY },\n>         { \"hash-object\", cmd_hash_object },\n>         { \"help\", cmd_help },\n> +       { \"history\", cmd_history, RUN_SETUP },\n>         { \"hook\", cmd_hook, RUN_SETUP },\n>         { \"index-pack\", cmd_index_pack, RUN_SETUP_GENTLY | NO_PARSEOPT },\n>         { \"init\", cmd_init_db },\n> diff --git a/meson.build b/meson.build\n> index a5a4e99b25..3a1d12caa4 100644\n> --- a/meson.build\n> +++ b/meson.build\n> @@ -610,6 +610,7 @@ builtin_sources = [\n>    'builtin/grep.c',\n>    'builtin/hash-object.c',\n>    'builtin/help.c',\n> +  'builtin/history.c',\n>    'builtin/hook.c',\n>    'builtin/index-pack.c',\n>    'builtin/init-db.c',\n> diff --git a/t/meson.build b/t/meson.build\n> index 459c52a489..73006b095a 100644\n> --- a/t/meson.build\n> +++ b/t/meson.build\n> @@ -387,6 +387,7 @@ integration_tests = [\n>    't3436-rebase-more-options.sh',\n>    't3437-rebase-fixup-options.sh',\n>    't3438-rebase-broken-files.sh',\n> +  't3450-history.sh',\n>    't3500-cherry.sh',\n>    't3501-revert-cherry-pick.sh',\n>    't3502-cherry-pick-merge.sh',\n> diff --git a/t/t3450-history.sh b/t/t3450-history.sh\n> new file mode 100755\n> index 0000000000..417c343d43\n> --- /dev/null\n> +++ b/t/t3450-history.sh\n> @@ -0,0 +1,17 @@\n> +#!/bin/sh\n> +\n> +test_description='tests for git-history command'\n> +\n> +. ./test-lib.sh\n> +\n> +test_expect_success 'does nothing without any arguments' '\n> +       git history >out 2>&1 &&\n> +       test_must_be_empty out\n> +'\n> +\n> +test_expect_success 'raises an error with unknown argument' '\n> +       test_must_fail git history garbage 2>err &&\n> +       test_grep \"unrecognized argument: garbage\" err\n> +'\n> +\n> +test_done\n>\n> --\n> 2.52.0.542.g9473a8513b.dirty\n"},{"id":"533463","messageId":"CABPp-BEVZbN08zF2P0wNWuOZozc+tbWodfOjtiAkX+XhMiyC6w@mail.gmail.com","threadId":"63986","inReplyTo":"20260109-b4-pks-history-builtin-v9-0-8766101814c6@pks.im","subject":"Re: [PATCH v9 0/7] Introduce git-history(1) command for easy history editing","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2026-01-10T01:26:48Z","receivedAt":"2026-01-10T01:27:01Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Fri, Jan 9, 2026 at 12:35 AM Patrick Steinhardt <ps@pks.im> wrote:\n> Changes in v9:\n>   - Rename `struct replay_ref_updates` to `struct replay_result` to make\n>     its semantics less focussed on ref updates, only.\n>   - Clarify and fix return codes of git-replay(1) so that we return 1 on\n>     conflict, 128 on an error and 0 on success.\n>   - The usual small improvements to commit messages.\n>   - Link to v8: https://lore.kernel.org/r/20260107-b4-pks-history-builtin-v8-0-18e9779e3a26@pks.im\n\nI read through this series in detail; it's forming into shape nicely.\nI think there's still a number of small implementation things to fix\nup (see my comments on the individual patches), but the design looks\ngood to me now.  I suspect we'll be ready to merge before long.\n\nThanks for your patience and hard work on this!\n"},{"id":"533495","messageId":"aWKI2BxszQuo1mRn@szeder.dev","threadId":"63986","inReplyTo":"CABPp-BEVZbN08zF2P0wNWuOZozc+tbWodfOjtiAkX+XhMiyC6w@mail.gmail.com","subject":"Re: [PATCH v9 0/7] Introduce git-history(1) command for easy history editing","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2026-01-10T17:14:00Z","receivedAt":"2026-01-10T17:14:03Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"On Fri, Jan 09, 2026 at 05:26:48PM -0800, Elijah Newren wrote:\n> On Fri, Jan 9, 2026 at 12:35 AM Patrick Steinhardt <ps@pks.im> wrote:\n> > Changes in v9:\n> >   - Rename `struct replay_ref_updates` to `struct replay_result` to make\n> >     its semantics less focussed on ref updates, only.\n> >   - Clarify and fix return codes of git-replay(1) so that we return 1 on\n> >     conflict, 128 on an error and 0 on success.\n> >   - The usual small improvements to commit messages.\n> >   - Link to v8: https://lore.kernel.org/r/20260107-b4-pks-history-builtin-v8-0-18e9779e3a26@pks.im\n> \n> I read through this series in detail; it's forming into shape nicely.\n> I think there's still a number of small implementation things to fix\n> up (see my comments on the individual patches), but the design looks\n> good to me now.  I suspect we'll be ready to merge before long.\n\nI don't think this should be merged until we have clear and feasible\nplans for future subcommands that will cause conflicts, to avoid\npainting ourselves into a corner, like when we couldn't change the not\nwell thought out details of 'git switch/restore' anymore.\n\n"},{"id":"533513","messageId":"20260111055859.408891-1-newren@gmail.com","threadId":"63986","inReplyTo":"20260109-b4-pks-history-builtin-v9-0-8766101814c6@pks.im","subject":"[PATCH 0/2] Some","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2026-01-11T05:58:57Z","receivedAt":"2026-01-11T05:59:14Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"I've been waiting for the history series to consolidate before\nupstream some local GitHub changes to replay and adding some others of\nmy own.\n\nSo, I figured sending some of my suggestions from my reviews as a\n\"SQUASH ME\" patch that Patrick could split up and squash into his series\nmight be helpful.\n\nPlus, I noticed a bug (pre-dating his series) with handling of detached\nheads in replay that has propagated with his history stuff, so I added a\nsecond patch to fix that bug in both replay and history..\n\nElijah Newren (2):\n  SQUASH ME: Fixups\n  history: fix detached HEAD handling\n\n Documentation/git-history.adoc |  2 +-\n builtin/history.c              | 31 ++++++++++++----------\n builtin/replay.c               | 10 ++------\n replay.c                       | 28 +++++++++++++-------\n replay.h                       | 21 +--------------\n t/t3451-history-reword.sh      | 47 ++++++++++++++++++++++++++++++++++\n 6 files changed, 87 insertions(+), 52 deletions(-)\n\n-- \n2.52.0.410.g227cdbe8e36\n\n"},{"id":"533514","messageId":"20260111055859.408891-2-newren@gmail.com","threadId":"63986","inReplyTo":"20260111055859.408891-1-newren@gmail.com","subject":"[PATCH v9 8/7] SQUASH ME: Fixups","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2026-01-11T05:58:58Z","receivedAt":"2026-01-11T05:59:16Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"This includes several fixes I highlighted in my review and needs to be\nsplit up and squashed into the relevant previous patches.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n Documentation/git-history.adoc |  2 +-\n builtin/history.c              | 16 ++++------------\n builtin/replay.c               | 10 ++--------\n replay.c                       | 10 ++++------\n replay.h                       | 21 +--------------------\n 5 files changed, 12 insertions(+), 47 deletions(-)\n\ndiff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc\nindex 4eea317e5cb..457a1314a51 100644\n--- a/Documentation/git-history.adoc\n+++ b/Documentation/git-history.adoc\n@@ -63,7 +63,7 @@ OPTIONS\n `--ref-action=(branches|head|print)`::\n \tControl which references will be updated by the command, if any. With\n \t`branches`, all local branches that point to commits which are\n-\tdecendants of the original commit will be rewritten. With `head`, only\n+\tdescendants of the original commit will be rewritten. With `head`, only\n \tthe current `HEAD` reference will be rewritten. With `print`, all\n \tupdates as they would be performed with `branches` are printed in a\n \tformat that can be consumed by linkgit:git-update-ref[1].\ndiff --git a/builtin/history.c b/builtin/history.c\nindex 28db6fc5b9e..60c5b5c5c76 100644\n--- a/builtin/history.c\n+++ b/builtin/history.c\n@@ -178,9 +178,7 @@ static int handle_reference_updates(enum ref_action action,\n {\n \tconst struct name_decoration *decoration;\n \tstruct replay_revisions_options opts = { 0 };\n-\tstruct replay_result result = {\n-\t\t.final_oid = rewritten->object.oid,\n-\t};\n+\tstruct replay_result result = { 0 };\n \tstruct ref_transaction *transaction = NULL;\n \tstruct strvec args = STRVEC_INIT;\n \tstruct strbuf err = STRBUF_INIT;\n@@ -233,7 +231,7 @@ static int handle_reference_updates(enum ref_action action,\n \t\t\tgoto out;\n \t\t}\n \n-\t\tstrvec_push(&args, oid_to_hex(&head->object.oid));\n+\t\tstrvec_push(&args, \"HEAD\");\n \t} else {\n \t\tstrvec_push(&args, \"--branches\");\n \t}\n@@ -244,13 +242,14 @@ static int handle_reference_updates(enum ref_action action,\n \n \topts.onto = oid_to_hex_r(hex, &rewritten->object.oid);\n \n-\tret = replay_revisions(repo, &revs, &opts, &result);\n+\tret = replay_revisions(&revs, &opts, &result);\n \tif (ret)\n \t\tgoto out;\n \n \tswitch (action) {\n \tcase REF_ACTION_DEFAULT:\n \tcase REF_ACTION_BRANCHES:\n+\tcase REF_ACTION_HEAD:\n \t\ttransaction = ref_store_transaction_begin(get_main_ref_store(repo), 0, &err);\n \t\tif (!transaction) {\n \t\t\tret = error(_(\"failed to begin ref transaction: %s\"), err.buf);\n@@ -300,13 +299,6 @@ static int handle_reference_updates(enum ref_action action,\n \t\t}\n \n \t\tbreak;\n-\tcase REF_ACTION_HEAD:\n-\t\tret = refs_update_ref(get_main_ref_store(repo), reflog_msg, \"HEAD\",\n-\t\t\t\t      &result.final_oid, &head->object.oid, 0,\n-\t\t\t\t      UPDATE_REFS_MSG_ON_ERR);\n-\t\tif (ret)\n-\t\t\tgoto out;\n-\t\tbreak;\n \tcase REF_ACTION_PRINT:\n \t\tfor (size_t i = 0; i < result.updates_nr; i++)\n \t\t\tprintf(\"update %s %s %s\\n\",\ndiff --git a/builtin/replay.c b/builtin/replay.c\nindex da8b7202f6a..f66e1a1fcd3 100644\n--- a/builtin/replay.c\n+++ b/builtin/replay.c\n@@ -167,7 +167,7 @@ int cmd_replay(int argc,\n \t\trevs.simplify_history = 0;\n \t}\n \n-\tret = replay_revisions(repo, &revs, &opts, &result);\n+\tret = replay_revisions(&revs, &opts, &result);\n \tif (ret)\n \t\tgoto cleanup;\n \n@@ -220,11 +220,5 @@ int cmd_replay(int argc,\n \tstrbuf_release(&reflog_msg);\n \trelease_revisions(&revs);\n \n-\tif (ret) {\n-\t\tif (result.merge_conflict)\n-\t\t\treturn 1;\n-\t\treturn 128;\n-\t}\n-\n-\treturn 0;\n+\treturn ret;\n }\ndiff --git a/replay.c b/replay.c\nindex 74e45ed27a2..ea300e3c36c 100644\n--- a/replay.c\n+++ b/replay.c\n@@ -5,11 +5,11 @@\n #include \"hex.h\"\n #include \"merge-ort.h\"\n #include \"object-name.h\"\n-#include \"oidset.h\"\n #include \"parse-options.h\"\n #include \"refs.h\"\n #include \"replay.h\"\n #include \"revision.h\"\n+#include \"strmap.h\"\n #include \"tree.h\"\n \n static const char *short_commit_name(struct repository *repo,\n@@ -256,7 +256,7 @@ static void replay_result_queue_update(struct replay_result *result,\n \tresult->updates_nr++;\n }\n \n-int replay_revisions(struct repository *repo, struct rev_info *revs,\n+int replay_revisions(struct rev_info *revs,\n \t\t     struct replay_revisions_options *opts,\n \t\t     struct replay_result *out)\n {\n@@ -265,6 +265,7 @@ int replay_revisions(struct repository *repo, struct rev_info *revs,\n \tstruct commit *last_commit = NULL;\n \tstruct commit *commit;\n \tstruct commit *onto = NULL;\n+\tstruct repository *repo = revs->repo;\n \tstruct merge_options merge_opt;\n \tstruct merge_result result = {\n \t\t.clean = 1,\n@@ -328,8 +329,7 @@ int replay_revisions(struct repository *repo, struct rev_info *revs,\n \t}\n \n \tif (!result.clean) {\n-\t\tout->merge_conflict = true;\n-\t\tret = -1;\n+\t\tret = 1;\n \t\tgoto out;\n \t}\n \n@@ -339,8 +339,6 @@ int replay_revisions(struct repository *repo, struct rev_info *revs,\n \t\t\t\t\t   &onto->object.oid,\n \t\t\t\t\t   &last_commit->object.oid);\n \n-\tout->final_oid = last_commit->object.oid;\n-\n \tret = 0;\n \n out:\ndiff --git a/replay.h b/replay.h\nindex f8f9889112e..9b31c85dd9b 100644\n--- a/replay.h\n+++ b/replay.h\n@@ -43,25 +43,6 @@ struct replay_result {\n \t\tstruct object_id new_oid;\n \t} *updates;\n \tsize_t updates_nr, updates_alloc;\n-\n-\t/* Set to true in case the replay failed with a merge conflict. */\n-\tbool merge_conflict;\n-\n-\t/*\n-\t * The final object ID that was rewritten. Note that this field has\n-\t * somewhat special semantics and may or may not be what you want:\n-\t *\n-\t *   - If no commits were rewritten it will remain uninitialized.\n-\t *\n-\t *   - If a thicket of branches is rewritten it is undefined in which\n-\t *     order those branches will be rewritten, and thus the final object\n-\t *     ID may point to a different commit than you'd expect.\n-\t *\n-\t * That being said, this field can still be useful when you know that\n-\t * you only replay a single strand of commits. In that case, the final\n-\t * commit will point to the tip of the rewritten strand of commits.\n-\t */\n-\tstruct object_id final_oid;\n };\n \n void replay_result_release(struct replay_result *result);\n@@ -73,7 +54,7 @@ void replay_result_release(struct replay_result *result);\n  *\n  * Returns 0 on success, a negative error code otherwise.\n  */\n-int replay_revisions(struct repository *repo, struct rev_info *revs,\n+int replay_revisions(struct rev_info *revs,\n \t\t     struct replay_revisions_options *opts,\n \t\t     struct replay_result *out);\n \n-- \n2.52.0.410.g227cdbe8e36\n\n"},{"id":"533515","messageId":"20260111055859.408891-3-newren@gmail.com","threadId":"63986","inReplyTo":"20260111055859.408891-1-newren@gmail.com","subject":"[PATCH v9 9/7] history: fix detached HEAD handling","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2026-01-11T05:58:59Z","receivedAt":"2026-01-11T05:59:17Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"The default behavior for history is to work on all local branches.  When\nHEAD is detached, it should be treated like a local branch as well.\nThe primary fix for this is just to make sure that in addition to passing\n--branches to the revision machinery that we pass HEAD as well.\n\nHowever, that doesn't quite do the trick, because we also process the\n\"decorations\" that point at commits that we have processed, and we do\nthis in two places -- in replay_revisions() as we replay commits, and\nin handle_reference_updates() when there are no commits to replay\nbecause the commit at the tip of the revision range was the one edited.\nIn both cases, we previously keyed off of DECORATION_REF_LOCAL to make\nsure we only looked at local branches.  Now, we need to also pay\nattention to DECORATION_REF_HEAD.  However, in order to avoid doing two\nupdates to the same branch (which will the ref transaction framework\nwould throw an error on), we need to only pay attention to\nDECORATION_REF_HEAD when we have a detached HEAD.\n\nSigned-off-by: Elijah Newren <newren@gmail.com>\n---\n builtin/history.c         | 15 +++++++++++--\n replay.c                  | 18 ++++++++++++---\n t/t3451-history-reword.sh | 47 +++++++++++++++++++++++++++++++++++++++\n 3 files changed, 75 insertions(+), 5 deletions(-)\n\ndiff --git a/builtin/history.c b/builtin/history.c\nindex 60c5b5c5c76..dd0df89c94e 100644\n--- a/builtin/history.c\n+++ b/builtin/history.c\n@@ -183,10 +183,18 @@ static int handle_reference_updates(enum ref_action action,\n \tstruct strvec args = STRVEC_INIT;\n \tstruct strbuf err = STRBUF_INIT;\n \tstruct commit *head = NULL;\n+\tchar *head_ref = NULL;\n+\tbool detached_head = false;\n \tstruct rev_info revs;\n \tchar hex[GIT_MAX_HEXSZ + 1];\n \tint ret;\n \n+\thead_ref = refs_resolve_refdup(get_main_ref_store(repo), \"HEAD\",\n+\t\t\t\t       RESOLVE_REF_READING, NULL, NULL);\n+\tif (!strcmp(head_ref, \"HEAD\"))\n+\t\tdetached_head = true;\n+\tfree(head_ref);\n+\n \trepo_init_revisions(repo, &revs, NULL);\n \tstrvec_push(&args, \"ignored\");\n \tstrvec_push(&args, \"--reverse\");\n@@ -234,6 +242,7 @@ static int handle_reference_updates(enum ref_action action,\n \t\tstrvec_push(&args, \"HEAD\");\n \t} else {\n \t\tstrvec_push(&args, \"--branches\");\n+\t\tstrvec_push(&args, \"HEAD\");\n \t}\n \n \tsetup_revisions_from_strvec(&args, &revs, NULL);\n@@ -278,9 +287,11 @@ static int handle_reference_updates(enum ref_action action,\n \t\t     decoration;\n \t\t     decoration = decoration->next)\n \t\t{\n-\t\t\tif (decoration->type != DECORATION_REF_LOCAL)\n+\t\t\tif ((decoration->type != DECORATION_REF_HEAD ||\n+\t\t\t     (action != REF_ACTION_HEAD && !detached_head)) &&\n+\t\t\t    (decoration->type != DECORATION_REF_LOCAL ||\n+\t\t\t     action == REF_ACTION_HEAD))\n \t\t\t\tcontinue;\n-\n \t\t\tret = ref_transaction_update(transaction,\n \t\t\t\t\t\t     decoration->name,\n \t\t\t\t\t\t     &rewritten->object.oid,\ndiff --git a/replay.c b/replay.c\nindex ea300e3c36c..7c98b1d1044 100644\n--- a/replay.c\n+++ b/replay.c\n@@ -151,11 +151,20 @@ static void get_ref_information(struct repository *repo,\n static void set_up_replay_mode(struct repository *repo,\n \t\t\t       struct rev_cmdline_info *cmd_info,\n \t\t\t       const char *onto_name,\n+\t\t\t       bool *detached_head,\n \t\t\t       char **advance_name,\n \t\t\t       struct commit **onto,\n \t\t\t       struct strset **update_refs)\n {\n \tstruct ref_info rinfo;\n+\tchar *head_ref;\n+\n+\t*detached_head = false;\n+\thead_ref = refs_resolve_refdup(get_main_ref_store(repo), \"HEAD\",\n+\t\t\t\t       RESOLVE_REF_READING, NULL, NULL);\n+\tif (!strcmp(head_ref, \"HEAD\"))\n+\t\t*detached_head = true;\n+\tfree(head_ref);\n \n \tget_ref_information(repo, cmd_info, &rinfo);\n \tif (!rinfo.positive_refexprs)\n@@ -271,11 +280,12 @@ int replay_revisions(struct rev_info *revs,\n \t\t.clean = 1,\n \t};\n \tchar *advance;\n+\tbool detached_head;\n \tint ret;\n \n \tadvance = xstrdup_or_null(opts->advance);\n-\tset_up_replay_mode(repo, &revs->cmdline, opts->onto, &advance,\n-\t\t\t   &onto, &update_refs);\n+\tset_up_replay_mode(repo, &revs->cmdline, opts->onto,\n+\t\t\t   &detached_head, &advance, &onto, &update_refs);\n \n \t/* FIXME: Should allow replaying commits with the first as a root commit */\n \n@@ -317,7 +327,9 @@ int replay_revisions(struct rev_info *revs,\n \t\tif (!decoration)\n \t\t\tcontinue;\n \t\twhile (decoration) {\n-\t\t\tif (decoration->type == DECORATION_REF_LOCAL &&\n+\t\t\tif ((decoration->type == DECORATION_REF_LOCAL ||\n+\t\t\t     (decoration->type == DECORATION_REF_HEAD &&\n+\t\t\t      detached_head)) &&\n \t\t\t    (opts->contained || strset_contains(update_refs,\n \t\t\t\t\t\t\t\tdecoration->name))) {\n \t\t\t\treplay_result_queue_update(out, decoration->name,\ndiff --git a/t/t3451-history-reword.sh b/t/t3451-history-reword.sh\nindex cd5883051d6..7ddbab8e676 100755\n--- a/t/t3451-history-reword.sh\n+++ b/t/t3451-history-reword.sh\n@@ -77,6 +77,53 @@ test_expect_success 'can reword commit in the middle' '\n \t)\n '\n \n+test_expect_success 'can reword commit in the middle even on detached head' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit first &&\n+\t\ttest_commit second &&\n+\t\ttest_commit third_on_main &&\n+\t\tgit checkout --detach HEAD^ &&\n+\t\ttest_commit third_on_head &&\n+\n+\t\treword_with_message HEAD~ <<-EOF &&\n+\t\tsecond reworded\n+\t\tEOF\n+\n+\t\texpect_log HEAD --branches --graph <<-\\EOF\n+\t\t* third_on_head\n+\t\t| * third_on_main\n+\t\t|/  \n+\t\t* second reworded\n+\t\t* first\n+\t\tEOF\n+\t)\n+'\n+\n+test_expect_success 'can reword the detached head' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit first &&\n+\t\ttest_commit second &&\n+\t\tgit checkout --detach HEAD &&\n+\t\ttest_commit third &&\n+\n+\t\treword_with_message HEAD <<-EOF &&\n+\t\tthird reworded\n+\t\tEOF\n+\n+\t\texpect_log <<-\\EOF\n+\t\tthird reworded\n+\t\tsecond\n+\t\tfirst\n+\t\tEOF\n+\t)\n+'\n+\n test_expect_success 'can reword root commit' '\n \ttest_when_finished \"rm -rf repo\" &&\n \tgit init repo &&\n-- \n2.52.0.410.g227cdbe8e36\n\n"},{"id":"533516","messageId":"CABPp-BFOqa5aTYgQEqxGF-2WUFiM3uTfwyNmvu9BaVFxYPpggw@mail.gmail.com","threadId":"63986","inReplyTo":"20260111055859.408891-1-newren@gmail.com","subject":"Re: [PATCH 0/2] Some","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2026-01-11T06:15:35Z","receivedAt":"2026-01-11T06:15:48Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"The subject should have been \"Some fixes for ps/history and replay\".  Oops.\n\nOn Sat, Jan 10, 2026 at 9:59 PM Elijah Newren <newren@gmail.com> wrote:\n>\n> I've been waiting for the history series to consolidate before\n> upstream some local GitHub changes to replay and adding some others of\n> my own.\n>\n> So, I figured sending some of my suggestions from my reviews as a\n> \"SQUASH ME\" patch that Patrick could split up and squash into his series\n> might be helpful.\n>\n> Plus, I noticed a bug (pre-dating his series) with handling of detached\n> heads in replay that has propagated with his history stuff, so I added a\n> second patch to fix that bug in both replay and history..\n>\n> Elijah Newren (2):\n>   SQUASH ME: Fixups\n>   history: fix detached HEAD handling\n>\n>  Documentation/git-history.adoc |  2 +-\n>  builtin/history.c              | 31 ++++++++++++----------\n>  builtin/replay.c               | 10 ++------\n>  replay.c                       | 28 +++++++++++++-------\n>  replay.h                       | 21 +--------------\n>  t/t3451-history-reword.sh      | 47 ++++++++++++++++++++++++++++++++++\n>  6 files changed, 87 insertions(+), 52 deletions(-)\n>\n> --\n> 2.52.0.410.g227cdbe8e36\n>\n"},{"id":"533612","messageId":"aWTwyiNuiybFIFAW@pks.im","threadId":"63986","inReplyTo":"CABPp-BFmQHjyeT0pXYV5eE5cnG5H6XXHeT_RsjWxq83ck3781w@mail.gmail.com","subject":"Re: [PATCH v9 1/7] builtin/replay: extract core logic to replay revisions","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-01-12T13:02:02Z","receivedAt":"2026-01-12T13:02:15Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Fri, Jan 09, 2026 at 05:14:48PM -0800, Elijah Newren wrote:\n> On Fri, Jan 9, 2026 at 12:35 AM Patrick Steinhardt <ps@pks.im> wrote:\n> > diff --git a/builtin/replay.c b/builtin/replay.c\n> > index 1960bbbee8..df3b32a52d 100644\n> > --- a/builtin/replay.c\n> > +++ b/builtin/replay.c\n> > @@ -278,6 +278,137 @@ static enum ref_action_mode get_ref_action_mode(struct repository *repo, const c\n> >         return REF_ACTION_UPDATE;\n> >  }\n> >\n> > +struct replay_revisions_options {\n> > +       const char *advance;\n> > +       const char *onto;\n> > +       int contained;\n> > +};\n> > +\n> > +struct replay_result {\n> > +       struct replay_ref_update {\n> > +               char *refname;\n> > +               struct object_id old_oid;\n> > +               struct object_id new_oid;\n> > +       } *updates;\n> > +       size_t updates_nr, updates_alloc;\n> > +\n> > +       bool merge_conflict;\n> > +};\n> > +\n> > +static void replay_result_release(struct replay_result *result)\n> > +{\n> > +       for (size_t i = 0; i < result->updates_nr; i++)\n> > +               free(result->updates[i].refname);\n> > +       free(result->updates);\n> > +}\n> > +\n> > +static void replay_result_queue_update(struct replay_result *result,\n> > +                                      const char *refname,\n> > +                                      const struct object_id *old_oid,\n> > +                                      const struct object_id *new_oid)\n> > +{\n> > +       ALLOC_GROW(result->updates, result->updates_nr + 1, result->updates_alloc);\n> > +       result->updates[result->updates_nr].refname = xstrdup(refname);\n> > +       result->updates[result->updates_nr].old_oid = *old_oid;\n> > +       result->updates[result->updates_nr].new_oid = *new_oid;\n> > +       result->updates_nr++;\n> > +}\n> > +\n> > +static int replay_revisions(struct repository *repo, struct rev_info *revs,\n> > +                           struct replay_revisions_options *opts,\n> > +                           struct replay_result *out)\n> \n> Why have both repo & revs?  Can't we get repo from revs->repo?\n\nTrue indeed, will change.\n\n> > @@ -517,24 +578,19 @@ int cmd_replay(int argc,\n> >                 }\n> >         }\n> >\n> > -       merge_finalize(&merge_opt, &result);\n> > -       kh_destroy_oid_map(replayed_commits);\n> > -       if (update_refs) {\n> > -               strset_clear(update_refs);\n> > -               free(update_refs);\n> > -       }\n> > -       ret = result.clean;\n> > -\n> >  cleanup:\n> >         if (transaction)\n> >                 ref_transaction_free(transaction);\n> > +       replay_result_release(&result);\n> >         strbuf_release(&transaction_err);\n> >         strbuf_release(&reflog_msg);\n> >         release_revisions(&revs);\n> > -       free(advance_name);\n> >\n> > -       /* Return */\n> > -       if (ret < 0)\n> > -               exit(128);\n> > -       return ret ? 0 : 1;\n> > +       if (ret) {\n> > +               if (result.merge_conflict)\n> > +                       return 1;\n> \n> This feels like a rather Rube-Goldberg way to get this.  Above where I\n> highlighted where you set ret to -1 incorrectly, can we just set it to\n> 1 there and then lose all this special logic in favor of \"return ret;\"\n> here?  Or am I overlooking something?\n\nYou're not really overlooking anything, no. The reason I did it this way\nhere is to make the logic more explicit -- a simple return value is very\nfragile, and I broke this in v8 of this patch series already.\n\nFurthermore, this is preparing for us to move this code into a separate\nlibrary. Not all callers may want to handle conflicts the exact same, so\nI think it's sensible to explicitly tell the caller about such error\ncases. One way to do this is to use e.g. an enum to surface the info,\nbut that is somewhat limiting. We may for example tell the caller later\non what the conflicting commit was, and that's becoming way easier by\nhaving the info in `sturct replay_result`.\n\nPatrick\n"},{"id":"533613","messageId":"aWTw4ezgeloLB0R2@pks.im","threadId":"63986","inReplyTo":"CABPp-BEo5jGBgJBkCdu_GHstsbEm4mCpKO3NWvNfcjDVC+SbLQ@mail.gmail.com","subject":"Re: [PATCH v9 2/7] builtin/replay: move core logic into \"libgit.a\"","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-01-12T13:02:25Z","receivedAt":"2026-01-12T13:02:31Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Fri, Jan 09, 2026 at 05:16:41PM -0800, Elijah Newren wrote:\n> On Fri, Jan 9, 2026 at 12:35 AM Patrick Steinhardt <ps@pks.im> wrote:\n> > diff --git a/replay.c b/replay.c\n> > new file mode 100644\n> > index 0000000000..fc7186ef09\n> > --- /dev/null\n> > +++ b/replay.c\n> > @@ -0,0 +1,355 @@\n> > +#define USE_THE_REPOSITORY_VARIABLE\n> > +#define DISABLE_SIGN_COMPARE_WARNINGS\n> > +\n> > +#include \"git-compat-util.h\"\n> > +#include \"environment.h\"\n> > +#include \"hex.h\"\n> > +#include \"merge-ort.h\"\n> > +#include \"object-name.h\"\n> > +#include \"oidset.h\"\n> \n> I don't think oidset is used here?   Although, come to think of it,\n> I'm not sure it was used in replay to begin with.  Looks like I added\n> this include during the switch from t/helper/test-fast-rebase, but\n> never used it even then.  I must have thought I was going to use it,\n> and added it, but never did.  Anyway, maybe this is a good time to get\n> rid of it?\n\nYeah, let's.\n\n> > +#include \"parse-options.h\"\n> \n> Why would parse-options be included here?  Oh,\n> die_for_incompatible_opt2()?  Feels a little weird that we have option\n> parsing logic outside of builtin/, but...maybe it's all fine?\n\nI'm not a huge fan of it, either. We could for example change this so\nthat we `BUG()` here and instead have the option in \"builtin/replay.c\"?\nSomething like this:\n\ndiff --git a/builtin/replay.c b/builtin/replay.c\nindex 4a11ef0f1b..649c93200e 100644\n--- a/builtin/replay.c\n+++ b/builtin/replay.c\n@@ -177,8 +177,9 @@ static void set_up_replay_mode(struct repository *repo,\n \tif (!rinfo.positive_refexprs)\n \t\tdie(_(\"need some commits to replay\"));\n \n-\tdie_for_incompatible_opt2(!!onto_name, \"--onto\",\n-\t\t\t\t  !!*advance_name, \"--advance\");\n+\tif (!(!!onto_name ^ !!*advance_name))\n+\t\tBUG(\"expected either onto_name or *advance_name in this function\");\n+\n \tif (onto_name) {\n \t\t*onto = peel_committish(repo, onto_name, \"--onto\");\n \t\tif (rinfo.positive_refexprs <\n@@ -191,9 +192,6 @@ static void set_up_replay_mode(struct repository *repo,\n \t\tstruct object_id oid;\n \t\tchar *fullname = NULL;\n \n-\t\tif (!*advance_name)\n-\t\t\tBUG(\"expected either onto_name or *advance_name in this function\");\n-\n \t\tif (repo_dwim_ref(repo, *advance_name, strlen(*advance_name),\n \t\t\t     &oid, &fullname, 0) == 1) {\n \t\t\tfree(*advance_name);\n@@ -478,6 +476,8 @@ int cmd_replay(int argc,\n \n \tdie_for_incompatible_opt2(!!opts.advance, \"--advance\",\n \t\t\t\t  opts.contained, \"--contained\");\n+\tdie_for_incompatible_opt2(!!opts.advance, \"--advance\",\n+\t\t\t\t  !!opts.onto, \"--onto\");\n \n \t/* Parse ref action mode from command line or config */\n \tref_mode = get_ref_action_mode(repo, ref_action);\n\nYeha, I think that's cleaner, even if it repeats some of the\nconditionals.\n\n> > +#include \"refs.h\"\n> > +#include \"replay.h\"\n> > +#include \"revision.h\"\n> \n> Shouldn't strmap.h (for strset) also be included?  I think we get it\n> as a side-effect of something else, but since we use it directly, it'd\n> make sense to include directly.\n\nYup, will add.\n\n> > diff --git a/replay.h b/replay.h\n> > new file mode 100644\n> > index 0000000000..84bc8a7a5b\n> > --- /dev/null\n> > +++ b/replay.h\n> > @@ -0,0 +1,64 @@\n> > +#ifndef REPLAY_H\n> > +#define REPLAY_H\n> > +\n> > +#include \"hash.h\"\n> > +\n> > +struct repository;\n> > +struct rev_info;\n> > +\n> > +/*\n> > + * A set of options that can be passed to `replay_revisions()`.\n> > + */\n> > +struct replay_revisions_options {\n> > +       /*\n> > +        * Starting point at which to create the new commits; must be a branch\n> > +        * name. The branch will be updated to point to the rewritten commits.\n> > +        * This option is mutually exclusive with `onto`.\n> > +        */\n> > +       const char *advance;\n> > +\n> > +       /*\n> > +        * Starting point at which to create the new commits; must be a\n> > +        * committish. References pointing at decendants of `onto` will be\n> > +        * updated to point to the new commits.\n> > +        */\n> > +        const char *onto;\n> > +\n> > +       /*\n> > +        * Update branches that point at commits in the given revision range.\n> > +        * Requires `onto` to be set.\n> > +        */\n> > +       int contained;\n> > +};\n> > +\n> > +/* This struct is used as an out-parameter by `replay_revisions()`. */\n> > +struct replay_result {\n> > +       /*\n> > +        * The set of reference updates that are caused by replaying the\n> > +        * commits.\n> > +        */\n> > +       struct replay_ref_update {\n> > +               char *refname;\n> > +               struct object_id old_oid;\n> > +               struct object_id new_oid;\n> > +       } *updates;\n> > +       size_t updates_nr, updates_alloc;\n> > +\n> > +       /* Set to true in case the replay failed with a merge conflict. */\n> > +       bool merge_conflict;\n> > +};\n> > +\n> > +void replay_result_release(struct replay_result *result);\n> > +\n> > +/*\n> > + * Replay a set of commits onto a new location. Leaves both the working tree,\n> > + * index and references untouched. Reference updates caused by the replay will\n> > + * be recorded in the `updates` out pointer.\n> > + *\n> > + * Returns 0 on success, a negative error code otherwise.\n> > + */\n> > +int replay_revisions(struct repository *repo, struct rev_info *revs,\n> > +                    struct replay_revisions_options *opts,\n> > +                    struct replay_result *out);\n> > +\n> \n> stray extra line?\n\nWe typically have an empty line between the last declaration and the\n`#endif` in our headers.\n\n> > +#endif\n> \n> It feels duplicative to have replay_result include a merge_conflict\n> field and to have replay_revisions() return an int which signifies\n> whether there's a conflict.  Can we remove one of the two?  (Perhaps\n> the merge_conflict field was only a workaround to the weird ret\n> setting from the previous patch?)\n\nThe idea here is that we have a generic error code that tells the caller\nthat _something_ happened, whereas `struct replay_result` gives the\ncaller a bit more context around what exactly has happened. This allows\ncallers to handle merge conflicts differently from any other error and\nmakes the different failure modes a lot more explicit.\n\nSome context: at GitLab we actually have the use case to surface more\ninformation around what commits have conflicted, so there will be a\nfollow-up patch series that extends `struct replay_result` to return\nmore information about the actual conflict. I'm already planning ahead a\nbit in this patch series.\n\nPatrick\n"},{"id":"533614","messageId":"aWTw7I7JPUnRBFR2@pks.im","threadId":"63986","inReplyTo":"CABPp-BF3jat7zStaydVa=fGHd7b=k6_KgDHt41XkOz+fMF+gbQ@mail.gmail.com","subject":"Re: [PATCH v9 6/7] builtin: add new \"history\" command","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-01-12T13:02:36Z","receivedAt":"2026-01-12T13:02:42Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Fri, Jan 09, 2026 at 05:17:25PM -0800, Elijah Newren wrote:\n> On Fri, Jan 9, 2026 at 12:35 AM Patrick Steinhardt <ps@pks.im> wrote:\n> > diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc\n> > new file mode 100644\n> > index 0000000000..5a9d931efc\n> > --- /dev/null\n> > +++ b/Documentation/git-history.adoc\n> > @@ -0,0 +1,56 @@\n> > +git-history(1)\n> > +==============\n> > +\n> > +NAME\n> > +----\n> > +git-history - EXPERIMENTAL: Rewrite history\n> > +\n> > +SYNOPSIS\n> > +--------\n> > +[synopsis]\n> > +git history [<options>]\n> > +\n> > +DESCRIPTION\n> > +-----------\n> > +\n> > +Rewrite history by rearranging or modifying specific commits in the\n> > +history.\n> > +\n> > +THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.\n> > +\n> > +This command is related to linkgit:git-rebase[1] in that both commands can be\n> > +used to rewrite history. There are a couple of major differences though:\n> > +\n> > +* linkgit:git-history[1] can work in a bare repository as it does not need to\n> > +  touch either the index or the worktree.\n> > +* linkgit:git-history[1] does not execute any linkgit:githooks[5] at the\n> > +  current point in time. This may change in the future.\n> > +* linkgit:git-history[1] by default updates all branches that are descendants\n> > +  of the original commit to point to the rewritten commit.\n> > +\n> > +Overall, linkgit:git-history[1] aims to provide a more opinionated way to modify\n> > +your commit history that is simpler to use compared to linkgit:git-rebase[1] in\n> > +general.\n> > +\n> > +If you want to reapply a range of commits onto a different base, or interactive\n> > +rebases if you want to edit a range of commits.\n> \n> \"If ...\" with no \"then ...\" ?  This sentence isn't complete.\n\nOh, true. I noticed this error, but only fixed it in the next commit.\nWill move around.\n\n> > diff --git a/builtin/history.c b/builtin/history.c\n> > new file mode 100644\n> > index 0000000000..f6fe32610b\n> > --- /dev/null\n> > +++ b/builtin/history.c\n> > @@ -0,0 +1,22 @@\n> > +#include \"builtin.h\"\n> > +#include \"gettext.h\"\n> > +#include \"parse-options.h\"\n> > +\n> > +int cmd_history(int argc,\n> > +               const char **argv,\n> > +               const char *prefix,\n> > +               struct repository *repo UNUSED)\n> > +{\n> > +       const char * const usage[] = {\n> > +               N_(\"git history [<options>]\"),\n> > +               NULL,\n> > +       };\n> > +       struct option options[] = {\n> > +               OPT_END(),\n> > +       };\n> > +\n> > +       argc = parse_options(argc, argv, prefix, options, usage, 0);\n> > +       if (argc)\n> > +               usagef(\"unrecognized argument: %s\", argv[0]);\n> \n> Oh, the manual is just a placeholder that you'll extend later?  That\n> makes sense, but it would be less confusing if it started out with \"No\n> subcommands are yet supported, but will be added later\", with that\n> sentence removed in the next commit when you add a subcommand.\n\nYup. Will change.\n\nPatrick\n"},{"id":"533615","messageId":"aWTw9Zf9nz4nHxKK@pks.im","threadId":"63986","inReplyTo":"20260111055859.408891-1-newren@gmail.com","subject":"Re: [PATCH 0/2] Some","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-01-12T13:02:45Z","receivedAt":"2026-01-12T13:02:49Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Sat, Jan 10, 2026 at 09:58:57PM -0800, Elijah Newren wrote:\n> I've been waiting for the history series to consolidate before\n> upstream some local GitHub changes to replay and adding some others of\n> my own.\n> \n> So, I figured sending some of my suggestions from my reviews as a\n> \"SQUASH ME\" patch that Patrick could split up and squash into his series\n> might be helpful.\n> \n> Plus, I noticed a bug (pre-dating his series) with handling of detached\n> heads in replay that has propagated with his history stuff, so I added a\n> second patch to fix that bug in both replay and history..\n\nPerfect, I'll work these patches into my series. Thanks!\n\nPatrick\n"},{"id":"533616","messageId":"aWTxDlV5alleJnSq@pks.im","threadId":"63986","inReplyTo":"aWKI2BxszQuo1mRn@szeder.dev","subject":"Re: [PATCH v9 0/7] Introduce git-history(1) command for easy history editing","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-01-12T13:03:10Z","receivedAt":"2026-01-12T13:03:16Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Sat, Jan 10, 2026 at 06:14:00PM +0100, SZEDER Gábor wrote:\n> On Fri, Jan 09, 2026 at 05:26:48PM -0800, Elijah Newren wrote:\n> > On Fri, Jan 9, 2026 at 12:35 AM Patrick Steinhardt <ps@pks.im> wrote:\n> > > Changes in v9:\n> > >   - Rename `struct replay_ref_updates` to `struct replay_result` to make\n> > >     its semantics less focussed on ref updates, only.\n> > >   - Clarify and fix return codes of git-replay(1) so that we return 1 on\n> > >     conflict, 128 on an error and 0 on success.\n> > >   - The usual small improvements to commit messages.\n> > >   - Link to v8: https://lore.kernel.org/r/20260107-b4-pks-history-builtin-v8-0-18e9779e3a26@pks.im\n> > \n> > I read through this series in detail; it's forming into shape nicely.\n> > I think there's still a number of small implementation things to fix\n> > up (see my comments on the individual patches), but the design looks\n> > good to me now.  I suspect we'll be ready to merge before long.\n> \n> I don't think this should be merged until we have clear and feasible\n> plans for future subcommands that will cause conflicts, to avoid\n> painting ourselves into a corner, like when we couldn't change the not\n> well thought out details of 'git switch/restore' anymore.\n\nI agree that we need to think a bit about how we'll handle conflicts\neventually. But I don't agree that this means we cannot already merge\nthis series for subcommands that _cannot_ have conflicts as I don't\nreally see how design decisions around handling conflicts would have an\ninfluence on subcommands that never have any conflicts in the first\nplace.\n\nThe intent of this command is that it should allow users to easily\nhandle certain common operations, without them having to worry about the\nunderpinnings. So even if we eventually figure out that the replay\nsubsystem is not a good fit for e.g. a potential `git history edit`\ncommand I don't think that this means that the whole status quo of `git\nhistory reword` would be impacted. And even if we eventuall _do_ decide\nto switch back to for example the sequencer subsystem I wouldn't expect\nthat to impact the overall design of `git history reword` either.\n\nSo yes, I think we should have a discussion around how we can:\n\n  - Handle conflicts in replay-based commands in general.\n\n  - Support stacked branches workflows with conflicting commits.\n\nFrom my point of view this leads into the direction of first-class\nconflicts in Git, and I think Elijah also leans into that direction. But\nit feels like a separate discussion to be had that is independent from\nthe current proposed subcommands.\n\nAlso, with the new semantics that we have aligned on with dependent\nbranches being rewritten I also think that we'll want to get the inital\ninfrastructure out there to facilitate user feedback. Such feedback can\nalso help us align on a better design for git-history(1) going forward.\n\nThanks!\n\nPatrick\n"},{"id":"533617","messageId":"aWTxF0xRiCm49lng@pks.im","threadId":"63986","inReplyTo":"CABPp-BFXsZe5k-2qbkTfMaU7xxpViiHACOG+vwiRnf9xemd0QA@mail.gmail.com","subject":"Re: [PATCH v9 4/7] replay: yield the object ID of the final rewritten commit","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-01-12T13:03:19Z","receivedAt":"2026-01-12T13:03:24Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Fri, Jan 09, 2026 at 05:17:02PM -0800, Elijah Newren wrote:\n> On Fri, Jan 9, 2026 at 12:35 AM Patrick Steinhardt <ps@pks.im> wrote:\n> > diff --git a/replay.h b/replay.h\n> > index 84bc8a7a5b..f8f9889112 100644\n> > --- a/replay.h\n> > +++ b/replay.h\n> > @@ -46,6 +46,22 @@ struct replay_result {\n> >\n> >         /* Set to true in case the replay failed with a merge conflict. */\n> >         bool merge_conflict;\n> > +\n> > +       /*\n> > +        * The final object ID that was rewritten. Note that this field has\n> > +        * somewhat special semantics and may or may not be what you want:\n> > +        *\n> > +        *   - If no commits were rewritten it will remain uninitialized.\n> > +        *\n> > +        *   - If a thicket of branches is rewritten it is undefined in which\n> > +        *     order those branches will be rewritten, and thus the final object\n> > +        *     ID may point to a different commit than you'd expect.\n> > +        *\n> > +        * That being said, this field can still be useful when you know that\n> > +        * you only replay a single strand of commits. In that case, the final\n> > +        * commit will point to the tip of the rewritten strand of commits.\n> > +        */\n> > +       struct object_id final_oid;\n> >  };\n> \n> I don't understand why this is needed for the usecase you provide.\n> Are you perhaps trying to rewrite a set of commits whose tip is not a\n> branch or something (directly contradicting your first paragraph of\n> the commit message)?  That's the only case I can think of where this\n> would be useful, unless I'm missing something?\n\nIt's the case where you're rewriting a detached HEAD. But I'll discard\nthis commit in favor of what you've posted.\n\nThanks!\n\nPatrick\n"},{"id":"533697","messageId":"CABPp-BGM4AxoedD3uUnS+12n5c0egd8pw-=cRsO64oDs+G9RkA@mail.gmail.com","threadId":"63986","inReplyTo":"aWTw4ezgeloLB0R2@pks.im","subject":"Re: [PATCH v9 2/7] builtin/replay: move core logic into \"libgit.a\"","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2026-01-13T06:00:16Z","receivedAt":"2026-01-13T06:00:29Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Mon, Jan 12, 2026 at 5:02 AM Patrick Steinhardt <ps@pks.im> wrote:\n>\n> On Fri, Jan 09, 2026 at 05:16:41PM -0800, Elijah Newren wrote:\n[...]\n\n> > > +int replay_revisions(struct repository *repo, struct rev_info *revs,\n> > > +                    struct replay_revisions_options *opts,\n> > > +                    struct replay_result *out);\n> > > +\n> >\n> > stray extra line?\n>\n> We typically have an empty line between the last declaration and the\n> `#endif` in our headers.\n\nYeah, I was just confused; sorry.\n\n> > > +#endif\n> >\n> > It feels duplicative to have replay_result include a merge_conflict\n> > field and to have replay_revisions() return an int which signifies\n> > whether there's a conflict.  Can we remove one of the two?  (Perhaps\n> > the merge_conflict field was only a workaround to the weird ret\n> > setting from the previous patch?)\n>\n> The idea here is that we have a generic error code that tells the caller\n> that _something_ happened, whereas `struct replay_result` gives the\n> caller a bit more context around what exactly has happened. This allows\n> callers to handle merge conflicts differently from any other error and\n> makes the different failure modes a lot more explicit.\n>\n> Some context: at GitLab we actually have the use case to surface more\n> information around what commits have conflicted, so there will be a\n\nInteresting, but doesn't answering that question presume first-class\nconflict handling?  How do you determine which commits conflict\nwithout that?  Or, is the first commit we process that hits a conflict\nsufficient information and you don't really need the commits that\nconflict, just one of them?  Or, do you presume that all unprocessed\ncommits after the first one that conflicted would have also conflicted\n(even if it touched files that are conflict-free so far, so that\ncommit would not have contributed to the conflicts)?  If this is done\nwithout first-class conflict handling, there may also be an assumption\nhere about linear single-branch history, or else some kind of attempt\nto continue processing whichever commits don't have an ancestor that\nhas conflicted, so that we can enumerate \"commits [which] have\nconflicted\".\n\n> follow-up patch series that extends `struct replay_result` to return\n> more information about the actual conflict. I'm already planning ahead a\n> bit in this patch series.\n\nWait, above you said you wanted to know the \"commits [which] have\nconflicted\", here you seem to be suggesting you want to know about\n\"the actual conflict\" which might mean you only care about the first\ncommit that conflicted but you want details about what conflicted\nwithin it.  Or is it perhaps the set of files that would have had\nconflicts across replaying the whole sequence of patches (in which\ncase a bunch of the previous questions are still valid)?\n"},{"id":"533710","messageId":"aWX0wyQ_2Mn9ZOEJ@pks.im","threadId":"63986","inReplyTo":"CABPp-BGM4AxoedD3uUnS+12n5c0egd8pw-=cRsO64oDs+G9RkA@mail.gmail.com","subject":"Re: [PATCH v9 2/7] builtin/replay: move core logic into \"libgit.a\"","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-01-13T07:31:15Z","receivedAt":"2026-01-13T07:31:23Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Mon, Jan 12, 2026 at 10:00:16PM -0800, Elijah Newren wrote:\n> On Mon, Jan 12, 2026 at 5:02 AM Patrick Steinhardt <ps@pks.im> wrote:\n> >\n> > On Fri, Jan 09, 2026 at 05:16:41PM -0800, Elijah Newren wrote:\n> [...]\n> > > It feels duplicative to have replay_result include a merge_conflict\n> > > field and to have replay_revisions() return an int which signifies\n> > > whether there's a conflict.  Can we remove one of the two?  (Perhaps\n> > > the merge_conflict field was only a workaround to the weird ret\n> > > setting from the previous patch?)\n> >\n> > The idea here is that we have a generic error code that tells the caller\n> > that _something_ happened, whereas `struct replay_result` gives the\n> > caller a bit more context around what exactly has happened. This allows\n> > callers to handle merge conflicts differently from any other error and\n> > makes the different failure modes a lot more explicit.\n> >\n> > Some context: at GitLab we actually have the use case to surface more\n> > information around what commits have conflicted, so there will be a\n> \n> Interesting, but doesn't answering that question presume first-class\n> conflict handling?  How do you determine which commits conflict\n> without that?  Or, is the first commit we process that hits a conflict\n> sufficient information and you don't really need the commits that\n> conflict, just one of them?  Or, do you presume that all unprocessed\n> commits after the first one that conflicted would have also conflicted\n> (even if it touched files that are conflict-free so far, so that\n> commit would not have contributed to the conflicts)?  If this is done\n> without first-class conflict handling, there may also be an assumption\n> here about linear single-branch history, or else some kind of attempt\n> to continue processing whichever commits don't have an ancestor that\n> has conflicted, so that we can enumerate \"commits [which] have\n> conflicted\".\n\nYup, knowing about the first commit that conflicts is sufficient for our\nuse case.\n\n> > follow-up patch series that extends `struct replay_result` to return\n> > more information about the actual conflict. I'm already planning ahead a\n> > bit in this patch series.\n> \n> Wait, above you said you wanted to know the \"commits [which] have\n> conflicted\", here you seem to be suggesting you want to know about\n> \"the actual conflict\" which might mean you only care about the first\n> commit that conflicted but you want details about what conflicted\n> within it.  Or is it perhaps the set of files that would have had\n> conflicts across replaying the whole sequence of patches (in which\n> case a bunch of the previous questions are still valid)?\n\nI was simply being inaccurate there, we only care about the first\nconflicting commit, no plural.\n\nThanks!\n\nPatrick\n"},{"id":"533716","messageId":"20260113-b4-pks-history-builtin-v11-0-e74ebfa2652d@pks.im","threadId":"63986","inReplyTo":"20250819-b4-pks-history-builtin-v1-0-9b77c32688fe@pks.im","subject":"[PATCH v11 0/8] Introduce git-history(1) command for easy history editing","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-01-13T09:54:31Z","receivedAt":"2026-01-13T09:54:40Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Hi,\n\nover recent months I've been playing around with Jujutsu quite\nfrequently. While I still prefer using Git, there's been a couple\nfeatures in it that I really like and that I'd like to have in Git, as\nwell.\n\nA copule of these features relate to history editing. Most importantly,\nI really dig the following commands:\n\n  - jj-abandon(1) to drop a specific commit from your history.\n\n  - jj-absorb(1) to take some changes and automatically apply them to\n    commits in your history that last modified the respective hunks.\n\n  - jj-split(1) to split a commit into two.\n\n  - jj-new(1) to insert a new commit after or before a specific other\n    commit.\n\nNot all of these commands can be ported directly into Git. jj-new(1) for\nexample doesn't really make a ton of sense for us, I'd claim. But some\nof these commands _do_ make sense.\n\nThis patch series is a starting point for such a command. For now, it\nonly implements rewording of commits. I'll continue introducing new\ncommands once this patch series has been merged though.\n\nChanges in v11:\n  - Fix overly clever `BUG()` condition.\n  - Drop the `struct replay_result::merge_conflict` field.\n  - Return a positive value from `replay_revisions()` in case there was\n    a conflict.\n  - Improve a commit message.\n  - Fix check for whether `setup_revisions_from_strvec()` was\n    successful.\n  - Link to v10: https://lore.kernel.org/r/20260112-b4-pks-history-builtin-v10-0-e3c6aa5b4cec@pks.im\n\nChanges in v10:\n  - Split out changes that make `replay_revisions()` handle empty\n    revision ranges properly.\n  - Drop redundant `struct repository` parameter in\n    `replay_revisions()`.\n  - Move around `die_for_incompatible_opt2()`.\n  - Fix up some of the headers in \"replay.c\".\n  - Split out the changes that make `replay_revisions()` handle empty\n    revision ranges properly.\n  - Create a separate commit that fixes updating detached HEAD.\n  - Strip \"encoding\" header as we're reencoding commit messages.\n  - Merge the logic to update refs in in `REF_ACTION_HEAD` and\n    `REF_ACTION_BRANCHES` modes.\n  - Link to v9: https://lore.kernel.org/r/20260109-b4-pks-history-builtin-v9-0-8766101814c6@pks.im\n\nChanges in v9:\n  - Rename `struct replay_ref_updates` to `struct replay_result` to make\n    its semantics less focussed on ref updates, only.\n  - Clarify and fix return codes of git-replay(1) so that we return 1 on\n    conflict, 128 on an error and 0 on success.\n  - The usual small improvements to commit messages.\n  - Link to v8: https://lore.kernel.org/r/20260107-b4-pks-history-builtin-v8-0-18e9779e3a26@pks.im\n\nChanges in v8:\n  - The series is now built on top of e0bfec3dfc (The 15th batch,\n    2026-01-06) with kh/replay-invalid-onto-advance at 56b77a687e\n    (t3650: add more regression tests for failure conditions,\n    2026-01-05) merged into it. The latter is expected to hit 'next'\n    soonish.\n  - Another biggish revamp to build almost completely on top of the\n    git-replay(1) logic.\n  - We now advance all branches descending from the rewritten commit by\n    default.\n  - I've dropped the patches introducing `git history split` and will\n    send this as a follow-up patch series once this once has been\n    merged. This was done to focus attention on the underlying mechanics\n    as much as possible (and to keep my own sanity with the frequent\n    revamps).\n  - Link to v7: https://lore.kernel.org/r/20251203-b4-pks-history-builtin-v7-0-9e9f849bfd0e@pks.im\n\nChanges in v7:\n  - I've rebased the patch series on top of f0ef5b6d9b (The fifth batch,\n    2025-11-30) with pw/replay-exclude-gpgsig-fix at 9f3a115087 (replay:\n    do not copy \"gpgsign-sha256\" header, 2025-11-26) merged into it to\n    fix some conflicts.\n  - I refactored a bunch of code to be shared between split and reword.\n  - I am now using \"--ancestry-path\" and \"--reverse\" to enumerate\n    commits.\n  - `git history split` now asks for commit messages for both commits.\n  - `git history split` no longer allows the user to edit hunks so that\n    conflicts become impossible. This may be reintroduced at a later\n    point in time.\n  - A bunch of test improvements.\n  - Several commit message and documentation improvements.\n  - Link to v6: https://lore.kernel.org/r/20251027-b4-pks-history-builtin-v6-0-407dd3f57ad3@pks.im\n\nChanges in v6:\n  - I've rebased the patch series again to pull in the latest updates\n    from sa/replay-atomic-ref-updates and fix conflicts. It is now based\n    on 4e98b730f1 (The twenty-fourth batch, 2025-10-24) with ab661bb1bb\n    (replay: add replay.refAction config option, 2025-10-23) merged into\n    it.\n  - I've dropped the \"-m\" options for now, so commit messages are always\n    asked for via the editor. These can be introduced in a subsequent\n    patch series once discussion around them has settled.\n  - We don't use the merge machinery anymore to pick the commits.\n  - Drop the commit to parse commits in the replay machinery. It didn't\n    seem to be necessary in v5 anymore, and now that we don't use the\n    merge machinery at all we don't ever take that code path in the\n    first place.\n  - Link to v5:\n    https://lore.kernel.org/r/20251021-b4-pks-history-builtin-v5-0-78d23f578fe6@pks.im\n\nChanges in v5:\n  - I've changed the patch series to be based on top of 133d151831 (The\n    twenty-first batch, 2025-10-20) with sa/replay-atomic-ref-updates at\n    a1c22e627e (SQAUASH??? t0450 band-aid, 2025-10-14) merged into it.\n    This is one the one hand to fix a conflict, but also to get some of\n    the CI updates to make GitLab CI work again.\n  - Some slight commit message improvements.\n  - Deduplicate subcommand usage strings by using defines.\n  - Fix the desendancy checks to properly verify that HEAD is a\n    descendant of the commit to be rewritten. Also add some tests for\n    this.\n  - Fix the hint that mentions that lines starting with the comment\n    character will be tripped after having written the commit message.\n  - Move an include to the correct commit.\n  - Link to v4: https://lore.kernel.org/r/20251001-b4-pks-history-builtin-v4-0-8e61ddb86317@pks.im\n\nChanges in v4:\n  - I've rebuilt the patch series. It is now based on 821f583da6 (The\n    thirteenth batcn, 2025-09-29) with sa/replay-atomic-ref-updates\n    at 665c66a743 (replay: make atomic ref updates the default behavior,\n    2025-09-27) merged into it. This should fix all conflicts with seen.\n  - I've reworked this patch series to use the same infra as\n    git-replay(1), as discussed during the Contributor's Summit.\n  - I've slimmed down the patch series to only tackle those commands\n    that cannot result in a conflict to keep it simple. I also learned\n    that Elijah has been working on a \"git replay edit\" command, so I\n    dropped that command so that we can instead use his version.\n  - During the Contributor's Summit we have agreed that for now, we\n    won't care about hook execution just yet. This may be backfilled at\n    a later point in time.\n  - I dropped \"commit.verbose\" handling for now, as my understanding of\n    it was wrong at first. This is something we should backfill.\n  - Link to v3: https://lore.kernel.org/r/20250904-b4-pks-history-builtin-v3-0-509053514755@pks.im\n\nChanges in v3:\n  - Add logic to drive the \"post-rewrite\" hook and add tests to verify\n    that all hooks are executed as expected.\n  - Deduplicate logic to turn a replay action into a todo command.\n  - Move the addition of tests for the top-level git-history(1) command\n    to the correct commit.\n  - Some smaller commit message fixes.\n  - Honor \"commit.verbose\".\n  - Fix copy-paste error with an error message.\n  - Link to v2: https://lore.kernel.org/r/20250824-b4-pks-history-builtin-v2-0-964ac12f65bd@pks.im\n\nChanges in v2:\n  - Add a new \"reword\" subcommand.\n  - List git-history(1) in \"command-list.txt\".\n  - Add some missing error handling.\n  - Simplify calling convention of `apply_commits()` to handle root\n    commits internally instead of requiring every caller to do so.\n  - Add tests to verify that git-history(1) refuses to work with changes\n    in the worktree or index.\n  - Mark git-history(1) as experimental.\n  - Introduce commands to manage interrupted history edits.\n  - A bunch of improvements to the manpage.\n  - Link to v1: https://lore.kernel.org/r/20250819-b4-pks-history-builtin-v1-0-9b77c32688fe@pks.im\n\nThanks!\n\nPatrick\n\n---\nPatrick Steinhardt (8):\n      builtin/replay: extract core logic to replay revisions\n      builtin/replay: move core logic into \"libgit.a\"\n      replay: small set of cleanups\n      replay: support empty commit ranges\n      replay: support updating detached HEAD\n      wt-status: provide function to expose status for trees\n      builtin: add new \"history\" command\n      builtin/history: implement \"reword\" subcommand\n\n .gitignore                     |   1 +\n Documentation/git-history.adoc |  73 +++++++\n Documentation/meson.build      |   1 +\n Makefile                       |   2 +\n builtin.h                      |   1 +\n builtin/history.c              | 427 +++++++++++++++++++++++++++++++++++++++++\n builtin/replay.c               | 373 +++--------------------------------\n command-list.txt               |   1 +\n git.c                          |   1 +\n meson.build                    |   2 +\n replay.c                       | 371 +++++++++++++++++++++++++++++++++++\n replay.h                       |  61 ++++++\n t/meson.build                  |   2 +\n t/t3450-history.sh             |  17 ++\n t/t3451-history-reword.sh      | 391 +++++++++++++++++++++++++++++++++++++\n t/t3650-replay-basics.sh       |   9 +\n wt-status.c                    |  24 +++\n wt-status.h                    |   9 +\n 18 files changed, 1425 insertions(+), 341 deletions(-)\n\nRange-diff versus v10:\n\n1:  f02aa5abd1 ! 1:  bdefcad12d builtin/replay: extract core logic to replay revisions\n    @@ builtin/replay.c: static void set_up_replay_mode(struct repository *repo,\n      \n     -\tdie_for_incompatible_opt2(!!onto_name, \"--onto\",\n     -\t\t\t\t  !!*advance_name, \"--advance\");\n    -+\tif (!(!!onto_name ^ !!*advance_name))\n    -+\t\tBUG(\"expected either onto_name or *advance_name in this function\");\n    ++\tif (!onto_name == !*advance_name)\n    ++\t\tBUG(\"one and only one of onto_name and *advance_name must be given\");\n     +\n      \tif (onto_name) {\n      \t\t*onto = peel_committish(repo, onto_name, \"--onto\");\n    @@ builtin/replay.c: static struct commit *pick_regular_commit(struct repository *r\n     +\t\tstruct object_id new_oid;\n     +\t} *updates;\n     +\tsize_t updates_nr, updates_alloc;\n    -+\n    -+\tbool merge_conflict;\n     +};\n     +\n     +static void replay_result_release(struct replay_result *result)\n    @@ builtin/replay.c: static struct commit *pick_regular_commit(struct repository *r\n     +\t}\n     +\n     +\tif (!result.clean) {\n    -+\t\tout->merge_conflict = true;\n    -+\t\tret = -1;\n    ++\t\tret = 1;\n     +\t\tgoto out;\n     +\t}\n     +\n    @@ builtin/replay.c: int cmd_replay(int argc,\n     -\t\tfree(update_refs);\n     -\t}\n     -\tret = result.clean;\n    --\n    ++\tret = 0;\n    + \n      cleanup:\n      \tif (transaction)\n      \t\tref_transaction_free(transaction);\n    @@ builtin/replay.c: int cmd_replay(int argc,\n      \trelease_revisions(&revs);\n     -\tfree(advance_name);\n      \n    --\t/* Return */\n    --\tif (ret < 0)\n    --\t\texit(128);\n    + \t/* Return */\n    + \tif (ret < 0)\n    + \t\texit(128);\n     -\treturn ret ? 0 : 1;\n    -+\tif (ret) {\n    -+\t\tif (result.merge_conflict)\n    -+\t\t\treturn 1;\n    -+\t\treturn 128;\n    -+\t}\n    -+\n    -+\treturn 0;\n    ++\treturn ret;\n      }\n2:  078d713391 ! 2:  80eaf3f9fd builtin/replay: move core logic into \"libgit.a\"\n    @@ builtin/replay.c\n     -\tif (!rinfo.positive_refexprs)\n     -\t\tdie(_(\"need some commits to replay\"));\n     -\n    --\tif (!(!!onto_name ^ !!*advance_name))\n    --\t\tBUG(\"expected either onto_name or *advance_name in this function\");\n    +-\tif (!onto_name == !*advance_name)\n    +-\t\tBUG(\"one and only one of onto_name and *advance_name must be given\");\n     -\n     -\tif (onto_name) {\n     -\t\t*onto = peel_committish(repo, onto_name, \"--onto\");\n    @@ builtin/replay.c\n     -\t\tstruct object_id new_oid;\n     -\t} *updates;\n     -\tsize_t updates_nr, updates_alloc;\n    --\n    --\tbool merge_conflict;\n     -};\n     -\n     -static void replay_result_release(struct replay_result *result)\n    @@ builtin/replay.c\n     -\t}\n     -\n     -\tif (!result.clean) {\n    --\t\tout->merge_conflict = true;\n    --\t\tret = -1;\n    +-\t\tret = 1;\n     -\t\tgoto out;\n     -\t}\n     -\n    @@ replay.c (new)\n     +\tif (!rinfo.positive_refexprs)\n     +\t\tdie(_(\"need some commits to replay\"));\n     +\n    -+\tif (!(!!onto_name ^ !!*advance_name))\n    -+\t\tBUG(\"expected either onto_name or *advance_name in this function\");\n    ++\tif (!onto_name == !*advance_name)\n    ++\t\tBUG(\"one and only one of onto_name and *advance_name must be given\");\n     +\n     +\tif (onto_name) {\n     +\t\t*onto = peel_committish(repo, onto_name, \"--onto\");\n    @@ replay.c (new)\n     +\t}\n     +\n     +\tif (!result.clean) {\n    -+\t\tout->merge_conflict = true;\n    -+\t\tret = -1;\n    ++\t\tret = 1;\n     +\t\tgoto out;\n     +\t}\n     +\n    @@ replay.h (new)\n     +\t\tstruct object_id new_oid;\n     +\t} *updates;\n     +\tsize_t updates_nr, updates_alloc;\n    -+\n    -+\t/* Set to true in case the replay failed with a merge conflict. */\n    -+\tbool merge_conflict;\n     +};\n     +\n     +void replay_result_release(struct replay_result *result);\n    @@ replay.h (new)\n     + * index and references untouched. Reference updates caused by the replay will\n     + * be recorded in the `updates` out pointer.\n     + *\n    -+ * Returns 0 on success, a negative error code otherwise.\n    ++ * Returns 0 on success, 1 on conflict and a negative error code otherwise.\n     + */\n     +int replay_revisions(struct rev_info *revs,\n     +\t\t     struct replay_revisions_options *opts,\n3:  44f96f6b93 = 3:  bd6783570b replay: small set of cleanups\n4:  0c861b1a37 ! 4:  e927f66224 replay: support empty commit ranges\n    @@ Commit message\n     \n         In a subsequent commit we're about to introduce a new user of the replay\n         subsystem. With that new user, the range of commits that we'll want to\n    -    replay will be identified implicitly via \"HEAD\". With such implicit\n    -    ranges it becomes likely that the range of revisions that we're asked to\n    -    replay becomes empty. This case does not make sense with git-replay(1),\n    -    but with the new command it will.\n    +    replay will be identified implicitly via a single commit, and will\n    +    include all descendants of that commit to any branch. If that commit has\n    +    no descendants (because it's the tip of some branch), then the range of\n    +    revisions that we're asked to replay becomes empty. This case does not\n    +    make sense with git-replay(1), but with the new command it will.\n     \n         This case is not currently supported by `replay_revisions()` though\n         because we zero-initialize `struct merge_result`. This includes its\n5:  03a64727ab = 5:  c8ed32bfae replay: support updating detached HEAD\n6:  453a5769a6 = 6:  79f8bf7613 wt-status: provide function to expose status for trees\n7:  f73ad9a468 = 7:  d0d56cc4fc builtin: add new \"history\" command\n8:  e7af330353 ! 8:  464f5467ab builtin/history: implement \"reword\" subcommand\n    @@ builtin/history.c\n     +\t}\n     +\n     +\tsetup_revisions_from_strvec(&args, &revs, NULL);\n    -+\tif (revs.nr)\n    -+\t\tBUG(\"revisions were set up with invalid argument '%s'\", args.v[0]);\n    ++\tif (args.nr != 1)\n    ++\t\tBUG(\"revisions were set up with invalid argument\");\n     +\n     +\topts.onto = oid_to_hex_r(hex, &rewritten->object.oid);\n     +\n\n---\nbase-commit: 02bdc12118c43e0a65258eb742492965859bffb6\nchange-id: 20250819-b4-pks-history-builtin-83398f9a05f0\n\n"},{"id":"533717","messageId":"20260113-b4-pks-history-builtin-v11-1-e74ebfa2652d@pks.im","threadId":"63986","inReplyTo":"20260113-b4-pks-history-builtin-v11-0-e74ebfa2652d@pks.im","subject":"[PATCH v11 1/8] builtin/replay: extract core logic to replay revisions","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-01-13T09:54:32Z","receivedAt":"2026-01-13T09:54:42Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"We're about to move the core logic used to replay revisions onto a new\nbase into the \"libgit.a\" library. Prepare for this by pulling out the\nlogic into a new function `replay_revisions()` that:\n\n  1. Takes a set of revisions to replay and some options that tell it how\n     it ought to replay the revisions.\n\n  2. Replays the commits.\n\n  3. Records any reference updates that would be caused by replaying the\n     commits in a structure that is owned by the caller.\n\nThe logic itself will be moved into a separate file in the next commit.\nThis change is not expected to cause user-visible change in behaviour.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/replay.c | 269 +++++++++++++++++++++++++++++++++----------------------\n 1 file changed, 162 insertions(+), 107 deletions(-)\n\ndiff --git a/builtin/replay.c b/builtin/replay.c\nindex 1960bbbee8..daf907668c 100644\n--- a/builtin/replay.c\n+++ b/builtin/replay.c\n@@ -177,8 +177,9 @@ static void set_up_replay_mode(struct repository *repo,\n \tif (!rinfo.positive_refexprs)\n \t\tdie(_(\"need some commits to replay\"));\n \n-\tdie_for_incompatible_opt2(!!onto_name, \"--onto\",\n-\t\t\t\t  !!*advance_name, \"--advance\");\n+\tif (!onto_name == !*advance_name)\n+\t\tBUG(\"one and only one of onto_name and *advance_name must be given\");\n+\n \tif (onto_name) {\n \t\t*onto = peel_committish(repo, onto_name, \"--onto\");\n \t\tif (rinfo.positive_refexprs <\n@@ -253,6 +254,134 @@ static struct commit *pick_regular_commit(struct repository *repo,\n \treturn create_commit(repo, result->tree, pickme, replayed_base);\n }\n \n+struct replay_revisions_options {\n+\tconst char *advance;\n+\tconst char *onto;\n+\tint contained;\n+};\n+\n+struct replay_result {\n+\tstruct replay_ref_update {\n+\t\tchar *refname;\n+\t\tstruct object_id old_oid;\n+\t\tstruct object_id new_oid;\n+\t} *updates;\n+\tsize_t updates_nr, updates_alloc;\n+};\n+\n+static void replay_result_release(struct replay_result *result)\n+{\n+\tfor (size_t i = 0; i < result->updates_nr; i++)\n+\t\tfree(result->updates[i].refname);\n+\tfree(result->updates);\n+}\n+\n+static void replay_result_queue_update(struct replay_result *result,\n+\t\t\t\t       const char *refname,\n+\t\t\t\t       const struct object_id *old_oid,\n+\t\t\t\t       const struct object_id *new_oid)\n+{\n+\tALLOC_GROW(result->updates, result->updates_nr + 1, result->updates_alloc);\n+\tresult->updates[result->updates_nr].refname = xstrdup(refname);\n+\tresult->updates[result->updates_nr].old_oid = *old_oid;\n+\tresult->updates[result->updates_nr].new_oid = *new_oid;\n+\tresult->updates_nr++;\n+}\n+\n+static int replay_revisions(struct rev_info *revs,\n+\t\t\t    struct replay_revisions_options *opts,\n+\t\t\t    struct replay_result *out)\n+{\n+\tkh_oid_map_t *replayed_commits = NULL;\n+\tstruct strset *update_refs = NULL;\n+\tstruct commit *last_commit = NULL;\n+\tstruct commit *commit;\n+\tstruct commit *onto = NULL;\n+\tstruct merge_options merge_opt;\n+\tstruct merge_result result;\n+\tchar *advance;\n+\tint ret;\n+\n+\tadvance = xstrdup_or_null(opts->advance);\n+\tset_up_replay_mode(revs->repo, &revs->cmdline, opts->onto, &advance,\n+\t\t\t   &onto, &update_refs);\n+\n+\t/* FIXME: Should allow replaying commits with the first as a root commit */\n+\n+\tif (prepare_revision_walk(revs) < 0) {\n+\t\tret = error(_(\"error preparing revisions\"));\n+\t\tgoto out;\n+\t}\n+\n+\tinit_basic_merge_options(&merge_opt, revs->repo);\n+\tmemset(&result, 0, sizeof(result));\n+\tmerge_opt.show_rename_progress = 0;\n+\tlast_commit = onto;\n+\treplayed_commits = kh_init_oid_map();\n+\twhile ((commit = get_revision(revs))) {\n+\t\tconst struct name_decoration *decoration;\n+\t\tkhint_t pos;\n+\t\tint hr;\n+\n+\t\tif (!commit->parents)\n+\t\t\tdie(_(\"replaying down from root commit is not supported yet!\"));\n+\t\tif (commit->parents->next)\n+\t\t\tdie(_(\"replaying merge commits is not supported yet!\"));\n+\n+\t\tlast_commit = pick_regular_commit(revs->repo, commit, replayed_commits,\n+\t\t\t\t\t\t  onto, &merge_opt, &result);\n+\t\tif (!last_commit)\n+\t\t\tbreak;\n+\n+\t\t/* Record commit -> last_commit mapping */\n+\t\tpos = kh_put_oid_map(replayed_commits, commit->object.oid, &hr);\n+\t\tif (hr == 0)\n+\t\t\tBUG(\"Duplicate rewritten commit: %s\\n\",\n+\t\t\t    oid_to_hex(&commit->object.oid));\n+\t\tkh_value(replayed_commits, pos) = last_commit;\n+\n+\t\t/* Update any necessary branches */\n+\t\tif (advance)\n+\t\t\tcontinue;\n+\t\tdecoration = get_name_decoration(&commit->object);\n+\t\tif (!decoration)\n+\t\t\tcontinue;\n+\t\twhile (decoration) {\n+\t\t\tif (decoration->type == DECORATION_REF_LOCAL &&\n+\t\t\t    (opts->contained || strset_contains(update_refs,\n+\t\t\t\t\t\t\t\tdecoration->name))) {\n+\t\t\t\treplay_result_queue_update(out, decoration->name,\n+\t\t\t\t\t\t\t   &commit->object.oid,\n+\t\t\t\t\t\t\t   &last_commit->object.oid);\n+\t\t\t}\n+\t\t\tdecoration = decoration->next;\n+\t\t}\n+\t}\n+\n+\tif (!result.clean) {\n+\t\tret = 1;\n+\t\tgoto out;\n+\t}\n+\n+\t/* In --advance mode, advance the target ref */\n+\tif (advance)\n+\t\treplay_result_queue_update(out, advance,\n+\t\t\t\t\t   &onto->object.oid,\n+\t\t\t\t\t   &last_commit->object.oid);\n+\n+\tret = 0;\n+\n+out:\n+\tif (update_refs) {\n+\t\tstrset_clear(update_refs);\n+\t\tfree(update_refs);\n+\t}\n+\tkh_destroy_oid_map(replayed_commits);\n+\tmerge_finalize(&merge_opt, &result);\n+\tfree(advance);\n+\treturn ret;\n+}\n+\n static enum ref_action_mode parse_ref_action_mode(const char *ref_action, const char *source)\n {\n \tif (!ref_action || !strcmp(ref_action, \"update\"))\n@@ -306,21 +435,11 @@ int cmd_replay(int argc,\n \t       const char *prefix,\n \t       struct repository *repo)\n {\n-\tconst char *advance_name_opt = NULL;\n-\tchar *advance_name = NULL;\n-\tstruct commit *onto = NULL;\n-\tconst char *onto_name = NULL;\n-\tint contained = 0;\n+\tstruct replay_revisions_options opts = { 0 };\n+\tstruct replay_result result = { 0 };\n \tconst char *ref_action = NULL;\n \tenum ref_action_mode ref_mode;\n-\n \tstruct rev_info revs;\n-\tstruct commit *last_commit = NULL;\n-\tstruct commit *commit;\n-\tstruct merge_options merge_opt;\n-\tstruct merge_result result;\n-\tstruct strset *update_refs = NULL;\n-\tkh_oid_map_t *replayed_commits;\n \tstruct ref_transaction *transaction = NULL;\n \tstruct strbuf transaction_err = STRBUF_INIT;\n \tstruct strbuf reflog_msg = STRBUF_INIT;\n@@ -333,13 +452,13 @@ int cmd_replay(int argc,\n \t\tNULL\n \t};\n \tstruct option replay_options[] = {\n-\t\tOPT_STRING(0, \"advance\", &advance_name_opt,\n+\t\tOPT_STRING(0, \"advance\", &opts.advance,\n \t\t\t   N_(\"branch\"),\n \t\t\t   N_(\"make replay advance given branch\")),\n-\t\tOPT_STRING(0, \"onto\", &onto_name,\n+\t\tOPT_STRING(0, \"onto\", &opts.onto,\n \t\t\t   N_(\"revision\"),\n \t\t\t   N_(\"replay onto given commit\")),\n-\t\tOPT_BOOL(0, \"contained\", &contained,\n+\t\tOPT_BOOL(0, \"contained\", &opts.contained,\n \t\t\t N_(\"update all branches that point at commits in <revision-range>\")),\n \t\tOPT_STRING(0, \"ref-action\", &ref_action,\n \t\t\t   N_(\"mode\"),\n@@ -350,19 +469,19 @@ int cmd_replay(int argc,\n \targc = parse_options(argc, argv, prefix, replay_options, replay_usage,\n \t\t\t     PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT);\n \n-\tif (!onto_name && !advance_name_opt) {\n+\tif (!opts.onto && !opts.advance) {\n \t\terror(_(\"option --onto or --advance is mandatory\"));\n \t\tusage_with_options(replay_usage, replay_options);\n \t}\n \n-\tdie_for_incompatible_opt2(!!advance_name_opt, \"--advance\",\n-\t\t\t\t  contained, \"--contained\");\n+\tdie_for_incompatible_opt2(!!opts.advance, \"--advance\",\n+\t\t\t\t  opts.contained, \"--contained\");\n+\tdie_for_incompatible_opt2(!!opts.advance, \"--advance\",\n+\t\t\t\t  !!opts.onto, \"--onto\");\n \n \t/* Parse ref action mode from command line or config */\n \tref_mode = get_ref_action_mode(repo, ref_action);\n \n-\tadvance_name = xstrdup_or_null(advance_name_opt);\n-\n \trepo_init_revisions(repo, &revs, prefix);\n \n \t/*\n@@ -414,18 +533,19 @@ int cmd_replay(int argc,\n \t\trevs.simplify_history = 0;\n \t}\n \n-\tset_up_replay_mode(repo, &revs.cmdline,\n-\t\t\t   onto_name, &advance_name,\n-\t\t\t   &onto, &update_refs);\n-\n-\t/* FIXME: Should allow replaying commits with the first as a root commit */\n+\tret = replay_revisions(&revs, &opts, &result);\n+\tif (ret)\n+\t\tgoto cleanup;\n \n \t/* Build reflog message */\n-\tif (advance_name_opt)\n-\t\tstrbuf_addf(&reflog_msg, \"replay --advance %s\", advance_name_opt);\n-\telse\n-\t\tstrbuf_addf(&reflog_msg, \"replay --onto %s\",\n-\t\t\t    oid_to_hex(&onto->object.oid));\n+\tif (opts.advance) {\n+\t\tstrbuf_addf(&reflog_msg, \"replay --advance %s\", opts.advance);\n+\t} else {\n+\t\tstruct object_id oid;\n+\t\tif (repo_get_oid_committish(repo, opts.onto, &oid))\n+\t\t\tBUG(\"--onto commit should have been resolved beforehand already\");\n+\t\tstrbuf_addf(&reflog_msg, \"replay --onto %s\", oid_to_hex(&oid));\n+\t}\n \n \t/* Initialize ref transaction if using update mode */\n \tif (ref_mode == REF_ACTION_UPDATE) {\n@@ -438,78 +558,19 @@ int cmd_replay(int argc,\n \t\t}\n \t}\n \n-\tif (prepare_revision_walk(&revs) < 0) {\n-\t\tret = error(_(\"error preparing revisions\"));\n-\t\tgoto cleanup;\n-\t}\n-\n-\tinit_basic_merge_options(&merge_opt, repo);\n-\tmemset(&result, 0, sizeof(result));\n-\tmerge_opt.show_rename_progress = 0;\n-\tlast_commit = onto;\n-\treplayed_commits = kh_init_oid_map();\n-\twhile ((commit = get_revision(&revs))) {\n-\t\tconst struct name_decoration *decoration;\n-\t\tkhint_t pos;\n-\t\tint hr;\n-\n-\t\tif (!commit->parents)\n-\t\t\tdie(_(\"replaying down from root commit is not supported yet!\"));\n-\t\tif (commit->parents->next)\n-\t\t\tdie(_(\"replaying merge commits is not supported yet!\"));\n-\n-\t\tlast_commit = pick_regular_commit(repo, commit, replayed_commits,\n-\t\t\t\t\t\t  onto, &merge_opt, &result);\n-\t\tif (!last_commit)\n-\t\t\tbreak;\n-\n-\t\t/* Record commit -> last_commit mapping */\n-\t\tpos = kh_put_oid_map(replayed_commits, commit->object.oid, &hr);\n-\t\tif (hr == 0)\n-\t\t\tBUG(\"Duplicate rewritten commit: %s\\n\",\n-\t\t\t    oid_to_hex(&commit->object.oid));\n-\t\tkh_value(replayed_commits, pos) = last_commit;\n-\n-\t\t/* Update any necessary branches */\n-\t\tif (advance_name)\n-\t\t\tcontinue;\n-\t\tdecoration = get_name_decoration(&commit->object);\n-\t\tif (!decoration)\n-\t\t\tcontinue;\n-\t\twhile (decoration) {\n-\t\t\tif (decoration->type == DECORATION_REF_LOCAL &&\n-\t\t\t    (contained || strset_contains(update_refs,\n-\t\t\t\t\t\t\t  decoration->name))) {\n-\t\t\t\tif (handle_ref_update(ref_mode, transaction,\n-\t\t\t\t\t\t      decoration->name,\n-\t\t\t\t\t\t      &last_commit->object.oid,\n-\t\t\t\t\t\t      &commit->object.oid,\n-\t\t\t\t\t\t      reflog_msg.buf,\n-\t\t\t\t\t\t      &transaction_err) < 0) {\n-\t\t\t\t\tret = error(_(\"failed to update ref '%s': %s\"),\n-\t\t\t\t\t\t    decoration->name, transaction_err.buf);\n-\t\t\t\t\tgoto cleanup;\n-\t\t\t\t}\n-\t\t\t}\n-\t\t\tdecoration = decoration->next;\n-\t\t}\n-\t}\n-\n-\t/* In --advance mode, advance the target ref */\n-\tif (result.clean == 1 && advance_name) {\n-\t\tif (handle_ref_update(ref_mode, transaction, advance_name,\n-\t\t\t\t      &last_commit->object.oid,\n-\t\t\t\t      &onto->object.oid,\n-\t\t\t\t      reflog_msg.buf,\n-\t\t\t\t      &transaction_err) < 0) {\n+\tfor (size_t i = 0; i < result.updates_nr; i++) {\n+\t\tret = handle_ref_update(ref_mode, transaction, result.updates[i].refname,\n+\t\t\t\t\t&result.updates[i].new_oid, &result.updates[i].old_oid,\n+\t\t\t\t\treflog_msg.buf, &transaction_err);\n+\t\tif (ret) {\n \t\t\tret = error(_(\"failed to update ref '%s': %s\"),\n-\t\t\t\t    advance_name, transaction_err.buf);\n+\t\t\t\t    result.updates[i].refname, transaction_err.buf);\n \t\t\tgoto cleanup;\n \t\t}\n \t}\n \n \t/* Commit the ref transaction if we have one */\n-\tif (transaction && result.clean == 1) {\n+\tif (transaction) {\n \t\tif (ref_transaction_commit(transaction, &transaction_err)) {\n \t\t\tret = error(_(\"failed to commit ref transaction: %s\"),\n \t\t\t\t    transaction_err.buf);\n@@ -517,24 +578,18 @@ int cmd_replay(int argc,\n \t\t}\n \t}\n \n-\tmerge_finalize(&merge_opt, &result);\n-\tkh_destroy_oid_map(replayed_commits);\n-\tif (update_refs) {\n-\t\tstrset_clear(update_refs);\n-\t\tfree(update_refs);\n-\t}\n-\tret = result.clean;\n+\tret = 0;\n \n cleanup:\n \tif (transaction)\n \t\tref_transaction_free(transaction);\n+\treplay_result_release(&result);\n \tstrbuf_release(&transaction_err);\n \tstrbuf_release(&reflog_msg);\n \trelease_revisions(&revs);\n-\tfree(advance_name);\n \n \t/* Return */\n \tif (ret < 0)\n \t\texit(128);\n-\treturn ret ? 0 : 1;\n+\treturn ret;\n }\n\n-- \n2.52.0.590.g1f87b77810.dirty\n\n"},{"id":"533718","messageId":"20260113-b4-pks-history-builtin-v11-2-e74ebfa2652d@pks.im","threadId":"63986","inReplyTo":"20260113-b4-pks-history-builtin-v11-0-e74ebfa2652d@pks.im","subject":"[PATCH v11 2/8] builtin/replay: move core logic into \"libgit.a\"","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-01-13T09:54:33Z","receivedAt":"2026-01-13T09:54:44Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Move the core logic used to replay commits into \"libgit.a\" so that it\ncan be easily reused by other commands. It will be used in a subsequent\ncommit where we're about to introduce a new git-history(1) command.\n\nNote that with this change we have no sign-comparison warnings anymore,\nand neither do we depend on `the_repository`.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Makefile         |   1 +\n builtin/replay.c | 366 +------------------------------------------------------\n meson.build      |   1 +\n replay.c         | 354 +++++++++++++++++++++++++++++++++++++++++++++++++++++\n replay.h         |  61 ++++++++++\n 5 files changed, 418 insertions(+), 365 deletions(-)\n\ndiff --git a/Makefile b/Makefile\nindex b7eba509c6..1c64a5d2ae 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -1285,6 +1285,7 @@ LIB_OBJS += repack-geometry.o\n LIB_OBJS += repack-midx.o\n LIB_OBJS += repack-promisor.o\n LIB_OBJS += replace-object.o\n+LIB_OBJS += replay.o\n LIB_OBJS += repo-settings.o\n LIB_OBJS += repository.o\n LIB_OBJS += rerere.o\ndiff --git a/builtin/replay.c b/builtin/replay.c\nindex daf907668c..2cdde830a8 100644\n--- a/builtin/replay.c\n+++ b/builtin/replay.c\n@@ -2,386 +2,22 @@\n  * \"git replay\" builtin command\n  */\n \n-#define USE_THE_REPOSITORY_VARIABLE\n-#define DISABLE_SIGN_COMPARE_WARNINGS\n-\n #include \"git-compat-util.h\"\n \n #include \"builtin.h\"\n #include \"config.h\"\n-#include \"environment.h\"\n #include \"hex.h\"\n-#include \"lockfile.h\"\n-#include \"merge-ort.h\"\n #include \"object-name.h\"\n #include \"parse-options.h\"\n #include \"refs.h\"\n+#include \"replay.h\"\n #include \"revision.h\"\n-#include \"strmap.h\"\n-#include <oidset.h>\n-#include <tree.h>\n \n enum ref_action_mode {\n \tREF_ACTION_UPDATE,\n \tREF_ACTION_PRINT,\n };\n \n-static const char *short_commit_name(struct repository *repo,\n-\t\t\t\t     struct commit *commit)\n-{\n-\treturn repo_find_unique_abbrev(repo, &commit->object.oid,\n-\t\t\t\t       DEFAULT_ABBREV);\n-}\n-\n-static struct commit *peel_committish(struct repository *repo,\n-\t\t\t\t      const char *name,\n-\t\t\t\t      const char *mode)\n-{\n-\tstruct object *obj;\n-\tstruct object_id oid;\n-\n-\tif (repo_get_oid(repo, name, &oid))\n-\t\tdie(_(\"'%s' is not a valid commit-ish for %s\"), name, mode);\n-\tobj = parse_object_or_die(repo, &oid, name);\n-\treturn (struct commit *)repo_peel_to_type(repo, name, 0, obj,\n-\t\t\t\t\t\t  OBJ_COMMIT);\n-}\n-\n-static char *get_author(const char *message)\n-{\n-\tsize_t len;\n-\tconst char *a;\n-\n-\ta = find_commit_header(message, \"author\", &len);\n-\tif (a)\n-\t\treturn xmemdupz(a, len);\n-\n-\treturn NULL;\n-}\n-\n-static struct commit *create_commit(struct repository *repo,\n-\t\t\t\t    struct tree *tree,\n-\t\t\t\t    struct commit *based_on,\n-\t\t\t\t    struct commit *parent)\n-{\n-\tstruct object_id ret;\n-\tstruct object *obj = NULL;\n-\tstruct commit_list *parents = NULL;\n-\tchar *author;\n-\tchar *sign_commit = NULL; /* FIXME: cli users might want to sign again */\n-\tstruct commit_extra_header *extra = NULL;\n-\tstruct strbuf msg = STRBUF_INIT;\n-\tconst char *out_enc = get_commit_output_encoding();\n-\tconst char *message = repo_logmsg_reencode(repo, based_on,\n-\t\t\t\t\t\t   NULL, out_enc);\n-\tconst char *orig_message = NULL;\n-\tconst char *exclude_gpgsig[] = { \"gpgsig\", \"gpgsig-sha256\", NULL };\n-\n-\tcommit_list_insert(parent, &parents);\n-\textra = read_commit_extra_headers(based_on, exclude_gpgsig);\n-\tfind_commit_subject(message, &orig_message);\n-\tstrbuf_addstr(&msg, orig_message);\n-\tauthor = get_author(message);\n-\treset_ident_date();\n-\tif (commit_tree_extended(msg.buf, msg.len, &tree->object.oid, parents,\n-\t\t\t\t &ret, author, NULL, sign_commit, extra)) {\n-\t\terror(_(\"failed to write commit object\"));\n-\t\tgoto out;\n-\t}\n-\n-\tobj = parse_object(repo, &ret);\n-\n-out:\n-\trepo_unuse_commit_buffer(the_repository, based_on, message);\n-\tfree_commit_extra_headers(extra);\n-\tfree_commit_list(parents);\n-\tstrbuf_release(&msg);\n-\tfree(author);\n-\treturn (struct commit *)obj;\n-}\n-\n-struct ref_info {\n-\tstruct commit *onto;\n-\tstruct strset positive_refs;\n-\tstruct strset negative_refs;\n-\tint positive_refexprs;\n-\tint negative_refexprs;\n-};\n-\n-static void get_ref_information(struct repository *repo,\n-\t\t\t\tstruct rev_cmdline_info *cmd_info,\n-\t\t\t\tstruct ref_info *ref_info)\n-{\n-\tint i;\n-\n-\tref_info->onto = NULL;\n-\tstrset_init(&ref_info->positive_refs);\n-\tstrset_init(&ref_info->negative_refs);\n-\tref_info->positive_refexprs = 0;\n-\tref_info->negative_refexprs = 0;\n-\n-\t/*\n-\t * When the user specifies e.g.\n-\t *   git replay origin/main..mybranch\n-\t *   git replay ^origin/next mybranch1 mybranch2\n-\t * we want to be able to determine where to replay the commits.  In\n-\t * these examples, the branches are probably based on an old version\n-\t * of either origin/main or origin/next, so we want to replay on the\n-\t * newest version of that branch.  In contrast we would want to error\n-\t * out if they ran\n-\t *   git replay ^origin/master ^origin/next mybranch\n-\t *   git replay mybranch~2..mybranch\n-\t * the first of those because there's no unique base to choose, and\n-\t * the second because they'd likely just be replaying commits on top\n-\t * of the same commit and not making any difference.\n-\t */\n-\tfor (i = 0; i < cmd_info->nr; i++) {\n-\t\tstruct rev_cmdline_entry *e = cmd_info->rev + i;\n-\t\tstruct object_id oid;\n-\t\tconst char *refexpr = e->name;\n-\t\tchar *fullname = NULL;\n-\t\tint can_uniquely_dwim = 1;\n-\n-\t\tif (*refexpr == '^')\n-\t\t\trefexpr++;\n-\t\tif (repo_dwim_ref(repo, refexpr, strlen(refexpr), &oid, &fullname, 0) != 1)\n-\t\t\tcan_uniquely_dwim = 0;\n-\n-\t\tif (e->flags & BOTTOM) {\n-\t\t\tif (can_uniquely_dwim)\n-\t\t\t\tstrset_add(&ref_info->negative_refs, fullname);\n-\t\t\tif (!ref_info->negative_refexprs)\n-\t\t\t\tref_info->onto = lookup_commit_reference_gently(repo,\n-\t\t\t\t\t\t\t\t\t\t&e->item->oid, 1);\n-\t\t\tref_info->negative_refexprs++;\n-\t\t} else {\n-\t\t\tif (can_uniquely_dwim)\n-\t\t\t\tstrset_add(&ref_info->positive_refs, fullname);\n-\t\t\tref_info->positive_refexprs++;\n-\t\t}\n-\n-\t\tfree(fullname);\n-\t}\n-}\n-\n-static void set_up_replay_mode(struct repository *repo,\n-\t\t\t       struct rev_cmdline_info *cmd_info,\n-\t\t\t       const char *onto_name,\n-\t\t\t       char **advance_name,\n-\t\t\t       struct commit **onto,\n-\t\t\t       struct strset **update_refs)\n-{\n-\tstruct ref_info rinfo;\n-\n-\tget_ref_information(repo, cmd_info, &rinfo);\n-\tif (!rinfo.positive_refexprs)\n-\t\tdie(_(\"need some commits to replay\"));\n-\n-\tif (!onto_name == !*advance_name)\n-\t\tBUG(\"one and only one of onto_name and *advance_name must be given\");\n-\n-\tif (onto_name) {\n-\t\t*onto = peel_committish(repo, onto_name, \"--onto\");\n-\t\tif (rinfo.positive_refexprs <\n-\t\t    strset_get_size(&rinfo.positive_refs))\n-\t\t\tdie(_(\"all positive revisions given must be references\"));\n-\t\t*update_refs = xcalloc(1, sizeof(**update_refs));\n-\t\t**update_refs = rinfo.positive_refs;\n-\t\tmemset(&rinfo.positive_refs, 0, sizeof(**update_refs));\n-\t} else {\n-\t\tstruct object_id oid;\n-\t\tchar *fullname = NULL;\n-\n-\t\tif (!*advance_name)\n-\t\t\tBUG(\"expected either onto_name or *advance_name in this function\");\n-\n-\t\tif (repo_dwim_ref(repo, *advance_name, strlen(*advance_name),\n-\t\t\t     &oid, &fullname, 0) == 1) {\n-\t\t\tfree(*advance_name);\n-\t\t\t*advance_name = fullname;\n-\t\t} else {\n-\t\t\tdie(_(\"argument to --advance must be a reference\"));\n-\t\t}\n-\t\t*onto = peel_committish(repo, *advance_name, \"--advance\");\n-\t\tif (rinfo.positive_refexprs > 1)\n-\t\t\tdie(_(\"cannot advance target with multiple sources because ordering would be ill-defined\"));\n-\t}\n-\tstrset_clear(&rinfo.negative_refs);\n-\tstrset_clear(&rinfo.positive_refs);\n-}\n-\n-static struct commit *mapped_commit(kh_oid_map_t *replayed_commits,\n-\t\t\t\t    struct commit *commit,\n-\t\t\t\t    struct commit *fallback)\n-{\n-\tkhint_t pos = kh_get_oid_map(replayed_commits, commit->object.oid);\n-\tif (pos == kh_end(replayed_commits))\n-\t\treturn fallback;\n-\treturn kh_value(replayed_commits, pos);\n-}\n-\n-static struct commit *pick_regular_commit(struct repository *repo,\n-\t\t\t\t\t  struct commit *pickme,\n-\t\t\t\t\t  kh_oid_map_t *replayed_commits,\n-\t\t\t\t\t  struct commit *onto,\n-\t\t\t\t\t  struct merge_options *merge_opt,\n-\t\t\t\t\t  struct merge_result *result)\n-{\n-\tstruct commit *base, *replayed_base;\n-\tstruct tree *pickme_tree, *base_tree;\n-\n-\tbase = pickme->parents->item;\n-\treplayed_base = mapped_commit(replayed_commits, base, onto);\n-\n-\tresult->tree = repo_get_commit_tree(repo, replayed_base);\n-\tpickme_tree = repo_get_commit_tree(repo, pickme);\n-\tbase_tree = repo_get_commit_tree(repo, base);\n-\n-\tmerge_opt->branch1 = short_commit_name(repo, replayed_base);\n-\tmerge_opt->branch2 = short_commit_name(repo, pickme);\n-\tmerge_opt->ancestor = xstrfmt(\"parent of %s\", merge_opt->branch2);\n-\n-\tmerge_incore_nonrecursive(merge_opt,\n-\t\t\t\t  base_tree,\n-\t\t\t\t  result->tree,\n-\t\t\t\t  pickme_tree,\n-\t\t\t\t  result);\n-\n-\tfree((char*)merge_opt->ancestor);\n-\tmerge_opt->ancestor = NULL;\n-\tif (!result->clean)\n-\t\treturn NULL;\n-\treturn create_commit(repo, result->tree, pickme, replayed_base);\n-}\n-\n-struct replay_revisions_options {\n-\tconst char *advance;\n-\tconst char *onto;\n-\tint contained;\n-};\n-\n-struct replay_result {\n-\tstruct replay_ref_update {\n-\t\tchar *refname;\n-\t\tstruct object_id old_oid;\n-\t\tstruct object_id new_oid;\n-\t} *updates;\n-\tsize_t updates_nr, updates_alloc;\n-};\n-\n-static void replay_result_release(struct replay_result *result)\n-{\n-\tfor (size_t i = 0; i < result->updates_nr; i++)\n-\t\tfree(result->updates[i].refname);\n-\tfree(result->updates);\n-}\n-\n-static void replay_result_queue_update(struct replay_result *result,\n-\t\t\t\t       const char *refname,\n-\t\t\t\t       const struct object_id *old_oid,\n-\t\t\t\t       const struct object_id *new_oid)\n-{\n-\tALLOC_GROW(result->updates, result->updates_nr + 1, result->updates_alloc);\n-\tresult->updates[result->updates_nr].refname = xstrdup(refname);\n-\tresult->updates[result->updates_nr].old_oid = *old_oid;\n-\tresult->updates[result->updates_nr].new_oid = *new_oid;\n-\tresult->updates_nr++;\n-}\n-\n-static int replay_revisions(struct rev_info *revs,\n-\t\t\t    struct replay_revisions_options *opts,\n-\t\t\t    struct replay_result *out)\n-{\n-\tkh_oid_map_t *replayed_commits = NULL;\n-\tstruct strset *update_refs = NULL;\n-\tstruct commit *last_commit = NULL;\n-\tstruct commit *commit;\n-\tstruct commit *onto = NULL;\n-\tstruct merge_options merge_opt;\n-\tstruct merge_result result;\n-\tchar *advance;\n-\tint ret;\n-\n-\tadvance = xstrdup_or_null(opts->advance);\n-\tset_up_replay_mode(revs->repo, &revs->cmdline, opts->onto, &advance,\n-\t\t\t   &onto, &update_refs);\n-\n-\t/* FIXME: Should allow replaying commits with the first as a root commit */\n-\n-\tif (prepare_revision_walk(revs) < 0) {\n-\t\tret = error(_(\"error preparing revisions\"));\n-\t\tgoto out;\n-\t}\n-\n-\tinit_basic_merge_options(&merge_opt, revs->repo);\n-\tmemset(&result, 0, sizeof(result));\n-\tmerge_opt.show_rename_progress = 0;\n-\tlast_commit = onto;\n-\treplayed_commits = kh_init_oid_map();\n-\twhile ((commit = get_revision(revs))) {\n-\t\tconst struct name_decoration *decoration;\n-\t\tkhint_t pos;\n-\t\tint hr;\n-\n-\t\tif (!commit->parents)\n-\t\t\tdie(_(\"replaying down from root commit is not supported yet!\"));\n-\t\tif (commit->parents->next)\n-\t\t\tdie(_(\"replaying merge commits is not supported yet!\"));\n-\n-\t\tlast_commit = pick_regular_commit(revs->repo, commit, replayed_commits,\n-\t\t\t\t\t\t  onto, &merge_opt, &result);\n-\t\tif (!last_commit)\n-\t\t\tbreak;\n-\n-\t\t/* Record commit -> last_commit mapping */\n-\t\tpos = kh_put_oid_map(replayed_commits, commit->object.oid, &hr);\n-\t\tif (hr == 0)\n-\t\t\tBUG(\"Duplicate rewritten commit: %s\\n\",\n-\t\t\t    oid_to_hex(&commit->object.oid));\n-\t\tkh_value(replayed_commits, pos) = last_commit;\n-\n-\t\t/* Update any necessary branches */\n-\t\tif (advance)\n-\t\t\tcontinue;\n-\t\tdecoration = get_name_decoration(&commit->object);\n-\t\tif (!decoration)\n-\t\t\tcontinue;\n-\t\twhile (decoration) {\n-\t\t\tif (decoration->type == DECORATION_REF_LOCAL &&\n-\t\t\t    (opts->contained || strset_contains(update_refs,\n-\t\t\t\t\t\t\t\tdecoration->name))) {\n-\t\t\t\treplay_result_queue_update(out, decoration->name,\n-\t\t\t\t\t\t\t   &commit->object.oid,\n-\t\t\t\t\t\t\t   &last_commit->object.oid);\n-\t\t\t}\n-\t\t\tdecoration = decoration->next;\n-\t\t}\n-\t}\n-\n-\tif (!result.clean) {\n-\t\tret = 1;\n-\t\tgoto out;\n-\t}\n-\n-\t/* In --advance mode, advance the target ref */\n-\tif (advance)\n-\t\treplay_result_queue_update(out, advance,\n-\t\t\t\t\t   &onto->object.oid,\n-\t\t\t\t\t   &last_commit->object.oid);\n-\n-\tret = 0;\n-\n-out:\n-\tif (update_refs) {\n-\t\tstrset_clear(update_refs);\n-\t\tfree(update_refs);\n-\t}\n-\tkh_destroy_oid_map(replayed_commits);\n-\tmerge_finalize(&merge_opt, &result);\n-\tfree(advance);\n-\treturn ret;\n-}\n-\n static enum ref_action_mode parse_ref_action_mode(const char *ref_action, const char *source)\n {\n \tif (!ref_action || !strcmp(ref_action, \"update\"))\ndiff --git a/meson.build b/meson.build\nindex dd52efd1c8..a5a4e99b25 100644\n--- a/meson.build\n+++ b/meson.build\n@@ -471,6 +471,7 @@ libgit_sources = [\n   'repack-midx.c',\n   'repack-promisor.c',\n   'replace-object.c',\n+  'replay.c',\n   'repo-settings.c',\n   'repository.c',\n   'rerere.c',\ndiff --git a/replay.c b/replay.c\nnew file mode 100644\nindex 0000000000..c580a9caa2\n--- /dev/null\n+++ b/replay.c\n@@ -0,0 +1,354 @@\n+#define USE_THE_REPOSITORY_VARIABLE\n+#define DISABLE_SIGN_COMPARE_WARNINGS\n+\n+#include \"git-compat-util.h\"\n+#include \"environment.h\"\n+#include \"hex.h\"\n+#include \"merge-ort.h\"\n+#include \"object-name.h\"\n+#include \"refs.h\"\n+#include \"replay.h\"\n+#include \"revision.h\"\n+#include \"strmap.h\"\n+#include \"tree.h\"\n+\n+static const char *short_commit_name(struct repository *repo,\n+\t\t\t\t     struct commit *commit)\n+{\n+\treturn repo_find_unique_abbrev(repo, &commit->object.oid,\n+\t\t\t\t       DEFAULT_ABBREV);\n+}\n+\n+static struct commit *peel_committish(struct repository *repo,\n+\t\t\t\t      const char *name,\n+\t\t\t\t      const char *mode)\n+{\n+\tstruct object *obj;\n+\tstruct object_id oid;\n+\n+\tif (repo_get_oid(repo, name, &oid))\n+\t\tdie(_(\"'%s' is not a valid commit-ish for %s\"), name, mode);\n+\tobj = parse_object_or_die(repo, &oid, name);\n+\treturn (struct commit *)repo_peel_to_type(repo, name, 0, obj,\n+\t\t\t\t\t\t  OBJ_COMMIT);\n+}\n+\n+static char *get_author(const char *message)\n+{\n+\tsize_t len;\n+\tconst char *a;\n+\n+\ta = find_commit_header(message, \"author\", &len);\n+\tif (a)\n+\t\treturn xmemdupz(a, len);\n+\n+\treturn NULL;\n+}\n+\n+static struct commit *create_commit(struct repository *repo,\n+\t\t\t\t    struct tree *tree,\n+\t\t\t\t    struct commit *based_on,\n+\t\t\t\t    struct commit *parent)\n+{\n+\tstruct object_id ret;\n+\tstruct object *obj = NULL;\n+\tstruct commit_list *parents = NULL;\n+\tchar *author;\n+\tchar *sign_commit = NULL; /* FIXME: cli users might want to sign again */\n+\tstruct commit_extra_header *extra = NULL;\n+\tstruct strbuf msg = STRBUF_INIT;\n+\tconst char *out_enc = get_commit_output_encoding();\n+\tconst char *message = repo_logmsg_reencode(repo, based_on,\n+\t\t\t\t\t\t   NULL, out_enc);\n+\tconst char *orig_message = NULL;\n+\tconst char *exclude_gpgsig[] = { \"gpgsig\", \"gpgsig-sha256\", NULL };\n+\n+\tcommit_list_insert(parent, &parents);\n+\textra = read_commit_extra_headers(based_on, exclude_gpgsig);\n+\tfind_commit_subject(message, &orig_message);\n+\tstrbuf_addstr(&msg, orig_message);\n+\tauthor = get_author(message);\n+\treset_ident_date();\n+\tif (commit_tree_extended(msg.buf, msg.len, &tree->object.oid, parents,\n+\t\t\t\t &ret, author, NULL, sign_commit, extra)) {\n+\t\terror(_(\"failed to write commit object\"));\n+\t\tgoto out;\n+\t}\n+\n+\tobj = parse_object(repo, &ret);\n+\n+out:\n+\trepo_unuse_commit_buffer(the_repository, based_on, message);\n+\tfree_commit_extra_headers(extra);\n+\tfree_commit_list(parents);\n+\tstrbuf_release(&msg);\n+\tfree(author);\n+\treturn (struct commit *)obj;\n+}\n+\n+struct ref_info {\n+\tstruct commit *onto;\n+\tstruct strset positive_refs;\n+\tstruct strset negative_refs;\n+\tint positive_refexprs;\n+\tint negative_refexprs;\n+};\n+\n+static void get_ref_information(struct repository *repo,\n+\t\t\t\tstruct rev_cmdline_info *cmd_info,\n+\t\t\t\tstruct ref_info *ref_info)\n+{\n+\tint i;\n+\n+\tref_info->onto = NULL;\n+\tstrset_init(&ref_info->positive_refs);\n+\tstrset_init(&ref_info->negative_refs);\n+\tref_info->positive_refexprs = 0;\n+\tref_info->negative_refexprs = 0;\n+\n+\t/*\n+\t * When the user specifies e.g.\n+\t *   git replay origin/main..mybranch\n+\t *   git replay ^origin/next mybranch1 mybranch2\n+\t * we want to be able to determine where to replay the commits.  In\n+\t * these examples, the branches are probably based on an old version\n+\t * of either origin/main or origin/next, so we want to replay on the\n+\t * newest version of that branch.  In contrast we would want to error\n+\t * out if they ran\n+\t *   git replay ^origin/master ^origin/next mybranch\n+\t *   git replay mybranch~2..mybranch\n+\t * the first of those because there's no unique base to choose, and\n+\t * the second because they'd likely just be replaying commits on top\n+\t * of the same commit and not making any difference.\n+\t */\n+\tfor (i = 0; i < cmd_info->nr; i++) {\n+\t\tstruct rev_cmdline_entry *e = cmd_info->rev + i;\n+\t\tstruct object_id oid;\n+\t\tconst char *refexpr = e->name;\n+\t\tchar *fullname = NULL;\n+\t\tint can_uniquely_dwim = 1;\n+\n+\t\tif (*refexpr == '^')\n+\t\t\trefexpr++;\n+\t\tif (repo_dwim_ref(repo, refexpr, strlen(refexpr), &oid, &fullname, 0) != 1)\n+\t\t\tcan_uniquely_dwim = 0;\n+\n+\t\tif (e->flags & BOTTOM) {\n+\t\t\tif (can_uniquely_dwim)\n+\t\t\t\tstrset_add(&ref_info->negative_refs, fullname);\n+\t\t\tif (!ref_info->negative_refexprs)\n+\t\t\t\tref_info->onto = lookup_commit_reference_gently(repo,\n+\t\t\t\t\t\t\t\t\t\t&e->item->oid, 1);\n+\t\t\tref_info->negative_refexprs++;\n+\t\t} else {\n+\t\t\tif (can_uniquely_dwim)\n+\t\t\t\tstrset_add(&ref_info->positive_refs, fullname);\n+\t\t\tref_info->positive_refexprs++;\n+\t\t}\n+\n+\t\tfree(fullname);\n+\t}\n+}\n+\n+static void set_up_replay_mode(struct repository *repo,\n+\t\t\t       struct rev_cmdline_info *cmd_info,\n+\t\t\t       const char *onto_name,\n+\t\t\t       char **advance_name,\n+\t\t\t       struct commit **onto,\n+\t\t\t       struct strset **update_refs)\n+{\n+\tstruct ref_info rinfo;\n+\n+\tget_ref_information(repo, cmd_info, &rinfo);\n+\tif (!rinfo.positive_refexprs)\n+\t\tdie(_(\"need some commits to replay\"));\n+\n+\tif (!onto_name == !*advance_name)\n+\t\tBUG(\"one and only one of onto_name and *advance_name must be given\");\n+\n+\tif (onto_name) {\n+\t\t*onto = peel_committish(repo, onto_name, \"--onto\");\n+\t\tif (rinfo.positive_refexprs <\n+\t\t    strset_get_size(&rinfo.positive_refs))\n+\t\t\tdie(_(\"all positive revisions given must be references\"));\n+\t\t*update_refs = xcalloc(1, sizeof(**update_refs));\n+\t\t**update_refs = rinfo.positive_refs;\n+\t\tmemset(&rinfo.positive_refs, 0, sizeof(**update_refs));\n+\t} else {\n+\t\tstruct object_id oid;\n+\t\tchar *fullname = NULL;\n+\n+\t\tif (!*advance_name)\n+\t\t\tBUG(\"expected either onto_name or *advance_name in this function\");\n+\n+\t\tif (repo_dwim_ref(repo, *advance_name, strlen(*advance_name),\n+\t\t\t     &oid, &fullname, 0) == 1) {\n+\t\t\tfree(*advance_name);\n+\t\t\t*advance_name = fullname;\n+\t\t} else {\n+\t\t\tdie(_(\"argument to --advance must be a reference\"));\n+\t\t}\n+\t\t*onto = peel_committish(repo, *advance_name, \"--advance\");\n+\t\tif (rinfo.positive_refexprs > 1)\n+\t\t\tdie(_(\"cannot advance target with multiple sources because ordering would be ill-defined\"));\n+\t}\n+\tstrset_clear(&rinfo.negative_refs);\n+\tstrset_clear(&rinfo.positive_refs);\n+}\n+\n+static struct commit *mapped_commit(kh_oid_map_t *replayed_commits,\n+\t\t\t\t    struct commit *commit,\n+\t\t\t\t    struct commit *fallback)\n+{\n+\tkhint_t pos = kh_get_oid_map(replayed_commits, commit->object.oid);\n+\tif (pos == kh_end(replayed_commits))\n+\t\treturn fallback;\n+\treturn kh_value(replayed_commits, pos);\n+}\n+\n+static struct commit *pick_regular_commit(struct repository *repo,\n+\t\t\t\t\t  struct commit *pickme,\n+\t\t\t\t\t  kh_oid_map_t *replayed_commits,\n+\t\t\t\t\t  struct commit *onto,\n+\t\t\t\t\t  struct merge_options *merge_opt,\n+\t\t\t\t\t  struct merge_result *result)\n+{\n+\tstruct commit *base, *replayed_base;\n+\tstruct tree *pickme_tree, *base_tree;\n+\n+\tbase = pickme->parents->item;\n+\treplayed_base = mapped_commit(replayed_commits, base, onto);\n+\n+\tresult->tree = repo_get_commit_tree(repo, replayed_base);\n+\tpickme_tree = repo_get_commit_tree(repo, pickme);\n+\tbase_tree = repo_get_commit_tree(repo, base);\n+\n+\tmerge_opt->branch1 = short_commit_name(repo, replayed_base);\n+\tmerge_opt->branch2 = short_commit_name(repo, pickme);\n+\tmerge_opt->ancestor = xstrfmt(\"parent of %s\", merge_opt->branch2);\n+\n+\tmerge_incore_nonrecursive(merge_opt,\n+\t\t\t\t  base_tree,\n+\t\t\t\t  result->tree,\n+\t\t\t\t  pickme_tree,\n+\t\t\t\t  result);\n+\n+\tfree((char*)merge_opt->ancestor);\n+\tmerge_opt->ancestor = NULL;\n+\tif (!result->clean)\n+\t\treturn NULL;\n+\treturn create_commit(repo, result->tree, pickme, replayed_base);\n+}\n+\n+void replay_result_release(struct replay_result *result)\n+{\n+\tfor (size_t i = 0; i < result->updates_nr; i++)\n+\t\tfree(result->updates[i].refname);\n+\tfree(result->updates);\n+}\n+\n+static void replay_result_queue_update(struct replay_result *result,\n+\t\t\t\t       const char *refname,\n+\t\t\t\t       const struct object_id *old_oid,\n+\t\t\t\t       const struct object_id *new_oid)\n+{\n+\tALLOC_GROW(result->updates, result->updates_nr + 1, result->updates_alloc);\n+\tresult->updates[result->updates_nr].refname = xstrdup(refname);\n+\tresult->updates[result->updates_nr].old_oid = *old_oid;\n+\tresult->updates[result->updates_nr].new_oid = *new_oid;\n+\tresult->updates_nr++;\n+}\n+\n+int replay_revisions(struct rev_info *revs,\n+\t\t     struct replay_revisions_options *opts,\n+\t\t     struct replay_result *out)\n+{\n+\tkh_oid_map_t *replayed_commits = NULL;\n+\tstruct strset *update_refs = NULL;\n+\tstruct commit *last_commit = NULL;\n+\tstruct commit *commit;\n+\tstruct commit *onto = NULL;\n+\tstruct merge_options merge_opt;\n+\tstruct merge_result result;\n+\tchar *advance;\n+\tint ret;\n+\n+\tadvance = xstrdup_or_null(opts->advance);\n+\tset_up_replay_mode(revs->repo, &revs->cmdline, opts->onto, &advance,\n+\t\t\t   &onto, &update_refs);\n+\n+\t/* FIXME: Should allow replaying commits with the first as a root commit */\n+\n+\tif (prepare_revision_walk(revs) < 0) {\n+\t\tret = error(_(\"error preparing revisions\"));\n+\t\tgoto out;\n+\t}\n+\n+\tinit_basic_merge_options(&merge_opt, revs->repo);\n+\tmemset(&result, 0, sizeof(result));\n+\tmerge_opt.show_rename_progress = 0;\n+\tlast_commit = onto;\n+\treplayed_commits = kh_init_oid_map();\n+\twhile ((commit = get_revision(revs))) {\n+\t\tconst struct name_decoration *decoration;\n+\t\tkhint_t pos;\n+\t\tint hr;\n+\n+\t\tif (!commit->parents)\n+\t\t\tdie(_(\"replaying down from root commit is not supported yet!\"));\n+\t\tif (commit->parents->next)\n+\t\t\tdie(_(\"replaying merge commits is not supported yet!\"));\n+\n+\t\tlast_commit = pick_regular_commit(revs->repo, commit, replayed_commits,\n+\t\t\t\t\t\t  onto, &merge_opt, &result);\n+\t\tif (!last_commit)\n+\t\t\tbreak;\n+\n+\t\t/* Record commit -> last_commit mapping */\n+\t\tpos = kh_put_oid_map(replayed_commits, commit->object.oid, &hr);\n+\t\tif (hr == 0)\n+\t\t\tBUG(\"Duplicate rewritten commit: %s\\n\",\n+\t\t\t    oid_to_hex(&commit->object.oid));\n+\t\tkh_value(replayed_commits, pos) = last_commit;\n+\n+\t\t/* Update any necessary branches */\n+\t\tif (advance)\n+\t\t\tcontinue;\n+\t\tdecoration = get_name_decoration(&commit->object);\n+\t\tif (!decoration)\n+\t\t\tcontinue;\n+\t\twhile (decoration) {\n+\t\t\tif (decoration->type == DECORATION_REF_LOCAL &&\n+\t\t\t    (opts->contained || strset_contains(update_refs,\n+\t\t\t\t\t\t\t\tdecoration->name))) {\n+\t\t\t\treplay_result_queue_update(out, decoration->name,\n+\t\t\t\t\t\t\t   &commit->object.oid,\n+\t\t\t\t\t\t\t   &last_commit->object.oid);\n+\t\t\t}\n+\t\t\tdecoration = decoration->next;\n+\t\t}\n+\t}\n+\n+\tif (!result.clean) {\n+\t\tret = 1;\n+\t\tgoto out;\n+\t}\n+\n+\t/* In --advance mode, advance the target ref */\n+\tif (advance)\n+\t\treplay_result_queue_update(out, advance,\n+\t\t\t\t\t   &onto->object.oid,\n+\t\t\t\t\t   &last_commit->object.oid);\n+\n+\tret = 0;\n+\n+out:\n+\tif (update_refs) {\n+\t\tstrset_clear(update_refs);\n+\t\tfree(update_refs);\n+\t}\n+\tkh_destroy_oid_map(replayed_commits);\n+\tmerge_finalize(&merge_opt, &result);\n+\tfree(advance);\n+\treturn ret;\n+}\ndiff --git a/replay.h b/replay.h\nnew file mode 100644\nindex 0000000000..d8407dc7f7\n--- /dev/null\n+++ b/replay.h\n@@ -0,0 +1,61 @@\n+#ifndef REPLAY_H\n+#define REPLAY_H\n+\n+#include \"hash.h\"\n+\n+struct repository;\n+struct rev_info;\n+\n+/*\n+ * A set of options that can be passed to `replay_revisions()`.\n+ */\n+struct replay_revisions_options {\n+\t/*\n+\t * Starting point at which to create the new commits; must be a branch\n+\t * name. The branch will be updated to point to the rewritten commits.\n+\t * This option is mutually exclusive with `onto`.\n+\t */\n+\tconst char *advance;\n+\n+\t/*\n+\t * Starting point at which to create the new commits; must be a\n+\t * committish. References pointing at decendants of `onto` will be\n+\t * updated to point to the new commits.\n+\t */\n+\t const char *onto;\n+\n+\t/*\n+\t * Update branches that point at commits in the given revision range.\n+\t * Requires `onto` to be set.\n+\t */\n+\tint contained;\n+};\n+\n+/* This struct is used as an out-parameter by `replay_revisions()`. */\n+struct replay_result {\n+\t/*\n+\t * The set of reference updates that are caused by replaying the\n+\t * commits.\n+\t */\n+\tstruct replay_ref_update {\n+\t\tchar *refname;\n+\t\tstruct object_id old_oid;\n+\t\tstruct object_id new_oid;\n+\t} *updates;\n+\tsize_t updates_nr, updates_alloc;\n+};\n+\n+void replay_result_release(struct replay_result *result);\n+\n+/*\n+ * Replay a set of commits onto a new location. Leaves both the working tree,\n+ * index and references untouched. Reference updates caused by the replay will\n+ * be recorded in the `updates` out pointer.\n+ *\n+ * Returns 0 on success, 1 on conflict and a negative error code otherwise.\n+ */\n+int replay_revisions(struct rev_info *revs,\n+\t\t     struct replay_revisions_options *opts,\n+\t\t     struct replay_result *out);\n+\n+#endif\n\n-- \n2.52.0.590.g1f87b77810.dirty\n\n"},{"id":"533719","messageId":"20260113-b4-pks-history-builtin-v11-3-e74ebfa2652d@pks.im","threadId":"63986","inReplyTo":"20260113-b4-pks-history-builtin-v11-0-e74ebfa2652d@pks.im","subject":"[PATCH v11 3/8] replay: small set of cleanups","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-01-13T09:54:34Z","receivedAt":"2026-01-13T09:54:47Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Perform a small set of cleanups so that the \"replay\" logic compiles with\n\"-Wsign-compare\" and doesn't use `the_repository` anymore. Note that\nthere are still some implicit dependencies on `the_repository`, e.g.\nbecause we use `get_commit_output_encoding()`.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n replay.c | 11 ++++-------\n 1 file changed, 4 insertions(+), 7 deletions(-)\n\ndiff --git a/replay.c b/replay.c\nindex c580a9caa2..ae13b59abc 100644\n--- a/replay.c\n+++ b/replay.c\n@@ -1,5 +1,4 @@\n #define USE_THE_REPOSITORY_VARIABLE\n-#define DISABLE_SIGN_COMPARE_WARNINGS\n \n #include \"git-compat-util.h\"\n #include \"environment.h\"\n@@ -78,7 +77,7 @@ static struct commit *create_commit(struct repository *repo,\n \tobj = parse_object(repo, &ret);\n \n out:\n-\trepo_unuse_commit_buffer(the_repository, based_on, message);\n+\trepo_unuse_commit_buffer(repo, based_on, message);\n \tfree_commit_extra_headers(extra);\n \tfree_commit_list(parents);\n \tstrbuf_release(&msg);\n@@ -90,16 +89,14 @@ struct ref_info {\n \tstruct commit *onto;\n \tstruct strset positive_refs;\n \tstruct strset negative_refs;\n-\tint positive_refexprs;\n-\tint negative_refexprs;\n+\tsize_t positive_refexprs;\n+\tsize_t negative_refexprs;\n };\n \n static void get_ref_information(struct repository *repo,\n \t\t\t\tstruct rev_cmdline_info *cmd_info,\n \t\t\t\tstruct ref_info *ref_info)\n {\n-\tint i;\n-\n \tref_info->onto = NULL;\n \tstrset_init(&ref_info->positive_refs);\n \tstrset_init(&ref_info->negative_refs);\n@@ -121,7 +118,7 @@ static void get_ref_information(struct repository *repo,\n \t * the second because they'd likely just be replaying commits on top\n \t * of the same commit and not making any difference.\n \t */\n-\tfor (i = 0; i < cmd_info->nr; i++) {\n+\tfor (size_t i = 0; i < cmd_info->nr; i++) {\n \t\tstruct rev_cmdline_entry *e = cmd_info->rev + i;\n \t\tstruct object_id oid;\n \t\tconst char *refexpr = e->name;\n\n-- \n2.52.0.590.g1f87b77810.dirty\n\n"},{"id":"533721","messageId":"20260113-b4-pks-history-builtin-v11-4-e74ebfa2652d@pks.im","threadId":"63986","inReplyTo":"20260113-b4-pks-history-builtin-v11-0-e74ebfa2652d@pks.im","subject":"[PATCH v11 4/8] replay: support empty commit ranges","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-01-13T09:54:35Z","receivedAt":"2026-01-13T09:54:50Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"In a subsequent commit we're about to introduce a new user of the replay\nsubsystem. With that new user, the range of commits that we'll want to\nreplay will be identified implicitly via a single commit, and will\ninclude all descendants of that commit to any branch. If that commit has\nno descendants (because it's the tip of some branch), then the range of\nrevisions that we're asked to replay becomes empty. This case does not\nmake sense with git-replay(1), but with the new command it will.\n\nThis case is not currently supported by `replay_revisions()` though\nbecause we zero-initialize `struct merge_result`. This includes its\n`.clean` member, which indicates whether the merge ran into a conflict\nor not. But given that we don't have any revision to replay, we won't\never perform any merge at all, and consequently that member will never\nbe set to `1`. We thus later think that there's been a merge conflict\nand return an error from `replay_commits()`.\n\nAddress this issue by initializing the `.clean` member to `1`.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n replay.c | 5 +++--\n 1 file changed, 3 insertions(+), 2 deletions(-)\n\ndiff --git a/replay.c b/replay.c\nindex ae13b59abc..6680d50bd7 100644\n--- a/replay.c\n+++ b/replay.c\n@@ -266,7 +266,9 @@ int replay_revisions(struct rev_info *revs,\n \tstruct commit *commit;\n \tstruct commit *onto = NULL;\n \tstruct merge_options merge_opt;\n-\tstruct merge_result result;\n+\tstruct merge_result result = {\n+\t\t.clean = 1,\n+\t};\n \tchar *advance;\n \tint ret;\n \n@@ -282,7 +284,6 @@ int replay_revisions(struct rev_info *revs,\n \t}\n \n \tinit_basic_merge_options(&merge_opt, revs->repo);\n-\tmemset(&result, 0, sizeof(result));\n \tmerge_opt.show_rename_progress = 0;\n \tlast_commit = onto;\n \treplayed_commits = kh_init_oid_map();\n\n-- \n2.52.0.590.g1f87b77810.dirty\n\n"},{"id":"533722","messageId":"20260113-b4-pks-history-builtin-v11-5-e74ebfa2652d@pks.im","threadId":"63986","inReplyTo":"20260113-b4-pks-history-builtin-v11-0-e74ebfa2652d@pks.im","subject":"[PATCH v11 5/8] replay: support updating detached HEAD","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-01-13T09:54:36Z","receivedAt":"2026-01-13T09:54:53Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"In a subsequent commit we're about to introduce a new git-history(1)\ncommand, which will by default work on all local branches and HEAD. This\nis already well-supported by the replay machinery for most of the part:\nupdating branches is one of its prime use cases, and the HEAD ref is\nalso updated in case it points to any of the branches.\n\nHowever, what's not supported yet is to update HEAD in case it is not a\nsymbolic ref. We determine the refs that need to be updated by iterating\nthrough the decorations of the original commit, but we only update those\nrefs that are `DECORATION_REF_LOCAL`, which covers local branches.\n\nAddress this gap by also handling `DECORATION_REF_HEAD`. Note though\nthat this needs to only happen in case we're working on a detached HEAD.\nIf HEAD is pointing to a branch, then we'd already update that branch\nvia `DECORATION_REF_LOCAL`.\n\nRefactor the loop that iterates through the decorations a bit to make\nthe individual conditions easier to understand.\n\nBased-on-patch-by: Elijah Newren <newren@gmail.com>\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n replay.c                 | 47 +++++++++++++++++++++++++++++++++--------------\n t/t3650-replay-basics.sh |  9 +++++++++\n 2 files changed, 42 insertions(+), 14 deletions(-)\n\ndiff --git a/replay.c b/replay.c\nindex 6680d50bd7..94fb76384b 100644\n--- a/replay.c\n+++ b/replay.c\n@@ -150,11 +150,17 @@ static void get_ref_information(struct repository *repo,\n static void set_up_replay_mode(struct repository *repo,\n \t\t\t       struct rev_cmdline_info *cmd_info,\n \t\t\t       const char *onto_name,\n+\t\t\t       bool *detached_head,\n \t\t\t       char **advance_name,\n \t\t\t       struct commit **onto,\n \t\t\t       struct strset **update_refs)\n {\n \tstruct ref_info rinfo;\n+\tint head_flags = 0;\n+\n+\trefs_read_ref_full(get_main_ref_store(repo), \"HEAD\",\n+\t\t\t   RESOLVE_REF_NO_RECURSE, NULL, &head_flags);\n+\t*detached_head = !(head_flags & REF_ISSYMREF);\n \n \tget_ref_information(repo, cmd_info, &rinfo);\n \tif (!rinfo.positive_refexprs)\n@@ -269,12 +275,13 @@ int replay_revisions(struct rev_info *revs,\n \tstruct merge_result result = {\n \t\t.clean = 1,\n \t};\n+\tbool detached_head;\n \tchar *advance;\n \tint ret;\n \n \tadvance = xstrdup_or_null(opts->advance);\n-\tset_up_replay_mode(revs->repo, &revs->cmdline, opts->onto, &advance,\n-\t\t\t   &onto, &update_refs);\n+\tset_up_replay_mode(revs->repo, &revs->cmdline, opts->onto,\n+\t\t\t   &detached_head, &advance, &onto, &update_refs);\n \n \t/* FIXME: Should allow replaying commits with the first as a root commit */\n \n@@ -312,18 +319,30 @@ int replay_revisions(struct rev_info *revs,\n \t\t/* Update any necessary branches */\n \t\tif (advance)\n \t\t\tcontinue;\n-\t\tdecoration = get_name_decoration(&commit->object);\n-\t\tif (!decoration)\n-\t\t\tcontinue;\n-\t\twhile (decoration) {\n-\t\t\tif (decoration->type == DECORATION_REF_LOCAL &&\n-\t\t\t    (opts->contained || strset_contains(update_refs,\n-\t\t\t\t\t\t\t\tdecoration->name))) {\n-\t\t\t\treplay_result_queue_update(out, decoration->name,\n-\t\t\t\t\t\t\t   &commit->object.oid,\n-\t\t\t\t\t\t\t   &last_commit->object.oid);\n-\t\t\t}\n-\t\t\tdecoration = decoration->next;\n+\n+\t\tfor (decoration = get_name_decoration(&commit->object);\n+\t\t     decoration;\n+\t\t     decoration = decoration->next)\n+\t\t{\n+\t\t\tif (decoration->type != DECORATION_REF_LOCAL &&\n+\t\t\t    decoration->type != DECORATION_REF_HEAD)\n+\t\t\t\tcontinue;\n+\n+\t\t\t/*\n+\t\t\t * We only need to update HEAD separately in case it's\n+\t\t\t * detached. If it's not we'd already update the branch\n+\t\t\t * it is pointing to.\n+\t\t\t */\n+\t\t\tif (decoration->type == DECORATION_REF_HEAD && !detached_head)\n+\t\t\t\tcontinue;\n+\n+\t\t\tif (!opts->contained &&\n+\t\t\t    !strset_contains(update_refs, decoration->name))\n+\t\t\t\tcontinue;\n+\n+\t\t\treplay_result_queue_update(out, decoration->name,\n+\t\t\t\t\t\t   &commit->object.oid,\n+\t\t\t\t\t\t   &last_commit->object.oid);\n \t\t}\n \t}\n \ndiff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh\nindex 307101eeb9..c862aa39f3 100755\n--- a/t/t3650-replay-basics.sh\n+++ b/t/t3650-replay-basics.sh\n@@ -249,6 +249,15 @@ test_expect_success 'using replay on bare repo to rebase multiple divergent bran\n \tdone\n '\n \n+test_expect_success 'using replay to update detached HEAD' '\n+\tcurrent_head=$(git branch --show-current) &&\n+\ttest_when_finished git switch \"$current_head\" &&\n+\tgit switch --detach &&\n+\ttest_commit something &&\n+\tgit replay --ref-action=print --onto HEAD~2 --ref-action=print HEAD~..HEAD >updates &&\n+\ttest_grep \"update HEAD \" updates\n+'\n+\n test_expect_success 'merge.directoryRenames=false' '\n \t# create a test case that stress-tests the rename caching\n \tgit switch -c rename-onto &&\n\n-- \n2.52.0.590.g1f87b77810.dirty\n\n"},{"id":"533723","messageId":"20260113-b4-pks-history-builtin-v11-6-e74ebfa2652d@pks.im","threadId":"63986","inReplyTo":"20260113-b4-pks-history-builtin-v11-0-e74ebfa2652d@pks.im","subject":"[PATCH v11 6/8] wt-status: provide function to expose status for trees","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-01-13T09:54:37Z","receivedAt":"2026-01-13T09:54:55Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The \"wt-status\" subsystem is responsible for printing status information\naround the current state of the working tree. This most importantly\nincludes information around whether the working tree or the index have\nany changes.\n\nWe're about to introduce a new command where the changes in neither of\nthem are actually relevant to us. Instead, what we want is to format the\nchanges between two different trees. While it is a little bit of a\nstretch to add this as functionality to _working tree_ status, it\ndoesn't make any sense to open-code this functionality, either.\n\nImplement a new function `wt_status_collect_changes_trees()` that diffs\ntwo trees and formats the status accordingly. This function is not yet\nused, but will be in a subsequent commit.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n wt-status.c | 24 ++++++++++++++++++++++++\n wt-status.h |  9 +++++++++\n 2 files changed, 33 insertions(+)\n\ndiff --git a/wt-status.c b/wt-status.c\nindex e12adb26b9..95942399f8 100644\n--- a/wt-status.c\n+++ b/wt-status.c\n@@ -612,6 +612,30 @@ static void wt_status_collect_updated_cb(struct diff_queue_struct *q,\n \t}\n }\n \n+void wt_status_collect_changes_trees(struct wt_status *s,\n+\t\t\t\t     const struct object_id *old_treeish,\n+\t\t\t\t     const struct object_id *new_treeish)\n+{\n+\tstruct diff_options opts = { 0 };\n+\n+\trepo_diff_setup(s->repo, &opts);\n+\topts.output_format = DIFF_FORMAT_CALLBACK;\n+\topts.format_callback = wt_status_collect_updated_cb;\n+\topts.format_callback_data = s;\n+\topts.detect_rename = s->detect_rename >= 0 ? s->detect_rename : opts.detect_rename;\n+\topts.rename_limit = s->rename_limit >= 0 ? s->rename_limit : opts.rename_limit;\n+\topts.rename_score = s->rename_score >= 0 ? s->rename_score : opts.rename_score;\n+\topts.flags.recursive = 1;\n+\tdiff_setup_done(&opts);\n+\n+\tdiff_tree_oid(old_treeish, new_treeish, \"\", &opts);\n+\tdiffcore_std(&opts);\n+\tdiff_flush(&opts);\n+\twt_status_get_state(s->repo, &s->state, 0);\n+\n+\tdiff_free(&opts);\n+}\n+\n static void wt_status_collect_changes_worktree(struct wt_status *s)\n {\n \tstruct rev_info rev;\ndiff --git a/wt-status.h b/wt-status.h\nindex e40a27214a..e9fe32e98c 100644\n--- a/wt-status.h\n+++ b/wt-status.h\n@@ -153,6 +153,15 @@ void wt_status_add_cut_line(struct wt_status *s);\n void wt_status_prepare(struct repository *r, struct wt_status *s);\n void wt_status_print(struct wt_status *s);\n void wt_status_collect(struct wt_status *s);\n+\n+/*\n+ * Collect all changes between the two trees. Changes will be displayed as if\n+ * they were staged into the index.\n+ */\n+void wt_status_collect_changes_trees(struct wt_status *s,\n+\t\t\t\t     const struct object_id *old_treeish,\n+\t\t\t\t     const struct object_id *new_treeish);\n+\n /*\n  * Frees the buffers allocated by wt_status_collect.\n  */\n\n-- \n2.52.0.590.g1f87b77810.dirty\n\n"},{"id":"533724","messageId":"20260113-b4-pks-history-builtin-v11-7-e74ebfa2652d@pks.im","threadId":"63986","inReplyTo":"20260113-b4-pks-history-builtin-v11-0-e74ebfa2652d@pks.im","subject":"[PATCH v11 7/8] builtin: add new \"history\" command","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-01-13T09:54:38Z","receivedAt":"2026-01-13T09:54:58Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"When rewriting history via git-rebase(1) there are a few very common use\ncases:\n\n  - The ordering of two commits should be reversed.\n\n  - A commit should be split up into two commits.\n\n  - A commit should be dropped from the history completely.\n\n  - Multiple commits should be squashed into one.\n\n  - Editing an existing commit that is not the tip of the current\n    branch.\n\nWhile these operations are all doable, it often feels needlessly kludgey\nto do so by doing an interactive rebase, using the editor to say what\none wants, and then perform the actions. Also, some operations like\nsplitting up a commit into two are way more involved than that and\nrequire a whole series of commands.\n\nRebases also do not update dependent branches. The use of stacked\nbranches has grown quite common with competing version control systems\nlike Jujutsu though, so it clearly is a need that users have. While\nrebases _can_ serve this use case if one always works on the latest\nstacked branch, it is somewhat awkward and very easy to get wrong.\n\nAdd a new \"history\" command to plug these gaps. This command will have\nseveral different subcommands to imperatively rewrite history for common\nuse cases like the above.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n .gitignore                     |  1 +\n Documentation/git-history.adoc | 57 ++++++++++++++++++++++++++++++++++++++++++\n Documentation/meson.build      |  1 +\n Makefile                       |  1 +\n builtin.h                      |  1 +\n builtin/history.c              | 22 ++++++++++++++++\n command-list.txt               |  1 +\n git.c                          |  1 +\n meson.build                    |  1 +\n t/meson.build                  |  1 +\n t/t3450-history.sh             | 17 +++++++++++++\n 11 files changed, 104 insertions(+)\n\ndiff --git a/.gitignore b/.gitignore\nindex 78a45cb5be..24635cf2d6 100644\n--- a/.gitignore\n+++ b/.gitignore\n@@ -79,6 +79,7 @@\n /git-grep\n /git-hash-object\n /git-help\n+/git-history\n /git-hook\n /git-http-backend\n /git-http-fetch\ndiff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc\nnew file mode 100644\nindex 0000000000..68c35f36b9\n--- /dev/null\n+++ b/Documentation/git-history.adoc\n@@ -0,0 +1,57 @@\n+git-history(1)\n+==============\n+\n+NAME\n+----\n+git-history - EXPERIMENTAL: Rewrite history\n+\n+SYNOPSIS\n+--------\n+[synopsis]\n+git history [<options>]\n+\n+DESCRIPTION\n+-----------\n+\n+Rewrite history by rearranging or modifying specific commits in the\n+history.\n+\n+THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.\n+\n+This command is related to linkgit:git-rebase[1] in that both commands can be\n+used to rewrite history. There are a couple of major differences though:\n+\n+* linkgit:git-history[1] can work in a bare repository as it does not need to\n+  touch either the index or the worktree.\n+* linkgit:git-history[1] does not execute any linkgit:githooks[5] at the\n+  current point in time. This may change in the future.\n+* linkgit:git-history[1] by default updates all branches that are descendants\n+  of the original commit to point to the rewritten commit.\n+\n+Overall, linkgit:git-history[1] aims to provide a more opinionated way to modify\n+your commit history that is simpler to use compared to linkgit:git-rebase[1] in\n+general.\n+\n+Use linkgit:git-rebase[1] if you want to reapply a range of commits onto a\n+different base, or interactive rebases if you want to edit a range of commits\n+at once.\n+\n+LIMITATIONS\n+-----------\n+\n+This command does not (yet) work with histories that contain merges. You\n+should use linkgit:git-rebase[1] with the `--rebase-merges` flag instead.\n+\n+Furthermore, the command does not support operations that can result in merge\n+conflicts. This limitation is by design as history rewrites are not intended to\n+be stateful operations. The limitation can be lifted once (if) Git learns about\n+first-class conflicts.\n+\n+COMMANDS\n+--------\n+\n+No commands are supported yet.\n+\n+GIT\n+---\n+Part of the linkgit:git[1] suite\ndiff --git a/Documentation/meson.build b/Documentation/meson.build\nindex f02dbc20cb..fd2e8cc02d 100644\n--- a/Documentation/meson.build\n+++ b/Documentation/meson.build\n@@ -64,6 +64,7 @@ manpages = {\n   'git-gui.adoc' : 1,\n   'git-hash-object.adoc' : 1,\n   'git-help.adoc' : 1,\n+  'git-history.adoc' : 1,\n   'git-hook.adoc' : 1,\n   'git-http-backend.adoc' : 1,\n   'git-http-fetch.adoc' : 1,\ndiff --git a/Makefile b/Makefile\nindex 1c64a5d2ae..c0569ed8e4 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -1418,6 +1418,7 @@ BUILTIN_OBJS += builtin/get-tar-commit-id.o\n BUILTIN_OBJS += builtin/grep.o\n BUILTIN_OBJS += builtin/hash-object.o\n BUILTIN_OBJS += builtin/help.o\n+BUILTIN_OBJS += builtin/history.o\n BUILTIN_OBJS += builtin/hook.o\n BUILTIN_OBJS += builtin/index-pack.o\n BUILTIN_OBJS += builtin/init-db.o\ndiff --git a/builtin.h b/builtin.h\nindex 1b35565fbd..93c91d07d4 100644\n--- a/builtin.h\n+++ b/builtin.h\n@@ -172,6 +172,7 @@ int cmd_get_tar_commit_id(int argc, const char **argv, const char *prefix, struc\n int cmd_grep(int argc, const char **argv, const char *prefix, struct repository *repo);\n int cmd_hash_object(int argc, const char **argv, const char *prefix, struct repository *repo);\n int cmd_help(int argc, const char **argv, const char *prefix, struct repository *repo);\n+int cmd_history(int argc, const char **argv, const char *prefix, struct repository *repo);\n int cmd_hook(int argc, const char **argv, const char *prefix, struct repository *repo);\n int cmd_index_pack(int argc, const char **argv, const char *prefix, struct repository *repo);\n int cmd_init_db(int argc, const char **argv, const char *prefix, struct repository *repo);\ndiff --git a/builtin/history.c b/builtin/history.c\nnew file mode 100644\nindex 0000000000..f6fe32610b\n--- /dev/null\n+++ b/builtin/history.c\n@@ -0,0 +1,22 @@\n+#include \"builtin.h\"\n+#include \"gettext.h\"\n+#include \"parse-options.h\"\n+\n+int cmd_history(int argc,\n+\t\tconst char **argv,\n+\t\tconst char *prefix,\n+\t\tstruct repository *repo UNUSED)\n+{\n+\tconst char * const usage[] = {\n+\t\tN_(\"git history [<options>]\"),\n+\t\tNULL,\n+\t};\n+\tstruct option options[] = {\n+\t\tOPT_END(),\n+\t};\n+\n+\targc = parse_options(argc, argv, prefix, options, usage, 0);\n+\tif (argc)\n+\t\tusagef(\"unrecognized argument: %s\", argv[0]);\n+\treturn 0;\n+}\ndiff --git a/command-list.txt b/command-list.txt\nindex accd3d0c4b..f9005cf459 100644\n--- a/command-list.txt\n+++ b/command-list.txt\n@@ -115,6 +115,7 @@ git-grep                                mainporcelain           info\n git-gui                                 mainporcelain\n git-hash-object                         plumbingmanipulators\n git-help                                ancillaryinterrogators          complete\n+git-history                             mainporcelain           history\n git-hook                                purehelpers\n git-http-backend                        synchingrepositories\n git-http-fetch                          synchelpers\ndiff --git a/git.c b/git.c\nindex c5fad56813..744cb6527e 100644\n--- a/git.c\n+++ b/git.c\n@@ -586,6 +586,7 @@ static struct cmd_struct commands[] = {\n \t{ \"grep\", cmd_grep, RUN_SETUP_GENTLY },\n \t{ \"hash-object\", cmd_hash_object },\n \t{ \"help\", cmd_help },\n+\t{ \"history\", cmd_history, RUN_SETUP },\n \t{ \"hook\", cmd_hook, RUN_SETUP },\n \t{ \"index-pack\", cmd_index_pack, RUN_SETUP_GENTLY | NO_PARSEOPT },\n \t{ \"init\", cmd_init_db },\ndiff --git a/meson.build b/meson.build\nindex a5a4e99b25..3a1d12caa4 100644\n--- a/meson.build\n+++ b/meson.build\n@@ -610,6 +610,7 @@ builtin_sources = [\n   'builtin/grep.c',\n   'builtin/hash-object.c',\n   'builtin/help.c',\n+  'builtin/history.c',\n   'builtin/hook.c',\n   'builtin/index-pack.c',\n   'builtin/init-db.c',\ndiff --git a/t/meson.build b/t/meson.build\nindex 459c52a489..73006b095a 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -387,6 +387,7 @@ integration_tests = [\n   't3436-rebase-more-options.sh',\n   't3437-rebase-fixup-options.sh',\n   't3438-rebase-broken-files.sh',\n+  't3450-history.sh',\n   't3500-cherry.sh',\n   't3501-revert-cherry-pick.sh',\n   't3502-cherry-pick-merge.sh',\ndiff --git a/t/t3450-history.sh b/t/t3450-history.sh\nnew file mode 100755\nindex 0000000000..417c343d43\n--- /dev/null\n+++ b/t/t3450-history.sh\n@@ -0,0 +1,17 @@\n+#!/bin/sh\n+\n+test_description='tests for git-history command'\n+\n+. ./test-lib.sh\n+\n+test_expect_success 'does nothing without any arguments' '\n+\tgit history >out 2>&1 &&\n+\ttest_must_be_empty out\n+'\n+\n+test_expect_success 'raises an error with unknown argument' '\n+\ttest_must_fail git history garbage 2>err &&\n+\ttest_grep \"unrecognized argument: garbage\" err\n+'\n+\n+test_done\n\n-- \n2.52.0.590.g1f87b77810.dirty\n\n"},{"id":"533725","messageId":"20260113-b4-pks-history-builtin-v11-8-e74ebfa2652d@pks.im","threadId":"63986","inReplyTo":"20260113-b4-pks-history-builtin-v11-0-e74ebfa2652d@pks.im","subject":"[PATCH v11 8/8] builtin/history: implement \"reword\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-01-13T09:54:39Z","receivedAt":"2026-01-13T09:55:01Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Implement a new \"reword\" subcommand for git-history(1). This subcommand\nis similar to the user performing an interactive rebase with a single\ncommit changed to use the \"reword\" instruction.\n\nThe \"reword\" subcommand is built on top of the replay subsystem\ninstead of the sequencer. This leads to some major differences compared\nto git-rebase(1):\n\n  - We do not check out the commit that is to be reworded and instead\n    perform the operation in-memory. This has the obvious benefit of\n    being significantly faster compared to git-rebase(1), but even more\n    importantly it allows the user to rewrite history even if there are\n    local changes in the working tree or in the index.\n\n  - We do not execute any hooks, even though we leave some room for\n    changing this in the future.\n\n  - By default, all local branches that contain the commit will be\n    rewritten. This especially helps with workflows that use stacked\n    branches.\n\nHelped-by: Elijah Newren <newren@gmail.com>\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/git-history.adoc |  20 +-\n builtin/history.c              | 415 ++++++++++++++++++++++++++++++++++++++++-\n t/meson.build                  |   1 +\n t/t3450-history.sh             |   6 +-\n t/t3451-history-reword.sh      | 391 ++++++++++++++++++++++++++++++++++++++\n 5 files changed, 823 insertions(+), 10 deletions(-)\n\ndiff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc\nindex 68c35f36b9..154e262b76 100644\n--- a/Documentation/git-history.adoc\n+++ b/Documentation/git-history.adoc\n@@ -8,7 +8,7 @@ git-history - EXPERIMENTAL: Rewrite history\n SYNOPSIS\n --------\n [synopsis]\n-git history [<options>]\n+git history reword <commit> [--ref-action=(branches|head|print)]\n \n DESCRIPTION\n -----------\n@@ -50,7 +50,23 @@ first-class conflicts.\n COMMANDS\n --------\n \n-No commands are supported yet.\n+The following commands are available to rewrite history in different ways:\n+\n+`reword <commit>`::\n+\tRewrite the commit message of the specified commit. All the other\n+\tdetails of this commit remain unchanged. This command will spawn an\n+\teditor with the current message of that commit.\n+\n+OPTIONS\n+-------\n+\n+`--ref-action=(branches|head|print)`::\n+\tControl which references will be updated by the command, if any. With\n+\t`branches`, all local branches that point to commits which are\n+\tdescendants of the original commit will be rewritten. With `head`, only\n+\tthe current `HEAD` reference will be rewritten. With `print`, all\n+\tupdates as they would be performed with `branches` are printed in a\n+\tformat that can be consumed by linkgit:git-update-ref[1].\n \n GIT\n ---\ndiff --git a/builtin/history.c b/builtin/history.c\nindex f6fe32610b..8dcb9a6046 100644\n--- a/builtin/history.c\n+++ b/builtin/history.c\n@@ -1,22 +1,427 @@\n+#define USE_THE_REPOSITORY_VARIABLE\n+\n #include \"builtin.h\"\n+#include \"commit.h\"\n+#include \"commit-reach.h\"\n+#include \"config.h\"\n+#include \"editor.h\"\n+#include \"environment.h\"\n #include \"gettext.h\"\n+#include \"hex.h\"\n #include \"parse-options.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 \"wt-status.h\"\n+\n+#define GIT_HISTORY_REWORD_USAGE \\\n+\tN_(\"git history reword <commit> [--ref-action=(branches|head|print)]\")\n+\n+static void change_data_free(void *util, const char *str UNUSED)\n+{\n+\tstruct wt_status_change_data *d = util;\n+\tfree(d->rename_source);\n+\tfree(d);\n+}\n+\n+static int fill_commit_message(struct repository *repo,\n+\t\t\t       const struct object_id *old_tree,\n+\t\t\t       const struct object_id *new_tree,\n+\t\t\t       const char *default_message,\n+\t\t\t       const char *action,\n+\t\t\t       struct strbuf *out)\n+{\n+\tconst char *path = git_path_commit_editmsg();\n+\tconst char *hint =\n+\t\t_(\"Please enter the commit message for the %s changes.\"\n+\t\t  \" Lines starting\\nwith '%s' will be ignored, and an\"\n+\t\t  \" empty message aborts the commit.\\n\");\n+\tstruct wt_status s;\n+\n+\tstrbuf_addstr(out, default_message);\n+\tstrbuf_addch(out, '\\n');\n+\tstrbuf_commented_addf(out, comment_line_str, hint, action, comment_line_str);\n+\twrite_file_buf(path, out->buf, out->len);\n+\n+\twt_status_prepare(repo, &s);\n+\tFREE_AND_NULL(s.branch);\n+\ts.ahead_behind_flags = AHEAD_BEHIND_QUICK;\n+\ts.commit_template = 1;\n+\ts.colopts = 0;\n+\ts.display_comment_prefix = 1;\n+\ts.hints = 0;\n+\ts.use_color = 0;\n+\ts.whence = FROM_COMMIT;\n+\ts.committable = 1;\n+\n+\ts.fp = fopen(git_path_commit_editmsg(), \"a\");\n+\tif (!s.fp)\n+\t\treturn error_errno(_(\"could not open '%s'\"), git_path_commit_editmsg());\n+\n+\twt_status_collect_changes_trees(&s, old_tree, new_tree);\n+\twt_status_print(&s);\n+\twt_status_collect_free_buffers(&s);\n+\tstring_list_clear_func(&s.change, change_data_free);\n+\n+\tstrbuf_reset(out);\n+\tif (launch_editor(path, out, NULL)) {\n+\t\tfprintf(stderr, _(\"Aborting commit as launching the editor failed.\\n\"));\n+\t\treturn -1;\n+\t}\n+\tstrbuf_stripspace(out, comment_line_str);\n+\n+\tcleanup_message(out, COMMIT_MSG_CLEANUP_ALL, 0);\n+\n+\tif (!out->len) {\n+\t\tfprintf(stderr, _(\"Aborting commit due to empty commit message.\\n\"));\n+\t\treturn -1;\n+\t}\n+\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+{\n+\tconst char *exclude_gpgsig[] = {\n+\t\t/* We reencode the message, so the encoding needs to be stripped. */\n+\t\t\"encoding\",\n+\t\t/* We need to strip signatures as those will become invalid. */\n+\t\t\"gpgsig\",\n+\t\t\"gpgsig-sha256\",\n+\t\tNULL,\n+\t};\n+\tconst char *original_message, *original_body, *ptr;\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+\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+\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+\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+\t\t\t\t   NULL, NULL, original_extra_headers);\n+\tif (ret < 0)\n+\t\tgoto out;\n+\n+\t*out = lookup_commit_or_die(&rewritten_commit_oid, \"rewritten commit\");\n+\n+out:\n+\tfree_commit_extra_headers(original_extra_headers);\n+\tstrbuf_release(&commit_message);\n+\tfree(original_author);\n+\treturn ret;\n+}\n+\n+enum ref_action {\n+\tREF_ACTION_DEFAULT,\n+\tREF_ACTION_BRANCHES,\n+\tREF_ACTION_HEAD,\n+\tREF_ACTION_PRINT,\n+};\n+\n+static int parse_ref_action(const struct option *opt, const char *value, int unset)\n+{\n+\tenum ref_action *action = opt->value;\n+\n+\tBUG_ON_OPT_NEG_NOARG(unset, value);\n+\tif (!strcmp(value, \"branches\")) {\n+\t\t*action = REF_ACTION_BRANCHES;\n+\t} else if (!strcmp(value, \"head\")) {\n+\t\t*action = REF_ACTION_HEAD;\n+\t} else if (!strcmp(value, \"print\")) {\n+\t\t*action = REF_ACTION_PRINT;\n+\t} else {\n+\t\treturn error(_(\"%s expects one of 'branches', 'head' or 'print'\"),\n+\t\t\t     opt->long_name);\n+\t}\n+\n+\treturn 0;\n+}\n+\n+static int handle_reference_updates(enum ref_action action,\n+\t\t\t\t    struct repository *repo,\n+\t\t\t\t    struct commit *original,\n+\t\t\t\t    struct commit *rewritten,\n+\t\t\t\t    const char *reflog_msg)\n+{\n+\tconst struct name_decoration *decoration;\n+\tstruct replay_revisions_options opts = { 0 };\n+\tstruct replay_result result = { 0 };\n+\tstruct ref_transaction *transaction = NULL;\n+\tstruct strvec args = STRVEC_INIT;\n+\tstruct strbuf err = STRBUF_INIT;\n+\tstruct commit *head = NULL;\n+\tstruct rev_info revs;\n+\tchar hex[GIT_MAX_HEXSZ + 1];\n+\tbool detached_head;\n+\tint head_flags = 0;\n+\tint ret;\n+\n+\trefs_read_ref_full(get_main_ref_store(repo), \"HEAD\",\n+\t\t\t   RESOLVE_REF_NO_RECURSE, NULL, &head_flags);\n+\tdetached_head = !(head_flags & REF_ISSYMREF);\n+\n+\trepo_init_revisions(repo, &revs, NULL);\n+\tstrvec_push(&args, \"ignored\");\n+\tstrvec_push(&args, \"--reverse\");\n+\tstrvec_push(&args, \"--topo-order\");\n+\tstrvec_push(&args, \"--full-history\");\n+\n+\t/* We only want to see commits that are descendants of the old commit. */\n+\tstrvec_pushf(&args, \"--ancestry-path=%s\",\n+\t\t     oid_to_hex(&original->object.oid));\n+\n+\t/*\n+\t * Ancestry path may also show ancestors of the old commit, but we\n+\t * don't want to see those, either.\n+\t */\n+\tstrvec_pushf(&args, \"^%s\", oid_to_hex(&original->object.oid));\n+\n+\t/*\n+\t * When we're asked to update HEAD we need to verify that the commit\n+\t * that we want to rewrite is actually an ancestor of it and, if so,\n+\t * update it. Otherwise we'll update (or print) all descendant\n+\t * branches.\n+\t */\n+\tif (action == REF_ACTION_HEAD) {\n+\t\tstruct commit_list *from_list = NULL;\n+\n+\t\thead = lookup_commit_reference_by_name(\"HEAD\");\n+\t\tif (!head) {\n+\t\t\tret = error(_(\"cannot look up HEAD\"));\n+\t\t\tgoto out;\n+\t\t}\n+\n+\t\tcommit_list_insert(original, &from_list);\n+\t\tret = repo_is_descendant_of(repo, head, from_list);\n+\t\tfree_commit_list(from_list);\n+\n+\t\tif (ret < 0) {\n+\t\t\tret = error(_(\"cannot determine descendance\"));\n+\t\t\tgoto out;\n+\t\t} else if (!ret) {\n+\t\t\tret = error(_(\"rewritten commit must be an ancestor \"\n+\t\t\t\t      \"of HEAD when using --ref-action=head\"));\n+\t\t\tgoto out;\n+\t\t}\n+\n+\t\tstrvec_push(&args, \"HEAD\");\n+\t} else {\n+\t\tstrvec_push(&args, \"--branches\");\n+\t\tstrvec_push(&args, \"HEAD\");\n+\t}\n+\n+\tsetup_revisions_from_strvec(&args, &revs, NULL);\n+\tif (args.nr != 1)\n+\t\tBUG(\"revisions were set up with invalid argument\");\n+\n+\topts.onto = oid_to_hex_r(hex, &rewritten->object.oid);\n+\n+\tret = replay_revisions(&revs, &opts, &result);\n+\tif (ret)\n+\t\tgoto out;\n+\n+\tswitch (action) {\n+\tcase REF_ACTION_BRANCHES:\n+\tcase REF_ACTION_HEAD:\n+\t\ttransaction = ref_store_transaction_begin(get_main_ref_store(repo), 0, &err);\n+\t\tif (!transaction) {\n+\t\t\tret = error(_(\"failed to begin ref transaction: %s\"), err.buf);\n+\t\t\tgoto out;\n+\t\t}\n+\n+\t\tfor (size_t i = 0; i < result.updates_nr; i++) {\n+\t\t\tret = ref_transaction_update(transaction,\n+\t\t\t\t\t\t     result.updates[i].refname,\n+\t\t\t\t\t\t     &result.updates[i].new_oid,\n+\t\t\t\t\t\t     &result.updates[i].old_oid,\n+\t\t\t\t\t\t     NULL, NULL, 0, reflog_msg, &err);\n+\t\t\tif (ret) {\n+\t\t\t\tret = error(_(\"failed to update ref '%s': %s\"),\n+\t\t\t\t\t    result.updates[i].refname, err.buf);\n+\t\t\t\tgoto out;\n+\t\t\t}\n+\t\t}\n+\n+\t\t/*\n+\t\t * `replay_revisions()` only updates references that are\n+\t\t * ancestors of `rewritten`, so we need to manually\n+\t\t * handle updating references that point to `original`.\n+\t\t */\n+\t\tfor (decoration = get_name_decoration(&original->object);\n+\t\t     decoration;\n+\t\t     decoration = decoration->next)\n+\t\t{\n+\t\t\tif (decoration->type != DECORATION_REF_LOCAL &&\n+\t\t\t    decoration->type != DECORATION_REF_HEAD)\n+\t\t\t\tcontinue;\n+\n+\t\t\tif (action == REF_ACTION_HEAD &&\n+\t\t\t    decoration->type != DECORATION_REF_HEAD)\n+\t\t\t\tcontinue;\n+\n+\t\t\t/*\n+\t\t\t * We only need to update HEAD separately in case it's\n+\t\t\t * detached. If it's not we'd already update the branch\n+\t\t\t * it is pointing to.\n+\t\t\t */\n+\t\t\tif (action == REF_ACTION_BRANCHES &&\n+\t\t\t    decoration->type == DECORATION_REF_HEAD &&\n+\t\t\t    !detached_head)\n+\t\t\t\tcontinue;\n+\n+\t\t\tret = ref_transaction_update(transaction,\n+\t\t\t\t\t\t     decoration->name,\n+\t\t\t\t\t\t     &rewritten->object.oid,\n+\t\t\t\t\t\t     &original->object.oid,\n+\t\t\t\t\t\t     NULL, NULL, 0, reflog_msg, &err);\n+\t\t\tif (ret) {\n+\t\t\t\tret = error(_(\"failed to update ref '%s': %s\"),\n+\t\t\t\t\t    decoration->name, err.buf);\n+\t\t\t\tgoto out;\n+\t\t\t}\n+\t\t}\n+\n+\t\tif (ref_transaction_commit(transaction, &err)) {\n+\t\t\tret = error(_(\"failed to commit ref transaction: %s\"), err.buf);\n+\t\t\tgoto out;\n+\t\t}\n+\n+\t\tbreak;\n+\tcase REF_ACTION_PRINT:\n+\t\tfor (size_t i = 0; i < result.updates_nr; i++)\n+\t\t\tprintf(\"update %s %s %s\\n\",\n+\t\t\t       result.updates[i].refname,\n+\t\t\t       oid_to_hex(&result.updates[i].new_oid),\n+\t\t\t       oid_to_hex(&result.updates[i].old_oid));\n+\t\tbreak;\n+\tdefault:\n+\t\tBUG(\"unsupported ref action %d\", action);\n+\t}\n+\n+\tret = 0;\n+\n+out:\n+\tref_transaction_free(transaction);\n+\treplay_result_release(&result);\n+\trelease_revisions(&revs);\n+\tstrbuf_release(&err);\n+\tstrvec_clear(&args);\n+\treturn ret;\n+}\n+\n+static int cmd_history_reword(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_REWORD_USAGE,\n+\t\tNULL,\n+\t};\n+\tenum ref_action action = REF_ACTION_DEFAULT;\n+\tstruct option options[] = {\n+\t\tOPT_CALLBACK_F(0, \"ref-action\", &action, N_(\"<action>\"),\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_END(),\n+\t};\n+\tstruct strbuf reflog_msg = STRBUF_INIT;\n+\tstruct commit *original, *rewritten;\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 single revision\"));\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+\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 = commit_tree_with_edited_message(repo, \"reworded\", original, &rewritten);\n+\tif (ret < 0) {\n+\t\tret = error(_(\"failed writing reworded commit\"));\n+\t\tgoto out;\n+\t}\n+\n+\tstrbuf_addf(&reflog_msg, \"reword: updating %s\", argv[0]);\n+\n+\tret = handle_reference_updates(action, repo, original, rewritten,\n+\t\t\t\t       reflog_msg.buf);\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+\treturn ret;\n+}\n \n int cmd_history(int argc,\n \t\tconst char **argv,\n \t\tconst char *prefix,\n-\t\tstruct repository *repo UNUSED)\n+\t\tstruct repository *repo)\n {\n \tconst char * const usage[] = {\n-\t\tN_(\"git history [<options>]\"),\n+\t\tGIT_HISTORY_REWORD_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_END(),\n \t};\n \n \targc = parse_options(argc, argv, prefix, options, usage, 0);\n-\tif (argc)\n-\t\tusagef(\"unrecognized argument: %s\", argv[0]);\n-\treturn 0;\n+\treturn fn(argc, argv, prefix, repo);\n }\ndiff --git a/t/meson.build b/t/meson.build\nindex 73006b095a..c9f92450dc 100644\n--- a/t/meson.build\n+++ b/t/meson.build\n@@ -388,6 +388,7 @@ integration_tests = [\n   't3437-rebase-fixup-options.sh',\n   't3438-rebase-broken-files.sh',\n   't3450-history.sh',\n+  't3451-history-reword.sh',\n   't3500-cherry.sh',\n   't3501-revert-cherry-pick.sh',\n   't3502-cherry-pick-merge.sh',\ndiff --git a/t/t3450-history.sh b/t/t3450-history.sh\nindex 417c343d43..f513463b92 100755\n--- a/t/t3450-history.sh\n+++ b/t/t3450-history.sh\n@@ -5,13 +5,13 @@ test_description='tests for git-history command'\n . ./test-lib.sh\n \n test_expect_success 'does nothing without any arguments' '\n-\tgit history >out 2>&1 &&\n-\ttest_must_be_empty out\n+\ttest_must_fail git history 2>err &&\n+\ttest_grep \"need a subcommand\" err\n '\n \n test_expect_success 'raises an error with unknown argument' '\n \ttest_must_fail git history garbage 2>err &&\n-\ttest_grep \"unrecognized argument: garbage\" err\n+\ttest_grep \"unknown subcommand: .garbage.\" err\n '\n \n test_done\ndiff --git a/t/t3451-history-reword.sh b/t/t3451-history-reword.sh\nnew file mode 100755\nindex 0000000000..3594421b68\n--- /dev/null\n+++ b/t/t3451-history-reword.sh\n@@ -0,0 +1,391 @@\n+#!/bin/sh\n+\n+test_description='tests for git-history reword subcommand'\n+\n+. ./test-lib.sh\n+. \"$TEST_DIRECTORY/lib-log-graph.sh\"\n+\n+reword_with_message () {\n+\tcat >message &&\n+\twrite_script fake-editor.sh <<-\\EOF &&\n+\tcp message \"$1\"\n+\tEOF\n+\ttest_set_editor \"$(pwd)\"/fake-editor.sh &&\n+\tgit history reword \"$@\" &&\n+\trm fake-editor.sh message\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+test_expect_success 'can reword tip of a branch' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit first &&\n+\t\ttest_commit second &&\n+\t\ttest_commit third &&\n+\n+\t\tgit symbolic-ref HEAD >expect &&\n+\t\treword_with_message HEAD <<-EOF &&\n+\t\tthird reworded\n+\t\tEOF\n+\t\tgit symbolic-ref HEAD >actual &&\n+\t\ttest_cmp expect actual &&\n+\n+\t\texpect_log <<-\\EOF &&\n+\t\tthird reworded\n+\t\tsecond\n+\t\tfirst\n+\t\tEOF\n+\n+\t\tgit reflog >reflog &&\n+\t\ttest_grep \"reword: updating HEAD\" reflog\n+\t)\n+'\n+\n+test_expect_success 'can reword commit in the middle' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit first &&\n+\t\ttest_commit second &&\n+\t\ttest_commit third &&\n+\n+\t\tgit symbolic-ref HEAD >expect &&\n+\t\treword_with_message HEAD~ <<-EOF &&\n+\t\tsecond reworded\n+\t\tEOF\n+\t\tgit symbolic-ref HEAD >actual &&\n+\t\ttest_cmp expect actual &&\n+\n+\t\texpect_log <<-\\EOF\n+\t\tthird\n+\t\tsecond reworded\n+\t\tfirst\n+\t\tEOF\n+\t)\n+'\n+\n+test_expect_success 'can reword commit in the middle even on detached head' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit first &&\n+\t\ttest_commit second &&\n+\t\ttest_commit third_on_main &&\n+\t\tgit checkout --detach HEAD^ &&\n+\t\ttest_commit third_on_head &&\n+\n+\t\treword_with_message HEAD~ <<-EOF &&\n+\t\tsecond reworded\n+\t\tEOF\n+\n+\t\texpect_graph HEAD --branches <<-\\EOF\n+\t\t* third_on_head\n+\t\t| * third_on_main\n+\t\t|/\n+\t\t* second reworded\n+\t\t* first\n+\t\tEOF\n+       )\n+'\n+\n+test_expect_success 'can reword the detached head' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit first &&\n+\t\ttest_commit second &&\n+\t\tgit checkout --detach HEAD &&\n+\t\ttest_commit third &&\n+\n+\t\treword_with_message HEAD <<-EOF &&\n+\t\tthird reworded\n+\t\tEOF\n+\n+\t\texpect_log <<-\\EOF\n+\t\tthird reworded\n+\t\tsecond\n+\t\tfirst\n+\t\tEOF\n+\t)\n+'\n+\n+test_expect_success 'can reword root commit' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit first &&\n+\t\ttest_commit second &&\n+\t\ttest_commit third &&\n+\t\treword_with_message HEAD~2 <<-EOF &&\n+\t\tfirst reworded\n+\t\tEOF\n+\n+\t\texpect_log <<-\\EOF\n+\t\tthird\n+\t\tsecond\n+\t\tfirst reworded\n+\t\tEOF\n+\t)\n+'\n+\n+test_expect_success 'can reword in a bare repo' '\n+\ttest_when_finished \"rm -rf repo repo.git\" &&\n+\tgit init repo &&\n+\ttest_commit -C repo first &&\n+\tgit clone --bare repo repo.git &&\n+\t(\n+\t\tcd repo.git &&\n+\t\treword_with_message HEAD <<-EOF &&\n+\t\treworded\n+\t\tEOF\n+\n+\t\texpect_log <<-\\EOF\n+\t\treworded\n+\t\tEOF\n+\t)\n+'\n+\n+test_expect_success 'can reword a commit on a different 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 theirs &&\n+\t\ttest_commit ours &&\n+\t\tgit switch theirs &&\n+\t\ttest_commit theirs &&\n+\n+\t\tgit rev-parse ours >ours-before &&\n+\t\treword_with_message theirs <<-EOF &&\n+\t\tReworded theirs\n+\t\tEOF\n+\t\tgit rev-parse ours >ours-after &&\n+\t\ttest_cmp ours-before ours-after &&\n+\n+\t\texpect_graph --branches <<-\\EOF\n+\t\t* Reworded theirs\n+\t\t| * ours\n+\t\t|/\n+\t\t* base\n+\t\tEOF\n+\t)\n+'\n+\n+test_expect_success 'can reword a merge 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\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+\n+\t\t# It is not possible to replay merge commits embedded in the\n+\t\t# history (yet).\n+\t\ttest_must_fail git history reword HEAD~ 2>err &&\n+\t\ttest_grep \"replaying merge commits is not supported yet\" err &&\n+\n+\t\t# But it is possible to reword a merge commit directly.\n+\t\treword_with_message HEAD <<-EOF &&\n+\t\tReworded merge commit\n+\t\tEOF\n+\t\texpect_graph <<-\\EOF\n+\t\t*   Reworded merge commit\n+\t\t|\\\n+\t\t| * theirs\n+\t\t* | ours\n+\t\t|/\n+\t\t* base\n+\t\tEOF\n+\t)\n+'\n+\n+test_expect_success '--ref-action=print prints ref updates without modifying repo' '\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\tgit branch branch &&\n+\t\ttest_commit ours &&\n+\t\tgit switch branch &&\n+\t\ttest_commit theirs &&\n+\n+\t\tgit refs list >refs-expect &&\n+\t\treword_with_message --ref-action=print base >updates <<-\\EOF &&\n+\t\treworded commit\n+\t\tEOF\n+\t\tgit refs list >refs-actual &&\n+\t\ttest_cmp refs-expect refs-actual &&\n+\n+\t\ttest_grep \"update refs/heads/branch\" updates &&\n+\t\ttest_grep \"update refs/heads/main\" updates &&\n+\t\tgit update-ref --stdin <updates &&\n+\t\texpect_log --branches <<-\\EOF\n+\t\ttheirs\n+\t\tours\n+\t\treworded commit\n+\t\tEOF\n+\t)\n+'\n+\n+test_expect_success '--ref-action=head updates only HEAD' '\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\tgit branch branch &&\n+\t\ttest_commit theirs &&\n+\t\tgit switch branch &&\n+\t\ttest_commit ours &&\n+\n+\t\t# When told to update HEAD, only, the command will refuse to\n+\t\t# rewrite commits that are not an ancestor of HEAD.\n+\t\ttest_must_fail git history reword --ref-action=head theirs 2>err &&\n+\t\ttest_grep \"rewritten commit must be an ancestor of HEAD\" err &&\n+\n+\t\treword_with_message --ref-action=head base >updates <<-\\EOF &&\n+\t\treworded base\n+\t\tEOF\n+\t\texpect_log HEAD <<-\\EOF &&\n+\t\tours\n+\t\treworded base\n+\t\tEOF\n+\t\texpect_log main <<-\\EOF\n+\t\ttheirs\n+\t\tbase\n+\t\tEOF\n+\t)\n+'\n+\n+test_expect_success 'editor shows proper status' '\n+\ttest_when_finished \"rm -rf repo\" &&\n+\tgit init repo &&\n+\t(\n+\t\tcd repo &&\n+\t\ttest_commit first &&\n+\n+\t\twrite_script fake-editor.sh <<-\\EOF &&\n+\t\tcp \"$1\" . &&\n+\t\tprintf \"\\namend a comment\\n\" >>\"$1\"\n+\t\tEOF\n+\t\ttest_set_editor \"$(pwd)\"/fake-editor.sh &&\n+\t\tgit history reword HEAD &&\n+\n+\t\tcat >expect <<-EOF &&\n+\t\tfirst\n+\n+\t\t# Please enter the commit message for the reworded 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:   first.t\n+\t\t#\n+\t\tEOF\n+\t\ttest_cmp expect COMMIT_EDITMSG &&\n+\n+\t\ttest_commit_message HEAD <<-\\EOF\n+\t\tfirst\n+\n+\t\tamend a comment\n+\t\tEOF\n+\t)\n+'\n+\n+# For now, git-history(1) does not yet execute any hooks. This is subject to\n+# change in the future, and if it does this test here is expected to start\n+# failing. In other words, this test is not an endorsement of the current\n+# status quo.\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\ttest_commit first &&\n+\t\ttest_commit second &&\n+\t\ttest_commit third &&\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\treword_with_message HEAD~ <<-EOF &&\n+\t\tsecond reworded\n+\t\tEOF\n+\n+\t\tcat >expect <<-EOF &&\n+\t\tthird\n+\t\tsecond reworded\n+\t\tfirst\n+\t\tEOF\n+\t\tgit log --format=%s >actual &&\n+\t\ttest_cmp expect actual &&\n+\n+\t\ttest_path_is_missing hooks.log\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\ttest_commit first &&\n+\n+\t\t! reword_with_message HEAD 2>err </dev/null &&\n+\t\ttest_grep \"Aborting commit due to empty commit message.\" 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\ttouch a b &&\n+\t\tgit add . &&\n+\t\tgit commit -m \"initial commit\" &&\n+\t\techo foo >a &&\n+\t\techo bar >b &&\n+\t\tgit add b &&\n+\t\treword_with_message HEAD <<-EOF &&\n+\t\tmessage\n+\t\tEOF\n+\t\tcat >expect <<-\\EOF &&\n+\t\t M a\n+\t\tM  b\n+\t\t?? actual\n+\t\t?? expect\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.52.0.590.g1f87b77810.dirty\n\n"},{"id":"534011","messageId":"CABPp-BFbUEGqNAeUtUghLd3oKcZiD88P04AYuTtY4T01F1rzdQ@mail.gmail.com","threadId":"63986","inReplyTo":"20260113-b4-pks-history-builtin-v11-0-e74ebfa2652d@pks.im","subject":"Re: [PATCH v11 0/8] Introduce git-history(1) command for easy history editing","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2026-01-16T06:43:04Z","receivedAt":"2026-01-16T06:43:17Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Tue, Jan 13, 2026 at 1:54 AM Patrick Steinhardt <ps@pks.im> wrote:\n[...]\n> Changes in v11:\n>   - Fix overly clever `BUG()` condition.\n>   - Drop the `struct replay_result::merge_conflict` field.\n>   - Return a positive value from `replay_revisions()` in case there was\n>     a conflict.\n>   - Improve a commit message.\n>   - Fix check for whether `setup_revisions_from_strvec()` was\n>     successful.\n>   - Link to v10: https://lore.kernel.org/r/20260112-b4-pks-history-builtin-v10-0-e3c6aa5b4cec@pks.im\n\nThis version looks good to merge down.\n"},{"id":"534045","messageId":"xmqqy0lxa8i6.fsf@gitster.g","threadId":"63986","inReplyTo":"CABPp-BFbUEGqNAeUtUghLd3oKcZiD88P04AYuTtY4T01F1rzdQ@mail.gmail.com","subject":"Re: [PATCH v11 0/8] Introduce git-history(1) command for easy history editing","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-01-16T15:21:21Z","receivedAt":"2026-01-16T15:21:25Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Elijah Newren <newren@gmail.com> writes:\n\n> On Tue, Jan 13, 2026 at 1:54 AM Patrick Steinhardt <ps@pks.im> wrote:\n> [...]\n>> Changes in v11:\n>>   - Fix overly clever `BUG()` condition.\n>>   - Drop the `struct replay_result::merge_conflict` field.\n>>   - Return a positive value from `replay_revisions()` in case there was\n>>     a conflict.\n>>   - Improve a commit message.\n>>   - Fix check for whether `setup_revisions_from_strvec()` was\n>>     successful.\n>>   - Link to v10: https://lore.kernel.org/r/20260112-b4-pks-history-builtin-v10-0-e3c6aa5b4cec@pks.im\n>\n> This version looks good to merge down.\n\nThanks.\n"},{"id":"534051","messageId":"aWpnFqTmWB9XIWUW@szeder.dev","threadId":"63986","inReplyTo":"20260113-b4-pks-history-builtin-v11-8-e74ebfa2652d@pks.im","subject":"Re: [PATCH v11 8/8] builtin/history: implement \"reword\" subcommand","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2026-01-16T16:28:06Z","receivedAt":"2026-01-16T16:28:09Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"On Tue, Jan 13, 2026 at 10:54:39AM +0100, Patrick Steinhardt wrote:\n> Implement a new \"reword\" subcommand for git-history(1). This subcommand\n> is similar to the user performing an interactive rebase with a single\n> commit changed to use the \"reword\" instruction.\n> \n> The \"reword\" subcommand is built on top of the replay subsystem\n> instead of the sequencer. This leads to some major differences compared\n> to git-rebase(1):\n> \n>   - We do not check out the commit that is to be reworded and instead\n>     perform the operation in-memory. This has the obvious benefit of\n>     being significantly faster compared to git-rebase(1), but even more\n>     importantly it allows the user to rewrite history even if there are\n>     local changes in the working tree or in the index.\n\nIn an earlier round I pointed out some of the differences between the\n'reword' instruction of 'git rebase' and 'git history rebase',\nincluding some drawbacks of the latter.  It's disheartening to see\nthat you only picked those differences that are in favor of your 'git\nhistory' command, but neglected its drawbacks.\n\nPlease strive for less biased and more objective commit messages.\n\n>   - We do not execute any hooks, even though we leave some room for\n>     changing this in the future.\n> \n>   - By default, all local branches that contain the commit will be\n>     rewritten. This especially helps with workflows that use stacked\n>     branches.\n\nPlease don't just state that all local branches containing the\nmodified commit are rewritten, but justify why it behaves that way.\n\nGit's porcelain commands operate on the current branch, unless the\nuser specifies a different branch or an option like '--all' or\n'--branches'.  The default chosen here is inconsistent with the rest\nof Git.\n\nThis is a bad default for any future subcommands implementing common\nhistory rewriting operations that can cause conflicts.\n\nUsers must remember to specify a non-default '--ref-action' if they\ndon't want this behavior.  If they forget to do so and don't notice\nit, the old commits will be gc-ed away.  Therefore, I consider this\nto be a dangerous default that can lead to data loss.\n\nI firmly believe that operating on all local branches must always be\nthe result of an explicit user action.\n\n\n"},{"id":"534052","messageId":"aWpoMcgwsfRCCrr4@szeder.dev","threadId":"63986","inReplyTo":"xmqqy0lxa8i6.fsf@gitster.g","subject":"Re: [PATCH v11 0/8] Introduce git-history(1) command for easy history editing","fromName":"SZEDER Gábor","fromEmail":"szeder.dev@gmail.com","sentAt":"2026-01-16T16:32:49Z","receivedAt":"2026-01-16T16:32:52Z","isPatch":true,"sender":{"key":"szeder.dev@gmail.com","avatar":"https://avatars.githubusercontent.com/u/116324?v=4"},"body":"On Fri, Jan 16, 2026 at 07:21:21AM -0800, Junio C Hamano wrote:\n> Elijah Newren <newren@gmail.com> writes:\n> \n> > On Tue, Jan 13, 2026 at 1:54 AM Patrick Steinhardt <ps@pks.im> wrote:\n> > [...]\n> >> Changes in v11:\n> >>   - Fix overly clever `BUG()` condition.\n> >>   - Drop the `struct replay_result::merge_conflict` field.\n> >>   - Return a positive value from `replay_revisions()` in case there was\n> >>     a conflict.\n> >>   - Improve a commit message.\n> >>   - Fix check for whether `setup_revisions_from_strvec()` was\n> >>     successful.\n> >>   - Link to v10: https://lore.kernel.org/r/20260112-b4-pks-history-builtin-v10-0-e3c6aa5b4cec@pks.im\n> >\n> > This version looks good to merge down.\n> \n> Thanks.\n\nFWIW, I think it's far from ready to be merged.\n\n"},{"id":"534130","messageId":"CABPp-BHkNLdH4C7U4sFoVhrsSPH8KAaDtOdLEQGyajmXZz9hVg@mail.gmail.com","threadId":"63986","inReplyTo":"aWpnFqTmWB9XIWUW@szeder.dev","subject":"Re: [PATCH v11 8/8] builtin/history: implement \"reword\" subcommand","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2026-01-17T22:56:19Z","receivedAt":"2026-01-17T22:56:32Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Fri, Jan 16, 2026 at 8:28 AM SZEDER Gábor <szeder.dev@gmail.com> wrote:\n>\n> On Tue, Jan 13, 2026 at 10:54:39AM +0100, Patrick Steinhardt wrote:\n> > Implement a new \"reword\" subcommand for git-history(1). This subcommand\n> > is similar to the user performing an interactive rebase with a single\n> > commit changed to use the \"reword\" instruction.\n> >\n> > The \"reword\" subcommand is built on top of the replay subsystem\n> > instead of the sequencer. This leads to some major differences compared\n> > to git-rebase(1):\n> >\n> >   - We do not check out the commit that is to be reworded and instead\n> >     perform the operation in-memory. This has the obvious benefit of\n> >     being significantly faster compared to git-rebase(1), but even more\n> >     importantly it allows the user to rewrite history even if there are\n> >     local changes in the working tree or in the index.\n>\n> In an earlier round I pointed out some of the differences between the\n> 'reword' instruction of 'git rebase' and 'git history rebase',\n> including some drawbacks of the latter.  It's disheartening to see\n> that you only picked those differences that are in favor of your 'git\n> history' command, but neglected its drawbacks.\n\nThat'd be https://lore.kernel.org/git/aSVpXPtrqa0cBsEm@szeder.dev/ ,\nright?  I see two things you commented on there.  Although I'm not\nPatrick, let me respond to each, in reverse order:\n\nPersonally, I'm having difficulty understanding your second stated\nadvantage of picking the commit from the rebase instruction sheet.\nThat instruction sheet is obtained by rebasing on top of something,\nand it seems as easy to me to see the commits since that point in a\ngit log command and pick your desired commit from log output as it is\nto invoke an interactive rebase on top of that base commit to get the\nrebase instruction sheet and then pick out your desired commit from\nthere.  Same number of operations and work either way, so I don't see\nhow either is more or less work than the other.\n\nYou did make a good point that one of the differences is that you\ndon't have the commit checked out.  That's a useful distinction to be\naware of.  To me, that seems to be somewhat implied already both by\n\"in-memory\" and \"even if there are local changes in the working tree\nor index\", though it wouldn't hurt to explicitly call it out.\n\n> Please strive for less biased and more objective commit messages.\n\nHe rewrote the commit message from v6 to v7\n(https://lore.kernel.org/git/20251027-b4-pks-history-builtin-v6-5-407dd3f57ad3@pks.im/\n-> https://lore.kernel.org/git/20251203-b4-pks-history-builtin-v7-5-9e9f849bfd0e@pks.im/),\nas far as I can tell precisely to respond to your feedback.  Perhaps\nhe didn't achieve what you wanted, but why jump to the conclusion of\n\"bias\" rather than that he missed conveying an important distinction\nas clearly as you wanted?\n\n> >   - We do not execute any hooks, even though we leave some room for\n> >     changing this in the future.\n> >\n> >   - By default, all local branches that contain the commit will be\n> >     rewritten. This especially helps with workflows that use stacked\n> >     branches.\n>\n> Please don't just state that all local branches containing the\n> modified commit are rewritten, but justify why it behaves that way.\n\nIt feels like each of your complaints with the new proposed commands\n(given commit not checked out, rebase instruction sheet vs listing\ncommit, and HEAD-only) can be boiled down to the fact that they don't\nbehave like `git rebase`.  Is that accurate?  If you like rebase, is\nthere a reason you are worried you can't just keep using it?  I don't\nsee why others should be required to implement another exact copy of\nrebase, though.  Further, if we only wanted minor modifications, we\ncould have just done those to git rebase.\n\nIf you aren't just arguing to match git rebase's behavior exactly, let\nme try to explain the all-descendant-branches thing from my angle.\n\nOne of the issues that I've long hated about git rebase is that if you\nhave multiple inter-dependent branches, it's a royal pain to rebase\nthem all.  You cannot rebase them independently, because that\ndisconnects the history by duplicating the shared portions so that you\nhave N copies of each of those.  And I couldn't see a way to fix that\ninside git rebase; its design basically ties you to a single branch.\n\nFurther, maybe it'd be useful to explore the different proposed\ndefaults a bit with the 'history reword' example.  Let's assume we\nused your preferred default (rewrite only a single branch) for a user\nwho didn't like that default.  If a user does a history reword, and\nlater realizes that their other branches didn't get updated, how do\nthey fix the others?  They might be inclined to loop over the other\nbranches and do a 'git history reword CommitZ' on each of them, but\nthen they'd get the nasty surprise of having N different reworded\nCommitZ's (even if they reworded identically), one per branch.\nAlternatively, if they are aware that they can't simply reuse the same\ncommand to replay the other branches, they'll then start asking\nquestions about how exactly to fix up all the other branches...and the\ncommand(s) they need to run is going to be a lot more complicated,\nespecially if they have since added additional commits on top of the\nactive branch that they don't want to undo and lose.  It feels like a\nhard recovery story.  In contrast, we can consider the case of the\ndefault being to update all branches for a user who doesn't want it.\nIf that user finds that other local branches were also rebased and\nthey didn't want them to be, they just go reset that branch or\nbranches from the reflog, which is pretty easy.\n\n> Git's porcelain commands operate on the current branch, unless the\n> user specifies a different branch or an option like '--all' or\n> '--branches'.  The default chosen here is inconsistent with the rest\n> of Git.\n\nThat's a good point that there are existing commands that default to\nthe current branch.  By my accounting, commands that operate on a\nrange of commits are:\n   \"view a range of commits\": log, and derivatives like rev-list\n   \"edit a range of commits\": fast-import, filter-branch, filter-repo,\nrebase, replay\n   \"copy a range of commits (or copy their inverse)\": cherry-pick, revert\n\nHopefully I didn't miss any from skimming over 'git help --all'; my\napologies if so.  Anyway, let's consider these commands, in reverse\norder:\n\nI'm not sure the \"copy a range of commits\" provide much of a precedent\nfor editing a range, since the replay equivalent was always envisioned\nto only update a single branch for those despite otherwise being\nenvisioned as a rewrite-all-branches-by-default thing.  But there's\nactually a unifying piece of logic that can lead you to the different\nbranch handling: we want to avoid having duplicates of commits\nsimultaneously in use and thus default to as few copies as possible.\nThat logic means that for \"copy a range\", we apply them to just one\nbranch.  The same logic for \"edit a range\", means to rebase all\ndescendants on any local branch.\n\nFor the \"edit a range of commits\", filter-branch did default to a\nsingle branch but is deeply deprecated and more or less warns you that\nthe whole tool was a design mistake.  The replacement (filter-repo) in\nits name makes it clear that one of the mistakes was single branch vs.\nall branches.  fast-import is also more of an all-branch thing, though\nit doesn't so much have a default.  git-rebase does have a\nsingle-branch precedent, but that was called out rather strongly in my\ndesign goals for its newer alternative, git-replay (\"Decapitate\nHEAD-centric assumptions\",\nhttps://lore.kernel.org/git/20230407072415.1360068-1-christian.couder@gmail.com/).\nIf I hadn't viewed the single-branch handling in rebase as a mistake,\nI probably would have never created git-replay and instead just\nincrementally improved git-rebase.  Since it was that mistake that\nlead to new tools in the first place, I'm not sure how much of a\nprecedent git-rebase should be considered to be setting here.\n\nFor \"viewing a range of commits\", git log does lend towards your\nargument; it could be seen as precedent setting in favor of\njust-the-current-branch.\n\nSo, the precedent seems to not be uniform among git commands, though I\nthink git log is strongly in your favor.  As a really rough measure of\nhow much that precedent matters, we can look at how folks have\ncommented on what they think the default should be so far in this\nwhole thread:\n\nRebase-all-descendant branches:\n  * Phillip (https://lore.kernel.org/git/91bd9241-96c1-4b34-98a9-af3bad345c4d@gmail.com/)\n  * Junio (https://lore.kernel.org/git/xmqqms3qh13e.fsf@gitster.g/)\n  * Kristoffer (https://lore.kernel.org/git/b3ddfaa4-526b-41e3-b12a-0fec846ac7bc@app.fastmail.com/)\n  * Ben (https://lore.kernel.org/git/3600D877-4999-4EE3-8C1C-893E12D35B6A@gmail.com/)\n  * me (this email, among others)\n  * Martin (https://lore.kernel.org/git/CANiSa6hxjghKQMhURx8qC2t=+1gEE7p8YaHbWkg3rYOYa=poVg@mail.gmail.com/)\n  * Patrick (\"Yup\" from\nhttps://lore.kernel.org/git/aKs3tqjE510MF0T-@pks.im/, plus this v11\nwe're responding to, though he did seem to vacillate over the course\nof the series)\n\nRebase-current-branch-only:\n  * You (your email that I'm responding to)\n\nDo not allow editing a commit shared by multiple branches:\n  * Matthias (https://lore.kernel.org/git/4m6rmefbv4hftclimitz5rp6yapswjtnjsxymrsdkuan4jbg3u@dm5jzdiq5cxz/)\n\n(Sorry if I missed any, I tried to find them all in the threads on\nthis topic.)\n\n> This is a bad default for any future subcommands implementing common\n> history rewriting operations that can cause conflicts.\n\nTotally disagree; we'd want rebase-all-descendant-local-branches for\nthose commands too.  In fact, that's precisely what I did with \"git\nreplay edit\".  (True, \"git replay edit\" was just a proof-of-concept\nbecause I didn't have conflict handling implemented, but it was very\nmuch an intentional default for a command known to need to deal with\nconflicts before being productionized).\n\nFurther, we have two semi-independent implementations of\nreplay-all-descendant-branches-by-default in the form of JJ and\nGitButler, with real world use (not just demos) and apparent consensus\nthat it not only works but was a good decision.\n\nSo, I'm a little unsure at how you arrived at this conclusion; do you\ncare to elucidate?\n\n> Users must remember to specify a non-default '--ref-action' if they\n> don't want this behavior.  If they forget to do so and don't notice\n> it, the old commits will be gc-ed away.  Therefore, I consider this\n> to be a dangerous default that can lead to data loss.\n\nYou elided over \"for a really long time so that the reflogs expire\",\nbut you bring up a good point.  I think the key here is \"don't notice\nit\".  With \"git replay edit\", I'd print notices about what was updated\nafter each operation (particularly important since the operation could\nbe a `git commit --amend` or `git reset HEAD~1` or whatever, which\ncauses commits which are the descendants of the one you are operating\non to be replayed).  To avoid the \"don't notice it\" issue, we could do\nthe same with Patrick's history command.  We could also prevent a\nforgotten --ref-action by allowing a config variable, which I'll cover\nmore below.\n\n> I firmly believe that operating on all local branches must always be\n> the result of an explicit user action.\n\nOver at https://lore.kernel.org/git/aUVaEPGoOkATQGl3@szeder.dev/ , you\nalternatively suggested the idea of an \"escape hatch\".  I think that\nmay be a good idea.  What if we had a \"history.scope\" config variable,\nwith values like \"descendant-branches\", \"current-branch\", and\n\"error-if-multiple-branches\", corresponding to each of the requested\ndefaults we've seen in response to this series?  I can't imagine using\nanything other than descendant-branches, and the preponderance of\nthose who have commented on the default so far seem to be in\nagreement, but it would allow you and Matthias and others like you two\nto pick an alternative.  Thoughts?\n"},{"id":"534148","messageId":"87ms2a3j4l.fsf@gitster.g","threadId":"63986","inReplyTo":"CABPp-BHkNLdH4C7U4sFoVhrsSPH8KAaDtOdLEQGyajmXZz9hVg@mail.gmail.com","subject":"Re: [PATCH v11 8/8] builtin/history: implement \"reword\" subcommand","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-01-18T17:50:34Z","receivedAt":"2026-01-18T17:50:39Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Elijah Newren <newren@gmail.com> writes:\n\n> ... that they don't want to undo and lose.  It feels like a\n> hard recovery story.  In contrast, we can consider the case of the\n> default being to update all branches for a user who doesn't want it.\n> If that user finds that other local branches were also rebased and\n> they didn't want them to be, they just go reset that branch or\n> branches from the reflog, which is pretty easy.\n\nVery nicely put.  I originally thought that the \"reflog is your\nfriend\" argument would apply in both direction, but leaving the tips\nof some branches behind while rewriting the tips of some other\nbranches, that can reach the tips of these left-bhind branches,\nwould mean we have no other commit to go back to.\n\nThanks.\n"},{"id":"534377","messageId":"xmqq4ioeu6un.fsf@gitster.g","threadId":"63986","inReplyTo":"aWpoMcgwsfRCCrr4@szeder.dev","subject":"Re: [PATCH v11 0/8] Introduce git-history(1) command for easy history editing","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-01-21T19:02:40Z","receivedAt":"2026-01-21T19:02:44Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"SZEDER Gábor <szeder.dev@gmail.com> writes:\n\n> On Fri, Jan 16, 2026 at 07:21:21AM -0800, Junio C Hamano wrote:\n>> Elijah Newren <newren@gmail.com> writes:\n>> \n>> > On Tue, Jan 13, 2026 at 1:54 AM Patrick Steinhardt <ps@pks.im> wrote:\n>> > [...]\n>> >> Changes in v11:\n>> >>   - Fix overly clever `BUG()` condition.\n>> >>   - Drop the `struct replay_result::merge_conflict` field.\n>> >>   - Return a positive value from `replay_revisions()` in case there was\n>> >>     a conflict.\n>> >>   - Improve a commit message.\n>> >>   - Fix check for whether `setup_revisions_from_strvec()` was\n>> >>     successful.\n>> >>   - Link to v10: https://lore.kernel.org/r/20260112-b4-pks-history-builtin-v10-0-e3c6aa5b4cec@pks.im\n>> >\n>> > This version looks good to merge down.\n>> \n>> Thanks.\n>\n> FWIW, I think it's far from ready to be merged.\n\nWill the discussion continue in the other subthread, i.e.,\n\nhttps://lore.kernel.org/git/CABPp-BHkNLdH4C7U4sFoVhrsSPH8KAaDtOdLEQGyajmXZz9hVg@mail.gmail.com/\n\n"},{"id":"535029","messageId":"xmqqsebivgop.fsf@gitster.g","threadId":"63986","inReplyTo":"CABPp-BHkNLdH4C7U4sFoVhrsSPH8KAaDtOdLEQGyajmXZz9hVg@mail.gmail.com","subject":"Re: [PATCH v11 8/8] builtin/history: implement \"reword\" subcommand","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2026-02-03T00:01:10Z","receivedAt":"2026-02-03T00:01:14Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Elijah Newren <newren@gmail.com> writes:\n\n> On Fri, Jan 16, 2026 at 8:28 AM SZEDER Gábor <szeder.dev@gmail.com> wrote:\n>>\n> ...\n> Personally, I'm having difficulty understanding your second stated\n> advantage of picking the commit from the rebase instruction sheet.\n> That instruction sheet is obtained by rebasing on top of something,\n> and it seems as easy to me to see the commits since that point in a\n> git log command and pick your desired commit from log output as it is\n> to invoke an interactive rebase on top of that base commit to get the\n> rebase instruction sheet and then pick out your desired commit from\n> there.  Same number of operations and work either way, so I don't see\n> how either is more or less work than the other.\n>\n> You did make a good point that one of the differences is that you\n> don't have the commit checked out.  That's a useful distinction to be\n> aware of.  To me, that seems to be somewhat implied already both by\n> \"in-memory\" and \"even if there are local changes in the working tree\n> or index\", though it wouldn't hurt to explicitly call it out.\n>\n> It feels like each of your complaints with the new proposed commands\n> (given commit not checked out, rebase instruction sheet vs listing\n> commit, and HEAD-only) can be boiled down to the fact that they don't\n> behave like `git rebase`.  Is that accurate?  If you like rebase, is\n> there a reason you are worried you can't just keep using it?  I don't\n> see why others should be required to implement another exact copy of\n> rebase, though.  Further, if we only wanted minor modifications, we\n> could have just done those to git rebase.\n>\n>> I firmly believe that operating on all local branches must always be\n>> the result of an explicit user action.\n>\n> Over at https://lore.kernel.org/git/aUVaEPGoOkATQGl3@szeder.dev/ , you\n> alternatively suggested the idea of an \"escape hatch\".  I think that\n> may be a good idea.  What if we had a \"history.scope\" config variable,\n> with values like \"descendant-branches\", \"current-branch\", and\n> \"error-if-multiple-branches\", corresponding to each of the requested\n> defaults we've seen in response to this series?  I can't imagine using\n> anything other than descendant-branches, and the preponderance of\n> those who have commented on the default so far seem to be in\n> agreement, but it would allow you and Matthias and others like you two\n> to pick an alternative.  Thoughts?\n\nThe discussion stalled after this exchange.  What is the next move?\nI think the it is SZEDER's turn to respond?\n\nThe topic is depended on Phillip's \"git replay\" tweak to drop\ncommits that are originally not-empty and become empty in the\nreplayed history, blocking its advance, which is doubly unfortunate.\n\nThanks.\n\n"},{"id":"535213","messageId":"aYRSjhnS8VQPSHa0@pks.im","threadId":"63986","inReplyTo":"xmqqsebivgop.fsf@gitster.g","subject":"Re: [PATCH v11 8/8] builtin/history: implement \"reword\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-02-05T08:19:26Z","receivedAt":"2026-02-05T08:19:38Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Mon, Feb 02, 2026 at 04:01:10PM -0800, Junio C Hamano wrote:\n> Elijah Newren <newren@gmail.com> writes:\n> \n> > On Fri, Jan 16, 2026 at 8:28 AM SZEDER Gábor <szeder.dev@gmail.com> wrote:\n> >>\n> > ...\n> > Personally, I'm having difficulty understanding your second stated\n> > advantage of picking the commit from the rebase instruction sheet.\n> > That instruction sheet is obtained by rebasing on top of something,\n> > and it seems as easy to me to see the commits since that point in a\n> > git log command and pick your desired commit from log output as it is\n> > to invoke an interactive rebase on top of that base commit to get the\n> > rebase instruction sheet and then pick out your desired commit from\n> > there.  Same number of operations and work either way, so I don't see\n> > how either is more or less work than the other.\n> >\n> > You did make a good point that one of the differences is that you\n> > don't have the commit checked out.  That's a useful distinction to be\n> > aware of.  To me, that seems to be somewhat implied already both by\n> > \"in-memory\" and \"even if there are local changes in the working tree\n> > or index\", though it wouldn't hurt to explicitly call it out.\n> >\n> > It feels like each of your complaints with the new proposed commands\n> > (given commit not checked out, rebase instruction sheet vs listing\n> > commit, and HEAD-only) can be boiled down to the fact that they don't\n> > behave like `git rebase`.  Is that accurate?  If you like rebase, is\n> > there a reason you are worried you can't just keep using it?  I don't\n> > see why others should be required to implement another exact copy of\n> > rebase, though.  Further, if we only wanted minor modifications, we\n> > could have just done those to git rebase.\n\ngit-history(1) is definitely quite different compared to git-rebase(1),\nand that's by design. Not everyone will like it, and many folks will\nprobably continue to just use git-rebase(1) themselves. But as you say,\nit does address some use cases that git-rebase(1) doesn't handle itself,\nand it tries to provide an opinionated interface to make common tasks\neasier to handle for folks who aren't advanced users.\n\nI of course wouldn't mind highlighting more of the differences in the\ndocs and/or the commit message, of course. It's tough sometimes to find\nthe right level of details.\n\n> >> I firmly believe that operating on all local branches must always be\n> >> the result of an explicit user action.\n> >\n> > Over at https://lore.kernel.org/git/aUVaEPGoOkATQGl3@szeder.dev/ , you\n> > alternatively suggested the idea of an \"escape hatch\".  I think that\n> > may be a good idea.  What if we had a \"history.scope\" config variable,\n> > with values like \"descendant-branches\", \"current-branch\", and\n> > \"error-if-multiple-branches\", corresponding to each of the requested\n> > defaults we've seen in response to this series?  I can't imagine using\n> > anything other than descendant-branches, and the preponderance of\n> > those who have commented on the default so far seem to be in\n> > agreement, but it would allow you and Matthias and others like you two\n> > to pick an alternative.  Thoughts?\n\nI would certainly be happy to add such an escape hatch to the command so\nthat it only rewrites the current branch. We already have the\ninfrastructure for that in place anyway, so it's trivial to add a\nconfiguration key that changes the default.\n\n> The discussion stalled after this exchange.  What is the next move?\n> I think the it is SZEDER's turn to respond?\n\nI dunno. From my point of view I still believe this is ready to be\nmerged. Sure, there will be follow-up topics:\n\n  - The above configuration to make it work on a single branch, only.\n\n  - First-class conflicts. This is something that I also discussed with\n    Elijah already, and we will coordinate on this topic going forward.\n\n  - Additional subcommands, of course.\n\nBut I don't think that missing support for first-class conflicts needs\nto hold back this topic. The proposed command cannot result in conflicts\nanyway, and there's more commands that won't. Furthermore, the command\nis marked as experimental, so the initial versions are expected to be\nsomewhat limited in their functionality so that we can get feedback and\niterate on the design if we see that it needs some tweaking.\n\nThanks!\n\nPatrick\n"}]}