diff --git a/lib/overcommit/hook/pre_push/protected_branches.rb b/lib/overcommit/hook/pre_push/protected_branches.rb index fe943695..aca3f2ec 100644 --- a/lib/overcommit/hook/pre_push/protected_branches.rb +++ b/lib/overcommit/hook/pre_push/protected_branches.rb @@ -1,7 +1,9 @@ # frozen_string_literal: true module Overcommit::Hook::PrePush - # Prevents destructive updates to specified branches. + # Prevents updates to specified branches. + # Accepts a 'destructive_only' option globally or per branch + # to only prevent destructive updates. class ProtectedBranches < Base def run return :pass unless illegal_pushes.any? @@ -17,32 +19,55 @@ def run def illegal_pushes @illegal_pushes ||= pushed_refs.select do |pushed_ref| - protected?(pushed_ref.remote_ref) && allow_non_destructive?(pushed_ref) + protected?(pushed_ref) end end - def protected?(remote_ref) + def protected?(ref) + find_pattern(ref.remote_ref)&.destructive?(ref) + end + + def find_pattern(remote_ref) ref_name = remote_ref[%r{refs/heads/(.*)}, 1] - return false if ref_name.nil? - protected_branch_patterns.any? do |pattern| - File.fnmatch(pattern, ref_name) + return if ref_name.nil? + + patterns.find do |pattern| + File.fnmatch(pattern.to_s, ref_name) end end - def protected_branch_patterns - @protected_branch_patterns ||= Array(config['branches']). - concat(Array(config['branch_patterns'])) + def patterns + @patterns ||= fetch_patterns end - def destructive_only? + def fetch_patterns + branch_configurations.map do |pattern| + if pattern.is_a?(Hash) + Pattern.new(pattern.keys.first, pattern['destructive_only']) + else + Pattern.new(pattern, global_destructive_only?) + end + end + end + + def branch_configurations + config['branches'].to_a + config['branch_patterns'].to_a + end + + def global_destructive_only? config['destructive_only'].nil? || config['destructive_only'] end - def allow_non_destructive?(ref) - if destructive_only? - ref.destructive? - else - true + Pattern = Struct.new('Pattern', :name, :destructive_only) do + alias_method :to_s, :name + alias_method :destructive_only?, :destructive_only + + def destructive?(ref) + if destructive_only? + ref.destructive? + else + true + end end end end diff --git a/spec/integration/protected_branches_spec.rb b/spec/integration/protected_branches_spec.rb index 8eade771..115be29d 100644 --- a/spec/integration/protected_branches_spec.rb +++ b/spec/integration/protected_branches_spec.rb @@ -24,6 +24,8 @@ enabled: true branches: - protected + - protected_for_destructive_only: + destructive_only: true YML around do |example| diff --git a/spec/overcommit/hook/pre_push/protected_branches_spec.rb b/spec/overcommit/hook/pre_push/protected_branches_spec.rb index 92a27d07..0f8db55a 100644 --- a/spec/overcommit/hook/pre_push/protected_branches_spec.rb +++ b/spec/overcommit/hook/pre_push/protected_branches_spec.rb @@ -15,13 +15,15 @@ let(:context) { double('context') } subject { described_class.new(config, context) } - let(:protected_branch_patterns) { ['master', 'release/*'] } + let(:branch_configurations) do + ['master', 'release/*', { 'destructive_only_branch' => nil, 'destructive_only' => true }] + end let(:pushed_ref) do instance_double(Overcommit::HookContext::PrePush::PushedRef) end before do - subject.stub(protected_branch_patterns: protected_branch_patterns) + subject.stub(branch_configurations: branch_configurations) pushed_ref.stub(:remote_ref).and_return("refs/heads/#{pushed_ref_name}") context.stub(:pushed_refs).and_return([pushed_ref]) end @@ -94,6 +96,16 @@ let(:pushed_ref_name) { 'release/0.1.0' } include_examples 'protected branch' end + + context 'when branch overwrites global destructive_only' do + before do + pushed_ref.stub(:destructive?).and_return(true) + end + let(:pushed_ref_name) { 'destructive_only_branch' } + let(:hook_config) { { 'destructive_only' => false } } + + it { should fail_hook } + end end context 'when pushing tags' do