overcommit 0.23.0 → 0.24.0

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