The story goes back to 94bc671 (Add directory pattern matching to
attributes - 2012-12-08). Before this commit, directories are passed
to path_matches without the trailing slash. This is fine for matching
pattern "subdir" with "foo/subdir".

Patterns like "subdir/" (i.e. match _directory_ subdir) won't work
though. So paths are now passed to path_matches with the trailing
slash (i.e. "subdir/"). The trailing slash is used as the directory
indicator (similar to dtype in exclude case). This makes pattern
"subdir/" match directory "subdir/". Pattern "subdir" no longer match
subdir, which is now "subdir/".

As the trailing slash in pathname is the directory indicator, we do
not need to keep it in the pathname for matching. The trailing slash
should be turned to dtype "DT_DIR" and stripped out of pathname. This
keeps the code pattern similar to exclude.

The same applies for the pattern "subdir/". The trailing slash is
converted to flag EXC_FLAG_MUSTBEDIR and should not remain in the
pattern, as noted in parse_exclude_pattern(). prepare_attr_stack()
breaks this and keeps the trailing slash anyway.

To sum up, both patterns and pathnames should never have the trailing
slash when it comes to match_basename.

Reported-and-analyzed-by: Jeff King <>
Signed-off-by: Nguyễn Thái Ngọc Duy <>
 attr.c                          | 11 +++++++++--
 t/ |  6 ++++++
 2 files changed, 15 insertions(+), 2 deletions(-)

diff --git a/attr.c b/attr.c
index 1818ba5..a95c837 100644
--- a/attr.c
+++ b/attr.c
@@ -256,7 +256,7 @@ static struct match_attr *parse_attr_line(const char *line, 
const char *src,
                if (res->u.pat.flags & EXC_FLAG_MUSTBEDIR)
-                       res->u.pat.patternlen++;
+                       p[res->u.pat.patternlen] = '\0';
                if (res->u.pat.flags & EXC_FLAG_NEGATIVE) {
                        warning(_("Negative patterns are ignored in git 
                                  "Use '\\!' for literal leading 
@@ -665,9 +665,16 @@ static int path_matches(const char *pathname, int pathlen,
        const char *pattern = pat->pattern;
        int prefix = pat->nowildcardlen;
+       int dtype;
+       if (pathlen && pathname[pathlen-1] == '/') {
+               dtype = DT_DIR;
+               pathlen--;
+       } else
+               dtype = DT_REG;
        if ((pat->flags & EXC_FLAG_MUSTBEDIR) &&
-           ((!pathlen) || (pathname[pathlen-1] != '/')))
+           dtype != DT_DIR)
                return 0;
        if (pat->flags & EXC_FLAG_NODIR) {
diff --git a/t/ b/t/
index 0c847fb..98ccc3c 100755
--- a/t/
+++ b/t/
@@ -27,6 +27,10 @@ test_expect_success 'setup' '
        echo ignored-only-if-dir/ export-ignore >>.git/info/attributes &&
        git add ignored-only-if-dir &&
+       mkdir -p ignored-without-slash &&
+       echo ignored without slash >ignored-without-slash/foo &&
+       git add ignored-without-slash/foo &&
+       echo ignored-without-slash export-ignore >>.git/info/attributes &&
        mkdir -p one-level-lower/two-levels-lower/ignored-only-if-dir &&
        echo ignored by ignored dir 
>one-level-lower/two-levels-lower/ignored-only-if-dir/ignored-by-ignored-dir &&
@@ -49,6 +53,8 @@ test_expect_exists    
 test_expect_exists     archive/not-ignored-dir/
 test_expect_missing    archive/ignored-only-if-dir/
 test_expect_missing    archive/ignored-ony-if-dir/ignored-by-ignored-dir
+test_expect_missing    archive/ignored-without-slash/ &&
+test_expect_missing    archive/ignored-without-slash/foo &&
 test_expect_exists     archive/one-level-lower/

To unsubscribe from this list: send the line "unsubscribe git" in
the body of a message to
More majordomo info at

Reply via email to