dideler
1/14/2014 - 7:16 PM

pre-push script: Protects some branches from destructive actions.

pre-push script: Protects some branches from destructive actions.

#!/usr/bin/env ruby

# NOTE! this is a work in progress.  This is not tested or used regularly.

# Ensures we do not call destructive commands on protected branches.
#
# Called by "git push" after it has checked the remote status,
# but before anything has been pushed.
#
# If this script exits with a non-zero status nothing will be pushed.
#
# Steps to install, from the root directory of your repo...
# 1. Copy the file into your repo at `.git/hooks/pre-push`
# 2. Set executable permissions, run `chmod +x .git/hooks/pre-push`
# 3. Or, use `rake hooks:pre_push` to install
#
# Try a force push to master, you should get a message `*** [Policy] never force push...`
#
# The commands below will not be allowed...
# `git push --force origin master`
# `git push --delete origin master`
# `git push origin :master`
#
# Nor will a force push while on the master branch be allowed...
# `git co master`
# `git push --force origin`
# 
# Requires git 1.8.2 or newer
#
# Git 1.8.2 release notes cover the new pre-push hook:
# <https://github.com/git/git/blob/master/Documentation/RelNotes/1.8.2.txt>
#
# See Sample pre-push script:
# <https://github.com/git/git/blob/87c86dd14abe8db7d00b0df5661ef8cf147a72a3/templates/hooks--pre-push.sample>
#
# Also pulled ideas from:
# * http://blog.bigbinary.com/2013/09/19/do-not-allow-force-pusht-to-master.html
# * https://mug.im/how-to-prevent-yourself-from-force-pushing-to-master/

class ProtectedBranchesHandler

  def handle
    if pushing_to_protected_branch? && command_is_destructive?
      reject 
    else
      exit 0
    end
  end
 
  def protected_branches
    %w[master production]
  end
  
  private
  
  def command_is_delete?(command)
    command =~ /--delete/
  end

  def command_is_destructive?
    command_is_forced_push?(current_command) || command_is_delete?(current_command)
  end

  def command_is_forced_push?(command)
    command =~ /--force|-f|--pfush/
  end

  def current_branch
    result = %x{git symbolic-ref HEAD | sed -e 's,.*/\(.*\),\1,'}
    if result =~ /^failure/
      exit_as_failure result
    else
      result
    end
  end
  
  def current_command
    $(ps -ocommand= -p $PPID)
  end

  def exit_as_failure(messages)
    messages = Array(messages)
    unless messages.empty?
      puts "*"*40
      [messages].flatten.each do |message|
        puts message
      end
      puts "*"*40
    end
    
    exit 1
  end
  
  def pushing_to_protected_branch?
    protected_branches.include? current_branch
  end

  def reject
    messages = ["Your attempt to run a destructive command on '#{current_branch}' has been rejected."]
    messages << "If you still want to FORCE PUSH then you need to ignore the pre_push git hook by executing following command."
    messages << "git push master --force --no-verify"
    exit_as_failure messages
  end

end

ProtectedBranchesHandler.new.handle