Skip to content

Commit c3a5261

Browse files
committed
Merge branch 'ar/submodule-gitdir-tweak'
Avoid local submodule repository directory paths overlapping with each other by encoding submodule names before using them as path components. * ar/submodule-gitdir-tweak: submodule: detect conflicts with existing gitdir configs submodule: hash the submodule name for the gitdir path submodule: fix case-folding gitdir filesystem collisions submodule--helper: fix filesystem collisions by encoding gitdir paths builtin/credential-store: move is_rfc3986_unreserved to url.[ch] submodule--helper: add gitdir migration command submodule: allow runtime enabling extensions.submodulePathConfig submodule: introduce extensions.submodulePathConfig builtin/submodule--helper: add gitdir command submodule: always validate gitdirs inside submodule_name_to_gitdir submodule--helper: use submodule_name_to_gitdir in add_submodule
2 parents ae78735 + e897c9b commit c3a5261

16 files changed

Lines changed: 1026 additions & 51 deletions

Documentation/config/extensions.adoc

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,35 @@ relativeWorktrees:::
7373
repaired with either the `--relative-paths` option or with the
7474
`worktree.useRelativePaths` config set to `true`.
7575
76+
submodulePathConfig:::
77+
This extension is for the minority of users who:
78+
+
79+
--
80+
* Encounter errors like `refusing to create ... in another submodule's git dir`
81+
due to a number of reasons, like case-insensitive filesystem conflicts when
82+
creating modules named `foo` and `Foo`.
83+
* Require more flexible submodule layouts, for example due to nested names like
84+
`foo`, `foo/bar` and `foo/baz` not supported by the default gitdir mechanism
85+
which uses `.git/modules/<plain-name>` locations, causing further conflicts.
86+
--
87+
+
88+
When `extensions.submodulePathConfig` is enabled, the `submodule.<name>.gitdir`
89+
config becomes the single source of truth for all submodule gitdir paths and is
90+
automatically set for all new submodules both during clone and init operations.
91+
+
92+
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 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
103+
`git config --global init.defaultSubmodulePathConfig true`.
104+
76105
worktreeConfig:::
77106
If enabled, then worktrees will load config settings from the
78107
`$GIT_DIR/config.worktree` file in addition to the

Documentation/config/init.adoc

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,9 @@ endif::[]
1818
See `--ref-format=` in linkgit:git-init[1]. Both the command line
1919
option and the `GIT_DEFAULT_REF_FORMAT` environment variable take
2020
precedence over this config.
21+
22+
init.defaultSubmodulePathConfig::
23+
A boolean that specifies if `git init` and `git clone` should
24+
automatically set `extensions.submodulePathConfig` to `true`. This
25+
allows all new repositories to automatically use the submodule path
26+
extension. Defaults to `false` when unset.

Documentation/config/submodule.adoc

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,13 @@ submodule.<name>.active::
5252
submodule.active config option. See linkgit:gitsubmodules[7] for
5353
details.
5454

55+
submodule.<name>.gitdir::
56+
This sets the gitdir path for submodule <name>. This configuration is
57+
respected when `extensions.submodulePathConfig` is enabled, otherwise it
58+
has no effect. When enabled, this config becomes the single source of
59+
truth for submodule gitdir paths and Git will error if it is missing.
60+
See linkgit:git-config[1] for details.
61+
5562
submodule.active::
5663
A repeated field which contains a pathspec used to match against a
5764
submodule's path to determine if the submodule is of interest to git

builtin/credential-store.c

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#include "path.h"
88
#include "string-list.h"
99
#include "parse-options.h"
10+
#include "url.h"
1011
#include "write-or-die.h"
1112

1213
static struct lock_file credential_lock;
@@ -76,12 +77,6 @@ static void rewrite_credential_file(const char *fn, struct credential *c,
7677
die_errno("unable to write credential store");
7778
}
7879

79-
static int is_rfc3986_unreserved(char ch)
80-
{
81-
return isalnum(ch) ||
82-
ch == '-' || ch == '_' || ch == '.' || ch == '~';
83-
}
84-
8580
static int is_rfc3986_reserved_or_unreserved(char ch)
8681
{
8782
if (is_rfc3986_unreserved(ch))

builtin/submodule--helper.c

Lines changed: 192 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
#include "list-objects-filter-options.h"
3535
#include "wildmatch.h"
3636
#include "strbuf.h"
37+
#include "url.h"
3738

3839
#define OPT_QUIET (1 << 0)
3940
#define OPT_CACHED (1 << 1)
@@ -435,6 +436,102 @@ struct init_cb {
435436
};
436437
#define INIT_CB_INIT { 0 }
437438

439+
static int validate_and_set_submodule_gitdir(struct strbuf *gitdir_path,
440+
const char *submodule_name)
441+
{
442+
const char *value;
443+
char *key;
444+
445+
if (validate_submodule_git_dir(gitdir_path->buf, submodule_name))
446+
return -1;
447+
448+
key = xstrfmt("submodule.%s.gitdir", submodule_name);
449+
450+
/* Nothing to do if the config already exists. */
451+
if (!repo_config_get_string_tmp(the_repository, key, &value)) {
452+
free(key);
453+
return 0;
454+
}
455+
456+
if (repo_config_set_gently(the_repository, key, gitdir_path->buf)) {
457+
free(key);
458+
return -1;
459+
}
460+
461+
free(key);
462+
return 0;
463+
}
464+
465+
static void create_default_gitdir_config(const char *submodule_name)
466+
{
467+
struct strbuf gitdir_path = STRBUF_INIT;
468+
struct git_hash_ctx ctx;
469+
char hex_name_hash[GIT_MAX_HEXSZ + 1], header[128];
470+
unsigned char raw_name_hash[GIT_MAX_RAWSZ];
471+
int header_len;
472+
473+
/* Case 1: try the plain module name */
474+
repo_git_path_append(the_repository, &gitdir_path, "modules/%s", submodule_name);
475+
if (!validate_and_set_submodule_gitdir(&gitdir_path, submodule_name)) {
476+
strbuf_release(&gitdir_path);
477+
return;
478+
}
479+
480+
/* Case 2.1: Try URI-safe (RFC3986) encoding first, this fixes nested gitdirs */
481+
strbuf_reset(&gitdir_path);
482+
repo_git_path_append(the_repository, &gitdir_path, "modules/");
483+
strbuf_addstr_urlencode(&gitdir_path, submodule_name, is_rfc3986_unreserved);
484+
if (!validate_and_set_submodule_gitdir(&gitdir_path, submodule_name)) {
485+
strbuf_release(&gitdir_path);
486+
return;
487+
}
488+
489+
/* Case 2.2: Try extended uppercase URI (RFC3986) encoding, to fix case-folding */
490+
strbuf_reset(&gitdir_path);
491+
repo_git_path_append(the_repository, &gitdir_path, "modules/");
492+
strbuf_addstr_urlencode(&gitdir_path, submodule_name, is_casefolding_rfc3986_unreserved);
493+
if (!validate_and_set_submodule_gitdir(&gitdir_path, submodule_name))
494+
return;
495+
496+
/* Case 2.3: Try some derived gitdir names, see if one sticks */
497+
for (char c = '0'; c <= '9'; c++) {
498+
strbuf_reset(&gitdir_path);
499+
repo_git_path_append(the_repository, &gitdir_path, "modules/");
500+
strbuf_addstr_urlencode(&gitdir_path, submodule_name, is_rfc3986_unreserved);
501+
strbuf_addch(&gitdir_path, c);
502+
if (!validate_and_set_submodule_gitdir(&gitdir_path, submodule_name))
503+
return;
504+
505+
strbuf_reset(&gitdir_path);
506+
repo_git_path_append(the_repository, &gitdir_path, "modules/");
507+
strbuf_addstr_urlencode(&gitdir_path, submodule_name, is_casefolding_rfc3986_unreserved);
508+
strbuf_addch(&gitdir_path, c);
509+
if (!validate_and_set_submodule_gitdir(&gitdir_path, submodule_name))
510+
return;
511+
}
512+
513+
/* Case 2.4: If all the above failed, try a hash of the name as a last resort */
514+
header_len = snprintf(header, sizeof(header), "blob %zu", strlen(submodule_name));
515+
the_hash_algo->init_fn(&ctx);
516+
the_hash_algo->update_fn(&ctx, header, header_len);
517+
the_hash_algo->update_fn(&ctx, "\0", 1);
518+
the_hash_algo->update_fn(&ctx, submodule_name, strlen(submodule_name));
519+
the_hash_algo->final_fn(raw_name_hash, &ctx);
520+
hash_to_hex_algop_r(hex_name_hash, raw_name_hash, the_hash_algo);
521+
strbuf_reset(&gitdir_path);
522+
repo_git_path_append(the_repository, &gitdir_path, "modules/%s", hex_name_hash);
523+
if (!validate_and_set_submodule_gitdir(&gitdir_path, submodule_name)) {
524+
strbuf_release(&gitdir_path);
525+
return;
526+
}
527+
528+
/* Case 3: nothing worked, error out */
529+
die(_("failed to set a valid default config for 'submodule.%s.gitdir'. "
530+
"Please ensure it is set, for example by running something like: "
531+
"'git config submodule.%s.gitdir .git/modules/%s'"),
532+
submodule_name, submodule_name, submodule_name);
533+
}
534+
438535
static void init_submodule(const char *path, const char *prefix,
439536
const char *super_prefix,
440537
unsigned int flags)
@@ -511,6 +608,10 @@ static void init_submodule(const char *path, const char *prefix,
511608
if (repo_config_set_gently(the_repository, sb.buf, upd))
512609
die(_("Failed to register update mode for submodule path '%s'"), displaypath);
513610
}
611+
612+
if (the_repository->repository_format_submodule_path_cfg)
613+
create_default_gitdir_config(sub->name);
614+
514615
strbuf_release(&sb);
515616
free(displaypath);
516617
free(url);
@@ -1204,6 +1305,82 @@ static int module_summary(int argc, const char **argv, const char *prefix,
12041305
return ret;
12051306
}
12061307

1308+
static int module_gitdir(int argc, const char **argv, const char *prefix UNUSED,
1309+
struct repository *repo)
1310+
{
1311+
struct strbuf gitdir = STRBUF_INIT;
1312+
1313+
if (argc != 2)
1314+
usage(_("git submodule--helper gitdir <name>"));
1315+
1316+
submodule_name_to_gitdir(&gitdir, repo, argv[1]);
1317+
1318+
printf("%s\n", gitdir.buf);
1319+
1320+
strbuf_release(&gitdir);
1321+
return 0;
1322+
}
1323+
1324+
static int module_migrate(int argc UNUSED, const char **argv UNUSED,
1325+
const char *prefix UNUSED, struct repository *repo)
1326+
{
1327+
struct strbuf module_dir = STRBUF_INIT;
1328+
DIR *dir;
1329+
struct dirent *de;
1330+
int repo_version = 0;
1331+
1332+
repo_git_path_append(repo, &module_dir, "modules/");
1333+
1334+
dir = opendir(module_dir.buf);
1335+
if (!dir)
1336+
die(_("could not open '%s'"), module_dir.buf);
1337+
1338+
while ((de = readdir(dir))) {
1339+
struct strbuf gitdir_path = STRBUF_INIT;
1340+
char *key;
1341+
const char *value;
1342+
1343+
if (is_dot_or_dotdot(de->d_name))
1344+
continue;
1345+
1346+
strbuf_addf(&gitdir_path, "%s/%s", module_dir.buf, de->d_name);
1347+
if (!is_git_directory(gitdir_path.buf)) {
1348+
strbuf_release(&gitdir_path);
1349+
continue;
1350+
}
1351+
strbuf_release(&gitdir_path);
1352+
1353+
key = xstrfmt("submodule.%s.gitdir", de->d_name);
1354+
if (!repo_config_get_string_tmp(repo, key, &value)) {
1355+
/* Already has a gitdir config, nothing to do. */
1356+
free(key);
1357+
continue;
1358+
}
1359+
free(key);
1360+
1361+
create_default_gitdir_config(de->d_name);
1362+
}
1363+
1364+
closedir(dir);
1365+
strbuf_release(&module_dir);
1366+
1367+
repo_config_get_int(the_repository, "core.repositoryformatversion", &repo_version);
1368+
if (repo_version == 0 &&
1369+
repo_config_set_gently(repo, "core.repositoryformatversion", "1"))
1370+
die(_("could not set core.repositoryformatversion to 1.\n"
1371+
"Please set it for migration to work, for example:\n"
1372+
"git config core.repositoryformatversion 1"));
1373+
1374+
if (repo_config_set_gently(repo, "extensions.submodulePathConfig", "true"))
1375+
die(_("could not enable submodulePathConfig extension. It is required\n"
1376+
"for migration to work. Please enable it in the root repo:\n"
1377+
"git config extensions.submodulePathConfig true"));
1378+
1379+
repo->repository_format_submodule_path_cfg = 1;
1380+
1381+
return 0;
1382+
}
1383+
12071384
struct sync_cb {
12081385
const char *prefix;
12091386
const char *super_prefix;
@@ -1699,10 +1876,6 @@ static int clone_submodule(const struct module_clone_data *clone_data,
16991876
clone_data_path = to_free = xstrfmt("%s/%s", repo_get_work_tree(the_repository),
17001877
clone_data->path);
17011878

1702-
if (validate_submodule_git_dir(sm_gitdir, clone_data->name) < 0)
1703-
die(_("refusing to create/use '%s' in another submodule's "
1704-
"git dir"), sm_gitdir);
1705-
17061879
if (!file_exists(sm_gitdir)) {
17071880
if (clone_data->require_init && !stat(clone_data_path, &st) &&
17081881
!is_empty_dir(clone_data_path))
@@ -1789,8 +1962,9 @@ static int clone_submodule(const struct module_clone_data *clone_data,
17891962
char *head = xstrfmt("%s/HEAD", sm_gitdir);
17901963
unlink(head);
17911964
free(head);
1792-
die(_("refusing to create/use '%s' in another submodule's "
1793-
"git dir"), sm_gitdir);
1965+
die(_("refusing to create/use '%s' in another submodule's git dir. "
1966+
"Enabling extensions.submodulePathConfig should fix this."),
1967+
sm_gitdir);
17941968
}
17951969

17961970
connect_work_tree_and_git_dir(clone_data_path, sm_gitdir, 0);
@@ -3190,13 +3364,13 @@ static void append_fetch_remotes(struct strbuf *msg, const char *git_dir_path)
31903364

31913365
static int add_submodule(const struct add_data *add_data)
31923366
{
3193-
char *submod_gitdir_path;
31943367
struct module_clone_data clone_data = MODULE_CLONE_DATA_INIT;
31953368
struct string_list reference = STRING_LIST_INIT_NODUP;
31963369
int ret = -1;
31973370

31983371
/* perhaps the path already exists and is already a git repo, else clone it */
31993372
if (is_directory(add_data->sm_path)) {
3373+
char *submod_gitdir_path;
32003374
struct strbuf sm_path = STRBUF_INIT;
32013375
strbuf_addstr(&sm_path, add_data->sm_path);
32023376
submod_gitdir_path = xstrfmt("%s/.git", add_data->sm_path);
@@ -3210,10 +3384,11 @@ static int add_submodule(const struct add_data *add_data)
32103384
free(submod_gitdir_path);
32113385
} else {
32123386
struct child_process cp = CHILD_PROCESS_INIT;
3387+
struct strbuf submod_gitdir = STRBUF_INIT;
32133388

3214-
submod_gitdir_path = xstrfmt(".git/modules/%s", add_data->sm_name);
3389+
submodule_name_to_gitdir(&submod_gitdir, the_repository, add_data->sm_name);
32153390

3216-
if (is_directory(submod_gitdir_path)) {
3391+
if (is_directory(submod_gitdir.buf)) {
32173392
if (!add_data->force) {
32183393
struct strbuf msg = STRBUF_INIT;
32193394
char *die_msg;
@@ -3222,8 +3397,8 @@ static int add_submodule(const struct add_data *add_data)
32223397
"locally with remote(s):\n"),
32233398
add_data->sm_name);
32243399

3225-
append_fetch_remotes(&msg, submod_gitdir_path);
3226-
free(submod_gitdir_path);
3400+
append_fetch_remotes(&msg, submod_gitdir.buf);
3401+
strbuf_release(&submod_gitdir);
32273402

32283403
strbuf_addf(&msg, _("If you want to reuse this local git "
32293404
"directory instead of cloning again from\n"
@@ -3241,7 +3416,7 @@ static int add_submodule(const struct add_data *add_data)
32413416
"submodule '%s'\n"), add_data->sm_name);
32423417
}
32433418
}
3244-
free(submod_gitdir_path);
3419+
strbuf_release(&submod_gitdir);
32453420

32463421
clone_data.prefix = add_data->prefix;
32473422
clone_data.path = add_data->sm_path;
@@ -3569,6 +3744,9 @@ static int module_add(int argc, const char **argv, const char *prefix,
35693744
add_data.progress = !!progress;
35703745
add_data.dissociate = !!dissociate;
35713746

3747+
if (the_repository->repository_format_submodule_path_cfg)
3748+
create_default_gitdir_config(add_data.sm_name);
3749+
35723750
if (add_submodule(&add_data))
35733751
goto cleanup;
35743752
configure_added_submodule(&add_data);
@@ -3594,6 +3772,8 @@ int cmd_submodule__helper(int argc,
35943772
NULL
35953773
};
35963774
struct option options[] = {
3775+
OPT_SUBCOMMAND("migrate-gitdir-configs", &fn, module_migrate),
3776+
OPT_SUBCOMMAND("gitdir", &fn, module_gitdir),
35973777
OPT_SUBCOMMAND("clone", &fn, module_clone),
35983778
OPT_SUBCOMMAND("add", &fn, module_add),
35993779
OPT_SUBCOMMAND("update", &fn, module_update),

repository.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,7 @@ int repo_init(struct repository *repo,
281281
repo->repository_format_worktree_config = format.worktree_config;
282282
repo->repository_format_relative_worktrees = format.relative_worktrees;
283283
repo->repository_format_precious_objects = format.precious_objects;
284+
repo->repository_format_submodule_path_cfg = format.submodule_path_cfg;
284285

285286
/* take ownership of format.partial_clone */
286287
repo->repository_format_partial_clone = format.partial_clone;

repository.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@ struct repository {
165165
int repository_format_worktree_config;
166166
int repository_format_relative_worktrees;
167167
int repository_format_precious_objects;
168+
int repository_format_submodule_path_cfg;
168169

169170
/* Indicate if a repository has a different 'commondir' from 'gitdir' */
170171
unsigned different_commondir:1;

0 commit comments

Comments
 (0)