| // Essentially, this is a fstream.DirReader class, but with a |
| // bit of special logic to read the specified sort of ignore files, |
| // and a filter that prevents it from picking up anything excluded |
| // by those files. |
| |
| var Minimatch = require("minimatch").Minimatch |
| , fstream = require("fstream") |
| , DirReader = fstream.DirReader |
| , inherits = require("inherits") |
| , path = require("path") |
| , fs = require("fs") |
| |
| module.exports = IgnoreReader |
| |
| inherits(IgnoreReader, DirReader) |
| |
| function IgnoreReader (props) { |
| if (!(this instanceof IgnoreReader)) { |
| return new IgnoreReader(props) |
| } |
| |
| // must be a Directory type |
| if (typeof props === "string") { |
| props = { path: path.resolve(props) } |
| } |
| |
| props.type = "Directory" |
| props.Directory = true |
| |
| if (!props.ignoreFiles) props.ignoreFiles = [".ignore"] |
| this.ignoreFiles = props.ignoreFiles |
| |
| this.ignoreRules = null |
| |
| // ensure that .ignore files always show up at the top of the list |
| // that way, they can be read before proceeding to handle other |
| // entries in that same folder |
| if (props.sort) { |
| this._sort = props.sort === "alpha" ? alphasort : props.sort |
| props.sort = null |
| } |
| |
| this.on("entries", function () { |
| // if there are any ignore files in the list, then |
| // pause and add them. |
| // then, filter the list based on our ignoreRules |
| |
| var hasIg = this.entries.some(this.isIgnoreFile, this) |
| |
| if (!hasIg) return this.filterEntries() |
| |
| this.addIgnoreFiles() |
| }) |
| |
| // we filter entries before we know what they are. |
| // however, directories have to be re-tested against |
| // rules with a "/" appended, because "a/b/" will only |
| // match if "a/b" is a dir, and not otherwise. |
| this.on("_entryStat", function (entry, props) { |
| var t = entry.basename |
| if (!this.applyIgnores(entry.basename, |
| entry.type === "Directory", |
| entry)) { |
| entry.abort() |
| } |
| }.bind(this)) |
| |
| DirReader.call(this, props) |
| } |
| |
| |
| IgnoreReader.prototype.addIgnoreFiles = function () { |
| if (this._paused) { |
| this.once("resume", this.addIgnoreFiles) |
| return |
| } |
| if (this._ignoreFilesAdded) return |
| this._ignoreFilesAdded = true |
| |
| var newIg = this.entries.filter(this.isIgnoreFile, this) |
| , count = newIg.length |
| , errState = null |
| |
| if (!count) return |
| |
| this.pause() |
| |
| var then = function (er) { |
| if (errState) return |
| if (er) return this.emit("error", errState = er) |
| if (-- count === 0) { |
| this.filterEntries() |
| this.resume() |
| } else { |
| this.addIgnoreFile(newIg[newIg.length - count], then) |
| } |
| }.bind(this) |
| |
| this.addIgnoreFile(newIg[0], then) |
| } |
| |
| |
| IgnoreReader.prototype.isIgnoreFile = function (e) { |
| return e !== "." && |
| e !== ".." && |
| -1 !== this.ignoreFiles.indexOf(e) |
| } |
| |
| |
| IgnoreReader.prototype.getChildProps = function (stat) { |
| var props = DirReader.prototype.getChildProps.call(this, stat) |
| props.ignoreFiles = this.ignoreFiles |
| |
| // Directories have to be read as IgnoreReaders |
| // otherwise fstream.Reader will create a DirReader instead. |
| if (stat.isDirectory()) { |
| props.type = this.constructor |
| } |
| return props |
| } |
| |
| |
| IgnoreReader.prototype.addIgnoreFile = function (e, cb) { |
| // read the file, and then call addIgnoreRules |
| // if there's an error, then tell the cb about it. |
| |
| var ig = path.resolve(this.path, e) |
| fs.readFile(ig, function (er, data) { |
| if (er) return cb(er) |
| |
| this.emit("ignoreFile", e, data) |
| var rules = this.readRules(data, e) |
| this.addIgnoreRules(rules, e) |
| cb() |
| }.bind(this)) |
| } |
| |
| |
| IgnoreReader.prototype.readRules = function (buf, e) { |
| return buf.toString().split(/\r?\n/) |
| } |
| |
| |
| // Override this to do fancier things, like read the |
| // "files" array from a package.json file or something. |
| IgnoreReader.prototype.addIgnoreRules = function (set, e) { |
| // filter out anything obvious |
| set = set.filter(function (s) { |
| s = s.trim() |
| return s && !s.match(/^#/) |
| }) |
| |
| // no rules to add! |
| if (!set.length) return |
| |
| // now get a minimatch object for each one of these. |
| // Note that we need to allow dot files by default, and |
| // not switch the meaning of their exclusion |
| var mmopt = { matchBase: true, dot: true, flipNegate: true } |
| , mm = set.map(function (s) { |
| var m = new Minimatch(s, mmopt) |
| m.ignoreFile = e |
| return m |
| }) |
| |
| if (!this.ignoreRules) this.ignoreRules = [] |
| this.ignoreRules.push.apply(this.ignoreRules, mm) |
| } |
| |
| |
| IgnoreReader.prototype.filterEntries = function () { |
| // this exclusion is at the point where we know the list of |
| // entries in the dir, but don't know what they are. since |
| // some of them *might* be directories, we have to run the |
| // match in dir-mode as well, so that we'll pick up partials |
| // of files that will be included later. Anything included |
| // at this point will be checked again later once we know |
| // what it is. |
| this.entries = this.entries.filter(function (entry) { |
| // at this point, we don't know if it's a dir or not. |
| return this.applyIgnores(entry) || this.applyIgnores(entry, true) |
| }, this) |
| } |
| |
| |
| IgnoreReader.prototype.applyIgnores = function (entry, partial, obj) { |
| var included = true |
| |
| // this = /a/b/c |
| // entry = d |
| // parent /a/b sees c/d |
| if (this.parent && this.parent.applyIgnores) { |
| var pt = this.basename + "/" + entry |
| included = this.parent.applyIgnores(pt, partial) |
| } |
| |
| // Negated Rules |
| // Since we're *ignoring* things here, negating means that a file |
| // is re-included, if it would have been excluded by a previous |
| // rule. So, negated rules are only relevant if the file |
| // has been excluded. |
| // |
| // Similarly, if a file has been excluded, then there's no point |
| // trying it against rules that have already been applied |
| // |
| // We're using the "flipnegate" flag here, which tells minimatch |
| // to set the "negate" for our information, but still report |
| // whether the core pattern was a hit or a miss. |
| |
| if (!this.ignoreRules) { |
| return included |
| } |
| |
| this.ignoreRules.forEach(function (rule) { |
| // negation means inclusion |
| if (rule.negate && included || |
| !rule.negate && !included) { |
| // unnecessary |
| return |
| } |
| |
| // first, match against /foo/bar |
| var match = rule.match("/" + entry) |
| |
| if (!match) { |
| // try with the leading / trimmed off the test |
| // eg: foo/bar instead of /foo/bar |
| match = rule.match(entry) |
| } |
| |
| // if the entry is a directory, then it will match |
| // with a trailing slash. eg: /foo/bar/ or foo/bar/ |
| if (!match && partial) { |
| match = rule.match("/" + entry + "/") || |
| rule.match(entry + "/") |
| } |
| |
| // When including a file with a negated rule, it's |
| // relevant if a directory partially matches, since |
| // it may then match a file within it. |
| // Eg, if you ignore /a, but !/a/b/c |
| if (!match && rule.negate && partial) { |
| match = rule.match("/" + entry, true) || |
| rule.match(entry, true) |
| } |
| |
| if (match) { |
| included = rule.negate |
| } |
| }, this) |
| |
| return included |
| } |
| |
| |
| IgnoreReader.prototype.sort = function (a, b) { |
| var aig = this.ignoreFiles.indexOf(a) !== -1 |
| , big = this.ignoreFiles.indexOf(b) !== -1 |
| |
| if (aig && !big) return -1 |
| if (big && !aig) return 1 |
| return this._sort(a, b) |
| } |
| |
| IgnoreReader.prototype._sort = function (a, b) { |
| return 0 |
| } |
| |
| function alphasort (a, b) { |
| return a === b ? 0 |
| : a.toLowerCase() > b.toLowerCase() ? 1 |
| : a.toLowerCase() < b.toLowerCase() ? -1 |
| : a > b ? 1 |
| : -1 |
| } |