fast_ignore 0.14.0 → 0.15.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: 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