Merge branch 'ex/deprecate-empty-pathspec-as-match-all'
authorJunio C Hamano <gitster@pobox.com>
Mon, 6 Nov 2017 04:11:29 +0000 (13:11 +0900)
committerJunio C Hamano <gitster@pobox.com>
Mon, 6 Nov 2017 04:11:29 +0000 (13:11 +0900)
The final step to make an empty string as a pathspec element
illegal. We started this by first deprecating and warning a
pathspec that has such an element in 2.11 (Nov 2016).

Hopefully we can merge this down to the 'master' by the end of the
year? A deprecation warning period that is about 1 year does not
sound too bad.

* ex/deprecate-empty-pathspec-as-match-all:
pathspec: die on empty strings as pathspec
t0027: do not use an empty string as a pathspec element

1  2 
pathspec.c
t/t0027-auto-crlf.sh
t/t3600-rm.sh
t/t3700-add.sh
diff --combined pathspec.c
index cdefdc7cc0e0be248297d1191eefe72418923f7a,c32503d079ff7eee36b4fdc2dee48b01a2d55fea..82eb39cd679ffbce82abac691c6f4343335b9924
@@@ -1,9 -1,6 +1,9 @@@
 +#define NO_THE_INDEX_COMPATIBILITY_MACROS
  #include "cache.h"
 +#include "config.h"
  #include "dir.h"
  #include "pathspec.h"
 +#include "attr.h"
  
  /*
   * Finds which of the given pathspecs match items in the index.
@@@ -19,7 -16,6 +19,7 @@@
   * to use find_pathspecs_matching_against_index() instead.
   */
  void add_pathspec_matches_against_index(const struct pathspec *pathspec,
 +                                      const struct index_state *istate,
                                        char *seen)
  {
        int num_unmatched = 0, i;
@@@ -35,8 -31,8 +35,8 @@@
                        num_unmatched++;
        if (!num_unmatched)
                return;
 -      for (i = 0; i < active_nr; i++) {
 -              const struct cache_entry *ce = active_cache[i];
 +      for (i = 0; i < istate->cache_nr; i++) {
 +              const struct cache_entry *ce = istate->cache[i];
                ce_path_match(ce, pathspec, seen);
        }
  }
   * nature of the "closest" (i.e. most specific) matches which each of the
   * given pathspecs achieves against all items in the index.
   */
 -char *find_pathspecs_matching_against_index(const struct pathspec *pathspec)
 +char *find_pathspecs_matching_against_index(const struct pathspec *pathspec,
 +                                          const struct index_state *istate)
  {
        char *seen = xcalloc(pathspec->nr, 1);
 -      add_pathspec_matches_against_index(pathspec, seen);
 +      add_pathspec_matches_against_index(pathspec, istate, seen);
        return seen;
  }
  
@@@ -72,20 -67,20 +72,20 @@@ static struct pathspec_magic 
        char mnemonic; /* this cannot be ':'! */
        const char *name;
  } pathspec_magic[] = {
 -      { PATHSPEC_FROMTOP, '/', "top" },
 -      { PATHSPEC_LITERAL,   0, "literal" },
 -      { PATHSPEC_GLOB,   '\0', "glob" },
 -      { PATHSPEC_ICASE,  '\0', "icase" },
 -      { PATHSPEC_EXCLUDE, '!', "exclude" },
 +      { PATHSPEC_FROMTOP,  '/', "top" },
 +      { PATHSPEC_LITERAL, '\0', "literal" },
 +      { PATHSPEC_GLOB,    '\0', "glob" },
 +      { PATHSPEC_ICASE,   '\0', "icase" },
 +      { PATHSPEC_EXCLUDE,  '!', "exclude" },
 +      { PATHSPEC_ATTR,    '\0', "attr" },
  };
  
 -static void prefix_short_magic(struct strbuf *sb, int prefixlen,
 -                             unsigned short_magic)
 +static void prefix_magic(struct strbuf *sb, int prefixlen, unsigned magic)
  {
        int i;
        strbuf_addstr(sb, ":(");
        for (i = 0; i < ARRAY_SIZE(pathspec_magic); i++)
 -              if (short_magic & pathspec_magic[i].bit) {
 +              if (magic & pathspec_magic[i].bit) {
                        if (sb->buf[sb->len - 1] != '(')
                                strbuf_addch(sb, ',');
                        strbuf_addstr(sb, pathspec_magic[i].name);
        strbuf_addf(sb, ",prefix:%d)", prefixlen);
  }
  
 -/*
 - * Take an element of a pathspec and check for magic signatures.
 - * Append the result to the prefix. Return the magic bitmap.
 - *
 - * For now, we only parse the syntax and throw out anything other than
 - * "top" magic.
 - *
 - * NEEDSWORK: This needs to be rewritten when we start migrating
 - * get_pathspec() users to use the "struct pathspec" interface.  For
 - * example, a pathspec element may be marked as case-insensitive, but
 - * the prefix part must always match literally, and a single stupid
 - * string cannot express such a case.
 - */
 -static unsigned prefix_pathspec(struct pathspec_item *item,
 -                              unsigned *p_short_magic,
 -                              const char **raw, unsigned flags,
 -                              const char *prefix, int prefixlen,
 -                              const char *elt)
 +static size_t strcspn_escaped(const char *s, const char *stop)
  {
 -      static int literal_global = -1;
 -      static int glob_global = -1;
 -      static int noglob_global = -1;
 -      static int icase_global = -1;
 -      unsigned magic = 0, short_magic = 0, global_magic = 0;
 -      const char *copyfrom = elt, *long_magic_end = NULL;
 -      char *match;
 -      int i, pathspec_prefix = -1;
 +      const char *i;
 +
 +      for (i = s; *i; i++) {
 +              /* skip the escaped character */
 +              if (i[0] == '\\' && i[1]) {
 +                      i++;
 +                      continue;
 +              }
 +
 +              if (strchr(stop, *i))
 +                      break;
 +      }
 +      return i - s;
 +}
  
 -      if (literal_global < 0)
 -              literal_global = git_env_bool(GIT_LITERAL_PATHSPECS_ENVIRONMENT, 0);
 -      if (literal_global)
 +static inline int invalid_value_char(const char ch)
 +{
 +      if (isalnum(ch) || strchr(",-_", ch))
 +              return 0;
 +      return -1;
 +}
 +
 +static char *attr_value_unescape(const char *value)
 +{
 +      const char *src;
 +      char *dst, *ret;
 +
 +      ret = xmallocz(strlen(value));
 +      for (src = value, dst = ret; *src; src++, dst++) {
 +              if (*src == '\\') {
 +                      if (!src[1])
 +                              die(_("Escape character '\\' not allowed as "
 +                                    "last character in attr value"));
 +                      src++;
 +              }
 +              if (invalid_value_char(*src))
 +                      die("cannot use '%c' for value matching", *src);
 +              *dst = *src;
 +      }
 +      *dst = '\0';
 +      return ret;
 +}
 +
 +static void parse_pathspec_attr_match(struct pathspec_item *item, const char *value)
 +{
 +      struct string_list_item *si;
 +      struct string_list list = STRING_LIST_INIT_DUP;
 +
 +      if (item->attr_check || item->attr_match)
 +              die(_("Only one 'attr:' specification is allowed."));
 +
 +      if (!value || !*value)
 +              die(_("attr spec must not be empty"));
 +
 +      string_list_split(&list, value, ' ', -1);
 +      string_list_remove_empty_items(&list, 0);
 +
 +      item->attr_check = attr_check_alloc();
 +      item->attr_match = xcalloc(list.nr, sizeof(struct attr_match));
 +
 +      for_each_string_list_item(si, &list) {
 +              size_t attr_len;
 +              char *attr_name;
 +              const struct git_attr *a;
 +
 +              int j = item->attr_match_nr++;
 +              const char *attr = si->string;
 +              struct attr_match *am = &item->attr_match[j];
 +
 +              switch (*attr) {
 +              case '!':
 +                      am->match_mode = MATCH_UNSPECIFIED;
 +                      attr++;
 +                      attr_len = strlen(attr);
 +                      break;
 +              case '-':
 +                      am->match_mode = MATCH_UNSET;
 +                      attr++;
 +                      attr_len = strlen(attr);
 +                      break;
 +              default:
 +                      attr_len = strcspn(attr, "=");
 +                      if (attr[attr_len] != '=')
 +                              am->match_mode = MATCH_SET;
 +                      else {
 +                              const char *v = &attr[attr_len + 1];
 +                              am->match_mode = MATCH_VALUE;
 +                              am->value = attr_value_unescape(v);
 +                      }
 +                      break;
 +              }
 +
 +              attr_name = xmemdupz(attr, attr_len);
 +              a = git_attr(attr_name);
 +              if (!a)
 +                      die(_("invalid attribute name %s"), attr_name);
 +
 +              attr_check_append(item->attr_check, a);
 +
 +              free(attr_name);
 +      }
 +
 +      if (item->attr_check->nr != item->attr_match_nr)
 +              die("BUG: should have same number of entries");
 +
 +      string_list_clear(&list, 0);
 +}
 +
 +static inline int get_literal_global(void)
 +{
 +      static int literal = -1;
 +
 +      if (literal < 0)
 +              literal = git_env_bool(GIT_LITERAL_PATHSPECS_ENVIRONMENT, 0);
 +
 +      return literal;
 +}
 +
 +static inline int get_glob_global(void)
 +{
 +      static int glob = -1;
 +
 +      if (glob < 0)
 +              glob = git_env_bool(GIT_GLOB_PATHSPECS_ENVIRONMENT, 0);
 +
 +      return glob;
 +}
 +
 +static inline int get_noglob_global(void)
 +{
 +      static int noglob = -1;
 +
 +      if (noglob < 0)
 +              noglob = git_env_bool(GIT_NOGLOB_PATHSPECS_ENVIRONMENT, 0);
 +
 +      return noglob;
 +}
 +
 +static inline int get_icase_global(void)
 +{
 +      static int icase = -1;
 +
 +      if (icase < 0)
 +              icase = git_env_bool(GIT_ICASE_PATHSPECS_ENVIRONMENT, 0);
 +
 +      return icase;
 +}
 +
 +static int get_global_magic(int element_magic)
 +{
 +      int global_magic = 0;
 +
 +      if (get_literal_global())
                global_magic |= PATHSPEC_LITERAL;
  
 -      if (glob_global < 0)
 -              glob_global = git_env_bool(GIT_GLOB_PATHSPECS_ENVIRONMENT, 0);
 -      if (glob_global)
 +      /* --glob-pathspec is overridden by :(literal) */
 +      if (get_glob_global() && !(element_magic & PATHSPEC_LITERAL))
                global_magic |= PATHSPEC_GLOB;
  
 -      if (noglob_global < 0)
 -              noglob_global = git_env_bool(GIT_NOGLOB_PATHSPECS_ENVIRONMENT, 0);
 -
 -      if (glob_global && noglob_global)
 +      if (get_glob_global() && get_noglob_global())
                die(_("global 'glob' and 'noglob' pathspec settings are incompatible"));
  
 -
 -      if (icase_global < 0)
 -              icase_global = git_env_bool(GIT_ICASE_PATHSPECS_ENVIRONMENT, 0);
 -      if (icase_global)
 +      if (get_icase_global())
                global_magic |= PATHSPEC_ICASE;
  
        if ((global_magic & PATHSPEC_LITERAL) &&
                die(_("global 'literal' pathspec setting is incompatible "
                      "with all other global pathspec settings"));
  
 -      if (flags & PATHSPEC_LITERAL_PATH)
 -              global_magic = 0;
 +      /* --noglob-pathspec adds :(literal) _unless_ :(glob) is specified */
 +      if (get_noglob_global() && !(element_magic & PATHSPEC_GLOB))
 +              global_magic |= PATHSPEC_LITERAL;
 +
 +      return global_magic;
 +}
  
 -      if (elt[0] != ':' || literal_global ||
 -          (flags & PATHSPEC_LITERAL_PATH)) {
 -              ; /* nothing to do */
 -      } else if (elt[1] == '(') {
 -              /* longhand */
 -              const char *nextat;
 -              for (copyfrom = elt + 2;
 -                   *copyfrom && *copyfrom != ')';
 -                   copyfrom = nextat) {
 -                      size_t len = strcspn(copyfrom, ",)");
 -                      if (copyfrom[len] == ',')
 -                              nextat = copyfrom + len + 1;
 -                      else
 -                              /* handle ')' and '\0' */
 -                              nextat = copyfrom + len;
 -                      if (!len)
 -                              continue;
 -                      for (i = 0; i < ARRAY_SIZE(pathspec_magic); i++) {
 -                              if (strlen(pathspec_magic[i].name) == len &&
 -                                  !strncmp(pathspec_magic[i].name, copyfrom, len)) {
 -                                      magic |= pathspec_magic[i].bit;
 -                                      break;
 -                              }
 -                              if (starts_with(copyfrom, "prefix:")) {
 -                                      char *endptr;
 -                                      pathspec_prefix = strtol(copyfrom + 7,
 -                                                               &endptr, 10);
 -                                      if (endptr - copyfrom != len)
 -                                              die(_("invalid parameter for pathspec magic 'prefix'"));
 -                                      /* "i" would be wrong, but it does not matter */
 -                                      break;
 -                              }
 +/*
 + * Parse the pathspec element looking for long magic
 + *
 + * saves all magic in 'magic'
 + * if prefix magic is used, save the prefix length in 'prefix_len'
 + * returns the position in 'elem' after all magic has been parsed
 + */
 +static const char *parse_long_magic(unsigned *magic, int *prefix_len,
 +                                  struct pathspec_item *item,
 +                                  const char *elem)
 +{
 +      const char *pos;
 +      const char *nextat;
 +
 +      for (pos = elem + 2; *pos && *pos != ')'; pos = nextat) {
 +              size_t len = strcspn_escaped(pos, ",)");
 +              int i;
 +
 +              if (pos[len] == ',')
 +                      nextat = pos + len + 1; /* handle ',' */
 +              else
 +                      nextat = pos + len; /* handle ')' and '\0' */
 +
 +              if (!len)
 +                      continue;
 +
 +              if (starts_with(pos, "prefix:")) {
 +                      char *endptr;
 +                      *prefix_len = strtol(pos + 7, &endptr, 10);
 +                      if (endptr - pos != len)
 +                              die(_("invalid parameter for pathspec magic 'prefix'"));
 +                      continue;
 +              }
 +
 +              if (starts_with(pos, "attr:")) {
 +                      char *attr_body = xmemdupz(pos + 5, len - 5);
 +                      parse_pathspec_attr_match(item, attr_body);
 +                      *magic |= PATHSPEC_ATTR;
 +                      free(attr_body);
 +                      continue;
 +              }
 +
 +              for (i = 0; i < ARRAY_SIZE(pathspec_magic); i++) {
 +                      if (strlen(pathspec_magic[i].name) == len &&
 +                          !strncmp(pathspec_magic[i].name, pos, len)) {
 +                              *magic |= pathspec_magic[i].bit;
 +                              break;
                        }
 -                      if (ARRAY_SIZE(pathspec_magic) <= i)
 -                              die(_("Invalid pathspec magic '%.*s' in '%s'"),
 -                                  (int) len, copyfrom, elt);
                }
 -              if (*copyfrom != ')')
 -                      die(_("Missing ')' at the end of pathspec magic in '%s'"), elt);
 -              long_magic_end = copyfrom;
 -              copyfrom++;
 -      } else {
 -              /* shorthand */
 -              for (copyfrom = elt + 1;
 -                   *copyfrom && *copyfrom != ':';
 -                   copyfrom++) {
 -                      char ch = *copyfrom;
  
 -                      if (!is_pathspec_magic(ch))
 +              if (ARRAY_SIZE(pathspec_magic) <= i)
 +                      die(_("Invalid pathspec magic '%.*s' in '%s'"),
 +                          (int) len, pos, elem);
 +      }
 +
 +      if (*pos != ')')
 +              die(_("Missing ')' at the end of pathspec magic in '%s'"),
 +                  elem);
 +      pos++;
 +
 +      return pos;
 +}
 +
 +/*
 + * Parse the pathspec element looking for short magic
 + *
 + * saves all magic in 'magic'
 + * returns the position in 'elem' after all magic has been parsed
 + */
 +static const char *parse_short_magic(unsigned *magic, const char *elem)
 +{
 +      const char *pos;
 +
 +      for (pos = elem + 1; *pos && *pos != ':'; pos++) {
 +              char ch = *pos;
 +              int i;
 +
 +              /* Special case alias for '!' */
 +              if (ch == '^') {
 +                      *magic |= PATHSPEC_EXCLUDE;
 +                      continue;
 +              }
 +
 +              if (!is_pathspec_magic(ch))
 +                      break;
 +
 +              for (i = 0; i < ARRAY_SIZE(pathspec_magic); i++) {
 +                      if (pathspec_magic[i].mnemonic == ch) {
 +                              *magic |= pathspec_magic[i].bit;
                                break;
 -                      for (i = 0; i < ARRAY_SIZE(pathspec_magic); i++)
 -                              if (pathspec_magic[i].mnemonic == ch) {
 -                                      short_magic |= pathspec_magic[i].bit;
 -                                      break;
 -                              }
 -                      if (ARRAY_SIZE(pathspec_magic) <= i)
 -                              die(_("Unimplemented pathspec magic '%c' in '%s'"),
 -                                  ch, elt);
 +                      }
                }
 -              if (*copyfrom == ':')
 -                      copyfrom++;
 +
 +              if (ARRAY_SIZE(pathspec_magic) <= i)
 +                      die(_("Unimplemented pathspec magic '%c' in '%s'"),
 +                          ch, elem);
        }
  
 -      magic |= short_magic;
 -      *p_short_magic = short_magic;
 +      if (*pos == ':')
 +              pos++;
  
 -      /* --noglob-pathspec adds :(literal) _unless_ :(glob) is specified */
 -      if (noglob_global && !(magic & PATHSPEC_GLOB))
 -              global_magic |= PATHSPEC_LITERAL;
 +      return pos;
 +}
  
 -      /* --glob-pathspec is overridden by :(literal) */
 -      if ((global_magic & PATHSPEC_GLOB) && (magic & PATHSPEC_LITERAL))
 -              global_magic &= ~PATHSPEC_GLOB;
 +static const char *parse_element_magic(unsigned *magic, int *prefix_len,
 +                                     struct pathspec_item *item,
 +                                     const char *elem)
 +{
 +      if (elem[0] != ':' || get_literal_global())
 +              return elem; /* nothing to do */
 +      else if (elem[1] == '(')
 +              /* longhand */
 +              return parse_long_magic(magic, prefix_len, item, elem);
 +      else
 +              /* shorthand */
 +              return parse_short_magic(magic, elem);
 +}
 +
 +/*
 + * Perform the initialization of a pathspec_item based on a pathspec element.
 + */
 +static void init_pathspec_item(struct pathspec_item *item, unsigned flags,
 +                             const char *prefix, int prefixlen,
 +                             const char *elt)
 +{
 +      unsigned magic = 0, element_magic = 0;
 +      const char *copyfrom = elt;
 +      char *match;
 +      int pathspec_prefix = -1;
 +
 +      item->attr_check = NULL;
 +      item->attr_match = NULL;
 +      item->attr_match_nr = 0;
 +
 +      /* PATHSPEC_LITERAL_PATH ignores magic */
 +      if (flags & PATHSPEC_LITERAL_PATH) {
 +              magic = PATHSPEC_LITERAL;
 +      } else {
 +              copyfrom = parse_element_magic(&element_magic,
 +                                             &pathspec_prefix,
 +                                             item,
 +                                             elt);
 +              magic |= element_magic;
 +              magic |= get_global_magic(element_magic);
 +      }
  
 -      magic |= global_magic;
 +      item->magic = magic;
  
        if (pathspec_prefix >= 0 &&
            (prefixlen || (prefix && *prefix)))
        if ((magic & PATHSPEC_LITERAL) && (magic & PATHSPEC_GLOB))
                die(_("%s: 'literal' and 'glob' are incompatible"), elt);
  
 +      /* Create match string which will be used for pathspec matching */
        if (pathspec_prefix >= 0) {
                match = xstrdup(copyfrom);
                prefixlen = pathspec_prefix;
                match = xstrdup(copyfrom);
                prefixlen = 0;
        } else {
 -              match = prefix_path_gently(prefix, prefixlen, &prefixlen, copyfrom);
 +              match = prefix_path_gently(prefix, prefixlen,
 +                                         &prefixlen, copyfrom);
                if (!match)
                        die(_("%s: '%s' is outside repository"), elt, copyfrom);
        }
 -      *raw = item->match = match;
 +
 +      item->match = match;
 +      item->len = strlen(item->match);
 +      item->prefix = prefixlen;
 +
        /*
         * Prefix the pathspec (keep all magic) and assign to
         * original. Useful for passing to another command.
         */
 -      if (flags & PATHSPEC_PREFIX_ORIGIN) {
 +      if ((flags & PATHSPEC_PREFIX_ORIGIN) &&
 +          !get_literal_global()) {
                struct strbuf sb = STRBUF_INIT;
 -              if (prefixlen && !literal_global) {
 -                      /* Preserve the actual prefix length of each pattern */
 -                      if (short_magic)
 -                              prefix_short_magic(&sb, prefixlen, short_magic);
 -                      else if (long_magic_end) {
 -                              strbuf_add(&sb, elt, long_magic_end - elt);
 -                              strbuf_addf(&sb, ",prefix:%d)", prefixlen);
 -                      } else
 -                              strbuf_addf(&sb, ":(prefix:%d)", prefixlen);
 -              }
 +
 +              /* Preserve the actual prefix length of each pattern */
 +              prefix_magic(&sb, prefixlen, element_magic);
 +
                strbuf_addstr(&sb, match);
                item->original = strbuf_detach(&sb, NULL);
 -      } else
 -              item->original = elt;
 -      item->len = strlen(item->match);
 -      item->prefix = prefixlen;
 -
 -      if ((flags & PATHSPEC_STRIP_SUBMODULE_SLASH_CHEAP) &&
 -          (item->len >= 1 && item->match[item->len - 1] == '/') &&
 -          (i = cache_name_pos(item->match, item->len - 1)) >= 0 &&
 -          S_ISGITLINK(active_cache[i]->ce_mode)) {
 -              item->len--;
 -              match[item->len] = '\0';
 +      } else {
 +              item->original = xstrdup(elt);
        }
  
 -      if (flags & PATHSPEC_STRIP_SUBMODULE_SLASH_EXPENSIVE)
 -              for (i = 0; i < active_nr; i++) {
 -                      struct cache_entry *ce = active_cache[i];
 -                      int ce_len = ce_namelen(ce);
 -
 -                      if (!S_ISGITLINK(ce->ce_mode))
 -                              continue;
 -
 -                      if (item->len <= ce_len || match[ce_len] != '/' ||
 -                          memcmp(ce->name, match, ce_len))
 -                              continue;
 -                      if (item->len == ce_len + 1) {
 -                              /* strip trailing slash */
 -                              item->len--;
 -                              match[item->len] = '\0';
 -                      } else
 -                              die (_("Pathspec '%s' is in submodule '%.*s'"),
 -                                   elt, ce_len, ce->name);
 -              }
 -
 -      if (magic & PATHSPEC_LITERAL)
 +      if (magic & PATHSPEC_LITERAL) {
                item->nowildcard_len = item->len;
 -      else {
 +      else {
                item->nowildcard_len = simple_length(item->match);
                if (item->nowildcard_len < prefixlen)
                        item->nowildcard_len = prefixlen;
        }
 +
        item->flags = 0;
        if (magic & PATHSPEC_GLOB) {
                /*
        }
  
        /* sanity checks, pathspec matchers assume these are sane */
 -      assert(item->nowildcard_len <= item->len &&
 -             item->prefix         <= item->len);
 -      return magic;
 +      if (item->nowildcard_len > item->len ||
 +          item->prefix         > item->len) {
 +              die ("BUG: error initializing pathspec_item");
 +      }
  }
  
  static int pathspec_item_cmp(const void *a_, const void *b_)
  }
  
  static void NORETURN unsupported_magic(const char *pattern,
 -                                     unsigned magic,
 -                                     unsigned short_magic)
 +                                     unsigned magic)
  {
        struct strbuf sb = STRBUF_INIT;
 -      int i, n;
 -      for (n = i = 0; i < ARRAY_SIZE(pathspec_magic); i++) {
 +      int i;
 +      for (i = 0; i < ARRAY_SIZE(pathspec_magic); i++) {
                const struct pathspec_magic *m = pathspec_magic + i;
                if (!(magic & m->bit))
                        continue;
                if (sb.len)
 -                      strbuf_addch(&sb, ' ');
 -              if (short_magic & m->bit)
 -                      strbuf_addf(&sb, "'%c'", m->mnemonic);
 +                      strbuf_addstr(&sb, ", ");
 +
 +              if (m->mnemonic)
 +                      strbuf_addf(&sb, _("'%s' (mnemonic: '%c')"),
 +                                  m->name, m->mnemonic);
                else
                        strbuf_addf(&sb, "'%s'", m->name);
 -              n++;
        }
        /*
         * We may want to substitute "this command" with a command
            pattern, sb.buf);
  }
  
 -/*
 - * Given command line arguments and a prefix, convert the input to
 - * pathspec. die() if any magic in magic_mask is used.
 - */
  void parse_pathspec(struct pathspec *pathspec,
                    unsigned magic_mask, unsigned flags,
                    const char *prefix, const char **argv)
  {
        struct pathspec_item *item;
        const char *entry = argv ? *argv : NULL;
-       int i, n, prefixlen, warn_empty_string, nr_exclude = 0;
+       int i, n, prefixlen, nr_exclude = 0;
  
        memset(pathspec, 0, sizeof(*pathspec));
  
  
        /* No arguments with prefix -> prefix pathspec */
        if (!entry) {
 -              static const char *raw[2];
 -
                if (flags & PATHSPEC_PREFER_FULL)
                        return;
  
                        die("BUG: PATHSPEC_PREFER_CWD requires arguments");
  
                pathspec->items = item = xcalloc(1, sizeof(*item));
 -              item->match = prefix;
 -              item->original = prefix;
 +              item->match = xstrdup(prefix);
 +              item->original = xstrdup(prefix);
                item->nowildcard_len = item->len = strlen(prefix);
                item->prefix = item->len;
 -              raw[0] = prefix;
 -              raw[1] = NULL;
                pathspec->nr = 1;
 -              pathspec->_raw = raw;
                return;
        }
  
        n = 0;
-       warn_empty_string = 1;
        while (argv[n]) {
-               if (*argv[n] == '\0' && warn_empty_string) {
-                       warning(_("empty strings as pathspecs will be made invalid in upcoming releases. "
-                                 "please use . instead if you meant to match all paths"));
-                       warn_empty_string = 0;
-               }
+               if (*argv[n] == '\0')
+                       die("empty string is not a valid pathspec. "
+                                 "please use . instead if you meant to match all paths");
                n++;
        }
  
        pathspec->nr = n;
 -      ALLOC_ARRAY(pathspec->items, n);
 +      ALLOC_ARRAY(pathspec->items, n + 1);
        item = pathspec->items;
 -      pathspec->_raw = argv;
        prefixlen = prefix ? strlen(prefix) : 0;
  
        for (i = 0; i < n; i++) {
 -              unsigned short_magic;
                entry = argv[i];
  
 -              item[i].magic = prefix_pathspec(item + i, &short_magic,
 -                                              argv + i, flags,
 -                                              prefix, prefixlen, entry);
 -              if ((flags & PATHSPEC_LITERAL_PATH) &&
 -                  !(magic_mask & PATHSPEC_LITERAL))
 -                      item[i].magic |= PATHSPEC_LITERAL;
 +              init_pathspec_item(item + i, flags, prefix, prefixlen, entry);
 +
                if (item[i].magic & PATHSPEC_EXCLUDE)
                        nr_exclude++;
                if (item[i].magic & magic_mask)
 -                      unsupported_magic(entry,
 -                                        item[i].magic & magic_mask,
 -                                        short_magic);
 +                      unsupported_magic(entry, item[i].magic & magic_mask);
  
                if ((flags & PATHSPEC_SYMLINK_LEADING_PATH) &&
                    has_symlink_leading_path(item[i].match, item[i].len)) {
                pathspec->magic |= item[i].magic;
        }
  
 -      if (nr_exclude == n)
 -              die(_("There is nothing to exclude from by :(exclude) patterns.\n"
 -                    "Perhaps you forgot to add either ':/' or '.' ?"));
 -
 +      /*
 +       * If everything is an exclude pattern, add one positive pattern
 +       * that matches everything. We allocated an extra one for this.
 +       */
 +      if (nr_exclude == n) {
 +              int plen = (!(flags & PATHSPEC_PREFER_CWD)) ? 0 : prefixlen;
 +              init_pathspec_item(item + n, 0, prefix, plen, "");
 +              pathspec->nr++;
 +      }
  
        if (pathspec->magic & PATHSPEC_MAXDEPTH) {
                if (flags & PATHSPEC_KEEP_ORDER)
                        die("BUG: PATHSPEC_MAXDEPTH_VALID and PATHSPEC_KEEP_ORDER are incompatible");
 -              qsort(pathspec->items, pathspec->nr,
 -                    sizeof(struct pathspec_item), pathspec_item_cmp);
 +              QSORT(pathspec->items, pathspec->nr, pathspec_item_cmp);
        }
  }
  
 -/*
 - * N.B. get_pathspec() is deprecated in favor of the "struct pathspec"
 - * based interface - see pathspec.c:parse_pathspec().
 - *
 - * Arguments:
 - *  - prefix - a path relative to the root of the working tree
 - *  - pathspec - a list of paths underneath the prefix path
 - *
 - * Iterates over pathspec, prepending each path with prefix,
 - * and return the resulting list.
 - *
 - * If pathspec is empty, return a singleton list containing prefix.
 - *
 - * If pathspec and prefix are both empty, return an empty list.
 - *
 - * This is typically used by built-in commands such as add.c, in order
 - * to normalize argv arguments provided to the built-in into a list of
 - * paths to process, all relative to the root of the working tree.
 - */
 -const char **get_pathspec(const char *prefix, const char **pathspec)
 -{
 -      struct pathspec ps;
 -      parse_pathspec(&ps,
 -                     PATHSPEC_ALL_MAGIC &
 -                     ~(PATHSPEC_FROMTOP | PATHSPEC_LITERAL),
 -                     PATHSPEC_PREFER_CWD,
 -                     prefix, pathspec);
 -      return ps._raw;
 -}
 -
  void copy_pathspec(struct pathspec *dst, const struct pathspec *src)
  {
 +      int i, j;
 +
        *dst = *src;
        ALLOC_ARRAY(dst->items, dst->nr);
 -      memcpy(dst->items, src->items,
 -             sizeof(struct pathspec_item) * dst->nr);
 +      COPY_ARRAY(dst->items, src->items, dst->nr);
 +
 +      for (i = 0; i < dst->nr; i++) {
 +              struct pathspec_item *d = &dst->items[i];
 +              struct pathspec_item *s = &src->items[i];
 +
 +              d->match = xstrdup(s->match);
 +              d->original = xstrdup(s->original);
 +
 +              ALLOC_ARRAY(d->attr_match, d->attr_match_nr);
 +              COPY_ARRAY(d->attr_match, s->attr_match, d->attr_match_nr);
 +              for (j = 0; j < d->attr_match_nr; j++) {
 +                      const char *value = s->attr_match[j].value;
 +                      d->attr_match[j].value = xstrdup_or_null(value);
 +              }
 +
 +              d->attr_check = attr_check_dup(s->attr_check);
 +      }
  }
  
 -void free_pathspec(struct pathspec *pathspec)
 +void clear_pathspec(struct pathspec *pathspec)
  {
 -      free(pathspec->items);
 -      pathspec->items = NULL;
 +      int i, j;
 +
 +      for (i = 0; i < pathspec->nr; i++) {
 +              free(pathspec->items[i].match);
 +              free(pathspec->items[i].original);
 +
 +              for (j = 0; j < pathspec->items[i].attr_match_nr; j++)
 +                      free(pathspec->items[i].attr_match[j].value);
 +              free(pathspec->items[i].attr_match);
 +
 +              if (pathspec->items[i].attr_check)
 +                      attr_check_free(pathspec->items[i].attr_check);
 +      }
 +
 +      FREE_AND_NULL(pathspec->items);
 +      pathspec->nr = 0;
  }
diff --combined t/t0027-auto-crlf.sh
index deb3ae7813052d01b6dab92586e2c37d313ef8ff,e41c9b3bb26ffdfa66a57fa2d01701803182c890..68108d956a3f65c868b08ecb81bae87e9c1f5e67
@@@ -4,6 -4,12 +4,6 @@@ test_description='CRLF conversion all c
  
  . ./test-lib.sh
  
 -if ! test_have_prereq EXPENSIVE
 -then
 -      skip_all="EXPENSIVE not set"
 -      test_done
 -fi
 -
  compare_files () {
        tr '\015\000' QN <"$1" >"$1".expect &&
        tr '\015\000' QN <"$2" | tr -d 'Z' >"$2".actual &&
@@@ -69,7 -75,7 +69,7 @@@ check_warning () 
        *) echo >&2 "Illegal 1": "$1" ; return false ;;
        esac
        grep "will be replaced by" "$2" | sed -e "s/\(.*\) in [^ ]*$/\1/" | uniq  >"$2".actual
 -      test_cmp "$2".expect "$2".actual
 +      test_i18ncmp "$2".expect "$2".actual
  }
  
  commit_check_warn () {
@@@ -113,7 -119,8 +113,7 @@@ commit_chk_wrnNNO () 
                fname=${pfx}_$f.txt &&
                cp $f $fname &&
                printf Z >>"$fname" &&
 -              git -c core.autocrlf=$crlf add $fname 2>/dev/null &&
 -              git -c core.autocrlf=$crlf commit -m "commit_$fname" $fname >"${pfx}_$f.err" 2>&1
 +              git -c core.autocrlf=$crlf add $fname 2>"${pfx}_$f.err"
        done
  
        test_expect_success "commit NNO files crlf=$crlf attr=$attr LF" '
@@@ -168,8 -175,8 +168,8 @@@ attr_ascii () 
        text,lf)   echo "text eol=lf" ;;
        text,crlf) echo "text eol=crlf" ;;
        auto,)     echo "text=auto" ;;
 -      auto,lf)   echo "text eol=lf" ;;
 -      auto,crlf) echo "text eol=crlf" ;;
 +      auto,lf)   echo "text=auto eol=lf" ;;
 +      auto,crlf) echo "text=auto eol=crlf" ;;
        lf,)       echo "text eol=lf" ;;
        crlf,)     echo "text eol=crlf" ;;
        ,) echo "" ;;
@@@ -315,7 -322,7 +315,7 @@@ test_expect_success 'setup master' 
        echo >.gitattributes &&
        git checkout -b master &&
        git add .gitattributes &&
-       git commit -m "add .gitattributes" "" &&
+       git commit -m "add .gitattributes" . &&
        printf "\$Id: 0000000000000000000000000000000000000000 \$\nLINEONE\nLINETWO\nLINETHREE"     >LF &&
        printf "\$Id: 0000000000000000000000000000000000000000 \$\r\nLINEONE\r\nLINETWO\r\nLINETHREE" >CRLF &&
        printf "\$Id: 0000000000000000000000000000000000000000 \$\nLINEONE\r\nLINETWO\nLINETHREE"   >CRLF_mix_LF &&
@@@ -390,9 -397,10 +390,9 @@@ commit_chk_wrnNNO ""      ""      fals
  commit_chk_wrnNNO ""      ""      true    LF_CRLF   ""        ""          ""          ""
  commit_chk_wrnNNO ""      ""      input   ""        ""        ""          ""          ""
  
 -commit_chk_wrnNNO "auto"  ""      false   "$WILC"   "$WICL"   "$WAMIX"    ""          ""
 -commit_chk_wrnNNO "auto"  ""      true    LF_CRLF   ""        LF_CRLF     ""          ""
 -commit_chk_wrnNNO "auto"  ""      input   ""        CRLF_LF   CRLF_LF     ""          ""
 -
 +commit_chk_wrnNNO "auto"  ""      false   "$WILC"   ""        ""          ""          ""
 +commit_chk_wrnNNO "auto"  ""      true    LF_CRLF   ""        ""          ""          ""
 +commit_chk_wrnNNO "auto"  ""      input   ""        ""        ""          ""          ""
  for crlf in true false input
  do
        commit_chk_wrnNNO -text ""      $crlf   ""        ""        ""          ""          ""
        commit_chk_wrnNNO -text crlf    $crlf   ""        ""        ""          ""          ""
        commit_chk_wrnNNO ""    lf      $crlf   ""       CRLF_LF    CRLF_LF      ""         CRLF_LF
        commit_chk_wrnNNO ""    crlf    $crlf   LF_CRLF   ""        LF_CRLF     LF_CRLF     ""
 -      commit_chk_wrnNNO auto  lf      $crlf   ""       CRLF_LF    CRLF_LF     ""          CRLF_LF
 -      commit_chk_wrnNNO auto  crlf    $crlf   LF_CRLF   ""        LF_CRLF     LF_CRLF     ""
 +      commit_chk_wrnNNO auto  lf      $crlf   ""        ""        ""          ""          ""
 +      commit_chk_wrnNNO auto  crlf    $crlf   LF_CRLF   ""        ""          ""          ""
        commit_chk_wrnNNO text  lf      $crlf   ""       CRLF_LF    CRLF_LF     ""          CRLF_LF
        commit_chk_wrnNNO text  crlf    $crlf   LF_CRLF   ""        LF_CRLF     LF_CRLF     ""
  done
@@@ -410,8 -418,7 +410,8 @@@ commit_chk_wrnNNO "text"  ""      fals
  commit_chk_wrnNNO "text"  ""      true    LF_CRLF   ""        LF_CRLF     LF_CRLF     ""
  commit_chk_wrnNNO "text"  ""      input   ""        CRLF_LF   CRLF_LF     ""          CRLF_LF
  
 -test_expect_success 'create files cleanup' '
 +test_expect_success 'commit NNO and cleanup' '
 +      git commit -m "commit files on top of NNO" &&
        rm -f *.txt &&
        git -c core.autocrlf=false reset --hard
  '
@@@ -447,9 -454,9 +447,9 @@@ d
        check_in_repo_NNO -text ""     $crlf   LF  CRLF  CRLF_mix_LF  LF_mix_CR  CRLF_nul
        check_in_repo_NNO -text lf     $crlf   LF  CRLF  CRLF_mix_LF  LF_mix_CR  CRLF_nul
        check_in_repo_NNO -text crlf   $crlf   LF  CRLF  CRLF_mix_LF  LF_mix_CR  CRLF_nul
 -      check_in_repo_NNO auto  ""     $crlf   LF  LF    LF           LF_mix_CR  CRLF_nul
 -      check_in_repo_NNO auto  lf     $crlf   LF  LF    LF           LF_mix_CR  LF_nul
 -      check_in_repo_NNO auto  crlf   $crlf   LF  LF    LF           LF_mix_CR  LF_nul
 +      check_in_repo_NNO auto  ""     $crlf   LF  CRLF  CRLF_mix_LF  LF_mix_CR  CRLF_nul
 +      check_in_repo_NNO auto  lf     $crlf   LF  CRLF  CRLF_mix_LF  LF_mix_CR  CRLF_nul
 +      check_in_repo_NNO auto  crlf   $crlf   LF  CRLF  CRLF_mix_LF  LF_mix_CR  CRLF_nul
        check_in_repo_NNO text  ""     $crlf   LF  LF    LF           LF_mix_CR  LF_nul
        check_in_repo_NNO text  lf     $crlf   LF  LF    LF           LF_mix_CR  LF_nul
        check_in_repo_NNO text  crlf   $crlf   LF  LF    LF           LF_mix_CR  LF_nul
@@@ -502,7 -509,7 +502,7 @@@ d
                        checkout_files text  "$id" "crlf" "$crlf" "$ceol"  CRLF  CRLF  CRLF         CRLF_mix_CR  CRLF_nul
                        # currently the same as text, eol=XXX
                        checkout_files auto  "$id" "lf"   "$crlf" "$ceol"  LF    CRLF  CRLF_mix_LF  LF_mix_CR    LF_nul
 -                      checkout_files auto  "$id" "crlf" "$crlf" "$ceol"  CRLF  CRLF  CRLF         CRLF_mix_CR  CRLF_nul
 +                      checkout_files auto  "$id" "crlf" "$crlf" "$ceol"  CRLF  CRLF  CRLF_mix_LF  LF_mix_CR    LF_nul
                done
  
                # core.autocrlf false, different core.eol
                # core.autocrlf true
                checkout_files   ""    "$id" ""     true    "$ceol"  CRLF  CRLF  CRLF_mix_LF  LF_mix_CR    LF_nul
                # text: core.autocrlf = true overrides core.eol
 -              checkout_files   auto  "$id" ""     true    "$ceol"  CRLF  CRLF  CRLF         LF_mix_CR    LF_nul
 +              checkout_files   auto  "$id" ""     true    "$ceol"  CRLF  CRLF  CRLF_mix_LF  LF_mix_CR    LF_nul
                checkout_files   text  "$id" ""     true    "$ceol"  CRLF  CRLF  CRLF         CRLF_mix_CR  CRLF_nul
                # text: core.autocrlf = input overrides core.eol
                checkout_files   text  "$id" ""     input   "$ceol"  LF    CRLF  CRLF_mix_LF  LF_mix_CR    LF_nul
        checkout_files     text  "$id" ""     false   ""       $NL   CRLF  $MIX_CRLF_LF $MIX_LF_CR   $LFNUL
        checkout_files     text  "$id" ""     false   native   $NL   CRLF  $MIX_CRLF_LF $MIX_LF_CR   $LFNUL
        # auto: core.autocrlf=false and core.eol unset(or native) uses native eol
 -      checkout_files     auto  "$id" ""     false   ""       $NL   CRLF  $MIX_CRLF_LF LF_mix_CR    LF_nul
 -      checkout_files     auto  "$id" ""     false   native   $NL   CRLF  $MIX_CRLF_LF LF_mix_CR    LF_nul
 +      checkout_files     auto  "$id" ""     false   ""       $NL   CRLF  CRLF_mix_LF  LF_mix_CR    LF_nul
 +      checkout_files     auto  "$id" ""     false   native   $NL   CRLF  CRLF_mix_LF  LF_mix_CR    LF_nul
  done
  
  # Should be the last test case: remove some files from the worktree
diff --combined t/t3600-rm.sh
index f8568f8841d34d17f3d8fdae4d8d2404a6693c4d,44624605e3c29cb31e06ddd8ea13a6cb86dd0b10..81c6059a2d9fe4d23e3fac11d765ecaf20aef756
@@@ -97,9 -97,9 +97,9 @@@ test_expect_success FUNNYNAMES 
  embedded'"
  
  test_expect_success SANITY 'Test that "git rm -f" fails if its rm fails' '
 +      test_when_finished "chmod 775 ." &&
        chmod a-w . &&
 -      test_must_fail git rm -f baz &&
 -      chmod 775 .
 +      test_must_fail git rm -f baz
  '
  
  test_expect_success \
@@@ -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 .. &&
@@@ -268,14 -268,6 +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
@@@ -295,7 -287,7 +295,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
@@@ -306,7 -298,7 +306,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
@@@ -317,7 -309,7 +317,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
@@@ -328,7 -320,7 +328,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
  '
  
@@@ -343,15 -335,17 +343,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
@@@ -424,30 -418,34 +424,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 &&
 -      test_cmp expect.modified actual &&
 +      git status -s -uno --ignore-submodules=none >actual &&
 +      test_cmp expect.modified_inside 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 &&
 -      test_cmp expect.modified actual &&
 +      git status -s -uno --ignore-submodules=none >actual &&
 +      test_cmp expect.modified_untracked 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
  '
  
@@@ -463,12 -461,16 +463,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"
  '
@@@ -484,7 -486,7 +484,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
  '
  
@@@ -492,16 -494,18 +492,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
@@@ -511,16 -515,18 +511,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
@@@ -530,16 -536,18 +530,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
  '
  
@@@ -556,12 -564,12 +556,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
@@@ -573,26 -581,30 +573,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 status -s -uno --ignore-submodules=none > actual &&
 -      ! test -s actual &&
 -      rm -rf submod
 +      git rm submod 2>output.err &&
 +      ! test -d submod &&
 +      ! test -d submod/.git &&
 +      git status -s -uno --ignore-submodules=none >actual &&
 +      test -s actual &&
 +      test_i18ngrep Migrating output.err
  '
  
  cat >expect.deepmodified <<EOF
@@@ -617,83 -629,94 +617,83 @@@ 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 &&
 -      test_cmp expect.modified actual &&
 +      git status -s -uno --ignore-submodules=none >actual &&
 +      test_cmp expect.modified_inside 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 &&
 -      test_cmp expect.modified actual &&
 +      git status -s -uno --ignore-submodules=none >actual &&
 +      test_cmp expect.modified_inside 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 &&
 -      test_cmp expect.modified actual &&
 +      git status -s -uno --ignore-submodules=none >actual &&
 +      test_cmp expect.modified_untracked 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 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 &&
                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 status -s -uno --ignore-submodules=none > actual &&
 -      ! test -s actual &&
 -      rm -rf submod
 +      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 &&
 +      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
  '
  
@@@ -858,9 -881,8 +858,8 @@@ test_expect_success 'rm files with two 
        test_i18ncmp expect actual
  '
  
- test_expect_success 'rm empty string should invoke warning' '
-       git rm -rf "" 2>output &&
-       test_i18ngrep "warning: empty strings" output
+ test_expect_success 'rm empty string should fail' '
+       test_must_fail git rm -rf ""
  '
  
  test_done
diff --combined t/t3700-add.sh
index 0aae21d6984bf0bd5c7c7de425a021da6122beee,6357d6e74e6e50876924574aa30578c7bb411577..2748805642201d7c514792bab8d8b3940fb4086c
@@@ -7,20 -7,6 +7,20 @@@ test_description='Test of git add, incl
  
  . ./test-lib.sh
  
 +# Test the file mode "$1" of the file "$2" in the index.
 +test_mode_in_index () {
 +      case "$(git ls-files -s "$2")" in
 +      "$1 "*" $2")
 +              echo pass
 +              ;;
 +      *)
 +              echo fail
 +              git ls-files -s "$2"
 +              return 1
 +              ;;
 +      esac
 +}
 +
  test_expect_success \
      'Test of git add' \
      'touch foo && git add foo'
@@@ -39,12 -25,18 +39,12 @@@ test_expect_success 
         echo foo >xfoo1 &&
         chmod 755 xfoo1 &&
         git add xfoo1 &&
 -       case "$(git ls-files --stage xfoo1)" in
 -       100644" "*xfoo1) echo pass;;
 -       *) echo fail; git ls-files --stage xfoo1; (exit 1);;
 -       esac'
 +       test_mode_in_index 100644 xfoo1'
  
  test_expect_success 'git add: filemode=0 should not get confused by symlink' '
        rm -f xfoo1 &&
        test_ln_s_add foo xfoo1 &&
 -      case "$(git ls-files --stage xfoo1)" in
 -      120000" "*xfoo1) echo pass;;
 -      *) echo fail; git ls-files --stage xfoo1; (exit 1);;
 -      esac
 +      test_mode_in_index 120000 xfoo1
  '
  
  test_expect_success \
         echo foo >xfoo2 &&
         chmod 755 xfoo2 &&
         git update-index --add xfoo2 &&
 -       case "$(git ls-files --stage xfoo2)" in
 -       100644" "*xfoo2) echo pass;;
 -       *) echo fail; git ls-files --stage xfoo2; (exit 1);;
 -       esac'
 +       test_mode_in_index 100644 xfoo2'
  
  test_expect_success 'git add: filemode=0 should not get confused by symlink' '
        rm -f xfoo2 &&
        test_ln_s_add foo xfoo2 &&
 -      case "$(git ls-files --stage xfoo2)" in
 -      120000" "*xfoo2) echo pass;;
 -      *) echo fail; git ls-files --stage xfoo2; (exit 1);;
 -      esac
 +      test_mode_in_index 120000 xfoo2
  '
  
  test_expect_success \
        'git update-index --add: Test that executable bit is not used...' \
        'git config core.filemode 0 &&
         test_ln_s_add xfoo2 xfoo3 &&   # runs git update-index --add
 -       case "$(git ls-files --stage xfoo3)" in
 -       120000" "*xfoo3) echo pass;;
 -       *) echo fail; git ls-files --stage xfoo3; (exit 1);;
 -       esac'
 +       test_mode_in_index 120000 xfoo3'
  
  test_expect_success '.gitignore test setup' '
        echo "*.ig" >.gitignore &&
@@@ -331,77 -332,8 +331,76 @@@ test_expect_success 'git add --dry-run 
        test_i18ncmp expect.err actual.err
  '
  
- test_expect_success 'git add empty string should invoke warning' '
-       git add "" 2>output &&
-       test_i18ngrep "warning: empty strings" output
+ test_expect_success 'git add empty string should fail' '
+       test_must_fail git add ""
  '
  
 +test_expect_success 'git add --chmod=[+-]x stages correctly' '
 +      rm -f foo1 &&
 +      echo foo >foo1 &&
 +      git add --chmod=+x foo1 &&
 +      test_mode_in_index 100755 foo1 &&
 +      git add --chmod=-x foo1 &&
 +      test_mode_in_index 100644 foo1
 +'
 +
 +test_expect_success POSIXPERM,SYMLINKS 'git add --chmod=+x with symlinks' '
 +      git config core.filemode 1 &&
 +      git config core.symlinks 1 &&
 +      rm -f foo2 &&
 +      echo foo >foo2 &&
 +      git add --chmod=+x foo2 &&
 +      test_mode_in_index 100755 foo2
 +'
 +
 +test_expect_success 'git add --chmod=[+-]x changes index with already added file' '
 +      rm -f foo3 xfoo3 &&
 +      git reset --hard &&
 +      echo foo >foo3 &&
 +      git add foo3 &&
 +      git add --chmod=+x foo3 &&
 +      test_mode_in_index 100755 foo3 &&
 +      echo foo >xfoo3 &&
 +      chmod 755 xfoo3 &&
 +      git add xfoo3 &&
 +      git add --chmod=-x xfoo3 &&
 +      test_mode_in_index 100644 xfoo3
 +'
 +
 +test_expect_success POSIXPERM 'git add --chmod=[+-]x does not change the working tree' '
 +      echo foo >foo4 &&
 +      git add foo4 &&
 +      git add --chmod=+x foo4 &&
 +      ! test -x foo4
 +'
 +
 +test_expect_success 'no file status change if no pathspec is given' '
 +      >foo5 &&
 +      >foo6 &&
 +      git add foo5 foo6 &&
 +      git add --chmod=+x &&
 +      test_mode_in_index 100644 foo5 &&
 +      test_mode_in_index 100644 foo6
 +'
 +
 +test_expect_success 'no file status change if no pathspec is given in subdir' '
 +      mkdir -p sub &&
 +      (
 +              cd sub &&
 +              >sub-foo1 &&
 +              >sub-foo2 &&
 +              git add . &&
 +              git add --chmod=+x &&
 +              test_mode_in_index 100644 sub-foo1 &&
 +              test_mode_in_index 100644 sub-foo2
 +      )
 +'
 +
 +test_expect_success 'all statuses changed in folder if . is given' '
 +      git add --chmod=+x . &&
 +      test $(git ls-files --stage | grep ^100644 | wc -l) -eq 0 &&
 +      git add --chmod=-x . &&
 +      test $(git ls-files --stage | grep ^100755 | wc -l) -eq 0
 +'
 +
  test_done