Skip to content

Commit 47962f7

Browse files
dclunasds
authored andcommitted
prepare-commit-message hook (sds#520)
* Add prepare-commit-msg hook * This commit adds the ReplaceBranch prepare-commit-msg hook, which, when correctly configured, automatically generates a commit message template based on the branch name. Groups captured in the branch_pattern regex can be used in replacement_text; see the accompanying spec for details. Also, replacement_text can be a path to a file, whose text will be processed following the same rules. * Address code review comments relating to concurrency and default.yml config file * Updating ReplaceBranch hook * Fixing CI errors * Hopefully fixing Appveyor windows build * Not removing test file in Appveyor due to build errors * Fixing replace_branch with the new commit_message_source API
1 parent bcef6bb commit 47962f7

File tree

9 files changed

+372
-2
lines changed

9 files changed

+372
-2
lines changed

config/default.yml

+14
Original file line numberDiff line numberDiff line change
@@ -1143,6 +1143,20 @@ PostRewrite:
11431143
- 'package.json'
11441144
- 'yarn.lock'
11451145

1146+
# Hooks that run during the `prepare-commit-msg` hook.
1147+
PrepareCommitMsg:
1148+
ALL:
1149+
requires_files: false
1150+
required: false
1151+
quiet: false
1152+
1153+
ReplaceBranch:
1154+
enabled: false
1155+
description: 'Prepends the commit message with text based on the branch name'
1156+
branch_pattern: '\A.*\w+[-_](\d+).*\z'
1157+
replacement_text: '[#\1]'
1158+
on_fail: warn
1159+
11461160
# Hooks that run during `git push`, after remote refs have been updated but
11471161
# before any objects have been transferred.
11481162
PrePush:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
require 'forwardable'
2+
3+
module Overcommit::Hook::PrepareCommitMsg
4+
# Functionality common to all prepare-commit-msg hooks.
5+
class Base < Overcommit::Hook::Base
6+
extend Forwardable
7+
8+
def_delegators :@context,
9+
:commit_message_filename, :commit_message_source, :commit, :lock
10+
11+
def modify_commit_message
12+
raise 'This expects a block!' unless block_given?
13+
# NOTE: this assumes all the hooks of the same type share the context's
14+
# memory. If that's not the case, this won't work.
15+
lock.synchronize do
16+
contents = File.read(commit_message_filename)
17+
File.open(commit_message_filename, 'w') do |f|
18+
f << (yield contents)
19+
end
20+
end
21+
end
22+
end
23+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
module Overcommit::Hook::PrepareCommitMsg
2+
# Prepends the commit message with a message based on the branch name.
3+
# It's possible to reference parts of the branch name through the captures in
4+
# the `branch_pattern` regex.
5+
class ReplaceBranch < Base
6+
def run
7+
return :pass unless !commit_message_source ||
8+
commit_message_source == :commit # NOTE: avoid 'merge' and 'rebase'
9+
Overcommit::Utils.log.debug(
10+
"Checking if '#{Overcommit::GitRepo.current_branch}' matches #{branch_pattern}"
11+
)
12+
if branch_pattern.match(Overcommit::GitRepo.current_branch)
13+
Overcommit::Utils.log.debug("Writing #{commit_message_filename} with #{new_template}")
14+
modify_commit_message do |old_contents|
15+
"#{new_template}\n#{old_contents}"
16+
end
17+
:pass
18+
else
19+
:warn
20+
end
21+
end
22+
23+
def new_template
24+
@new_template ||= Overcommit::GitRepo.current_branch.gsub(branch_pattern, replacement_text)
25+
end
26+
27+
def branch_pattern
28+
@branch_pattern ||=
29+
begin
30+
pattern = config['branch_pattern']
31+
Regexp.new((pattern || '').empty? ? '\A.*\w+[-_](\d+).*\z' : pattern)
32+
end
33+
end
34+
35+
def replacement_text
36+
@replacement_text ||=
37+
begin
38+
if File.exist?(replacement_text_config)
39+
File.read(replacement_text_config)
40+
else
41+
replacement_text_config
42+
end
43+
end
44+
end
45+
46+
def replacement_text_config
47+
@replacement_text_config ||= config['replacement_text']
48+
end
49+
end
50+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
module Overcommit::HookContext
2+
# Contains helpers related to contextual information used by prepare-commit-msg
3+
# hooks.
4+
class PrepareCommitMsg < Base
5+
# Returns the name of the file that contains the commit log message
6+
def commit_message_filename
7+
@args[0]
8+
end
9+
10+
# Returns the source of the commit message, and can be: message (if a -m or
11+
# -F option was given); template (if a -t option was given or the
12+
# configuration option commit.template is set); merge (if the commit is a
13+
# merge or a .git/MERGE_MSG file exists); squash (if a .git/SQUASH_MSG file
14+
# exists); or commit, followed by a commit SHA-1 (if a -c, -C or --amend
15+
# option was given)
16+
def commit_message_source
17+
@args[1].to_sym if @args[1]
18+
end
19+
20+
# Returns the commit's SHA-1.
21+
# If commit_message_source is :commit, it's passed through the command-line.
22+
def commit_message_source_ref
23+
@args[2] || `git rev-parse HEAD`
24+
end
25+
26+
# Lock for the pre_commit_message file. Should be shared by all
27+
# prepare-commit-message hooks
28+
def lock
29+
@lock ||= Monitor.new
30+
end
31+
end
32+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
require 'spec_helper'
2+
require 'overcommit/hook_context/prepare_commit_msg'
3+
4+
describe Overcommit::Hook::PrepareCommitMsg::Base do
5+
let(:config) { Overcommit::ConfigurationLoader.default_configuration }
6+
let(:context) { Overcommit::HookContext::PrepareCommitMsg.new(config, [], StringIO.new) }
7+
let(:printer) { double('printer') }
8+
9+
context 'when multiple hooks run simultaneously' do
10+
let(:hook_1) { described_class.new(config, context) }
11+
let(:hook_2) { described_class.new(config, context) }
12+
13+
let(:tempfile) { 'test-prepare-commit-msg.txt' }
14+
15+
let(:initial_content) { "This is a test\n" }
16+
17+
before do
18+
File.open(tempfile, 'w') do |f|
19+
f << initial_content
20+
end
21+
end
22+
23+
after do
24+
File.delete(tempfile)
25+
end
26+
27+
it 'works well with concurrency' do
28+
allow(context).to receive(:commit_message_filename).and_return(tempfile)
29+
allow(hook_1).to receive(:run) do
30+
hook_1.modify_commit_message do |contents|
31+
"alpha\n" + contents
32+
end
33+
end
34+
allow(hook_2).to receive(:run) do
35+
hook_2.modify_commit_message do |contents|
36+
contents + "bravo\n"
37+
end
38+
end
39+
Thread.new { hook_1.run }
40+
Thread.new { hook_2.run }
41+
Thread.list.each { |t| t.join unless t == Thread.current }
42+
expect(File.read(tempfile)).to match(/alpha\n#{initial_content}bravo\n/m)
43+
end
44+
end
45+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
require 'spec_helper'
2+
require 'overcommit/hook_context/prepare_commit_msg'
3+
4+
describe Overcommit::Hook::PrepareCommitMsg::ReplaceBranch do
5+
let(:config) { Overcommit::ConfigurationLoader.default_configuration }
6+
let(:context) do
7+
Overcommit::HookContext::PrepareCommitMsg.new(
8+
config, [prepare_commit_message_file, 'commit'], StringIO.new
9+
)
10+
end
11+
12+
let(:prepare_commit_message_file) { 'prepare_commit_message_file.txt' }
13+
14+
subject(:hook) { described_class.new(config, context) }
15+
16+
before do
17+
File.open(prepare_commit_message_file, 'w')
18+
allow(Overcommit::Utils).to receive_message_chain(:log, :debug)
19+
allow(Overcommit::GitRepo).to receive(:current_branch).and_return(new_head)
20+
end
21+
22+
after do
23+
File.delete(prepare_commit_message_file) unless ENV['APPVEYOR']
24+
end
25+
26+
let(:new_head) { 'userbeforeid-12345-branch-description' }
27+
28+
describe '#run' do
29+
context 'when the checked out branch matches the pattern' do
30+
it { is_expected.to pass }
31+
32+
context 'template contents' do
33+
subject(:template) { hook.new_template }
34+
35+
before do
36+
hook.stub(:replacement_text).and_return('Id is: \1')
37+
end
38+
39+
it { is_expected.to eq('Id is: 12345') }
40+
end
41+
end
42+
43+
context 'when the checked out branch does not match the pattern' do
44+
let(:new_head) { "this shouldn't match the default pattern" }
45+
46+
it { is_expected.to warn }
47+
end
48+
end
49+
50+
describe '#replacement_text' do
51+
subject(:replacement_text) { hook.replacement_text }
52+
let(:replacement_template_file) { 'valid_filename.txt' }
53+
let(:replacement) { 'Id is: \1' }
54+
55+
context 'when the replacement text points to a valid filename' do
56+
before do
57+
hook.stub(:replacement_text_config).and_return(replacement_template_file)
58+
File.stub(:exist?).and_return(true)
59+
File.stub(:read).with(replacement_template_file).and_return(replacement)
60+
end
61+
62+
describe 'it reads it as the replacement template' do
63+
it { is_expected.to eq(replacement) }
64+
end
65+
end
66+
end
67+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
require 'spec_helper'
2+
require 'overcommit/hook_context/prepare_commit_msg'
3+
4+
describe Overcommit::HookContext::PrepareCommitMsg do
5+
let(:config) { double('config') }
6+
let(:args) { [commit_message_filename, commit_message_source] }
7+
let(:commit_message_filename) { 'message-template.txt' }
8+
let(:commit_message_source) { :file }
9+
let(:commit) { 'SHA-1 here' }
10+
let(:input) { double('input') }
11+
let(:context) { described_class.new(config, args, input) }
12+
13+
describe '#commit_message_filename' do
14+
subject { context.commit_message_filename }
15+
16+
it { should == commit_message_filename }
17+
end
18+
19+
describe '#commit_message_source' do
20+
subject { context.commit_message_source }
21+
22+
it { should == commit_message_source }
23+
end
24+
end

spec/overcommit/utils_spec.rb

+2-2
Original file line numberDiff line numberDiff line change
@@ -118,15 +118,15 @@
118118
subject { described_class.supported_hook_types }
119119

120120
# rubocop:disable Metrics/LineLength
121-
it { should =~ %w[commit-msg pre-commit post-checkout post-commit post-merge post-rewrite pre-push pre-rebase] }
121+
it { should =~ %w[commit-msg pre-commit post-checkout post-commit post-merge post-rewrite pre-push pre-rebase prepare-commit-msg] }
122122
# rubocop:enable Metrics/LineLength
123123
end
124124

125125
describe '.supported_hook_type_classes' do
126126
subject { described_class.supported_hook_type_classes }
127127

128128
# rubocop:disable Metrics/LineLength
129-
it { should =~ %w[CommitMsg PreCommit PostCheckout PostCommit PostMerge PostRewrite PrePush PreRebase] }
129+
it { should =~ %w[CommitMsg PreCommit PostCheckout PostCommit PostMerge PostRewrite PrePush PreRebase PrepareCommitMsg] }
130130
# rubocop:enable Metrics/LineLength
131131
end
132132

0 commit comments

Comments
 (0)