Skip to content

Commit 3d8314f

Browse files
peffgitster
authored andcommitted
clone: propagate empty remote HEAD even with other branches
Unless "--branch" was given, clone generally tries to match the local HEAD to the remote one. For most repositories, this is easy: the remote tells us which branch HEAD was pointing to, and we call our local checkout() function on that branch. When cloning an empty repository, it's a little more tricky: we have special code that checks the transport's "unborn" extension, or falls back to our local idea of what the default branch should be. In either case, we point the new HEAD to that, and set up the branch.* config. But that leaves one case unhandled: when the remote repository _isn't_ empty, but its HEAD is unborn. The checkout() function is smart enough to realize we didn't fetch the remote HEAD and it bails with a warning. But we'll have ignored any information the remote gave us via the unborn extension. This leads to nonsense outcomes: - If the remote has its HEAD pointing to an unborn "foo" and contains another branch "bar", cloning will get branch "bar" but leave the local HEAD pointing at "master" (or whatever our local default is), which is useless. The project does not use "master" as a branch. - Worse, if the other branch "bar" is instead called "master" (but again, the remote HEAD is not pointing to it), then we end up with a local unborn branch "master", which is not connected to the remote "master" (it shares no history, and there's no branch.* config). Instead, we should try to use the remote's HEAD, even if its unborn, to be consistent with the other cases. The reason this case was missed is that cmd_clone() handles empty and non-empty repositories on two different sides of a conditional: if (we have any refs) { fetch refs; check for --branch; otherwise, try to point our head at remote head; otherwise, our head is NULL; } else { check for --branch; otherwise, try to use "unborn" extension; otherwise, fall back to our default name name; } So the smallest change would be to repeat the "unborn" logic at the end of the first block. But we can note some other overlaps and inconsistencies: - both sides have to handle --branch (though note that it's always an error for the empty repo case, since an empty repo by definition does not have a matching branch) - the fall back to the default name is much more explicit in the empty-repo case. The non-empty case eventually ends up bailing from checkout() with a warning, which produces a similar result, but fails to set up the branch config we do in the empty case. So let's pull the HEAD setup out of this conditional entirely. This de-duplicates some of the code and the result is easy to follow, because helper functions like find_ref_by_name() do the right thing even in the empty-repo case (i.e., by returning NULL). There are two subtleties: - for a remote with a detached HEAD, it will advertise an oid for HEAD (which we store in our "remote_head" variable), but we won't find a matching refname (so our "remote_head_points_at" is NULL). In this case we make a local detached HEAD to match. Right now this happens implicitly by reaching update_head() with a non-NULL remote_head (since we skip all of the unborn-fallback). We'll now need to account for it explicitly before doing the fallback. - for an empty repo, we issue a warning to the user that they've cloned an empty repo. The text of that warning doesn't make sense for a non-empty repo with an unborn HEAD, so we'll have to differentiate the two cases there. We could just use different text, but instead let's allow the code to continue down to checkout(), which will issue an appropriate warning, like: remote HEAD refers to nonexistent ref, unable to checkout Continuing down to checkout() will make it easier to do more fixes on top (see below). Note that this patch fixes the case where the other side reports an unborn head to us using the protocol extension. It _doesn't_ fix the case where the other side doesn't tell us, we locally guess "master", and the other side happens to have a "master" which its HEAD doesn't point. But it doesn't make anything worse there, and it should actually make it easier to fix that problem on top. Signed-off-by: Jeff King <[email protected]> Signed-off-by: Junio C Hamano <[email protected]>
1 parent f77710c commit 3d8314f

File tree

2 files changed

+55
-22
lines changed

2 files changed

+55
-22
lines changed

builtin/clone.c

Lines changed: 17 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1266,36 +1266,31 @@ int cmd_clone(int argc, const char **argv, const char *prefix)
12661266
if (transport_fetch_refs(transport, mapped_refs))
12671267
die(_("remote transport reported error"));
12681268
}
1269+
}
12691270

1270-
remote_head = find_ref_by_name(refs, "HEAD");
1271-
remote_head_points_at =
1272-
guess_remote_head(remote_head, mapped_refs, 0);
1273-
1274-
if (option_branch) {
1275-
our_head_points_at =
1276-
find_remote_branch(mapped_refs, option_branch);
1271+
remote_head = find_ref_by_name(refs, "HEAD");
1272+
remote_head_points_at = guess_remote_head(remote_head, mapped_refs, 0);
12771273

1278-
if (!our_head_points_at)
1279-
die(_("Remote branch %s not found in upstream %s"),
1280-
option_branch, remote_name);
1281-
}
1282-
else
1283-
our_head_points_at = remote_head_points_at;
1284-
}
1285-
else {
1274+
if (option_branch) {
1275+
our_head_points_at = find_remote_branch(mapped_refs, option_branch);
1276+
if (!our_head_points_at)
1277+
die(_("Remote branch %s not found in upstream %s"),
1278+
option_branch, remote_name);
1279+
} else if (remote_head_points_at) {
1280+
our_head_points_at = remote_head_points_at;
1281+
} else if (remote_head) {
1282+
our_head_points_at = NULL;
1283+
} else {
12861284
const char *branch;
12871285
const char *ref;
12881286
char *ref_free = NULL;
12891287

1290-
if (option_branch)
1291-
die(_("Remote branch %s not found in upstream %s"),
1292-
option_branch, remote_name);
1288+
if (!mapped_refs) {
1289+
warning(_("You appear to have cloned an empty repository."));
1290+
option_no_checkout = 1;
1291+
}
12931292

1294-
warning(_("You appear to have cloned an empty repository."));
12951293
our_head_points_at = NULL;
1296-
remote_head_points_at = NULL;
1297-
remote_head = NULL;
1298-
option_no_checkout = 1;
12991294

13001295
if (transport_ls_refs_options.unborn_head_target &&
13011296
skip_prefix(transport_ls_refs_options.unborn_head_target,

t/t5702-protocol-v2.sh

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,44 @@ test_expect_success 'bare clone propagates empty default branch' '
250250
grep "refs/heads/mydefaultbranch" file_empty_child.git/HEAD
251251
'
252252

253+
test_expect_success 'clone propagates unborn HEAD from non-empty repo' '
254+
test_when_finished "rm -rf file_unborn_parent file_unborn_child" &&
255+
256+
git init file_unborn_parent &&
257+
(
258+
cd file_unborn_parent &&
259+
git checkout -b branchwithstuff &&
260+
test_commit --no-tag stuff &&
261+
git symbolic-ref HEAD refs/heads/mydefaultbranch
262+
) &&
263+
264+
GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME= \
265+
git -c init.defaultBranch=main -c protocol.version=2 \
266+
clone "file://$(pwd)/file_unborn_parent" \
267+
file_unborn_child 2>stderr &&
268+
grep "refs/heads/mydefaultbranch" file_unborn_child/.git/HEAD &&
269+
grep "warning: remote HEAD refers to nonexistent ref" stderr
270+
'
271+
272+
test_expect_success 'bare clone propagates unborn HEAD from non-empty repo' '
273+
test_when_finished "rm -rf file_unborn_parent file_unborn_child.git" &&
274+
275+
git init file_unborn_parent &&
276+
(
277+
cd file_unborn_parent &&
278+
git checkout -b branchwithstuff &&
279+
test_commit --no-tag stuff &&
280+
git symbolic-ref HEAD refs/heads/mydefaultbranch
281+
) &&
282+
283+
GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME= \
284+
git -c init.defaultBranch=main -c protocol.version=2 \
285+
clone --bare "file://$(pwd)/file_unborn_parent" \
286+
file_unborn_child.git 2>stderr &&
287+
grep "refs/heads/mydefaultbranch" file_unborn_child.git/HEAD &&
288+
! grep "warning:" stderr
289+
'
290+
253291
test_expect_success 'fetch with file:// using protocol v2' '
254292
test_when_finished "rm -f log" &&
255293

0 commit comments

Comments
 (0)