git/list[1] front-page[2] threads[3] people[4] search[5] about
 

[PATCH v10 0/8] Introduce git-history(1) command for easy history editing

From
Patrick Steinhardt <ps@pks.im>
Date
Jan 12, 2026, 14:15 UTC
Message-ID
<20260112-b4-pks-history-builtin-v10-0-e3c6aa5b4cec@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 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               | 382 ++++--------------------------------
 command-list.txt               |   1 +
 git.c                          |   1 +
 meson.build                    |   2 +
 replay.c                       | 372 +++++++++++++++++++++++++++++++++++
 replay.h                       |  64 ++++++
 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, 1434 insertions(+), 345 deletions(-)
Range-diff versus v9:
1:  0cae2364ee ! 1:  99b47e1af2 builtin/replay: extract core logic to replay revisions
    @@ Commit message
         Signed-off-by: Patrick Steinhardt <ps@pks.im>
     
      ## builtin/replay.c ##
    -@@ builtin/replay.c: static enum ref_action_mode get_ref_action_mode(struct repository *repo, const c
    - 	return REF_ACTION_UPDATE;
    +@@ builtin/replay.c: static void set_up_replay_mode(struct repository *repo,
    + 	if (!rinfo.positive_refexprs)
    + 		die(_("need some commits to replay"));
    + 
    +-	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) {
    + 		*onto = peel_committish(repo, onto_name, "--onto");
    + 		if (rinfo.positive_refexprs <
    +@@ builtin/replay.c: static struct commit *pick_regular_commit(struct repository *repo,
    + 	return create_commit(repo, result->tree, pickme, replayed_base);
      }
      
     +struct replay_revisions_options {
    @@ builtin/replay.c: static enum ref_action_mode get_ref_action_mode(struct reposit
     +	result->updates_nr++;
     +}
     +
    -+static int replay_revisions(struct repository *repo, struct rev_info *revs,
    ++static int replay_revisions(struct rev_info *revs,
     +			    struct replay_revisions_options *opts,
     +			    struct replay_result *out)
     +{
    @@ builtin/replay.c: static enum ref_action_mode get_ref_action_mode(struct reposit
     +	int ret;
     +
     +	advance = xstrdup_or_null(opts->advance);
    -+	set_up_replay_mode(repo, &revs->cmdline, opts->onto, &advance,
    ++	set_up_replay_mode(revs->repo, &revs->cmdline, opts->onto, &advance,
     +			   &onto, &update_refs);
     +
     +	/* FIXME: Should allow replaying commits with the first as a root commit */
    @@ builtin/replay.c: static enum ref_action_mode get_ref_action_mode(struct reposit
     +		goto out;
     +	}
     +
    -+	init_basic_merge_options(&merge_opt, repo);
    ++	init_basic_merge_options(&merge_opt, revs->repo);
     +	memset(&result, 0, sizeof(result));
     +	merge_opt.show_rename_progress = 0;
     +	last_commit = onto;
    @@ builtin/replay.c: static enum ref_action_mode get_ref_action_mode(struct reposit
     +		if (commit->parents->next)
     +			die(_("replaying merge commits is not supported yet!"));
     +
    -+		last_commit = pick_regular_commit(repo, commit, replayed_commits,
    ++		last_commit = pick_regular_commit(revs->repo, commit, replayed_commits,
     +						  onto, &merge_opt, &result);
     +		if (!last_commit)
     +			break;
    @@ builtin/replay.c: static enum ref_action_mode get_ref_action_mode(struct reposit
     +	return ret;
     +}
     +
    - static int handle_ref_update(enum ref_action_mode mode,
    - 			     struct ref_transaction *transaction,
    - 			     const char *refname,
    + static enum ref_action_mode parse_ref_action_mode(const char *ref_action, const char *source)
    + {
    + 	if (!ref_action || !strcmp(ref_action, "update"))
     @@ builtin/replay.c: int cmd_replay(int argc,
      	       const char *prefix,
      	       struct repository *repo)
    @@ builtin/replay.c: int cmd_replay(int argc,
     -				  contained, "--contained");
     +	die_for_incompatible_opt2(!!opts.advance, "--advance",
     +				  opts.contained, "--contained");
    ++	die_for_incompatible_opt2(!!opts.advance, "--advance",
    ++				  !!opts.onto, "--onto");
      
      	/* Parse ref action mode from command line or config */
      	ref_mode = get_ref_action_mode(repo, ref_action);
    @@ builtin/replay.c: int cmd_replay(int argc,
     -			   &onto, &update_refs);
     -
     -	/* FIXME: Should allow replaying commits with the first as a root commit */
    -+	ret = replay_revisions(repo, &revs, &opts, &result);
    ++	ret = replay_revisions(&revs, &opts, &result);
     +	if (ret)
     +		goto cleanup;
      
2:  c5fca546e5 ! 2:  0945b53ecb builtin/replay: move core logic into "libgit.a"
    @@ builtin/replay.c
     -	if (!rinfo.positive_refexprs)
     -		die(_("need some commits to replay"));
     -
    --	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) {
     -		*onto = peel_committish(repo, onto_name, "--onto");
     -		if (rinfo.positive_refexprs <
    @@ builtin/replay.c
     -	return create_commit(repo, result->tree, pickme, replayed_base);
     -}
     -
    - static enum ref_action_mode parse_ref_action_mode(const char *ref_action, const char *source)
    - {
    - 	if (!ref_action || !strcmp(ref_action, "update"))
    -@@ builtin/replay.c: static enum ref_action_mode get_ref_action_mode(struct repository *repo, const c
    - 	return REF_ACTION_UPDATE;
    - }
    - 
     -struct replay_revisions_options {
     -	const char *advance;
     -	const char *onto;
    @@ builtin/replay.c: static enum ref_action_mode get_ref_action_mode(struct reposit
     -	result->updates_nr++;
     -}
     -
    --static int replay_revisions(struct repository *repo, struct rev_info *revs,
    +-static int replay_revisions(struct rev_info *revs,
     -			    struct replay_revisions_options *opts,
     -			    struct replay_result *out)
     -{
    @@ builtin/replay.c: static enum ref_action_mode get_ref_action_mode(struct reposit
     -	int ret;
     -
     -	advance = xstrdup_or_null(opts->advance);
    --	set_up_replay_mode(repo, &revs->cmdline, opts->onto, &advance,
    +-	set_up_replay_mode(revs->repo, &revs->cmdline, opts->onto, &advance,
     -			   &onto, &update_refs);
     -
     -	/* FIXME: Should allow replaying commits with the first as a root commit */
    @@ builtin/replay.c: static enum ref_action_mode get_ref_action_mode(struct reposit
     -		goto out;
     -	}
     -
    --	init_basic_merge_options(&merge_opt, repo);
    +-	init_basic_merge_options(&merge_opt, revs->repo);
     -	memset(&result, 0, sizeof(result));
     -	merge_opt.show_rename_progress = 0;
     -	last_commit = onto;
    @@ builtin/replay.c: static enum ref_action_mode get_ref_action_mode(struct reposit
     -		if (commit->parents->next)
     -			die(_("replaying merge commits is not supported yet!"));
     -
    --		last_commit = pick_regular_commit(repo, commit, replayed_commits,
    +-		last_commit = pick_regular_commit(revs->repo, commit, replayed_commits,
     -						  onto, &merge_opt, &result);
     -		if (!last_commit)
     -			break;
    @@ builtin/replay.c: static enum ref_action_mode get_ref_action_mode(struct reposit
     -	return ret;
     -}
     -
    - static int handle_ref_update(enum ref_action_mode mode,
    - 			     struct ref_transaction *transaction,
    - 			     const char *refname,
    + static enum ref_action_mode parse_ref_action_mode(const char *ref_action, const char *source)
    + {
    + 	if (!ref_action || !strcmp(ref_action, "update"))
     
      ## meson.build ##
     @@ meson.build: libgit_sources = [
    @@ replay.c (new)
     +#include "hex.h"
     +#include "merge-ort.h"
     +#include "object-name.h"
    -+#include "oidset.h"
    -+#include "parse-options.h"
     +#include "refs.h"
     +#include "replay.h"
     +#include "revision.h"
    ++#include "strmap.h"
     +#include "tree.h"
     +
     +static const char *short_commit_name(struct repository *repo,
    @@ replay.c (new)
     +	if (!rinfo.positive_refexprs)
     +		die(_("need some commits to replay"));
     +
    -+	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) {
     +		*onto = peel_committish(repo, onto_name, "--onto");
     +		if (rinfo.positive_refexprs <
    @@ replay.c (new)
     +	result->updates_nr++;
     +}
     +
    -+int replay_revisions(struct repository *repo, struct rev_info *revs,
    ++int replay_revisions(struct rev_info *revs,
     +		     struct replay_revisions_options *opts,
     +		     struct replay_result *out)
     +{
    @@ replay.c (new)
     +	int ret;
     +
     +	advance = xstrdup_or_null(opts->advance);
    -+	set_up_replay_mode(repo, &revs->cmdline, opts->onto, &advance,
    ++	set_up_replay_mode(revs->repo, &revs->cmdline, opts->onto, &advance,
     +			   &onto, &update_refs);
     +
     +	/* FIXME: Should allow replaying commits with the first as a root commit */
    @@ replay.c (new)
     +		goto out;
     +	}
     +
    -+	init_basic_merge_options(&merge_opt, repo);
    ++	init_basic_merge_options(&merge_opt, revs->repo);
     +	memset(&result, 0, sizeof(result));
     +	merge_opt.show_rename_progress = 0;
     +	last_commit = onto;
    @@ replay.c (new)
     +		if (commit->parents->next)
     +			die(_("replaying merge commits is not supported yet!"));
     +
    -+		last_commit = pick_regular_commit(repo, commit, replayed_commits,
    ++		last_commit = pick_regular_commit(revs->repo, commit, replayed_commits,
     +						  onto, &merge_opt, &result);
     +		if (!last_commit)
     +			break;
    @@ replay.h (new)
     + *
     + * Returns 0 on success, a negative error code otherwise.
     + */
    -+int replay_revisions(struct repository *repo, struct rev_info *revs,
    ++int replay_revisions(struct rev_info *revs,
     +		     struct replay_revisions_options *opts,
     +		     struct replay_result *out);
     +
3:  1d55aab054 = 3:  85ebe5853f replay: small set of cleanups
4:  451717f951 < -:  ---------- replay: yield the object ID of the final rewritten commit
-:  ---------- > 4:  a24641dbfb replay: support empty commit ranges
-:  ---------- > 5:  e0ab803a46 replay: support updating detached HEAD
5:  7cde288c8d = 6:  abc33b25ca wt-status: provide function to expose status for trees
6:  b315c04169 ! 7:  387d1ded10 builtin: add new "history" command
    @@ Documentation/git-history.adoc (new)
     +your commit history that is simpler to use compared to linkgit:git-rebase[1] in
     +general.
     +
    -+If you want to reapply a range of commits onto a different base, or interactive
    -+rebases if you want to edit a range of commits.
    ++Use linkgit:git-rebase[1] if you want to reapply a range of commits onto a
    ++different base, or interactive rebases if you want to edit a range of commits
    ++at once.
     +
     +LIMITATIONS
     +-----------
    @@ Documentation/git-history.adoc (new)
     +COMMANDS
     +--------
     +
    -+Several commands are available to rewrite history in different ways:
    ++No commands are supported yet.
     +
     +GIT
     +---
7:  f71911586d ! 8:  aeb895475b builtin/history: implement "reword" subcommand
    @@ Commit message
             rewritten. This especially helps with workflows that use stacked
             branches.
     
    +    Helped-by: Elijah Newren <newren@gmail.com>
         Signed-off-by: Patrick Steinhardt <ps@pks.im>
     
      ## Documentation/git-history.adoc ##
    @@ Documentation/git-history.adoc: git-history - EXPERIMENTAL: Rewrite history
      
      DESCRIPTION
      -----------
    -@@ Documentation/git-history.adoc: Overall, linkgit:git-history[1] aims to provide a more opinionated way to modify
    - your commit history that is simpler to use compared to linkgit:git-rebase[1] in
    - general.
    - 
    --If you want to reapply a range of commits onto a different base, or interactive
    --rebases if you want to edit a range of commits.
    -+Use linkgit:git-rebase[1] if you want to reapply a range of commits onto a
    -+different base, or interactive rebases if you want to edit a range of commits
    -+at once.
    - 
    - LIMITATIONS
    - -----------
    -@@ Documentation/git-history.adoc: COMMANDS
    - 
    - Several commands are available to rewrite history in different ways:
    +@@ Documentation/git-history.adoc: first-class conflicts.
    + COMMANDS
    + --------
      
    +-No commands are supported yet.
    ++The following commands are available to rewrite history in different ways:
    ++
     +`reword <commit>`::
     +	Rewrite the commit message of the specified commit. All the other
     +	details of this commit remain unchanged. This command will spawn an
    @@ Documentation/git-history.adoc: COMMANDS
     +`--ref-action=(branches|head|print)`::
     +	Control which references will be updated by the command, if any. With
     +	`branches`, all local branches that point to commits which are
    -+	decendants of the original commit will be rewritten. With `head`, only
    ++	descendants of the original commit will be rewritten. With `head`, only
     +	the current `HEAD` reference will be rewritten. With `print`, all
     +	updates as they would be performed with `branches` are printed in a
     +	format that can be consumed by linkgit:git-update-ref[1].
    -+
    + 
      GIT
      ---
    - Part of the linkgit:git[1] suite
     
      ## builtin/history.c ##
     @@
    @@ builtin/history.c
     +					   struct commit *original,
     +					   struct commit **out)
     +{
    -+	const char *exclude_gpgsig[] = { "gpgsig", "gpgsig-sha256", NULL };
    ++	const char *exclude_gpgsig[] = {
    ++		/* We reencode the message, so the encoding needs to be stripped. */
    ++		"encoding",
    ++		/* We need to strip signatures as those will become invalid. */
    ++		"gpgsig",
    ++		"gpgsig-sha256",
    ++		NULL,
    ++	};
     +	const char *original_message, *original_body, *ptr;
     +	struct commit_extra_header *original_extra_headers = NULL;
     +	struct strbuf commit_message = STRBUF_INIT;
    @@ builtin/history.c
     +{
     +	const struct name_decoration *decoration;
     +	struct replay_revisions_options opts = { 0 };
    -+	struct replay_result result = {
    -+		.final_oid = rewritten->object.oid,
    -+	};
    ++	struct replay_result result = { 0 };
     +	struct ref_transaction *transaction = NULL;
     +	struct strvec args = STRVEC_INIT;
     +	struct strbuf err = STRBUF_INIT;
     +	struct commit *head = NULL;
     +	struct rev_info revs;
     +	char hex[GIT_MAX_HEXSZ + 1];
    ++	bool detached_head;
    ++	int head_flags = 0;
     +	int ret;
     +
    ++	refs_read_ref_full(get_main_ref_store(repo), "HEAD",
    ++			   RESOLVE_REF_NO_RECURSE, NULL, &head_flags);
    ++	detached_head = !(head_flags & REF_ISSYMREF);
    ++
     +	repo_init_revisions(repo, &revs, NULL);
     +	strvec_push(&args, "ignored");
     +	strvec_push(&args, "--reverse");
    @@ builtin/history.c
     +			goto out;
     +		}
     +
    -+		strvec_push(&args, oid_to_hex(&head->object.oid));
    ++		strvec_push(&args, "HEAD");
     +	} else {
     +		strvec_push(&args, "--branches");
    ++		strvec_push(&args, "HEAD");
     +	}
     +
     +	setup_revisions_from_strvec(&args, &revs, NULL);
    @@ builtin/history.c
     +
     +	opts.onto = oid_to_hex_r(hex, &rewritten->object.oid);
     +
    -+	ret = replay_revisions(repo, &revs, &opts, &result);
    ++	ret = replay_revisions(&revs, &opts, &result);
     +	if (ret)
     +		goto out;
     +
     +	switch (action) {
    -+	case REF_ACTION_DEFAULT:
     +	case REF_ACTION_BRANCHES:
    ++	case REF_ACTION_HEAD:
     +		transaction = ref_store_transaction_begin(get_main_ref_store(repo), 0, &err);
     +		if (!transaction) {
     +			ret = error(_("failed to begin ref transaction: %s"), err.buf);
    @@ builtin/history.c
     +		     decoration;
     +		     decoration = decoration->next)
     +		{
    -+			if (decoration->type != DECORATION_REF_LOCAL)
    ++			if (decoration->type != DECORATION_REF_LOCAL &&
    ++			    decoration->type != DECORATION_REF_HEAD)
    ++				continue;
    ++
    ++			if (action == REF_ACTION_HEAD &&
    ++			    decoration->type != DECORATION_REF_HEAD)
    ++				continue;
    ++
    ++			/*
    ++			 * We only need to update HEAD separately in case it's
    ++			 * detached. If it's not we'd already update the branch
    ++			 * it is pointing to.
    ++			 */
    ++			if (action == REF_ACTION_BRANCHES &&
    ++			    decoration->type == DECORATION_REF_HEAD &&
    ++			    !detached_head)
     +				continue;
     +
     +			ret = ref_transaction_update(transaction,
    @@ builtin/history.c
     +		}
     +
     +		break;
    -+	case REF_ACTION_HEAD:
    -+		ret = refs_update_ref(get_main_ref_store(repo), reflog_msg, "HEAD",
    -+				      &result.final_oid, &head->object.oid, 0,
    -+				      UPDATE_REFS_MSG_ON_ERR);
    -+		if (ret)
    -+			goto out;
    -+		break;
     +	case REF_ACTION_PRINT:
     +		for (size_t i = 0; i < result.updates_nr; i++)
     +			printf("update %s %s %s\n",
    @@ builtin/history.c
     +	}
     +	repo_config(repo, git_default_config, NULL);
     +
    ++	if (action == REF_ACTION_DEFAULT)
    ++		action = REF_ACTION_BRANCHES;
    ++
     +	original = lookup_commit_reference_by_name(argv[0]);
     +	if (!original) {
     +		ret = error(_("commit cannot be found: %s"), argv[0]);
    @@ builtin/history.c
     +	return fn(argc, argv, prefix, repo);
      }
     
    - ## replay.c ##
    -@@ replay.c: int replay_revisions(struct repository *repo, struct rev_info *revs,
    - 	struct commit *commit;
    - 	struct commit *onto = NULL;
    - 	struct merge_options merge_opt;
    --	struct merge_result result;
    -+	struct merge_result result = {
    -+		.clean = 1,
    -+	};
    - 	char *advance;
    - 	int ret;
    - 
    -@@ replay.c: int replay_revisions(struct repository *repo, struct rev_info *revs,
    - 	}
    - 
    - 	init_basic_merge_options(&merge_opt, repo);
    --	memset(&result, 0, sizeof(result));
    - 	merge_opt.show_rename_progress = 0;
    - 	last_commit = onto;
    - 	replayed_commits = kh_init_oid_map();
    -
      ## t/meson.build ##
     @@ t/meson.build: integration_tests = [
        't3437-rebase-fixup-options.sh',
    @@ t/t3451-history-reword.sh (new)
     +	)
     +'
     +
    ++test_expect_success 'can reword commit in the middle even on detached head' '
    ++	test_when_finished "rm -rf repo" &&
    ++	git init repo &&
    ++	(
    ++		cd repo &&
    ++		test_commit first &&
    ++		test_commit second &&
    ++		test_commit third_on_main &&
    ++		git checkout --detach HEAD^ &&
    ++		test_commit third_on_head &&
    ++
    ++		reword_with_message HEAD~ <<-EOF &&
    ++		second reworded
    ++		EOF
    ++
    ++		expect_graph HEAD --branches <<-\EOF
    ++		* third_on_head
    ++		| * third_on_main
    ++		|/
    ++		* second reworded
    ++		* first
    ++		EOF
    ++       )
    ++'
    ++
    ++test_expect_success 'can reword the detached head' '
    ++	test_when_finished "rm -rf repo" &&
    ++	git init repo &&
    ++	(
    ++		cd repo &&
    ++		test_commit first &&
    ++		test_commit second &&
    ++		git checkout --detach HEAD &&
    ++		test_commit third &&
    ++
    ++		reword_with_message HEAD <<-EOF &&
    ++		third reworded
    ++		EOF
    ++
    ++		expect_log <<-\EOF
    ++		third reworded
    ++		second
    ++		first
    ++		EOF
    ++	)
    ++'
    ++
     +test_expect_success 'can reword root commit' '
     +	test_when_finished "rm -rf repo" &&
     +	git init repo &&

--- base-commit: 02bdc12118c43e0a65258eb742492965859bffb6 change-id: 20250819-b4-pks-history-builtin-83398f9a05f0

Next: Patrick Steinhardt
Message 1 of 19 in “Introduce git-history(1) command for easy history editing”
  1. 0/8 Introduce git-history(1) command for easy history editingPatrick Steinhardt, Jan 12, 2026
  2. 1/8 builtin/replay: extract core logic to replay revisionsPatrick Steinhardt, Jan 12, 2026
  3. Junio C HamanoJan 12, 2026
  4. Patrick SteinhardtJan 12, 2026
  5. Elijah NewrenJan 13, 2026
  6. Patrick SteinhardtJan 13, 2026
  7. 2/8 builtin/replay: move core logic into "libgit.a"Patrick Steinhardt, Jan 12, 2026
  8. 3/8 replay: small set of cleanupsPatrick Steinhardt, Jan 12, 2026
  9. 4/8 replay: support empty commit rangesPatrick Steinhardt, Jan 12, 2026
  10. Elijah NewrenJan 13, 2026
  11. Patrick SteinhardtJan 13, 2026
  12. 5/8 replay: support updating detached HEADPatrick Steinhardt, Jan 12, 2026
  13. Elijah NewrenJan 13, 2026
  14. Patrick SteinhardtJan 13, 2026
  15. 6/8 wt-status: provide function to expose status for treesPatrick Steinhardt, Jan 12, 2026
  16. 7/8 builtin: add new "history" commandPatrick Steinhardt, Jan 12, 2026
  17. 8/8 builtin/history: implement "reword" subcommandPatrick Steinhardt, Jan 12, 2026
  18. Elijah NewrenJan 13, 2026
  19. Elijah NewrenJan 13, 2026

Read the whole thread, see it on lore, or plain text.

$ cat FOOTERMessages come from the public archive at lore.kernel.org/git, fetched every hour. The front page is chosen and written each morning by an AI editor and can be wrong; the threads themselves are the record. About and API. For agents: an MCP server at https://gitlist.dev/mcp, and any thread, story or person page as Markdown by adding .md to its URL (or sending Accept: text/markdown). Details in /llms.txt.