Skip to content

Commit 2cb6088

Browse files
committed
Fix gitignore patterns in subdirectories not applying recursively
Fixes #146
1 parent 0a4df0e commit 2cb6088

File tree

5 files changed

+64
-3
lines changed

5 files changed

+64
-3
lines changed

fixtures/gitignore-nested/.gitkeep

Whitespace-only changes.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
*.log
2+
specific.txt
3+
deep/*.tmp

fixtures/gitignore-nested/subdir/deep/nested/.gitkeep

Whitespace-only changes.

ignore.js

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,41 @@ const ignoreFilesGlobOptions = {
2121

2222
export const GITIGNORE_FILES_PATTERN = '**/.gitignore';
2323

24-
const applyBaseToPattern = (pattern, base) => isNegativePattern(pattern)
25-
? '!' + path.posix.join(base, pattern.slice(1))
26-
: path.posix.join(base, pattern);
24+
// Apply base path to gitignore patterns based on .gitignore spec 2.22.1
25+
// https://git-scm.com/docs/gitignore#_pattern_format
26+
// See also https://github.com/sindresorhus/globby/issues/146
27+
const applyBaseToPattern = (pattern, base) => {
28+
if (!base) {
29+
return pattern;
30+
}
31+
32+
const isNegative = isNegativePattern(pattern);
33+
const cleanPattern = isNegative ? pattern.slice(1) : pattern;
34+
35+
// Check if pattern has non-trailing slashes
36+
const slashIndex = cleanPattern.indexOf('/');
37+
const hasNonTrailingSlash = slashIndex !== -1 && slashIndex !== cleanPattern.length - 1;
38+
39+
let result;
40+
if (!hasNonTrailingSlash) {
41+
// "If there is no separator at the beginning or middle of the pattern,
42+
// then the pattern may also match at any level below the .gitignore level."
43+
// So patterns like '*.log' or 'temp' or 'build/' (trailing slash) match recursively.
44+
result = path.posix.join(base, '**', cleanPattern);
45+
} else if (cleanPattern.startsWith('/')) {
46+
// "If there is a separator at the beginning [...] of the pattern,
47+
// then the pattern is relative to the directory level of the particular .gitignore file itself."
48+
// Leading slash anchors the pattern to the .gitignore's directory.
49+
result = path.posix.join(base, cleanPattern.slice(1));
50+
} else {
51+
// "If there is a separator [...] middle [...] of the pattern,
52+
// then the pattern is relative to the directory level of the particular .gitignore file itself."
53+
// Patterns like 'src/foo' are relative to the .gitignore's directory.
54+
result = path.posix.join(base, cleanPattern);
55+
}
56+
57+
return isNegative ? '!' + result : result;
58+
};
2759

2860
const parseIgnoreFile = (file, cwd) => {
2961
const base = slash(path.relative(cwd, path.dirname(file.filePath)));

tests/ignore.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,32 @@ test('check file', async t => {
144144
}
145145
});
146146

147+
test('gitignore patterns in subdirectories apply recursively', async t => {
148+
const cwd = path.join(PROJECT_ROOT, 'fixtures', 'gitignore-nested');
149+
const isIgnored = await isGitIgnored({cwd});
150+
151+
// Pattern '*.log' in subdir/.gitignore should ignore files at any level below
152+
t.true(isIgnored('subdir/file.log'));
153+
t.true(isIgnored('subdir/deep/file.log'));
154+
t.true(isIgnored('subdir/deep/deeper/file.log'));
155+
t.false(isIgnored('file.log')); // Not under subdir
156+
157+
// Pattern 'specific.txt' should ignore at any level below
158+
t.true(isIgnored('subdir/specific.txt'));
159+
t.true(isIgnored('subdir/deep/specific.txt'));
160+
t.false(isIgnored('specific.txt')); // Not under subdir
161+
});
162+
163+
test('gitignore patterns with slashes are relative to gitignore location', async t => {
164+
const cwd = path.join(PROJECT_ROOT, 'fixtures', 'gitignore-nested');
165+
const isIgnored = await isGitIgnored({cwd});
166+
167+
// Pattern 'deep/*.tmp' should only ignore direct children of deep/
168+
t.true(isIgnored('subdir/deep/file.tmp'));
169+
t.false(isIgnored('subdir/deep/nested/file.tmp'));
170+
t.false(isIgnored('subdir/file.tmp'));
171+
});
172+
147173
test('custom ignore files', async t => {
148174
const cwd = path.join(PROJECT_ROOT, 'fixtures/ignore-files');
149175
const files = [

0 commit comments

Comments
 (0)