Skip to content

Commit f6f8586

Browse files
stefanbellergitster
authored andcommitted
submodule: add absorb-git-dir function
When a submodule has its git dir inside the working dir, the submodule support for checkout that we plan to add in a later patch will fail. Add functionality to migrate the git directory to be absorbed into the superprojects git directory. The newly added code in this patch is structured such that other areas of Git can also make use of it. The code in the submodule--helper is a mere wrapper and option parser for the function `absorb_git_dir_into_superproject`, that takes care of embedding the submodules git directory into the superprojects git dir. That function makes use of the more abstract function for this use case `relocate_gitdir`, which can be used by e.g. the worktree code eventually to move around a git directory. Signed-off-by: Stefan Beller <[email protected]> Signed-off-by: Junio C Hamano <[email protected]>
1 parent 47e83eb commit f6f8586

File tree

8 files changed

+282
-1
lines changed

8 files changed

+282
-1
lines changed

Documentation/git-submodule.txt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ SYNOPSIS
2222
[commit] [--] [<path>...]
2323
'git submodule' [--quiet] foreach [--recursive] <command>
2424
'git submodule' [--quiet] sync [--recursive] [--] [<path>...]
25+
'git submodule' [--quiet] absorbgitdirs [--] [<path>...]
2526

2627

2728
DESCRIPTION
@@ -245,6 +246,20 @@ sync::
245246
If `--recursive` is specified, this command will recurse into the
246247
registered submodules, and sync any nested submodules within.
247248

249+
absorbgitdirs::
250+
If a git directory of a submodule is inside the submodule,
251+
move the git directory of the submodule into its superprojects
252+
`$GIT_DIR/modules` path and then connect the git directory and
253+
its working directory by setting the `core.worktree` and adding
254+
a .git file pointing to the git directory embedded in the
255+
superprojects git directory.
256+
+
257+
A repository that was cloned independently and later added as a submodule or
258+
old setups have the submodules git directory inside the submodule instead of
259+
embedded into the superprojects git directory.
260+
+
261+
This command is recursive by default.
262+
248263
OPTIONS
249264
-------
250265
-q::

builtin/submodule--helper.c

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1076,6 +1076,43 @@ static int resolve_remote_submodule_branch(int argc, const char **argv,
10761076
return 0;
10771077
}
10781078

1079+
static int absorb_git_dirs(int argc, const char **argv, const char *prefix)
1080+
{
1081+
int i;
1082+
struct pathspec pathspec;
1083+
struct module_list list = MODULE_LIST_INIT;
1084+
unsigned flags = ABSORB_GITDIR_RECURSE_SUBMODULES;
1085+
1086+
struct option embed_gitdir_options[] = {
1087+
OPT_STRING(0, "prefix", &prefix,
1088+
N_("path"),
1089+
N_("path into the working tree")),
1090+
OPT_BIT(0, "--recursive", &flags, N_("recurse into submodules"),
1091+
ABSORB_GITDIR_RECURSE_SUBMODULES),
1092+
OPT_END()
1093+
};
1094+
1095+
const char *const git_submodule_helper_usage[] = {
1096+
N_("git submodule--helper embed-git-dir [<path>...]"),
1097+
NULL
1098+
};
1099+
1100+
argc = parse_options(argc, argv, prefix, embed_gitdir_options,
1101+
git_submodule_helper_usage, 0);
1102+
1103+
gitmodules_config();
1104+
git_config(submodule_config, NULL);
1105+
1106+
if (module_list_compute(argc, argv, prefix, &pathspec, &list) < 0)
1107+
return 1;
1108+
1109+
for (i = 0; i < list.nr; i++)
1110+
absorb_git_dir_into_superproject(prefix,
1111+
list.entries[i]->name, flags);
1112+
1113+
return 0;
1114+
}
1115+
10791116
#define SUPPORT_SUPER_PREFIX (1<<0)
10801117

10811118
struct cmd_struct {
@@ -1094,6 +1131,7 @@ static struct cmd_struct commands[] = {
10941131
{"resolve-relative-url-test", resolve_relative_url_test, 0},
10951132
{"init", module_init, 0},
10961133
{"remote-branch", resolve_remote_submodule_branch, 0},
1134+
{"absorb-git-dirs", absorb_git_dirs, SUPPORT_SUPER_PREFIX},
10971135
};
10981136

10991137
int cmd_submodule__helper(int argc, const char **argv, const char *prefix)

dir.c

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2773,3 +2773,15 @@ void connect_work_tree_and_git_dir(const char *work_tree_, const char *git_dir_)
27732773
free(work_tree);
27742774
free(git_dir);
27752775
}
2776+
2777+
/*
2778+
* Migrate the git directory of the given path from old_git_dir to new_git_dir.
2779+
*/
2780+
void relocate_gitdir(const char *path, const char *old_git_dir, const char *new_git_dir)
2781+
{
2782+
if (rename(old_git_dir, new_git_dir) < 0)
2783+
die_errno(_("could not migrate git directory from '%s' to '%s'"),
2784+
old_git_dir, new_git_dir);
2785+
2786+
connect_work_tree_and_git_dir(path, new_git_dir);
2787+
}

dir.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,4 +336,7 @@ void write_untracked_extension(struct strbuf *out, struct untracked_cache *untra
336336
void add_untracked_cache(struct index_state *istate);
337337
void remove_untracked_cache(struct index_state *istate);
338338
extern void connect_work_tree_and_git_dir(const char *work_tree, const char *git_dir);
339+
extern void relocate_gitdir(const char *path,
340+
const char *old_git_dir,
341+
const char *new_git_dir);
339342
#endif

git-submodule.sh

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1131,6 +1131,11 @@ cmd_sync()
11311131
done
11321132
}
11331133

1134+
cmd_absorbgitdirs()
1135+
{
1136+
git submodule--helper absorb-git-dirs --prefix "$wt_prefix" "$@"
1137+
}
1138+
11341139
# This loop parses the command line arguments to find the
11351140
# subcommand name to dispatch. Parsing of the subcommand specific
11361141
# options are primarily done by the subcommand implementations.
@@ -1140,7 +1145,7 @@ cmd_sync()
11401145
while test $# != 0 && test -z "$command"
11411146
do
11421147
case "$1" in
1143-
add | foreach | init | deinit | update | status | summary | sync)
1148+
add | foreach | init | deinit | update | status | summary | sync | absorbgitdirs)
11441149
command=$1
11451150
;;
11461151
-q|--quiet)

submodule.c

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
#include "blob.h"
1515
#include "thread-utils.h"
1616
#include "quote.h"
17+
#include "worktree.h"
1718

1819
static int config_fetch_recurse_submodules = RECURSE_SUBMODULES_ON_DEMAND;
1920
static int parallel_jobs = 1;
@@ -1237,3 +1238,105 @@ void prepare_submodule_repo_env(struct argv_array *out)
12371238
}
12381239
argv_array_push(out, "GIT_DIR=.git");
12391240
}
1241+
1242+
/*
1243+
* Embeds a single submodules git directory into the superprojects git dir,
1244+
* non recursively.
1245+
*/
1246+
static void relocate_single_git_dir_into_superproject(const char *prefix,
1247+
const char *path)
1248+
{
1249+
char *old_git_dir = NULL, *real_old_git_dir = NULL, *real_new_git_dir = NULL;
1250+
const char *new_git_dir;
1251+
const struct submodule *sub;
1252+
1253+
if (submodule_uses_worktrees(path))
1254+
die(_("relocate_gitdir for submodule '%s' with "
1255+
"more than one worktree not supported"), path);
1256+
1257+
old_git_dir = xstrfmt("%s/.git", path);
1258+
if (read_gitfile(old_git_dir))
1259+
/* If it is an actual gitfile, it doesn't need migration. */
1260+
return;
1261+
1262+
real_old_git_dir = xstrdup(real_path(old_git_dir));
1263+
1264+
sub = submodule_from_path(null_sha1, path);
1265+
if (!sub)
1266+
die(_("could not lookup name for submodule '%s'"), path);
1267+
1268+
new_git_dir = git_path("modules/%s", sub->name);
1269+
if (safe_create_leading_directories_const(new_git_dir) < 0)
1270+
die(_("could not create directory '%s'"), new_git_dir);
1271+
real_new_git_dir = xstrdup(real_path(new_git_dir));
1272+
1273+
if (!prefix)
1274+
prefix = get_super_prefix();
1275+
1276+
fprintf(stderr, _("Migrating git directory of '%s%s' from\n'%s' to\n'%s'\n"),
1277+
prefix ? prefix : "", path,
1278+
real_old_git_dir, real_new_git_dir);
1279+
1280+
relocate_gitdir(path, real_old_git_dir, real_new_git_dir);
1281+
1282+
free(old_git_dir);
1283+
free(real_old_git_dir);
1284+
free(real_new_git_dir);
1285+
}
1286+
1287+
/*
1288+
* Migrate the git directory of the submodule given by path from
1289+
* having its git directory within the working tree to the git dir nested
1290+
* in its superprojects git dir under modules/.
1291+
*/
1292+
void absorb_git_dir_into_superproject(const char *prefix,
1293+
const char *path,
1294+
unsigned flags)
1295+
{
1296+
const char *sub_git_dir, *v;
1297+
char *real_sub_git_dir = NULL, *real_common_git_dir = NULL;
1298+
struct strbuf gitdir = STRBUF_INIT;
1299+
1300+
strbuf_addf(&gitdir, "%s/.git", path);
1301+
sub_git_dir = resolve_gitdir(gitdir.buf);
1302+
1303+
/* Not populated? */
1304+
if (!sub_git_dir)
1305+
goto out;
1306+
1307+
/* Is it already absorbed into the superprojects git dir? */
1308+
real_sub_git_dir = xstrdup(real_path(sub_git_dir));
1309+
real_common_git_dir = xstrdup(real_path(get_git_common_dir()));
1310+
if (!skip_prefix(real_sub_git_dir, real_common_git_dir, &v))
1311+
relocate_single_git_dir_into_superproject(prefix, path);
1312+
1313+
if (flags & ABSORB_GITDIR_RECURSE_SUBMODULES) {
1314+
struct child_process cp = CHILD_PROCESS_INIT;
1315+
struct strbuf sb = STRBUF_INIT;
1316+
1317+
if (flags & ~ABSORB_GITDIR_RECURSE_SUBMODULES)
1318+
die("BUG: we don't know how to pass the flags down?");
1319+
1320+
if (get_super_prefix())
1321+
strbuf_addstr(&sb, get_super_prefix());
1322+
strbuf_addstr(&sb, path);
1323+
strbuf_addch(&sb, '/');
1324+
1325+
cp.dir = path;
1326+
cp.git_cmd = 1;
1327+
cp.no_stdin = 1;
1328+
argv_array_pushl(&cp.args, "--super-prefix", sb.buf,
1329+
"submodule--helper",
1330+
"absorb-git-dirs", NULL);
1331+
prepare_submodule_repo_env(&cp.env_array);
1332+
if (run_command(&cp))
1333+
die(_("could not recurse into submodule '%s'"), path);
1334+
1335+
strbuf_release(&sb);
1336+
}
1337+
1338+
out:
1339+
strbuf_release(&gitdir);
1340+
free(real_sub_git_dir);
1341+
free(real_common_git_dir);
1342+
}

submodule.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,4 +74,8 @@ int parallel_submodules(void);
7474
*/
7575
void prepare_submodule_repo_env(struct argv_array *out);
7676

77+
#define ABSORB_GITDIR_RECURSE_SUBMODULES (1<<0)
78+
extern void absorb_git_dir_into_superproject(const char *prefix,
79+
const char *path,
80+
unsigned flags);
7781
#endif

t/t7412-submodule-absorbgitdirs.sh

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
#!/bin/sh
2+
3+
test_description='Test submodule absorbgitdirs
4+
5+
This test verifies that `git submodue absorbgitdirs` moves a submodules git
6+
directory into the superproject.
7+
'
8+
9+
. ./test-lib.sh
10+
11+
test_expect_success 'setup a real submodule' '
12+
git init sub1 &&
13+
test_commit -C sub1 first &&
14+
git submodule add ./sub1 &&
15+
test_tick &&
16+
git commit -m superproject
17+
'
18+
19+
test_expect_success 'absorb the git dir' '
20+
>expect.1 &&
21+
>expect.2 &&
22+
>actual.1 &&
23+
>actual.2 &&
24+
git status >expect.1 &&
25+
git -C sub1 rev-parse HEAD >expect.2 &&
26+
git submodule absorbgitdirs &&
27+
git fsck &&
28+
test -f sub1/.git &&
29+
test -d .git/modules/sub1 &&
30+
git status >actual.1 &&
31+
git -C sub1 rev-parse HEAD >actual.2 &&
32+
test_cmp expect.1 actual.1 &&
33+
test_cmp expect.2 actual.2
34+
'
35+
36+
test_expect_success 'absorbing does not fail for deinitalized submodules' '
37+
test_when_finished "git submodule update --init" &&
38+
git submodule deinit --all &&
39+
git submodule absorbgitdirs &&
40+
test -d .git/modules/sub1 &&
41+
test -d sub1 &&
42+
! test -e sub1/.git
43+
'
44+
45+
test_expect_success 'setup nested submodule' '
46+
git init sub1/nested &&
47+
test_commit -C sub1/nested first_nested &&
48+
git -C sub1 submodule add ./nested &&
49+
test_tick &&
50+
git -C sub1 commit -m "add nested" &&
51+
git add sub1 &&
52+
git commit -m "sub1 to include nested submodule"
53+
'
54+
55+
test_expect_success 'absorb the git dir in a nested submodule' '
56+
git status >expect.1 &&
57+
git -C sub1/nested rev-parse HEAD >expect.2 &&
58+
git submodule absorbgitdirs &&
59+
test -f sub1/nested/.git &&
60+
test -d .git/modules/sub1/modules/nested &&
61+
git status >actual.1 &&
62+
git -C sub1/nested rev-parse HEAD >actual.2 &&
63+
test_cmp expect.1 actual.1 &&
64+
test_cmp expect.2 actual.2
65+
'
66+
67+
test_expect_success 'setup a gitlink with missing .gitmodules entry' '
68+
git init sub2 &&
69+
test_commit -C sub2 first &&
70+
git add sub2 &&
71+
git commit -m superproject
72+
'
73+
74+
test_expect_success 'absorbing the git dir fails for incomplete submodules' '
75+
git status >expect.1 &&
76+
git -C sub2 rev-parse HEAD >expect.2 &&
77+
test_must_fail git submodule absorbgitdirs &&
78+
git -C sub2 fsck &&
79+
test -d sub2/.git &&
80+
git status >actual &&
81+
git -C sub2 rev-parse HEAD >actual.2 &&
82+
test_cmp expect.1 actual.1 &&
83+
test_cmp expect.2 actual.2
84+
'
85+
86+
test_expect_success 'setup a submodule with multiple worktrees' '
87+
# first create another unembedded git dir in a new submodule
88+
git init sub3 &&
89+
test_commit -C sub3 first &&
90+
git submodule add ./sub3 &&
91+
test_tick &&
92+
git commit -m "add another submodule" &&
93+
git -C sub3 worktree add ../sub3_second_work_tree
94+
'
95+
96+
test_expect_success 'absorbing fails for a submodule with multiple worktrees' '
97+
test_must_fail git submodule absorbgitdirs sub3 2>error &&
98+
test_i18ngrep "not supported" error
99+
'
100+
101+
test_done

0 commit comments

Comments
 (0)