Merge branch 'kn/fix-fetch-backfill-tag-with-batched-ref-updates' into jch

"git fetch" that involves fetching tags, when a tag being fetched
needs to overwrite existing one, failed to fetch other tags, which
has been corrected.

* kn/fix-fetch-backfill-tag-with-batched-ref-updates:
  fetch: fix failed batched updates skipping operations
  fetch: fix non-conflicting tags not being committed
  fetch: extract out reference committing logic
This commit is contained in:
Junio C Hamano
2025-12-12 15:53:05 +09:00
2 changed files with 195 additions and 26 deletions

View File

@@ -1681,6 +1681,36 @@ static void ref_transaction_rejection_handler(const char *refname,
*data->retcode = 1;
}
/*
* Commit the reference transaction. If it isn't an atomic transaction, handle
* rejected updates as part of using batched updates.
*/
static int commit_ref_transaction(struct ref_transaction **transaction,
bool is_atomic, const char *remote_name,
struct strbuf *err)
{
int retcode = ref_transaction_commit(*transaction, err);
if (retcode)
goto out;
if (!is_atomic) {
struct ref_rejection_data data = {
.conflict_msg_shown = 0,
.remote_name = remote_name,
.retcode = &retcode,
};
ref_transaction_for_each_rejected_update(*transaction,
ref_transaction_rejection_handler,
&data);
}
out:
ref_transaction_free(*transaction);
*transaction = NULL;
return retcode;
}
static int do_fetch(struct transport *transport,
struct refspec *rs,
const struct fetch_config *config)
@@ -1853,33 +1883,14 @@ static int do_fetch(struct transport *transport,
if (retcode)
goto cleanup;
retcode = ref_transaction_commit(transaction, &err);
if (retcode) {
/*
* Explicitly handle transaction cleanup to avoid
* aborting an already closed transaction.
*/
ref_transaction_free(transaction);
transaction = NULL;
retcode = commit_ref_transaction(&transaction, atomic_fetch,
transport->remote->name, &err);
/*
* With '--atomic', bail out if the transaction fails. Without '--atomic',
* continue to fetch head and perform other post-fetch operations.
*/
if (retcode && atomic_fetch)
goto cleanup;
}
if (!atomic_fetch) {
struct ref_rejection_data data = {
.retcode = &retcode,
.conflict_msg_shown = 0,
.remote_name = transport->remote->name,
};
ref_transaction_for_each_rejected_update(transaction,
ref_transaction_rejection_handler,
&data);
if (retcode) {
ref_transaction_free(transaction);
transaction = NULL;
goto cleanup;
}
}
commit_fetch_head(&fetch_head);
@@ -1945,6 +1956,14 @@ static int do_fetch(struct transport *transport,
}
cleanup:
/*
* When using batched updates, we want to commit the non-rejected
* updates and also handle the rejections.
*/
if (retcode && !atomic_fetch && transaction)
commit_ref_transaction(&transaction, false,
transport->remote->name, &err);
if (retcode) {
if (err.len) {
error("%s", err.buf);

View File

@@ -1552,6 +1552,7 @@ test_expect_success CASE_INSENSITIVE_FS,REFFILES 'D/F conflict on case insensiti
'
test_expect_success REFFILES 'D/F conflict on case sensitive filesystem with lock' '
test_when_finished rm -rf base repo &&
(
git init --ref-format=reftable base &&
cd base &&
@@ -1577,6 +1578,155 @@ test_expect_success REFFILES 'D/F conflict on case sensitive filesystem with loc
)
'
test_expect_success 'fetch --tags fetches existing tags' '
test_when_finished rm -rf base repo &&
git init base &&
git -C base commit --allow-empty -m "empty-commit" &&
git clone --bare base repo &&
git -C base tag tag-1 &&
git -C repo for-each-ref >out &&
test_grep ! "tag-1" out &&
git -C repo fetch --tags &&
git -C repo for-each-ref >out &&
test_grep "tag-1" out
'
test_expect_success 'fetch --tags fetches non-conflicting tags' '
test_when_finished rm -rf base repo &&
git init base &&
git -C base commit --allow-empty -m "empty-commit" &&
git -C base tag tag-1 &&
git clone --bare base repo &&
git -C base tag tag-2 &&
git -C repo for-each-ref >out &&
test_grep ! "tag-2" out &&
git -C base commit --allow-empty -m "second empty-commit" &&
git -C base tag -f tag-1 &&
test_must_fail git -C repo fetch --tags 2>out &&
test_grep "tag-1 (would clobber existing tag)" out &&
git -C repo for-each-ref >out &&
test_grep "tag-2" out
'
test_expect_success "backfill tags when providing a refspec" '
test_when_finished rm -rf source target &&
git init source &&
git -C source commit --allow-empty --message common &&
git clone file://"$(pwd)"/source target &&
(
cd source &&
test_commit history &&
test_commit fetch-me
) &&
# The "history" tag is backfilled even though we requested
# to only fetch HEAD
git -C target fetch origin HEAD:branch &&
git -C target tag -l >actual &&
cat >expect <<-\EOF &&
fetch-me
history
EOF
test_cmp expect actual
'
test_expect_success REFFILES "FETCH_HEAD is updated even if ref updates fail" '
test_when_finished rm -rf base repo &&
git init base &&
(
cd base &&
test_commit "updated" &&
git update-ref refs/heads/foo @ &&
git update-ref refs/heads/branch @
) &&
git init --bare repo &&
(
cd repo &&
rm -f FETCH_HEAD &&
git remote add origin ../base &&
>refs/heads/foo.lock &&
test_must_fail git fetch -f origin "refs/heads/*:refs/heads/*" 2>err &&
test_grep "error: fetching ref refs/heads/foo failed: reference already exists" err &&
test_grep "branch ${SQ}branch${SQ} of ../base" FETCH_HEAD &&
test_grep "branch ${SQ}foo${SQ} of ../base" FETCH_HEAD
)
'
test_expect_success "upstream tracking info is added with --set-upstream" '
test_when_finished rm -rf base repo &&
git init --initial-branch=main base &&
test_commit -C base "updated" &&
git init --bare --initial-branch=main repo &&
(
cd repo &&
git remote add origin ../base &&
git fetch origin --set-upstream main &&
git config get branch.main.remote >actual &&
echo "origin" >expect &&
test_cmp expect actual
)
'
test_expect_success REFFILES "upstream tracking info is added even with conflicts" '
test_when_finished rm -rf base repo &&
git init --initial-branch=main base &&
test_commit -C base "updated" &&
git init --bare --initial-branch=main repo &&
(
cd repo &&
git remote add origin ../base &&
test_must_fail git config get branch.main.remote &&
mkdir -p refs/remotes/origin &&
>refs/remotes/origin/main.lock &&
test_must_fail git fetch origin --set-upstream main &&
git config get branch.main.remote >actual &&
echo "origin" >expect &&
test_cmp expect actual
)
'
test_expect_success REFFILES "HEAD is updated even with conflicts" '
test_when_finished rm -rf base repo &&
git init base &&
(
cd base &&
test_commit "updated" &&
git update-ref refs/heads/foo @ &&
git update-ref refs/heads/branch @
) &&
git init --bare repo &&
(
cd repo &&
git remote add origin ../base &&
test_path_is_missing refs/remotes/origin/HEAD &&
mkdir -p refs/remotes/origin &&
>refs/remotes/origin/branch.lock &&
test_must_fail git fetch origin &&
test -f refs/remotes/origin/HEAD
)
'
. "$TEST_DIRECTORY"/lib-httpd.sh
start_httpd