Subject: [PATCH 5/6] sequencer: duplicate the result of resolve_ref_unsafe()

From: SZEDER Gábor <[email-masked]>
Date: Fri, 30 Sep 2022 16:09:47 +0200

When 'git rebase -i --update-refs' generates the todo list for the
rebased commit range, an 'update-ref' instruction is inserted for each
ref that points to one of those commits, except for the rebased branch
(because at the end of the rebase it will be updated anyway) and any
refs that are checked out in a worktree; for the latter a "Ref <ref>
checked out at '<worktree>'" comment is added.  One of these comments
can be missing under some circumstances: if the oldest commit with a
ref pointing to it has multiple refs pointing to it, and at least one
of those refs is checked out in a worktree, and one of them (but not
the first) is checked out in the worktree associated with the last
directory entry in '.git/worktrees'.

The culprit is the add_decorations_to_list() function, which calls
resolve_ref_unsafe() to figure out the refname of the rebased branch.
However, resolve_ref_unsafe() can (and in this case does) return a
pointer to a static buffer.  Alas, add_decorations_to_list() holds on
that static buffer until the end of the function, using its contents
to compare refnames with the rebased branch, while it also calls a
function that invokes refs_resolve_ref_unsafe() internally [1], and
which overwrites the content of that static buffer, and messes up
subsequent refname comparisons.

Use xstrdup_or_null() to keep a copy of resolve_ref_unsafe()'s return
value for the duration of add_decorations_to_list().

[1] #0  refs_resolve_ref_unsafe (refs=0x555555a23d00,
        refname=0x555555928523 "HEAD", resolve_flags=0, oid=0x555555a23c78,
        flags=0x7fffffffc0cc) at refs.c:1781
    #1  0x000055555587a1d9 in add_head_info (wt=0x555555a23c50) at worktree.c:33
    #2  0x000055555587a446 in get_linked_worktree (id=0x555555a19c43 "WorkTree")
        at worktree.c:91
    #3  0x000055555587a628 in get_worktrees () at worktree.c:135
    #4  0x00005555556a7676 in prepare_checked_out_branches () at branch.c:385
    #5  0x00005555556a7a76 in branch_checked_out (
        refname=0x555555a12e9c "refs/heads/branch2") at branch.c:446
    #6  0x0000555555824f55 in add_decorations_to_list (commit=0x5555559efd08,
        ctx=0x7fffffffc3e0) at sequencer.c:5946

Signed-off-by: SZEDER Gábor <[email-masked]>
---
 sequencer.c                   |  5 +++--
 t/t3404-rebase-interactive.sh | 34 ++++++++++++++++++++++++++++++++++
 2 files changed, 37 insertions(+), 2 deletions(-)

diff --git a/sequencer.c b/sequencer.c
index fba92c90b1..f1732f88f3 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -5917,10 +5917,10 @@ static int add_decorations_to_list(const struct commit *commit,
 				   struct todo_add_branch_context *ctx)
 {
 	const struct name_decoration *decoration = get_name_decoration(&commit->object);
-	const char *head_ref = resolve_ref_unsafe("HEAD",
+	const char *head_ref = xstrdup_or_null(resolve_ref_unsafe("HEAD",
 						  RESOLVE_REF_READING,
 						  NULL,
-						  NULL);
+						  NULL));
 
 	while (decoration) {
 		struct todo_item *item;
@@ -5965,6 +5965,7 @@ static int add_decorations_to_list(const struct commit *commit,
 		decoration = decoration->next;
 	}
 
+	free((char *)head_ref);
 	return 0;
 }
 
diff --git a/t/t3404-rebase-interactive.sh b/t/t3404-rebase-interactive.sh
index 7f0df58628..2e081b3914 100755
--- a/t/t3404-rebase-interactive.sh
+++ b/t/t3404-rebase-interactive.sh
@@ -2016,6 +2016,40 @@ test_expect_success REFFILES '--update-refs: check failed ref update' '
 	test_cmp expect err.trimmed
 '
 
+test_expect_success 'what should I call this?!' '
+	test_when_finished "rm -rf repo" &&
+	git init repo &&
+	(
+		cd repo &&
+		test_commit_bulk --message="%s" 4 &&
+		git branch branch1 HEAD^ &&
+		git branch branch2 HEAD^ &&
+		git branch branch3 HEAD^ &&
+
+		git worktree add WorkTree branch2 &&
+		git worktree list --porcelain >out &&
+		wtpath=$(sed -n -e "s%^worktree \(.*/WorkTree\)%\1%p" out) &&
+
+		cat >expect <<-EOF &&
+		pick $(git log -1 --format=%h HEAD^^) 2
+		pick $(git log -1 --format=%h HEAD^) 3
+		update-ref refs/heads/branch3
+		# Ref refs/heads/branch2 checked out at $SQ$wtpath$SQ
+		update-ref refs/heads/branch1
+		pick $(git log -1 --format=%h HEAD) 4
+		EOF
+
+		write_script fake-editor.sh <<-\EOF &&
+		sed -n -e "/^$/q;p" "$1"
+		exit 1
+		EOF
+		test_set_editor "$(pwd)/fake-editor.sh" &&
+		test_must_fail git rebase -i --update-refs HEAD^^^ >actual &&
+
+		test_cmp expect actual
+	)
+'
+
 # This must be the last test in this file
 test_expect_success '$EDITOR and friends are unchanged' '
 	test_editor_unchanged
-- 
2.38.0.rc2.542.g9b62912f7f