require "ruby_git_hooks/version"
module RubyGitHooks
CAN_FAIL_HOOKS = [ "pre-commit", "pre-receive", "commit-msg" ]
NO_FAIL_HOOKS = [ "post-receive", "post-commit" ]
HOOK_NAMES = CAN_FAIL_HOOKS + NO_FAIL_HOOKS
class Hook
class << self
attr_reader :registered_hooks
attr_reader :run_as
attr_reader :run_from
attr_reader :run_as_hook
attr_reader :has_run
attr_accessor :files_changed
attr_accessor :file_contents
attr_accessor :file_diffs
attr_accessor :ls_files
attr_accessor :commit_message
attr_accessor :commit_message_file
attr_accessor :commits
attr_accessor :commit_ref_map
attr_accessor :branches_changed
end
HOOK_INFO = [ :files_changed, :file_contents, :file_diffs, :ls_files,
:commits, :commit_message, :commit_message_file, :commit_ref_map, :branches_changed ]
HOOK_INFO.each do |info_method|
define_method(info_method) do |*args, &block|
Hook.send(info_method, *args, &block)
end
end
HOOK_TYPE_SETUP = {
"pre-receive" => proc {
def commit_date(c)
date = Hook.shell!("git log #{c} --pretty=%ct -1").strip.to_i
end
changes = []
STDIN.each_line do |line|
base, commit, ref = line.strip.split
changes.push [base, commit, ref]
end
self.branches_changed = {}
self.commit_ref_map = {}
exclude_refs = [] all_branches = Hook.shell!("git for-each-ref --format='%(refname)' refs/heads/").split
changes.each do |base, _ , ref|
all_branches.delete(ref) exclude_refs << "^#{base}" unless base =~ /\A0+\z/ end
all_branches.each { |ref| exclude_refs << "^#{ref}" }
self.files_changed = []
self.file_contents = {}
self.file_diffs = {}
changes.each do |base, commit, ref|
self.branches_changed[ref] = [base, commit]
if base =~ /\A0+\z/
files_with_status = []
else
files_with_status = Hook.shell!("git diff --name-status #{base}..#{commit}").split("\n")
end
files_with_status.each do |f|
status, file_changed = f.scan(/([ACDMRTUXB])\s+(\S+)$/).flatten
self.files_changed << file_changed
file_diffs[file_changed] = Hook.shell!("git log -p #{commit} -- #{file_changed}")
begin
file_contents[file_changed] = status == "D" ? "" : Hook.shell!("git show #{commit}:#{file_changed}")
rescue
file_contents[file_changed] = ""
end
end
new_commits = Hook.shell!("git rev-list #{commit} #{exclude_refs.join(' ')} --").split("\n")
new_commits.each do |one_commit|
self.commit_ref_map[one_commit] ||= [];
self.commit_ref_map[one_commit] << ref end
end
self.commits = self.commit_ref_map.keys.sort{|a,b|commit_date(b) <=> commit_date(a)}
if !self.commits.empty?
file_list_revision = self.commits.first self.ls_files = Hook.shell!("git ls-tree --full-tree --name-only -r #{file_list_revision}").split("\n")
end
},
"pre-commit" => proc {
files_with_status = Hook.shell!("git diff --name-status --cached").split("\n")
self.files_changed = []
self.file_contents = {}
self.file_diffs = {}
self.commits = []
files_with_status.each do |f|
status, file_changed = f.scan(/([ACDMRTUXB])\s+(\S+)$/).flatten
self.files_changed << file_changed
file_diffs[file_changed] = Hook.shell!("git diff --cached -- #{file_changed}")
file_contents[file_changed] = status == "D"? "": Hook.shell!("git show :#{file_changed}")
end
self.ls_files = Hook.shell!("git ls-files").split("\n")
},
"post-commit" => proc {
last_commit_files = Hook.shell!("git log --oneline --name-status -1")
files_with_status = last_commit_files.split("\n")[1..-1]
self.files_changed = []
self.commits = [ Hook.shell!("git log -n 1 --pretty=format:%H").chomp ]
self.file_contents = {}
self.file_diffs = {}
files_with_status.each do |f|
status, file_changed = f.scan(/([ACDMRTUXB])\s+(\S+)$/).flatten
self.files_changed << file_changed
file_diffs[file_changed] = Hook.shell!("git log --oneline -p -1 -- #{file_changed}")
file_contents[file_changed] = status == "D"? "": Hook.shell!("git show :#{file_changed}")
end
self.ls_files = Hook.shell!("git ls-files").split("\n")
self.commit_message = Hook.shell!("git log -1 --pretty=%B")
},
"commit-msg" => proc {
files_with_status = Hook.shell!("git diff --name-status --cached").split("\n")
self.files_changed = []
self.file_contents = {}
self.file_diffs = {}
self.commits = []
files_with_status.each do |f|
status, file_changed = f.scan(/([ACDMRTUXB])\s+(\S+)$/).flatten
self.files_changed << file_changed
file_diffs[file_changed] = Hook.shell!("git diff --cached -- #{file_changed}")
file_contents[file_changed] = status == "D"? "": Hook.shell!("git show :#{file_changed}")
end
self.ls_files = Hook.shell!("git ls-files").split("\n")
self.commit_message = File.read(ARGV[0])
self.commit_message_file = ARGV[0]
}
}
HOOK_TYPE_SETUP["post-receive"] = HOOK_TYPE_SETUP["pre-receive"]
def self.initial_setup
return if @run_from
@run_from = Dir.getwd
@run_as = $0
end
def setup
Dir.chdir Hook.run_from do
yield
end
ensure
end
def self.get_hooks_to_run(hook_specs)
@registered_hooks ||= {}
if hook_specs.empty?
return @registered_hooks.values.inject([], &:+)
end
hook_specs.flat_map do |spec|
if @registered_hooks[spec]
@registered_hooks[spec]
elsif spec.is_a?(Hook)
[ spec ]
elsif spec.is_a?(String)
@registered_hooks[Object.const_get(spec)]
else
raise "Can't find hook for specification: #{spec.inspect}!"
end
end
end
def self.run(*hook_specs)
if @has_run
STDERR.puts <<ERR
In this version, you can't call .run more than once. For now, please
register your hooks individually and then call .run with no args, or
else call .run with both as arguments. This may be fixed in a future
version. Sorry!
ERR
exit 1
end
@has_run = true
initial_setup
run_as_specific_githook
hooks_to_run = get_hooks_to_run(hook_specs.flatten)
failed_hooks = []
val = nil
hooks_to_run.each do |hook|
begin
hook.setup { val = hook.check } failed_hooks.push(hook) unless val
rescue
STDERR.puts "Hook #{hook.inspect} raised exception: #{$!.inspect}!\n#{$!.backtrace.join("\n")}"
failed_hooks.push hook
end
end
if CAN_FAIL_HOOKS.include?(@run_as_hook) && failed_hooks.size > 0
STDERR.puts "Hooks failed: #{failed_hooks}"
STDERR.puts "Use 'git commit -eF .git/COMMIT_EDITMSG' to restore your commit message" if commit_message
STDERR.puts "Exiting!"
exit 1
end
end
def self.run_as_specific_githook
return if @run_as_hook
self.initial_setup
if ARGV.include? "--hook"
idx = ARGV.find_index "--hook"
@run_as_hook = ARGV[idx + 1]
2.times { ARGV.delete_at(idx) }
else
@run_as_hook = HOOK_NAMES.detect { |hook| @run_as.include?(hook) }
end
unless @run_as_hook
STDERR.puts "Name #{@run_as.inspect} doesn't include " +
"any of: #{HOOK_NAMES.inspect}"
exit 1
end
unless HOOK_TYPE_SETUP[@run_as_hook]
STDERR.puts "No setup defined for hook type #{@run_as_hook.inspect}!"
exit 1
end
self.instance_eval(&HOOK_TYPE_SETUP[@run_as_hook])
end
def self.register(hook)
@registered_hooks ||= {}
@registered_hooks[hook.class.name] ||= []
@registered_hooks[hook.class.name].push hook
end
def self.shell!(*args)
output = `#{args.join(" ")}`
unless $?.success?
STDERR.puts "Job #{args.inspect} failed in dir #{Dir.getwd.inspect}"
STDERR.puts "Failed job output:\n#{output}\n======"
raise "Exec of #{args.inspect} failed: #{$?}!"
end
output
end
end
class << self
[ :run, :register, :run_as ].each do |method|
define_method(method) do |*args, &block|
RubyGitHooks::Hook.send(method, *args, &block)
end
end
end
def self.shebang
ENV['RUBYGITHOOKS_SHEBANG']
end
def self.current_hook
RubyGitHooks::Hook.run_as_specific_githook
RubyGitHooks::Hook.run_as_hook
end
end
ENV['RUBYGITHOOKS_SHEBANG'] ||= "#!/usr/bin/env ruby"