Skip to content

Commit 450fc2b

Browse files
peffgitster
authored andcommitted
refs: do not clobber dangling symrefs
When given an expected "before" state, the ref-writing code will avoid overwriting any ref that does not match that expected state. We use the null oid as a sentinel value for "nothing should exist", and likewise that is the sentinel value we get when trying to read a ref that does not exist. But there's one corner case where this is ambiguous: dangling symrefs. Trying to read them will yield the null oid, but there is potentially something of value there: the dangling symref itself. For a normal recursive write, this is OK. Imagine we have a symref "FOO_HEAD" that points to a ref "refs/heads/bar" that does not exist, and we try to write to it with a create operation like: oid=$(git rev-parse HEAD) ;# or whatever git symbolic-ref FOO_HEAD refs/heads/bar echo "create FOO_HEAD $oid" | git update-ref --stdin The attempt to resolve FOO_HEAD will actually resolve "bar", yielding the null oid. That matches our expectation, and the write proceeds. This is correct, because we are not writing FOO_HEAD at all, but writing its destination "bar", which in fact does not exist. But what if the operation asked not to dereference symrefs? Like this: echo "create FOO_HEAD $oid" | git update-ref --no-deref --stdin Resolving FOO_HEAD would still result in a null oid, and the write will proceed. But it will overwrite FOO_HEAD itself, removing the fact that it ever pointed to "bar". This case is a little esoteric; we are clobbering a symref with a no-deref write of a regular ref value. But the same problem occurs when writing symrefs. For example: echo "symref-create FOO_HEAD refs/heads/other" | git update-ref --no-deref --stdin The "create" operation asked us to create FOO_HEAD only if it did not exist. But we silently overwrite the existing value. You can trigger this without using update-ref via the fetch followRemoteHEAD code. In "create" mode, it should not overwrite an existing value. But if you manually create a symref pointing to a value that does not yet exist (either via symbolic-ref or with "remote add -m"), create mode will happily overwrite it. Instead, we should detect this case and refuse to write. The correct specification to overwrite FOO_HEAD in this case is to provide an expected target ref value, like: echo "symref-update FOO_HEAD refs/heads/other ref refs/heads/bar" | git update-ref --no-deref --stdin Note that the non-symref "update" directive does not allow you to do this (you can only specify an oid). This is a weakness in the update-ref interface, and you'd have to overwrite unconditionally, like: echo "update FOO_HEAD $oid" | git update-ref --no-deref --stdin Likewise other symref operations like symref-delete do not accept the "ref" keyword. You should be able to do: echo "symref-delete FOO_HEAD ref refs/heads/bar" but cannot (and can only delete unconditionally). This patch doesn't address those gaps. We may want to do so in a future patch for completeness, but it's not clear if anybody actually wants to perform those operations. The symref update case (specifically, via followRemoteHEAD) is what I ran into in the wild. 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. (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). Signed-off-by: Jeff King <[email protected]> Signed-off-by: Junio C Hamano <[email protected]>
1 parent f1c2a42 commit 450fc2b

File tree

4 files changed

+87
-7
lines changed

4 files changed

+87
-7
lines changed

refs/files-backend.c

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2512,13 +2512,37 @@ static enum ref_transaction_error split_symref_update(struct ref_update *update,
25122512
*/
25132513
static enum ref_transaction_error check_old_oid(struct ref_update *update,
25142514
struct object_id *oid,
2515+
struct strbuf *referent,
25152516
struct strbuf *err)
25162517
{
25172518
if (update->flags & REF_LOG_ONLY ||
2518-
!(update->flags & REF_HAVE_OLD) ||
2519-
oideq(oid, &update->old_oid))
2519+
!(update->flags & REF_HAVE_OLD))
25202520
return 0;
25212521

2522+
if (oideq(oid, &update->old_oid)) {
2523+
/*
2524+
* Normally matching the expected old oid is enough. Either we
2525+
* found the ref at the expected state, or we are creating and
2526+
* expect the null oid (and likewise found nothing).
2527+
*
2528+
* But there is one exception for the null oid: if we found a
2529+
* symref pointing to nothing we'll also get the null oid. In
2530+
* regular recursive mode, that's good (we'll write to what the
2531+
* symref points to, which doesn't exist). But in no-deref
2532+
* mode, it means we'll clobber the symref, even though the
2533+
* caller asked for this to be a creation event. So flag
2534+
* that case to preserve the dangling symref.
2535+
*/
2536+
if ((update->flags & REF_NO_DEREF) && referent->len &&
2537+
is_null_oid(oid)) {
2538+
strbuf_addf(err, "cannot lock ref '%s': "
2539+
"dangling symref already exists",
2540+
ref_update_original_update_refname(update));
2541+
return REF_TRANSACTION_ERROR_CREATE_EXISTS;
2542+
}
2543+
return 0;
2544+
}
2545+
25222546
if (is_null_oid(&update->old_oid)) {
25232547
strbuf_addf(err, "cannot lock ref '%s': "
25242548
"reference already exists",
@@ -2658,7 +2682,8 @@ static enum ref_transaction_error lock_ref_for_update(struct files_ref_store *re
26582682
if (update->old_target)
26592683
ret = ref_update_check_old_target(referent.buf, update, err);
26602684
else
2661-
ret = check_old_oid(update, &lock->old_oid, err);
2685+
ret = check_old_oid(update, &lock->old_oid,
2686+
&referent, err);
26622687
if (ret)
26632688
goto out;
26642689
} else {
@@ -2690,7 +2715,8 @@ static enum ref_transaction_error lock_ref_for_update(struct files_ref_store *re
26902715
ret = REF_TRANSACTION_ERROR_EXPECTED_SYMREF;
26912716
goto out;
26922717
} else {
2693-
ret = check_old_oid(update, &lock->old_oid, err);
2718+
ret = check_old_oid(update, &lock->old_oid,
2719+
&referent, err);
26942720
if (ret) {
26952721
goto out;
26962722
}

refs/reftable-backend.c

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1272,9 +1272,33 @@ static enum ref_transaction_error prepare_single_update(struct reftable_ref_stor
12721272
ret = ref_update_check_old_target(referent->buf, u, err);
12731273
if (ret)
12741274
return ret;
1275-
} else if ((u->flags & (REF_LOG_ONLY | REF_HAVE_OLD)) == REF_HAVE_OLD &&
1276-
!oideq(&current_oid, &u->old_oid)) {
1277-
if (is_null_oid(&u->old_oid)) {
1275+
} else if ((u->flags & (REF_LOG_ONLY | REF_HAVE_OLD)) == REF_HAVE_OLD) {
1276+
if (oideq(&current_oid, &u->old_oid)) {
1277+
/*
1278+
* Normally matching the expected old oid is enough. Either we
1279+
* found the ref at the expected state, or we are creating and
1280+
* expect the null oid (and likewise found nothing).
1281+
*
1282+
* But there is one exception for the null oid: if we found a
1283+
* symref pointing to nothing we'll also get the null oid. In
1284+
* regular recursive mode, that's good (we'll write to what the
1285+
* symref points to, which doesn't exist). But in no-deref
1286+
* mode, it means we'll clobber the symref, even though the
1287+
* caller asked for this to be a creation event. So flag
1288+
* that case to preserve the dangling symref.
1289+
*
1290+
* Everything else is OK and we can fall through to the
1291+
* end of the conditional chain.
1292+
*/
1293+
if ((u->flags & REF_NO_DEREF) &&
1294+
referent->len &&
1295+
is_null_oid(&u->old_oid)) {
1296+
strbuf_addf(err, _("cannot lock ref '%s': "
1297+
"dangling symref already exists"),
1298+
ref_update_original_update_refname(u));
1299+
return REF_TRANSACTION_ERROR_CREATE_EXISTS;
1300+
}
1301+
} else if (is_null_oid(&u->old_oid)) {
12781302
strbuf_addf(err, _("cannot lock ref '%s': "
12791303
"reference already exists"),
12801304
ref_update_original_update_refname(u));

t/t1400-update-ref.sh

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2310,4 +2310,25 @@ test_expect_success 'update-ref should also create reflog for HEAD' '
23102310
test_cmp expect actual
23112311
'
23122312

2313+
test_expect_success 'dangling symref not overwritten by creation' '
2314+
test_when_finished "git update-ref -d refs/heads/dangling" &&
2315+
git symbolic-ref refs/heads/dangling refs/heads/does-not-exist &&
2316+
test_must_fail git update-ref --no-deref --stdin 2>err <<-\EOF &&
2317+
create refs/heads/dangling HEAD
2318+
EOF
2319+
test_grep "cannot lock.*dangling symref already exists" err &&
2320+
test_must_fail git rev-parse --verify refs/heads/dangling &&
2321+
test_must_fail git rev-parse --verify refs/heads/does-not-exist
2322+
'
2323+
2324+
test_expect_success 'dangling symref overwritten without old oid' '
2325+
test_when_finished "git update-ref -d refs/heads/dangling" &&
2326+
git symbolic-ref refs/heads/dangling refs/heads/does-not-exist &&
2327+
git update-ref --no-deref --stdin <<-\EOF &&
2328+
update refs/heads/dangling HEAD
2329+
EOF
2330+
git rev-parse --verify refs/heads/dangling &&
2331+
test_must_fail git rev-parse --verify refs/heads/does-not-exist
2332+
'
2333+
23132334
test_done

t/t5510-fetch.sh

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,15 @@ test_expect_success 'followRemoteHEAD does not kick in with refspecs' '
232232
test_cmp expect actual
233233
'
234234

235+
test_expect_success 'followRemoteHEAD create does not overwrite dangling symref' '
236+
git -C two remote add -m does-not-exist custom-head ../one &&
237+
test_config -C two remote.custom-head.followRemoteHEAD create &&
238+
git -C two fetch custom-head &&
239+
echo refs/remotes/custom-head/does-not-exist >expect &&
240+
git -C two symbolic-ref refs/remotes/custom-head/HEAD >actual &&
241+
test_cmp expect actual
242+
'
243+
235244
test_expect_success 'fetch --prune on its own works as expected' '
236245
git clone . prune &&
237246
(

0 commit comments

Comments
 (0)