Merge branch 'jk/no-clobber-dangling-symref-with-fetch'

"git fetch" can clobber a symref that is dangling when the
remote-tracking HEAD is set to auto update, which has been
corrected.

* jk/no-clobber-dangling-symref-with-fetch:
  refs: do not clobber dangling symrefs
  t5510: prefer "git -C" to subshell for followRemoteHEAD tests
  t5510: stop changing top-level working directory
  t5510: make confusing config cleanup more explicit
This commit is contained in:
Junio C Hamano
2025-08-29 09:44:37 -07:00
4 changed files with 320 additions and 310 deletions

View File

@@ -2515,13 +2515,37 @@ static enum ref_transaction_error split_symref_update(struct ref_update *update,
*/ */
static enum ref_transaction_error check_old_oid(struct ref_update *update, static enum ref_transaction_error check_old_oid(struct ref_update *update,
struct object_id *oid, struct object_id *oid,
struct strbuf *referent,
struct strbuf *err) struct strbuf *err)
{ {
if (update->flags & REF_LOG_ONLY || if (update->flags & REF_LOG_ONLY ||
!(update->flags & REF_HAVE_OLD) || !(update->flags & REF_HAVE_OLD))
oideq(oid, &update->old_oid))
return 0; return 0;
if (oideq(oid, &update->old_oid)) {
/*
* Normally matching the expected old oid is enough. Either we
* found the ref at the expected state, or we are creating and
* expect the null oid (and likewise found nothing).
*
* But there is one exception for the null oid: if we found a
* symref pointing to nothing we'll also get the null oid. In
* regular recursive mode, that's good (we'll write to what the
* symref points to, which doesn't exist). But in no-deref
* mode, it means we'll clobber the symref, even though the
* caller asked for this to be a creation event. So flag
* that case to preserve the dangling symref.
*/
if ((update->flags & REF_NO_DEREF) && referent->len &&
is_null_oid(oid)) {
strbuf_addf(err, "cannot lock ref '%s': "
"dangling symref already exists",
ref_update_original_update_refname(update));
return REF_TRANSACTION_ERROR_CREATE_EXISTS;
}
return 0;
}
if (is_null_oid(&update->old_oid)) { if (is_null_oid(&update->old_oid)) {
strbuf_addf(err, "cannot lock ref '%s': " strbuf_addf(err, "cannot lock ref '%s': "
"reference already exists", "reference already exists",
@@ -2661,7 +2685,8 @@ static enum ref_transaction_error lock_ref_for_update(struct files_ref_store *re
if (update->old_target) if (update->old_target)
ret = ref_update_check_old_target(referent.buf, update, err); ret = ref_update_check_old_target(referent.buf, update, err);
else else
ret = check_old_oid(update, &lock->old_oid, err); ret = check_old_oid(update, &lock->old_oid,
&referent, err);
if (ret) if (ret)
goto out; goto out;
} else { } else {
@@ -2693,7 +2718,8 @@ static enum ref_transaction_error lock_ref_for_update(struct files_ref_store *re
ret = REF_TRANSACTION_ERROR_EXPECTED_SYMREF; ret = REF_TRANSACTION_ERROR_EXPECTED_SYMREF;
goto out; goto out;
} else { } else {
ret = check_old_oid(update, &lock->old_oid, err); ret = check_old_oid(update, &lock->old_oid,
&referent, err);
if (ret) { if (ret) {
goto out; goto out;
} }

View File

@@ -1274,9 +1274,33 @@ static enum ref_transaction_error prepare_single_update(struct reftable_ref_stor
ret = ref_update_check_old_target(referent->buf, u, err); ret = ref_update_check_old_target(referent->buf, u, err);
if (ret) if (ret)
return ret; return ret;
} else if ((u->flags & (REF_LOG_ONLY | REF_HAVE_OLD)) == REF_HAVE_OLD && } else if ((u->flags & (REF_LOG_ONLY | REF_HAVE_OLD)) == REF_HAVE_OLD) {
!oideq(&current_oid, &u->old_oid)) { if (oideq(&current_oid, &u->old_oid)) {
if (is_null_oid(&u->old_oid)) { /*
* Normally matching the expected old oid is enough. Either we
* found the ref at the expected state, or we are creating and
* expect the null oid (and likewise found nothing).
*
* But there is one exception for the null oid: if we found a
* symref pointing to nothing we'll also get the null oid. In
* regular recursive mode, that's good (we'll write to what the
* symref points to, which doesn't exist). But in no-deref
* mode, it means we'll clobber the symref, even though the
* caller asked for this to be a creation event. So flag
* that case to preserve the dangling symref.
*
* Everything else is OK and we can fall through to the
* end of the conditional chain.
*/
if ((u->flags & REF_NO_DEREF) &&
referent->len &&
is_null_oid(&u->old_oid)) {
strbuf_addf(err, _("cannot lock ref '%s': "
"dangling symref already exists"),
ref_update_original_update_refname(u));
return REF_TRANSACTION_ERROR_CREATE_EXISTS;
}
} else if (is_null_oid(&u->old_oid)) {
strbuf_addf(err, _("cannot lock ref '%s': " strbuf_addf(err, _("cannot lock ref '%s': "
"reference already exists"), "reference already exists"),
ref_update_original_update_refname(u)); ref_update_original_update_refname(u));

View File

@@ -2368,4 +2368,25 @@ test_expect_success REFFILES 'empty directories are pruned when not committing'
test_path_is_missing .git/refs/heads/nested test_path_is_missing .git/refs/heads/nested
' '
test_expect_success 'dangling symref not overwritten by creation' '
test_when_finished "git update-ref -d refs/heads/dangling" &&
git symbolic-ref refs/heads/dangling refs/heads/does-not-exist &&
test_must_fail git update-ref --no-deref --stdin 2>err <<-\EOF &&
create refs/heads/dangling HEAD
EOF
test_grep "cannot lock.*dangling symref already exists" err &&
test_must_fail git rev-parse --verify refs/heads/dangling &&
test_must_fail git rev-parse --verify refs/heads/does-not-exist
'
test_expect_success 'dangling symref overwritten without old oid' '
test_when_finished "git update-ref -d refs/heads/dangling" &&
git symbolic-ref refs/heads/dangling refs/heads/does-not-exist &&
git update-ref --no-deref --stdin <<-\EOF &&
update refs/heads/dangling HEAD
EOF
git rev-parse --verify refs/heads/dangling &&
test_must_fail git rev-parse --verify refs/heads/does-not-exist
'
test_done test_done

View File

@@ -14,8 +14,6 @@ then
test_done test_done
fi fi
D=$(pwd)
test_expect_success setup ' test_expect_success setup '
echo >file original && echo >file original &&
git add file && git add file &&
@@ -51,46 +49,50 @@ test_expect_success "clone and setup child repos" '
' '
test_expect_success "fetch test" ' test_expect_success "fetch test" '
cd "$D" &&
echo >file updated by origin && echo >file updated by origin &&
git commit -a -m "updated by origin" && git commit -a -m "updated by origin" &&
cd two && (
git fetch && cd two &&
git rev-parse --verify refs/heads/one && git fetch &&
mine=$(git rev-parse refs/heads/one) && git rev-parse --verify refs/heads/one &&
his=$(cd ../one && git rev-parse refs/heads/main) && mine=$(git rev-parse refs/heads/one) &&
test "z$mine" = "z$his" his=$(cd ../one && git rev-parse refs/heads/main) &&
test "z$mine" = "z$his"
)
' '
test_expect_success "fetch test for-merge" ' test_expect_success "fetch test for-merge" '
cd "$D" && (
cd three && cd three &&
git fetch && git fetch &&
git rev-parse --verify refs/heads/two && git rev-parse --verify refs/heads/two &&
git rev-parse --verify refs/heads/one && git rev-parse --verify refs/heads/one &&
main_in_two=$(cd ../two && git rev-parse main) && main_in_two=$(cd ../two && git rev-parse main) &&
one_in_two=$(cd ../two && git rev-parse one) && one_in_two=$(cd ../two && git rev-parse one) &&
{ {
echo "$one_in_two " && echo "$one_in_two " &&
echo "$main_in_two not-for-merge" echo "$main_in_two not-for-merge"
} >expected && } >expected &&
cut -f -2 .git/FETCH_HEAD >actual && cut -f -2 .git/FETCH_HEAD >actual &&
test_cmp expected actual' test_cmp expected actual
)
'
test_expect_success "fetch test remote HEAD" ' test_expect_success "fetch test remote HEAD" '
cd "$D" && (
cd two && cd two &&
git fetch && git fetch &&
git rev-parse --verify refs/remotes/origin/HEAD && git rev-parse --verify refs/remotes/origin/HEAD &&
git rev-parse --verify refs/remotes/origin/main && git rev-parse --verify refs/remotes/origin/main &&
head=$(git rev-parse refs/remotes/origin/HEAD) && head=$(git rev-parse refs/remotes/origin/HEAD) &&
branch=$(git rev-parse refs/remotes/origin/main) && branch=$(git rev-parse refs/remotes/origin/main) &&
test "z$head" = "z$branch"' test "z$head" = "z$branch"
)
'
test_expect_success "fetch test remote HEAD in bare repository" ' test_expect_success "fetch test remote HEAD in bare repository" '
test_when_finished rm -rf barerepo && test_when_finished rm -rf barerepo &&
( (
cd "$D" &&
git init --bare barerepo && git init --bare barerepo &&
cd barerepo && cd barerepo &&
git remote add upstream ../two && git remote add upstream ../two &&
@@ -105,262 +107,235 @@ test_expect_success "fetch test remote HEAD in bare repository" '
test_expect_success "fetch test remote HEAD change" ' test_expect_success "fetch test remote HEAD change" '
cd "$D" && (
cd two && cd two &&
git switch -c other && git switch -c other &&
git push -u origin other && git push -u origin other &&
git rev-parse --verify refs/remotes/origin/HEAD && git rev-parse --verify refs/remotes/origin/HEAD &&
git rev-parse --verify refs/remotes/origin/main && git rev-parse --verify refs/remotes/origin/main &&
git rev-parse --verify refs/remotes/origin/other && git rev-parse --verify refs/remotes/origin/other &&
git remote set-head origin other && git remote set-head origin other &&
git fetch && git fetch &&
head=$(git rev-parse refs/remotes/origin/HEAD) && head=$(git rev-parse refs/remotes/origin/HEAD) &&
branch=$(git rev-parse refs/remotes/origin/other) && branch=$(git rev-parse refs/remotes/origin/other) &&
test "z$head" = "z$branch"' test "z$head" = "z$branch"
)
'
test_expect_success "fetch test followRemoteHEAD never" ' test_expect_success "fetch test followRemoteHEAD never" '
test_when_finished "git config unset remote.origin.followRemoteHEAD" && git -C two update-ref --no-deref -d refs/remotes/origin/HEAD &&
( test_config -C two remote.origin.followRemoteHEAD "never" &&
cd "$D" && GIT_TRACE_PACKET=$PWD/trace.out git -C two fetch &&
cd two && # Confirm that we do not even ask for HEAD when we are
git update-ref --no-deref -d refs/remotes/origin/HEAD && # not going to act on it.
git config set remote.origin.followRemoteHEAD "never" && test_grep ! "ref-prefix HEAD" trace.out &&
GIT_TRACE_PACKET=$PWD/trace.out git fetch && test_must_fail git -C two rev-parse --verify refs/remotes/origin/HEAD
# Confirm that we do not even ask for HEAD when we are
# not going to act on it.
test_grep ! "ref-prefix HEAD" trace.out &&
test_must_fail git rev-parse --verify refs/remotes/origin/HEAD
)
' '
test_expect_success "fetch test followRemoteHEAD warn no change" ' test_expect_success "fetch test followRemoteHEAD warn no change" '
test_when_finished "git config unset remote.origin.followRemoteHEAD" && git -C two rev-parse --verify refs/remotes/origin/other &&
( git -C two remote set-head origin other &&
cd "$D" && git -C two rev-parse --verify refs/remotes/origin/HEAD &&
cd two && git -C two rev-parse --verify refs/remotes/origin/main &&
git rev-parse --verify refs/remotes/origin/other && test_config -C two remote.origin.followRemoteHEAD "warn" &&
git remote set-head origin other && git -C two fetch >output &&
git rev-parse --verify refs/remotes/origin/HEAD && echo "${SQ}HEAD${SQ} at ${SQ}origin${SQ} is ${SQ}main${SQ}," \
git rev-parse --verify refs/remotes/origin/main && "but we have ${SQ}other${SQ} locally." >expect &&
git config set remote.origin.followRemoteHEAD "warn" && test_cmp expect output &&
git fetch >output && head=$(git -C two rev-parse refs/remotes/origin/HEAD) &&
echo "${SQ}HEAD${SQ} at ${SQ}origin${SQ} is ${SQ}main${SQ}," \ branch=$(git -C two rev-parse refs/remotes/origin/other) &&
"but we have ${SQ}other${SQ} locally." >expect && test "z$head" = "z$branch"
test_cmp expect output &&
head=$(git rev-parse refs/remotes/origin/HEAD) &&
branch=$(git rev-parse refs/remotes/origin/other) &&
test "z$head" = "z$branch"
)
' '
test_expect_success "fetch test followRemoteHEAD warn create" ' test_expect_success "fetch test followRemoteHEAD warn create" '
test_when_finished "git config unset remote.origin.followRemoteHEAD" && git -C two update-ref --no-deref -d refs/remotes/origin/HEAD &&
( test_config -C two remote.origin.followRemoteHEAD "warn" &&
cd "$D" && git -C two rev-parse --verify refs/remotes/origin/main &&
cd two && output=$(git -C two fetch) &&
git update-ref --no-deref -d refs/remotes/origin/HEAD && test "z" = "z$output" &&
git config set remote.origin.followRemoteHEAD "warn" && head=$(git -C two rev-parse refs/remotes/origin/HEAD) &&
git rev-parse --verify refs/remotes/origin/main && branch=$(git -C two rev-parse refs/remotes/origin/main) &&
output=$(git fetch) && test "z$head" = "z$branch"
test "z" = "z$output" &&
head=$(git rev-parse refs/remotes/origin/HEAD) &&
branch=$(git rev-parse refs/remotes/origin/main) &&
test "z$head" = "z$branch"
)
' '
test_expect_success "fetch test followRemoteHEAD warn detached" ' test_expect_success "fetch test followRemoteHEAD warn detached" '
test_when_finished "git config unset remote.origin.followRemoteHEAD" && git -C two update-ref --no-deref -d refs/remotes/origin/HEAD &&
( git -C two update-ref refs/remotes/origin/HEAD HEAD &&
cd "$D" && HEAD=$(git -C two log --pretty="%H") &&
cd two && test_config -C two remote.origin.followRemoteHEAD "warn" &&
git update-ref --no-deref -d refs/remotes/origin/HEAD && git -C two fetch >output &&
git update-ref refs/remotes/origin/HEAD HEAD && echo "${SQ}HEAD${SQ} at ${SQ}origin${SQ} is ${SQ}main${SQ}," \
HEAD=$(git log --pretty="%H") && "but we have a detached HEAD pointing to" \
git config set remote.origin.followRemoteHEAD "warn" && "${SQ}${HEAD}${SQ} locally." >expect &&
git fetch >output && test_cmp expect output
echo "${SQ}HEAD${SQ} at ${SQ}origin${SQ} is ${SQ}main${SQ}," \
"but we have a detached HEAD pointing to" \
"${SQ}${HEAD}${SQ} locally." >expect &&
test_cmp expect output
)
' '
test_expect_success "fetch test followRemoteHEAD warn quiet" ' test_expect_success "fetch test followRemoteHEAD warn quiet" '
test_when_finished "git config unset remote.origin.followRemoteHEAD" && git -C two rev-parse --verify refs/remotes/origin/other &&
( git -C two remote set-head origin other &&
cd "$D" && git -C two rev-parse --verify refs/remotes/origin/HEAD &&
cd two && git -C two rev-parse --verify refs/remotes/origin/main &&
git rev-parse --verify refs/remotes/origin/other && test_config -C two remote.origin.followRemoteHEAD "warn" &&
git remote set-head origin other && output=$(git -C two fetch --quiet) &&
git rev-parse --verify refs/remotes/origin/HEAD && test "z" = "z$output" &&
git rev-parse --verify refs/remotes/origin/main && head=$(git -C two rev-parse refs/remotes/origin/HEAD) &&
git config set remote.origin.followRemoteHEAD "warn" && branch=$(git -C two rev-parse refs/remotes/origin/other) &&
output=$(git fetch --quiet) && test "z$head" = "z$branch"
test "z" = "z$output" &&
head=$(git rev-parse refs/remotes/origin/HEAD) &&
branch=$(git rev-parse refs/remotes/origin/other) &&
test "z$head" = "z$branch"
)
' '
test_expect_success "fetch test followRemoteHEAD warn-if-not-branch branch is same" ' test_expect_success "fetch test followRemoteHEAD warn-if-not-branch branch is same" '
test_when_finished "git config unset remote.origin.followRemoteHEAD" && git -C two rev-parse --verify refs/remotes/origin/other &&
( git -C two remote set-head origin other &&
cd "$D" && git -C two rev-parse --verify refs/remotes/origin/HEAD &&
cd two && git -C two rev-parse --verify refs/remotes/origin/main &&
git rev-parse --verify refs/remotes/origin/other && test_config -C two remote.origin.followRemoteHEAD "warn-if-not-main" &&
git remote set-head origin other && actual=$(git -C two fetch) &&
git rev-parse --verify refs/remotes/origin/HEAD && test "z" = "z$actual" &&
git rev-parse --verify refs/remotes/origin/main && head=$(git -C two rev-parse refs/remotes/origin/HEAD) &&
git config set remote.origin.followRemoteHEAD "warn-if-not-main" && branch=$(git -C two rev-parse refs/remotes/origin/other) &&
actual=$(git fetch) && test "z$head" = "z$branch"
test "z" = "z$actual" &&
head=$(git rev-parse refs/remotes/origin/HEAD) &&
branch=$(git rev-parse refs/remotes/origin/other) &&
test "z$head" = "z$branch"
)
' '
test_expect_success "fetch test followRemoteHEAD warn-if-not-branch branch is different" ' test_expect_success "fetch test followRemoteHEAD warn-if-not-branch branch is different" '
test_when_finished "git config unset remote.origin.followRemoteHEAD" && git -C two rev-parse --verify refs/remotes/origin/other &&
( git -C two remote set-head origin other &&
cd "$D" && git -C two rev-parse --verify refs/remotes/origin/HEAD &&
cd two && git -C two rev-parse --verify refs/remotes/origin/main &&
git rev-parse --verify refs/remotes/origin/other && test_config -C two remote.origin.followRemoteHEAD "warn-if-not-some/different-branch" &&
git remote set-head origin other && git -C two fetch >actual &&
git rev-parse --verify refs/remotes/origin/HEAD && echo "${SQ}HEAD${SQ} at ${SQ}origin${SQ} is ${SQ}main${SQ}," \
git rev-parse --verify refs/remotes/origin/main && "but we have ${SQ}other${SQ} locally." >expect &&
git config set remote.origin.followRemoteHEAD "warn-if-not-some/different-branch" && test_cmp expect actual &&
git fetch >actual && head=$(git -C two rev-parse refs/remotes/origin/HEAD) &&
echo "${SQ}HEAD${SQ} at ${SQ}origin${SQ} is ${SQ}main${SQ}," \ branch=$(git -C two rev-parse refs/remotes/origin/other) &&
"but we have ${SQ}other${SQ} locally." >expect && test "z$head" = "z$branch"
test_cmp expect actual &&
head=$(git rev-parse refs/remotes/origin/HEAD) &&
branch=$(git rev-parse refs/remotes/origin/other) &&
test "z$head" = "z$branch"
)
' '
test_expect_success "fetch test followRemoteHEAD always" ' test_expect_success "fetch test followRemoteHEAD always" '
test_when_finished "git config unset remote.origin.followRemoteHEAD" && git -C two rev-parse --verify refs/remotes/origin/other &&
( git -C two remote set-head origin other &&
cd "$D" && git -C two rev-parse --verify refs/remotes/origin/HEAD &&
cd two && git -C two rev-parse --verify refs/remotes/origin/main &&
git rev-parse --verify refs/remotes/origin/other && test_config -C two remote.origin.followRemoteHEAD "always" &&
git remote set-head origin other && git -C two fetch &&
git rev-parse --verify refs/remotes/origin/HEAD && head=$(git -C two rev-parse refs/remotes/origin/HEAD) &&
git rev-parse --verify refs/remotes/origin/main && branch=$(git -C two rev-parse refs/remotes/origin/main) &&
git config set remote.origin.followRemoteHEAD "always" && test "z$head" = "z$branch"
git fetch &&
head=$(git rev-parse refs/remotes/origin/HEAD) &&
branch=$(git rev-parse refs/remotes/origin/main) &&
test "z$head" = "z$branch"
)
' '
test_expect_success 'followRemoteHEAD does not kick in with refspecs' ' test_expect_success 'followRemoteHEAD does not kick in with refspecs' '
test_when_finished "git config unset remote.origin.followRemoteHEAD" && git -C two remote set-head origin other &&
( test_config -C two remote.origin.followRemoteHEAD always &&
cd "$D" && git -C two fetch origin refs/heads/main:refs/remotes/origin/main &&
cd two && echo refs/remotes/origin/other >expect &&
git remote set-head origin other && git -C two symbolic-ref refs/remotes/origin/HEAD >actual &&
git config set remote.origin.followRemoteHEAD always && test_cmp expect actual
git fetch origin refs/heads/main:refs/remotes/origin/main && '
echo refs/remotes/origin/other >expect &&
git symbolic-ref refs/remotes/origin/HEAD >actual && test_expect_success 'followRemoteHEAD create does not overwrite dangling symref' '
test_cmp expect actual git -C two remote add -m does-not-exist custom-head ../one &&
) test_config -C two remote.custom-head.followRemoteHEAD create &&
git -C two fetch custom-head &&
echo refs/remotes/custom-head/does-not-exist >expect &&
git -C two symbolic-ref refs/remotes/custom-head/HEAD >actual &&
test_cmp expect actual
' '
test_expect_success 'fetch --prune on its own works as expected' ' test_expect_success 'fetch --prune on its own works as expected' '
cd "$D" &&
git clone . prune && git clone . prune &&
cd prune && (
git update-ref refs/remotes/origin/extrabranch main && cd prune &&
git update-ref refs/remotes/origin/extrabranch main &&
git fetch --prune origin && git fetch --prune origin &&
test_must_fail git rev-parse origin/extrabranch test_must_fail git rev-parse origin/extrabranch
)
' '
test_expect_success 'fetch --prune with a branch name keeps branches' ' test_expect_success 'fetch --prune with a branch name keeps branches' '
cd "$D" &&
git clone . prune-branch && git clone . prune-branch &&
cd prune-branch && (
git update-ref refs/remotes/origin/extrabranch main && cd prune-branch &&
git update-ref refs/remotes/origin/extrabranch main &&
git fetch --prune origin main && git fetch --prune origin main &&
git rev-parse origin/extrabranch git rev-parse origin/extrabranch
)
' '
test_expect_success 'fetch --prune with a namespace keeps other namespaces' ' test_expect_success 'fetch --prune with a namespace keeps other namespaces' '
cd "$D" &&
git clone . prune-namespace && git clone . prune-namespace &&
cd prune-namespace && (
cd prune-namespace &&
git fetch --prune origin refs/heads/a/*:refs/remotes/origin/a/* && git fetch --prune origin refs/heads/a/*:refs/remotes/origin/a/* &&
git rev-parse origin/main git rev-parse origin/main
)
' '
test_expect_success 'fetch --prune handles overlapping refspecs' ' test_expect_success 'fetch --prune handles overlapping refspecs' '
cd "$D" &&
git update-ref refs/pull/42/head main && git update-ref refs/pull/42/head main &&
git clone . prune-overlapping && git clone . prune-overlapping &&
cd prune-overlapping && (
git config --add remote.origin.fetch refs/pull/*/head:refs/remotes/origin/pr/* && cd prune-overlapping &&
git config --add remote.origin.fetch refs/pull/*/head:refs/remotes/origin/pr/* &&
git fetch --prune origin && git fetch --prune origin &&
git rev-parse origin/main && git rev-parse origin/main &&
git rev-parse origin/pr/42 && git rev-parse origin/pr/42 &&
git config --unset-all remote.origin.fetch && git config --unset-all remote.origin.fetch &&
git config remote.origin.fetch refs/pull/*/head:refs/remotes/origin/pr/* && git config remote.origin.fetch refs/pull/*/head:refs/remotes/origin/pr/* &&
git config --add remote.origin.fetch refs/heads/*:refs/remotes/origin/* && git config --add remote.origin.fetch refs/heads/*:refs/remotes/origin/* &&
git fetch --prune origin && git fetch --prune origin &&
git rev-parse origin/main && git rev-parse origin/main &&
git rev-parse origin/pr/42 git rev-parse origin/pr/42
)
' '
test_expect_success 'fetch --prune --tags prunes branches but not tags' ' test_expect_success 'fetch --prune --tags prunes branches but not tags' '
cd "$D" &&
git clone . prune-tags && git clone . prune-tags &&
cd prune-tags && (
git tag sometag main && cd prune-tags &&
# Create what looks like a remote-tracking branch from an earlier git tag sometag main &&
# fetch that has since been deleted from the remote: # Create what looks like a remote-tracking branch from an earlier
git update-ref refs/remotes/origin/fake-remote main && # fetch that has since been deleted from the remote:
git update-ref refs/remotes/origin/fake-remote main &&
git fetch --prune --tags origin && git fetch --prune --tags origin &&
git rev-parse origin/main && git rev-parse origin/main &&
test_must_fail git rev-parse origin/fake-remote && test_must_fail git rev-parse origin/fake-remote &&
git rev-parse sometag git rev-parse sometag
)
' '
test_expect_success 'fetch --prune --tags with branch does not prune other things' ' test_expect_success 'fetch --prune --tags with branch does not prune other things' '
cd "$D" &&
git clone . prune-tags-branch && git clone . prune-tags-branch &&
cd prune-tags-branch && (
git tag sometag main && cd prune-tags-branch &&
git update-ref refs/remotes/origin/extrabranch main && git tag sometag main &&
git update-ref refs/remotes/origin/extrabranch main &&
git fetch --prune --tags origin main && git fetch --prune --tags origin main &&
git rev-parse origin/extrabranch && git rev-parse origin/extrabranch &&
git rev-parse sometag git rev-parse sometag
)
' '
test_expect_success 'fetch --prune --tags with refspec prunes based on refspec' ' test_expect_success 'fetch --prune --tags with refspec prunes based on refspec' '
cd "$D" &&
git clone . prune-tags-refspec && git clone . prune-tags-refspec &&
cd prune-tags-refspec && (
git tag sometag main && cd prune-tags-refspec &&
git update-ref refs/remotes/origin/foo/otherbranch main && git tag sometag main &&
git update-ref refs/remotes/origin/extrabranch main && git update-ref refs/remotes/origin/foo/otherbranch main &&
git update-ref refs/remotes/origin/extrabranch main &&
git fetch --prune --tags origin refs/heads/foo/*:refs/remotes/origin/foo/* && git fetch --prune --tags origin refs/heads/foo/*:refs/remotes/origin/foo/* &&
test_must_fail git rev-parse refs/remotes/origin/foo/otherbranch && test_must_fail git rev-parse refs/remotes/origin/foo/otherbranch &&
git rev-parse origin/extrabranch && git rev-parse origin/extrabranch &&
git rev-parse sometag git rev-parse sometag
)
' '
test_expect_success 'fetch --tags gets tags even without a configured remote' ' test_expect_success 'fetch --tags gets tags even without a configured remote' '
@@ -381,21 +356,21 @@ test_expect_success 'fetch --tags gets tags even without a configured remote' '
' '
test_expect_success REFFILES 'fetch --prune fails to delete branches' ' test_expect_success REFFILES 'fetch --prune fails to delete branches' '
cd "$D" &&
git clone . prune-fail && git clone . prune-fail &&
cd prune-fail && (
git update-ref refs/remotes/origin/extrabranch main && cd prune-fail &&
git pack-refs --all && git update-ref refs/remotes/origin/extrabranch main &&
: this will prevent --prune from locking packed-refs for deleting refs, but adding loose refs still succeeds && git pack-refs --all &&
>.git/packed-refs.new && : this will prevent --prune from locking packed-refs for deleting refs, but adding loose refs still succeeds &&
>.git/packed-refs.new &&
test_must_fail git fetch --prune origin test_must_fail git fetch --prune origin
)
' '
test_expect_success 'fetch --atomic works with a single branch' ' test_expect_success 'fetch --atomic works with a single branch' '
test_when_finished "rm -rf \"$D\"/atomic" && test_when_finished "rm -rf atomic" &&
cd "$D" &&
git clone . atomic && git clone . atomic &&
git branch atomic-branch && git branch atomic-branch &&
oid=$(git rev-parse atomic-branch) && oid=$(git rev-parse atomic-branch) &&
@@ -408,9 +383,8 @@ test_expect_success 'fetch --atomic works with a single branch' '
' '
test_expect_success 'fetch --atomic works with multiple branches' ' test_expect_success 'fetch --atomic works with multiple branches' '
test_when_finished "rm -rf \"$D\"/atomic" && test_when_finished "rm -rf atomic" &&
cd "$D" &&
git clone . atomic && git clone . atomic &&
git branch atomic-branch-1 && git branch atomic-branch-1 &&
git branch atomic-branch-2 && git branch atomic-branch-2 &&
@@ -423,9 +397,8 @@ test_expect_success 'fetch --atomic works with multiple branches' '
' '
test_expect_success 'fetch --atomic works with mixed branches and tags' ' test_expect_success 'fetch --atomic works with mixed branches and tags' '
test_when_finished "rm -rf \"$D\"/atomic" && test_when_finished "rm -rf atomic" &&
cd "$D" &&
git clone . atomic && git clone . atomic &&
git branch atomic-mixed-branch && git branch atomic-mixed-branch &&
git tag atomic-mixed-tag && git tag atomic-mixed-tag &&
@@ -437,9 +410,8 @@ test_expect_success 'fetch --atomic works with mixed branches and tags' '
' '
test_expect_success 'fetch --atomic prunes references' ' test_expect_success 'fetch --atomic prunes references' '
test_when_finished "rm -rf \"$D\"/atomic" && test_when_finished "rm -rf atomic" &&
cd "$D" &&
git branch atomic-prune-delete && git branch atomic-prune-delete &&
git clone . atomic && git clone . atomic &&
git branch --delete atomic-prune-delete && git branch --delete atomic-prune-delete &&
@@ -453,9 +425,8 @@ test_expect_success 'fetch --atomic prunes references' '
' '
test_expect_success 'fetch --atomic aborts with non-fast-forward update' ' test_expect_success 'fetch --atomic aborts with non-fast-forward update' '
test_when_finished "rm -rf \"$D\"/atomic" && test_when_finished "rm -rf atomic" &&
cd "$D" &&
git branch atomic-non-ff && git branch atomic-non-ff &&
git clone . atomic && git clone . atomic &&
git rev-parse HEAD >actual && git rev-parse HEAD >actual &&
@@ -472,9 +443,8 @@ test_expect_success 'fetch --atomic aborts with non-fast-forward update' '
' '
test_expect_success 'fetch --atomic executes a single reference transaction only' ' test_expect_success 'fetch --atomic executes a single reference transaction only' '
test_when_finished "rm -rf \"$D\"/atomic" && test_when_finished "rm -rf atomic" &&
cd "$D" &&
git clone . atomic && git clone . atomic &&
git branch atomic-hooks-1 && git branch atomic-hooks-1 &&
git branch atomic-hooks-2 && git branch atomic-hooks-2 &&
@@ -499,9 +469,8 @@ test_expect_success 'fetch --atomic executes a single reference transaction only
' '
test_expect_success 'fetch --atomic aborts all reference updates if hook aborts' ' test_expect_success 'fetch --atomic aborts all reference updates if hook aborts' '
test_when_finished "rm -rf \"$D\"/atomic" && test_when_finished "rm -rf atomic" &&
cd "$D" &&
git clone . atomic && git clone . atomic &&
git branch atomic-hooks-abort-1 && git branch atomic-hooks-abort-1 &&
git branch atomic-hooks-abort-2 && git branch atomic-hooks-abort-2 &&
@@ -536,9 +505,8 @@ test_expect_success 'fetch --atomic aborts all reference updates if hook aborts'
' '
test_expect_success 'fetch --atomic --append appends to FETCH_HEAD' ' test_expect_success 'fetch --atomic --append appends to FETCH_HEAD' '
test_when_finished "rm -rf \"$D\"/atomic" && test_when_finished "rm -rf atomic" &&
cd "$D" &&
git clone . atomic && git clone . atomic &&
oid=$(git rev-parse HEAD) && oid=$(git rev-parse HEAD) &&
@@ -574,8 +542,7 @@ test_expect_success REFFILES 'fetch --atomic fails transaction if reference lock
' '
test_expect_success '--refmap="" ignores configured refspec' ' test_expect_success '--refmap="" ignores configured refspec' '
cd "$TRASH_DIRECTORY" && git clone . remote-refs &&
git clone "$D" remote-refs &&
git -C remote-refs rev-parse remotes/origin/main >old && git -C remote-refs rev-parse remotes/origin/main >old &&
git -C remote-refs update-ref refs/remotes/origin/main main~1 && git -C remote-refs update-ref refs/remotes/origin/main main~1 &&
git -C remote-refs rev-parse remotes/origin/main >new && git -C remote-refs rev-parse remotes/origin/main >new &&
@@ -599,34 +566,26 @@ test_expect_success '--refmap="" and --prune' '
test_expect_success 'fetch tags when there is no tags' ' test_expect_success 'fetch tags when there is no tags' '
cd "$D" && git init notags &&
git -C notags fetch -t ..
mkdir notags &&
cd notags &&
git init &&
git fetch -t ..
' '
test_expect_success 'fetch following tags' ' test_expect_success 'fetch following tags' '
cd "$D" &&
git tag -a -m "annotated" anno HEAD && git tag -a -m "annotated" anno HEAD &&
git tag light HEAD && git tag light HEAD &&
mkdir four && git init four &&
cd four && (
git init && cd four &&
git fetch .. :track &&
git fetch .. :track && git show-ref --verify refs/tags/anno &&
git show-ref --verify refs/tags/anno && git show-ref --verify refs/tags/light
git show-ref --verify refs/tags/light )
' '
test_expect_success 'fetch uses remote ref names to describe new refs' ' test_expect_success 'fetch uses remote ref names to describe new refs' '
cd "$D" &&
git init descriptive && git init descriptive &&
( (
cd descriptive && cd descriptive &&
@@ -654,30 +613,20 @@ test_expect_success 'fetch uses remote ref names to describe new refs' '
test_expect_success 'fetch must not resolve short tag name' ' test_expect_success 'fetch must not resolve short tag name' '
cd "$D" && git init five &&
test_must_fail git -C five fetch .. anno:five
mkdir five &&
cd five &&
git init &&
test_must_fail git fetch .. anno:five
' '
test_expect_success 'fetch can now resolve short remote name' ' test_expect_success 'fetch can now resolve short remote name' '
cd "$D" &&
git update-ref refs/remotes/six/HEAD HEAD && git update-ref refs/remotes/six/HEAD HEAD &&
mkdir six && git init six &&
cd six && git -C six fetch .. six:six
git init &&
git fetch .. six:six
' '
test_expect_success 'create bundle 1' ' test_expect_success 'create bundle 1' '
cd "$D" &&
echo >file updated again by origin && echo >file updated again by origin &&
git commit -a -m "tip" && git commit -a -m "tip" &&
git bundle create --version=3 bundle1 main^..main git bundle create --version=3 bundle1 main^..main
@@ -691,35 +640,36 @@ test_expect_success 'header of bundle looks right' '
OID refs/heads/main OID refs/heads/main
EOF EOF
sed -e "s/$OID_REGEX/OID/g" -e "5q" "$D"/bundle1 >actual && sed -e "s/$OID_REGEX/OID/g" -e "5q" bundle1 >actual &&
test_cmp expect actual test_cmp expect actual
' '
test_expect_success 'create bundle 2' ' test_expect_success 'create bundle 2' '
cd "$D" &&
git bundle create bundle2 main~2..main git bundle create bundle2 main~2..main
' '
test_expect_success 'unbundle 1' ' test_expect_success 'unbundle 1' '
cd "$D/bundle" && (
git checkout -b some-branch && cd bundle &&
test_must_fail git fetch "$D/bundle1" main:main git checkout -b some-branch &&
test_must_fail git fetch bundle1 main:main
)
' '
test_expect_success 'bundle 1 has only 3 files ' ' test_expect_success 'bundle 1 has only 3 files ' '
cd "$D" &&
test_bundle_object_count bundle1 3 test_bundle_object_count bundle1 3
' '
test_expect_success 'unbundle 2' ' test_expect_success 'unbundle 2' '
cd "$D/bundle" && (
git fetch ../bundle2 main:main && cd bundle &&
test "tip" = "$(git log -1 --pretty=oneline main | cut -d" " -f2)" git fetch ../bundle2 main:main &&
test "tip" = "$(git log -1 --pretty=oneline main | cut -d" " -f2)"
)
' '
test_expect_success 'bundle does not prerequisite objects' ' test_expect_success 'bundle does not prerequisite objects' '
cd "$D" &&
touch file2 && touch file2 &&
git add file2 && git add file2 &&
git commit -m add.file2 file2 && git commit -m add.file2 file2 &&
@@ -729,7 +679,6 @@ test_expect_success 'bundle does not prerequisite objects' '
test_expect_success 'bundle should be able to create a full history' ' test_expect_success 'bundle should be able to create a full history' '
cd "$D" &&
git tag -a -m "1.0" v1.0 main && git tag -a -m "1.0" v1.0 main &&
git bundle create bundle4 v1.0 git bundle create bundle4 v1.0
@@ -783,7 +732,6 @@ test_expect_success 'quoting of a strangely named repo' '
test_expect_success 'bundle should record HEAD correctly' ' test_expect_success 'bundle should record HEAD correctly' '
cd "$D" &&
git bundle create bundle5 HEAD main && git bundle create bundle5 HEAD main &&
git bundle list-heads bundle5 >actual && git bundle list-heads bundle5 >actual &&
for h in HEAD refs/heads/main for h in HEAD refs/heads/main
@@ -803,7 +751,6 @@ test_expect_success 'mark initial state of origin/main' '
test_expect_success 'explicit fetch should update tracking' ' test_expect_success 'explicit fetch should update tracking' '
cd "$D" &&
git branch -f side && git branch -f side &&
( (
cd three && cd three &&
@@ -818,7 +765,6 @@ test_expect_success 'explicit fetch should update tracking' '
test_expect_success 'explicit pull should update tracking' ' test_expect_success 'explicit pull should update tracking' '
cd "$D" &&
git branch -f side && git branch -f side &&
( (
cd three && cd three &&
@@ -832,7 +778,6 @@ test_expect_success 'explicit pull should update tracking' '
' '
test_expect_success 'explicit --refmap is allowed only with command-line refspec' ' test_expect_success 'explicit --refmap is allowed only with command-line refspec' '
cd "$D" &&
( (
cd three && cd three &&
test_must_fail git fetch --refmap="*:refs/remotes/none/*" test_must_fail git fetch --refmap="*:refs/remotes/none/*"
@@ -840,7 +785,6 @@ test_expect_success 'explicit --refmap is allowed only with command-line refspec
' '
test_expect_success 'explicit --refmap option overrides remote.*.fetch' ' test_expect_success 'explicit --refmap option overrides remote.*.fetch' '
cd "$D" &&
git branch -f side && git branch -f side &&
( (
cd three && cd three &&
@@ -855,7 +799,6 @@ test_expect_success 'explicit --refmap option overrides remote.*.fetch' '
' '
test_expect_success 'explicitly empty --refmap option disables remote.*.fetch' ' test_expect_success 'explicitly empty --refmap option disables remote.*.fetch' '
cd "$D" &&
git branch -f side && git branch -f side &&
( (
cd three && cd three &&
@@ -870,7 +813,6 @@ test_expect_success 'explicitly empty --refmap option disables remote.*.fetch' '
test_expect_success 'configured fetch updates tracking' ' test_expect_success 'configured fetch updates tracking' '
cd "$D" &&
git branch -f side && git branch -f side &&
( (
cd three && cd three &&
@@ -884,7 +826,6 @@ test_expect_success 'configured fetch updates tracking' '
' '
test_expect_success 'non-matching refspecs do not confuse tracking update' ' test_expect_success 'non-matching refspecs do not confuse tracking update' '
cd "$D" &&
git update-ref refs/odd/location HEAD && git update-ref refs/odd/location HEAD &&
( (
cd three && cd three &&
@@ -901,14 +842,12 @@ test_expect_success 'non-matching refspecs do not confuse tracking update' '
test_expect_success 'pushing nonexistent branch by mistake should not segv' ' test_expect_success 'pushing nonexistent branch by mistake should not segv' '
cd "$D" &&
test_must_fail git push seven no:no test_must_fail git push seven no:no
' '
test_expect_success 'auto tag following fetches minimum' ' test_expect_success 'auto tag following fetches minimum' '
cd "$D" &&
git clone .git follow && git clone .git follow &&
git checkout HEAD^0 && git checkout HEAD^0 &&
( (
@@ -1307,7 +1246,7 @@ test_expect_success 'fetch --prune prints the remotes url' '
cd only-prunes && cd only-prunes &&
git fetch --prune origin 2>&1 | head -n1 >../actual git fetch --prune origin 2>&1 | head -n1 >../actual
) && ) &&
echo "From ${D}/." >expect && echo "From $(pwd)/." >expect &&
test_cmp expect actual test_cmp expect actual
' '
@@ -1357,14 +1296,14 @@ test_expect_success 'fetching with auto-gc does not lock up' '
echo "$*" && echo "$*" &&
false false
EOF EOF
git clone "file://$D" auto-gc && git clone "file://$PWD" auto-gc &&
test_commit test2 && test_commit test2 &&
( (
cd auto-gc && cd auto-gc &&
git config fetch.unpackLimit 1 && git config fetch.unpackLimit 1 &&
git config gc.autoPackLimit 1 && git config gc.autoPackLimit 1 &&
git config gc.autoDetach false && git config gc.autoDetach false &&
GIT_ASK_YESNO="$D/askyesno" git fetch --verbose >fetch.out 2>&1 && GIT_ASK_YESNO="$TRASH_DIRECTORY/askyesno" git fetch --verbose >fetch.out 2>&1 &&
test_grep "Auto packing the repository" fetch.out && test_grep "Auto packing the repository" fetch.out &&
! grep "Should I try again" fetch.out ! grep "Should I try again" fetch.out
) )