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 +4 -4
- data/CHANGELOG.md +4 -0
- data/README.md +24 -21
- data/lib/fast_ignore.rb +18 -13
- data/lib/fast_ignore/backports.rb +2 -2
- data/lib/fast_ignore/gitignore_rule_builder.rb +212 -0
- data/lib/fast_ignore/global_gitignore.rb +42 -0
- data/lib/fast_ignore/rule.rb +23 -12
- data/lib/fast_ignore/rule_builder.rb +10 -41
- data/lib/fast_ignore/rule_set.rb +7 -11
- data/lib/fast_ignore/rule_sets.rb +112 -0
- data/lib/fast_ignore/shebang_rule.rb +21 -10
- data/lib/fast_ignore/unmatchable_rule.rb +37 -0
- data/lib/fast_ignore/version.rb +1 -1
- metadata +6 -4
- data/lib/fast_ignore/fn_match_to_re.rb +0 -96
- data/lib/fast_ignore/rule_set_builder.rb +0 -141
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 05f6139deefedc14e0c58acd66909aa25af5240a83e67a6463eacedd6cec9eb3
|
4
|
+
data.tar.gz: c6becf91ea8bc9ccf0524309c5cad1e01485c4e2324222f231ade33855c65528
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cca040e6401d0c412498dff47b460ba7a19d5c9cb810cc205151a56b9db959a88ea3c4137610537bda43c5520611b1711abc8124b0ab98dd6bda261de9638588
|
7
|
+
data.tar.gz: a3fe257c77f8350c2dde1bcfcde01293b5af87b5d6b627b4e44555b3b6a45154c3d567d1e68f8f03520d21e28e78eda439e92b7a8b5b2d0b43b6f2a23a2f5ae4
|
data/CHANGELOG.md
CHANGED
@@ -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
|
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)
|
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
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
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).
|
data/lib/fast_ignore.rb
CHANGED
@@ -3,12 +3,15 @@
|
|
3
3
|
require_relative './fast_ignore/backports'
|
4
4
|
|
5
5
|
require 'set'
|
6
|
-
|
7
|
-
require_relative './fast_ignore/
|
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::
|
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.
|
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,
|
77
|
+
def load_gitignore(parent_path, check_exists: true)
|
71
78
|
return if @loaded_gitignore_files.include?(parent_path)
|
72
79
|
|
73
|
-
|
74
|
-
|
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.
|
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
|
data/lib/fast_ignore/rule.rb
CHANGED
@@ -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 :
|
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
|
21
|
-
|
22
|
-
|
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
|
-
@
|
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
|
-
|
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,
|
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,
|
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
|
-
|
36
|
-
return
|
35
|
+
rule = ::FastIgnore::ShebangRule.new(/\A#!.*\b#{::Regexp.escape(rule)}\b/i, allow)
|
36
|
+
return rule unless allow
|
37
37
|
|
38
|
-
|
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,
|
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
|
-
|
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
|
-
|
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
|
data/lib/fast_ignore/rule_set.rb
CHANGED
@@ -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
|
-
@
|
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
|
28
|
-
@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
|
-
|
42
|
+
not @allow
|
45
43
|
end
|
46
44
|
|
47
45
|
def squash_rules(rules)
|
48
|
-
|
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.
|
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, :
|
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 :
|
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
|
-
@
|
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(
|
52
|
-
|
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
|
data/lib/fast_ignore/version.rb
CHANGED
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.
|
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-
|
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/
|
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/
|
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
|