fetch: add fetch.submoduleErrors to make submodule fetch errors non-fatal

When fetching with --recurse-submodules, a submodule commit that is not yet reachable from any of the submodule's remote refs causes the entire fetch to fail. This is overly strict when the missing commit belongs to an upstream branch that is still being prepared (e.g. an in-progress merge topic): the local branch does not need that commit, so there is no reason to treat its absence as fatal. Add a new config key fetch.submoduleErrors (values: fail/warn) and a corresponding --submodule-errors=(fail|warn) command-line option that control this behaviour. The default remains fail (existing behaviour); setting the value to warn causes submodule fetch failures to be reported on stderr without affecting the overall exit status of git fetch / git pull. Forward the option to child fetches in add_options_to_argv() so that it also takes effect for `git fetch --all` / `--multiple` (where per-remote child processes handle the submodule recursion themselves) and for nested submodule recursion. The resolved value is forwarded whenever it was set explicitly, in either direction: the per-remote children re-read the repository configuration, so a command-line --submodule-errors=fail must be passed down to them to override fetch.submoduleErrors=warn from the configuration. When neither the configuration nor the command line sets a value, nothing is forwarded and the child processes fall back to their own configuration. Helped-by: Jean-Noël Avila <avila.jn@gmail.com> Helped-by: Ramsay Jones <ramsay@ramsayjones.plus.com> Helped-by: Junio C Hamano <gitster@pobox.com> Signed-off-by: Paulius Zaleckas <paulius.zaleckas@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Paulius Zaleckas committed Jul 16, 2026 at 17:09 UTC 0b977622b8434e6753425fb66024e2fd93bba0c8
6 files changed +192 -4
Documentation/config/fetch.adoc
+14
@@ -10,6 +10,20 @@
10 reference.
11 Defaults to `on-demand`, or to the value of `submodule.recurse` if set.
12
13 +`fetch.submoduleErrors`::
14 + Controls how errors from submodule fetches are handled when
15 + `--recurse-submodules` is in effect. When set to `fail` (the default),
16 + any submodule fetch error causes the overall `git fetch` or `git pull`
17 + to exit with a non-zero status. When set to `warn`, submodule fetch
18 + errors are reported to standard error but do not affect the exit
19 + status of the command. This is useful when working in repositories
20 + where some branches reference submodule commits that are not yet
21 + available on the submodule remote, but those commits are not needed
22 + for the currently checked-out branch.
23 ++
24 +The value of this option can be overridden by the `--submodule-errors`
25 +option of linkgit:git-fetch[1].
26 +
27 `fetch.fsckObjects`::
28 If it is set to true, git-fetch-pack will check all fetched
29 objects. See `transfer.fsckObjects` for what's
Documentation/fetch-options.adoc
+8
@@ -294,6 +294,14 @@ ifndef::git-pull[]
294 `--no-recurse-submodules`::
295 Disable recursive fetching of submodules (this has the same effect as
296 using the `--recurse-submodules=no` option).
297 +
298 +`--submodule-errors=(fail|warn)`::
299 + Control how errors from submodule fetches are handled when
300 + `--recurse-submodules` is in effect. When set to `fail` (the default),
301 + any submodule fetch error causes the overall `git fetch` to exit with a
302 + non-zero status. When set to `warn`, submodule fetch errors are reported
303 + to standard error but do not affect the exit status of the command. Can
304 + also be configured via `fetch.submoduleErrors`. See linkgit:git-config[1].
305 endif::git-pull[]
306
307 `--set-upstream`::
builtin/fetch.c
+69 -1
@@ -110,8 +110,30 @@ struct fetch_config {
110 int recurse_submodules;
111 int parallel;
112 int submodule_fetch_jobs;
113 + int submodule_errors;
114 };
115
116 +/* really private - use accessors below to parse and format */
117 +static const char *submodule_error_name[] = {
118 + [SUBMODULE_ERRORS_FAIL] = "fail",
119 + [SUBMODULE_ERRORS_WARN] = "warn",
120 +};
121 +
122 +static const char *submodule_error(unsigned num)
123 +{
124 + if (ARRAY_SIZE(submodule_error_name) <= num)
125 + BUG("invalid submodule errors mode %u", num);
126 + return submodule_error_name[num];
127 +}
128 +
129 +static int parse_submodule_error(const char *name)
130 +{
131 + for (unsigned num = 0; num < ARRAY_SIZE(submodule_error_name); num++)
132 + if (!strcmp(submodule_error_name[num], name))
133 + return num;
134 + return -1;
135 +}
136 +
137 static int git_fetch_config(const char *k, const char *v,
138 const struct config_context *ctx, void *cb)
139 {
@@ -152,6 +174,19 @@ static int git_fetch_config(const char *k, const char *v,
174 return 0;
175 }
176
177 + if (!strcmp(k, "fetch.submoduleerrors")) {
178 + int mode;
179 +
180 + if (!v)
181 + return config_error_nonbool(k);
182 + mode = parse_submodule_error(v);
183 + if (mode < 0)
184 + die(_("invalid value for '%s': '%s'"),
185 + "fetch.submoduleErrors", v);
186 + fetch_config->submodule_errors = mode;
187 + return 0;
188 + }
189 +
190 if (!strcmp(k, "fetch.parallel")) {
191 fetch_config->parallel = git_config_int(k, v, ctx->kvi);
192 if (fetch_config->parallel < 0)
@@ -2205,6 +2240,9 @@ static void add_options_to_argv(struct strvec *argv,
2240 strvec_push(argv, "--no-recurse-submodules");
2241 else if (config->recurse_submodules == RECURSE_SUBMODULES_ON_DEMAND)
2242 strvec_push(argv, "--recurse-submodules=on-demand");
2243 + if (config->submodule_errors != -1)
2244 + strvec_pushf(argv, "--submodule-errors=%s",
2245 + submodule_error(config->submodule_errors));
2246 if (tags == TAGS_SET)
2247 strvec_push(argv, "--tags");
2248 else if (tags == TAGS_UNSET)
@@ -2464,6 +2502,23 @@ static int fetch_one(struct remote *remote, int argc, const char **argv,
2502 return exit_code;
2503 }
2504
2505 +static int option_parse_submodule_errors(const struct option *opt,
2506 + const char *arg, int unset)
2507 +{
2508 + int *v = opt->value;
2509 + int mode;
2510 +
2511 + if (unset) {
2512 + *v = SUBMODULE_ERRORS_FAIL;
2513 + return 0;
2514 + }
2515 + mode = parse_submodule_error(arg);
2516 + if (mode < 0)
2517 + die(_("invalid value for '%s': '%s'"), "--submodule-errors", arg);
2518 + *v = mode;
2519 + return 0;
2520 +}
2521 +
2522 int cmd_fetch(int argc,
2523 const char **argv,
2524 const char *prefix,
@@ -2477,6 +2532,7 @@ int cmd_fetch(int argc,
2532 .recurse_submodules = RECURSE_SUBMODULES_DEFAULT,
2533 .parallel = 1,
2534 .submodule_fetch_jobs = -1,
2535 + .submodule_errors = -1, /* unset */
2536 };
2537 const char *submodule_prefix = "";
2538 const char *bundle_uri;
@@ -2491,6 +2547,7 @@ int cmd_fetch(int argc,
2547 int max_jobs = -1;
2548 int recurse_submodules_cli = RECURSE_SUBMODULES_DEFAULT;
2549 int recurse_submodules_default = RECURSE_SUBMODULES_ON_DEMAND;
2550 + int submodule_errors_cli = -1; /* -1: not set on command line */
2551 int fetch_write_commit_graph = -1;
2552 int stdin_refspecs = 0;
2553 int negotiate_only = 0;
@@ -2527,6 +2584,10 @@ int cmd_fetch(int argc,
2584 OPT_CALLBACK_F(0, "recurse-submodules", &recurse_submodules_cli, N_("on-demand"),
2585 N_("control recursive fetching of submodules"),
2586 PARSE_OPT_OPTARG, option_fetch_parse_recurse_submodules),
2587 + OPT_CALLBACK_F(0, "submodule-errors", &submodule_errors_cli,
2588 + N_("(fail|warn)"),
2589 + N_("control how submodule fetch errors are handled"),
2590 + 0, option_parse_submodule_errors),
2591 OPT_BOOL(0, "dry-run", &dry_run,
2592 N_("dry run")),
2593 OPT_BOOL(0, "porcelain", &porcelain, N_("machine-readable output")),
@@ -2616,6 +2677,9 @@ int cmd_fetch(int argc,
2677 if (recurse_submodules_cli != RECURSE_SUBMODULES_DEFAULT)
2678 config.recurse_submodules = recurse_submodules_cli;
2679
2680 + if (submodule_errors_cli != -1)
2681 + config.submodule_errors = submodule_errors_cli;
2682 +
2683 if (negotiate_only) {
2684 switch (recurse_submodules_cli) {
2685 case RECURSE_SUBMODULES_OFF:
@@ -2819,11 +2883,14 @@ int cmd_fetch(int argc,
2883 if (!result && remote && (config.recurse_submodules != RECURSE_SUBMODULES_OFF)) {
2884 struct strvec options = STRVEC_INIT;
2885 int max_children = max_jobs;
2886 + int submodule_errors = config.submodule_errors;
2887
2888 if (max_children < 0)
2889 max_children = config.submodule_fetch_jobs;
2890 if (max_children < 0)
2891 max_children = config.parallel;
2892 + if (submodule_errors < 0)
2893 + submodule_errors = SUBMODULE_ERRORS_FAIL;
2894
2895 add_options_to_argv(&options, &config);
2896 trace2_region_enter_printf("fetch", "recurse-submodule", the_repository, "%s", submodule_prefix);
@@ -2833,7 +2900,8 @@ int cmd_fetch(int argc,
2900 config.recurse_submodules,
2901 recurse_submodules_default,
2902 verbosity < 0,
2836 - max_children);
2903 + max_children,
2904 + submodule_errors);
2905 trace2_region_leave_printf("fetch", "recurse-submodule", the_repository, "%s", submodule_prefix);
2906 strvec_clear(&options);
2907 }
submodule.c
+6 -2
@@ -1409,6 +1409,7 @@ struct submodule_parallel_fetch {
1409 int oid_fetch_tasks_nr, oid_fetch_tasks_alloc;
1410
1411 struct strbuf submodules_with_errors;
1412 + int submodule_errors;
1413 };
1414 #define SPF_INIT { \
1415 .args = STRVEC_INIT, \
@@ -1565,7 +1566,8 @@ static struct fetch_task *fetch_task_create(struct submodule_parallel_fetch *spf
1566 static void record_fetch_error(struct submodule_parallel_fetch *spf,
1567 const char *name)
1568 {
1568 - spf->result = 1;
1569 + if (spf->submodule_errors == SUBMODULE_ERRORS_FAIL)
1570 + spf->result = 1;
1571 strbuf_addf(&spf->submodules_with_errors, "\t%s\n", name);
1572 }
1573
@@ -1851,7 +1853,8 @@ int fetch_submodules(struct repository *r,
1853 const struct strvec *options,
1854 const char *prefix, int command_line_option,
1855 int default_option,
1854 - int quiet, int max_parallel_jobs)
1856 + int quiet, int max_parallel_jobs,
1857 + int submodule_errors)
1858 {
1859 struct submodule_parallel_fetch spf = SPF_INIT;
1860 const struct run_process_parallel_opts opts = {
@@ -1871,6 +1874,7 @@ int fetch_submodules(struct repository *r,
1874 spf.default_option = default_option;
1875 spf.quiet = quiet;
1876 spf.prefix = prefix;
1877 + spf.submodule_errors = submodule_errors;
1878
1879 if (!r->worktree)
1880 goto out;
submodule.h
+6 -1
@@ -90,12 +90,17 @@ int should_update_submodules(void);
90 */
91 const struct submodule *submodule_from_ce(const struct cache_entry *ce);
92 void check_for_new_submodule_commits(struct object_id *oid);
93 +/* Values for the submodule_errors parameter of fetch_submodules(). */
94 +#define SUBMODULE_ERRORS_FAIL 0 /* submodule fetch errors are fatal (default) */
95 +#define SUBMODULE_ERRORS_WARN 1 /* submodule fetch errors are non-fatal warnings */
96 +
97 int fetch_submodules(struct repository *r,
98 const struct strvec *options,
99 const char *prefix,
100 int command_line_option,
101 int default_option,
98 - int quiet, int max_parallel_jobs);
102 + int quiet, int max_parallel_jobs,
103 + int submodule_errors);
104 unsigned is_submodule_modified(const char *path, int ignore_untracked);
105 int submodule_uses_gitfile(const char *path);
106
t/t5526-fetch-submodules.sh
+89
@@ -1307,6 +1307,57 @@ test_expect_success 'setup for submodule fetch error tests' '
1307 git config --global protocol.file.allow always
1308 '
1309
1310 +test_expect_success 'fetch --recurse-submodules fails when submodule commit is unreachable (default)' '
1311 + test_when_finished "rm -fr env_default" &&
1312 + create_err_env env_default &&
1313 + push_unreachable_commit env_default &&
1314 + test_must_fail git -C env_default/clone fetch --recurse-submodules 2>err &&
1315 + test_grep "Errors during submodule fetch" err
1316 +'
1317 +
1318 +test_expect_success 'fetch.submoduleErrors=warn: unreachable submodule commit is non-fatal' '
1319 + test_when_finished "rm -fr env_warn_cfg" &&
1320 + create_err_env env_warn_cfg &&
1321 + push_unreachable_commit env_warn_cfg &&
1322 + git -C env_warn_cfg/clone -c fetch.submoduleErrors=warn \
1323 + fetch --recurse-submodules 2>err &&
1324 + test_grep "Errors during submodule fetch" err
1325 +'
1326 +
1327 +test_expect_success '--submodule-errors=warn: unreachable submodule commit is non-fatal' '
1328 + test_when_finished "rm -fr env_warn_cli" &&
1329 + create_err_env env_warn_cli &&
1330 + push_unreachable_commit env_warn_cli &&
1331 + git -C env_warn_cli/clone fetch --recurse-submodules \
1332 + --submodule-errors=warn 2>err &&
1333 + test_grep "Errors during submodule fetch" err
1334 +'
1335 +
1336 +test_expect_success '--submodule-errors=fail: unreachable submodule commit is fatal' '
1337 + test_when_finished "rm -fr env_fail_cli" &&
1338 + create_err_env env_fail_cli &&
1339 + push_unreachable_commit env_fail_cli &&
1340 + test_must_fail git -C env_fail_cli/clone fetch --recurse-submodules \
1341 + --submodule-errors=fail 2>err &&
1342 + test_grep "Errors during submodule fetch" err
1343 +'
1344 +
1345 +test_expect_success 'fetch.submoduleErrors=warn does not suppress successful fetch' '
1346 + # A new reachable submodule commit (pushed to sub_bare) should be
1347 + # fetched without any error summary.
1348 + test_when_finished "rm -fr env_ok" &&
1349 + create_err_env env_ok &&
1350 + test_commit -C env_ok/sub_work reachable_ok &&
1351 + git -C env_ok/sub_work push &&
1352 + git -C env_ok/super_work submodule update --remote &&
1353 + git -C env_ok/super_work add sub &&
1354 + git -C env_ok/super_work commit -m "point sub to reachable commit" &&
1355 + git -C env_ok/super_work push &&
1356 + git -C env_ok/clone -c fetch.submoduleErrors=warn \
1357 + fetch --recurse-submodules 2>err &&
1358 + test_grep ! "Errors during submodule fetch" err
1359 +'
1360 +
1361 test_expect_success 'failed submodule fetch is fatal even when its commits are present locally' '
1362 # Create the same commit (unreferenced, via commit-tree with fixed
1363 # dates) in both super_work/sub and clone/sub, point the gitlink at
@@ -1334,4 +1385,42 @@ test_expect_success 'failed submodule fetch is fatal even when its commits are p
1385 test_grep "Errors during submodule fetch" err
1386 '
1387
1388 +test_expect_success '--submodule-errors=warn is honored by fetch --all' '
1389 + # A second remote forces fetch_multiple(), which hands the submodule
1390 + # recursion off to per-remote child processes; the option must be
1391 + # forwarded to them.
1392 + test_when_finished "rm -fr env_all" &&
1393 + create_err_env env_all &&
1394 + push_unreachable_commit env_all &&
1395 + git -C env_all/clone remote add second "$pwd/env_all/super_bare" &&
1396 + git -C env_all/clone fetch --all --recurse-submodules \
1397 + --submodule-errors=warn 2>err &&
1398 + test_grep "Errors during submodule fetch" err
1399 +'
1400 +
1401 +test_expect_success '--submodule-errors=fail overrides warn config for fetch --all' '
1402 + # The per-remote child processes re-read the repository config, so
1403 + # the command-line override must be forwarded to them explicitly.
1404 + test_when_finished "rm -fr env_override" &&
1405 + create_err_env env_override &&
1406 + push_unreachable_commit env_override &&
1407 + git -C env_override/clone remote add second "$pwd/env_override/super_bare" &&
1408 + git -C env_override/clone config fetch.submoduleErrors warn &&
1409 + test_must_fail git -C env_override/clone fetch --all --recurse-submodules \
1410 + --submodule-errors=fail 2>err &&
1411 + test_grep "Errors during submodule fetch" err
1412 +'
1413 +
1414 +test_expect_success 'fetch.submoduleErrors=warn: inaccessible submodule is non-fatal' '
1415 + test_when_finished "rm -fr env_access" &&
1416 + create_err_env env_access &&
1417 + rm env_access/clone/sub/.git &&
1418 + rm -r env_access/clone/.git/modules/sub &&
1419 + git -C env_access/clone -c fetch.submoduleErrors=warn \
1420 + fetch --recurse-submodules 2>err &&
1421 + test_grep "Could not access submodule" err &&
1422 + test_must_fail git -C env_access/clone fetch --recurse-submodules 2>err &&
1423 + test_grep "Could not access submodule" err
1424 +'
1425 +
1426 test_done