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