Skip to content

Commit 2859dcd

Browse files
peffgitster
authored andcommitted
lock_ref_sha1_basic: handle REF_NODEREF with invalid refs
We sometimes call lock_ref_sha1_basic with REF_NODEREF to operate directly on a symbolic ref. This is used, for example, to move to a detached HEAD, or when updating the contents of HEAD via checkout or symbolic-ref. However, the first step of the function is to resolve the refname to get the "old" sha1, and we do so without telling resolve_ref_unsafe() that we are only interested in the symref. As a result, we may detect a problem there not with the symref itself, but with something it points to. The real-world example I found (and what is used in the test suite) is a HEAD pointing to a ref that cannot exist, because it would cause a directory/file conflict with other existing refs. This situation is somewhat broken, of course, as trying to _commit_ on that HEAD would fail. But it's not explicitly forbidden, and we should be able to move away from it. However, neither "git checkout" nor "git symbolic-ref" can do so. We try to take the lock on HEAD, which is pointing to a non-existent ref. We bail from resolve_ref_unsafe() with errno set to EISDIR, and the lock code thinks we are attempting to create a d/f conflict. Of course we're not. The problem is that the lock code has no idea what level we were at when we got EISDIR, so trying to diagnose or remove empty directories for HEAD is not useful. To make things even more complicated, we only get EISDIR in the loose-ref case. If the refs are packed, the resolution may "succeed", giving us the pointed-to ref in "refname", but a null oid. Later, we say "ah, the null oid means we are creating; let's make sure there is room for it", but mistakenly check against the _resolved_ refname, not the original. We can fix this by making two tweaks: 1. Call resolve_ref_unsafe() with RESOLVE_REF_NO_RECURSE when REF_NODEREF is set. This means any errors we get will be from the orig_refname, and we can act accordingly. We already do this in the REF_DELETING case, but we should do it for update, too. 2. If we do get a "refname" return from resolve_ref_unsafe(), even with RESOLVE_REF_NO_RECURSE it may be the name of the ref pointed-to by a symref. We already normalize this back to orig_refname before taking the lockfile, but we need to do so before the null_oid check. While we're rearranging the REF_NODEREF handling, we can also bump the initialization of lflags to the top of the function, where we are setting up other flags. This saves us from having yet another conditional block on REF_NODEREF just to set it later. Signed-off-by: Jeff King <[email protected]> Signed-off-by: Junio C Hamano <[email protected]>
1 parent 6294dcb commit 2859dcd

File tree

3 files changed

+50
-9
lines changed

3 files changed

+50
-9
lines changed

refs/files-backend.c

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1887,7 +1887,8 @@ static struct ref_lock *lock_ref_sha1_basic(const char *refname,
18871887
const char *orig_refname = refname;
18881888
struct ref_lock *lock;
18891889
int last_errno = 0;
1890-
int type, lflags;
1890+
int type;
1891+
int lflags = 0;
18911892
int mustexist = (old_sha1 && !is_null_sha1(old_sha1));
18921893
int resolve_flags = 0;
18931894
int attempts_remaining = 3;
@@ -1898,10 +1899,11 @@ static struct ref_lock *lock_ref_sha1_basic(const char *refname,
18981899

18991900
if (mustexist)
19001901
resolve_flags |= RESOLVE_REF_READING;
1901-
if (flags & REF_DELETING) {
1902+
if (flags & REF_DELETING)
19021903
resolve_flags |= RESOLVE_REF_ALLOW_BAD_NAME;
1903-
if (flags & REF_NODEREF)
1904-
resolve_flags |= RESOLVE_REF_NO_RECURSE;
1904+
if (flags & REF_NODEREF) {
1905+
resolve_flags |= RESOLVE_REF_NO_RECURSE;
1906+
lflags |= LOCK_NO_DEREF;
19051907
}
19061908

19071909
refname = resolve_ref_unsafe(refname, resolve_flags,
@@ -1937,6 +1939,10 @@ static struct ref_lock *lock_ref_sha1_basic(const char *refname,
19371939

19381940
goto error_return;
19391941
}
1942+
1943+
if (flags & REF_NODEREF)
1944+
refname = orig_refname;
1945+
19401946
/*
19411947
* If the ref did not exist and we are creating it, make sure
19421948
* there is no existing packed ref whose name begins with our
@@ -1952,11 +1958,6 @@ static struct ref_lock *lock_ref_sha1_basic(const char *refname,
19521958

19531959
lock->lk = xcalloc(1, sizeof(struct lock_file));
19541960

1955-
lflags = 0;
1956-
if (flags & REF_NODEREF) {
1957-
refname = orig_refname;
1958-
lflags |= LOCK_NO_DEREF;
1959-
}
19601961
lock->ref_name = xstrdup(refname);
19611962
lock->orig_ref_name = xstrdup(orig_refname);
19621963
strbuf_git_path(&ref_file, "%s", refname);

t/t1401-symbolic-ref.sh

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,4 +122,11 @@ test_expect_success 'symbolic-ref does not create ref d/f conflicts' '
122122
test_must_fail git symbolic-ref refs/heads/df/conflict refs/heads/df
123123
'
124124

125+
test_expect_success 'symbolic-ref handles existing pointer to invalid name' '
126+
head=$(git rev-parse HEAD) &&
127+
git symbolic-ref HEAD refs/heads/outer &&
128+
git update-ref refs/heads/outer/inner $head &&
129+
git symbolic-ref HEAD refs/heads/unrelated
130+
'
131+
125132
test_done

t/t2011-checkout-invalid-head.sh

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,37 @@ test_expect_success 'checkout notices failure to lock HEAD' '
2525
test_must_fail git checkout -b other
2626
'
2727

28+
test_expect_success 'create ref directory/file conflict scenario' '
29+
git update-ref refs/heads/outer/inner master &&
30+
31+
# do not rely on symbolic-ref to get a known state,
32+
# as it may use the same code we are testing
33+
reset_to_df () {
34+
echo "ref: refs/heads/outer" >.git/HEAD
35+
}
36+
'
37+
38+
test_expect_success 'checkout away from d/f HEAD (unpacked, to branch)' '
39+
reset_to_df &&
40+
git checkout master
41+
'
42+
43+
test_expect_success 'checkout away from d/f HEAD (unpacked, to detached)' '
44+
reset_to_df &&
45+
git checkout --detach master
46+
'
47+
48+
test_expect_success 'pack refs' '
49+
git pack-refs --all --prune
50+
'
51+
52+
test_expect_success 'checkout away from d/f HEAD (packed, to branch)' '
53+
reset_to_df &&
54+
git checkout master
55+
'
56+
57+
test_expect_success 'checkout away from d/f HEAD (packed, to detached)' '
58+
reset_to_df &&
59+
git checkout --detach master
60+
'
2861
test_done

0 commit comments

Comments
 (0)