├── .gitignore ├── .travis.yml ├── LICENSE.md ├── README.md ├── lib └── index.js ├── package.json └── test ├── .gitignore-no-negatives ├── index.js └── mocha.opts /.gitignore: -------------------------------------------------------------------------------- 1 | # This is a comment in a .gitignore file! 2 | /node_modules 3 | *.log 4 | 5 | # Ignore this nonexistent file 6 | /nonexistent 7 | 8 | # Do not ignore this file 9 | !/nonexistent/foo 10 | 11 | # Ignore some files 12 | 13 | /baz 14 | 15 | /foo/*.wat 16 | 17 | # Ignore some deep sub folders 18 | /othernonexistent/**/what 19 | 20 | # Unignore some other sub folders 21 | !/othernonexistent/**/what/foo 22 | 23 | 24 | *.swp 25 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # Copyright 2014 codemix ltd. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gitignore Parser 2 | 3 | A very simple `.gitignore` parser for node.js. 4 | 5 | [![Build Status](https://travis-ci.org/codemix/gitignore-parser.svg?branch=master)](https://travis-ci.org/codemix/gitignore-parser) 6 | 7 | 8 | ## Installation 9 | 10 | `npm install gitignore-parser` 11 | 12 | 13 | ## Usage 14 | 15 | ```js 16 | var parser = require('gitignore-parser'), 17 | fs = require('fs'); 18 | 19 | var gitignore = parser.compile(fs.readFileSync('.gitignore', 'utf8')); 20 | 21 | gitignore.accepts('LICENSE.md') === true; 22 | gitignore.denies('LICENSE.md') === false; 23 | 24 | gitignore.accepts('node_modules/mocha/bin') === false; 25 | gitignore.denies('node_modules/mocha/bin') === true; 26 | 27 | 28 | var files = [ 29 | '.gitignore', 30 | '.travis.yml', 31 | 'LICENSE.md', 32 | 'README.md', 33 | 'package.json', 34 | 'lib/index.js', 35 | 'test/index.js', 36 | 'test/mocha.opts', 37 | 'node_modules/mocha/bin/mocha', 38 | 'node_modules/mocha/README.md' 39 | ]; 40 | 41 | // only files that are not gitignored 42 | files.filter(gitignore.accepts); 43 | 44 | // only files that *are* gitignored 45 | files.filter(gitignore.denies); 46 | 47 | ``` 48 | 49 | 50 | ### License 51 | 52 | Apache 2, see [LICENSE.md](./LICENSE.md). 53 | 54 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Compile the given `.gitignore` content (not filename!) 3 | * and return an object with `accepts`, `denies` and `maybe` methods. 4 | * These methods each accepts a single filename and determines whether 5 | * they are acceptable or unacceptable according to the `.gitignore` definition. 6 | * 7 | * 8 | * @param {String} content The `.gitignore` content to compile. 9 | * @return {Object} The helper object with methods that operate on the compiled content. 10 | */ 11 | exports.compile = function (content) { 12 | var parsed = exports.parse(content), 13 | positives = parsed[0], 14 | negatives = parsed[1]; 15 | return { 16 | accepts: function (input) { 17 | if (input[0] === '/') input = input.slice(1); 18 | return negatives[0].test(input) || !positives[0].test(input); 19 | }, 20 | denies: function (input) { 21 | if (input[0] === '/') input = input.slice(1); 22 | return !(negatives[0].test(input) || !positives[0].test(input)); 23 | }, 24 | maybe: function (input) { 25 | if (input[0] === '/') input = input.slice(1); 26 | return negatives[1].test(input) || !positives[1].test(input); 27 | } 28 | }; 29 | }; 30 | 31 | /** 32 | * Parse the given `.gitignore` content and return an array 33 | * containing two further arrays - positives and negatives. 34 | * Each of these two arrays in turn contains two regexps, one 35 | * strict and one for 'maybe'. 36 | * 37 | * @param {String} content The content to parse, 38 | * @return {Array[]} The parsed positive and negatives definitions. 39 | */ 40 | exports.parse = function (content) { 41 | return content.split('\n') 42 | .map(function (line) { 43 | line = line.trim(); 44 | return line; 45 | }) 46 | .filter(function (line) { 47 | return line && line[0] !== '#'; 48 | }) 49 | .reduce(function (lists, line) { 50 | var isNegative = line[0] === '!'; 51 | if (isNegative) { 52 | line = line.slice(1); 53 | } 54 | if (line[0] === '/') 55 | line = line.slice(1); 56 | if (isNegative) { 57 | lists[1].push(line); 58 | } 59 | else { 60 | lists[0].push(line); 61 | } 62 | return lists; 63 | }, [[], []]) 64 | .map(function (list) { 65 | return list 66 | .sort() 67 | .map(prepareRegexes) 68 | .reduce(function (list, prepared) { 69 | list[0].push(prepared[0]); 70 | list[1].push(prepared[1]); 71 | return list; 72 | }, [[], [], []]); 73 | }) 74 | .map(function (item) { 75 | return [ 76 | item[0].length > 0 ? new RegExp('^((' + item[0].join(')|(') + '))') : new RegExp('$^'), 77 | item[1].length > 0 ? new RegExp('^((' + item[1].join(')|(') + '))') : new RegExp('$^') 78 | ] 79 | }); 80 | }; 81 | 82 | function prepareRegexes (pattern) { 83 | return [ 84 | // exact regex 85 | prepareRegexPattern(pattern), 86 | // partial regex 87 | preparePartialRegex(pattern) 88 | ]; 89 | }; 90 | 91 | function prepareRegexPattern (pattern) { 92 | return escapeRegex(pattern).replace('**', '(.+)').replace('*', '([^\\/]+)'); 93 | } 94 | 95 | function preparePartialRegex (pattern) { 96 | return pattern 97 | .split('/') 98 | .map(function (item, index) { 99 | if (index) 100 | return '([\\/]?(' + prepareRegexPattern(item) + '\\b|$))'; 101 | else 102 | return '(' + prepareRegexPattern(item) + '\\b)'; 103 | }) 104 | .join(''); 105 | } 106 | 107 | function escapeRegex (pattern) { 108 | return pattern.replace(/[\-\[\]\/\{\}\(\)\+\?\.\\\^\$\|]/g, "\\$&"); 109 | } 110 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gitignore-parser", 3 | "version": "0.0.2", 4 | "description": "A simple .gitignore parser.", 5 | "keywords": [ 6 | "gitignore", 7 | "git", 8 | ".gitignore", 9 | "git ignore" 10 | ], 11 | "main": "lib/index.js", 12 | "dependencies": {}, 13 | "devDependencies": { 14 | "mocha": "*", 15 | "should": "*", 16 | "expect.js": "*" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "http://github.com/codemix/gitignore-parser.git" 21 | }, 22 | "main": "./lib/index.js", 23 | "directories": { 24 | "lib": "./lib" 25 | }, 26 | "engines": { 27 | "node": ">=0.10.0" 28 | }, 29 | "scripts": { 30 | "test": "node ./node_modules/mocha/bin/mocha", 31 | "watch": "node ./node_modules/mocha/bin/mocha --watch" 32 | }, 33 | "author": { 34 | "name": "Charles Pick", 35 | "email": "charles@codemix.com" 36 | }, 37 | "licenses": [ 38 | { 39 | "type": "Apache License, Version 2.0", 40 | "url": "http://www.apache.org/licenses/LICENSE-2.0" 41 | } 42 | ] 43 | } 44 | -------------------------------------------------------------------------------- /test/.gitignore-no-negatives: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | var LIB = require('../lib'), 2 | fs = require('fs'); 3 | 4 | var FIXTURE = fs.readFileSync(__dirname + '/../.gitignore', 'utf8'); 5 | var NO_NEGATIVES_FIXTURE = fs.readFileSync(__dirname + '/./.gitignore-no-negatives', 'utf8'); 6 | 7 | describe('gitignore parser', function () { 8 | describe('parse()', function () { 9 | it('should parse some content', function () { 10 | var parsed = LIB.parse(FIXTURE); 11 | parsed.length.should.equal(2); 12 | }); 13 | }); 14 | 15 | describe('compile()', function () { 16 | beforeEach(function () { 17 | this.gitignore = LIB.compile(FIXTURE); 18 | this.gitignoreNoNegatives = LIB.compile(NO_NEGATIVES_FIXTURE); 19 | }); 20 | 21 | describe('accepts()', function () { 22 | it('should accept the given filenames', function () { 23 | this.gitignore.accepts('test/index.js').should.be.true; 24 | this.gitignore.accepts('wat/test/index.js').should.be.true; 25 | this.gitignoreNoNegatives.accepts('test/index.js').should.be.true; 26 | }); 27 | 28 | it('should not accept the given filenames', function () { 29 | this.gitignore.accepts('test.swp').should.be.false; 30 | this.gitignore.accepts('node_modules/wat.js').should.be.false; 31 | this.gitignore.accepts('foo/bar.wat').should.be.false; 32 | this.gitignoreNoNegatives.accepts('node_modules/wat.js').should.be.false; 33 | }); 34 | 35 | it('should not accept the given directory', function () { 36 | this.gitignore.accepts('nonexistent').should.be.false; 37 | this.gitignore.accepts('nonexistent/bar').should.be.false; 38 | this.gitignoreNoNegatives.accepts('node_modules').should.be.false; 39 | }); 40 | 41 | it('should accept unignored files in ignored directories', function () { 42 | this.gitignore.accepts('nonexistent/foo').should.be.true; 43 | }); 44 | 45 | it('should accept nested unignored files in ignored directories', function () { 46 | this.gitignore.accepts('nonexistent/foo/wat').should.be.true; 47 | }); 48 | }); 49 | 50 | describe('denies()', function () { 51 | it('should deny the given filenames', function () { 52 | this.gitignore.denies('test.swp').should.be.true; 53 | this.gitignore.denies('node_modules/wat.js').should.be.true; 54 | this.gitignore.denies('foo/bar.wat').should.be.true; 55 | this.gitignoreNoNegatives.denies('node_modules/wat.js').should.be.true; 56 | }); 57 | 58 | it('should not deny the given filenames', function () { 59 | this.gitignore.denies('test/index.js').should.be.false; 60 | this.gitignore.denies('wat/test/index.js').should.be.false; 61 | this.gitignoreNoNegatives.denies('test/index.js').should.be.false; 62 | this.gitignoreNoNegatives.denies('wat/test/index.js').should.be.false; 63 | }); 64 | 65 | it('should deny the given directory', function () { 66 | this.gitignore.denies('nonexistent').should.be.true; 67 | this.gitignore.denies('nonexistent/bar').should.be.true; 68 | this.gitignoreNoNegatives.denies('node_modules').should.be.true; 69 | this.gitignoreNoNegatives.denies('node_modules/foo').should.be.true; 70 | }); 71 | 72 | it('should not deny unignored files in ignored directories', function () { 73 | this.gitignore.denies('nonexistent/foo').should.be.false; 74 | }); 75 | 76 | it('should not deny nested unignored files in ignored directories', function () { 77 | this.gitignore.denies('nonexistent/foo/wat').should.be.false; 78 | }); 79 | }); 80 | 81 | describe('maybe()', function () { 82 | it('should return true for directories not mentioned by .gitignore', function () { 83 | this.gitignore.maybe('lib').should.be.true; 84 | this.gitignore.maybe('lib/foo/bar').should.be.true; 85 | this.gitignoreNoNegatives.maybe('lib').should.be.true; 86 | this.gitignoreNoNegatives.maybe('lib/foo/bar').should.be.true; 87 | }); 88 | 89 | it('should return false for directories explicitly mentioned by .gitignore', function () { 90 | this.gitignore.maybe('baz').should.be.false; 91 | this.gitignore.maybe('baz/wat/foo').should.be.false; 92 | this.gitignoreNoNegatives.maybe('node_modules').should.be.false; 93 | }); 94 | 95 | it('should return true for ignored directories that have exceptions', function () { 96 | this.gitignore.maybe('nonexistent').should.be.true; 97 | this.gitignore.maybe('nonexistent/foo').should.be.true; 98 | this.gitignore.maybe('nonexistent/foo/bar').should.be.true; 99 | }); 100 | 101 | it('should return false for non exceptions of ignored subdirectories', function () { 102 | this.gitignore.maybe('nonexistent/wat').should.be.false; 103 | this.gitignore.maybe('nonexistent/wat/foo').should.be.false; 104 | this.gitignoreNoNegatives.maybe('node_modules/wat/foo').should.be.false; 105 | }); 106 | }); 107 | }); 108 | }); 109 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --reporter=spec 2 | --require=should 3 | --------------------------------------------------------------------------------