replay: support updating detached HEAD

In a subsequent commit we're about to introduce a new git-history(1) command, which will by default work on all local branches and HEAD. This is already well-supported by the replay machinery for most of the part: updating branches is one of its prime use cases, and the HEAD ref is also updated in case it points to any of the branches. However, what's not supported yet is to update HEAD in case it is not a symbolic ref. We determine the refs that need to be updated by iterating through the decorations of the original commit, but we only update those refs that are `DECORATION_REF_LOCAL`, which covers local branches. Address this gap by also handling `DECORATION_REF_HEAD`. Note though that this needs to only happen in case we're working on a detached HEAD. If HEAD is pointing to a branch, then we'd already update that branch via `DECORATION_REF_LOCAL`. Refactor the loop that iterates through the decorations a bit to make the individual conditions easier to understand. Based-on-patch-by: Elijah Newren <newren@gmail.com> Signed-off-by: Patrick Steinhardt <ps@pks.im> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Patrick Steinhardt committed Jan 13, 2026 at 10:54 UTC 48a72f61f04cb2357544f373677acd5b4149237e
2 files changed +42 -14
replay.c
+33 -14
@@ -150,11 +150,17 @@ static void get_ref_information(struct repository *repo,
150 static void set_up_replay_mode(struct repository *repo,
151 struct rev_cmdline_info *cmd_info,
152 const char *onto_name,
153 + bool *detached_head,
154 char **advance_name,
155 struct commit **onto,
156 struct strset **update_refs)
157 {
158 struct ref_info rinfo;
159 + int head_flags = 0;
160 +
161 + refs_read_ref_full(get_main_ref_store(repo), "HEAD",
162 + RESOLVE_REF_NO_RECURSE, NULL, &head_flags);
163 + *detached_head = !(head_flags & REF_ISSYMREF);
164
165 get_ref_information(repo, cmd_info, &rinfo);
166 if (!rinfo.positive_refexprs)
@@ -269,12 +275,13 @@ int replay_revisions(struct rev_info *revs,
275 struct merge_result result = {
276 .clean = 1,
277 };
278 + bool detached_head;
279 char *advance;
280 int ret;
281
282 advance = xstrdup_or_null(opts->advance);
276 - set_up_replay_mode(revs->repo, &revs->cmdline, opts->onto, &advance,
277 - &onto, &update_refs);
283 + set_up_replay_mode(revs->repo, &revs->cmdline, opts->onto,
284 + &detached_head, &advance, &onto, &update_refs);
285
286 /* FIXME: Should allow replaying commits with the first as a root commit */
287
@@ -312,18 +319,30 @@ int replay_revisions(struct rev_info *revs,
319 /* Update any necessary branches */
320 if (advance)
321 continue;
315 - decoration = get_name_decoration(&commit->object);
316 - if (!decoration)
317 - continue;
318 - while (decoration) {
319 - if (decoration->type == DECORATION_REF_LOCAL &&
320 - (opts->contained || strset_contains(update_refs,
321 - decoration->name))) {
322 - replay_result_queue_update(out, decoration->name,
323 - &commit->object.oid,
324 - &last_commit->object.oid);
325 - }
326 - decoration = decoration->next;
322 +
323 + for (decoration = get_name_decoration(&commit->object);
324 + decoration;
325 + decoration = decoration->next)
326 + {
327 + if (decoration->type != DECORATION_REF_LOCAL &&
328 + decoration->type != DECORATION_REF_HEAD)
329 + continue;
330 +
331 + /*
332 + * We only need to update HEAD separately in case it's
333 + * detached. If it's not we'd already update the branch
334 + * it is pointing to.
335 + */
336 + if (decoration->type == DECORATION_REF_HEAD && !detached_head)
337 + continue;
338 +
339 + if (!opts->contained &&
340 + !strset_contains(update_refs, decoration->name))
341 + continue;
342 +
343 + replay_result_queue_update(out, decoration->name,
344 + &commit->object.oid,
345 + &last_commit->object.oid);
346 }
347 }
348
t/t3650-replay-basics.sh
+9
@@ -249,6 +249,15 @@ test_expect_success 'using replay on bare repo to rebase multiple divergent bran
249 done
250 '
251
252 +test_expect_success 'using replay to update detached HEAD' '
253 + current_head=$(git branch --show-current) &&
254 + test_when_finished git switch "$current_head" &&
255 + git switch --detach &&
256 + test_commit something &&
257 + git replay --ref-action=print --onto HEAD~2 --ref-action=print HEAD~..HEAD >updates &&
258 + test_grep "update HEAD " updates
259 +'
260 +
261 test_expect_success 'merge.directoryRenames=false' '
262 # create a test case that stress-tests the rename caching
263 git switch -c rename-onto &&