When deleting a submodule, we need to keep the actual git directory around, such that we do not lose local changes in there and at a later checkout of the submodule we don't need to clone it again. Implement `depopulate_submodule`, that migrates the git directory before deletion of a submodule and afterwards the equivalent of "rm -rf", which is already found in entry.c, so expose that and for clarity add a suffix "_or_die" to it. Signed-off-by: Stefan Beller <sbeller@xxxxxxxxxx> --- builtin/rm.c | 30 ++++++++---------------------- cache.h | 2 ++ entry.c | 5 +++++ submodule.c | 31 +++++++++++++++++++++++++++++++ submodule.h | 6 ++++++ t/t3600-rm.sh | 39 +++++++++++++++------------------------ 6 files changed, 67 insertions(+), 46 deletions(-) diff --git a/builtin/rm.c b/builtin/rm.c index fdd7183f61..8c9c535a88 100644 --- a/builtin/rm.c +++ b/builtin/rm.c @@ -392,28 +392,14 @@ int cmd_rm(int argc, const char **argv, const char *prefix) for (i = 0; i < list.nr; i++) { const char *path = list.entry[i].name; if (list.entry[i].is_submodule) { - if (is_empty_dir(path)) { - if (!rmdir(path)) { - removed = 1; - if (!remove_path_from_gitmodules(path)) - gitmodules_modified = 1; - continue; - } - } else { - strbuf_reset(&buf); - strbuf_addstr(&buf, path); - if (!remove_dir_recursively(&buf, 0)) { - removed = 1; - if (!remove_path_from_gitmodules(path)) - gitmodules_modified = 1; - strbuf_release(&buf); - continue; - } else if (!file_exists(path)) - /* Submodule was removed by user */ - if (!remove_path_from_gitmodules(path)) - gitmodules_modified = 1; - /* Fallthrough and let remove_path() fail. */ - } + if (is_empty_dir(path) && rmdir(path)) + die_errno("git rm: '%s'", path); + if (file_exists(path)) + depopulate_submodule(path); + removed = 1; + if (!remove_path_from_gitmodules(path)) + gitmodules_modified = 1; + continue; } if (!remove_path(path)) { removed = 1; diff --git a/cache.h b/cache.h index a50a61a197..b645ca2f9a 100644 --- a/cache.h +++ b/cache.h @@ -2018,4 +2018,6 @@ void sleep_millisec(int millisec); */ void safe_create_dir(const char *dir, int share); +extern void remove_directory_or_die(struct strbuf *path); + #endif /* CACHE_H */ diff --git a/entry.c b/entry.c index c6eea240b6..02c4ac9f22 100644 --- a/entry.c +++ b/entry.c @@ -73,6 +73,11 @@ static void remove_subtree(struct strbuf *path) die_errno("cannot rmdir '%s'", path->buf); } +void remove_directory_or_die(struct strbuf *path) +{ + remove_subtree(path); +} + static int create_file(const char *path, unsigned int mode) { mode = (mode & 0100) ? 0777 : 0666; diff --git a/submodule.c b/submodule.c index e42efa2337..3770ecb7b9 100644 --- a/submodule.c +++ b/submodule.c @@ -308,6 +308,37 @@ static void print_submodule_summary(struct rev_info *rev, FILE *f, strbuf_release(&sb); } +void depopulate_submodule(const char *path) +{ + struct strbuf pathbuf = STRBUF_INIT; + char *dot_git = xstrfmt("%s/.git", path); + + /* Is it populated? */ + if (!resolve_gitdir(dot_git)) + goto out; + + /* Does it have a .git directory? */ + if (!submodule_uses_gitfile(path)) { + absorb_git_dir_into_superproject("", path, + ABSORB_GITDIR_RECURSE_SUBMODULES); + + if (!submodule_uses_gitfile(path)) { + /* + * We should be using a gitfile by now. Let's double + * check as losing the git dir would be fatal. + */ + die("BUG: could not absorb git directory for '%s'", path); + } + } + + strbuf_addstr(&pathbuf, path); + remove_directory_or_die(&pathbuf); + +out: + strbuf_release(&pathbuf); + free(dot_git); +} + /* Helper function to display the submodule header line prior to the full * summary output. If it can locate the submodule objects directory it will * attempt to lookup both the left and right commits and put them into the diff --git a/submodule.h b/submodule.h index 3ed3aa479a..516e377a12 100644 --- a/submodule.h +++ b/submodule.h @@ -53,6 +53,12 @@ extern void show_submodule_inline_diff(FILE *f, const char *path, const char *del, const char *add, const char *reset, const struct diff_options *opt); extern void set_config_fetch_recurse_submodules(int value); + +/* + * Removes a submodule from a given path. When the submodule contains its + * git directory instead of a gitlink, migrate that first into the superproject. + */ +extern void depopulate_submodule(const char *path); extern void check_for_new_submodule_commits(unsigned char new_sha1[20]); extern int fetch_populated_submodules(const struct argv_array *options, const char *prefix, int command_line_option, diff --git a/t/t3600-rm.sh b/t/t3600-rm.sh index bcbb680651..5aa6db584c 100755 --- a/t/t3600-rm.sh +++ b/t/t3600-rm.sh @@ -569,26 +569,22 @@ test_expect_success 'rm of a conflicted unpopulated submodule succeeds' ' test_cmp expect actual ' -test_expect_success 'rm of a populated submodule with a .git directory fails even when forced' ' +test_expect_success 'rm of a populated submodule with a .git directory migrates git dir' ' git checkout -f master && git reset --hard && git submodule update && (cd submod && rm .git && cp -R ../.git/modules/sub .git && - GIT_WORK_TREE=. git config --unset core.worktree + GIT_WORK_TREE=. git config --unset core.worktree && + rm -r ../.git/modules/sub ) && - test_must_fail git rm submod && - test -d submod && - test -d submod/.git && - git status -s -uno --ignore-submodules=none >actual && - ! test -s actual && - test_must_fail git rm -f submod && - test -d submod && - test -d submod/.git && + git rm submod 2>output.err && + ! test -d submod && + ! test -d submod/.git && git status -s -uno --ignore-submodules=none >actual && - ! test -s actual && - rm -rf submod + test -s actual && + test_i18ngrep Migrating output.err ' cat >expect.deepmodified <<EOF @@ -667,24 +663,19 @@ test_expect_success 'rm of a populated nested submodule with a nested .git direc git submodule update --recursive && (cd submod/subsubmod && rm .git && - cp -R ../../.git/modules/sub/modules/sub .git && + mv ../../.git/modules/sub/modules/sub .git && GIT_WORK_TREE=. git config --unset core.worktree ) && - test_must_fail git rm submod && - test -d submod && - test -d submod/subsubmod/.git && - git status -s -uno --ignore-submodules=none >actual && - ! test -s actual && - test_must_fail git rm -f submod && - test -d submod && - test -d submod/subsubmod/.git && + git rm submod 2>output.err && + ! test -d submod && + ! test -d submod/subsubmod/.git && git status -s -uno --ignore-submodules=none >actual && - ! test -s actual && - rm -rf submod + test -s actual && + test_i18ngrep Migrating output.err ' test_expect_success 'checking out a commit after submodule removal needs manual updates' ' - git commit -m "submodule removal" submod && + git commit -m "submodule removal" submod .gitmodules && git checkout HEAD^ && git submodule update && git checkout -q HEAD^ && -- 2.11.0.rc2.35.g26e18c9