git/list[1] front-page[2] threads[3] people[4] search[5] about
 

Re: [PATCH v2] fetch: fix non-conflicting tags not being committed

From
Karthik Nayak <karthik.188@gmail.com>
Date
Nov 7, 2025, 13:15 UTC
Message-ID
<CAOLa=ZQpTqnCQs4=wcUwJOWy5mXiG4y_eTiFtPkS2uOk4U66Tw@mail.gmail.com>
In-Reply-To
<aQyLfD_zx0ndCLvU@pks.im>
Patrick Steinhardt <ps@pks.im> writes:
Show 22 quoted lines
> On Thu, Nov 06, 2025 at 09:39:25AM +0100, Karthik Nayak wrote:
>> The commit 0e358de64a (fetch: use batched reference updates, 2025-05-19)
>> updated the 'git-fetch(1)' command to use batched updates. This batches
>> updates to gain performance improvements. When fetching references, each
>> update is added to the transaction. Finally, when committing, individual
>> updates are allowed to fail with reason, while the transaction itself
>> succeeds.
>>
>> One scenario which was missed here, was fetching tags. When fetching
>> conflicting tags, the `fetch_and_consume_refs()` function returns '1',
>> which skipped committing the transaction and directly jumped to the
>> cleanup section. This mean that no updates were applied.
>
> Okay, this is obviously broken indeed.
>
>> This also extends to backfilling tags when using the now deprecated
>> 'branches/' format for remotes.
>
> I'm a bit lost here -- what does backfilling have to do with the
> "branches/" directory? The backfill is supposed to create tags that
> point into the history that one has just fetched. So:
>
I didn't read the code well enough. Let me walk through what I read:
The block for backfilling tags, is only triggered in `do_fetch()`, if
    if (tags == TAGS_DEFAULT && autotags) { ... }

This means that the autotags must be '1'. And I see at the start of the function that:

   int autotags = (transport->remote->fetch_tags == 1);

So I went into looking when `transport->remote->fetch_tags` would be set to '1'. This is only done in `read_branches_file()` which is done when parsing the now deprecated 'branches/' directory.

I was correct until here. But, there is something I missed.

We also pass a pointer to `autotags` to the `get_ref_map()` function. In this function, we set `autotags` to '1' for any of the following conditions:

   - When there is a refspec specified by the user.
   - We have a default branch with a remote specified.
So this means there are other scenarios we use the backfill() command.

That brings us to the second part of it, if we specify the '--tags' flag, then we fetch all tags, even the ones which aren't part of our history. This also happens as part of the `get_ref_map()` function. This flow also skips the 'backfill()' function.

So in effect, we only backfill tags, when the user doesn't specify either '--tags' or '--no-tags'.

Show 17 quoted lines
>   - With `--tags` we fetch all tags announced by the remote.
>
>   - With `--no-tags` we fetch no tags.
>
>   - Otherwise we fetch those tags that point into our history.
>
> The last behaviour is a bit more on the esoteric side, but it's
> described as such in git-fetch(1):
>
>     By default, any tag that points into the histories being fetched is
>     also fetched; the effect is to fetch tags that point at branches
>     that you are interested in. This default behavior can be changed by
>     using the --tags or --no-tags options or by configuring
>     remote.<name>.tagOpt. By using a refspec that fetches tags
>     explicitly, you can fetch tags that do not point into branches you
>     are interested in as well.
>

But backfilling isn't about diverged history, no? It's about fetching history of refs being requested.

Show 23 quoted lines
> The following test demonstrates this behaviour:
>
> 	test_expect_success "fetch single branch without explicit tag option" '
> 		git init source &&
> 		git -C source commit --allow-empty --message common &&
> 		git clone file://"$(pwd)"/source target &&
> 		(
> 			cd source &&
> 			git commit --allow-empty --message discard-me &&
> 			git tag discard-me &&
> 			git commit --amend --allow-empty --message fetch-me &&
> 			git tag fetch-me
> 		) &&
>
> 		# The "discard-me" tag does not point into the history that we are
> 		# about to fetch, so it should not have been created.
> 		git -C target fetch origin &&
> 		git -C target tag -l >actual &&
> 		echo "fetch-me" >expect &&
>
> 		# But with "--tags" we instruct git-fetch(1) to fetch all tags, so we
> 		# should now see it.
> 		git -C target fetch origin --tags &&

Here, we don't really backfill, but rather we request all tags from the remote, hence we end up with the 'discard-me' tag. Not because of the diverged history. I also confirmed this by adding a breakpoint into the `backfill_tags()` function, while running this test.

Show 7 quoted lines
> 		git -C target tag -l >actual &&
> 		cat >expect <<-\EOF &&
> 		discard-me
> 		fetch-me
> 		EOF
> 		test_cmp expect actual
> 	'
But I was able to slightly modify the test to get the required affect:
  test_expect_success "backfill tags when providing a refspec" '
  	git init source &&
  	git -C source commit --allow-empty --message common &&
  	git clone file://"$(pwd)"/source target &&
  	(
  	    cd source &&
  	    git commit --allow-empty --message history &&
  	    git tag history &&
  	    git commit --allow-empty --message fetch-me &&
  	    git tag fetch-me
  	) &&
  	# The "history" tag is backfilled eventhough we requested
  	# to only fetch the master
  	git -C target fetch origin master:branch &&
  	git -C target tag -l >actual &&
  	cat >expect <<-\EOF &&
  	fetch-me
  	history
  	EOF
  	test_cmp expect actual
  '

I will add this in. Thanks for the explanation, it really helped consolidate my understanding here.

Show 44 quoted lines
>> diff --git a/builtin/fetch.c b/builtin/fetch.c
>> index c7ff3480fb..d5aee5af10 100644
>> --- a/builtin/fetch.c
>> +++ b/builtin/fetch.c
>> @@ -1686,6 +1686,42 @@ 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) {
>> +		/*
>> +		 * Explicitly handle transaction cleanup to avoid
>> +		 * aborting an already closed transaction.
>> +		 */
>> +		ref_transaction_free(*transaction);
>> +		*transaction = NULL;
>> +	}
>> +
>> +	if (*transaction && !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);
>> +
>> +		ref_transaction_free(*transaction);
>> +		*transaction = NULL;
>> +	}
>
> Okay. Do we need to discern cases where this is called and we haven't
> managed to even queue a single reference update?
>

I don't see a reason. This is anyways a post-commit action, if there are no updates, there will be no rejections. So this will be a no-op.

Show 10 quoted lines
>> +	return retcode;
>> +}
>> +
>>  static int do_fetch(struct transport *transport,
>>  		    struct refspec *rs,
>>  		    const struct fetch_config *config)
>
> Nit: it might make sense to have a preparatory commit that extracts the
> function but that is otherwise a no-op change.
>

Let me do that. I was thinking the change is small. But perhaps that'd be easier for reviewing.

Show 35 quoted lines
>> @@ -1826,6 +1862,10 @@ static int do_fetch(struct transport *transport,
>>
>>  	if (fetch_and_consume_refs(&display_state, transport, transaction, ref_map,
>>  				   &fetch_head, config)) {
>> +		/* As we're using batched updates, commit any pending updates. */
>> +		if (!atomic_fetch)
>> +			commit_ref_transaction(&transaction, false,
>> +					       transport->remote->name, &err);
>>  		retcode = 1;
>>  		goto cleanup;
>>  	}
>
> Hm. Don't we also have to unset the transaction now? Ah, no, you pass
> the pointer to the transaction here and set it to `NULL` in
> `commit_ref_transaction()`. Makes sense.
>
>> @@ -1848,8 +1888,12 @@ static int do_fetch(struct transport *transport,
>>  			 * the transaction and don't commit anything.
>>  			 */
>>  			if (backfill_tags(&display_state, transport, transaction, tags_ref_map,
>> -					  &fetch_head, config))
>> +					  &fetch_head, config)) {
>> +				if (!atomic_fetch)
>> +					commit_ref_transaction(&transaction, false,
>> +							       transport->remote->name, &err);
>>  				retcode = 1;
>> +			}
>>  		}
>>
>>  		free_refs(tags_ref_map);
>
> We now have three different callsites where we commit the transaction.
> It gets better due to the newly introduced function, but it overall
> feels somewhat fragile regardless of that.
>

Yeah I must agree with that. I could think of a cleaner way, but will spend some time here.

> Thanks!
>
> Patrick

Thanks, Karthik

Previous: Junio C HamanoNext: Patrick Steinhardt
Message 8 of 54 in “fetch: fix non-conflicting tags not being committed”
  1. fetch: fix non-conflicting tags not being committedKarthik Nayak, Nov 3, 2025
  2. Eric SunshineNov 3, 2025
  3. Karthik NayakNov 3, 2025
  4. Justin ToblerNov 3, 2025
  5. fetch: fix non-conflicting tags not being committedKarthik Nayak, Nov 6, 2025
  6. Patrick SteinhardtNov 6, 2025
  7. Junio C HamanoNov 6, 2025
  8. Karthik NayakNov 7, 2025
  9. Patrick SteinhardtNov 7, 2025
  10. Karthik NayakNov 7, 2025
  11. Justin ToblerNov 6, 2025
  12. Karthik NayakNov 7, 2025
  13. 0/2 fetch: fix non-conflicting tags not being committedKarthik Nayak, Nov 8, 2025
  14. 1/2 fetch: extract out reference committing logicKarthik Nayak, Nov 8, 2025
  15. Patrick SteinhardtNov 10, 2025
  16. Karthik NayakNov 10, 2025
  17. 2/2 fetch: fix non-conflicting tags not being committedKarthik Nayak, Nov 8, 2025
  18. Patrick SteinhardtNov 10, 2025
  19. Karthik NayakNov 10, 2025
  20. 0/2 fetch: fix non-conflicting tags not being committedKarthik Nayak, Nov 11, 2025
  21. 1/2 fetch: extract out reference committing logicKarthik Nayak, Nov 11, 2025
  22. 2/2 fetch: fix non-conflicting tags not being committedKarthik Nayak, Nov 11, 2025
  23. Patrick SteinhardtNov 12, 2025
  24. Karthik NayakNov 12, 2025
  25. Junio C HamanoNov 12, 2025
  26. 0/2 fetch: fix non-conflicting tags not being committedKarthik Nayak, Nov 13, 2025
  27. 1/2 fetch: extract out reference committing logicKarthik Nayak, Nov 13, 2025
  28. 2/2 fetch: fix non-conflicting tags not being committedKarthik Nayak, Nov 13, 2025
  29. Junio C HamanoNov 13, 2025
  30. Karthik NayakNov 15, 2025
  31. Junio C HamanoNov 17, 2025
  32. Karthik NayakNov 17, 2025
  33. 0/3 fetch: fix non-conflicting tags not being committedKarthik Nayak, Nov 18, 2025
  34. 1/3 fetch: extract out reference committing logicKarthik Nayak, Nov 18, 2025
  35. 2/3 fetch: fix non-conflicting tags not being committedKarthik Nayak, Nov 18, 2025
  36. 3/3 fetch: fix failed batched updates skipping operationsKarthik Nayak, Nov 18, 2025
  37. Junio C HamanoNov 18, 2025
  38. Karthik NayakNov 19, 2025
  39. 0/3 fetch: fix non-conflicting tags not being committedKarthik Nayak, Nov 19, 2025
  40. 1/3 fetch: extract out reference committing logicKarthik Nayak, Nov 19, 2025
  41. 2/3 fetch: fix non-conflicting tags not being committedKarthik Nayak, Nov 19, 2025
  42. 3/3 fetch: fix failed batched updates skipping operationsKarthik Nayak, Nov 19, 2025
  43. Eric SunshineNov 19, 2025
  44. Junio C HamanoNov 19, 2025
  45. Karthik NayakNov 21, 2025
  46. 0/3 fetch: fix non-conflicting tags not being committedKarthik Nayak, Nov 21, 2025
  47. 1/3 fetch: extract out reference committing logicKarthik Nayak, Nov 21, 2025
  48. 2/3 fetch: fix non-conflicting tags not being committedKarthik Nayak, Nov 21, 2025
  49. Patrick SteinhardtDec 1, 2025
  50. Karthik NayakDec 2, 2025
  51. 3/3 fetch: fix failed batched updates skipping operationsKarthik Nayak, Nov 21, 2025
  52. Patrick SteinhardtDec 1, 2025
  53. Karthik NayakDec 2, 2025
  54. Junio C HamanoNov 21, 2025

Read the whole thread, see it on lore, or plain text.

$ cat FOOTERMessages come from the public archive at lore.kernel.org/git, fetched every hour. The front page is chosen and written each morning by an AI editor and can be wrong; the threads themselves are the record. About and API. For agents: an MCP server at https://gitlist.dev/mcp, and any thread, story or person page as Markdown by adding .md to its URL (or sending Accept: text/markdown). Details in /llms.txt.