Merge branch 'sb/submodule-rm-absorb'
authorJunio C Hamano <gitster@pobox.com>
Wed, 18 Jan 2017 23:12:11 +0000 (15:12 -0800)
committerJunio C Hamano <gitster@pobox.com>
Wed, 18 Jan 2017 23:12:11 +0000 (15:12 -0800)
"git rm" used to refuse to remove a submodule when it has its own
git repository embedded in its working tree. It learned to move
the repository away to $GIT_DIR/modules/ of the superproject
instead, and allow the submodule to be deleted (as long as there
will be no loss of local modifications, that is).

* sb/submodule-rm-absorb:
rm: absorb a submodules git dir before deletion
submodule: rename and add flags to ok_to_remove_submodule
submodule: modernize ok_to_remove_submodule to use argv_array
submodule.h: add extern keyword to functions

1  2 
builtin/rm.c
submodule.c
submodule.h
t/t3600-rm.sh
diff --combined builtin/rm.c
index 7f15a3d7f82a7b610dcb8c6c9f713b485614e2f7,20635dca94e874c812461f3aec7c860700d78a62..452170a3ab45d24e03ab11965448572a411c349f
@@@ -59,27 -59,9 +59,9 @@@ static void print_error_files(struct st
        }
  }
  
- static void error_removing_concrete_submodules(struct string_list *files, int *errs)
- {
-       print_error_files(files,
-                         Q_("the following submodule (or one of its nested "
-                            "submodules)\n"
-                            "uses a .git directory:",
-                            "the following submodules (or one of their nested "
-                            "submodules)\n"
-                            "use a .git directory:", files->nr),
-                         _("\n(use 'rm -rf' if you really want to remove "
-                           "it including all of its history)"),
-                         errs);
-       string_list_clear(files, 0);
- }
- static int check_submodules_use_gitfiles(void)
+ static void submodules_absorb_gitdir_if_needed(const char *prefix)
  {
        int i;
-       int errs = 0;
-       struct string_list files = STRING_LIST_INIT_NODUP;
        for (i = 0; i < list.nr; i++) {
                const char *name = list.entry[i].name;
                int pos;
                        continue;
  
                if (!submodule_uses_gitfile(name))
-                       string_list_append(&files, name);
+                       absorb_git_dir_into_superproject(prefix, name,
+                               ABSORB_GITDIR_RECURSE_SUBMODULES);
        }
-       error_removing_concrete_submodules(&files, &errs);
-       return errs;
  }
  
  static int check_local_mod(struct object_id *head, int index_only)
        int errs = 0;
        struct string_list files_staged = STRING_LIST_INIT_NODUP;
        struct string_list files_cached = STRING_LIST_INIT_NODUP;
-       struct string_list files_submodule = STRING_LIST_INIT_NODUP;
        struct string_list files_local = STRING_LIST_INIT_NODUP;
  
        no_head = is_null_oid(head);
                 */
                if (ce_match_stat(ce, &st, 0) ||
                    (S_ISGITLINK(ce->ce_mode) &&
-                    !ok_to_remove_submodule(ce->name)))
+                    bad_to_remove_submodule(ce->name,
+                               SUBMODULE_REMOVAL_DIE_ON_ERROR |
+                               SUBMODULE_REMOVAL_IGNORE_IGNORED_UNTRACKED)))
                        local_changes = 1;
  
                /*
                else if (!index_only) {
                        if (staged_changes)
                                string_list_append(&files_cached, name);
-                       if (local_changes) {
-                               if (S_ISGITLINK(ce->ce_mode) &&
-                                   !submodule_uses_gitfile(name))
-                                       string_list_append(&files_submodule, name);
-                               else
-                                       string_list_append(&files_local, name);
-                       }
+                       if (local_changes)
+                               string_list_append(&files_local, name);
                }
        }
        print_error_files(&files_staged,
                          &errs);
        string_list_clear(&files_cached, 0);
  
-       error_removing_concrete_submodules(&files_submodule, &errs);
        print_error_files(&files_local,
                          Q_("the following file has local modifications:",
                             "the following files have local modifications:",
@@@ -292,7 -265,7 +265,7 @@@ int cmd_rm(int argc, const char **argv
        if (!index_only)
                setup_work_tree();
  
 -      hold_locked_index(&lock_file, 1);
 +      hold_locked_index(&lock_file, LOCK_DIE_ON_ERROR);
  
        if (read_cache() < 0)
                die(_("index file corrupt"));
                        exit(0);
        }
  
+       if (!index_only)
+               submodules_absorb_gitdir_if_needed(prefix);
        /*
         * If not forced, the file, the index and the HEAD (if exists)
         * must match; but the file can already been removed, since
                        oidclr(&oid);
                if (check_local_mod(&oid, index_only))
                        exit(1);
-       } else if (!index_only) {
-               if (check_submodules_use_gitfiles())
-                       exit(1);
        }
  
        /*
         */
        if (!index_only) {
                int removed = 0, gitmodules_modified = 0;
-               struct strbuf buf = STRBUF_INIT;
                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. */
-                               }
+                               struct strbuf buf = STRBUF_INIT;
+                               strbuf_addstr(&buf, path);
+                               if (remove_dir_recursively(&buf, 0))
+                                       die(_("could not remove '%s'"), path);
+                               strbuf_release(&buf);
+                               removed = 1;
+                               if (!remove_path_from_gitmodules(path))
+                                       gitmodules_modified = 1;
+                               continue;
                        }
                        if (!remove_path(path)) {
                                removed = 1;
                        if (!removed)
                                die_errno("git rm: '%s'", path);
                }
-               strbuf_release(&buf);
                if (gitmodules_modified)
                        stage_updated_gitmodules();
        }
diff --combined submodule.c
index f8fee3dfddd27b569b3b505877b569d30dcd656d,1cc04d24e5766f6bb895014c9321b938fd6f2d43..11ff11ed893879601cf0643fb345afe80942734f
@@@ -199,56 -199,6 +199,56 @@@ void gitmodules_config(void
        }
  }
  
 +void gitmodules_config_sha1(const unsigned char *commit_sha1)
 +{
 +      struct strbuf rev = STRBUF_INIT;
 +      unsigned char sha1[20];
 +
 +      if (gitmodule_sha1_from_commit(commit_sha1, sha1, &rev)) {
 +              git_config_from_blob_sha1(submodule_config, rev.buf,
 +                                        sha1, NULL);
 +      }
 +      strbuf_release(&rev);
 +}
 +
 +/*
 + * Determine if a submodule has been initialized at a given 'path'
 + */
 +int is_submodule_initialized(const char *path)
 +{
 +      int ret = 0;
 +      const struct submodule *module = NULL;
 +
 +      module = submodule_from_path(null_sha1, path);
 +
 +      if (module) {
 +              char *key = xstrfmt("submodule.%s.url", module->name);
 +              char *value = NULL;
 +
 +              ret = !git_config_get_string(key, &value);
 +
 +              free(value);
 +              free(key);
 +      }
 +
 +      return ret;
 +}
 +
 +/*
 + * Determine if a submodule has been populated at a given 'path'
 + */
 +int is_submodule_populated(const char *path)
 +{
 +      int ret = 0;
 +      char *gitdir = xstrfmt("%s/.git", path);
 +
 +      if (resolve_gitdir(gitdir))
 +              ret = 1;
 +
 +      free(gitdir);
 +      return ret;
 +}
 +
  int parse_submodule_update_strategy(const char *value,
                struct submodule_update_strategy *dst)
  {
@@@ -551,67 -501,27 +551,67 @@@ static int has_remote(const char *refna
        return 1;
  }
  
 -static int submodule_needs_pushing(const char *path, const unsigned char sha1[20])
 +static int append_sha1_to_argv(const unsigned char sha1[20], void *data)
 +{
 +      struct argv_array *argv = data;
 +      argv_array_push(argv, sha1_to_hex(sha1));
 +      return 0;
 +}
 +
 +static int check_has_commit(const unsigned char sha1[20], void *data)
 +{
 +      int *has_commit = data;
 +
 +      if (!lookup_commit_reference(sha1))
 +              *has_commit = 0;
 +
 +      return 0;
 +}
 +
 +static int submodule_has_commits(const char *path, struct sha1_array *commits)
  {
 -      if (add_submodule_odb(path) || !lookup_commit_reference(sha1))
 +      int has_commit = 1;
 +
 +      if (add_submodule_odb(path))
 +              return 0;
 +
 +      sha1_array_for_each_unique(commits, check_has_commit, &has_commit);
 +      return has_commit;
 +}
 +
 +static int submodule_needs_pushing(const char *path, struct sha1_array *commits)
 +{
 +      if (!submodule_has_commits(path, commits))
 +              /*
 +               * NOTE: We do consider it safe to return "no" here. The
 +               * correct answer would be "We do not know" instead of
 +               * "No push needed", but it is quite hard to change
 +               * the submodule pointer without having the submodule
 +               * around. If a user did however change the submodules
 +               * without having the submodule around, this indicates
 +               * an expert who knows what they are doing or a
 +               * maintainer integrating work from other people. In
 +               * both cases it should be safe to skip this check.
 +               */
                return 0;
  
        if (for_each_remote_ref_submodule(path, has_remote, NULL) > 0) {
                struct child_process cp = CHILD_PROCESS_INIT;
 -              const char *argv[] = {"rev-list", NULL, "--not", "--remotes", "-n", "1" , NULL};
                struct strbuf buf = STRBUF_INIT;
                int needs_pushing = 0;
  
 -              argv[1] = sha1_to_hex(sha1);
 -              cp.argv = argv;
 +              argv_array_push(&cp.args, "rev-list");
 +              sha1_array_for_each_unique(commits, append_sha1_to_argv, &cp.args);
 +              argv_array_pushl(&cp.args, "--not", "--remotes", "-n", "1" , NULL);
 +
                prepare_submodule_repo_env(&cp.env_array);
                cp.git_cmd = 1;
                cp.no_stdin = 1;
                cp.out = -1;
                cp.dir = path;
                if (start_command(&cp))
 -                      die("Could not run 'git rev-list %s --not --remotes -n 1' command in submodule %s",
 -                              sha1_to_hex(sha1), path);
 +                      die("Could not run 'git rev-list <commits> --not --remotes -n 1' command in submodule %s",
 +                                      path);
                if (strbuf_read(&buf, cp.out, 41))
                        needs_pushing = 1;
                finish_command(&cp);
        return 0;
  }
  
 +static struct sha1_array *submodule_commits(struct string_list *submodules,
 +                                          const char *path)
 +{
 +      struct string_list_item *item;
 +
 +      item = string_list_insert(submodules, path);
 +      if (item->util)
 +              return (struct sha1_array *) item->util;
 +
 +      /* NEEDSWORK: should we have sha1_array_init()? */
 +      item->util = xcalloc(1, sizeof(struct sha1_array));
 +      return (struct sha1_array *) item->util;
 +}
 +
  static void collect_submodules_from_diff(struct diff_queue_struct *q,
                                         struct diff_options *options,
                                         void *data)
  {
        int i;
 -      struct string_list *needs_pushing = data;
 +      struct string_list *submodules = data;
  
        for (i = 0; i < q->nr; i++) {
                struct diff_filepair *p = q->queue[i];
 +              struct sha1_array *commits;
                if (!S_ISGITLINK(p->two->mode))
                        continue;
 -              if (submodule_needs_pushing(p->two->path, p->two->oid.hash))
 -                      string_list_insert(needs_pushing, p->two->path);
 +              commits = submodule_commits(submodules, p->two->path);
 +              sha1_array_append(commits, p->two->oid.hash);
        }
  }
  
@@@ -666,63 -561,46 +666,63 @@@ static void find_unpushed_submodule_com
        diff_tree_combined_merge(commit, 1, &rev);
  }
  
 -int find_unpushed_submodules(unsigned char new_sha1[20],
 +static void free_submodules_sha1s(struct string_list *submodules)
 +{
 +      struct string_list_item *item;
 +      for_each_string_list_item(item, submodules)
 +              sha1_array_clear((struct sha1_array *) item->util);
 +      string_list_clear(submodules, 1);
 +}
 +
 +int find_unpushed_submodules(struct sha1_array *commits,
                const char *remotes_name, struct string_list *needs_pushing)
  {
        struct rev_info rev;
        struct commit *commit;
 -      const char *argv[] = {NULL, NULL, "--not", "NULL", NULL};
 -      int argc = ARRAY_SIZE(argv) - 1;
 -      char *sha1_copy;
 -
 -      struct strbuf remotes_arg = STRBUF_INIT;
 +      struct string_list submodules = STRING_LIST_INIT_DUP;
 +      struct string_list_item *submodule;
 +      struct argv_array argv = ARGV_ARRAY_INIT;
  
 -      strbuf_addf(&remotes_arg, "--remotes=%s", remotes_name);
        init_revisions(&rev, NULL);
 -      sha1_copy = xstrdup(sha1_to_hex(new_sha1));
 -      argv[1] = sha1_copy;
 -      argv[3] = remotes_arg.buf;
 -      setup_revisions(argc, argv, &rev, NULL);
 +
 +      /* argv.argv[0] will be ignored by setup_revisions */
 +      argv_array_push(&argv, "find_unpushed_submodules");
 +      sha1_array_for_each_unique(commits, append_sha1_to_argv, &argv);
 +      argv_array_push(&argv, "--not");
 +      argv_array_pushf(&argv, "--remotes=%s", remotes_name);
 +
 +      setup_revisions(argv.argc, argv.argv, &rev, NULL);
        if (prepare_revision_walk(&rev))
                die("revision walk setup failed");
  
        while ((commit = get_revision(&rev)) != NULL)
 -              find_unpushed_submodule_commits(commit, needs_pushing);
 +              find_unpushed_submodule_commits(commit, &submodules);
  
        reset_revision_walk();
 -      free(sha1_copy);
 -      strbuf_release(&remotes_arg);
 +      argv_array_clear(&argv);
 +
 +      for_each_string_list_item(submodule, &submodules) {
 +              struct sha1_array *commits = (struct sha1_array *) submodule->util;
 +
 +              if (submodule_needs_pushing(submodule->string, commits))
 +                      string_list_insert(needs_pushing, submodule->string);
 +      }
 +      free_submodules_sha1s(&submodules);
  
        return needs_pushing->nr;
  }
  
 -static int push_submodule(const char *path)
 +static int push_submodule(const char *path, int dry_run)
  {
        if (add_submodule_odb(path))
                return 1;
  
        if (for_each_remote_ref_submodule(path, has_remote, NULL) > 0) {
                struct child_process cp = CHILD_PROCESS_INIT;
 -              const char *argv[] = {"push", NULL};
 +              argv_array_push(&cp.args, "push");
 +              if (dry_run)
 +                      argv_array_push(&cp.args, "--dry-run");
  
 -              cp.argv = argv;
                prepare_submodule_repo_env(&cp.env_array);
                cp.git_cmd = 1;
                cp.no_stdin = 1;
        return 1;
  }
  
 -int push_unpushed_submodules(unsigned char new_sha1[20], const char *remotes_name)
 +int push_unpushed_submodules(struct sha1_array *commits,
 +                           const char *remotes_name,
 +                           int dry_run)
  {
        int i, ret = 1;
        struct string_list needs_pushing = STRING_LIST_INIT_DUP;
  
 -      if (!find_unpushed_submodules(new_sha1, remotes_name, &needs_pushing))
 +      if (!find_unpushed_submodules(commits, remotes_name, &needs_pushing))
                return 1;
  
        for (i = 0; i < needs_pushing.nr; i++) {
                const char *path = needs_pushing.items[i].string;
                fprintf(stderr, "Pushing submodule '%s'\n", path);
 -              if (!push_submodule(path)) {
 +              if (!push_submodule(path, dry_run)) {
                        fprintf(stderr, "Unable to push submodule '%s'\n", path);
                        ret = 0;
                }
@@@ -1143,45 -1019,64 +1143,64 @@@ int submodule_uses_gitfile(const char *
        return 1;
  }
  
- int ok_to_remove_submodule(const char *path)
+ /*
+  * Check if it is a bad idea to remove a submodule, i.e. if we'd lose data
+  * when doing so.
+  *
+  * Return 1 if we'd lose data, return 0 if the removal is fine,
+  * and negative values for errors.
+  */
+ int bad_to_remove_submodule(const char *path, unsigned flags)
  {
        ssize_t len;
        struct child_process cp = CHILD_PROCESS_INIT;
-       const char *argv[] = {
-               "status",
-               "--porcelain",
-               "-u",
-               "--ignore-submodules=none",
-               NULL,
-       };
        struct strbuf buf = STRBUF_INIT;
-       int ok_to_remove = 1;
+       int ret = 0;
  
        if (!file_exists(path) || is_empty_dir(path))
-               return 1;
+               return 0;
  
        if (!submodule_uses_gitfile(path))
-               return 0;
+               return 1;
+       argv_array_pushl(&cp.args, "status", "--porcelain",
+                                  "--ignore-submodules=none", NULL);
+       if (flags & SUBMODULE_REMOVAL_IGNORE_UNTRACKED)
+               argv_array_push(&cp.args, "-uno");
+       else
+               argv_array_push(&cp.args, "-uall");
+       if (!(flags & SUBMODULE_REMOVAL_IGNORE_IGNORED_UNTRACKED))
+               argv_array_push(&cp.args, "--ignored");
  
-       cp.argv = argv;
        prepare_submodule_repo_env(&cp.env_array);
        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);
+       if (start_command(&cp)) {
+               if (flags & SUBMODULE_REMOVAL_DIE_ON_ERROR)
+                       die(_("could not start 'git status in submodule '%s'"),
+                               path);
+               ret = -1;
+               goto out;
+       }
  
        len = strbuf_read(&buf, cp.out, 1024);
        if (len > 2)
-               ok_to_remove = 0;
+               ret = 1;
        close(cp.out);
  
-       if (finish_command(&cp))
-               die("'git status --porcelain -uall --ignore-submodules=none' failed in submodule %s", path);
+       if (finish_command(&cp)) {
+               if (flags & SUBMODULE_REMOVAL_DIE_ON_ERROR)
+                       die(_("could not run 'git status in submodule '%s'"),
+                               path);
+               ret = -1;
+       }
+ out:
        strbuf_release(&buf);
-       return ok_to_remove;
+       return ret;
  }
  
  static int find_first_merges(struct object_array *result, const char *path,
@@@ -1383,7 -1278,7 +1402,7 @@@ static void relocate_single_git_dir_int
                /* If it is an actual gitfile, it doesn't need migration. */
                return;
  
 -      real_old_git_dir = xstrdup(real_path(old_git_dir));
 +      real_old_git_dir = real_pathdup(old_git_dir);
  
        sub = submodule_from_path(null_sha1, path);
        if (!sub)
        new_git_dir = git_path("modules/%s", sub->name);
        if (safe_create_leading_directories_const(new_git_dir) < 0)
                die(_("could not create directory '%s'"), new_git_dir);
 -      real_new_git_dir = xstrdup(real_path(new_git_dir));
 +      real_new_git_dir = real_pathdup(new_git_dir);
  
        if (!prefix)
                prefix = get_super_prefix();
@@@ -1429,8 -1324,8 +1448,8 @@@ void absorb_git_dir_into_superproject(c
                goto out;
  
        /* Is it already absorbed into the superprojects git dir? */
 -      real_sub_git_dir = xstrdup(real_path(sub_git_dir));
 -      real_common_git_dir = xstrdup(real_path(get_git_common_dir()));
 +      real_sub_git_dir = real_pathdup(sub_git_dir);
 +      real_common_git_dir = real_pathdup(get_git_common_dir());
        if (!skip_prefix(real_sub_git_dir, real_common_git_dir, &v))
                relocate_single_git_dir_into_superproject(prefix, path);
  
diff --combined submodule.h
index 1ccaf0e6ba29168d3fe0a2354d4c643e4f3a080e,21b1569413829b36ab51b21705ddb23048ec0c26..b7fe4d20279dfe165338dc412595fe1ccf6ad73c
@@@ -3,7 -3,6 +3,7 @@@
  
  struct diff_options;
  struct argv_array;
 +struct sha1_array;
  
  enum {
        RECURSE_SUBMODULES_CHECK = -4,
@@@ -30,55 -29,59 +30,63 @@@ struct submodule_update_strategy 
  };
  #define SUBMODULE_UPDATE_STRATEGY_INIT {SM_UPDATE_UNSPECIFIED, NULL}
  
- int is_staging_gitmodules_ok(void);
- int update_path_in_gitmodules(const char *oldpath, const char *newpath);
- int remove_path_from_gitmodules(const char *path);
- void stage_updated_gitmodules(void);
void set_diffopt_flags_from_submodule_config(struct diff_options *diffopt,
extern int is_staging_gitmodules_ok(void);
extern int update_path_in_gitmodules(const char *oldpath, const char *newpath);
extern int remove_path_from_gitmodules(const char *path);
extern void stage_updated_gitmodules(void);
extern void set_diffopt_flags_from_submodule_config(struct diff_options *,
                const char *path);
- int submodule_config(const char *var, const char *value, void *cb);
- void gitmodules_config(void);
+ extern int submodule_config(const char *var, const char *value, void *cb);
+ extern void gitmodules_config(void);
 +extern void gitmodules_config_sha1(const unsigned char *commit_sha1);
 +extern int is_submodule_initialized(const char *path);
 +extern int is_submodule_populated(const char *path);
- int parse_submodule_update_strategy(const char *value,
extern int parse_submodule_update_strategy(const char *value,
                struct submodule_update_strategy *dst);
- const char *submodule_strategy_to_string(const struct submodule_update_strategy *s);
void handle_ignore_submodules_arg(struct diff_options *diffopt, const char *);
- void show_submodule_summary(FILE *f, const char *path,
extern const char *submodule_strategy_to_string(const struct submodule_update_strategy *s);
extern void handle_ignore_submodules_arg(struct diff_options *, const char *);
extern void show_submodule_summary(FILE *f, const char *path,
                const char *line_prefix,
                struct object_id *one, struct object_id *two,
                unsigned dirty_submodule, const char *meta,
                const char *del, const char *add, const char *reset);
- void show_submodule_inline_diff(FILE *f, const char *path,
extern void show_submodule_inline_diff(FILE *f, const char *path,
                const char *line_prefix,
                struct object_id *one, struct object_id *two,
                unsigned dirty_submodule, const char *meta,
                const char *del, const char *add, const char *reset,
                const struct diff_options *opt);
- void set_config_fetch_recurse_submodules(int value);
- void check_for_new_submodule_commits(unsigned char new_sha1[20]);
- int fetch_populated_submodules(const struct argv_array *options,
extern void set_config_fetch_recurse_submodules(int value);
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,
                               int quiet, int max_parallel_jobs);
- 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(struct sha1_array *commits, const char *remotes_name,
-               struct string_list *needs_pushing);
+ extern unsigned is_submodule_modified(const char *path, int ignore_untracked);
+ extern int submodule_uses_gitfile(const char *path);
+ #define SUBMODULE_REMOVAL_DIE_ON_ERROR (1<<0)
+ #define SUBMODULE_REMOVAL_IGNORE_UNTRACKED (1<<1)
+ #define SUBMODULE_REMOVAL_IGNORE_IGNORED_UNTRACKED (1<<2)
+ extern int bad_to_remove_submodule(const char *path, unsigned flags);
+ extern 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);
 -extern int find_unpushed_submodules(unsigned char new_sha1[20],
++extern int find_unpushed_submodules(struct sha1_array *commits,
+                                   const char *remotes_name,
+                                   struct string_list *needs_pushing);
 -extern int push_unpushed_submodules(unsigned char new_sha1[20],
 -                                  const char *remotes_name);
 +extern int push_unpushed_submodules(struct sha1_array *commits,
 +                                  const char *remotes_name,
 +                                  int dry_run);
- int parallel_submodules(void);
+ extern void connect_work_tree_and_git_dir(const char *work_tree, const char *git_dir);
+ extern int parallel_submodules(void);
  
  /*
   * Prepare the "env_array" parameter of a "struct child_process" for executing
   * a submodule by clearing any repo-specific envirionment variables, but
   * retaining any config in the environment.
   */
- void prepare_submodule_repo_env(struct argv_array *out);
extern void prepare_submodule_repo_env(struct argv_array *out);
  
  #define ABSORB_GITDIR_RECURSE_SUBMODULES (1<<0)
  extern void absorb_git_dir_into_superproject(const char *prefix,
diff --combined t/t3600-rm.sh
index bcbb68065113f6a2032a3879fabb3353c42e79d0,030d6c32ae94b2132d676a698a7a1d053e63a784..5aa6db584cd0dbddf6060eeb46f4c1c9836bc935
@@@ -111,21 -111,21 +111,21 @@@ test_expect_success 'Remove nonexisten
  '
  
  test_expect_success '"rm" command printed' '
 -      echo frotz > test-file &&
 +      echo frotz >test-file &&
        git add test-file &&
        git commit -m "add file for rm test" &&
 -      git rm test-file > rm-output &&
 +      git rm test-file >rm-output &&
        test $(grep "^rm " rm-output | wc -l) = 1 &&
        rm -f test-file rm-output &&
        git commit -m "remove file from rm test"
  '
  
  test_expect_success '"rm" command suppressed with --quiet' '
 -      echo frotz > test-file &&
 +      echo frotz >test-file &&
        git add test-file &&
        git commit -m "add file for rm --quiet test" &&
 -      git rm --quiet test-file > rm-output &&
 -      test $(wc -l < rm-output) = 0 &&
 +      git rm --quiet test-file >rm-output &&
 +      test_must_be_empty rm-output &&
        rm -f test-file rm-output &&
        git commit -m "remove file from rm --quiet test"
  '
@@@ -221,7 -221,7 +221,7 @@@ test_expect_success 'Call "rm" from out
        mkdir repo &&
        (cd repo &&
         git init &&
 -       echo something > somefile &&
 +       echo something >somefile &&
         git add somefile &&
         git commit -m "add a file" &&
         (cd .. &&
@@@ -287,7 -287,7 +287,7 @@@ test_expect_success 'rm removes empty s
        git commit -m "add submodule" &&
        git rm submod &&
        test ! -e submod &&
 -      git status -s -uno --ignore-submodules=none > actual &&
 +      git status -s -uno --ignore-submodules=none >actual &&
        test_cmp expect actual &&
        test_must_fail git config -f .gitmodules submodule.sub.url &&
        test_must_fail git config -f .gitmodules submodule.sub.path
@@@ -298,7 -298,7 +298,7 @@@ test_expect_success 'rm removes remove
        git submodule update &&
        rm -rf submod &&
        git rm submod &&
 -      git status -s -uno --ignore-submodules=none > actual &&
 +      git status -s -uno --ignore-submodules=none >actual &&
        test_cmp expect actual &&
        test_must_fail git config -f .gitmodules submodule.sub.url &&
        test_must_fail git config -f .gitmodules submodule.sub.path
@@@ -309,7 -309,7 +309,7 @@@ test_expect_success 'rm removes work tr
        git submodule update &&
        git rm submod &&
        test ! -d submod &&
 -      git status -s -uno --ignore-submodules=none > actual &&
 +      git status -s -uno --ignore-submodules=none >actual &&
        test_cmp expect actual &&
        test_must_fail git config -f .gitmodules submodule.sub.url &&
        test_must_fail git config -f .gitmodules submodule.sub.path
@@@ -320,7 -320,7 +320,7 @@@ test_expect_success 'rm removes a submo
        git submodule update &&
        git rm submod/ &&
        test ! -d submod &&
 -      git status -s -uno --ignore-submodules=none > actual &&
 +      git status -s -uno --ignore-submodules=none >actual &&
        test_cmp expect actual
  '
  
@@@ -335,15 -335,17 +335,15 @@@ test_expect_success 'rm succeeds when g
  test_expect_success 'rm of a populated submodule with different HEAD fails unless forced' '
        git reset --hard &&
        git submodule update &&
 -      (cd submod &&
 -              git checkout HEAD^
 -      ) &&
 +      git -C submod checkout HEAD^ &&
        test_must_fail git rm submod &&
        test -d submod &&
        test -f submod/.git &&
 -      git status -s -uno --ignore-submodules=none > actual &&
 +      git status -s -uno --ignore-submodules=none >actual &&
        test_cmp expect.modified actual &&
        git rm -f submod &&
        test ! -d submod &&
 -      git status -s -uno --ignore-submodules=none > actual &&
 +      git status -s -uno --ignore-submodules=none >actual &&
        test_cmp expect actual &&
        test_must_fail git config -f .gitmodules submodule.sub.url &&
        test_must_fail git config -f .gitmodules submodule.sub.path
@@@ -416,30 -418,34 +416,30 @@@ test_expect_success 'rm issues a warnin
  test_expect_success 'rm of a populated submodule with modifications fails unless forced' '
        git reset --hard &&
        git submodule update &&
 -      (cd submod &&
 -              echo X >empty
 -      ) &&
 +      echo X >submod/empty &&
        test_must_fail git rm submod &&
        test -d submod &&
        test -f submod/.git &&
 -      git status -s -uno --ignore-submodules=none > actual &&
 +      git status -s -uno --ignore-submodules=none >actual &&
        test_cmp expect.modified actual &&
        git rm -f submod &&
        test ! -d submod &&
 -      git status -s -uno --ignore-submodules=none > actual &&
 +      git status -s -uno --ignore-submodules=none >actual &&
        test_cmp expect actual
  '
  
  test_expect_success 'rm of a populated submodule with untracked files fails unless forced' '
        git reset --hard &&
        git submodule update &&
 -      (cd submod &&
 -              echo X >untracked
 -      ) &&
 +      echo X >submod/untracked &&
        test_must_fail git rm submod &&
        test -d submod &&
        test -f submod/.git &&
 -      git status -s -uno --ignore-submodules=none > actual &&
 +      git status -s -uno --ignore-submodules=none >actual &&
        test_cmp expect.modified actual &&
        git rm -f submod &&
        test ! -d submod &&
 -      git status -s -uno --ignore-submodules=none > actual &&
 +      git status -s -uno --ignore-submodules=none >actual &&
        test_cmp expect actual
  '
  
@@@ -455,12 -461,16 +455,12 @@@ test_expect_success 'setup submodule co
        git add nitfol &&
        git commit -m "added nitfol 2" &&
        git checkout -b conflict1 master &&
 -      (cd submod &&
 -              git fetch &&
 -              git checkout branch1
 -      ) &&
 +      git -C submod fetch &&
 +      git -C submod checkout branch1 &&
        git add submod &&
        git commit -m "submod 1" &&
        git checkout -b conflict2 master &&
 -      (cd submod &&
 -              git checkout branch2
 -      ) &&
 +      git -C submod checkout branch2 &&
        git add submod &&
        git commit -m "submod 2"
  '
@@@ -476,7 -486,7 +476,7 @@@ test_expect_success 'rm removes work tr
        test_must_fail git merge conflict2 &&
        git rm submod &&
        test ! -d submod &&
 -      git status -s -uno --ignore-submodules=none > actual &&
 +      git status -s -uno --ignore-submodules=none >actual &&
        test_cmp expect actual
  '
  
@@@ -484,16 -494,18 +484,16 @@@ test_expect_success 'rm of a conflicte
        git checkout conflict1 &&
        git reset --hard &&
        git submodule update &&
 -      (cd submod &&
 -              git checkout HEAD^
 -      ) &&
 +      git -C submod checkout HEAD^ &&
        test_must_fail git merge conflict2 &&
        test_must_fail git rm submod &&
        test -d submod &&
        test -f submod/.git &&
 -      git status -s -uno --ignore-submodules=none > actual &&
 +      git status -s -uno --ignore-submodules=none >actual &&
        test_cmp expect.conflict actual &&
        git rm -f submod &&
        test ! -d submod &&
 -      git status -s -uno --ignore-submodules=none > actual &&
 +      git status -s -uno --ignore-submodules=none >actual &&
        test_cmp expect actual &&
        test_must_fail git config -f .gitmodules submodule.sub.url &&
        test_must_fail git config -f .gitmodules submodule.sub.path
@@@ -503,16 -515,18 +503,16 @@@ test_expect_success 'rm of a conflicte
        git checkout conflict1 &&
        git reset --hard &&
        git submodule update &&
 -      (cd submod &&
 -              echo X >empty
 -      ) &&
 +      echo X >submod/empty &&
        test_must_fail git merge conflict2 &&
        test_must_fail git rm submod &&
        test -d submod &&
        test -f submod/.git &&
 -      git status -s -uno --ignore-submodules=none > actual &&
 +      git status -s -uno --ignore-submodules=none >actual &&
        test_cmp expect.conflict actual &&
        git rm -f submod &&
        test ! -d submod &&
 -      git status -s -uno --ignore-submodules=none > actual &&
 +      git status -s -uno --ignore-submodules=none >actual &&
        test_cmp expect actual &&
        test_must_fail git config -f .gitmodules submodule.sub.url &&
        test_must_fail git config -f .gitmodules submodule.sub.path
@@@ -522,16 -536,18 +522,16 @@@ test_expect_success 'rm of a conflicte
        git checkout conflict1 &&
        git reset --hard &&
        git submodule update &&
 -      (cd submod &&
 -              echo X >untracked
 -      ) &&
 +      echo X >submod/untracked &&
        test_must_fail git merge conflict2 &&
        test_must_fail git rm submod &&
        test -d submod &&
        test -f submod/.git &&
 -      git status -s -uno --ignore-submodules=none > actual &&
 +      git status -s -uno --ignore-submodules=none >actual &&
        test_cmp expect.conflict actual &&
        git rm -f submod &&
        test ! -d submod &&
 -      git status -s -uno --ignore-submodules=none > actual &&
 +      git status -s -uno --ignore-submodules=none >actual &&
        test_cmp expect actual
  '
  
@@@ -548,12 -564,12 +548,12 @@@ test_expect_success 'rm of a conflicte
        test_must_fail git rm submod &&
        test -d submod &&
        test -d submod/.git &&
 -      git status -s -uno --ignore-submodules=none > actual &&
 +      git status -s -uno --ignore-submodules=none >actual &&
        test_cmp expect.conflict actual &&
        test_must_fail git rm -f submod &&
        test -d submod &&
        test -d submod/.git &&
 -      git status -s -uno --ignore-submodules=none > actual &&
 +      git status -s -uno --ignore-submodules=none >actual &&
        test_cmp expect.conflict actual &&
        git merge --abort &&
        rm -rf submod
@@@ -565,30 -581,26 +565,26 @@@ test_expect_success 'rm of a conflicte
        test_must_fail git merge conflict2 &&
        git rm submod &&
        test ! -d submod &&
 -      git status -s -uno --ignore-submodules=none > actual &&
 +      git status -s -uno --ignore-submodules=none >actual &&
        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
@@@ -613,52 -625,58 +609,52 @@@ test_expect_success 'setup subsubmodule
  test_expect_success 'rm recursively removes work tree of unmodified submodules' '
        git rm submod &&
        test ! -d submod &&
 -      git status -s -uno --ignore-submodules=none > actual &&
 +      git status -s -uno --ignore-submodules=none >actual &&
        test_cmp expect actual
  '
  
  test_expect_success 'rm of a populated nested submodule with different nested HEAD fails unless forced' '
        git reset --hard &&
        git submodule update --recursive &&
 -      (cd submod/subsubmod &&
 -              git checkout HEAD^
 -      ) &&
 +      git -C submod/subsubmod checkout HEAD^ &&
        test_must_fail git rm submod &&
        test -d submod &&
        test -f submod/.git &&
 -      git status -s -uno --ignore-submodules=none > actual &&
 +      git status -s -uno --ignore-submodules=none >actual &&
        test_cmp expect.modified actual &&
        git rm -f submod &&
        test ! -d submod &&
 -      git status -s -uno --ignore-submodules=none > actual &&
 +      git status -s -uno --ignore-submodules=none >actual &&
        test_cmp expect actual
  '
  
  test_expect_success 'rm of a populated nested submodule with nested modifications fails unless forced' '
        git reset --hard &&
        git submodule update --recursive &&
 -      (cd submod/subsubmod &&
 -              echo X >empty
 -      ) &&
 +      echo X >submod/subsubmod/empty &&
        test_must_fail git rm submod &&
        test -d submod &&
        test -f submod/.git &&
 -      git status -s -uno --ignore-submodules=none > actual &&
 +      git status -s -uno --ignore-submodules=none >actual &&
        test_cmp expect.modified actual &&
        git rm -f submod &&
        test ! -d submod &&
 -      git status -s -uno --ignore-submodules=none > actual &&
 +      git status -s -uno --ignore-submodules=none >actual &&
        test_cmp expect actual
  '
  
  test_expect_success 'rm of a populated nested submodule with nested untracked files fails unless forced' '
        git reset --hard &&
        git submodule update --recursive &&
 -      (cd submod/subsubmod &&
 -              echo X >untracked
 -      ) &&
 +      echo X >submod/subsubmod/untracked &&
        test_must_fail git rm submod &&
        test -d submod &&
        test -f submod/.git &&
 -      git status -s -uno --ignore-submodules=none > actual &&
 +      git status -s -uno --ignore-submodules=none >actual &&
        test_cmp expect.modified actual &&
        git rm -f submod &&
        test ! -d submod &&
 -      git status -s -uno --ignore-submodules=none > actual &&
 +      git status -s -uno --ignore-submodules=none >actual &&
        test_cmp expect actual
  '
  
@@@ -667,34 -685,29 +663,29 @@@ test_expect_success 'rm of a populated 
        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>actual &&
 +      git checkout -q HEAD^ &&
        git checkout -q master 2>actual &&
        test_i18ngrep "^warning: unable to rmdir submod:" actual &&
        git status -s submod >actual &&
        echo "?? submod/" >expected &&
        test_cmp expected actual &&
        rm -rf submod &&
 -      git status -s -uno --ignore-submodules=none > actual &&
 +      git status -s -uno --ignore-submodules=none >actual &&
        ! test -s actual
  '