Skip to content

Commit f87f949

Browse files
author
Junio C Hamano
committed
git-ls-files: --exclude mechanism updates.
Add --exclude-per-directory=<name> option that specifies a file to contain exclude patterns local to that directory and its subdirectories. Update the exclusion logic to be able to say "include files that match this more specific pattern, even though later exclude patterns may match them". Also enhances that a pattern can contain '/' in which case fnmatch is called with FNM_PATHNAME flag to match the entire path. Signed-off-by: Junio C Hamano <junkio@cox.net>
1 parent b7e438f commit f87f949

File tree

2 files changed

+157
-21
lines changed

2 files changed

+157
-21
lines changed

ls-files.c

Lines changed: 102 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -25,20 +25,31 @@ static const char *tag_removed = "";
2525
static const char *tag_other = "";
2626
static const char *tag_killed = "";
2727

28+
static char *exclude_per_dir = NULL;
2829
static int nr_excludes;
29-
static const char **excludes;
3030
static int excludes_alloc;
31+
static struct exclude {
32+
const char *pattern;
33+
const char *base;
34+
int baselen;
35+
} **excludes;
3136

32-
static void add_exclude(const char *string)
37+
static void add_exclude(const char *string, const char *base, int baselen)
3338
{
39+
struct exclude *x = xmalloc(sizeof (*x));
40+
41+
x->pattern = string;
42+
x->base = base;
43+
x->baselen = baselen;
3444
if (nr_excludes == excludes_alloc) {
3545
excludes_alloc = alloc_nr(excludes_alloc);
3646
excludes = realloc(excludes, excludes_alloc*sizeof(char *));
3747
}
38-
excludes[nr_excludes++] = string;
48+
excludes[nr_excludes++] = x;
3949
}
4050

41-
static void add_excludes_from_file(const char *fname)
51+
static int add_excludes_from_file_1(const char *fname,
52+
const char *base, int baselen)
4253
{
4354
int fd, i;
4455
long size;
@@ -53,7 +64,7 @@ static void add_excludes_from_file(const char *fname)
5364
lseek(fd, 0, SEEK_SET);
5465
if (size == 0) {
5566
close(fd);
56-
return;
67+
return 0;
5768
}
5869
buf = xmalloc(size);
5970
if (read(fd, buf, size) != size)
@@ -63,28 +74,89 @@ static void add_excludes_from_file(const char *fname)
6374
entry = buf;
6475
for (i = 0; i < size; i++) {
6576
if (buf[i] == '\n') {
66-
if (entry != buf + i) {
77+
if (entry != buf + i && entry[0] != '#') {
6778
buf[i] = 0;
68-
add_exclude(entry);
79+
add_exclude(entry, base, baselen);
6980
}
7081
entry = buf + i + 1;
7182
}
7283
}
73-
return;
84+
return 0;
85+
86+
err:
87+
if (0 <= fd)
88+
close(fd);
89+
return -1;
90+
}
91+
92+
static void add_excludes_from_file(const char *fname)
93+
{
94+
if (add_excludes_from_file_1(fname, "", 0) < 0)
95+
die("cannot use %s as an exclude file", fname);
96+
}
97+
98+
static int push_exclude_per_directory(const char *base, int baselen)
99+
{
100+
char exclude_file[PATH_MAX];
101+
int current_nr = nr_excludes;
102+
103+
if (exclude_per_dir) {
104+
memcpy(exclude_file, base, baselen);
105+
strcpy(exclude_file + baselen, exclude_per_dir);
106+
add_excludes_from_file_1(exclude_file, base, baselen);
107+
}
108+
return current_nr;
109+
}
74110

75-
err: perror(fname);
76-
exit(1);
111+
static void pop_exclude_per_directory(int stk)
112+
{
113+
while (stk < nr_excludes)
114+
free(excludes[--nr_excludes]);
77115
}
78116

79117
static int excluded(const char *pathname)
80118
{
81119
int i;
120+
82121
if (nr_excludes) {
83-
const char *basename = strrchr(pathname, '/');
84-
basename = (basename) ? basename+1 : pathname;
85-
for (i = 0; i < nr_excludes; i++)
86-
if (fnmatch(excludes[i], basename, 0) == 0)
87-
return 1;
122+
int pathlen = strlen(pathname);
123+
124+
for (i = 0; i < nr_excludes; i++) {
125+
struct exclude *x = excludes[i];
126+
const char *exclude = x->pattern;
127+
int to_exclude = 1;
128+
129+
if (*exclude == '!') {
130+
to_exclude = 0;
131+
exclude++;
132+
}
133+
134+
if (!strchr(exclude, '/')) {
135+
/* match basename */
136+
const char *basename = strrchr(pathname, '/');
137+
basename = (basename) ? basename+1 : pathname;
138+
if (fnmatch(exclude, basename, 0) == 0)
139+
return to_exclude;
140+
}
141+
else {
142+
/* match with FNM_PATHNAME:
143+
* exclude has base (baselen long) inplicitly
144+
* in front of it.
145+
*/
146+
int baselen = x->baselen;
147+
if (*exclude == '/')
148+
exclude++;
149+
150+
if (pathlen < baselen ||
151+
(baselen && pathname[baselen-1] != '/') ||
152+
strncmp(pathname, x->base, baselen))
153+
continue;
154+
155+
if (fnmatch(exclude, pathname+baselen,
156+
FNM_PATHNAME) == 0)
157+
return to_exclude;
158+
}
159+
}
88160
}
89161
return 0;
90162
}
@@ -121,18 +193,21 @@ static void add_name(const char *pathname, int len)
121193
* doesn't handle them at all yet. Maybe that will change some
122194
* day.
123195
*
124-
* Also, we currently ignore all names starting with a dot.
196+
* Also, we ignore the name ".git" (even if it is not a directory).
125197
* That likely will not change.
126198
*/
127199
static void read_directory(const char *path, const char *base, int baselen)
128200
{
129201
DIR *dir = opendir(path);
130202

131203
if (dir) {
204+
int exclude_stk;
132205
struct dirent *de;
133206
char fullname[MAXPATHLEN + 1];
134207
memcpy(fullname, base, baselen);
135208

209+
exclude_stk = push_exclude_per_directory(base, baselen);
210+
136211
while ((de = readdir(dir)) != NULL) {
137212
int len;
138213

@@ -141,10 +216,10 @@ static void read_directory(const char *path, const char *base, int baselen)
141216
!strcmp(de->d_name + 1, ".") ||
142217
!strcmp(de->d_name + 1, "git")))
143218
continue;
144-
if (excluded(de->d_name) != show_ignored)
145-
continue;
146219
len = strlen(de->d_name);
147220
memcpy(fullname + baselen, de->d_name, len+1);
221+
if (excluded(fullname) != show_ignored)
222+
continue;
148223

149224
switch (DTYPE(de)) {
150225
struct stat st;
@@ -170,6 +245,8 @@ static void read_directory(const char *path, const char *base, int baselen)
170245
add_name(fullname, baselen + len);
171246
}
172247
closedir(dir);
248+
249+
pop_exclude_per_directory(exclude_stk);
173250
}
174251
}
175252

@@ -287,7 +364,9 @@ static void show_files(void)
287364

288365
static const char *ls_files_usage =
289366
"git-ls-files [-z] [-t] (--[cached|deleted|others|stage|unmerged|killed])* "
290-
"[ --ignored [--exclude=<pattern>] [--exclude-from=<file>) ]";
367+
"[ --ignored ] [--exclude=<pattern>] [--exclude-from=<file>] "
368+
"[ --exclude-per-directory=<filename> ]";
369+
;
291370

292371
int main(int argc, char **argv)
293372
{
@@ -323,13 +402,15 @@ int main(int argc, char **argv)
323402
show_stage = 1;
324403
show_unmerged = 1;
325404
} else if (!strcmp(arg, "-x") && i+1 < argc) {
326-
add_exclude(argv[++i]);
405+
add_exclude(argv[++i], "", 0);
327406
} else if (!strncmp(arg, "--exclude=", 10)) {
328-
add_exclude(arg+10);
407+
add_exclude(arg+10, "", 0);
329408
} else if (!strcmp(arg, "-X") && i+1 < argc) {
330409
add_excludes_from_file(argv[++i]);
331410
} else if (!strncmp(arg, "--exclude-from=", 15)) {
332411
add_excludes_from_file(arg+15);
412+
} else if (!strncmp(arg, "--exclude-per-directory=", 24)) {
413+
exclude_per_dir = arg + 24;
333414
} else
334415
usage(ls_files_usage);
335416
}

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

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
#!/bin/sh
2+
#
3+
# Copyright (c) 2005 Junio C Hamano
4+
#
5+
6+
test_description='git-ls-files --others --exclude
7+
8+
This test runs git-ls-files --others and tests --exclude patterns.
9+
'
10+
11+
. ./test-lib.sh
12+
13+
rm -fr one three
14+
for dir in . one one/two three
15+
do
16+
mkdir -p $dir &&
17+
for i in 1 2 3 4 5
18+
do
19+
>$dir/a.$i
20+
done
21+
done
22+
23+
cat >expect <<EOF
24+
a.2
25+
a.4
26+
a.5
27+
one/a.3
28+
one/a.4
29+
one/a.5
30+
one/two/a.3
31+
one/two/a.5
32+
three/a.2
33+
three/a.3
34+
three/a.4
35+
three/a.5
36+
EOF
37+
38+
echo '.gitignore
39+
output
40+
expect
41+
.gitignore
42+
' >.git/ignore
43+
44+
echo '*.1
45+
/*.3' >.gitignore
46+
echo '*.2
47+
two/*.4' >one/.gitignore
48+
49+
test_expect_success \
50+
'git-ls-files --others --exclude.' \
51+
'git-ls-files --others \
52+
--exclude-per-directory=.gitignore \
53+
--exclude-from=.git/ignore \
54+
>output &&
55+
diff -u expect output'

0 commit comments

Comments
 (0)