overcommit 0.31.0 → 0.32.0.rc1

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 8f165cfdcb2a7a4293e715b273489473ed22d4a3
4
- data.tar.gz: 3aac84eccfbc7f7096cb7a7216118eac072996b7
3
+ metadata.gz: b45a46c1290a6ddd0fd418f28012e7138f01470e
4
+ data.tar.gz: 1443470dae06fb17b84fe99823d8e2c655bdb792
5
5
  SHA512:
6
- metadata.gz: 39205b47a623e3ed2377af1305290c138e35c1926f12b98c2be2e18533ad808b138c666f40b4ab59393b0ca5ef16ecca971279d65f3b51b8a56209ba791e2669
7
- data.tar.gz: e129c516f6953be502dc60737c263487a38ab1ecc0a8866a28e8edf980026c45aec3c340675e8508f9ce59981f72893af946b13491882990e1746b325e3ab898
6
+ metadata.gz: 133dc22840e178a2473670780a385fb0b4c46a48d175c5ab7ad391eccfb0fc1517a1f4318f8ad91e1ef53ca40cd2925d3e3912fb203b91e8aeeb8a9285892df1
7
+ data.tar.gz: 0847b5f3045de5b413be8704a76fbd6fdef908a34e11b64414a58628eadc3f961d77efa1e2257ef0a7b77b6e861b2496a1ec97f5752a02cbe1f9a17fe1cd1adb
data/config/default.yml CHANGED
@@ -39,6 +39,13 @@ gemfile: false
39
39
  # to the root of the repository.
40
40
  plugin_directory: '.git-hooks'
41
41
 
42
+ # Number of hooks that can be run concurrently. Typically this won't need to be
43
+ # adjusted, but if you know that some of your hooks themselves use multiple
44
+ # processors you can lower this value accordingly. You can define
45
+ # single-operator mathematical expressions, e.g. '%{processors} * 2', or
46
+ # '%{processors} / 2'.
47
+ concurrency: '%{processors}'
48
+
42
49
  # Whether to check if a hook plugin has changed since Overcommit last ran it.
43
50
  # This is a defense mechanism when working with repositories which can contain
44
51
  # untrusted code (e.g. when you fetch a pull request from a third party).
@@ -13,7 +13,10 @@ module Overcommit
13
13
  def initialize(hash, options = {})
14
14
  @options = options.dup
15
15
  @options[:logger] ||= Overcommit::Logger.silent
16
- @hash = Overcommit::ConfigurationValidator.new.validate(hash, options)
16
+ @hash = hash # Assign so validator can read original values
17
+ unless options[:validate] == false
18
+ @hash = Overcommit::ConfigurationValidator.new.validate(self, hash, options)
19
+ end
17
20
  end
18
21
 
19
22
  def ==(other)
@@ -34,6 +37,26 @@ module Overcommit
34
37
  File.join(Overcommit::Utils.repo_root, @hash['plugin_directory'] || '.git-hooks')
35
38
  end
36
39
 
40
+ def concurrency
41
+ @concurrency ||=
42
+ begin
43
+ cores = Overcommit::Utils.processor_count
44
+ content = @hash.fetch('concurrency', '%{processors}')
45
+ if content.is_a?(String)
46
+ concurrency_expr = content % { processors: cores }
47
+
48
+ a, op, b = concurrency_expr.scan(%r{(\d+)\s*([+\-*\/])\s*(\d+)})[0]
49
+ if a
50
+ a.to_i.send(op, b.to_i)
51
+ else
52
+ concurrency_expr.to_i
53
+ end
54
+ else
55
+ content.to_i
56
+ end
57
+ end
58
+ end
59
+
37
60
  # Returns configuration for all hooks in each hook type.
38
61
  #
39
62
  # @return [Hash]
@@ -3,12 +3,13 @@ module Overcommit
3
3
  class ConfigurationValidator
4
4
  # Validates hash for any invalid options, normalizing where possible.
5
5
  #
6
+ # @param config [Overcommit::Configuration]
6
7
  # @param hash [Hash] hash representation of YAML config
7
8
  # @param options[Hash]
8
9
  # @option default [Boolean] whether hash represents the default built-in config
9
10
  # @option logger [Overcommit::Logger] logger to output warnings to
10
11
  # @return [Hash] validated hash (potentially modified)
11
- def validate(hash, options)
12
+ def validate(config, hash, options)
12
13
  @options = options.dup
13
14
  @log = options[:logger]
14
15
 
@@ -16,6 +17,7 @@ module Overcommit
16
17
  ensure_hook_type_sections_exist(hash)
17
18
  check_hook_name_format(hash)
18
19
  check_for_missing_enabled_option(hash) unless @options[:default]
20
+ check_for_too_many_processors(config, hash)
19
21
  check_for_verify_plugin_signatures_option(hash)
20
22
 
21
23
  hash
@@ -96,6 +98,31 @@ module Overcommit
96
98
  @log.newline if any_warnings
97
99
  end
98
100
 
101
+ # Prints a warning if any hook has a number of processors larger than the
102
+ # global `concurrency` setting.
103
+ def check_for_too_many_processors(config, hash)
104
+ concurrency = config.concurrency
105
+
106
+ errors = []
107
+ Overcommit::Utils.supported_hook_type_classes.each do |hook_type|
108
+ hash.fetch(hook_type, {}).each do |hook_name, hook_config|
109
+ processors = hook_config.fetch('processors', 1)
110
+ if processors > concurrency
111
+ errors << "#{hook_type}::#{hook_name} `processors` value " \
112
+ "(#{processors}) is larger than the global `concurrency` " \
113
+ "option (#{concurrency})"
114
+ end
115
+ end
116
+ end
117
+
118
+ if errors.any?
119
+ @log.error errors.join("\n") if @log
120
+ @log.newline if @log
121
+ raise Overcommit::Exceptions::ConfigurationError,
122
+ 'One or more hooks had invalid `processor` value configured'
123
+ end
124
+ end
125
+
99
126
  # Prints a warning if the `verify_plugin_signatures` option is used instead
100
127
  # of the new `verify_signatures` option.
101
128
  def check_for_verify_plugin_signatures_option(hash)
@@ -61,6 +61,14 @@ module Overcommit::Hook
61
61
  @config['required']
62
62
  end
63
63
 
64
+ def parallelize?
65
+ @config['parallelize'] != false
66
+ end
67
+
68
+ def processors
69
+ @config.fetch('processors', 1)
70
+ end
71
+
64
72
  def quiet?
65
73
  @config['quiet']
66
74
  end
@@ -1,7 +1,7 @@
1
1
  module Overcommit
2
2
  # Responsible for loading the hooks the repository has configured and running
3
3
  # them, collecting and displaying the results.
4
- class HookRunner
4
+ class HookRunner # rubocop:disable Metrics/ClassLength
5
5
  # @param config [Overcommit::Configuration]
6
6
  # @param logger [Overcommit::Logger]
7
7
  # @param context [Overcommit::HookContext]
@@ -12,6 +12,10 @@ module Overcommit
12
12
  @context = context
13
13
  @printer = printer
14
14
  @hooks = []
15
+
16
+ @lock = Mutex.new
17
+ @resource = ConditionVariable.new
18
+ @slots_available = @config.concurrency
15
19
  end
16
20
 
17
21
  # Loads and runs the hooks registered for this {HookRunner}.
@@ -51,78 +55,120 @@ module Overcommit
51
55
  if @hooks.any?(&:enabled?)
52
56
  @printer.start_run
53
57
 
54
- interrupted = false
55
- run_failed = false
56
- run_warned = false
57
-
58
- @hooks.each do |hook|
59
- hook_status = run_hook(hook)
58
+ # Sort so hooks requiring fewer processors get queued first. This
59
+ # ensures we make better use of our available processors
60
+ @hooks_left = @hooks.sort_by { |hook| processors_for_hook(hook) }
61
+ @threads = Array.new(@config.concurrency) { Thread.new(&method(:consume)) }
60
62
 
61
- run_failed = true if hook_status == :fail
62
- run_warned = true if hook_status == :warn
63
-
64
- if hook_status == :interrupt
65
- # Stop running any more hooks and assume a bad result
66
- interrupted = true
67
- break
63
+ begin
64
+ InterruptHandler.disable_until_finished_or_interrupted do
65
+ @threads.each(&:join)
68
66
  end
67
+ rescue Interrupt
68
+ @printer.interrupt_triggered
69
+ # We received an interrupt on the main thread, so alert the
70
+ # remaining workers that an exception occurred
71
+ @interrupted = true
72
+ @threads.each { |thread| thread.raise Interrupt }
69
73
  end
70
74
 
71
- print_results(run_failed, run_warned, interrupted)
75
+ print_results
72
76
 
73
- !(run_failed || interrupted)
77
+ !(@failed || @interrupted)
74
78
  else
75
79
  @printer.nothing_to_run
76
80
  true # Run was successful
77
81
  end
78
82
  end
79
83
 
80
- # @param failed [Boolean]
81
- # @param warned [Boolean]
82
- # @param interrupted [Boolean]
83
- def print_results(failed, warned, interrupted)
84
- if interrupted
84
+ def consume
85
+ loop do
86
+ hook = @lock.synchronize { @hooks_left.pop }
87
+ break unless hook
88
+ run_hook(hook)
89
+ end
90
+ end
91
+
92
+ def wait_for_slot(hook)
93
+ @lock.synchronize do
94
+ slots_needed = processors_for_hook(hook)
95
+
96
+ loop do
97
+ if @slots_available >= slots_needed
98
+ @slots_available -= slots_needed
99
+
100
+ # Give another thread a chance since there are still slots available
101
+ @resource.signal if @slots_available > 0
102
+ break
103
+ elsif @slots_available > 0
104
+ # It's possible that another hook that requires fewer slots can be
105
+ # served, so give another a chance
106
+ @resource.signal
107
+
108
+ # Wait for a signal from another thread to try again
109
+ @resource.wait(@lock)
110
+ end
111
+ end
112
+ end
113
+ end
114
+
115
+ def release_slot(hook)
116
+ @lock.synchronize do
117
+ slots_released = processors_for_hook(hook)
118
+ @slots_available += slots_released
119
+
120
+ if @hooks_left.any?
121
+ # Signal once. `wait_for_slot` will perform additional signals if
122
+ # there are still slots available. This prevents us from sending out
123
+ # useless signals
124
+ @resource.signal
125
+ end
126
+ end
127
+ end
128
+
129
+ def processors_for_hook(hook)
130
+ hook.parallelize? ? hook.processors : @config.concurrency
131
+ end
132
+
133
+ def print_results
134
+ if @interrupted
85
135
  @printer.run_interrupted
86
- elsif failed
136
+ elsif @failed
87
137
  @printer.run_failed
88
- elsif warned
138
+ elsif @warned
89
139
  @printer.run_warned
90
140
  else
91
141
  @printer.run_succeeded
92
142
  end
93
143
  end
94
144
 
95
- def run_hook(hook)
96
- return if should_skip?(hook)
97
-
98
- @printer.start_hook(hook)
99
-
145
+ def run_hook(hook) # rubocop:disable Metrics/CyclomaticComplexity
100
146
  status, output = nil, nil
101
147
 
102
148
  begin
103
- # Disable the interrupt handler during individual hook run so that
104
- # Ctrl-C actually stops the current hook from being run, but doesn't
105
- # halt the entire process.
106
- InterruptHandler.disable_until_finished_or_interrupted do
107
- status, output = hook.run_and_transform
108
- end
149
+ wait_for_slot(hook)
150
+ return if should_skip?(hook)
151
+
152
+ status, output = hook.run_and_transform
109
153
  rescue => ex
110
154
  status = :fail
111
155
  output = "Hook raised unexpected error\n#{ex.message}\n#{ex.backtrace.join("\n")}"
112
- rescue Interrupt
113
- # At this point, interrupt has been handled and protection is back in
114
- # effect thanks to the InterruptHandler.
115
- status = :interrupt
116
- output = 'Hook was interrupted by Ctrl-C; restoring repo state...'
117
156
  end
118
157
 
119
- @printer.end_hook(hook, status, output)
158
+ @failed = true if status == :fail
159
+ @warned = true if status == :warn
160
+
161
+ @printer.end_hook(hook, status, output) unless @interrupted
120
162
 
121
163
  status
164
+ rescue Interrupt
165
+ @interrupted = true
166
+ ensure
167
+ release_slot(hook)
122
168
  end
123
169
 
124
170
  def should_skip?(hook)
125
- return true unless hook.enabled?
171
+ return true if @interrupted || !hook.enabled?
126
172
 
127
173
  if hook.skip?
128
174
  if hook.required?
@@ -1,5 +1,7 @@
1
1
  # encoding: utf-8
2
2
 
3
+ require 'monitor'
4
+
3
5
  module Overcommit
4
6
  # Provide a set of callbacks which can be executed as events occur during the
5
7
  # course of {HookRunner#run}.
@@ -9,6 +11,8 @@ module Overcommit
9
11
  def initialize(logger, context)
10
12
  @log = logger
11
13
  @context = context
14
+ @lock = Monitor.new # Need to use monitor so we can have re-entrant locks
15
+ synchronize_all_methods
12
16
  end
13
17
 
14
18
  # Executed at the very beginning of running the collection of hooks.
@@ -20,13 +24,6 @@ module Overcommit
20
24
  log.debug "✓ No applicable #{hook_script_name} hooks to run"
21
25
  end
22
26
 
23
- # Executed at the start of an individual hook run.
24
- def start_hook(hook)
25
- unless hook.quiet?
26
- print_header(hook)
27
- end
28
- end
29
-
30
27
  def hook_skipped(hook)
31
28
  log.warning "Skipping #{hook.name}"
32
29
  end
@@ -39,11 +36,16 @@ module Overcommit
39
36
  def end_hook(hook, status, output)
40
37
  # Want to print the header for quiet hooks only if the result wasn't good
41
38
  # so that the user knows what failed
42
- print_header(hook) if hook.quiet? && status != :pass
39
+ print_header(hook) if !hook.quiet? || status != :pass
43
40
 
44
41
  print_result(hook, status, output)
45
42
  end
46
43
 
44
+ def interrupt_triggered
45
+ log.newline
46
+ log.error 'Interrupt signal received. Stopping hooks...'
47
+ end
48
+
47
49
  # Executed when a hook run was interrupted/cancelled by user.
48
50
  def run_interrupted
49
51
  log.newline
@@ -108,5 +110,27 @@ module Overcommit
108
110
  def hook_script_name
109
111
  @context.hook_script_name
110
112
  end
113
+
114
+ # Get all public methods that were defined on this class and wrap them with
115
+ # synchronization locks so we ensure the output isn't interleaved amongst
116
+ # the various threads.
117
+ def synchronize_all_methods
118
+ methods = self.class.instance_methods - self.class.superclass.instance_methods
119
+
120
+ methods.each do |method_name|
121
+ old_method = :"old_#{method_name}"
122
+ new_method = :"synchronized_#{method_name}"
123
+
124
+ self.class.__send__(:alias_method, old_method, method_name)
125
+
126
+ self.class.send(:define_method, new_method) do |*args|
127
+ @lock.synchronize do
128
+ __send__(old_method, *args)
129
+ end
130
+ end
131
+
132
+ self.class.__send__(:alias_method, method_name, new_method)
133
+ end
134
+ end
111
135
  end
112
136
  end
@@ -211,6 +211,42 @@ module Overcommit
211
211
  Subprocess.spawn_detached(args)
212
212
  end
213
213
 
214
+ # Return the number of processors used by the OS for process scheduling.
215
+ #
216
+ # @see https://github.com/grosser/parallel/blob/v1.6.1/lib/parallel/processor_count.rb#L17-L51
217
+ def processor_count # rubocop:disable all
218
+ @processor_count ||=
219
+ begin
220
+ if Overcommit::OS.windows?
221
+ require 'win32ole'
222
+ result = WIN32OLE.connect('winmgmts://').ExecQuery(
223
+ 'select NumberOfLogicalProcessors from Win32_Processor')
224
+ result.to_enum.collect(&:NumberOfLogicalProcessors).reduce(:+)
225
+ elsif File.readable?('/proc/cpuinfo')
226
+ IO.read('/proc/cpuinfo').scan(/^processor/).size
227
+ elsif File.executable?('/usr/bin/hwprefs')
228
+ IO.popen('/usr/bin/hwprefs thread_count').read.to_i
229
+ elsif File.executable?('/usr/sbin/psrinfo')
230
+ IO.popen('/usr/sbin/psrinfo').read.scan(/^.*on-*line/).size
231
+ elsif File.executable?('/usr/sbin/ioscan')
232
+ IO.popen('/usr/sbin/ioscan -kC processor') do |out|
233
+ out.read.scan(/^.*processor/).size
234
+ end
235
+ elsif File.executable?('/usr/sbin/pmcycles')
236
+ IO.popen('/usr/sbin/pmcycles -m').read.count("\n")
237
+ elsif File.executable?('/usr/sbin/lsdev')
238
+ IO.popen('/usr/sbin/lsdev -Cc processor -S 1').read.count("\n")
239
+ elsif File.executable?('/usr/sbin/sysctl')
240
+ IO.popen('/usr/sbin/sysctl -n hw.ncpu').read.to_i
241
+ elsif File.executable?('/sbin/sysctl')
242
+ IO.popen('/sbin/sysctl -n hw.ncpu').read.to_i
243
+ else
244
+ # Unknown platform; assume 1 processor
245
+ 1
246
+ end
247
+ end
248
+ end
249
+
214
250
  # Calls a block of code with a modified set of environment variables,
215
251
  # restoring them once the code has executed.
216
252
  def with_environment(env)
@@ -2,5 +2,5 @@
2
2
 
3
3
  # Defines the gem version.
4
4
  module Overcommit
5
- VERSION = '0.31.0'.freeze
5
+ VERSION = '0.32.0.rc1'.freeze
6
6
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: overcommit
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.31.0
4
+ version: 0.32.0.rc1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brigade Engineering
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2016-01-26 00:00:00.000000000 Z
12
+ date: 2016-01-29 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: childprocess
@@ -263,9 +263,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
263
263
  version: 1.9.3
264
264
  required_rubygems_version: !ruby/object:Gem::Requirement
265
265
  requirements:
266
- - - ">="
266
+ - - ">"
267
267
  - !ruby/object:Gem::Version
268
- version: '0'
268
+ version: 1.3.1
269
269
  requirements: []
270
270
  rubyforge_project:
271
271
  rubygems_version: 2.4.5.1