{"thread":{"id":"64739","subject":"[PATCH v8 1/7] builtin/replay: extract core logic to replay revisions","startedAt":"2026-01-07T10:10:18Z","lastAt":"2026-01-12T13:03:38Z","messageCount":18,"participants":["Patrick Steinhardt","D. Ben Knoble","Elijah Newren"],"isPatch":true,"patchVersion":8,"patchTotal":7},"messages":[{"id":"533196","messageId":"20260107-b4-pks-history-builtin-v8-0-18e9779e3a26@pks.im","threadId":"64739","inReplyTo":"20250819-b4-pks-history-builtin-v1-0-9b77c32688fe@pks.im","subject":"[PATCH v8 0/7] Introduce git-history(1) command for easy history editing","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-01-07T10:10:08Z","receivedAt":"2026-01-07T10:10:18Z","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 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               | 374 +++-----------------------------------\n command-list.txt               |   1 +\n git.c                          |   1 +\n meson.build                    |   2 +\n replay.c                       | 347 +++++++++++++++++++++++++++++++++++\n replay.h                       |  77 ++++++++\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, 1335 insertions(+), 345 deletions(-)\n\nRange-diff versus v7:\n\n -:  ---------- >  1:  53a845e874 builtin/replay: extract core logic to replay revisions\n -:  ---------- >  2:  3ff1c0bacf builtin/replay: move core logic into \"libgit.a\"\n -:  ---------- >  3:  598df4e186 replay: small set of cleanups\n -:  ---------- >  4:  fd6a0ec5b8 replay: yield the object ID of the final rewritten commit\n 1:  0e2d8db69f =  5:  04b832320f wt-status: provide function to expose status for trees\n 2:  087c563575 <  -:  ---------- replay: extract logic to pick commits\n 3:  4ab2a6f807 <  -:  ---------- replay: stop using `the_repository`\n 4:  d2138e95d4 !  6:  e223659b86 builtin: add new \"history\" command\n    @@ Commit message\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    +    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    -    Add a new \"history\" command to plug this gap. This command will have\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    +\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. Some of these subcommands will be implemented\n    -    in subsequent commits.\n    +    use cases like the above.\n     \n         Signed-off-by: Patrick Steinhardt <ps@pks.im>\n     \n    @@ Documentation/git-history.adoc (new)\n     +\n     +NAME\n     +----\n    -+git-history - EXPERIMENTAL: Rewrite history of the current branch\n    ++git-history - EXPERIMENTAL: Rewrite history\n     +\n     +SYNOPSIS\n     +--------\n    @@ Documentation/git-history.adoc (new)\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    ++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    -+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    ++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    -+CONFIGURATION\n    -+-------------\n    -+\n    -+include::includes/cmd-config-section-all.adoc[]\n    -+\n     +GIT\n     +---\n     +Part of the linkgit:git[1] suite\n 5:  4279bd9522 !  7:  2644bbb605 builtin/history: implement \"reword\" subcommand\n    @@ Commit message\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    +    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    +      - 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         Signed-off-by: Patrick Steinhardt <ps@pks.im>\n     \n      ## Documentation/git-history.adoc ##\n    -@@ Documentation/git-history.adoc: git-history - EXPERIMENTAL: Rewrite history of the current branch\n    +@@ Documentation/git-history.adoc: git-history - EXPERIMENTAL: Rewrite history\n      SYNOPSIS\n      --------\n      [synopsis]\n     -git history [<options>]\n    -+git history reword <commit>\n    ++git history reword <commit> [--ref-action=(branches|head|print)]\n      \n      DESCRIPTION\n      -----------\n    +@@ Documentation/git-history.adoc: 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     @@ Documentation/git-history.adoc: COMMANDS\n      \n      Several commands are available to rewrite history in different ways:\n    @@ Documentation/git-history.adoc: COMMANDS\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    - \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\n     \n      ## builtin/history.c ##\n     @@\n     +#define USE_THE_REPOSITORY_VARIABLE\n     +\n      #include \"builtin.h\"\n    -+#include \"commit-reach.h\"\n     +#include \"commit.h\"\n    ++#include \"commit-reach.h\"\n     +#include \"config.h\"\n     +#include \"editor.h\"\n     +#include \"environment.h\"\n    @@ builtin/history.c\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    ++#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    @@ builtin/history.c\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    ++\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_commit, NULL, NULL);\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, new_tree_oid,\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_commit, exclude_gpgsig);\n    ++\toriginal_extra_headers = read_commit_extra_headers(original, 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    ++\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    @@ builtin/history.c\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_ref_updates updates = {\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, &updates);\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 < 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     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\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      &updates.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\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\tbreak;\n    ++\tdefault:\n    ++\t\tBUG(\"unsupported ref action %d\", action);\n    ++\t}\n    ++\n    ++\tret = 0;\n    ++\n    ++out:\n    ++\treplay_ref_updates_release(&updates);\n    ++\tref_transaction_free(transaction);\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    @@ builtin/history.c\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 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 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    @@ builtin/history.c\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    ++\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    -+\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    ++\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    -+\treplace_commits(&commits, &original_commit->object.oid, &rewritten_commit, 1);\n    ++\tstrbuf_addf(&reflog_msg, \"reword: updating %s\", argv[0]);\n     +\n    -+\tret = apply_commits(repo, &commits, parent, head, \"reword\");\n    -+\tif (ret < 0)\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    -+\tstrvec_clear(&commits);\n    ++\tstrbuf_release(&reflog_msg);\n     +\treturn ret;\n     +}\n      \n    @@ builtin/history.c\n     +\treturn fn(argc, argv, prefix, repo);\n      }\n     \n    + ## replay.c ##\n    +@@ replay.c: 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    +@@ replay.c: 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();\n    +\n      ## t/meson.build ##\n     @@ t/meson.build: integration_tests = [\n        't3437-rebase-fixup-options.sh',\n    @@ t/t3451-history-reword.sh (new)\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    @@ t/t3451-history-reword.sh (new)\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    ++expect_graph () {\n    ++\tcat >expect &&\n    ++\tlib_test_cmp_graph --graph --format=%s \"$@\"\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    ++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    @@ t/t3451-history-reword.sh (new)\n     +\t\tgit symbolic-ref HEAD >actual &&\n     +\t\ttest_cmp expect actual &&\n     +\n    -+\t\tcat >expect <<-EOF &&\n    ++\t\texpect_log <<-\\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/t3451-history-reword.sh (new)\n     +\t\tgit symbolic-ref HEAD >actual &&\n     +\t\ttest_cmp expect actual &&\n     +\n    -+\t\tcat >expect <<-EOF &&\n    ++\t\texpect_log <<-\\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    @@ t/t3451-history-reword.sh (new)\n     +\t\tfirst reworded\n     +\t\tEOF\n     +\n    -+\t\tcat >expect <<-EOF &&\n    ++\t\texpect_log <<-\\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 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 6:  725bea8b19 <  -:  ---------- add-patch: split out header from \"add-interactive.h\"\n 7:  09af5da957 <  -:  ---------- add-patch: split out `struct interactive_options`\n 8:  9faad738f3 <  -:  ---------- add-patch: remove dependency on \"add-interactive\" subsystem\n 9:  2ccce1afd7 <  -:  ---------- add-patch: add support for in-memory index patching\n10:  b5fb633f9c <  -:  ---------- add-patch: allow disabling editing of hunks\n11:  0c30a1eae2 <  -:  ---------- cache-tree: allow writing in-memory index as tree\n12:  c7898ca2ff <  -:  ---------- builtin/history: implement \"split\" subcommand\n\n---\nbase-commit: 02bdc12118c43e0a65258eb742492965859bffb6\nchange-id: 20250819-b4-pks-history-builtin-83398f9a05f0\n\n"},{"id":"533195","messageId":"20260107-b4-pks-history-builtin-v8-1-18e9779e3a26@pks.im","threadId":"64739","inReplyTo":"20260107-b4-pks-history-builtin-v8-0-18e9779e3a26@pks.im","subject":"[PATCH v8 1/7] builtin/replay: extract core logic to replay revisions","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-01-07T10:10:09Z","receivedAt":"2026-01-07T10:10:20Z","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 | 258 ++++++++++++++++++++++++++++++++-----------------------\n 1 file changed, 149 insertions(+), 109 deletions(-)\n\ndiff --git a/builtin/replay.c b/builtin/replay.c\nindex 1960bbbee8..d7523fdbc2 100644\n--- a/builtin/replay.c\n+++ b/builtin/replay.c\n@@ -278,6 +278,127 @@ 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_ref_updates {\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+};\n+\n+static void replay_ref_updates_release(struct replay_ref_updates *updates)\n+{\n+\tfor (size_t i = 0; i < updates->nr; i++)\n+\t\tfree(updates->items[i].refname);\n+\tfree(updates->items);\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+{\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\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}\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\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+\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 +427,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_ref_updates updates = { 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 +444,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 +461,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 +523,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, &updates);\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 +548,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 < 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+\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\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 +568,13 @@ 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_ref_updates_release(&updates);\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 ? 1 : 0;\n }\n\n-- \n2.52.0.542.g9473a8513b.dirty\n\n"},{"id":"533197","messageId":"20260107-b4-pks-history-builtin-v8-2-18e9779e3a26@pks.im","threadId":"64739","inReplyTo":"20260107-b4-pks-history-builtin-v8-0-18e9779e3a26@pks.im","subject":"[PATCH v8 2/7] builtin/replay: move core logic into \"libgit.a\"","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-01-07T10:10:10Z","receivedAt":"2026-01-07T10:10:22Z","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 | 358 +------------------------------------------------------\n meson.build      |   1 +\n replay.c         | 347 +++++++++++++++++++++++++++++++++++++++++++++++++++++\n replay.h         |  61 ++++++++++\n 5 files changed, 411 insertions(+), 357 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 d7523fdbc2..24f0b5f050 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,127 +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_ref_updates {\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-};\n-\n-static void replay_ref_updates_release(struct replay_ref_updates *updates)\n-{\n-\tfor (size_t i = 0; i < updates->nr; i++)\n-\t\tfree(updates->items[i].refname);\n-\tfree(updates->items);\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-{\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\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}\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\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-\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..1926fca4b9\n--- /dev/null\n+++ b/replay.c\n@@ -0,0 +1,347 @@\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_ref_updates_release(struct replay_ref_updates *updates)\n+{\n+\tfor (size_t i = 0; i < updates->nr; i++)\n+\t\tfree(updates->items[i].refname);\n+\tfree(updates->items);\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+{\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\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}\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\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+\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..bc7a321a5e\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_ref_updates {\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} *items;\n+\tsize_t nr, alloc;\n+};\n+\n+void replay_ref_updates_release(struct replay_ref_updates *updates);\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_ref_updates *updates);\n+\n+#endif\n\n-- \n2.52.0.542.g9473a8513b.dirty\n\n"},{"id":"533198","messageId":"20260107-b4-pks-history-builtin-v8-3-18e9779e3a26@pks.im","threadId":"64739","inReplyTo":"20260107-b4-pks-history-builtin-v8-0-18e9779e3a26@pks.im","subject":"[PATCH v8 3/7] replay: small set of cleanups","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-01-07T10:10:11Z","receivedAt":"2026-01-07T10:10:25Z","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 1926fca4b9..367951395a 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":"533199","messageId":"20260107-b4-pks-history-builtin-v8-4-18e9779e3a26@pks.im","threadId":"64739","inReplyTo":"20260107-b4-pks-history-builtin-v8-0-18e9779e3a26@pks.im","subject":"[PATCH v8 4/7] replay: yield the object ID of the final rewritten commit","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-01-07T10:10:12Z","receivedAt":"2026-01-07T10:10:28Z","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 367951395a..8c2f2d3710 100644\n--- a/replay.c\n+++ b/replay.c\n@@ -330,6 +330,8 @@ int replay_revisions(struct repository *repo, struct rev_info *revs,\n \t\tupdates->nr++;\n \t}\n \n+\tupdates->final_oid = last_commit->object.oid;\n+\n \tret = 0;\n \n out:\ndiff --git a/replay.h b/replay.h\nindex bc7a321a5e..e00bb4214e 100644\n--- a/replay.h\n+++ b/replay.h\n@@ -43,6 +43,22 @@ struct replay_ref_updates {\n \t\tstruct object_id new_oid;\n \t} *items;\n \tsize_t nr, alloc;\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_ref_updates_release(struct replay_ref_updates *updates);\n\n-- \n2.52.0.542.g9473a8513b.dirty\n\n"},{"id":"533200","messageId":"20260107-b4-pks-history-builtin-v8-5-18e9779e3a26@pks.im","threadId":"64739","inReplyTo":"20260107-b4-pks-history-builtin-v8-0-18e9779e3a26@pks.im","subject":"[PATCH v8 5/7] wt-status: provide function to expose status for trees","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-01-07T10:10:13Z","receivedAt":"2026-01-07T10:10:31Z","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":"533201","messageId":"20260107-b4-pks-history-builtin-v8-6-18e9779e3a26@pks.im","threadId":"64739","inReplyTo":"20260107-b4-pks-history-builtin-v8-0-18e9779e3a26@pks.im","subject":"[PATCH v8 6/7] builtin: add new \"history\" command","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-01-07T10:10:14Z","receivedAt":"2026-01-07T10:10:33Z","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\nAnother problem that rebases have is that dependent branches are not\nbeing updated. The use of stacked branches has grown quite common with\ncompetiting version control systems like Jujutsu though, so it clearly\nis a need that users have. While rebases _can_ serve this use case if\none always works on the latest stacked branch, it is somewhat awkward\nand 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":"533202","messageId":"20260107-b4-pks-history-builtin-v8-7-18e9779e3a26@pks.im","threadId":"64739","inReplyTo":"20260107-b4-pks-history-builtin-v8-0-18e9779e3a26@pks.im","subject":"[PATCH v8 7/7] builtin/history: implement \"reword\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-01-07T10:10:15Z","receivedAt":"2026-01-07T10:10:36Z","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..59011ea517 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_ref_updates updates = {\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, &updates);\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 < 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     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\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      &updates.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\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\tbreak;\n+\tdefault:\n+\t\tBUG(\"unsupported ref action %d\", action);\n+\t}\n+\n+\tret = 0;\n+\n+out:\n+\treplay_ref_updates_release(&updates);\n+\tref_transaction_free(transaction);\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 8c2f2d3710..5203f9db4c 100644\n--- a/replay.c\n+++ b/replay.c\n@@ -254,7 +254,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@@ -270,7 +272,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":"533235","messageId":"CALnO6CDhDFtz5WY2pd8as5nH-URxzfNUfkouQ2Cf6USuRRTrKw@mail.gmail.com","threadId":"64739","inReplyTo":"20260107-b4-pks-history-builtin-v8-0-18e9779e3a26@pks.im","subject":"Re: [PATCH v8 0/7] Introduce git-history(1) command for easy history editing","fromName":"D. Ben Knoble","fromEmail":"ben.knoble@gmail.com","sentAt":"2026-01-07T17:39:47Z","receivedAt":"2026-01-07T17:40:00Z","isPatch":true,"sender":{"key":"ben.knoble@gmail.com","avatar":"https://avatars.githubusercontent.com/u/22802209?v=4"},"body":"On Wed, Jan 7, 2026 at 5:10 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> - 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\nSane, if sad for my custom build which has enjoyed having \"git history\" commands\navailable :) I can wait for other patches, or try to contribute some, though.\n\n[reads further]\n\nOh, it's just split that drops; we still have reword, etc. Cool!\n\n> Range-diff versus v7:\n>\n>  -:  ---------- >  1:  53a845e874 builtin/replay: extract core logic to replay revisions\n>  -:  ---------- >  2:  3ff1c0bacf builtin/replay: move core logic into \"libgit.a\"\n>  -:  ---------- >  3:  598df4e186 replay: small set of cleanups\n>  -:  ---------- >  4:  fd6a0ec5b8 replay: yield the object ID of the final rewritten commit\n>  1:  0e2d8db69f =  5:  04b832320f wt-status: provide function to expose status for trees\n>  2:  087c563575 <  -:  ---------- replay: extract logic to pick commits\n>  3:  4ab2a6f807 <  -:  ---------- replay: stop using `the_repository`\n>  4:  d2138e95d4 !  6:  e223659b86 builtin: add new \"history\" command\n>     @@ Commit message\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>     +    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>     -    Add a new \"history\" command to plug this gap. This command will have\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\n\"[D]ependent branches are not being updated\" reads weirdly to me.\n\"Rebases also do not update dependent branches\" perhaps?\n\n>     +    competiting version control systems like Jujutsu though, so it clearly\n\n\"competing\" ;)\n\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>     +\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. Some of these subcommands will be implemented\n>     -    in subsequent commits.\n>     +    use cases like the above.\n>\n>          Signed-off-by: Patrick Steinhardt <ps@pks.im>\n>\n>     @@ Documentation/git-history.adoc (new)\n>      +\n>      +NAME\n>      +----\n>     -+git-history - EXPERIMENTAL: Rewrite history of the current branch\n>     ++git-history - EXPERIMENTAL: Rewrite history\n>      +\n>      +SYNOPSIS\n>      +--------\n>     @@ Documentation/git-history.adoc (new)\n[snip]\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\nHm? This feels incomplete to me.\n\nBest,\nBen\n"},{"id":"533236","messageId":"CALnO6CAMX8K6oNzTmcg_stqkU2FCUepdvNfPTGaA-jSaTMzj0g@mail.gmail.com","threadId":"64739","inReplyTo":"20260107-b4-pks-history-builtin-v8-1-18e9779e3a26@pks.im","subject":"Re: [PATCH v8 1/7] builtin/replay: extract core logic to replay revisions","fromName":"D. Ben Knoble","fromEmail":"ben.knoble@gmail.com","sentAt":"2026-01-07T17:53:59Z","receivedAt":"2026-01-07T17:54:14Z","isPatch":true,"sender":{"key":"ben.knoble@gmail.com","avatar":"https://avatars.githubusercontent.com/u/22802209?v=4"},"body":"On Wed, Jan 7, 2026 at 5:10 AM Patrick Steinhardt <ps@pks.im> wrote:\n\n> diff --git a/builtin/replay.c b/builtin/replay.c\n> index 1960bbbee8..d7523fdbc2 100644\n> --- a/builtin/replay.c\n> +++ b/builtin/replay.c\n\n> @@ -517,24 +568,13 @@ 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_ref_updates_release(&updates);\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> +       return ret ? 1 : 0;\n>  }\n\nI tried checking the tree after applying this patch, too, and it looks\nto me like the return code flipped here? In particular, some callsites\nthat assign ret = error(…) are untouched, so I don't think the meaning\nof ret has changed. Now, error() returns -1, which is truthy, so\nreturning 1 instead of 0 makes sense here… was this a bug in the\noriginal? I can't quite tell, but that seems unlikely.\n\nThe original blames to 81613be31e (replay: make it a minimal server\nside command, 2023-11-24), but there it seems like ret is\n\"result.clean\" (except for some error cases? which are handled by the\nnegative conditional), and \"result.clean == 0\" is the success\nindicator (in other words, _falsey_ means success here).\n\nSo overall this flip _seems_ correct, but it was hard for me to follow\nat a glance. Hm.\n\n-- \nD. Ben Knoble\n"},{"id":"533237","messageId":"CALnO6CDG222mhCeWyBCpGU3fQiHPVwBh=5EygRFrFN3JOcx6Sw@mail.gmail.com","threadId":"64739","inReplyTo":"20260107-b4-pks-history-builtin-v8-7-18e9779e3a26@pks.im","subject":"Re: [PATCH v8 7/7] builtin/history: implement \"reword\" subcommand","fromName":"D. Ben Knoble","fromEmail":"ben.knoble@gmail.com","sentAt":"2026-01-07T18:01:11Z","receivedAt":"2026-01-07T18:01:23Z","isPatch":true,"sender":{"key":"ben.knoble@gmail.com","avatar":"https://avatars.githubusercontent.com/u/22802209?v=4"},"body":"On Wed, Jan 7, 2026 at 5:10 AM Patrick Steinhardt <ps@pks.im> wrote:\n>\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> diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc\n> index 5a9d931efc..4eea317e5c 100644\n> --- a/Documentation/git-history.adoc\n> +++ b/Documentation/git-history.adoc\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\nAh, looks like this commit fixed one of my befuddlements during the range-diff.\nBad history edit? :)\n"},{"id":"533313","messageId":"aWCvdHGDzZC5OLFU@pks.im","threadId":"64739","inReplyTo":"CALnO6CDhDFtz5WY2pd8as5nH-URxzfNUfkouQ2Cf6USuRRTrKw@mail.gmail.com","subject":"Re: [PATCH v8 0/7] Introduce git-history(1) command for easy history editing","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-01-09T07:34:12Z","receivedAt":"2026-01-09T07:34:25Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Wed, Jan 07, 2026 at 12:39:47PM -0500, D. Ben Knoble wrote:\n> On Wed, Jan 7, 2026 at 5:10 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> > - 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> \n> Sane, if sad for my custom build which has enjoyed having \"git history\" commands\n> available :) I can wait for other patches, or try to contribute some, though.\n> \n> [reads further]\n> \n> Oh, it's just split that drops; we still have reword, etc. Cool!\n\nWe only have \"reword\" for now. In any case, I'll definitely upstream\nmore subcommands once the initial version has landed. Probably the\ncomplete set of commands I was proposing initially: \"drop\", \"split\" and\n\"reorder\". Afterwards I'd also like to have a look at \"absorb\", but I'll\nalso gladly accept any help.\n\n> > Range-diff versus v7:\n> >\n> >  -:  ---------- >  1:  53a845e874 builtin/replay: extract core logic to replay revisions\n> >  -:  ---------- >  2:  3ff1c0bacf builtin/replay: move core logic into \"libgit.a\"\n> >  -:  ---------- >  3:  598df4e186 replay: small set of cleanups\n> >  -:  ---------- >  4:  fd6a0ec5b8 replay: yield the object ID of the final rewritten commit\n> >  1:  0e2d8db69f =  5:  04b832320f wt-status: provide function to expose status for trees\n> >  2:  087c563575 <  -:  ---------- replay: extract logic to pick commits\n> >  3:  4ab2a6f807 <  -:  ---------- replay: stop using `the_repository`\n> >  4:  d2138e95d4 !  6:  e223659b86 builtin: add new \"history\" command\n> >     @@ Commit message\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> >     +    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> >     -    Add a new \"history\" command to plug this gap. This command will have\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> \n> \"[D]ependent branches are not being updated\" reads weirdly to me.\n> \"Rebases also do not update dependent branches\" perhaps?\n> \n> >     +    competiting version control systems like Jujutsu though, so it clearly\n> \n> \"competing\" ;)\n\nFixed both, thanks!\n\nPatrick\n"},{"id":"533314","messageId":"aWCwNZrJl1w-Vibw@pks.im","threadId":"64739","inReplyTo":"CALnO6CAMX8K6oNzTmcg_stqkU2FCUepdvNfPTGaA-jSaTMzj0g@mail.gmail.com","subject":"Re: [PATCH v8 1/7] builtin/replay: extract core logic to replay revisions","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-01-09T07:37:25Z","receivedAt":"2026-01-09T07:37:31Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Wed, Jan 07, 2026 at 12:53:59PM -0500, D. Ben Knoble wrote:\n> On Wed, Jan 7, 2026 at 5:10 AM Patrick Steinhardt <ps@pks.im> wrote:\n> \n> > diff --git a/builtin/replay.c b/builtin/replay.c\n> > index 1960bbbee8..d7523fdbc2 100644\n> > --- a/builtin/replay.c\n> > +++ b/builtin/replay.c\n> \n> > @@ -517,24 +568,13 @@ 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_ref_updates_release(&updates);\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> > +       return ret ? 1 : 0;\n> >  }\n> \n> I tried checking the tree after applying this patch, too, and it looks\n> to me like the return code flipped here? In particular, some callsites\n> that assign ret = error(…) are untouched, so I don't think the meaning\n> of ret has changed. Now, error() returns -1, which is truthy, so\n> returning 1 instead of 0 makes sense here… was this a bug in the\n> original? I can't quite tell, but that seems unlikely.\n> \n> The original blames to 81613be31e (replay: make it a minimal server\n> side command, 2023-11-24), but there it seems like ret is\n> \"result.clean\" (except for some error cases? which are handled by the\n> negative conditional), and \"result.clean == 0\" is the success\n> indicator (in other words, _falsey_ means success here).\n> \n> So overall this flip _seems_ correct, but it was hard for me to follow\n> at a glance. Hm.\n\nI think you're onto something here. The intent seems to be that:\n\n  - We exit with 128 in case there was any generic error.\n\n  - We exit with 1 in case there was a merge conflict.\n\n  - We exit with 0 in case the command was successful.\n\nBut the extracted `replay_revisions()` command always returns negative\non error now. I was initially returning that value directly, which has\ncaused a test failure. I fixed that with the above condition, but I\ndidn't realize that we explicitly wanted to tell apart those two error\ncases.\n\nI'll fix this code and refactor it a bit to make it more explicit,\nthanks!\n\nPatrick\n"},{"id":"533315","messageId":"aWCwOjKG_ZwqPCpz@pks.im","threadId":"64739","inReplyTo":"CALnO6CDG222mhCeWyBCpGU3fQiHPVwBh=5EygRFrFN3JOcx6Sw@mail.gmail.com","subject":"Re: [PATCH v8 7/7] builtin/history: implement \"reword\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-01-09T07:37:30Z","receivedAt":"2026-01-09T07:37:36Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Wed, Jan 07, 2026 at 01:01:11PM -0500, D. Ben Knoble wrote:\n> On Wed, Jan 7, 2026 at 5:10 AM Patrick Steinhardt <ps@pks.im> wrote:\n> >\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> > diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc\n> > index 5a9d931efc..4eea317e5c 100644\n> > --- a/Documentation/git-history.adoc\n> > +++ b/Documentation/git-history.adoc\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> \n> Ah, looks like this commit fixed one of my befuddlements during the range-diff.\n> Bad history edit? :)\n\nHuh, something's weird here indeed, the range-diff and the actual\nchanges do not match. I use b4 though, so this isn't an issue that was\ncaused by weird manual munging of patches. Hum.\n\nWell, I'll not go down this rabbit hole today :) Thanks for your review!\n\nPatrick\n"},{"id":"533452","messageId":"CALnO6CAqMR_AZ-8REj-u89fNHBj-pmxre-Yr53CHPQDf_R_y=g@mail.gmail.com","threadId":"64739","inReplyTo":"aWCwOjKG_ZwqPCpz@pks.im","subject":"Re: [PATCH v8 7/7] builtin/history: implement \"reword\" subcommand","fromName":"D. Ben Knoble","fromEmail":"ben.knoble@gmail.com","sentAt":"2026-01-09T23:24:19Z","receivedAt":"2026-01-09T23:24:31Z","isPatch":true,"sender":{"key":"ben.knoble@gmail.com","avatar":"https://avatars.githubusercontent.com/u/22802209?v=4"},"body":"[I think this is present in v9 or I wouldn't bother following up :)]\n\nOn Fri, Jan 9, 2026 at 2:37 AM Patrick Steinhardt <ps@pks.im> wrote:\n>\n> On Wed, Jan 07, 2026 at 01:01:11PM -0500, D. Ben Knoble wrote:\n> > On Wed, Jan 7, 2026 at 5:10 AM Patrick Steinhardt <ps@pks.im> wrote:\n> > >\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> > > diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc\n> > > index 5a9d931efc..4eea317e5c 100644\n> > > --- a/Documentation/git-history.adoc\n> > > +++ b/Documentation/git-history.adoc\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> >\n> > Ah, looks like this commit fixed one of my befuddlements during the range-diff.\n> > Bad history edit? :)\n>\n> Huh, something's weird here indeed, the range-diff and the actual\n> changes do not match. I use b4 though, so this isn't an issue that was\n> caused by weird manual munging of patches. Hum.\n>\n> Well, I'll not go down this rabbit hole today :) Thanks for your review!\n>\n> Patrick\n\nWhat's weird is the pre-image, which comes from the patch right before\nit. It seems like an incomplete sentence that could be dropped from\nwhat I see in Junio's mirror as baf46a880d (builtin: add new \"history\"\ncommand, 2026-01-09).\n\n-- \nD. Ben Knoble\n"},{"id":"533453","messageId":"CALnO6CB_XBXKAcCwEJZHotioxcAKNrNO2bZZ0nwoZ0KufVs+mA@mail.gmail.com","threadId":"64739","inReplyTo":"aWCvdHGDzZC5OLFU@pks.im","subject":"Re: [PATCH v8 0/7] Introduce git-history(1) command for easy history editing","fromName":"D. Ben Knoble","fromEmail":"ben.knoble@gmail.com","sentAt":"2026-01-09T23:27:51Z","receivedAt":"2026-01-09T23:28:02Z","isPatch":true,"sender":{"key":"ben.knoble@gmail.com","avatar":"https://avatars.githubusercontent.com/u/22802209?v=4"},"body":"On Fri, Jan 9, 2026 at 2:34 AM Patrick Steinhardt <ps@pks.im> wrote:\n>\n> We only have \"reword\" for now. In any case, I'll definitely upstream\n> more subcommands once the initial version has landed. Probably the\n> complete set of commands I was proposing initially: \"drop\", \"split\" and\n> \"reorder\". Afterwards I'd also like to have a look at \"absorb\", but I'll\n> also gladly accept any help.\n\nI figured as much. I stumbled onto a version of git-absorb [1] today,\nand I have some notes on how I might script such a thing myself using\nmy \"git-touching\" [2] script. But my script has a slight bug [3] when\nhandling staged changes that might make it not quite ready for the\nattempt yet. Anyway, some food for thought.\n\n[1]: https://github.com/tummychow/git-absorb\n[2]: https://github.com/benknoble/Dotfiles/blob/master/links/bin/git-touching\n[3]: From my todo list: git: `touching -c` only considers staged\nfiles, not staged lines. So mixed files give the wrong outputs.\n\n-- \nD. Ben Knoble\n"},{"id":"533461","messageId":"CABPp-BHFwvg5A295kXkc_axoibNhGDn4ZUkm0uE1u+358xSZzw@mail.gmail.com","threadId":"64739","inReplyTo":"20260107-b4-pks-history-builtin-v8-7-18e9779e3a26@pks.im","subject":"Re: [PATCH v8 7/7] builtin/history: implement \"reword\" subcommand","fromName":"Elijah Newren","fromEmail":"newren@gmail.com","sentAt":"2026-01-10T01:20:04Z","receivedAt":"2026-01-10T01:20:16Z","isPatch":true,"sender":{"key":"newren@gmail.com","avatar":"https://avatars.githubusercontent.com/u/5455730?v=4"},"body":"On Wed, Jan 7, 2026 at 2:10 AM Patrick Steinhardt <ps@pks.im> wrote:\n>\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>   - 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> Signed-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>\n> diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc\n> index 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\nAh, was the previous sentence here from the former patch just a bad\nsplitting when you were rewriting?\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> +       Rewrite the commit message of the specified commit. All the other\n> +       details of this commit remain unchanged. This command will spawn an\n> +       editor with the current message of that commit.\n\nOne isn't exactly \"several\"; I know you'll add more later, but since\nthis series ends here, should that word be changed?\n\n> +\n> +OPTIONS\n> +-------\n> +\n> +`--ref-action=(branches|head|print)`::\n> +       Control which references will be updated by the command, if any. With\n> +       `branches`, all local branches that point to commits which are\n> +       decendants of the original commit will be rewritten. With `head`, only\n\ndecendants -> descendants .   Or maybe double down on the typo and\nextend it a bit into either 'decedent' or 'decadent'.  That could be\nfun.\n\n> +       the current `HEAD` reference will be rewritten. With `print`, all\n> +       updates as they would be performed with `branches` are printed in a\n> +       format that can be consumed by linkgit:git-update-ref[1].\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 f6fe32610b..59011ea517 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> +       N_(\"git history reword <commit> [--ref-action=(branches|head|print)]\")\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 *action,\n> +                              struct strbuf *out)\n> +{\n> +       const char *path = git_path_commit_editmsg();\n> +       const char *hint =\n> +               _(\"Please enter the commit message for the %s changes.\"\n> +                 \" Lines starting\\nwith '%s' will be ignored, and an\"\n> +                 \" empty message aborts the commit.\\n\");\n> +       struct wt_status s;\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> +\n> +       strbuf_reset(out);\n> +       if (launch_editor(path, out, NULL)) {\n> +               fprintf(stderr, _(\"Aborting commit as launching the editor failed.\\n\"));\n> +               return -1;\n> +       }\n> +       strbuf_stripspace(out, comment_line_str);\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> +               return -1;\n> +       }\n> +\n> +       return 0;\n> +}\n> +\n> +static int commit_tree_with_edited_message(struct repository *repo,\n> +                                          const char *action,\n> +                                          struct commit *original,\n> +                                          struct commit **out)\n> +{\n> +       const char *exclude_gpgsig[] = { \"gpgsig\", \"gpgsig-sha256\", NULL };\n> +       const char *original_message, *original_body, *ptr;\n> +       struct commit_extra_header *original_extra_headers = NULL;\n> +       struct strbuf commit_message = STRBUF_INIT;\n> +       struct object_id rewritten_commit_oid;\n> +       struct object_id original_tree_oid;\n> +       struct object_id parent_tree_oid;\n> +       char *original_author = NULL;\n> +       struct commit *parent;\n> +       size_t len;\n> +       int ret;\n> +\n> +       original_tree_oid = repo_get_commit_tree(repo, original)->object.oid;\n> +\n> +       parent = original->parents ? original->parents->item : NULL;\n> +       if (parent) {\n> +               if (repo_parse_commit(repo, parent)) {\n> +                       ret = error(_(\"unable to parse parent commit %s\"),\n> +                                   oid_to_hex(&parent->object.oid));\n> +                       goto out;\n> +               }\n> +\n> +               parent_tree_oid = repo_get_commit_tree(repo, parent)->object.oid;\n> +       } else {\n> +               oidcpy(&parent_tree_oid, repo->hash_algo->empty_tree);\n> +       }\n> +\n> +       /* We retain authorship of the original commit. */\n> +       original_message = repo_logmsg_reencode(repo, original, NULL, NULL);\n> +       ptr = find_commit_header(original_message, \"author\", &len);\n> +       if (ptr)\n> +               original_author = xmemdupz(ptr, len);\n> +       find_commit_subject(original_message, &original_body);\n> +\n> +       ret = fill_commit_message(repo, &parent_tree_oid, &original_tree_oid,\n> +                                 original_body, action, &commit_message);\n> +       if (ret < 0)\n> +               goto out;\n> +\n> +       original_extra_headers = read_commit_extra_headers(original, exclude_gpgsig);\n\nDoes this grab encoding?  If so, should it be excluded as well given\nthe repo_logmsg_reencode() call?\n\n> +\n> +       ret = commit_tree_extended(commit_message.buf, commit_message.len, &original_tree_oid,\n> +                                  original->parents, &rewritten_commit_oid, original_author,\n> +                                  NULL, NULL, original_extra_headers);\n> +       if (ret < 0)\n> +               goto out;\n> +\n> +       *out = lookup_commit_or_die(&rewritten_commit_oid, \"rewritten commit\");\n> +\n> +out:\n> +       free_commit_extra_headers(original_extra_headers);\n> +       strbuf_release(&commit_message);\n> +       free(original_author);\n> +       return ret;\n> +}\n> +\n> +enum ref_action {\n> +       REF_ACTION_DEFAULT,\n> +       REF_ACTION_BRANCHES,\n> +       REF_ACTION_HEAD,\n> +       REF_ACTION_PRINT,\n> +};\n> +\n> +static int parse_ref_action(const struct option *opt, const char *value, int unset)\n> +{\n> +       enum ref_action *action = opt->value;\n> +\n> +       BUG_ON_OPT_NEG_NOARG(unset, value);\n> +       if (!strcmp(value, \"branches\")) {\n> +               *action = REF_ACTION_BRANCHES;\n> +       } else if (!strcmp(value, \"head\")) {\n> +               *action = REF_ACTION_HEAD;\n> +       } else if (!strcmp(value, \"print\")) {\n> +               *action = REF_ACTION_PRINT;\n> +       } else {\n> +               return error(_(\"%s expects one of 'branches', 'head' or 'print'\"),\n> +                            opt->long_name);\n> +       }\n> +\n> +       return 0;\n> +}\n> +\n> +static int handle_reference_updates(enum ref_action action,\n> +                                   struct repository *repo,\n> +                                   struct commit *original,\n> +                                   struct commit *rewritten,\n> +                                   const char *reflog_msg)\n> +{\n> +       const struct name_decoration *decoration;\n> +       struct replay_revisions_options opts = { 0 };\n> +       struct replay_ref_updates updates = {\n> +               .final_oid = rewritten->object.oid,\n> +       };\n> +       struct ref_transaction *transaction = NULL;\n> +       struct strvec args = STRVEC_INIT;\n> +       struct strbuf err = STRBUF_INIT;\n> +       struct commit *head = NULL;\n> +       struct rev_info revs;\n> +       char hex[GIT_MAX_HEXSZ + 1];\n> +       int ret;\n> +\n> +       repo_init_revisions(repo, &revs, NULL);\n> +       strvec_push(&args, \"ignored\");\n> +       strvec_push(&args, \"--reverse\");\n> +       strvec_push(&args, \"--topo-order\");\n> +       strvec_push(&args, \"--full-history\");\n> +\n> +       /* We only want to see commits that are descendants of the old commit. */\n> +       strvec_pushf(&args, \"--ancestry-path=%s\",\n> +                    oid_to_hex(&original->object.oid));\n> +\n> +       /*\n> +        * Ancestry path may also show ancestors of the old commit, but we\n> +        * don't want to see those, either.\n> +        */\n> +       strvec_pushf(&args, \"^%s\", oid_to_hex(&original->object.oid));\n> +\n> +       /*\n> +        * When we're asked to update HEAD we need to verify that the commit\n> +        * that we want to rewrite is actually an ancestor of it and, if so,\n> +        * update it. Otherwise we'll update (or print) all descendant\n> +        * branches.\n> +        */\n> +       if (action == REF_ACTION_HEAD) {\n> +               struct commit_list *from_list = NULL;\n> +\n> +               head = lookup_commit_reference_by_name(\"HEAD\");\n> +               if (!head) {\n> +                       ret = error(_(\"cannot look up HEAD\"));\n> +                       goto out;\n> +               }\n> +\n> +               commit_list_insert(original, &from_list);\n> +               ret = repo_is_descendant_of(repo, head, from_list);\n> +               free_commit_list(from_list);\n> +\n> +               if (ret < 0) {\n> +                       ret = error(_(\"cannot determine descendance\"));\n> +                       goto out;\n> +               } else if (!ret) {\n> +                       ret = error(_(\"rewritten commit must be an ancestor \"\n> +                                     \"of HEAD when using --ref-action=head\"));\n> +                       goto out;\n> +               }\n> +\n> +               strvec_push(&args, oid_to_hex(&head->object.oid));\n\nIf you used \"HEAD\" here instead of its oid, then you wouldn't need a\nseparate codepath for REF_ACTION_HEAD in your \"switch(action)\" block\nbelow.\n\n> +       } else {\n> +               strvec_push(&args, \"--branches\");\n> +       }\n> +\n> +       setup_revisions_from_strvec(&args, &revs, NULL);\n> +       if (revs.nr)\n> +               BUG(\"revisions were set up with invalid argument '%s'\", args.v[0]);\n> +\n> +       opts.onto = oid_to_hex_r(hex, &rewritten->object.oid);\n> +\n> +       ret = replay_revisions(repo, &revs, &opts, &updates);\n> +       if (ret)\n> +               goto out;\n> +\n> +       switch (action) {\n> +       case REF_ACTION_DEFAULT:\n> +       case REF_ACTION_BRANCHES:\n> +               transaction = ref_store_transaction_begin(get_main_ref_store(repo), 0, &err);\n> +               if (!transaction) {\n> +                       ret = error(_(\"failed to begin ref transaction: %s\"), err.buf);\n> +                       goto out;\n> +               }\n> +\n> +               for (size_t i = 0; i < updates.nr; i++) {\n> +                       ret = ref_transaction_update(transaction,\n> +                                                    updates.items[i].refname,\n> +                                                    &updates.items[i].new_oid,\n> +                                                    &updates.items[i].old_oid,\n> +                                                    NULL, NULL, 0, reflog_msg, &err);\n> +                       if (ret) {\n> +                               ret = error(_(\"failed to update ref '%s': %s\"),\n> +                                           updates.items[i].refname, err.buf);\n> +                               goto out;\n> +                       }\n> +               }\n> +\n> +               /*\n> +                * `replay_revisions()` only updates references that are\n> +                * ancestors of `rewritten`, so we need to manually\n> +                * handle updating references that point to `original`.\n> +                */\n\nThis is a good catch; I was wondering if there was a way to put this\nlogic into replay_revisions() so that other callers need not duplicate\nit, but since it just takes the revisions to walk over and that list\nis empty, it'd somehow need to know about original->object.oid; it\ndoesn't have that info.  Hmmm...\n\n> +               for (decoration = get_name_decoration(&original->object);\n> +                    decoration;\n> +                    decoration = decoration->next)\n> +               {\n> +                       if (decoration->type != DECORATION_REF_LOCAL)\n> +                               continue;\n> +\n> +                       ret = ref_transaction_update(transaction,\n> +                                                    decoration->name,\n> +                                                    &rewritten->object.oid,\n> +                                                    &original->object.oid,\n> +                                                    NULL, NULL, 0, reflog_msg, &err);\n> +                       if (ret) {\n> +                               ret = error(_(\"failed to update ref '%s': %s\"),\n> +                                           decoration->name, err.buf);\n> +                               goto out;\n> +                       }\n> +               }\n> +\n> +               if (ref_transaction_commit(transaction, &err)) {\n> +                       ret = error(_(\"failed to commit ref transaction: %s\"), err.buf);\n> +                       goto out;\n> +               }\n> +\n> +               break;\n> +       case REF_ACTION_HEAD:\n> +               ret = refs_update_ref(get_main_ref_store(repo), reflog_msg, \"HEAD\",\n> +                                     &updates.final_oid, &head->object.oid, 0,\n> +                                     UPDATE_REFS_MSG_ON_ERR);\n> +               if (ret)\n> +                       goto out;\n> +               break;\n\nAs noted above, you wouldn't need this separate REF_ACTION_HEAD block\nif you passed either \"HEAD\" or the refname it resolved to instead of\npassing its oid to the revision range.\n\n> +       case REF_ACTION_PRINT:\n> +               for (size_t i = 0; i < updates.nr; i++)\n> +                       printf(\"update %s %s %s\\n\",\n> +                              updates.items[i].refname,\n> +                              oid_to_hex(&updates.items[i].new_oid),\n> +                              oid_to_hex(&updates.items[i].old_oid));\n> +               break;\n> +       default:\n> +               BUG(\"unsupported ref action %d\", action);\n> +       }\n> +\n> +       ret = 0;\n> +\n> +out:\n> +       replay_ref_updates_release(&updates);\n> +       ref_transaction_free(transaction);\n> +       release_revisions(&revs);\n> +       strbuf_release(&err);\n> +       strvec_clear(&args);\n> +       return ret;\n> +}\n> +\n> +static int cmd_history_reword(int argc,\n> +                             const char **argv,\n> +                             const char *prefix,\n> +                             struct repository *repo)\n> +{\n> +       const char * const usage[] = {\n> +               GIT_HISTORY_REWORD_USAGE,\n> +               NULL,\n> +       };\n> +       enum ref_action action = REF_ACTION_DEFAULT;\n> +       struct option options[] = {\n> +               OPT_CALLBACK_F(0, \"ref-action\", &action, N_(\"<action>\"),\n> +                              N_(\"control ref update behavior (branches|head|print)\"),\n> +                              PARSE_OPT_NONEG, parse_ref_action),\n> +               OPT_END(),\n> +       };\n> +       struct strbuf reflog_msg = STRBUF_INIT;\n> +       struct commit *original, *rewritten;\n> +       int ret;\n> +\n> +       argc = parse_options(argc, argv, prefix, options, usage, 0);\n> +       if (argc != 1) {\n> +               ret = error(_(\"command expects a single revision\"));\n> +               goto out;\n> +       }\n> +       repo_config(repo, git_default_config, NULL);\n> +\n> +       original = lookup_commit_reference_by_name(argv[0]);\n> +       if (!original) {\n> +               ret = error(_(\"commit cannot be found: %s\"), argv[0]);\n> +               goto out;\n> +       }\n> +\n> +       ret = commit_tree_with_edited_message(repo, \"reworded\", original, &rewritten);\n> +       if (ret < 0) {\n> +               ret = error(_(\"failed writing reworded commit\"));\n> +               goto out;\n> +       }\n> +\n> +       strbuf_addf(&reflog_msg, \"reword: updating %s\", argv[0]);\n> +\n> +       ret = handle_reference_updates(action, repo, original, rewritten,\n> +                                      reflog_msg.buf);\n> +       if (ret < 0) {\n> +               ret = error(_(\"failed replaying descendants\"));\n> +               goto out;\n> +       }\n> +\n> +       ret = 0;\n> +\n> +out:\n> +       strbuf_release(&reflog_msg);\n> +       return ret;\n> +}\n>\n>  int cmd_history(int argc,\n>                 const char **argv,\n>                 const char *prefix,\n> -               struct repository *repo UNUSED)\n> +               struct repository *repo)\n>  {\n>         const char * const usage[] = {\n> -               N_(\"git history [<options>]\"),\n> +               GIT_HISTORY_REWORD_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_END(),\n>         };\n>\n>         argc = parse_options(argc, argv, prefix, options, usage, 0);\n> -       if (argc)\n> -               usagef(\"unrecognized argument: %s\", argv[0]);\n> -       return 0;\n> +       return fn(argc, argv, prefix, repo);\n>  }\n> diff --git a/replay.c b/replay.c\n> index 8c2f2d3710..5203f9db4c 100644\n> --- a/replay.c\n> +++ b/replay.c\n> @@ -254,7 +254,9 @@ int replay_revisions(struct repository *repo, struct rev_info *revs,\n>         struct commit *commit;\n>         struct commit *onto = NULL;\n>         struct merge_options merge_opt;\n> -       struct merge_result result;\n> +       struct merge_result result = {\n> +               .clean = 1,\n> +       };\n\nWait, what?  Why is this being initialized this way?\n\nSame as I said over in\nhttps://lore.kernel.org/git/CABPp-BEh7VEM6UQjkK3CxJcv54vEmueTmh9+-SyTKUxgy7Mkcg@mail.gmail.com/,\nwhy is this change here?  Was this due to hitting an empty range?\nActually, while supporting empty ranges didn't make sense back when I\nmentioned it to Siddharth (because users always specified the ranges),\nI think it actually does make sense now that ranges are implicit.\nSomeone could use \"git history reword HEAD\" (even if \"git commit\n--amend\" already exists), and that'd result in an empty range.  So, I\nthink the change makes sense now, but I think this particular change\nreally ought to be documented and motivated in a separate commit\nmessage rather than lumped in with the other changes in this commit.\n\n>         char *advance;\n>         int ret;\n>\n> @@ -270,7 +272,6 @@ int replay_revisions(struct repository *repo, struct rev_info *revs,\n>         }\n>\n>         init_basic_merge_options(&merge_opt, repo);\n> -       memset(&result, 0, sizeof(result));\n\nAnd this is related to the new initialization of result, so this\nchange would be part of that separate patch.\n\n\n>         merge_opt.show_rename_progress = 0;\n>         last_commit = onto;\n>         replayed_commits = kh_init_oid_map();\n> diff --git a/t/meson.build b/t/meson.build\n> index 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',\n> diff --git a/t/t3450-history.sh b/t/t3450-history.sh\n> index 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> -       git history >out 2>&1 &&\n> -       test_must_be_empty out\n> +       test_must_fail git history 2>err &&\n> +       test_grep \"need a subcommand\" err\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> +       test_grep \"unknown subcommand: .garbage.\" err\n>  '\n>\n>  test_done\n> diff --git a/t/t3451-history-reword.sh b/t/t3451-history-reword.sh\n> new file mode 100755\n> index 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> +       cat >message &&\n> +       write_script fake-editor.sh <<-\\EOF &&\n> +       cp message \"$1\"\n> +       EOF\n> +       test_set_editor \"$(pwd)\"/fake-editor.sh &&\n> +       git history reword \"$@\" &&\n> +       rm fake-editor.sh message\n> +}\n> +\n> +expect_graph () {\n> +       cat >expect &&\n> +       lib_test_cmp_graph --graph --format=%s \"$@\"\n> +}\n> +\n> +expect_log () {\n> +       git log --format=\"%s\" \"$@\" >actual &&\n> +       cat >expect &&\n> +       test_cmp expect actual\n> +}\n> +\n> +test_expect_success 'can reword tip of a branch' '\n> +       test_when_finished \"rm -rf repo\" &&\n> +       git init repo &&\n> +       (\n> +               cd repo &&\n> +               test_commit first &&\n> +               test_commit second &&\n> +               test_commit third &&\n> +\n> +               git symbolic-ref HEAD >expect &&\n> +               reword_with_message HEAD <<-EOF &&\n> +               third reworded\n> +               EOF\n> +               git symbolic-ref HEAD >actual &&\n> +               test_cmp expect actual &&\n> +\n> +               expect_log <<-\\EOF &&\n> +               third reworded\n> +               second\n> +               first\n> +               EOF\n> +\n> +               git reflog >reflog &&\n> +               test_grep \"reword: updating HEAD\" reflog\n> +       )\n> +'\n> +\n> +test_expect_success 'can reword commit in the middle' '\n> +       test_when_finished \"rm -rf repo\" &&\n> +       git init repo &&\n> +       (\n> +               cd repo &&\n> +               test_commit first &&\n> +               test_commit second &&\n> +               test_commit third &&\n> +\n> +               git symbolic-ref HEAD >expect &&\n> +               reword_with_message HEAD~ <<-EOF &&\n> +               second reworded\n> +               EOF\n> +               git symbolic-ref HEAD >actual &&\n> +               test_cmp expect actual &&\n> +\n> +               expect_log <<-\\EOF\n> +               third\n> +               second reworded\n> +               first\n> +               EOF\n> +       )\n> +'\n> +\n> +test_expect_success 'can reword root commit' '\n> +       test_when_finished \"rm -rf repo\" &&\n> +       git init repo &&\n> +       (\n> +               cd repo &&\n> +               test_commit first &&\n> +               test_commit second &&\n> +               test_commit third &&\n> +               reword_with_message HEAD~2 <<-EOF &&\n> +               first reworded\n> +               EOF\n> +\n> +               expect_log <<-\\EOF\n> +               third\n> +               second\n> +               first reworded\n> +               EOF\n> +       )\n> +'\n> +\n> +test_expect_success 'can reword in a bare repo' '\n> +       test_when_finished \"rm -rf repo repo.git\" &&\n> +       git init repo &&\n> +       test_commit -C repo first &&\n> +       git clone --bare repo repo.git &&\n> +       (\n> +               cd repo.git &&\n> +               reword_with_message HEAD <<-EOF &&\n> +               reworded\n> +               EOF\n> +\n> +               expect_log <<-\\EOF\n> +               reworded\n> +               EOF\n> +       )\n> +'\n> +\n> +test_expect_success 'can reword a commit on a different branch' '\n> +       test_when_finished \"rm -rf repo\" &&\n> +       git init repo &&\n> +       (\n> +               cd repo &&\n> +               test_commit base &&\n> +               git branch theirs &&\n> +               test_commit ours &&\n> +               git switch theirs &&\n> +               test_commit theirs &&\n> +\n> +               git rev-parse ours >ours-before &&\n> +               reword_with_message theirs <<-EOF &&\n> +               Reworded theirs\n> +               EOF\n> +               git rev-parse ours >ours-after &&\n> +               test_cmp ours-before ours-after &&\n> +\n> +               expect_graph --branches <<-\\EOF\n> +               * Reworded theirs\n> +               | * ours\n> +               |/\n> +               * base\n> +               EOF\n> +       )\n> +'\n> +\n> +test_expect_success 'can reword a merge commit' '\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> +\n> +               # It is not possible to replay merge commits embedded in the\n> +               # history (yet).\n> +               test_must_fail git history reword HEAD~ 2>err &&\n> +               test_grep \"replaying merge commits is not supported yet\" err &&\n> +\n> +               # But it is possible to reword a merge commit directly.\n> +               reword_with_message HEAD <<-EOF &&\n> +               Reworded merge commit\n> +               EOF\n> +               expect_graph <<-\\EOF\n> +               *   Reworded merge commit\n> +               |\\\n> +               | * theirs\n> +               * | ours\n> +               |/\n> +               * base\n> +               EOF\n> +       )\n> +'\n> +\n> +test_expect_success '--ref-action=print prints ref updates without modifying repo' '\n> +       test_when_finished \"rm -rf repo\" &&\n> +       git init repo --initial-branch=main &&\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> +\n> +               git refs list >refs-expect &&\n> +               reword_with_message --ref-action=print base >updates <<-\\EOF &&\n> +               reworded commit\n> +               EOF\n> +               git refs list >refs-actual &&\n> +               test_cmp refs-expect refs-actual &&\n> +\n> +               test_grep \"update refs/heads/branch\" updates &&\n> +               test_grep \"update refs/heads/main\" updates &&\n> +               git update-ref --stdin <updates &&\n> +               expect_log --branches <<-\\EOF\n> +               theirs\n> +               ours\n> +               reworded commit\n> +               EOF\n> +       )\n> +'\n> +\n> +test_expect_success '--ref-action=head updates only HEAD' '\n> +       test_when_finished \"rm -rf repo\" &&\n> +       git init repo --initial-branch=main &&\n> +       (\n> +               cd repo &&\n> +               test_commit base &&\n> +               git branch branch &&\n> +               test_commit theirs &&\n> +               git switch branch &&\n> +               test_commit ours &&\n> +\n> +               # When told to update HEAD, only, the command will refuse to\n> +               # rewrite commits that are not an ancestor of HEAD.\n> +               test_must_fail git history reword --ref-action=head theirs 2>err &&\n> +               test_grep \"rewritten commit must be an ancestor of HEAD\" err &&\n> +\n> +               reword_with_message --ref-action=head base >updates <<-\\EOF &&\n> +               reworded base\n> +               EOF\n> +               expect_log HEAD <<-\\EOF &&\n> +               ours\n> +               reworded base\n> +               EOF\n> +               expect_log main <<-\\EOF\n> +               theirs\n> +               base\n> +               EOF\n> +       )\n> +'\n> +\n> +test_expect_success 'editor shows proper status' '\n> +       test_when_finished \"rm -rf repo\" &&\n> +       git init repo &&\n> +       (\n> +               cd repo &&\n> +               test_commit first &&\n> +\n> +               write_script fake-editor.sh <<-\\EOF &&\n> +               cp \"$1\" . &&\n> +               printf \"\\namend a comment\\n\" >>\"$1\"\n> +               EOF\n> +               test_set_editor \"$(pwd)\"/fake-editor.sh &&\n> +               git history reword HEAD &&\n> +\n> +               cat >expect <<-EOF &&\n> +               first\n> +\n> +               # Please enter the commit message for the reworded changes. Lines starting\n> +               # with ${SQ}#${SQ} will be ignored, and an empty message aborts the commit.\n> +               # Changes to be committed:\n> +               #       new file:   first.t\n> +               #\n> +               EOF\n> +               test_cmp expect COMMIT_EDITMSG &&\n> +\n> +               test_commit_message HEAD <<-\\EOF\n> +               first\n> +\n> +               amend a comment\n> +               EOF\n> +       )\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> +       test_when_finished \"rm -rf repo\" &&\n> +       git init repo &&\n> +       (\n> +               cd repo &&\n> +               test_commit first &&\n> +               test_commit second &&\n> +               test_commit third &&\n> +\n> +               ORIG_PATH=\"$(pwd)\" &&\n> +               export ORIG_PATH &&\n> +               for hook in prepare-commit-msg pre-commit post-commit post-rewrite commit-msg\n> +               do\n> +                       write_script .git/hooks/$hook <<-\\EOF || exit 1\n> +                       touch \"$ORIG_PATH/hooks.log\n> +                       EOF\n> +               done &&\n> +\n> +               reword_with_message HEAD~ <<-EOF &&\n> +               second reworded\n> +               EOF\n> +\n> +               cat >expect <<-EOF &&\n> +               third\n> +               second reworded\n> +               first\n> +               EOF\n> +               git log --format=%s >actual &&\n> +               test_cmp expect actual &&\n> +\n> +               test_path_is_missing hooks.log\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> +               test_commit first &&\n> +\n> +               ! reword_with_message HEAD 2>err </dev/null &&\n> +               test_grep \"Aborting commit due to empty commit message.\" 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> +               touch a b &&\n> +               git add . &&\n> +               git commit -m \"initial commit\" &&\n> +               echo foo >a &&\n> +               echo bar >b &&\n> +               git add b &&\n> +               reword_with_message HEAD <<-EOF &&\n> +               message\n> +               EOF\n> +               cat >expect <<-\\EOF &&\n> +                M a\n> +               M  b\n> +               ?? actual\n> +               ?? expect\n> +               EOF\n> +               git status --porcelain >actual &&\n> +               test_cmp expect actual\n> +       )\n> +'\n> +\n> +test_done\n>\n> --\n> 2.52.0.542.g9473a8513b.dirty\n"},{"id":"533618","messageId":"aWTxI-j__VGkPrVb@pks.im","threadId":"64739","inReplyTo":"CABPp-BHFwvg5A295kXkc_axoibNhGDn4ZUkm0uE1u+358xSZzw@mail.gmail.com","subject":"Re: [PATCH v8 7/7] builtin/history: implement \"reword\" subcommand","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2026-01-12T13:03:31Z","receivedAt":"2026-01-12T13:03:38Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Fri, Jan 09, 2026 at 05:20:04PM -0800, Elijah Newren wrote:\n> On Wed, Jan 7, 2026 at 2:10 AM Patrick Steinhardt <ps@pks.im> wrote:\n> > diff --git a/Documentation/git-history.adoc b/Documentation/git-history.adoc\n> > index 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> Ah, was the previous sentence here from the former patch just a bad\n> splitting when you were rewriting?\n\nDunno what happened here, to be honest.\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> > +       Rewrite the commit message of the specified commit. All the other\n> > +       details of this commit remain unchanged. This command will spawn an\n> > +       editor with the current message of that commit.\n> \n> One isn't exactly \"several\"; I know you'll add more later, but since\n> this series ends here, should that word be changed?\n\nWe can just say \"The following commands\", there is no need to be\nspecific.\n\n> > +\n> > +OPTIONS\n> > +-------\n> > +\n> > +`--ref-action=(branches|head|print)`::\n> > +       Control which references will be updated by the command, if any. With\n> > +       `branches`, all local branches that point to commits which are\n> > +       decendants of the original commit will be rewritten. With `head`, only\n> \n> decendants -> descendants .   Or maybe double down on the typo and\n> extend it a bit into either 'decedent' or 'decadent'.  That could be\n> fun.\n\nThanks, this made me laugh. :)\n\n> > diff --git a/builtin/history.c b/builtin/history.c\n> > index f6fe32610b..59011ea517 100644\n> > --- a/builtin/history.c\n> > +++ b/builtin/history.c\n> > @@ -1,22 +1,404 @@\n[snip]\n> > +static int commit_tree_with_edited_message(struct repository *repo,\n> > +                                          const char *action,\n> > +                                          struct commit *original,\n> > +                                          struct commit **out)\n> > +{\n> > +       const char *exclude_gpgsig[] = { \"gpgsig\", \"gpgsig-sha256\", NULL };\n> > +       const char *original_message, *original_body, *ptr;\n> > +       struct commit_extra_header *original_extra_headers = NULL;\n> > +       struct strbuf commit_message = STRBUF_INIT;\n> > +       struct object_id rewritten_commit_oid;\n> > +       struct object_id original_tree_oid;\n> > +       struct object_id parent_tree_oid;\n> > +       char *original_author = NULL;\n> > +       struct commit *parent;\n> > +       size_t len;\n> > +       int ret;\n> > +\n> > +       original_tree_oid = repo_get_commit_tree(repo, original)->object.oid;\n> > +\n> > +       parent = original->parents ? original->parents->item : NULL;\n> > +       if (parent) {\n> > +               if (repo_parse_commit(repo, parent)) {\n> > +                       ret = error(_(\"unable to parse parent commit %s\"),\n> > +                                   oid_to_hex(&parent->object.oid));\n> > +                       goto out;\n> > +               }\n> > +\n> > +               parent_tree_oid = repo_get_commit_tree(repo, parent)->object.oid;\n> > +       } else {\n> > +               oidcpy(&parent_tree_oid, repo->hash_algo->empty_tree);\n> > +       }\n> > +\n> > +       /* We retain authorship of the original commit. */\n> > +       original_message = repo_logmsg_reencode(repo, original, NULL, NULL);\n> > +       ptr = find_commit_header(original_message, \"author\", &len);\n> > +       if (ptr)\n> > +               original_author = xmemdupz(ptr, len);\n> > +       find_commit_subject(original_message, &original_body);\n> > +\n> > +       ret = fill_commit_message(repo, &parent_tree_oid, &original_tree_oid,\n> > +                                 original_body, action, &commit_message);\n> > +       if (ret < 0)\n> > +               goto out;\n> > +\n> > +       original_extra_headers = read_commit_extra_headers(original, exclude_gpgsig);\n> \n> Does this grab encoding?  If so, should it be excluded as well given\n> the repo_logmsg_reencode() call?\n\nHm, good question indeed. I think that makes sense.\n\n[snip]\n> > +       } else {\n> > +               strvec_push(&args, \"--branches\");\n> > +       }\n> > +\n> > +       setup_revisions_from_strvec(&args, &revs, NULL);\n> > +       if (revs.nr)\n> > +               BUG(\"revisions were set up with invalid argument '%s'\", args.v[0]);\n> > +\n> > +       opts.onto = oid_to_hex_r(hex, &rewritten->object.oid);\n> > +\n> > +       ret = replay_revisions(repo, &revs, &opts, &updates);\n> > +       if (ret)\n> > +               goto out;\n> > +\n> > +       switch (action) {\n> > +       case REF_ACTION_DEFAULT:\n> > +       case REF_ACTION_BRANCHES:\n> > +               transaction = ref_store_transaction_begin(get_main_ref_store(repo), 0, &err);\n> > +               if (!transaction) {\n> > +                       ret = error(_(\"failed to begin ref transaction: %s\"), err.buf);\n> > +                       goto out;\n> > +               }\n> > +\n> > +               for (size_t i = 0; i < updates.nr; i++) {\n> > +                       ret = ref_transaction_update(transaction,\n> > +                                                    updates.items[i].refname,\n> > +                                                    &updates.items[i].new_oid,\n> > +                                                    &updates.items[i].old_oid,\n> > +                                                    NULL, NULL, 0, reflog_msg, &err);\n> > +                       if (ret) {\n> > +                               ret = error(_(\"failed to update ref '%s': %s\"),\n> > +                                           updates.items[i].refname, err.buf);\n> > +                               goto out;\n> > +                       }\n> > +               }\n> > +\n> > +               /*\n> > +                * `replay_revisions()` only updates references that are\n> > +                * ancestors of `rewritten`, so we need to manually\n> > +                * handle updating references that point to `original`.\n> > +                */\n> \n> This is a good catch; I was wondering if there was a way to put this\n> logic into replay_revisions() so that other callers need not duplicate\n> it, but since it just takes the revisions to walk over and that list\n> is empty, it'd somehow need to know about original->object.oid; it\n> doesn't have that info.  Hmmm...\n\nYeah, exactly. I was also thinking about whether this can be part of\ngit-replay(1), but ultimately it didn't really seem to make sense. After\nall this is about a commit that we're _not_ replaying at all, but that\nwe have manually edited.\n\n> > diff --git a/replay.c b/replay.c\n> > index 8c2f2d3710..5203f9db4c 100644\n> > --- a/replay.c\n> > +++ b/replay.c\n> > @@ -254,7 +254,9 @@ int replay_revisions(struct repository *repo, struct rev_info *revs,\n> >         struct commit *commit;\n> >         struct commit *onto = NULL;\n> >         struct merge_options merge_opt;\n> > -       struct merge_result result;\n> > +       struct merge_result result = {\n> > +               .clean = 1,\n> > +       };\n> \n> Wait, what?  Why is this being initialized this way?\n> \n> Same as I said over in\n> https://lore.kernel.org/git/CABPp-BEh7VEM6UQjkK3CxJcv54vEmueTmh9+-SyTKUxgy7Mkcg@mail.gmail.com/,\n> why is this change here?  Was this due to hitting an empty range?\n\nYeah, exactly.\n\n> Actually, while supporting empty ranges didn't make sense back when I\n> mentioned it to Siddharth (because users always specified the ranges),\n> I think it actually does make sense now that ranges are implicit.\n> Someone could use \"git history reword HEAD\" (even if \"git commit\n> --amend\" already exists), and that'd result in an empty range.  So, I\n> think the change makes sense now, but I think this particular change\n> really ought to be documented and motivated in a separate commit\n> message rather than lumped in with the other changes in this commit.\n\nThat's fair, will do.\n\nThanks!\n\nPatrick\n"}]}