Merge branch 'ap/status-ignored-in-ignored-directory'
authorJunio C Hamano <gitster@pobox.com>
Mon, 14 Jan 2013 16:15:40 +0000 (08:15 -0800)
committerJunio C Hamano <gitster@pobox.com>
Mon, 14 Jan 2013 16:15:43 +0000 (08:15 -0800)
Output from "git status --ignored" showed an unexpected interaction
with "--untracked".

* ap/status-ignored-in-ignored-directory:
  status: always report ignored tracked directories
  git-status: Test --ignored behavior
  dir.c: Make git-status --ignored more consistent

1  2 
dir.c
wt-status.c

diff --combined dir.c
index e883a91483981ad9e2da4841063c1c8bad6a25fd,f836590b40f7ea69c94523b973c664e7a0c48c85..9dde68a2f3a1a325d2c5b90fa4d20dbf7670652c
--- 1/dir.c
--- 2/dir.c
+++ b/dir.c
@@@ -2,15 -2,12 +2,15 @@@
   * This handles recursive filename detection with exclude
   * files, index knowledge etc..
   *
 + * See Documentation/technical/api-directory-listing.txt
 + *
   * Copyright (C) Linus Torvalds, 2005-2006
   *             Junio Hamano, 2005-2006
   */
  #include "cache.h"
  #include "dir.h"
  #include "refs.h"
 +#include "wildmatch.h"
  
  struct path_simplify {
        int len;
@@@ -37,33 -34,10 +37,33 @@@ int fnmatch_icase(const char *pattern, 
        return fnmatch(pattern, string, flags | (ignore_case ? FNM_CASEFOLD : 0));
  }
  
 +inline int git_fnmatch(const char *pattern, const char *string,
 +                     int flags, int prefix)
 +{
 +      int fnm_flags = 0;
 +      if (flags & GFNM_PATHNAME)
 +              fnm_flags |= FNM_PATHNAME;
 +      if (prefix > 0) {
 +              if (strncmp(pattern, string, prefix))
 +                      return FNM_NOMATCH;
 +              pattern += prefix;
 +              string += prefix;
 +      }
 +      if (flags & GFNM_ONESTAR) {
 +              int pattern_len = strlen(++pattern);
 +              int string_len = strlen(string);
 +              return string_len < pattern_len ||
 +                     strcmp(pattern,
 +                            string + string_len - pattern_len);
 +      }
 +      return fnmatch(pattern, string, fnm_flags);
 +}
 +
  static size_t common_prefix_len(const char **pathspec)
  {
        const char *n, *first;
        size_t max = 0;
 +      int literal = limit_pathspec_to_literal();
  
        if (!pathspec)
                return max;
@@@ -73,7 -47,7 +73,7 @@@
                size_t i, len = 0;
                for (i = 0; first == n || i < max; i++) {
                        char c = n[i];
 -                      if (!c || c != first[i] || is_glob_special(c))
 +                      if (!c || c != first[i] || (!literal && is_glob_special(c)))
                                break;
                        if (c == '/')
                                len = i + 1;
@@@ -143,7 -117,6 +143,7 @@@ int within_depth(const char *name, int 
  static int match_one(const char *match, const char *name, int namelen)
  {
        int matchlen;
 +      int literal = limit_pathspec_to_literal();
  
        /* If the match was just the prefix, we matched */
        if (!*match)
                for (;;) {
                        unsigned char c1 = tolower(*match);
                        unsigned char c2 = tolower(*name);
 -                      if (c1 == '\0' || is_glob_special(c1))
 +                      if (c1 == '\0' || (!literal && is_glob_special(c1)))
                                break;
                        if (c1 != c2)
                                return 0;
                for (;;) {
                        unsigned char c1 = *match;
                        unsigned char c2 = *name;
 -                      if (c1 == '\0' || is_glob_special(c1))
 +                      if (c1 == '\0' || (!literal && is_glob_special(c1)))
                                break;
                        if (c1 != c2)
                                return 0;
                }
        }
  
 -
        /*
         * If we don't match the matchstring exactly,
         * we need to match by fnmatch
         */
        matchlen = strlen(match);
 -      if (strncmp_icase(match, name, matchlen))
 +      if (strncmp_icase(match, name, matchlen)) {
 +              if (literal)
 +                      return 0;
                return !fnmatch_icase(match, name, 0) ? MATCHED_FNMATCH : 0;
 +      }
  
        if (namelen == matchlen)
                return MATCHED_EXACTLY;
@@@ -259,10 -230,7 +259,10 @@@ static int match_pathspec_item(const st
                        return MATCHED_RECURSIVELY;
        }
  
 -      if (item->use_wildcard && !fnmatch(match, name, 0))
 +      if (item->nowildcard_len < item->len &&
 +          !git_fnmatch(match, name,
 +                       item->flags & PATHSPEC_ONESTAR ? GFNM_ONESTAR : 0,
 +                       item->nowildcard_len - prefix))
                return MATCHED_FNMATCH;
  
        return 0;
@@@ -340,71 -308,44 +340,71 @@@ static int no_wildcard(const char *stri
        return string[simple_length(string)] == '\0';
  }
  
 +void parse_exclude_pattern(const char **pattern,
 +                         int *patternlen,
 +                         int *flags,
 +                         int *nowildcardlen)
 +{
 +      const char *p = *pattern;
 +      size_t i, len;
 +
 +      *flags = 0;
 +      if (*p == '!') {
 +              *flags |= EXC_FLAG_NEGATIVE;
 +              p++;
 +      }
 +      len = strlen(p);
 +      if (len && p[len - 1] == '/') {
 +              len--;
 +              *flags |= EXC_FLAG_MUSTBEDIR;
 +      }
 +      for (i = 0; i < len; i++) {
 +              if (p[i] == '/')
 +                      break;
 +      }
 +      if (i == len)
 +              *flags |= EXC_FLAG_NODIR;
 +      *nowildcardlen = simple_length(p);
 +      /*
 +       * we should have excluded the trailing slash from 'p' too,
 +       * but that's one more allocation. Instead just make sure
 +       * nowildcardlen does not exceed real patternlen
 +       */
 +      if (*nowildcardlen > len)
 +              *nowildcardlen = len;
 +      if (*p == '*' && no_wildcard(p + 1))
 +              *flags |= EXC_FLAG_ENDSWITH;
 +      *pattern = p;
 +      *patternlen = len;
 +}
 +
  void add_exclude(const char *string, const char *base,
 -               int baselen, struct exclude_list *which)
 +               int baselen, struct exclude_list *el)
  {
        struct exclude *x;
 -      size_t len;
 -      int to_exclude = 1;
 -      int flags = 0;
 +      int patternlen;
 +      int flags;
 +      int nowildcardlen;
  
 -      if (*string == '!') {
 -              to_exclude = 0;
 -              string++;
 -      }
 -      len = strlen(string);
 -      if (len && string[len - 1] == '/') {
 +      parse_exclude_pattern(&string, &patternlen, &flags, &nowildcardlen);
 +      if (flags & EXC_FLAG_MUSTBEDIR) {
                char *s;
 -              x = xmalloc(sizeof(*x) + len);
 +              x = xmalloc(sizeof(*x) + patternlen + 1);
                s = (char *)(x+1);
 -              memcpy(s, string, len - 1);
 -              s[len - 1] = '\0';
 -              string = s;
 +              memcpy(s, string, patternlen);
 +              s[patternlen] = '\0';
                x->pattern = s;
 -              flags = EXC_FLAG_MUSTBEDIR;
        } else {
                x = xmalloc(sizeof(*x));
                x->pattern = string;
        }
 -      x->to_exclude = to_exclude;
 -      x->patternlen = strlen(string);
 +      x->patternlen = patternlen;
 +      x->nowildcardlen = nowildcardlen;
        x->base = base;
        x->baselen = baselen;
        x->flags = flags;
 -      if (!strchr(string, '/'))
 -              x->flags |= EXC_FLAG_NODIR;
 -      x->nowildcardlen = simple_length(string);
 -      if (*string == '*' && no_wildcard(string+1))
 -              x->flags |= EXC_FLAG_ENDSWITH;
 -      ALLOC_GROW(which->excludes, which->nr + 1, which->alloc);
 -      which->excludes[which->nr++] = x;
 +      ALLOC_GROW(el->excludes, el->nr + 1, el->alloc);
 +      el->excludes[el->nr++] = x;
  }
  
  static void *read_skip_worktree_file_from_index(const char *path, size_t *size)
        return data;
  }
  
 -void free_excludes(struct exclude_list *el)
 +/*
 + * Frees memory within el which was allocated for exclude patterns and
 + * the file buffer.  Does not free el itself.
 + */
 +void clear_exclude_list(struct exclude_list *el)
  {
        int i;
  
@@@ -450,7 -387,7 +450,7 @@@ int add_excludes_from_file_to_list(cons
                                   const char *base,
                                   int baselen,
                                   char **buf_p,
 -                                 struct exclude_list *which,
 +                                 struct exclude_list *el,
                                   int check_index)
  {
        struct stat st;
                if (buf[i] == '\n') {
                        if (entry != buf + i && entry[0] != '#') {
                                buf[i - (i && buf[i-1] == '\r')] = 0;
 -                              add_exclude(entry, base, baselen, which);
 +                              add_exclude(entry, base, baselen, el);
                        }
                        entry = buf + i + 1;
                }
@@@ -514,10 -451,6 +514,10 @@@ void add_excludes_from_file(struct dir_
                die("cannot use %s as an exclude file", fname);
  }
  
 +/*
 + * Loads the per-directory exclude list for the substring of base
 + * which has a char length of baselen.
 + */
  static void prep_exclude(struct dir_struct *dir, const char *base, int baselen)
  {
        struct exclude_list *el;
            (baselen + strlen(dir->exclude_per_dir) >= PATH_MAX))
                return; /* too long a path -- ignore */
  
 -      /* Pop the ones that are not the prefix of the path being checked. */
 +      /* Pop the directories that are not the prefix of the path being checked. */
        el = &dir->exclude_list[EXC_DIRS];
        while ((stk = dir->exclude_stack) != NULL) {
                if (stk->baselen <= baselen &&
        dir->basebuf[baselen] = '\0';
  }
  
 -/* Scan the list and let the last match determine the fate.
 - * Return 1 for exclude, 0 for include and -1 for undecided.
 +int match_basename(const char *basename, int basenamelen,
 +                 const char *pattern, int prefix, int patternlen,
 +                 int flags)
 +{
 +      if (prefix == patternlen) {
 +              if (!strcmp_icase(pattern, basename))
 +                      return 1;
 +      } else if (flags & EXC_FLAG_ENDSWITH) {
 +              if (patternlen - 1 <= basenamelen &&
 +                  !strcmp_icase(pattern + 1,
 +                                basename + basenamelen - patternlen + 1))
 +                      return 1;
 +      } else {
 +              if (fnmatch_icase(pattern, basename, 0) == 0)
 +                      return 1;
 +      }
 +      return 0;
 +}
 +
 +int match_pathname(const char *pathname, int pathlen,
 +                 const char *base, int baselen,
 +                 const char *pattern, int prefix, int patternlen,
 +                 int flags)
 +{
 +      const char *name;
 +      int namelen;
 +
 +      /*
 +       * match with FNM_PATHNAME; the pattern has base implicitly
 +       * in front of it.
 +       */
 +      if (*pattern == '/') {
 +              pattern++;
 +              prefix--;
 +      }
 +
 +      /*
 +       * baselen does not count the trailing slash. base[] may or
 +       * may not end with a trailing slash though.
 +       */
 +      if (pathlen < baselen + 1 ||
 +          (baselen && pathname[baselen] != '/') ||
 +          strncmp_icase(pathname, base, baselen))
 +              return 0;
 +
 +      namelen = baselen ? pathlen - baselen - 1 : pathlen;
 +      name = pathname + pathlen - namelen;
 +
 +      if (prefix) {
 +              /*
 +               * if the non-wildcard part is longer than the
 +               * remaining pathname, surely it cannot match.
 +               */
 +              if (prefix > namelen)
 +                      return 0;
 +
 +              if (strncmp_icase(pattern, name, prefix))
 +                      return 0;
 +              pattern += prefix;
 +              name    += prefix;
 +              namelen -= prefix;
 +      }
 +
 +      return wildmatch(pattern, name,
 +                       ignore_case ? FNM_CASEFOLD : 0) == 0;
 +}
 +
 +/*
 + * Scan the given exclude list in reverse to see whether pathname
 + * should be ignored.  The first match (i.e. the last on the list), if
 + * any, determines the fate.  Returns the exclude_list element which
 + * matched, or NULL for undecided.
   */
 -int excluded_from_list(const char *pathname,
 -                     int pathlen, const char *basename, int *dtype,
 -                     struct exclude_list *el)
 +static struct exclude *last_exclude_matching_from_list(const char *pathname,
 +                                                     int pathlen,
 +                                                     const char *basename,
 +                                                     int *dtype,
 +                                                     struct exclude_list *el)
  {
        int i;
  
        if (!el->nr)
 -              return -1;      /* undefined */
 +              return NULL;    /* undefined */
  
        for (i = el->nr - 1; 0 <= i; i--) {
                struct exclude *x = el->excludes[i];
 -              const char *name, *exclude = x->pattern;
 -              int to_exclude = x->to_exclude;
 -              int namelen, prefix = x->nowildcardlen;
 +              const char *exclude = x->pattern;
 +              int prefix = x->nowildcardlen;
  
                if (x->flags & EXC_FLAG_MUSTBEDIR) {
                        if (*dtype == DT_UNKNOWN)
                }
  
                if (x->flags & EXC_FLAG_NODIR) {
 -                      /* match basename */
 -                      if (prefix == x->patternlen) {
 -                              if (!strcmp_icase(exclude, basename))
 -                                      return to_exclude;
 -                      } else if (x->flags & EXC_FLAG_ENDSWITH) {
 -                              if (x->patternlen - 1 <= pathlen &&
 -                                  !strcmp_icase(exclude + 1, pathname + pathlen - x->patternlen + 1))
 -                                      return to_exclude;
 -                      } else {
 -                              if (fnmatch_icase(exclude, basename, 0) == 0)
 -                                      return to_exclude;
 -                      }
 +                      if (match_basename(basename,
 +                                         pathlen - (basename - pathname),
 +                                         exclude, prefix, x->patternlen,
 +                                         x->flags))
 +                              return x;
                        continue;
                }
  
 -              /* match with FNM_PATHNAME:
 -               * exclude has base (baselen long) implicitly in front of it.
 -               */
 -              if (*exclude == '/') {
 -                      exclude++;
 -                      prefix--;
 -              }
 -
 -              if (pathlen < x->baselen ||
 -                  (x->baselen && pathname[x->baselen-1] != '/') ||
 -                  strncmp_icase(pathname, x->base, x->baselen))
 -                      continue;
 -
 -              namelen = x->baselen ? pathlen - x->baselen : pathlen;
 -              name = pathname + pathlen  - namelen;
 -
 -              /* if the non-wildcard part is longer than the
 -                 remaining pathname, surely it cannot match */
 -              if (prefix > namelen)
 -                      continue;
 -
 -              if (prefix) {
 -                      if (strncmp_icase(exclude, name, prefix))
 -                              continue;
 -                      exclude += prefix;
 -                      name    += prefix;
 -                      namelen -= prefix;
 -              }
 -
 -              if (!namelen || !fnmatch_icase(exclude, name, FNM_PATHNAME))
 -                      return to_exclude;
 +              assert(x->baselen == 0 || x->base[x->baselen - 1] == '/');
 +              if (match_pathname(pathname, pathlen,
 +                                 x->base, x->baselen ? x->baselen - 1 : 0,
 +                                 exclude, prefix, x->patternlen, x->flags))
 +                      return x;
        }
 +      return NULL; /* undecided */
 +}
 +
 +/*
 + * Scan the list and let the last match determine the fate.
 + * Return 1 for exclude, 0 for include and -1 for undecided.
 + */
 +int is_excluded_from_list(const char *pathname,
 +                        int pathlen, const char *basename, int *dtype,
 +                        struct exclude_list *el)
 +{
 +      struct exclude *exclude;
 +      exclude = last_exclude_matching_from_list(pathname, pathlen, basename, dtype, el);
 +      if (exclude)
 +              return exclude->flags & EXC_FLAG_NEGATIVE ? 0 : 1;
        return -1; /* undecided */
  }
  
 -static int excluded(struct dir_struct *dir, const char *pathname, int *dtype_p)
 +/*
 + * Loads the exclude lists for the directory containing pathname, then
 + * scans all exclude lists to determine whether pathname is excluded.
 + * Returns the exclude_list element which matched, or NULL for
 + * undecided.
 + */
 +static struct exclude *last_exclude_matching(struct dir_struct *dir,
 +                                           const char *pathname,
 +                                           int *dtype_p)
  {
        int pathlen = strlen(pathname);
        int st;
 +      struct exclude *exclude;
        const char *basename = strrchr(pathname, '/');
        basename = (basename) ? basename+1 : pathname;
  
        prep_exclude(dir, pathname, basename-pathname);
        for (st = EXC_CMDL; st <= EXC_FILE; st++) {
 -              switch (excluded_from_list(pathname, pathlen, basename,
 -                                         dtype_p, &dir->exclude_list[st])) {
 -              case 0:
 -                      return 0;
 -              case 1:
 -                      return 1;
 -              }
 +              exclude = last_exclude_matching_from_list(
 +                      pathname, pathlen, basename, dtype_p,
 +                      &dir->exclude_list[st]);
 +              if (exclude)
 +                      return exclude;
        }
 +      return NULL;
 +}
 +
 +/*
 + * Loads the exclude lists for the directory containing pathname, then
 + * scans all exclude lists to determine whether pathname is excluded.
 + * Returns 1 if true, otherwise 0.
 + */
 +static int is_excluded(struct dir_struct *dir, const char *pathname, int *dtype_p)
 +{
 +      struct exclude *exclude =
 +              last_exclude_matching(dir, pathname, dtype_p);
 +      if (exclude)
 +              return exclude->flags & EXC_FLAG_NEGATIVE ? 0 : 1;
        return 0;
  }
  
@@@ -746,7 -605,6 +746,7 @@@ void path_exclude_check_init(struct pat
                             struct dir_struct *dir)
  {
        check->dir = dir;
 +      check->exclude = NULL;
        strbuf_init(&check->path, 256);
  }
  
@@@ -756,41 -614,32 +756,41 @@@ void path_exclude_check_clear(struct pa
  }
  
  /*
 - * Is this name excluded?  This is for a caller like show_files() that
 - * do not honor directory hierarchy and iterate through paths that are
 - * possibly in an ignored directory.
 + * For each subdirectory in name, starting with the top-most, checks
 + * to see if that subdirectory is excluded, and if so, returns the
 + * corresponding exclude structure.  Otherwise, checks whether name
 + * itself (which is presumably a file) is excluded.
   *
   * A path to a directory known to be excluded is left in check->path to
   * optimize for repeated checks for files in the same excluded directory.
   */
 -int path_excluded(struct path_exclude_check *check,
 -                const char *name, int namelen, int *dtype)
 +struct exclude *last_exclude_matching_path(struct path_exclude_check *check,
 +                                         const char *name, int namelen,
 +                                         int *dtype)
  {
        int i;
        struct strbuf *path = &check->path;
 +      struct exclude *exclude;
  
        /*
         * we allow the caller to pass namelen as an optimization; it
         * must match the length of the name, as we eventually call
 -       * excluded() on the whole name string.
 +       * is_excluded() on the whole name string.
         */
        if (namelen < 0)
                namelen = strlen(name);
  
 +      /*
 +       * If path is non-empty, and name is equal to path or a
 +       * subdirectory of path, name should be excluded, because
 +       * it's inside a directory which is already known to be
 +       * excluded and was previously left in check->path.
 +       */
        if (path->len &&
            path->len <= namelen &&
            !memcmp(name, path->buf, path->len) &&
            (!name[path->len] || name[path->len] == '/'))
 -              return 1;
 +              return check->exclude;
  
        strbuf_setlen(path, 0);
        for (i = 0; name[i]; i++) {
  
                if (ch == '/') {
                        int dt = DT_DIR;
 -                      if (excluded(check->dir, path->buf, &dt))
 -                              return 1;
 +                      exclude = last_exclude_matching(check->dir,
 +                                                      path->buf, &dt);
 +                      if (exclude) {
 +                              check->exclude = exclude;
 +                              return exclude;
 +                      }
                }
                strbuf_addch(path, ch);
        }
        /* An entry in the index; cannot be a directory with subentries */
        strbuf_setlen(path, 0);
  
 -      return excluded(check->dir, name, dtype);
 +      return last_exclude_matching(check->dir, name, dtype);
 +}
 +
 +/*
 + * Is this name excluded?  This is for a caller like show_files() that
 + * do not honor directory hierarchy and iterate through paths that are
 + * possibly in an ignored directory.
 + */
 +int is_path_excluded(struct path_exclude_check *check,
 +                const char *name, int namelen, int *dtype)
 +{
 +      struct exclude *exclude =
 +              last_exclude_matching_path(check, name, namelen, dtype);
 +      if (exclude)
 +              return exclude->flags & EXC_FLAG_NEGATIVE ? 0 : 1;
 +      return 0;
  }
  
  static struct dir_entry *dir_entry_new(const char *pathname, int len)
  
  static struct dir_entry *dir_add_name(struct dir_struct *dir, const char *pathname, int len)
  {
-       if (cache_name_exists(pathname, len, ignore_case))
+       if (!(dir->flags & DIR_SHOW_IGNORED) &&
+           cache_name_exists(pathname, len, ignore_case))
                return NULL;
  
        ALLOC_GROW(dir->entries, dir->nr+1, dir->alloc);
@@@ -944,8 -775,9 +945,9 @@@ static enum exist_status directory_exis
   * traversal routine.
   *
   * Case 1: If we *already* have entries in the index under that
-  * directory name, we always recurse into the directory to see
-  * all the files.
+  * directory name, we recurse into the directory to see all the files,
+  * unless the directory is excluded and we want to show ignored
+  * directories
   *
   * Case 2: If we *already* have that directory name as a gitlink,
   * we always continue to see it as a gitlink, regardless of whether
   *      just a directory, unless "hide_empty_directories" is
   *      also true and the directory is empty, in which case
   *      we just ignore it entirely.
+  *      if we are looking for ignored directories, look if it
+  *      contains only ignored files to decide if it must be shown as
+  *      ignored or not.
   *  (b) if it looks like a git directory, and we don't have
   *      'no_gitlinks' set we treat it as a gitlink, and show it
   *      as a directory.
@@@ -971,12 -806,15 +976,15 @@@ enum directory_treatment 
  };
  
  static enum directory_treatment treat_directory(struct dir_struct *dir,
-       const char *dirname, int len,
+       const char *dirname, int len, int exclude,
        const struct path_simplify *simplify)
  {
        /* The "len-1" is to strip the final '/' */
        switch (directory_exists_in_index(dirname, len-1)) {
        case index_directory:
+               if ((dir->flags & DIR_SHOW_OTHER_DIRECTORIES) && exclude)
+                       break;
                return recurse_into_directory;
  
        case index_gitdir:
        }
  
        /* This is the "show_other_directories" case */
-       if (!(dir->flags & DIR_HIDE_EMPTY_DIRECTORIES))
+       /*
+        * We are looking for ignored files and our directory is not ignored,
+        * check if it contains only ignored files
+        */
+       if ((dir->flags & DIR_SHOW_IGNORED) && !exclude) {
+               int ignored;
+               dir->flags &= ~DIR_SHOW_IGNORED;
+               dir->flags |= DIR_HIDE_EMPTY_DIRECTORIES;
+               ignored = read_directory_recursive(dir, dirname, len, 1, simplify);
+               dir->flags &= ~DIR_HIDE_EMPTY_DIRECTORIES;
+               dir->flags |= DIR_SHOW_IGNORED;
+               return ignored ? ignore_directory : show_directory;
+       }
+       if (!(dir->flags & DIR_SHOW_IGNORED) &&
+           !(dir->flags & DIR_HIDE_EMPTY_DIRECTORIES))
                return show_directory;
        if (!read_directory_recursive(dir, dirname, len, 1, simplify))
                return ignore_directory;
        return show_directory;
  }
  
 -              if (!path_excluded(&check, path->buf, path->len, dtype))
+ /*
+  * Decide what to do when we find a file while traversing the
+  * filesystem. Mostly two cases:
+  *
+  *  1. We are looking for ignored files
+  *   (a) File is ignored, include it
+  *   (b) File is in ignored path, include it
+  *   (c) File is not ignored, exclude it
+  *
+  *  2. Other scenarios, include the file if not excluded
+  *
+  * Return 1 for exclude, 0 for include.
+  */
+ static int treat_file(struct dir_struct *dir, struct strbuf *path, int exclude, int *dtype)
+ {
+       struct path_exclude_check check;
+       int exclude_file = 0;
+       if (exclude)
+               exclude_file = !(dir->flags & DIR_SHOW_IGNORED);
+       else if (dir->flags & DIR_SHOW_IGNORED) {
+               /* Always exclude indexed files */
+               struct cache_entry *ce = index_name_exists(&the_index,
+                   path->buf, path->len, ignore_case);
+               if (ce)
+                       return 1;
+               path_exclude_check_init(&check, dir);
++              if (!is_path_excluded(&check, path->buf, path->len, dtype))
+                       exclude_file = 1;
+               path_exclude_check_clear(&check);
+       }
+       return exclude_file;
+ }
  /*
   * This is an inexact early pruning of any recursive directory
   * reading - if the path cannot possibly be in the pathspec,
@@@ -1126,7 -1019,7 +1189,7 @@@ static enum path_treatment treat_one_pa
                                          const struct path_simplify *simplify,
                                          int dtype, struct dirent *de)
  {
 -      int exclude = excluded(dir, path->buf, &dtype);
 +      int exclude = is_excluded(dir, path->buf, &dtype);
        if (exclude && (dir->flags & DIR_COLLECT_IGNORED)
            && exclude_matches_pathspec(path->buf, path->len, simplify))
                dir_add_ignored(dir, path->buf, path->len);
        if (dtype == DT_UNKNOWN)
                dtype = get_dtype(de, path->buf, path->len);
  
-       /*
-        * Do we want to see just the ignored files?
-        * We still need to recurse into directories,
-        * even if we don't ignore them, since the
-        * directory may contain files that we do..
-        */
-       if (!exclude && (dir->flags & DIR_SHOW_IGNORED)) {
-               if (dtype != DT_DIR)
-                       return path_ignored;
-       }
        switch (dtype) {
        default:
                return path_ignored;
        case DT_DIR:
                strbuf_addch(path, '/');
-               switch (treat_directory(dir, path->buf, path->len, simplify)) {
+               switch (treat_directory(dir, path->buf, path->len, exclude, simplify)) {
                case show_directory:
-                       if (exclude != !!(dir->flags
-                                         & DIR_SHOW_IGNORED))
-                               return path_ignored;
                        break;
                case recurse_into_directory:
                        return path_recurse;
                break;
        case DT_REG:
        case DT_LNK:
-               break;
+               switch (treat_file(dir, path, exclude, &dtype)) {
+               case 1:
+                       return path_ignored;
+               default:
+                       break;
+               }
        }
        return path_handled;
  }
@@@ -1539,18 -1424,9 +1594,18 @@@ int init_pathspec(struct pathspec *path
  
                item->match = path;
                item->len = strlen(path);
 -              item->use_wildcard = !no_wildcard(path);
 -              if (item->use_wildcard)
 -                      pathspec->has_wildcard = 1;
 +              item->flags = 0;
 +              if (limit_pathspec_to_literal()) {
 +                      item->nowildcard_len = item->len;
 +              } else {
 +                      item->nowildcard_len = simple_length(path);
 +                      if (item->nowildcard_len < item->len) {
 +                              pathspec->has_wildcard = 1;
 +                              if (path[item->nowildcard_len] == '*' &&
 +                                  no_wildcard(path + item->nowildcard_len + 1))
 +                                      item->flags |= PATHSPEC_ONESTAR;
 +                      }
 +              }
        }
  
        qsort(pathspec->items, pathspec->nr,
@@@ -1564,11 -1440,3 +1619,11 @@@ void free_pathspec(struct pathspec *pat
        free(pathspec->items);
        pathspec->items = NULL;
  }
 +
 +int limit_pathspec_to_literal(void)
 +{
 +      static int flag = -1;
 +      if (flag < 0)
 +              flag = git_env_bool(GIT_LITERAL_PATHSPECS_ENVIRONMENT, 0);
 +      return flag;
 +}
diff --combined wt-status.c
index 2a9658bad4c5035893e9363b51985f285dd3b34c,3d477f4bd2a7c3465755a4ff9854b5fb512c21ec..d7cfe8f31cd3d2b4b8bdc18c5da2449255b0531b
@@@ -100,8 -100,8 +100,8 @@@ void status_printf(struct wt_status *s
        va_end(ap);
  }
  
 -void status_printf_more(struct wt_status *s, const char *color,
 -                      const char *fmt, ...)
 +static void status_printf_more(struct wt_status *s, const char *color,
 +                             const char *fmt, ...)
  {
        va_list ap;
  
@@@ -221,7 -221,7 +221,7 @@@ static void wt_status_print_other_heade
                                         const char *how)
  {
        const char *c = color(WT_STATUS_HEADER, s);
 -      status_printf_ln(s, c, _("%s files:"), what);
 +      status_printf_ln(s, c, "%s:", what);
        if (!advice_status_hints)
                return;
        status_printf_ln(s, c, _("  (use \"git %s <file>...\" to include in what will be committed)"), how);
@@@ -516,7 -516,9 +516,9 @@@ static void wt_status_collect_untracked
  
        if (s->show_ignored_files) {
                dir.nr = 0;
-               dir.flags = DIR_SHOW_IGNORED | DIR_SHOW_OTHER_DIRECTORIES;
+               dir.flags = DIR_SHOW_IGNORED;
+               if (s->show_untracked_files != SHOW_ALL_UNTRACKED_FILES)
+                       dir.flags |= DIR_SHOW_OTHER_DIRECTORIES;
                fill_directory(&dir, s->pathspec);
                for (i = 0; i < dir.nr; i++) {
                        struct dir_entry *ent = dir.entries[i];
@@@ -1008,9 -1010,9 +1010,9 @@@ void wt_status_print(struct wt_status *
                wt_status_print_submodule_summary(s, 1);  /* unstaged */
        }
        if (s->show_untracked_files) {
 -              wt_status_print_other(s, &s->untracked, _("Untracked"), "add");
 +              wt_status_print_other(s, &s->untracked, _("Untracked files"), "add");
                if (s->show_ignored_files)
 -                      wt_status_print_other(s, &s->ignored, _("Ignored"), "add -f");
 +                      wt_status_print_other(s, &s->ignored, _("Ignored files"), "add -f");
        } else if (s->commitable)
                status_printf_ln(s, GIT_COLOR_NORMAL, _("Untracked files not listed%s"),
                        advice_status_hints
                        status_printf_ln(s, GIT_COLOR_NORMAL, _("No changes"));
                else if (s->nowarn)
                        ; /* nothing */
 -              else if (s->workdir_dirty)
 -                      printf(_("no changes added to commit%s\n"),
 -                              advice_status_hints
 -                              ? _(" (use \"git add\" and/or \"git commit -a\")") : "");
 -              else if (s->untracked.nr)
 -                      printf(_("nothing added to commit but untracked files present%s\n"),
 -                              advice_status_hints
 -                              ? _(" (use \"git add\" to track)") : "");
 -              else if (s->is_initial)
 -                      printf(_("nothing to commit%s\n"), advice_status_hints
 -                              ? _(" (create/copy files and use \"git add\" to track)") : "");
 -              else if (!s->show_untracked_files)
 -                      printf(_("nothing to commit%s\n"), advice_status_hints
 -                              ? _(" (use -u to show untracked files)") : "");
 -              else
 -                      printf(_("nothing to commit%s\n"), advice_status_hints
 -                              ? _(" (working directory clean)") : "");
 +              else if (s->workdir_dirty) {
 +                      if (advice_status_hints)
 +                              printf(_("no changes added to commit "
 +                                       "(use \"git add\" and/or \"git commit -a\")\n"));
 +                      else
 +                              printf(_("no changes added to commit\n"));
 +              } else if (s->untracked.nr) {
 +                      if (advice_status_hints)
 +                              printf(_("nothing added to commit but untracked files "
 +                                       "present (use \"git add\" to track)\n"));
 +                      else
 +                              printf(_("nothing added to commit but untracked files present\n"));
 +              } else if (s->is_initial) {
 +                      if (advice_status_hints)
 +                              printf(_("nothing to commit (create/copy files "
 +                                       "and use \"git add\" to track)\n"));
 +                      else
 +                              printf(_("nothing to commit\n"));
 +              } else if (!s->show_untracked_files) {
 +                      if (advice_status_hints)
 +                              printf(_("nothing to commit (use -u to show untracked files)\n"));
 +                      else
 +                              printf(_("nothing to commit\n"));
 +              } else
 +                      printf(_("nothing to commit, working directory clean\n"));
        }
  }