submodule: fix premature failure in recursive submodule fetch

When git fetch --recurse-submodules encounters a failure fetching a submodule's refs (phase 1), it immediately marks the overall operation as failed, even though a subsequent OID-based fetch (phase 2) is about to be attempted for any missing commits. If phase 2 succeeds, the overall result should be success, but the prematurely set failure flag makes it look like an error. Restructure fetch_finish() so that a phase-1 failure does not record an error immediately. Instead, the decision is deferred: - If missing commits trigger a phase-2 (OID-based) retry and that retry succeeds, no error is recorded. - If the phase-2 retry also fails, the error is recorded then. - If the submodule was fetched unconditionally (RECURSE_SUBMODULES_ON) and is not in the changed list, a phase-1 failure is recorded right away since there is no OID retry to fall back on. - If phase 1 fails but all required commits are already present locally, there is no retry to defer to; the failure is still recorded, since the fetch itself went wrong (e.g. a transport error) even though the wanted commits happen to be available. This resolves the NEEDSWORK comment added by bd5e567dc7 (submodule: explain first attempt failure clearly, 2019-03-13). Extract the common error-recording logic into a helper record_fetch_error() and use it in fetch_start_failure() and for the "Could not access submodule" error in get_fetch_task_from_index() as well; the latter now also lists the submodule in the final error summary. Add a test ensuring a failed submodule fetch is still reported when the gitlinked commits happen to be present locally. Helped-by: Ramsay Jones <ramsay@ramsayjones.plus.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 d1e198ff9ebd2f3af5fb281f618f5e088489971e
2 files changed +110 -14
submodule.c
+38 -14
@@ -1562,6 +1562,13 @@ static struct fetch_task *fetch_task_create(struct submodule_parallel_fetch *spf
1562 return NULL;
1563 }
1564
1565 +static void record_fetch_error(struct submodule_parallel_fetch *spf,
1566 + const char *name)
1567 +{
1568 + spf->result = 1;
1569 + strbuf_addf(&spf->submodules_with_errors, "\t%s\n", name);
1570 +}
1571 +
1572 static struct fetch_task *
1573 get_fetch_task_from_index(struct submodule_parallel_fetch *spf,
1574 struct strbuf *err)
@@ -1599,7 +1606,7 @@ get_fetch_task_from_index(struct submodule_parallel_fetch *spf,
1606 ce->name);
1607 if (S_ISGITLINK(ce->ce_mode) &&
1608 !is_empty_dir(empty_submodule_path.buf)) {
1602 - spf->result = 1;
1609 + record_fetch_error(spf, ce->name);
1610 strbuf_addf(err,
1611 _("Could not access submodule '%s'\n"),
1612 ce->name);
@@ -1753,7 +1760,7 @@ static int fetch_start_failure(struct strbuf *err UNUSED,
1760 struct submodule_parallel_fetch *spf = cb;
1761 struct fetch_task *task = task_cb;
1762
1756 - spf->result = 1;
1763 + record_fetch_error(spf, task->sub->name);
1764
1765 fetch_task_free(task);
1766 return 0;
@@ -1779,18 +1786,12 @@ static int fetch_finish(int retvalue, struct strbuf *err UNUSED,
1786 if (!task || !task->sub)
1787 BUG("callback cookie bogus");
1788
1782 - if (retvalue) {
1789 + if (retvalue && task->commits) {
1790 /*
1784 - * NEEDSWORK: This indicates that the overall fetch
1785 - * failed, even though there may be a subsequent fetch
1786 - * by commit hash that might work. It may be a good
1787 - * idea to not indicate failure in this case, and only
1788 - * indicate failure if the subsequent fetch fails.
1791 + * This is the second pass (OID-based fetch) and it failed.
1792 + * The commits are genuinely unavailable from the remote.
1793 */
1790 - spf->result = 1;
1791 -
1792 - strbuf_addf(&spf->submodules_with_errors, "\t%s\n",
1793 - task->sub->name);
1794 + record_fetch_error(spf, task->sub->name);
1795 }
1796
1797 /* Is this the second time we process this submodule? */
@@ -1798,9 +1799,17 @@ static int fetch_finish(int retvalue, struct strbuf *err UNUSED,
1799 goto out;
1800
1801 it = string_list_lookup(&spf->changed_submodule_names, task->sub->name);
1801 - if (!it)
1802 - /* Could be an unchanged submodule, not contained in the list */
1802 + if (!it) {
1803 + /*
1804 + * This submodule is not in the changed list (e.g. it was
1805 + * fetched because RECURSE_SUBMODULES_ON fetches all populated
1806 + * submodules). A phase 1 failure here has no OID-based retry
1807 + * to fall back on, so it is a genuine error.
1808 + */
1809 + if (retvalue)
1810 + record_fetch_error(spf, task->sub->name);
1811 goto out;
1812 + }
1813
1814 cs_data = it->util;
1815 oid_array_filter(&cs_data->new_commits,
@@ -1809,6 +1818,11 @@ static int fetch_finish(int retvalue, struct strbuf *err UNUSED,
1818
1819 /* Are there commits we want, but do not exist? */
1820 if (cs_data->new_commits.nr) {
1821 + /*
1822 + * Schedule an OID-based phase 2 fetch to retrieve the missing
1823 + * commits directly. Defer any error from phase 1: if phase 2
1824 + * succeeds, the overall operation should still succeed.
1825 + */
1826 task->commits = &cs_data->new_commits;
1827 ALLOC_GROW(spf->oid_fetch_tasks,
1828 spf->oid_fetch_tasks_nr + 1,
@@ -1818,6 +1832,16 @@ static int fetch_finish(int retvalue, struct strbuf *err UNUSED,
1832 return 0;
1833 }
1834
1835 + /*
1836 + * All required commits are already present locally (they were either
1837 + * fetched by phase 1 or existed beforehand), so there is no phase 2
1838 + * retry to defer to. If phase 1 failed, the fetch itself went wrong
1839 + * (e.g. a transport error) and must still be reported, even though
1840 + * the gitlinked commits are available.
1841 + */
1842 + if (retvalue)
1843 + record_fetch_error(spf, task->sub->name);
1844 +
1845 out:
1846 fetch_task_free(task);
1847 return 0;
t/t5526-fetch-submodules.sh
+72
@@ -1262,4 +1262,76 @@ test_expect_success "fetch --all with --no-recurse-submodules only fetches super
1262 ! grep "Fetching submodule" fetch-log
1263 '
1264
1265 +# Create an isolated environment for submodule fetch error tests.
1266 +#
1267 +# Sets up sub_bare (the submodule upstream), super_bare (the superproject
1268 +# upstream), super_work (a working clone of super_bare with an initialized
1269 +# submodule), and clone (a clone of super_bare with an initialized submodule
1270 +# at a reachable commit). The caller can then create an unreachable commit
1271 +# and push the superproject to put the clone one commit behind a state it
1272 +# cannot fully fetch.
1273 +#
1274 +# Usage: create_err_env <envdir>
1275 +create_err_env () {
1276 + local envdir="$1" &&
1277 + mkdir "$envdir" &&
1278 +
1279 + git init --bare "$envdir/sub_bare" &&
1280 + git clone "$envdir/sub_bare" "$envdir/sub_work" &&
1281 + test_commit -C "$envdir/sub_work" "${envdir}_base" &&
1282 + git -C "$envdir/sub_work" push &&
1283 +
1284 + git init --bare "$envdir/super_bare" &&
1285 + git clone "$envdir/super_bare" "$envdir/super_work" &&
1286 + git -C "$envdir/super_work" submodule add \
1287 + "$pwd/$envdir/sub_bare" sub &&
1288 + git -C "$envdir/super_work" commit -m "add submodule" &&
1289 + git -C "$envdir/super_work" push &&
1290 +
1291 + git clone "$envdir/super_bare" "$envdir/clone" &&
1292 + git -C "$envdir/clone" submodule update --init
1293 +}
1294 +
1295 +# Push a commit to <envdir>/super_bare that records a submodule SHA that is
1296 +# present locally in super_work/sub but NOT pushed to sub_bare, making the
1297 +# submodule commit unreachable from clone's sub remote.
1298 +push_unreachable_commit () {
1299 + local envdir="$1" &&
1300 + git -C "$envdir/super_work/sub" commit --allow-empty -m "unreachable" &&
1301 + git -C "$envdir/super_work" add sub &&
1302 + git -C "$envdir/super_work" commit -m "point sub to unreachable commit" &&
1303 + git -C "$envdir/super_work" push
1304 +}
1305 +
1306 +test_expect_success 'setup for submodule fetch error tests' '
1307 + git config --global protocol.file.allow always
1308 +'
1309 +
1310 +test_expect_success 'failed submodule fetch is fatal even when its commits are present locally' '
1311 + # Create the same commit (unreferenced, via commit-tree with fixed
1312 + # dates) in both super_work/sub and clone/sub, point the gitlink at
1313 + # it, and break clone/sub'\''s remote. The commit exists in clone/sub
1314 + # but is unreachable, so the submodule stays in the changed list; the
1315 + # fetch failure must still be reported even though there is nothing
1316 + # left to fetch by commit hash.
1317 + test_when_finished "rm -fr env_phase1" &&
1318 + create_err_env env_phase1 &&
1319 + commit=$(GIT_AUTHOR_DATE="1234567890 +0000" \
1320 + GIT_COMMITTER_DATE="1234567890 +0000" \
1321 + git -C env_phase1/super_work/sub commit-tree \
1322 + "HEAD^{tree}" -p HEAD -m present) &&
1323 + present=$(GIT_AUTHOR_DATE="1234567890 +0000" \
1324 + GIT_COMMITTER_DATE="1234567890 +0000" \
1325 + git -C env_phase1/clone/sub commit-tree \
1326 + "HEAD^{tree}" -p HEAD -m present) &&
1327 + test "$commit" = "$present" &&
1328 + git -C env_phase1/super_work/sub checkout "$commit" &&
1329 + git -C env_phase1/super_work add sub &&
1330 + git -C env_phase1/super_work commit -m "gitlink to locally-present commit" &&
1331 + git -C env_phase1/super_work push &&
1332 + git -C env_phase1/clone/sub remote set-url origin "$pwd/env_phase1/missing" &&
1333 + test_must_fail git -C env_phase1/clone fetch --recurse-submodules 2>err &&
1334 + test_grep "Errors during submodule fetch" err
1335 +'
1336 +
1337 test_done