Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
GlobGitignore doesn't preprocess patterns
Browse files Browse the repository at this point in the history
robotdana committed Nov 23, 2023

Verified

This commit was signed with the committer’s verified signature.
robotdana Dana Sherson
1 parent c22b18d commit e1c9c61
Showing 11 changed files with 588 additions and 99 deletions.
2 changes: 2 additions & 0 deletions .spellr_wordlists/english.txt
Original file line number Diff line number Diff line change
@@ -136,13 +136,15 @@ torvolds
tsx
ttributes
txt
umc
unanchorable
unc
unexpandable
unfuck
unnegated
unrecursive
unstaged
unstub
untr
upcase
urrent
2 changes: 1 addition & 1 deletion lib/path_list/autoloader.rb
Original file line number Diff line number Diff line change
@@ -19,7 +19,7 @@ def autoload(klass)
def class_from_path(path)
name = ::File.basename(path).delete_suffix('.rb')

if name == 'version' || name == 'expandable_path'
if name == 'version' || name == 'expandable_path' || name == 'scanner'
name.upcase
else
name.gsub(/(?:^|_)(\w)/, &:upcase).delete('_')
59 changes: 34 additions & 25 deletions lib/path_list/pattern_parser/gitignore.rb
Original file line number Diff line number Diff line change
@@ -15,12 +15,14 @@ class PatternParser
class Gitignore
Autoloader.autoload(self)

SCANNER = RuleScanner

# @api private
# @param pattern [String]
# @param polarity [:ignore, :allow]
# @param root [String]
def initialize(pattern, polarity, root)
@s = RuleScanner.new(pattern)
@s = self.class::SCANNER.new(pattern)
@default_polarity = polarity
@rule_polarity = polarity
@root = root
@@ -60,6 +62,7 @@ def prepare_regexp_builder
end

@start_any_dir_position = @re.length - 1
@re.delete_at(@start_any_dir_position) if @root && @anchored
end

def break!
@@ -94,7 +97,7 @@ def dir_only?

def anchored!
@anchored ||= begin
@re.delete_at(@start_any_dir_position)
@re.delete_at(@start_any_dir_position) if defined?(@re)
true
end
end
@@ -121,8 +124,8 @@ def emit_end
break!
end

def process_backslash
return unless @s.backslash?
def process_escape
return unless @s.escape?

if @re.append_string(@s.next_character)
emitted!
@@ -142,7 +145,7 @@ def process_character_class

until @s.character_class_end?
next if process_character_class_range
next if process_backslash
next if process_escape
next if append_string(@s.character_class_literal)

unmatchable_rule!
@@ -158,11 +161,9 @@ def process_character_class_range
start = @s.character_class_range_start
return unless start

start = start.delete_prefix('\\')

append_string(start)

finish = @s.character_class_range_end.delete_prefix('\\')
finish = @s.character_class_range_end

return true unless start < finish

@@ -184,31 +185,39 @@ def process_rule
catch :abort_build do
blank! if @s.hash?
negated! if @s.exclamation_mark?
prepare_regexp_builder
anchored! if !@anchored && @s.slash?
process_first_characters

catch :break do
loop do
next if process_backslash
next unmatchable_rule! if @s.star_star_slash_slash?
next append_part(:any) && dir_only! if @s.star_star_slash_end?
next append_part(:any_dir) && anchored! if @s.star_star_slash?
next unmatchable_rule! if @s.slash_slash?
next append_part(:dir) && append_part(:any) && anchored! if @s.slash_star_star_end?
next append_part(:any_non_dir) if @s.star?
next dir_only! if @s.slash_end?
next append_part(:dir) && anchored! if @s.slash?
next append_part(:one_non_dir) if @s.question_mark?
next if process_character_class
next if append_string(@s.literal)
next if append_string(@s.significant_whitespace)

process_end
process_next_characters
end
end
end
end

def process_first_characters
prepare_regexp_builder
anchored! if !@anchored && @s.slash?
end

def process_next_characters
return if process_escape
return unmatchable_rule! if @s.star_star_slash_slash?
return append_part(:any) && dir_only! if @s.star_star_slash_end?
return append_part(:any_dir) && anchored! if @s.star_star_slash?
return unmatchable_rule! if @s.slash_slash?
return append_part(:dir) && append_part(:any) && anchored! if @s.slash_star_star_end?
return append_part(:any_non_dir) if @s.star?
return dir_only! if @s.slash_end?
return append_part(:dir) && anchored! if @s.slash?
return append_part(:one_non_dir) if @s.question_mark?
return if process_character_class
return if append_string(@s.literal)
return if append_string(@s.significant_whitespace)

process_end
end

def build_matcher
@main_re ||= @re.dup.compress

46 changes: 43 additions & 3 deletions lib/path_list/pattern_parser/gitignore/rule_scanner.rb
Original file line number Diff line number Diff line change
@@ -27,13 +27,53 @@ def slash?
skip(%r{/})
end

# @return [String, nil]
def root
matched if scan(%r{/})
end

# @return [String, nil]
def home
scan(%r{~[^/]*})
end

# @return [Boolean]
def dot_slash?
skip(%r{\./})
end

# @return [Boolean]
def dot_end?
skip(/\.\s*\z/)
end

# @return [Boolean]
def dot_slash_end?
skip(%r{\./\s*\z})
end

# @return [Boolean]
def dot_dot_slash_end?
skip(%r{\.\./\s*\z})
end

# @return [Boolean]
def dot_dot_slash?
skip(%r{\.\./})
end

# @return [Boolean]
def dot_dot_end?
skip(/\.\.\s*\z/)
end

# @return [Boolean]
def slash_end?
skip(%r{/\s*\z})
end

# @return [Boolean]
def backslash?
def escape?
skip(/\\/)
end

@@ -84,7 +124,7 @@ def character_class_literal

# @return [String, nil]
def character_class_range_start
matched if scan(/(\\.|[^\\\]])(?=-(\\.|[^\\\]]))/)
matched.delete_prefix('\\') if scan(/(\\.|[^\\\]])(?=-(\\.|[^\\\]]))/)
end

# @return [String, nil]
@@ -93,7 +133,7 @@ def character_class_range_end
# with the lookahead in character_class_range_start
skip(/-/)
scan(/(\\.|[^\\\]])/)
matched
matched.delete_prefix('\\')
end

# @return [String, nil]
108 changes: 108 additions & 0 deletions lib/path_list/pattern_parser/gitignore/windows_rule_scanner.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# frozen_string_literal: true

require 'strscan'

class PathList
class PatternParser
class Gitignore
# @api private
class WindowsRuleScanner < RuleScanner
# @return [Boolean]
def slash?
skip(%r{[\\/]})
end

# @return [String, nil]
def home
# not sure this makes sense on windows, but just for similarity
scan(%r{~[^/\\]*})
end

# @return [String, nil]
def root
# / or \ or UMC path or driver letter
matched if scan(%r{(?:[\\/]{1,2}|[a-zA-Z]:[\\/])})
end

# @return [Boolean]
def dot_slash?
skip(%r{\.[\\/]})
end

# @return [Boolean]
def dot_slash_end?
skip(%r{\.[\\/]\s*\z})
end

# @return [Boolean]
def dot_dot_slash_end?
skip(%r{\.\.[\\/]\s*\z})
end

# @return [Boolean]
def dot_dot_slash?
skip(%r{\.\.[\\/]})
end

# @return [Boolean]
def slash_end?
skip(%r{[\\/]\s*\z})
end

# @return [Boolean]
def escape?
skip(/`/)
end

# @return [Boolean]
def star_star_slash_end?
skip(%r{\*{2,}[\\/]\s*\z})
end

# @return [Boolean]
def star_star_slash_slash?
skip(%r{\*{2,}[\\/]{2}})
end

# @return [Boolean]
def slash_slash?
skip(%r{[\\/]{2}})
end

# @return [Boolean]
def star_star_slash?
skip(%r{\*{2,}[\\/]})
end

# @return [Boolean]
def slash_star_star_end?
skip(%r{[\\/]\*{2,}\s*\z})
end

# @return [String, nil]
def character_class_literal
matched if scan(/[^\]`][^\]`-]*(?!-)/)
end

# @return [String, nil]
def character_class_range_start
matched.delete_prefix('`') if scan(/(`.|[^`\]])(?=-(`.|[^`\]]))/)
end

# @return [String, nil]
def character_class_range_end
# we already confirmed this was going to match
# with the lookahead in character_class_range_start
skip(/-/)
scan(/(`.|[^`\]])/)
matched.delete_prefix('`')
end

# @return [String, nil]
def literal
matched if scan(%r{[^*[\\/]?\[`\s]+})
end
end
end
end
end
97 changes: 67 additions & 30 deletions lib/path_list/pattern_parser/glob_gitignore.rb
Original file line number Diff line number Diff line change
@@ -17,16 +17,16 @@ class PatternParser
# - Patterns beginning with `/` (or `!/`) are absolute. Not relative to the `root:` directory.
# - Patterns beginning with `~` (or `!~`) are resolved relative to the `$HOME` or `~user` directory
# - Patterns beginning with `./` or `../` (or `!./` or `!../`) are resolved relative to the `root:` directory
# - Patterns containing with `/../` are resolved relative to the `root:` directory
# - Patterns beginning with `*` (or `!*`) will match any descendant of the `root:` directory
# - Other patterns match children (not descendants) of the `root:` directory
# - Patterns containing with `/../` will remove the previous path segment, (`/**/` counts as one path segment)
# - Additionally, on windows:
# - either / or \ (slash or backslash) can be used as path separators.
# - therefore \ (backslash) isn't available to be used as an escape character
# - instead ` (grave accent) is used as an escape character
# - instead ` (grave accent) is used as an escape character anywhere a backslash would be used
# - patterns beginning with `c:/`, `d:\`, or `!c:/`, or etc are absolute.
# - a path beginning with / or \ is a shortcut for the current working directory drive.
# - there is no cross platform escape character, this is intended to match the current shell
# - a path beginning with / or \ is a shortcut for the current working directory's drive.
# - there is no cross platform escape character.
# @example
# PathList.only(ARGV, format: :glob_gitignore)
# PathList.only(
@@ -47,41 +47,78 @@ class PatternParser
class GlobGitignore < Gitignore
Autoloader.autoload(self)

# @api private
# @param pattern [String]
# @param polarity [:ignore, :allow]
# @param root [String]
def initialize(pattern, polarity, root)
pattern = +'' if pattern.start_with?('#')
negated_sigil = '!' if pattern.delete_prefix!('!')
pattern = normalize_slash(pattern)
if pattern.start_with?('*')
pattern = "#{negated_sigil}#{pattern}"
elsif pattern.match?(EXPANDABLE_PATH)
dir_only! if pattern.match?(%r{/\s*\z}) # expand_path will remove it
# @return [Boolean]
def process_root
root = @s.root
return false unless root

pattern = "#{negated_sigil}#{CanonicalPath.full_path_from(pattern, root)}"
root = nil
@anchored = true
else
pattern = "#{negated_sigil}/#{pattern}"
end
@root = ::File.expand_path(root)
emitted!
true
end

# @return [Boolean]
def process_home
home = @s.home
return false unless home

@root = ::File.expand_path(home)
emitted!
true
rescue ArgumentError
@s.unscan
nil
end

super(normalize_escape(pattern), polarity, root)
# @return [true]
def process_up_a_level
@re.up_a_level
emitted!
true
end

private
# @return [Boolean]
def end_with_dir?
@re.end_with_dir?
end

# @return [true]
def remove_trailing_dir
@re.remove_trailing_dir
emitted!
true
end

# @return [void]
def process_first_characters
if process_root || process_home
anchored!
prepare_regexp_builder
return
end

def normalize_slash(pattern)
return pattern unless ::File::ALT_SEPARATOR
prepare_regexp_builder
return @s.unscan if @s.star?

pattern.tr('\\', '/')
anchored!
return dir_only! && remove_trailing_dir if @s.dot_slash_end?
return remove_trailing_dir if @s.dot_end?
return emitted! if @s.dot_slash?
return process_up_a_level && remove_trailing_dir && dir_only! if @s.dot_dot_slash_end?
return process_up_a_level && remove_trailing_dir if @s.dot_dot_end?
return process_up_a_level if @s.dot_dot_slash?
end

def normalize_escape(pattern)
return pattern unless ::File::ALT_SEPARATOR
# @return [void]
def process_next_characters
return dir_only! && remove_trailing_dir if end_with_dir? && @s.dot_slash_end?
return remove_trailing_dir if end_with_dir? && @s.dot_end?
return emitted! if end_with_dir? && @s.dot_slash?
return process_up_a_level && remove_trailing_dir && dir_only! if end_with_dir? && @s.dot_dot_slash_end?
return process_up_a_level && remove_trailing_dir if end_with_dir? && @s.dot_dot_end?
return process_up_a_level if end_with_dir? && @s.dot_dot_slash?

pattern.tr('`', '\\')
super
end
end
end
42 changes: 24 additions & 18 deletions lib/path_list/pattern_parser/glob_gitignore/expandable_path.rb
Original file line number Diff line number Diff line change
@@ -5,25 +5,31 @@ class PatternParser
class GlobGitignore
# :nocov:
# this isn't actually nocov, but it's cov is because i reload the file
EXPANDABLE_PATH = %r{(?:
\A(?:
[~/] # start with slash or tilde
EXPANDABLE_PATH = if ::File.expand_path('/') == '/'
%r{(?:
\A(?:
[~/] # start with slash or tilde
|
\.{1,2}(?:/|\z) # start with dot or dot dot followed by slash or nothing
)
|
\.{1,2}(?:/|\z) # start with dot or dot dot followed by slash or nothing
#{
if ::File.expand_path('/') != '/' # only if drive letters are applicable
"
|
[a-zA-Z]:/ # drive letter
|
// # UNC path
"
end
}
)
|
(?:[^\\]|\A)(?:\\{2})*/\.\./) # unescaped slash dot dot slash
}x.freeze
(?:[^\\]|\A)(?:\\{2})*/\.\./ # unescaped slash dot dot slash
)}x
else
%r{(?:
\A(?:
[~\\/] # start with slash or tilde
|
\.{1,2}(?:[\\/]|\z) # start with dot or dot dot followed by slash or nothing
|
[a-zA-Z]:[\\/] # start with drive letter
|
[\\/]{2} # UNC path
)
|
(?:[^`]|\A)(?:`{2})*[\\/]\.\.[\\/] # unescaped slash dot dot slash
)}x.freeze
end
# :nocov:
end
end
16 changes: 16 additions & 0 deletions lib/path_list/pattern_parser/glob_gitignore/scanner.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# frozen_string_literal: true

class PathList
class PatternParser
class GlobGitignore
# :nocov:
# this isn't actually nocov, but it's cov is because i reload the file
SCANNER = if ::File::ALT_SEPARATOR
Gitignore::WindowsRuleScanner
else
Gitignore::RuleScanner
end
# :nocov:
end
end
end
21 changes: 21 additions & 0 deletions lib/path_list/token_regexp/path.rb
Original file line number Diff line number Diff line change
@@ -36,6 +36,27 @@ def compress
self
end

# @return [void]
def up_a_level
return if @parts.count(:dir) <= 1

@parts.pop # remove trailing dir
@parts.pop until end_with_dir?
end

# @return [void]
def remove_trailing_dir
return if @parts.count(:dir) <= 1

@parts.pop if @parts.last == :dir
end

# @return [Boolean]
def end_with_dir?
last = @parts.last
last == :dir || last == :any_dir
end

# @return [Array<TokenRegexp::Path>]
def ancestors
prev_rule = []
12 changes: 8 additions & 4 deletions spec/ignore_or_include_spec.rb
Original file line number Diff line number Diff line change
@@ -53,8 +53,10 @@
end
end

# can't have literal backslashes in filenames in windows
describe 'literal backslashes in filenames', skip: windows? do
describe(
'literal backslashes in filenames',
skip: ("can't have literal backslashes in filenames in windows" if windows?)
) do
it "never matches backslashes when they're not in the pattern" do
gitignore 'foo'

@@ -90,8 +92,10 @@
end
end

# can't end with literal backslashes in filenames in windows
describe 'Trailing spaces are ignored unless they are quoted with backslash ("\")', skip: windows? do
describe(
'Trailing spaces are ignored unless they are quoted with backslash ("\")',
skip: ("can't end with literal backslashes in filenames in windows" if windows?)
) do
it 'ignores trailing spaces in the gitignore file' do
gitignore 'foo '

282 changes: 264 additions & 18 deletions spec/pattern_parser/glob_gitignore_spec.rb
Original file line number Diff line number Diff line change
@@ -35,53 +35,299 @@ def build(pattern)
it { expect(build('foo')).to be_like PathList::Matcher::ExactString.new('/a/path/foo', :ignore) }
end

describe 'initial ../' do
it { expect(build('../foo')).to be_like PathList::Matcher::ExactString.new('/a/foo', :ignore) }
it { expect(build('../../foo')).to be_like PathList::Matcher::ExactString.new('/foo', :ignore) }
it { expect(build('../../../foo')).to be_like PathList::Matcher::ExactString.new('/foo', :ignore) }
it { expect(build('..foo')).to be_like PathList::Matcher::ExactString.new('/a/path/..foo', :ignore) }
end

describe 'mid /../' do
it { expect(build('bar/../foo')).to be_like PathList::Matcher::ExactString.new('/a/path/foo', :ignore) }
it { expect(build('bar/../../foo')).to be_like PathList::Matcher::ExactString.new('/a/foo', :ignore) }
it { expect(build('bar/../../../foo')).to be_like PathList::Matcher::ExactString.new('/foo', :ignore) }
it { expect(build('bar/../../../../foo')).to be_like PathList::Matcher::ExactString.new('/foo', :ignore) }

it do
expect(build('bar/**/../foo'))
.to be_like PathList::Matcher::ExactString.new('/a/path/bar/foo', :ignore)
end

it { expect(build('bar**/../foo')).to be_like PathList::Matcher::ExactString.new('/a/path/foo', :ignore) }
it { expect(build('bar../foo')).to be_like PathList::Matcher::ExactString.new('/a/path/bar../foo', :ignore) }
it { expect(build('bar/..foo')).to be_like PathList::Matcher::ExactString.new('/a/path/bar/..foo', :ignore) }
end

describe 'trailing /..' do
it { expect(build('bar/..')).to be_like PathList::Matcher::ExactString.new('/a/path', :ignore) }
it { expect(build('b[ai]r/..')).to be_like PathList::Matcher::ExactString.new('/a/path', :ignore) }
it { expect(build('ba[rb]/..')).to be_like PathList::Matcher::ExactString.new('/a/path', :ignore) }

it { expect(build('bar/../..')).to be_like PathList::Matcher::ExactString.new('/a', :ignore) }
it { expect(build('b[ai]r/../..')).to be_like PathList::Matcher::ExactString.new('/a', :ignore) }
it { expect(build('ba[rb]/../..')).to be_like PathList::Matcher::ExactString.new('/a', :ignore) }

it { expect(build('bar/../../..')).to be_like PathList::Matcher::ExactString.new('/', :ignore) }
it { expect(build('bar/../../../..')).to be_like PathList::Matcher::ExactString.new('/', :ignore) }

it { expect(build('bar..')).to be_like PathList::Matcher::ExactString.new('/a/path/bar..', :ignore) }
it { expect(build('bar../..')).to be_like PathList::Matcher::ExactString.new('/a/path', :ignore) }
end

describe 'trailing /../' do
it do
expect(build('bar/../'))
.to be_like PathList::Matcher::MatchIfDir.new(
PathList::Matcher::ExactString.new('/a/path', :ignore)
)
end

it do
expect(build('b[ai]r/../'))
.to be_like PathList::Matcher::MatchIfDir.new(
PathList::Matcher::ExactString.new('/a/path', :ignore)
)
end

it do
expect(build('ba[rb]/../'))
.to be_like PathList::Matcher::MatchIfDir.new(
PathList::Matcher::ExactString.new('/a/path', :ignore)
)
end

it do
expect(build('bar/../../'))
.to be_like PathList::Matcher::MatchIfDir.new(
PathList::Matcher::ExactString.new('/a', :ignore)
)
end

it do
expect(build('b[ai]r/../../'))
.to be_like PathList::Matcher::MatchIfDir.new(
PathList::Matcher::ExactString.new('/a', :ignore)
)
end

it do
expect(build('ba[rb]/../../'))
.to be_like PathList::Matcher::MatchIfDir.new(
PathList::Matcher::ExactString.new('/a', :ignore)
)
end

it do
expect(build('bar/../../../'))
.to be_like PathList::Matcher::MatchIfDir.new(
PathList::Matcher::ExactString.new('/', :ignore)
)
end

it do
expect(build('bar/../../../../'))
.to be_like PathList::Matcher::MatchIfDir.new(
PathList::Matcher::ExactString.new('/', :ignore)
)
end

it do
expect(build('bar../'))
.to be_like PathList::Matcher::MatchIfDir.new(
PathList::Matcher::ExactString.new('/a/path/bar..', :ignore)
)
end

it do
expect(build('bar../../'))
.to be_like PathList::Matcher::MatchIfDir.new(
PathList::Matcher::ExactString.new('/a/path', :ignore)
)
end
end

describe 'only ../' do
it do
expect(build('..'))
.to be_like PathList::Matcher::ExactString.new('/a', :ignore)
end

it do
expect(build('../'))
.to be_like PathList::Matcher::MatchIfDir.new(
PathList::Matcher::ExactString.new('/a', :ignore)
)
end

it do
expect(build('../../'))
.to be_like PathList::Matcher::MatchIfDir.new(
PathList::Matcher::ExactString.new('/', :ignore)
)
end

it do
expect(build('../../../'))
.to be_like PathList::Matcher::MatchIfDir.new(
PathList::Matcher::ExactString.new('/', :ignore)
)
end
end

describe 'initial ./' do
it { expect(build('./foo')).to be_like PathList::Matcher::ExactString.new('/a/path/foo', :ignore) }
it { expect(build('././foo')).to be_like PathList::Matcher::ExactString.new('/a/path/foo', :ignore) }

it { expect(build('.foo')).to be_like PathList::Matcher::ExactString.new('/a/path/.foo', :ignore) }
it { expect(build('./.foo')).to be_like PathList::Matcher::ExactString.new('/a/path/.foo', :ignore) }
end

describe 'mid /./' do
it { expect(build('bar/./foo')).to be_like PathList::Matcher::ExactString.new('/a/path/bar/foo', :ignore) }
it { expect(build('bar/././foo')).to be_like PathList::Matcher::ExactString.new('/a/path/bar/foo', :ignore) }

it { expect(build('bar./foo')).to be_like PathList::Matcher::ExactString.new('/a/path/bar./foo', :ignore) }
it { expect(build('bar/.foo')).to be_like PathList::Matcher::ExactString.new('/a/path/bar/.foo', :ignore) }

it { expect(build('bar././foo')).to be_like PathList::Matcher::ExactString.new('/a/path/bar./foo', :ignore) }
it { expect(build('bar/./.foo')).to be_like PathList::Matcher::ExactString.new('/a/path/bar/.foo', :ignore) }
end

describe 'trailing /.' do
it { expect(build('bar/.')).to be_like PathList::Matcher::ExactString.new('/a/path/bar', :ignore) }
it { expect(build('bar/./.')).to be_like PathList::Matcher::ExactString.new('/a/path/bar', :ignore) }

it { expect(build('bar.')).to be_like PathList::Matcher::ExactString.new('/a/path/bar.', :ignore) }
it { expect(build('bar./.')).to be_like PathList::Matcher::ExactString.new('/a/path/bar.', :ignore) }
end

describe 'trailing /./' do
it do
expect(build('bar/./'))
.to be_like PathList::Matcher::MatchIfDir.new(
PathList::Matcher::ExactString.new('/a/path/bar', :ignore)
)
end

it do
expect(build('bar/././'))
.to be_like PathList::Matcher::MatchIfDir.new(
PathList::Matcher::ExactString.new('/a/path/bar', :ignore)
)
end

it do
expect(build('bar./'))
.to be_like PathList::Matcher::MatchIfDir.new(
PathList::Matcher::ExactString.new('/a/path/bar.', :ignore)
)
end
end

describe 'only ./' do
it do
expect(build('.'))
.to be_like PathList::Matcher::ExactString.new('/a/path', :ignore)
end

it do
expect(build('./'))
.to be_like PathList::Matcher::MatchIfDir.new(
PathList::Matcher::ExactString.new('/a/path', :ignore)
)
end

it do
expect(build('./.'))
.to be_like PathList::Matcher::ExactString.new('/a/path', :ignore)
end

it do
expect(build('././'))
.to be_like PathList::Matcher::MatchIfDir.new(
PathList::Matcher::ExactString.new('/a/path', :ignore)
)
end
end

describe 'The windows case' do
let(:drive_letter) { windows? ? File.expand_path('/')[0] : 'D' }

before do
next if windows?

# use windows expand_path:
allow(File).to receive(:expand_path).and_call_original
allow(File).to receive(:expand_path).with('/').and_return('D:/')
allow(File).to receive(:expand_path)
.with(a_string_matching(%r{D:/.*}), '/a/path') do |path, _|
path.tr('\\\\', '/').delete_suffix('/')
end
.with(%r{\A(?:#{drive_letter}:)?[\\/]\z})
.and_return("#{drive_letter}:/")
stub_const('::File::ALT_SEPARATOR', '\\')

silence_warnings do
load File.expand_path('../../lib/path_list/pattern_parser/glob_gitignore/expandable_path.rb', __dir__)
load File.expand_path('../../lib/path_list/pattern_parser/glob_gitignore/scanner.rb', __dir__)
end
end

after do
allow(File).to receive(:expand_path).with('/').and_call_original
next if windows?

allow(File).to receive(:expand_path).and_call_original
# i need this to be as i expect for the rest of the tests, and i'm not sure how to unstub it properly:
stub_const('::File::ALT_SEPARATOR', windows? ? '\\' : nil)

silence_warnings do
load File.expand_path('../../lib/path_list/pattern_parser/glob_gitignore/expandable_path.rb', __dir__)
load File.expand_path('../../lib/path_list/pattern_parser/glob_gitignore/scanner.rb', __dir__)
end
end

it { expect(build('D:/foo/bar')).to be_like PathList::Matcher::ExactString.new('D:/foo/bar', :ignore) }
it { expect(build('D:\\foo\\bar')).to be_like PathList::Matcher::ExactString.new('D:/foo/bar', :ignore) }
it { expect(build('D:\foo/bar')).to be_like PathList::Matcher::ExactString.new('D:/foo/bar', :ignore) }
it { expect(build('D:\`f`o`o`/`b`a`r')).to be_like PathList::Matcher::ExactString.new('D:/foo/bar', :ignore) }
it do
expect(build("#{drive_letter}:/foo/bar"))
.to be_like PathList::Matcher::ExactString.new("#{drive_letter}:/foo/bar", :ignore)
end

it do
expect(build("#{drive_letter}:\\foo\\bar"))
.to be_like PathList::Matcher::ExactString.new("#{drive_letter}:/foo/bar", :ignore)
end

it do
expect(build("#{drive_letter}:\\foo/bar"))
.to be_like PathList::Matcher::ExactString.new("#{drive_letter}:/foo/bar", :ignore)
end

it do
expect(build("#{drive_letter}:\\`f`o`o`/`b`a`r"))
.to be_like PathList::Matcher::ExactString.new("#{drive_letter}:/foo/bar", :ignore)
end

it do
expect(build('D:/foo/bar/'))
.to be_like PathList::Matcher::MatchIfDir.new(PathList::Matcher::ExactString.new('D:/foo/bar', :ignore))
expect(build("#{drive_letter}:/foo/bar/"))
.to be_like PathList::Matcher::MatchIfDir.new(
PathList::Matcher::ExactString.new("#{drive_letter}:/foo/bar", :ignore)
)
end

it do
expect(build('D:\\foo\\bar/'))
.to be_like PathList::Matcher::MatchIfDir.new(PathList::Matcher::ExactString.new('D:/foo/bar', :ignore))
expect(build("#{drive_letter}:\\foo\\bar/"))
.to be_like PathList::Matcher::MatchIfDir.new(
PathList::Matcher::ExactString.new("#{drive_letter}:/foo/bar", :ignore)
)
end

it do
expect(build('D:\\foo/bar/'))
.to be_like PathList::Matcher::MatchIfDir.new(PathList::Matcher::ExactString.new('D:/foo/bar', :ignore))
expect(build("#{drive_letter}:\\foo/bar/"))
.to be_like PathList::Matcher::MatchIfDir.new(
PathList::Matcher::ExactString.new("#{drive_letter}:/foo/bar", :ignore)
)
end

it do
expect(build('D:\\foo/bar\\'))
.to be_like PathList::Matcher::MatchIfDir.new(PathList::Matcher::ExactString.new('D:/foo/bar', :ignore))
expect(build("#{drive_letter}:\\foo/bar\\"))
.to be_like PathList::Matcher::MatchIfDir.new(
PathList::Matcher::ExactString.new("#{drive_letter}:/foo/bar", :ignore)
)
end

it do
@@ -113,7 +359,7 @@ def build(pattern)
end

describe 'leading ./ means current directory based on the root' do
it { expect(build('./foo')).to be_like PathList::Matcher::ExactString.new("#{FSROOT}a/path/foo", :ignore) }
it { expect(build('./foo')).to be_like PathList::Matcher::ExactString.new('/a/path/foo', :ignore) }
end

describe 'A line starting with # serves as a comment.' do

0 comments on commit e1c9c61

Please sign in to comment.