overcommit 0.23.0 → 0.24.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 (66) hide show
  1. checksums.yaml +4 -4
  2. data/bin/overcommit +1 -1
  3. data/config/default.yml +154 -18
  4. data/config/starter.yml +3 -3
  5. data/lib/overcommit.rb +2 -1
  6. data/lib/overcommit/cli.rb +11 -8
  7. data/lib/overcommit/configuration.rb +18 -4
  8. data/lib/overcommit/configuration_loader.rb +45 -28
  9. data/lib/overcommit/configuration_validator.rb +33 -1
  10. data/lib/overcommit/constants.rb +5 -3
  11. data/lib/overcommit/exceptions.rb +3 -0
  12. data/lib/overcommit/git_repo.rb +116 -0
  13. data/lib/overcommit/git_version.rb +15 -0
  14. data/lib/overcommit/hook/base.rb +42 -5
  15. data/lib/overcommit/hook/commit_msg/capitalized_subject.rb +13 -0
  16. data/lib/overcommit/hook/commit_msg/spell_check.rb +41 -0
  17. data/lib/overcommit/hook/post_checkout/submodule_status.rb +30 -0
  18. data/lib/overcommit/hook/post_commit/submodule_status.rb +30 -0
  19. data/lib/overcommit/hook/post_merge/submodule_status.rb +30 -0
  20. data/lib/overcommit/hook/post_rewrite/submodule_status.rb +30 -0
  21. data/lib/overcommit/hook/pre_commit/base.rb +2 -2
  22. data/lib/overcommit/hook/pre_commit/bundle_check.rb +1 -1
  23. data/lib/overcommit/hook/pre_commit/case_conflicts.rb +20 -0
  24. data/lib/overcommit/hook/pre_commit/coffee_lint.rb +29 -2
  25. data/lib/overcommit/hook/pre_commit/css_lint.rb +1 -8
  26. data/lib/overcommit/hook/pre_commit/go_lint.rb +8 -2
  27. data/lib/overcommit/hook/pre_commit/go_vet.rb +20 -0
  28. data/lib/overcommit/hook/pre_commit/html_tidy.rb +1 -10
  29. data/lib/overcommit/hook/pre_commit/image_optim.rb +11 -28
  30. data/lib/overcommit/hook/pre_commit/js_lint.rb +18 -0
  31. data/lib/overcommit/hook/pre_commit/jsl.rb +24 -0
  32. data/lib/overcommit/hook/pre_commit/json_syntax.rb +4 -7
  33. data/lib/overcommit/hook/pre_commit/rails_schema_up_to_date.rb +1 -1
  34. data/lib/overcommit/hook/pre_commit/ruby_lint.rb +19 -0
  35. data/lib/overcommit/hook/pre_commit/scss_lint.rb +8 -1
  36. data/lib/overcommit/hook/pre_commit/w3c_css.rb +4 -18
  37. data/lib/overcommit/hook/pre_commit/w3c_html.rb +4 -18
  38. data/lib/overcommit/hook/pre_commit/xml_syntax.rb +19 -0
  39. data/lib/overcommit/hook/pre_commit/yaml_syntax.rb +4 -8
  40. data/lib/overcommit/hook/pre_push/base.rb +10 -0
  41. data/lib/overcommit/hook/pre_push/protected_branches.rb +27 -0
  42. data/lib/overcommit/hook/pre_push/r_spec.rb +12 -0
  43. data/lib/overcommit/hook/pre_rebase/base.rb +11 -0
  44. data/lib/overcommit/hook_context.rb +2 -2
  45. data/lib/overcommit/hook_context/base.rb +5 -7
  46. data/lib/overcommit/hook_context/pre_commit.rb +66 -16
  47. data/lib/overcommit/hook_context/pre_push.rb +44 -0
  48. data/lib/overcommit/hook_context/pre_rebase.rb +36 -0
  49. data/lib/overcommit/hook_runner.rb +27 -7
  50. data/lib/overcommit/installer.rb +46 -7
  51. data/lib/overcommit/message_processor.rb +3 -0
  52. data/lib/overcommit/printer.rb +8 -12
  53. data/lib/overcommit/subprocess.rb +11 -0
  54. data/lib/overcommit/utils.rb +28 -6
  55. data/lib/overcommit/version.rb +1 -1
  56. data/template-dir/hooks/commit-msg +2 -2
  57. data/template-dir/hooks/overcommit-hook +2 -2
  58. data/template-dir/hooks/post-checkout +2 -2
  59. data/template-dir/hooks/post-commit +2 -2
  60. data/template-dir/hooks/post-merge +2 -2
  61. data/template-dir/hooks/post-rewrite +2 -2
  62. data/template-dir/hooks/pre-commit +2 -2
  63. data/template-dir/hooks/pre-push +81 -0
  64. data/template-dir/hooks/pre-rebase +81 -0
  65. metadata +33 -13
  66. data/lib/overcommit/hook/pre_commit/pry_binding.rb +0 -14
@@ -2,40 +2,23 @@ module Overcommit::Hook::PreCommit
2
2
  # Checks for images that can be optimized with `image_optim`.
3
3
  class ImageOptim < Base
4
4
  def run
5
- begin
6
- require 'image_optim'
7
- rescue LoadError
8
- return :fail, 'image_optim not installed -- run `gem install image_optim`'
9
- end
5
+ result = execute(command + applicable_files)
6
+ return [:fail, result.stdout + result.stderr] unless result.success?
10
7
 
11
- optimized_images =
12
- begin
13
- optimize_images(applicable_files)
14
- rescue ::ImageOptim::BinResolver::BinNotFound => e
15
- return :fail, "#{e.message}. The image_optim gem is dependendent on this binary."
16
- end
8
+ optimized_files = extract_optimized_files(result.stdout)
9
+ return :pass if optimized_files.empty?
17
10
 
18
- if optimized_images.any?
19
- return :fail,
20
- "The following images are optimizable:\n#{optimized_images.join("\n")}" \
21
- "\n\nOptimize them by running:\n" \
22
- " image_optim --skip-missing-workers #{optimized_images.join(' ')}"
23
- end
24
-
25
- :pass
11
+ output = "The following images are optimizable:\n#{optimized_files.join("\n")}"
12
+ output += "\n\nOptimize them by running `#{command.join(' ')} #{optimized_files.join(' ')}`"
13
+ [:fail, output]
26
14
  end
27
15
 
28
16
  private
29
17
 
30
- def optimize_images(image_paths)
31
- image_optim = ::ImageOptim.new(skip_missing_workers: true)
32
-
33
- optimized_images =
34
- image_optim.optimize_images(image_paths) do |path, optimized|
35
- path if optimized
36
- end
37
-
38
- optimized_images.compact
18
+ def extract_optimized_files(output)
19
+ output.split("\n").
20
+ select { |line| line =~ /^\d+/ }.
21
+ map { |line| line.split(/\s+/).last }
39
22
  end
40
23
  end
41
24
  end
@@ -0,0 +1,18 @@
1
+ module Overcommit::Hook::PreCommit
2
+ # Runs `jslint` against any modified JavaScript files.
3
+ class JsLint < Base
4
+ MESSAGE_REGEX = /(?<file>[^:]+):(?<line>\d+)/
5
+
6
+ def run
7
+ result = execute(command + applicable_files)
8
+ return :pass if result.success?
9
+
10
+ # example message:
11
+ # path/to/file.js:1:1: Error message
12
+ extract_messages(
13
+ result.stdout.split("\n").grep(MESSAGE_REGEX),
14
+ MESSAGE_REGEX
15
+ )
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,24 @@
1
+ module Overcommit::Hook::PreCommit
2
+ # Runs `jsl` against any modified JavaScript files.
3
+ class Jsl < Base
4
+ MESSAGE_REGEX = /(?<file>.+)\((?<line>\d+)\):(?<type>[^:]+)/
5
+
6
+ MESSAGE_TYPE_CATEGORIZER = lambda do |type|
7
+ type =~ /warning/ ? :warning : :error
8
+ end
9
+
10
+ def run
11
+ file_flags = applicable_files.map { |file| ['-process', file] }
12
+ result = execute(command + file_flags.flatten)
13
+ return :pass if result.success?
14
+
15
+ # example message:
16
+ # path/to/file.js(1): lint warning: Error message
17
+ extract_messages(
18
+ result.stdout.split("\n").grep(MESSAGE_REGEX),
19
+ MESSAGE_REGEX,
20
+ MESSAGE_TYPE_CATEGORIZER
21
+ )
22
+ end
23
+ end
24
+ end
@@ -1,22 +1,19 @@
1
- require 'json'
2
-
3
1
  module Overcommit::Hook::PreCommit
4
2
  # Checks the syntax of any modified JSON files.
5
3
  class JsonSyntax < Base
6
4
  def run
7
- output = []
5
+ messages = []
8
6
 
9
7
  applicable_files.each do |file|
10
8
  begin
11
9
  JSON.parse(IO.read(file))
12
10
  rescue JSON::ParserError => e
13
- output << "#{e.message} parsing #{file}"
11
+ error = "#{e.message} parsing #{file}"
12
+ messages << Overcommit::Hook::Message.new(:error, file, nil, error)
14
13
  end
15
14
  end
16
15
 
17
- return :pass if output.empty?
18
-
19
- [:fail, output]
16
+ messages
20
17
  end
21
18
  end
22
19
  end
@@ -37,7 +37,7 @@ module Overcommit::Hook::PreCommit
37
37
  end
38
38
 
39
39
  def schema_files
40
- @schema_files ||= applicable_files.select do |file|
40
+ @schema_files ||= applicable_files.select do |file|
41
41
  file.match %r{db/schema\.rb|db/structure.*\.sql}
42
42
  end
43
43
  end
@@ -0,0 +1,19 @@
1
+ module Overcommit::Hook::PreCommit
2
+ # Runs `ruby-lint` against any modified Ruby files.
3
+ class RubyLint < Base
4
+ MESSAGE_TYPE_CATEGORIZER = lambda do |type|
5
+ type.include?('W') ? :warning : :error
6
+ end
7
+
8
+ def run
9
+ result = execute(command + applicable_files)
10
+ return :pass if result.success?
11
+
12
+ extract_messages(
13
+ result.stdout.split("\n"),
14
+ /^(?<file>[^:]+):(?<type>[^:]+):(?<line>\d+)/,
15
+ MESSAGE_TYPE_CATEGORIZER
16
+ )
17
+ end
18
+ end
19
+ end
@@ -7,7 +7,14 @@ module Overcommit::Hook::PreCommit
7
7
 
8
8
  def run
9
9
  result = execute(command + applicable_files)
10
- return :pass if result.success?
10
+
11
+ # Status code 81 indicates the applicable files were all filtered by
12
+ # exclusions defined by the configuration. In this case, we're happy to
13
+ # return success since there were technically no lints.
14
+ return :pass if [0, 81].include?(result.status)
15
+
16
+ # Any status that isn't indicating lint warnings or errors indicates failure
17
+ return :fail, result.stdout unless [1, 2].include?(result.status)
11
18
 
12
19
  extract_messages(
13
20
  result.stdout.split("\n"),
@@ -2,24 +2,10 @@ module Overcommit::Hook::PreCommit
2
2
  # Runs `w3c_validators` against any modified CSS files.
3
3
  class W3cCss < Base
4
4
  def run
5
- begin
6
- require 'w3c_validators'
7
- rescue LoadError
8
- return :fail, 'w3c_validators not installed -- run `gem install w3c_validators`'
9
- end
10
-
11
- result_messages =
12
- begin
13
- collect_messages
14
- rescue W3CValidators::ValidatorUnavailable => e
15
- return :fail, e.message
16
- rescue W3CValidators::ParsingError => e
17
- return :fail, e.message
18
- end
19
-
20
- return :pass if result_messages.empty?
21
-
22
- result_messages
5
+ collect_messages
6
+ rescue W3CValidators::ParsingError,
7
+ W3CValidators::ValidatorUnavailable => e
8
+ [:fail, e.message]
23
9
  end
24
10
 
25
11
  private
@@ -2,24 +2,10 @@ module Overcommit::Hook::PreCommit
2
2
  # Runs `w3c_validators` against any modified HTML files.
3
3
  class W3cHtml < Base
4
4
  def run
5
- begin
6
- require 'w3c_validators'
7
- rescue LoadError
8
- return :fail, 'w3c_validators not installed -- run `gem install w3c_validators`'
9
- end
10
-
11
- result_messages =
12
- begin
13
- collect_messages
14
- rescue W3CValidators::ValidatorUnavailable => e
15
- return :fail, e.message
16
- rescue W3CValidators::ParsingError => e
17
- return :fail, e.message
18
- end
19
-
20
- return :pass if result_messages.empty?
21
-
22
- result_messages
5
+ collect_messages
6
+ rescue W3CValidators::ParsingError,
7
+ W3CValidators::ValidatorUnavailable => e
8
+ [:fail, e.message]
23
9
  end
24
10
 
25
11
  private
@@ -0,0 +1,19 @@
1
+ module Overcommit::Hook::PreCommit
2
+ # Checks the syntax of any modified XML files.
3
+ class XmlSyntax < Base
4
+ def run
5
+ messages = []
6
+
7
+ applicable_files.each do |file|
8
+ begin
9
+ REXML::Document.new(IO.read(file))
10
+ rescue REXML::ParseException => e
11
+ error = "Error parsing #{file}: #{e.message}"
12
+ messages << Overcommit::Hook::Message.new(:error, file, nil, error)
13
+ end
14
+ end
15
+
16
+ messages
17
+ end
18
+ end
19
+ end
@@ -1,22 +1,18 @@
1
- require 'yaml'
2
-
3
1
  module Overcommit::Hook::PreCommit
4
2
  # Checks the syntax of any modified YAML files.
5
3
  class YamlSyntax < Base
6
4
  def run
7
- output = []
5
+ messages = []
8
6
 
9
7
  applicable_files.each do |file|
10
8
  begin
11
9
  YAML.load_file(file)
12
- rescue ArgumentError => e
13
- output << "#{e.message} parsing #{file}"
10
+ rescue ArgumentError, Psych::SyntaxError => e
11
+ messages << Overcommit::Hook::Message.new(:error, file, nil, e.message)
14
12
  end
15
13
  end
16
14
 
17
- return :pass if output.empty?
18
-
19
- [:fail, output]
15
+ messages
20
16
  end
21
17
  end
22
18
  end
@@ -0,0 +1,10 @@
1
+ require 'forwardable'
2
+
3
+ module Overcommit::Hook::PrePush
4
+ # Functionality common to all pre-push hooks.
5
+ class Base < Overcommit::Hook::Base
6
+ extend Forwardable
7
+
8
+ def_delegators :@context, :remote_name, :remote_url, :pushed_refs
9
+ end
10
+ end
@@ -0,0 +1,27 @@
1
+ module Overcommit::Hook::PrePush
2
+ # Prevents destructive updates to specified branches.
3
+ class ProtectedBranches < Base
4
+ def run
5
+ return :pass unless illegal_pushes.any?
6
+
7
+ messages = illegal_pushes.map do |pushed_ref|
8
+ "Deleting or force-pushing to #{pushed_ref.remote_ref} is not allowed."
9
+ end
10
+
11
+ [:fail, messages.join("\n")]
12
+ end
13
+
14
+ private
15
+
16
+ def branches
17
+ @branches ||= config['branches']
18
+ end
19
+
20
+ def illegal_pushes
21
+ @illegal_pushes ||= pushed_refs.select do |pushed_ref|
22
+ (pushed_ref.deleted? || pushed_ref.forced?) &&
23
+ branches.any? { |branch| pushed_ref.remote_ref == "refs/heads/#{branch}" }
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,12 @@
1
+ module Overcommit::Hook::PrePush
2
+ # Runs `rspec` test suite before push
3
+ class RSpec < Base
4
+ def run
5
+ result = execute(command)
6
+ return :pass if result.success?
7
+
8
+ output = result.stdout + result.stderr
9
+ [:fail, output]
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,11 @@
1
+ require 'forwardable'
2
+
3
+ module Overcommit::Hook::PreRebase
4
+ # Functionality common to all pre-rebase hooks.
5
+ class Base < Overcommit::Hook::Base
6
+ extend Forwardable
7
+
8
+ def_delegators :@context,
9
+ :upstream_branch, :rebased_branch, :fast_forward?, :rebased_commits
10
+ end
11
+ end
@@ -1,12 +1,12 @@
1
1
  # Utility module which manages the creation of {HookContext}s.
2
2
  module Overcommit::HookContext
3
- def self.create(hook_type, config, args)
3
+ def self.create(hook_type, config, args, input)
4
4
  hook_type_class = Overcommit::Utils.camel_case(hook_type)
5
5
  underscored_hook_type = Overcommit::Utils.snake_case(hook_type)
6
6
 
7
7
  require "overcommit/hook_context/#{underscored_hook_type}"
8
8
 
9
- Overcommit::HookContext.const_get(hook_type_class).new(config, args)
9
+ Overcommit::HookContext.const_get(hook_type_class).new(config, args, input)
10
10
  rescue LoadError, NameError => error
11
11
  # Could happen when a symlink was created for a hook type Overcommit does
12
12
  # not yet support.
@@ -13,9 +13,10 @@ module Overcommit::HookContext
13
13
  class Base
14
14
  # @param config [Overcommit::Configuration]
15
15
  # @param args [Array<String>]
16
- def initialize(config, args)
16
+ def initialize(config, args, input)
17
17
  @config = config
18
18
  @args = args
19
+ @input = input
19
20
  end
20
21
 
21
22
  # Returns the camel-cased type of this hook (e.g. PreCommit)
@@ -58,12 +59,9 @@ module Overcommit::HookContext
58
59
  []
59
60
  end
60
61
 
61
- # Returns a set of lines that have been modified for a file.
62
- #
63
- # By default, this returns an empty set. Subclasses should implement if
64
- # there is a concept of files changing for the type of hook being run.
65
- def modified_lines(_file)
66
- Set.new
62
+ # Returns an array of lines passed to the hook via STDIN.
63
+ def input_lines
64
+ @input_lines ||= @input.read.split("\n")
67
65
  end
68
66
  end
69
67
  end
@@ -7,7 +7,30 @@ module Overcommit::HookContext
7
7
  # This includes staged files, which lines of those files have been modified,
8
8
  # etc. It is also responsible for saving/restoring the state of the repo so
9
9
  # hooks only inspect staged changes.
10
- class PreCommit < Base
10
+ class PreCommit < Base # rubocop:disable ClassLength
11
+ # Returns whether this hook run was triggered by `git commit --amend`
12
+ def amendment?
13
+ return @amendment unless @amendment.nil?
14
+
15
+ cmd = Overcommit::Utils.parent_command
16
+ amend_pattern = 'commit(\s.*)?\s--amend(\s|$)'
17
+
18
+ return @amendment if
19
+ # True if the command is a commit with the --amend flag
20
+ @amendment = !(/\s#{amend_pattern}/ =~ cmd).nil?
21
+
22
+ # Check for git aliases that call `commit --amend`
23
+ `git config --get-regexp '^alias\\.' '#{amend_pattern}'`.
24
+ scan(/alias\.([-\w]+)/). # Extract the alias
25
+ each do |match|
26
+ return @amendment if
27
+ # True if the command uses a git alias for `commit --amend`
28
+ @amendment = !(/git\s+#{match[0]}/ =~ cmd).nil?
29
+ end
30
+
31
+ @amendment
32
+ end
33
+
11
34
  # Stash unstaged contents of files so hooks don't see changes that aren't
12
35
  # about to be committed.
13
36
  def setup_environment
@@ -18,9 +41,9 @@ module Overcommit::HookContext
18
41
  if !initial_commit? && any_changes?
19
42
  @stash_attempted = true
20
43
 
44
+ stash_message = "Overcommit: Stash of repo state before hook run at #{Time.now}"
21
45
  result = Overcommit::Utils.execute(
22
- %w[git stash save --keep-index --quiet] +
23
- ["Overcommit: Stash of repo state before hook run at #{Time.now}"]
46
+ %w[git stash save --keep-index --quiet] + [stash_message]
24
47
  )
25
48
 
26
49
  unless result.success?
@@ -31,7 +54,7 @@ module Overcommit::HookContext
31
54
  "\nSTDOUT:#{result.stdout}\nSTDERR:#{result.stderr}"
32
55
  end
33
56
 
34
- @changes_stashed = true
57
+ @changes_stashed = `git stash list -1`.include?(stash_message)
35
58
  end
36
59
 
37
60
  # While running the hooks make it appear as if nothing changed
@@ -64,34 +87,58 @@ module Overcommit::HookContext
64
87
  # Get a list of added, copied, or modified files that have been staged.
65
88
  # Renames and deletions are ignored, since there should be nothing to check.
66
89
  def modified_files
67
- @modified_files ||= Overcommit::GitRepo.modified_files(staged: true)
68
- end
90
+ unless @modified_files
91
+ @modified_files = Overcommit::GitRepo.modified_files(staged: true)
69
92
 
70
- # @deprecated
71
- # TODO: Remove this once we've moved all existing hooks to stop using this
72
- # endpoint
73
- def modified_lines(file)
74
- modified_lines_in_file(file)
93
+ # Include files modified in last commit if amending
94
+ if amendment?
95
+ subcmd = 'show --format=%n'
96
+ @modified_files += Overcommit::GitRepo.modified_files(subcmd: subcmd)
97
+ end
98
+ end
99
+ @modified_files
75
100
  end
76
101
 
77
102
  # Returns the set of line numbers corresponding to the lines that were
78
103
  # changed in a specified file.
79
104
  def modified_lines_in_file(file)
80
105
  @modified_lines ||= {}
81
- @modified_lines[file] ||=
82
- Overcommit::GitRepo.extract_modified_lines(file, staged: true)
106
+ unless @modified_lines[file]
107
+ @modified_lines[file] =
108
+ Overcommit::GitRepo.extract_modified_lines(file, staged: true)
109
+
110
+ # Include lines modified in last commit if amending
111
+ if amendment?
112
+ subcmd = 'show --format=%n'
113
+ @modified_lines[file] +=
114
+ Overcommit::GitRepo.extract_modified_lines(file, subcmd: subcmd)
115
+ end
116
+ end
117
+ @modified_lines[file]
83
118
  end
84
119
 
85
120
  private
86
121
 
87
122
  # Clears the working tree so that the stash can be applied.
88
123
  def clear_working_tree
124
+ removed_submodules = Overcommit::GitRepo.staged_submodule_removals
125
+
89
126
  result = Overcommit::Utils.execute(%w[git reset --hard])
90
127
  unless result.success?
91
128
  raise Overcommit::Exceptions::HookCleanupFailed,
92
129
  "Unable to cleanup working tree after #{hook_script_name} hooks run:" \
93
130
  "\nSTDOUT:#{result.stdout}\nSTDERR:#{result.stderr}"
94
131
  end
132
+
133
+ # Hard-resetting a staged submodule removal results in the index being
134
+ # reset but the submodule being restored as an empty directory. This empty
135
+ # directory prevents us from stashing on a subsequent run if a hook fails.
136
+ #
137
+ # Work around this by removing these empty submodule directories as there
138
+ # doesn't appear any reason to keep them around.
139
+ removed_submodules.each do |submodule|
140
+ FileUtils.rmdir(submodule.path)
141
+ end
95
142
  end
96
143
 
97
144
  # Applies the stash to the working tree to restore the user's state.
@@ -128,8 +175,12 @@ module Overcommit::HookContext
128
175
  def store_modified_times
129
176
  @modified_times = {}
130
177
 
131
- modified_files.each do |file|
178
+ staged_files = modified_files
179
+ unstaged_files = Overcommit::GitRepo.modified_files(staged: false)
180
+
181
+ (staged_files + unstaged_files).each do |file|
132
182
  next if Overcommit::Utils.broken_symlink?(file)
183
+ next unless File.exist?(file) # Ignore renamed files (old file no longer exists)
133
184
  @modified_times[file] = File.mtime(file)
134
185
  end
135
186
  end
@@ -137,10 +188,9 @@ module Overcommit::HookContext
137
188
  # Restores the file modification times for all modified files to make it
138
189
  # appear like they never changed.
139
190
  def restore_modified_times
140
- modified_files.each do |file|
191
+ @modified_times.each do |file, time|
141
192
  next if Overcommit::Utils.broken_symlink?(file)
142
193
  next unless File.exist?(file)
143
- time = @modified_times[file]
144
194
  File.utime(time, time, file)
145
195
  end
146
196
  end