diff --git a/Documentation/git-worktree.adoc b/Documentation/git-worktree.adoc index fbf8426cd974e4..594dac225a96ec 100644 --- a/Documentation/git-worktree.adoc +++ b/Documentation/git-worktree.adoc @@ -10,7 +10,8 @@ SYNOPSIS -------- [synopsis] git worktree add [-f] [--detach] [--checkout] [--lock [--reason ]] - [--orphan] [(-b | -B) ] [] + [--orphan] [--recurse-submodules] + [(-b | -B) ] [] git worktree list [-v | --porcelain [-z]] git worktree lock [--reason ] git worktree move @@ -213,6 +214,16 @@ To remove a locked worktree, specify `--force` twice. such as configuring sparse-checkout. See "Sparse checkout" in linkgit:git-read-tree[1]. +`--recurse-submodules`:: + After the new worktree has been checked out, initialize and update + each submodule in it, as if running `git submodule update --init + --recursive` from within the new working tree. Each submodule's + gitdir is set up as a linked worktree of the shared submodule + repository at `$GIT_DIR/modules//`, so that the submodule's + objects and refs are shared across all worktrees. ++ +Incompatible with `--no-checkout` and `--orphan`. + `--guess-remote`:: `--no-guess-remote`:: With `worktree add `, without __, instead diff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c index 2f589e3b378d3f..b110c8f519a06c 100644 --- a/builtin/submodule--helper.c +++ b/builtin/submodule--helper.c @@ -36,6 +36,8 @@ #include "wildmatch.h" #include "strbuf.h" #include "url.h" +#include "worktree.h" +#include "copy.h" #define OPT_QUIET (1 << 0) #define OPT_CACHED (1 << 1) @@ -1896,6 +1898,72 @@ static int dir_contains_only_dotgit(const char *path) return ret; } +/* + * Best-effort: if the main worktree has the submodule with an in-tree .git/ + * directory, relocate it to $GIT_COMMON_DIR/modules//. Stderr is + * suppressed; if nothing is absorbed the caller will clone from URL instead. + */ +static void absorb_in_main_worktree(const char *sub_path) +{ + struct worktree **worktrees = get_worktrees(); + const char *main_path = NULL; + int i; + + for (i = 0; worktrees[i]; i++) { + if (is_main_worktree(worktrees[i])) { + main_path = worktrees[i]->path; + break; + } + } + if (main_path) { + struct child_process cp = CHILD_PROCESS_INIT; + cp.git_cmd = 1; + cp.dir = main_path; + cp.no_stdin = 1; + cp.no_stderr = 1; + strvec_pushl(&cp.args, "submodule", "absorbgitdirs", + "--", sub_path, NULL); + prepare_submodule_repo_env(&cp.env); + run_command(&cp); + } + + free_worktrees(worktrees); +} + +/* + * Create sm_gitdir ($GIT_COMMON_DIR/modules//worktrees//) as a + * linked-worktree gitdir of shared_sm_gitdir. Because it lives under the + * shared repo's worktrees/ tree, "git gc" on the submodule sees it via + * get_worktrees() and protects per-worktree objects from pruning. + * connect_work_tree_and_git_dir() writes the "gitdir" back-pointer afterwards. + */ +static void setup_worktree_submodule_gitdir(const char *sm_gitdir, + const char *shared_sm_gitdir) +{ + struct strbuf path = STRBUF_INIT, rel = STRBUF_INIT; + char *head_src; + + if (safe_create_leading_directories_const(the_repository, sm_gitdir) < 0) + die(_("could not create directory '%s'"), sm_gitdir); + if (mkdir(sm_gitdir, 0777) && errno != EEXIST) + die_errno(_("could not create directory '%s'"), sm_gitdir); + + strbuf_addf(&path, "%s/commondir", sm_gitdir); + relative_path(shared_sm_gitdir, sm_gitdir, &rel); + strbuf_strip_suffix(&rel, "/"); + write_file(path.buf, "%s", rel.buf); + strbuf_reset(&path); + + strbuf_addf(&path, "%s/HEAD", sm_gitdir); + head_src = xstrfmt("%s/HEAD", shared_sm_gitdir); + if (copy_file(path.buf, head_src, 0666)) + die(_("could not copy '%s' to '%s'"), head_src, path.buf); + free(head_src); + + strbuf_release(&path); + strbuf_release(&rel); +} + static int clone_submodule(const struct module_clone_data *clone_data, struct string_list *reference) { @@ -1906,21 +1974,31 @@ static int clone_submodule(const struct module_clone_data *clone_data, struct child_process cp = CHILD_PROCESS_INIT; const char *clone_data_path = clone_data->path; char *to_free = NULL; + /* + * In a linked worktree, submodule_name_to_gitdir() returns a per-worktree + * gitdir at $GIT_COMMON_DIR/modules//worktrees// (sm_gitdir). + * Any cloning targets the shared location shared_sm_gitdir; afterwards we + * set sm_gitdir up as a linked-worktree gitdir pointing at it so that + * "git gc" on the submodule finds per-worktree objects via get_worktrees(). + */ + char *shared_sm_gitdir = + xstrfmt("%s/modules/%s", + the_repository->commondir, clone_data->name); + /* True when the superproject is in a linked worktree (gitdir != commondir). */ + int linked_worktree = the_repository->different_commondir; - if (validate_submodule_path(clone_data_path) < 0) - die(NULL); - - if (!is_absolute_path(clone_data->path)) - clone_data_path = to_free = xstrfmt("%s/%s", repo_get_work_tree(the_repository), - clone_data->path); + /* Auto-absorb a legacy in-tree gitdir if shared location is missing. */ + if (linked_worktree && !is_git_directory(shared_sm_gitdir)) + absorb_in_main_worktree(clone_data->path); - if (!file_exists(sm_gitdir)) { + if (!file_exists(shared_sm_gitdir)) { if (clone_data->require_init && !stat(clone_data_path, &st) && !is_empty_dir(clone_data_path)) die(_("directory not empty: '%s'"), clone_data_path); - if (safe_create_leading_directories_const(the_repository, sm_gitdir) < 0) - die(_("could not create directory '%s'"), sm_gitdir); + if (safe_create_leading_directories_const(the_repository, + shared_sm_gitdir) < 0) + die(_("could not create directory '%s'"), shared_sm_gitdir); prepare_possible_alternates(clone_data->name, reference); @@ -1944,8 +2022,8 @@ static int clone_submodule(const struct module_clone_data *clone_data, ref_storage_format_to_name(clone_data->ref_storage_format)); if (clone_data->dissociate) strvec_push(&cp.args, "--dissociate"); - if (sm_gitdir && *sm_gitdir) - strvec_pushl(&cp.args, "--separate-git-dir", sm_gitdir, NULL); + if (shared_sm_gitdir && *shared_sm_gitdir) + strvec_pushl(&cp.args, "--separate-git-dir", shared_sm_gitdir, NULL); if (clone_data->filter_options && clone_data->filter_options->choice) strvec_pushf(&cp.args, "--filter=%s", expand_list_objects_filter_spec( @@ -1996,15 +2074,18 @@ static int clone_submodule(const struct module_clone_data *clone_data, * To prevent further harm coming from this unintentionally-nested * gitdir, let's disable it by deleting the `HEAD` file. */ - if (validate_submodule_git_dir(sm_gitdir, clone_data->name) < 0) { - char *head = xstrfmt("%s/HEAD", sm_gitdir); + if (validate_submodule_git_dir(shared_sm_gitdir, clone_data->name) < 0) { + char *head = xstrfmt("%s/HEAD", shared_sm_gitdir); unlink(head); free(head); die(_("refusing to create/use '%s' in another submodule's git dir. " "Enabling extensions.submodulePathConfig should fix this."), - sm_gitdir); + shared_sm_gitdir); } + if (linked_worktree) + setup_worktree_submodule_gitdir(sm_gitdir, shared_sm_gitdir); + connect_work_tree_and_git_dir(clone_data_path, sm_gitdir, 0); p = repo_submodule_path(the_repository, clone_data_path, "config"); @@ -2025,6 +2106,7 @@ static int clone_submodule(const struct module_clone_data *clone_data, free(error_strategy); free(sm_gitdir); + free(shared_sm_gitdir); free(p); free(to_free); return 0; @@ -2728,6 +2810,23 @@ static int ensure_core_worktree(const char *path) const char *rel_path; struct strbuf sb = STRBUF_INIT; + /* + * In a worktree-style submodule (a per-worktree gitdir + * pointing at a shared modules// via "commondir"), + * repo_git_path() redirects "config" to the shared file, + * which holds the main worktree's core.worktree. Writing + * a per-worktree value there would corrupt the main + * worktree's view of the submodule -- and the read in the + * "if" above already returned that shared value, so there + * is nothing useful to refresh. Worktree-private state + * lives next to the worktree's .git pointer file; leave + * the shared config alone. + */ + if (subrepo.different_commondir) { + repo_clear(&subrepo); + return 0; + } + cfg_file = repo_git_path(&subrepo, "config"); abs_path = absolute_pathdup(path); diff --git a/builtin/worktree.c b/builtin/worktree.c index d21c43fde38b5e..949a2052711ab2 100644 --- a/builtin/worktree.c +++ b/builtin/worktree.c @@ -123,6 +123,7 @@ struct add_opts { int checkout; int orphan; int relative_paths; + int recurse_submodules; const char *keep_locked; }; @@ -593,6 +594,20 @@ static int add_worktree(const char *path, const char *refname, (ret = checkout_worktree(opts, &child_env))) goto done; + if (!ret && opts->checkout && opts->recurse_submodules) { + struct child_process cp = CHILD_PROCESS_INIT; + cp.git_cmd = 1; + cp.dir = path; + strvec_pushl(&cp.args, "submodule", "update", + "--init", "--recursive", NULL); + if (opts->quiet) + strvec_push(&cp.args, "--quiet"); + strvec_pushv(&cp.env, child_env.v); + ret = run_command(&cp); + if (ret) + goto done; + } + is_junk = 0; FREE_AND_NULL(junk_work_tree); FREE_AND_NULL(junk_git_dir); @@ -823,6 +838,8 @@ static int add(int ac, const char **av, const char *prefix, N_("try to match the new branch name with a remote-tracking branch")), OPT_BOOL(0, "relative-paths", &opts.relative_paths, N_("use relative paths for worktrees")), + OPT_BOOL(0, "recurse-submodules", &opts.recurse_submodules, + N_("initialize submodules in the new worktree")), OPT_END() }; int ret; @@ -842,6 +859,12 @@ static int add(int ac, const char **av, const char *prefix, if (opts.orphan && !opts.checkout) die(_("options '%s' and '%s' cannot be used together"), "--orphan", "--no-checkout"); + if (opts.recurse_submodules && !opts.checkout) + die(_("options '%s' and '%s' cannot be used together"), + "--recurse-submodules", "--no-checkout"); + if (opts.recurse_submodules && opts.orphan) + die(_("options '%s' and '%s' cannot be used together"), + "--recurse-submodules", "--orphan"); if (opts.orphan && ac == 2) die(_("option '%s' and commit-ish cannot be used together"), "--orphan"); @@ -1200,6 +1223,143 @@ static int unlock_worktree(int ac, const char **av, const char *prefix, return ret; } +/* + * Recursively check whether every submodule repo under modules_dir has a + * worktrees// entry. Recurses into repo/modules/ for nested + * submodules and into bare subdirectories for slash-named submodule paths. + * Returns > 0 if all repos have the entry, 0 if any lack it, < 0 if none found. + */ +static int scan_modules_for_wt_id(const char *modules_dir, const char *wt_id) +{ + DIR *dir; + struct dirent *de; + int found = 0, all_have_entry = 1; + + dir = opendir(modules_dir); + if (!dir) + return -1; + + while ((de = readdir_skip_dot_and_dotdot(dir)) != NULL) { + struct strbuf entry = STRBUF_INIT; + + strbuf_addf(&entry, "%s/%s", modules_dir, de->d_name); + if (!is_directory(entry.buf)) { + strbuf_release(&entry); + continue; + } + + strbuf_addstr(&entry, "/HEAD"); + if (file_exists(entry.buf)) { + struct strbuf probe = STRBUF_INIT; + + strbuf_strip_suffix(&entry, "/HEAD"); + found = 1; + + strbuf_addf(&probe, "%s/worktrees/%s", entry.buf, wt_id); + if (!is_directory(probe.buf)) + all_have_entry = 0; + + /* Check nested submodules. */ + strbuf_reset(&probe); + strbuf_addf(&probe, "%s/modules", entry.buf); + if (is_directory(probe.buf)) { + int nested = scan_modules_for_wt_id(probe.buf, + wt_id); + if (nested == 0) + all_have_entry = 0; + } + strbuf_release(&probe); + } else { + int sub; + /* Intermediate path component for slash-named submodule. */ + strbuf_strip_suffix(&entry, "/HEAD"); + sub = scan_modules_for_wt_id(entry.buf, wt_id); + if (sub >= 0) { + found = 1; + if (sub == 0) + all_have_entry = 0; + } + } + strbuf_release(&entry); + } + closedir(dir); + + if (!found) + return -1; + return all_have_entry ? 1 : 0; +} + +/* + * Delete worktrees// from every submodule shared repo under modules_dir. + * Called by remove_worktree() to clean up per-worktree submodule gitdirs. + */ +static void remove_submodule_wt_entries(const char *modules_dir, + const char *wt_id) +{ + DIR *dir; + struct dirent *de; + + dir = opendir(modules_dir); + if (!dir) + return; + + while ((de = readdir_skip_dot_and_dotdot(dir)) != NULL) { + struct strbuf entry = STRBUF_INIT; + struct strbuf head = STRBUF_INIT; + + strbuf_addf(&entry, "%s/%s", modules_dir, de->d_name); + if (!is_directory(entry.buf)) { + strbuf_release(&entry); + strbuf_release(&head); + continue; + } + + strbuf_addf(&head, "%s/HEAD", entry.buf); + if (file_exists(head.buf)) { + struct strbuf wt_path = STRBUF_INIT; + struct strbuf nested = STRBUF_INIT; + + strbuf_addf(&wt_path, "%s/worktrees/%s", entry.buf, wt_id); + if (is_directory(wt_path.buf)) + remove_dir_recursively(&wt_path, 0); + + strbuf_addf(&nested, "%s/modules", entry.buf); + if (is_directory(nested.buf)) + remove_submodule_wt_entries(nested.buf, wt_id); + + strbuf_release(&wt_path); + strbuf_release(&nested); + } else { + remove_submodule_wt_entries(entry.buf, wt_id); + } + strbuf_release(&entry); + strbuf_release(&head); + } + closedir(dir); +} + +/* + * Return true when every submodule in has its per-worktree gitdir set up + * at $GIT_COMMON_DIR/modules//worktrees/id>/. Returns false if any + * submodule uses the legacy layout or is not yet initialized. + */ +static int worktree_has_per_worktree_submodule_gitdirs( + const struct worktree *wt) +{ + struct strbuf modules_path = STRBUF_INIT; + int result; + + strbuf_addf(&modules_path, "%s/modules", the_repository->commondir); + if (!is_directory(modules_path.buf)) { + strbuf_release(&modules_path); + return 0; + } + + result = scan_modules_for_wt_id(modules_path.buf, wt->id); + strbuf_release(&modules_path); + return result > 0; +} + static void validate_no_submodules(const struct worktree *wt) { struct index_state istate = INDEX_STATE_INIT(the_repository); @@ -1330,10 +1490,18 @@ static void check_clean_worktree(struct worktree *wt, int ret; /* - * Until we sort this out, all submodules are "dirty" and - * will abort this function. + * Per-worktree submodule gitdirs (identified by a "commondir" file + * under the worktree's modules/ directory) are nested inside the + * worktree's own gitdir and therefore disappear for free when the + * gitdir is deleted. Their working-tree cleanliness is checked by + * "git status --ignore-submodules=none" below, so skip the blanket + * submodule block in that case. + * + * For any other submodule layout (e.g. old shared gitdirs without a + * "commondir" file) retain the existing hard block. */ - validate_no_submodules(wt); + if (!worktree_has_per_worktree_submodule_gitdirs(wt)) + validate_no_submodules(wt); child_process_init(&cp); strvec_pushf(&cp.env, "%s=%s/.git", @@ -1424,6 +1592,20 @@ static int remove_worktree(int ac, const char **av, const char *prefix, * from here. */ ret |= delete_git_dir(wt->id); + + /* + * Per-worktree submodule gitdirs live at + * $GIT_COMMON_DIR/modules//worktrees// and are not + * removed by delete_git_dir(); clean them up explicitly. + */ + { + struct strbuf modules_path = STRBUF_INIT; + strbuf_addf(&modules_path, "%s/modules", the_repository->commondir); + if (is_directory(modules_path.buf)) + remove_submodule_wt_entries(modules_path.buf, wt->id); + strbuf_release(&modules_path); + } + delete_worktrees_dir_if_empty(); free_worktrees(worktrees); diff --git a/submodule.c b/submodule.c index b1a0363f9d2a96..8c0e0652a05c28 100644 --- a/submodule.c +++ b/submodule.c @@ -2400,15 +2400,24 @@ static int validate_submodule_encoded_git_dir(char *git_dir, const char *submodu static int validate_submodule_legacy_git_dir(char *git_dir, const char *submodule_name) { - size_t len = strlen(git_dir), suffix_len = strlen(submodule_name); - char *p; + size_t len, suffix_len = strlen(submodule_name); + char *check_dir, *p; + char *wt_sep; int ret = 0; if (the_repository->repository_format_submodule_path_cfg) BUG("validate_submodule_git_dir() must be called with " "extensions.submodulePathConfig disabled."); - if (len <= suffix_len || (p = git_dir + len - suffix_len)[-1] != '/' || + /* + * Per-worktree paths are modules//worktrees//; strip the + * suffix so the sibling-clash check applies to modules// as usual. + */ + wt_sep = strstr(git_dir, "/worktrees/"); + check_dir = wt_sep ? xmemdupz(git_dir, wt_sep - git_dir) : git_dir; + len = strlen(check_dir); + + if (len <= suffix_len || (p = check_dir + len - suffix_len)[-1] != '/' || strcmp(p, submodule_name)) BUG("submodule name '%s' not a suffix of git dir '%s'", submodule_name, git_dir); @@ -2428,19 +2437,23 @@ static int validate_submodule_legacy_git_dir(char *git_dir, const char *submodul char c = *p; *p = '\0'; - if (is_git_directory(git_dir)) + if (is_git_directory(check_dir)) ret = -1; *p = c; - if (ret < 0) - return error(_("submodule git dir '%s' is " - "inside git dir '%.*s'"), - git_dir, - (int)(p - git_dir), git_dir); + if (ret < 0) { + error(_("submodule git dir '%s' is " + "inside git dir '%.*s'"), + check_dir, + (int)(p - check_dir), check_dir); + break; + } } } - return 0; + if (wt_sep) + free(check_dir); + return ret; } int validate_submodule_git_dir(char *git_dir, const char *submodule_name) @@ -2737,10 +2750,19 @@ void submodule_name_to_gitdir(struct strbuf *buf, struct repository *r, { if (!r->repository_format_submodule_path_cfg) { /* - * If extensions.submodulePathConfig is disabled, - * continue to use the plain path. + * In a linked worktree return $GIT_COMMON_DIR/modules//worktrees// + * so the per-worktree gitdir lives under the shared submodule repo's + * worktrees/ tree and is visible to "git gc" via get_worktrees(). + * In the main worktree return $GIT_COMMON_DIR/modules//. */ - repo_git_path_append(r, buf, "modules/%s", submodule_name); + if (r->different_commondir) { + const char *sep = find_last_dir_sep(r->gitdir); + const char *wt_id = sep ? sep + 1 : r->gitdir; + strbuf_addf(buf, "%s/modules/%s/worktrees/%s", + r->commondir, submodule_name, wt_id); + } else { + repo_git_path_append(r, buf, "modules/%s", submodule_name); + } } else { const char *gitdir; char *key; @@ -2761,8 +2783,12 @@ void submodule_name_to_gitdir(struct strbuf *buf, struct repository *r, strbuf_addstr(buf, gitdir); } - /* validate because users might have modified the config */ - if (validate_submodule_git_dir(buf->buf, submodule_name)) { + /* + * The per-worktree path modules//worktrees// doesn't end + * with , but sibling clashes can't occur there; skip validation. + */ + if (!r->different_commondir && + validate_submodule_git_dir(buf->buf, submodule_name)) { advise(_("enabling extensions.submodulePathConfig might fix the " "following error, if it's not already enabled.")); die(_("refusing to create/use '%s' in another submodule's " diff --git a/t/t2403-worktree-move.sh b/t/t2403-worktree-move.sh index 0bb33e8b1b90fb..dd10998e61c30b 100755 --- a/t/t2403-worktree-move.sh +++ b/t/t2403-worktree-move.sh @@ -236,13 +236,62 @@ test_expect_success 'remove a repo with uninitialized submodule' ' ) ' -test_expect_success 'not remove a repo with initialized submodule' ' +test_expect_success 'remove a clean worktree with per-worktree submodule gitdir' ' test_config_global protocol.file.allow always && ( cd withsub && git worktree add to-remove HEAD && git -C to-remove submodule update && - test_must_fail git worktree remove to-remove + git worktree remove to-remove + ) +' + +test_expect_success 'not remove a repo with dirty initialized submodule' ' + test_config_global protocol.file.allow always && + ( + cd withsub && + git worktree add to-remove-dirty HEAD && + git -C to-remove-dirty submodule update && + echo dirty >to-remove-dirty/sub/dirty-file && + test_must_fail git worktree remove to-remove-dirty && + git worktree remove --force to-remove-dirty + ) +' + +test_expect_success 'not remove a worktree with mixed per-worktree and legacy submodule gitdirs' ' + test_config_global protocol.file.allow always && + ( + cd withsub && + git worktree add mixed-layout HEAD && + git -C mixed-layout submodule update && + # Inject a fake shared submodule repo in modules/ that lacks a + # worktrees/mixed-layout/ entry (legacy model). scan_modules_for_wt_id + # finds sub/ (has the entry) and legacy-sub/ (lacks it), returning 0. + mkdir -p .git/modules/legacy-sub && + printf "ref: refs/heads/main\n" >.git/modules/legacy-sub/HEAD && + test_must_fail git worktree remove mixed-layout && + # Clean up the injected repo and the worktree. + rm -rf .git/modules/legacy-sub && + git worktree remove --force mixed-layout + ) +' + +test_expect_success 'not remove a worktree whose per-worktree submodule has a legacy nested submodule gitdir' ' + test_config_global protocol.file.allow always && + ( + cd withsub && + git worktree add nested-sm HEAD && + git -C nested-sm submodule update && + # Inject a fake nested shared repo inside sub/modules/ that lacks + # a worktrees/nested-sm/ entry. scan_modules_for_wt_id recurses + # into sub/modules/ and finds it, forcing rejection. + mkdir -p .git/modules/sub/modules/nested && + printf "ref: refs/heads/main\n" \ + >.git/modules/sub/modules/nested/HEAD && + test_must_fail git worktree remove nested-sm && + # Clean up. + rm -rf .git/modules/sub/modules && + git worktree remove --force nested-sm ) ' diff --git a/t/t2405-worktree-submodule.sh b/t/t2405-worktree-submodule.sh index 11018f37c70c02..ac164cd50799f0 100755 --- a/t/t2405-worktree-submodule.sh +++ b/t/t2405-worktree-submodule.sh @@ -34,11 +34,38 @@ test_expect_success 'add superproject worktree' ' git -C main worktree add "$base_path/worktree" "$rev1_hash_main" ' -test_expect_failure 'submodule is checked out just after worktree add' ' +test_expect_failure 'submodule is checked out just after worktree add (without flag)' ' git -C worktree diff --submodule main"^!" >out && grep "file1 updated" out ' +test_expect_success 'worktree add --recurse-submodules initializes submodules' ' + git -C main worktree add --recurse-submodules \ + "$base_path/worktree-recurse" "$rev1_hash_main" && + git -C worktree-recurse diff --submodule main"^!" >out && + grep "file1 updated" out +' + +test_expect_success 'submodule in --recurse-submodules worktree uses per-worktree gitdir' ' + # The per-worktree submodule gitdir must live under the worktree entry, + # not under $GIT_COMMON_DIR/modules/, so it is cleaned up with the + # worktree and does not disturb the main worktree submodule. + sub_gitdir="$base_path/main/.git/modules/sub/worktrees/worktree-recurse" && + test -d "$sub_gitdir" && + # .git pointer in the working tree must reference the per-worktree gitdir + echo "gitdir: ../../main/.git/modules/sub/worktrees/worktree-recurse" \ + >expect-gitfile && + cat "$base_path/worktree-recurse/sub/.git" >actual-gitfile && + test_cmp expect-gitfile actual-gitfile && + # The per-worktree gitdir must have a commondir file pointing at the + # shared submodule repo, not its own object store. + echo "../.." >expect-commondir && + test_cmp expect-commondir "$sub_gitdir/commondir" && + test_path_is_missing "$sub_gitdir/objects" && + # The working tree is populated (test_commit creates .t files) + test -f "$base_path/worktree-recurse/sub/file1.t" +' + test_expect_success 'add superproject worktree and initialize submodules' ' git -C main worktree add "$base_path/worktree-submodule-update" "$rev1_hash_main" && git -C worktree-submodule-update submodule update @@ -62,7 +89,7 @@ test_expect_success 'submodule is checked out after manually adding submodule wo test_expect_success 'checkout --recurse-submodules uses $GIT_DIR for submodules in a linked worktree' ' git -C main worktree add "$base_path/checkout-recurse" --detach && git -C checkout-recurse submodule update --init && - echo "gitdir: ../../main/.git/worktrees/checkout-recurse/modules/sub" >expect-gitfile && + echo "gitdir: ../../main/.git/modules/sub/worktrees/checkout-recurse" >expect-gitfile && cat checkout-recurse/sub/.git >actual-gitfile && test_cmp expect-gitfile actual-gitfile && git -C main/sub rev-parse HEAD >expect-head-main && @@ -73,22 +100,94 @@ test_expect_success 'checkout --recurse-submodules uses $GIT_DIR for submodules test_cmp expect-head-main actual-head-main ' -test_expect_success 'core.worktree is removed in $GIT_DIR/modules//config, not in $GIT_COMMON_DIR/modules//config' ' +test_expect_success 'per-worktree submodule gitdir uses commondir; shared config is unchanged' ' + # The shared submodule repo core.worktree points at the main worktree. echo "../../../sub" >expect-main && git -C main/sub config --get core.worktree >actual-main && test_cmp expect-main actual-main && - echo "../../../../../../checkout-recurse/sub" >expect-linked && - git -C checkout-recurse/sub config --get core.worktree >actual-linked && - test_cmp expect-linked actual-linked && + + # The per-worktree submodule gitdir has a commondir file pointing at + # the shared submodule repo, not its own objects or refs. + linked_sm_gitdir="main/.git/modules/sub/worktrees/checkout-recurse" && + test -f "$linked_sm_gitdir/commondir" && + echo "../.." >expect-commondir && + test_cmp expect-commondir "$linked_sm_gitdir/commondir" && + + # Checking out a commit that removes the submodule leaves the shared + # submodule repo intact. git -C checkout-recurse checkout --recurse-submodules first && - test_expect_code 1 git -C main/.git/worktrees/checkout-recurse/modules/sub config --get core.worktree >linked-config && - test_must_be_empty linked-config && git -C main/sub config --get core.worktree >actual-main && test_cmp expect-main actual-main ' +test_expect_success 'worktree remove cleans up per-worktree submodule gitdir' ' + git -C main worktree add "$base_path/remove-recurse" "$rev1_hash_main" && + git -C remove-recurse submodule update --init && + test -d "main/.git/modules/sub/worktrees/remove-recurse" && + git -C main worktree remove remove-recurse && + test_path_is_missing "main/.git/worktrees/remove-recurse" && + test_path_is_missing "remove-recurse" && + # The per-worktree submodule gitdir must also be removed. + test_path_is_missing "main/.git/modules/sub/worktrees/remove-recurse" && + # The shared submodule repo must not be affected. + test -d "main/.git/modules/sub" && + git -C main/sub log --oneline -1 +' + test_expect_success 'unsetting core.worktree does not prevent running commands directly against the submodule repository' ' - git -C main/.git/worktrees/checkout-recurse/modules/sub log + git -C main/.git/modules/sub/worktrees/checkout-recurse log +' + +test_expect_success 'auto-absorb: submodule with in-tree gitdir is absorbed on first linked-worktree submodule init' ' + # Clone the superproject without initializing the submodule, then + # clone the submodule in-tree (legacy layout: .git/ is a directory, + # not absorbed into $GIT_DIR/modules/). + git clone "$base_path/origin/main" main-intree && + test_when_finished "rm -rf main-intree worktree-absorb" && + git -C main-intree submodule init && + git clone "$base_path/origin/sub" main-intree/sub && + git -C main-intree/sub checkout "$rev1_hash_sub" && + # The submodule gitdir is in-tree: a directory, not a pointer file. + test -d "main-intree/sub/.git" && + test_path_is_missing "main-intree/.git/modules" && + + # Initialize the submodule in a linked worktree: absorb_in_main_worktree + # should relocate the in-tree gitdir to modules/sub, then set up the + # per-worktree gitdir with commondir indirection. + git -C main-intree worktree add "$base_path/worktree-absorb" "$rev1_hash_main" && + git -C worktree-absorb submodule update --init && + + # Absorption happened: submodule gitdir now lives under modules/. + test -d "main-intree/.git/modules/sub" && + # Per-worktree gitdir with commondir exists. + test -f "main-intree/.git/modules/sub/worktrees/worktree-absorb/commondir" && + # Submodule working tree is populated. + test -f "worktree-absorb/sub/file1.t" +' + +test_expect_success 'worktree remove handles submodule with slash in name (nested modules path)' ' + # Create a superproject with a submodule whose path contains a slash so + # that its gitdir lives at modules/nested/sub/ rather than modules/sub/. + git init nested-super && + test_when_finished "rm -rf nested-super nested-worktree" && + git init nested-super/nested/sub && + test_commit -C nested-super/nested/sub file1 && + ( + cd nested-super && + git -c protocol.file.allow=always submodule add ./nested/sub nested/sub && + git commit -m "add nested/sub" + ) && + + git -C nested-super worktree add "$base_path/nested-worktree" HEAD && + git -C nested-worktree submodule update --init && + + # The per-worktree gitdir should be at modules/nested/sub/ with commondir. + test -f "nested-super/.git/modules/nested/sub/worktrees/nested-worktree/commondir" && + + # worktree remove must succeed: all submodule gitdirs are per-worktree. + git -C nested-super worktree remove "$base_path/nested-worktree" && + test_path_is_missing "nested-super/.git/worktrees/nested-worktree" && + test_path_is_missing "nested-worktree" ' test_done