Merge branch 'nd/gitignore-trailing-whitespace'
authorJunio C Hamano <gitster@pobox.com>
Fri, 14 Mar 2014 21:23:37 +0000 (14:23 -0700)
committerJunio C Hamano <gitster@pobox.com>
Fri, 14 Mar 2014 21:23:37 +0000 (14:23 -0700)
Trailing whitespaces in .gitignore files, unless they are quoted for
fnmatch(3), e.g. "path\ ", are warned and ignored.

Strictly speaking, this is a backward incompatible change, but very
unlikely to bite any sane user and adjusting should be obvious and
easy.

* nd/gitignore-trailing-whitespace:
t0008: skip trailing space test on Windows
dir: ignore trailing spaces in exclude patterns
dir: warn about trailing spaces in exclude patterns

1  2 
Documentation/gitignore.txt
dir.c
index b08d34d84ec92eec6aa54572e3823062424c23bf,aa776234e145f5f65a6f0137df5dc2c4a8e1c7ac..8734c1566ca918e8c62f759aebdf7d0b497e860b
@@@ -7,7 -7,7 +7,7 @@@ gitignore - Specifies intentionally unt
  
  SYNOPSIS
  --------
 -$GIT_DIR/info/exclude, .gitignore
 +$HOME/.config/git/ignore, $GIT_DIR/info/exclude, .gitignore
  
  DESCRIPTION
  -----------
@@@ -77,12 -77,13 +77,15 @@@ PATTERN FORMA
     Put a backslash ("`\`") in front of the first hash for patterns
     that begin with a hash.
  
+  - Trailing spaces are ignored unless they are quoted with backlash
+    ("`\`").
   - An optional prefix "`!`" which negates the pattern; any
     matching file excluded by a previous pattern will become
 -   included again.  If a negated pattern matches, this will
 -   override lower precedence patterns sources.
 +   included again. It is not possible to re-include a file if a parent
 +   directory of that file is excluded. Git doesn't list excluded
 +   directories for performance reasons, so any patterns on contained
 +   files have no effect, no matter where they are defined.
     Put a backslash ("`\`") in front of the first "`!`" for patterns
     that begin with a literal "`!`", for example, "`\!important!.txt`".
  
@@@ -184,19 -185,6 +187,19 @@@ Another example
  The second .gitignore prevents Git from ignoring
  `arch/foo/kernel/vmlinux.lds.S`.
  
 +Example to exclude everything except a specific directory `foo/bar`
 +(note the `/*` - without the slash, the wildcard would also exclude
 +everything within `foo/bar`):
 +
 +--------------------------------------------------------------
 +    $ cat .gitignore
 +    # exclude everything except directory foo/bar
 +    /*
 +    !/foo
 +    /foo/*
 +    !/foo/bar
 +--------------------------------------------------------------
 +
  SEE ALSO
  --------
  linkgit:git-rm[1],
diff --combined dir.c
index 98bb50fbabb69d25443df8ca4d29e11dea746a60,f6743b36cdfba5d6225e72b7f2e3278e46dcc006..6c6a5d13c2e65b02bfd9e3201c35b3a9f270d093
--- 1/dir.c
--- 2/dir.c
+++ b/dir.c
@@@ -126,13 -126,10 +126,13 @@@ static size_t common_prefix_len(const s
                       PATHSPEC_MAXDEPTH |
                       PATHSPEC_LITERAL |
                       PATHSPEC_GLOB |
 -                     PATHSPEC_ICASE);
 +                     PATHSPEC_ICASE |
 +                     PATHSPEC_EXCLUDE);
  
        for (n = 0; n < pathspec->nr; n++) {
                size_t i = 0, len = 0, item_len;
 +              if (pathspec->items[n].magic & PATHSPEC_EXCLUDE)
 +                      continue;
                if (pathspec->items[n].magic & PATHSPEC_ICASE)
                        item_len = pathspec->items[n].prefix;
                else
@@@ -195,9 -192,6 +195,9 @@@ int within_depth(const char *name, int 
        return 1;
  }
  
 +#define DO_MATCH_EXCLUDE   1
 +#define DO_MATCH_DIRECTORY 2
 +
  /*
   * Does 'match' match the given name?
   * A match is found if
   * It returns 0 when there is no match.
   */
  static int match_pathspec_item(const struct pathspec_item *item, int prefix,
 -                             const char *name, int namelen)
 +                             const char *name, int namelen, unsigned flags)
  {
        /* name/namelen has prefix cut off by caller */
        const char *match = item->match + prefix;
         * The normal call pattern is:
         * 1. prefix = common_prefix_len(ps);
         * 2. prune something, or fill_directory
 -       * 3. match_pathspec_depth()
 +       * 3. match_pathspec()
         *
         * 'prefix' at #1 may be shorter than the command's prefix and
         * it's ok for #2 to match extra files. Those extras will be
  
                if (match[matchlen-1] == '/' || name[matchlen] == '/')
                        return MATCHED_RECURSIVELY;
 -      }
 +      } else if ((flags & DO_MATCH_DIRECTORY) &&
 +                 match[matchlen - 1] == '/' &&
 +                 namelen == matchlen - 1 &&
 +                 !ps_strncmp(item, match, name, namelen))
 +              return MATCHED_EXACTLY;
  
        if (item->nowildcard_len < item->len &&
            !git_fnmatch(item, match, name,
   * pathspec did not match any names, which could indicate that the
   * user mistyped the nth pathspec.
   */
 -int match_pathspec_depth(const struct pathspec *ps,
 -                       const char *name, int namelen,
 -                       int prefix, char *seen)
 +static int do_match_pathspec(const struct pathspec *ps,
 +                           const char *name, int namelen,
 +                           int prefix, char *seen,
 +                           unsigned flags)
  {
 -      int i, retval = 0;
 +      int i, retval = 0, exclude = flags & DO_MATCH_EXCLUDE;
  
        GUARD_PATHSPEC(ps,
                       PATHSPEC_FROMTOP |
                       PATHSPEC_MAXDEPTH |
                       PATHSPEC_LITERAL |
                       PATHSPEC_GLOB |
 -                     PATHSPEC_ICASE);
 +                     PATHSPEC_ICASE |
 +                     PATHSPEC_EXCLUDE);
  
        if (!ps->nr) {
                if (!ps->recursive ||
  
        for (i = ps->nr - 1; i >= 0; i--) {
                int how;
 +
 +              if ((!exclude &&   ps->items[i].magic & PATHSPEC_EXCLUDE) ||
 +                  ( exclude && !(ps->items[i].magic & PATHSPEC_EXCLUDE)))
 +                      continue;
 +
                if (seen && seen[i] == MATCHED_EXACTLY)
                        continue;
 -              how = match_pathspec_item(ps->items+i, prefix, name, namelen);
 +              /*
 +               * Make exclude patterns optional and never report
 +               * "pathspec ':(exclude)foo' matches no files"
 +               */
 +              if (seen && ps->items[i].magic & PATHSPEC_EXCLUDE)
 +                      seen[i] = MATCHED_FNMATCH;
 +              how = match_pathspec_item(ps->items+i, prefix, name,
 +                                        namelen, flags);
                if (ps->recursive &&
                    (ps->magic & PATHSPEC_MAXDEPTH) &&
                    ps->max_depth != -1 &&
        return retval;
  }
  
 +int match_pathspec(const struct pathspec *ps,
 +                 const char *name, int namelen,
 +                 int prefix, char *seen, int is_dir)
 +{
 +      int positive, negative;
 +      unsigned flags = is_dir ? DO_MATCH_DIRECTORY : 0;
 +      positive = do_match_pathspec(ps, name, namelen,
 +                                   prefix, seen, flags);
 +      if (!(ps->magic & PATHSPEC_EXCLUDE) || !positive)
 +              return positive;
 +      negative = do_match_pathspec(ps, name, namelen,
 +                                   prefix, seen,
 +                                   flags | DO_MATCH_EXCLUDE);
 +      return negative ? 0 : positive;
 +}
 +
  /*
   * Return the length of the "simple" part of a path match limiter.
   */
@@@ -503,6 -463,25 +503,25 @@@ void clear_exclude_list(struct exclude_
        el->filebuf = NULL;
  }
  
+ static void trim_trailing_spaces(char *buf)
+ {
+       int i, last_space = -1, nr_spaces, len = strlen(buf);
+       for (i = 0; i < len; i++)
+               if (buf[i] == '\\')
+                       i++;
+               else if (buf[i] == ' ') {
+                       if (last_space == -1) {
+                               last_space = i;
+                               nr_spaces = 1;
+                       } else
+                               nr_spaces++;
+               } else
+                       last_space = -1;
+       if (last_space != -1 && last_space + nr_spaces == len)
+               buf[last_space] = '\0';
+ }
  int add_excludes_from_file_to_list(const char *fname,
                                   const char *base,
                                   int baselen,
                if (buf[i] == '\n') {
                        if (entry != buf + i && entry[0] != '#') {
                                buf[i - (i && buf[i-1] == '\r')] = 0;
+                               trim_trailing_spaces(entry);
                                add_exclude(entry, base, baselen, el, lineno);
                        }
                        lineno++;
@@@ -1415,18 -1395,11 +1435,18 @@@ int read_directory(struct dir_struct *d
                               PATHSPEC_MAXDEPTH |
                               PATHSPEC_LITERAL |
                               PATHSPEC_GLOB |
 -                             PATHSPEC_ICASE);
 +                             PATHSPEC_ICASE |
 +                             PATHSPEC_EXCLUDE);
  
        if (has_symlink_leading_path(path, len))
                return dir->nr;
  
 +      /*
 +       * exclude patterns are treated like positive ones in
 +       * create_simplify. Usually exclude patterns should be a
 +       * subset of positive ones, which has no impacts on
 +       * create_simplify().
 +       */
        simplify = create_simplify(pathspec ? pathspec->_raw : NULL);
        if (!len || treat_leading_path(dir, path, len, simplify))
                read_directory_recursive(dir, path, len, 0, simplify);
@@@ -1523,13 -1496,8 +1543,13 @@@ static int remove_dir_recurse(struct st
        flag &= ~REMOVE_DIR_KEEP_TOPLEVEL;
        dir = opendir(path->buf);
        if (!dir) {
 -              /* an empty dir could be removed even if it is unreadble */
 -              if (!keep_toplevel)
 +              if (errno == ENOENT)
 +                      return keep_toplevel ? -1 : 0;
 +              else if (errno == EACCES && !keep_toplevel)
 +                      /*
 +                       * An empty dir could be removable even if it
 +                       * is unreadable:
 +                       */
                        return rmdir(path->buf);
                else
                        return -1;
  
                strbuf_setlen(path, len);
                strbuf_addstr(path, e->d_name);
 -              if (lstat(path->buf, &st))
 -                      ; /* fall thru */
 -              else if (S_ISDIR(st.st_mode)) {
 +              if (lstat(path->buf, &st)) {
 +                      if (errno == ENOENT)
 +                              /*
 +                               * file disappeared, which is what we
 +                               * wanted anyway
 +                               */
 +                              continue;
 +                      /* fall thru */
 +              } else if (S_ISDIR(st.st_mode)) {
                        if (!remove_dir_recurse(path, flag, &kept_down))
                                continue; /* happy */
 -              } else if (!only_empty && !unlink(path->buf))
 +              } else if (!only_empty &&
 +                         (!unlink(path->buf) || errno == ENOENT)) {
                        continue; /* happy, too */
 +              }
  
                /* path too long, stat fails, or non-directory still exists */
                ret = -1;
  
        strbuf_setlen(path, original_len);
        if (!ret && !keep_toplevel && !kept_down)
 -              ret = rmdir(path->buf);
 +              ret = (!rmdir(path->buf) || errno == ENOENT) ? 0 : -1;
        else if (kept_up)
                /*
                 * report the uplevel that it is not an error that we