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 <sbeller@google.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Stefan Beller committed Dec 12, 2016 at 11:04 UTC f6f858614003a3da794385cefdbddf00b85f7501
8 files changed +282 -1
Documentation/git-submodule.txt
+15
@@ -22,6 +22,7 @@ SYNOPSIS
22 [commit] [--] [<path>...]
23 'git submodule' [--quiet] foreach [--recursive] <command>
24 'git submodule' [--quiet] sync [--recursive] [--] [<path>...]
25 +'git submodule' [--quiet] absorbgitdirs [--] [<path>...]
26
27
28 DESCRIPTION
@@ -245,6 +246,20 @@ sync::
246 If `--recursive` is specified, this command will recurse into the
247 registered submodules, and sync any nested submodules within.
248
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 +
263 OPTIONS
264 -------
265 -q::
builtin/submodule--helper.c
+38
@@ -1076,6 +1076,43 @@ static int resolve_remote_submodule_branch(int argc, const char **argv,
1076 return 0;
1077 }
1078
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 +
1116 #define SUPPORT_SUPER_PREFIX (1<<0)
1117
1118 struct cmd_struct {
@@ -1094,6 +1131,7 @@ static struct cmd_struct commands[] = {
1131 {"resolve-relative-url-test", resolve_relative_url_test, 0},
1132 {"init", module_init, 0},
1133 {"remote-branch", resolve_remote_submodule_branch, 0},
1134 + {"absorb-git-dirs", absorb_git_dirs, SUPPORT_SUPER_PREFIX},
1135 };
1136
1137 int cmd_submodule__helper(int argc, const char **argv, const char *prefix)
dir.c
+12
@@ -2773,3 +2773,15 @@ void connect_work_tree_and_git_dir(const char *work_tree_, const char *git_dir_)
2773 free(work_tree);
2774 free(git_dir);
2775 }
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
+3
@@ -336,4 +336,7 @@ void write_untracked_extension(struct strbuf *out, struct untracked_cache *untra
336 void add_untracked_cache(struct index_state *istate);
337 void remove_untracked_cache(struct index_state *istate);
338 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);
342 #endif
git-submodule.sh
+6 -1
@@ -1131,6 +1131,11 @@ cmd_sync()
1131 done
1132 }
1133
1134 +cmd_absorbgitdirs()
1135 +{
1136 + git submodule--helper absorb-git-dirs --prefix "$wt_prefix" "$@"
1137 +}
1138 +
1139 # This loop parses the command line arguments to find the
1140 # subcommand name to dispatch. Parsing of the subcommand specific
1141 # options are primarily done by the subcommand implementations.
@@ -1140,7 +1145,7 @@ cmd_sync()
1145 while test $# != 0 && test -z "$command"
1146 do
1147 case "$1" in
1143 - add | foreach | init | deinit | update | status | summary | sync)
1148 + add | foreach | init | deinit | update | status | summary | sync | absorbgitdirs)
1149 command=$1
1150 ;;
1151 -q|--quiet)
submodule.c
+103
@@ -14,6 +14,7 @@
14 #include "blob.h"
15 #include "thread-utils.h"
16 #include "quote.h"
17 +#include "worktree.h"
18
19 static int config_fetch_recurse_submodules = RECURSE_SUBMODULES_ON_DEMAND;
20 static int parallel_jobs = 1;
@@ -1237,3 +1238,105 @@ void prepare_submodule_repo_env(struct argv_array *out)
1238 }
1239 argv_array_push(out, "GIT_DIR=.git");
1240 }
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
+4
@@ -74,4 +74,8 @@ int parallel_submodules(void);
74 */
75 void prepare_submodule_repo_env(struct argv_array *out);
76
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);
81 #endif
t/t7412-submodule-absorbgitdirs.sh new
+101
@@ -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