diff --git a/Documentation/config/fetch.adoc b/Documentation/config/fetch.adoc index 00435e9a16..20bf199b44 100644 --- a/Documentation/config/fetch.adoc +++ b/Documentation/config/fetch.adoc @@ -10,6 +10,20 @@ reference. Defaults to `on-demand`, or to the value of `submodule.recurse` if set. +`fetch.submoduleErrors`:: + Controls how errors from submodule fetches are handled when + `--recurse-submodules` is in effect. When set to `fail` (the default), + any submodule fetch error causes the overall `git fetch` or `git pull` + to exit with a non-zero status. When set to `warn`, submodule fetch + errors are reported to standard error but do not affect the exit + status of the command. This is useful when working in repositories + where some branches reference submodule commits that are not yet + available on the submodule remote, but those commits are not needed + for the currently checked-out branch. ++ +The value of this option can be overridden by the `--submodule-errors` +option of linkgit:git-fetch[1]. + `fetch.fsckObjects`:: If it is set to true, git-fetch-pack will check all fetched objects. See `transfer.fsckObjects` for what's diff --git a/Documentation/fetch-options.adoc b/Documentation/fetch-options.adoc index 035f780e58..78525f6848 100644 --- a/Documentation/fetch-options.adoc +++ b/Documentation/fetch-options.adoc @@ -294,6 +294,14 @@ ifndef::git-pull[] `--no-recurse-submodules`:: Disable recursive fetching of submodules (this has the same effect as using the `--recurse-submodules=no` option). + +`--submodule-errors=(fail|warn)`:: + Control how errors from submodule fetches are handled when + `--recurse-submodules` is in effect. When set to `fail` (the default), + any submodule fetch error causes the overall `git fetch` to exit with a + non-zero status. When set to `warn`, submodule fetch errors are reported + to standard error but do not affect the exit status of the command. Can + also be configured via `fetch.submoduleErrors`. See linkgit:git-config[1]. endif::git-pull[] `--set-upstream`:: diff --git a/builtin/fetch.c b/builtin/fetch.c index ab7db2be06..a492b2809f 100644 --- a/builtin/fetch.c +++ b/builtin/fetch.c @@ -111,8 +111,30 @@ struct fetch_config { int recurse_submodules; int parallel; int submodule_fetch_jobs; + int submodule_errors; }; +/* really private - use accessors below to parse and format */ +static const char *submodule_error_name[] = { + [SUBMODULE_ERRORS_FAIL] = "fail", + [SUBMODULE_ERRORS_WARN] = "warn", +}; + +static const char *submodule_error(unsigned num) +{ + if (ARRAY_SIZE(submodule_error_name) <= num) + BUG("invalid submodule errors mode %u", num); + return submodule_error_name[num]; +} + +static int parse_submodule_error(const char *name) +{ + for (unsigned num = 0; num < ARRAY_SIZE(submodule_error_name); num++) + if (!strcmp(submodule_error_name[num], name)) + return num; + return -1; +} + static int git_fetch_config(const char *k, const char *v, const struct config_context *ctx, void *cb) { @@ -153,6 +175,19 @@ static int git_fetch_config(const char *k, const char *v, return 0; } + if (!strcmp(k, "fetch.submoduleerrors")) { + int mode; + + if (!v) + return config_error_nonbool(k); + mode = parse_submodule_error(v); + if (mode < 0) + die(_("invalid value for '%s': '%s'"), + "fetch.submoduleErrors", v); + fetch_config->submodule_errors = mode; + return 0; + } + if (!strcmp(k, "fetch.parallel")) { fetch_config->parallel = git_config_int(k, v, ctx->kvi); if (fetch_config->parallel < 0) @@ -2243,6 +2278,9 @@ static void add_options_to_argv(struct strvec *argv, strvec_push(argv, "--no-recurse-submodules"); else if (config->recurse_submodules == RECURSE_SUBMODULES_ON_DEMAND) strvec_push(argv, "--recurse-submodules=on-demand"); + if (config->submodule_errors != -1) + strvec_pushf(argv, "--submodule-errors=%s", + submodule_error(config->submodule_errors)); if (tags == TAGS_SET) strvec_push(argv, "--tags"); else if (tags == TAGS_UNSET) @@ -2502,6 +2540,23 @@ static int fetch_one(struct remote *remote, int argc, const char **argv, return exit_code; } +static int option_parse_submodule_errors(const struct option *opt, + const char *arg, int unset) +{ + int *v = opt->value; + int mode; + + if (unset) { + *v = SUBMODULE_ERRORS_FAIL; + return 0; + } + mode = parse_submodule_error(arg); + if (mode < 0) + die(_("invalid value for '%s': '%s'"), "--submodule-errors", arg); + *v = mode; + return 0; +} + int cmd_fetch(int argc, const char **argv, const char *prefix, @@ -2516,6 +2571,7 @@ int cmd_fetch(int argc, .recurse_submodules = RECURSE_SUBMODULES_DEFAULT, .parallel = 1, .submodule_fetch_jobs = -1, + .submodule_errors = -1, /* unset */ }; const char *submodule_prefix = ""; const char *bundle_uri; @@ -2530,6 +2586,7 @@ int cmd_fetch(int argc, int max_jobs = -1; int recurse_submodules_cli = RECURSE_SUBMODULES_DEFAULT; int recurse_submodules_default = RECURSE_SUBMODULES_ON_DEMAND; + int submodule_errors_cli = -1; /* -1: not set on command line */ int fetch_write_commit_graph = -1; int stdin_refspecs = 0; int negotiate_only = 0; @@ -2566,6 +2623,10 @@ int cmd_fetch(int argc, OPT_CALLBACK_F(0, "recurse-submodules", &recurse_submodules_cli, N_("on-demand"), N_("control recursive fetching of submodules"), PARSE_OPT_OPTARG, option_fetch_parse_recurse_submodules), + OPT_CALLBACK_F(0, "submodule-errors", &submodule_errors_cli, + N_("(fail|warn)"), + N_("control how submodule fetch errors are handled"), + 0, option_parse_submodule_errors), OPT_BOOL(0, "dry-run", &dry_run, N_("dry run")), OPT_BOOL(0, "porcelain", &porcelain, N_("machine-readable output")), @@ -2657,6 +2718,9 @@ int cmd_fetch(int argc, if (recurse_submodules_cli != RECURSE_SUBMODULES_DEFAULT) config.recurse_submodules = recurse_submodules_cli; + if (submodule_errors_cli != -1) + config.submodule_errors = submodule_errors_cli; + if (negotiate_only) { switch (recurse_submodules_cli) { case RECURSE_SUBMODULES_OFF: @@ -2860,11 +2924,14 @@ int cmd_fetch(int argc, if (!result && remote && (config.recurse_submodules != RECURSE_SUBMODULES_OFF)) { struct strvec options = STRVEC_INIT; int max_children = max_jobs; + int submodule_errors = config.submodule_errors; if (max_children < 0) max_children = config.submodule_fetch_jobs; if (max_children < 0) max_children = config.parallel; + if (submodule_errors < 0) + submodule_errors = SUBMODULE_ERRORS_FAIL; add_options_to_argv(&options, &config); trace2_region_enter_printf("fetch", "recurse-submodule", the_repository, "%s", submodule_prefix); @@ -2874,7 +2941,8 @@ int cmd_fetch(int argc, config.recurse_submodules, recurse_submodules_default, verbosity < 0, - max_children); + max_children, + submodule_errors); trace2_region_leave_printf("fetch", "recurse-submodule", the_repository, "%s", submodule_prefix); strvec_clear(&options); } diff --git a/submodule.c b/submodule.c index 5c92575888..9eed3cb85b 100644 --- a/submodule.c +++ b/submodule.c @@ -1409,6 +1409,7 @@ struct submodule_parallel_fetch { int oid_fetch_tasks_nr, oid_fetch_tasks_alloc; struct strbuf submodules_with_errors; + int submodule_errors; }; #define SPF_INIT { \ .args = STRVEC_INIT, \ @@ -1562,6 +1563,14 @@ static struct fetch_task *fetch_task_create(struct submodule_parallel_fetch *spf return NULL; } +static void record_fetch_error(struct submodule_parallel_fetch *spf, + const char *name) +{ + if (spf->submodule_errors == SUBMODULE_ERRORS_FAIL) + spf->result = 1; + strbuf_addf(&spf->submodules_with_errors, "\t%s\n", name); +} + static struct fetch_task * get_fetch_task_from_index(struct submodule_parallel_fetch *spf, struct strbuf *err) @@ -1599,7 +1608,7 @@ get_fetch_task_from_index(struct submodule_parallel_fetch *spf, ce->name); if (S_ISGITLINK(ce->ce_mode) && !is_empty_dir(empty_submodule_path.buf)) { - spf->result = 1; + record_fetch_error(spf, ce->name); strbuf_addf(err, _("Could not access submodule '%s'\n"), ce->name); @@ -1753,7 +1762,7 @@ static int fetch_start_failure(struct strbuf *err UNUSED, struct submodule_parallel_fetch *spf = cb; struct fetch_task *task = task_cb; - spf->result = 1; + record_fetch_error(spf, task->sub->name); fetch_task_free(task); return 0; @@ -1779,18 +1788,12 @@ static int fetch_finish(int retvalue, struct strbuf *err UNUSED, if (!task || !task->sub) BUG("callback cookie bogus"); - if (retvalue) { + if (retvalue && task->commits) { /* - * NEEDSWORK: This indicates that the overall fetch - * failed, even though there may be a subsequent fetch - * by commit hash that might work. It may be a good - * idea to not indicate failure in this case, and only - * indicate failure if the subsequent fetch fails. + * This is the second pass (OID-based fetch) and it failed. + * The commits are genuinely unavailable from the remote. */ - spf->result = 1; - - strbuf_addf(&spf->submodules_with_errors, "\t%s\n", - task->sub->name); + record_fetch_error(spf, task->sub->name); } /* Is this the second time we process this submodule? */ @@ -1798,9 +1801,17 @@ static int fetch_finish(int retvalue, struct strbuf *err UNUSED, goto out; it = string_list_lookup(&spf->changed_submodule_names, task->sub->name); - if (!it) - /* Could be an unchanged submodule, not contained in the list */ + if (!it) { + /* + * This submodule is not in the changed list (e.g. it was + * fetched because RECURSE_SUBMODULES_ON fetches all populated + * submodules). A phase 1 failure here has no OID-based retry + * to fall back on, so it is a genuine error. + */ + if (retvalue) + record_fetch_error(spf, task->sub->name); goto out; + } cs_data = it->util; oid_array_filter(&cs_data->new_commits, @@ -1809,6 +1820,11 @@ static int fetch_finish(int retvalue, struct strbuf *err UNUSED, /* Are there commits we want, but do not exist? */ if (cs_data->new_commits.nr) { + /* + * Schedule an OID-based phase 2 fetch to retrieve the missing + * commits directly. Defer any error from phase 1: if phase 2 + * succeeds, the overall operation should still succeed. + */ task->commits = &cs_data->new_commits; ALLOC_GROW(spf->oid_fetch_tasks, spf->oid_fetch_tasks_nr + 1, @@ -1818,6 +1834,16 @@ static int fetch_finish(int retvalue, struct strbuf *err UNUSED, return 0; } + /* + * All required commits are already present locally (they were either + * fetched by phase 1 or existed beforehand), so there is no phase 2 + * retry to defer to. If phase 1 failed, the fetch itself went wrong + * (e.g. a transport error) and must still be reported, even though + * the gitlinked commits are available. + */ + if (retvalue) + record_fetch_error(spf, task->sub->name); + out: fetch_task_free(task); return 0; @@ -1827,7 +1853,8 @@ int fetch_submodules(struct repository *r, const struct strvec *options, const char *prefix, int command_line_option, int default_option, - int quiet, int max_parallel_jobs) + int quiet, int max_parallel_jobs, + int submodule_errors) { struct submodule_parallel_fetch spf = SPF_INIT; const struct run_process_parallel_opts opts = { @@ -1847,6 +1874,7 @@ int fetch_submodules(struct repository *r, spf.default_option = default_option; spf.quiet = quiet; spf.prefix = prefix; + spf.submodule_errors = submodule_errors; if (!r->worktree) goto out; diff --git a/submodule.h b/submodule.h index b10e16e6c0..c80b687d2a 100644 --- a/submodule.h +++ b/submodule.h @@ -90,12 +90,17 @@ int should_update_submodules(void); */ const struct submodule *submodule_from_ce(const struct cache_entry *ce); void check_for_new_submodule_commits(struct object_id *oid); +/* Values for the submodule_errors parameter of fetch_submodules(). */ +#define SUBMODULE_ERRORS_FAIL 0 /* submodule fetch errors are fatal (default) */ +#define SUBMODULE_ERRORS_WARN 1 /* submodule fetch errors are non-fatal warnings */ + int fetch_submodules(struct repository *r, const struct strvec *options, const char *prefix, int command_line_option, int default_option, - int quiet, int max_parallel_jobs); + int quiet, int max_parallel_jobs, + int submodule_errors); unsigned is_submodule_modified(const char *path, int ignore_untracked); int submodule_uses_gitfile(const char *path); diff --git a/t/t5526-fetch-submodules.sh b/t/t5526-fetch-submodules.sh index 7b3b7359da..3456e36644 100755 --- a/t/t5526-fetch-submodules.sh +++ b/t/t5526-fetch-submodules.sh @@ -1262,4 +1262,165 @@ test_expect_success "fetch --all with --no-recurse-submodules only fetches super test_grep ! "Fetching submodule" fetch-log ' +# Create an isolated environment for submodule fetch error tests. +# +# Sets up sub_bare (the submodule upstream), super_bare (the superproject +# upstream), super_work (a working clone of super_bare with an initialized +# submodule), and clone (a clone of super_bare with an initialized submodule +# at a reachable commit). The caller can then create an unreachable commit +# and push the superproject to put the clone one commit behind a state it +# cannot fully fetch. +# +# Usage: create_err_env +create_err_env () { + local envdir="$1" && + mkdir "$envdir" && + + git init --bare "$envdir/sub_bare" && + git clone "$envdir/sub_bare" "$envdir/sub_work" && + test_commit -C "$envdir/sub_work" "${envdir}_base" && + git -C "$envdir/sub_work" push && + + git init --bare "$envdir/super_bare" && + git clone "$envdir/super_bare" "$envdir/super_work" && + git -C "$envdir/super_work" submodule add \ + "$pwd/$envdir/sub_bare" sub && + git -C "$envdir/super_work" commit -m "add submodule" && + git -C "$envdir/super_work" push && + + git clone "$envdir/super_bare" "$envdir/clone" && + git -C "$envdir/clone" submodule update --init +} + +# Push a commit to /super_bare that records a submodule SHA that is +# present locally in super_work/sub but NOT pushed to sub_bare, making the +# submodule commit unreachable from clone's sub remote. +push_unreachable_commit () { + local envdir="$1" && + git -C "$envdir/super_work/sub" commit --allow-empty -m "unreachable" && + git -C "$envdir/super_work" add sub && + git -C "$envdir/super_work" commit -m "point sub to unreachable commit" && + git -C "$envdir/super_work" push +} + +test_expect_success 'setup for submodule fetch error tests' ' + git config --global protocol.file.allow always +' + +test_expect_success 'fetch --recurse-submodules fails when submodule commit is unreachable (default)' ' + test_when_finished "rm -fr env_default" && + create_err_env env_default && + push_unreachable_commit env_default && + test_must_fail git -C env_default/clone fetch --recurse-submodules 2>err && + test_grep "Errors during submodule fetch" err +' + +test_expect_success 'fetch.submoduleErrors=warn: unreachable submodule commit is non-fatal' ' + test_when_finished "rm -fr env_warn_cfg" && + create_err_env env_warn_cfg && + push_unreachable_commit env_warn_cfg && + git -C env_warn_cfg/clone -c fetch.submoduleErrors=warn \ + fetch --recurse-submodules 2>err && + test_grep "Errors during submodule fetch" err +' + +test_expect_success '--submodule-errors=warn: unreachable submodule commit is non-fatal' ' + test_when_finished "rm -fr env_warn_cli" && + create_err_env env_warn_cli && + push_unreachable_commit env_warn_cli && + git -C env_warn_cli/clone fetch --recurse-submodules \ + --submodule-errors=warn 2>err && + test_grep "Errors during submodule fetch" err +' + +test_expect_success '--submodule-errors=fail: unreachable submodule commit is fatal' ' + test_when_finished "rm -fr env_fail_cli" && + create_err_env env_fail_cli && + push_unreachable_commit env_fail_cli && + test_must_fail git -C env_fail_cli/clone fetch --recurse-submodules \ + --submodule-errors=fail 2>err && + test_grep "Errors during submodule fetch" err +' + +test_expect_success 'fetch.submoduleErrors=warn does not suppress successful fetch' ' + # A new reachable submodule commit (pushed to sub_bare) should be + # fetched without any error summary. + test_when_finished "rm -fr env_ok" && + create_err_env env_ok && + test_commit -C env_ok/sub_work reachable_ok && + git -C env_ok/sub_work push && + git -C env_ok/super_work submodule update --remote && + git -C env_ok/super_work add sub && + git -C env_ok/super_work commit -m "point sub to reachable commit" && + git -C env_ok/super_work push && + git -C env_ok/clone -c fetch.submoduleErrors=warn \ + fetch --recurse-submodules 2>err && + test_grep ! "Errors during submodule fetch" err +' + +test_expect_success 'failed submodule fetch is fatal even when its commits are present locally' ' + # Create the same commit (unreferenced, via commit-tree with fixed + # dates) in both super_work/sub and clone/sub, point the gitlink at + # it, and break clone/sub'\''s remote. The commit exists in clone/sub + # but is unreachable, so the submodule stays in the changed list; the + # fetch failure must still be reported even though there is nothing + # left to fetch by commit hash. + test_when_finished "rm -fr env_phase1" && + create_err_env env_phase1 && + commit=$(GIT_AUTHOR_DATE="1234567890 +0000" \ + GIT_COMMITTER_DATE="1234567890 +0000" \ + git -C env_phase1/super_work/sub commit-tree \ + "HEAD^{tree}" -p HEAD -m present) && + present=$(GIT_AUTHOR_DATE="1234567890 +0000" \ + GIT_COMMITTER_DATE="1234567890 +0000" \ + git -C env_phase1/clone/sub commit-tree \ + "HEAD^{tree}" -p HEAD -m present) && + test "$commit" = "$present" && + git -C env_phase1/super_work/sub checkout "$commit" && + git -C env_phase1/super_work add sub && + git -C env_phase1/super_work commit -m "gitlink to locally-present commit" && + git -C env_phase1/super_work push && + git -C env_phase1/clone/sub remote set-url origin "$pwd/env_phase1/missing" && + test_must_fail git -C env_phase1/clone fetch --recurse-submodules 2>err && + test_grep "Errors during submodule fetch" err +' + +test_expect_success '--submodule-errors=warn is honored by fetch --all' ' + # A second remote forces fetch_multiple(), which hands the submodule + # recursion off to per-remote child processes; the option must be + # forwarded to them. + test_when_finished "rm -fr env_all" && + create_err_env env_all && + push_unreachable_commit env_all && + git -C env_all/clone remote add second "$pwd/env_all/super_bare" && + git -C env_all/clone fetch --all --recurse-submodules \ + --submodule-errors=warn 2>err && + test_grep "Errors during submodule fetch" err +' + +test_expect_success '--submodule-errors=fail overrides warn config for fetch --all' ' + # The per-remote child processes re-read the repository config, so + # the command-line override must be forwarded to them explicitly. + test_when_finished "rm -fr env_override" && + create_err_env env_override && + push_unreachable_commit env_override && + git -C env_override/clone remote add second "$pwd/env_override/super_bare" && + git -C env_override/clone config fetch.submoduleErrors warn && + test_must_fail git -C env_override/clone fetch --all --recurse-submodules \ + --submodule-errors=fail 2>err && + test_grep "Errors during submodule fetch" err +' + +test_expect_success 'fetch.submoduleErrors=warn: inaccessible submodule is non-fatal' ' + test_when_finished "rm -fr env_access" && + create_err_env env_access && + rm env_access/clone/sub/.git && + rm -r env_access/clone/.git/modules/sub && + git -C env_access/clone -c fetch.submoduleErrors=warn \ + fetch --recurse-submodules 2>err && + test_grep "Could not access submodule" err && + test_must_fail git -C env_access/clone fetch --recurse-submodules 2>err && + test_grep "Could not access submodule" err +' + test_done