fast_ignore 0.12.1 → 0.15.2

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.
@@ -0,0 +1,50 @@
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
+ private
15
+
16
+ def gitconfig_gitignore_path(config_path)
17
+ return unless config_path
18
+ return unless ::File.exist?(config_path)
19
+
20
+ ignore_path = ::File.readlines(config_path).find { |l| l.sub!(/\A\s*excludesfile\s*=/, '') }
21
+ return unless ignore_path
22
+
23
+ ignore_path.strip!
24
+ return ignore_path if ignore_path.empty? # don't expand path in this case
25
+
26
+ ::File.expand_path(ignore_path)
27
+ end
28
+
29
+ def xdg_config_path
30
+ xdg_config_home? && ::File.expand_path('git/config', xdg_config_home)
31
+ end
32
+
33
+ def default_global_gitignore_path
34
+ if xdg_config_home?
35
+ ::File.expand_path('git/ignore', xdg_config_home)
36
+ else
37
+ ::File.expand_path('~/.config/git/ignore')
38
+ end
39
+ end
40
+
41
+ def xdg_config_home
42
+ ::ENV['XDG_CONFIG_HOME']
43
+ end
44
+
45
+ def xdg_config_home?
46
+ xdg_config_home && (not xdg_config_home.empty?)
47
+ end
48
+ end
49
+ end
50
+ end
@@ -6,24 +6,40 @@ class FastIgnore
6
6
  alias_method :negation?, :negation
7
7
  undef :negation
8
8
 
9
+ attr_reader :component_rules
10
+ attr_reader :component_rules_count
11
+
9
12
  attr_reader :dir_only
10
13
  alias_method :dir_only?, :dir_only
11
14
  undef :dir_only
12
15
 
13
- attr_reader :unanchored
14
- alias_method :unanchored?, :unanchored
15
- undef :unanchored
16
-
17
- attr_reader :type
16
+ attr_reader :squashable_type
18
17
  attr_reader :rule
19
18
 
20
- def initialize(rule, negation, unanchored = nil, dir_only = nil)
21
- @rule = rule.is_a?(Regexp) ? rule : ::FastIgnore::FNMatchToRegex.call(rule)
22
- @unanchored = unanchored
19
+ def squash(rules)
20
+ # component rules is to improve the performance of repos with many .gitignore files. e.g. linux.
21
+ component_rules = rules.flat_map(&:component_rules)
22
+ ::FastIgnore::Rule.new(
23
+ ::Regexp.union(component_rules.map(&:rule)).freeze,
24
+ @negation, @anchored, @dir_only, component_rules
25
+ )
26
+ end
27
+
28
+ def initialize(rule, negation, anchored, dir_only, component_rules = self) # rubocop:disable Metrics/MethodLength
29
+ @rule = rule
30
+ @anchored = anchored
23
31
  @dir_only = dir_only
24
32
  @negation = negation
33
+ @component_rules = component_rules
34
+ @component_rules_count = component_rules == self ? 1 : component_rules.length
25
35
 
26
- @type = negation ? 1 : 0
36
+ @squashable_type = if anchored && negation
37
+ 1
38
+ elsif anchored
39
+ 0
40
+ else
41
+ ::Float::NAN # because it doesn't equal itself
42
+ end
27
43
 
28
44
  freeze
29
45
  end
@@ -32,17 +48,17 @@ class FastIgnore
32
48
  false
33
49
  end
34
50
 
35
- def shebang
36
- nil
51
+ def shebang?
52
+ false
37
53
  end
38
54
 
39
55
  # :nocov:
40
56
  def inspect
41
- "#<Rule #{'!' if @negation}#{@rule}#{'/' if @dir_only}>"
57
+ "#<Rule #{'!' if @negation}#{'/' if @anchored}#{@rule}#{'/' if @dir_only}>"
42
58
  end
43
59
  # :nocov:
44
60
 
45
- def match?(relative_path, _, _, _)
61
+ def match?(relative_path, _full_path, _filename, _content)
46
62
  @rule.match?(relative_path)
47
63
  end
48
64
  end
@@ -7,96 +7,34 @@ class FastIgnore
7
7
  using ::FastIgnore::Backports::DeletePrefixSuffix if defined?(::FastIgnore::Backports::DeletePrefixSuffix)
8
8
  # :nocov:
9
9
 
10
- def build(rule, allow, expand_path, file_root)
11
- return shebang_rules(rule, allow) if remove_shebang(rule)
12
-
13
- strip(rule)
14
- return [] if skip?(rule)
15
-
16
- gitignore_rules(rule, allow, expand_path, file_root)
10
+ def build(rule, allow, expand_path_with, file_root)
11
+ if rule.delete_prefix!('#!:')
12
+ shebang_rules(rule, allow, file_root)
13
+ else
14
+ gitignore_rules(rule, allow, file_root, expand_path_with)
15
+ end
17
16
  end
18
17
 
19
18
  private
20
19
 
21
- def strip(rule)
22
- rule.chomp!
23
- rule.rstrip! unless rule.end_with?('\\ ')
24
- end
25
-
26
- def remove_shebang(rule)
27
- return unless rule.delete_prefix!('#!:')
28
-
29
- rule.strip!
30
-
31
- true
32
- end
33
-
34
- def shebang_rules(rule, allow)
35
- rules = [::FastIgnore::ShebangRule.new(/\A#!.*\b#{Regexp.escape(rule)}\b/.freeze, allow)]
36
- return rules unless allow
20
+ def shebang_rules(shebang, allow, file_root)
21
+ shebang.strip!
22
+ pattern = /\A#!.*\b#{::Regexp.escape(shebang)}\b/i
23
+ rule = ::FastIgnore::ShebangRule.new(pattern, allow, file_root&.shebang_path_pattern)
24
+ return rule unless allow
37
25
 
38
- rules << ::FastIgnore::Rule.new('**/*', true, true, true)
26
+ rules = gitignore_rules('*/'.dup, allow, file_root)
27
+ rules.pop # don't want the include all children one.
28
+ rules << rule
39
29
  rules
40
30
  end
41
31
 
42
- def skip?(rule)
43
- rule.empty? || rule.start_with?('#')
44
- end
45
-
46
- def gitignore_rules(rule, allow, expand_path, file_root)
47
- dir_only = extract_dir_only(rule)
48
- negation = extract_negation(rule, allow)
49
-
50
- unanchored = if expand_path
51
- expand_rule_path(rule, expand_path)
32
+ def gitignore_rules(rule, allow, file_root, expand_path_with = nil)
33
+ if allow
34
+ ::FastIgnore::GitignoreIncludeRuleBuilder.new(rule, file_root, expand_path_with).build
52
35
  else
53
- unanchored?(rule)
54
- end
55
- rule.delete_prefix!('/')
56
-
57
- rule.prepend("#{file_root}#{'**/' if unanchored}") if file_root || unanchored
58
-
59
- build_gitignore_rules(rule, unanchored, allow, dir_only, negation)
60
- end
61
-
62
- def extract_dir_only(rule)
63
- rule.delete_suffix!('/')
64
- end
65
-
66
- def extract_negation(rule, allow)
67
- return allow unless rule.delete_prefix!('!')
68
-
69
- not allow
70
- end
71
-
72
- EXPAND_PATH_RE = %r{^(?:[~/]|\.{1,2}/)}.freeze
73
- def expand_rule_path(rule, root)
74
- rule.replace(::File.expand_path(rule)) if rule.match?(EXPAND_PATH_RE)
75
- 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)
36
+ ::FastIgnore::GitignoreRuleBuilder.new(rule, file_root).build
97
37
  end
98
-
99
- ancestor_rules
100
38
  end
101
39
  end
102
40
  end
@@ -6,11 +6,10 @@ class FastIgnore
6
6
  alias_method :gitignore?, :gitignore
7
7
  undef :gitignore
8
8
 
9
- def initialize(rules, allow, gitignore)
10
- @dir_rules = squash_rules(rules.reject(&:file_only?)).freeze
11
- @file_rules = squash_rules(rules.reject(&:dir_only?)).freeze
12
- @any_not_anchored = rules.any?(&:unanchored?)
13
- @has_shebang_rules = rules.any?(&:shebang)
9
+ def initialize(rules, allow, gitignore, squash = true)
10
+ @dir_rules = (squash ? squash_rules(rules.reject(&:file_only?)) : rules.reject(&:file_only?)).freeze
11
+ @file_rules = (squash ? squash_rules(rules.reject(&:dir_only?)) : rules.reject(&:dir_only?)).freeze
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 += other.dir_rules
28
- @file_rules += other.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,17 +39,24 @@ class FastIgnore
41
39
  return rule.negation? if rule.match?(relative_path, full_path, filename, content)
42
40
  end
43
41
 
44
- (not @allow) || (dir && @any_not_anchored)
42
+ not @allow
45
43
  end
46
44
 
47
- def squash_rules(rules)
48
- out = rules.chunk_while { |a, b| a.type == b.type }.map do |chunk|
49
- next chunk.first if chunk.length == 1
45
+ def squash_rules(rules) # rubocop:disable Metrics/MethodLength
46
+ running_component_rule_size = rules.first&.component_rules_count || 0
47
+ rules.chunk_while do |a, b|
48
+ # a.squashable_type == b.squashable_type
49
+ next true if a.squashable_type == b.squashable_type &&
50
+ (running_component_rule_size + b.component_rules_count <= 40)
50
51
 
51
- chunk.first.class.new(Regexp.union(chunk.map(&:rule)), chunk.first.negation?)
52
- end
52
+ running_component_rule_size = b.component_rules_count
53
+ false
54
+ end.map do |chunk| # rubocop:disable Style/MultilineBlockChain
55
+ first = chunk.first
56
+ next first if chunk.length == 1
53
57
 
54
- out
58
+ first.squash(chunk)
59
+ end
55
60
  end
56
61
 
57
62
  def weight
@@ -64,6 +69,8 @@ class FastIgnore
64
69
 
65
70
  protected
66
71
 
67
- attr_reader :dir_rules, :file_rules, :any_not_anchored, :has_shebang_rules
72
+ attr_reader :dir_rules
73
+ attr_reader :file_rules
74
+ attr_reader :has_shebang_rules
68
75
  end
69
76
  end
@@ -0,0 +1,113 @@
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) # rubocop:disable Metrics/MethodLength
39
+ if @gitignore_rule_set
40
+ new_gitignore = build_set_from_file(relative_path, gitignore: true, check_exists: check_exists, squash: false)
41
+ return if !new_gitignore || new_gitignore.empty?
42
+
43
+ @gitignore_rule_set << new_gitignore
44
+ else
45
+ new_gitignore = build_set_from_file(relative_path, gitignore: true, check_exists: check_exists)
46
+ return if !new_gitignore || new_gitignore.empty?
47
+
48
+ @array << new_gitignore
49
+ @gitignore_rule_set = new_gitignore
50
+ @array.sort_by!(&:weight) && @array.freeze
51
+ end
52
+ new_gitignore
53
+ end
54
+
55
+ private
56
+
57
+ def append_and_return_if_present(value)
58
+ return unless value && !value.empty?
59
+
60
+ @array << value
61
+ value
62
+ end
63
+
64
+ def append_root_gitignore(gitignore)
65
+ return @gitignore_rule_set = nil unless gitignore
66
+
67
+ append_set_from_array('.git')
68
+ gi = ::FastIgnore::RuleSet.new([], false, true)
69
+ gi << build_from_root_gitignore_file(::FastIgnore::GlobalGitignore.path(root: @project_root))
70
+ gi << build_from_root_gitignore_file("#{@project_root}.git/info/exclude")
71
+ gi << build_from_root_gitignore_file("#{@project_root}.gitignore")
72
+ @gitignore_rule_set = append_and_return_if_present(gi)
73
+ end
74
+
75
+ def build_from_root_gitignore_file(path)
76
+ return unless ::File.exist?(path)
77
+
78
+ build_rule_set(::File.readlines(path), false, gitignore: true)
79
+ end
80
+
81
+ def build_rule_set(rules, allow, expand_path_with: nil, file_root: nil, gitignore: false, squash: true) # rubocop:disable Metrics/ParameterLists
82
+ rules = rules.flat_map do |rule|
83
+ ::FastIgnore::RuleBuilder.build(rule, allow, expand_path_with, file_root)
84
+ end
85
+
86
+ ::FastIgnore::RuleSet.new(rules, allow, gitignore, squash)
87
+ end
88
+
89
+ def build_set_from_file(filename, allow: false, gitignore: false, check_exists: false, squash: true)
90
+ filename = ::File.expand_path(filename, @project_root)
91
+ return if check_exists && !::File.exist?(filename)
92
+ raise ::FastIgnore::Error, "#{filename} is not within #{@project_root}" unless filename.start_with?(@project_root)
93
+
94
+ file_root = ::FastIgnore::FileRoot.build(filename, @project_root)
95
+ build_rule_set(::File.readlines(filename), allow, file_root: file_root, gitignore: gitignore, squash: squash)
96
+ end
97
+
98
+ def append_sets_from_files(files, allow: false)
99
+ Array(files).each do |file|
100
+ append_and_return_if_present(build_set_from_file(file, allow: allow))
101
+ end
102
+ end
103
+
104
+ def append_set_from_array(rules, allow: false, expand_path_with: nil)
105
+ return unless rules
106
+
107
+ rules = Array(rules).flat_map { |string| string.to_s.lines }
108
+ return if rules.empty?
109
+
110
+ append_and_return_if_present(build_rule_set(rules, allow, expand_path_with: expand_path_with))
111
+ end
112
+ end
113
+ end
@@ -7,16 +7,25 @@ class FastIgnore
7
7
  undef :negation
8
8
 
9
9
  attr_reader :rule
10
- alias_method :shebang, :rule
11
10
 
12
- attr_reader :type
11
+ attr_reader :file_path_pattern
13
12
 
14
- def initialize(rule, negation)
13
+ attr_reader :squashable_type
14
+
15
+ def squash(rules)
16
+ ::FastIgnore::ShebangRule.new(::Regexp.union(rules.map(&:rule)).freeze, negation?, file_path_pattern)
17
+ end
18
+
19
+ def component_rules_count
20
+ 1
21
+ end
22
+
23
+ def initialize(rule, negation, file_path_pattern)
15
24
  @rule = rule
16
25
  @negation = negation
26
+ @file_path_pattern = file_path_pattern
17
27
 
18
- @type = 2
19
- @type += 1 if negation
28
+ @squashable_type = (negation ? 13 : 12) + file_path_pattern.object_id
20
29
 
21
30
  freeze
22
31
  end
@@ -29,28 +38,39 @@ class FastIgnore
29
38
  false
30
39
  end
31
40
 
32
- def unanchored?
33
- true
34
- end
35
-
36
41
  # :nocov:
37
42
  def inspect
38
- "#<ShebangRule #{'allow ' if @negation}#!:#{@rule.to_s[15..-4]}>"
43
+ allow_fragment = 'allow ' if @negation
44
+ in_fragment = " in #{@file_path_pattern}" if @file_path_pattern
45
+ "#<ShebangRule #{allow_fragment}#!:#{@rule.to_s[15..-4]}#{in_fragment}>"
39
46
  end
40
47
  # :nocov:
41
48
 
42
- def match?(_, full_path, filename, content)
49
+ def match?(relative_path, full_path, filename, content)
43
50
  return false if filename.include?('.')
51
+ return false unless (not @file_path_pattern) || @file_path_pattern.match?(relative_path)
44
52
 
45
53
  (content || first_line(full_path))&.match?(@rule)
46
54
  end
47
55
 
56
+ def shebang?
57
+ true
58
+ end
59
+
48
60
  private
49
61
 
50
- def first_line(path)
62
+ def first_line(path) # rubocop:disable Metrics/MethodLength
51
63
  file = ::File.new(path)
52
- first_line = file.sysread(25)
53
- first_line += file.sysread(50) until first_line.include?("\n")
64
+ first_line = new_fragment = file.sysread(64)
65
+ if first_line.start_with?('#!')
66
+ until new_fragment.include?("\n")
67
+ new_fragment = file.sysread(64)
68
+ first_line += new_fragment
69
+ end
70
+ else
71
+ file.close
72
+ return
73
+ end
54
74
  file.close
55
75
  first_line
56
76
  rescue ::EOFError, ::SystemCallError