Skip to content

Commit 58ae3ac

Browse files
committed
Merge branch 'ar/submodule-gitdir-tweak' into seen
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: fix case-folding gitdir filesystem colisions submodule: add extension to encode gitdir paths builtin/credential-store: move is_rfc3986_unreserved to url.[ch] submodule--helper: use submodule_name_to_gitdir in add_submodule
2 parents d91410e + 12e839a commit 58ae3ac

15 files changed

+387
-39
lines changed

Documentation/config/extensions.adoc

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,12 @@ relativeWorktrees:::
7373
repaired with either the `--relative-paths` option or with the
7474
`worktree.useRelativePaths` config set to `true`.
7575
76+
submoduleEncoding:::
77+
If enabled, submodule gitdir paths are encoded to avoid filesystem
78+
conflicts due to nested gitdirs, case insensitivity or other issues
79+
When enabled, the submodule.<name>.gitdir config is always set for
80+
all submodulesand is the single point of authority for gitdir paths.
81+
7682
worktreeConfig:::
7783
If enabled, then worktrees will load config settings from the
7884
`$GIT_DIR/config.worktree` file in addition to the

Documentation/config/submodule.adoc

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

55+
submodule.<name>.gitdir::
56+
This option sets the gitdir path for submodule <name>, allowing users to
57+
override the default path. Only works when `extensions.submoduleEncoding`
58+
is enabled, otherwise does nothing. See linkgit:git-config[1] for details.
59+
5560
submodule.active::
5661
A repeated field which contains a pathspec used to match against a
5762
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: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1204,6 +1204,22 @@ static int module_summary(int argc, const char **argv, const char *prefix,
12041204
return ret;
12051205
}
12061206

1207+
static int module_gitdir(int argc, const char **argv, const char *prefix UNUSED,
1208+
struct repository *repo)
1209+
{
1210+
struct strbuf gitdir = STRBUF_INIT;
1211+
1212+
if (argc != 2)
1213+
usage(_("git submodule--helper gitdir <name>"));
1214+
1215+
submodule_name_to_gitdir(&gitdir, repo, argv[1]);
1216+
1217+
printf("%s\n", gitdir.buf);
1218+
1219+
strbuf_release(&gitdir);
1220+
return 0;
1221+
}
1222+
12071223
struct sync_cb {
12081224
const char *prefix;
12091225
const char *super_prefix;
@@ -3183,13 +3199,13 @@ static void append_fetch_remotes(struct strbuf *msg, const char *git_dir_path)
31833199

31843200
static int add_submodule(const struct add_data *add_data)
31853201
{
3186-
char *submod_gitdir_path;
31873202
struct module_clone_data clone_data = MODULE_CLONE_DATA_INIT;
31883203
struct string_list reference = STRING_LIST_INIT_NODUP;
31893204
int ret = -1;
31903205

31913206
/* perhaps the path already exists and is already a git repo, else clone it */
31923207
if (is_directory(add_data->sm_path)) {
3208+
char *submod_gitdir_path;
31933209
struct strbuf sm_path = STRBUF_INIT;
31943210
strbuf_addstr(&sm_path, add_data->sm_path);
31953211
submod_gitdir_path = xstrfmt("%s/.git", add_data->sm_path);
@@ -3203,10 +3219,11 @@ static int add_submodule(const struct add_data *add_data)
32033219
free(submod_gitdir_path);
32043220
} else {
32053221
struct child_process cp = CHILD_PROCESS_INIT;
3222+
struct strbuf submod_gitdir = STRBUF_INIT;
32063223

3207-
submod_gitdir_path = xstrfmt(".git/modules/%s", add_data->sm_name);
3224+
submodule_name_to_gitdir(&submod_gitdir, the_repository, add_data->sm_name);
32083225

3209-
if (is_directory(submod_gitdir_path)) {
3226+
if (is_directory(submod_gitdir.buf)) {
32103227
if (!add_data->force) {
32113228
struct strbuf msg = STRBUF_INIT;
32123229
char *die_msg;
@@ -3215,8 +3232,8 @@ static int add_submodule(const struct add_data *add_data)
32153232
"locally with remote(s):\n"),
32163233
add_data->sm_name);
32173234

3218-
append_fetch_remotes(&msg, submod_gitdir_path);
3219-
free(submod_gitdir_path);
3235+
append_fetch_remotes(&msg, submod_gitdir.buf);
3236+
strbuf_release(&submod_gitdir);
32203237

32213238
strbuf_addf(&msg, _("If you want to reuse this local git "
32223239
"directory instead of cloning again from\n"
@@ -3234,7 +3251,7 @@ static int add_submodule(const struct add_data *add_data)
32343251
"submodule '%s'\n"), add_data->sm_name);
32353252
}
32363253
}
3237-
free(submod_gitdir_path);
3254+
strbuf_release(&submod_gitdir);
32383255

32393256
clone_data.prefix = add_data->prefix;
32403257
clone_data.path = add_data->sm_path;
@@ -3586,6 +3603,7 @@ int cmd_submodule__helper(int argc,
35863603
NULL
35873604
};
35883605
struct option options[] = {
3606+
OPT_SUBCOMMAND("gitdir", &fn, module_gitdir),
35893607
OPT_SUBCOMMAND("clone", &fn, module_clone),
35903608
OPT_SUBCOMMAND("add", &fn, module_add),
35913609
OPT_SUBCOMMAND("update", &fn, module_update),

repository.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,7 @@ int repo_init(struct repository *repo,
288288
repo->repository_format_worktree_config = format.worktree_config;
289289
repo->repository_format_relative_worktrees = format.relative_worktrees;
290290
repo->repository_format_precious_objects = format.precious_objects;
291+
repo->repository_format_submodule_encoding = format.submodule_encoding;
291292

292293
/* take ownership of format.partial_clone */
293294
repo->repository_format_partial_clone = format.partial_clone;

repository.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ struct repository {
158158
int repository_format_worktree_config;
159159
int repository_format_relative_worktrees;
160160
int repository_format_precious_objects;
161+
int repository_format_submodule_encoding;
161162

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

setup.c

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -687,6 +687,9 @@ static enum extension_result handle_extension(const char *var,
687687
} else if (!strcmp(ext, "relativeworktrees")) {
688688
data->relative_worktrees = git_config_bool(var, value);
689689
return EXTENSION_OK;
690+
} else if (!strcmp(ext, "submoduleencoding")) {
691+
data->submodule_encoding = git_config_bool(var, value);
692+
return EXTENSION_OK;
690693
}
691694
return EXTENSION_UNKNOWN;
692695
}
@@ -1865,6 +1868,8 @@ const char *setup_git_directory_gently(int *nongit_ok)
18651868
repo_fmt.worktree_config;
18661869
the_repository->repository_format_relative_worktrees =
18671870
repo_fmt.relative_worktrees;
1871+
the_repository->repository_format_submodule_encoding =
1872+
repo_fmt.submodule_encoding;
18681873
/* take ownership of repo_fmt.partial_clone */
18691874
the_repository->repository_format_partial_clone =
18701875
repo_fmt.partial_clone;
@@ -1963,6 +1968,8 @@ void check_repository_format(struct repository_format *fmt)
19631968
fmt->ref_storage_format);
19641969
the_repository->repository_format_worktree_config =
19651970
fmt->worktree_config;
1971+
the_repository->repository_format_submodule_encoding =
1972+
fmt->submodule_encoding;
19661973
the_repository->repository_format_relative_worktrees =
19671974
fmt->relative_worktrees;
19681975
the_repository->repository_format_partial_clone =

setup.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ struct repository_format {
130130
char *partial_clone; /* value of extensions.partialclone */
131131
int worktree_config;
132132
int relative_worktrees;
133+
int submodule_encoding;
133134
int is_bare;
134135
int hash_algo;
135136
int compat_hash_algo;

submodule.c

Lines changed: 128 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
#include "commit-reach.h"
3232
#include "read-cache-ll.h"
3333
#include "setup.h"
34+
#include "url.h"
3435

3536
static int config_update_recurse_submodules = RECURSE_SUBMODULES_OFF;
3637
static int initialized_fetch_ref_tips;
@@ -2250,16 +2251,30 @@ int submodule_move_head(const char *path, const char *super_prefix,
22502251
return ret;
22512252
}
22522253

2254+
/*
2255+
* Find the last submodule name in the gitdir path (modules can be nested).
2256+
* Returns a pointer into `path` to the beginning of the name or NULL if not found.
2257+
*/
2258+
static char *find_last_submodule_name(char *git_dir_path)
2259+
{
2260+
const char *modules_marker = "/modules/";
2261+
char *p = git_dir_path;
2262+
char *last = NULL;
2263+
2264+
while ((p = strstr(p, modules_marker))) {
2265+
last = p + strlen(modules_marker);
2266+
p++;
2267+
}
2268+
2269+
return last;
2270+
}
2271+
22532272
int validate_submodule_git_dir(char *git_dir, const char *submodule_name)
22542273
{
22552274
size_t len = strlen(git_dir), suffix_len = strlen(submodule_name);
2256-
char *p;
2257-
int ret = 0;
2258-
2259-
if (len <= suffix_len || (p = git_dir + len - suffix_len)[-1] != '/' ||
2260-
strcmp(p, submodule_name))
2261-
BUG("submodule name '%s' not a suffix of git dir '%s'",
2262-
submodule_name, git_dir);
2275+
char *p = git_dir + len - suffix_len;
2276+
bool suffixes_match = !strcmp(p, submodule_name);
2277+
int ret = 0, config_ignorecase = 0;
22632278

22642279
/*
22652280
* We prevent the contents of sibling submodules' git directories to
@@ -2271,7 +2286,7 @@ int validate_submodule_git_dir(char *git_dir, const char *submodule_name)
22712286
* but the latter directory is already designated to contain the hooks
22722287
* of the former.
22732288
*/
2274-
for (; *p; p++) {
2289+
for (; *p && suffixes_match; p++) {
22752290
if (is_dir_sep(*p)) {
22762291
char c = *p;
22772292

@@ -2288,6 +2303,51 @@ int validate_submodule_git_dir(char *git_dir, const char *submodule_name)
22882303
}
22892304
}
22902305

2306+
/* tests after this check are only for encoded names, when the extension is enabled */
2307+
if (!the_repository->repository_format_submodule_encoding)
2308+
return 0;
2309+
2310+
/* Prevent the use of '/' in names */
2311+
p = find_last_submodule_name(git_dir);
2312+
if (p && strchr(p, '/') != NULL)
2313+
return error("submodule gitdir name '%s' contains unexpected '/'", p);
2314+
2315+
/* Prevent conflicts on case-folding filesystems */
2316+
repo_config_get_bool(the_repository, "core.ignorecase", &config_ignorecase);
2317+
if (ignore_case || config_ignorecase) {
2318+
char *lower_gitdir = xstrdup(git_dir);
2319+
char *module_name = find_last_submodule_name(lower_gitdir);
2320+
2321+
if (module_name) {
2322+
for (p = module_name; *p; p++)
2323+
*p = tolower(*p);
2324+
2325+
/*
2326+
* If lower path is different and already exists, check for collision.
2327+
* Intentionally double-check to eliminate false-positives.
2328+
*/
2329+
if (strcmp(lower_gitdir, git_dir) && is_git_directory(lower_gitdir)) {
2330+
char *canonical = real_pathdup(git_dir, 0);
2331+
if (canonical) {
2332+
struct strbuf norm_git_dir = STRBUF_INIT;
2333+
strbuf_addstr(&norm_git_dir, git_dir);
2334+
strbuf_normalize_path(&norm_git_dir);
2335+
2336+
if (strcmp(canonical, norm_git_dir.buf))
2337+
ret = error(_("submodule git dir '%s' "
2338+
"collides with '%s'"),
2339+
canonical, norm_git_dir.buf);
2340+
2341+
strbuf_release(&norm_git_dir);
2342+
FREE_AND_NULL(canonical);
2343+
}
2344+
}
2345+
}
2346+
2347+
FREE_AND_NULL(lower_gitdir);
2348+
return ret;
2349+
}
2350+
22912351
return 0;
22922352
}
22932353

@@ -2575,29 +2635,70 @@ int submodule_to_gitdir(struct repository *repo,
25752635
return ret;
25762636
}
25772637

2638+
static int validate_and_set_submodule_gitdir(struct strbuf *gitdir_path,
2639+
const char *submodule_name)
2640+
{
2641+
char *key;
2642+
2643+
if (validate_submodule_git_dir(gitdir_path->buf, submodule_name))
2644+
return -1;
2645+
2646+
key = xstrfmt("submodule.%s.gitdir", submodule_name);
2647+
repo_config_set_gently(the_repository, key, gitdir_path->buf);
2648+
FREE_AND_NULL(key);
2649+
2650+
return 0;
2651+
2652+
}
2653+
25782654
void submodule_name_to_gitdir(struct strbuf *buf, struct repository *r,
25792655
const char *submodule_name)
25802656
{
2657+
const char *gitdir;
2658+
char *key;
2659+
2660+
repo_git_path_append(r, buf, "modules/");
2661+
strbuf_addstr(buf, submodule_name);
2662+
2663+
/* If extensions.submoduleEncoding is disabled, use the plain path set above */
2664+
if (!r->repository_format_submodule_encoding)
2665+
return;
2666+
2667+
/* Extension is enabled: use the gitdir config if it exists */
2668+
key = xstrfmt("submodule.%s.gitdir", submodule_name);
2669+
if (!repo_config_get_string_tmp(r, key, &gitdir)) {
2670+
strbuf_reset(buf);
2671+
strbuf_addstr(buf, gitdir);
2672+
FREE_AND_NULL(key);
2673+
return;
2674+
}
2675+
FREE_AND_NULL(key);
2676+
25812677
/*
2582-
* NEEDSWORK: The current way of mapping a submodule's name to
2583-
* its location in .git/modules/ has problems with some naming
2584-
* schemes. For example, if a submodule is named "foo" and
2585-
* another is named "foo/bar" (whether present in the same
2586-
* superproject commit or not - the problem will arise if both
2587-
* superproject commits have been checked out at any point in
2588-
* time), or if two submodule names only have different cases in
2589-
* a case-insensitive filesystem.
2590-
*
2591-
* There are several solutions, including encoding the path in
2592-
* some way, introducing a submodule.<name>.gitdir config in
2593-
* .git/config (not .gitmodules) that allows overriding what the
2594-
* gitdir of a submodule would be (and teach Git, upon noticing
2595-
* a clash, to automatically determine a non-clashing name and
2596-
* to write such a config), or introducing a
2597-
* submodule.<name>.gitdir config in .gitmodules that repo
2598-
* administrators can explicitly set. Nothing has been decided,
2599-
* so for now, just append the name at the end of the path.
2678+
* The gitdir config does not exist, even though the extension is enabled.
2679+
* Therefore we are in one of the following cases:
26002680
*/
2681+
2682+
/* Case 1: legacy migration of valid plain submodule names */
2683+
if (!validate_and_set_submodule_gitdir(buf, submodule_name))
2684+
return;
2685+
2686+
/* Case 2.1: Try URI-safe (RFC3986) encoding first, this fixes nested gitdirs */
2687+
strbuf_reset(buf);
26012688
repo_git_path_append(r, buf, "modules/");
2602-
strbuf_addstr(buf, submodule_name);
2689+
strbuf_addstr_urlencode(buf, submodule_name, is_rfc3986_unreserved);
2690+
if (!validate_and_set_submodule_gitdir(buf, submodule_name))
2691+
return;
2692+
2693+
/* Case 2.2: Try extended uppercase URI (RFC3986) encoding, to fix case-folding */
2694+
strbuf_reset(buf);
2695+
repo_git_path_append(r, buf, "modules/");
2696+
strbuf_addstr_urlencode(buf, submodule_name, is_casefolding_rfc3986_unreserved);
2697+
if (!validate_and_set_submodule_gitdir(buf, submodule_name))
2698+
return;
2699+
2700+
/* Case 3: error out */
2701+
die(_("Cannot construct a valid gitdir path for submodule '%s': "
2702+
"please set a unique git config for 'submodule.%s.gitdir'."),
2703+
submodule_name, submodule_name);
26032704
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Helper to verify if repo $1 contains a submodule named $2 with gitdir path $3
2+
3+
# This does not check filesystem existence. That is done in submodule.c via the
4+
# submodule_name_to_gitdir() API which this helper ends up calling. The gitdirs
5+
# might or might not exist (e.g. when adding a new submodule), so this only
6+
# checks the expected configuration path, which might be overridden by the user.
7+
8+
verify_submodule_gitdir_path() {
9+
repo="$1" &&
10+
name="$2" &&
11+
path="$3" &&
12+
(
13+
cd "$repo" &&
14+
# Compute expected absolute path
15+
expected="$(git rev-parse --git-common-dir)/$path" &&
16+
expected="$(test-tool path-utils real_path "$expected")" &&
17+
# Compute actual absolute path
18+
actual="$(git submodule--helper gitdir "$name")" &&
19+
actual="$(test-tool path-utils real_path "$actual")" &&
20+
echo "$expected" >expect &&
21+
echo "$actual" >actual &&
22+
test_cmp expect actual
23+
)
24+
}

0 commit comments

Comments
 (0)