submodule: fixup nested submodules after moving the submodule

connect_work_tree_and_git_dir is used to connect a submodule worktree with its git directory and vice versa after events that require a reconnection such as moving around the working tree. As submodules can have nested submodules themselves, we'd also want to fix the nested submodules when asked to. Add an option to recurse into the nested submodules and connect them as well. As submodules are identified by their name (which determines their git directory in relation to their superproject's git directory) internally and by their path in the working tree of the superproject, we need to make sure that the mapping of name <-> path is kept intact. We can do that in the git-mv command by writing out the gitmodules file first and then forcing a reload of the submodule config machinery. Signed-off-by: Stefan Beller <sbeller@google.com> Reviewed-by: Jonathan Tan <jonathantanmy@google.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Stefan Beller committed Mar 28, 2018 at 15:35 UTC da62f786d2df60f3c8255cf4cbbcd9c1f9458d99
8 files changed +83 -15
builtin/mv.c
+4 -2
@@ -275,10 +275,12 @@ int cmd_mv(int argc, const char **argv, const char *prefix)
275 die_errno(_("renaming '%s' failed"), src);
276 }
277 if (submodule_gitfile[i]) {
278 - if (submodule_gitfile[i] != SUBMODULE_WITH_GITDIR)
279 - connect_work_tree_and_git_dir(dst, submodule_gitfile[i]);
278 if (!update_path_in_gitmodules(src, dst))
279 gitmodules_modified = 1;
280 + if (submodule_gitfile[i] != SUBMODULE_WITH_GITDIR)
281 + connect_work_tree_and_git_dir(dst,
282 + submodule_gitfile[i],
283 + 1);
284 }
285
286 if (mode == WORKING_DIRECTORY)
builtin/submodule--helper.c
+1 -2
@@ -1260,8 +1260,7 @@ static int module_clone(int argc, const char **argv, const char *prefix)
1260 strbuf_reset(&sb);
1261 }
1262
1263 - /* Connect module worktree and git dir */
1264 - connect_work_tree_and_git_dir(path, sm_gitdir);
1263 + connect_work_tree_and_git_dir(path, sm_gitdir, 0);
1264
1265 p = git_pathdup_submodule(path, "config");
1266 if (!p)
dir.c
+57 -3
@@ -19,6 +19,7 @@
19 #include "varint.h"
20 #include "ewah/ewok.h"
21 #include "fsmonitor.h"
22 +#include "submodule-config.h"
23
24 /*
25 * Tells read_directory_recursive how a file or directory should be treated.
@@ -2988,8 +2989,57 @@ void untracked_cache_add_to_index(struct index_state *istate,
2989 untracked_cache_invalidate_path(istate, path);
2990 }
2991
2991 -/* Update gitfile and core.worktree setting to connect work tree and git dir */
2992 -void connect_work_tree_and_git_dir(const char *work_tree_, const char *git_dir_)
2992 +static void connect_wt_gitdir_in_nested(const char *sub_worktree,
2993 + const char *sub_gitdir)
2994 +{
2995 + int i;
2996 + struct repository subrepo;
2997 + struct strbuf sub_wt = STRBUF_INIT;
2998 + struct strbuf sub_gd = STRBUF_INIT;
2999 +
3000 + const struct submodule *sub;
3001 +
3002 + /* If the submodule has no working tree, we can ignore it. */
3003 + if (repo_init(&subrepo, sub_gitdir, sub_worktree))
3004 + return;
3005 +
3006 + if (repo_read_index(&subrepo) < 0)
3007 + die("index file corrupt in repo %s", subrepo.gitdir);
3008 +
3009 + for (i = 0; i < subrepo.index->cache_nr; i++) {
3010 + const struct cache_entry *ce = subrepo.index->cache[i];
3011 +
3012 + if (!S_ISGITLINK(ce->ce_mode))
3013 + continue;
3014 +
3015 + while (i + 1 < subrepo.index->cache_nr &&
3016 + !strcmp(ce->name, subrepo.index->cache[i + 1]->name))
3017 + /*
3018 + * Skip entries with the same name in different stages
3019 + * to make sure an entry is returned only once.
3020 + */
3021 + i++;
3022 +
3023 + sub = submodule_from_path(&subrepo, &null_oid, ce->name);
3024 + if (!sub || !is_submodule_active(&subrepo, ce->name))
3025 + /* .gitmodules broken or inactive sub */
3026 + continue;
3027 +
3028 + strbuf_reset(&sub_wt);
3029 + strbuf_reset(&sub_gd);
3030 + strbuf_addf(&sub_wt, "%s/%s", sub_worktree, sub->path);
3031 + strbuf_addf(&sub_gd, "%s/modules/%s", sub_gitdir, sub->name);
3032 +
3033 + connect_work_tree_and_git_dir(sub_wt.buf, sub_gd.buf, 1);
3034 + }
3035 + strbuf_release(&sub_wt);
3036 + strbuf_release(&sub_gd);
3037 + repo_clear(&subrepo);
3038 +}
3039 +
3040 +void connect_work_tree_and_git_dir(const char *work_tree_,
3041 + const char *git_dir_,
3042 + int recurse_into_nested)
3043 {
3044 struct strbuf gitfile_sb = STRBUF_INIT;
3045 struct strbuf cfg_sb = STRBUF_INIT;
@@ -3019,6 +3069,10 @@ void connect_work_tree_and_git_dir(const char *work_tree_, const char *git_dir_)
3069 strbuf_release(&gitfile_sb);
3070 strbuf_release(&cfg_sb);
3071 strbuf_release(&rel_path);
3072 +
3073 + if (recurse_into_nested)
3074 + connect_wt_gitdir_in_nested(work_tree, git_dir);
3075 +
3076 free(work_tree);
3077 free(git_dir);
3078 }
@@ -3032,5 +3086,5 @@ void relocate_gitdir(const char *path, const char *old_git_dir, const char *new_
3086 die_errno(_("could not migrate git directory from '%s' to '%s'"),
3087 old_git_dir, new_git_dir);
3088
3035 - connect_work_tree_and_git_dir(path, new_git_dir);
3089 + connect_work_tree_and_git_dir(path, new_git_dir, 0);
3090 }
dir.h
+11 -1
@@ -359,7 +359,17 @@ struct untracked_cache *read_untracked_extension(const void *data, unsigned long
359 void write_untracked_extension(struct strbuf *out, struct untracked_cache *untracked);
360 void add_untracked_cache(struct index_state *istate);
361 void remove_untracked_cache(struct index_state *istate);
362 -extern void connect_work_tree_and_git_dir(const char *work_tree, const char *git_dir);
362 +
363 +/*
364 + * Connect a worktree to a git directory by creating (or overwriting) a
365 + * '.git' file containing the location of the git directory. In the git
366 + * directory set the core.worktree setting to indicate where the worktree is.
367 + * When `recurse_into_nested` is set, recurse into any nested submodules,
368 + * connecting them as well.
369 + */
370 +extern void connect_work_tree_and_git_dir(const char *work_tree,
371 + const char *git_dir,
372 + int recurse_into_nested);
373 extern void relocate_gitdir(const char *path,
374 const char *old_git_dir,
375 const char *new_git_dir);
repository.c
+3 -3
@@ -135,9 +135,9 @@ static int read_and_verify_repository_format(struct repository_format *format,
135 * Initialize 'repo' based on the provided 'gitdir'.
136 * Return 0 upon success and a non-zero value upon failure.
137 */
138 -static int repo_init(struct repository *repo,
139 - const char *gitdir,
140 - const char *worktree)
138 +int repo_init(struct repository *repo,
139 + const char *gitdir,
140 + const char *worktree)
141 {
142 struct repository_format format;
143 memset(repo, 0, sizeof(*repo));
repository.h
+3
@@ -97,6 +97,9 @@ extern void repo_set_gitdir(struct repository *repo,
97 extern void repo_set_worktree(struct repository *repo, const char *path);
98 extern void repo_set_hash_algo(struct repository *repo, int algo);
99 extern void initialize_the_repository(void);
100 +extern int repo_init(struct repository *r,
101 + const char *gitdir,
102 + const char *worktree);
103 extern int repo_submodule_init(struct repository *submodule,
104 struct repository *superproject,
105 const char *path);
submodule.c
+3 -3
@@ -1625,7 +1625,7 @@ int submodule_move_head(const char *path,
1625 } else {
1626 char *gitdir = xstrfmt("%s/modules/%s",
1627 get_git_common_dir(), sub->name);
1628 - connect_work_tree_and_git_dir(path, gitdir);
1628 + connect_work_tree_and_git_dir(path, gitdir, 0);
1629 free(gitdir);
1630
1631 /* make sure the index is clean as well */
@@ -1635,7 +1635,7 @@ int submodule_move_head(const char *path,
1635 if (old && (flags & SUBMODULE_MOVE_HEAD_FORCE)) {
1636 char *gitdir = xstrfmt("%s/modules/%s",
1637 get_git_common_dir(), sub->name);
1638 - connect_work_tree_and_git_dir(path, gitdir);
1638 + connect_work_tree_and_git_dir(path, gitdir, 1);
1639 free(gitdir);
1640 }
1641 }
@@ -1948,7 +1948,7 @@ void absorb_git_dir_into_superproject(const char *prefix,
1948 if (!sub)
1949 die(_("could not lookup name for submodule '%s'"), path);
1950 connect_work_tree_and_git_dir(path,
1951 - git_path("modules/%s", sub->name));
1951 + git_path("modules/%s", sub->name), 0);
1952 } else {
1953 /* Is it already absorbed into the superprojects git dir? */
1954 char *real_sub_git_dir = real_pathdup(sub_git_dir, 1);
t/t7001-mv.sh
+1 -1
@@ -491,7 +491,7 @@ test_expect_success 'moving a submodule in nested directories' '
491 test_cmp expect actual
492 '
493
494 -test_expect_failure 'moving nested submodules' '
494 +test_expect_success 'moving nested submodules' '
495 git commit -am "cleanup commit" &&
496 mkdir sub_nested_nested &&
497 (cd sub_nested_nested &&