fast_ignore 0.13.0 → 0.14.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 +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
|