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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 05f6139deefedc14e0c58acd66909aa25af5240a83e67a6463eacedd6cec9eb3
4
- data.tar.gz: c6becf91ea8bc9ccf0524309c5cad1e01485c4e2324222f231ade33855c65528
3
+ metadata.gz: 88d6249cc842976d1b5c0e984816cfc260cc3edcbe4774ea4580e934bedcf925
4
+ data.tar.gz: 3ab13efc019bee9b5cadcb45f485021132f07787bce73d66bb9ef63b76bc51ca
5
5
  SHA512:
6
- metadata.gz: cca040e6401d0c412498dff47b460ba7a19d5c9cb810cc205151a56b9db959a88ea3c4137610537bda43c5520611b1711abc8124b0ab98dd6bda261de9638588
7
- data.tar.gz: a3fe257c77f8350c2dde1bcfcde01293b5af87b5d6b627b4e44555b3b6a45154c3d567d1e68f8f03520d21e28e78eda439e92b7a8b5b2d0b43b6f2a23a2f5ae4
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=master)](https://travis-ci.com/robotdana/fast_ignore)
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-2.7 & jruby
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