Skip to content

Commit 57534ee

Browse files
pcloudsgitster
authored andcommitted
dir.c: don't exclude whole dir prematurely if neg pattern may match
If there is a pattern "!foo/bar", this patch makes it not exclude "foo" right away. This gives us a chance to examine "foo" and re-include "foo/bar". In order for it to detect that the directory under examination should not be excluded right away, in other words it is a parent directory of a negative pattern, the "directory path" of the negative pattern must be literal. Patterns like "!f?o/bar" can't stop "foo" from being excluded. Basename matching (i.e. "no slashes in the pattern") or must-be-dir matching (i.e. "trailing slash in the pattern") does not work well with this. For example, if we descend in "foo" and are examining "foo/abc", current code for "foo/" pattern will check if path "foo/abc", not "foo", is a directory. The same problem with basename matching. These may need big code reorg to make it work. Signed-off-by: Nguyễn Thái Ngọc Duy <[email protected]> Signed-off-by: Junio C Hamano <[email protected]>
1 parent e6efecc commit 57534ee

File tree

3 files changed

+117
-5
lines changed

3 files changed

+117
-5
lines changed

Documentation/gitignore.txt

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -82,12 +82,12 @@ PATTERN FORMAT
8282

8383
- An optional prefix "`!`" which negates the pattern; any
8484
matching file excluded by a previous pattern will become
85-
included again. It is not possible to re-include a file if a parent
86-
directory of that file is excluded. Git doesn't list excluded
87-
directories for performance reasons, so any patterns on contained
88-
files have no effect, no matter where they are defined.
85+
included again.
8986
Put a backslash ("`\`") in front of the first "`!`" for patterns
9087
that begin with a literal "`!`", for example, "`\!important!.txt`".
88+
It is possible to re-include a file if a parent directory of that
89+
file is excluded if certain conditions are met. See section NOTES
90+
for detail.
9191

9292
- If the pattern ends with a slash, it is removed for the
9393
purpose of the following description, but it would only find
@@ -141,6 +141,21 @@ not tracked by Git remain untracked.
141141
To stop tracking a file that is currently tracked, use
142142
'git rm --cached'.
143143

144+
To re-include files or directories when their parent directory is
145+
excluded, the following conditions must be met:
146+
147+
- The rules to exclude a directory and re-include a subset back must
148+
be in the same .gitignore file.
149+
150+
- The directory part in the re-include rules must be literal (i.e. no
151+
wildcards)
152+
153+
- The rules to exclude the parent directory must not end with a
154+
trailing slash.
155+
156+
- The rules to exclude the parent directory must have at least one
157+
slash.
158+
144159
EXAMPLES
145160
--------
146161

dir.c

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -733,13 +733,74 @@ int match_pathname(const char *pathname, int pathlen,
733733
*/
734734
if (!patternlen && !namelen)
735735
return 1;
736+
/*
737+
* This can happen when we ignore some exclude rules
738+
* on directories in other to see if negative rules
739+
* may match. E.g.
740+
*
741+
* /abc
742+
* !/abc/def/ghi
743+
*
744+
* The pattern of interest is "/abc". On the first
745+
* try, we should match path "abc" with this pattern
746+
* in the "if" statement right above, but the caller
747+
* ignores it.
748+
*
749+
* On the second try with paths within "abc",
750+
* e.g. "abc/xyz", we come here and try to match it
751+
* with "/abc".
752+
*/
753+
if (!patternlen && namelen && *name == '/')
754+
return 1;
736755
}
737756

738757
return fnmatch_icase_mem(pattern, patternlen,
739758
name, namelen,
740759
WM_PATHNAME) == 0;
741760
}
742761

762+
/*
763+
* Return non-zero if pathname is a directory and an ancestor of the
764+
* literal path in a (negative) pattern. This is used to keep
765+
* descending in "foo" and "foo/bar" when the pattern is
766+
* "!foo/bar/.gitignore". "foo/notbar" will not be descended however.
767+
*/
768+
static int match_neg_path(const char *pathname, int pathlen, int *dtype,
769+
const char *base, int baselen,
770+
const char *pattern, int prefix, int patternlen,
771+
int flags)
772+
{
773+
assert((flags & EXC_FLAG_NEGATIVE) && !(flags & EXC_FLAG_NODIR));
774+
775+
if (*dtype == DT_UNKNOWN)
776+
*dtype = get_dtype(NULL, pathname, pathlen);
777+
if (*dtype != DT_DIR)
778+
return 0;
779+
780+
if (*pattern == '/') {
781+
pattern++;
782+
patternlen--;
783+
prefix--;
784+
}
785+
786+
if (baselen) {
787+
if (((pathlen < baselen && base[pathlen] == '/') ||
788+
pathlen == baselen) &&
789+
!strncmp_icase(pathname, base, pathlen))
790+
return 1;
791+
pathname += baselen + 1;
792+
pathlen -= baselen + 1;
793+
}
794+
795+
796+
if (prefix &&
797+
((pathlen < prefix && pattern[pathlen] == '/') &&
798+
!strncmp_icase(pathname, pattern, pathlen)))
799+
return 1;
800+
801+
return 0;
802+
}
803+
743804
/*
744805
* Scan the given exclude list in reverse to see whether pathname
745806
* should be ignored. The first match (i.e. the last on the list), if
@@ -753,7 +814,7 @@ static struct exclude *last_exclude_matching_from_list(const char *pathname,
753814
struct exclude_list *el)
754815
{
755816
struct exclude *exc = NULL; /* undecided */
756-
int i;
817+
int i, matched_negative_path = 0;
757818

758819
if (!el->nr)
759820
return NULL; /* undefined */
@@ -788,7 +849,18 @@ static struct exclude *last_exclude_matching_from_list(const char *pathname,
788849
exc = x;
789850
break;
790851
}
852+
853+
if ((x->flags & EXC_FLAG_NEGATIVE) && !matched_negative_path &&
854+
match_neg_path(pathname, pathlen, dtype, x->base,
855+
x->baselen ? x->baselen - 1 : 0,
856+
exclude, prefix, x->patternlen, x->flags))
857+
matched_negative_path = 1;
791858
}
859+
if (exc &&
860+
!(exc->flags & EXC_FLAG_NEGATIVE) &&
861+
!(exc->flags & EXC_FLAG_NODIR) &&
862+
matched_negative_path)
863+
exc = NULL;
792864
return exc;
793865
}
794866

t/t3001-ls-files-others-exclude.sh

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,4 +305,29 @@ test_expect_success 'ls-files with "**" patterns and no slashes' '
305305
test_cmp expect actual
306306
'
307307

308+
test_expect_success 'negative patterns' '
309+
git init reinclude &&
310+
(
311+
cd reinclude &&
312+
cat >.gitignore <<-\EOF &&
313+
/fooo
314+
/foo
315+
!foo/bar/bar
316+
EOF
317+
mkdir fooo &&
318+
cat >fooo/.gitignore <<-\EOF &&
319+
!/*
320+
EOF
321+
mkdir -p foo/bar &&
322+
touch abc foo/def foo/bar/ghi foo/bar/bar &&
323+
git ls-files -o --exclude-standard >../actual &&
324+
cat >../expected <<-\EOF &&
325+
.gitignore
326+
abc
327+
foo/bar/bar
328+
EOF
329+
test_cmp ../expected ../actual
330+
)
331+
'
332+
308333
test_done

0 commit comments

Comments
 (0)