Subject: [PATCH] replace-refs: fix support of qualified replace ref paths

From: "Tao Klerks via GitGitGadget" <[email-masked]>
Date: Sat, 26 Apr 2025 07:10:52 +0000

From: Tao Klerks <[email-masked]>

The enactment of replace refs in
replace-object.c#register_replace_ref() explicitly uses the last
portion of the ref "path", the substring after the last slash, as the
object ID to be replaced. This means that replace refs can be
organized into different paths; you can separate replace refs created
for different purposes, like "refs/replace/2012-migration/*". This in
turn makes it practical to store prepared replace refs in different ref
paths on a git server, and have users "map" them, via specific
refspecs, into their local repos; different types of replacements can
be mapped into different sub-paths of refs/replace/.

The only way this didn't "work" is in the commit decoration process,
in log-tree.c#add_ref_decoration(), where different logic was used to
obtain the replaced object ID, removing the "refs/replace/" prefix
only. This inconsistent logic meant that more structured replace ref
paths caused a warning to be printed, and the "replaced" decoration to
be omitted.

Fix this decoration logic to use the same "last part of ref path"
logic, fixing spurious warnings (and missing decorations) for users
of more structured replace ref paths. Also add tests for qualified
replace ref paths.

Signed-off-by: Tao Klerks <[email-masked]>
---
    replace-refs: fix support of qualified replace ref paths

Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-1903%2FTaoK%2Fstructured-replace-ref-decoration-fix-v1
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-1903/TaoK/structured-replace-ref-decoration-fix-v1
Pull-Request: https://github.com/gitgitgadget/git/pull/1903

 log-tree.c                       |  5 +++--
 t/t4207-log-decoration-colors.sh | 17 +++++++++++++++++
 t/t6050-replace.sh               | 10 ++++++++++
 3 files changed, 30 insertions(+), 2 deletions(-)

diff --git a/log-tree.c b/log-tree.c
index a4d4ab59ca0..6a87724527b 100644
--- a/log-tree.c
+++ b/log-tree.c
@@ -163,10 +163,11 @@ static int add_ref_decoration(const char *refname, const char *referent UNUSED,
 
 	if (starts_with(refname, git_replace_ref_base)) {
 		struct object_id original_oid;
+		const char *slash = strrchr(refname, '/');
+		const char *hash = slash ? slash + 1 : refname;
 		if (!replace_refs_enabled(the_repository))
 			return 0;
-		if (get_oid_hex(refname + strlen(git_replace_ref_base),
-				&original_oid)) {
+		if (get_oid_hex(hash, &original_oid)) {
 			warning("invalid replace ref %s", refname);
 			return 0;
 		}
diff --git a/t/t4207-log-decoration-colors.sh b/t/t4207-log-decoration-colors.sh
index 2e83cc820a1..f80684efcff 100755
--- a/t/t4207-log-decoration-colors.sh
+++ b/t/t4207-log-decoration-colors.sh
@@ -131,4 +131,21 @@ ${c_tag}tag: ${c_reset}${c_tag}A${c_reset}${c_commit})${c_reset} A
 	cmp_filtered_decorations
 '
 
+test_expect_success 'test replace decoration for nested replace path' '
+	test_when_finished remove_replace_refs &&
+
+	CURRENT_HASH=$(git rev-parse --verify HEAD) &&
+	git replace --graft HEAD HEAD~2 &&
+	git update-ref refs/tmp/tmpref refs/replace/$CURRENT_HASH &&
+	git update-ref -d refs/replace/$CURRENT_HASH &&
+	git update-ref refs/replace/nested-path/abc/$CURRENT_HASH refs/tmp/tmpref &&
+
+	git log --decorate -1 HEAD >actual &&
+	test_grep "replaced" actual &&
+
+	git --no-replace-objects log --decorate -1 HEAD >actual &&
+	test_grep ! "replaced" actual
+
+'
+
 test_done
diff --git a/t/t6050-replace.sh b/t/t6050-replace.sh
index aa1b5351873..9b06baa1439 100755
--- a/t/t6050-replace.sh
+++ b/t/t6050-replace.sh
@@ -546,4 +546,14 @@ test_expect_success '--convert-graft-file' '
 	test_grep "$EMPTY_BLOB $EMPTY_TREE" .git/info/grafts
 '
 
+test_expect_success 'replace ref in a nested path' '
+	git cat-file commit $HASH2 >actual &&
+	R=$(sed -e "s/A U/O/" actual | git hash-object -t commit --stdin -w) &&
+	git update-ref refs/replace/abc1/$HASH2 $R &&
+	git show $HASH2 >actual &&
+	test_grep "O Thor" actual &&
+	git --no-replace-objects show $HASH2 >actual &&
+	test_grep "A U Thor" actual
+'
+
 test_done

base-commit: f65182a99e545d2f2bc22e6c1c2da192133b16a3
-- 
gitgitgadget