overcommit 0.26.0 → 0.27.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/config/default.yml +38 -0
  3. data/config/starter.yml +2 -1
  4. data/lib/overcommit/cli.rb +2 -0
  5. data/lib/overcommit/command_splitter.rb +144 -0
  6. data/lib/overcommit/configuration.rb +45 -10
  7. data/lib/overcommit/exceptions.rb +3 -0
  8. data/lib/overcommit/git_repo.rb +8 -0
  9. data/lib/overcommit/hook/base.rb +16 -2
  10. data/lib/overcommit/hook/post_rewrite/base.rb +1 -1
  11. data/lib/overcommit/hook/pre_commit/case_conflicts.rb +1 -1
  12. data/lib/overcommit/hook/pre_commit/hard_tabs.rb +1 -1
  13. data/lib/overcommit/hook/pre_commit/html_hint.rb +21 -0
  14. data/lib/overcommit/hook/pre_commit/local_paths_in_gemfile.rb +1 -1
  15. data/lib/overcommit/hook/pre_commit/merge_conflicts.rb +1 -1
  16. data/lib/overcommit/hook/pre_commit/semi_standard.rb +2 -2
  17. data/lib/overcommit/hook/pre_commit/standard.rb +2 -2
  18. data/lib/overcommit/hook/pre_commit/trailing_whitespace.rb +1 -1
  19. data/lib/overcommit/hook_context/base.rb +43 -1
  20. data/lib/overcommit/hook_context/commit_msg.rb +2 -2
  21. data/lib/overcommit/hook_context/post_checkout.rb +8 -0
  22. data/lib/overcommit/hook_context/post_rewrite.rb +29 -0
  23. data/lib/overcommit/hook_context/pre_commit.rb +1 -14
  24. data/lib/overcommit/hook_loader/plugin_hook_loader.rb +42 -3
  25. data/lib/overcommit/hook_runner.rb +12 -0
  26. data/lib/overcommit/hook_signer.rb +41 -6
  27. data/lib/overcommit/logger.rb +5 -0
  28. data/lib/overcommit/printer.rb +1 -1
  29. data/lib/overcommit/subprocess.rb +22 -1
  30. data/lib/overcommit/utils.rb +58 -4
  31. data/lib/overcommit/version.rb +1 -1
  32. data/template-dir/hooks/commit-msg +24 -4
  33. data/template-dir/hooks/overcommit-hook +24 -4
  34. data/template-dir/hooks/post-checkout +24 -4
  35. data/template-dir/hooks/post-commit +24 -4
  36. data/template-dir/hooks/post-merge +24 -4
  37. data/template-dir/hooks/post-rewrite +24 -4
  38. data/template-dir/hooks/pre-commit +24 -4
  39. data/template-dir/hooks/pre-push +24 -4
  40. data/template-dir/hooks/pre-rebase +24 -4
  41. metadata +6 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 9f071e1a8038ef082833913a8eb83e6bfb44b371
4
- data.tar.gz: eb442b2dc4457f8173f9fff48572f575839c39e9
3
+ metadata.gz: b15d613ceeafdf7cf3c762c611b309b596bf9fd9
4
+ data.tar.gz: 89d01be1ecf271fc7f8f6b6472fb6e056e8980c7
5
5
  SHA512:
6
- metadata.gz: c2e2faad1aea169d67e252967d22397bf05a4511fdb5247a57f3b2d12e66fa186ea594a24819619e648d56d90b30a965e643027e320e8b4340c785f31e6ae4b9
7
- data.tar.gz: a8b53b06f1ab3b4eb6cbf31cf77abfdf053a7af1719f2b0e1482ca5c1e4ea62082357d00a921415286f8340675e9734cad1d73d0a69fd0dd261bf6194ef43b27
6
+ metadata.gz: 5a665c68e8e3066f1f4f2b62997474b20f910360a7aadf2b24d5c2caad64260a3c2f485ffbd9174b7681fbd24a743a07a97820a2c4fed38e5d54c00c95ceccd8
7
+ data.tar.gz: a570dcdcf2ac15e352f837ebb404819f197d74428881586f2c2eec1f6a4f4eebfbf5e4c80536cb38fe2a6ca941b5de129357ae0437c8b739a747738a4f2d1baf
data/config/default.yml CHANGED
@@ -2,6 +2,37 @@
2
2
  #
3
3
  # This is an opinionated list of which hooks are valuable to run and what their
4
4
  # out-of-the-box settings should be.
5
+ #-------------------------------------------------------------------------------
6
+
7
+ # Loads Bundler context from a Gemfile. If false, does nothing (default).
8
+ #
9
+ # Specifying a Gemfile for Bundler to load allows you to control which gems are
10
+ # available in the load path (i.e. loadable via `require`) within your hook
11
+ # runs. Note that having a Gemfile requires you to include `overcommit` itself
12
+ # in your Gemfile (otherwise Overcommit can't load itself!).
13
+ #
14
+ # This is useful if you want to:
15
+ #
16
+ # - Enforce a specific version of Overcommit to use for all hook runs
17
+ # (or to use a version from the master branch that has not been released yet)
18
+ # - Enforce a specific version or unreleased branch is used for a gem you want
19
+ # to use in your git hooks
20
+ #
21
+ # WARNING: This makes your hook runs slower, but you can work around this!
22
+ #
23
+ # Loading a Bundler context necessarily adds a startup delay to your hook runs
24
+ # as Bundler parses the Gemfile and checks that the dependencies are satisfied.
25
+ # Thus for projects with many gems this can introduce a noticeable delay.
26
+ #
27
+ # The recommended workaround is to create a separate Gemfile in the root of your
28
+ # repository (call it `.overcommit_gems.rb`), and include only the gems that
29
+ # your Overcommit hooks need in order to run. This significantly reduces the
30
+ # startup delay in your hook runs. Make sure to commit both
31
+ # `.overcommit_gems.rb` and the resulting `.overcommit_gems.rb.lock` file to
32
+ # your repository, and then set the `gemfile` option below to the name you gave
33
+ # the file.
34
+ # (Generate lock file by running `bundle install --gemfile=.overcommit_gems.rb`)
35
+ gemfile: false
5
36
 
6
37
  # Where to store hook plugins specific to a repository. These are loaded in
7
38
  # addition to the default hooks Overcommit comes with. The location is relative
@@ -195,6 +226,13 @@ PreCommit:
195
226
  required_executable: 'grep'
196
227
  flags: ['-IHn', "\t"]
197
228
 
229
+ HtmlHint:
230
+ enabled: false
231
+ description: 'Analyzing with HTMLHint'
232
+ required_executable: 'htmlhint'
233
+ install_command: 'npm install -g htmlhint'
234
+ include: '**/*.html'
235
+
198
236
  HtmlTidy:
199
237
  enabled: false
200
238
  description: 'Analyzing HTML with tidy'
data/config/starter.yml CHANGED
@@ -17,6 +17,7 @@
17
17
 
18
18
  #PreCommit:
19
19
  # RuboCop:
20
+ # enabled: true
20
21
  # on_warn: fail # Treat all warnings as failures
21
22
  #
22
23
  # TrailingWhitespace:
@@ -25,7 +26,7 @@
25
26
  #
26
27
  #PostCheckout:
27
28
  # ALL: # Special hook name that customizes all hooks of this type
28
- # quiet: true # Change all post-checkout hooks to only display output on failure
29
+ # quiet: true # Change all post-checkout hooks to only display output on failure
29
30
  #
30
31
  # IndexTags:
31
32
  # enabled: true # Generate a tags file with `ctags` each time HEAD changes
@@ -9,6 +9,8 @@ module Overcommit
9
9
  @input = input
10
10
  @log = logger
11
11
  @options = {}
12
+
13
+ Overcommit::Utils.log = logger
12
14
  end
13
15
 
14
16
  def run
@@ -0,0 +1,144 @@
1
+ module Overcommit
2
+ # Distributes a list of arguments over multiple invocations of a command.
3
+ #
4
+ # This accomplishes the same functionality provided by `xargs` but in a
5
+ # cross-platform way that does not require any pre-existing tools.
6
+ #
7
+ # One of the tradeoffs with this approach is that we no longer deal with a
8
+ # single exit status from a command, but multiple (one for each invocation).
9
+ #
10
+ # This will return a struct similar to `Subprocess::Result` but with
11
+ # additional `statuses`, `stdouts`, and `stderrs` fields so hook authors can
12
+ # actually see the results of each invocation. If they don't care, the
13
+ # standard `status`, `stdout`, and `stderr` will still work but be a
14
+ # aggregation/concatenation of all statuses/outputs.
15
+ class CommandSplitter
16
+ # Encapsulates the result of a split argument run.
17
+ #
18
+ # @attr_reader statuses [Array<Integer>] status codes for invocations
19
+ # @attr_reader stdouts [Array<String>] standard outputs from invocations
20
+ # @attr_reader stderrs [Array<String>] standard error outputs from invocations
21
+ Result = Struct.new(:statuses, :stdouts, :stderrs) do
22
+ # Returns whether all invocations were successful.
23
+ #
24
+ # @return [true,false]
25
+ def success?
26
+ status == 0
27
+ end
28
+
29
+ # Returns `0` if all invocations returned `0`; `1` otherwise.
30
+ #
31
+ # @return [true,false]
32
+ def status
33
+ statuses.all? { |code| code == 0 } ? 0 : 1
34
+ end
35
+
36
+ # Returns concatenated standard output streams of all invocations in the
37
+ # order they were executed.
38
+ #
39
+ # @return [String]
40
+ def stdout
41
+ stdouts.join
42
+ end
43
+
44
+ # Returns concatenated standard error streams of all invocations in the
45
+ # order they were executed.
46
+ #
47
+ # @return [String]
48
+ def stderr
49
+ stderrs.join
50
+ end
51
+ end
52
+
53
+ class << self
54
+ def execute(initial_args, options)
55
+ options = options.dup
56
+
57
+ if (splittable_args = (options.delete(:args) { [] })).empty?
58
+ raise Overcommit::Exceptions::InvalidCommandArgs,
59
+ 'Must specify list of arguments to split on'
60
+ end
61
+
62
+ # Execute each chunk of arguments in serial. We don't parallelize (yet)
63
+ # since in theory we want to support parallelization at the hook level
64
+ # and not within individual hooks.
65
+ results = extract_argument_lists(initial_args, splittable_args).map do |arg_list|
66
+ Overcommit::Subprocess.spawn(arg_list, options)
67
+ end
68
+
69
+ Result.new(results.map(&:status), results.map(&:stdout), results.map(&:stderr))
70
+ end
71
+
72
+ private
73
+
74
+ # Given a list of prefix arguments and suffix arguments that can be split,
75
+ # returns a list of argument lists that are executable on the current OS
76
+ # without exceeding command line limitations.
77
+ def extract_argument_lists(args, splittable_args)
78
+ # Total number of bytes needed to contain the prefix command
79
+ # (including byte separators between each argument)
80
+ prefix_bytes = (args.size - 1) + args.reduce(0) { |sum, arg| sum + arg.bytesize }
81
+
82
+ if prefix_bytes >= max_command_length
83
+ raise Overcommit::Exceptions::InvalidCommandArgs,
84
+ "Command `#{args.take(5).join(' ')} ...` is longer than the " \
85
+ 'maximum number of bytes allowed by the operating system ' \
86
+ "(#{max_command_length})"
87
+ end
88
+
89
+ arg_lists = []
90
+ index = 0
91
+ while index <= splittable_args.length - 1
92
+ arg_list, index = arguments_under_limit(splittable_args,
93
+ index,
94
+ max_command_length - prefix_bytes)
95
+ arg_lists << args + arg_list
96
+ end
97
+
98
+ arg_lists
99
+ end
100
+
101
+ # @return [Array<Array<String>, Integer>] tuple of arguments and new index
102
+ def arguments_under_limit(splittable_args, start_index, byte_limit)
103
+ index = start_index
104
+ total_bytes = 0
105
+
106
+ loop do
107
+ break if index > splittable_args.length - 1
108
+ total_bytes += splittable_args[index].bytesize
109
+ break if total_bytes > byte_limit # Not enough room
110
+ index += 1
111
+ end
112
+
113
+ if index == start_index
114
+ # No argument was consumed; perhaps a really long argument?
115
+ raise Overcommit::Exceptions::InvalidCommandArgs,
116
+ "Argument `#{splittable_args[index][0..5]}...` exceeds the " \
117
+ 'maximum command length when appended to command prefix and ' \
118
+ "can't be split further"
119
+ end
120
+
121
+ [splittable_args[start_index...index], index]
122
+ end
123
+
124
+ # Returns the maximum number of arguments allowed in a single command on
125
+ # this system.
126
+ #
127
+ # @return [Integer]
128
+ def max_command_length
129
+ @max_command_length ||=
130
+ if Gem.win_platform?
131
+ # Windows is limited to 2048 since that is a worst-case scenario.
132
+ # http://blogs.msdn.com/b/oldnewthing/archive/2003/12/10/56028.aspx
133
+ 2048
134
+ else
135
+ # We fudge factor this by halving the buffer size since *nix systems
136
+ # usually have pretty large limits, and the actual limit changes
137
+ # depending on how much of your stack is environment variables.
138
+ # Definitely erring on the side of overly cautious.
139
+ `getconf ARG_MAX`.to_i / 2
140
+ end
141
+ end
142
+ end
143
+ end
144
+ end
@@ -18,10 +18,18 @@ module Overcommit
18
18
  end
19
19
  alias_method :eql?, :==
20
20
 
21
+ # Access the configuration as if it were a hash.
22
+ #
23
+ # @param key [String]
24
+ # @return [Array,Hash,Number,String]
25
+ def [](key)
26
+ @hash[key]
27
+ end
28
+
21
29
  # Returns absolute path to the directory that external hook plugins should
22
30
  # be loaded from.
23
31
  def plugin_directory
24
- File.join(Overcommit::Utils.repo_root, @hash['plugin_directory'] || '.githooks')
32
+ File.join(Overcommit::Utils.repo_root, @hash['plugin_directory'] || '.git-hooks')
25
33
  end
26
34
 
27
35
  def verify_plugin_signatures?
@@ -88,6 +96,14 @@ module Overcommit
88
96
  select { |hook_name| hook_enabled?(hook_context, hook_name) }
89
97
  end
90
98
 
99
+ # Returns the ad hoc hooks that have been enabled for a hook type.
100
+ def enabled_ad_hoc_hooks(hook_context)
101
+ @hash[hook_context.hook_class_name].keys.
102
+ select { |hook_name| hook_name != 'ALL' }.
103
+ select { |hook_name| ad_hoc_hook?(hook_context, hook_name) }.
104
+ select { |hook_name| hook_enabled?(hook_context, hook_name) }
105
+ end
106
+
91
107
  # Returns a non-modifiable configuration for a hook.
92
108
  def for_hook(hook, hook_type = nil)
93
109
  unless hook_type
@@ -117,17 +133,25 @@ module Overcommit
117
133
  # environment variables.
118
134
  def apply_environment!(hook_context, env)
119
135
  skipped_hooks = "#{env['SKIP']} #{env['SKIP_CHECKS']} #{env['SKIP_HOOKS']}".split(/[:, ]/)
136
+ only_hooks = env.fetch('ONLY', '').split(/[:, ]/)
120
137
  hook_type = hook_context.hook_class_name
121
138
 
122
- if skipped_hooks.include?('all') || skipped_hooks.include?('ALL')
139
+ if only_hooks.any? || skipped_hooks.include?('all') || skipped_hooks.include?('ALL')
123
140
  @hash[hook_type]['ALL']['skip'] = true
124
- else
125
- skipped_hooks.select { |hook_name| hook_exists?(hook_context, hook_name) }.
126
- map { |hook_name| Overcommit::Utils.camel_case(hook_name) }.
127
- each do |hook_name|
128
- @hash[hook_type][hook_name] ||= {}
129
- @hash[hook_type][hook_name]['skip'] = true
130
- end
141
+ end
142
+
143
+ only_hooks.select { |hook_name| hook_exists?(hook_context, hook_name) }.
144
+ map { |hook_name| Overcommit::Utils.camel_case(hook_name) }.
145
+ each do |hook_name|
146
+ @hash[hook_type][hook_name] ||= {}
147
+ @hash[hook_type][hook_name]['skip'] = false
148
+ end
149
+
150
+ skipped_hooks.select { |hook_name| hook_exists?(hook_context, hook_name) }.
151
+ map { |hook_name| Overcommit::Utils.camel_case(hook_name) }.
152
+ each do |hook_name|
153
+ @hash[hook_type][hook_name] ||= {}
154
+ @hash[hook_type][hook_name]['skip'] = true
131
155
  end
132
156
  end
133
157
 
@@ -149,6 +173,16 @@ module Overcommit
149
173
 
150
174
  private
151
175
 
176
+ def ad_hoc_hook?(hook_context, hook_name)
177
+ ad_hoc_conf = @hash.fetch(hook_context.hook_class_name, {}).fetch(hook_name, {})
178
+
179
+ # Ad hoc hooks are neither built-in nor have a plugin file written but
180
+ # still have a `command` specified to be run
181
+ !built_in_hook?(hook_context, hook_name) &&
182
+ !plugin_hook?(hook_context, hook_name) &&
183
+ (ad_hoc_conf['command'] || ad_hoc_conf['required_executable'])
184
+ end
185
+
152
186
  def built_in_hook?(hook_context, hook_name)
153
187
  hook_name = Overcommit::Utils.snake_case(hook_name)
154
188
 
@@ -158,7 +192,8 @@ module Overcommit
158
192
 
159
193
  def hook_exists?(hook_context, hook_name)
160
194
  built_in_hook?(hook_context, hook_name) ||
161
- plugin_hook?(hook_context, hook_name)
195
+ plugin_hook?(hook_context, hook_name) ||
196
+ ad_hoc_hook?(hook_context, hook_name)
162
197
  end
163
198
 
164
199
  def hook_enabled?(hook_context_or_type, hook_name)
@@ -30,6 +30,9 @@ module Overcommit::Exceptions
30
30
  # Raised when an installation target is not a valid git repository.
31
31
  class InvalidGitRepo < StandardError; end
32
32
 
33
+ # Raised when a hook was defined incorrectly.
34
+ class InvalidHookDefinition < StandardError; end
35
+
33
36
  # Raised when one or more hook plugin signatures have changed.
34
37
  class InvalidHookSignature < StandardError; end
35
38
 
@@ -113,6 +113,14 @@ module Overcommit
113
113
  reject { |file| File.directory?(file) } # Exclude submodule directories
114
114
  end
115
115
 
116
+ # Returns whether the specified file/path is tracked by this repository.
117
+ #
118
+ # @param path [String]
119
+ # @return [true,false]
120
+ def tracked?(path)
121
+ Overcommit::Utils.execute(%W[git ls-files #{path} --error-unmatch]).success?
122
+ end
123
+
116
124
  # Returns the names of all files that are tracked by git.
117
125
  #
118
126
  # @return [Array<String>] list of absolute file paths
@@ -82,8 +82,22 @@ module Overcommit::Hook
82
82
  Overcommit::Utils.in_path?(cmd)
83
83
  end
84
84
 
85
- def execute(cmd)
86
- Overcommit::Utils.execute(cmd)
85
+ # Execute a command in a separate process.
86
+ #
87
+ # If `splittable_args` is specified, ensures that those arguments are
88
+ # concatenated onto the end of the `cmd` arguments, but split up so that the
89
+ # operating system's maximum command length is not exceeded. This is useful
90
+ # for splitting up long file lists.
91
+ #
92
+ # @param cmd [Array<String>] command arguments
93
+ # @param options [Hash]
94
+ # @option options [Array<String>] :args arguments that can be split up over
95
+ # multiple invocations (usually a list of files)
96
+ # @option options [String] :input string to pass to process' standard input
97
+ # stream
98
+ # @return [#status,#stdout,#stderr] struct containing result of invocation
99
+ def execute(cmd, options = {})
100
+ Overcommit::Utils.execute(cmd, options)
87
101
  end
88
102
 
89
103
  def execute_in_background(cmd)
@@ -5,6 +5,6 @@ module Overcommit::Hook::PostRewrite
5
5
  class Base < Overcommit::Hook::Base
6
6
  extend Forwardable
7
7
 
8
- def_delegators :@context, :amend?, :rebase?
8
+ def_delegators :@context, :amend?, :rebase?, :rewritten_commits
9
9
  end
10
10
  end
@@ -6,7 +6,7 @@ module Overcommit::Hook::PreCommit
6
6
  repo_files = Set.new(applicable_files)
7
7
 
8
8
  unless Overcommit::GitRepo.initial_commit?
9
- paths = repo_files.map { |file| File.dirname(file) + File::SEPARATOR }
9
+ paths = repo_files.map { |file| File.dirname(file) + File::SEPARATOR }.uniq
10
10
  repo_files += Overcommit::GitRepo.list_files(paths)
11
11
  end
12
12
 
@@ -2,7 +2,7 @@ module Overcommit::Hook::PreCommit
2
2
  # Checks for hard tabs in files.
3
3
  class HardTabs < Base
4
4
  def run
5
- result = execute(command + applicable_files)
5
+ result = execute(command, args: applicable_files)
6
6
 
7
7
  extract_messages(
8
8
  result.stdout.split("\n"),
@@ -0,0 +1,21 @@
1
+ module Overcommit::Hook::PreCommit
2
+ # Runs `htmlhint` against any modified HTML files.
3
+ #
4
+ # @see http://htmlhint.com/
5
+ class HtmlHint < Base
6
+ def run
7
+ result = execute(command + applicable_files)
8
+ output = Overcommit::Utils.strip_color_codes(result.stdout.chomp)
9
+
10
+ message_groups = output.split("\n\n")[0..-2]
11
+ message_groups.map do |group|
12
+ lines = group.split("\n").map(&:strip)
13
+ file = lines[0][/(.+):/, 1]
14
+ extract_messages(
15
+ lines[1..-1].map { |msg| "#{file}: #{msg}" },
16
+ /^(?<file>[^:]+): line (?<line>\d+)/
17
+ )
18
+ end.flatten
19
+ end
20
+ end
21
+ end
@@ -2,7 +2,7 @@ module Overcommit::Hook::PreCommit
2
2
  # Checks for local paths in files and issues a warning
3
3
  class LocalPathsInGemfile < Base
4
4
  def run
5
- result = execute(command + applicable_files)
5
+ result = execute(command, args: applicable_files)
6
6
 
7
7
  unless result.stdout.empty?
8
8
  return :warn, "Avoid pointing to local paths in Gemfiles:\n#{result.stdout}"