Skip to content

Commit

Permalink
Cache matchers from pattern args
Browse files Browse the repository at this point in the history
  • Loading branch information
robotdana committed Nov 25, 2023
1 parent f246cc5 commit b68140b
Show file tree
Hide file tree
Showing 15 changed files with 376 additions and 79 deletions.
2 changes: 0 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,6 @@ PathList.gitignore.match?("is/this")
PathList.gitignore.match?("is/this/hypothetical/directory", directory: true)
```

**Note: If you want use the same PathList match rules more than once, save the pathlist to a variable to avoid having to read and parse the patterns over and over again**

See the [full PathList documentation](docs/PathList).

## Limitations
Expand Down
6 changes: 4 additions & 2 deletions lib/path_list.rb
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,9 @@ def ignore(*patterns, patterns_from_file: nil, format: :gitignore, root: nil)
# @see .ignore
# @see #ignore
def ignore!(*patterns, patterns_from_file: nil, format: :gitignore, root: nil)
and_matcher(PatternParser.build(patterns, patterns_from_file: patterns_from_file, format: format, root: root))
and_matcher(PatternParser.build(
patterns: patterns, patterns_from_file: patterns_from_file, format: format, root: root, polarity: :ignore
))
end

# @!group Only methods
Expand Down Expand Up @@ -335,7 +337,7 @@ def only(*patterns, patterns_from_file: nil, format: :gitignore, root: nil)
def only!(*patterns, patterns_from_file: nil, format: :gitignore, root: nil)
and_matcher(
PatternParser.build(
patterns, patterns_from_file: patterns_from_file, format: format, root: root, polarity: :allow
patterns: patterns, patterns_from_file: patterns_from_file, format: format, root: root, polarity: :allow
)
)
end
Expand Down
51 changes: 51 additions & 0 deletions lib/path_list/cache.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# frozen_string_literal: true

class PathList
# @api private
class Cache
Key = Struct.new(
:patterns,
:patterns_from_file,
:gitignore_global,
:root,
:pwd,
:polarity,
:parser,
:default,
keyword_init: true
)

# @api private
class Key
def initialize( # rubocop:disable Metrics/ParameterLists
patterns: nil,
patterns_from_file: nil,
gitignore_global: nil,
root: nil,
pwd: Dir.pwd,
polarity: :ignore,
parser: PatternParser::Gitignore,
default: nil
)
super
freeze
end

freeze
end

@cache = {}
class << self
# @yield
# @return [Object] Whatever the block returns
def cache(**args)
@cache[Key.new(**args)] ||= yield
end

# @return [void]
def clear
@cache.clear
end
end
end
end
101 changes: 55 additions & 46 deletions lib/path_list/gitignore.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,65 +2,74 @@

class PathList
# @api private
module Gitignore
class << self
# @param root [String, #to_s, nil] the root, when nil will find the $GIT_DIR like git does
# @param config [Boolean] whether to load the configured core.excludesFile
# @return [PathList::Matcher]
def build(root:, config: true)
root = if root
CanonicalPath.full_path(root)
else
find_root
end
class Gitignore
# @param root [String, #to_s, nil] the root, when nil will find the $GIT_DIR like git does
# @param config [Boolean] whether to load the configured core.excludesFile
# @return [PathList::Matcher]
def self.build(root:, config:)
Cache.cache(root: root, gitignore_global: config) do
new(root: root, config: config).matcher
end
end

# @param (see .build)
def initialize(root:, config:)
@root = if root
CanonicalPath.full_path(root)
else
find_root
end
@config = config
end

collector = build_collector(root)
# @return [Matcher]
def matcher
collector = build_collector(@root)

append(collector, root, Gitconfig::CoreExcludesfile.path(repo_root: root)) if config
append(collector, root, '.git/info/exclude')
append(collector, root, '.gitignore')
append(collector, @root, Gitconfig::CoreExcludesfile.path(repo_root: @root)) if @config
append(collector, @root, '.git/info/exclude')
append(collector, @root, '.gitignore')

Matcher::LastMatch.build([collector, build_dot_git_matcher])
end
Matcher::LastMatch.build([collector, build_dot_git_matcher])
end

private
private

def find_root
home = ::Dir.home
dir = pwd = ::Dir.pwd
def find_root
home = ::Dir.home
dir = pwd = ::Dir.pwd

loop do
return dir if ::File.exist?("#{dir}/.git")
return pwd if dir.casecmp(home).zero? || dir.end_with?('/')
loop do
return dir if ::File.exist?("#{dir}/.git")
return pwd if dir.casecmp(home).zero? || dir.end_with?('/')

dir = ::File.dirname(dir)
end
dir = ::File.dirname(dir)
end
end

def append(collector, root, path)
return unless path
def append(collector, root, path)
return unless path

collector.append(CanonicalPath.full_path_from(path, root), root: root)
end
collector.append(CanonicalPath.full_path_from(path, root), root: root)
end

def build_dot_git_matcher
Matcher::MatchIfDir.new(
Matcher::PathRegexp.build([[:dir, '.git', :end_anchor]], :ignore)
)
end
def build_dot_git_matcher
Matcher::MatchIfDir.new(
Matcher::PathRegexp.build([[:dir, '.git', :end_anchor]], :ignore)
)
end

def build_collector(root)
root_re = TokenRegexp::Path.new_from_path(root)
root_re_children = root_re.dup
root_re_children.replace_end :dir
def build_collector(root)
root_re = TokenRegexp::Path.new_from_path(root)
root_re_children = root_re.dup
root_re_children.replace_end :dir

Matcher::CollectGitignore.build(
Matcher::MatchIfDir.new(
Matcher::PathRegexp.build([root_re_children.parts, root_re.parts], :allow)
),
Matcher::Allow
)
end
Matcher::CollectGitignore.build(
Matcher::MatchIfDir.new(
Matcher::PathRegexp.build([root_re_children.parts, root_re.parts], :allow)
),
Matcher::Allow
)
end
end
end
10 changes: 7 additions & 3 deletions lib/path_list/matcher/collect_gitignore.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,13 @@ def append(file, root:)

@loaded << file

patterns = PatternParser.new(patterns_from_file: file, root: root, parser: PatternParser::Gitignore)
new_matcher = patterns.build_ignore_matcher(Blank)

new_matcher = PatternParser.build!(
patterns_from_file: file,
root: root,
parser: PatternParser::Gitignore,
default: Blank,
polarity: :ignore
)
return if new_matcher == Blank

@dir_matcher.matcher = LastMatch.build([@dir_matcher.matcher, new_matcher.dir_matcher])
Expand Down
75 changes: 53 additions & 22 deletions lib/path_list/pattern_parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,27 +21,57 @@ class << self
# @param root [String, nil]
# @param polarity [:allow, :ignore]
# @return [PathList::Matcher]
def build(patterns, patterns_from_file: nil, format: nil, root: nil, polarity: :ignore)
if (patterns && !patterns.empty?) && patterns_from_file
raise Error, 'use only one of `*patterns` or `patterns_from_file:`'
end

def build(patterns:, patterns_from_file:, format:, root:, polarity:)
parser = PARSERS.fetch(format || :gitignore, nil)
raise Error, "`format:` must be one of #{PARSERS.keys.map(&:inspect).join(', ')}" unless parser

root = CanonicalPath.full_path(root) if root

if patterns_from_file
patterns_from_file = CanonicalPath.full_path(patterns_from_file)
root ||= ::File.dirname(patterns_from_file)
else
patterns = patterns.flatten.flat_map { |string| string.to_s.lines }
Cache.cache(
patterns: patterns,
patterns_from_file: patterns_from_file,
parser: parser,
root: root,
polarity: polarity
) do
raise Error, 'use only one of `*patterns` or `patterns_from_file:`' if !patterns.empty? && patterns_from_file
raise Error, "`format:` must be one of #{PARSERS.keys.map(&:inspect).join(', ')}" unless parser

root = CanonicalPath.full_path(root) if root

if patterns_from_file
patterns_from_file = CanonicalPath.full_path(patterns_from_file)
root ||= ::File.dirname(patterns_from_file)
else
patterns = patterns.flatten.flat_map { |string| string.to_s.lines }
end

root ||= CanonicalPath.full_path(root)

new(patterns: patterns, patterns_from_file: patterns_from_file, parser: parser, root: root,
polarity: polarity, default: nil).matcher
end
end

root ||= CanonicalPath.full_path(root)

new(patterns: patterns, patterns_from_file: patterns_from_file, parser: parser, root: root,
polarity: polarity).matcher
# @api private
# like build but without the error checking and pre-processing for when we already know it's fine
# root must be an absolute path
# patterns_from_file must be an absolute path if given
def build!(parser:, root:, polarity:, default:, patterns: nil, patterns_from_file: nil)
Cache.cache(
patterns: patterns,
patterns_from_file: patterns_from_file,
parser: parser,
pwd: nil,
root: root,
polarity: polarity,
default: default
) do
new(
patterns: patterns,
patterns_from_file: patterns_from_file,
parser: parser,
root: root,
polarity: polarity,
default: default
).matcher
end
end
end

Expand All @@ -50,12 +80,13 @@ def build(patterns, patterns_from_file: nil, format: nil, root: nil, polarity: :
# @param parser [Class<GlobGitignore>, Class<Gitignore>, Class<Shebang>, Class<ExactPath>]
# @param root [String]
# @param polarity [:allow, :ignore]
def initialize(parser:, root:, patterns: nil, patterns_from_file: nil, polarity: :ignore)
def initialize(parser:, root:, patterns:, patterns_from_file:, polarity:, default:)
@patterns = patterns
@patterns_from_file = patterns_from_file
@parser = parser
@root = root
@polarity = polarity
@default = default
end

# return [PathList::Matcher]
Expand All @@ -67,6 +98,8 @@ def matcher
end
end

private

# return [PathList::Matcher]
def build_only_matcher
pattern_parsers = read_patterns.map { |pattern| @parser.new(pattern, @polarity, @root) }
Expand All @@ -81,14 +114,12 @@ def build_only_matcher

# @param default [PathList::Matcher] what to insert as the default
# return [PathList::Matcher]
def build_ignore_matcher(default = Matcher::Allow)
def build_ignore_matcher(default = @default || Matcher::Allow)
matchers = read_patterns.map { |pattern| @parser.new(pattern, @polarity, @root).matcher }
matchers.unshift(default)
Matcher::LastMatch.build(matchers)
end

private

def read_patterns
if @patterns_from_file
::File.exist?(@patterns_from_file) ? ::File.readlines(@patterns_from_file) : []
Expand Down
4 changes: 4 additions & 0 deletions spec/gitconfig/core_excludesfile_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@
let(:config_content) { "[core]\n\texcludesfile = #{excludesfile_value}\n" }
let(:excludesfile_value) { '~/.global_gitignore' }

before do
stub_blank_global_config
end

context 'with no core.excludesfile defined' do
it 'returns the default path' do
expect(subject).to eq default_ignore_path
Expand Down
Loading

0 comments on commit b68140b

Please sign in to comment.