path_list 0.16

Sign up to get free protection for your applications and to get access to all the features.
data/lib/path_list.rb ADDED
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './path_list/backports'
4
+
5
+ require 'set'
6
+ require 'strscan'
7
+ require_relative 'path_list/rule_groups'
8
+ require_relative 'path_list/global_gitignore'
9
+ require_relative 'path_list/rule_builder'
10
+ require_relative 'path_list/gitignore_rule_builder'
11
+ require_relative 'path_list/gitignore_include_rule_builder'
12
+ require_relative 'path_list/path_regexp_builder'
13
+ require_relative 'path_list/gitignore_rule_scanner'
14
+ require_relative 'path_list/rule_group'
15
+ require_relative 'path_list/matchers/unmatchable'
16
+ require_relative 'path_list/matchers/shebang_regexp'
17
+ require_relative 'path_list/root_candidate'
18
+ require_relative 'path_list/relative_candidate'
19
+ require_relative 'path_list/matchers/within_dir'
20
+ require_relative 'path_list/matchers/allow_any_dir'
21
+ require_relative 'path_list/matchers/allow_path_regexp'
22
+ require_relative 'path_list/matchers/ignore_path_regexp'
23
+ require_relative 'path_list/patterns'
24
+ require_relative 'path_list/walkers/file_system'
25
+ require_relative 'path_list/walkers/gitignore_collecting_file_system'
26
+ require_relative 'path_list/gitignore_rule_group'
27
+
28
+ class PathList
29
+ class Error < StandardError; end
30
+
31
+ include ::Enumerable
32
+
33
+ # :nocov:
34
+ using ::PathList::Backports::DeletePrefixSuffix if defined?(::PathList::Backports::DeletePrefixSuffix)
35
+ using ::PathList::Backports::DirEachChild if defined?(::PathList::Backports::DirEachChild)
36
+ # :nocov:
37
+
38
+ def initialize(root: nil, gitignore: :auto, **rule_group_builder_args)
39
+ @root = "#{::File.expand_path(root.to_s, Dir.pwd)}/"
40
+ rule_groups = ::PathList::RuleGroups.new(root: @root, gitignore: gitignore, **rule_group_builder_args)
41
+
42
+ walker_class = gitignore ? ::PathList::Walkers::GitignoreCollectingFileSystem : ::PathList::Walkers::FileSystem
43
+ @walker = walker_class.new(rule_groups)
44
+ freeze
45
+ end
46
+
47
+ def allowed?(path, directory: nil, content: nil)
48
+ @walker.allowed?(path, directory: directory, content: content)
49
+ end
50
+ alias_method :===, :allowed?
51
+
52
+ def to_proc
53
+ method(:allowed?).to_proc
54
+ end
55
+
56
+ def each(root = ::Dir.pwd, &block)
57
+ return enum_for(:each, root) unless block_given?
58
+
59
+ root = "#{::File.expand_path(root.to_s, Dir.pwd)}/"
60
+ @walker.each(root, '', &block)
61
+ end
62
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ class PathList
4
+ module Backports
5
+ ruby_major, ruby_minor = ::RUBY_VERSION.split('.', 2)
6
+ unless ruby_major.to_i > 2 || ruby_major.to_i == 2 && ruby_minor.to_i > 5
7
+ module DirEachChild
8
+ refine ::Dir.singleton_class do
9
+ def children(path)
10
+ ::Dir.entries(path) - ['.', '..']
11
+ end
12
+ end
13
+ end
14
+
15
+ module DeletePrefixSuffix
16
+ refine ::String do
17
+ # delete_prefix!(prefix) -> self or nil
18
+ # Deletes leading prefix from str, returning nil if no change was made.
19
+ #
20
+ # "hello".delete_prefix!("hel") #=> "lo"
21
+ # "hello".delete_prefix!("llo") #=> nil
22
+ def delete_prefix!(str)
23
+ return unless start_with?(str)
24
+
25
+ slice!(0..(str.length - 1))
26
+ self
27
+ end
28
+
29
+ # delete_suffix!(suffix) -> self or nil
30
+ # Deletes trailing suffix from str, returning nil if no change was made.
31
+ #
32
+ # "hello".delete_suffix!("llo") #=> "he"
33
+ # "hello".delete_suffix!("hel") #=> nil
34
+ def delete_suffix!(str)
35
+ return unless end_with?(str)
36
+
37
+ slice!(-str.length..-1)
38
+ self
39
+ end
40
+
41
+ # delete_prefix(prefix) -> new_str click to toggle source
42
+ # Returns a copy of str with leading prefix deleted.
43
+ #
44
+ # "hello".delete_prefix("hel") #=> "lo"
45
+ # "hello".delete_prefix("llo") #=> "hello"
46
+ def delete_prefix(str)
47
+ s = dup
48
+ s.delete_prefix!(str)
49
+ s
50
+ end
51
+
52
+ # delete_suffix(suffix) -> new_str
53
+ # Returns a copy of str with trailing suffix deleted.
54
+ #
55
+ # "hello".delete_suffix("llo") #=> "he"
56
+ # "hello".delete_suffix("hel") #=> "hello"
57
+ def delete_suffix(str) # leftovers:allowed
58
+ s = dup
59
+ s.delete_suffix!(str)
60
+ s
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ class PathList
4
+ class GitignoreIncludeRuleBuilder < GitignoreRuleBuilder
5
+ # :nocov:
6
+ using ::PathList::Backports::DeletePrefixSuffix if defined?(::PathList::Backports::DeletePrefixSuffix)
7
+ # :nocov:
8
+
9
+ def initialize(rule, expand_path_with: nil)
10
+ super(rule)
11
+
12
+ @negation = true
13
+ @expand_path_from = expand_path_with
14
+ end
15
+
16
+ def expand_rule_path
17
+ anchored! unless @s.match?(/\*/)
18
+ return unless @s.match?(%r{(?:[~/]|\.{1,2}/|.*/\.\./)})
19
+
20
+ dir_only! if @s.match?(%r{.*/\s*\z})
21
+ @s.string.replace(::File.expand_path(@s.rest))
22
+ @s.string.delete_prefix!(@expand_path_from)
23
+ @s.pos = 0
24
+ end
25
+
26
+ def negated!
27
+ @negation = false
28
+ end
29
+
30
+ def unmatchable_rule!
31
+ throw :abort_build, ::PathList::Matchers::Unmatchable
32
+ end
33
+
34
+ def emit_end
35
+ if @dir_only
36
+ @child_re = @re.dup
37
+ @re.append_end_anchor
38
+ else
39
+ @re.append_dir_or_end_anchor
40
+ end
41
+
42
+ break!
43
+ end
44
+
45
+ def build_parent_dir_rules
46
+ return unless @negation
47
+
48
+ if @anchored
49
+ parent_pattern = @s.string.dup
50
+ if parent_pattern.sub!(%r{/[^/]+/?\s*\z}, '/')
51
+ ::PathList::GitignoreIncludeRuleBuilder.new(parent_pattern).build_as_parent
52
+ end
53
+ else
54
+ [::PathList::Matchers::AllowAnyDir]
55
+ end
56
+ end
57
+
58
+ def build_child_file_rule # rubocop:disable Metrics/MethodLength
59
+ if @child_re.end_with?('/')
60
+ @child_re.append_many_non_dir.append_dir if @dir_only
61
+ else
62
+ @child_re.append_dir
63
+ end
64
+
65
+ @child_re.prepend(prefix)
66
+
67
+ if @negation
68
+ ::PathList::Matchers::AllowPathRegexp.new(@child_re.to_regexp, @anchored, false)
69
+ else
70
+ ::PathList::Matchers::IgnorePathRegexp.new(@child_re.to_regexp, @anchored, false)
71
+ end
72
+ end
73
+
74
+ def build_as_parent
75
+ anchored!
76
+ dir_only!
77
+
78
+ catch :abort_build do
79
+ process_rule
80
+ build_rule(child_file_rule: false)
81
+ end
82
+ end
83
+
84
+ def build_rule(child_file_rule: true)
85
+ @child_re ||= @re.dup # in case emit_end wasn't called
86
+
87
+ [super(), *build_parent_dir_rules, (build_child_file_rule if child_file_rule)].compact
88
+ end
89
+
90
+ def process_rule
91
+ expand_rule_path if @expand_path_from
92
+ super
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,192 @@
1
+ # frozen_string_literal: true
2
+
3
+ class PathList
4
+ class GitignoreRuleBuilder # rubocop:disable Metrics/ClassLength
5
+ def initialize(rule)
6
+ @re = ::PathList::PathRegexpBuilder.new
7
+ @s = ::PathList::GitignoreRuleScanner.new(rule)
8
+
9
+ @negation = false
10
+ @anchored = false
11
+ @dir_only = false
12
+ end
13
+
14
+ def break!
15
+ throw :break
16
+ end
17
+
18
+ def blank!
19
+ throw :abort_build, []
20
+ end
21
+
22
+ def unmatchable_rule!
23
+ throw :abort_build, []
24
+ end
25
+
26
+ def negated!
27
+ @negation = true
28
+ end
29
+
30
+ def anchored!
31
+ @anchored ||= true
32
+ end
33
+
34
+ def never_anchored!
35
+ @anchored = :never
36
+ end
37
+
38
+ def dir_only!
39
+ @dir_only = true
40
+ end
41
+
42
+ def nothing_emitted?
43
+ @re.empty?
44
+ end
45
+
46
+ def emit_dir
47
+ anchored!
48
+ @re.append_dir
49
+ end
50
+
51
+ def emit_any_dir
52
+ anchored!
53
+ @re.append_any_dir
54
+ end
55
+
56
+ def emit_end
57
+ @re.append_end_anchor
58
+ break!
59
+ end
60
+
61
+ def process_backslash
62
+ return unless @s.backslash?
63
+
64
+ @re.append_escaped(@s.next_character) || unmatchable_rule!
65
+ end
66
+
67
+ def process_star_end_after_slash # rubocop:disable Metrics/MethodLength
68
+ if @s.star_end?
69
+ @re.append_many_non_dir
70
+ emit_end
71
+ elsif @s.two_star_end?
72
+ break!
73
+ elsif @s.star_slash_end?
74
+ @re.append_many_non_dir
75
+ dir_only!
76
+ emit_end
77
+ elsif @s.two_star_slash_end?
78
+ dir_only!
79
+ break!
80
+ else
81
+ true
82
+ end
83
+ end
84
+
85
+ def process_slash
86
+ return unless @s.slash?
87
+ return dir_only! if @s.end?
88
+ return unmatchable_rule! if @s.slash?
89
+
90
+ emit_dir
91
+ process_star_end_after_slash
92
+ end
93
+
94
+ def process_two_stars # rubocop:disable Metrics/MethodLength
95
+ return unless @s.two_stars?
96
+ return break! if @s.end?
97
+
98
+ if @s.slash?
99
+ if @s.end?
100
+ @re.append_any_non_dir
101
+ dir_only!
102
+ elsif @s.slash?
103
+ unmatchable_rule!
104
+ else
105
+ if nothing_emitted?
106
+ never_anchored!
107
+ else
108
+ emit_any_dir
109
+ end
110
+ process_star_end_after_slash
111
+ end
112
+ else
113
+ @re.append_any_non_dir
114
+ end
115
+ end
116
+
117
+ def process_character_class # rubocop:disable Metrics/MethodLength
118
+ return unless @s.character_class_start?
119
+
120
+ @re.append_character_class_open
121
+ @re.append_character_class_negation if @s.character_class_negation?
122
+ unmatchable_rule! if @s.character_class_end?
123
+
124
+ until @s.character_class_end?
125
+ next if process_backslash
126
+ next @re.append_character_class_dash if @s.dash?
127
+ next if @re.append_escaped(@s.character_class_literal)
128
+
129
+ unmatchable_rule!
130
+ end
131
+
132
+ @re.append_character_class_close
133
+ end
134
+
135
+ def process_end
136
+ blank! if nothing_emitted?
137
+
138
+ emit_end
139
+ end
140
+
141
+ def process_rule # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
142
+ anchored! if @s.slash?
143
+
144
+ catch :break do
145
+ loop do
146
+ next if process_backslash
147
+ next if process_slash
148
+ next if process_two_stars
149
+ next @re.append_any_non_dir if @s.star?
150
+ next @re.append_one_non_dir if @s.question_mark?
151
+ next if process_character_class
152
+ next if @re.append_escaped(@s.literal)
153
+ next if @re.append_escaped(@s.significant_whitespace)
154
+
155
+ process_end
156
+ end
157
+ end
158
+ end
159
+
160
+ def prefix
161
+ out = ::PathList::PathRegexpBuilder.new
162
+
163
+ if @anchored
164
+ out.append_start_anchor
165
+ else
166
+ out.append_dir_or_start_anchor
167
+ end
168
+ out
169
+ end
170
+
171
+ def build_rule
172
+ @re.prepend(prefix)
173
+ if @negation
174
+ ::PathList::Matchers::AllowPathRegexp.new(@re.to_regexp, @anchored, @dir_only)
175
+ else
176
+ ::PathList::Matchers::IgnorePathRegexp.new(@re.to_regexp, @anchored, @dir_only)
177
+ end
178
+ end
179
+
180
+ def build
181
+ catch :abort_build do
182
+ blank! if @s.hash?
183
+ negated! if @s.exclamation_mark?
184
+ process_rule
185
+
186
+ @anchored = false if @anchored == :never
187
+
188
+ build_rule
189
+ end
190
+ end
191
+ end
192
+ end
@@ -0,0 +1,31 @@
1
+ # frozen-string-literal: true
2
+
3
+ require 'set'
4
+
5
+ class PathList
6
+ class GitignoreRuleGroup < ::PathList::RuleGroup
7
+ def initialize(root)
8
+ @root = root
9
+ @loaded_paths = Set[root]
10
+
11
+ super([
12
+ ::PathList::Patterns.new('.git', root: '/'),
13
+ ::PathList::Patterns.new(from_file: ::PathList::GlobalGitignore.path(root: root), root: root),
14
+ ::PathList::Patterns.new(from_file: "#{root}.git/info/exclude", root: root),
15
+ ::PathList::Patterns.new(from_file: "#{root}.gitignore", root: root)
16
+ ], false)
17
+ end
18
+
19
+ def add_gitignore(dir)
20
+ return if @loaded_paths.include?(dir)
21
+
22
+ @loaded_paths << dir
23
+ matcher = ::PathList::Patterns.new(from_file: "#{dir}.gitignore").build_matchers(include: false)
24
+ @matchers += matcher unless !matcher || matcher.empty?
25
+ end
26
+
27
+ def add_gitignore_to_root(path)
28
+ add_gitignore(path) until @loaded_paths.include?(path = "#{::File.dirname(path)}/")
29
+ end
30
+ end
31
+ end