Subject: [PATCH v9 9/7] history: fix detached HEAD handling

From: Elijah Newren <[email-masked]>
Date: Sat, 10 Jan 2026 21:58:59 -0800

The default behavior for history is to work on all local branches.  When
HEAD is detached, it should be treated like a local branch as well.
The primary fix for this is just to make sure that in addition to passing
--branches to the revision machinery that we pass HEAD as well.

However, that doesn't quite do the trick, because we also process the
"decorations" that point at commits that we have processed, and we do
this in two places -- in replay_revisions() as we replay commits, and
in handle_reference_updates() when there are no commits to replay
because the commit at the tip of the revision range was the one edited.
In both cases, we previously keyed off of DECORATION_REF_LOCAL to make
sure we only looked at local branches.  Now, we need to also pay
attention to DECORATION_REF_HEAD.  However, in order to avoid doing two
updates to the same branch (which will the ref transaction framework
would throw an error on), we need to only pay attention to
DECORATION_REF_HEAD when we have a detached HEAD.

Signed-off-by: Elijah Newren <[email-masked]>
---
 builtin/history.c         | 15 +++++++++++--
 replay.c                  | 18 ++++++++++++---
 t/t3451-history-reword.sh | 47 +++++++++++++++++++++++++++++++++++++++
 3 files changed, 75 insertions(+), 5 deletions(-)

diff --git a/builtin/history.c b/builtin/history.c
index 60c5b5c5c76..dd0df89c94e 100644
--- a/builtin/history.c
+++ b/builtin/history.c
@@ -183,10 +183,18 @@ static int handle_reference_updates(enum ref_action action,
 	struct strvec args = STRVEC_INIT;
 	struct strbuf err = STRBUF_INIT;
 	struct commit *head = NULL;
+	char *head_ref = NULL;
+	bool detached_head = false;
 	struct rev_info revs;
 	char hex[GIT_MAX_HEXSZ + 1];
 	int ret;
 
+	head_ref = refs_resolve_refdup(get_main_ref_store(repo), "HEAD",
+				       RESOLVE_REF_READING, NULL, NULL);
+	if (!strcmp(head_ref, "HEAD"))
+		detached_head = true;
+	free(head_ref);
+
 	repo_init_revisions(repo, &revs, NULL);
 	strvec_push(&args, "ignored");
 	strvec_push(&args, "--reverse");
@@ -234,6 +242,7 @@ static int handle_reference_updates(enum ref_action action,
 		strvec_push(&args, "HEAD");
 	} else {
 		strvec_push(&args, "--branches");
+		strvec_push(&args, "HEAD");
 	}
 
 	setup_revisions_from_strvec(&args, &revs, NULL);
@@ -278,9 +287,11 @@ static int handle_reference_updates(enum ref_action action,
 		     decoration;
 		     decoration = decoration->next)
 		{
-			if (decoration->type != DECORATION_REF_LOCAL)
+			if ((decoration->type != DECORATION_REF_HEAD ||
+			     (action != REF_ACTION_HEAD && !detached_head)) &&
+			    (decoration->type != DECORATION_REF_LOCAL ||
+			     action == REF_ACTION_HEAD))
 				continue;
-
 			ret = ref_transaction_update(transaction,
 						     decoration->name,
 						     &rewritten->object.oid,
diff --git a/replay.c b/replay.c
index ea300e3c36c..7c98b1d1044 100644
--- a/replay.c
+++ b/replay.c
@@ -151,11 +151,20 @@ static void get_ref_information(struct repository *repo,
 static void set_up_replay_mode(struct repository *repo,
 			       struct rev_cmdline_info *cmd_info,
 			       const char *onto_name,
+			       bool *detached_head,
 			       char **advance_name,
 			       struct commit **onto,
 			       struct strset **update_refs)
 {
 	struct ref_info rinfo;
+	char *head_ref;
+
+	*detached_head = false;
+	head_ref = refs_resolve_refdup(get_main_ref_store(repo), "HEAD",
+				       RESOLVE_REF_READING, NULL, NULL);
+	if (!strcmp(head_ref, "HEAD"))
+		*detached_head = true;
+	free(head_ref);
 
 	get_ref_information(repo, cmd_info, &rinfo);
 	if (!rinfo.positive_refexprs)
@@ -271,11 +280,12 @@ int replay_revisions(struct rev_info *revs,
 		.clean = 1,
 	};
 	char *advance;
+	bool detached_head;
 	int ret;
 
 	advance = xstrdup_or_null(opts->advance);
-	set_up_replay_mode(repo, &revs->cmdline, opts->onto, &advance,
-			   &onto, &update_refs);
+	set_up_replay_mode(repo, &revs->cmdline, opts->onto,
+			   &detached_head, &advance, &onto, &update_refs);
 
 	/* FIXME: Should allow replaying commits with the first as a root commit */
 
@@ -317,7 +327,9 @@ int replay_revisions(struct rev_info *revs,
 		if (!decoration)
 			continue;
 		while (decoration) {
-			if (decoration->type == DECORATION_REF_LOCAL &&
+			if ((decoration->type == DECORATION_REF_LOCAL ||
+			     (decoration->type == DECORATION_REF_HEAD &&
+			      detached_head)) &&
 			    (opts->contained || strset_contains(update_refs,
 								decoration->name))) {
 				replay_result_queue_update(out, decoration->name,
diff --git a/t/t3451-history-reword.sh b/t/t3451-history-reword.sh
index cd5883051d6..7ddbab8e676 100755
--- a/t/t3451-history-reword.sh
+++ b/t/t3451-history-reword.sh
@@ -77,6 +77,53 @@ test_expect_success 'can reword commit in the middle' '
 	)
 '
 
+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_log HEAD --branches --graph <<-\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 &&
-- 
2.52.0.410.g227cdbe8e36