Add a --no-tags option to clone without fetching any tags. Without this change there's no easy way to clone a repository without also fetching its tags. When supplying --single-branch the primary remote branch will be cloned, but in addition tags will be followed & retrieved. Now --no-tags can be added --single-branch to clone a repository without tags, and which only tracks a single upstream branch. This option works without --single-branch as well, and will do a normal clone but not fetch any tags. Many git commands pay some fixed overhead as a function of the number of references. E.g. creating ~40k tags in linux.git will cause a command like `git log -1 >/dev/null` to run in over a second instead of in a matter of milliseconds, in addition numerous other things will slow down, e.g. "git log <TAB>" with the bash completion will slowly show ~40k references instead of 1. The user might want to avoid all of that overhead to simply use a repository like that to browse the "master" branch, or something like a CI tool might want to keep that one branch up-to-date without caring about any other references. Without this change the only way of accomplishing this was either by manually tweaking the config in a fresh repository: git init git && cat >git/.git/config <<EOF && [remote "origin"] url = git@xxxxxxxxxx:git/git.git tagOpt = --no-tags fetch = +refs/heads/master:refs/remotes/origin/master [branch "master"] remote = origin merge = refs/heads/master EOF cd git && git pull Which requires hardcoding the "master" name, which may not be the main --single-branch would have retrieved, or alternatively by setting tagOpt=--no-tags right after cloning & deleting any existing tags: git clone --single-branch git@xxxxxxxxxx:git/git.git && cd git && git config remote.origin.tagOpt --no-tags && git tag -l | xargs git tag -d Which of course was also subtly buggy if --branch was pointed at a tag, leaving the user in a detached head: git clone --single-branch --branch v2.12.0 git@xxxxxxxxxx:git/git.git && cd git && git config remote.origin.tagOpt --no-tags && git tag -l | xargs git tag -d Now all this complexity becomes the much simpler: git clone --single-branch --no-tags git@xxxxxxxxxx:git/git.git Or in the case of cloning a single tag "branch": git clone --single-branch --branch v2.12.0 --no-tags git@xxxxxxxxxx:git/git.git Signed-off-by: Ævar Arnfjörð Bjarmason <avarab@xxxxxxxxx> --- Documentation/git-clone.txt | 14 ++++++- builtin/clone.c | 13 +++++- contrib/completion/git-completion.bash | 1 + t/t5612-clone-refspec.sh | 76 +++++++++++++++++++++++++++++++++- 4 files changed, 99 insertions(+), 5 deletions(-) diff --git a/Documentation/git-clone.txt b/Documentation/git-clone.txt index 30052cce49..83c8e9b394 100644 --- a/Documentation/git-clone.txt +++ b/Documentation/git-clone.txt @@ -13,7 +13,7 @@ SYNOPSIS [-l] [-s] [--no-hardlinks] [-q] [-n] [--bare] [--mirror] [-o <name>] [-b <name>] [-u <upload-pack>] [--reference <repository>] [--dissociate] [--separate-git-dir <git dir>] - [--depth <depth>] [--[no-]single-branch] + [--depth <depth>] [--[no-]single-branch] [--no-tags] [--recurse-submodules] [--[no-]shallow-submodules] [--jobs <n>] [--] <repository> [<directory>] @@ -215,6 +215,18 @@ objects from the source repository into a pack in the cloned repository. branch when `--single-branch` clone was made, no remote-tracking branch is created. +--no-tags:: + Don't clone any tags, and set + `remote.<remote>.tagOpt=--no-tags` in the config, ensuring + that future `git pull` and `git fetch` operations won't follow + any tags. Subsequent explicit tag fetches will still work, + (see linkgit:git-fetch[1]). ++ +Can be used in conjunction with `--single-branch` to clone and +maintain a branch with no references other than a single cloned +branch. This is useful e.g. to maintain minimal clones of the default +branch of some repository for search indexing. + --recurse-submodules[=<pathspec]:: After the clone is created, initialize and clone submodules within based on the provided pathspec. If no pathspec is diff --git a/builtin/clone.c b/builtin/clone.c index de85b85254..05f52d6f2b 100644 --- a/builtin/clone.c +++ b/builtin/clone.c @@ -40,6 +40,7 @@ static const char * const builtin_clone_usage[] = { static int option_no_checkout, option_bare, option_mirror, option_single_branch = -1; static int option_local = -1, option_no_hardlinks, option_shared; +static int option_no_tags; static int option_shallow_submodules; static int deepen; static char *option_template, *option_depth, *option_since; @@ -120,6 +121,8 @@ static struct option builtin_clone_options[] = { N_("deepen history of shallow clone, excluding rev")), OPT_BOOL(0, "single-branch", &option_single_branch, N_("clone only one branch, HEAD or --branch")), + OPT_BOOL(0, "no-tags", &option_no_tags, + N_("don't clone any tags, and make later fetches not to follow them")), OPT_BOOL(0, "shallow-submodules", &option_shallow_submodules, N_("any cloned submodules will be shallow")), OPT_STRING(0, "separate-git-dir", &real_git_dir, N_("gitdir"), @@ -563,7 +566,7 @@ static struct ref *wanted_peer_refs(const struct ref *refs, } else get_fetch_map(refs, refspec, &tail, 0); - if (!option_mirror && !option_single_branch) + if (!option_mirror && !option_single_branch && !option_no_tags) get_fetch_map(refs, tag_refspec, &tail, 0); return local_refs; @@ -652,7 +655,7 @@ static void update_remote_refs(const struct ref *refs, if (refs) { write_remote_refs(mapped_refs); - if (option_single_branch) + if (option_single_branch && !option_no_tags) write_followtags(refs, msg); } @@ -1035,6 +1038,12 @@ int cmd_clone(int argc, const char **argv, const char *prefix) git_config_set(key.buf, repo); strbuf_reset(&key); + if (option_no_tags) { + strbuf_addf(&key, "remote.%s.tagOpt", option_origin); + git_config_set(key.buf, "--no-tags"); + strbuf_reset(&key); + } + if (option_required_reference.nr || option_optional_reference.nr) setup_reference(); diff --git a/contrib/completion/git-completion.bash b/contrib/completion/git-completion.bash index 1150164d5c..6da997cf42 100644 --- a/contrib/completion/git-completion.bash +++ b/contrib/completion/git-completion.bash @@ -1309,6 +1309,7 @@ _git_clone () --template= --depth --single-branch + --no-tags --branch --recurse-submodules --no-single-branch diff --git a/t/t5612-clone-refspec.sh b/t/t5612-clone-refspec.sh index 97c847fab6..fac5a73851 100755 --- a/t/t5612-clone-refspec.sh +++ b/t/t5612-clone-refspec.sh @@ -17,13 +17,20 @@ test_expect_success 'setup' ' echo four >file && git commit -a -m four && git checkout master && + git tag five && # default clone git clone . dir_all && + # default clone --no-tags + git clone --no-tags . dir_all_no_tags && + # default --single that follows HEAD=master git clone --single-branch . dir_master && + # default --single that follows HEAD=master with no tags + git clone --single-branch --no-tags . dir_master_no_tags && + # default --single that follows HEAD=side git checkout side && git clone --single-branch . dir_side && @@ -45,6 +52,9 @@ test_expect_success 'setup' ' # explicit --single with tag git clone --single-branch --branch two . dir_tag && + # explicit --single with tag and --no-tags + git clone --single-branch --no-tags --branch two . dir_tag_no_tags && + # advance both "master" and "side" branches git checkout side && echo five >file && @@ -77,7 +87,18 @@ test_expect_success 'by default no tags will be kept updated' ' git for-each-ref refs/tags >../actual ) && git for-each-ref refs/tags >expect && - test_must_fail test_cmp expect actual + test_must_fail test_cmp expect actual && + test_line_count = 2 actual +' + +test_expect_success 'clone with --no-tags' ' + ( + cd dir_all_no_tags && + git fetch && + git for-each-ref refs/tags >../actual + ) && + >expect && + test_cmp expect actual ' test_expect_success '--single-branch while HEAD pointing at master' ' @@ -90,7 +111,47 @@ test_expect_success '--single-branch while HEAD pointing at master' ' ) && # only follow master git for-each-ref refs/heads/master >expect && - test_cmp expect actual + # get & check latest tags + test_cmp expect actual && + ( + cd dir_master && + git fetch --tags && + git for-each-ref refs/tags >../actual + ) && + git for-each-ref refs/tags >expect && + test_cmp expect actual && + test_line_count = 2 actual +' + +test_expect_success '--single-branch while HEAD pointing at master and --no-tags' ' + ( + cd dir_master_no_tags && + git fetch && + git for-each-ref refs/remotes/origin | + sed -e "/HEAD$/d" \ + -e "s|/remotes/origin/|/heads/|" >../actual + ) && + # only follow master + git for-each-ref refs/heads/master >expect && + test_cmp expect actual && + # get tags (noop) + ( + cd dir_master_no_tags && + git fetch && + git for-each-ref refs/tags >../actual + ) && + >expect && + test_cmp expect actual && + test_line_count = 0 actual && + # get tags with --tags overrides tagOpt + ( + cd dir_master_no_tags && + git fetch --tags && + git for-each-ref refs/tags >../actual + ) && + git for-each-ref refs/tags >expect && + test_cmp expect actual && + test_line_count = 2 actual ' test_expect_success '--single-branch while HEAD pointing at side' ' @@ -129,6 +190,17 @@ test_expect_success '--single-branch with explicit --branch with tag fetches upd test_cmp expect actual ' +test_expect_success '--single-branch with explicit --branch with tag fetches updated tag despite --no-tags' ' + ( + cd dir_tag_no_tags && + git fetch && + git for-each-ref refs/tags >../actual + ) && + git for-each-ref refs/tags/two >expect && + test_cmp expect actual && + test_line_count = 1 actual +' + test_expect_success '--single-branch with --mirror' ' ( cd dir_mirror && -- 2.11.0