overcommit 0.31.0 → 0.32.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
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