fast_ignore 0.14.0 → 0.16.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -2,210 +2,180 @@
2
2
 
3
3
  class FastIgnore
4
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)
5
+ def initialize(rule, file_path)
6
+ @re = ::FastIgnore::GitignoreRuleRegexpBuilder.new
7
+ @s = ::FastIgnore::GitignoreRuleScanner.new(rule)
15
8
 
16
- @dir_only = dir_only
17
- @file_path = (file_path if file_path && !file_path.empty?)
18
- @negation = negation
9
+ @file_path = file_path
10
+ @negation = false
19
11
  @anchored = false
20
- @trailing_stars = false
12
+ @dir_only = false
21
13
  end
22
14
 
23
- def process_escaped_char
24
- @segment_re << ::Regexp.escape(@s.matched[1]) if @s.scan(/\\./)
15
+ def break!
16
+ throw :break
25
17
  end
26
18
 
27
- def process_character_class
28
- return unless @s.skip(/\[/)
19
+ def blank!
20
+ throw :abort_build, []
21
+ end
29
22
 
30
- @segment_re << '['
31
- process_character_class_body(false)
23
+ def unmatchable_rule!
24
+ throw :abort_build, []
32
25
  end
33
26
 
34
- def process_negated_character_class
35
- return unless @s.skip(/\[\^/)
27
+ def negated!
28
+ @negation = true
29
+ end
36
30
 
37
- @segment_re << '[^'
38
- process_character_class_body(true)
31
+ def anchored!
32
+ @anchored ||= true
39
33
  end
40
34
 
41
- def unmatchable_rule!
42
- throw :unmatchable_rule, (
43
- @allow ? ::FastIgnore::UnmatchableRule : []
44
- )
35
+ def never_anchored!
36
+ @anchored = :never
45
37
  end
46
38
 
47
- def process_character_class_end
48
- return unless @s.skip(/\]/)
39
+ def dir_only!
40
+ @dir_only = true
41
+ end
49
42
 
50
- unmatchable_rule! unless @has_characters_in_group
43
+ def nothing_emitted?
44
+ @re.empty?
45
+ end
51
46
 
52
- @segment_re << ']'
47
+ def emit_dir
48
+ anchored!
49
+ @re.append_dir
53
50
  end
54
51
 
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
52
+ def emit_end
53
+ @re.append_end_anchor
54
+ break!
75
55
  end
76
56
 
77
- def process_star_star_slash
78
- return unless @s.skip(%r{\*{2,}/})
57
+ def process_backslash
58
+ return unless @s.backslash?
79
59
 
80
- if @allow
81
- if @segment_re.empty?
82
- @parent_re << '.*'
83
- else
84
- process_slash_allow('.*')
85
- end
86
- end
87
- process_slash('(?:.*/)?')
60
+ @re.append_escaped(@s.next_character) || unmatchable_rule!
88
61
  end
89
62
 
90
- def process_star_slash
91
- return unless @s.skip(%r{\*/})
63
+ def process_star_end_after_slash
64
+ return true unless @s.star_end?
92
65
 
93
- process_slash_allow('[^/]*/') if @allow
94
- process_slash('[^/]*/')
66
+ @re.append_many_non_dir
67
+ emit_end
95
68
  end
96
69
 
97
- def process_no_star_slash
98
- return unless @s.skip(%r{/})
70
+ def process_slash
71
+ return unless @s.slash?
72
+ return dir_only! if @s.end?
73
+ return unmatchable_rule! if @s.slash?
99
74
 
100
- process_slash_allow('/') if @allow
101
- process_slash('/')
75
+ emit_dir
76
+ process_star_end_after_slash
102
77
  end
103
78
 
104
- def process_slash(append)
105
- @re << @segment_re
106
- @re << append
107
- @segment_re.clear
108
- @anchored = true
109
- end
79
+ def process_two_stars # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
80
+ return unless @s.two_stars?
81
+ return break! if @s.end?
110
82
 
111
- def process_slash_allow(append)
112
- @segments += 1
113
- @parent_re << '(?:'
114
- @parent_re << @segment_re
115
- @parent_re << append
83
+ if @s.slash?
84
+ if @s.end?
85
+ @re.append_any_non_dir
86
+ dir_only!
87
+ elsif @s.slash?
88
+ unmatchable_rule!
89
+ else
90
+ if nothing_emitted?
91
+ never_anchored!
92
+ else
93
+ @re.append_any_dir
94
+ anchored!
95
+ end
96
+ process_star_end_after_slash
97
+ end
98
+ else
99
+ @re.append_any_non_dir
100
+ end
116
101
  end
117
102
 
118
- def process_stars
119
- (@segment_re << '[^/]*') if @s.scan(%r{\*+(?=[^*/])})
120
- end
103
+ def process_character_class # rubocop:disable Metrics/MethodLength
104
+ return unless @s.character_class_start?
121
105
 
122
- def process_question_mark
123
- (@segment_re << '[^/]') if @s.skip(/\?/)
124
- end
106
+ @re.append_character_class_open
107
+ @re.append_character_class_negation if @s.character_class_negation?
108
+ unmatchable_rule! if @s.character_class_end?
109
+
110
+ until @s.character_class_end?
111
+ next if process_backslash
112
+ next @re.append_character_class_dash if @s.dash?
113
+ next if @re.append_escaped(@s.character_class_literal)
125
114
 
126
- def process_text
127
- (@segment_re << ::Regexp.escape(@s.matched)) if @s.scan(%r{[^*/?\[\\]+})
115
+ unmatchable_rule!
116
+ end
117
+
118
+ @re.append_character_class_close
128
119
  end
129
120
 
130
121
  def process_end
131
- return unless @s.scan(/\*+\z/)
122
+ blank! if nothing_emitted?
132
123
 
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'
124
+ emit_end
125
+ end
126
+
127
+ def process_rule # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
128
+ anchored! if @s.slash?
129
+
130
+ catch :break do
131
+ loop do
132
+ next if process_backslash
133
+ next if process_slash
134
+ next if process_two_stars
135
+ next @re.append_any_non_dir if @s.star?
136
+ next @re.append_one_non_dir if @s.question_mark?
137
+ next if process_character_class
138
+ next if @re.append_escaped(@s.literal)
139
+ next if @re.append_escaped(@s.significant_whitespace)
140
+
141
+ process_end
138
142
  end
139
143
  end
140
- @trailing_stars = true
141
144
  end
142
145
 
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
146
+ def prefix # rubocop:disable Metrics/MethodLength
147
+ out = ::FastIgnore::GitignoreRuleRegexpBuilder.new
148
+ if @file_path
149
+ out.append_start_anchor.append(@file_path.escaped)
150
+ out.append_any_dir unless @anchored
151
+ else
152
+ if @anchored
153
+ out.append_start_anchor
154
+ else
155
+ out.append_start_dir_or_anchor
156
+ end
150
157
  end
158
+ out
151
159
  end
152
160
 
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
161
+ def build_rule
162
+ @re.prepend(prefix)
163
+ ::FastIgnore::Rule.new(@re.to_regexp, @negation, anchored_or_file_path, @dir_only)
164
+ end
158
165
 
159
- @re << @segment_re
166
+ def anchored_or_file_path
167
+ @anchored || @file_path
168
+ end
160
169
 
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
170
+ def build
171
+ catch :abort_build do
172
+ blank! if @s.hash?
173
+ negated! if @s.exclamation_mark?
174
+ process_rule
175
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
176
+ @anchored = false if @anchored == :never
205
177
 
206
- # Regexp::IGNORECASE = 1
207
- ::FastIgnore::Rule.new(::Regexp.new(@re, 1), @negation, anchored_or_file_path, @dir_only)
208
- end
178
+ build_rule
209
179
  end
210
180
  end
211
181
  end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ class FastIgnore
4
+ class GitignoreRuleRegexpBuilder < String
5
+ def to_regexp
6
+ # Regexp::IGNORECASE = 1
7
+ ::Regexp.new(self, 1)
8
+ end
9
+
10
+ def append(value)
11
+ self.<<(value)
12
+
13
+ self
14
+ end
15
+
16
+ def append_escaped(value)
17
+ return unless value
18
+
19
+ append(::Regexp.escape(value))
20
+ end
21
+
22
+ def append_dir
23
+ append('/')
24
+ end
25
+
26
+ def append_any_dir
27
+ append('(?:.*/)?')
28
+ end
29
+
30
+ def append_one_non_dir
31
+ append('[^/]')
32
+ end
33
+
34
+ def append_any_non_dir
35
+ append_one_non_dir
36
+ append('*')
37
+ end
38
+
39
+ def append_many_non_dir
40
+ append_one_non_dir
41
+ append('+')
42
+ end
43
+
44
+ def append_end_anchor
45
+ append('\\z')
46
+ end
47
+
48
+ def append_start_anchor
49
+ append('\\A')
50
+ end
51
+
52
+ def append_start_dir_or_anchor
53
+ append('(?:\\A|/)')
54
+ end
55
+
56
+ def append_end_dir_or_anchor
57
+ append('(?:/|\\z)')
58
+ end
59
+
60
+ def append_character_class_open
61
+ append('(?!/)[')
62
+ end
63
+
64
+ def append_character_class_negation
65
+ append('^')
66
+ end
67
+
68
+ def append_character_class_dash
69
+ append('-')
70
+ end
71
+
72
+ def append_character_class_close
73
+ append(']')
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ class FastIgnore
4
+ class GitignoreRuleScanner < StringScanner
5
+ def character_class_end?
6
+ skip(/\]/)
7
+ end
8
+
9
+ def character_class_start?
10
+ skip(/\[/)
11
+ end
12
+
13
+ def character_class_negation?
14
+ skip(/\^|!/)
15
+ end
16
+
17
+ def end?
18
+ skip(/\s*\z/)
19
+ end
20
+
21
+ def slash?
22
+ skip(%r{/})
23
+ end
24
+
25
+ def backslash?
26
+ skip(/\\/)
27
+ end
28
+
29
+ def two_stars?
30
+ skip(/\*{2,}/)
31
+ end
32
+
33
+ def star?
34
+ skip(/\*/)
35
+ end
36
+
37
+ def next_character
38
+ matched if scan(/./)
39
+ end
40
+
41
+ def star_end?
42
+ skip(/\*\s*\z/)
43
+ end
44
+
45
+ def question_mark?
46
+ skip(/\?/)
47
+ end
48
+
49
+ def dash?
50
+ skip(/-/)
51
+ end
52
+
53
+ def character_class_literal
54
+ matched if scan(/[^\]\-\\]+/)
55
+ end
56
+
57
+ def literal
58
+ matched if scan(%r{[^*/?\[\\\s]+})
59
+ end
60
+
61
+ def significant_whitespace
62
+ matched if scan(/\s+(?!\s|\z)/)
63
+ end
64
+
65
+ def exclamation_mark?
66
+ skip(/!/)
67
+ end
68
+
69
+ def hash?
70
+ skip(/#/)
71
+ end
72
+ end
73
+ end
@@ -11,32 +11,40 @@ class FastIgnore
11
11
  default_global_gitignore_path
12
12
  end
13
13
 
14
+ private
15
+
14
16
  def gitconfig_gitignore_path(config_path)
15
17
  return unless config_path
16
- return unless ::File.exist?(config_path)
18
+ return unless ::File.readable?(config_path)
17
19
 
18
- ignore_path = ::File.readlines(config_path).find { |l| l.sub!(/\A\s*excludesfile\s*=/, '') }
20
+ ignore_path = ::FastIgnore::GitconfigParser.parse(config_path)
19
21
  return unless ignore_path
20
22
 
21
23
  ignore_path.strip!
22
- return ignore_path if ignore_path.empty? # don't expand path in this case
24
+ return '' if ignore_path.empty? # don't expand path in this case
23
25
 
24
26
  ::File.expand_path(ignore_path)
25
27
  end
26
28
 
27
29
  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'])
30
+ xdg_config_home? && ::File.expand_path('git/config', xdg_config_home)
31
31
  end
32
32
 
33
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'])
34
+ if xdg_config_home?
35
+ ::File.expand_path('git/ignore', xdg_config_home)
36
36
  else
37
37
  ::File.expand_path('~/.config/git/ignore')
38
38
  end
39
39
  end
40
+
41
+ def xdg_config_home
42
+ ::ENV['XDG_CONFIG_HOME']
43
+ end
44
+
45
+ def xdg_config_home?
46
+ xdg_config_home && (not xdg_config_home.empty?)
47
+ end
40
48
  end
41
49
  end
42
50
  end
@@ -7,6 +7,7 @@ class FastIgnore
7
7
  undef :negation
8
8
 
9
9
  attr_reader :component_rules
10
+ attr_reader :component_rules_count
10
11
 
11
12
  attr_reader :dir_only
12
13
  alias_method :dir_only?, :dir_only
@@ -17,8 +18,11 @@ class FastIgnore
17
18
 
18
19
  def squash(rules)
19
20
  # 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)
21
+ component_rules = rules.flat_map(&:component_rules)
22
+ ::FastIgnore::Rule.new(
23
+ ::Regexp.union(component_rules.map(&:rule)).freeze,
24
+ @negation, @anchored, @dir_only, component_rules
25
+ )
22
26
  end
23
27
 
24
28
  def initialize(rule, negation, anchored, dir_only, component_rules = self) # rubocop:disable Metrics/MethodLength
@@ -27,6 +31,7 @@ class FastIgnore
27
31
  @dir_only = dir_only
28
32
  @negation = negation
29
33
  @component_rules = component_rules
34
+ @component_rules_count = component_rules == self ? 1 : component_rules.length
30
35
 
31
36
  @squashable_type = if anchored && negation
32
37
  1
@@ -53,7 +58,7 @@ class FastIgnore
53
58
  end
54
59
  # :nocov:
55
60
 
56
- def match?(relative_path, _, _, _)
61
+ def match?(relative_path, _full_path, _filename, _content)
57
62
  @rule.match?(relative_path)
58
63
  end
59
64
  end
@@ -8,64 +8,33 @@ class FastIgnore
8
8
  # :nocov:
9
9
 
10
10
  def build(rule, allow, expand_path_with, file_root)
11
- return shebang_rules(rule, allow) if remove_shebang(rule)
12
-
13
- strip(rule)
14
- return [] if skip?(rule)
15
-
16
- gitignore_rules(rule, allow, expand_path_with, file_root)
11
+ if rule.delete_prefix!('#!:')
12
+ shebang_rules(rule, allow, file_root)
13
+ else
14
+ gitignore_rules(rule, allow, file_root, expand_path_with)
15
+ end
17
16
  end
18
17
 
19
18
  private
20
19
 
21
- def strip(rule)
22
- rule.chomp!
23
- rule.rstrip! unless rule.end_with?('\\ ')
24
- end
25
-
26
- def remove_shebang(rule)
27
- return unless rule.delete_prefix!('#!:')
28
-
29
- rule.strip!
30
-
31
- true
32
- end
33
-
34
- def shebang_rules(rule, allow)
35
- rule = ::FastIgnore::ShebangRule.new(/\A#!.*\b#{::Regexp.escape(rule)}\b/i, allow)
20
+ def shebang_rules(shebang, allow, file_root)
21
+ shebang.strip!
22
+ pattern = /\A#!.*\b#{::Regexp.escape(shebang)}\b/i
23
+ rule = ::FastIgnore::ShebangRule.new(pattern, allow, file_root&.shebang_path_pattern)
36
24
  return rule unless allow
37
25
 
38
- [::FastIgnore::Rule.new(//, true, true, true), rule]
39
- end
40
-
41
- def skip?(rule)
42
- rule.empty? || rule.start_with?('#')
43
- end
44
-
45
- def gitignore_rules(rule, allow, expand_path_with, file_root)
46
- dir_only = extract_dir_only(rule)
47
- negation = extract_negation(rule, allow)
48
-
49
- expand_rule_path(rule, expand_path_with) if expand_path_with
50
-
51
- ::FastIgnore::GitignoreRuleBuilder.new(rule, negation, dir_only, file_root, allow).build
52
- end
53
-
54
- def extract_dir_only(rule)
55
- rule.delete_suffix!('/')
56
- end
57
-
58
- def extract_negation(rule, allow)
59
- return allow unless rule.delete_prefix!('!')
60
-
61
- not allow
26
+ rules = gitignore_rules('*/'.dup, allow, file_root)
27
+ rules.pop # don't want the include all children one.
28
+ rules << rule
29
+ rules
62
30
  end
63
31
 
64
- EXPAND_PATH_RE = %r{(^(?:[~/]|\.{1,2}/)|/\.\./)}.freeze
65
- def expand_rule_path(rule, root)
66
- rule.replace(::File.expand_path(rule)) if rule.match?(EXPAND_PATH_RE)
67
- rule.delete_prefix!(root)
68
- rule.prepend('/') unless rule.start_with?('*')
32
+ def gitignore_rules(rule, allow, file_root, expand_path_with = nil)
33
+ if allow
34
+ ::FastIgnore::GitignoreIncludeRuleBuilder.new(rule, file_root, expand_path_with).build
35
+ else
36
+ ::FastIgnore::GitignoreRuleBuilder.new(rule, file_root).build
37
+ end
69
38
  end
70
39
  end
71
40
  end