Skip to content

Commit 8110e60

Browse files
committed
implement diff hook context
1 parent 3219b60 commit 8110e60

File tree

5 files changed

+242
-0
lines changed

5 files changed

+242
-0
lines changed

lib/overcommit/cli.rb

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ def run
2929
sign
3030
when :run_all
3131
run_all
32+
when :diff
33+
diff
3234
end
3335
rescue Overcommit::Exceptions::ConfigurationSignatureChanged => e
3436
puts e
@@ -99,6 +101,11 @@ def add_installation_options(opts)
99101
@options[:action] = :run_all
100102
@options[:hook_to_run] = arg ? arg.to_s : 'run-all'
101103
end
104+
105+
opts.on('--diff [ref]', 'Run pre_commit hooks against the diff between a given ref. Defaults to `main`.') do |arg| # rubocop:disable Layout/LineLength
106+
@options[:action] = :diff
107+
arg
108+
end
102109
end
103110

104111
def add_other_options(opts)
@@ -210,6 +217,19 @@ def run_all
210217
halt(status ? 0 : 65)
211218
end
212219

220+
def diff
221+
empty_stdin = File.open(File::NULL) # pre-commit hooks don't take input
222+
context = Overcommit::HookContext.create('diff', config, @arguments, empty_stdin, **@cli_options) # rubocop:disable Layout/LineLength
223+
config.apply_environment!(context, ENV)
224+
225+
printer = Overcommit::Printer.new(config, log, context)
226+
runner = Overcommit::HookRunner.new(config, log, context, printer)
227+
228+
status = runner.run
229+
230+
halt(status ? 0 : 65)
231+
end
232+
213233
# Used for ease of stubbing in tests
214234
def halt(status = 0)
215235
exit status
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# frozen_string_literal: true
2+
3+
require 'overcommit/git_repo'
4+
5+
module Overcommit::HookContext
6+
# Simulates a pre-commit context based on the diff with another git ref.
7+
#
8+
# This results in pre-commit hooks running against the changes between the current
9+
# and another ref, which is useful for automated CI scripts.
10+
class Diff < Base
11+
def modified_files
12+
@modified_files ||= Overcommit::GitRepo.modified_files(refs: @options[:diff])
13+
end
14+
15+
def modified_lines_in_file(file)
16+
@modified_lines ||= {}
17+
@modified_lines[file] ||= Overcommit::GitRepo.extract_modified_lines(file,
18+
refs: @options[:diff])
19+
end
20+
21+
def hook_class_name
22+
'PreCommit'
23+
end
24+
25+
def hook_type_name
26+
'pre_commit'
27+
end
28+
29+
def hook_script_name
30+
'pre-commit'
31+
end
32+
33+
def initial_commit?
34+
@initial_commit ||= Overcommit::GitRepo.initial_commit?
35+
end
36+
end
37+
end

spec/integration/diff_flag_spec.rb

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# frozen_string_literal: true
2+
3+
require 'spec_helper'
4+
5+
describe 'overcommit --diff' do
6+
subject { shell(%w[overcommit --diff main]) }
7+
8+
context 'when using an existing pre-commit hook script' do
9+
let(:script_name) { 'test-script' }
10+
let(:script_contents) { "#!/bin/bash\nexit 0" }
11+
let(:script_path) { ".#{Overcommit::OS::SEPARATOR}#{script_name}" }
12+
13+
let(:config) do
14+
{
15+
'PreCommit' => {
16+
'MyHook' => {
17+
'enabled' => true,
18+
'required_executable' => script_path,
19+
}
20+
}
21+
}
22+
end
23+
24+
around do |example|
25+
repo do
26+
File.open('.overcommit.yml', 'w') { |f| f.puts(config.to_yaml) }
27+
echo(script_contents, script_path)
28+
`git add #{script_path}`
29+
FileUtils.chmod(0o755, script_path)
30+
example.run
31+
end
32+
end
33+
34+
it 'completes successfully without blocking' do
35+
wait_until(timeout: 10) { subject } # Need to wait long time for JRuby startup
36+
subject.status.should == 0
37+
end
38+
end
39+
end

spec/overcommit/cli_spec.rb

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
require 'spec_helper'
44
require 'overcommit/cli'
5+
require 'overcommit/hook_context/diff'
56
require 'overcommit/hook_context/run_all'
67

78
describe Overcommit::CLI do
@@ -125,5 +126,29 @@
125126
subject
126127
end
127128
end
129+
130+
context 'with the diff switch specified' do
131+
let(:arguments) { ['--diff some-branch'] }
132+
let(:config) { Overcommit::ConfigurationLoader.default_configuration }
133+
134+
before do
135+
cli.stub(:halt)
136+
end
137+
138+
it 'creates a HookRunner with the diff context' do
139+
Overcommit::HookRunner.should_receive(:new).
140+
with(config,
141+
logger,
142+
instance_of(Overcommit::HookContext::Diff),
143+
instance_of(Overcommit::Printer)).
144+
and_call_original
145+
subject
146+
end
147+
148+
it 'runs the HookRunner' do
149+
Overcommit::HookRunner.any_instance.should_receive(:run)
150+
subject
151+
end
152+
end
128153
end
129154
end
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
# frozen_string_literal: true
2+
3+
require 'spec_helper'
4+
require 'overcommit/hook_context/diff'
5+
6+
describe Overcommit::HookContext::Diff do
7+
let(:config) { double('config') }
8+
let(:args) { [] }
9+
let(:input) { double('input') }
10+
let(:context) { described_class.new(config, args, input, diff: 'master') }
11+
12+
describe '#modified_files' do
13+
subject { context.modified_files }
14+
15+
context 'when repo contains no files' do
16+
around do |example|
17+
repo do
18+
`git checkout -b other-branch 2>&1`
19+
example.run
20+
end
21+
end
22+
23+
it { should be_empty }
24+
end
25+
26+
context 'when the repo contains files that are unchanged from the ref' do
27+
around do |example|
28+
repo do
29+
touch('some-file')
30+
`git add some-file`
31+
touch('some-other-file')
32+
`git add some-other-file`
33+
`git commit -m "Add files"`
34+
`git checkout -b other-branch 2>&1`
35+
example.run
36+
end
37+
end
38+
39+
it { should be_empty }
40+
end
41+
42+
context 'when repo contains files that have been changed from the ref' do
43+
around do |example|
44+
repo do
45+
touch('some-file')
46+
`git add some-file`
47+
touch('some-other-file')
48+
`git add some-other-file`
49+
`git commit -m "Add files"`
50+
`git checkout -b other-branch 2>&1`
51+
File.open('some-file', 'w') { |f| f.write("hello\n") }
52+
`git add some-file`
53+
`git commit -m "Edit file"`
54+
example.run
55+
end
56+
end
57+
58+
it { should == %w[some-file].map { |file| File.expand_path(file) } }
59+
end
60+
61+
context 'when repo contains submodules' do
62+
around do |example|
63+
submodule = repo do
64+
touch 'foo'
65+
`git add foo`
66+
`git commit -m "Initial commit"`
67+
end
68+
69+
repo do
70+
`git submodule add #{submodule} test-sub 2>&1 > #{File::NULL}`
71+
`git checkout -b other-branch 2>&1`
72+
example.run
73+
end
74+
end
75+
76+
it { should_not include File.expand_path('test-sub') }
77+
end
78+
end
79+
80+
describe '#modified_lines_in_file' do
81+
let(:modified_file) { 'some-file' }
82+
subject { context.modified_lines_in_file(modified_file) }
83+
84+
context 'when file contains a trailing newline' do
85+
around do |example|
86+
repo do
87+
touch(modified_file)
88+
`git add #{modified_file}`
89+
`git commit -m "Add file"`
90+
`git checkout -b other-branch 2>&1`
91+
File.open(modified_file, 'w') { |f| (1..3).each { |i| f.write("#{i}\n") } }
92+
`git add #{modified_file}`
93+
`git commit -m "Edit file"`
94+
example.run
95+
end
96+
end
97+
98+
it { should == Set.new(1..3) }
99+
end
100+
101+
context 'when file does not contain a trailing newline' do
102+
around do |example|
103+
repo do
104+
touch(modified_file)
105+
`git add #{modified_file}`
106+
`git commit -m "Add file"`
107+
`git checkout -b other-branch 2>&1`
108+
File.open(modified_file, 'w') do |f|
109+
(1..2).each { |i| f.write("#{i}\n") }
110+
f.write(3)
111+
end
112+
`git add #{modified_file}`
113+
`git commit -m "Edit file"`
114+
example.run
115+
end
116+
end
117+
118+
it { should == Set.new(1..3) }
119+
end
120+
end
121+
end

0 commit comments

Comments
 (0)