Skip to content

Commit

Permalink
Merge branch 'jl/submodule-rm'
Browse files Browse the repository at this point in the history
"git rm submodule" cannot blindly remove a submodule directory as
its working tree may have local changes, and worse yet, it may even
have its repository embedded in it.  Teach it some special cases
where it is safe to remove a submodule, specifically, when there is
no local changes in the submodule working tree, and its repository
is not embedded in its working tree but is elsewhere and uses the
gitfile mechanism to point at it.

* jl/submodule-rm:
  submodule: teach rm to remove submodules unless they contain a git directory
  • Loading branch information
Jeff King committed Oct 29, 2012
2 parents 2cfceef + 293ab15 commit d21240f
Show file tree
Hide file tree
Showing 5 changed files with 550 additions and 15 deletions.
15 changes: 15 additions & 0 deletions Documentation/git-rm.txt
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,21 @@ as well as modifications of existing paths.
Typically you would first remove all tracked files from the working
tree using this command:

Submodules
~~~~~~~~~~
Only submodules using a gitfile (which means they were cloned
with a git version 1.7.8 or newer) will be removed from the work
tree, as their repository lives inside the .git directory of the
superproject. If a submodule (or one of those nested inside it)
still uses a .git directory, `git rm` will fail - no matter if forced
or not - to protect the submodule's history.

A submodule is considered up-to-date when 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.

----------------
git ls-files -z | xargs -0 rm -f
----------------
Expand Down
125 changes: 110 additions & 15 deletions builtin/rm.c
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include "cache-tree.h"
#include "tree-walk.h"
#include "parse-options.h"
#include "submodule.h"

static const char * const builtin_rm_usage[] = {
N_("git rm [options] [--] <file>..."),
Expand All @@ -17,9 +18,58 @@ static const char * const builtin_rm_usage[] = {

static struct {
int nr, alloc;
const char **name;
struct {
const char *name;
char is_submodule;
} *entry;
} list;

static int get_ours_cache_pos(const char *path, int pos)
{
int i = -pos - 1;

while ((i < active_nr) && !strcmp(active_cache[i]->name, path)) {
if (ce_stage(active_cache[i]) == 2)
return i;
i++;
}
return -1;
}

static int check_submodules_use_gitfiles(void)
{
int i;
int errs = 0;

for (i = 0; i < list.nr; i++) {
const char *name = list.entry[i].name;
int pos;
struct cache_entry *ce;
struct stat st;

pos = cache_name_pos(name, strlen(name));
if (pos < 0) {
pos = get_ours_cache_pos(name, pos);
if (pos < 0)
continue;
}
ce = active_cache[pos];

if (!S_ISGITLINK(ce->ce_mode) ||
(lstat(ce->name, &st) < 0) ||
is_empty_dir(name))
continue;

if (!submodule_uses_gitfile(name))
errs = error(_("submodule '%s' (or one of its nested "
"submodules) uses a .git directory\n"
"(use 'rm -rf' if you really want to remove "
"it including all of its history)"), name);
}

return errs;
}

static int check_local_mod(unsigned char *head, int index_only)
{
/*
Expand All @@ -37,15 +87,26 @@ static int check_local_mod(unsigned char *head, int index_only)
struct stat st;
int pos;
struct cache_entry *ce;
const char *name = list.name[i];
const char *name = list.entry[i].name;
unsigned char sha1[20];
unsigned mode;
int local_changes = 0;
int staged_changes = 0;

pos = cache_name_pos(name, strlen(name));
if (pos < 0)
continue; /* removing unmerged entry */
if (pos < 0) {
/*
* Skip unmerged entries except for populated submodules
* that could lose history when removed.
*/
pos = get_ours_cache_pos(name, pos);
if (pos < 0)
continue;

if (!S_ISGITLINK(active_cache[pos]->ce_mode) ||
is_empty_dir(name))
continue;
}
ce = active_cache[pos];

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

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

/*
* Is the index different from the file in the work tree?
* If it's a submodule, is its work tree modified?
*/
if (ce_match_stat(ce, &st, 0))
if (ce_match_stat(ce, &st, 0) ||
(S_ISGITLINK(ce->ce_mode) &&
!ok_to_remove_submodule(ce->name)))
local_changes = 1;

/*
Expand Down Expand Up @@ -115,10 +180,18 @@ static int check_local_mod(unsigned char *head, int index_only)
errs = error(_("'%s' has changes staged in the index\n"
"(use --cached to keep the file, "
"or -f to force removal)"), name);
if (local_changes)
errs = error(_("'%s' has local modifications\n"
"(use --cached to keep the file, "
"or -f to force removal)"), name);
if (local_changes) {
if (S_ISGITLINK(ce->ce_mode) &&
!submodule_uses_gitfile(name)) {
errs = error(_("submodule '%s' (or one of its nested "
"submodules) uses a .git directory\n"
"(use 'rm -rf' if you really want to remove "
"it including all of its history)"), name);
} else
errs = error(_("'%s' has local modifications\n"
"(use --cached to keep the file, "
"or -f to force removal)"), name);
}
}
}
return errs;
Expand Down Expand Up @@ -173,8 +246,9 @@ int cmd_rm(int argc, const char **argv, const char *prefix)
struct cache_entry *ce = active_cache[i];
if (!match_pathspec(pathspec, ce->name, ce_namelen(ce), 0, seen))
continue;
ALLOC_GROW(list.name, list.nr + 1, list.alloc);
list.name[list.nr++] = ce->name;
ALLOC_GROW(list.entry, list.nr + 1, list.alloc);
list.entry[list.nr].name = ce->name;
list.entry[list.nr++].is_submodule = S_ISGITLINK(ce->ce_mode);
}

if (pathspec) {
Expand Down Expand Up @@ -215,14 +289,17 @@ int cmd_rm(int argc, const char **argv, const char *prefix)
hashclr(sha1);
if (check_local_mod(sha1, index_only))
exit(1);
} else if (!index_only) {
if (check_submodules_use_gitfiles())
exit(1);
}

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

Expand All @@ -244,7 +321,25 @@ int cmd_rm(int argc, const char **argv, const char *prefix)
if (!index_only) {
int removed = 0;
for (i = 0; i < list.nr; i++) {
const char *path = list.name[i];
const char *path = list.entry[i].name;
if (list.entry[i].is_submodule) {
if (is_empty_dir(path)) {
if (!rmdir(path)) {
removed = 1;
continue;
}
} else {
struct strbuf buf = STRBUF_INIT;
strbuf_addstr(&buf, path);
if (!remove_dir_recursively(&buf, 0)) {
removed = 1;
strbuf_release(&buf);
continue;
}
strbuf_release(&buf);
/* Fallthrough and let remove_path() fail. */
}
}
if (!remove_path(path)) {
removed = 1;
continue;
Expand Down
80 changes: 80 additions & 0 deletions submodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -759,6 +759,86 @@ unsigned is_submodule_modified(const char *path, int ignore_untracked)
return dirty_submodule;
}

int submodule_uses_gitfile(const char *path)
{
struct child_process cp;
const char *argv[] = {
"submodule",
"foreach",
"--quiet",
"--recursive",
"test -f .git",
NULL,
};
struct strbuf buf = STRBUF_INIT;
const char *git_dir;

strbuf_addf(&buf, "%s/.git", path);
git_dir = read_gitfile(buf.buf);
if (!git_dir) {
strbuf_release(&buf);
return 0;
}
strbuf_release(&buf);

/* Now test that all nested submodules use a gitfile too */
memset(&cp, 0, sizeof(cp));
cp.argv = argv;
cp.env = local_repo_env;
cp.git_cmd = 1;
cp.no_stdin = 1;
cp.no_stderr = 1;
cp.no_stdout = 1;
cp.dir = path;
if (run_command(&cp))
return 0;

return 1;
}

int ok_to_remove_submodule(const char *path)
{
struct stat st;
ssize_t len;
struct child_process cp;
const char *argv[] = {
"status",
"--porcelain",
"-u",
"--ignore-submodules=none",
NULL,
};
struct strbuf buf = STRBUF_INIT;
int ok_to_remove = 1;

if ((lstat(path, &st) < 0) || is_empty_dir(path))
return 1;

if (!submodule_uses_gitfile(path))
return 0;

memset(&cp, 0, sizeof(cp));
cp.argv = argv;
cp.env = local_repo_env;
cp.git_cmd = 1;
cp.no_stdin = 1;
cp.out = -1;
cp.dir = path;
if (start_command(&cp))
die("Could not run 'git status --porcelain -uall --ignore-submodules=none' in submodule %s", path);

len = strbuf_read(&buf, cp.out, 1024);
if (len > 2)
ok_to_remove = 0;
close(cp.out);

if (finish_command(&cp))
die("'git status --porcelain -uall --ignore-submodules=none' failed in submodule %s", path);

strbuf_release(&buf);
return ok_to_remove;
}

static int find_first_merges(struct object_array *result, const char *path,
struct commit *a, struct commit *b)
{
Expand Down
2 changes: 2 additions & 0 deletions submodule.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ int fetch_populated_submodules(const struct argv_array *options,
const char *prefix, int command_line_option,
int quiet);
unsigned is_submodule_modified(const char *path, int ignore_untracked);
int submodule_uses_gitfile(const char *path);
int ok_to_remove_submodule(const char *path);
int merge_submodule(unsigned char result[20], const char *path, const unsigned char base[20],
const unsigned char a[20], const unsigned char b[20], int search);
int find_unpushed_submodules(unsigned char new_sha1[20], const char *remotes_name,
Expand Down
Loading

0 comments on commit d21240f

Please sign in to comment.