submodule--helper: add gitdir migration command

Manually running "git config submodule.<name>.gitdir .git/modules/<name>" for each submodule can be impractical, so add a migration command to submodule--helper to automatically create configs for all submodules as required by extensions.submodulePathConfig. The command calls create_default_gitdir_config() which validates the gitdir paths before adding the configs. Suggested-by: Junio C Hamano <gitster@pobox.com> Suggested-by: Patrick Steinhardt <ps@pks.im> Signed-off-by: Adrian Ratiu <adrian.ratiu@collabora.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Adrian Ratiu committed Jan 12, 2026 at 20:46 UTC e14349d58eeae0eac23bf7f740d22f51fc90a49d
3 files changed +132 -2
Documentation/config/extensions.adoc
+4 -2
@@ -93,8 +93,10 @@ Git will error out if a module does not have a corresponding
93 `submodule.<name>.gitdir` set.
94 +
95 Existing (pre-extension) submodules need to be migrated by adding the missing
96 -config entries. This is done manually for now, e.g. for each submodule:
97 -`git config submodule.<name>.gitdir .git/modules/<name>`.
96 +config entries. This can be done manually, e.g. for each submodule:
97 +`git config submodule.<name>.gitdir .git/modules/<name>`, or via the
98 +`git submodule--helper migrate-gitdir-configs` command which iterates over all
99 +submodules and attempts to migrate them.
100 +
101 The extension can be enabled automatically for new repositories by setting
102 `init.defaultSubmodulePathConfig` to `true`, for example by running
builtin/submodule--helper.c
+61
@@ -1270,6 +1270,66 @@ static int module_gitdir(int argc, const char **argv, const char *prefix UNUSED,
1270 return 0;
1271 }
1272
1273 +static int module_migrate(int argc UNUSED, const char **argv UNUSED,
1274 + const char *prefix UNUSED, struct repository *repo)
1275 +{
1276 + struct strbuf module_dir = STRBUF_INIT;
1277 + DIR *dir;
1278 + struct dirent *de;
1279 + int repo_version = 0;
1280 +
1281 + repo_git_path_append(repo, &module_dir, "modules/");
1282 +
1283 + dir = opendir(module_dir.buf);
1284 + if (!dir)
1285 + die(_("could not open '%s'"), module_dir.buf);
1286 +
1287 + while ((de = readdir(dir))) {
1288 + struct strbuf gitdir_path = STRBUF_INIT;
1289 + char *key;
1290 + const char *value;
1291 +
1292 + if (is_dot_or_dotdot(de->d_name))
1293 + continue;
1294 +
1295 + strbuf_addf(&gitdir_path, "%s/%s", module_dir.buf, de->d_name);
1296 + if (!is_git_directory(gitdir_path.buf)) {
1297 + strbuf_release(&gitdir_path);
1298 + continue;
1299 + }
1300 + strbuf_release(&gitdir_path);
1301 +
1302 + key = xstrfmt("submodule.%s.gitdir", de->d_name);
1303 + if (!repo_config_get_string_tmp(repo, key, &value)) {
1304 + /* Already has a gitdir config, nothing to do. */
1305 + free(key);
1306 + continue;
1307 + }
1308 + free(key);
1309 +
1310 + create_default_gitdir_config(de->d_name);
1311 + }
1312 +
1313 + closedir(dir);
1314 + strbuf_release(&module_dir);
1315 +
1316 + repo_config_get_int(the_repository, "core.repositoryformatversion", &repo_version);
1317 + if (repo_version == 0 &&
1318 + repo_config_set_gently(repo, "core.repositoryformatversion", "1"))
1319 + die(_("could not set core.repositoryformatversion to 1.\n"
1320 + "Please set it for migration to work, for example:\n"
1321 + "git config core.repositoryformatversion 1"));
1322 +
1323 + if (repo_config_set_gently(repo, "extensions.submodulePathConfig", "true"))
1324 + die(_("could not enable submodulePathConfig extension. It is required\n"
1325 + "for migration to work. Please enable it in the root repo:\n"
1326 + "git config extensions.submodulePathConfig true"));
1327 +
1328 + repo->repository_format_submodule_path_cfg = 1;
1329 +
1330 + return 0;
1331 +}
1332 +
1333 struct sync_cb {
1334 const char *prefix;
1335 const char *super_prefix;
@@ -3653,6 +3713,7 @@ int cmd_submodule__helper(int argc,
3713 NULL
3714 };
3715 struct option options[] = {
3716 + OPT_SUBCOMMAND("migrate-gitdir-configs", &fn, module_migrate),
3717 OPT_SUBCOMMAND("gitdir", &fn, module_gitdir),
3718 OPT_SUBCOMMAND("clone", &fn, module_clone),
3719 OPT_SUBCOMMAND("add", &fn, module_add),
t/t7425-submodule-gitdir-path-extension.sh
+67
@@ -279,4 +279,71 @@ test_expect_success '`git clone --recurse-submodules` respects init.defaultSubmo
279 )
280 '
281
282 +test_expect_success 'submodule--helper migrates legacy modules' '
283 + (
284 + cd upstream &&
285 +
286 + # previous submodules exist and were not migrated yet
287 + test_must_fail git config submodule.sub1.gitdir &&
288 + test_must_fail git config submodule.sub2.gitdir &&
289 + test_path_is_dir .git/modules/sub1 &&
290 + test_path_is_dir .git/modules/sub2 &&
291 +
292 + # run migration
293 + git submodule--helper migrate-gitdir-configs &&
294 +
295 + # test that migration worked
296 + git config submodule.sub1.gitdir >actual &&
297 + echo ".git/modules/sub1" >expect &&
298 + test_cmp expect actual &&
299 + git config submodule.sub2.gitdir >actual &&
300 + echo ".git/modules/sub2" >expect &&
301 + test_cmp expect actual &&
302 +
303 + # repository extension is enabled after migration
304 + git config extensions.submodulePathConfig >actual &&
305 + echo "true" >expect &&
306 + test_cmp expect actual
307 + )
308 +'
309 +
310 +test_expect_success '`git clone --recurse-submodules` works after migration' '
311 + test_when_finished "rm -rf repo-clone-recursive" &&
312 +
313 + # test with extension disabled after the upstream repo was migrated
314 + git clone --recurse-submodules upstream repo-clone-recursive &&
315 + (
316 + cd repo-clone-recursive &&
317 +
318 + # init.defaultSubmodulePathConfig was disabled before clone, so
319 + # the repo extension config should also be off, the migration ignored
320 + test_must_fail git config extensions.submodulePathConfig &&
321 +
322 + # modules should look like there was no migration done
323 + test_must_fail git config submodule.sub1.gitdir &&
324 + test_must_fail git config submodule.sub2.gitdir &&
325 + test_path_is_dir .git/modules/sub1 &&
326 + test_path_is_dir .git/modules/sub2
327 + ) &&
328 + rm -rf repo-clone-recursive &&
329 +
330 + # enable the extension, then retry the clone
331 + test_config_global init.defaultSubmodulePathConfig true &&
332 + git clone --recurse-submodules upstream repo-clone-recursive &&
333 + (
334 + cd repo-clone-recursive &&
335 +
336 + # repository extension is enabled
337 + git config extensions.submodulePathConfig >actual &&
338 + echo "true" >expect &&
339 + test_cmp expect actual &&
340 +
341 + # gitdir configs exist for submodules
342 + git config submodule.sub1.gitdir &&
343 + git config submodule.sub2.gitdir &&
344 + test_path_is_dir .git/modules/sub1 &&
345 + test_path_is_dir .git/modules/sub2
346 + )
347 +'
348 +
349 test_done