@gerhobbelt/gitignore-parser
Version:
A simple .gitignore parser.
435 lines (378 loc) • 16.3 kB
JavaScript
/* eslint-env mocha, es6 */
let LIB = require('../');
let fs = require('fs');
let path = require('path');
let assert = require('assert');
let testgenerator = require('@gerhobbelt/markdown-it-testgen');
let FIXTURE = fs.readFileSync(path.join(__dirname, '/./.gitignore-fixture'), 'utf8');
let NO_NEGATIVES_FIXTURE = fs.readFileSync(path.join(__dirname, '/./.gitignore-no-negatives'), 'utf8');
describe('gitignore parser', function () {
describe('parse()', function () {
it('should parse some content', function () {
let parsed = LIB.parse(FIXTURE);
assert.strictEqual(parsed.length, 2);
});
it('should accept ** wildcards', function () {
let parsed = LIB.parse('a/**/b');
assert.deepEqual(parsed, [
[
/(?:^\/a(?:\/|(?:\/.+\/))b(?:$|\/))/,
[
/^\/a(?:\/|(?:\/.+\/))b(?:$|\/)/
]
],
[
/$^/,
[]
]
]
);
});
it('should accept * wildcards', function () {
let parsed = LIB.parse('a/b*c');
assert.deepEqual(parsed, [
[
/(?:^\/a\/b[^\/]*c(?:$|\/))/,
[
/^\/a\/b[^\/]*c(?:$|\/)/
]
],
[
/$^/,
[]
]
]
);
});
it('should accept ? wildcards', function () {
let parsed = LIB.parse('a/b?');
assert.deepEqual(parsed, [
[
/(?:^\/a\/b[^\/](?:$|\/))/,
[
/^\/a\/b[^\/](?:$|\/)/
]
],
[
/$^/,
[]
]
]
);
});
it('should correctly encode literals', function () {
let parsed = LIB.parse('a/b.c[d](e){f}\\slash^_$+$$$');
assert.deepEqual(parsed, [
[
/(?:^\/a\/b\.c[d]\(e\)\{f\}slash\^_\$\+\$\$\$(?:$|\/))/,
[
/^\/a\/b\.c[d]\(e\)\{f\}slash\^_\$\+\$\$\$(?:$|\/)/
]
],
[
/$^/,
[]
]
]
);
});
it('should correctly parse character ranges', function () {
let parsed = LIB.parse('a[c-z$].[1-9-][\\[\\]A-Z]-\\[...]');
assert.deepEqual(parsed, [
[
/(?:\/a[c-z$]\.[1-9-][\[\]A-Z]\-\[\.\.\.\](?:$|\/))/,
[
/\/a[c-z$]\.[1-9-][\[\]A-Z]\-\[\.\.\.\](?:$|\/)/
]
],
[
/$^/,
[]
]
]
);
});
it('should treat "/a" as rooted, while "b" should match anywhere', function () {
// see bullet 6 of https://git-scm.com/docs/gitignore#_pattern_format
//
// If there is a separator at the beginning or middle (or both) of the pattern,
// then the pattern is relative to the directory level of the particular
// .gitignore file itself. Otherwise the pattern may also match at any level
// below the .gitignore level.
let parsed = LIB.parse('/a\nb');
assert.deepEqual(parsed, [
[
/(?:^\/a(?:$|\/))|(?:\/b(?:$|\/))/,
[
/^\/a(?:$|\/)/,
/\/b(?:$|\/)/
]
],
[
/$^/,
[]
]
]
);
});
it('should correctly transpile rooted and relative path specs', function () {
let parsed = LIB.parse('a/b\n/c/d\ne/\nf');
assert.deepEqual(parsed, [
[
/(?:^\/c\/d(?:$|\/))|(?:^\/a\/b(?:$|\/))|(?:\/e\/)|(?:\/f(?:$|\/))/,
[
/^\/c\/d(?:$|\/)/,
/^\/a\/b(?:$|\/)/,
/\/e\//,
/\/f(?:$|\/)/
]
],
[
/$^/,
[]
]
]
);
});
});
describe('compile()', function () {
let ignore, gitignoreNoNegatives;
// gitignore file contents
// -----------------------
//
// # This is a comment in a .gitignore file!
// /node_modules
// *.log
//
// # Ignore this nonexistent file
// /nonexistent
//
// # Do not ignore this file
// !/nonexistent/foo
//
// # Ignore some files
//
// /baz
//
// /foo/*.wat
//
// # Ignore some deep sub folders
// /othernonexistent/**/what
//
// # Ignore deep folders with more than one wildcard
// */**/__MACOSX/**/*
//
// # Unignore some other sub folders
// !/othernonexistent/**/what/foo
//
// *.swp
//
beforeEach(function () {
gitignore = LIB.compile(FIXTURE);
gitignoreNoNegatives = LIB.compile(NO_NEGATIVES_FIXTURE);
});
function gitignoreAccepts(str, expects) {
assert.strictEqual(gitignore.accepts(str, expects), expects, `expected '${str}' to be ${ expects ? 'accepted' : 'rejected' }`);
}
function gitignoreNoNegativesAccepts(str, expects) {
assert.strictEqual(gitignoreNoNegatives.accepts(str, expects), expects, `expected '${str}' to be ${ expects ? 'accepted' : 'rejected' } when we use gitignoreNoNegatives`);
}
describe('accepts()', function () {
it('should accept the given filenames', function () {
gitignoreAccepts('test/index.js', true);
gitignoreAccepts('wat/test/index.js', true);
gitignoreNoNegativesAccepts('test/index.js', true);
gitignoreNoNegativesAccepts('node_modules.json', true);
});
it('should not accept the given filenames', function () {
gitignoreAccepts('test.swp', false);
gitignoreAccepts('foo/test.swp', false);
gitignoreAccepts('node_modules/wat.js', false);
gitignoreAccepts('foo/bar.wat', false);
gitignoreNoNegativesAccepts('node_modules/wat.js', false);
});
it('should not accept the given directory', function () {
gitignoreAccepts('nonexistent', false);
gitignoreAccepts('nonexistent/bar', false);
gitignoreNoNegativesAccepts('node_modules', false);
});
it('should accept unignored files in ignored directories', function () {
gitignoreAccepts('nonexistent/foo', true);
});
it('should accept nested unignored files in ignored directories', function () {
gitignoreAccepts('nonexistent/foo/wat', true);
});
});
function gitignoreDenies(str, expects) {
assert.strictEqual(gitignore.denies(str, expects), expects, `expected '${str}' to be ${ expects ? 'denied' : 'accepted' }`);
}
function gitignoreNoNegativesDenies(str, expects) {
assert.strictEqual(gitignoreNoNegatives.denies(str, expects), expects, `expected '${str}' to be ${ expects ? 'denied' : 'accepted' } when we use gitignoreNoNegatives`);
}
describe('denies()', function () {
it('should deny the given filenames', function () {
gitignoreDenies('test.swp', true);
gitignoreDenies('foo/test.swp', true);
gitignoreDenies('node_modules/wat.js', true);
gitignoreDenies('foo/bar.wat', true);
gitignoreNoNegativesDenies('node_modules/wat.js', true);
});
it('should not deny the given filenames', function () {
gitignoreDenies('test/index.js', false);
gitignoreDenies('wat/test/index.js', false);
gitignoreNoNegativesDenies('test/index.js', false);
gitignoreNoNegativesDenies('wat/test/index.js', false);
gitignoreNoNegativesDenies('node_modules.json', false);
});
it('should deny the given directory', function () {
gitignoreDenies('nonexistent', true);
gitignoreDenies('nonexistent/bar', true);
gitignoreNoNegativesDenies('node_modules', true);
gitignoreNoNegativesDenies('node_modules/foo', true);
});
it('should not deny unignored files in ignored directories', function () {
gitignoreDenies('nonexistent/foo', false);
});
it('should not deny nested unignored files in ignored directories', function () {
gitignoreDenies('nonexistent/foo/wat', false);
});
});
function gitignoreIsInspected(str, expects) {
assert.strictEqual(gitignore.inspects(str, expects), expects, `expected '${str}' to ${ expects ? 'be inspected' : 'NOT be inspected' }`);
}
function gitignoreNoNegativesIsInspected(str, expects) {
assert.strictEqual(gitignoreNoNegatives.inspects(str, expects), expects, `expected '${str}' to ${ expects ? 'be inspected' : 'NOT be inspected' } when we use gitignoreNoNegatives`);
}
describe('inspects()', function () {
it('should return false for directories not mentioned by .gitignore', function () {
gitignoreIsInspected('lib', false);
gitignoreIsInspected('lib/foo/bar', false);
gitignoreNoNegativesIsInspected('lib', false);
gitignoreNoNegativesIsInspected('lib/foo/bar', false);
});
it('should return true for directories explicitly mentioned by .gitignore', function () {
gitignoreIsInspected('baz', true);
gitignoreIsInspected('baz/wat/foo', true);
gitignoreNoNegativesIsInspected('node_modules', true);
});
it('should return true for ignored directories that have exceptions', function () {
gitignoreIsInspected('nonexistent', true);
gitignoreIsInspected('nonexistent/foo', true);
gitignoreIsInspected('nonexistent/foo/bar', true);
});
it('should return true for non exceptions of ignored subdirectories', function () {
gitignoreIsInspected('nonexistent/wat', true);
gitignoreIsInspected('nonexistent/wat/foo', true);
gitignoreNoNegativesIsInspected('node_modules/wat/foo', true);
});
});
});
describe('github issues & misc tests', function () {
function gitignoreAccepts(gitignore, str, expects) {
assert.strictEqual(gitignore.accepts(str, expects), expects, `expected '${str}' to be ${ expects ? 'accepted' : 'rejected' }`);
}
function gitignoreDenies(gitignore, str, expects) {
assert.strictEqual(gitignore.denies(str, expects), expects, `expected '${str}' to be ${ expects ? 'denied' : 'accepted' }`);
}
describe('Test case: a', function () {
it('should only accept a/2/a', function () {
const gitignore = LIB.compile(fs.readFileSync(path.join(__dirname, '/a/.gitignore-fixture'), 'utf8'));
gitignoreAccepts(gitignore, 'a/2/a', true);
gitignoreAccepts(gitignore, 'a/3/a', false);
});
});
describe('issue #11', function () {
it('should not fail test A', function () {
let gitignore = LIB.compile('foo.txt');
gitignoreDenies(gitignore, 'foo.txt', true);
gitignoreAccepts(gitignore, 'foo.txt', false);
});
it('should not fail test B', function () {
let gitignore = LIB.compile('foo.txt');
gitignoreDenies(gitignore, 'a/foo.txt', true);
gitignoreAccepts(gitignore, 'a/foo.txt', false);
});
});
describe('issue #12', function () {
it('should not fail test A', function () {
let gitignore = LIB.compile('/ajax/libs/bPopup/*b*');
gitignoreAccepts(gitignore, '/ajax/libs/bPopup/0.9.0', true); //output false
});
it('should not fail test B', function () {
let gitignore = LIB.compile('/ajax/libs/jquery-form-validator/2.2');
gitignoreAccepts(gitignore, '/ajax/libs/jquery-form-validator/2.2.43', true); //output false
});
it('should not fail test C', function () {
let gitignore = LIB.compile('/ajax/libs/punycode/2.0');
gitignoreAccepts(gitignore, '/ajax/libs/punycode/2.0.0', true); //output false
});
it('should not fail test D', function () {
let gitignore = LIB.compile('/ajax/libs/typescript/*dev*');
gitignoreAccepts(gitignore, '/ajax/libs/typescript/2.0.6-insiders.20161014', true); //output false
});
});
describe('issue #14', function () {
it('should not fail test A', function () {
let gitignore = LIB.compile('node-modules');
gitignoreDenies(gitignore, 'packages/my-package/node-modules', true);
gitignoreAccepts(gitignore, 'packages/my-package/node-modules', false);
gitignoreDenies(gitignore, 'packages/my-package/node-modules/a', true);
gitignoreAccepts(gitignore, 'packages/my-package/node-modules/a', false);
gitignoreDenies(gitignore, 'node-modules/a', true);
gitignoreAccepts(gitignore, 'node-modules/a', false);
});
});
describe('when using the `expected` argument, accepts/denies should fire the `diagnose()` callback', function () {
it('accepts() should fail and call diagnose()', function () {
let gitignore = LIB.compile('node_modules');
let count = 0;
gitignore.diagnose = function () { count++; };
assert.strictEqual(gitignore.accepts('node_modules', true /* INTENTIONALLY WRONG */), false, 'accepts() call is expected to return FALSE');
assert.strictEqual(count, 1, 'gitignore.diagnose() should have been invoked once from inside the accepts() call');
// the next one is matching the expectation, hence no diagnose() call:
assert.strictEqual(gitignore.accepts('node_modules', false), false, 'accepts() call is expected to return FALSE');
assert.strictEqual(count, 1, 'gitignore.diagnose() should NOT have been invoked once from inside the accepts() call');
assert.strictEqual(gitignore.denies('node_modules', false /* INTENTIONALLY WRONG */), true, 'denies() call is expected to return TRUE');
assert.strictEqual(count, 2, 'gitignore.diagnose() should have been invoked once from inside the denies() call');
// the next one is matching the expectation, hence no diagnose() call:
assert.strictEqual(gitignore.denies('node_modules', true), true, 'denies() call is expected to return TRUE');
assert.strictEqual(count, 2, 'gitignore.diagnose() should NOT have been invoked once from inside the denies() call');
assert.strictEqual(gitignore.inspects('node_modules', false /* INTENTIONALLY WRONG */), true, 'inspects() call is expected to return TRUE');
assert.strictEqual(count, 3, 'gitignore.diagnose() should have been invoked once from inside the inspects() call');
// the next one is matching the expectation, hence no diagnose() call:
assert.strictEqual(gitignore.inspects('node_modules', true), true, 'inspects() call is expected to return TRUE');
assert.strictEqual(count, 3, 'gitignore.diagnose() should NOT have been invoked once from inside the inspects() call');
});
});
});
testgenerator(path.join(__dirname, 'fixtures/gitignore.manpage.txt'), {
desc: 'gitignore specification / manpage',
test: function test_gitignore(_it_, title, fixture, options, md, env) {
let gitignore = LIB.compile(fixture.first.text);
function mkTest(title, test) {
let t = test.split('⟹').map((l) => l.trim());
let expect = t[1].toLowerCase();
let expectAccept = expect.replace(/^.*(accept|reject).*$/, '$1') === 'accept';
// 'inspects' expectation: unless overridden, any 'accept' won't have been inspected, while all 'reject's certainly have:
let expectNoInspection = /inspect/.test(expect) ? /\!inspects|not inspect/.test(expect) : expectAccept;
it(title, function () {
assert.strictEqual(gitignore.accepts(t[0], expectAccept), expectAccept, `expected: ${t[0]} ⟹ ${expectAccept ? 'accept' : 'reject'}`);
assert.strictEqual(gitignore.denies(t[0], !expectAccept), !expectAccept, `expected: ${t[0]} ⟹ ${expectAccept ? 'accept' : 'reject'}`);
assert.strictEqual(gitignore.inspects(t[0], !expectNoInspection), !expectNoInspection, `expected inspection: ${t[0]} ⟹ ${expectNoInspection ? 'does not inspect (!inspects)' : 'inspects'}`);
});
}
let tests = fixture.second.text.split('\n');
let line = fixture.second.range[0];
for (let i = 0; i < tests.length; i++) {
let test = tests[i].trim();
// don't do this bit in a .filter() expression as we want to keep our
// line numbers intact for the real tests:
if (test.length === 0 || test.startsWith('#')) {
continue;
}
// watch out for closure use inside loop! --> use indirection via mkTest()
mkTest('line ' + (line + i + 1), test);
}
}
});
});