diff --git a/config/default.yml b/config/default.yml index 9c808f68..16fbbffb 100644 --- a/config/default.yml +++ b/config/default.yml @@ -489,3 +489,11 @@ PrePush: enabled: false description: 'Running rspec test suite' required_executable: 'rspec' + +# Hooks that run during `git rebase`, before any commits are rebased. +# If a hook fails, the rebase is aborted. +PreRebase: + ALL: + requires_files: false + required: false + quiet: false diff --git a/lib/overcommit/hook/pre_rebase/base.rb b/lib/overcommit/hook/pre_rebase/base.rb new file mode 100644 index 00000000..91500f04 --- /dev/null +++ b/lib/overcommit/hook/pre_rebase/base.rb @@ -0,0 +1,11 @@ +require 'forwardable' + +module Overcommit::Hook::PreRebase + # Functionality common to all pre-rebase hooks. + class Base < Overcommit::Hook::Base + extend Forwardable + + def_delegators :@context, + :upstream_branch, :rebased_branch, :fast_forward?, :rebased_commits + end +end diff --git a/lib/overcommit/hook_context/pre_rebase.rb b/lib/overcommit/hook_context/pre_rebase.rb new file mode 100644 index 00000000..5a2ca149 --- /dev/null +++ b/lib/overcommit/hook_context/pre_rebase.rb @@ -0,0 +1,36 @@ +module Overcommit::HookContext + # Contains helpers related to contextual information used by pre-rebase + # hooks. + class PreRebase < Base + # Returns the name of the branch we are rebasing onto. + def upstream_branch + @args[0] + end + + # Returns the name of the branch being rebased. Empty if rebasing a + # detached HEAD. + def rebased_branch + @rebased_branch ||= + @args[1] || `git symbolic-ref --short --quiet HEAD`.chomp + end + + # Returns whether we are rebasing a detached HEAD rather than a branch + def detached_head? + rebased_branch.empty? + end + + # Returns whether this rebase is a fast-forward + def fast_forward? + rebased_commits.empty? + end + + # Returns the SHA1-sums of the series of commits to be rebased + # in reverse topological order. + def rebased_commits + rebased_ref = detached_head? ? 'HEAD' : rebased_branch + @rebased_commits ||= + `git rev-list --topo-order --reverse #{upstream_branch}..#{rebased_ref}`. + split("\n") + end + end +end diff --git a/spec/overcommit/hook_context/pre_rebase_spec.rb b/spec/overcommit/hook_context/pre_rebase_spec.rb new file mode 100644 index 00000000..311b25b3 --- /dev/null +++ b/spec/overcommit/hook_context/pre_rebase_spec.rb @@ -0,0 +1,107 @@ +require 'spec_helper' +require 'overcommit/hook_context/pre_rebase' + +describe Overcommit::HookContext::PreRebase do + let(:config) { double('config') } + let(:args) { [upstream_branch, rebased_branch] } + let(:upstream_branch) { 'master' } + let(:rebased_branch) { 'topic' } + let(:input) { double('input') } + let(:context) { described_class.new(config, args, input) } + + describe '#upstream_branch' do + subject { context.upstream_branch } + + it { should == upstream_branch } + end + + describe '#rebased_branch' do + subject { context.rebased_branch } + + it { should == rebased_branch } + + context 'when rebasing current branch' do + let(:rebased_branch) { nil } + let(:current_branch) { 'master' } + + around do |example| + repo do + `git checkout -b #{current_branch} &> /dev/null` + example.run + end + end + + it { should == current_branch } + end + end + + describe '#fast_forward?' do + subject { context.fast_forward? } + + context 'when upstream branch is descendent from rebased branch' do + before do + context.stub(:rebased_commits).and_return([]) + end + + it { should == true } + end + + context 'when upstream branch is not descendent from rebased branch' do + before do + context.stub(:rebased_commits).and_return([random_hash]) + end + + it { should == false } + end + end + + describe '#detached_head?' do + subject { context.detached_head? } + + context 'when rebasing a detached HEAD' do + let(:rebased_branch) { '' } + + it { should == true } + end + + context 'when rebasing a branch' do + let(:rebased_branch) { 'topic' } + + it { should == false } + end + end + + describe '#rebased_commits' do + subject { context.rebased_commits } + + let(:base_branch) { 'master' } + let(:topic_branch_1) { 'topic-1' } + let(:topic_branch_2) { 'topic-2' } + + around do |example| + repo do + `git checkout -b #{base_branch} &> /dev/null` + `git commit --allow-empty -m "Initial Commit"` + `git checkout -b #{topic_branch_1} &> /dev/null` + `git commit --allow-empty -m "Hello World"` + `git checkout -b #{topic_branch_2} #{base_branch} &> /dev/null` + `git commit --allow-empty -m "Hello Again"` + example.run + end + end + + context 'when upstream branch is descendent from rebased branch' do + let(:upstream_branch) { topic_branch_1 } + let(:rebased_branch) { base_branch } + + it { should be_empty } + end + + context 'when upstream branch is not descendent from rebased branch' do + let(:upstream_branch) { topic_branch_1 } + let(:rebased_branch) { topic_branch_2 } + + it { should_not be_empty } + end + end +end diff --git a/spec/overcommit/utils_spec.rb b/spec/overcommit/utils_spec.rb index 8d3777c8..91bb32ef 100644 --- a/spec/overcommit/utils_spec.rb +++ b/spec/overcommit/utils_spec.rb @@ -109,7 +109,7 @@ subject { described_class.supported_hook_types } # rubocop:disable Metrics/LineLength - it { should =~ %w[commit-msg pre-commit post-checkout post-commit post-merge post-rewrite pre-push] } + it { should =~ %w[commit-msg pre-commit post-checkout post-commit post-merge post-rewrite pre-push pre-rebase] } # rubocop:enable Metrics/LineLength end @@ -117,7 +117,7 @@ subject { described_class.supported_hook_type_classes } # rubocop:disable Metrics/LineLength - it { should =~ %w[CommitMsg PreCommit PostCheckout PostCommit PostMerge PostRewrite PrePush] } + it { should =~ %w[CommitMsg PreCommit PostCheckout PostCommit PostMerge PostRewrite PrePush PreRebase] } # rubocop:enable Metrics/LineLength end diff --git a/template-dir/hooks/pre-rebase b/template-dir/hooks/pre-rebase new file mode 120000 index 00000000..d4cfaf72 --- /dev/null +++ b/template-dir/hooks/pre-rebase @@ -0,0 +1 @@ +overcommit-hook \ No newline at end of file