Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion lib/overcommit/cli.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ module Overcommit
class CLI # rubocop:disable Metrics/ClassLength
def initialize(arguments, input, logger)
@arguments = arguments
@cli_options = {}
@input = input
@log = logger
@options = {}
Expand All @@ -28,6 +29,8 @@ def run
sign
when :run_all
run_all
when :diff
diff
end
rescue Overcommit::Exceptions::ConfigurationSignatureChanged => e
puts e
Expand All @@ -45,7 +48,7 @@ def parse_arguments
@parser = create_option_parser

begin
@parser.parse!(@arguments)
@parser.parse!(@arguments, into: @cli_options)

# Default action is to install
@options[:action] ||= :install
Expand Down Expand Up @@ -98,6 +101,11 @@ def add_installation_options(opts)
@options[:action] = :run_all
@options[:hook_to_run] = arg ? arg.to_s : 'run-all'
end

opts.on('--diff [ref]', 'Run pre_commit hooks against the diff between a given ref. Defaults to `main`.') do |arg| # rubocop:disable Layout/LineLength
@options[:action] = :diff
arg
end
end

def add_other_options(opts)
Expand Down Expand Up @@ -209,6 +217,19 @@ def run_all
halt(status ? 0 : 65)
end

def diff
empty_stdin = File.open(File::NULL) # pre-commit hooks don't take input
context = Overcommit::HookContext.create('diff', config, @arguments, empty_stdin, **@cli_options) # rubocop:disable Layout/LineLength
config.apply_environment!(context, ENV)

printer = Overcommit::Printer.new(config, log, context)
runner = Overcommit::HookRunner.new(config, log, context, printer)

status = runner.run

halt(status ? 0 : 65)
end

# Used for ease of stubbing in tests
def halt(status = 0)
exit status
Expand Down
4 changes: 2 additions & 2 deletions lib/overcommit/hook_context.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@

# Utility module which manages the creation of {HookContext}s.
module Overcommit::HookContext
def self.create(hook_type, config, args, input)
def self.create(hook_type, config, args, input, **cli_options)
hook_type_class = Overcommit::Utils.camel_case(hook_type)
underscored_hook_type = Overcommit::Utils.snake_case(hook_type)

require "overcommit/hook_context/#{underscored_hook_type}"

Overcommit::HookContext.const_get(hook_type_class).new(config, args, input)
Overcommit::HookContext.const_get(hook_type_class).new(config, args, input, **cli_options)
rescue LoadError, NameError => e
# Could happen when a symlink was created for a hook type Overcommit does
# not yet support.
Expand Down
4 changes: 3 additions & 1 deletion lib/overcommit/hook_context/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,12 @@ class Base
# @param config [Overcommit::Configuration]
# @param args [Array<String>]
# @param input [IO] standard input stream
def initialize(config, args, input)
# @param options [Hash] cli options
def initialize(config, args, input, **options)
@config = config
@args = args
@input = input
@options = options
end

# Executes a command as if it were a regular git hook, passing all
Expand Down
37 changes: 37 additions & 0 deletions lib/overcommit/hook_context/diff.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# frozen_string_literal: true

require 'overcommit/git_repo'

module Overcommit::HookContext
# Simulates a pre-commit context based on the diff with another git ref.
#
# This results in pre-commit hooks running against the changes between the current
# and another ref, which is useful for automated CI scripts.
class Diff < Base
def modified_files
@modified_files ||= Overcommit::GitRepo.modified_files(refs: @options[:diff])
end

def modified_lines_in_file(file)
@modified_lines ||= {}
@modified_lines[file] ||= Overcommit::GitRepo.extract_modified_lines(file,
refs: @options[:diff])
end

def hook_class_name
'PreCommit'
end

def hook_type_name
'pre_commit'
end

def hook_script_name
'pre-commit'
end

def initial_commit?
@initial_commit ||= Overcommit::GitRepo.initial_commit?
end
end
end
39 changes: 39 additions & 0 deletions spec/integration/diff_flag_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# frozen_string_literal: true

require 'spec_helper'

describe 'overcommit --diff' do
subject { shell(%w[overcommit --diff main]) }

context 'when using an existing pre-commit hook script' do
let(:script_name) { 'test-script' }
let(:script_contents) { "#!/bin/bash\nexit 0" }
let(:script_path) { ".#{Overcommit::OS::SEPARATOR}#{script_name}" }

let(:config) do
{
'PreCommit' => {
'MyHook' => {
'enabled' => true,
'required_executable' => script_path,
}
}
}
end

around do |example|
repo do
File.open('.overcommit.yml', 'w') { |f| f.puts(config.to_yaml) }
echo(script_contents, script_path)
`git add #{script_path}`
FileUtils.chmod(0o755, script_path)
example.run
end
end

it 'completes successfully without blocking' do
wait_until(timeout: 10) { subject } # Need to wait long time for JRuby startup
subject.status.should == 0
end
end
end
26 changes: 26 additions & 0 deletions spec/overcommit/cli_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

require 'spec_helper'
require 'overcommit/cli'
require 'overcommit/hook_context/diff'
require 'overcommit/hook_context/run_all'

describe Overcommit::CLI do
Expand Down Expand Up @@ -125,5 +126,30 @@
subject
end
end

context 'with the diff switch specified' do
let(:arguments) { ['--diff=some-ref'] }
let(:config) { Overcommit::ConfigurationLoader.default_configuration }

before do
cli.stub(:halt)
Overcommit::HookRunner.any_instance.stub(:run)
end

it 'creates a HookRunner with the diff context' do
Overcommit::HookRunner.should_receive(:new).
with(config,
logger,
instance_of(Overcommit::HookContext::Diff),
instance_of(Overcommit::Printer)).
and_call_original
subject
end

it 'runs the HookRunner' do
Overcommit::HookRunner.any_instance.should_receive(:run)
subject
end
end
end
end
143 changes: 143 additions & 0 deletions spec/overcommit/hook_context/diff_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
# frozen_string_literal: true

require 'spec_helper'
require 'overcommit/hook_context/diff'

describe Overcommit::HookContext::Diff do
let(:config) { double('config') }
let(:args) { [] }
let(:input) { double('input') }
let(:context) { described_class.new(config, args, input, diff: 'master') }

describe '#modified_files' do
subject { context.modified_files }

context 'when repo contains no files' do
around do |example|
repo do
`git commit --allow-empty -m "Initial commit"`
`git checkout -b other-branch 2>&1`
example.run
end
end

it { should be_empty }
end

context 'when the repo contains files that are unchanged from the ref' do
around do |example|
repo do
touch('some-file')
`git add some-file`
touch('some-other-file')
`git add some-other-file`
`git commit -m "Add files"`
`git checkout -b other-branch 2>&1`
example.run
end
end

it { should be_empty }
end

context 'when repo contains files that have been changed from the ref' do
around do |example|
repo do
touch('some-file')
`git add some-file`
touch('some-other-file')
`git add some-other-file`
`git commit -m "Add files"`
`git checkout -b other-branch 2>&1`
File.open('some-file', 'w') { |f| f.write("hello\n") }
`git add some-file`
`git commit -m "Edit file"`
example.run
end
end

it { should == %w[some-file].map { |file| File.expand_path(file) } }
end

context 'when repo contains submodules' do
around do |example|
submodule = repo do
touch 'foo'
`git add foo`
`git commit -m "Initial commit"`
end

repo do
`git submodule add #{submodule} test-sub 2>&1 > #{File::NULL}`
`git commit --allow-empty -m "Initial commit"`
`git checkout -b other-branch 2>&1`
example.run
end
end

it { should_not include File.expand_path('test-sub') }
end
end

describe '#modified_lines_in_file' do
let(:modified_file) { 'some-file' }
subject { context.modified_lines_in_file(modified_file) }

context 'when file contains a trailing newline' do
around do |example|
repo do
touch(modified_file)
`git add #{modified_file}`
`git commit -m "Add file"`
`git checkout -b other-branch 2>&1`
File.open(modified_file, 'w') { |f| (1..3).each { |i| f.write("#{i}\n") } }
`git add #{modified_file}`
`git commit -m "Edit file"`
example.run
end
end

it { should == Set.new(1..3) }
end

context 'when file does not contain a trailing newline' do
around do |example|
repo do
touch(modified_file)
`git add #{modified_file}`
`git commit -m "Add file"`
`git checkout -b other-branch 2>&1`
File.open(modified_file, 'w') do |f|
(1..2).each { |i| f.write("#{i}\n") }
f.write(3)
end
`git add #{modified_file}`
`git commit -m "Edit file"`
example.run
end
end

it { should == Set.new(1..3) }
end
end

describe '#hook_type_name' do
subject { context.hook_type_name }

it { should == 'pre_commit' }
end

describe '#hook_script_name' do
subject { context.hook_script_name }

it { should == 'pre-commit' }
end

describe '#initial_commit?' do
subject { context.initial_commit? }

before { Overcommit::GitRepo.stub(:initial_commit?).and_return(true) }

it { should == true }
end
end