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, :branches_changed ]
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.branches_changed = {}  # {ref => [base, commit], ref2 => [base, commit]}

    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]}

    # figure out which commits have already been processed (everything we have seen before)
    exclude_refs = []  # we know we have seen under these refs already
        # includes all branches not referenced in this push
        # and all commits before the base of referenced branches
    all_branches = Hook.shell!("git for-each-ref --format='%(refname)' refs/heads/").split
    changes.each do |base, _ , ref|
      # ref is of the form refs/heads/branch_name
      all_branches.delete(ref)  # we don't want to use the new ref for this branch
      exclude_refs << "^#{base}" unless base =~  /\A0+\z/ # add the old ref for this branch to the exclude list
            # (don't add if it's 0, this is a new branch with no old ref)
    end

    # add the branches which aren't included in this push if any
    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]

      # TODO : calculate file_diffs and file_contents PER COMMIT for pre and post receive hooks
      # for now it just does the overall diffs
      if base =~  /\A0+\z/
        # 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
      
      # calculate which commits are new - exclude any commits that are on any other branches
      # e.g. git rev-list <commit for B3> ^old_B3 ^master ^B2 --
      # (the "--" at the end tells git these are refs NOT file references)
      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  # 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

.branches_changedObject

branches included in this push



64
65
66
# File 'lib/ruby_git_hooks.rb', line 64

def branches_changed
  @branches_changed
end

.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



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

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



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

def self.initial_setup
  return if @run_from

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

.register(hook) ⇒ Object



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

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



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
312
# File 'lib/ruby_git_hooks.rb', line 274

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

  # 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



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

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



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

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



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

def setup
  Dir.chdir Hook.run_from do
    yield
  end

ensure
  # Nothing yet
end