Skip to content

Commit a2b6d2a

Browse files
committed
Add Gem hook plugin support
Support loading Overcommit hooks supplied by Gems in the same fashion as hooks that can be provided in Repo-Specific hooks.
1 parent 192c84b commit a2b6d2a

File tree

9 files changed

+203
-0
lines changed

9 files changed

+203
-0
lines changed

README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ any Ruby code.
4141
* [PreRebase](#prerebase)
4242
* [Repo-Specific Hooks](#repo-specific-hooks)
4343
* [Adding Existing Git Hooks](#adding-existing-git-hooks)
44+
* [Gem-provided Hooks](#gem-provided-hooks)
4445
* [Security](#security)
4546
* [Contributing](#contributing)
4647
* [Community](#community)
@@ -671,6 +672,24 @@ of hook, see the [git-hooks documentation][GHD].
671672

672673
[GHD]: https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks
673674

675+
## Gem-provided Hooks
676+
677+
For hooks which should be available from multiple repositories, but which do not
678+
make sense to contribute back to Overcommit, you can package them as a Ruby Gem
679+
and `overcommit` can use them as if they were repo-specific hooks. This would be
680+
useful for an organization that has custom hooks they wish to apply to many
681+
repositories, but manage the hooks in a central way.
682+
683+
These are implemented similarly to Repo-Specific Hooks, but need to be placed
684+
in a Gem in the normal `overcommit` hook path. For example, to add `MyCustomHook`
685+
as a pre_commit hook you would put it here:
686+
`./lib/overcommit/hooks/pre_commit/my_custom_hook.rb`
687+
688+
You must ensure that the Gem is available in the environment where `overcommit` is
689+
being run.
690+
691+
See [Repo-Specific Hooks](#repo-specific-hooks) for details.
692+
674693
## Security
675694

676695
While Overcommit can make managing Git hooks easier and more convenient,

config/default.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@
3434
# (Generate lock file by running `bundle install --gemfile=.overcommit_gems.rb`)
3535
gemfile: false
3636

37+
# If enabled, Overcommit will look for plugins provided anywhere in the
38+
# standard load path and load them as it would with built-in or repo-specific
39+
# hooks.
40+
gem_plugins: true
41+
3742
# Where to store hook plugins specific to a repository. These are loaded in
3843
# addition to the default hooks Overcommit comes with. The location is relative
3944
# to the root of the repository.

lib/overcommit.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
require 'overcommit/hook_signer'
1818
require 'overcommit/hook_loader/base'
1919
require 'overcommit/hook_loader/built_in_hook_loader'
20+
require 'overcommit/hook_loader/gem_hook_loader'
2021
require 'overcommit/hook_loader/plugin_hook_loader'
2122
require 'overcommit/interrupt_handler'
2223
require 'overcommit/printer'

lib/overcommit/configuration.rb

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,15 @@ def enabled_builtin_hooks(hook_context)
119119
select { |hook_name| hook_enabled?(hook_context, hook_name) }
120120
end
121121

122+
# Returns the gem-provided hooks that have been enabled for a hook type.
123+
def enabled_gem_hooks(hook_context)
124+
@hash[hook_context.hook_class_name].keys.
125+
reject { |hook_name| hook_name == 'ALL' }.
126+
reject { |hook_name| built_in_hook?(hook_context, hook_name) }.
127+
select { |hook_name| gem_hook?(hook_context, hook_name) }.
128+
select { |hook_name| hook_enabled?(hook_context, hook_name) }
129+
end
130+
122131
# Returns the ad hoc hooks that have been enabled for a hook type.
123132
def enabled_ad_hoc_hooks(hook_context)
124133
@hash[hook_context.hook_class_name].keys.
@@ -259,6 +268,7 @@ def ad_hoc_hook?(hook_context, hook_name)
259268
# Ad hoc hooks are neither built-in nor have a plugin file written but
260269
# still have a `command` specified to be run
261270
!built_in_hook?(hook_context, hook_name) &&
271+
!gem_hook?(hook_context, hook_name) &&
262272
!plugin_hook?(hook_context, hook_name) &&
263273
(ad_hoc_conf['command'] || ad_hoc_conf['required_executable'])
264274
end
@@ -270,8 +280,18 @@ def built_in_hook?(hook_context, hook_name)
270280
hook_context.hook_type_name, "#{hook_name}.rb"))
271281
end
272282

283+
def gem_hook?(hook_context, hook_name)
284+
hook_name = Overcommit::Utils.snake_case(hook_name)
285+
286+
$LOAD_PATH.any? do |path|
287+
File.exist?(File.join(path, 'overcommit', 'hook',
288+
hook_context.hook_type_name, "#{hook_name}.rb"))
289+
end
290+
end
291+
273292
def hook_exists?(hook_context, hook_name)
274293
built_in_hook?(hook_context, hook_name) ||
294+
gem_hook?(hook_context, hook_name) ||
275295
plugin_hook?(hook_context, hook_name) ||
276296
ad_hoc_hook?(hook_context, hook_name)
277297
end
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# frozen_string_literal: true
2+
3+
module Overcommit::HookLoader
4+
# Responsible for loading custom hooks that users install via Gems
5+
class GemHookLoader < Base
6+
def load_hooks
7+
@config.enabled_gem_hooks(@context).map do |hook_name|
8+
underscored_hook_name = Overcommit::Utils.snake_case(hook_name)
9+
require "overcommit/hook/#{@context.hook_type_name}/#{underscored_hook_name}"
10+
create_hook(hook_name)
11+
end
12+
end
13+
end
14+
end

lib/overcommit/hook_runner.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,11 @@ def load_hooks
200200

201201
@hooks += HookLoader::BuiltInHookLoader.new(@config, @context, @log).load_hooks
202202

203+
# Load Gem-based hooks next, if gem_plugins is enabled:
204+
if @config['gem_plugins']
205+
@hooks += HookLoader::GemHookLoader.new(@config, @context, @log).load_hooks
206+
end
207+
203208
# Load plugin hooks after so they can subclass existing hooks
204209
@hooks += HookLoader::PluginHookLoader.new(@config, @context, @log).load_hooks
205210
rescue LoadError => ex

spec/overcommit/configuration_spec.rb

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,4 +294,58 @@
294294
end
295295
end
296296
end
297+
298+
describe '#enabled_gem_hooks' do
299+
let(:hash) do
300+
{
301+
'PreCommit' => {
302+
'MyCustomHook' => {
303+
'enabled' => true
304+
},
305+
'MyOtherHook' => {
306+
'enabled' => false
307+
},
308+
}
309+
}
310+
end
311+
312+
let(:context) { double('context') }
313+
subject { config.enabled_gem_hooks(context) }
314+
315+
before do
316+
context.stub(hook_class_name: 'PreCommit',
317+
hook_type_name: 'pre_commit')
318+
end
319+
320+
it 'excludes hooks that are not found' do
321+
subject.should_not include 'MyCustomHook'
322+
subject.should_not include 'MyOtherHook'
323+
end
324+
325+
context 'when custom hooks are found' do
326+
before do
327+
$LOAD_PATH.unshift('/my/custom/path/lib')
328+
allow(File).to receive(:exist?).
329+
with('/my/custom/path/lib/overcommit/hook/pre_commit/my_custom_hook.rb').
330+
and_return(true)
331+
allow(File).to receive(:exist?).
332+
with(File.join(Overcommit::HOME, 'lib/overcommit/hook/pre_commit/my_custom_hook.rb')).
333+
and_return(false)
334+
allow(File).to receive(:exist?).
335+
with('/my/custom/path/lib/overcommit/hook/pre_commit/my_other_hook.rb').
336+
and_return(true)
337+
allow(File).to receive(:exist?).
338+
with(File.join(Overcommit::HOME, 'lib/overcommit/hook/pre_commit/my_other_hook.rb')).
339+
and_return(false)
340+
end
341+
342+
it 'includes hooks that are enabled and found' do
343+
subject.should include 'MyCustomHook'
344+
end
345+
346+
it 'excludes hooks that are not enable but found' do
347+
subject.should_not include 'MyOtherHook'
348+
end
349+
end
350+
end
297351
end
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# frozen_string_literal: true
2+
3+
require 'spec_helper'
4+
5+
describe Overcommit::HookLoader::GemHookLoader do
6+
let(:hash) { {} }
7+
let(:config) { Overcommit::Configuration.new(hash) }
8+
let(:logger) { double('logger') }
9+
let(:context) { double('context') }
10+
let(:loader) { described_class.new(config, context, logger) }
11+
12+
describe '#load_hooks' do
13+
subject(:load_hooks) { loader.send(:load_hooks) }
14+
15+
before do
16+
context.stub(hook_class_name: 'PreCommit',
17+
hook_type_name: 'pre_commit')
18+
end
19+
20+
it 'loads enabled gem hooks' do
21+
allow(config).to receive(:enabled_gem_hooks).with(context).and_return(['MyCustomHook'])
22+
23+
allow(loader).to receive(:require).
24+
with('overcommit/hook/pre_commit/my_custom_hook').
25+
and_return(true)
26+
allow(loader).to receive(:create_hook).with('MyCustomHook')
27+
expect(loader).to receive(:require).with('overcommit/hook/pre_commit/my_custom_hook')
28+
load_hooks
29+
end
30+
end
31+
end

spec/overcommit/hook_runner_spec.rb

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# frozen_string_literal: true
2+
3+
require 'spec_helper'
4+
5+
describe Overcommit::HookRunner do
6+
let(:hash) { {} }
7+
let(:config) { Overcommit::Configuration.new(hash) }
8+
let(:logger) { double('logger') }
9+
let(:context) { double('context') }
10+
let(:printer) { double('printer') }
11+
let(:runner) { described_class.new(config, logger, context, printer) }
12+
13+
describe '#load_hooks' do
14+
subject(:load_hooks) { runner.send(:load_hooks) }
15+
16+
before do
17+
context.stub(hook_class_name: 'PreCommit',
18+
hook_type_name: 'pre_commit')
19+
allow_any_instance_of(Overcommit::HookLoader::BuiltInHookLoader).
20+
to receive(:load_hooks).and_return([])
21+
allow_any_instance_of(Overcommit::HookLoader::PluginHookLoader).
22+
to receive(:load_hooks).and_return([])
23+
end
24+
25+
context 'when gem_plugins is disabled' do
26+
let(:hash) do
27+
{
28+
'gem_plugins' => false
29+
}
30+
end
31+
32+
it 'expects not to load Gem hooks' do
33+
expect_any_instance_of(Overcommit::HookLoader::GemHookLoader).
34+
not_to receive(:load_hooks)
35+
load_hooks
36+
end
37+
end
38+
39+
context 'when gem_plugins is enabled' do
40+
let(:hash) do
41+
{
42+
'gem_plugins' => true
43+
}
44+
end
45+
let(:gemhookloader) { Overcommit::HookLoader::GemHookLoader.new(config, context, logger) }
46+
47+
it 'expects to load Gem hooks' do
48+
expect_any_instance_of(Overcommit::HookLoader::GemHookLoader).
49+
to receive(:load_hooks).and_call_original
50+
load_hooks
51+
end
52+
end
53+
end
54+
end

0 commit comments

Comments
 (0)