fast_ignore 0.14.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 05f6139deefedc14e0c58acd66909aa25af5240a83e67a6463eacedd6cec9eb3
4
- data.tar.gz: c6becf91ea8bc9ccf0524309c5cad1e01485c4e2324222f231ade33855c65528
3
+ metadata.gz: f96030c8670a5d709139689212b3a78a0eb0b5266cbd0057954863ae89e11745
4
+ data.tar.gz: a2815ec68f6c367a4450efce8799aa359adeee1ad1aaa971b589cd8612ddbb61
5
5
  SHA512:
6
- metadata.gz: cca040e6401d0c412498dff47b460ba7a19d5c9cb810cc205151a56b9db959a88ea3c4137610537bda43c5520611b1711abc8124b0ab98dd6bda261de9638588
7
- data.tar.gz: a3fe257c77f8350c2dde1bcfcde01293b5af87b5d6b627b4e44555b3b6a45154c3d567d1e68f8f03520d21e28e78eda439e92b7a8b5b2d0b43b6f2a23a2f5ae4
6
+ metadata.gz: 5c22c6c79a6605473156a041d5ddb5b81f943605dc4360dd8577acd56718c928c106e044169b13bc73c7e884ffcc5516c7a6b5d6134fee9aaa4aa73d0178b791
7
+ data.tar.gz: 1d165e44b50d9b1c301c5cad8ff3c443bfb2f9134be09e28a4e90539c960a269083485f6ff30bc6df6e2ec82347630187ebf0082cd2d06ba7416e9c70f37ea6f
@@ -1,3 +1,10 @@
1
+ # v0.15.0
2
+ - fixed a handful of character class edge cases to match git behavior
3
+ - mostly ranges with - or / as one end of the range
4
+ - major refactoring of the regexp builder that shouldn't have any behaviour implications but should make development easier (e.g. seeing those unhandled edge cases).
5
+ - improved speed of repos with many sub-gitignore files
6
+ - mentioned submodules & sparse checkout in the readme as yet another thing git does that this project doesn't because submodule details are hidden in the git index.
7
+
1
8
  # v0.14.0
2
9
  - significant performance improvements ~50% faster
3
10
  - add `FastIgnore#to_proc` for no good reason
data/README.md CHANGED
@@ -309,11 +309,13 @@ This is not required, and if FastIgnore does have to go to the filesystem for th
309
309
  (It does handle changing the current working directory between [`FastIgnore#allowed?`](#allowed) calls)
310
310
  - FastIgnore always matches patterns case-insensitively. (git varies by filesystem).
311
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`
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`. To avoid these differences you may want to use the [`git_ls`](https://github.com/robotdana/git_ls) gem instead
313
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
314
  - Untracked files will be returned by FastIgnore, but not by `git ls-files`
315
315
  - Deleted files whose deletions haven't been committed will be returned by `git ls-files`, but not by FastIgnore
316
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.
317
+ - FastIgnore is unaware of submodules and just treats them like regular directories. For example: `git ls-files --recurse-submodules` won't use the parent repo's gitignore on a submodule, while FastIgnore doesn't know it's a submodule and will.
318
+ - FastIgnore will only return the files actually on the file system when using `git sparse-checkout`.
317
319
 
318
320
  ## Contributing
319
321
 
@@ -9,6 +9,10 @@ require_relative './fast_ignore/rule_set'
9
9
  require_relative './fast_ignore/global_gitignore'
10
10
  require_relative './fast_ignore/rule_builder'
11
11
  require_relative './fast_ignore/gitignore_rule_builder'
12
+ require_relative './fast_ignore/gitignore_include_rule_builder'
13
+ require_relative './fast_ignore/gitignore_rule_regexp_builder'
14
+ require_relative './fast_ignore/gitignore_rule_scanner'
15
+ require_relative './fast_ignore/file_root'
12
16
  require_relative './fast_ignore/rule'
13
17
  require_relative './fast_ignore/unmatchable_rule'
14
18
  require_relative './fast_ignore/shebang_rule'
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ class FastIgnore
4
+ class FileRoot
5
+ # :nocov:
6
+ using ::FastIgnore::Backports::DeletePrefixSuffix if defined?(::FastIgnore::Backports::DeletePrefixSuffix)
7
+ # :nocov:
8
+
9
+ def self.build(file_path, project_root)
10
+ file_root = "#{::File.dirname(file_path)}/".delete_prefix(project_root)
11
+
12
+ new(file_root) unless file_root.empty?
13
+ end
14
+
15
+ def initialize(file_root)
16
+ @file_root = file_root
17
+ end
18
+
19
+ def shebang_path_pattern
20
+ @shebang_path_pattern ||= /\A#{escaped}./
21
+ end
22
+
23
+ def escaped
24
+ @escaped ||= ::Regexp.escape(@file_root)
25
+ end
26
+
27
+ def escaped_segments
28
+ @escaped_segments ||= escaped.split('/')
29
+ end
30
+
31
+ def escaped_segments_length
32
+ @escaped_segments_length ||= escaped_segments.length
33
+ end
34
+
35
+ def escaped_segments_joined
36
+ @escaped_segments_joined ||= escaped_segments.join('(?:/') + '(?:/'
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ class FastIgnore
4
+ class GitignoreIncludeRuleBuilder < GitignoreRuleBuilder
5
+ # :nocov:
6
+ using ::FastIgnore::Backports::DeletePrefixSuffix if defined?(::FastIgnore::Backports::DeletePrefixSuffix)
7
+ # :nocov:
8
+
9
+ def initialize(rule, file_path, expand_path_from = nil)
10
+ super(rule, file_path)
11
+
12
+ @parent_segments = []
13
+ @negation = true
14
+ @expand_path_from = expand_path_from
15
+ end
16
+
17
+ def expand_rule_path
18
+ anchored! unless @s.match?(/\*/)
19
+ return unless @s.match?(%r{(?:[~/]|\.{1,2}/|.*/\.\./)})
20
+
21
+ dir_only! if @s.match?(%r{.*/\s*\z})
22
+ @s.string.replace(::File.expand_path(@s.rest))
23
+ @s.string.delete_prefix!(@expand_path_from)
24
+ @s.pos = 0
25
+ end
26
+
27
+ def negated!
28
+ @negation = false
29
+ end
30
+
31
+ def unmatchable_rule!
32
+ throw :abort_build, ::FastIgnore::UnmatchableRule
33
+ end
34
+
35
+ def nothing_emitted?
36
+ @re.empty? && @parent_segments.empty?
37
+ end
38
+
39
+ def emit_dir
40
+ anchored!
41
+
42
+ @parent_segments << @re
43
+ @re = ::FastIgnore::GitignoreRuleRegexpBuilder.new
44
+ end
45
+
46
+ def emit_end
47
+ @dir_only || @re.append_end_dir_or_anchor
48
+ break!
49
+ end
50
+
51
+ def parent_dir_re # rubocop:disable Metrics/MethodLength
52
+ segment_joins_count = @parent_segments.length
53
+ parent_prefix = if @file_path
54
+ segment_joins_count += @file_path.escaped_segments_length
55
+
56
+ if @anchored
57
+ "\\A#{@file_path.escaped_segments_joined}"
58
+ else
59
+ "\\A#{@file_path.escaped_segments_joined}(?:.*/)?"
60
+ end
61
+ else
62
+ prefix
63
+ end
64
+
65
+ out = parent_prefix.dup
66
+ unless @parent_segments.empty?
67
+ out << '(?:'
68
+ out << @parent_segments.join('/(?:')
69
+ out << '/'
70
+ end
71
+ out << (')?' * segment_joins_count)
72
+ out
73
+ end
74
+
75
+ def build_parent_dir_rule
76
+ # Regexp::IGNORECASE = 1
77
+ ::FastIgnore::Rule.new(::Regexp.new(parent_dir_re, 1), true, anchored_or_file_path, true)
78
+ end
79
+
80
+ def build_child_file_rule
81
+ # Regexp::IGNORECASE = 1
82
+ ::FastIgnore::Rule.new(@re.append_dir.to_regexp, @negation, anchored_or_file_path, false)
83
+ end
84
+
85
+ def build_rule
86
+ joined_re = ::FastIgnore::GitignoreRuleRegexpBuilder.new
87
+ joined_re.append(@parent_segments.join('/'))
88
+ joined_re.append_dir unless @parent_segments.empty?
89
+ joined_re.append(@re)
90
+ @re = joined_re
91
+
92
+ rules = [super, build_parent_dir_rule]
93
+ (rules << build_child_file_rule) if @dir_only
94
+ rules
95
+ end
96
+
97
+ def process_rule
98
+ expand_rule_path if @expand_path_from
99
+ super
100
+ end
101
+ end
102
+ end
@@ -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, Metrics/AbcSize
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,6 +11,8 @@ 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
18
  return unless ::File.exist?(config_path)
@@ -25,18 +27,24 @@ class FastIgnore
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
@@ -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
@@ -6,9 +6,9 @@ class FastIgnore
6
6
  alias_method :gitignore?, :gitignore
7
7
  undef :gitignore
8
8
 
9
- def initialize(rules, allow, gitignore)
10
- @dir_rules = squash_rules(rules.reject(&:file_only?)).freeze
11
- @file_rules = squash_rules(rules.reject(&:dir_only?)).freeze
9
+ def initialize(rules, allow, gitignore, squash = true)
10
+ @dir_rules = (squash ? squash_rules(rules.reject(&:file_only?)) : rules.reject(&:file_only?)).freeze
11
+ @file_rules = (squash ? squash_rules(rules.reject(&:dir_only?)) : rules.reject(&:dir_only?)).freeze
12
12
  @has_shebang_rules = rules.any?(&:shebang?)
13
13
 
14
14
  @allowed_recursive = { '.' => true }
@@ -42,8 +42,16 @@ class FastIgnore
42
42
  not @allow
43
43
  end
44
44
 
45
- def squash_rules(rules)
46
- rules.chunk_while { |a, b| a.squashable_type == b.squashable_type }.map do |chunk|
45
+ def squash_rules(rules) # rubocop:disable Metrics/MethodLength
46
+ running_component_rule_size = rules.first&.component_rules_count || 0
47
+ rules.chunk_while do |a, b|
48
+ # a.squashable_type == b.squashable_type
49
+ next true if a.squashable_type == b.squashable_type &&
50
+ (running_component_rule_size + b.component_rules_count <= 40)
51
+
52
+ running_component_rule_size = b.component_rules_count
53
+ false
54
+ end.map do |chunk| # rubocop:disable Style/MultilineBlockChain
47
55
  first = chunk.first
48
56
  next first if chunk.length == 1
49
57
 
@@ -61,6 +69,8 @@ class FastIgnore
61
69
 
62
70
  protected
63
71
 
64
- attr_reader :dir_rules, :file_rules, :has_shebang_rules
72
+ attr_reader :dir_rules
73
+ attr_reader :file_rules
74
+ attr_reader :has_shebang_rules
65
75
  end
66
76
  end
@@ -35,13 +35,16 @@ class FastIgnore
35
35
  @array.all? { |r| r.allowed_unrecursive?(relative_path, dir, full_path, filename, nil) }
36
36
  end
37
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
-
38
+ def append_subdir_gitignore(relative_path:, check_exists: true) # rubocop:disable Metrics/MethodLength
42
39
  if @gitignore_rule_set
40
+ new_gitignore = build_set_from_file(relative_path, gitignore: true, check_exists: check_exists, squash: false)
41
+ return if !new_gitignore || new_gitignore.empty?
42
+
43
43
  @gitignore_rule_set << new_gitignore
44
44
  else
45
+ new_gitignore = build_set_from_file(relative_path, gitignore: true, check_exists: check_exists)
46
+ return if !new_gitignore || new_gitignore.empty?
47
+
45
48
  @array << new_gitignore
46
49
  @gitignore_rule_set = new_gitignore
47
50
  @array.sort_by!(&:weight) && @array.freeze
@@ -75,23 +78,21 @@ class FastIgnore
75
78
  build_rule_set(::File.readlines(path), false, gitignore: true)
76
79
  end
77
80
 
78
- def build_rule_set(rules, allow, expand_path_with: nil, file_root: nil, gitignore: false)
81
+ def build_rule_set(rules, allow, expand_path_with: nil, file_root: nil, gitignore: false, squash: true) # rubocop:disable Metrics/ParameterLists
79
82
  rules = rules.flat_map do |rule|
80
83
  ::FastIgnore::RuleBuilder.build(rule, allow, expand_path_with, file_root)
81
84
  end
82
85
 
83
- ::FastIgnore::RuleSet.new(rules, allow, gitignore)
86
+ ::FastIgnore::RuleSet.new(rules, allow, gitignore, squash)
84
87
  end
85
88
 
86
- def build_set_from_file(filename, allow: false, file_root: nil, gitignore: false, check_exists: false)
89
+ def build_set_from_file(filename, allow: false, gitignore: false, check_exists: false, squash: true)
87
90
  filename = ::File.expand_path(filename, @project_root)
88
91
  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
+ raise ::FastIgnore::Error, "#{filename} is not within #{@project_root}" unless filename.start_with?(@project_root)
92
93
 
93
- file_root ||= "#{::File.dirname(filename)}/".delete_prefix(@project_root)
94
- build_rule_set(::File.readlines(filename), allow, file_root: file_root, gitignore: gitignore)
94
+ file_root = ::FastIgnore::FileRoot.build(filename, @project_root)
95
+ build_rule_set(::File.readlines(filename), allow, file_root: file_root, gitignore: gitignore, squash: squash)
95
96
  end
96
97
 
97
98
  def append_sets_from_files(files, allow: false)
@@ -8,17 +8,24 @@ class FastIgnore
8
8
 
9
9
  attr_reader :rule
10
10
 
11
+ attr_reader :file_path_pattern
12
+
11
13
  attr_reader :squashable_type
12
14
 
13
15
  def squash(rules)
14
- ::FastIgnore::ShebangRule.new(::Regexp.union(rules.map(&:rule)).freeze, negation?)
16
+ ::FastIgnore::ShebangRule.new(::Regexp.union(rules.map(&:rule)).freeze, negation?, file_path_pattern)
17
+ end
18
+
19
+ def component_rules_count
20
+ 1
15
21
  end
16
22
 
17
- def initialize(rule, negation)
23
+ def initialize(rule, negation, file_path_pattern)
18
24
  @rule = rule
19
25
  @negation = negation
26
+ @file_path_pattern = file_path_pattern
20
27
 
21
- @squashable_type = negation ? 3 : 2
28
+ @squashable_type = (negation ? 13 : 12) + file_path_pattern.object_id
22
29
 
23
30
  freeze
24
31
  end
@@ -33,12 +40,15 @@ class FastIgnore
33
40
 
34
41
  # :nocov:
35
42
  def inspect
36
- "#<ShebangRule #{'allow ' if @negation}#!:#{@rule.to_s[15..-4]}>"
43
+ allow_fragment = 'allow ' if @negation
44
+ in_fragment = " in #{@file_path_pattern}" if @file_path_pattern
45
+ "#<ShebangRule #{allow_fragment}#!:#{@rule.to_s[15..-4]}#{in_fragment}>"
37
46
  end
38
47
  # :nocov:
39
48
 
40
- def match?(_, full_path, filename, content)
49
+ def match?(path, full_path, filename, content)
41
50
  return false if filename.include?('.')
51
+ return false unless (not @file_path_pattern) || @file_path_pattern.match?(path)
42
52
 
43
53
  (content || first_line(full_path))&.match?(@rule)
44
54
  end
@@ -11,6 +11,10 @@ class FastIgnore
11
11
  5
12
12
  end
13
13
 
14
+ def component_rules_count
15
+ 1
16
+ end
17
+
14
18
  def dir_only?
15
19
  false
16
20
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class FastIgnore
4
- VERSION = '0.14.0'
4
+ VERSION = '0.15.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.14.0
4
+ version: 0.15.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-28 00:00:00.000000000 Z
11
+ date: 2020-07-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -148,7 +148,11 @@ files:
148
148
  - README.md
149
149
  - lib/fast_ignore.rb
150
150
  - lib/fast_ignore/backports.rb
151
+ - lib/fast_ignore/file_root.rb
152
+ - lib/fast_ignore/gitignore_include_rule_builder.rb
151
153
  - lib/fast_ignore/gitignore_rule_builder.rb
154
+ - lib/fast_ignore/gitignore_rule_regexp_builder.rb
155
+ - lib/fast_ignore/gitignore_rule_scanner.rb
152
156
  - lib/fast_ignore/global_gitignore.rb
153
157
  - lib/fast_ignore/rule.rb
154
158
  - lib/fast_ignore/rule_builder.rb