Merge branch 'sb/submodule-short-status'
authorJunio C Hamano <gitster@pobox.com>
Thu, 20 Apr 2017 04:37:12 +0000 (21:37 -0700)
committerJunio C Hamano <gitster@pobox.com>
Thu, 20 Apr 2017 04:37:12 +0000 (21:37 -0700)
The output from "git status --short" has been extended to show
various kinds of dirtyness in submodules differently; instead of to
"M" for modified, 'm' and '?' can be shown to signal changes only
to the working tree of the submodule but not the commit that is
checked out.

* sb/submodule-short-status:
submodule.c: correctly handle nested submodules in is_submodule_modified
short status: improve reporting for submodule changes
submodule.c: stricter checking for submodules in is_submodule_modified
submodule.c: port is_submodule_modified to use porcelain 2
submodule.c: convert is_submodule_modified to use strbuf_getwholeline
submodule.c: factor out early loop termination in is_submodule_modified
submodule.c: use argv_array in is_submodule_modified

1  2 
submodule.c
t/t3600-rm.sh
diff --combined submodule.c
index 7c3c4b17fb10b45536bc0680b85d19a8b811f919,3da65100e39b85ff9aaa2f40f60c9b35820c6512..2ee970572917f8dfdb6be58dfd8252ff58d07c12
@@@ -17,7 -17,6 +17,7 @@@
  #include "worktree.h"
  
  static int config_fetch_recurse_submodules = RECURSE_SUBMODULES_ON_DEMAND;
 +static int config_update_recurse_submodules = RECURSE_SUBMODULES_DEFAULT;
  static int parallel_jobs = 1;
  static struct string_list changed_submodule_paths = STRING_LIST_INIT_NODUP;
  static int initialized_fetch_ref_tips;
@@@ -213,68 -212,37 +213,68 @@@ void gitmodules_config_sha1(const unsig
  }
  
  /*
 + * NEEDSWORK: With the addition of different configuration options to determine
 + * if a submodule is of interests, the validity of this function's name comes
 + * into question.  Once the dust has settled and more concrete terminology is
 + * decided upon, come up with a more proper name for this function.  One
 + * potential candidate could be 'is_submodule_active()'.
 + *
   * 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;
 +      char *key = NULL;
 +      char *value = NULL;
 +      const struct string_list *sl;
 +      const struct submodule *module = submodule_from_path(null_sha1, path);
 +
 +      /* early return if there isn't a path->module mapping */
 +      if (!module)
 +              return 0;
 +
 +      /* submodule.<name>.active is set */
 +      key = xstrfmt("submodule.%s.active", module->name);
 +      if (!git_config_get_bool(key, &ret)) {
 +              free(key);
 +              return ret;
 +      }
 +      free(key);
  
 -      module = submodule_from_path(null_sha1, path);
 +      /* submodule.active is set */
 +      sl = git_config_get_value_multi("submodule.active");
 +      if (sl) {
 +              struct pathspec ps;
 +              struct argv_array args = ARGV_ARRAY_INIT;
 +              const struct string_list_item *item;
  
 -      if (module) {
 -              char *key = xstrfmt("submodule.%s.url", module->name);
 -              char *value = NULL;
 +              for_each_string_list_item(item, sl) {
 +                      argv_array_push(&args, item->string);
 +              }
  
 -              ret = !git_config_get_string(key, &value);
 +              parse_pathspec(&ps, 0, 0, NULL, args.argv);
 +              ret = match_pathspec(&ps, path, strlen(path), 0, NULL, 1);
  
 -              free(value);
 -              free(key);
 +              argv_array_clear(&args);
 +              clear_pathspec(&ps);
 +              return ret;
        }
  
 +      /* fallback to checking if the URL is set */
 +      key = xstrfmt("submodule.%s.url", module->name);
 +      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 is_submodule_populated_gently(const char *path, int *return_error_code)
  {
        int ret = 0;
        char *gitdir = xstrfmt("%s/.git", path);
  
 -      if (resolve_gitdir(gitdir))
 +      if (resolve_gitdir_gently(gitdir, return_error_code))
                ret = 1;
  
        free(gitdir);
@@@ -390,23 -358,6 +390,23 @@@ static void print_submodule_summary(str
        strbuf_release(&sb);
  }
  
 +static void prepare_submodule_repo_env_no_git_dir(struct argv_array *out)
 +{
 +      const char * const *var;
 +
 +      for (var = local_repo_env; *var; var++) {
 +              if (strcmp(*var, CONFIG_DATA_ENVIRONMENT))
 +                      argv_array_push(out, *var);
 +      }
 +}
 +
 +void prepare_submodule_repo_env(struct argv_array *out)
 +{
 +      prepare_submodule_repo_env_no_git_dir(out);
 +      argv_array_pushf(out, "%s=%s", GIT_DIR_ENVIRONMENT,
 +                       DEFAULT_GIT_DIR_ENVIRONMENT);
 +}
 +
  /* 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
@@@ -576,7 -527,6 +576,7 @@@ void show_submodule_inline_diff(FILE *f
        if (!(dirty_submodule & DIRTY_SUBMODULE_MODIFIED))
                argv_array_push(&cp.args, oid_to_hex(new));
  
 +      prepare_submodule_repo_env(&cp.env_array);
        if (run_command(&cp))
                fprintf(f, "(diff failed)\n");
  
@@@ -595,27 -545,6 +595,27 @@@ void set_config_fetch_recurse_submodule
        config_fetch_recurse_submodules = value;
  }
  
 +void set_config_update_recurse_submodules(int value)
 +{
 +      config_update_recurse_submodules = value;
 +}
 +
 +int should_update_submodules(void)
 +{
 +      return config_update_recurse_submodules == RECURSE_SUBMODULES_ON;
 +}
 +
 +const struct submodule *submodule_from_ce(const struct cache_entry *ce)
 +{
 +      if (!S_ISGITLINK(ce->ce_mode))
 +              return NULL;
 +
 +      if (!should_update_submodules())
 +              return NULL;
 +
 +      return submodule_from_path(null_sha1, ce->name);
 +}
 +
  static int has_remote(const char *refname, const struct object_id *oid,
                      int flags, void *cb_data)
  {
  
  unsigned is_submodule_modified(const char *path, int ignore_untracked)
  {
-       ssize_t len;
        struct child_process cp = CHILD_PROCESS_INIT;
-       const char *argv[] = {
-               "status",
-               "--porcelain",
-               NULL,
-               NULL,
-       };
        struct strbuf buf = STRBUF_INIT;
+       FILE *fp;
        unsigned dirty_submodule = 0;
-       const char *line, *next_line;
        const char *git_dir;
+       int ignore_cp_exit_code = 0;
  
        strbuf_addf(&buf, "%s/.git", path);
        git_dir = read_gitfile(buf.buf);
        if (!git_dir)
                git_dir = buf.buf;
-       if (!is_directory(git_dir)) {
+       if (!is_git_directory(git_dir)) {
+               if (is_directory(git_dir))
+                       die(_("'%s' not recognized as a git repository"), git_dir);
                strbuf_release(&buf);
                /* The submodule is not checked out, so it is not modified */
                return 0;
        }
        strbuf_reset(&buf);
  
+       argv_array_pushl(&cp.args, "status", "--porcelain=2", NULL);
        if (ignore_untracked)
-               argv[2] = "-uno";
+               argv_array_push(&cp.args, "-uno");
  
-       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' in submodule %s", path);
+               die("Could not run 'git status --porcelain=2' in submodule %s", path);
  
-       len = strbuf_read(&buf, cp.out, 1024);
-       line = buf.buf;
-       while (len > 2) {
-               if ((line[0] == '?') && (line[1] == '?')) {
+       fp = xfdopen(cp.out, "r");
+       while (strbuf_getwholeline(&buf, fp, '\n') != EOF) {
+               /* regular untracked files */
+               if (buf.buf[0] == '?')
                        dirty_submodule |= DIRTY_SUBMODULE_UNTRACKED;
-                       if (dirty_submodule & DIRTY_SUBMODULE_MODIFIED)
-                               break;
-               } else {
-                       dirty_submodule |= DIRTY_SUBMODULE_MODIFIED;
-                       if (ignore_untracked ||
-                           (dirty_submodule & DIRTY_SUBMODULE_UNTRACKED))
-                               break;
+               if (buf.buf[0] == 'u' ||
+                   buf.buf[0] == '1' ||
+                   buf.buf[0] == '2') {
+                       /* T = line type, XY = status, SSSS = submodule state */
+                       if (buf.len < strlen("T XY SSSS"))
+                               die("BUG: invalid status --porcelain=2 line %s",
+                                   buf.buf);
+                       if (buf.buf[5] == 'S' && buf.buf[8] == 'U')
+                               /* nested untracked file */
+                               dirty_submodule |= DIRTY_SUBMODULE_UNTRACKED;
+                       if (buf.buf[0] == 'u' ||
+                           buf.buf[0] == '2' ||
+                           memcmp(buf.buf + 5, "S..U", 4))
+                               /* other change */
+                               dirty_submodule |= DIRTY_SUBMODULE_MODIFIED;
                }
-               next_line = strchr(line, '\n');
-               if (!next_line)
+               if ((dirty_submodule & DIRTY_SUBMODULE_MODIFIED) &&
+                   ((dirty_submodule & DIRTY_SUBMODULE_UNTRACKED) ||
+                    ignore_untracked)) {
+                       /*
+                        * We're not interested in any further information from
+                        * the child any more, neither output nor its exit code.
+                        */
+                       ignore_cp_exit_code = 1;
                        break;
-               next_line++;
-               len -= (next_line - line);
-               line = next_line;
+               }
        }
-       close(cp.out);
+       fclose(fp);
  
-       if (finish_command(&cp))
-               die("'git status --porcelain' failed in submodule %s", path);
+       if (finish_command(&cp) && !ignore_cp_exit_code)
+               die("'git status --porcelain=2' failed in submodule %s", path);
  
        strbuf_release(&buf);
        return dirty_submodule;
        return ret;
  }
  
 +static const char *get_super_prefix_or_empty(void)
 +{
 +      const char *s = get_super_prefix();
 +      if (!s)
 +              s = "";
 +      return s;
 +}
 +
 +static int submodule_has_dirty_index(const struct submodule *sub)
 +{
 +      struct child_process cp = CHILD_PROCESS_INIT;
 +
 +      prepare_submodule_repo_env_no_git_dir(&cp.env_array);
 +
 +      cp.git_cmd = 1;
 +      argv_array_pushl(&cp.args, "diff-index", "--quiet",
 +                                 "--cached", "HEAD", NULL);
 +      cp.no_stdin = 1;
 +      cp.no_stdout = 1;
 +      cp.dir = sub->path;
 +      if (start_command(&cp))
 +              die("could not recurse into submodule '%s'", sub->path);
 +
 +      return finish_command(&cp);
 +}
 +
 +static void submodule_reset_index(const char *path)
 +{
 +      struct child_process cp = CHILD_PROCESS_INIT;
 +      prepare_submodule_repo_env_no_git_dir(&cp.env_array);
 +
 +      cp.git_cmd = 1;
 +      cp.no_stdin = 1;
 +      cp.dir = path;
 +
 +      argv_array_pushf(&cp.args, "--super-prefix=%s%s/",
 +                                 get_super_prefix_or_empty(), path);
 +      argv_array_pushl(&cp.args, "read-tree", "-u", "--reset", NULL);
 +
 +      argv_array_push(&cp.args, EMPTY_TREE_SHA1_HEX);
 +
 +      if (run_command(&cp))
 +              die("could not reset submodule index");
 +}
 +
 +/**
 + * Moves a submodule at a given path from a given head to another new head.
 + * For edge cases (a submodule coming into existence or removing a submodule)
 + * pass NULL for old or new respectively.
 + */
 +int submodule_move_head(const char *path,
 +                       const char *old,
 +                       const char *new,
 +                       unsigned flags)
 +{
 +      int ret = 0;
 +      struct child_process cp = CHILD_PROCESS_INIT;
 +      const struct submodule *sub;
 +
 +      sub = submodule_from_path(null_sha1, path);
 +
 +      if (!sub)
 +              die("BUG: could not get submodule information for '%s'", path);
 +
 +      if (old && !(flags & SUBMODULE_MOVE_HEAD_FORCE)) {
 +              /* Check if the submodule has a dirty index. */
 +              if (submodule_has_dirty_index(sub))
 +                      return error(_("submodule '%s' has dirty index"), path);
 +      }
 +
 +      if (!(flags & SUBMODULE_MOVE_HEAD_DRY_RUN)) {
 +              if (old) {
 +                      if (!submodule_uses_gitfile(path))
 +                              absorb_git_dir_into_superproject("", path,
 +                                      ABSORB_GITDIR_RECURSE_SUBMODULES);
 +              } else {
 +                      struct strbuf sb = STRBUF_INIT;
 +                      strbuf_addf(&sb, "%s/modules/%s",
 +                                  get_git_common_dir(), sub->name);
 +                      connect_work_tree_and_git_dir(path, sb.buf);
 +                      strbuf_release(&sb);
 +
 +                      /* make sure the index is clean as well */
 +                      submodule_reset_index(path);
 +              }
 +      }
 +
 +      prepare_submodule_repo_env_no_git_dir(&cp.env_array);
 +
 +      cp.git_cmd = 1;
 +      cp.no_stdin = 1;
 +      cp.dir = path;
 +
 +      argv_array_pushf(&cp.args, "--super-prefix=%s%s/",
 +                      get_super_prefix_or_empty(), path);
 +      argv_array_pushl(&cp.args, "read-tree", NULL);
 +
 +      if (flags & SUBMODULE_MOVE_HEAD_DRY_RUN)
 +              argv_array_push(&cp.args, "-n");
 +      else
 +              argv_array_push(&cp.args, "-u");
 +
 +      if (flags & SUBMODULE_MOVE_HEAD_FORCE)
 +              argv_array_push(&cp.args, "--reset");
 +      else
 +              argv_array_push(&cp.args, "-m");
 +
 +      argv_array_push(&cp.args, old ? old : EMPTY_TREE_SHA1_HEX);
 +      argv_array_push(&cp.args, new ? new : EMPTY_TREE_SHA1_HEX);
 +
 +      if (run_command(&cp)) {
 +              ret = -1;
 +              goto out;
 +      }
 +
 +      if (!(flags & SUBMODULE_MOVE_HEAD_DRY_RUN)) {
 +              if (new) {
 +                      struct child_process cp1 = CHILD_PROCESS_INIT;
 +                      /* also set the HEAD accordingly */
 +                      cp1.git_cmd = 1;
 +                      cp1.no_stdin = 1;
 +                      cp1.dir = path;
 +
 +                      argv_array_pushl(&cp1.args, "update-ref", "HEAD",
 +                                       new ? new : EMPTY_TREE_SHA1_HEX, NULL);
 +
 +                      if (run_command(&cp1)) {
 +                              ret = -1;
 +                              goto out;
 +                      }
 +              } else {
 +                      struct strbuf sb = STRBUF_INIT;
 +
 +                      strbuf_addf(&sb, "%s/.git", path);
 +                      unlink_or_warn(sb.buf);
 +                      strbuf_release(&sb);
 +
 +                      if (is_empty_dir(path))
 +                              rmdir_or_warn(path);
 +              }
 +      }
 +out:
 +      return ret;
 +}
 +
  static int find_first_merges(struct object_array *result, const char *path,
                struct commit *a, struct commit *b)
  {
        memset(&rev_opts, 0, sizeof(rev_opts));
  
        /* get all revisions that merge commit a */
 -      snprintf(merged_revision, sizeof(merged_revision), "^%s",
 +      xsnprintf(merged_revision, sizeof(merged_revision), "^%s",
                        oid_to_hex(&a->object.oid));
        init_revisions(&revs, NULL);
        rev_opts.submodule = path;
@@@ -1587,6 -1382,18 +1598,6 @@@ int parallel_submodules(void
        return parallel_jobs;
  }
  
 -void prepare_submodule_repo_env(struct argv_array *out)
 -{
 -      const char * const *var;
 -
 -      for (var = local_repo_env; *var; var++) {
 -              if (strcmp(*var, CONFIG_DATA_ENVIRONMENT))
 -                      argv_array_push(out, *var);
 -      }
 -      argv_array_pushf(out, "%s=%s", GIT_DIR_ENVIRONMENT,
 -                       DEFAULT_GIT_DIR_ENVIRONMENT);
 -}
 -
  /*
   * Embeds a single submodules git directory into the superprojects git dir,
   * non recursively.
@@@ -1618,8 -1425,11 +1629,8 @@@ static void relocate_single_git_dir_int
                die(_("could not create directory '%s'"), new_git_dir);
        real_new_git_dir = real_pathdup(new_git_dir, 1);
  
 -      if (!prefix)
 -              prefix = get_super_prefix();
 -
        fprintf(stderr, _("Migrating git directory of '%s%s' from\n'%s' to\n'%s'\n"),
 -              prefix ? prefix : "", path,
 +              get_super_prefix_or_empty(), path,
                real_old_git_dir, real_new_git_dir);
  
        relocate_gitdir(path, real_old_git_dir, real_new_git_dir);
@@@ -1646,6 -1456,8 +1657,6 @@@ void absorb_git_dir_into_superproject(c
  
        /* Not populated? */
        if (!sub_git_dir) {
 -              char *real_new_git_dir;
 -              const char *new_git_dir;
                const struct submodule *sub;
  
                if (err_code == READ_GITFILE_ERR_STAT_FAILED) {
                sub = submodule_from_path(null_sha1, path);
                if (!sub)
                        die(_("could not lookup name for submodule '%s'"), path);
 -              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 = real_pathdup(new_git_dir, 1);
 -              connect_work_tree_and_git_dir(path, real_new_git_dir);
 -
 -              free(real_new_git_dir);
 +              connect_work_tree_and_git_dir(path,
 +                      git_path("modules/%s", sub->name));
        } else {
                /* Is it already absorbed into the superprojects git dir? */
                char *real_sub_git_dir = real_pathdup(sub_git_dir, 1);
                if (flags & ~ABSORB_GITDIR_RECURSE_SUBMODULES)
                        die("BUG: we don't know how to pass the flags down?");
  
 -              if (get_super_prefix())
 -                      strbuf_addstr(&sb, get_super_prefix());
 +              strbuf_addstr(&sb, get_super_prefix_or_empty());
                strbuf_addstr(&sb, path);
                strbuf_addch(&sb, '/');
  
diff --combined t/t3600-rm.sh
index 3c63455729c9bad8aae1210172225e5fb1aab229,b58793448b55c3ad339703a4bbe316c5aaf8320b..5f9913ba33d3edc848b03fc37bed587fe5c54849
@@@ -268,6 -268,14 +268,14 @@@ cat >expect.modified <<EO
   M submod
  EOF
  
+ cat >expect.modified_inside <<EOF
+  m submod
+ EOF
+ cat >expect.modified_untracked <<EOF
+  ? submod
+ EOF
  cat >expect.cached <<EOF
  D  submod
  EOF
@@@ -421,7 -429,7 +429,7 @@@ test_expect_success 'rm of a populated 
        test -d submod &&
        test -f submod/.git &&
        git status -s -uno --ignore-submodules=none >actual &&
-       test_cmp expect.modified actual &&
+       test_cmp expect.modified_inside actual &&
        git rm -f submod &&
        test ! -d submod &&
        git status -s -uno --ignore-submodules=none >actual &&
@@@ -436,7 -444,7 +444,7 @@@ test_expect_success 'rm of a populated 
        test -d submod &&
        test -f submod/.git &&
        git status -s -uno --ignore-submodules=none >actual &&
-       test_cmp expect.modified actual &&
+       test_cmp expect.modified_untracked actual &&
        git rm -f submod &&
        test ! -d submod &&
        git status -s -uno --ignore-submodules=none >actual &&
@@@ -621,7 -629,7 +629,7 @@@ test_expect_success 'rm of a populated 
        test -d submod &&
        test -f submod/.git &&
        git status -s -uno --ignore-submodules=none >actual &&
-       test_cmp expect.modified actual &&
+       test_cmp expect.modified_inside actual &&
        git rm -f submod &&
        test ! -d submod &&
        git status -s -uno --ignore-submodules=none >actual &&
@@@ -636,7 -644,7 +644,7 @@@ test_expect_success 'rm of a populated 
        test -d submod &&
        test -f submod/.git &&
        git status -s -uno --ignore-submodules=none >actual &&
-       test_cmp expect.modified actual &&
+       test_cmp expect.modified_inside actual &&
        git rm -f submod &&
        test ! -d submod &&
        git status -s -uno --ignore-submodules=none >actual &&
@@@ -651,14 -659,14 +659,14 @@@ test_expect_success 'rm of a populated 
        test -d submod &&
        test -f submod/.git &&
        git status -s -uno --ignore-submodules=none >actual &&
-       test_cmp expect.modified actual &&
+       test_cmp expect.modified_untracked actual &&
        git rm -f submod &&
        test ! -d submod &&
        git status -s -uno --ignore-submodules=none >actual &&
        test_cmp expect actual
  '
  
 -test_expect_success 'rm of a populated nested submodule with a nested .git directory fails even when forced' '
 +test_expect_success "rm absorbs submodule's nested .git directory" '
        git reset --hard &&
        git submodule update --recursive &&
        (cd submod/subsubmod &&