overcommit 0.26.0 → 0.27.0

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 (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}"