ignore.js 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  1. // Essentially, this is a fstream.DirReader class, but with a
  2. // bit of special logic to read the specified sort of ignore files,
  3. // and a filter that prevents it from picking up anything excluded
  4. // by those files.
  5. var Minimatch = require("minimatch").Minimatch
  6. , fstream = require("fstream")
  7. , DirReader = fstream.DirReader
  8. , inherits = require("inherits")
  9. , path = require("path")
  10. , fs = require("fs")
  11. module.exports = IgnoreReader
  12. inherits(IgnoreReader, DirReader)
  13. function IgnoreReader (props) {
  14. if (!(this instanceof IgnoreReader)) {
  15. return new IgnoreReader(props)
  16. }
  17. // must be a Directory type
  18. if (typeof props === "string") {
  19. props = { path: path.resolve(props) }
  20. }
  21. props.type = "Directory"
  22. props.Directory = true
  23. if (!props.ignoreFiles) props.ignoreFiles = [".ignore"]
  24. this.ignoreFiles = props.ignoreFiles
  25. this.ignoreRules = null
  26. // ensure that .ignore files always show up at the top of the list
  27. // that way, they can be read before proceeding to handle other
  28. // entries in that same folder
  29. if (props.sort) {
  30. this._sort = props.sort === "alpha" ? alphasort : props.sort
  31. props.sort = null
  32. }
  33. this.on("entries", function () {
  34. // if there are any ignore files in the list, then
  35. // pause and add them.
  36. // then, filter the list based on our ignoreRules
  37. var hasIg = this.entries.some(this.isIgnoreFile, this)
  38. if (!hasIg) return this.filterEntries()
  39. this.addIgnoreFiles()
  40. })
  41. // we filter entries before we know what they are.
  42. // however, directories have to be re-tested against
  43. // rules with a "/" appended, because "a/b/" will only
  44. // match if "a/b" is a dir, and not otherwise.
  45. this.on("_entryStat", function (entry, props) {
  46. var t = entry.basename
  47. if (!this.applyIgnores(entry.basename,
  48. entry.type === "Directory",
  49. entry)) {
  50. entry.abort()
  51. }
  52. }.bind(this))
  53. DirReader.call(this, props)
  54. }
  55. IgnoreReader.prototype.addIgnoreFiles = function () {
  56. if (this._paused) {
  57. this.once("resume", this.addIgnoreFiles)
  58. return
  59. }
  60. if (this._ignoreFilesAdded) return
  61. this._ignoreFilesAdded = true
  62. var newIg = this.entries.filter(this.isIgnoreFile, this)
  63. , count = newIg.length
  64. , errState = null
  65. if (!count) return
  66. this.pause()
  67. var then = function (er) {
  68. if (errState) return
  69. if (er) return this.emit("error", errState = er)
  70. if (-- count === 0) {
  71. this.filterEntries()
  72. this.resume()
  73. } else {
  74. this.addIgnoreFile(newIg[newIg.length - count], then)
  75. }
  76. }.bind(this)
  77. this.addIgnoreFile(newIg[0], then)
  78. }
  79. IgnoreReader.prototype.isIgnoreFile = function (e) {
  80. return e !== "." &&
  81. e !== ".." &&
  82. -1 !== this.ignoreFiles.indexOf(e)
  83. }
  84. IgnoreReader.prototype.getChildProps = function (stat) {
  85. var props = DirReader.prototype.getChildProps.call(this, stat)
  86. props.ignoreFiles = this.ignoreFiles
  87. // Directories have to be read as IgnoreReaders
  88. // otherwise fstream.Reader will create a DirReader instead.
  89. if (stat.isDirectory()) {
  90. props.type = this.constructor
  91. }
  92. return props
  93. }
  94. IgnoreReader.prototype.addIgnoreFile = function (e, cb) {
  95. // read the file, and then call addIgnoreRules
  96. // if there's an error, then tell the cb about it.
  97. var ig = path.resolve(this.path, e)
  98. fs.readFile(ig, function (er, data) {
  99. if (er) return cb(er)
  100. this.emit("ignoreFile", e, data)
  101. var rules = this.readRules(data, e)
  102. this.addIgnoreRules(rules, e)
  103. cb()
  104. }.bind(this))
  105. }
  106. IgnoreReader.prototype.readRules = function (buf, e) {
  107. return buf.toString().split(/\r?\n/)
  108. }
  109. // Override this to do fancier things, like read the
  110. // "files" array from a package.json file or something.
  111. IgnoreReader.prototype.addIgnoreRules = function (set, e) {
  112. // filter out anything obvious
  113. set = set.filter(function (s) {
  114. s = s.trim()
  115. return s && !s.match(/^#/)
  116. })
  117. // no rules to add!
  118. if (!set.length) return
  119. // now get a minimatch object for each one of these.
  120. // Note that we need to allow dot files by default, and
  121. // not switch the meaning of their exclusion
  122. var mmopt = { matchBase: true, dot: true, flipNegate: true }
  123. , mm = set.map(function (s) {
  124. var m = new Minimatch(s, mmopt)
  125. m.ignoreFile = e
  126. return m
  127. })
  128. if (!this.ignoreRules) this.ignoreRules = []
  129. this.ignoreRules.push.apply(this.ignoreRules, mm)
  130. }
  131. IgnoreReader.prototype.filterEntries = function () {
  132. // this exclusion is at the point where we know the list of
  133. // entries in the dir, but don't know what they are. since
  134. // some of them *might* be directories, we have to run the
  135. // match in dir-mode as well, so that we'll pick up partials
  136. // of files that will be included later. Anything included
  137. // at this point will be checked again later once we know
  138. // what it is.
  139. this.entries = this.entries.filter(function (entry) {
  140. // at this point, we don't know if it's a dir or not.
  141. return this.applyIgnores(entry) || this.applyIgnores(entry, true)
  142. }, this)
  143. }
  144. IgnoreReader.prototype.applyIgnores = function (entry, partial, obj) {
  145. var included = true
  146. // this = /a/b/c
  147. // entry = d
  148. // parent /a/b sees c/d
  149. if (this.parent && this.parent.applyIgnores) {
  150. var pt = this.basename + "/" + entry
  151. included = this.parent.applyIgnores(pt, partial)
  152. }
  153. // Negated Rules
  154. // Since we're *ignoring* things here, negating means that a file
  155. // is re-included, if it would have been excluded by a previous
  156. // rule. So, negated rules are only relevant if the file
  157. // has been excluded.
  158. //
  159. // Similarly, if a file has been excluded, then there's no point
  160. // trying it against rules that have already been applied
  161. //
  162. // We're using the "flipnegate" flag here, which tells minimatch
  163. // to set the "negate" for our information, but still report
  164. // whether the core pattern was a hit or a miss.
  165. if (!this.ignoreRules) {
  166. return included
  167. }
  168. this.ignoreRules.forEach(function (rule) {
  169. // negation means inclusion
  170. if (rule.negate && included ||
  171. !rule.negate && !included) {
  172. // unnecessary
  173. return
  174. }
  175. // first, match against /foo/bar
  176. var match = rule.match("/" + entry)
  177. if (!match) {
  178. // try with the leading / trimmed off the test
  179. // eg: foo/bar instead of /foo/bar
  180. match = rule.match(entry)
  181. }
  182. // if the entry is a directory, then it will match
  183. // with a trailing slash. eg: /foo/bar/ or foo/bar/
  184. if (!match && partial) {
  185. match = rule.match("/" + entry + "/") ||
  186. rule.match(entry + "/")
  187. }
  188. // When including a file with a negated rule, it's
  189. // relevant if a directory partially matches, since
  190. // it may then match a file within it.
  191. // Eg, if you ignore /a, but !/a/b/c
  192. if (!match && rule.negate && partial) {
  193. match = rule.match("/" + entry, true) ||
  194. rule.match(entry, true)
  195. }
  196. if (match) {
  197. included = rule.negate
  198. }
  199. }, this)
  200. return included
  201. }
  202. IgnoreReader.prototype.sort = function (a, b) {
  203. var aig = this.ignoreFiles.indexOf(a) !== -1
  204. , big = this.ignoreFiles.indexOf(b) !== -1
  205. if (aig && !big) return -1
  206. if (big && !aig) return 1
  207. return this._sort(a, b)
  208. }
  209. IgnoreReader.prototype._sort = function (a, b) {
  210. return 0
  211. }
  212. function alphasort (a, b) {
  213. return a === b ? 0
  214. : a.toLowerCase() > b.toLowerCase() ? 1
  215. : a.toLowerCase() < b.toLowerCase() ? -1
  216. : a > b ? 1
  217. : -1
  218. }