Skip to content

Commit 293ab15

Browse files
jlehmanngitster
authored andcommitted
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 <[email protected]> Signed-off-by: Junio C Hamano <[email protected]>
1 parent 31e0100 commit 293ab15

File tree

5 files changed

+550
-15
lines changed

5 files changed

+550
-15
lines changed

Documentation/git-rm.txt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,21 @@ as well as modifications of existing paths.
107107
Typically you would first remove all tracked files from the working
108108
tree using this command:
109109

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+
110125
----------------
111126
git ls-files -z | xargs -0 rm -f
112127
----------------

builtin/rm.c

Lines changed: 110 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#include "cache-tree.h"
1010
#include "tree-walk.h"
1111
#include "parse-options.h"
12+
#include "submodule.h"
1213

1314
static const char * const builtin_rm_usage[] = {
1415
"git rm [options] [--] <file>...",
@@ -17,9 +18,58 @@ static const char * const builtin_rm_usage[] = {
1718

1819
static struct {
1920
int nr, alloc;
20-
const char **name;
21+
struct {
22+
const char *name;
23+
char is_submodule;
24+
} *entry;
2125
} list;
2226

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+
2373
static int check_local_mod(unsigned char *head, int index_only)
2474
{
2575
/*
@@ -37,15 +87,26 @@ static int check_local_mod(unsigned char *head, int index_only)
3787
struct stat st;
3888
int pos;
3989
struct cache_entry *ce;
40-
const char *name = list.name[i];
90+
const char *name = list.entry[i].name;
4191
unsigned char sha1[20];
4292
unsigned mode;
4393
int local_changes = 0;
4494
int staged_changes = 0;
4595

4696
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+
}
49110
ce = active_cache[pos];
50111

51112
if (lstat(ce->name, &st) < 0) {
@@ -58,9 +119,10 @@ static int check_local_mod(unsigned char *head, int index_only)
58119
/* if a file was removed and it is now a
59120
* directory, that is the same as ENOENT as
60121
* far as git is concerned; we do not track
61-
* directories.
122+
* directories unless they are submodules.
62123
*/
63-
continue;
124+
if (!S_ISGITLINK(ce->ce_mode))
125+
continue;
64126
}
65127

66128
/*
@@ -80,8 +142,11 @@ static int check_local_mod(unsigned char *head, int index_only)
80142

81143
/*
82144
* Is the index different from the file in the work tree?
145+
* If it's a submodule, is its work tree modified?
83146
*/
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)))
85150
local_changes = 1;
86151

87152
/*
@@ -115,10 +180,18 @@ static int check_local_mod(unsigned char *head, int index_only)
115180
errs = error(_("'%s' has changes staged in the index\n"
116181
"(use --cached to keep the file, "
117182
"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+
}
122195
}
123196
}
124197
return errs;
@@ -173,8 +246,9 @@ int cmd_rm(int argc, const char **argv, const char *prefix)
173246
struct cache_entry *ce = active_cache[i];
174247
if (!match_pathspec(pathspec, ce->name, ce_namelen(ce), 0, seen))
175248
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);
178252
}
179253

180254
if (pathspec) {
@@ -215,14 +289,17 @@ int cmd_rm(int argc, const char **argv, const char *prefix)
215289
hashclr(sha1);
216290
if (check_local_mod(sha1, index_only))
217291
exit(1);
292+
} else if (!index_only) {
293+
if (check_submodules_use_gitfiles())
294+
exit(1);
218295
}
219296

220297
/*
221298
* First remove the names from the index: we won't commit
222299
* the index unless all of them succeed.
223300
*/
224301
for (i = 0; i < list.nr; i++) {
225-
const char *path = list.name[i];
302+
const char *path = list.entry[i].name;
226303
if (!quiet)
227304
printf("rm '%s'\n", path);
228305

@@ -244,7 +321,25 @@ int cmd_rm(int argc, const char **argv, const char *prefix)
244321
if (!index_only) {
245322
int removed = 0;
246323
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+
}
248343
if (!remove_path(path)) {
249344
removed = 1;
250345
continue;

submodule.c

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -758,6 +758,86 @@ unsigned is_submodule_modified(const char *path, int ignore_untracked)
758758
return dirty_submodule;
759759
}
760760

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+
761841
static int find_first_merges(struct object_array *result, const char *path,
762842
struct commit *a, struct commit *b)
763843
{

submodule.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ int fetch_populated_submodules(int num_options, const char **options,
2727
const char *prefix, int command_line_option,
2828
int quiet);
2929
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);
3032
int merge_submodule(unsigned char result[20], const char *path, const unsigned char base[20],
3133
const unsigned char a[20], const unsigned char b[20], int search);
3234
int find_unpushed_submodules(unsigned char new_sha1[20], const char *remotes_name,

0 commit comments

Comments
 (0)