Class: RubyGitHooks::Hook

Inherits:
Object
  • Object
show all
Defined in:
lib/ruby_git_hooks.rb

Overview

applypatch-msg, pre-applypatch, post-applypatch prepare-commit-msg, commit-msg pre-rebase, post-checkout, post-merge, update, post-update, pre-auto-gc, post-rewrite

Constant Summary collapse

HOOK_INFO =

Instances of Hook delegate these methods to the class methods.

[ :files_changed, :file_contents, :file_diffs, :ls_files,
:commits, :commit_message, :commit_message_file, :commit_ref_map ]
HOOK_TYPE_SETUP =
{

  # Pre-receive gets no args, but STDIN with a list of changes.
  "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|
      # STDERR.puts line # for debugging
      base, commit, ref = line.strip.split
      changes.push [base, commit, ref]
    end
    self.commit_ref_map = {}  #
    # commit_ref_map is a list of which new commits are in this push, and which branches they are associated with
    # as {commit1 => [ref1, ref2], commit2 => [ref1]}
    # For existing branches, this information is sent in directly "base commit ref"
    # BUT for branch new branches, the pre/post-receive hook gets "0 commit ref"
    # ref is of the form refs/heads/branch_name

    new_branches = changes.select{|base, _, _ | base =~  /\A0+\z/ }.collect{|_,_, ref| ref[/refs\/heads\/(\S+)/,1] }

    if !new_branches.empty?
      # For new branches, we will calculate which commits are new by specifically not including commits which are
      # present in any other branch (and therefore will have been processed with that branch)
      all_branches = Hook.shell!("git branch").split(/[* \n]+/).select{|b| !b.empty?}  # remove spaces and the *
      # ref is like refs/heads/<branch_name>
      existing_branches = all_branches - new_branches
      exclude_branches = existing_branches.inject("") {|str, b| str + " ^" + b} # "^B1 ^B2"
    end


    self.files_changed = []
    self.file_contents = {}
    self.file_diffs = {}

    changes.each do |base, commit, ref|
      new_branch = base =~  /\A0+\z/
      if new_branch
        # if base is 000 then this is a new branch and we have no easy way know what files were added
        # so for now just don't include files changed in a new branch
        # because really this should be done per commit or at least per branch anyway
        # TODO: we could figure it out based on the branch commit calculations per branch (see below)
        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
          # weird bug where some repos can't run the git show command even when it's not a deleted file.
          # example: noah-gibbs/barkeep/test/fixtures/text_git_repo  I haven't figured out what's
          # weird about it yet but this fails, so put in a hack for now.  May want to leave this since
          # we'd rather continue without the changes than fail, right?
          file_contents[file_changed] = ""
        end
      end

      # now calculate which commits are new
      if new_branch
        # new branch, but we don't want to include all commits from beginning of time
        # so exclude any commits that are on any other branches
        # e.g. git rev-list <commit for B3> ^master ^B2
        # NOTE: have to use commit, not ref, because if this is called in pre-receive the branch name of ref won't
        # actually have been set up yet!
        new_commits = Hook.shell!("git rev-list #{commit} #{exclude_branches}").split("\n")
      else
        # existing branch, base..commit is right
        new_commits = Hook.shell!("git rev-list #{base}..#{commit}").split("\n")
      end

      new_commits.each do |one_commit|
        self.commit_ref_map[one_commit] ||= [];
        self.commit_ref_map[one_commit] << ref  # name of the branch associated with this commit
      end
    end

    # we want the list of commits sorted by commit date
    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 # can't just use HEAD - remote may be on branch with no HEAD
        self.ls_files = Hook.shell!("git ls-tree --full-tree --name-only -r #{file_list_revision}").split("\n")
      # TODO should store ls_files per commit (with status)?
    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")
    # Split, cut off leading line to get actual files with status
    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]
  }
}

Class Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Class Attribute Details

.commit_messageObject

Commit message for current commit



50
51
52
# File 'lib/ruby_git_hooks.rb', line 50

def commit_message
  @commit_message
end

.commit_message_fileObject

Commit message file for current commit



53
54
55
# File 'lib/ruby_git_hooks.rb', line 53

def commit_message_file
  @commit_message_file
end

.commit_ref_mapObject

refs associated with each commit



61
62
63
# File 'lib/ruby_git_hooks.rb', line 61

def commit_ref_map
  @commit_ref_map
end

.commitsObject

the following are for hooks which involve multiple commits (pre-receive, post-receive): (may be empty in other hooks) All current commits



58
59
60
# File 'lib/ruby_git_hooks.rb', line 58

def commits
  @commits
end

.file_contentsObject

Latest contents of all changed files



41
42
43
# File 'lib/ruby_git_hooks.rb', line 41

def file_contents
  @file_contents
end

.file_diffsObject

A human-readable diff per file



44
45
46
# File 'lib/ruby_git_hooks.rb', line 44

def file_diffs
  @file_diffs
end

.files_changedObject

Array of what files were changed



38
39
40
# File 'lib/ruby_git_hooks.rb', line 38

def files_changed
  @files_changed
end

.has_runObject (readonly)

Whether .run has ever been called



35
36
37
# File 'lib/ruby_git_hooks.rb', line 35

def has_run
  @has_run
end

.ls_filesObject

All filenames in repo



47
48
49
# File 'lib/ruby_git_hooks.rb', line 47

def ls_files
  @ls_files
end

.registered_hooksObject (readonly)

What hooks are running



23
24
25
# File 'lib/ruby_git_hooks.rb', line 23

def registered_hooks
  @registered_hooks
end

.run_asObject (readonly)

What command line was run



26
27
28
# File 'lib/ruby_git_hooks.rb', line 26

def run_as
  @run_as
end

.run_as_hookObject (readonly)

What git hook is being run



32
33
34
# File 'lib/ruby_git_hooks.rb', line 32

def run_as_hook
  @run_as_hook
end

.run_fromObject (readonly)

What directory to run from



29
30
31
# File 'lib/ruby_git_hooks.rb', line 29

def run_from
  @run_from
end

Class Method Details

.get_hooks_to_run(hook_specs) ⇒ Object



247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
# File 'lib/ruby_git_hooks.rb', line 247

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)
      # A string is assumed to be a class name
      @registered_hooks[Object.const_get(spec)]
    else
      raise "Can't find hook for specification: #{spec.inspect}!"
    end
  end
end

.initial_setupObject



231
232
233
234
235
236
# File 'lib/ruby_git_hooks.rb', line 231

def self.initial_setup
  return if @run_from

  @run_from = Dir.getwd
  @run_as = $0
end

.register(hook) ⇒ Object



338
339
340
341
342
343
344
345
346
347
348
349
# File 'lib/ruby_git_hooks.rb', line 338

def self.register(hook)
  @registered_hooks ||= {}
  @registered_hooks[hook.class.name] ||= []
  @registered_hooks[hook.class.name].push hook

  # Figure out when to set this up...
  #at_exit do
  #  unless RubyGitHooks::Hook.has_run
  #    STDERR.puts "No call to RubyGitHooks.run happened, so no hooks ran!"
  #  end
  #end
end

.run(*hook_specs) ⇒ Object

Run takes a list of hook specifications. Those can be Hook classnames or instances of type Hook.

Parameters:

  • hook_specs

    Array[Hook or Class or String] A list of hooks or hook classes



273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
# File 'lib/ruby_git_hooks.rb', line 273

def self.run(*hook_specs)
  if @has_run
    STDERR.puts "In this version, you can't call .run more than once.  For now, please\nregister your hooks individually and then call .run with no args, or\nelse call .run with both as arguments.  This may be fixed in a future\nversion.  Sorry!\n"
    exit 1
  end
  @has_run = true

  initial_setup

  run_as_specific_githook

  # By default, run all hooks
  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 }  # Re-init each time, just in case
      failed_hooks.push(hook) unless val
    rescue
      # Failed.  Return non-zero if that makes a difference.
      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

.run_as_specific_githookObject



313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
# File 'lib/ruby_git_hooks.rb', line 313

def self.run_as_specific_githook
  return if @run_as_hook  # Already did this

  self.initial_setup  # Might have already done this

  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

.shell!(*args) ⇒ Object



351
352
353
354
355
356
357
358
359
360
361
# File 'lib/ruby_git_hooks.rb', line 351

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

Instance Method Details

#setupObject



238
239
240
241
242
243
244
245
# File 'lib/ruby_git_hooks.rb', line 238

def setup
  Dir.chdir Hook.run_from do
    yield
  end

ensure
  # Nothing yet
end