]> git.ipfire.org Git - thirdparty/git.git/commitdiff
replay: support updating detached HEAD
authorPatrick Steinhardt <ps@pks.im>
Tue, 13 Jan 2026 09:54:36 +0000 (10:54 +0100)
committerJunio C Hamano <gitster@pobox.com>
Tue, 13 Jan 2026 13:41:16 +0000 (05:41 -0800)
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>
replay.c
t/t3650-replay-basics.sh

index 6680d50bd7cfcde4338002f08b9e94bcb0fae741..94fb76384b49d159f25ce42f8ae722dd5bc63470 100644 (file)
--- a/replay.c
+++ b/replay.c
@@ -150,11 +150,17 @@ 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;
+       int head_flags = 0;
+
+       refs_read_ref_full(get_main_ref_store(repo), "HEAD",
+                          RESOLVE_REF_NO_RECURSE, NULL, &head_flags);
+       *detached_head = !(head_flags & REF_ISSYMREF);
 
        get_ref_information(repo, cmd_info, &rinfo);
        if (!rinfo.positive_refexprs)
@@ -269,12 +275,13 @@ int replay_revisions(struct rev_info *revs,
        struct merge_result result = {
                .clean = 1,
        };
+       bool detached_head;
        char *advance;
        int ret;
 
        advance = xstrdup_or_null(opts->advance);
-       set_up_replay_mode(revs->repo, &revs->cmdline, opts->onto, &advance,
-                          &onto, &update_refs);
+       set_up_replay_mode(revs->repo, &revs->cmdline, opts->onto,
+                          &detached_head, &advance, &onto, &update_refs);
 
        /* FIXME: Should allow replaying commits with the first as a root commit */
 
@@ -312,18 +319,30 @@ int replay_revisions(struct rev_info *revs,
                /* Update any necessary branches */
                if (advance)
                        continue;
-               decoration = get_name_decoration(&commit->object);
-               if (!decoration)
-                       continue;
-               while (decoration) {
-                       if (decoration->type == DECORATION_REF_LOCAL &&
-                           (opts->contained || strset_contains(update_refs,
-                                                               decoration->name))) {
-                               replay_result_queue_update(out, decoration->name,
-                                                          &commit->object.oid,
-                                                          &last_commit->object.oid);
-                       }
-                       decoration = decoration->next;
+
+               for (decoration = get_name_decoration(&commit->object);
+                    decoration;
+                    decoration = decoration->next)
+               {
+                       if (decoration->type != DECORATION_REF_LOCAL &&
+                           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 (decoration->type == DECORATION_REF_HEAD && !detached_head)
+                               continue;
+
+                       if (!opts->contained &&
+                           !strset_contains(update_refs, decoration->name))
+                               continue;
+
+                       replay_result_queue_update(out, decoration->name,
+                                                  &commit->object.oid,
+                                                  &last_commit->object.oid);
                }
        }
 
index 307101eeb911f76a688fa6ef406f7fc40c71a33a..c862aa39f31e08042ef65b0f719825b29861c7a3 100755 (executable)
@@ -249,6 +249,15 @@ test_expect_success 'using replay on bare repo to rebase multiple divergent bran
        done
 '
 
+test_expect_success 'using replay to update detached HEAD' '
+       current_head=$(git branch --show-current) &&
+       test_when_finished git switch "$current_head" &&
+       git switch --detach &&
+       test_commit something &&
+       git replay --ref-action=print --onto HEAD~2 --ref-action=print HEAD~..HEAD >updates &&
+       test_grep "update HEAD " updates
+'
+
 test_expect_success 'merge.directoryRenames=false' '
        # create a test case that stress-tests the rename caching
        git switch -c rename-onto &&