Skip to content

Commit 5e57f9c

Browse files
committed
Merge branch 'nd/exclusion-regression-fix'
Another try to add support to the ignore mechanism that lets you say "this is excluded" and then later say "oh, no, this part (that is a subset of the previous part) is not excluded". * nd/exclusion-regression-fix: dir.c: don't exclude whole dir prematurely dir.c: support marking some patterns already matched dir.c: support tracing exclude dir.c: fix match_pathname()
2 parents e79112d + d589a67 commit 5e57f9c

File tree

7 files changed

+378
-12
lines changed

7 files changed

+378
-12
lines changed

Documentation/git-check-ignore.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ SEE ALSO
114114
linkgit:gitignore[5]
115115
linkgit:gitconfig[5]
116116
linkgit:git-ls-files[1]
117+
GIT_TRACE_EXCLUDE in linkgit:git[1]
117118

118119
GIT
119120
---

Documentation/git.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1065,6 +1065,11 @@ of clones and fetches.
10651065
cloning of shallow repositories.
10661066
See 'GIT_TRACE' for available trace output options.
10671067

1068+
'GIT_TRACE_EXCLUDE'::
1069+
Enables trace messages that can help debugging .gitignore
1070+
processing. See 'GIT_TRACE' for available trace output
1071+
options.
1072+
10681073
'GIT_LITERAL_PATHSPECS'::
10691074
Setting this variable to `1` will cause Git to treat all
10701075
pathspecs literally, rather than as glob patterns. For example,

Documentation/gitignore.txt

Lines changed: 13 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,15 @@ 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+
144153
EXAMPLES
145154
--------
146155

dir.c

Lines changed: 201 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ static enum path_treatment read_directory_recursive(struct dir_struct *dir,
5353
int check_only, const struct path_simplify *simplify);
5454
static int get_dtype(struct dirent *de, const char *path, int len);
5555

56+
static struct trace_key trace_exclude = TRACE_KEY_INIT(EXCLUDE);
57+
5658
/* helper string functions with support for the ignore_case flag */
5759
int strcmp_icase(const char *a, const char *b)
5860
{
@@ -519,6 +521,7 @@ void add_exclude(const char *string, const char *base,
519521
x->baselen = baselen;
520522
x->flags = flags;
521523
x->srcpos = srcpos;
524+
string_list_init(&x->sticky_paths, 1);
522525
ALLOC_GROW(el->excludes, el->nr + 1, el->alloc);
523526
el->excludes[el->nr++] = x;
524527
x->el = el;
@@ -559,8 +562,10 @@ void clear_exclude_list(struct exclude_list *el)
559562
{
560563
int i;
561564

562-
for (i = 0; i < el->nr; i++)
565+
for (i = 0; i < el->nr; i++) {
566+
string_list_clear(&el->excludes[i]->sticky_paths, 0);
563567
free(el->excludes[i]);
568+
}
564569
free(el->excludes);
565570
free(el->filebuf);
566571

@@ -878,7 +883,7 @@ int match_pathname(const char *pathname, int pathlen,
878883
* then our prefix match is all we need; we
879884
* do not need to call fnmatch at all.
880885
*/
881-
if (!patternlen && !namelen)
886+
if (!patternlen && (!namelen || *name == '/'))
882887
return 1;
883888
}
884889

@@ -887,6 +892,113 @@ int match_pathname(const char *pathname, int pathlen,
887892
WM_PATHNAME) == 0;
888893
}
889894

895+
static void add_sticky(struct exclude *exc, const char *pathname, int pathlen)
896+
{
897+
struct strbuf sb = STRBUF_INIT;
898+
int i;
899+
900+
for (i = exc->sticky_paths.nr - 1; i >= 0; i--) {
901+
const char *sticky = exc->sticky_paths.items[i].string;
902+
int len = strlen(sticky);
903+
904+
if (pathlen < len && sticky[pathlen] == '/' &&
905+
!strncmp(pathname, sticky, pathlen))
906+
return;
907+
}
908+
909+
strbuf_add(&sb, pathname, pathlen);
910+
string_list_append_nodup(&exc->sticky_paths, strbuf_detach(&sb, NULL));
911+
}
912+
913+
static int match_sticky(struct exclude *exc, const char *pathname, int pathlen, int dtype)
914+
{
915+
int i;
916+
917+
for (i = exc->sticky_paths.nr - 1; i >= 0; i--) {
918+
const char *sticky = exc->sticky_paths.items[i].string;
919+
int len = strlen(sticky);
920+
921+
if (pathlen == len && dtype == DT_DIR &&
922+
!strncmp(pathname, sticky, len))
923+
return 1;
924+
925+
if (pathlen > len && pathname[len] == '/' &&
926+
!strncmp(pathname, sticky, len))
927+
return 1;
928+
}
929+
930+
return 0;
931+
}
932+
933+
static inline int different_decisions(const struct exclude *a,
934+
const struct exclude *b)
935+
{
936+
return (a->flags & EXC_FLAG_NEGATIVE) != (b->flags & EXC_FLAG_NEGATIVE);
937+
}
938+
939+
/*
940+
* Return non-zero if pathname is a directory and an ancestor of the
941+
* literal path in a pattern.
942+
*/
943+
static int match_directory_part(const char *pathname, int pathlen,
944+
int *dtype, struct exclude *x)
945+
{
946+
const char *base = x->base;
947+
int baselen = x->baselen ? x->baselen - 1 : 0;
948+
const char *pattern = x->pattern;
949+
int prefix = x->nowildcardlen;
950+
int patternlen = x->patternlen;
951+
952+
if (*dtype == DT_UNKNOWN)
953+
*dtype = get_dtype(NULL, pathname, pathlen);
954+
if (*dtype != DT_DIR)
955+
return 0;
956+
957+
if (*pattern == '/') {
958+
pattern++;
959+
patternlen--;
960+
prefix--;
961+
}
962+
963+
if (baselen) {
964+
if (((pathlen < baselen && base[pathlen] == '/') ||
965+
pathlen == baselen) &&
966+
!strncmp_icase(pathname, base, pathlen))
967+
return 1;
968+
pathname += baselen + 1;
969+
pathlen -= baselen + 1;
970+
}
971+
972+
973+
if (prefix &&
974+
(((pathlen < prefix && pattern[pathlen] == '/') ||
975+
pathlen == prefix) &&
976+
!strncmp_icase(pathname, pattern, pathlen)))
977+
return 1;
978+
979+
return 0;
980+
}
981+
982+
static struct exclude *should_descend(const char *pathname, int pathlen,
983+
int *dtype, struct exclude_list *el,
984+
struct exclude *exc)
985+
{
986+
int i;
987+
988+
for (i = el->nr - 1; 0 <= i; i--) {
989+
struct exclude *x = el->excludes[i];
990+
991+
if (x == exc)
992+
break;
993+
994+
if (!(x->flags & EXC_FLAG_NODIR) &&
995+
different_decisions(x, exc) &&
996+
match_directory_part(pathname, pathlen, dtype, x))
997+
return x;
998+
}
999+
return NULL;
1000+
}
1001+
8901002
/*
8911003
* Scan the given exclude list in reverse to see whether pathname
8921004
* should be ignored. The first match (i.e. the last on the list), if
@@ -900,16 +1012,32 @@ static struct exclude *last_exclude_matching_from_list(const char *pathname,
9001012
struct exclude_list *el)
9011013
{
9021014
struct exclude *exc = NULL; /* undecided */
903-
int i;
1015+
int i, maybe_descend = 0;
9041016

9051017
if (!el->nr)
9061018
return NULL; /* undefined */
9071019

1020+
trace_printf_key(&trace_exclude, "exclude: from %s\n", el->src);
1021+
9081022
for (i = el->nr - 1; 0 <= i; i--) {
9091023
struct exclude *x = el->excludes[i];
9101024
const char *exclude = x->pattern;
9111025
int prefix = x->nowildcardlen;
9121026

1027+
if (!maybe_descend && i < el->nr - 1 &&
1028+
different_decisions(x, el->excludes[i+1]))
1029+
maybe_descend = 1;
1030+
1031+
if (x->sticky_paths.nr) {
1032+
if (*dtype == DT_UNKNOWN)
1033+
*dtype = get_dtype(NULL, pathname, pathlen);
1034+
if (match_sticky(x, pathname, pathlen, *dtype)) {
1035+
exc = x;
1036+
break;
1037+
}
1038+
continue;
1039+
}
1040+
9131041
if (x->flags & EXC_FLAG_MUSTBEDIR) {
9141042
if (*dtype == DT_UNKNOWN)
9151043
*dtype = get_dtype(NULL, pathname, pathlen);
@@ -936,6 +1064,45 @@ static struct exclude *last_exclude_matching_from_list(const char *pathname,
9361064
break;
9371065
}
9381066
}
1067+
1068+
if (!exc) {
1069+
trace_printf_key(&trace_exclude, "exclude: %.*s => n/a\n",
1070+
pathlen, pathname);
1071+
return NULL;
1072+
}
1073+
1074+
/*
1075+
* We have found a matching pattern "exc" that may exclude whole
1076+
* directory. We also found that there may be a pattern that matches
1077+
* something inside the directory and reincludes stuff.
1078+
*
1079+
* Go through the patterns again, find that pattern and double check.
1080+
* If it's true, return "undecided" and keep descending in. "exc" is
1081+
* marked sticky so that it continues to match inside the directory.
1082+
*/
1083+
if (!(exc->flags & EXC_FLAG_NEGATIVE) && maybe_descend) {
1084+
struct exclude *x;
1085+
1086+
if (*dtype == DT_UNKNOWN)
1087+
*dtype = get_dtype(NULL, pathname, pathlen);
1088+
1089+
if (*dtype == DT_DIR &&
1090+
(x = should_descend(pathname, pathlen, dtype, el, exc))) {
1091+
add_sticky(exc, pathname, pathlen);
1092+
trace_printf_key(&trace_exclude,
1093+
"exclude: %.*s vs %s at line %d => %s,"
1094+
" forced open by %s at line %d => n/a\n",
1095+
pathlen, pathname, exc->pattern, exc->srcpos,
1096+
exc->flags & EXC_FLAG_NEGATIVE ? "no" : "yes",
1097+
x->pattern, x->srcpos);
1098+
return NULL;
1099+
}
1100+
}
1101+
1102+
trace_printf_key(&trace_exclude, "exclude: %.*s vs %s at line %d => %s%s\n",
1103+
pathlen, pathname, exc->pattern, exc->srcpos,
1104+
exc->flags & EXC_FLAG_NEGATIVE ? "no" : "yes",
1105+
exc->sticky_paths.nr ? " (stuck)" : "");
9391106
return exc;
9401107
}
9411108

@@ -1683,9 +1850,13 @@ static enum path_treatment read_directory_recursive(struct dir_struct *dir,
16831850
struct cached_dir cdir;
16841851
enum path_treatment state, subdir_state, dir_state = path_none;
16851852
struct strbuf path = STRBUF_INIT;
1853+
static int level = 0;
16861854

16871855
strbuf_add(&path, base, baselen);
16881856

1857+
trace_printf_key(&trace_exclude, "exclude: [%d] enter '%.*s'\n",
1858+
level++, baselen, base);
1859+
16891860
if (open_cached_dir(&cdir, dir, untracked, &path, check_only))
16901861
goto out;
16911862

@@ -1749,6 +1920,8 @@ static enum path_treatment read_directory_recursive(struct dir_struct *dir,
17491920
}
17501921
close_cached_dir(&cdir);
17511922
out:
1923+
trace_printf_key(&trace_exclude, "exclude: [%d] leave '%.*s'\n",
1924+
--level, baselen, base);
17521925
strbuf_release(&path);
17531926

17541927
return dir_state;
@@ -1985,6 +2158,25 @@ static struct untracked_cache_dir *validate_untracked_cache(struct dir_struct *d
19852158
return root;
19862159
}
19872160

2161+
static void clear_sticky(struct dir_struct *dir)
2162+
{
2163+
struct exclude_list_group *g;
2164+
struct exclude_list *el;
2165+
struct exclude *x;
2166+
int i, j, k;
2167+
2168+
for (i = EXC_CMDL; i <= EXC_FILE; i++) {
2169+
g = &dir->exclude_list_group[i];
2170+
for (j = g->nr - 1; j >= 0; j--) {
2171+
el = &g->el[j];
2172+
for (k = el->nr - 1; 0 <= k; k--) {
2173+
x = el->excludes[k];
2174+
string_list_clear(&x->sticky_paths, 0);
2175+
}
2176+
}
2177+
}
2178+
}
2179+
19882180
int read_directory(struct dir_struct *dir, const char *path, int len, const struct pathspec *pathspec)
19892181
{
19902182
struct path_simplify *simplify;
@@ -2005,6 +2197,12 @@ int read_directory(struct dir_struct *dir, const char *path, int len, const stru
20052197
if (has_symlink_leading_path(path, len))
20062198
return dir->nr;
20072199

2200+
/*
2201+
* Stay on the safe side. if read_directory() has run once on
2202+
* "dir", some sticky flag may have been left. Clear them all.
2203+
*/
2204+
clear_sticky(dir);
2205+
20082206
/*
20092207
* exclude patterns are treated like positive ones in
20102208
* create_simplify. Usually exclude patterns should be a

dir.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
/* See Documentation/technical/api-directory-listing.txt */
55

66
#include "strbuf.h"
7+
#include "string-list.h"
78

89
struct dir_entry {
910
unsigned int len;
@@ -34,6 +35,8 @@ struct exclude {
3435
* and from -1 decrementing for patterns from CLI args.
3536
*/
3637
int srcpos;
38+
39+
struct string_list sticky_paths;
3740
};
3841

3942
/*

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

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -175,13 +175,10 @@ test_expect_success 'negated exclude matches can override previous ones' '
175175
grep "^a.1" output
176176
'
177177

178-
test_expect_success 'excluded directory overrides content patterns' '
178+
test_expect_success 'excluded directory does not override content patterns' '
179179
180180
git ls-files --others --exclude="one" --exclude="!one/a.1" >output &&
181-
if grep "^one/a.1" output
182-
then
183-
false
184-
fi
181+
grep "^one/a.1" output
185182
'
186183

187184
test_expect_success 'negated directory doesn'\''t affect content patterns' '

0 commit comments

Comments
 (0)