ruby_git_hooks 0.0.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,343 @@
1
+ # Copyright (C) 2013 OL2, Inc. See LICENSE.txt for details.
2
+
3
+ require "ruby_git_hooks/version"
4
+
5
+ # This module is the core of the ruby_git_hooks code. It includes the
6
+ # Git commands, the hook types and in general most of the interface.
7
+ # README.md is the best overall documentation for this package, but
8
+ # this is where you can dig into the lowest-level Git specifics.
9
+
10
+ module RubyGitHooks
11
+ # This isn't all hook names, just the ones we already support.
12
+ CAN_FAIL_HOOKS = [ "pre-commit", "pre-receive", "commit-msg" ]
13
+ NO_FAIL_HOOKS = [ "post-receive", "post-commit" ]
14
+ HOOK_NAMES = CAN_FAIL_HOOKS + NO_FAIL_HOOKS
15
+ # applypatch-msg, pre-applypatch, post-applypatch
16
+ # prepare-commit-msg, commit-msg
17
+ # pre-rebase, post-checkout, post-merge, update, post-update,
18
+ # pre-auto-gc, post-rewrite
19
+
20
+ class Hook
21
+ class << self
22
+ # What hooks are running
23
+ attr_reader :registered_hooks
24
+
25
+ # What command line was run
26
+ attr_reader :run_as
27
+
28
+ # What directory to run from
29
+ attr_reader :run_from
30
+
31
+ # What git hook is being run
32
+ attr_reader :run_as_hook
33
+
34
+ # Whether .run has ever been called
35
+ attr_reader :has_run
36
+
37
+ # Array of what files were changed
38
+ attr_accessor :files_changed
39
+
40
+ # Latest contents of all changed files
41
+ attr_accessor :file_contents
42
+
43
+ # A human-readable diff per file
44
+ attr_accessor :file_diffs
45
+
46
+ # All filenames in repo
47
+ attr_accessor :ls_files
48
+
49
+ # All current commits (sometimes empty)
50
+ attr_accessor :commits
51
+
52
+ # Commit message for current commit
53
+ attr_accessor :commit_message
54
+
55
+ # Commit message file for current commit
56
+ attr_accessor :commit_message_file
57
+ end
58
+
59
+ # Instances of Hook delegate these methods to the class methods.
60
+ HOOK_INFO = [ :files_changed, :file_contents, :file_diffs, :ls_files,
61
+ :commits, :commit_message, :commit_message_file ]
62
+ HOOK_INFO.each do |info_method|
63
+ define_method(info_method) do |*args, &block|
64
+ Hook.send(info_method, *args, &block)
65
+ end
66
+ end
67
+
68
+ HOOK_TYPE_SETUP = {
69
+
70
+ # Pre-receive gets no args, but STDIN with a list of changes.
71
+ "pre-receive" => proc {
72
+ changes = []
73
+ STDIN.each_line do |line|
74
+ base, commit, ref = line.strip.split
75
+ changes.push [base, commit, ref]
76
+ end
77
+ self.commits = []
78
+
79
+ self.files_changed = []
80
+ self.file_contents = {}
81
+ self.file_diffs = {}
82
+
83
+ changes.each do |base, commit, ref|
84
+ no_base = false
85
+ if base =~ /\A0+\z/
86
+ # if base is 000... (initial commit), then all files were added, and git diff won't work
87
+ no_base = true
88
+ files_with_status = Hook.shell!("git ls-tree --name-status -r #{commit}").split("\n")
89
+ # put the A at the front
90
+ files_with_status.map!{|filename| "A\t" + filename}
91
+ else
92
+ files_with_status = Hook.shell!("git diff --name-status #{base}..#{commit}").split("\n")
93
+ end
94
+
95
+ files_with_status.each do |f|
96
+ status, file_changed = f.scan(/([ACDMRTUXB])\s+(\S+)$/).flatten
97
+ self.files_changed << file_changed
98
+
99
+ file_diffs[file_changed] = Hook.shell!("git log -p #{commit} -- #{file_changed}")
100
+ begin
101
+ file_contents[file_changed] = status == "D" ? "" : Hook.shell!("git show #{commit}:#{file_changed}")
102
+ rescue
103
+ # weird bug where some repos can't run the git show command even when it's not a deleted file.
104
+ # example: noah-gibbs/barkeep/test/fixtures/text_git_repo I haven't figured out what's
105
+ # weird about it yet but this fails, so put in a hack for now. May want to leave this since
106
+ # we'd rather continue without the changes than fail, right?
107
+ file_contents[file_changed] = ""
108
+ end
109
+ end
110
+ commit_range = no_base ? commit : "#{base}..#{commit}"
111
+ new_commits = Hook.shell!("git log --pretty=format:%H #{commit_range}").split("\n")
112
+ self.commits = self.commits | new_commits
113
+ end
114
+
115
+ if !self.commits.empty?
116
+ file_list_revision = self.commits.first # can't just use HEAD - remote may be on branch with no HEAD
117
+ self.ls_files = Hook.shell!("git ls-tree --full-tree --name-only -r #{file_list_revision}").split("\n")
118
+ # TODO should store ls_files per commit and ls_files with branch name (in case commits on multiple branches)?
119
+ end
120
+
121
+
122
+
123
+ },
124
+
125
+ "pre-commit" => proc {
126
+ files_with_status = Hook.shell!("git diff --name-status --cached").split("\n")
127
+
128
+ self.files_changed = []
129
+ self.file_contents = {}
130
+ self.file_diffs = {}
131
+ self.commits = []
132
+
133
+ files_with_status.each do |f|
134
+ status, file_changed = f.scan(/([ACDMRTUXB])\s+(\S+)$/).flatten
135
+ self.files_changed << file_changed
136
+
137
+ file_diffs[file_changed] = Hook.shell!("git diff --cached -- #{file_changed}")
138
+ file_contents[file_changed] = status == "D"? "": Hook.shell!("git show :#{file_changed}")
139
+ end
140
+
141
+ self.ls_files = Hook.shell!("git ls-files").split("\n")
142
+ },
143
+
144
+ "post-commit" => proc {
145
+ last_commit_files = Hook.shell!("git log --oneline --name-status -1")
146
+ # Split, cut off leading line to get actual files with status
147
+ files_with_status = last_commit_files.split("\n")[1..-1]
148
+
149
+ self.files_changed = []
150
+ self.commits = [ Hook.shell!("git log -n 1 --pretty=format:%H").chomp ]
151
+ self.file_contents = {}
152
+ self.file_diffs = {}
153
+
154
+ files_with_status.each do |f|
155
+ status, file_changed = f.scan(/([ACDMRTUXB])\s+(\S+)$/).flatten
156
+ self.files_changed << file_changed
157
+
158
+ file_diffs[file_changed] = Hook.shell!("git log --oneline -p -1 -- #{file_changed}")
159
+ file_contents[file_changed] = status == "D"? "": Hook.shell!("git show :#{file_changed}")
160
+ end
161
+
162
+ self.ls_files = Hook.shell!("git ls-files").split("\n")
163
+ self.commit_message = Hook.shell!("git log -1 --pretty=%B")
164
+ },
165
+
166
+ "commit-msg" => proc {
167
+ files_with_status = Hook.shell!("git diff --name-status --cached").split("\n")
168
+
169
+ self.files_changed = []
170
+ self.file_contents = {}
171
+ self.file_diffs = {}
172
+ self.commits = []
173
+
174
+ files_with_status.each do |f|
175
+ status, file_changed = f.scan(/([ACDMRTUXB])\s+(\S+)$/).flatten
176
+ self.files_changed << file_changed
177
+
178
+ file_diffs[file_changed] = Hook.shell!("git diff --cached -- #{file_changed}")
179
+ file_contents[file_changed] = status == "D"? "": Hook.shell!("git show :#{file_changed}")
180
+ end
181
+
182
+ self.ls_files = Hook.shell!("git ls-files").split("\n")
183
+ self.commit_message = File.read(ARGV[0])
184
+ self.commit_message_file = ARGV[0]
185
+ }
186
+ }
187
+ HOOK_TYPE_SETUP["post-receive"] = HOOK_TYPE_SETUP["pre-receive"]
188
+
189
+ def self.initial_setup
190
+ return if @run_from
191
+
192
+ @run_from = Dir.getwd
193
+ @run_as = $0
194
+ end
195
+
196
+ def setup
197
+ Dir.chdir Hook.run_from do
198
+ yield
199
+ end
200
+
201
+ ensure
202
+ # Nothing yet
203
+ end
204
+
205
+ def self.get_hooks_to_run(hook_specs)
206
+ @registered_hooks ||= {}
207
+
208
+ if hook_specs.empty?
209
+ return @registered_hooks.values.inject([], &:+)
210
+ end
211
+
212
+ hook_specs.flat_map do |spec|
213
+ if @registered_hooks[spec]
214
+ @registered_hooks[spec]
215
+ elsif spec.is_a?(Hook)
216
+ [ spec ]
217
+ elsif spec.is_a?(String)
218
+ # A string is assumed to be a class name
219
+ @registered_hooks[Object.const_get(spec)]
220
+ else
221
+ raise "Can't find hook for specification: #{spec.inspect}!"
222
+ end
223
+ end
224
+ end
225
+
226
+ # Run takes a list of hook specifications.
227
+ # Those can be Hook classnames or instances of type
228
+ # Hook.
229
+ #
230
+ # @param hook_specs Array[Hook or Class or String] A list of hooks or hook classes
231
+ def self.run(*hook_specs)
232
+ if @has_run
233
+ STDERR.puts <<ERR
234
+ In this version, you can't call .run more than once. For now, please
235
+ register your hooks individually and then call .run with no args, or
236
+ else call .run with both as arguments. This may be fixed in a future
237
+ version. Sorry!
238
+ ERR
239
+ exit 1
240
+ end
241
+ @has_run = true
242
+
243
+ initial_setup
244
+
245
+ run_as_specific_githook
246
+
247
+ # By default, run all hooks
248
+ hooks_to_run = get_hooks_to_run(hook_specs.flatten)
249
+
250
+ failed_hooks = []
251
+ val = nil
252
+ hooks_to_run.each do |hook|
253
+ begin
254
+ hook.setup { val = hook.check } # Re-init each time, just in case
255
+ failed_hooks.push(hook) unless val
256
+ rescue
257
+ # Failed. Return non-zero if that makes a difference.
258
+ STDERR.puts "Hook #{hook.inspect} raised exception: #{$!.inspect}!\n#{$!.backtrace.join("\n")}"
259
+ failed_hooks.push hook
260
+ end
261
+ end
262
+
263
+ if CAN_FAIL_HOOKS.include?(@run_as_hook) && failed_hooks.size > 0
264
+ STDERR.puts "Hooks failed: #{failed_hooks}"
265
+ STDERR.puts "Use 'git commit -eF .git/COMMIT_EDITMSG' to restore your commit message" if commit_message
266
+ STDERR.puts "Exiting!"
267
+ exit 1
268
+ end
269
+ end
270
+
271
+ def self.run_as_specific_githook
272
+ return if @run_as_hook # Already did this
273
+
274
+ self.initial_setup # Might have already done this
275
+
276
+ if ARGV.include? "--hook"
277
+ idx = ARGV.find_index "--hook"
278
+ @run_as_hook = ARGV[idx + 1]
279
+ 2.times { ARGV.delete_at(idx) }
280
+ else
281
+ @run_as_hook = HOOK_NAMES.detect { |hook| @run_as.include?(hook) }
282
+ end
283
+
284
+ unless @run_as_hook
285
+ STDERR.puts "Name #{@run_as.inspect} doesn't include " +
286
+ "any of: #{HOOK_NAMES.inspect}"
287
+ exit 1
288
+ end
289
+
290
+ unless HOOK_TYPE_SETUP[@run_as_hook]
291
+ STDERR.puts "No setup defined for hook type #{@run_as_hook.inspect}!"
292
+ exit 1
293
+ end
294
+ self.instance_eval(&HOOK_TYPE_SETUP[@run_as_hook])
295
+ end
296
+
297
+ def self.register(hook)
298
+ @registered_hooks ||= {}
299
+ @registered_hooks[hook.class.name] ||= []
300
+ @registered_hooks[hook.class.name].push hook
301
+
302
+ # Figure out when to set this up...
303
+ #at_exit do
304
+ # unless RubyGitHooks::Hook.has_run
305
+ # STDERR.puts "No call to RubyGitHooks.run happened, so no hooks ran!"
306
+ # end
307
+ #end
308
+ end
309
+
310
+ def self.shell!(*args)
311
+ output = `#{args.join(" ")}`
312
+
313
+ unless $?.success?
314
+ STDERR.puts "Job #{args.inspect} failed in dir #{Dir.getwd.inspect}"
315
+ STDERR.puts "Failed job output:\n#{output}\n======"
316
+ raise "Exec of #{args.inspect} failed: #{$?}!"
317
+ end
318
+
319
+ output
320
+ end
321
+ end
322
+
323
+ # Forward these calls from RubyGitHooks to RubyGitHooks::Hook
324
+ class << self
325
+ [ :run, :register, :run_as ].each do |method|
326
+ define_method(method) do |*args, &block|
327
+ RubyGitHooks::Hook.send(method, *args, &block)
328
+ end
329
+ end
330
+ end
331
+
332
+ def self.shebang
333
+ ENV['RUBYGITHOOKS_SHEBANG']
334
+ end
335
+
336
+ def self.current_hook
337
+ RubyGitHooks::Hook.run_as_specific_githook
338
+ RubyGitHooks::Hook.run_as_hook
339
+ end
340
+ end
341
+
342
+ # Default to /usr/bin/env ruby for shebang line
343
+ ENV['RUBYGITHOOKS_SHEBANG'] ||= "#!/usr/bin/env ruby"
@@ -0,0 +1,37 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'ruby_git_hooks/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "ruby_git_hooks"
8
+ spec.version = RubyGitHooks::VERSION
9
+ spec.authors = ["Noah Gibbs", "Ruth Helfinstein", "Alex Snyatkov"]
10
+ spec.email = ["noah@onlive.com", "ruth.helfinstein@onlive.com",
11
+ "alex.snyatkov@onlive.com"]
12
+ spec.description = <<DESC
13
+ Ruby_git_hooks is a library to allow easy writing and installing of
14
+ git hooks in Ruby. It abstracts away the differences between
15
+ different hook interfaces and supplies implementations of some common
16
+ Git hooks. It allows overriding "git clone" to automatically
17
+ install your prefered hooks.
18
+ DESC
19
+ spec.summary = %q{DSL and manager for git hooks in Ruby.}
20
+ spec.homepage = ""
21
+ spec.license = "MIT"
22
+
23
+ spec.files = `git ls-files`.split($/)
24
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
25
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
26
+ spec.require_paths = ["lib"]
27
+ spec.bindir = "bin"
28
+
29
+ spec.add_runtime_dependency "pony" # For email
30
+ spec.add_runtime_dependency "rest-client"
31
+ spec.add_runtime_dependency "json"
32
+
33
+ spec.add_development_dependency "bundler", "~> 1.3"
34
+ spec.add_development_dependency "minitest"
35
+ spec.add_development_dependency "rr"
36
+ spec.add_development_dependency "rake"
37
+ end
@@ -0,0 +1,143 @@
1
+ # Copyright (C) 2013 OL2, Inc. See LICENSE.txt for details.
2
+
3
+ require "test_helper"
4
+
5
+ require "minitest/autorun"
6
+
7
+ class BasicHookTest < HookTestCase
8
+ REPOS_DIR = File.expand_path File.join(File.dirname(__FILE__), "repos")
9
+ TEST_PATH = File.join(REPOS_DIR, "hook_test_file")
10
+ TEST_HOOK_BODY = <<HOOK
11
+ #{RubyGitHooks.shebang}
12
+ require "ruby_git_hooks"
13
+
14
+ class TestHook < RubyGitHooks::Hook
15
+ def check
16
+ File.open("#{TEST_PATH}", "w") do |f|
17
+ f.puts files_changed.inspect, file_contents.inspect
18
+ end
19
+
20
+ puts "Test hook runs!"
21
+ true
22
+ end
23
+ end
24
+
25
+ RubyGitHooks.run TestHook.new
26
+ HOOK
27
+
28
+ TEST_HOOK_COMMIT_MSG = <<HOOK
29
+ #{RubyGitHooks.shebang}
30
+ require "ruby_git_hooks"
31
+
32
+ class TestHook < RubyGitHooks::Hook
33
+ def check
34
+ File.open("#{TEST_PATH}", "w") do |f|
35
+ f.puts commit_message
36
+ end
37
+
38
+ puts "Test hook runs!"
39
+ true
40
+ end
41
+ end
42
+
43
+ RubyGitHooks.run TestHook.new
44
+ HOOK
45
+
46
+ def setup(do_first_commit = true)
47
+ # Empty out the test repos dir
48
+ Hook.shell! "rm -rf #{File.join(REPOS_DIR, "*")}"
49
+
50
+ # Create local parent and child repos with a single shared commit
51
+ Dir.chdir REPOS_DIR
52
+
53
+ new_bare_repo
54
+ clone_repo
55
+ if do_first_commit
56
+ new_single_file_commit
57
+ git_push
58
+ end
59
+ end
60
+
61
+ def test_simple_pre_commit
62
+ add_hook("child_repo", "pre-commit", TEST_HOOK_BODY)
63
+
64
+ new_single_file_commit "child_repo"
65
+
66
+ assert File.exist?(TEST_PATH), "Test pre-commit hook didn't run!"
67
+ assert File.read(TEST_PATH).include?("Single-file commit"),
68
+ "No file contents reached pre-commit hook!"
69
+ end
70
+
71
+ def test_pre_commit_with_delete
72
+ add_hook("child_repo", "pre-commit", TEST_HOOK_BODY)
73
+ new_commit "child_repo", "file_to_delete"
74
+ git_delete "child_repo", "file_to_delete"
75
+ git_commit "child_repo", "Deleted file_to_delete"
76
+
77
+ assert File.exist?(TEST_PATH), "Test pre-commit hook didn't run!"
78
+ assert File.read(TEST_PATH).include?('"file_to_delete"=>""'),
79
+ "File not deleted properly"
80
+ end
81
+
82
+ def test_pre_commit_with_rename
83
+ add_hook("child_repo", "pre-commit", TEST_HOOK_BODY)
84
+ new_commit "child_repo", "file_to_rename"
85
+ git_rename "child_repo", "file_to_rename", "renamed_file"
86
+ new_commit "child_repo", "renamed_file", nil, "Renamed file"
87
+
88
+ assert File.exist?(TEST_PATH), "Test pre-commit hook didn't run!"
89
+ assert File.read(TEST_PATH).include?('"file_to_rename"=>""'),
90
+ "File not deleted properly"
91
+ end
92
+
93
+ def test_simple_pre_receive
94
+ add_hook("parent_repo.git", "pre-receive", TEST_HOOK_BODY)
95
+
96
+ new_single_file_commit "child_repo"
97
+ git_push("child_repo")
98
+
99
+ assert File.exist?(TEST_PATH), "Test pre-receive hook didn't run!"
100
+ assert File.read(TEST_PATH).include?("Single-file commit"),
101
+ "No file contents reached pre-receive hook!"
102
+ end
103
+
104
+
105
+ def test_pre_receive_with_delete
106
+ add_hook("parent_repo.git", "pre-receive", TEST_HOOK_BODY)
107
+
108
+ new_commit "child_repo", "file_to_delete"
109
+ git_push "child_repo"
110
+
111
+
112
+ git_delete "child_repo", "file_to_delete"
113
+ git_commit "child_repo", "Deleted file_to_delete"
114
+ git_push "child_repo"
115
+
116
+
117
+ assert File.exist?(TEST_PATH), "Test pre-receive hook didn't run!"
118
+ assert File.read(TEST_PATH).include?('"file_to_delete"=>""'),
119
+ "File deletion did not reach pre-receive hook!"
120
+ end
121
+
122
+ def test_commit_msg
123
+ add_hook("child_repo", "commit-msg", TEST_HOOK_COMMIT_MSG)
124
+ new_commit "child_repo", "my_file", "Commit contents", "This is my commit message"
125
+ assert File.exist?(TEST_PATH), "Test commit-msg hook didn't run!"
126
+ assert File.read(TEST_PATH).include?("This is my commit message"),
127
+ "Commit message did not reach commit-msg hook"
128
+ end
129
+
130
+ def test_post_commit_has_commit_msg
131
+ add_hook("child_repo", "post-commit", TEST_HOOK_COMMIT_MSG)
132
+ new_commit "child_repo", "my_file", "Commit contents", "This is my commit message"
133
+ assert File.exist?(TEST_PATH), "Test post-commit hook didn't run!"
134
+ assert File.read(TEST_PATH).include?("This is my commit message"),
135
+ "Commit message did not reach post-commit hook"
136
+ end
137
+
138
+ def test_first_pre_receive
139
+ setup(false) # don't do first commit
140
+ test_simple_pre_receive
141
+ end
142
+
143
+ end
@@ -0,0 +1,58 @@
1
+ # Copyright (C) 2013 OL2, Inc. See LICENSE.txt for details.
2
+
3
+ require "test_helper"
4
+
5
+ require "minitest/autorun"
6
+
7
+ class CaseClashHookTest < HookTestCase
8
+ REPOS_DIR = File.expand_path File.join(File.dirname(__FILE__), "repos")
9
+ TEST_HOOK_BODY = <<TEST
10
+ #{RubyGitHooks.shebang}
11
+ require "ruby_git_hooks/case_clash"
12
+
13
+ RubyGitHooks.run CaseClashHook.new
14
+ TEST
15
+
16
+ def setup
17
+ # Empty out the test repos dir
18
+ Hook.shell! "rm -rf #{File.join(REPOS_DIR, "*")}"
19
+
20
+ # Create local parent and child repos with a single shared commit
21
+ Dir.chdir REPOS_DIR
22
+
23
+ new_bare_repo
24
+ clone_repo
25
+ new_commit "child_repo", "README"
26
+ git_push
27
+ end
28
+
29
+ def test_case_clash_pre_commit
30
+ add_hook("child_repo", "pre-commit", TEST_HOOK_BODY)
31
+
32
+ new_commit "child_repo", "CaseClashFile1"
33
+ case1_sha = last_commit_sha
34
+ rewind_one_commit
35
+
36
+ new_commit "child_repo", "CASECLASHFILE1"
37
+ case2_sha = last_commit_sha
38
+
39
+ # Cherry-pick new content into place -- this means both files.
40
+ Hook.shell!("cd child_repo && git cherry-pick #{case1_sha}")
41
+ case_both = last_commit_sha
42
+
43
+ rewind_one_commit
44
+
45
+ # Soft-reset so content is still present
46
+ Hook.shell!("cd child_repo && git reset #{case_both}")
47
+
48
+ # Should reject w/ pre-commit hook
49
+ assert_raises RuntimeError do
50
+ Hook.shell!("cd child_repo && git commit -m \"Message\"")
51
+ end
52
+ end
53
+
54
+ #def test_case_clash_pre_receive
55
+ # add_hook("parent_repo.git", "pre-receive", TEST_HOOK_BODY)
56
+ #end
57
+
58
+ end