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 ]
HOOK_TYPE_SETUP =
{

  # Pre-receive gets no args, but STDIN with a list of changes.
  "pre-receive" => proc {
    changes = []
    STDIN.each_line do |line|
      base, commit, ref = line.strip.split
      changes.push [base, commit, ref]
    end
    self.commits = []

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

    changes.each do |base, commit, ref|
      no_base = false
      if base =~  /\A0+\z/
        # if base is 000... (initial commit), then all files were added, and git diff won't work
        no_base = true
        files_with_status = Hook.shell!("git ls-tree --name-status -r #{commit}").split("\n")
        # put the A at the front
        files_with_status.map!{|filename| "A\t" + filename}
      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
      commit_range  = no_base ? commit : "#{base}..#{commit}"
      new_commits = Hook.shell!("git log --pretty=format:%H #{commit_range}").split("\n")
      self.commits = self.commits | new_commits
    end

    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 and ls_files with branch name (in case commits on multiple branches)?
    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



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

def commit_message
  @commit_message
end

.commit_message_fileObject

Commit message file for current commit



56
57
58
# File 'lib/ruby_git_hooks.rb', line 56

def commit_message_file
  @commit_message_file
end

.commitsObject

All current commits (sometimes empty)



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

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



205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
# File 'lib/ruby_git_hooks.rb', line 205

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



189
190
191
192
193
194
# File 'lib/ruby_git_hooks.rb', line 189

def self.initial_setup
  return if @run_from

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

.register(hook) ⇒ Object



297
298
299
300
301
302
303
304
305
306
307
308
# File 'lib/ruby_git_hooks.rb', line 297

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



231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
# File 'lib/ruby_git_hooks.rb', line 231

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



271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
# File 'lib/ruby_git_hooks.rb', line 271

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



310
311
312
313
314
315
316
317
318
319
320
# File 'lib/ruby_git_hooks.rb', line 310

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



196
197
198
199
200
201
202
203
# File 'lib/ruby_git_hooks.rb', line 196

def setup
  Dir.chdir Hook.run_from do
    yield
  end

ensure
  # Nothing yet
end