diff --git a/lib/path_list.rb b/lib/path_list.rb index a01da15..b9fbc09 100644 --- a/lib/path_list.rb +++ b/lib/path_list.rb @@ -40,29 +40,45 @@ def initialize end def gitignore(root: nil, config: true) - new_and_matcher(Gitignore.build(root: root, config: config)) + new_with_matcher(Matcher::All.build([ + @matcher, + Gitignore.build(root: root, config: config), + Gitignore.ignore_dot_git_matcher + ])) end def ignore(*patterns, patterns_from_file: nil, format: :gitignore, root: nil) - new_and_matcher( + new_with_matcher(Matcher::All.build([ + @matcher, PatternParser.build( - patterns: patterns, patterns_from_file: patterns_from_file, format: format, root: root, polarity: :ignore + patterns: patterns, + patterns_from_file: patterns_from_file, + format: format, + root: root, + polarity: :ignore ) - ) + ])) end def only(*patterns, patterns_from_file: nil, format: :gitignore, root: nil) - new_and_matcher( + new_with_matcher(Matcher::All.build([ + @matcher, PatternParser.build( - patterns: 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 def union(path_list, *path_lists) - new_with_matcher( - Matcher::Any.build([@matcher, path_list.matcher, *path_lists.map { |l| l.matcher }]) # rubocop:disable Style/SymbolProc - ) + new_with_matcher(Matcher::Any.build([ + @matcher, + path_list.matcher, + *path_lists.map { |l| l.matcher } # rubocop:disable Style/SymbolProc + ])) end def |(other) @@ -70,9 +86,11 @@ def |(other) end def intersection(path_list, *path_lists) - new_with_matcher( - Matcher::All.build([@matcher, path_list.matcher, path_lists.map { |l| l.matcher }]) # rubocop:disable Style/SymbolProc - ) + new_with_matcher(Matcher::All.build([ + @matcher, + path_list.matcher, + path_lists.map { |l| l.matcher } # rubocop:disable Style/SymbolProc + ])) end def &(other) @@ -156,10 +174,6 @@ def new_with_matcher(matcher) path_list end - def new_and_matcher(matcher) - new_with_matcher(Matcher::All.build([@matcher, matcher])) - end - def dir_matcher @dir_matcher ||= @matcher.dir_matcher end diff --git a/lib/path_list/candidate.rb b/lib/path_list/candidate.rb index 7238a87..0a8bd59 100644 --- a/lib/path_list/candidate.rb +++ b/lib/path_list/candidate.rb @@ -50,7 +50,7 @@ def child_candidates def children @children ||= begin ::Dir.children(@full_path) - rescue ::SystemCallError + rescue ::IOError, ::SystemCallError [] end end @@ -118,7 +118,7 @@ def ftype else ::File.ftype(@full_path) end - rescue ::SystemCallError + rescue ::IOError, ::SystemCallError @ftype = 'error' end # :nocov: @@ -128,7 +128,7 @@ def ftype return @ftype if @ftype @ftype = ::File.ftype(@full_path) - rescue ::SystemCallError + rescue ::IOError, ::SystemCallError @ftype = 'error' end end diff --git a/lib/path_list/gitconfig/core_excludesfile.rb b/lib/path_list/gitconfig/core_excludesfile.rb index ab037fd..2b81f2d 100644 --- a/lib/path_list/gitconfig/core_excludesfile.rb +++ b/lib/path_list/gitconfig/core_excludesfile.rb @@ -6,10 +6,10 @@ module Gitconfig # Find the configured git core.excludesFile module CoreExcludesfile class << self - # @param repo_root [String] + # @param git_dir [String] # @return [String, nil] - def path(repo_root:) - ignore_path = gitconfigs_core_excludesfile_path(repo_root) || + def path(git_dir:) + ignore_path = gitconfigs_core_excludesfile_path(git_dir) || default_core_excludesfile_path ignore_path unless ignore_path.empty? @@ -17,22 +17,22 @@ def path(repo_root:) private - def gitconfigs_core_excludesfile_path(repo_root) - gitconfig_core_excludesfile_path(repo_config_path(repo_root)) || - gitconfig_core_excludesfile_path(global_config_path) || - gitconfig_core_excludesfile_path(default_user_config_path) || - gitconfig_core_excludesfile_path(system_config_path) + def gitconfigs_core_excludesfile_path(git_dir) + gitconfig_core_excludesfile_path(repo_config_path(git_dir), git_dir) || + gitconfig_core_excludesfile_path(global_config_path, git_dir) || + gitconfig_core_excludesfile_path(default_user_config_path, git_dir) || + gitconfig_core_excludesfile_path(system_config_path, git_dir) rescue ParseError => e ::Warning.warn("PathList gitconfig parser failed\n" + e.message) '' end - def gitconfig_core_excludesfile_path(config_path) + def gitconfig_core_excludesfile_path(config_path, git_dir) return unless config_path return unless ::File.readable?(config_path) - ignore_path = FileParser.parse(config_path).excludesfile + ignore_path = FileParser.parse(config_path, git_dir: git_dir).excludesfile return unless ignore_path ignore_path.strip! @@ -51,8 +51,8 @@ def default_core_excludesfile_path CanonicalPath.full_path_from('git/ignore', default_config_home) end - def repo_config_path(root) - CanonicalPath.full_path_from('.git/config', root) + def repo_config_path(git_dir) + CanonicalPath.full_path_from('config', git_dir) if git_dir end def global_config_path diff --git a/lib/path_list/gitconfig/file_parser.rb b/lib/path_list/gitconfig/file_parser.rb index efbb803..f2d78c5 100644 --- a/lib/path_list/gitconfig/file_parser.rb +++ b/lib/path_list/gitconfig/file_parser.rb @@ -8,22 +8,21 @@ module Gitconfig # Parse git config file for the core.excludesFile class FileParser # @param file [String] - # @param root [String] + # @param git_dir [String] # @param nesting [Integer] # @return [String] # @raise [ParseError] - def self.parse(file, root: Dir.pwd, nesting: 1, find: :'core.excludesFile') - new(file, root: root, nesting: nesting, find: find).parse + def self.parse(file, git_dir: nil, nesting: 1) + new(file, git_dir: git_dir, nesting: nesting).parse end # @param file [String] - # @param root [String] + # @param git_dir [String] # @param nesting [Integer] - def initialize(path, root: Dir.pwd, nesting: 1, find: :'core.excludesFile') + def initialize(path, git_dir: nil, nesting: 1) @path = path - @root = root + @git_dir = git_dir @nesting = nesting - @find = find end # @return [String] @@ -42,7 +41,7 @@ def parse attr_reader :nesting attr_reader :path - attr_reader :root + attr_reader :git_dir attr_accessor :within_quotes attr_accessor :section @@ -75,7 +74,7 @@ def read_file(path) result = self.class.parse( CanonicalPath.full_path_from(include_path, ::File.dirname(path)), - root: root, + git_dir: git_dir, nesting: nesting + 1 ) self.excludesfile = result.excludesfile if result.excludesfile @@ -125,22 +124,24 @@ def include_if(file) def on_branch?(branch_pattern) branch_pattern += '**' if branch_pattern.end_with?('/') - current_branch = ::File.readable?("#{root}/.git/HEAD") && - ::File.read("#{root}/.git/HEAD").delete_prefix('ref: refs/heads/') + current_branch = ::File.readable?("#{git_dir}/HEAD") && + ::File.read("#{git_dir}/HEAD").delete_prefix('ref: refs/heads/') return false unless current_branch # goddamit git what does 'a pattern with standard globbing wildcards' mean ::File.fnmatch(branch_pattern, current_branch, ::File::FNM_PATHNAME | ::File::FNM_DOTMATCH) end - def gitdir?(gitdir, path:, case_insensitive: false) - gitdir += '**' if gitdir.end_with?('/') - gitdir.sub!(%r{\A~/}, Dir.home + '/') - gitdir.sub!(/\A\./, path + '/') - gitdir = "**/#{gitdir}" unless gitdir.start_with?('/') + def gitdir?(gitdir_value, path:, case_insensitive: false) + return unless git_dir + + gitdir_value += '**' if gitdir_value.end_with?('/') + gitdir_value.sub!(%r{\A~/}, Dir.home + '/') + gitdir_value.sub!(/\A\./, path + '/') + gitdir_value = "**/#{gitdir_value}" unless gitdir_value.start_with?('/') options = ::File::FNM_PATHNAME | ::File::FNM_DOTMATCH options |= ::File::FNM_CASEFOLD if case_insensitive - ::File.fnmatch(gitdir, ::File.join(root, '.git'), options) + ::File.fnmatch(gitdir_value, git_dir, options) end def scan_value(file) diff --git a/lib/path_list/gitignore.rb b/lib/path_list/gitignore.rb index c0f599e..e6de43b 100644 --- a/lib/path_list/gitignore.rb +++ b/lib/path_list/gitignore.rb @@ -3,71 +3,123 @@ class PathList # @api private 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 + 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:) + Cache.cache(root: root, gitignore_global: config) do + root = if root + CanonicalPath.full_path(root) + else + find_root + end + new(root: root, config: config).matcher + end + end + + # assumes root to be absolute + def build!(root:, config:) + Cache.cache(root: root, pwd: nil, gitignore_global: config) do + new(root: root, config: config).matcher + end + end + + def ignore_dot_git_matcher + Matcher::LastMatch.build([ + Matcher::Allow, + Matcher::PathRegexp.build([[:dir, '.git', :end_anchor]], :ignore) + ]) + end + + private + + 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?('/') + + dir = ::File.dirname(dir) + end end end # @param (see .build) def initialize(root:, config:) - @root = if root - CanonicalPath.full_path(root) - else - find_root - end + @root = root + @git_dir = find_git_dir + @submodule_paths = find_submodule_paths @config = config end # @return [Matcher] def matcher - collector = build_collector(@root) + @matcher = Matcher::CollectGitignore.build(collect_matcher, Matcher::Allow) + append(Gitconfig::CoreExcludesfile.path(git_dir: @git_dir)) if @config + require 'pry' + binding.pry + append("#{@git_dir}/info/exclude") if @git_dir + append('.gitignore') + return @matcher unless @submodule_paths - 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]) + Matcher::All.build([@matcher, *submodule_matchers]) end private - def find_root - home = ::Dir.home - dir = pwd = ::Dir.pwd + def submodule_matchers + @submodule_paths.map do |submodule_path| + self.class.build!(root: submodule_path, config: @config) + end + end + + def find_submodule_paths + Gitconfig::FileParser + .parse("#{@root}/.gitmodules") + .submodule_paths + &.map { |submodule_path| "#{@root}/#{submodule_path}" } + end - loop do - return dir if ::File.exist?("#{dir}/.git") - return pwd if dir.casecmp(home).zero? || dir.end_with?('/') + def find_git_dir + dot_git = Candidate.new("#{@root}/.git") - dir = ::File.dirname(dir) + if dot_git.directory? + dot_git.full_path + elsif (dot_git_content = ::File.read(dot_git.full_path)) + dot_git_content.delete_prefix!('gitdir: ') + dot_git_content.chomp! + CanonicalPath.full_path_from( + dot_git_content, @root + ) end + rescue ::IOError, ::SystemCallError + nil end - def append(collector, root, path) + def append(path) return unless path - collector.append(CanonicalPath.full_path_from(path, root), root: root) + @matcher.append(CanonicalPath.full_path_from(path, @root), root: @root) end - def build_dot_git_matcher - Matcher::PathRegexp.build([[:dir, '.git', :end_anchor]], :ignore) - end - - def build_collector(root) - root_re = TokenRegexp::Path.new_from_path(root) + def collect_matcher + 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 + descendant_dirs_matcher = Matcher::MatchIfDir.new( + Matcher::PathRegexp.build([root_re_children.parts, root_re.parts], :allow) ) + + return descendant_dirs_matcher unless @submodule_paths + + Matcher::LastMatch.build([ + descendant_dirs_matcher, + Matcher::ExactString.build(@submodule_paths, :ignore) + ]) end end end diff --git a/lib/path_list/pattern_parser/gitignore.rb b/lib/path_list/pattern_parser/gitignore.rb index 0d44ec6..ed1025c 100644 --- a/lib/path_list/pattern_parser/gitignore.rb +++ b/lib/path_list/pattern_parser/gitignore.rb @@ -54,9 +54,9 @@ def implicit_matcher def prepare_regexp_builder @re = if @root.end_with?('/') - TokenRegexp::Path.new_from_path(@root, [:any_dir]) + TokenRegexp::Path.new_from_path(@root, tail: [:any_dir]) else - TokenRegexp::Path.new_from_path(@root, [:dir, :any_dir]) + TokenRegexp::Path.new_from_path(@root, tail: [:dir, :any_dir]) end @start_any_dir_position = @re.length - 1 diff --git a/lib/path_list/pattern_parser/shebang.rb b/lib/path_list/pattern_parser/shebang.rb index b515d65..9ea8c90 100644 --- a/lib/path_list/pattern_parser/shebang.rb +++ b/lib/path_list/pattern_parser/shebang.rb @@ -29,7 +29,7 @@ def initialize(pattern, polarity, root) @polarity = polarity @root = root - @root_re = TokenRegexp::Path.new_from_path(root, []) + @root_re = TokenRegexp::Path.new_from_path(root, tail: [:dir, :any_dir]) end # @api private @@ -55,7 +55,7 @@ def matcher # @api private # @return [PathList::Matcher] def implicit_matcher - ancestors = @root_re.dup.concat([:dir, :any_dir]).ancestors # rubocop:disable Style/ConcatArrayLiterals + ancestors = @root_re.ancestors exact, regexp = ancestors.partition(&:exact_path?) exact = Matcher::ExactString.build(exact.map(&:to_s), :allow) diff --git a/lib/path_list/token_regexp/path.rb b/lib/path_list/token_regexp/path.rb index 1ea96c0..fd1be8e 100644 --- a/lib/path_list/token_regexp/path.rb +++ b/lib/path_list/token_regexp/path.rb @@ -7,8 +7,8 @@ class Path < TokenRegexp # @param path [String] # @param tail [Array] # @return [TokenRegexp::Path] - def self.new_from_path(path, tail = [:end_anchor]) - parts = [:start_anchor] + def self.new_from_path(path, head: [:start_anchor], tail: [:end_anchor]) + parts = head split = path.split('/') (parts << (CanonicalPath.case_insensitive? ? split[0].downcase : split[0])) if split[0] && !split[0].empty? (parts << :dir) if split.length == 1 || (split.empty? && path == '/') diff --git a/spec/path_list/gitconfig/core_excludesfile_spec.rb b/spec/path_list/gitconfig/core_excludesfile_spec.rb index e2d3240..52301b3 100644 --- a/spec/path_list/gitconfig/core_excludesfile_spec.rb +++ b/spec/path_list/gitconfig/core_excludesfile_spec.rb @@ -1,12 +1,12 @@ # frozen_string_literal: true RSpec.describe(PathList::Gitconfig::CoreExcludesfile) do - subject { described_class.path(repo_root: root) } + subject { described_class.path(git_dir: git_dir) } let(:default_ignore_path) { "#{home}/.config/git/ignore" } let(:home) { File.expand_path(Dir.home) } - let(:root) { Dir.pwd } + let(:git_dir) { Dir.pwd + '/.git' } let(:config_content) { "[core]\n\texcludesfile = #{excludesfile_value}\n" } let(:excludesfile_value) { '~/.global_gitignore' } @@ -43,7 +43,7 @@ context 'with excludesfile defined in repo config' do before do - stub_file(config_content, path: "#{root}/.git/config") + stub_file(config_content, path: "#{git_dir}/config") end it 'returns a literal unquoted value for the path' do @@ -106,7 +106,7 @@ context "when repo config exists but doesn't set excludesfile, and global config file does" do before do - stub_file(<<~GITCONFIG, path: "#{root}/.git/config") + stub_file(<<~GITCONFIG, path: "#{git_dir}/config") [core] attributesfile = ~/.global_gitattributes GITCONFIG diff --git a/spec/path_list/gitconfig/file_parser_spec.rb b/spec/path_list/gitconfig/file_parser_spec.rb index 1f793fe..9f6c49b 100644 --- a/spec/path_list/gitconfig/file_parser_spec.rb +++ b/spec/path_list/gitconfig/file_parser_spec.rb @@ -3,16 +3,21 @@ RSpec.describe PathList::Gitconfig::FileParser do within_temp_dir + subject(:parsed_file) { described_class.parse(config_path, git_dir: git_dir) } + + let(:git_dir) { Dir.pwd + '/.git' } + let(:config_path) { '.gitconfig' } + it 'returns nil for empty file' do - create_file('', path: '.gitconfig') + create_file('', path: config_path) - expect(described_class.parse('.gitconfig').excludesfile).to be_nil + expect(parsed_file.excludesfile).to be_nil end it 'raises for invalid file' do - create_file('[', path: '.gitconfig') + create_file('[', path: config_path) - expect { described_class.parse('.gitconfig') }.to raise_error(PathList::Gitconfig::ParseError) do |e| + expect { parsed_file }.to raise_error(PathList::Gitconfig::ParseError) do |e| expect(e.message).to eq <<~MESSAGE Unexpected character .gitconfig:1:0 @@ -23,9 +28,9 @@ end it 'raises for another invalid file' do - create_file('x[', path: '.gitconfig') + create_file('x[', path: config_path) - expect { described_class.parse('.gitconfig') }.to raise_error(PathList::Gitconfig::ParseError) do |e| + expect { parsed_file }.to raise_error(PathList::Gitconfig::ParseError) do |e| expect(e.message).to eq <<~MESSAGE Unexpected character .gitconfig:1:0 @@ -36,41 +41,41 @@ end it 'returns nil for nonexistent file' do - expect(described_class.parse('.gitconfig').excludesfile).to be_nil + expect(parsed_file.excludesfile).to be_nil end it 'returns nil for file with no [core]' do - create_file(<<~GITCONFIG, path: '.gitconfig') + create_file(<<~GITCONFIG, path: config_path) [remote "origin"] url = https://github.com/robotdana/path_list.git fetch = +refs/heads/*:refs/remotes/origin/* GITCONFIG - expect(described_class.parse('.gitconfig').excludesfile).to be_nil + expect(parsed_file.excludesfile).to be_nil end it 'returns nil for file with [core] but no excludesfile' do - create_file(<<~GITCONFIG, path: '.gitconfig') + create_file(<<~GITCONFIG, path: config_path) [core] mergeoptions = --no-edit hooksPath = ~/.dotfiles/hooks editor = mate --wait GITCONFIG - expect(described_class.parse('.gitconfig').excludesfile).to be_nil + expect(parsed_file.excludesfile).to be_nil end it 'returns value for file with excludesfile' do - create_file(<<~GITCONFIG, path: '.gitconfig') + create_file(<<~GITCONFIG, path: config_path) [core] excludesfile = ~/.gitignore GITCONFIG - expect(described_class.parse('.gitconfig').excludesfile).to eq('~/.gitignore') + expect(parsed_file.excludesfile).to eq('~/.gitignore') end it 'returns value for file with submodule..path' do - create_file(<<~GITCONFIG, path: '.gitconfig') + create_file(<<~GITCONFIG, path: config_path) [submodule "foo_name"] path = subdir/foo [submodule "bar_project"] @@ -79,42 +84,42 @@ path = "vendor/baz" GITCONFIG - expect(described_class.parse('.gitconfig').submodule_paths) + expect(parsed_file.submodule_paths) .to eq(['subdir/foo', 'subdir/bar', 'vendor/baz']) end it 'returns value for file with excludesfile after other stuff' do - create_file(<<~GITCONFIG, path: '.gitconfig') + create_file(<<~GITCONFIG, path: config_path) [core] mergeoptions = --no-edit excludesfile = ~/.gitignore GITCONFIG - expect(described_class.parse('.gitconfig').excludesfile).to eq('~/.gitignore') + expect(parsed_file.excludesfile).to eq('~/.gitignore') end it 'returns value for file with excludesfile before other stuff' do - create_file(<<~GITCONFIG, path: '.gitconfig') + create_file(<<~GITCONFIG, path: config_path) [core] excludesfile = ~/.gitignore mergeoptions = --no-edit GITCONFIG - expect(described_class.parse('.gitconfig').excludesfile).to eq('~/.gitignore') + expect(parsed_file.excludesfile).to eq('~/.gitignore') end it 'returns value for file with excludesfile after boolean true key' do - create_file(<<~GITCONFIG, path: '.gitconfig') + create_file(<<~GITCONFIG, path: config_path) [core] ignoreCase excludesfile = ~/.gitignore GITCONFIG - expect(described_class.parse('.gitconfig').excludesfile).to eq('~/.gitignore') + expect(parsed_file.excludesfile).to eq('~/.gitignore') end it 'returns value for file with [core] after other stuff' do - create_file(<<~GITCONFIG, path: '.gitconfig') + create_file(<<~GITCONFIG, path: config_path) [remote "origin"] url = https://github.com/robotdana/path_list.git fetch = +refs/heads/*:refs/remotes/origin/* @@ -122,11 +127,11 @@ excludesfile = ~/.gitignore GITCONFIG - expect(described_class.parse('.gitconfig').excludesfile).to eq('~/.gitignore') + expect(parsed_file.excludesfile).to eq('~/.gitignore') end it 'returns value for file with [core] before other stuff' do - create_file(<<~GITCONFIG, path: '.gitconfig') + create_file(<<~GITCONFIG, path: config_path) [core] excludesfile = ~/.gitignore [remote "origin"] @@ -134,315 +139,315 @@ fetch = +refs/heads/*:refs/remotes/origin/* GITCONFIG - expect(described_class.parse('.gitconfig').excludesfile).to eq('~/.gitignore') + expect(parsed_file.excludesfile).to eq('~/.gitignore') end it 'returns nil for file with commented excludesfile line' do - create_file(<<~GITCONFIG, path: '.gitconfig') + create_file(<<~GITCONFIG, path: config_path) [core] # excludesfile = ~/.gitignore GITCONFIG - expect(described_class.parse('.gitconfig').excludesfile).to be_nil + expect(parsed_file.excludesfile).to be_nil end it 'returns value for file with excludesfile in quotes' do - create_file(<<~GITCONFIG, path: '.gitconfig') + create_file(<<~GITCONFIG, path: config_path) [core] excludesfile = "~/gitignore" GITCONFIG - expect(described_class.parse('.gitconfig').excludesfile).to eq('~/gitignore') + expect(parsed_file.excludesfile).to eq('~/gitignore') end it 'returns value for file with excludesFile in with camel casing' do - create_file(<<~GITCONFIG, path: '.gitconfig') + create_file(<<~GITCONFIG, path: config_path) [core] excludesFile = ~/gitignore GITCONFIG - expect(described_class.parse('.gitconfig').excludesfile).to eq('~/gitignore') + expect(parsed_file.excludesfile).to eq('~/gitignore') end it 'returns value for file with excludesFile in with uppercase' do - create_file(<<~GITCONFIG, path: '.gitconfig') + create_file(<<~GITCONFIG, path: config_path) [core] EXCLUDESFILE = ~/gitignore GITCONFIG - expect(described_class.parse('.gitconfig').excludesfile).to eq('~/gitignore') + expect(parsed_file.excludesfile).to eq('~/gitignore') end it 'returns value for file with excludesFile in uppercase CORE' do - create_file(<<~GITCONFIG, path: '.gitconfig') + create_file(<<~GITCONFIG, path: config_path) [CORE] excludesFile = ~/gitignore GITCONFIG - expect(described_class.parse('.gitconfig').excludesfile).to eq('~/gitignore') + expect(parsed_file.excludesfile).to eq('~/gitignore') end it 'returns value for file with excludesfile after attributesfile in quotes' do - create_file(<<~GITCONFIG, path: '.gitconfig') + create_file(<<~GITCONFIG, path: config_path) [core] attributesfile = "~/gitattributes" excludesfile = ~/gitignore GITCONFIG - expect(described_class.parse('.gitconfig').excludesfile).to eq('~/gitignore') + expect(parsed_file.excludesfile).to eq('~/gitignore') end it "doesn't return value for file with excludesfile after attributesfile with line continuation" do - create_file(<<~GITCONFIG, path: '.gitconfig') + create_file(<<~GITCONFIG, path: config_path) [core] attributesfile = ~/gitattributes\ excludesfile = ~/gitignore GITCONFIG - expect(described_class.parse('.gitconfig').excludesfile).to be_nil + expect(parsed_file.excludesfile).to be_nil end it 'returns earlier value for file with excludesfile after attributesfile with line continuation' do - create_file(<<~GITCONFIG, path: '.gitconfig') + create_file(<<~GITCONFIG, path: config_path) [core] excludesfile = ~/gitignore attributesfile = ~/gitattributes\ excludesfile = ~/gitignore2 GITCONFIG - expect(described_class.parse('.gitconfig').excludesfile).to eq('~/gitignore') + expect(parsed_file.excludesfile).to eq('~/gitignore') end it 'returns later value for file with multiple excludesfile' do - create_file(<<~GITCONFIG, path: '.gitconfig') + create_file(<<~GITCONFIG, path: config_path) [core] excludesfile = ~/gitignore excludesfile = ~/gitignore2 GITCONFIG - expect(described_class.parse('.gitconfig').excludesfile).to eq('~/gitignore2') + expect(parsed_file.excludesfile).to eq('~/gitignore2') end it 'returns value for file with excludesfile partially in quotes' do - create_file(<<~GITCONFIG, path: '.gitconfig') + create_file(<<~GITCONFIG, path: config_path) [core] excludesfile = ~/git"ignore" GITCONFIG - expect(described_class.parse('.gitconfig').excludesfile).to eq('~/gitignore') + expect(parsed_file.excludesfile).to eq('~/gitignore') end it 'returns value for file with excludesfile with escaped quote character' do - create_file(<<~GITCONFIG, path: '.gitconfig') + create_file(<<~GITCONFIG, path: config_path) [core] excludesfile = ~/git\\"ignore GITCONFIG - expect(described_class.parse('.gitconfig').excludesfile).to eq('~/git"ignore') + expect(parsed_file.excludesfile).to eq('~/git"ignore') end it 'returns value for file with excludesfile after attributesfile with escaped quote character' do - create_file(<<~GITCONFIG, path: '.gitconfig') + create_file(<<~GITCONFIG, path: config_path) [core] attributesfile = ~/git\\"attributes excludesfile = ~/gitignore GITCONFIG - expect(described_class.parse('.gitconfig').excludesfile).to eq('~/gitignore') + expect(parsed_file.excludesfile).to eq('~/gitignore') end it 'returns value for file with excludesfile with escaped newline (why)' do - create_file(<<~GITCONFIG, path: '.gitconfig') + create_file(<<~GITCONFIG, path: config_path) [core] excludesfile = ~/git\\nignore GITCONFIG - expect(described_class.parse('.gitconfig').excludesfile).to eq("~/git\nignore") + expect(parsed_file.excludesfile).to eq("~/git\nignore") end it 'returns value for file with excludesfile after attributesfile with escaped newline' do - create_file(<<~GITCONFIG, path: '.gitconfig') + create_file(<<~GITCONFIG, path: config_path) [core] attributesfile = ~/git\\nattributes excludesfile = ~/gitignore GITCONFIG - expect(described_class.parse('.gitconfig').excludesfile).to eq('~/gitignore') + expect(parsed_file.excludesfile).to eq('~/gitignore') end it 'returns value for file with excludesfile with escaped tab' do - create_file(<<~GITCONFIG, path: '.gitconfig') + create_file(<<~GITCONFIG, path: config_path) [core] excludesfile = ~/git\\tignore GITCONFIG - expect(described_class.parse('.gitconfig').excludesfile).to eq("~/git\tignore") + expect(parsed_file.excludesfile).to eq("~/git\tignore") end it 'returns value for file with excludesfile after attributesfile with escaped tab' do - create_file(<<~GITCONFIG, path: '.gitconfig') + create_file(<<~GITCONFIG, path: config_path) [core] attributesfile = ~/git\\tattributes excludesfile = ~/gitignore GITCONFIG - expect(described_class.parse('.gitconfig').excludesfile).to eq('~/gitignore') + expect(parsed_file.excludesfile).to eq('~/gitignore') end it 'returns value for file with excludesfile with literal space' do - create_file(<<~GITCONFIG, path: '.gitconfig') + create_file(<<~GITCONFIG, path: config_path) [core] excludesfile = ~/git ignore GITCONFIG - expect(described_class.parse('.gitconfig').excludesfile).to eq('~/git ignore') + expect(parsed_file.excludesfile).to eq('~/git ignore') end it 'returns value for file with excludesfile after attributesfile with literal space' do - create_file(<<~GITCONFIG, path: '.gitconfig') + create_file(<<~GITCONFIG, path: config_path) [core] attributesfile = ~/git attributes excludesfile = ~/gitignore GITCONFIG - expect(described_class.parse('.gitconfig').excludesfile).to eq('~/gitignore') + expect(parsed_file.excludesfile).to eq('~/gitignore') end # i suspect this may be incorrect and it should actually be turned into a literal space character. it 'returns value for file with excludesfile with literal tab' do - create_file(<<~GITCONFIG, path: '.gitconfig') + create_file(<<~GITCONFIG, path: config_path) [core] excludesfile = ~/git\tignore GITCONFIG - expect(described_class.parse('.gitconfig').excludesfile).to eq("~/git\tignore") + expect(parsed_file.excludesfile).to eq("~/git\tignore") end it 'returns value for file with excludesfile after attributesfile with literal tab' do - create_file(<<~GITCONFIG, path: '.gitconfig') + create_file(<<~GITCONFIG, path: config_path) [core] attributesfile = ~/git\tattributes excludesfile = ~/gitignore GITCONFIG - expect(described_class.parse('.gitconfig').excludesfile).to eq('~/gitignore') + expect(parsed_file.excludesfile).to eq('~/gitignore') end it 'returns value for file with excludesfile with literal backspace' do - create_file(<<~GITCONFIG, path: '.gitconfig') + create_file(<<~GITCONFIG, path: config_path) [core] excludesfile = ~/gith\\bignore GITCONFIG - expect(described_class.parse('.gitconfig').excludesfile).to eq('~/gitignore') + expect(parsed_file.excludesfile).to eq('~/gitignore') end it 'returns value for file with excludesfile after attributesfile with literal backspace' do - create_file(<<~GITCONFIG, path: '.gitconfig') + create_file(<<~GITCONFIG, path: config_path) [core] attributesfile = ~/git\battributes excludesfile = ~/gitignore GITCONFIG - expect(described_class.parse('.gitconfig').excludesfile).to eq('~/gitignore') + expect(parsed_file.excludesfile).to eq('~/gitignore') end it 'returns value for file with excludesfile with an escaped literal slash' do - create_file(<<~GITCONFIG, path: '.gitconfig') + create_file(<<~GITCONFIG, path: config_path) [core] excludesfile = ~/git\\\\ignore GITCONFIG - expect(described_class.parse('.gitconfig').excludesfile).to eq('~/git\\ignore') + expect(parsed_file.excludesfile).to eq('~/git\\ignore') end it 'returns value for file with excludesfile after attributesfile with escaped slash' do - create_file(<<~GITCONFIG, path: '.gitconfig') + create_file(<<~GITCONFIG, path: config_path) [core] attributesfile = ~/git\\\\attributes excludesfile = ~/gitignore GITCONFIG - expect(described_class.parse('.gitconfig').excludesfile).to eq('~/gitignore') + expect(parsed_file.excludesfile).to eq('~/gitignore') end it 'returns value for file with excludesfile with a ; comment' do - create_file(<<~GITCONFIG, path: '.gitconfig') + create_file(<<~GITCONFIG, path: config_path) [core] excludesfile = ~/gitignore ; comment GITCONFIG - expect(described_class.parse('.gitconfig').excludesfile).to eq('~/gitignore') + expect(parsed_file.excludesfile).to eq('~/gitignore') end it 'returns value for file with excludesfile with a ; comment with no space' do - create_file(<<~GITCONFIG, path: '.gitconfig') + create_file(<<~GITCONFIG, path: config_path) [core] excludesfile = ~/gitignore;comment GITCONFIG - expect(described_class.parse('.gitconfig').excludesfile).to eq('~/gitignore') + expect(parsed_file.excludesfile).to eq('~/gitignore') end it 'returns value for file with excludesfile with a # comment' do - create_file(<<~GITCONFIG, path: '.gitconfig') + create_file(<<~GITCONFIG, path: config_path) [core] excludesfile = ~/gitignore # comment GITCONFIG - expect(described_class.parse('.gitconfig').excludesfile).to eq('~/gitignore') + expect(parsed_file.excludesfile).to eq('~/gitignore') end it 'returns value for file with excludesfile with a # in quotes' do - create_file(<<~GITCONFIG, path: '.gitconfig') + create_file(<<~GITCONFIG, path: config_path) [core] excludesfile = "~/git#ignore" GITCONFIG - expect(described_class.parse('.gitconfig').excludesfile).to eq('~/git#ignore') + expect(parsed_file.excludesfile).to eq('~/git#ignore') end it 'returns value for file with excludesfile with a ; in quotes' do - create_file(<<~GITCONFIG, path: '.gitconfig') + create_file(<<~GITCONFIG, path: config_path) [core] excludesfile = "~/git;ignore" GITCONFIG - expect(described_class.parse('.gitconfig').excludesfile).to eq('~/git;ignore') + expect(parsed_file.excludesfile).to eq('~/git;ignore') end it 'returns value with no trailing whitespace' do - create_file("[core]\n excludesfile = ~/gitignore \n", path: '.gitconfig') + create_file("[core]\n excludesfile = ~/gitignore \n", path: config_path) - expect(described_class.parse('.gitconfig').excludesfile).to eq('~/gitignore') + expect(parsed_file.excludesfile).to eq('~/gitignore') end it 'returns value for file with trailing whitespace when quoted' do - create_file(<<~GITCONFIG, path: '.gitconfig') + create_file(<<~GITCONFIG, path: config_path) [core] excludesfile = "~/gitignore " GITCONFIG - expect(described_class.parse('.gitconfig').excludesfile).to eq('~/gitignore ') + expect(parsed_file.excludesfile).to eq('~/gitignore ') end it 'continues with escaped newlines' do - create_file(<<~GITCONFIG, path: '.gitconfig') + create_file(<<~GITCONFIG, path: config_path) [core] excludesfile = ~/git\\ ignore GITCONFIG - expect(described_class.parse('.gitconfig').excludesfile).to eq('~/gitignore') + expect(parsed_file.excludesfile).to eq('~/gitignore') end it 'raises for file with unclosed quote' do - create_file(<<~GITCONFIG, path: '.gitconfig') + create_file(<<~GITCONFIG, path: config_path) [core] excludesfile = "~/gitignore GITCONFIG - expect { described_class.parse('.gitconfig') }.to raise_error(PathList::Gitconfig::ParseError) do |e| + expect { parsed_file }.to raise_error(PathList::Gitconfig::ParseError) do |e| expect(e.message).to eq <<~MESSAGE Unexpected character in quoted value .gitconfig:2:29 @@ -453,12 +458,12 @@ end it 'raises for file with unclosed quote and no trailing newline' do - create_file(<<~GITCONFIG.chomp, path: '.gitconfig') + create_file(<<~GITCONFIG.chomp, path: config_path) [core] excludesfile = "~/gitignore GITCONFIG - expect { described_class.parse('.gitconfig') }.to raise_error(PathList::Gitconfig::ParseError) do |e| + expect { parsed_file }.to raise_error(PathList::Gitconfig::ParseError) do |e| expect(e.message).to eq <<~MESSAGE Unclosed quoted value .gitconfig:2:29 @@ -469,13 +474,13 @@ end it 'raises for file with excludesfile after attributesfile with unclosed quote' do - create_file(<<~GITCONFIG, path: '.gitconfig') + create_file(<<~GITCONFIG, path: config_path) [core] attributesfile = "~/gitattributes excludesfile = ~/gitignore GITCONFIG - expect { described_class.parse('.gitconfig') }.to raise_error(PathList::Gitconfig::ParseError) do |e| + expect { parsed_file }.to raise_error(PathList::Gitconfig::ParseError) do |e| expect(e.message).to eq <<~MESSAGE Unexpected character in quoted value .gitconfig:2:35 @@ -486,13 +491,13 @@ end it 'raises for file with excludesfile before attributesfile with unclosed quote and no trailing newline' do - create_file(<<~GITCONFIG.chomp, path: '.gitconfig') + create_file(<<~GITCONFIG.chomp, path: config_path) [core] excludesfile = ~/gitignore attributesfile = "~/gitattributes GITCONFIG - expect { described_class.parse('.gitconfig') }.to raise_error(PathList::Gitconfig::ParseError) do |e| + expect { parsed_file }.to raise_error(PathList::Gitconfig::ParseError) do |e| expect(e.message).to eq <<~MESSAGE Unclosed quoted value .gitconfig:3:35 @@ -503,13 +508,13 @@ end it 'raises for file with unclosed quote followed by more stuff' do - create_file(<<~GITCONFIG, path: '.gitconfig') + create_file(<<~GITCONFIG, path: config_path) [core] excludesfile = "~/gitignore mergeoptions = --no-edit GITCONFIG - expect { described_class.parse('.gitconfig') }.to raise_error(PathList::Gitconfig::ParseError) do |e| + expect { parsed_file }.to raise_error(PathList::Gitconfig::ParseError) do |e| expect(e.message).to eq <<~MESSAGE Unexpected character in quoted value .gitconfig:2:29 @@ -520,13 +525,13 @@ end it 'raises for file with quote containing a newline' do - create_file(<<~GITCONFIG, path: '.gitconfig') + create_file(<<~GITCONFIG, path: config_path) [core] excludesfile = "~/git ignore" GITCONFIG - expect { described_class.parse('.gitconfig') }.to raise_error(PathList::Gitconfig::ParseError) do |e| + expect { parsed_file }.to raise_error(PathList::Gitconfig::ParseError) do |e| expect(e.message).to eq <<~MESSAGE Unexpected character in quoted value .gitconfig:2:23 @@ -537,14 +542,14 @@ end it 'raises for file with excludesfile after attributesfile with quoted newline' do - create_file(<<~GITCONFIG, path: '.gitconfig') + create_file(<<~GITCONFIG, path: config_path) [core] attributesfile = "~/git attributes" excludesfile = ~/gitignore GITCONFIG - expect { described_class.parse('.gitconfig') }.to raise_error(PathList::Gitconfig::ParseError) do |e| + expect { parsed_file }.to raise_error(PathList::Gitconfig::ParseError) do |e| expect(e.message).to eq <<~MESSAGE Unexpected character in quoted value .gitconfig:2:25 @@ -555,12 +560,12 @@ end it 'raises for file with invalid \ escape' do - create_file(<<~GITCONFIG, path: '.gitconfig') + create_file(<<~GITCONFIG, path: config_path) [core] excludesfile = "~/gitignore\\x" GITCONFIG - expect { described_class.parse('.gitconfig') }.to raise_error(PathList::Gitconfig::ParseError) do |e| + expect { parsed_file }.to raise_error(PathList::Gitconfig::ParseError) do |e| expect(e.message).to eq <<~MESSAGE Unrecognized escape sequence in value .gitconfig:2:30 @@ -571,13 +576,13 @@ end it 'raises for file with excludesfile after attributesfile with invalid escape' do - create_file(<<~GITCONFIG, path: '.gitconfig') + create_file(<<~GITCONFIG, path: config_path) [core] attributesfile = "~/git\\xattributes excludesfile = ~/gitignore GITCONFIG - expect { described_class.parse('.gitconfig') }.to raise_error(PathList::Gitconfig::ParseError) do |e| + expect { parsed_file }.to raise_error(PathList::Gitconfig::ParseError) do |e| expect(e.message).to eq <<~MESSAGE Unrecognized escape sequence in value .gitconfig:2:26 @@ -588,7 +593,7 @@ end it 'returns value for file when included' do - create_file(<<~GITCONFIG, path: '.gitconfig') + create_file(<<~GITCONFIG, path: config_path) [include] path = .gitconfig_include GITCONFIG @@ -598,11 +603,11 @@ excludesfile = ~/.gitignore GITCONFIG - expect(described_class.parse('.gitconfig').excludesfile).to eq('~/.gitignore') + expect(parsed_file.excludesfile).to eq('~/.gitignore') end it 'returns value for file when includeif onbranch' do - create_file(<<~GITCONFIG, path: '.gitconfig') + create_file(<<~GITCONFIG, path: config_path) [includeif "onbranch:main"] path = .gitconfig_include GITCONFIG @@ -614,11 +619,11 @@ excludesfile = ~/.gitignore GITCONFIG - expect(described_class.parse('.gitconfig').excludesfile).to eq('~/.gitignore') + expect(parsed_file.excludesfile).to eq('~/.gitignore') end it 'returns value for file when includeif onbranch pattern' do - create_file(<<~GITCONFIG, path: '.gitconfig') + create_file(<<~GITCONFIG, path: config_path) [includeif "onbranch:m*"] path = .gitconfig_include GITCONFIG @@ -630,11 +635,11 @@ excludesfile = ~/.gitignore GITCONFIG - expect(described_class.parse('.gitconfig').excludesfile).to eq('~/.gitignore') + expect(parsed_file.excludesfile).to eq('~/.gitignore') end it 'returns value for file when includeif onbranch pattern ending in /' do - create_file(<<~GITCONFIG, path: '.gitconfig') + create_file(<<~GITCONFIG, path: config_path) [includeif "onbranch:feature/"] path = .gitconfig_include GITCONFIG @@ -646,11 +651,11 @@ excludesfile = ~/.gitignore GITCONFIG - expect(described_class.parse('.gitconfig').excludesfile).to eq('~/.gitignore') + expect(parsed_file.excludesfile).to eq('~/.gitignore') end it 'returns nil for file when includeif onbranch is not the right branch' do - create_file(<<~GITCONFIG, path: '.gitconfig') + create_file(<<~GITCONFIG, path: config_path) [includeif "onbranch:main"] path = .gitconfig_include GITCONFIG @@ -662,11 +667,11 @@ excludesfile = ~/.gitignore GITCONFIG - expect(described_class.parse('.gitconfig').excludesfile).to be_nil + expect(parsed_file.excludesfile).to be_nil end it 'returns nil for file when includeif nonsense' do - create_file(<<~GITCONFIG, path: '.gitconfig') + create_file(<<~GITCONFIG, path: config_path) [includeif "nonsense"] path = .gitconfig_include GITCONFIG @@ -678,11 +683,11 @@ excludesfile = ~/.gitignore GITCONFIG - expect(described_class.parse('.gitconfig').excludesfile).to be_nil + expect(parsed_file.excludesfile).to be_nil end it 'returns nil for file when includeif onbranch and no .git dir' do - create_file(<<~GITCONFIG, path: '.gitconfig') + create_file(<<~GITCONFIG, path: config_path) [includeif "onbranch:main"] path = .gitconfig_include GITCONFIG @@ -692,11 +697,11 @@ excludesfile = ~/.gitignore GITCONFIG - expect(described_class.parse('.gitconfig').excludesfile).to be_nil + expect(parsed_file.excludesfile).to be_nil end it 'raises for file when includeif onbranch with newline' do - create_file(<<~GITCONFIG, path: '.gitconfig') + create_file(<<~GITCONFIG, path: config_path) [includeif "onbranch:ma in"] path = .gitconfig_include @@ -707,7 +712,7 @@ excludesfile = ~/.gitignore GITCONFIG - expect { described_class.parse('.gitconfig') }.to raise_error(PathList::Gitconfig::ParseError) do |e| + expect { parsed_file }.to raise_error(PathList::Gitconfig::ParseError) do |e| expect(e.message).to eq <<~MESSAGE Unexpected character in condition .gitconfig:1:21 @@ -718,7 +723,7 @@ end it 'raises for file when includeif nonsense with newline' do - create_file(<<~GITCONFIG, path: '.gitconfig') + create_file(<<~GITCONFIG, path: config_path) [includeif "nonsense in"] path = .gitconfig_include @@ -729,7 +734,7 @@ excludesfile = ~/.gitignore GITCONFIG - expect { described_class.parse('.gitconfig') }.to raise_error(PathList::Gitconfig::ParseError) do |e| + expect { parsed_file }.to raise_error(PathList::Gitconfig::ParseError) do |e| expect(e.message).to eq <<~MESSAGE Unexpected character in condition .gitconfig:1:12 @@ -740,7 +745,7 @@ end it 'raises for file when includeif onbranch with null' do - create_file(<<~GITCONFIG, path: '.gitconfig') + create_file(<<~GITCONFIG, path: config_path) [includeif "onbranch:ma\0in"] path = .gitconfig_include GITCONFIG @@ -750,7 +755,7 @@ excludesfile = ~/.gitignore GITCONFIG - expect { described_class.parse('.gitconfig') }.to raise_error(PathList::Gitconfig::ParseError) do |e| + expect { parsed_file }.to raise_error(PathList::Gitconfig::ParseError) do |e| expect(e.message).to eq <<~MESSAGE Unexpected character in condition .gitconfig:1:21 @@ -761,7 +766,7 @@ end it 'returns value for file when includeif gitdir matches leading **/' do - create_file(<<~GITCONFIG, path: '.gitconfig') + create_file(<<~GITCONFIG, path: config_path) [includeif "gitdir:**/.git"] path = .gitconfig_include GITCONFIG @@ -771,11 +776,11 @@ excludesfile = ~/.gitignore GITCONFIG - expect(described_class.parse('.gitconfig').excludesfile).to eq('~/.gitignore') + expect(parsed_file.excludesfile).to eq('~/.gitignore') end it 'returns value for file when includeif gitdir/i matches leading **/' do - create_file(<<~GITCONFIG, path: '.gitconfig') + create_file(<<~GITCONFIG, path: config_path) [includeif "gitdir/i:**/.GIT"] path = .gitconfig_include GITCONFIG @@ -785,11 +790,11 @@ excludesfile = ~/.gitignore GITCONFIG - expect(described_class.parse('.gitconfig').excludesfile).to eq('~/.gitignore') + expect(parsed_file.excludesfile).to eq('~/.gitignore') end it 'returns value for file when includeif gitdir matches trailing /' do - create_file(<<~GITCONFIG, path: '.gitconfig') + create_file(<<~GITCONFIG, path: config_path) [includeif "gitdir:#{Dir.pwd}/"] path = .gitconfig_include GITCONFIG @@ -799,11 +804,11 @@ excludesfile = ~/.gitignore GITCONFIG - expect(described_class.parse('.gitconfig').excludesfile).to eq('~/.gitignore') + expect(parsed_file.excludesfile).to eq('~/.gitignore') end it "doesn't leak the section for file when included" do - create_file(<<~GITCONFIG, path: '.gitconfig') + create_file(<<~GITCONFIG, path: config_path) [core] excludesfile = ~/.gitignore [include] @@ -816,11 +821,11 @@ attributesfile = ~/.gitattributes GITCONFIG - expect(described_class.parse('.gitconfig').excludesfile).to eq('~/.gitignore') + expect(parsed_file.excludesfile).to eq('~/.gitignore') end it 'returns the most recent value when included' do - create_file(<<~GITCONFIG, path: '.gitconfig') + create_file(<<~GITCONFIG, path: config_path) [core] excludesfile = ~/.gitignore [include] @@ -832,11 +837,11 @@ excludesfile = ~/.gitignore2 GITCONFIG - expect(described_class.parse('.gitconfig').excludesfile).to eq('~/.gitignore2') + expect(parsed_file.excludesfile).to eq('~/.gitignore2') end it 'returns the most recent value after included' do - create_file(<<~GITCONFIG, path: '.gitconfig') + create_file(<<~GITCONFIG, path: config_path) [include] path = .gitconfig_include @@ -849,21 +854,21 @@ excludesfile = ~/.gitignore2 GITCONFIG - expect(described_class.parse('.gitconfig').excludesfile).to eq('~/.gitignore') + expect(parsed_file.excludesfile).to eq('~/.gitignore') end it 'raises when including itself' do - create_file(<<~GITCONFIG, path: '.gitconfig') + create_file(<<~GITCONFIG, path: config_path) [include] path = .gitconfig GITCONFIG - expect { described_class.parse('.gitconfig') } + expect { parsed_file } .to raise_error(PathList::Gitconfig::ParseError, "Include level too deep #{Dir.pwd}/.gitconfig") end it 'returns value for file when included nestedly' do - create_file(<<~GITCONFIG, path: '.gitconfig') + create_file(<<~GITCONFIG, path: config_path) [include] path = .gitconfig_include_1 GITCONFIG @@ -878,6 +883,6 @@ excludesfile = ~/.gitignore GITCONFIG - expect(described_class.parse('.gitconfig').excludesfile).to eq('~/.gitignore') + expect(parsed_file.excludesfile).to eq('~/.gitignore') end end diff --git a/spec/path_list_git_submodule_spec.rb b/spec/path_list_git_submodule_spec.rb index 9f90a2d..227646f 100644 --- a/spec/path_list_git_submodule_spec.rb +++ b/spec/path_list_git_submodule_spec.rb @@ -6,9 +6,9 @@ let(:root) { Dir.pwd } shared_examples 'gitignore' do - let(:parent_repo) { RealGit.new('./parent_repo') } - let(:submodule_foo) { RealGit.new('./submodule_foo') } - let(:submodule_bar) { RealGit.new('./submodule_bar') } + let(:parent_repo) { real_git('./parent_repo') } + let(:submodule_foo) { real_git('./submodule_foo') } + let(:submodule_bar) { real_git('./submodule_bar') } before do submodule_bar.commit('--allow-empty') @@ -17,10 +17,42 @@ parent_repo.add_submodule(submodule_foo.path) end - # NOTE: .git is a file when a submodule - it 'ignore .git in submodule' do - subject + it 'considers patterns in the global config is relative to submodule root' do + gitignore '/a', path: '.global_gitignore' + parent_repo.configure_excludesfile("#{Dir.pwd}/.global_gitignore") + parent_repo.configure_excludesfile("#{Dir.pwd}/.global_gitignore", chdir: "#{Dir.pwd}/parent_repo/submodule_foo") + parent_repo.configure_excludesfile("#{Dir.pwd}/.global_gitignore", + chdir: "#{Dir.pwd}/parent_repo/submodule_foo/submodule_bar") + + create_file_list( + 'parent_repo/a', + 'parent_repo/b/a', + 'parent_repo/submodule_foo/a', + 'parent_repo/submodule_foo/b/a', + 'parent_repo/submodule_foo/submodule_bar/a', + 'parent_repo/submodule_foo/submodule_bar/b/a' + ) + + Dir.chdir(parent_repo.path) do + require 'pry' + binding.pry + + expect(subject).to match_files( + 'a', + 'submodule_foo/a', + 'submodule_foo/submodule_bar/a' + ) + + expect(subject).not_to match_files( + 'parent_repo/b/a', + 'parent_repo/submodule_foo/b/a', + 'parent_repo/submodule_foo/submodule_bar/b/a' + ) + end + end + + it 'config relative to submodule' do Dir.chdir(parent_repo.path) do expect(subject).to match_files( '.git/WHATEVER', @@ -38,7 +70,7 @@ end describe '.gitignore' do - subject { described_class.gitignore(root: './parent') } + subject { described_class.gitignore(root: parent_repo.path) } it_behaves_like 'gitignore' end diff --git a/spec/path_list_gitignore_patterns_spec.rb b/spec/path_list_gitignore_patterns_spec.rb index 3dac421..38344f4 100644 --- a/spec/path_list_gitignore_patterns_spec.rb +++ b/spec/path_list_gitignore_patterns_spec.rb @@ -1070,7 +1070,7 @@ end describe 'git ls-files', :real_git do - subject { RealGit.new } + subject { real_git } it_behaves_like 'the gitignore documentation' end diff --git a/spec/path_list_gitignore_spec.rb b/spec/path_list_gitignore_spec.rb index 15aaec3..3101c81 100644 --- a/spec/path_list_gitignore_spec.rb +++ b/spec/path_list_gitignore_spec.rb @@ -78,7 +78,7 @@ end describe 'git ls-files', :real_git do - subject { RealGit.new } + subject { real_git } it_behaves_like 'gitignore' end diff --git a/spec/path_list_spec.rb b/spec/path_list_spec.rb index 8d61ba8..6075cf5 100644 --- a/spec/path_list_spec.rb +++ b/spec/path_list_spec.rb @@ -186,8 +186,8 @@ 10.times { described_class.gitignore(root: '..') } expect(PathList::Gitignore).to have_received(:new).exactly(2).times - expect(PathList::Gitignore).to have_received(:new).with(root: nil, config: true).once - expect(PathList::Gitignore).to have_received(:new).with(root: '..', config: true).once + expect(PathList::Gitignore).to have_received(:new).with(root: Dir.pwd, config: true).once + expect(PathList::Gitignore).to have_received(:new).with(root: File.dirname(Dir.pwd), config: true).once end it 'caches fs calls with different config arg separately when setting up the matcher' do @@ -198,8 +198,8 @@ 10.times { described_class.gitignore(config: false) } expect(PathList::Gitignore).to have_received(:new).exactly(2).times - expect(PathList::Gitignore).to have_received(:new).with(root: nil, config: true).once - expect(PathList::Gitignore).to have_received(:new).with(root: nil, config: false).once + expect(PathList::Gitignore).to have_received(:new).with(root: Dir.pwd, config: true).once + expect(PathList::Gitignore).to have_received(:new).with(root: Dir.pwd, config: false).once end it 'returns all files when there is no gitignore' do @@ -212,7 +212,11 @@ gitignore 'foo', 'bar/' allow(PathList::CanonicalPath).to receive(:case_insensitive?).and_return(true) - expect(subject.send(:dir_matcher)).to be_like PathList::Matcher::LastMatch::Two.new([ + expect(subject.send(:dir_matcher)).to be_like PathList::Matcher::All::Two.new([ + PathList::Matcher::LastMatch::Two.new([ + PathList::Matcher::Allow, + PathList::Matcher::PathRegexp::CaseInsensitive.new(%r{/\.git\z}, :ignore) + ]), PathList::Matcher::CollectGitignore.new( PathList::Matcher::PathRegexp::CaseInsensitive.new(%r{\A#{Regexp.escape(Dir.pwd).downcase}(?:\z|/)}, :allow), PathList::Matcher::Mutable.new( @@ -223,25 +227,34 @@ ) ]) ) - ), - PathList::Matcher::PathRegexp::CaseInsensitive.new(%r{/\.git\z}, :ignore) + ) ]) - expect(subject.send(:file_matcher)).to be_like PathList::Matcher::Mutable.new( + expect(subject.send(:file_matcher)).to be_like PathList::Matcher::All::Two.new([ PathList::Matcher::LastMatch::Two.new([ PathList::Matcher::Allow, - PathList::Matcher::PathRegexp::CaseInsensitive.new( - %r{\A#{Regexp.escape(Dir.pwd).downcase}/(?:.*/)?foo\z}, :ignore - ) - ]) - ) + PathList::Matcher::PathRegexp::CaseInsensitive.new(%r{/\.git\z}, :ignore) + ]), + PathList::Matcher::Mutable.new( + PathList::Matcher::LastMatch::Two.new([ + PathList::Matcher::Allow, + PathList::Matcher::PathRegexp::CaseInsensitive.new( + %r{\A#{Regexp.escape(Dir.pwd).downcase}/(?:.*/)?foo\z}, :ignore + ) + ]) + ) + ]) end it 'creates a sensible list of matchers when case sensitive' do gitignore 'foo', 'bar/' allow(PathList::CanonicalPath).to receive(:case_insensitive?).and_return(false) - expect(subject.send(:dir_matcher)).to be_like PathList::Matcher::LastMatch::Two.new([ + expect(subject.send(:dir_matcher)).to be_like PathList::Matcher::All::Two.new([ + PathList::Matcher::LastMatch::Two.new([ + PathList::Matcher::Allow, + PathList::Matcher::PathRegexp.new(%r{/\.git\z}, :ignore) + ]), PathList::Matcher::CollectGitignore.new( PathList::Matcher::PathRegexp.new(%r{\A#{Regexp.escape(Dir.pwd)}(?:\z|/)}, :allow), PathList::Matcher::Mutable.new( @@ -252,16 +265,21 @@ ) ]) ) - ), - PathList::Matcher::PathRegexp.new(%r{/\.git\z}, :ignore) + ) ]) - expect(subject.send(:file_matcher)).to be_like PathList::Matcher::Mutable.new( + expect(subject.send(:file_matcher)).to be_like PathList::Matcher::All::Two.new([ PathList::Matcher::LastMatch::Two.new([ PathList::Matcher::Allow, - PathList::Matcher::PathRegexp.new(%r{\A#{Regexp.escape(Dir.pwd)}/(?:.*/)?foo\z}, :ignore) - ]) - ) + PathList::Matcher::PathRegexp.new(%r{/\.git\z}, :ignore) + ]), + PathList::Matcher::Mutable.new( + PathList::Matcher::LastMatch::Two.new([ + PathList::Matcher::Allow, + PathList::Matcher::PathRegexp.new(%r{\A#{Regexp.escape(Dir.pwd)}/(?:.*/)?foo\z}, :ignore) + ]) + ) + ]) end it 'can match files with case equality' do @@ -469,7 +487,8 @@ describe 'builder interface combinations' do it 'caches fs calls regardless of how things are built' do - core_excludes = PathList::Gitconfig::CoreExcludesfile.path(repo_root: '.') + gitignore 'x', path: '.git/info/exclude' + core_excludes = PathList::Gitconfig::CoreExcludesfile.path(git_dir: '.git') allow(PathList::PatternParser).to receive(:new).and_call_original allow(PathList::Gitignore).to receive(:new).and_call_original @@ -480,7 +499,6 @@ 10.times { described_class.only('a').intersection(described_class.gitignore, described_class.only('a')) } expect(PathList::Gitignore).to have_received(:new).once - expect(PathList::PatternParser).to have_received(:new).exactly(4).times expect(PathList::PatternParser) .to have_received(:new) .with(hash_including(patterns: ['a'], polarity: :allow)) @@ -497,6 +515,7 @@ .to have_received(:new) .with(hash_including(patterns_from_file: core_excludes, polarity: :ignore)) .once + expect(PathList::PatternParser).to have_received(:new).exactly(4).times end it 'works for .gitignore and #only' do diff --git a/spec/support/real_git.rb b/spec/support/real_git.rb index 33512fc..3c712f4 100644 --- a/spec/support/real_git.rb +++ b/spec/support/real_git.rb @@ -5,32 +5,38 @@ class RealGit attr_reader :path - def initialize(path = '.') + def initialize(path = '.', env) @path = ::File.expand_path(path) + @env = env FileUtils.mkpath(@path) git('init') + configure_excludesfile('') end - def git(*subcommand, chdir: @path, out: File::NULL, err: File::NULL, **options) + def git(*subcommand, **options) system( + @env.transform_keys(&:to_s), 'git', '-c', "core.hooksPath=''", - '-c', "core.excludesFile=''", *subcommand, - out: out, - err: err, - chdir: chdir, + chdir: @path, + out: File::NULL, + err: File::NULL, **options ) end + def configure_excludesfile(path, **options) + git('config', '--local', 'core.excludesfile', path, **options) + end + def add(*args) git('add', '.', *args) end def commit(*args) add - git('commit', '-m', 'Commit', *args) + git('commit', '-m', 'Commit', '--no-verify', *args) end def add_submodule(path) @@ -64,3 +70,13 @@ def to_a ls_files end end + +module RealGitHelper + def real_git(path = '.') + RealGit.new(path, stubbed_env) + end +end + +RSpec.configure do |config| + config.include RealGitHelper +end diff --git a/spec/support/stub_env_helper.rb b/spec/support/stub_env_helper.rb index 07ed324..19cae94 100644 --- a/spec/support/stub_env_helper.rb +++ b/spec/support/stub_env_helper.rb @@ -13,6 +13,12 @@ def stub_env(**values) values.each do |key, value| allow(::ENV).to receive(:[]).with(key.to_s).at_least(:once).and_return(value) end + + stubbed_env.merge!(values) + end + + def stubbed_env + @stubbed_env ||= {} end end