submodule: teach rm to remove submodules unless they contain a git directory

Currently using "git rm" on a submodule - populated or not - fails with this error: fatal: git rm: '<submodule path>': Is a directory This made sense in the past as there was no way to remove a submodule without possibly removing unpushed parts of the submodule's history contained in its .git directory too, so erroring out here protected the user from possible loss of data. But submodules cloned with a recent git version do not contain the .git directory anymore, they use a gitfile to point to their git directory which is safely stored inside the superproject's .git directory. The work tree of these submodules can safely be removed without losing history, so let's teach git to do so. Using rm on an unpopulated submodule now removes the empty directory from the work tree and the gitlink from the index. If the submodule's directory is missing from the work tree, it will still be removed from the index. Using rm on a populated submodule using a gitfile will apply the usual checks for work tree modification adapted to submodules (unless forced). For a submodule that means that the HEAD is the same as recorded in the index, no tracked files are modified and no untracked files that aren't ignored are present in the submodules work tree (ignored files are deemed expendable and won't stop a submodule's work tree from being removed). That logic has to be applied in all nested submodules too. Using rm on a submodule which has its .git directory inside the work trees top level directory will just error out like it did before to protect the repository, even when forced. In the future git could either provide a message informing the user to convert the submodule to use a gitfile or even attempt to do the conversion itself, but that is not part of this change. Signed-off-by: Jens Lehmann <Jens.Lehmann@web.de> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Jens Lehmann committed Sep 26, 2012 at 20:21 UTC 293ab15eea341ffe8705bac99136f2e3a286db5f
5 files changed +550 -15
Documentation/git-rm.txt
+15
@@ -107,6 +107,21 @@ as well as modifications of existing paths.
107 Typically you would first remove all tracked files from the working
108 tree using this command:
109
110 +Submodules
111 +~~~~~~~~~~
112 +Only submodules using a gitfile (which means they were cloned
113 +with a git version 1.7.8 or newer) will be removed from the work
114 +tree, as their repository lives inside the .git directory of the
115 +superproject. If a submodule (or one of those nested inside it)
116 +still uses a .git directory, `git rm` will fail - no matter if forced
117 +or not - to protect the submodule's history.
118 +
119 +A submodule is considered up-to-date when the HEAD is the same as
120 +recorded in the index, no tracked files are modified and no untracked
121 +files that aren't ignored are present in the submodules work tree.
122 +Ignored files are deemed expendable and won't stop a submodule's work
123 +tree from being removed.
124 +
125 ----------------
126 git ls-files -z | xargs -0 rm -f
127 ----------------
builtin/rm.c
+110 -15
@@ -9,6 +9,7 @@
9 #include "cache-tree.h"
10 #include "tree-walk.h"
11 #include "parse-options.h"
12 +#include "submodule.h"
13
14 static const char * const builtin_rm_usage[] = {
15 "git rm [options] [--] <file>...",
@@ -17,9 +18,58 @@ static const char * const builtin_rm_usage[] = {
18
19 static struct {
20 int nr, alloc;
20 - const char **name;
21 + struct {
22 + const char *name;
23 + char is_submodule;
24 + } *entry;
25 } list;
26
27 +static int get_ours_cache_pos(const char *path, int pos)
28 +{
29 + int i = -pos - 1;
30 +
31 + while ((i < active_nr) && !strcmp(active_cache[i]->name, path)) {
32 + if (ce_stage(active_cache[i]) == 2)
33 + return i;
34 + i++;
35 + }
36 + return -1;
37 +}
38 +
39 +static int check_submodules_use_gitfiles(void)
40 +{
41 + int i;
42 + int errs = 0;
43 +
44 + for (i = 0; i < list.nr; i++) {
45 + const char *name = list.entry[i].name;
46 + int pos;
47 + struct cache_entry *ce;
48 + struct stat st;
49 +
50 + pos = cache_name_pos(name, strlen(name));
51 + if (pos < 0) {
52 + pos = get_ours_cache_pos(name, pos);
53 + if (pos < 0)
54 + continue;
55 + }
56 + ce = active_cache[pos];
57 +
58 + if (!S_ISGITLINK(ce->ce_mode) ||
59 + (lstat(ce->name, &st) < 0) ||
60 + is_empty_dir(name))
61 + continue;
62 +
63 + if (!submodule_uses_gitfile(name))
64 + errs = error(_("submodule '%s' (or one of its nested "
65 + "submodules) uses a .git directory\n"
66 + "(use 'rm -rf' if you really want to remove "
67 + "it including all of its history)"), name);
68 + }
69 +
70 + return errs;
71 +}
72 +
73 static int check_local_mod(unsigned char *head, int index_only)
74 {
75 /*
@@ -37,15 +87,26 @@ static int check_local_mod(unsigned char *head, int index_only)
87 struct stat st;
88 int pos;
89 struct cache_entry *ce;
40 - const char *name = list.name[i];
90 + const char *name = list.entry[i].name;
91 unsigned char sha1[20];
92 unsigned mode;
93 int local_changes = 0;
94 int staged_changes = 0;
95
96 pos = cache_name_pos(name, strlen(name));
47 - if (pos < 0)
48 - continue; /* removing unmerged entry */
97 + if (pos < 0) {
98 + /*
99 + * Skip unmerged entries except for populated submodules
100 + * that could lose history when removed.
101 + */
102 + pos = get_ours_cache_pos(name, pos);
103 + if (pos < 0)
104 + continue;
105 +
106 + if (!S_ISGITLINK(active_cache[pos]->ce_mode) ||
107 + is_empty_dir(name))
108 + continue;
109 + }
110 ce = active_cache[pos];
111
112 if (lstat(ce->name, &st) < 0) {
@@ -58,9 +119,10 @@ static int check_local_mod(unsigned char *head, int index_only)
119 /* if a file was removed and it is now a
120 * directory, that is the same as ENOENT as
121 * far as git is concerned; we do not track
61 - * directories.
122 + * directories unless they are submodules.
123 */
63 - continue;
124 + if (!S_ISGITLINK(ce->ce_mode))
125 + continue;
126 }
127
128 /*
@@ -80,8 +142,11 @@ static int check_local_mod(unsigned char *head, int index_only)
142
143 /*
144 * Is the index different from the file in the work tree?
145 + * If it's a submodule, is its work tree modified?
146 */
84 - if (ce_match_stat(ce, &st, 0))
147 + if (ce_match_stat(ce, &st, 0) ||
148 + (S_ISGITLINK(ce->ce_mode) &&
149 + !ok_to_remove_submodule(ce->name)))
150 local_changes = 1;
151
152 /*
@@ -115,10 +180,18 @@ static int check_local_mod(unsigned char *head, int index_only)
180 errs = error(_("'%s' has changes staged in the index\n"
181 "(use --cached to keep the file, "
182 "or -f to force removal)"), name);
118 - if (local_changes)
119 - errs = error(_("'%s' has local modifications\n"
120 - "(use --cached to keep the file, "
121 - "or -f to force removal)"), name);
183 + if (local_changes) {
184 + if (S_ISGITLINK(ce->ce_mode) &&
185 + !submodule_uses_gitfile(name)) {
186 + errs = error(_("submodule '%s' (or one of its nested "
187 + "submodules) uses a .git directory\n"
188 + "(use 'rm -rf' if you really want to remove "
189 + "it including all of its history)"), name);
190 + } else
191 + errs = error(_("'%s' has local modifications\n"
192 + "(use --cached to keep the file, "
193 + "or -f to force removal)"), name);
194 + }
195 }
196 }
197 return errs;
@@ -173,8 +246,9 @@ int cmd_rm(int argc, const char **argv, const char *prefix)
246 struct cache_entry *ce = active_cache[i];
247 if (!match_pathspec(pathspec, ce->name, ce_namelen(ce), 0, seen))
248 continue;
176 - ALLOC_GROW(list.name, list.nr + 1, list.alloc);
177 - list.name[list.nr++] = ce->name;
249 + ALLOC_GROW(list.entry, list.nr + 1, list.alloc);
250 + list.entry[list.nr].name = ce->name;
251 + list.entry[list.nr++].is_submodule = S_ISGITLINK(ce->ce_mode);
252 }
253
254 if (pathspec) {
@@ -215,6 +289,9 @@ int cmd_rm(int argc, const char **argv, const char *prefix)
289 hashclr(sha1);
290 if (check_local_mod(sha1, index_only))
291 exit(1);
292 + } else if (!index_only) {
293 + if (check_submodules_use_gitfiles())
294 + exit(1);
295 }
296
297 /*
@@ -222,7 +299,7 @@ int cmd_rm(int argc, const char **argv, const char *prefix)
299 * the index unless all of them succeed.
300 */
301 for (i = 0; i < list.nr; i++) {
225 - const char *path = list.name[i];
302 + const char *path = list.entry[i].name;
303 if (!quiet)
304 printf("rm '%s'\n", path);
305
@@ -244,7 +321,25 @@ int cmd_rm(int argc, const char **argv, const char *prefix)
321 if (!index_only) {
322 int removed = 0;
323 for (i = 0; i < list.nr; i++) {
247 - const char *path = list.name[i];
324 + const char *path = list.entry[i].name;
325 + if (list.entry[i].is_submodule) {
326 + if (is_empty_dir(path)) {
327 + if (!rmdir(path)) {
328 + removed = 1;
329 + continue;
330 + }
331 + } else {
332 + struct strbuf buf = STRBUF_INIT;
333 + strbuf_addstr(&buf, path);
334 + if (!remove_dir_recursively(&buf, 0)) {
335 + removed = 1;
336 + strbuf_release(&buf);
337 + continue;
338 + }
339 + strbuf_release(&buf);
340 + /* Fallthrough and let remove_path() fail. */
341 + }
342 + }
343 if (!remove_path(path)) {
344 removed = 1;
345 continue;
submodule.c
+80
@@ -758,6 +758,86 @@ unsigned is_submodule_modified(const char *path, int ignore_untracked)
758 return dirty_submodule;
759 }
760
761 +int submodule_uses_gitfile(const char *path)
762 +{
763 + struct child_process cp;
764 + const char *argv[] = {
765 + "submodule",
766 + "foreach",
767 + "--quiet",
768 + "--recursive",
769 + "test -f .git",
770 + NULL,
771 + };
772 + struct strbuf buf = STRBUF_INIT;
773 + const char *git_dir;
774 +
775 + strbuf_addf(&buf, "%s/.git", path);
776 + git_dir = read_gitfile(buf.buf);
777 + if (!git_dir) {
778 + strbuf_release(&buf);
779 + return 0;
780 + }
781 + strbuf_release(&buf);
782 +
783 + /* Now test that all nested submodules use a gitfile too */
784 + memset(&cp, 0, sizeof(cp));
785 + cp.argv = argv;
786 + cp.env = local_repo_env;
787 + cp.git_cmd = 1;
788 + cp.no_stdin = 1;
789 + cp.no_stderr = 1;
790 + cp.no_stdout = 1;
791 + cp.dir = path;
792 + if (run_command(&cp))
793 + return 0;
794 +
795 + return 1;
796 +}
797 +
798 +int ok_to_remove_submodule(const char *path)
799 +{
800 + struct stat st;
801 + ssize_t len;
802 + struct child_process cp;
803 + const char *argv[] = {
804 + "status",
805 + "--porcelain",
806 + "-u",
807 + "--ignore-submodules=none",
808 + NULL,
809 + };
810 + struct strbuf buf = STRBUF_INIT;
811 + int ok_to_remove = 1;
812 +
813 + if ((lstat(path, &st) < 0) || is_empty_dir(path))
814 + return 1;
815 +
816 + if (!submodule_uses_gitfile(path))
817 + return 0;
818 +
819 + memset(&cp, 0, sizeof(cp));
820 + cp.argv = argv;
821 + cp.env = local_repo_env;
822 + cp.git_cmd = 1;
823 + cp.no_stdin = 1;
824 + cp.out = -1;
825 + cp.dir = path;
826 + if (start_command(&cp))
827 + die("Could not run 'git status --porcelain -uall --ignore-submodules=none' in submodule %s", path);
828 +
829 + len = strbuf_read(&buf, cp.out, 1024);
830 + if (len > 2)
831 + ok_to_remove = 0;
832 + close(cp.out);
833 +
834 + if (finish_command(&cp))
835 + die("'git status --porcelain -uall --ignore-submodules=none' failed in submodule %s", path);
836 +
837 + strbuf_release(&buf);
838 + return ok_to_remove;
839 +}
840 +
841 static int find_first_merges(struct object_array *result, const char *path,
842 struct commit *a, struct commit *b)
843 {
submodule.h
+2
@@ -27,6 +27,8 @@ int fetch_populated_submodules(int num_options, const char **options,
27 const char *prefix, int command_line_option,
28 int quiet);
29 unsigned is_submodule_modified(const char *path, int ignore_untracked);
30 +int submodule_uses_gitfile(const char *path);
31 +int ok_to_remove_submodule(const char *path);
32 int merge_submodule(unsigned char result[20], const char *path, const unsigned char base[20],
33 const unsigned char a[20], const unsigned char b[20], int search);
34 int find_unpushed_submodules(unsigned char new_sha1[20], const char *remotes_name,
t/t3600-rm.sh
+343
@@ -262,4 +262,347 @@ test_expect_success 'rm removes subdirectories recursively' '
262 ! test -d dir
263 '
264
265 +cat >expect <<EOF
266 +D submod
267 +EOF
268 +
269 +cat >expect.modified <<EOF
270 + M submod
271 +EOF
272 +
273 +test_expect_success 'rm removes empty submodules from work tree' '
274 + mkdir submod &&
275 + git update-index --add --cacheinfo 160000 $(git rev-parse HEAD) submod &&
276 + git config -f .gitmodules submodule.sub.url ./. &&
277 + git config -f .gitmodules submodule.sub.path submod &&
278 + git submodule init &&
279 + git add .gitmodules &&
280 + git commit -m "add submodule" &&
281 + git rm submod &&
282 + test ! -e submod &&
283 + git status -s -uno --ignore-submodules=none > actual &&
284 + test_cmp expect actual
285 +'
286 +
287 +test_expect_success 'rm removes removed submodule from index' '
288 + git reset --hard &&
289 + git submodule update &&
290 + rm -rf submod &&
291 + git rm submod &&
292 + git status -s -uno --ignore-submodules=none > actual &&
293 + test_cmp expect actual
294 +'
295 +
296 +test_expect_success 'rm removes work tree of unmodified submodules' '
297 + git reset --hard &&
298 + git submodule update &&
299 + git rm submod &&
300 + test ! -d submod &&
301 + git status -s -uno --ignore-submodules=none > actual &&
302 + test_cmp expect actual
303 +'
304 +
305 +test_expect_success 'rm of a populated submodule with different HEAD fails unless forced' '
306 + git reset --hard &&
307 + git submodule update &&
308 + (cd submod &&
309 + git checkout HEAD^
310 + ) &&
311 + test_must_fail git rm submod &&
312 + test -d submod &&
313 + test -f submod/.git &&
314 + git status -s -uno --ignore-submodules=none > actual &&
315 + test_cmp expect.modified actual &&
316 + git rm -f submod &&
317 + test ! -d submod &&
318 + git status -s -uno --ignore-submodules=none > actual &&
319 + test_cmp expect actual
320 +'
321 +
322 +test_expect_success 'rm of a populated submodule with modifications fails unless forced' '
323 + git reset --hard &&
324 + git submodule update &&
325 + (cd submod &&
326 + echo X >empty
327 + ) &&
328 + test_must_fail git rm submod &&
329 + test -d submod &&
330 + test -f submod/.git &&
331 + git status -s -uno --ignore-submodules=none > actual &&
332 + test_cmp expect.modified actual &&
333 + git rm -f submod &&
334 + test ! -d submod &&
335 + git status -s -uno --ignore-submodules=none > actual &&
336 + test_cmp expect actual
337 +'
338 +
339 +test_expect_success 'rm of a populated submodule with untracked files fails unless forced' '
340 + git reset --hard &&
341 + git submodule update &&
342 + (cd submod &&
343 + echo X >untracked
344 + ) &&
345 + test_must_fail git rm submod &&
346 + test -d submod &&
347 + test -f submod/.git &&
348 + git status -s -uno --ignore-submodules=none > actual &&
349 + test_cmp expect.modified actual &&
350 + git rm -f submod &&
351 + test ! -d submod &&
352 + git status -s -uno --ignore-submodules=none > actual &&
353 + test_cmp expect actual
354 +'
355 +
356 +test_expect_success 'setup submodule conflict' '
357 + git reset --hard &&
358 + git submodule update &&
359 + git checkout -b branch1 &&
360 + echo 1 >nitfol &&
361 + git add nitfol &&
362 + git commit -m "added nitfol 1" &&
363 + git checkout -b branch2 master &&
364 + echo 2 >nitfol &&
365 + git add nitfol &&
366 + git commit -m "added nitfol 2" &&
367 + git checkout -b conflict1 master &&
368 + (cd submod &&
369 + git fetch &&
370 + git checkout branch1
371 + ) &&
372 + git add submod &&
373 + git commit -m "submod 1" &&
374 + git checkout -b conflict2 master &&
375 + (cd submod &&
376 + git checkout branch2
377 + ) &&
378 + git add submod &&
379 + git commit -m "submod 2"
380 +'
381 +
382 +cat >expect.conflict <<EOF
383 +UU submod
384 +EOF
385 +
386 +test_expect_success 'rm removes work tree of unmodified conflicted submodule' '
387 + git checkout conflict1 &&
388 + git reset --hard &&
389 + git submodule update &&
390 + test_must_fail git merge conflict2 &&
391 + git rm submod &&
392 + test ! -d submod &&
393 + git status -s -uno --ignore-submodules=none > actual &&
394 + test_cmp expect actual
395 +'
396 +
397 +test_expect_success 'rm of a conflicted populated submodule with different HEAD fails unless forced' '
398 + git checkout conflict1 &&
399 + git reset --hard &&
400 + git submodule update &&
401 + (cd submod &&
402 + git checkout HEAD^
403 + ) &&
404 + test_must_fail git merge conflict2 &&
405 + test_must_fail git rm submod &&
406 + test -d submod &&
407 + test -f submod/.git &&
408 + git status -s -uno --ignore-submodules=none > actual &&
409 + test_cmp expect.conflict actual &&
410 + git rm -f submod &&
411 + test ! -d submod &&
412 + git status -s -uno --ignore-submodules=none > actual &&
413 + test_cmp expect actual
414 +'
415 +
416 +test_expect_success 'rm of a conflicted populated submodule with modifications fails unless forced' '
417 + git checkout conflict1 &&
418 + git reset --hard &&
419 + git submodule update &&
420 + (cd submod &&
421 + echo X >empty
422 + ) &&
423 + test_must_fail git merge conflict2 &&
424 + test_must_fail git rm submod &&
425 + test -d submod &&
426 + test -f submod/.git &&
427 + git status -s -uno --ignore-submodules=none > actual &&
428 + test_cmp expect.conflict actual &&
429 + git rm -f submod &&
430 + test ! -d submod &&
431 + git status -s -uno --ignore-submodules=none > actual &&
432 + test_cmp expect actual
433 +'
434 +
435 +test_expect_success 'rm of a conflicted populated submodule with untracked files fails unless forced' '
436 + git checkout conflict1 &&
437 + git reset --hard &&
438 + git submodule update &&
439 + (cd submod &&
440 + echo X >untracked
441 + ) &&
442 + test_must_fail git merge conflict2 &&
443 + test_must_fail git rm submod &&
444 + test -d submod &&
445 + test -f submod/.git &&
446 + git status -s -uno --ignore-submodules=none > actual &&
447 + test_cmp expect.conflict actual &&
448 + git rm -f submod &&
449 + test ! -d submod &&
450 + git status -s -uno --ignore-submodules=none > actual &&
451 + test_cmp expect actual
452 +'
453 +
454 +test_expect_success 'rm of a conflicted populated submodule with a .git directory fails even when forced' '
455 + git checkout conflict1 &&
456 + git reset --hard &&
457 + git submodule update &&
458 + (cd submod &&
459 + rm .git &&
460 + cp -a ../.git/modules/sub .git &&
461 + GIT_WORK_TREE=. git config --unset core.worktree
462 + ) &&
463 + test_must_fail git merge conflict2 &&
464 + test_must_fail git rm submod &&
465 + test -d submod &&
466 + test -d submod/.git &&
467 + git status -s -uno --ignore-submodules=none > actual &&
468 + test_cmp expect.conflict actual &&
469 + test_must_fail git rm -f submod &&
470 + test -d submod &&
471 + test -d submod/.git &&
472 + git status -s -uno --ignore-submodules=none > actual &&
473 + test_cmp expect.conflict actual &&
474 + git merge --abort &&
475 + rm -rf submod
476 +'
477 +
478 +test_expect_success 'rm of a conflicted unpopulated submodule succeeds' '
479 + git checkout conflict1 &&
480 + git reset --hard &&
481 + test_must_fail git merge conflict2 &&
482 + git rm submod &&
483 + test ! -d submod &&
484 + git status -s -uno --ignore-submodules=none > actual &&
485 + test_cmp expect actual
486 +'
487 +
488 +test_expect_success 'rm of a populated submodule with a .git directory fails even when forced' '
489 + git checkout -f master &&
490 + git reset --hard &&
491 + git submodule update &&
492 + (cd submod &&
493 + rm .git &&
494 + cp -a ../.git/modules/sub .git &&
495 + GIT_WORK_TREE=. git config --unset core.worktree
496 + ) &&
497 + test_must_fail git rm submod &&
498 + test -d submod &&
499 + test -d submod/.git &&
500 + git status -s -uno --ignore-submodules=none > actual &&
501 + ! test -s actual &&
502 + test_must_fail git rm -f submod &&
503 + test -d submod &&
504 + test -d submod/.git &&
505 + git status -s -uno --ignore-submodules=none > actual &&
506 + ! test -s actual &&
507 + rm -rf submod
508 +'
509 +
510 +cat >expect.deepmodified <<EOF
511 + M submod/subsubmod
512 +EOF
513 +
514 +test_expect_success 'setup subsubmodule' '
515 + git reset --hard &&
516 + git submodule update &&
517 + (cd submod &&
518 + git update-index --add --cacheinfo 160000 $(git rev-parse HEAD) subsubmod &&
519 + git config -f .gitmodules submodule.sub.url ../. &&
520 + git config -f .gitmodules submodule.sub.path subsubmod &&
521 + git submodule init &&
522 + git add .gitmodules &&
523 + git commit -m "add subsubmodule" &&
524 + git submodule update subsubmod
525 + ) &&
526 + git commit -a -m "added deep submodule"
527 +'
528 +
529 +test_expect_success 'rm recursively removes work tree of unmodified submodules' '
530 + git rm submod &&
531 + test ! -d submod &&
532 + git status -s -uno --ignore-submodules=none > actual &&
533 + test_cmp expect actual
534 +'
535 +
536 +test_expect_success 'rm of a populated nested submodule with different nested HEAD fails unless forced' '
537 + git reset --hard &&
538 + git submodule update --recursive &&
539 + (cd submod/subsubmod &&
540 + git checkout HEAD^
541 + ) &&
542 + test_must_fail git rm submod &&
543 + test -d submod &&
544 + test -f submod/.git &&
545 + git status -s -uno --ignore-submodules=none > actual &&
546 + test_cmp expect.modified actual &&
547 + git rm -f submod &&
548 + test ! -d submod &&
549 + git status -s -uno --ignore-submodules=none > actual &&
550 + test_cmp expect actual
551 +'
552 +
553 +test_expect_success 'rm of a populated nested submodule with nested modifications fails unless forced' '
554 + git reset --hard &&
555 + git submodule update --recursive &&
556 + (cd submod/subsubmod &&
557 + echo X >empty
558 + ) &&
559 + test_must_fail git rm submod &&
560 + test -d submod &&
561 + test -f submod/.git &&
562 + git status -s -uno --ignore-submodules=none > actual &&
563 + test_cmp expect.modified actual &&
564 + git rm -f submod &&
565 + test ! -d submod &&
566 + git status -s -uno --ignore-submodules=none > actual &&
567 + test_cmp expect actual
568 +'
569 +
570 +test_expect_success 'rm of a populated nested submodule with nested untracked files fails unless forced' '
571 + git reset --hard &&
572 + git submodule update --recursive &&
573 + (cd submod/subsubmod &&
574 + echo X >untracked
575 + ) &&
576 + test_must_fail git rm submod &&
577 + test -d submod &&
578 + test -f submod/.git &&
579 + git status -s -uno --ignore-submodules=none > actual &&
580 + test_cmp expect.modified actual &&
581 + git rm -f submod &&
582 + test ! -d submod &&
583 + git status -s -uno --ignore-submodules=none > actual &&
584 + test_cmp expect actual
585 +'
586 +
587 +test_expect_success 'rm of a populated nested submodule with a nested .git directory fails even when forced' '
588 + git reset --hard &&
589 + git submodule update --recursive &&
590 + (cd submod/subsubmod &&
591 + rm .git &&
592 + cp -a ../../.git/modules/sub/modules/sub .git &&
593 + GIT_WORK_TREE=. git config --unset core.worktree
594 + ) &&
595 + test_must_fail git rm submod &&
596 + test -d submod &&
597 + test -d submod/subsubmod/.git &&
598 + git status -s -uno --ignore-submodules=none > actual &&
599 + ! test -s actual &&
600 + test_must_fail git rm -f submod &&
601 + test -d submod &&
602 + test -d submod/subsubmod/.git &&
603 + git status -s -uno --ignore-submodules=none > actual &&
604 + ! test -s actual &&
605 + rm -rf submod
606 +'
607 +
608 test_done