diff options
-rw-r--r-- | Documentation/git-submodule.txt | 15 | ||||
-rw-r--r-- | builtin/submodule--helper.c | 69 | ||||
-rw-r--r-- | dir.c | 37 | ||||
-rw-r--r-- | dir.h | 4 | ||||
-rwxr-xr-x | git-submodule.sh | 7 | ||||
-rw-r--r-- | git.c | 2 | ||||
-rw-r--r-- | submodule.c | 127 | ||||
-rw-r--r-- | submodule.h | 5 | ||||
-rwxr-xr-x | t/t7412-submodule-absorbgitdirs.sh | 101 | ||||
-rw-r--r-- | t/test-lib-functions.sh | 20 | ||||
-rw-r--r-- | worktree.c | 50 | ||||
-rw-r--r-- | worktree.h | 5 |
12 files changed, 399 insertions, 43 deletions
diff --git a/Documentation/git-submodule.txt b/Documentation/git-submodule.txt index d841573475..918bd1d1bd 100644 --- a/Documentation/git-submodule.txt +++ b/Documentation/git-submodule.txt @@ -22,6 +22,7 @@ SYNOPSIS [commit] [--] [<path>...] 'git submodule' [--quiet] foreach [--recursive] <command> 'git submodule' [--quiet] sync [--recursive] [--] [<path>...] +'git submodule' [--quiet] absorbgitdirs [--] [<path>...] DESCRIPTION @@ -245,6 +246,20 @@ sync:: If `--recursive` is specified, this command will recurse into the registered submodules, and sync any nested submodules within. +absorbgitdirs:: + If a git directory of a submodule is inside the submodule, + move the git directory of the submodule into its superprojects + `$GIT_DIR/modules` path and then connect the git directory and + its working directory by setting the `core.worktree` and adding + a .git file pointing to the git directory embedded in the + superprojects git directory. ++ +A repository that was cloned independently and later added as a submodule or +old setups have the submodules git directory inside the submodule instead of +embedded into the superprojects git directory. ++ +This command is recursive by default. + OPTIONS ------- -q:: diff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c index 92fd676a2e..df0d9c166f 100644 --- a/builtin/submodule--helper.c +++ b/builtin/submodule--helper.c @@ -1091,21 +1091,62 @@ static int resolve_remote_submodule_branch(int argc, const char **argv, return 0; } +static int absorb_git_dirs(int argc, const char **argv, const char *prefix) +{ + int i; + struct pathspec pathspec; + struct module_list list = MODULE_LIST_INIT; + unsigned flags = ABSORB_GITDIR_RECURSE_SUBMODULES; + + struct option embed_gitdir_options[] = { + OPT_STRING(0, "prefix", &prefix, + N_("path"), + N_("path into the working tree")), + OPT_BIT(0, "--recursive", &flags, N_("recurse into submodules"), + ABSORB_GITDIR_RECURSE_SUBMODULES), + OPT_END() + }; + + const char *const git_submodule_helper_usage[] = { + N_("git submodule--helper embed-git-dir [<path>...]"), + NULL + }; + + argc = parse_options(argc, argv, prefix, embed_gitdir_options, + git_submodule_helper_usage, 0); + + gitmodules_config(); + git_config(submodule_config, NULL); + + if (module_list_compute(argc, argv, prefix, &pathspec, &list) < 0) + return 1; + + for (i = 0; i < list.nr; i++) + absorb_git_dir_into_superproject(prefix, + list.entries[i]->name, flags); + + return 0; +} + +#define SUPPORT_SUPER_PREFIX (1<<0) + struct cmd_struct { const char *cmd; int (*fn)(int, const char **, const char *); + unsigned option; }; static struct cmd_struct commands[] = { - {"list", module_list}, - {"name", module_name}, - {"clone", module_clone}, - {"update-clone", update_clone}, - {"relative-path", resolve_relative_path}, - {"resolve-relative-url", resolve_relative_url}, - {"resolve-relative-url-test", resolve_relative_url_test}, - {"init", module_init}, - {"remote-branch", resolve_remote_submodule_branch} + {"list", module_list, 0}, + {"name", module_name, 0}, + {"clone", module_clone, 0}, + {"update-clone", update_clone, 0}, + {"relative-path", resolve_relative_path, 0}, + {"resolve-relative-url", resolve_relative_url, 0}, + {"resolve-relative-url-test", resolve_relative_url_test, 0}, + {"init", module_init, 0}, + {"remote-branch", resolve_remote_submodule_branch, 0}, + {"absorb-git-dirs", absorb_git_dirs, SUPPORT_SUPER_PREFIX}, }; int cmd_submodule__helper(int argc, const char **argv, const char *prefix) @@ -1115,9 +1156,15 @@ int cmd_submodule__helper(int argc, const char **argv, const char *prefix) die(_("submodule--helper subcommand must be " "called with a subcommand")); - for (i = 0; i < ARRAY_SIZE(commands); i++) - if (!strcmp(argv[1], commands[i].cmd)) + for (i = 0; i < ARRAY_SIZE(commands); i++) { + if (!strcmp(argv[1], commands[i].cmd)) { + if (get_super_prefix() && + !(commands[i].option & SUPPORT_SUPER_PREFIX)) + die(_("%s doesn't support --super-prefix"), + commands[i].cmd); return commands[i].fn(argc - 1, argv + 1, prefix); + } + } die(_("'%s' is not a valid submodule--helper " "subcommand"), argv[1]); @@ -2748,3 +2748,40 @@ void untracked_cache_add_to_index(struct index_state *istate, { untracked_cache_invalidate_path(istate, path); } + +/* Update gitfile and core.worktree setting to connect work tree and git dir */ +void connect_work_tree_and_git_dir(const char *work_tree_, const char *git_dir_) +{ + struct strbuf file_name = STRBUF_INIT; + struct strbuf rel_path = STRBUF_INIT; + char *git_dir = xstrdup(real_path(git_dir_)); + char *work_tree = xstrdup(real_path(work_tree_)); + + /* Update gitfile */ + strbuf_addf(&file_name, "%s/.git", work_tree); + write_file(file_name.buf, "gitdir: %s", + relative_path(git_dir, work_tree, &rel_path)); + + /* Update core.worktree setting */ + strbuf_reset(&file_name); + strbuf_addf(&file_name, "%s/config", git_dir); + git_config_set_in_file(file_name.buf, "core.worktree", + relative_path(work_tree, git_dir, &rel_path)); + + strbuf_release(&file_name); + strbuf_release(&rel_path); + free(work_tree); + free(git_dir); +} + +/* + * Migrate the git directory of the given path from old_git_dir to new_git_dir. + */ +void relocate_gitdir(const char *path, const char *old_git_dir, const char *new_git_dir) +{ + if (rename(old_git_dir, new_git_dir) < 0) + die_errno(_("could not migrate git directory from '%s' to '%s'"), + old_git_dir, new_git_dir); + + connect_work_tree_and_git_dir(path, new_git_dir); +} @@ -335,4 +335,8 @@ struct untracked_cache *read_untracked_extension(const void *data, unsigned long void write_untracked_extension(struct strbuf *out, struct untracked_cache *untracked); void add_untracked_cache(struct index_state *istate); void remove_untracked_cache(struct index_state *istate); +extern void connect_work_tree_and_git_dir(const char *work_tree, const char *git_dir); +extern void relocate_gitdir(const char *path, + const char *old_git_dir, + const char *new_git_dir); #endif diff --git a/git-submodule.sh b/git-submodule.sh index 0a477b4c97..554bd1c494 100755 --- a/git-submodule.sh +++ b/git-submodule.sh @@ -1127,6 +1127,11 @@ cmd_sync() done } +cmd_absorbgitdirs() +{ + git submodule--helper absorb-git-dirs --prefix "$wt_prefix" "$@" +} + # This loop parses the command line arguments to find the # subcommand name to dispatch. Parsing of the subcommand specific # options are primarily done by the subcommand implementations. @@ -1136,7 +1141,7 @@ cmd_sync() while test $# != 0 && test -z "$command" do case "$1" in - add | foreach | init | deinit | update | status | summary | sync) + add | foreach | init | deinit | update | status | summary | sync | absorbgitdirs) command=$1 ;; -q|--quiet) @@ -493,7 +493,7 @@ static struct cmd_struct commands[] = { { "stage", cmd_add, RUN_SETUP | NEED_WORK_TREE }, { "status", cmd_status, RUN_SETUP | NEED_WORK_TREE }, { "stripspace", cmd_stripspace }, - { "submodule--helper", cmd_submodule__helper, RUN_SETUP }, + { "submodule--helper", cmd_submodule__helper, RUN_SETUP | SUPPORT_SUPER_PREFIX}, { "symbolic-ref", cmd_symbolic_ref, RUN_SETUP }, { "tag", cmd_tag, RUN_SETUP }, { "unpack-file", cmd_unpack_file, RUN_SETUP }, diff --git a/submodule.c b/submodule.c index ece17315d6..73521cdbb2 100644 --- a/submodule.c +++ b/submodule.c @@ -14,6 +14,7 @@ #include "blob.h" #include "thread-utils.h" #include "quote.h" +#include "worktree.h" static int config_fetch_recurse_submodules = RECURSE_SUBMODULES_ON_DEMAND; static int parallel_jobs = 1; @@ -1296,30 +1297,6 @@ int merge_submodule(unsigned char result[20], const char *path, return 0; } -/* Update gitfile and core.worktree setting to connect work tree and git dir */ -void connect_work_tree_and_git_dir(const char *work_tree, const char *git_dir) -{ - struct strbuf file_name = STRBUF_INIT; - struct strbuf rel_path = STRBUF_INIT; - const char *real_work_tree = xstrdup(real_path(work_tree)); - - /* Update gitfile */ - strbuf_addf(&file_name, "%s/.git", work_tree); - write_file(file_name.buf, "gitdir: %s", - relative_path(git_dir, real_work_tree, &rel_path)); - - /* Update core.worktree setting */ - strbuf_reset(&file_name); - strbuf_addf(&file_name, "%s/config", git_dir); - git_config_set_in_file(file_name.buf, "core.worktree", - relative_path(real_work_tree, git_dir, - &rel_path)); - - strbuf_release(&file_name); - strbuf_release(&rel_path); - free((void *)real_work_tree); -} - int parallel_submodules(void) { return parallel_jobs; @@ -1335,3 +1312,105 @@ void prepare_submodule_repo_env(struct argv_array *out) } argv_array_push(out, "GIT_DIR=.git"); } + +/* + * Embeds a single submodules git directory into the superprojects git dir, + * non recursively. + */ +static void relocate_single_git_dir_into_superproject(const char *prefix, + const char *path) +{ + char *old_git_dir = NULL, *real_old_git_dir = NULL, *real_new_git_dir = NULL; + const char *new_git_dir; + const struct submodule *sub; + + if (submodule_uses_worktrees(path)) + die(_("relocate_gitdir for submodule '%s' with " + "more than one worktree not supported"), path); + + old_git_dir = xstrfmt("%s/.git", path); + if (read_gitfile(old_git_dir)) + /* If it is an actual gitfile, it doesn't need migration. */ + return; + + real_old_git_dir = xstrdup(real_path(old_git_dir)); + + sub = submodule_from_path(null_sha1, path); + if (!sub) + die(_("could not lookup name for submodule '%s'"), path); + + new_git_dir = git_path("modules/%s", sub->name); + if (safe_create_leading_directories_const(new_git_dir) < 0) + die(_("could not create directory '%s'"), new_git_dir); + real_new_git_dir = xstrdup(real_path(new_git_dir)); + + if (!prefix) + prefix = get_super_prefix(); + + fprintf(stderr, _("Migrating git directory of '%s%s' from\n'%s' to\n'%s'\n"), + prefix ? prefix : "", path, + real_old_git_dir, real_new_git_dir); + + relocate_gitdir(path, real_old_git_dir, real_new_git_dir); + + free(old_git_dir); + free(real_old_git_dir); + free(real_new_git_dir); +} + +/* + * Migrate the git directory of the submodule given by path from + * having its git directory within the working tree to the git dir nested + * in its superprojects git dir under modules/. + */ +void absorb_git_dir_into_superproject(const char *prefix, + const char *path, + unsigned flags) +{ + const char *sub_git_dir, *v; + char *real_sub_git_dir = NULL, *real_common_git_dir = NULL; + struct strbuf gitdir = STRBUF_INIT; + + strbuf_addf(&gitdir, "%s/.git", path); + sub_git_dir = resolve_gitdir(gitdir.buf); + + /* Not populated? */ + if (!sub_git_dir) + goto out; + + /* Is it already absorbed into the superprojects git dir? */ + real_sub_git_dir = xstrdup(real_path(sub_git_dir)); + real_common_git_dir = xstrdup(real_path(get_git_common_dir())); + if (!skip_prefix(real_sub_git_dir, real_common_git_dir, &v)) + relocate_single_git_dir_into_superproject(prefix, path); + + if (flags & ABSORB_GITDIR_RECURSE_SUBMODULES) { + struct child_process cp = CHILD_PROCESS_INIT; + struct strbuf sb = STRBUF_INIT; + + if (flags & ~ABSORB_GITDIR_RECURSE_SUBMODULES) + die("BUG: we don't know how to pass the flags down?"); + + if (get_super_prefix()) + strbuf_addstr(&sb, get_super_prefix()); + strbuf_addstr(&sb, path); + strbuf_addch(&sb, '/'); + + cp.dir = path; + cp.git_cmd = 1; + cp.no_stdin = 1; + argv_array_pushl(&cp.args, "--super-prefix", sb.buf, + "submodule--helper", + "absorb-git-dirs", NULL); + prepare_submodule_repo_env(&cp.env_array); + if (run_command(&cp)) + die(_("could not recurse into submodule '%s'"), path); + + strbuf_release(&sb); + } + +out: + strbuf_release(&gitdir); + free(real_sub_git_dir); + free(real_common_git_dir); +} diff --git a/submodule.h b/submodule.h index 23d76682b1..b7576d6f43 100644 --- a/submodule.h +++ b/submodule.h @@ -68,7 +68,6 @@ int find_unpushed_submodules(struct sha1_array *commits, const char *remotes_nam extern int push_unpushed_submodules(struct sha1_array *commits, const char *remotes_name, int dry_run); -void connect_work_tree_and_git_dir(const char *work_tree, const char *git_dir); int parallel_submodules(void); /* @@ -78,4 +77,8 @@ int parallel_submodules(void); */ void prepare_submodule_repo_env(struct argv_array *out); +#define ABSORB_GITDIR_RECURSE_SUBMODULES (1<<0) +extern void absorb_git_dir_into_superproject(const char *prefix, + const char *path, + unsigned flags); #endif diff --git a/t/t7412-submodule-absorbgitdirs.sh b/t/t7412-submodule-absorbgitdirs.sh new file mode 100755 index 0000000000..1c47780e2b --- /dev/null +++ b/t/t7412-submodule-absorbgitdirs.sh @@ -0,0 +1,101 @@ +#!/bin/sh + +test_description='Test submodule absorbgitdirs + +This test verifies that `git submodue absorbgitdirs` moves a submodules git +directory into the superproject. +' + +. ./test-lib.sh + +test_expect_success 'setup a real submodule' ' + git init sub1 && + test_commit -C sub1 first && + git submodule add ./sub1 && + test_tick && + git commit -m superproject +' + +test_expect_success 'absorb the git dir' ' + >expect.1 && + >expect.2 && + >actual.1 && + >actual.2 && + git status >expect.1 && + git -C sub1 rev-parse HEAD >expect.2 && + git submodule absorbgitdirs && + git fsck && + test -f sub1/.git && + test -d .git/modules/sub1 && + git status >actual.1 && + git -C sub1 rev-parse HEAD >actual.2 && + test_cmp expect.1 actual.1 && + test_cmp expect.2 actual.2 +' + +test_expect_success 'absorbing does not fail for deinitalized submodules' ' + test_when_finished "git submodule update --init" && + git submodule deinit --all && + git submodule absorbgitdirs && + test -d .git/modules/sub1 && + test -d sub1 && + ! test -e sub1/.git +' + +test_expect_success 'setup nested submodule' ' + git init sub1/nested && + test_commit -C sub1/nested first_nested && + git -C sub1 submodule add ./nested && + test_tick && + git -C sub1 commit -m "add nested" && + git add sub1 && + git commit -m "sub1 to include nested submodule" +' + +test_expect_success 'absorb the git dir in a nested submodule' ' + git status >expect.1 && + git -C sub1/nested rev-parse HEAD >expect.2 && + git submodule absorbgitdirs && + test -f sub1/nested/.git && + test -d .git/modules/sub1/modules/nested && + git status >actual.1 && + git -C sub1/nested rev-parse HEAD >actual.2 && + test_cmp expect.1 actual.1 && + test_cmp expect.2 actual.2 +' + +test_expect_success 'setup a gitlink with missing .gitmodules entry' ' + git init sub2 && + test_commit -C sub2 first && + git add sub2 && + git commit -m superproject +' + +test_expect_success 'absorbing the git dir fails for incomplete submodules' ' + git status >expect.1 && + git -C sub2 rev-parse HEAD >expect.2 && + test_must_fail git submodule absorbgitdirs && + git -C sub2 fsck && + test -d sub2/.git && + git status >actual && + git -C sub2 rev-parse HEAD >actual.2 && + test_cmp expect.1 actual.1 && + test_cmp expect.2 actual.2 +' + +test_expect_success 'setup a submodule with multiple worktrees' ' + # first create another unembedded git dir in a new submodule + git init sub3 && + test_commit -C sub3 first && + git submodule add ./sub3 && + test_tick && + git commit -m "add another submodule" && + git -C sub3 worktree add ../sub3_second_work_tree +' + +test_expect_success 'absorbing fails for a submodule with multiple worktrees' ' + test_must_fail git submodule absorbgitdirs sub3 2>error && + test_i18ngrep "not supported" error +' + +test_done diff --git a/t/test-lib-functions.sh b/t/test-lib-functions.sh index adab7f51f4..bd357704cc 100644 --- a/t/test-lib-functions.sh +++ b/t/test-lib-functions.sh @@ -157,16 +157,21 @@ debug () { GIT_TEST_GDB=1 "$@" } -# Call test_commit with the arguments "<message> [<file> [<contents> [<tag>]]]" +# Call test_commit with the arguments +# [-C <directory>] <message> [<file> [<contents> [<tag>]]]" # # This will commit a file with the given contents and the given commit # message, and tag the resulting commit with the given tag name. # # <file>, <contents>, and <tag> all default to <message>. +# +# If the first argument is "-C", the second argument is used as a path for +# the git invocations. test_commit () { notick= && signoff= && + indir= && while test $# != 0 do case "$1" in @@ -176,21 +181,26 @@ test_commit () { --signoff) signoff="$1" ;; + -C) + indir="$2" + shift + ;; *) break ;; esac shift done && + indir=${indir:+"$indir"/} && file=${2:-"$1.t"} && - echo "${3-$1}" > "$file" && - git add "$file" && + echo "${3-$1}" > "$indir$file" && + git ${indir:+ -C "$indir"} add "$file" && if test -z "$notick" then test_tick fi && - git commit $signoff -m "$1" && - git tag "${4:-$1}" + git ${indir:+ -C "$indir"} commit $signoff -m "$1" && + git ${indir:+ -C "$indir"} tag "${4:-$1}" } # Call test_merge with the arguments "<message> <commit>", where <commit> diff --git a/worktree.c b/worktree.c index eb6121263b..828fd7a0ad 100644 --- a/worktree.c +++ b/worktree.c @@ -380,3 +380,53 @@ const struct worktree *find_shared_symref(const char *symref, return existing; } + +int submodule_uses_worktrees(const char *path) +{ + char *submodule_gitdir; + struct strbuf sb = STRBUF_INIT; + DIR *dir; + struct dirent *d; + int ret = 0; + struct repository_format format; + + submodule_gitdir = git_pathdup_submodule(path, "%s", ""); + if (!submodule_gitdir) + return 0; + + /* The env would be set for the superproject. */ + get_common_dir_noenv(&sb, submodule_gitdir); + + /* + * The check below is only known to be good for repository format + * version 0 at the time of writing this code. + */ + strbuf_addstr(&sb, "/config"); + read_repository_format(&format, sb.buf); + if (format.version != 0) { + strbuf_release(&sb); + return 1; + } + + /* Replace config by worktrees. */ + strbuf_setlen(&sb, sb.len - strlen("config")); + strbuf_addstr(&sb, "worktrees"); + + /* See if there is any file inside the worktrees directory. */ + dir = opendir(sb.buf); + strbuf_release(&sb); + free(submodule_gitdir); + + if (!dir) + return 0; + + while ((d = readdir(dir)) != NULL) { + if (is_dot_or_dotdot(d->d_name)) + continue; + + ret = 1; + break; + } + closedir(dir); + return ret; +} diff --git a/worktree.h b/worktree.h index d59ce1fee8..6bfb985203 100644 --- a/worktree.h +++ b/worktree.h @@ -28,6 +28,11 @@ struct worktree { extern struct worktree **get_worktrees(unsigned flags); /* + * Returns 1 if linked worktrees exist, 0 otherwise. + */ +extern int submodule_uses_worktrees(const char *path); + +/* * Return git dir of the worktree. Note that the path may be relative. * If wt is NULL, git dir of current worktree is returned. */ |