builtin/clone.c: add --reject-shallow option

In some scenarios, users may want more history than the repository offered for cloning, which happens to be a shallow repository, can give them. But because users don't know it is a shallow repository until they download it to local, we may want to refuse to clone this kind of repository, without creating any unnecessary files. The '--depth=x' option cannot be used as a solution; the source may be deep enough to give us 'x' commits when cloned, but the user may later need to deepen the history to arbitrary depth. Teach '--reject-shallow' option to "git clone" to abort as soon as we find out that we are cloning from a shallow repository. Signed-off-by: Li Linchao <lilinchao@oschina.cn> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Li Linchao committed Apr 1, 2021 at 10:46 UTC 4fe788b1b0ee6150173580d8fa70e7d5788cf7d3
10 files changed +108 -6
Documentation/config/clone.txt
+4
@@ -2,3 +2,7 @@ clone.defaultRemoteName::
2 The name of the remote to create when cloning a repository. Defaults to
3 `origin`, and can be overridden by passing the `--origin` command-line
4 option to linkgit:git-clone[1].
5 +
6 +clone.rejectShallow::
7 + Reject to clone a repository if it is a shallow one, can be overridden by
8 + passing option `--reject-shallow` in command line. See linkgit:git-clone[1]
Documentation/git-clone.txt
+6 -1
@@ -15,7 +15,7 @@ SYNOPSIS
15 [--dissociate] [--separate-git-dir <git dir>]
16 [--depth <depth>] [--[no-]single-branch] [--no-tags]
17 [--recurse-submodules[=<pathspec>]] [--[no-]shallow-submodules]
18 - [--[no-]remote-submodules] [--jobs <n>] [--sparse]
18 + [--[no-]remote-submodules] [--jobs <n>] [--sparse] [--[no-]reject-shallow]
19 [--filter=<filter>] [--] <repository>
20 [<directory>]
21
@@ -149,6 +149,11 @@ objects from the source repository into a pack in the cloned repository.
149 --no-checkout::
150 No checkout of HEAD is performed after the clone is complete.
151
152 +--[no-]reject-shallow::
153 + Fail if the source repository is a shallow repository.
154 + The 'clone.rejectShallow' configuration variable can be used to
155 + specify the default.
156 +
157 --bare::
158 Make a 'bare' Git repository. That is, instead of
159 creating `<directory>` and placing the administrative
builtin/clone.c
+21
@@ -50,6 +50,8 @@ static int option_no_checkout, option_bare, option_mirror, option_single_branch
50 static int option_local = -1, option_no_hardlinks, option_shared;
51 static int option_no_tags;
52 static int option_shallow_submodules;
53 +static int option_reject_shallow = -1; /* unspecified */
54 +static int config_reject_shallow = -1; /* unspecified */
55 static int deepen;
56 static char *option_template, *option_depth, *option_since;
57 static char *option_origin = NULL;
@@ -90,6 +92,8 @@ static struct option builtin_clone_options[] = {
92 OPT__VERBOSITY(&option_verbosity),
93 OPT_BOOL(0, "progress", &option_progress,
94 N_("force progress reporting")),
95 + OPT_BOOL(0, "reject-shallow", &option_reject_shallow,
96 + N_("don't clone shallow repository")),
97 OPT_BOOL('n', "no-checkout", &option_no_checkout,
98 N_("don't create a checkout")),
99 OPT_BOOL(0, "bare", &option_bare, N_("create a bare repository")),
@@ -858,6 +862,9 @@ static int git_clone_config(const char *k, const char *v, void *cb)
862 free(remote_name);
863 remote_name = xstrdup(v);
864 }
865 + if (!strcmp(k, "clone.rejectshallow"))
866 + config_reject_shallow = git_config_bool(k, v);
867 +
868 return git_default_config(k, v, cb);
869 }
870
@@ -963,6 +970,7 @@ static int path_exists(const char *path)
970 int cmd_clone(int argc, const char **argv, const char *prefix)
971 {
972 int is_bundle = 0, is_local;
973 + int reject_shallow = 0;
974 const char *repo_name, *repo, *work_tree, *git_dir;
975 char *path, *dir, *display_repo = NULL;
976 int dest_exists, real_dest_exists = 0;
@@ -1156,6 +1164,15 @@ int cmd_clone(int argc, const char **argv, const char *prefix)
1164 */
1165 git_config(git_clone_config, NULL);
1166
1167 + /*
1168 + * If option_reject_shallow is specified from CLI option,
1169 + * ignore config_reject_shallow from git_clone_config.
1170 + */
1171 + if (config_reject_shallow != -1)
1172 + reject_shallow = config_reject_shallow;
1173 + if (option_reject_shallow != -1)
1174 + reject_shallow = option_reject_shallow;
1175 +
1176 /*
1177 * apply the remote name provided by --origin only after this second
1178 * call to git_config, to ensure it overrides all config-based values.
@@ -1216,6 +1233,8 @@ int cmd_clone(int argc, const char **argv, const char *prefix)
1233 if (filter_options.choice)
1234 warning(_("--filter is ignored in local clones; use file:// instead."));
1235 if (!access(mkpath("%s/shallow", path), F_OK)) {
1236 + if (reject_shallow)
1237 + die(_("source repository is shallow, reject to clone."));
1238 if (option_local > 0)
1239 warning(_("source repository is shallow, ignoring --local"));
1240 is_local = 0;
@@ -1227,6 +1246,8 @@ int cmd_clone(int argc, const char **argv, const char *prefix)
1246
1247 transport_set_option(transport, TRANS_OPT_KEEP, "yes");
1248
1249 + if (reject_shallow)
1250 + transport_set_option(transport, TRANS_OPT_REJECT_SHALLOW, "1");
1251 if (option_depth)
1252 transport_set_option(transport, TRANS_OPT_DEPTH,
1253 option_depth);
fetch-pack.c
+8 -4
@@ -1129,9 +1129,11 @@ static struct ref *do_fetch_pack(struct fetch_pack_args *args,
1129 if (args->deepen)
1130 setup_alternate_shallow(&shallow_lock, &alternate_shallow_file,
1131 NULL);
1132 - else if (si->nr_ours || si->nr_theirs)
1132 + else if (si->nr_ours || si->nr_theirs) {
1133 + if (args->reject_shallow_remote)
1134 + die(_("source repository is shallow, reject to clone."));
1135 alternate_shallow_file = setup_temporary_shallow(si->shallow);
1134 - else
1136 + } else
1137 alternate_shallow_file = NULL;
1138 if (get_pack(args, fd, pack_lockfiles, NULL, sought, nr_sought,
1139 &gitmodules_oids))
@@ -1498,10 +1500,12 @@ static void receive_shallow_info(struct fetch_pack_args *args,
1500 * rejected (unless --update-shallow is set); do the same.
1501 */
1502 prepare_shallow_info(si, shallows);
1501 - if (si->nr_ours || si->nr_theirs)
1503 + if (si->nr_ours || si->nr_theirs) {
1504 + if (args->reject_shallow_remote)
1505 + die(_("source repository is shallow, reject to clone."));
1506 alternate_shallow_file =
1507 setup_temporary_shallow(si->shallow);
1504 - else
1508 + } else
1509 alternate_shallow_file = NULL;
1510 } else {
1511 alternate_shallow_file = NULL;
fetch-pack.h
+1
@@ -39,6 +39,7 @@ struct fetch_pack_args {
39 unsigned self_contained_and_connected:1;
40 unsigned cloning:1;
41 unsigned update_shallow:1;
42 + unsigned reject_shallow_remote:1;
43 unsigned deepen:1;
44
45 /*
t/t5601-clone.sh
+9
@@ -759,6 +759,15 @@ test_expect_success 'partial clone using HTTP' '
759 partial_clone "$HTTPD_DOCUMENT_ROOT_PATH/server" "$HTTPD_URL/smart/server"
760 '
761
762 +test_expect_success 'reject cloning shallow repository using HTTP' '
763 + test_when_finished "rm -rf repo" &&
764 + git clone --bare --no-local --depth=1 src "$HTTPD_DOCUMENT_ROOT_PATH/repo.git" &&
765 + test_must_fail git clone --reject-shallow $HTTPD_URL/smart/repo.git repo 2>err &&
766 + test_i18ngrep -e "source repository is shallow, reject to clone." err &&
767 +
768 + git clone --no-reject-shallow $HTTPD_URL/smart/repo.git repo
769 +'
770 +
771 # DO NOT add non-httpd-specific tests here, because the last part of this
772 # test script is only executed when httpd is available and enabled.
773
t/t5606-clone-options.sh
+26 -1
@@ -11,7 +11,8 @@ test_expect_success 'setup' '
11 mkdir parent &&
12 (cd parent && git init &&
13 echo one >file && git add file &&
14 - git commit -m one)
14 + git commit -m one) &&
15 + git clone --depth=1 --no-local parent shallow-repo
16
17 '
18
@@ -45,6 +46,30 @@ test_expect_success 'disallows --bare with --separate-git-dir' '
46
47 '
48
49 +test_expect_success 'reject cloning shallow repository' '
50 + test_when_finished "rm -rf repo" &&
51 + test_must_fail git clone --reject-shallow shallow-repo out 2>err &&
52 + test_i18ngrep -e "source repository is shallow, reject to clone." err &&
53 +
54 + git clone --no-reject-shallow shallow-repo repo
55 +'
56 +
57 +test_expect_success 'reject cloning non-local shallow repository' '
58 + test_when_finished "rm -rf repo" &&
59 + test_must_fail git clone --reject-shallow --no-local shallow-repo out 2>err &&
60 + test_i18ngrep -e "source repository is shallow, reject to clone." err &&
61 +
62 + git clone --no-reject-shallow --no-local shallow-repo repo
63 +'
64 +
65 +test_expect_success 'succeed cloning normal repository' '
66 + test_when_finished "rm -rf chilad1 child2 child3 child4 " &&
67 + git clone --reject-shallow parent child1 &&
68 + git clone --reject-shallow --no-local parent child2 &&
69 + git clone --no-reject-shallow parent child3 &&
70 + git clone --no-reject-shallow --no-local parent child4
71 +'
72 +
73 test_expect_success 'uses "origin" for default remote name' '
74
75 git clone parent clone-default-origin &&
t/t5611-clone-config.sh
+25
@@ -95,6 +95,31 @@ test_expect_success 'clone -c remote.<remote>.fetch=<refspec> --origin=<name>' '
95 test_cmp expect actual
96 '
97
98 +test_expect_success 'set up shallow repository' '
99 + git clone --depth=1 --no-local . shallow-repo
100 +'
101 +
102 +test_expect_success 'clone.rejectshallow=true should reject cloning shallow repo' '
103 + test_when_finished "rm -rf out" &&
104 + test_must_fail git -c clone.rejectshallow=true clone --no-local shallow-repo out 2>err &&
105 + test_i18ngrep -e "source repository is shallow, reject to clone." err &&
106 +
107 + git -c clone.rejectshallow=false clone --no-local shallow-repo out
108 +'
109 +
110 +test_expect_success 'option --[no-]reject-shallow override clone.rejectshallow config' '
111 + test_when_finished "rm -rf out" &&
112 + test_must_fail git -c clone.rejectshallow=false clone --reject-shallow --no-local shallow-repo out 2>err &&
113 + test_i18ngrep -e "source repository is shallow, reject to clone." err &&
114 +
115 + git -c clone.rejectshallow=true clone --no-reject-shallow --no-local shallow-repo out
116 +'
117 +
118 +test_expect_success 'clone.rejectshallow=true should succeed cloning normal repo' '
119 + test_when_finished "rm -rf out" &&
120 + git -c clone.rejectshallow=true clone --no-local . out
121 +'
122 +
123 test_expect_success MINGW 'clone -c core.hideDotFiles' '
124 test_commit attributes .gitattributes "" &&
125 rm -rf child &&
transport.c
+4
@@ -236,6 +236,9 @@ static int set_git_option(struct git_transport_options *opts,
236 list_objects_filter_die_if_populated(&opts->filter_options);
237 parse_list_objects_filter(&opts->filter_options, value);
238 return 0;
239 + } else if (!strcmp(name, TRANS_OPT_REJECT_SHALLOW)) {
240 + opts->reject_shallow = !!value;
241 + return 0;
242 }
243 return 1;
244 }
@@ -370,6 +373,7 @@ static int fetch_refs_via_pack(struct transport *transport,
373 args.stateless_rpc = transport->stateless_rpc;
374 args.server_options = transport->server_options;
375 args.negotiation_tips = data->options.negotiation_tips;
376 + args.reject_shallow_remote = transport->smart_options->reject_shallow;
377
378 if (!data->got_remote_heads) {
379 int i;
transport.h
+4
@@ -14,6 +14,7 @@ struct git_transport_options {
14 unsigned check_self_contained_and_connected : 1;
15 unsigned self_contained_and_connected : 1;
16 unsigned update_shallow : 1;
17 + unsigned reject_shallow : 1;
18 unsigned deepen_relative : 1;
19
20 /* see documentation of corresponding flag in fetch-pack.h */
@@ -194,6 +195,9 @@ void transport_check_allowed(const char *type);
195 /* Aggressively fetch annotated tags if possible */
196 #define TRANS_OPT_FOLLOWTAGS "followtags"
197
198 +/* Reject shallow repo transport */
199 +#define TRANS_OPT_REJECT_SHALLOW "rejectshallow"
200 +
201 /* Accept refs that may update .git/shallow without --depth */
202 #define TRANS_OPT_UPDATE_SHALLOW "updateshallow"
203