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

Re: [PATCH 4/4] refs: do not clobber dangling symrefs

From
Patrick Steinhardt <ps@pks.im>
Date
Aug 20, 2025, 07:27 UTC
Message-ID
<aKV44BDyIMyarinZ@pks.im>
In-Reply-To
<20250819192934.GD1059295@coredump.intra.peff.net>
On Tue, Aug 19, 2025 at 03:29:34PM -0400, Jeff King wrote:
Show 6 quoted lines
> The code for the fix is relatively straight-forward given the discussion
> above. But note that we have to implement it independently for the files
> and reftable backends. The "old oid" checks happen as part of the
> locking process, which is implemented separately for each system. We may
> want to factor this out somehow, but it's beyond the scope of this
> patch.

Yeah, there's a bunch of duplication here in general. I originally wanted to refactor this at some point in time, but I never got around to it. Also because I kind of shied away from it: the logic to lock and check refs is quite intertwined with one another in both backends, so I was afraid that this would ulmitately lead to splitting hairs.

> (Another curiosity is that the messages in the reftable code are
> marked for translation, but the ones in the files backend are not. I
> followed local convention in each case, but we may want to harmonize
> this at some point).

Oh, interesting. I guess translating these messages is the right thing to do, as the messages are user facing. But this definitely does not have to be part of this patch series.

Show 44 quoted lines
> diff --git a/refs/files-backend.c b/refs/files-backend.c
> index 905555365b..a4419ef62d 100644
> --- a/refs/files-backend.c
> +++ b/refs/files-backend.c
> @@ -2512,13 +2512,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,
>  						struct object_id *oid,
> +						struct strbuf *referent,
>  						struct strbuf *err)
>  {
>  	if (update->flags & REF_LOG_ONLY ||
> -	    !(update->flags & REF_HAVE_OLD) ||
> -	    oideq(oid, &update->old_oid))
> +	    !(update->flags & REF_HAVE_OLD))
>  		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)) {
>  		strbuf_addf(err, "cannot lock ref '%s': "
>  			    "reference already exists",

Makes sense. If we've got an all-zero old object ID _but_ the locked reference points to a nonexistet ref we refuse the update.

Show 38 quoted lines
> diff --git a/refs/reftable-backend.c b/refs/reftable-backend.c
> index 99fafd75eb..ef98584bf9 100644
> --- a/refs/reftable-backend.c
> +++ b/refs/reftable-backend.c
> @@ -1272,9 +1272,33 @@ static enum ref_transaction_error prepare_single_update(struct reftable_ref_stor
>  		ret = ref_update_check_old_target(referent->buf, u, err);
>  		if (ret)
>  			return ret;
> -	} else if ((u->flags & (REF_LOG_ONLY | REF_HAVE_OLD)) == REF_HAVE_OLD &&
> -		   !oideq(&current_oid, &u->old_oid)) {
> -		if (is_null_oid(&u->old_oid)) {
> +	} else if ((u->flags & (REF_LOG_ONLY | REF_HAVE_OLD)) == REF_HAVE_OLD) {
> +		if (oideq(&current_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)) {

Wouldn't it be more natural to put the new check into this `if (is_null_oid(&u->old_oid))` branch? Makes it a bit more explicit that we really only care about the case where we expect the ref to not exist.

Ah, no. I missed that you also change the original condition and move the `oideq()` call into the whole thing. Makes sense.

Show 27 quoted lines
> diff --git a/t/t1400-update-ref.sh b/t/t1400-update-ref.sh
> index d29d23cb89..29b31e3b9b 100755
> --- a/t/t1400-update-ref.sh
> +++ b/t/t1400-update-ref.sh
> @@ -2310,4 +2310,25 @@ test_expect_success 'update-ref should also create reflog for HEAD' '
>  	test_cmp expect actual
>  '
>  
> +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

Do we also want to verify that the dangling symref got converted into a normal ref? Or do we already have other tests that do so?

Patrick
Previous: Jeff KingNext: Jeff King
Message 14 of 22 in “dangling symrefs and fetchRemoteHEAD=create”
  1. 0/4 dangling symrefs and fetchRemoteHEAD=createJeff King, Aug 19, 2025
  2. Jeff KingAug 19, 2025
  3. 1/4 t5510: make confusing config cleanup more explicitJeff King, Aug 19, 2025
  4. Eric SunshineAug 19, 2025
  5. Eric SunshineAug 19, 2025
  6. Jeff KingAug 19, 2025
  7. 2/4 t5510: stop changing top-level working directoryJeff King, Aug 19, 2025
  8. 3/4 t5510: prefer "git -C" to subshell for followRemoteHEAD testsJeff King, Aug 19, 2025
  9. SZEDER GáborAug 24, 2025
  10. Junio C HamanoAug 25, 2025
  11. Jeff KingAug 26, 2025
  12. Junio C HamanoAug 26, 2025
  13. 4/4 refs: do not clobber dangling symrefsJeff King, Aug 19, 2025
  14. Patrick SteinhardtAug 20, 2025
  15. Jeff KingAug 20, 2025
  16. Toon ClaesSep 22, 2025
  17. Junio C HamanoSep 22, 2025
  18. Jeff KingSep 22, 2025
  19. Junio C HamanoSep 22, 2025
  20. Jeff KingSep 22, 2025
  21. Toon ClaesSep 23, 2025
  22. Jeff KingSep 23, 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.