Skip to content

Commit 829f0f6

Browse files
10ne1gitster
authored andcommitted
submodule: add extension to encode gitdir paths
Add a submoduleEncoding extension which fixes filesystem collisions by encoding gitdir paths. At a high level, this implements a mechanism to encode -> validate -> retry until a working gitdir path is found. Credit goes to Junio for coming up with this design: encoding is only applied when necessary, e.g. uppercase characters are encoded only on case-folding filesystems and only if a real conflict is detected. To make this work, we rely on the submodule.<name>.gitdir config as the single source of truth for gitidir paths: the config is always set when the extension is enabled. Users who care about gitdir paths are expected to get/set the config and not the underlying encoding implementation. This commit adds the basic encoding logic which addresses nested gitdirs. The next commit fixes case-folding, the next commit fixes names longer than NAME_MAX. The idea is the encoding can be improved over time in a way which is transparent to users. Suggested-by: Junio C Hamano <gitster@pobox.com> Suggested-by: Phillip Wood <phillip.wood123@gmail.com> Suggested-by: Patrick Steinhardt <ps@pks.im> Based-on-patch-by: Brandon Williams <bwilliams.eng@gmail.com> Signed-off-by: Adrian Ratiu <adrian.ratiu@collabora.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>
1 parent 0c1df3f commit 829f0f6

File tree

12 files changed

+294
-26
lines changed

12 files changed

+294
-26
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/submodule--helper.c

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1208,6 +1208,22 @@ static int module_summary(int argc, const char **argv, const char *prefix,
12081208
return ret;
12091209
}
12101210

1211+
static int module_gitdir(int argc, const char **argv, const char *prefix UNUSED,
1212+
struct repository *repo)
1213+
{
1214+
struct strbuf gitdir = STRBUF_INIT;
1215+
1216+
if (argc != 2)
1217+
usage(_("git submodule--helper gitdir <name>"));
1218+
1219+
submodule_name_to_gitdir(&gitdir, repo, argv[1]);
1220+
1221+
printf("%s\n", gitdir.buf);
1222+
1223+
strbuf_release(&gitdir);
1224+
return 0;
1225+
}
1226+
12111227
struct sync_cb {
12121228
const char *prefix;
12131229
const char *super_prefix;
@@ -3591,6 +3607,7 @@ int cmd_submodule__helper(int argc,
35913607
NULL
35923608
};
35933609
struct option options[] = {
3610+
OPT_SUBCOMMAND("gitdir", &fn, module_gitdir),
35943611
OPT_SUBCOMMAND("clone", &fn, module_clone),
35953612
OPT_SUBCOMMAND("add", &fn, module_add),
35963613
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: 84 additions & 26 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;
@@ -2256,17 +2257,31 @@ int submodule_move_head(const char *path, const char *super_prefix,
22562257
return ret;
22572258
}
22582259

2260+
/*
2261+
* Find the last submodule name in the gitdir path (modules can be nested).
2262+
* Returns a pointer into `path` to the beginning of the name or NULL if not found.
2263+
*/
2264+
static char *find_last_submodule_name(char *git_dir_path)
2265+
{
2266+
const char *modules_marker = "/modules/";
2267+
char *p = git_dir_path;
2268+
char *last = NULL;
2269+
2270+
while ((p = strstr(p, modules_marker))) {
2271+
last = p + strlen(modules_marker);
2272+
p++;
2273+
}
2274+
2275+
return last;
2276+
}
2277+
22592278
int validate_submodule_git_dir(char *git_dir, const char *submodule_name)
22602279
{
22612280
size_t len = strlen(git_dir), suffix_len = strlen(submodule_name);
2262-
char *p;
2281+
char *p = git_dir + len - suffix_len;
2282+
bool suffixes_match = !strcmp(p, submodule_name);
22632283
int ret = 0;
22642284

2265-
if (len <= suffix_len || (p = git_dir + len - suffix_len)[-1] != '/' ||
2266-
strcmp(p, submodule_name))
2267-
BUG("submodule name '%s' not a suffix of git dir '%s'",
2268-
submodule_name, git_dir);
2269-
22702285
/*
22712286
* We prevent the contents of sibling submodules' git directories to
22722287
* clash.
@@ -2277,7 +2292,7 @@ int validate_submodule_git_dir(char *git_dir, const char *submodule_name)
22772292
* but the latter directory is already designated to contain the hooks
22782293
* of the former.
22792294
*/
2280-
for (; *p; p++) {
2295+
for (; *p && suffixes_match; p++) {
22812296
if (is_dir_sep(*p)) {
22822297
char c = *p;
22832298

@@ -2294,6 +2309,15 @@ int validate_submodule_git_dir(char *git_dir, const char *submodule_name)
22942309
}
22952310
}
22962311

2312+
/* tests after this check are only for encoded names, when the extension is enabled */
2313+
if (!the_repository->repository_format_submodule_encoding)
2314+
return 0;
2315+
2316+
/* Prevent the use of '/' in names */
2317+
p = find_last_submodule_name(git_dir);
2318+
if (p && strchr(p, '/') != NULL)
2319+
return error("submodule gitdir name '%s' contains unexpected '/'", p);
2320+
22972321
return 0;
22982322
}
22992323

@@ -2581,29 +2605,63 @@ int submodule_to_gitdir(struct repository *repo,
25812605
return ret;
25822606
}
25832607

2608+
static int validate_and_set_submodule_gitdir(struct strbuf *gitdir_path,
2609+
const char *submodule_name)
2610+
{
2611+
char *key;
2612+
2613+
if (validate_submodule_git_dir(gitdir_path->buf, submodule_name))
2614+
return -1;
2615+
2616+
key = xstrfmt("submodule.%s.gitdir", submodule_name);
2617+
repo_config_set_gently(the_repository, key, gitdir_path->buf);
2618+
FREE_AND_NULL(key);
2619+
2620+
return 0;
2621+
2622+
}
2623+
25842624
void submodule_name_to_gitdir(struct strbuf *buf, struct repository *r,
25852625
const char *submodule_name)
25862626
{
2627+
const char *gitdir;
2628+
char *key;
2629+
2630+
repo_git_path_append(r, buf, "modules/");
2631+
strbuf_addstr(buf, submodule_name);
2632+
2633+
/* If extensions.submoduleEncoding is disabled, use the plain path set above */
2634+
if (!r->repository_format_submodule_encoding)
2635+
return;
2636+
2637+
/* Extension is enabled: use the gitdir config if it exists */
2638+
key = xstrfmt("submodule.%s.gitdir", submodule_name);
2639+
if (!repo_config_get_string_tmp(r, key, &gitdir)) {
2640+
strbuf_reset(buf);
2641+
strbuf_addstr(buf, gitdir);
2642+
FREE_AND_NULL(key);
2643+
return;
2644+
}
2645+
FREE_AND_NULL(key);
2646+
25872647
/*
2588-
* NEEDSWORK: The current way of mapping a submodule's name to
2589-
* its location in .git/modules/ has problems with some naming
2590-
* schemes. For example, if a submodule is named "foo" and
2591-
* another is named "foo/bar" (whether present in the same
2592-
* superproject commit or not - the problem will arise if both
2593-
* superproject commits have been checked out at any point in
2594-
* time), or if two submodule names only have different cases in
2595-
* a case-insensitive filesystem.
2596-
*
2597-
* There are several solutions, including encoding the path in
2598-
* some way, introducing a submodule.<name>.gitdir config in
2599-
* .git/config (not .gitmodules) that allows overriding what the
2600-
* gitdir of a submodule would be (and teach Git, upon noticing
2601-
* a clash, to automatically determine a non-clashing name and
2602-
* to write such a config), or introducing a
2603-
* submodule.<name>.gitdir config in .gitmodules that repo
2604-
* administrators can explicitly set. Nothing has been decided,
2605-
* so for now, just append the name at the end of the path.
2648+
* The gitdir config does not exist, even though the extension is enabled.
2649+
* Therefore we are in one of the following cases:
26062650
*/
2651+
2652+
/* Case 1: legacy migration of valid plain submodule names */
2653+
if (!validate_and_set_submodule_gitdir(buf, submodule_name))
2654+
return;
2655+
2656+
/* Case 2: Try URI-safe (RFC3986) encoding first, this fixes nested gitdirs */
2657+
strbuf_reset(buf);
26072658
repo_git_path_append(r, buf, "modules/");
2608-
strbuf_addstr(buf, submodule_name);
2659+
strbuf_addstr_urlencode(buf, submodule_name, is_rfc3986_unreserved);
2660+
if (!validate_and_set_submodule_gitdir(buf, submodule_name))
2661+
return;
2662+
2663+
/* Case 3: error out */
2664+
die(_("Cannot construct a valid gitdir path for submodule '%s': "
2665+
"please set a unique git config for 'submodule.%s.gitdir'."),
2666+
submodule_name, submodule_name);
26092667
}
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+
}

t/meson.build

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -884,6 +884,7 @@ integration_tests = [
884884
't7422-submodule-output.sh',
885885
't7423-submodule-symlinks.sh',
886886
't7424-submodule-mixed-ref-formats.sh',
887+
't7425-submodule-encoding.sh',
887888
't7450-bad-git-dotfiles.sh',
888889
't7500-commit-template-squash-signoff.sh',
889890
't7501-commit-basic-functionality.sh',

0 commit comments

Comments
 (0)