fast-import: avoid making replace refs point to themselves

If someone replaces a commit with a modified version, then builds on that commit, and then later decides to rewrite history in a format like git fast-export --all | CMD_TO_TWEAK_THE_STREAM | git fast-import and CMD_TO_TWEAK_THE_STREAM undoes the modifications that the replacement did, then at the end you'd get a replace ref that points to itself. For example: $ git show-ref | grep replace fb92ebc654641b310e7d0360d0a5a49316fd7264 refs/replace/fb92ebc654641b310e7d0360d0a5a49316fd7264 Git commands which pay attention to replace refs will die with an error when a self-referencing replace ref is present: $ git log fatal: replace depth too high for object fb92ebc654641b310e7d0360d0a5a49316fd7264 Avoid such problems by deleting replace refs that will simply end up pointing to themselves at the end of our writing. Unless users specify --quiet, warn them when we delete such a replace ref. Two notes about this patch: * We are not ignoring the problematic update of the replace ref (turning it into a no-op), we are replacing the update with a delete. The logic here is that if the repository had a value for the replace ref before fast-import was run, and the replace ref was explicitly named in the fast-import stream, we don't want the replace ref to be left with a pre-fast-import value. * While loops with more than one element (e.g. refs/replace/A points to B, and refs/replace/B points to A) are possible, they seem much less plausible. It is pretty easy to create a sequence of git-filter-repo commands that will trigger a self-referencing replace ref, but I do not know how to trigger a scenario with a cycle length greater than 1. Signed-off-by: Elijah Newren <newren@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Elijah Newren committed Nov 18, 2024 at 22:19 UTC 5e904f1a4ade49372970bd172d63f9c9fd7b2653
2 files changed +43 -1
builtin/fast-import.c
+15 -1
@@ -179,6 +179,7 @@ static unsigned long branch_load_count;
179 static int failure;
180 static FILE *pack_edges;
181 static unsigned int show_stats = 1;
182 +static unsigned int quiet;
183 static int global_argc;
184 static const char **global_argv;
185 static const char *global_prefix;
@@ -1604,7 +1605,19 @@ static int update_branch(struct branch *b)
1605 struct ref_transaction *transaction;
1606 struct object_id old_oid;
1607 struct strbuf err = STRBUF_INIT;
1607 -
1608 + static const char *replace_prefix = "refs/replace/";
1609 +
1610 + if (starts_with(b->name, replace_prefix) &&
1611 + !strcmp(b->name + strlen(replace_prefix),
1612 + oid_to_hex(&b->oid))) {
1613 + if (!quiet)
1614 + warning("Dropping %s since it would point to "
1615 + "itself (i.e. to %s)",
1616 + b->name, oid_to_hex(&b->oid));
1617 + refs_delete_ref(get_main_ref_store(the_repository),
1618 + NULL, b->name, NULL, 0);
1619 + return 0;
1620 + }
1621 if (is_null_oid(&b->oid)) {
1622 if (b->delete)
1623 refs_delete_ref(get_main_ref_store(the_repository),
@@ -3390,6 +3403,7 @@ static int parse_one_option(const char *option)
3403 option_export_pack_edges(option);
3404 } else if (!strcmp(option, "quiet")) {
3405 show_stats = 0;
3406 + quiet = 1;
3407 } else if (!strcmp(option, "stats")) {
3408 show_stats = 1;
3409 } else if (!strcmp(option, "allow-unsafe-features")) {
t/t9300-fast-import.sh
+28
@@ -3692,6 +3692,34 @@ test_expect_success 'X: handling encoding' '
3692 git log -1 --format=%B encoding | grep $(printf "\317\200")
3693 '
3694
3695 +test_expect_success 'X: replace ref that becomes useless is removed' '
3696 + git init -qb main testrepo &&
3697 + cd testrepo &&
3698 + (
3699 + test_commit test &&
3700 +
3701 + test_commit msg somename content &&
3702 +
3703 + git mv somename othername &&
3704 + NEW_TREE=$(git write-tree) &&
3705 + MSG="$(git log -1 --format=%B HEAD)" &&
3706 + NEW_COMMIT=$(git commit-tree -p HEAD^1 -m "$MSG" $NEW_TREE) &&
3707 + git replace main $NEW_COMMIT &&
3708 +
3709 + echo more >>othername &&
3710 + git add othername &&
3711 + git commit -qm more &&
3712 +
3713 + git fast-export --all >tmp &&
3714 + sed -e s/othername/somename/ tmp >tmp2 &&
3715 + git fast-import --force <tmp2 2>msgs &&
3716 +
3717 + grep "Dropping.*since it would point to itself" msgs &&
3718 + git show-ref >refs &&
3719 + ! grep refs/replace refs
3720 + )
3721 +'
3722 +
3723 ###
3724 ### series Y (submodules and hash algorithms)
3725 ###