Subject: [PATCH v3] Limit refs to fetch to minimum in shallow clones

From: Nguyễn Thái Ngọc Duy <[email-masked]>
Date: Fri, 6 Jan 2012 14:11:09 +0700

The main purpose of shallow clones is to reduce download by only
fetching objects up to a certain depth from the given refs. The number
of objects depend on how many refs to follow. So:

 - Only fetch HEAD or the ref specified by --branch
 - Only fetch tags that reference to downloaded objects

More tags/branches can be fetched later using git-fetch as usual.

The old behaviour can still be called with --no-single-branch

Signed-off-by: Nguyễn Thái Ngọc Duy <[email-masked]>
---
  - add --no-single-branch so t5500 works without big changes.
  - die() if we cannot find suitable branch to fetch (and suggest --no-single-branch)
  - and a bit more tests to exercise new code

  --branch=<tag> (or something similar) has to wait until my other
  patch gets in a good shape (or gets dropped)

 There may be something slightly wrong with shallow code. I expect it
 to fetch only 3 objects (1 commit, 1 tree, 1 blob) with --depth=1
 in my new test but it fetches 6 (one more commit).

 Documentation/git-clone.txt |   12 ++++++++-
 builtin/clone.c             |   54 ++++++++++++++++++++++++++++++++++++++----
 t/t5500-fetch-pack.sh       |   34 ++++++++++++++++++++++++++-
 3 files changed, 92 insertions(+), 8 deletions(-)

diff --git a/Documentation/git-clone.txt b/Documentation/git-clone.txt
index 4b8b26b..58f21d6 100644
--- a/Documentation/git-clone.txt
+++ b/Documentation/git-clone.txt
@@ -13,7 +13,8 @@ SYNOPSIS
 	  [-l] [-s] [--no-hardlinks] [-q] [-n] [--bare] [--mirror]
 	  [-o <name>] [-b <name>] [-u <upload-pack>] [--reference <repository>]
 	  [--separate-git-dir <git dir>]
-	  [--depth <depth>] [--recursive|--recurse-submodules] [--] <repository>
+	  [--depth <depth> [--[no-]single-branch]]
+	  [--recursive|--recurse-submodules] [--] <repository>
 	  [<directory>]
 
 DESCRIPTION
@@ -179,6 +180,15 @@ objects from the source repository into a pack in the cloned repository.
 	with a long history, and would want to send in fixes
 	as patches.
 
+--single-branch::
+--no-single-branch::
+	These options are only valid when --depth is given.
+	 `--single-branch` only fetches one branch (either HEAD or
+	specified by --branch) and tags that point to the downloaded
+	history. `--no-single-branch` fetches all branches and tags
+	like in normal clones. `--single-branch` is implied by
+	default.
+
 --recursive::
 --recurse-submodules::
 	After the clone is created, initialize all submodules within,
diff --git a/builtin/clone.c b/builtin/clone.c
index efe8b6c..3424e1c 100644
--- a/builtin/clone.c
+++ b/builtin/clone.c
@@ -37,7 +37,7 @@ static const char * const builtin_clone_usage[] = {
 	NULL
 };
 
-static int option_no_checkout, option_bare, option_mirror;
+static int option_no_checkout, option_bare, option_mirror, option_single_branch = 1;
 static int option_local, option_no_hardlinks, option_shared, option_recursive;
 static char *option_template, *option_depth;
 static char *option_origin = NULL;
@@ -48,6 +48,7 @@ static int option_verbosity;
 static int option_progress;
 static struct string_list option_config;
 static struct string_list option_reference;
+static const char *src_ref_prefix = "refs/heads/";
 
 static int opt_parse_reference(const struct option *opt, const char *arg, int unset)
 {
@@ -92,6 +93,8 @@ static struct option builtin_clone_options[] = {
 		   "path to git-upload-pack on the remote"),
 	OPT_STRING(0, "depth", &option_depth, "depth",
 		    "create a shallow clone of that depth"),
+	OPT_BOOL(0, "single-branch", &option_single_branch,
+		    "do not limit fetched refs in shallow clones"),
 	OPT_STRING(0, "separate-git-dir", &real_git_dir, "gitdir",
 		   "separate git dir from working tree"),
 	OPT_STRING_LIST('c', "config", &option_config, "key=value",
@@ -427,9 +430,29 @@ static struct ref *wanted_peer_refs(const struct ref *refs,
 	struct ref *local_refs = head;
 	struct ref **tail = head ? &head->next : &local_refs;
 
-	get_fetch_map(refs, refspec, &tail, 0);
-	if (!option_mirror)
-		get_fetch_map(refs, tag_refspec, &tail, 0);
+	if (!(option_depth && option_single_branch)) {
+		get_fetch_map(refs, refspec, &tail, 0);
+		if (!option_mirror)
+			get_fetch_map(refs, tag_refspec, &tail, 0);
+	} else {
+		struct ref *remote_head = NULL;
+
+		if (!option_branch)
+			remote_head = guess_remote_head(head, refs, 0);
+		else {
+			struct strbuf sb = STRBUF_INIT;
+			strbuf_addstr(&sb, src_ref_prefix);
+			strbuf_addstr(&sb, option_branch);
+			remote_head = find_ref_by_name(refs, sb.buf);
+			strbuf_release(&sb);
+		}
+
+		if (!remote_head)
+			die(_("Remote branch \"%s\" not found. Nothing to clone.\n"
+			      "Try --no-single-branch to fetch all refs."),
+			    option_branch ? option_branch : "HEAD");
+		get_fetch_map(remote_head, refspec, &tail, 0);
+	}
 
 	return local_refs;
 }
@@ -448,6 +471,21 @@ static void write_remote_refs(const struct ref *local_refs)
 	clear_extra_refs();
 }
 
+static void write_followtags(const struct ref *refs, const char *msg)
+{
+	const struct ref *ref;
+	for (ref = refs; ref; ref = ref->next) {
+		if (prefixcmp(ref->name, "refs/tags/"))
+			continue;
+		if (!suffixcmp(ref->name, "^{}"))
+			continue;
+		if (!has_sha1_file(ref->old_sha1))
+			continue;
+		update_ref(msg, ref->name, ref->old_sha1,
+			   NULL, 0, DIE_ON_ERR);
+	}
+}
+
 static int write_one_config(const char *key, const char *value, void *data)
 {
 	return git_config_set_multivar(key, value ? value : "true", "^$", 0);
@@ -478,7 +516,6 @@ int cmd_clone(int argc, const char **argv, const char *prefix)
 	struct strbuf key = STRBUF_INIT, value = STRBUF_INIT;
 	struct strbuf branch_top = STRBUF_INIT, reflog_msg = STRBUF_INIT;
 	struct transport *transport = NULL;
-	char *src_ref_prefix = "refs/heads/";
 	int err = 0;
 
 	struct refspec *refspec;
@@ -642,9 +679,12 @@ int cmd_clone(int argc, const char **argv, const char *prefix)
 
 		transport_set_option(transport, TRANS_OPT_KEEP, "yes");
 
-		if (option_depth)
+		if (option_depth) {
 			transport_set_option(transport, TRANS_OPT_DEPTH,
 					     option_depth);
+			transport_set_option(transport, TRANS_OPT_FOLLOWTAGS,
+					     option_single_branch ? "1" : NULL);
+		}
 
 		transport_set_verbosity(transport, option_verbosity, option_progress);
 
@@ -663,6 +703,8 @@ int cmd_clone(int argc, const char **argv, const char *prefix)
 		clear_extra_refs();
 
 		write_remote_refs(mapped_refs);
+		if (option_depth && option_single_branch)
+			write_followtags(refs, reflog_msg.buf);
 
 		remote_head = find_ref_by_name(refs, "HEAD");
 		remote_head_points_at =
diff --git a/t/t5500-fetch-pack.sh b/t/t5500-fetch-pack.sh
index bafcca7..c76a53b 100755
--- a/t/t5500-fetch-pack.sh
+++ b/t/t5500-fetch-pack.sh
@@ -115,7 +115,7 @@ pull_to_client 2nd "B" $((64*3))
 pull_to_client 3rd "A" $((1*3))
 
 test_expect_success 'clone shallow' '
-	git clone --depth 2 "file://$(pwd)/." shallow
+	git clone --no-single-branch --depth 2 "file://$(pwd)/." shallow
 '
 
 test_expect_success 'clone shallow object count' '
@@ -248,4 +248,36 @@ test_expect_success 'clone shallow object count' '
 	grep "^count: 52" count.shallow
 '
 
+test_expect_success 'clone shallow without --no-single-branch' '
+	git clone --depth 1 "file://$(pwd)/." shallow2
+'
+
+test_expect_success 'clone shallow object count' '
+	(
+		cd shallow2 &&
+		git count-objects -v
+	) > count.shallow2 &&
+	grep "^in-pack: 6" count.shallow2
+'
+
+test_expect_success 'shallow clone pulling tags' '
+	git tag -a -m A TAGA1 A &&
+	git tag -a -m B TAGB1 B &&
+	git tag TAGA2 A &&
+	git tag TAGB2 B &&
+	git clone --depth 1 "file://$(pwd)/." shallow3 &&
+
+	cat >taglist.expected <<\EOF &&
+TAGB1
+TAGB2
+EOF
+	GIT_DIR=shallow3/.git git tag -l >taglist.actual &&
+	test_cmp taglist.expected taglist.actual &&
+
+	echo "in-pack: 7" > count3.expected &&
+	GIT_DIR=shallow3/.git git count-objects -v |
+		grep "^in-pack" > count3.actual &&
+	test_cmp count3.expected count3.actual
+'
+
 test_done
-- 
1.7.3.1.256.g2539c.dirty