fast_ignore 0.13.0 → 0.14.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ee7e9eb231ff6441ca703c935e9c270e4cb6c2f8f3216d993d4fc891807e77f6
4
- data.tar.gz: 68f1ace7ab6420ce140a43a747a969f29401eba718dd9d1e94b9538774b0ac6d
3
+ metadata.gz: 05f6139deefedc14e0c58acd66909aa25af5240a83e67a6463eacedd6cec9eb3
4
+ data.tar.gz: c6becf91ea8bc9ccf0524309c5cad1e01485c4e2324222f231ade33855c65528
5
5
  SHA512:
6
- metadata.gz: 83609a93cf79d3d4063e69d213450d85f81c4b9bb7c1200468e46fa7fce01602c751c4afd6dbaef652d1761c0f180fa5806ff29d0913ca184a5e41850875a8d0
7
- data.tar.gz: dbf3e341e671fa1a51dce4c885c04e1eadf0717d78ffd94f1746c63b1024f78bd9d4b6915e7b892ca3af21d27ef2566df980be41101573c3dd586384b6f6d7c6
6
+ metadata.gz: cca040e6401d0c412498dff47b460ba7a19d5c9cb810cc205151a56b9db959a88ea3c4137610537bda43c5520611b1711abc8124b0ab98dd6bda261de9638588
7
+ data.tar.gz: a3fe257c77f8350c2dde1bcfcde01293b5af87b5d6b627b4e44555b3b6a45154c3d567d1e68f8f03520d21e28e78eda439e92b7a8b5b2d0b43b6f2a23a2f5ae4
@@ -1,3 +1,7 @@
1
+ # v0.14.0
2
+ - significant performance improvements ~50% faster
3
+ - add `FastIgnore#to_proc` for no good reason
4
+
1
5
  # v0.13.0
2
6
  - Attempt to improve documentation structure
3
7
  - Remove `gitignore: true` raising `Errno::ENOENT` if root:/.gitignore didn't exist. I can't think of a use. Now `gitignore: true` is just the default behaviour.
data/README.md CHANGED
@@ -23,7 +23,7 @@ FastIgnore.new(relative: true).sort == `git ls-files`.split("\n").sort
23
23
  - supports [matching by shebang](#shebang_rules) rather than filename for extensionless files: `#!:`
24
24
  - reads .gitignore in all subdirectories
25
25
  - reads .git/info/excludes
26
- - reads the ignore file mentioned in your git config
26
+ - reads the global gitignore file mentioned in your git config
27
27
 
28
28
  ## Installation
29
29
 
@@ -141,7 +141,7 @@ FastIgnore.new(root: '../relative/path/to/root').to_a
141
141
 
142
142
  A relative root will be found relative to the current working directory when the FastIgnore instance is initialized, and that will be the last time the current working directory is relevant.
143
143
 
144
- **Note: Changes to the current working directory (e.g. with `Dir.chdir`), after initialising a FastIgnore instance, will _not_ affect the FastIgnore instance. `root:` will always be what it was when the instance was initialized.**
144
+ **Note: Changes to the current working directory (e.g. with `Dir.chdir`), after initialising a FastIgnore instance, will _not_ affect the FastIgnore instance. `root:` will always be what it was when the instance was initialized, even as a default value.**
145
145
 
146
146
  ### `gitignore:`
147
147
 
@@ -302,34 +302,37 @@ FastIgnore.new.allowed?('relative/path', directory: false, content: "#!/usr/bin/
302
302
  ```
303
303
  This is not required, and if FastIgnore does have to go to the filesystem for this information it's well optimised to only read what is necessary.
304
304
 
305
-
306
- ## Known issues
305
+ ## Limitations
307
306
  - Doesn't know what to do if you change the current working directory inside the [`FastIgnore#each`](#each_map_etc) block.
308
307
  So don't do that.
309
308
 
310
- (It does handle changing the current working directory between [`FastIgnore#allowed?`](#allowed) calls) (changing directories doesn't affect the [`root:`](#root) directory, that's frozen at FastIgnore.new (this is a design decision, not an issue)).
309
+ (It does handle changing the current working directory between [`FastIgnore#allowed?`](#allowed) calls)
311
310
  - FastIgnore always matches patterns case-insensitively. (git varies by filesystem).
312
-
313
- ## Development
314
-
315
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake` to run the tests and linters.
316
-
317
- You can run `bin/console` for an interactive prompt that will allow you to experiment.
318
- `bin/ls [argv_rules]` will return something equivalent to `git ls-files` and `bin/time [argv_rules]` will give you the average time for 30 runs.
319
- This repo is too small to stress bin/time more than 0.01s, switch to a large repo and find the average time before and after changes.
320
-
321
- To install this gem onto your local machine, run `bundle exec rake install`.
322
-
323
- ### Goals
324
-
325
- 1. Match `git ls-files` behaviour quirk for quirk.
326
- 2. Provide a convenient interface for allowlist/denylist files in ruby.
327
- 3. Be fast.
311
+ - FastIgnore always outputs paths as literal UTF-8 characters. (git depends on your core.quotepath setting but by default outputs non ascii paths with octal escapes surrounded by quotes).
312
+ - Because git looks at its own index objects and FastIgnore looks at the file system there may be some differences between FastIgnore and `git ls-files`
313
+ - Tracked files that were committed before the matching ignore rule was committed will be returned by `git ls-files`, but not by FastIgnore.
314
+ - Untracked files will be returned by FastIgnore, but not by `git ls-files`
315
+ - Deleted files whose deletions haven't been committed will be returned by `git ls-files`, but not by FastIgnore
316
+ - On a case insensitive file system, with files that differ only by case, `git ls-files` will include all case variations, while FastIgnore will only include whichever variation git placed in the file system.
328
317
 
329
318
  ## Contributing
330
319
 
331
320
  Bug reports and pull requests are welcome on GitHub at https://github.com/robotdana/fast_ignore.
332
321
 
322
+ Some tools that may help:
323
+
324
+ - `bin/setup`: install development dependencies
325
+ - `bundle exec rspec`: run all tests
326
+ - `bundle exec rake`: run all tests and linters
327
+ - `bin/console`: open a `pry` console with everything required for experimenting
328
+ - `bin/ls [argv_rules]`: the equivalent of `git ls-files`
329
+ - `bin/prof/ls [argv_rules]`: ruby-prof report for `bin/ls`
330
+ - `bin/prof/parse [argv_rules]`: ruby-prof report for parsing root and global gitignore files and any arguments.
331
+ - `bin/time [argv_rules]`: the average time for 30 runs of `bin/ls`<br>
332
+ This repo is too small to stress bin/time more than 0.01s, switch to a large repo and find the average time before and after changes.
333
+ - `bin/compare`: compare the speed and output of FastIgnore and `git ls-files`.
334
+ (suppressing differences that are because of known [limitations](#limitations))
335
+
333
336
  ## License
334
337
 
335
338
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -3,12 +3,15 @@
3
3
  require_relative './fast_ignore/backports'
4
4
 
5
5
  require 'set'
6
- require_relative './fast_ignore/rule_set_builder'
7
- require_relative './fast_ignore/rule_builder'
6
+ require 'strscan'
7
+ require_relative './fast_ignore/rule_sets'
8
8
  require_relative './fast_ignore/rule_set'
9
+ require_relative './fast_ignore/global_gitignore'
10
+ require_relative './fast_ignore/rule_builder'
11
+ require_relative './fast_ignore/gitignore_rule_builder'
9
12
  require_relative './fast_ignore/rule'
13
+ require_relative './fast_ignore/unmatchable_rule'
10
14
  require_relative './fast_ignore/shebang_rule'
11
- require_relative './fast_ignore/fn_match_to_re'
12
15
 
13
16
  class FastIgnore
14
17
  class Error < StandardError; end
@@ -26,7 +29,7 @@ class FastIgnore
26
29
  @gitignore_enabled = gitignore
27
30
  @loaded_gitignore_files = ::Set[''] if gitignore
28
31
  @root = "#{::File.expand_path(root.to_s, Dir.pwd)}/"
29
- @rule_sets = ::FastIgnore::RuleSetBuilder.build(root: @root, gitignore: gitignore, **rule_set_builder_args)
32
+ @rule_sets = ::FastIgnore::RuleSets.new(root: @root, gitignore: gitignore, **rule_set_builder_args)
30
33
 
31
34
  freeze
32
35
  end
@@ -34,7 +37,7 @@ class FastIgnore
34
37
  def each(&block)
35
38
  return enum_for(:each) unless block_given?
36
39
 
37
- dir_pwd = Dir.pwd
40
+ dir_pwd = ::Dir.pwd
38
41
  root_from_pwd = @root.start_with?(dir_pwd) ? ".#{@root.delete_prefix(dir_pwd)}" : @root
39
42
 
40
43
  each_recursive(root_from_pwd, '', &block)
@@ -50,12 +53,16 @@ class FastIgnore
50
53
 
51
54
  filename = ::File.basename(relative_path)
52
55
 
53
- @rule_sets.all? { |r| r.allowed_recursive?(relative_path, false, full_path, filename, content) }
56
+ @rule_sets.allowed_recursive?(relative_path, full_path, filename, content)
54
57
  rescue ::Errno::ENOENT, ::Errno::EACCES, ::Errno::ENOTDIR, ::Errno::ELOOP, ::Errno::ENAMETOOLONG
55
58
  false
56
59
  end
57
60
  alias_method :===, :allowed?
58
61
 
62
+ def to_proc
63
+ method(:allowed?).to_proc
64
+ end
65
+
59
66
  private
60
67
 
61
68
  def load_gitignore_recursive(path)
@@ -67,19 +74,17 @@ class FastIgnore
67
74
  paths.reverse_each(&method(:load_gitignore))
68
75
  end
69
76
 
70
- def load_gitignore(parent_path, soft: true)
77
+ def load_gitignore(parent_path, check_exists: true)
71
78
  return if @loaded_gitignore_files.include?(parent_path)
72
79
 
73
- ::FastIgnore::RuleSetBuilder.append_gitignore(
74
- @rule_sets, project_root: @root, relative_path: parent_path + '.gitignore', soft: soft
75
- )
80
+ @rule_sets.append_subdir_gitignore(relative_path: parent_path + '.gitignore', check_exists: check_exists)
81
+
76
82
  @loaded_gitignore_files << parent_path
77
83
  end
78
84
 
79
85
  def each_recursive(parent_full_path, parent_relative_path, &block) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
80
86
  children = ::Dir.children(parent_full_path)
81
-
82
- load_gitignore(parent_relative_path, soft: false) if @gitignore_enabled && children.include?('.gitignore')
87
+ load_gitignore(parent_relative_path, check_exists: false) if @gitignore_enabled && children.include?('.gitignore')
83
88
 
84
89
  children.each do |filename|
85
90
  begin
@@ -87,7 +92,7 @@ class FastIgnore
87
92
  relative_path = parent_relative_path + filename
88
93
  dir = @follow_symlinks_method.call(full_path).directory?
89
94
 
90
- next unless @rule_sets.all? { |r| r.allowed_unrecursive?(relative_path, dir, full_path, filename, nil) }
95
+ next unless @rule_sets.allowed_unrecursive?(relative_path, dir, full_path, filename)
91
96
 
92
97
  if dir
93
98
  each_recursive(full_path + '/', relative_path + '/', &block)
@@ -2,12 +2,12 @@
2
2
 
3
3
  class FastIgnore
4
4
  module Backports
5
- ruby_major, ruby_minor = RUBY_VERSION.split('.', 2)
5
+ ruby_major, ruby_minor = ::RUBY_VERSION.split('.', 2)
6
6
  unless ruby_major.to_i > 2 || ruby_major.to_i == 2 && ruby_minor.to_i > 5
7
7
  module DirEachChild
8
8
  refine ::Dir.singleton_class do
9
9
  def children(path)
10
- Dir.entries(path) - ['.', '..']
10
+ ::Dir.entries(path) - ['.', '..']
11
11
  end
12
12
  end
13
13
  end
@@ -0,0 +1,212 @@
1
+ # frozen_string_literal: true
2
+
3
+ class FastIgnore
4
+ class GitignoreRuleBuilder # rubocop:disable Metrics/ClassLength
5
+ def initialize(rule, negation, dir_only, file_path, allow) # rubocop:disable Metrics/MethodLength
6
+ @re = ::String.new
7
+ @segment_re = ::String.new
8
+ @allow = allow
9
+ if @allow
10
+ @segments = 0
11
+ @parent_re = ::String.new
12
+ end
13
+
14
+ @s = ::StringScanner.new(rule)
15
+
16
+ @dir_only = dir_only
17
+ @file_path = (file_path if file_path && !file_path.empty?)
18
+ @negation = negation
19
+ @anchored = false
20
+ @trailing_stars = false
21
+ end
22
+
23
+ def process_escaped_char
24
+ @segment_re << ::Regexp.escape(@s.matched[1]) if @s.scan(/\\./)
25
+ end
26
+
27
+ def process_character_class
28
+ return unless @s.skip(/\[/)
29
+
30
+ @segment_re << '['
31
+ process_character_class_body(false)
32
+ end
33
+
34
+ def process_negated_character_class
35
+ return unless @s.skip(/\[\^/)
36
+
37
+ @segment_re << '[^'
38
+ process_character_class_body(true)
39
+ end
40
+
41
+ def unmatchable_rule!
42
+ throw :unmatchable_rule, (
43
+ @allow ? ::FastIgnore::UnmatchableRule : []
44
+ )
45
+ end
46
+
47
+ def process_character_class_end
48
+ return unless @s.skip(/\]/)
49
+
50
+ unmatchable_rule! unless @has_characters_in_group
51
+
52
+ @segment_re << ']'
53
+ end
54
+
55
+ def process_character_class_body(negated_class) # rubocop:disable Metrics/MethodLength
56
+ @has_characters_in_group = false
57
+ until process_character_class_end
58
+ if @s.eos?
59
+ unmatchable_rule!
60
+ elsif process_escaped_char
61
+ @has_characters_in_group = true
62
+ elsif @s.skip(%r{/})
63
+ next unless negated_class
64
+
65
+ @has_characters_in_group = true
66
+ @segment_re << '/'
67
+ elsif @s.skip(/-/)
68
+ @has_characters_in_group = true
69
+ @segment_re << '-'
70
+ else @s.scan(%r{[^/\]\-]+})
71
+ @has_characters_in_group = true
72
+ @segment_re << ::Regexp.escape(@s.matched)
73
+ end
74
+ end
75
+ end
76
+
77
+ def process_star_star_slash
78
+ return unless @s.skip(%r{\*{2,}/})
79
+
80
+ if @allow
81
+ if @segment_re.empty?
82
+ @parent_re << '.*'
83
+ else
84
+ process_slash_allow('.*')
85
+ end
86
+ end
87
+ process_slash('(?:.*/)?')
88
+ end
89
+
90
+ def process_star_slash
91
+ return unless @s.skip(%r{\*/})
92
+
93
+ process_slash_allow('[^/]*/') if @allow
94
+ process_slash('[^/]*/')
95
+ end
96
+
97
+ def process_no_star_slash
98
+ return unless @s.skip(%r{/})
99
+
100
+ process_slash_allow('/') if @allow
101
+ process_slash('/')
102
+ end
103
+
104
+ def process_slash(append)
105
+ @re << @segment_re
106
+ @re << append
107
+ @segment_re.clear
108
+ @anchored = true
109
+ end
110
+
111
+ def process_slash_allow(append)
112
+ @segments += 1
113
+ @parent_re << '(?:'
114
+ @parent_re << @segment_re
115
+ @parent_re << append
116
+ end
117
+
118
+ def process_stars
119
+ (@segment_re << '[^/]*') if @s.scan(%r{\*+(?=[^*/])})
120
+ end
121
+
122
+ def process_question_mark
123
+ (@segment_re << '[^/]') if @s.skip(/\?/)
124
+ end
125
+
126
+ def process_text
127
+ (@segment_re << ::Regexp.escape(@s.matched)) if @s.scan(%r{[^*/?\[\\]+})
128
+ end
129
+
130
+ def process_end
131
+ return unless @s.scan(/\*+\z/)
132
+
133
+ if @s.matched.length == 1
134
+ @segment_re << if @segment_re.empty? # at least something. this is to allow subdir negations to work
135
+ '[^/]+\\z'
136
+ else
137
+ '[^/]*\\z'
138
+ end
139
+ end
140
+ @trailing_stars = true
141
+ end
142
+
143
+ def process_rule
144
+ until @s.eos?
145
+ process_escaped_char ||
146
+ process_star_star_slash || process_star_slash || process_no_star_slash ||
147
+ process_stars || process_question_mark ||
148
+ process_negated_character_class || process_character_class ||
149
+ process_text || process_end
150
+ end
151
+ end
152
+
153
+ def build # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
154
+ @anchored = true if @s.skip(%r{/})
155
+
156
+ catch :unmatchable_rule do # rubocop:disable Metrics/BlockLength
157
+ process_rule
158
+
159
+ @re << @segment_re
160
+
161
+ prefix = if @file_path
162
+ escaped_file_path = ::Regexp.escape @file_path
163
+ if @anchored
164
+ "\\A#{escaped_file_path}"
165
+ else
166
+ "\\A#{escaped_file_path}(?:.*/)?"
167
+ end
168
+ else
169
+ if @anchored
170
+ '\\A'
171
+ else
172
+ '(?:\\A|/)'
173
+ end
174
+ end
175
+
176
+ @re.prepend(prefix)
177
+ anchored_or_file_path = @anchored || @file_path
178
+ if @allow
179
+ if @file_path
180
+ allow_escaped_file_path = escaped_file_path.gsub(%r{(?<!\\)(?:\\\\)*/}) do |e|
181
+ @segments += 1
182
+ "#{e[0..-2]}(?:/"
183
+ end
184
+
185
+ prefix = if @anchored
186
+ "\\A#{allow_escaped_file_path}"
187
+ else
188
+ "\\A#{allow_escaped_file_path}(?:.*/)?"
189
+ end
190
+ end
191
+ @parent_re.prepend(prefix)
192
+ @parent_re << (')?' * @segments)
193
+ (@re << '(/|\\z)') unless @dir_only || @trailing_stars
194
+ rules = [
195
+ # Regexp::IGNORECASE = 1
196
+ ::FastIgnore::Rule.new(::Regexp.new(@re, 1), @negation, anchored_or_file_path, @dir_only),
197
+ ::FastIgnore::Rule.new(::Regexp.new(@parent_re, 1), true, anchored_or_file_path, true)
198
+ ]
199
+ if @dir_only
200
+ (rules << ::FastIgnore::Rule.new(::Regexp.new((@re << '/.*'), 1), @negation, anchored_or_file_path, false))
201
+ end
202
+ rules
203
+ else
204
+ (@re << '\\z') unless @trailing_stars
205
+
206
+ # Regexp::IGNORECASE = 1
207
+ ::FastIgnore::Rule.new(::Regexp.new(@re, 1), @negation, anchored_or_file_path, @dir_only)
208
+ end
209
+ end
210
+ end
211
+ end
212
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ class FastIgnore
4
+ module GlobalGitignore
5
+ class << self
6
+ def path(root:)
7
+ gitconfig_gitignore_path(::File.expand_path('.git/config', root)) ||
8
+ gitconfig_gitignore_path(::File.expand_path('~/.gitconfig')) ||
9
+ gitconfig_gitignore_path(xdg_config_path) ||
10
+ gitconfig_gitignore_path('/etc/gitconfig') ||
11
+ default_global_gitignore_path
12
+ end
13
+
14
+ def gitconfig_gitignore_path(config_path)
15
+ return unless config_path
16
+ return unless ::File.exist?(config_path)
17
+
18
+ ignore_path = ::File.readlines(config_path).find { |l| l.sub!(/\A\s*excludesfile\s*=/, '') }
19
+ return unless ignore_path
20
+
21
+ ignore_path.strip!
22
+ return ignore_path if ignore_path.empty? # don't expand path in this case
23
+
24
+ ::File.expand_path(ignore_path)
25
+ end
26
+
27
+ def xdg_config_path
28
+ return unless ::ENV['XDG_CONFIG_HOME'] && !::ENV['XDG_CONFIG_HOME'].empty?
29
+
30
+ ::File.expand_path('git/config', ::ENV['XDG_CONFIG_HOME'])
31
+ end
32
+
33
+ def default_global_gitignore_path
34
+ if ::ENV['XDG_CONFIG_HOME'] && !::ENV['XDG_CONFIG_HOME'].empty?
35
+ ::File.expand_path('git/ignore', ::ENV['XDG_CONFIG_HOME'])
36
+ else
37
+ ::File.expand_path('~/.config/git/ignore')
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -6,24 +6,35 @@ class FastIgnore
6
6
  alias_method :negation?, :negation
7
7
  undef :negation
8
8
 
9
+ attr_reader :component_rules
10
+
9
11
  attr_reader :dir_only
10
12
  alias_method :dir_only?, :dir_only
11
13
  undef :dir_only
12
14
 
13
- attr_reader :unanchored
14
- alias_method :unanchored?, :unanchored
15
- undef :unanchored
16
-
17
- attr_reader :type
15
+ attr_reader :squashable_type
18
16
  attr_reader :rule
19
17
 
20
- def initialize(rule, negation, unanchored = nil, dir_only = nil)
21
- @rule = rule.is_a?(Regexp) ? rule : ::FastIgnore::FNMatchToRegex.call(rule)
22
- @unanchored = unanchored
18
+ def squash(rules)
19
+ # component rules is to improve the performance of repos with many .gitignore files. e.g. linux.
20
+ rules = rules.flat_map(&:component_rules)
21
+ ::FastIgnore::Rule.new(::Regexp.union(rules.map(&:rule)).freeze, @negation, @anchored, @dir_only, rules)
22
+ end
23
+
24
+ def initialize(rule, negation, anchored, dir_only, component_rules = self) # rubocop:disable Metrics/MethodLength
25
+ @rule = rule
26
+ @anchored = anchored
23
27
  @dir_only = dir_only
24
28
  @negation = negation
29
+ @component_rules = component_rules
25
30
 
26
- @type = negation ? 1 : 0
31
+ @squashable_type = if anchored && negation
32
+ 1
33
+ elsif anchored
34
+ 0
35
+ else
36
+ ::Float::NAN # because it doesn't equal itself
37
+ end
27
38
 
28
39
  freeze
29
40
  end
@@ -32,13 +43,13 @@ class FastIgnore
32
43
  false
33
44
  end
34
45
 
35
- def shebang
36
- nil
46
+ def shebang?
47
+ false
37
48
  end
38
49
 
39
50
  # :nocov:
40
51
  def inspect
41
- "#<Rule #{'!' if @negation}#{@rule}#{'/' if @dir_only}>"
52
+ "#<Rule #{'!' if @negation}#{'/' if @anchored}#{@rule}#{'/' if @dir_only}>"
42
53
  end
43
54
  # :nocov:
44
55
 
@@ -7,13 +7,13 @@ class FastIgnore
7
7
  using ::FastIgnore::Backports::DeletePrefixSuffix if defined?(::FastIgnore::Backports::DeletePrefixSuffix)
8
8
  # :nocov:
9
9
 
10
- def build(rule, allow, expand_path, file_root)
10
+ def build(rule, allow, expand_path_with, file_root)
11
11
  return shebang_rules(rule, allow) if remove_shebang(rule)
12
12
 
13
13
  strip(rule)
14
14
  return [] if skip?(rule)
15
15
 
16
- gitignore_rules(rule, allow, expand_path, file_root)
16
+ gitignore_rules(rule, allow, expand_path_with, file_root)
17
17
  end
18
18
 
19
19
  private
@@ -32,31 +32,23 @@ class FastIgnore
32
32
  end
33
33
 
34
34
  def shebang_rules(rule, allow)
35
- rules = [::FastIgnore::ShebangRule.new(/\A#!.*\b#{Regexp.escape(rule)}\b/i.freeze, allow)]
36
- return rules unless allow
35
+ rule = ::FastIgnore::ShebangRule.new(/\A#!.*\b#{::Regexp.escape(rule)}\b/i, allow)
36
+ return rule unless allow
37
37
 
38
- rules << ::FastIgnore::Rule.new('**/*', true, true, true)
39
- rules
38
+ [::FastIgnore::Rule.new(//, true, true, true), rule]
40
39
  end
41
40
 
42
41
  def skip?(rule)
43
42
  rule.empty? || rule.start_with?('#')
44
43
  end
45
44
 
46
- def gitignore_rules(rule, allow, expand_path, file_root)
45
+ def gitignore_rules(rule, allow, expand_path_with, file_root)
47
46
  dir_only = extract_dir_only(rule)
48
47
  negation = extract_negation(rule, allow)
49
48
 
50
- unanchored = if expand_path
51
- expand_rule_path(rule, expand_path)
52
- else
53
- unanchored?(rule)
54
- end
55
- rule.delete_prefix!('/')
49
+ expand_rule_path(rule, expand_path_with) if expand_path_with
56
50
 
57
- rule.prepend("#{file_root}#{'**/' if unanchored}") if file_root || unanchored
58
-
59
- build_gitignore_rules(rule, unanchored, allow, dir_only, negation)
51
+ ::FastIgnore::GitignoreRuleBuilder.new(rule, negation, dir_only, file_root, allow).build
60
52
  end
61
53
 
62
54
  def extract_dir_only(rule)
@@ -69,34 +61,11 @@ class FastIgnore
69
61
  not allow
70
62
  end
71
63
 
72
- EXPAND_PATH_RE = %r{^(?:[~/]|\.{1,2}/)}.freeze
64
+ EXPAND_PATH_RE = %r{(^(?:[~/]|\.{1,2}/)|/\.\./)}.freeze
73
65
  def expand_rule_path(rule, root)
74
66
  rule.replace(::File.expand_path(rule)) if rule.match?(EXPAND_PATH_RE)
75
67
  rule.delete_prefix!(root)
76
- rule.start_with?('*')
77
- end
78
-
79
- def unanchored?(rule)
80
- not rule.include?('/') # we've already removed the trailing '/' with extract_dir_only
81
- end
82
-
83
- def build_gitignore_rules(rule, unanchored, allow, dir_only, negation)
84
- rules = [::FastIgnore::Rule.new(rule, negation, unanchored, dir_only)]
85
- return rules unless allow
86
-
87
- rules << ::FastIgnore::Rule.new("#{rule}/**/*", negation, unanchored, false)
88
- rules + ancestor_rules(rule, unanchored)
89
- end
90
-
91
- def ancestor_rules(parent, unanchored)
92
- ancestor_rules = []
93
-
94
- while (parent = ::File.dirname(parent)) != '.'
95
- rule = ::File.basename(parent) == '**' ? "#{parent}/*" : parent.freeze
96
- ancestor_rules << ::FastIgnore::Rule.new(rule, true, unanchored, true)
97
- end
98
-
99
- ancestor_rules
68
+ rule.prepend('/') unless rule.start_with?('*')
100
69
  end
101
70
  end
102
71
  end
@@ -9,8 +9,7 @@ class FastIgnore
9
9
  def initialize(rules, allow, gitignore)
10
10
  @dir_rules = squash_rules(rules.reject(&:file_only?)).freeze
11
11
  @file_rules = squash_rules(rules.reject(&:dir_only?)).freeze
12
- @any_not_anchored = rules.any?(&:unanchored?)
13
- @has_shebang_rules = rules.any?(&:shebang)
12
+ @has_shebang_rules = rules.any?(&:shebang?)
14
13
 
15
14
  @allowed_recursive = { '.' => true }
16
15
  @allow = allow
@@ -22,10 +21,9 @@ class FastIgnore
22
21
  def <<(other)
23
22
  return unless other
24
23
 
25
- @any_not_anchored ||= other.any_not_anchored
26
24
  @has_shebang_rules ||= other.has_shebang_rules
27
- @dir_rules += other.dir_rules
28
- @file_rules += other.file_rules
25
+ @dir_rules = squash_rules(@dir_rules + other.dir_rules)
26
+ @file_rules = squash_rules(@file_rules + other.file_rules)
29
27
  end
30
28
 
31
29
  def allowed_recursive?(relative_path, dir, full_path, filename, content = nil)
@@ -41,18 +39,16 @@ class FastIgnore
41
39
  return rule.negation? if rule.match?(relative_path, full_path, filename, content)
42
40
  end
43
41
 
44
- (not @allow) || (dir && @any_not_anchored)
42
+ not @allow
45
43
  end
46
44
 
47
45
  def squash_rules(rules)
48
- out = rules.chunk_while { |a, b| a.type == b.type }.map do |chunk|
46
+ rules.chunk_while { |a, b| a.squashable_type == b.squashable_type }.map do |chunk|
49
47
  first = chunk.first
50
48
  next first if chunk.length == 1
51
49
 
52
- first.class.new(Regexp.union(chunk.map(&:rule)), first.negation?)
50
+ first.squash(chunk)
53
51
  end
54
-
55
- out
56
52
  end
57
53
 
58
54
  def weight
@@ -65,6 +61,6 @@ class FastIgnore
65
61
 
66
62
  protected
67
63
 
68
- attr_reader :dir_rules, :file_rules, :any_not_anchored, :has_shebang_rules
64
+ attr_reader :dir_rules, :file_rules, :has_shebang_rules
69
65
  end
70
66
  end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ class FastIgnore
4
+ class RuleSets
5
+ # :nocov:
6
+ using ::FastIgnore::Backports::DeletePrefixSuffix if defined?(::FastIgnore::Backports::DeletePrefixSuffix)
7
+ # :nocov:
8
+
9
+ def initialize( # rubocop:disable Metrics/ParameterLists
10
+ root:,
11
+ ignore_rules: nil,
12
+ ignore_files: nil,
13
+ gitignore: true,
14
+ include_rules: nil,
15
+ include_files: nil,
16
+ argv_rules: nil
17
+ )
18
+ @array = []
19
+ @project_root = root
20
+ append_root_gitignore(gitignore)
21
+ append_set_from_array(ignore_rules)
22
+ append_set_from_array(include_rules, allow: true)
23
+ append_set_from_array(argv_rules, allow: true, expand_path_with: @project_root)
24
+ append_sets_from_files(ignore_files)
25
+ append_sets_from_files(include_files, allow: true)
26
+ @array.sort_by!(&:weight)
27
+ @array.freeze if @gitignore_rule_set
28
+ end
29
+
30
+ def allowed_recursive?(relative_path, full_path, filename, content)
31
+ @array.all? { |r| r.allowed_recursive?(relative_path, false, full_path, filename, content) }
32
+ end
33
+
34
+ def allowed_unrecursive?(relative_path, dir, full_path, filename)
35
+ @array.all? { |r| r.allowed_unrecursive?(relative_path, dir, full_path, filename, nil) }
36
+ end
37
+
38
+ def append_subdir_gitignore(relative_path:, check_exists: true)
39
+ new_gitignore = build_set_from_file(relative_path, gitignore: true, check_exists: check_exists)
40
+ return if !new_gitignore || new_gitignore.empty?
41
+
42
+ if @gitignore_rule_set
43
+ @gitignore_rule_set << new_gitignore
44
+ else
45
+ @array << new_gitignore
46
+ @gitignore_rule_set = new_gitignore
47
+ @array.sort_by!(&:weight) && @array.freeze
48
+ end
49
+ new_gitignore
50
+ end
51
+
52
+ private
53
+
54
+ def append_and_return_if_present(value)
55
+ return unless value && !value.empty?
56
+
57
+ @array << value
58
+ value
59
+ end
60
+
61
+ def append_root_gitignore(gitignore)
62
+ return @gitignore_rule_set = nil unless gitignore
63
+
64
+ append_set_from_array('.git')
65
+ gi = ::FastIgnore::RuleSet.new([], false, true)
66
+ gi << build_from_root_gitignore_file(::FastIgnore::GlobalGitignore.path(root: @project_root))
67
+ gi << build_from_root_gitignore_file("#{@project_root}.git/info/exclude")
68
+ gi << build_from_root_gitignore_file("#{@project_root}.gitignore")
69
+ @gitignore_rule_set = append_and_return_if_present(gi)
70
+ end
71
+
72
+ def build_from_root_gitignore_file(path)
73
+ return unless ::File.exist?(path)
74
+
75
+ build_rule_set(::File.readlines(path), false, gitignore: true)
76
+ end
77
+
78
+ def build_rule_set(rules, allow, expand_path_with: nil, file_root: nil, gitignore: false)
79
+ rules = rules.flat_map do |rule|
80
+ ::FastIgnore::RuleBuilder.build(rule, allow, expand_path_with, file_root)
81
+ end
82
+
83
+ ::FastIgnore::RuleSet.new(rules, allow, gitignore)
84
+ end
85
+
86
+ def build_set_from_file(filename, allow: false, file_root: nil, gitignore: false, check_exists: false)
87
+ filename = ::File.expand_path(filename, @project_root)
88
+ return if check_exists && !::File.exist?(filename)
89
+ unless file_root || filename.start_with?(@project_root)
90
+ raise ::FastIgnore::Error, "#{filename} is not within #{@project_root}"
91
+ end
92
+
93
+ file_root ||= "#{::File.dirname(filename)}/".delete_prefix(@project_root)
94
+ build_rule_set(::File.readlines(filename), allow, file_root: file_root, gitignore: gitignore)
95
+ end
96
+
97
+ def append_sets_from_files(files, allow: false)
98
+ Array(files).each do |file|
99
+ append_and_return_if_present(build_set_from_file(file, allow: allow))
100
+ end
101
+ end
102
+
103
+ def append_set_from_array(rules, allow: false, expand_path_with: nil)
104
+ return unless rules
105
+
106
+ rules = Array(rules).flat_map { |string| string.to_s.lines }
107
+ return if rules.empty?
108
+
109
+ append_and_return_if_present(build_rule_set(rules, allow, expand_path_with: expand_path_with))
110
+ end
111
+ end
112
+ end
@@ -7,15 +7,18 @@ class FastIgnore
7
7
  undef :negation
8
8
 
9
9
  attr_reader :rule
10
- alias_method :shebang, :rule
11
10
 
12
- attr_reader :type
11
+ attr_reader :squashable_type
12
+
13
+ def squash(rules)
14
+ ::FastIgnore::ShebangRule.new(::Regexp.union(rules.map(&:rule)).freeze, negation?)
15
+ end
13
16
 
14
17
  def initialize(rule, negation)
15
18
  @rule = rule
16
19
  @negation = negation
17
20
 
18
- @type = negation ? 3 : 2
21
+ @squashable_type = negation ? 3 : 2
19
22
 
20
23
  freeze
21
24
  end
@@ -28,10 +31,6 @@ class FastIgnore
28
31
  false
29
32
  end
30
33
 
31
- def unanchored?
32
- true
33
- end
34
-
35
34
  # :nocov:
36
35
  def inspect
37
36
  "#<ShebangRule #{'allow ' if @negation}#!:#{@rule.to_s[15..-4]}>"
@@ -44,12 +43,24 @@ class FastIgnore
44
43
  (content || first_line(full_path))&.match?(@rule)
45
44
  end
46
45
 
46
+ def shebang?
47
+ true
48
+ end
49
+
47
50
  private
48
51
 
49
- def first_line(path)
52
+ def first_line(path) # rubocop:disable Metrics/MethodLength
50
53
  file = ::File.new(path)
51
- first_line = file.sysread(25)
52
- first_line += file.sysread(50) until first_line.include?("\n")
54
+ first_line = new_fragment = file.sysread(64)
55
+ if first_line.start_with?('#!')
56
+ until new_fragment.include?("\n")
57
+ new_fragment = file.sysread(64)
58
+ first_line += new_fragment
59
+ end
60
+ else
61
+ file.close
62
+ return
63
+ end
53
64
  file.close
54
65
  first_line
55
66
  rescue ::EOFError, ::SystemCallError
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ class FastIgnore
4
+ class UnmatchableRule
5
+ class << self
6
+ def squash(_)
7
+ self
8
+ end
9
+
10
+ def squashable_type
11
+ 5
12
+ end
13
+
14
+ def dir_only?
15
+ false
16
+ end
17
+
18
+ def file_only?
19
+ false
20
+ end
21
+
22
+ def shebang?
23
+ false
24
+ end
25
+
26
+ # :nocov:
27
+ def inspect
28
+ '#<UnmatchableRule>'
29
+ end
30
+ # :nocov:
31
+
32
+ def match?(_, _, _, _)
33
+ false
34
+ end
35
+ end
36
+ end
37
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class FastIgnore
4
- VERSION = '0.13.0'
4
+ VERSION = '0.14.0'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fast_ignore
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.13.0
4
+ version: 0.14.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dana Sherson
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-06-06 00:00:00.000000000 Z
11
+ date: 2020-06-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -148,12 +148,14 @@ files:
148
148
  - README.md
149
149
  - lib/fast_ignore.rb
150
150
  - lib/fast_ignore/backports.rb
151
- - lib/fast_ignore/fn_match_to_re.rb
151
+ - lib/fast_ignore/gitignore_rule_builder.rb
152
+ - lib/fast_ignore/global_gitignore.rb
152
153
  - lib/fast_ignore/rule.rb
153
154
  - lib/fast_ignore/rule_builder.rb
154
155
  - lib/fast_ignore/rule_set.rb
155
- - lib/fast_ignore/rule_set_builder.rb
156
+ - lib/fast_ignore/rule_sets.rb
156
157
  - lib/fast_ignore/shebang_rule.rb
158
+ - lib/fast_ignore/unmatchable_rule.rb
157
159
  - lib/fast_ignore/version.rb
158
160
  homepage: https://github.com/robotdana/fast_ignore
159
161
  licenses:
@@ -1,96 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class FastIgnore
4
- module FNMatchToRegex
5
- # This doesn't look rubyish because i ported it from rust (the only rust i ever wrote that worked)
6
- class << self
7
- def call(pattern) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
8
- re = '\\A'.dup
9
-
10
- in_character_group = false
11
- has_characters_in_group = false
12
- escape_next_character = false
13
- last_char_opened_character_group = false
14
- negated_character_group = false
15
- stars = 0
16
-
17
- pattern.each_char do |char| # rubocop:disable Metrics/BlockLength
18
- if escape_next_character
19
- re << Regexp.escape(char)
20
- escape_next_character = false
21
- elsif char == '\\' # single char, just needs to be escaped
22
- escape_next_character = true
23
- elsif in_character_group
24
- if char == '/'
25
- if negated_character_group
26
- has_characters_in_group = true
27
- re << char
28
- end
29
- elsif char == '^'
30
- if last_char_opened_character_group
31
- re << char
32
- negated_character_group = true
33
- else
34
- re << '\\^'
35
- has_characters_in_group = true
36
- end
37
- # not characters in group
38
- elsif char == ']'
39
- break unless has_characters_in_group
40
-
41
- re << ']'
42
- in_character_group = false
43
- has_characters_in_group = false
44
- negated_character_group = false
45
- last_char_opened_character_group = false
46
- elsif char == '-'
47
- has_characters_in_group = true
48
- re << char
49
- else
50
- has_characters_in_group = true
51
- re << Regexp.escape(char)
52
- end
53
- last_char_opened_character_group = false
54
- elsif char == '*'
55
- stars += 1
56
- elsif char == '/'
57
- re << if stars >= 2
58
- '(?:.*/)?'
59
- elsif stars.positive?
60
- '[^/]*/'
61
- else
62
- char
63
- end
64
- stars = 0
65
- else
66
- if stars.positive?
67
- re << '[^/]*'
68
- stars = 0
69
- end
70
- if char == '?'
71
- re << '[^/]'
72
- elsif char == '['
73
- re << '['
74
- in_character_group = true
75
- last_char_opened_character_group = true
76
- else
77
- re << Regexp.escape(char)
78
- end
79
- end
80
- end
81
-
82
- if in_character_group
83
- return /(?!)/ # impossible to match anything
84
- end
85
-
86
- if stars >= 2
87
- re << '.*'
88
- elsif stars.positive?
89
- re << '[^/]*'
90
- end
91
- re << '\\z'
92
- Regexp.new(re, Regexp::IGNORECASE)
93
- end
94
- end
95
- end
96
- end
@@ -1,141 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class FastIgnore
4
- module RuleSetBuilder # rubocop:disable Metrics/ModuleLength
5
- class << self
6
- # :nocov:
7
- using ::FastIgnore::Backports::DeletePrefixSuffix if defined?(::FastIgnore::Backports::DeletePrefixSuffix)
8
- # :nocov:
9
-
10
- def build( # rubocop:disable Metrics/ParameterLists
11
- root:,
12
- ignore_rules: nil,
13
- ignore_files: nil,
14
- gitignore: true,
15
- include_rules: nil,
16
- include_files: nil,
17
- argv_rules: nil
18
- )
19
- prepare [
20
- from_array(ignore_rules),
21
- *from_files(ignore_files, project_root: root),
22
- (from_array('.git') if gitignore),
23
- from_gitignore_arg(gitignore, project_root: root),
24
- from_array(include_rules, allow: true),
25
- *from_files(include_files, allow: true, project_root: root),
26
- from_array(argv_rules, allow: true, expand_path: root)
27
- ]
28
- end
29
-
30
- def append_gitignore(rule_sets, project_root:, relative_path:, soft: true)
31
- new_gitignore = from_file(relative_path, project_root: project_root, gitignore: true, soft: soft)
32
- return unless new_gitignore
33
-
34
- base_gitignore = rule_sets.find(&:gitignore?)
35
- if base_gitignore
36
- base_gitignore << new_gitignore
37
- else
38
- rule_sets << new_gitignore
39
- prepare(rule_sets)
40
- end
41
- end
42
-
43
- private
44
-
45
- def prepare(rule_sets)
46
- rule_sets.compact!
47
- rule_sets.reject!(&:empty?)
48
- rule_sets.sort_by!(&:weight)
49
- rule_sets
50
- end
51
-
52
- def from_file(filename, project_root:, allow: false, file_root: nil, gitignore: false, soft: false) # rubocop:disable Metrics/ParameterLists
53
- filename = ::File.expand_path(filename, project_root)
54
- return if soft && !::File.exist?(filename)
55
- unless file_root || filename.start_with?(project_root)
56
- raise FastIgnore::Error, "#{filename} is not within #{project_root}"
57
- end
58
-
59
- file_root ||= "#{::File.dirname(filename)}/".delete_prefix(project_root)
60
- build_rule_set(::File.readlines(filename), allow, file_root: file_root, gitignore: gitignore)
61
- end
62
-
63
- def from_files(files, project_root:, allow: false)
64
- Array(files).map do |file|
65
- from_file(file, project_root: project_root, allow: allow)
66
- end
67
- end
68
-
69
- def from_gitignore_arg(gitignore, project_root:)
70
- return unless gitignore
71
-
72
- gi = ::FastIgnore::RuleSet.new([], false, true)
73
- gi << from_root_gitignore_file(global_gitignore_path(root: project_root))
74
- gi << from_root_gitignore_file(::File.join(project_root, '.git/info/exclude'))
75
- gi << from_root_gitignore_file(::File.join(project_root, '.gitignore'))
76
- gi
77
- end
78
-
79
- def from_root_gitignore_file(path)
80
- return unless ::File.exist?(path)
81
-
82
- build_rule_set(::File.readlines(path), false, file_root: '', gitignore: true)
83
- end
84
-
85
- def global_gitignore_path(root:)
86
- gitconfig_gitignore_path(::File.expand_path('.git/config', root)) ||
87
- gitconfig_gitignore_path(::File.expand_path('~/.gitconfig')) ||
88
- gitconfig_gitignore_path(xdg_config_path) ||
89
- gitconfig_gitignore_path('/etc/gitconfig') ||
90
- default_global_gitignore_path
91
- end
92
-
93
- def gitconfig_gitignore_path(config_path)
94
- return unless config_path
95
- return unless ::File.exist?(config_path)
96
-
97
- ignore_path = ::File.readlines(config_path).find { |l| l.sub!(/\A\s*excludesfile\s*=/, '') }
98
- return unless ignore_path
99
-
100
- ignore_path.strip!
101
- return ignore_path if ignore_path.empty? # don't expand path in this case
102
-
103
- ::File.expand_path(ignore_path)
104
- end
105
-
106
- def xdg_config_path
107
- return unless ENV['XDG_CONFIG_HOME'] && !ENV['XDG_CONFIG_HOME'].empty?
108
-
109
- ::File.expand_path('git/config', ENV['XDG_CONFIG_HOME'])
110
- end
111
-
112
- def default_global_gitignore_path
113
- if ENV['XDG_CONFIG_HOME'] && !ENV['XDG_CONFIG_HOME'].empty?
114
- ::File.expand_path('git/ignore', ENV['XDG_CONFIG_HOME'])
115
- else
116
- ::File.expand_path('~/.config/git/ignore')
117
- end
118
- end
119
-
120
- def from_array(rules, allow: false, expand_path: false)
121
- return unless rules
122
-
123
- rules = Array(rules)
124
-
125
- return if rules.empty?
126
-
127
- rules = rules.flat_map { |string| string.to_s.lines }
128
-
129
- build_rule_set(rules, allow, expand_path: expand_path)
130
- end
131
-
132
- def build_rule_set(rules, allow, expand_path: false, file_root: nil, gitignore: false)
133
- rules = rules.flat_map do |rule|
134
- ::FastIgnore::RuleBuilder.build(rule, allow, expand_path, file_root)
135
- end
136
-
137
- ::FastIgnore::RuleSet.new(rules, allow, gitignore)
138
- end
139
- end
140
- end
141
- end