rabbitt-githooks 1.2.7

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.
Files changed (44) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +34 -0
  3. data/.rubocop.yml +73 -0
  4. data/Gemfile +21 -0
  5. data/Gemfile.lock +45 -0
  6. data/LICENSE.txt +339 -0
  7. data/README.md +4 -0
  8. data/Rakefile +19 -0
  9. data/bin/githooks +15 -0
  10. data/bin/githooks-runner +17 -0
  11. data/hooks/commit-messages.rb +29 -0
  12. data/hooks/formatting.rb +53 -0
  13. data/lib/githooks.rb +89 -0
  14. data/lib/githooks/action.rb +150 -0
  15. data/lib/githooks/cli.rb +93 -0
  16. data/lib/githooks/commands/config.rb +107 -0
  17. data/lib/githooks/core_ext.rb +23 -0
  18. data/lib/githooks/core_ext/array.rb +3 -0
  19. data/lib/githooks/core_ext/array/extract_options.rb +5 -0
  20. data/lib/githooks/core_ext/array/min_max.rb +37 -0
  21. data/lib/githooks/core_ext/array/select_with_index.rb +13 -0
  22. data/lib/githooks/core_ext/numbers.rb +1 -0
  23. data/lib/githooks/core_ext/numbers/infinity.rb +19 -0
  24. data/lib/githooks/core_ext/pathname.rb +27 -0
  25. data/lib/githooks/core_ext/process.rb +7 -0
  26. data/lib/githooks/core_ext/string.rb +3 -0
  27. data/lib/githooks/core_ext/string/git_option_path_split.rb +6 -0
  28. data/lib/githooks/core_ext/string/inflections.rb +67 -0
  29. data/lib/githooks/core_ext/string/strip_empty_lines.rb +9 -0
  30. data/lib/githooks/error.rb +10 -0
  31. data/lib/githooks/hook.rb +159 -0
  32. data/lib/githooks/repository.rb +152 -0
  33. data/lib/githooks/repository/config.rb +170 -0
  34. data/lib/githooks/repository/diff_index_entry.rb +80 -0
  35. data/lib/githooks/repository/file.rb +125 -0
  36. data/lib/githooks/repository/limiter.rb +55 -0
  37. data/lib/githooks/runner.rb +317 -0
  38. data/lib/githooks/section.rb +98 -0
  39. data/lib/githooks/system_utils.rb +109 -0
  40. data/lib/githooks/terminal_colors.rb +63 -0
  41. data/lib/githooks/version.rb +22 -0
  42. data/rabbitt-githooks.gemspec +49 -0
  43. data/thoughts.txt +56 -0
  44. metadata +175 -0
@@ -0,0 +1,55 @@
1
+ =begin
2
+ Copyright (C) 2013 Carl P. Corliss
3
+
4
+ This program is free software; you can redistribute it and/or modify
5
+ it under the terms of the GNU General Public License as published by
6
+ the Free Software Foundation; either version 2 of the License, or
7
+ (at your option) any later version.
8
+
9
+ This program is distributed in the hope that it will be useful,
10
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ GNU General Public License for more details.
13
+
14
+ You should have received a copy of the GNU General Public License along
15
+ with this program; if not, write to the Free Software Foundation, Inc.,
16
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17
+ =end
18
+
19
+ module GitHooks
20
+ class Repository::Limiter
21
+ attr_reader :type, :only
22
+
23
+ def initialize(type, options = {})
24
+ @type = type
25
+ @only = options.delete(:only) || options.delete(:to)
26
+ end
27
+
28
+ def only(*args)
29
+ return @only if args.empty?
30
+ @only = args.flatten
31
+ end
32
+ alias_method :to, :only
33
+
34
+ def limit(files)
35
+ files.select! do |file|
36
+ match_file(file, @only).tap do |result|
37
+ if GitHooks.debug?
38
+ result = (result ? 'success' : 'failure')
39
+ puts " #{file.path.to_s} (#{file.attribute_value(@type).inspect}) was a #{result}"
40
+ end
41
+ end
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ def match_file(file, match_value)
48
+ if match_value.is_a? Array
49
+ match_value.any? { |value| file.match(@type, value) }
50
+ else
51
+ file.match(@type, match_value)
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,317 @@
1
+ # encoding: utf-8
2
+ =begin
3
+ Copyright (C) 2013 Carl P. Corliss
4
+
5
+ This program is free software; you can redistribute it and/or modify
6
+ it under the terms of the GNU General Public License as published by
7
+ the Free Software Foundation; either version 2 of the License, or
8
+ (at your option) any later version.
9
+
10
+ This program is distributed in the hope that it will be useful,
11
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ GNU General Public License for more details.
14
+
15
+ You should have received a copy of the GNU General Public License along
16
+ with this program; if not, write to the Free Software Foundation, Inc.,
17
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18
+ =end
19
+
20
+ require 'fileutils'
21
+ require 'githooks/terminal_colors'
22
+ require 'shellwords'
23
+ require 'thor'
24
+
25
+ module GitHooks
26
+ module Runner
27
+ extend TerminalColors
28
+
29
+ MARK_SUCCESS = '✓'
30
+ MARK_FAILURE = 'X'
31
+ MARK_UNKNOWN = '?'
32
+
33
+ def run(options = {}) # rubocop:disable CyclomaticComplexity, MethodLength
34
+ # unfreeze options
35
+ options = Thor::CoreExt::HashWithIndifferentAccess.new(options)
36
+
37
+ repo = options['repo'] ||= Repository.root_path
38
+ script = options['script'] ||= Repository.instance(repo).config.script
39
+ libpath = options['path'] ||= Repository.instance(repo).config.path
40
+ args = options['args'] ||= []
41
+
42
+ GitHooks.verbose = !!ENV['GITHOOKS_VERBOSE']
43
+ GitHooks.debug = !!ENV['GITHOOKS_DEBUG']
44
+
45
+ if options['skip-pre']
46
+ puts 'Skipping PreRun Executables'
47
+ else
48
+ run_externals('pre-run-execute', repo, args)
49
+ end
50
+
51
+ if script && !(options['ignore-script'] || GitHooks.ignore_script)
52
+ command = "#{script} #{Pathname.new($0).to_s} #{Shellwords.join(ARGV)};"
53
+ puts "Kernel#exec(#{command.inspect})" if GitHooks.verbose
54
+ exec(command)
55
+ elsif libpath
56
+ load_tests(libpath, options['skip-bundler'])
57
+ start(options)
58
+ else
59
+ puts 'I can\'t figure out what to run - specify either path or script to give me a hint...'
60
+ end
61
+
62
+ if options['skip-post']
63
+ puts 'Skipping PostRun Executables'
64
+ else
65
+ run_externals('post-run-execute', repo, args)
66
+ end
67
+ rescue GitHooks::Error::NotAGitRepo
68
+ puts "Unable to find a valid git repo in #{repo}."
69
+ puts 'Please specify path to repo via --repo <path>' if GitHooks::SCRIPT_NAME == 'githooks'
70
+ end
71
+ module_function :run
72
+
73
+ def attach(options = {}) # rubocop:disable CyclomaticComplexity, MethodLength
74
+ repo_path = options[:repo] || Repository.root_path
75
+ repo_path = Pathname.new(repo_path) unless repo_path.nil?
76
+ repo_hooks = repo_path + '.git' + 'hooks'
77
+
78
+ entry_path = options[:script] || options[:path]
79
+
80
+ hook_phases = options[:hooks]
81
+ hook_phases ||= Hook::VALID_PHASES
82
+
83
+ bootstrapper = options[:bootstrap]
84
+ bootstrapper = Pathname.new(bootstrapper).realpath unless bootstrapper.nil?
85
+ entry_path = Pathname.new(entry_path).realdirpath
86
+
87
+ repo = Repository.instance(repo_path)
88
+
89
+ if entry_path.directory?
90
+ if path = repo.config['path'] # rubocop:disable AssignmentInCondition
91
+ fail Error::AlreadyAttached, "Repository [#{repo_path}] already attached to path #{path} - Detach to continue."
92
+ end
93
+ repo.config.set('path', entry_path)
94
+ elsif entry_path.executable?
95
+ if path = repo.config['script'] # rubocop:disable AssignmentInCondition
96
+ fail Error::AlreadyAttached, "Repository [#{repo_path}] already attached to script #{path}. Detach to continue."
97
+ end
98
+ repo.config.set('script', entry_path)
99
+ else
100
+ fail ArgumentError, "Provided path '#{entry_path}' is neither a directory nor an executable file."
101
+ end
102
+
103
+ gitrunner = bootstrapper
104
+ gitrunner ||= SystemUtils.which('githooks-runner')
105
+ gitrunner ||= (GitHooks::BIN_PATH + 'githooks-runner').realpath
106
+
107
+ hook_phases.each do |hook|
108
+ hook = (repo_hooks + hook).to_s
109
+ puts "Linking #{gitrunner.to_s} -> #{hook}" if GitHooks.verbose
110
+ FileUtils.ln_sf gitrunner.to_s, hook
111
+ end
112
+ end
113
+ module_function :attach
114
+
115
+ def detach(repo_path, hook_phases)
116
+ repo_path ||= Repository.root_path
117
+ repo_hooks = Pathname.new(repo_path) + '.git' + 'hooks'
118
+ hook_phases ||= Hook::VALID_PHASES
119
+
120
+ repo = Repository.instance(repo_path)
121
+
122
+ hook_phases.each do |hook|
123
+ if (repo_hook = repo_hooks + hook).symlink?
124
+ puts "Removing hook '#{hook}' from repository at: #{repo_path}" if GitHooks.verbose
125
+ FileUtils.rm_f repo_hook
126
+ end
127
+ end
128
+
129
+ active_hooks = Hook::VALID_PHASES.select { |hook| (repo_hooks + hook).exist? }
130
+
131
+ if active_hooks.empty?
132
+ puts 'All hooks detached. Removing configuration section.'
133
+ repo.config.remove_section(repo_path: repo_path)
134
+ else
135
+ puts "Keeping configuration for active hooks: #{active_hooks.join(', ')}"
136
+ end
137
+ end
138
+ module_function :detach
139
+
140
+ def list(repo_path)
141
+ repo_path ||= Pathname.new(Repository.root_path)
142
+
143
+ repo = Repository.instance(repo_path)
144
+ script = repo.config.script
145
+ libpath = repo.config.path
146
+
147
+ unless script || libpath
148
+ fail Error::NotAttached, 'Repository currently not configured. Usage attach to setup for use with githooks.'
149
+ end
150
+
151
+ if (executables = repo.config.pre_run_execute).size > 0
152
+ puts 'PreRun Executables (in execution order):'
153
+ executables.each do |exe|
154
+ puts " #{exe}"
155
+ end
156
+ puts
157
+ end
158
+
159
+ if script
160
+ puts 'Main Test Script:'
161
+ puts " #{script}"
162
+ puts
163
+ end
164
+
165
+ if libpath
166
+ puts 'Main Testing Library with Tests (in execution order):'
167
+ puts ' Tests loaded from:'
168
+ puts " #{libpath}"
169
+ puts
170
+
171
+ SystemUtils.quiet { load_tests(libpath, true) }
172
+
173
+ %w{ pre-commit commit-msg }.each do |phase|
174
+ next unless Hook.phases[phase]
175
+
176
+ puts " Phase #{phase.camelize}:"
177
+ Hook.phases[phase].sections.each_with_index do |section, section_index|
178
+ printf " %3d: %s\n", section_index + 1, section.title
179
+ section.all.each_with_index do |action, action_index|
180
+ printf " %3d: %s\n", action_index + 1, action.title
181
+ action.limiters.each_with_index do |limiter, limiter_index|
182
+ type, value = limiter.type.inspect, limiter.only
183
+ value = value.first if value.size == 1
184
+ printf " Limiter %d: %s -> %s\n", limiter_index + 1, type, value.inspect
185
+ end
186
+ end
187
+ end
188
+ end
189
+
190
+ puts
191
+ end
192
+
193
+ if (executables = repo.config.post_run_execute).size > 0
194
+ puts 'PostRun Executables (in execution order):'
195
+ executables.each do |exe|
196
+ puts " #{exe}"
197
+ end
198
+ puts
199
+ end
200
+ rescue GitHooks::Error::NotAGitRepo
201
+ puts "Unable to find a valid git repo in #{repo}."
202
+ puts 'Please specify path to repo via --repo <path>' if GitHooks::SCRIPT_NAME == 'githooks'
203
+ end
204
+ module_function :list
205
+
206
+ private
207
+
208
+ def run_externals(which, repo_path, args)
209
+ Repository.instance(repo_path).config[which].all? do |executable|
210
+ command = SystemUtils::Command.new(File.basename(executable), path: executable)
211
+
212
+ puts "#{which.camelize}: #{command.build_command(args)}" if GitHooks.verbose
213
+ unless (r = command.execute(*args)).status.success?
214
+ print "#{which.camelize} Executable [#{executable}] failed with error code #{r.status.exitstatus} and "
215
+ if r.error.empty?
216
+ puts 'no output'
217
+ else
218
+ puts "error message:\n\t#{r.error}"
219
+ end
220
+ end
221
+ r.status.success?
222
+ end || fail(TestsFailed, "Failed #{which.camelize} executables - giving up")
223
+ end
224
+ module_function :run_externals
225
+
226
+ def start(options = {}) # rubocop:disable MethodLength
227
+ phase = options[:hook] || GitHooks.hook_name || 'pre-commit'
228
+ puts "PHASE: #{phase}" if GitHooks.debug
229
+
230
+ if active_hook = Hook.phases[phase]
231
+ active_hook.args = options.delete(:args)
232
+ active_hook.unstaged = options.delete(:unstaged)
233
+ active_hook.untracked = options.delete(:untracked)
234
+ active_hook.repository_path = options.delete(:repo)
235
+ else
236
+ fail Error::InvalidPhase, "Hook '#{phase}' is not defined - have you registered any tests for this hook yet?"
237
+ end
238
+
239
+ success = active_hook.run
240
+ section_length = active_hook.sections.max { |s| s.title.length }
241
+ sections = active_hook.sections.select { |section| !section.actions.empty? }
242
+
243
+ sections.each do |section|
244
+ hash_tail_length = (section_length - section.title.length)
245
+ printf "===== %s %s=====\n", section.colored_name(phase), ('=' * hash_tail_length)
246
+
247
+ section.actions.each_with_index do |action, index|
248
+ printf " %d. [ %s ] %s\n", (index + 1), action.state_symbol, action.colored_title
249
+
250
+ action.errors.each do |error|
251
+ printf " %s %s\n", color_bright_red(MARK_FAILURE), error
252
+ end
253
+
254
+ state_string = ( action.success? ? color_bright_green(MARK_SUCCESS) : color_bright_yellow(MARK_UNKNOWN))
255
+ action.warnings.each do |warning|
256
+ printf " %s %s\n", state_string, warning
257
+ end
258
+ end
259
+ puts
260
+ end
261
+
262
+ success = false if ENV['GITHOOKS_FORCE_FAIL']
263
+
264
+ unless success
265
+ $stderr.puts 'Commit failed due to errors listed above.'
266
+ $stderr.puts 'Please fix and attempt your commit again.'
267
+ end
268
+
269
+ exit(success ? 0 : 1)
270
+ end
271
+ module_function :start
272
+
273
+ def load_tests(path, skip_bundler = false) # rubocop:disable MethodLength
274
+ hooks_root = Pathname.new(path).realpath
275
+ hooks_path = hooks_root + 'hooks'
276
+ hooks_libs = hooks_root + 'libs'
277
+ gemfile = hooks_root + 'Gemfile'
278
+
279
+ if gemfile.exist? && !skip_bundler
280
+ puts "loading Gemfile from: #{gemfile}" if GitHooks.verbose
281
+
282
+ begin
283
+ ENV['BUNDLE_GEMFILE'] = (hooks_root + 'Gemfile').to_s
284
+
285
+ # stupid RVM polluting my environment without asking via it's
286
+ # executable-hooks gem preloading bundler. hence the following ...
287
+ if defined? Bundler
288
+ [:@bundle_path, :@configured, :@definition, :@load].each do |var|
289
+ Bundler.instance_variable_set(var, nil)
290
+ end
291
+ # bundler tests for @settings using defined? - which means we need
292
+ # to forcibly remove it.
293
+ Bundler.send(:remove_instance_variable, :@settings)
294
+ else
295
+ require 'bundler'
296
+ end
297
+ Bundler.require(:default)
298
+ rescue LoadError
299
+ puts 'Unable to load bundler - please make sure it\'s installed.'
300
+ raise # rubocop:disable SignalException
301
+ rescue Bundler::GemNotFound => e
302
+ puts "Error: #{e.message}"
303
+ puts 'Did you bundle install your Gemfile?'
304
+ raise # rubocop:disable SignalException
305
+ end
306
+ end
307
+
308
+ $LOAD_PATH.unshift hooks_libs.to_s
309
+ Dir["#{hooks_path}/**/*.rb"].each do |lib|
310
+ lib.gsub!('.rb', '')
311
+ puts "Loading: #{lib}" if GitHooks.verbose
312
+ require lib
313
+ end
314
+ end
315
+ module_function :load_tests
316
+ end
317
+ end
@@ -0,0 +1,98 @@
1
+ # encoding: utf-8
2
+ =begin
3
+ Copyright (C) 2013 Carl P. Corliss
4
+
5
+ This program is free software; you can redistribute it and/or modify
6
+ it under the terms of the GNU General Public License as published by
7
+ the Free Software Foundation; either version 2 of the License, or
8
+ (at your option) any later version.
9
+
10
+ This program is distributed in the hope that it will be useful,
11
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ GNU General Public License for more details.
14
+
15
+ You should have received a copy of the GNU General Public License along
16
+ with this program; if not, write to the Free Software Foundation, Inc.,
17
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18
+ =end
19
+
20
+ require 'delegate'
21
+
22
+ module GitHooks
23
+ class Section < DelegateClass(Array)
24
+ include TerminalColors
25
+
26
+ attr_reader :name, :hook, :success, :actions
27
+ alias_method :title, :name
28
+ alias_method :success?, :success
29
+ alias_method :all, :actions
30
+
31
+ def initialize(name, hook, &block)
32
+ @name = name.to_s.titleize
33
+ @success = true
34
+ @actions = []
35
+ @hook = hook
36
+
37
+ instance_eval(&block)
38
+
39
+ waiting!
40
+ end
41
+
42
+ # overrides previous action method to only return
43
+ # actions that have a non-empty manifest
44
+ def actions
45
+ @actions.select { |action| !action.manifest.empty? }
46
+ end
47
+ alias_method :__getobj__, :actions
48
+
49
+ def <<(action)
50
+ @actions << action
51
+ end
52
+
53
+ %w(finished running waiting).each do |method|
54
+ define_method(:"#{method}?") { @status == method.to_sym }
55
+ define_method(:"#{method}!") { @status = method.to_sym }
56
+ end
57
+
58
+ def completed?
59
+ @actions.all? { |action| action.finished? }
60
+ end
61
+
62
+ def wait_count
63
+ @actions.select { |action| action.waiting? }.size
64
+ end
65
+
66
+ def name(phase = GitHooks::HOOK_NAME)
67
+ phase = (phase || GitHooks::HOOK_NAME).to_s.gsub('-', '_').camelize
68
+ "#{phase} :: #{@name}"
69
+ end
70
+
71
+ def colored_name(phase = GitHooks::HOOK_NAME)
72
+ status_colorize name(phase)
73
+ end
74
+
75
+ def action(title, options = {}, &block)
76
+ fail ArgumentError, 'Missing required block to #perform' unless block_given?
77
+ @actions << Action.new(title, self, &block)
78
+ self
79
+ end
80
+
81
+ def status_colorize(text)
82
+ if finished? && completed?
83
+ success? ? color_bright_green(text) : color_bright_red(text)
84
+ else
85
+ color_dark_cyan(text)
86
+ end
87
+ end
88
+
89
+ def run
90
+ running!
91
+ begin
92
+ actions.collect { |action| @success &= action.run }.all?
93
+ ensure
94
+ finished!
95
+ end
96
+ end
97
+ end
98
+ end