From b0f5b5d90bb0a47724e622aa7afd09e747cd4662 Mon Sep 17 00:00:00 2001 From: Shane da Silva Date: Tue, 10 Nov 2026 22:32:53 -0800 Subject: [PATCH 1/2] Complete branch coverage and enforce aggregated coverage --- .github/workflows/tests.yml | 4 +- AGENTS.md | 4 +- lib/overcommit/cli.rb | 4 +- .../hook/pre_commit/chamber_verification.rb | 6 +- .../hook/pre_commit/php_cs_fixer.rb | 4 +- lib/overcommit/hook/pre_commit/php_lint.rb | 11 +-- lib/overcommit/hook_context/pre_push.rb | 2 +- lib/overcommit/installer.rb | 2 - lib/overcommit/utils/messages_utils.rb | 2 - script/check_coverage.rb | 21 +++++- spec/coverage_check_spec.rb | 69 +++++++++++++++++++ spec/overcommit/cli_errors_spec.rb | 23 +++++++ .../configuration_validator_spec.rb | 11 +++ spec/overcommit/gem_plugin_spec.rb | 6 ++ spec/overcommit/git_config_spec.rb | 9 +++ spec/overcommit/git_repo_spec.rb | 52 ++++++++++++++ .../hook/post_commit/git_guilt_spec.rb | 7 ++ .../hook/pre_commit/bundle_check_spec.rb | 10 +++ .../hook/pre_commit/case_conflicts_spec.rb | 7 ++ .../pre_commit/chamber_verification_spec.rb | 12 ++++ spec/overcommit/hook/pre_commit/dogma_spec.rb | 7 ++ .../pre_commit/execute_permissions_spec.rb | 15 ++++ spec/overcommit/hook/pre_commit/flay_spec.rb | 17 +++++ .../hook/pre_commit/foodcritic_spec.rb | 18 +++++ .../hook/pre_commit/herb_lint_spec.rb | 15 ++++ spec/overcommit/hook/pre_commit/jscs_spec.rb | 5 ++ .../hook/pre_commit/line_endings_spec.rb | 21 ++++++ .../hook/pre_commit/phpcs_fixer_spec.rb | 5 ++ spec/overcommit/hook/pre_commit/phpcs_spec.rb | 12 ++++ .../hook/pre_commit/ruby_syntax_spec.rb | 8 +++ .../hook/pre_commit/scss_lint_spec.rb | 14 ++++ .../hook/pre_commit/w3c_css_spec.rb | 12 ++++ .../hook/pre_commit/w3c_html_spec.rb | 11 +++ .../hook/pre_commit/yard_coverage_spec.rb | 13 ++++ .../hook/pre_commit/yarn_check_spec.rb | 11 +++ .../hook/prepare_commit_msg/base_spec.rb | 6 ++ .../prepare_commit_msg/replace_branch_spec.rb | 10 +++ spec/overcommit/hook_context/base_spec.rb | 7 ++ .../helpers/hide_unstaged_changes_spec.rb | 12 ++++ .../hook_context/post_commit_spec.rb | 6 ++ .../hook_context/pre_commit_spec.rb | 16 +++++ spec/overcommit/hook_context/pre_push_spec.rb | 13 ++++ .../hook_context/pre_rebase_spec.rb | 7 ++ spec/overcommit/hook_runner_spec.rb | 28 ++++++++ spec/overcommit/hook_signer_files_spec.rb | 5 ++ spec/overcommit/installer_spec.rb | 16 +++++ spec/overcommit/logger_spec.rb | 5 ++ spec/overcommit/message_processor_spec.rb | 8 +++ spec/overcommit/printer_spec.rb | 6 ++ spec/overcommit/signature_spec.rb | 9 +++ spec/overcommit/subprocess_spec.rb | 5 ++ spec/overcommit/utils/messages_errors_spec.rb | 6 ++ spec/overcommit/utils_errors_spec.rb | 29 ++++++++ 53 files changed, 619 insertions(+), 25 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 625c918f..66708031 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -125,8 +125,8 @@ jobs: pattern: coverage-* path: coverage - - name: Require 100% combined line coverage - run: ruby script/check_coverage.rb coverage/*/lcov.info + - name: Require 100% combined line and branch coverage + run: ruby script/check_coverage.rb --branches coverage/*/lcov.info # Shard assignments shift when examples are added. Compare the complete # suite's coverage rather than unrelated fractions from individual shards. diff --git a/AGENTS.md b/AGENTS.md index 24a2a777..a250020c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,8 @@ # Repository guidance -- Maintain **100% line coverage** across the complete test suite. Add thorough +- Maintain **100% line and branch coverage**, aggregated across the complete + test suite and all CI platforms, Ruby versions, and shards. Individual shards + need not reach 100%; the combined coverage report must. Add thorough regression tests for new behavior and failure paths; do not exclude production code from coverage or lower the threshold to make checks pass. - Preserve existing tests unless they specifically test an implementation that diff --git a/lib/overcommit/cli.rb b/lib/overcommit/cli.rb index d889a353..bc11b993 100644 --- a/lib/overcommit/cli.rb +++ b/lib/overcommit/cli.rb @@ -21,8 +21,6 @@ def run # rubocop:disable Metrics/MethodLength parse_arguments case @options[:action] - when :install, :uninstall - install_or_uninstall when :template_dir print_template_directory_path when :sign @@ -31,6 +29,8 @@ def run # rubocop:disable Metrics/MethodLength run_all when :diff diff + else + install_or_uninstall end rescue Overcommit::Exceptions::ConfigurationSignatureChanged => e puts e diff --git a/lib/overcommit/hook/pre_commit/chamber_verification.rb b/lib/overcommit/hook/pre_commit/chamber_verification.rb index 8581c063..a0aca614 100644 --- a/lib/overcommit/hook/pre_commit/chamber_verification.rb +++ b/lib/overcommit/hook/pre_commit/chamber_verification.rb @@ -4,7 +4,7 @@ module Overcommit::Hook::PreCommit # Runs `chamber sign --verify`. # # @see https://github.com/thekompanee/chamber/wiki/Git-Commit-Hooks#chamber-verification-pre-commit-hook - # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity + # rubocop:disable Metrics/CyclomaticComplexity class ChamberVerification < Base def run approver_name = config.fetch('approver_name') { 'your approver' } @@ -22,7 +22,7 @@ def run compact. join("\n\n") - output = "\n\n#{output}" unless output.empty? + output = "\n\n#{output}" [ :warn, @@ -32,5 +32,5 @@ def run ] end end - # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity + # rubocop:enable Metrics/CyclomaticComplexity end diff --git a/lib/overcommit/hook/pre_commit/php_cs_fixer.rb b/lib/overcommit/hook/pre_commit/php_cs_fixer.rb index 87e63744..2e978b51 100644 --- a/lib/overcommit/hook/pre_commit/php_cs_fixer.rb +++ b/lib/overcommit/hook/pre_commit/php_cs_fixer.rb @@ -17,9 +17,7 @@ def run output = result.stdout.chomp exit_status_sum += result.status - if result.status - messages = output.lstrip.split("\n") - end + messages = output.lstrip.split("\n") end unless messages.empty? diff --git a/lib/overcommit/hook/pre_commit/php_lint.rb b/lib/overcommit/hook/pre_commit/php_lint.rb index f2ba2f24..9a44e954 100644 --- a/lib/overcommit/hook/pre_commit/php_lint.rb +++ b/lib/overcommit/hook/pre_commit/php_lint.rb @@ -21,19 +21,14 @@ def run result = execute(command, args: [file]) output = result.stdout.chomp exit_status_sum += result.status - if result.status - # `php -l` returns with a leading newline, and we only need the first - # line, there is usually some redundancy - messages << output.lstrip.split("\n").first - end + # `php -l` returns with a leading newline, and we only need the first + # line, there is usually some redundancy + messages << output.lstrip.split("\n").first end # If the sum of all lint status is zero, then none had exit status return :pass if exit_status_sum == 0 - # No messages is great news for us - return :pass if messages.empty? - # Return the list of message objects extract_messages( messages, diff --git a/lib/overcommit/hook_context/pre_push.rb b/lib/overcommit/hook_context/pre_push.rb index 39ec19ba..f5dc1d6f 100644 --- a/lib/overcommit/hook_context/pre_push.rb +++ b/lib/overcommit/hook_context/pre_push.rb @@ -79,7 +79,7 @@ def overwritten_commits result = Overcommit::Subprocess.spawn(%W[git rev-list #{remote_sha1} ^#{local_sha1}]) if result.success? - result.stdout.split("\n") + @overwritten_commits = result.stdout.split("\n") else raise Overcommit::Exceptions::GitRevListError, "Unable to check if commits on the remote ref will be overwritten: #{result.stderr}" diff --git a/lib/overcommit/installer.rb b/lib/overcommit/installer.rb index 79287b30..f9966692 100644 --- a/lib/overcommit/installer.rb +++ b/lib/overcommit/installer.rb @@ -129,8 +129,6 @@ def can_replace_file?(file) end def preserve_old_hooks - return unless File.directory?(hooks_path) - ensure_directory(old_hooks_path) Overcommit::Utils.supported_hook_types.each do |hook_type| hook_file = File.join(hooks_path, hook_type) diff --git a/lib/overcommit/utils/messages_utils.rb b/lib/overcommit/utils/messages_utils.rb index b83d3ac0..86562f96 100644 --- a/lib/overcommit/utils/messages_utils.rb +++ b/lib/overcommit/utils/messages_utils.rb @@ -65,8 +65,6 @@ def extract_file(match, message) end def extract_line(match, message) - return unless match.names.include?('line') - Integer(match[:line]) rescue ArgumentError, TypeError raise Overcommit::Exceptions::MessageProcessingError, diff --git a/script/check_coverage.rb b/script/check_coverage.rb index ced30509..ba0c053a 100644 --- a/script/check_coverage.rb +++ b/script/check_coverage.rb @@ -1,9 +1,11 @@ # frozen_string_literal: true -# Merge LCOV line hits across platforms and shards before enforcing coverage. +# Merge LCOV hits across Ruby versions, platforms, and shards before enforcing coverage. # Individual shards need not cover every line, and platform-specific lines may # only run on one operating system. +require_branches = ARGV.delete('--branches') lines = Hash.new(0) +branches = Hash.new(0) ARGV.each do |path| source = nil File.foreach(path) do |record| @@ -13,6 +15,10 @@ when /^DA:(\d+),(\d+)/ abort "Line coverage without a source file in #{path}" unless source lines[[source, Regexp.last_match(1).to_i]] += Regexp.last_match(2).to_i + when /^BRDA:(\d+),(\d+),(\d+),(-|\d+)/ + abort "Branch coverage without a source file in #{path}" unless source + line, block, branch, hits = Regexp.last_match.captures + branches[[source, line.to_i, block.to_i, branch.to_i]] += hits == '-' ? 0 : hits.to_i when /^end_of_record/ source = nil end @@ -26,3 +32,16 @@ abort '100% line coverage is required' end puts "Line coverage: 100% (#{lines.size}/#{lines.size})" + +if require_branches + abort 'No branch coverage found' if branches.empty? + + uncovered_branches = branches.select { |_, hits| hits == 0 }.keys + unless uncovered_branches.empty? + warn "Uncovered branches:\n#{uncovered_branches.sort.map do |file, line, block, branch| + "#{file}:#{line} (block #{block}, branch #{branch})" + end.join("\n")}" + abort '100% branch coverage is required' + end + puts "Branch coverage: 100% (#{branches.size}/#{branches.size})" +end diff --git a/spec/coverage_check_spec.rb b/spec/coverage_check_spec.rb index a2bf506a..43dd4181 100644 --- a/spec/coverage_check_spec.rb +++ b/spec/coverage_check_spec.rb @@ -59,3 +59,72 @@ def check_coverage(*reports) result.status.should_not == 0 end end + +describe 'combined branch coverage check' do + let(:script) { File.expand_path('../script/check_coverage.rb', __dir__) } + + def check_branches(*reports) + result = nil + directory do + paths = reports.each_with_index.map do |report, index| + path = "coverage-#{index}.info" + File.write(path, report) + path + end + result = shell([Gem.ruby, script, '--branches'] + paths) + end + result + end + + it 'combines complementary branch hits across platforms and shards' do + result = check_branches( + "SF:a.rb\nDA:1,1\nBRDA:1,0,0,1\nBRDA:1,0,1,-\nend_of_record\n", + "SF:a.rb\nDA:1,0\nBRDA:1,0,0,0\nBRDA:1,0,1,2\nend_of_record\n", + ) + expect(result.status).to eq(0) + expect(result.stdout).to include('Line coverage: 100% (1/1)', 'Branch coverage: 100% (2/2)') + end + + ['0', '-'].each do |hits| + it "fails for an uncovered branch recorded as #{hits.inspect}" do + result = check_branches("SF:a.rb\nDA:1,1\nBRDA:1,0,0,99999\nBRDA:1,0,1,#{hits}\n") + expect(result.status).not_to eq(0) + expect(result.stderr).to include('a.rb:1 (block 0, branch 1)', + '100% branch coverage is required') + end + end + + it 'keeps files, lines, blocks, and branch identities separate' do + reports = [ + "SF:a.rb\nDA:1,1\nBRDA:1,0,0,1\nBRDA:1,0,1,1\nBRDA:1,1,0,1\nBRDA:2,0,0,1\n", + "SF:b.rb\nDA:1,1\nBRDA:1,0,0,0\n", + ] + result = check_branches(*reports) + expect(result.status).not_to eq(0) + expect(result.stderr).to include('b.rb:1 (block 0, branch 0)') + end + + it 'requires branch records when enforcing branch coverage' do + result = check_branches("SF:a.rb\nDA:1,1\nend_of_record\n") + expect(result.status).not_to eq(0) + expect(result.stderr).to include('No branch coverage found') + end + + it 'rejects branch records without a source file' do + result = check_branches("BRDA:1,0,0,1\n") + expect(result.status).not_to eq(0) + expect(result.stderr).to include('Branch coverage without a source file') + end + + it 'resets the branch source at the end of each record' do + result = check_branches("SF:a.rb\nDA:1,1\nend_of_record\nBRDA:1,0,0,1\n") + expect(result.status).not_to eq(0) + expect(result.stderr).to include('Branch coverage without a source file') + end + + it 'continues enforcing line coverage when every branch is covered' do + result = check_branches("SF:a.rb\nDA:1,0\nBRDA:1,0,0,1\n") + expect(result.status).not_to eq(0) + expect(result.stderr).to include('100% line coverage is required') + end +end diff --git a/spec/overcommit/cli_errors_spec.rb b/spec/overcommit/cli_errors_spec.rb index e5107103..dfb4cf3d 100644 --- a/spec/overcommit/cli_errors_spec.rb +++ b/spec/overcommit/cli_errors_spec.rb @@ -91,4 +91,27 @@ expect { cli.run }.to raise_error(SystemExit) expect(output.string).to include('Example: enabled (plugin)', 'Other: disabled') end + + it 'runs an explicitly selected hook context' do + arguments.concat(['--run', 'pre-push']) + config = double('config', apply_environment!: nil) + context = double('context') + allow(cli).to receive(:config).and_return(config) + expect(Overcommit::HookContext).to receive(:create). + with('pre-push', config, [], an_instance_of(File)).and_return(context) + allow(Overcommit::Printer).to receive(:new).and_return(double('printer')) + allow(Overcommit::HookRunner).to receive(:new).and_return(double(run: true)) + expect { cli.run }.to raise_error(SystemExit) { |error| expect(error.status).to eq(0) } + end + + it 'exits successfully after diff hooks pass' do + arguments << '--diff' + config = double('config', apply_environment!: nil) + context = double('context') + allow(cli).to receive(:config).and_return(config) + allow(Overcommit::HookContext).to receive(:create).and_return(context) + allow(Overcommit::Printer).to receive(:new).and_return(double('printer')) + allow(Overcommit::HookRunner).to receive(:new).and_return(double(run: true)) + expect { cli.run }.to raise_error(SystemExit) { |error| expect(error.status).to eq(0) } + end end diff --git a/spec/overcommit/configuration_validator_spec.rb b/spec/overcommit/configuration_validator_spec.rb index d6b04da5..d5869e76 100644 --- a/spec/overcommit/configuration_validator_spec.rb +++ b/spec/overcommit/configuration_validator_spec.rb @@ -124,4 +124,15 @@ end end end + + context 'when processor counts are invalid and no logger is supplied' do + let(:options) { {} } + let(:config_hash) do + { 'concurrency' => 1, 'PreCommit' => { 'Example' => { 'processors' => 2 } } } + end + + it 'still raises a configuration error' do + expect { subject }.to raise_error(Overcommit::Exceptions::ConfigurationError, /processor/) + end + end end diff --git a/spec/overcommit/gem_plugin_spec.rb b/spec/overcommit/gem_plugin_spec.rb index 89fe970b..15b2a358 100644 --- a/spec/overcommit/gem_plugin_spec.rb +++ b/spec/overcommit/gem_plugin_spec.rb @@ -253,4 +253,10 @@ def changed_config(hash) /Unable to activate company-hooks/) end end + + it 'does not sign Overcommit itself as a plugin dependency' do + specification.add_runtime_dependency('overcommit') + expect(Gem::Specification).not_to receive(:find_by_name).with('overcommit', anything) + expect { plugin.update_signature! }.not_to raise_error + end end diff --git a/spec/overcommit/git_config_spec.rb b/spec/overcommit/git_config_spec.rb index 00eea50d..62ba5d43 100644 --- a/spec/overcommit/git_config_spec.rb +++ b/spec/overcommit/git_config_spec.rb @@ -237,4 +237,13 @@ it { should == '--worktree' } end end + + it 'resolves commit cleanup mode when no logger is configured' do + repo do + shell(%w[git config commit.cleanup strip]) + allow(Overcommit::Utils).to receive(:parent_command).and_return('git commit') + allow(Overcommit::Utils).to receive(:log).and_return(nil) + expect(described_class.commit_cleanup_mode).to eq('strip') + end + end end diff --git a/spec/overcommit/git_repo_spec.rb b/spec/overcommit/git_repo_spec.rb index 90aab79f..56dee1f2 100644 --- a/spec/overcommit/git_repo_spec.rb +++ b/spec/overcommit/git_repo_spec.rb @@ -595,4 +595,56 @@ def stage_files(paths) it { should be_empty } end end + + describe 'restoring absent operation state' do + around do |example| + repo do + %w[merge_head merge_mode merge_msg cherry_head].each do |name| + described_class.instance_variable_set(:"@#{name}", nil) + end + example.run + ensure + %w[merge_head merge_mode merge_msg cherry_head].each do |name| + described_class.instance_variable_set(:"@#{name}", nil) + end + end + end + + it 'leaves a repository without merge or cherry-pick state unchanged' do + described_class.store_merge_state + described_class.store_cherry_pick_state + described_class.restore_merge_state + described_class.restore_cherry_pick_state + %w[MERGE_HEAD MERGE_MODE MERGE_MSG CHERRY_PICK_HEAD].each do |name| + expect(File.exist?(Overcommit::Utils.git_path(name))).to eq(false) + end + end + + it 'restores a merge head without requiring a merge mode or message' do + File.write(Overcommit::Utils.git_path('MERGE_HEAD'), 'a' * 40) + described_class.store_merge_state + FileUtils.rm(Overcommit::Utils.git_path('MERGE_HEAD')) + described_class.restore_merge_state + expect(File.read(Overcommit::Utils.git_path('MERGE_HEAD'))).to eq('a' * 40) + expect(File.exist?(Overcommit::Utils.git_path('MERGE_MODE'))).to eq(false) + end + + it 'has no staged submodule removals without a gitmodules file' do + expect(described_class.staged_submodule_removals).to eq([]) + end + end + + it 'includes existing submodules when using Git older than 1.8.5' do + stub_const('Overcommit::GitRepo::GIT_VERSION', Overcommit::Utils::Version.new('1.8.4')) + allow(described_class).to receive(:`).and_return(<<~INI) + [submodule "example"] + path = example + url = https://example.com/example.git + INI + expect(Overcommit::Utils).to receive(:execute).with(%w[git submodule status example]). + and_return(double(success?: true)) + expect(described_class.submodules).to eq([ + described_class::Submodule.new('example', 'https://example.com/example.git') +]) + end end diff --git a/spec/overcommit/hook/post_commit/git_guilt_spec.rb b/spec/overcommit/hook/post_commit/git_guilt_spec.rb index d099b507..e7de093a 100644 --- a/spec/overcommit/hook/post_commit/git_guilt_spec.rb +++ b/spec/overcommit/hook/post_commit/git_guilt_spec.rb @@ -49,4 +49,11 @@ it { should pass } end + + it 'colorizes blame changes when output is a terminal' do + allow($stdout).to receive(:tty?).and_return(true) + allow(subject).to receive(:initial_commit?).and_return(false) + allow(subject).to receive(:execute).and_return(double(success?: true, stdout: 'Tester +++')) + expect(subject.run).to eq([:warn, "Tester \e[32m+++\e[0m\e[31m\e[0m"]) + end end diff --git a/spec/overcommit/hook/pre_commit/bundle_check_spec.rb b/spec/overcommit/hook/pre_commit/bundle_check_spec.rb index b8a471a1..b35c8ba0 100644 --- a/spec/overcommit/hook/pre_commit/bundle_check_spec.rb +++ b/spec/overcommit/hook/pre_commit/bundle_check_spec.rb @@ -60,4 +60,14 @@ end end end + + it 'preserves an existing lockfile when bundle check does not change it' do + repo do + File.write('Gemfile.lock', 'locked dependencies') + allow(subject).to receive(:execute).with(%w[git ls-files -o -i --exclude-standard]). + and_return(double(stdout: '')) + allow(subject).to receive(:execute).with(subject.command).and_return(double(success?: true)) + expect(subject.run).to eq(:pass) + end + end end diff --git a/spec/overcommit/hook/pre_commit/case_conflicts_spec.rb b/spec/overcommit/hook/pre_commit/case_conflicts_spec.rb index 1ce157d5..cdd1bba6 100644 --- a/spec/overcommit/hook/pre_commit/case_conflicts_spec.rb +++ b/spec/overcommit/hook/pre_commit/case_conflicts_spec.rb @@ -35,4 +35,11 @@ it { should pass } end + + it 'checks only new files on the initial commit' do + allow(Overcommit::GitRepo).to receive(:initial_commit?).and_return(true) + allow(subject).to receive(:applicable_files).and_return(%w[foo Foo]) + expect(Overcommit::GitRepo).not_to receive(:list_files) + expect(subject.run.map(&:file)).to include('foo', 'Foo') + end end diff --git a/spec/overcommit/hook/pre_commit/chamber_verification_spec.rb b/spec/overcommit/hook/pre_commit/chamber_verification_spec.rb index a49e2a98..042f0b4e 100644 --- a/spec/overcommit/hook/pre_commit/chamber_verification_spec.rb +++ b/spec/overcommit/hook/pre_commit/chamber_verification_spec.rb @@ -54,4 +54,16 @@ it { should warn } end + + context 'with an approver email configured' do + before do + allow(subject).to receive(:config).and_return(subject.config.dup) + subject.config['approver_email'] = 'approver@example.com' + allow(subject).to receive(:execute).and_return( + double(stdout: 'Signature mismatch', stderr: '') + ) + end + + it { should warn /your approver \(approver@example.com\)/ } + end end diff --git a/spec/overcommit/hook/pre_commit/dogma_spec.rb b/spec/overcommit/hook/pre_commit/dogma_spec.rb index 53b5cd62..5fcf41ef 100644 --- a/spec/overcommit/hook/pre_commit/dogma_spec.rb +++ b/spec/overcommit/hook/pre_commit/dogma_spec.rb @@ -48,4 +48,11 @@ it { should fail_hook } end end + + it 'returns no messages when unsuccessful output has no file groups' do + allow(subject).to receive(:execute).and_return( + double(success?: false, stdout: 'No matching files') + ) + expect(subject.run).to eq([]) + end end diff --git a/spec/overcommit/hook/pre_commit/execute_permissions_spec.rb b/spec/overcommit/hook/pre_commit/execute_permissions_spec.rb index 3e33b946..11901397 100644 --- a/spec/overcommit/hook/pre_commit/execute_permissions_spec.rb +++ b/spec/overcommit/hook/pre_commit/execute_permissions_spec.rb @@ -67,4 +67,19 @@ def make_executable_and_add(file, exec_bit) it_behaves_like 'a file permission hook' end + + it 'raises when the Git tree cannot be read' do + allow(context).to receive(:initial_commit?).and_return(false) + allow(subject).to receive(:execute).with(%w[git ls-tree HEAD --], args: [staged_file]). + and_return(double(success?: false)) + expect { subject.run }.to raise_error(RuntimeError, 'Unable to access git tree') + end + + it 'raises when the Git index cannot be read' do + allow(context).to receive(:initial_commit?).and_return(true) + allow(subject).to receive(:execute). + with(%w[git diff --raw --cached --no-color --], args: [staged_file]). + and_return(double(success?: false)) + expect { subject.run }.to raise_error(RuntimeError, 'Unable to access git index') + end end diff --git a/spec/overcommit/hook/pre_commit/flay_spec.rb b/spec/overcommit/hook/pre_commit/flay_spec.rb index 2af339bc..f5576252 100644 --- a/spec/overcommit/hook/pre_commit/flay_spec.rb +++ b/spec/overcommit/hook/pre_commit/flay_spec.rb @@ -69,4 +69,21 @@ it { should fail_hook } end + + context 'when liberal matching is enabled' do + let(:result) { double(success?: true, stdout: 'Total score = 0') } + let(:config) do + default = Overcommit::ConfigurationLoader.default_configuration + allow(default).to receive(:for_hook).and_return( + default.for_hook('Flay', 'PreCommit').merge('liberal' => true) + ) + default + end + + it 'passes the liberal flag to flay' do + expect(subject).to receive(:execute). + with(%w[flay --mass 16 --fuzzy 1 --liberal], args: applicable_files).and_return(result) + expect(subject.run).to eq([]) + end + end end diff --git a/spec/overcommit/hook/pre_commit/foodcritic_spec.rb b/spec/overcommit/hook/pre_commit/foodcritic_spec.rb index 2bbe3df2..74349ba8 100644 --- a/spec/overcommit/hook/pre_commit/foodcritic_spec.rb +++ b/spec/overcommit/hook/pre_commit/foodcritic_spec.rb @@ -123,4 +123,22 @@ end end end + + context 'with an absolute cookbook directory' do + let(:config) { Overcommit::ConfigurationLoader.default_configuration } + let(:applicable_files) { [] } + + it 'uses absolute cookbook paths without joining them to the repository root' do + cookbook_directory = File.expand_path('cookbooks') + allow(subject).to receive(:config).and_return( + subject.config.merge('cookbooks_directory' => cookbook_directory) + ) + files = ["#{cookbook_directory}/example/recipe.rb"] + allow(subject).to receive(:applicable_files).and_return(files) + expect(subject).to receive(:execute). + with(subject.command, args: ['-B', "#{cookbook_directory}/example"]). + and_return(double(success?: true)) + expect(subject.run).to eq(:pass) + end + end end diff --git a/spec/overcommit/hook/pre_commit/herb_lint_spec.rb b/spec/overcommit/hook/pre_commit/herb_lint_spec.rb index f8e13001..cb39facf 100644 --- a/spec/overcommit/hook/pre_commit/herb_lint_spec.rb +++ b/spec/overcommit/hook/pre_commit/herb_lint_spec.rb @@ -91,4 +91,19 @@ it { should fail_hook } end end + + it 'reports warning offenses with their location and rule' do + offense = { + 'severity' => 'warning', 'filename' => 'view.html.erb', + 'location' => { 'start' => { 'line' => 3 } }, + 'message' => 'Use a local variable', 'code' => 'no-instance-variables', + } + allow(subject).to receive(:execute).and_return( + double(success?: false, stdout: JSON.generate('offenses' => [offense])) + ) + message = subject.run.fetch(0) + expect([message.type, message.file, message.line, message.content]).to eq( + [:warning, 'view.html.erb', 3, 'view.html.erb:3 no-instance-variables Use a local variable'] + ) + end end diff --git a/spec/overcommit/hook/pre_commit/jscs_spec.rb b/spec/overcommit/hook/pre_commit/jscs_spec.rb index 4cc56dec..d6aba123 100644 --- a/spec/overcommit/hook/pre_commit/jscs_spec.rb +++ b/spec/overcommit/hook/pre_commit/jscs_spec.rb @@ -42,4 +42,9 @@ it { should fail_hook } end end + + it 'passes when jscs succeeds' do + allow(subject).to receive(:execute).and_return(double(success?: true)) + expect(subject.run).to eq(:pass) + end end diff --git a/spec/overcommit/hook/pre_commit/line_endings_spec.rb b/spec/overcommit/hook/pre_commit/line_endings_spec.rb index 78dbfdea..4d714ac7 100644 --- a/spec/overcommit/hook/pre_commit/line_endings_spec.rb +++ b/spec/overcommit/hook/pre_commit/line_endings_spec.rb @@ -75,4 +75,25 @@ it { should warn } end + + context 'when Git cannot list line endings' do + let(:contents) { "text\n" } + + it 'raises rather than silently skipping files' do + allow(subject).to receive(:execute).and_return(double(success?: false)) + expect { subject.run }.to raise_error(RuntimeError, 'Unable to access git tree') + end + end + + context 'when Git reports matching or binary line endings' do + let(:contents) { "text\n" } + + ['l/-text', 'l/lf'].each do |index_info| + it "ignores #{index_info} entries" do + output = "#{index_info} w/lf attr/\t#{staged_file}\0" + allow(subject).to receive(:execute).and_return(double(success?: true, stdout: output)) + expect(subject.run).to eq([]) + end + end + end end diff --git a/spec/overcommit/hook/pre_commit/phpcs_fixer_spec.rb b/spec/overcommit/hook/pre_commit/phpcs_fixer_spec.rb index a4b1e187..1e97f4c4 100644 --- a/spec/overcommit/hook/pre_commit/phpcs_fixer_spec.rb +++ b/spec/overcommit/hook/pre_commit/phpcs_fixer_spec.rb @@ -83,4 +83,9 @@ end it { should fail_hook } end + + it 'returns no feedback when no files need processing' do + allow(subject).to receive(:applicable_files).and_return([]) + expect(subject.run).to eq('') + end end diff --git a/spec/overcommit/hook/pre_commit/phpcs_spec.rb b/spec/overcommit/hook/pre_commit/phpcs_spec.rb index 6982a297..318ae7d2 100644 --- a/spec/overcommit/hook/pre_commit/phpcs_spec.rb +++ b/spec/overcommit/hook/pre_commit/phpcs_spec.rb @@ -65,4 +65,16 @@ it { should fail_hook } end end + + it 'fails when phpcs fails without reporting any offenses' do + allow(subject).to receive(:execute).and_return( + double(status: 1, success?: false, stdout: '') + ) + expect(subject.run).to eq(:fail) + end + + it 'fails when phpcs terminates without an exit status' do + allow(subject).to receive(:execute).and_return(double(status: nil, success?: false)) + expect(subject.run).to eq(:fail) + end end diff --git a/spec/overcommit/hook/pre_commit/ruby_syntax_spec.rb b/spec/overcommit/hook/pre_commit/ruby_syntax_spec.rb index 0eaa4459..eebe2eec 100644 --- a/spec/overcommit/hook/pre_commit/ruby_syntax_spec.rb +++ b/spec/overcommit/hook/pre_commit/ruby_syntax_spec.rb @@ -46,4 +46,12 @@ it { should fail_hook } end end + + it 'reports Ruby warnings without promoting them to errors' do + allow(subject).to receive(:execute).and_return( + double(stderr: 'file.rb:3: warning: assigned but unused variable, unused') + ) + message = subject.run.fetch(0) + expect([message.type, message.file, message.line]).to eq([:warning, 'file.rb', 3]) + end end diff --git a/spec/overcommit/hook/pre_commit/scss_lint_spec.rb b/spec/overcommit/hook/pre_commit/scss_lint_spec.rb index ae8b2f68..10466564 100644 --- a/spec/overcommit/hook/pre_commit/scss_lint_spec.rb +++ b/spec/overcommit/hook/pre_commit/scss_lint_spec.rb @@ -94,4 +94,18 @@ it { should pass } end end + + it 'reports command failures with both output streams' do + allow(subject).to receive(:execute).and_return( + double(status: 64, stdout: 'Usage: scss-lint', stderr: 'Invalid option') + ) + expect(subject.run).to eq([:fail, 'Usage: scss-lintInvalid option']) + end + + it 'reports a syntax error without a linter name' do + output = { 'test.scss' => [{ 'line' => 2, 'severity' => 'error', 'reason' => 'Syntax error' }] } + allow(subject).to receive(:execute).and_return(double(status: 2, stdout: JSON.generate(output))) + message = subject.run.fetch(0) + expect([message.type, message.content]).to eq([:error, 'test.scss:2 Syntax error']) + end end diff --git a/spec/overcommit/hook/pre_commit/w3c_css_spec.rb b/spec/overcommit/hook/pre_commit/w3c_css_spec.rb index 9ec45800..aac8e642 100644 --- a/spec/overcommit/hook/pre_commit/w3c_css_spec.rb +++ b/spec/overcommit/hook/pre_commit/w3c_css_spec.rb @@ -75,4 +75,16 @@ it { should fail_hook } end end + + it 'uses validator defaults when optional settings are nil' do + allow(subject).to receive(:config).and_return(subject.config.dup) + subject.config['language'] = nil + subject.config['profile'] = nil + subject.config['warn_level'] = nil + validator = double('validator', validate_file: double(errors: [], warnings: [])) + factory = double('validator class', new: validator) + stub_const('W3CValidators::CSSValidator', factory) + expect(validator).not_to receive(:set_language!) + expect(subject.run).to eq([]) + end end diff --git a/spec/overcommit/hook/pre_commit/w3c_html_spec.rb b/spec/overcommit/hook/pre_commit/w3c_html_spec.rb index e517bb34..b524a541 100644 --- a/spec/overcommit/hook/pre_commit/w3c_html_spec.rb +++ b/spec/overcommit/hook/pre_commit/w3c_html_spec.rb @@ -75,4 +75,15 @@ it { should fail_hook } end end + + it 'uses validator defaults when optional settings are nil' do + allow(subject).to receive(:config).and_return(subject.config.dup) + subject.config['charset'] = nil + subject.config['doctype'] = nil + validator = double('validator', validate_file: double(errors: [], warnings: [])) + factory = double('validator class', new: validator) + stub_const('W3CValidators::MarkupValidator', factory) + expect(validator).to receive(:set_debug!) + expect(subject.run).to eq([]) + end end diff --git a/spec/overcommit/hook/pre_commit/yard_coverage_spec.rb b/spec/overcommit/hook/pre_commit/yard_coverage_spec.rb index d463d25d..b4e56ab8 100644 --- a/spec/overcommit/hook/pre_commit/yard_coverage_spec.rb +++ b/spec/overcommit/hook/pre_commit/yard_coverage_spec.rb @@ -96,4 +96,17 @@ it { should fail_hook } end end + + it 'reports undocumented objects even when no minimum percentage is configured' do + allow(subject).to receive(:config).and_return( + subject.config.merge('min_coverage_percentage' => nil) + ) + output = (['Stats'] * 6 + ['50% documented', 'Undocumented Objects:', 'Example (file.rb:2)']). + join("\n") + allow(subject).to receive(:execute).and_return(double(stdout: output)) + messages = subject.run + expect(messages.last.file).to eq('file.rb') + expect(messages.last.line).to eq('2') + expect(messages.last.type).to eq(:error) + end end diff --git a/spec/overcommit/hook/pre_commit/yarn_check_spec.rb b/spec/overcommit/hook/pre_commit/yarn_check_spec.rb index f67fa9b2..b25380b4 100644 --- a/spec/overcommit/hook/pre_commit/yarn_check_spec.rb +++ b/spec/overcommit/hook/pre_commit/yarn_check_spec.rb @@ -86,4 +86,15 @@ it { should fail_hook } end end + + it 'preserves an existing lockfile when yarn does not change it' do + repo do + File.write('yarn.lock', 'locked dependencies') + allow(subject).to receive(:execute).with(%w[git ls-files -o -i --exclude-standard]). + and_return(double(stdout: '')) + allow(subject).to receive(:execute).with(subject.command).and_return(double(stderr: '')) + expect(subject.run).to eq(:pass) + expect(File.read('yarn.lock')).to eq('locked dependencies') + end + end end diff --git a/spec/overcommit/hook/prepare_commit_msg/base_spec.rb b/spec/overcommit/hook/prepare_commit_msg/base_spec.rb index 85b6452b..09c0a22a 100644 --- a/spec/overcommit/hook/prepare_commit_msg/base_spec.rb +++ b/spec/overcommit/hook/prepare_commit_msg/base_spec.rb @@ -48,4 +48,10 @@ expect(File.read(tempfile)).to match(/alpha\n#{initial_content}bravo\n/m) end end + + it 'requires a block before modifying a commit message' do + expect do + described_class.new(config, context).modify_commit_message + end.to raise_error(RuntimeError, 'This expects a block!') + end end diff --git a/spec/overcommit/hook/prepare_commit_msg/replace_branch_spec.rb b/spec/overcommit/hook/prepare_commit_msg/replace_branch_spec.rb index 3e7332c5..10c48bc7 100644 --- a/spec/overcommit/hook/prepare_commit_msg/replace_branch_spec.rb +++ b/spec/overcommit/hook/prepare_commit_msg/replace_branch_spec.rb @@ -335,4 +335,14 @@ def remove_file(name) end end end + + it 'recognizes an exact prefix that ends in a word character' do + allow(hook).to receive(:new_template).and_return('PROJ-12 ') + expect(hook.keep_message?('PROJ-12')).to eq(true) + end + + it 'uses the default pattern when the configured pattern is absent' do + allow(hook).to receive(:config).and_return(hook.config.merge('branch_pattern' => nil)) + expect(hook.branch_pattern).to eq(described_class::DEFAULT_BRANCH_PATTERN) + end end diff --git a/spec/overcommit/hook_context/base_spec.rb b/spec/overcommit/hook_context/base_spec.rb index ecae943b..bfe0c4f6 100644 --- a/spec/overcommit/hook_context/base_spec.rb +++ b/spec/overcommit/hook_context/base_spec.rb @@ -82,4 +82,11 @@ def finish_reading it { should be_nil } end + + it 'reads input only once across repeated calls' do + input = double('input') + expect(input).to receive(:read).once.and_return('hook input') + context = described_class.new(nil, [], input) + 2.times { expect(context.input_string).to eq('hook input') } + end end diff --git a/spec/overcommit/hook_context/helpers/hide_unstaged_changes_spec.rb b/spec/overcommit/hook_context/helpers/hide_unstaged_changes_spec.rb index a173c5c6..c7e9e640 100644 --- a/spec/overcommit/hook_context/helpers/hide_unstaged_changes_spec.rb +++ b/spec/overcommit/hook_context/helpers/hide_unstaged_changes_spec.rb @@ -444,3 +444,15 @@ def patches end end end + +describe Overcommit::HookContext::PreCommit, 'timestamp collection' do + it 'ignores missing files while storing modification times' do + repo do + context = described_class.new(nil, [], nil) + allow(context).to receive(:modified_files).and_return(['missing.rb']) + allow(Overcommit::GitRepo).to receive(:modified_files).with(staged: false).and_return([]) + expect(File).not_to receive(:mtime) + expect { context.send(:store_modified_times) }.not_to raise_error + end + end +end diff --git a/spec/overcommit/hook_context/post_commit_spec.rb b/spec/overcommit/hook_context/post_commit_spec.rb index 81758256..7d81ac32 100644 --- a/spec/overcommit/hook_context/post_commit_spec.rb +++ b/spec/overcommit/hook_context/post_commit_spec.rb @@ -143,4 +143,10 @@ it { should == true } end end + + it 'caches whether this is the initial commit' do + expect(Overcommit::Utils).to receive(:execute).once.with(%w[git rev-parse HEAD~]). + and_return(double(success?: false)) + 2.times { expect(context.initial_commit?).to eq(true) } + end end diff --git a/spec/overcommit/hook_context/pre_commit_spec.rb b/spec/overcommit/hook_context/pre_commit_spec.rb index 7f1f6abb..02b3af53 100644 --- a/spec/overcommit/hook_context/pre_commit_spec.rb +++ b/spec/overcommit/hook_context/pre_commit_spec.rb @@ -663,4 +663,20 @@ it { should == Set.new(1..4) } end end + + it 'does not infer an amendment when the parent process is unavailable' do + allow(Overcommit::Utils).to receive(:parent_command).and_return(nil) + expect(context.amendment?).to be_nil + end + + it 'reuses the modified file list and modified lines within one run' do + allow(context).to receive(:amendment?).and_return(false) + expect(Overcommit::GitRepo).to receive(:modified_files).once.with(staged: true).and_return([]) + expect(Overcommit::GitRepo).to receive(:extract_modified_lines).once. + with('file.rb', staged: true).and_return(Set[2]) + 2.times do + expect(context.modified_files).to eq([]) + expect(context.modified_lines_in_file('file.rb')).to eq(Set[2]) + end + end end diff --git a/spec/overcommit/hook_context/pre_push_spec.rb b/spec/overcommit/hook_context/pre_push_spec.rb index 101e0f1a..451960ec 100644 --- a/spec/overcommit/hook_context/pre_push_spec.rb +++ b/spec/overcommit/hook_context/pre_push_spec.rb @@ -472,4 +472,17 @@ end end end + + it 'caches an empty push as not deleting a remote ref' do + expect(input).to receive(:read).once.and_return('') + 2.times { expect(context.remote_ref_deletion?).to eq(false) } + end + + it 'reuses the remote history query when checking a pushed ref repeatedly' do + ref = described_class::PushedRef.new('local', 'a' * 40, 'remote', 'b' * 40) + expect(Overcommit::Subprocess).to receive(:spawn).once. + with(['git', 'rev-list', 'b' * 40, "^#{'a' * 40}"]). + and_return(double(success?: true, stdout: "overwritten-commit\n")) + 2.times { expect(ref.forced?).to eq(true) } + end end diff --git a/spec/overcommit/hook_context/pre_rebase_spec.rb b/spec/overcommit/hook_context/pre_rebase_spec.rb index 58e0e8c2..1e00ff31 100644 --- a/spec/overcommit/hook_context/pre_rebase_spec.rb +++ b/spec/overcommit/hook_context/pre_rebase_spec.rb @@ -106,4 +106,11 @@ it { should_not be_empty } end end + + it 'lists commits from HEAD when rebasing a detached checkout' do + allow(context).to receive(:rebased_branch).and_return('') + expect(context).to receive(:`).with('git rev-list --topo-order --reverse master..HEAD'). + and_return("abc123\n") + expect(context.rebased_commits).to eq(['abc123']) + end end diff --git a/spec/overcommit/hook_runner_spec.rb b/spec/overcommit/hook_runner_spec.rb index 95cc0469..251f231a 100644 --- a/spec/overcommit/hook_runner_spec.rb +++ b/spec/overcommit/hook_runner_spec.rb @@ -89,4 +89,32 @@ end.to raise_error(Overcommit::Exceptions::HookLoadError, /forget.*gem.*missing/m) end end + + it 'does not print recovery instructions when the context provides none' do + allow(context).to receive(:post_fail_message).and_return(nil) + allow(hook).to receive(:run_and_transform).and_return([:fail, nil]) + expect(printer).not_to receive(:hook_run_failed) + expect(runner.run).to eq(false) + end + + it 'does not print an end result after the run has been interrupted' do + allow(hook).to receive(:run_and_transform) do + runner.instance_variable_set(:@interrupted, true) + [:pass, nil] + end + expect(printer).not_to receive(:end_hook) + expect(runner.run).to eq(false) + end + + it 'does not announce skipped hooks that would not run anyway' do + allow(hook).to receive_messages(skip?: true, run?: false) + expect(printer).not_to receive(:hook_skipped) + expect(runner.run).to eq(true) + end + + it 'does not start another hook after interruption' do + runner.instance_variable_set(:@interrupted, true) + expect(hook).not_to receive(:run_and_transform) + expect(runner.send(:should_skip?, hook)).to eq(true) + end end diff --git a/spec/overcommit/hook_signer_files_spec.rb b/spec/overcommit/hook_signer_files_spec.rb index 36bb0aa4..8876aad2 100644 --- a/spec/overcommit/hook_signer_files_spec.rb +++ b/spec/overcommit/hook_signer_files_spec.rb @@ -49,4 +49,9 @@ include_examples 'a signed repository command' end + + it 'recognizes a Windows-relative command path' do + allow(Overcommit::OS).to receive(:windows?).and_return(true) + expect(signer.signable_file?('.\\hook-script')).to eq(true) + end end diff --git a/spec/overcommit/installer_spec.rb b/spec/overcommit/installer_spec.rb index 5283b2b6..ea3db413 100644 --- a/spec/overcommit/installer_spec.rb +++ b/spec/overcommit/installer_spec.rb @@ -250,4 +250,20 @@ def hook_files_installed?(hooks_dir) end end end + + it 'installs into a repository whose hooks directory is absent' do + repo do + FileUtils.rm_rf(Overcommit::GitConfig.hooks_path) + installer.run(Dir.pwd, action: :install) + expect(hook_files_installed?(Overcommit::GitConfig.hooks_path)).to eq(true) + end + end + + it 'can uninstall when the hooks directory is already absent' do + repo do + FileUtils.rm_rf(Overcommit::GitConfig.hooks_path) + expect { installer.run(Dir.pwd, action: :uninstall) }.not_to raise_error + expect(File.directory?(Overcommit::GitConfig.hooks_path)).to eq(false) + end + end end diff --git a/spec/overcommit/logger_spec.rb b/spec/overcommit/logger_spec.rb index 681e0dff..62e506f6 100644 --- a/spec/overcommit/logger_spec.rb +++ b/spec/overcommit/logger_spec.rb @@ -180,4 +180,9 @@ let(:color_code) { '1;33' } end end + + it 'can flush an output stream that does not implement flush' do + logger = described_class.new(double('output', tty?: false)) + expect { logger.flush }.not_to raise_error + end end diff --git a/spec/overcommit/message_processor_spec.rb b/spec/overcommit/message_processor_spec.rb index f90a1f0f..f640f448 100644 --- a/spec/overcommit/message_processor_spec.rb +++ b/spec/overcommit/message_processor_spec.rb @@ -289,4 +289,12 @@ def warning(file = nil, line = nil) end end end + + it 'retains file-wide errors when ignoring unmodified lines' do + hook = double('hook', modified_lines_in_file: Set.new) + message = Overcommit::Hook::Message.new(:error, 'file.rb', nil, 'File-wide error') + processor = described_class.new(hook, 'ignore') + expect(hook).not_to receive(:modified_lines_in_file) + expect(processor.hook_result([message])).to eq([:fail, "Errors:\nFile-wide error\n"]) + end end diff --git a/spec/overcommit/printer_spec.rb b/spec/overcommit/printer_spec.rb index 268157a7..ecc88f6b 100644 --- a/spec/overcommit/printer_spec.rb +++ b/spec/overcommit/printer_spec.rb @@ -93,4 +93,10 @@ printer.end_hook(hook, :fail, 'Details') end end + + it 'suppresses the run heading in quiet mode' do + config['quiet'] = true + expect(logger).not_to receive(:bold) + printer.start_run + end end diff --git a/spec/overcommit/signature_spec.rb b/spec/overcommit/signature_spec.rb index d051fd2a..7abe5b72 100644 --- a/spec/overcommit/signature_spec.rb +++ b/spec/overcommit/signature_spec.rb @@ -209,4 +209,13 @@ expect { store }.to raise_error(Overcommit::Exceptions::GitConfigError, /cannot lock config/) File.read(signature_file).should include(signature) end + + it 'tolerates another process removing a legacy signature during migration' do + allow(Overcommit::Utils).to receive(:execute).and_call_original + allow(Overcommit::Utils).to receive(:execute).with(%w[git config --local --get] + [key]). + and_return(Overcommit::Subprocess::Result.new(0, signature, '')) + allow(Overcommit::Utils).to receive(:execute).with(%w[git config --local --unset-all] + [key]). + and_return(Overcommit::Subprocess::Result.new(5, '', '')) + expect(store.include?(signature)).to eq(true) + end end diff --git a/spec/overcommit/subprocess_spec.rb b/spec/overcommit/subprocess_spec.rb index e2e1c698..2b480507 100644 --- a/spec/overcommit/subprocess_spec.rb +++ b/spec/overcommit/subprocess_spec.rb @@ -197,4 +197,9 @@ def expect_tempfiles_deleted described_class.spawn_detached(args) end end + + it 'preserves the startup error when no output streams could be assigned' do + allow(described_class).to receive(:assign_output_streams).and_raise(IOError, 'No temp space') + expect { described_class.spawn([ruby, '-e', '']) }.to raise_error(IOError, 'No temp space') + end end diff --git a/spec/overcommit/utils/messages_errors_spec.rb b/spec/overcommit/utils/messages_errors_spec.rb index c823cf7c..4ef8f81f 100644 --- a/spec/overcommit/utils/messages_errors_spec.rb +++ b/spec/overcommit/utils/messages_errors_spec.rb @@ -17,4 +17,10 @@ expect { described_class.extract_messages(['file'], /(?.*)/, ->(_) { :invalid }) }. to raise_error(Overcommit::Exceptions::MessageProcessingError, /Invalid message type/) end + + it 'categorizes missing types as errors with a string warning pattern' do + categorizer = described_class.create_type_categorizer('warning') + messages = described_class.extract_messages(['file.rb'], /(?.*)/, categorizer) + expect(messages.first.type).to eq(:error) + end end diff --git a/spec/overcommit/utils_errors_spec.rb b/spec/overcommit/utils_errors_spec.rb index b798f2d2..d35a7c4c 100644 --- a/spec/overcommit/utils_errors_spec.rb +++ b/spec/overcommit/utils_errors_spec.rb @@ -41,4 +41,33 @@ and_return("git commit\n") expect(described_class.parent_command).to eq('git commit') end + + it 'uses the legacy Git directory command with older Git versions' do + stub_const('Overcommit::Utils::GIT_VERSION', described_class::Version.new('2.4')) + expect(described_class).to receive(:execute).with(%w[git rev-parse --git-dir]). + and_return(double(success?: true, stdout: ".git\n")) + expect(described_class.git_dir).to eq(File.expand_path('.git')) + end + + it 'tries Windows executable extensions when PATHEXT is set' do + allow(described_class).to receive(:repo_root).and_return(Dir.pwd) + Overcommit::Utils.with_environment('PATHEXT' => '.EXE;.BAT') do + allow(File).to receive(:executable?).and_return(false) + expect(File).to receive(:executable?).with(File.join(Dir.pwd, 'tool.EXE')).and_return(true) + expect(described_class.in_path?('tool')).to eq(true) + end + end + + it 'recognizes commands that already have a Windows executable extension' do + allow(described_class).to receive(:repo_root).and_return(Dir.pwd) + Overcommit::Utils.with_environment('PATHEXT' => '.EXE') do + expect(File).to receive(:executable?).with(File.join(Dir.pwd, 'tool.exe')).and_return(true) + expect(described_class.in_path?('tool.exe')).to eq(true) + end + end + + it 'returns no parent command if PowerShell fails' do + allow(described_class).to receive(:execute).and_return(double(success?: false, stdout: '')) + expect(described_class.send(:powershell_windows_parent_command)).to be_nil + end end From f207084677476c23ee85f4c2cf683def1bec5c62 Mon Sep 17 00:00:00 2001 From: Shane da Silva Date: Tue, 10 Nov 2026 22:37:35 -0800 Subject: [PATCH 2/2] Use a portable rooted path in the Foodcritic regression test --- spec/overcommit/hook/pre_commit/foodcritic_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/overcommit/hook/pre_commit/foodcritic_spec.rb b/spec/overcommit/hook/pre_commit/foodcritic_spec.rb index 74349ba8..85199960 100644 --- a/spec/overcommit/hook/pre_commit/foodcritic_spec.rb +++ b/spec/overcommit/hook/pre_commit/foodcritic_spec.rb @@ -129,7 +129,7 @@ let(:applicable_files) { [] } it 'uses absolute cookbook paths without joining them to the repository root' do - cookbook_directory = File.expand_path('cookbooks') + cookbook_directory = File.join(File::SEPARATOR, 'cookbooks') allow(subject).to receive(:config).and_return( subject.config.merge('cookbooks_directory' => cookbook_directory) )