Skip to content

Commit d6b8fc3

Browse files
committed
gitignore(5): Allow "foo/" in ignore list to match directory "foo"
A pattern "foo/" in the exclude list did not match directory "foo", but a pattern "foo" did. This attempts to extend the exclude mechanism so that it would while not matching a regular file or a symbolic link "foo". In order to differentiate a directory and non directory, this passes down the type of path being checked to excluded() function. A downside is that the recursive directory walk may need to run lstat(2) more often on systems whose "struct dirent" do not give the type of the entry; earlier it did not have to do so for an excluded path, but we now need to figure out if a path is a directory before deciding to exclude it. This is especially bad because an idea similar to the earlier CE_UPTODATE optimization to reduce number of lstat(2) calls would by definition not apply to the codepaths involved, as (1) directories will not be registered in the index, and (2) excluded paths will not be in the index anyway. Signed-off-by: Junio C Hamano <gitster@pobox.com>
1 parent 7a2078b commit d6b8fc3

File tree

7 files changed

+98
-15
lines changed

7 files changed

+98
-15
lines changed

Documentation/gitignore.txt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,13 @@ Patterns have the following format:
5757
included again. If a negated pattern matches, this will
5858
override lower precedence patterns sources.
5959

60+
- If the pattern ends with a slash, it is removed for the
61+
purpose of the following description, but it would only find
62+
a match with a directory. In other words, `foo/` will match a
63+
directory `foo` and paths underneath it, but will not match a
64+
regular file or a symbolic link `foo` (this is consistent
65+
with the way how pathspec works in general in git).
66+
6067
- If the pattern does not contain a slash '/', git treats it as
6168
a shell glob pattern and checks for a match against the
6269
pathname without leading directories.

builtin-ls-files.c

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,8 @@ static void show_files(struct dir_struct *dir, const char *prefix)
238238
if (show_cached | show_stage) {
239239
for (i = 0; i < active_nr; i++) {
240240
struct cache_entry *ce = active_cache[i];
241-
if (excluded(dir, ce->name) != dir->show_ignored)
241+
if (excluded(dir, ce->name, ce_to_dtype(ce)) !=
242+
dir->show_ignored)
242243
continue;
243244
if (show_unmerged && !ce_stage(ce))
244245
continue;
@@ -252,7 +253,8 @@ static void show_files(struct dir_struct *dir, const char *prefix)
252253
struct cache_entry *ce = active_cache[i];
253254
struct stat st;
254255
int err;
255-
if (excluded(dir, ce->name) != dir->show_ignored)
256+
if (excluded(dir, ce->name, ce_to_dtype(ce)) !=
257+
dir->show_ignored)
256258
continue;
257259
err = lstat(ce->name, &st);
258260
if (show_deleted && err)

cache.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,18 @@ static inline unsigned int ce_mode_from_stat(struct cache_entry *ce, unsigned in
141141
}
142142
return create_ce_mode(mode);
143143
}
144+
static inline int ce_to_dtype(const struct cache_entry *ce)
145+
{
146+
unsigned ce_mode = ntohl(ce->ce_mode);
147+
if (S_ISREG(ce_mode))
148+
return DT_REG;
149+
else if (S_ISDIR(ce_mode) || S_ISGITLINK(ce_mode))
150+
return DT_DIR;
151+
else if (S_ISLNK(ce_mode))
152+
return DT_LNK;
153+
else
154+
return DT_UNKNOWN;
155+
}
144156
#define canon_mode(mode) \
145157
(S_ISREG(mode) ? (S_IFREG | ce_permissions(mode)) : \
146158
S_ISLNK(mode) ? S_IFLNK : S_ISDIR(mode) ? S_IFDIR : S_IFGITLINK)

dir.c

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -126,18 +126,34 @@ static int no_wildcard(const char *string)
126126
void add_exclude(const char *string, const char *base,
127127
int baselen, struct exclude_list *which)
128128
{
129-
struct exclude *x = xmalloc(sizeof (*x));
129+
struct exclude *x;
130+
size_t len;
131+
int to_exclude = 1;
132+
int flags = 0;
130133

131-
x->to_exclude = 1;
132134
if (*string == '!') {
133-
x->to_exclude = 0;
135+
to_exclude = 0;
134136
string++;
135137
}
136-
x->pattern = string;
138+
len = strlen(string);
139+
if (len && string[len - 1] == '/') {
140+
char *s;
141+
x = xmalloc(sizeof(*x) + len);
142+
s = (char*)(x+1);
143+
memcpy(s, string, len - 1);
144+
s[len - 1] = '\0';
145+
string = s;
146+
x->pattern = s;
147+
flags = EXC_FLAG_MUSTBEDIR;
148+
} else {
149+
x = xmalloc(sizeof(*x));
150+
x->pattern = string;
151+
}
152+
x->to_exclude = to_exclude;
137153
x->patternlen = strlen(string);
138154
x->base = base;
139155
x->baselen = baselen;
140-
x->flags = 0;
156+
x->flags = flags;
141157
if (!strchr(string, '/'))
142158
x->flags |= EXC_FLAG_NODIR;
143159
if (no_wildcard(string))
@@ -261,7 +277,7 @@ static void prep_exclude(struct dir_struct *dir, const char *base, int baselen)
261277
* Return 1 for exclude, 0 for include and -1 for undecided.
262278
*/
263279
static int excluded_1(const char *pathname,
264-
int pathlen, const char *basename,
280+
int pathlen, const char *basename, int dtype,
265281
struct exclude_list *el)
266282
{
267283
int i;
@@ -272,6 +288,10 @@ static int excluded_1(const char *pathname,
272288
const char *exclude = x->pattern;
273289
int to_exclude = x->to_exclude;
274290

291+
if ((x->flags & EXC_FLAG_MUSTBEDIR) &&
292+
(dtype != DT_DIR))
293+
continue;
294+
275295
if (x->flags & EXC_FLAG_NODIR) {
276296
/* match basename */
277297
if (x->flags & EXC_FLAG_NOWILDCARD) {
@@ -314,7 +334,7 @@ static int excluded_1(const char *pathname,
314334
return -1; /* undecided */
315335
}
316336

317-
int excluded(struct dir_struct *dir, const char *pathname)
337+
int excluded(struct dir_struct *dir, const char *pathname, int dtype)
318338
{
319339
int pathlen = strlen(pathname);
320340
int st;
@@ -323,7 +343,8 @@ int excluded(struct dir_struct *dir, const char *pathname)
323343

324344
prep_exclude(dir, pathname, basename-pathname);
325345
for (st = EXC_CMDL; st <= EXC_FILE; st++) {
326-
switch (excluded_1(pathname, pathlen, basename, &dir->exclude_list[st])) {
346+
switch (excluded_1(pathname, pathlen, basename,
347+
dtype, &dir->exclude_list[st])) {
327348
case 0:
328349
return 0;
329350
case 1:
@@ -560,7 +581,8 @@ static int read_directory_recursive(struct dir_struct *dir, const char *path, co
560581
if (simplify_away(fullname, baselen + len, simplify))
561582
continue;
562583

563-
exclude = excluded(dir, fullname);
584+
dtype = get_dtype(de, fullname);
585+
exclude = excluded(dir, fullname, dtype);
564586
if (exclude && dir->collect_ignored
565587
&& in_pathspec(fullname, baselen + len, simplify))
566588
dir_add_ignored(dir, fullname, baselen + len);
@@ -572,8 +594,6 @@ static int read_directory_recursive(struct dir_struct *dir, const char *path, co
572594
if (exclude && !dir->show_ignored)
573595
continue;
574596

575-
dtype = get_dtype(de, fullname);
576-
577597
/*
578598
* Do we want to see just the ignored files?
579599
* We still need to recurse into directories,

dir.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ struct dir_entry {
99
#define EXC_FLAG_NODIR 1
1010
#define EXC_FLAG_NOWILDCARD 2
1111
#define EXC_FLAG_ENDSWITH 4
12+
#define EXC_FLAG_MUSTBEDIR 8
1213

1314
struct exclude_list {
1415
int nr;
@@ -67,7 +68,7 @@ extern int match_pathspec(const char **pathspec, const char *name, int namelen,
6768

6869
extern int read_directory(struct dir_struct *, const char *path, const char *base, int baselen, const char **pathspec);
6970

70-
extern int excluded(struct dir_struct *, const char *);
71+
extern int excluded(struct dir_struct *, const char *, int);
7172
extern void add_excludes_from_file(struct dir_struct *, const char *fname);
7273
extern void add_exclude(const char *string, const char *base,
7374
int baselen, struct exclude_list *which);

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

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,4 +99,45 @@ EOF
9999
test_expect_success 'git-status honours core.excludesfile' \
100100
'diff -u expect output'
101101

102+
test_expect_success 'trailing slash in exclude allows directory match(1)' '
103+
104+
git ls-files --others --exclude=one/ >output &&
105+
if grep "^one/" output
106+
then
107+
echo Ooops
108+
false
109+
else
110+
: happy
111+
fi
112+
113+
'
114+
115+
test_expect_success 'trailing slash in exclude allows directory match (2)' '
116+
117+
git ls-files --others --exclude=one/two/ >output &&
118+
if grep "^one/two/" output
119+
then
120+
echo Ooops
121+
false
122+
else
123+
: happy
124+
fi
125+
126+
'
127+
128+
test_expect_success 'trailing slash in exclude forces directory match (1)' '
129+
130+
>two
131+
git ls-files --others --exclude=two/ >output &&
132+
grep "^two" output
133+
134+
'
135+
136+
test_expect_success 'trailing slash in exclude forces directory match (2)' '
137+
138+
git ls-files --others --exclude=one/a.1/ >output &&
139+
grep "^one/a.1" output
140+
141+
'
142+
102143
test_done

unpack-trees.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -523,7 +523,7 @@ static void verify_absent(struct cache_entry *ce, const char *action,
523523
if (!lstat(ce->name, &st)) {
524524
int cnt;
525525

526-
if (o->dir && excluded(o->dir, ce->name))
526+
if (o->dir && excluded(o->dir, ce->name, ce_to_dtype(ce)))
527527
/*
528528
* ce->name is explicitly excluded, so it is Ok to
529529
* overwrite it.

0 commit comments

Comments
 (0)