fast_ignore 0.14.0 → 0.16.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +17 -0
- data/README.md +5 -3
- data/lib/fast_ignore/file_root.rb +39 -0
- data/lib/fast_ignore/gitconfig_parser.rb +207 -0
- data/lib/fast_ignore/gitignore_include_rule_builder.rb +102 -0
- data/lib/fast_ignore/gitignore_rule_builder.rb +125 -155
- data/lib/fast_ignore/gitignore_rule_regexp_builder.rb +76 -0
- data/lib/fast_ignore/gitignore_rule_scanner.rb +73 -0
- data/lib/fast_ignore/global_gitignore.rb +16 -8
- data/lib/fast_ignore/rule.rb +8 -3
- data/lib/fast_ignore/rule_builder.rb +19 -50
- data/lib/fast_ignore/rule_set.rb +16 -6
- data/lib/fast_ignore/rule_sets.rb +13 -12
- data/lib/fast_ignore/shebang_rule.rb +15 -5
- data/lib/fast_ignore/unmatchable_rule.rb +5 -1
- data/lib/fast_ignore/version.rb +1 -1
- data/lib/fast_ignore.rb +15 -9
- metadata +43 -18
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 88d6249cc842976d1b5c0e984816cfc260cc3edcbe4774ea4580e934bedcf925
|
4
|
+
data.tar.gz: 3ab13efc019bee9b5cadcb45f485021132f07787bce73d66bb9ef63b76bc51ca
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 853124ae1ddf1fc599e8a422580ee84ff705907a70c7ece17db72502a4468ca0d5479f918c175f91dc1915c7cccb5ad4cd7eaa956adf2d7b0a5e2fa024821c3d
|
7
|
+
data.tar.gz: b90c827caa6c24c3491237ec5fb538b828b2daea879a4d94d6bc3ac4368807c7040bc0d6ea6405b8e9712483d00fddfb6d43e1a970786f374144a4d6165f59ee
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,20 @@
|
|
1
|
+
# v0.16.0
|
2
|
+
- Entirely rewrite the way that git config files are read. previously it was just a regexp. now we actually parse git config files according to the same rules as git.
|
3
|
+
- Add ruby 3 to the test matrix
|
4
|
+
|
5
|
+
# v0.15.2
|
6
|
+
- Updated methods with multiple `_` arguments to have different names to make sorbet happy
|
7
|
+
|
8
|
+
# v0.15.1
|
9
|
+
- Updated dependencies to allow running on ruby 3.0.0.preview1
|
10
|
+
|
11
|
+
# v0.15.0
|
12
|
+
- fixed a handful of character class edge cases to match git behavior
|
13
|
+
- mostly ranges with - or / as one end of the range
|
14
|
+
- 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).
|
15
|
+
- improved speed of repos with many sub-gitignore files
|
16
|
+
- 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.
|
17
|
+
|
1
18
|
# v0.14.0
|
2
19
|
- significant performance improvements ~50% faster
|
3
20
|
- add `FastIgnore#to_proc` for no good reason
|
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# FastIgnore
|
2
2
|
|
3
|
-
[![travis](https://travis-ci.com/robotdana/fast_ignore.svg?branch=
|
3
|
+
[![travis](https://travis-ci.com/robotdana/fast_ignore.svg?branch=main)](https://travis-ci.com/robotdana/fast_ignore)
|
4
4
|
[![Gem Version](https://badge.fury.io/rb/fast_ignore.svg)](https://rubygems.org/gems/fast_ignore)
|
5
5
|
|
6
6
|
This started as a way to quickly and natively ruby-ly parse gitignore files and find matching files.
|
@@ -15,7 +15,7 @@ FastIgnore.new(relative: true).sort == `git ls-files`.split("\n").sort
|
|
15
15
|
## Features
|
16
16
|
|
17
17
|
- Fast (faster than using `` `git ls-files`.split("\n") `` for small repos (because it avoids the overhead of ``` `` ```))
|
18
|
-
- Supports ruby 2.4-
|
18
|
+
- Supports ruby 2.4-3.0.x & jruby
|
19
19
|
- supports all [gitignore rule patterns](https://git-scm.com/docs/gitignore#_pattern_format)
|
20
20
|
- doesn't require git to be installed
|
21
21
|
- supports a gitignore-esque "include" patterns. ([`include_rules:`](#include_rules)/[`include_files:`](#include_files))
|
@@ -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
|
|
@@ -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,207 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'strscan'
|
4
|
+
class FastIgnore
|
5
|
+
class GitconfigParseError < FastIgnore::Error; end
|
6
|
+
|
7
|
+
class GitconfigParser # rubocop:disable Metrics/ClassLength
|
8
|
+
def self.parse(file, root: Dir.pwd, nesting: 1)
|
9
|
+
new(file, root: root, nesting: nesting).parse
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize(path, root: Dir.pwd, nesting: 1)
|
13
|
+
@path = path
|
14
|
+
@root = root
|
15
|
+
@nesting = nesting
|
16
|
+
end
|
17
|
+
|
18
|
+
def parse
|
19
|
+
raise ::FastIgnore::GitconfigParseError if nesting >= 10
|
20
|
+
|
21
|
+
read_file(path)
|
22
|
+
return unless value
|
23
|
+
|
24
|
+
value
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
attr_reader :nesting
|
30
|
+
attr_reader :path
|
31
|
+
attr_reader :root
|
32
|
+
attr_accessor :value
|
33
|
+
attr_accessor :within_quotes
|
34
|
+
attr_accessor :section
|
35
|
+
|
36
|
+
def read_file(path) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
37
|
+
return unless ::File.readable?(path)
|
38
|
+
|
39
|
+
file = StringScanner.new(::File.read(path))
|
40
|
+
|
41
|
+
until file.eos?
|
42
|
+
if file.skip(/(\s+|[#;].*\n)/)
|
43
|
+
# skip
|
44
|
+
elsif file.skip(/\[core\]/i)
|
45
|
+
self.section = :core
|
46
|
+
elsif file.skip(/\[include\]/i)
|
47
|
+
self.section = :include
|
48
|
+
elsif file.skip(/\[(?i:includeif) +"/)
|
49
|
+
self.section = include_if(file) ? :include : :not_include
|
50
|
+
elsif file.skip(/\[[\w.]+( "([^\0\\"]|\\(\\{2})*"|\\{2}*)+")?\]/)
|
51
|
+
self.section = :other
|
52
|
+
elsif section == :core && file.skip(/excludesfile\s*=(\s|\\\n)*/i)
|
53
|
+
self.value = scan_value(file)
|
54
|
+
elsif section == :include && file.skip(/path\s*=(\s|\\\n)*/)
|
55
|
+
include_path = scan_value(file)
|
56
|
+
|
57
|
+
value = ::FastIgnore::GitconfigParser.parse(
|
58
|
+
::File.expand_path(include_path, ::File.dirname(path)),
|
59
|
+
root: root,
|
60
|
+
nesting: nesting + 1
|
61
|
+
)
|
62
|
+
self.value = value if value
|
63
|
+
self.section = :include
|
64
|
+
elsif file.skip(/[a-zA-Z0-9]\w*\s*([#;].*)?\n/)
|
65
|
+
nil
|
66
|
+
elsif file.skip(/[a-zA-Z0-9]\w*\s*=(\s|\\\n)*/)
|
67
|
+
skip_value(file)
|
68
|
+
else
|
69
|
+
raise ::FastIgnore::GitconfigParseError
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def scan_condition_value(file)
|
75
|
+
if file.scan(/([^\0\\\n"]|\\(\\{2})*"|\\{2}*)+(?="\])/)
|
76
|
+
value = file.matched
|
77
|
+
file.skip(/"\]/)
|
78
|
+
value
|
79
|
+
else
|
80
|
+
raise ::FastIgnore::GitconfigParseError
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def skip_condition_value(file)
|
85
|
+
raise ::FastIgnore::GitconfigParseError unless file.skip(/([^\0\\\n"]|\\(\\{2})*"|\\{2}*)+"\]/)
|
86
|
+
end
|
87
|
+
|
88
|
+
def include_if(file)
|
89
|
+
if file.skip(/onbranch:/)
|
90
|
+
on_branch?(scan_condition_value(file))
|
91
|
+
elsif file.skip(/gitdir:/)
|
92
|
+
gitdir?(scan_condition_value(file), path: path)
|
93
|
+
elsif file.skip(%r{gitdir/i:})
|
94
|
+
gitdir?(scan_condition_value(file), case_insensitive: true, path: path)
|
95
|
+
else
|
96
|
+
skip_condition_value(file)
|
97
|
+
false
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def on_branch?(branch_pattern)
|
102
|
+
branch_pattern += '**' if branch_pattern.end_with?('/')
|
103
|
+
current_branch = ::File.readable?("#{root}/.git/HEAD") && ::File.read("#{root}/.git/HEAD").sub!(
|
104
|
+
%r{\Aref: refs/heads/}, ''
|
105
|
+
)
|
106
|
+
return unless current_branch
|
107
|
+
|
108
|
+
# goddamit git what does 'a pattern with standard globbing wildcards' mean
|
109
|
+
::File.fnmatch(branch_pattern, current_branch, ::File::FNM_PATHNAME | ::File::FNM_DOTMATCH)
|
110
|
+
end
|
111
|
+
|
112
|
+
def gitdir?(gitdir, path:, case_insensitive: false)
|
113
|
+
gitdir += '**' if gitdir.end_with?('/')
|
114
|
+
gitdir.sub!(%r{\A~/}, ENV['HOME'] + '/')
|
115
|
+
gitdir.sub!(/\A\./, path + '/')
|
116
|
+
gitdir = "**/#{gitdir}" unless gitdir.start_with?('/')
|
117
|
+
options = ::File::FNM_PATHNAME | ::File::FNM_DOTMATCH
|
118
|
+
options |= ::File::FNM_CASEFOLD if case_insensitive
|
119
|
+
::File.fnmatch(gitdir, ::File.join(root, '.git'), options)
|
120
|
+
end
|
121
|
+
|
122
|
+
def scan_value(file) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
123
|
+
value = +''
|
124
|
+
until file.eos?
|
125
|
+
if file.skip(/\\\n/)
|
126
|
+
# continue
|
127
|
+
elsif file.skip(/\\\\/)
|
128
|
+
value << '\\'
|
129
|
+
elsif file.skip(/\\n/)
|
130
|
+
value << "\n"
|
131
|
+
elsif file.skip(/\\t/)
|
132
|
+
value << "\t"
|
133
|
+
elsif file.skip(/\\b/)
|
134
|
+
value.chop!
|
135
|
+
elsif file.skip(/\\"/)
|
136
|
+
value << '"'
|
137
|
+
elsif file.skip(/\\/)
|
138
|
+
raise ::FastIgnore::GitconfigParseError
|
139
|
+
elsif within_quotes
|
140
|
+
if file.skip(/"/)
|
141
|
+
self.within_quotes = false
|
142
|
+
elsif file.scan(/[^"\\\n]+/)
|
143
|
+
value << file.matched
|
144
|
+
elsif file.skip(/\n/)
|
145
|
+
raise ::FastIgnore::GitconfigParseError
|
146
|
+
# :nocov:
|
147
|
+
else
|
148
|
+
raise "Unmatched #{file.rest}"
|
149
|
+
# :nocov:
|
150
|
+
end
|
151
|
+
elsif file.skip(/"/)
|
152
|
+
self.within_quotes = true
|
153
|
+
elsif file.scan(/[^;#"\s\\]+/)
|
154
|
+
value << file.matched
|
155
|
+
elsif file.skip(/\s*[;#\n]/)
|
156
|
+
break
|
157
|
+
elsif file.scan(/\s+/) # rubocop:disable Lint/DuplicateBranch
|
158
|
+
value << file.matched
|
159
|
+
# :nocov:
|
160
|
+
else
|
161
|
+
raise "Unmatched #{file.rest}"
|
162
|
+
# :nocov:
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
raise ::FastIgnore::GitconfigParseError if within_quotes
|
167
|
+
|
168
|
+
value
|
169
|
+
end
|
170
|
+
|
171
|
+
def skip_value(file) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
172
|
+
until file.eos?
|
173
|
+
if file.skip(/\\(?:\n|\\|n|t|b|")/)
|
174
|
+
nil
|
175
|
+
elsif file.skip(/\\/)
|
176
|
+
raise ::FastIgnore::GitconfigParseError
|
177
|
+
elsif within_quotes
|
178
|
+
if file.skip(/"/)
|
179
|
+
self.within_quotes = false
|
180
|
+
elsif file.skip(/[^"\\\n]+/)
|
181
|
+
nil
|
182
|
+
elsif file.scan(/\n/)
|
183
|
+
raise ::FastIgnore::GitconfigParseError
|
184
|
+
# :nocov:
|
185
|
+
else
|
186
|
+
raise "Unmatched #{file.rest}"
|
187
|
+
# :nocov:
|
188
|
+
end
|
189
|
+
elsif file.skip(/"/)
|
190
|
+
self.within_quotes = true
|
191
|
+
elsif file.skip(/[^;#"\s\\]+/) # rubocop:disable Lint/DuplicateBranch
|
192
|
+
nil
|
193
|
+
elsif file.skip(/\s*[;#\n]/)
|
194
|
+
break
|
195
|
+
elsif file.skip(/\s+/) # rubocop:disable Lint/DuplicateBranch
|
196
|
+
nil
|
197
|
+
# :nocov:
|
198
|
+
else
|
199
|
+
raise "Unmatched #{file.rest}"
|
200
|
+
# :nocov:
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
raise ::FastIgnore::GitconfigParseError if within_quotes
|
205
|
+
end
|
206
|
+
end
|
207
|
+
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
|