From: Patrick Steinhardt Date: Tue, 13 Jan 2026 09:54:31 GMT Subject: [PATCH v11 0/8] Introduce git-history(1) command for easy history editing Message-ID: <20260113-b4-pks-history-builtin-v11-0-e74ebfa2652d@pks.im> In-Reply-To: <20250819-b4-pks-history-builtin-v1-0-9b77c32688fe@pks.im> Hi, over recent months I've been playing around with Jujutsu quite frequently. While I still prefer using Git, there's been a couple features in it that I really like and that I'd like to have in Git, as well. A copule of these features relate to history editing. Most importantly, I really dig the following commands: - jj-abandon(1) to drop a specific commit from your history. - jj-absorb(1) to take some changes and automatically apply them to commits in your history that last modified the respective hunks. - jj-split(1) to split a commit into two. - jj-new(1) to insert a new commit after or before a specific other commit. Not all of these commands can be ported directly into Git. jj-new(1) for example doesn't really make a ton of sense for us, I'd claim. But some of these commands _do_ make sense. This patch series is a starting point for such a command. For now, it only implements rewording of commits. I'll continue introducing new commands once this patch series has been merged though. Changes in v11: - Fix overly clever `BUG()` condition. - Drop the `struct replay_result::merge_conflict` field. - Return a positive value from `replay_revisions()` in case there was a conflict. - Improve a commit message. - Fix check for whether `setup_revisions_from_strvec()` was successful. - Link to v10: https://lore.kernel.org/r/20260112-b4-pks-history-builtin-v10-0-e3c6aa5b4cec@pks.im Changes in v10: - Split out changes that make `replay_revisions()` handle empty revision ranges properly. - Drop redundant `struct repository` parameter in `replay_revisions()`. - Move around `die_for_incompatible_opt2()`. - Fix up some of the headers in "replay.c". - Split out the changes that make `replay_revisions()` handle empty revision ranges properly. - Create a separate commit that fixes updating detached HEAD. - Strip "encoding" header as we're reencoding commit messages. - Merge the logic to update refs in in `REF_ACTION_HEAD` and `REF_ACTION_BRANCHES` modes. - Link to v9: https://lore.kernel.org/r/20260109-b4-pks-history-builtin-v9-0-8766101814c6@pks.im Changes in v9: - Rename `struct replay_ref_updates` to `struct replay_result` to make its semantics less focussed on ref updates, only. - Clarify and fix return codes of git-replay(1) so that we return 1 on conflict, 128 on an error and 0 on success. - The usual small improvements to commit messages. - Link to v8: https://lore.kernel.org/r/20260107-b4-pks-history-builtin-v8-0-18e9779e3a26@pks.im Changes in v8: - The series is now built on top of e0bfec3dfc (The 15th batch, 2026-01-06) with kh/replay-invalid-onto-advance at 56b77a687e (t3650: add more regression tests for failure conditions, 2026-01-05) merged into it. The latter is expected to hit 'next' soonish. - Another biggish revamp to build almost completely on top of the git-replay(1) logic. - We now advance all branches descending from the rewritten commit by default. - I've dropped the patches introducing `git history split` and will send this as a follow-up patch series once this once has been merged. This was done to focus attention on the underlying mechanics as much as possible (and to keep my own sanity with the frequent revamps). - Link to v7: https://lore.kernel.org/r/20251203-b4-pks-history-builtin-v7-0-9e9f849bfd0e@pks.im Changes in v7: - I've rebased the patch series on top of f0ef5b6d9b (The fifth batch, 2025-11-30) with pw/replay-exclude-gpgsig-fix at 9f3a115087 (replay: do not copy "gpgsign-sha256" header, 2025-11-26) merged into it to fix some conflicts. - I refactored a bunch of code to be shared between split and reword. - I am now using "--ancestry-path" and "--reverse" to enumerate commits. - `git history split` now asks for commit messages for both commits. - `git history split` no longer allows the user to edit hunks so that conflicts become impossible. This may be reintroduced at a later point in time. - A bunch of test improvements. - Several commit message and documentation improvements. - Link to v6: https://lore.kernel.org/r/20251027-b4-pks-history-builtin-v6-0-407dd3f57ad3@pks.im Changes in v6: - I've rebased the patch series again to pull in the latest updates from sa/replay-atomic-ref-updates and fix conflicts. It is now based on 4e98b730f1 (The twenty-fourth batch, 2025-10-24) with ab661bb1bb (replay: add replay.refAction config option, 2025-10-23) merged into it. - I've dropped the "-m" options for now, so commit messages are always asked for via the editor. These can be introduced in a subsequent patch series once discussion around them has settled. - We don't use the merge machinery anymore to pick the commits. - Drop the commit to parse commits in the replay machinery. It didn't seem to be necessary in v5 anymore, and now that we don't use the merge machinery at all we don't ever take that code path in the first place. - Link to v5: https://lore.kernel.org/r/20251021-b4-pks-history-builtin-v5-0-78d23f578fe6@pks.im Changes in v5: - I've changed the patch series to be based on top of 133d151831 (The twenty-first batch, 2025-10-20) with sa/replay-atomic-ref-updates at a1c22e627e (SQAUASH??? t0450 band-aid, 2025-10-14) merged into it. This is one the one hand to fix a conflict, but also to get some of the CI updates to make GitLab CI work again. - Some slight commit message improvements. - Deduplicate subcommand usage strings by using defines. - Fix the desendancy checks to properly verify that HEAD is a descendant of the commit to be rewritten. Also add some tests for this. - Fix the hint that mentions that lines starting with the comment character will be tripped after having written the commit message. - Move an include to the correct commit. - Link to v4: https://lore.kernel.org/r/20251001-b4-pks-history-builtin-v4-0-8e61ddb86317@pks.im Changes in v4: - I've rebuilt the patch series. It is now based on 821f583da6 (The thirteenth batcn, 2025-09-29) with sa/replay-atomic-ref-updates at 665c66a743 (replay: make atomic ref updates the default behavior, 2025-09-27) merged into it. This should fix all conflicts with seen. - I've reworked this patch series to use the same infra as git-replay(1), as discussed during the Contributor's Summit. - I've slimmed down the patch series to only tackle those commands that cannot result in a conflict to keep it simple. I also learned that Elijah has been working on a "git replay edit" command, so I dropped that command so that we can instead use his version. - During the Contributor's Summit we have agreed that for now, we won't care about hook execution just yet. This may be backfilled at a later point in time. - I dropped "commit.verbose" handling for now, as my understanding of it was wrong at first. This is something we should backfill. - Link to v3: https://lore.kernel.org/r/20250904-b4-pks-history-builtin-v3-0-509053514755@pks.im Changes in v3: - Add logic to drive the "post-rewrite" hook and add tests to verify that all hooks are executed as expected. - Deduplicate logic to turn a replay action into a todo command. - Move the addition of tests for the top-level git-history(1) command to the correct commit. - Some smaller commit message fixes. - Honor "commit.verbose". - Fix copy-paste error with an error message. - Link to v2: https://lore.kernel.org/r/20250824-b4-pks-history-builtin-v2-0-964ac12f65bd@pks.im Changes in v2: - Add a new "reword" subcommand. - List git-history(1) in "command-list.txt". - Add some missing error handling. - Simplify calling convention of `apply_commits()` to handle root commits internally instead of requiring every caller to do so. - Add tests to verify that git-history(1) refuses to work with changes in the worktree or index. - Mark git-history(1) as experimental. - Introduce commands to manage interrupted history edits. - A bunch of improvements to the manpage. - Link to v1: https://lore.kernel.org/r/20250819-b4-pks-history-builtin-v1-0-9b77c32688fe@pks.im Thanks! Patrick --- Patrick Steinhardt (8): builtin/replay: extract core logic to replay revisions builtin/replay: move core logic into "libgit.a" replay: small set of cleanups replay: support empty commit ranges replay: support updating detached HEAD wt-status: provide function to expose status for trees builtin: add new "history" command builtin/history: implement "reword" subcommand .gitignore | 1 + Documentation/git-history.adoc | 73 +++++++ Documentation/meson.build | 1 + Makefile | 2 + builtin.h | 1 + builtin/history.c | 427 +++++++++++++++++++++++++++++++++++++++++ builtin/replay.c | 373 +++-------------------------------- command-list.txt | 1 + git.c | 1 + meson.build | 2 + replay.c | 371 +++++++++++++++++++++++++++++++++++ replay.h | 61 ++++++ t/meson.build | 2 + t/t3450-history.sh | 17 ++ t/t3451-history-reword.sh | 391 +++++++++++++++++++++++++++++++++++++ t/t3650-replay-basics.sh | 9 + wt-status.c | 24 +++ wt-status.h | 9 + 18 files changed, 1425 insertions(+), 341 deletions(-) Range-diff versus v10: 1: f02aa5abd1 ! 1: bdefcad12d builtin/replay: extract core logic to replay revisions @@ builtin/replay.c: static void set_up_replay_mode(struct repository *repo, - die_for_incompatible_opt2(!!onto_name, "--onto", - !!*advance_name, "--advance"); -+ if (!(!!onto_name ^ !!*advance_name)) -+ BUG("expected either onto_name or *advance_name in this function"); ++ if (!onto_name == !*advance_name) ++ BUG("one and only one of onto_name and *advance_name must be given"); + if (onto_name) { *onto = peel_committish(repo, onto_name, "--onto"); @@ builtin/replay.c: static struct commit *pick_regular_commit(struct repository *r + struct object_id new_oid; + } *updates; + size_t updates_nr, updates_alloc; -+ -+ bool merge_conflict; +}; + +static void replay_result_release(struct replay_result *result) @@ builtin/replay.c: static struct commit *pick_regular_commit(struct repository *r + } + + if (!result.clean) { -+ out->merge_conflict = true; -+ ret = -1; ++ ret = 1; + goto out; + } + @@ builtin/replay.c: int cmd_replay(int argc, - free(update_refs); - } - ret = result.clean; -- ++ ret = 0; + cleanup: if (transaction) ref_transaction_free(transaction); @@ builtin/replay.c: int cmd_replay(int argc, release_revisions(&revs); - free(advance_name); -- /* Return */ -- if (ret < 0) -- exit(128); + /* Return */ + if (ret < 0) + exit(128); - return ret ? 0 : 1; -+ if (ret) { -+ if (result.merge_conflict) -+ return 1; -+ return 128; -+ } -+ -+ return 0; ++ return ret; } 2: 078d713391 ! 2: 80eaf3f9fd builtin/replay: move core logic into "libgit.a" @@ builtin/replay.c - if (!rinfo.positive_refexprs) - die(_("need some commits to replay")); - -- if (!(!!onto_name ^ !!*advance_name)) -- BUG("expected either onto_name or *advance_name in this function"); +- if (!onto_name == !*advance_name) +- BUG("one and only one of onto_name and *advance_name must be given"); - - if (onto_name) { - *onto = peel_committish(repo, onto_name, "--onto"); @@ builtin/replay.c - struct object_id new_oid; - } *updates; - size_t updates_nr, updates_alloc; -- -- bool merge_conflict; -}; - -static void replay_result_release(struct replay_result *result) @@ builtin/replay.c - } - - if (!result.clean) { -- out->merge_conflict = true; -- ret = -1; +- ret = 1; - goto out; - } - @@ replay.c (new) + if (!rinfo.positive_refexprs) + die(_("need some commits to replay")); + -+ if (!(!!onto_name ^ !!*advance_name)) -+ BUG("expected either onto_name or *advance_name in this function"); ++ if (!onto_name == !*advance_name) ++ BUG("one and only one of onto_name and *advance_name must be given"); + + if (onto_name) { + *onto = peel_committish(repo, onto_name, "--onto"); @@ replay.c (new) + } + + if (!result.clean) { -+ out->merge_conflict = true; -+ ret = -1; ++ ret = 1; + goto out; + } + @@ replay.h (new) + struct object_id new_oid; + } *updates; + size_t updates_nr, updates_alloc; -+ -+ /* Set to true in case the replay failed with a merge conflict. */ -+ bool merge_conflict; +}; + +void replay_result_release(struct replay_result *result); @@ replay.h (new) + * index and references untouched. Reference updates caused by the replay will + * be recorded in the `updates` out pointer. + * -+ * Returns 0 on success, a negative error code otherwise. ++ * Returns 0 on success, 1 on conflict and a negative error code otherwise. + */ +int replay_revisions(struct rev_info *revs, + struct replay_revisions_options *opts, 3: 44f96f6b93 = 3: bd6783570b replay: small set of cleanups 4: 0c861b1a37 ! 4: e927f66224 replay: support empty commit ranges @@ Commit message In a subsequent commit we're about to introduce a new user of the replay subsystem. With that new user, the range of commits that we'll want to - replay will be identified implicitly via "HEAD". With such implicit - ranges it becomes likely that the range of revisions that we're asked to - replay becomes empty. This case does not make sense with git-replay(1), - but with the new command it will. + replay will be identified implicitly via a single commit, and will + include all descendants of that commit to any branch. If that commit has + no descendants (because it's the tip of some branch), then the range of + revisions that we're asked to replay becomes empty. This case does not + make sense with git-replay(1), but with the new command it will. This case is not currently supported by `replay_revisions()` though because we zero-initialize `struct merge_result`. This includes its 5: 03a64727ab = 5: c8ed32bfae replay: support updating detached HEAD 6: 453a5769a6 = 6: 79f8bf7613 wt-status: provide function to expose status for trees 7: f73ad9a468 = 7: d0d56cc4fc builtin: add new "history" command 8: e7af330353 ! 8: 464f5467ab builtin/history: implement "reword" subcommand @@ builtin/history.c + } + + setup_revisions_from_strvec(&args, &revs, NULL); -+ if (revs.nr) -+ BUG("revisions were set up with invalid argument '%s'", args.v[0]); ++ if (args.nr != 1) ++ BUG("revisions were set up with invalid argument"); + + opts.onto = oid_to_hex_r(hex, &rewritten->object.oid); + --- base-commit: 02bdc12118c43e0a65258eb742492965859bffb6 change-id: 20250819-b4-pks-history-builtin-83398f9a05f0