overcommit 0.5.0 → 0.6.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 (92) hide show
  1. checksums.yaml +4 -4
  2. data/bin/overcommit +3 -4
  3. data/config/default.yml +139 -0
  4. data/lib/overcommit.rb +7 -5
  5. data/lib/overcommit/cli.rb +59 -64
  6. data/lib/overcommit/configuration.rb +108 -34
  7. data/lib/overcommit/configuration_loader.rb +47 -0
  8. data/lib/overcommit/constants.rb +7 -0
  9. data/lib/overcommit/exceptions.rb +16 -0
  10. data/lib/overcommit/hook/base.rb +91 -0
  11. data/lib/overcommit/hook/commit_msg/base.rb +11 -0
  12. data/lib/overcommit/hook/commit_msg/gerrit_change_id.rb +18 -0
  13. data/lib/overcommit/{plugins → hook}/commit_msg/hard_tabs.rb +5 -6
  14. data/lib/overcommit/hook/commit_msg/russian_novel.rb +14 -0
  15. data/lib/overcommit/hook/commit_msg/single_line_subject.rb +12 -0
  16. data/lib/overcommit/hook/commit_msg/text_width.rb +20 -0
  17. data/lib/overcommit/hook/commit_msg/trailing_period.rb +12 -0
  18. data/lib/overcommit/hook/post_checkout/base.rb +11 -0
  19. data/lib/overcommit/hook/post_checkout/bundle_check.rb +29 -0
  20. data/lib/overcommit/hook/post_checkout/index_tags.rb +24 -0
  21. data/lib/overcommit/hook/pre_commit/author_email.rb +17 -0
  22. data/lib/overcommit/hook/pre_commit/author_name.rb +17 -0
  23. data/lib/overcommit/hook/pre_commit/base.rb +10 -0
  24. data/lib/overcommit/hook/pre_commit/bundle_check.rb +30 -0
  25. data/lib/overcommit/hook/pre_commit/coffee_lint.rb +14 -0
  26. data/lib/overcommit/hook/pre_commit/css_lint.rb +16 -0
  27. data/lib/overcommit/hook/pre_commit/haml_lint.rb +26 -0
  28. data/lib/overcommit/hook/pre_commit/hard_tabs.rb +16 -0
  29. data/lib/overcommit/hook/pre_commit/image_optim.rb +41 -0
  30. data/lib/overcommit/hook/pre_commit/js_hint.rb +15 -0
  31. data/lib/overcommit/hook/pre_commit/jscs.rb +31 -0
  32. data/lib/overcommit/hook/pre_commit/python_flake8.rb +14 -0
  33. data/lib/overcommit/hook/pre_commit/rubocop.rb +26 -0
  34. data/lib/overcommit/hook/pre_commit/scss_lint.rb +26 -0
  35. data/lib/overcommit/hook/pre_commit/trailing_whitespace.rb +15 -0
  36. data/lib/overcommit/hook/pre_commit/yaml_syntax.rb +22 -0
  37. data/lib/overcommit/hook_context.rb +16 -0
  38. data/lib/overcommit/hook_context/base.rb +68 -0
  39. data/lib/overcommit/hook_context/commit_msg.rb +32 -0
  40. data/lib/overcommit/hook_context/post_checkout.rb +24 -0
  41. data/lib/overcommit/hook_context/pre_commit.rb +96 -0
  42. data/lib/overcommit/hook_runner.rb +150 -0
  43. data/lib/overcommit/installer.rb +61 -68
  44. data/lib/overcommit/logger.rb +16 -13
  45. data/lib/overcommit/utils.rb +63 -38
  46. data/lib/overcommit/version.rb +1 -1
  47. data/{bin/scripts → libexec}/gerrit-change-id +0 -0
  48. data/{bin/scripts → libexec}/index-tags +1 -3
  49. data/template-dir/hooks/commit-msg +83 -0
  50. data/template-dir/hooks/overcommit-hook +83 -0
  51. data/template-dir/hooks/post-checkout +83 -0
  52. data/template-dir/hooks/pre-commit +83 -0
  53. metadata +76 -57
  54. data/bin/hooks/commit-msg +0 -8
  55. data/bin/hooks/post-checkout +0 -9
  56. data/bin/hooks/post-merge +0 -23
  57. data/bin/hooks/pre-commit +0 -8
  58. data/bin/hooks/prepare-commit-msg +0 -159
  59. data/bin/run-hook +0 -8
  60. data/bin/scripts/check-gemfile +0 -9
  61. data/bin/scripts/csslint-rhino.js +0 -9080
  62. data/bin/scripts/jshint.js +0 -5921
  63. data/bin/scripts/jshint_runner.js +0 -42
  64. data/lib/overcommit/errors.rb +0 -3
  65. data/lib/overcommit/git_hook.rb +0 -89
  66. data/lib/overcommit/hook_specific_check.rb +0 -110
  67. data/lib/overcommit/hooks/commit_msg.rb +0 -7
  68. data/lib/overcommit/hooks/pre_commit.rb +0 -9
  69. data/lib/overcommit/plugins/commit_msg/change_id.rb +0 -15
  70. data/lib/overcommit/plugins/commit_msg/release_note.rb +0 -25
  71. data/lib/overcommit/plugins/commit_msg/russian_novel.rb +0 -16
  72. data/lib/overcommit/plugins/commit_msg/single_line_subject.rb +0 -13
  73. data/lib/overcommit/plugins/commit_msg/text_width.rb +0 -20
  74. data/lib/overcommit/plugins/commit_msg/trailing_period.rb +0 -13
  75. data/lib/overcommit/plugins/pre_commit/author_name.rb +0 -16
  76. data/lib/overcommit/plugins/pre_commit/causes_email.rb +0 -15
  77. data/lib/overcommit/plugins/pre_commit/coffee_lint.rb +0 -16
  78. data/lib/overcommit/plugins/pre_commit/css_linter.rb +0 -17
  79. data/lib/overcommit/plugins/pre_commit/haml_style.rb +0 -34
  80. data/lib/overcommit/plugins/pre_commit/haml_syntax.rb +0 -24
  81. data/lib/overcommit/plugins/pre_commit/image_optimization.rb +0 -50
  82. data/lib/overcommit/plugins/pre_commit/js_console_log.rb +0 -16
  83. data/lib/overcommit/plugins/pre_commit/js_syntax.rb +0 -30
  84. data/lib/overcommit/plugins/pre_commit/python_flake8.rb +0 -15
  85. data/lib/overcommit/plugins/pre_commit/ruby_style.rb +0 -67
  86. data/lib/overcommit/plugins/pre_commit/ruby_syntax.rb +0 -19
  87. data/lib/overcommit/plugins/pre_commit/scss_lint.rb +0 -66
  88. data/lib/overcommit/plugins/pre_commit/test_history.rb +0 -58
  89. data/lib/overcommit/plugins/pre_commit/whitespace.rb +0 -21
  90. data/lib/overcommit/plugins/pre_commit/yaml_syntax.rb +0 -22
  91. data/lib/overcommit/reporter.rb +0 -90
  92. data/lib/overcommit/staged_file.rb +0 -86
@@ -0,0 +1,150 @@
1
+ # encoding: utf-8
2
+
3
+ module Overcommit
4
+ # Responsible for loading the hooks the repository has configured and running
5
+ # them, collecting and displaying the results.
6
+ class HookRunner
7
+ def initialize(config, logger, context)
8
+ @config = config
9
+ @log = logger
10
+ @context = context
11
+ @hooks = []
12
+ end
13
+
14
+ # Loads and runs the hooks registered for this {HookRunner}.
15
+ def run
16
+ load_hooks
17
+ @context.setup_environment
18
+ run_hooks
19
+ ensure
20
+ @context.cleanup_environment
21
+ end
22
+
23
+ private
24
+
25
+ attr_reader :log
26
+
27
+ def run_hooks
28
+ if @hooks.any? { |hook| hook.run? || hook.skip? }
29
+ log.bold "Running #{@context.hook_script_name} hooks"
30
+
31
+ statuses = @hooks.map { |hook| run_hook(hook) }.compact
32
+
33
+ log.log # Newline
34
+
35
+ run_failed = statuses.include?(:bad)
36
+
37
+ if run_failed
38
+ log.error "✗ One or more #{@context.hook_script_name} hooks failed"
39
+ else
40
+ log.success "✓ All #{@context.hook_script_name} hooks passed"
41
+ end
42
+
43
+ log.log # Newline
44
+
45
+ !run_failed
46
+ else
47
+ log.success "✓ No applicable #{@context.hook_script_name} hooks to run"
48
+ true # Run was successful
49
+ end
50
+ end
51
+
52
+ def run_hook(hook)
53
+ return unless hook.enabled?
54
+
55
+ if hook.skip?
56
+ if hook.required?
57
+ log.warning "Cannot skip #{hook.name} since it is required"
58
+ else
59
+ log.warning "Skipping #{hook.name}"
60
+ return
61
+ end
62
+ end
63
+
64
+ return unless hook.run?
65
+
66
+ unless hook.quiet?
67
+ print_header(hook)
68
+ end
69
+
70
+ begin
71
+ status, output = hook.run
72
+ rescue => ex
73
+ status = :bad
74
+ output = "Hook raised unexpected error\n#{ex.message}"
75
+ end
76
+
77
+ # Want to print the header in the event the result wasn't good so that the
78
+ # user knows what failed
79
+ if hook.quiet? && status != :good
80
+ print_header(hook)
81
+ end
82
+
83
+ case status
84
+ when :good
85
+ log.success 'OK' unless hook.quiet?
86
+ when :warn
87
+ log.warning 'WARNING'
88
+ print_report(output, :bold_warning)
89
+ when :bad
90
+ log.error 'FAILED'
91
+ print_report(output, :bold_error)
92
+ end
93
+
94
+ status
95
+ end
96
+
97
+ def print_header(hook)
98
+ log.partial hook.description
99
+ log.partial '.' * (70 - hook.description.length)
100
+ end
101
+
102
+ def print_report(output, format = :log)
103
+ log.send(format, output) unless output.empty?
104
+ end
105
+
106
+ # Loads hooks that will be run.
107
+ # This is done explicitly so that we only load hooks which will actually be
108
+ # used.
109
+ def load_hooks
110
+ require "overcommit/hook/#{@context.hook_type_name}/base"
111
+
112
+ load_builtin_hooks
113
+ load_hook_plugins # Load after so they can subclass/modify existing hooks
114
+ end
115
+
116
+ # Load hooks that ship with Overcommit, ignoring ones that are excluded from
117
+ # the repository's configuration.
118
+ def load_builtin_hooks
119
+ @config.enabled_builtin_hooks(@context.hook_class_name).each do |hook_name|
120
+ underscored_hook_name = Overcommit::Utils.snake_case(hook_name)
121
+ require "overcommit/hook/#{@context.hook_type_name}/#{underscored_hook_name}"
122
+ @hooks << create_hook(hook_name)
123
+ end
124
+ end
125
+
126
+ # Load hooks that are stored in the repository's plugin directory.
127
+ def load_hook_plugins
128
+ directory = File.join(@config.plugin_directory, @context.hook_type_name)
129
+
130
+ Dir[File.join(directory, '*.rb')].sort.each do |plugin|
131
+ require plugin
132
+
133
+ hook_name = Overcommit::Utils.camel_case(File.basename(plugin, '.rb'))
134
+ @hooks << create_hook(hook_name)
135
+ end
136
+ end
137
+
138
+ # Load and return a {Hook} from a CamelCase hook name and the given
139
+ # hook configuration.
140
+ def create_hook(hook_name)
141
+ Overcommit::Hook.const_get(@context.hook_class_name).
142
+ const_get(hook_name).
143
+ new(@config, @context)
144
+ rescue LoadError, NameError => error
145
+ raise Overcommit::Exceptions::HookLoadError,
146
+ "Unable to load hook '#{hook_name}': #{error}",
147
+ error.backtrace
148
+ end
149
+ end
150
+ end
@@ -1,113 +1,106 @@
1
1
  require 'fileutils'
2
- require 'yaml'
3
2
 
4
3
  module Overcommit
4
+ # Manages the installation of Overcommit hooks in a git repository.
5
5
  class Installer
6
- def initialize(options, target)
7
- @options = options
8
- @target = target
6
+ def initialize(logger)
7
+ @log = logger
9
8
  end
10
9
 
11
- def run
10
+ def run(target, options)
11
+ @target = target
12
+ @options = options
12
13
  validate_target
13
- @options[:uninstall] ? uninstall : install
14
+ @options[:action] == :uninstall ? uninstall : install
14
15
  end
15
16
 
17
+ private
18
+
19
+ attr_reader :log
20
+
16
21
  def install
17
22
  log.log "Installing hooks into #{@target}"
18
23
 
19
- install_scripts
20
- install_hooks
21
- write_configuration
24
+ ensure_hooks_directory
25
+ install_master_hook
26
+ install_hook_symlinks
27
+
28
+ log.success "Successfully installed hooks into #{@target}"
22
29
  end
23
30
 
24
31
  def uninstall
25
32
  log.log "Removing hooks from #{@target}"
26
33
 
27
- uninstall_scripts
28
- uninstall_hooks
29
- rm_configuration
30
- end
34
+ uninstall_master_hook
35
+ uninstall_hook_symlinks
31
36
 
32
- private
37
+ log.success "Successfully removed hooks from #{@target}"
38
+ end
33
39
 
34
- def log
35
- Logger.instance
40
+ def hooks_path
41
+ absolute_target = File.expand_path(@target)
42
+ File.join(absolute_target, '.git', 'hooks')
36
43
  end
37
44
 
38
- def hook_path
39
- absolute_target = File.expand_path @target
40
- File.join(absolute_target, '.git/hooks')
45
+ def ensure_hooks_directory
46
+ FileUtils.mkdir_p(hooks_path)
41
47
  end
42
48
 
43
49
  def validate_target
44
- absolute_target = File.expand_path @target
45
- unless File.directory? absolute_target
46
- raise NotAGitRepoError, 'is not a directory'
50
+ absolute_target = File.expand_path(@target)
51
+
52
+ unless File.directory?(absolute_target)
53
+ raise Overcommit::Exceptions::InvalidGitRepo, 'is not a directory'
47
54
  end
48
55
 
49
56
  unless File.directory?(File.join(absolute_target, '.git'))
50
- raise NotAGitRepoError, 'does not appear to be a git repository'
57
+ raise Overcommit::Exceptions::InvalidGitRepo, 'does not appear to be a git repository'
51
58
  end
52
59
  end
53
60
 
54
- # Make helper scripts available locally inside the repo
55
- def install_scripts
56
- FileUtils.cp_r Utils.absolute_path('bin/scripts'), hook_path
61
+ def install_master_hook
62
+ master_hook = File.join(OVERCOMMIT_HOME, 'template-dir', 'hooks', 'overcommit-hook')
63
+ install_location = File.join(hooks_path, 'overcommit-hook')
64
+ FileUtils.mkdir_p(hooks_path)
65
+ FileUtils.cp(master_hook, install_location)
57
66
  end
58
67
 
59
- # Install all available git hooks into the repo
60
- def install_hooks
61
- hooks.each do |hook|
62
- FileUtils.cp hook, File.join(hook_path, File.basename(hook))
63
- end
68
+ def uninstall_master_hook
69
+ install_location = File.join(hooks_path, 'overcommit-hook')
70
+ FileUtils.rm_rf(install_location)
64
71
  end
65
72
 
66
- def uninstall_hooks
67
- hooks.each do |hook|
68
- delete File.join(hook_path, File.basename(hook))
73
+ def install_hook_symlinks
74
+ # Link each hook type (pre-commit, commit-msg, etc.) to the master hook.
75
+ # We change directories so that the relative symlink paths work regardless
76
+ # of where the repository is located.
77
+ Dir.chdir(hooks_path) do
78
+ Overcommit::Utils.supported_hook_types.each do |hook_type|
79
+ unless can_replace_file?(hook_type)
80
+ raise Overcommit::Exceptions::PreExistingHooks,
81
+ "Hook '#{File.expand_path(hook_type)}' already exists and was not installed by Overcommit"
82
+ end
83
+ FileUtils.ln_sf('overcommit-hook', hook_type)
84
+ end
69
85
  end
70
86
  end
71
87
 
72
- def uninstall_scripts
73
- scripts = File.join(hook_path, 'scripts')
74
- FileUtils.rm_r scripts rescue false
88
+ def can_replace_file?(file)
89
+ @options[:force] ||
90
+ !File.exists?(file) ||
91
+ overcommit_symlink?(file)
75
92
  end
76
93
 
77
- def hooks
78
- Dir[Utils.absolute_path('bin/hooks/*')]
79
- end
80
-
81
- # Dump a YAML document containing requested configuration
82
- def write_configuration
83
- template = @options.fetch(:template, 'default')
84
- base_config = Overcommit.config.templates[template]
85
- if base_config.nil?
86
- raise ArgumentError, "No such template '#{template}'"
87
- end
88
-
89
- base_config = base_config.dup
90
- (base_config['excludes'] ||= {}).
91
- merge!(@options[:excludes] || {}) do |_, a, b|
92
- # Concat the arrays together
93
- a + b
94
- end
95
-
96
- File.open(configuration_location, 'w') do |config|
97
- YAML.dump(base_config, config)
94
+ def uninstall_hook_symlinks
95
+ Dir.chdir(hooks_path) do
96
+ Overcommit::Utils.supported_hook_types.each do |hook_type|
97
+ FileUtils.rm_rf(hook_type) if overcommit_symlink?(hook_type)
98
+ end
98
99
  end
99
100
  end
100
101
 
101
- def rm_configuration
102
- delete configuration_location
103
- end
104
-
105
- def configuration_location
106
- File.join(hook_path, 'overcommit.yml')
107
- end
108
-
109
- def delete(file)
110
- File.delete file rescue false
102
+ def overcommit_symlink?(file)
103
+ File.symlink?(file) && File.readlink(file) == 'overcommit-hook'
111
104
  end
112
105
  end
113
106
  end
@@ -1,18 +1,21 @@
1
- require 'singleton'
2
-
3
- # This class centralizes all communication to STDOUT
4
1
  module Overcommit
2
+ # Encapsulates all communication to an output source.
5
3
  class Logger
6
- include Singleton
4
+ # Helper for creating a logger which outputs nothing.
5
+ def self.silent
6
+ new(File.open('/dev/null', 'w'))
7
+ end
7
8
 
8
- attr_accessor :output
9
+ def initialize(out)
10
+ @out = out
11
+ end
9
12
 
10
13
  def partial(*args)
11
- out.print *args
14
+ @out.print(*args)
12
15
  end
13
16
 
14
17
  def log(*args)
15
- out.puts *args
18
+ @out.puts(*args)
16
19
  end
17
20
 
18
21
  def bold(str)
@@ -23,6 +26,10 @@ module Overcommit
23
26
  color(31, str)
24
27
  end
25
28
 
29
+ def bold_error(str)
30
+ color('1;31', str)
31
+ end
32
+
26
33
  def success(str)
27
34
  color(32, str)
28
35
  end
@@ -31,18 +38,14 @@ module Overcommit
31
38
  color(33, str)
32
39
  end
33
40
 
34
- def notice(str)
41
+ def bold_warning(str)
35
42
  color('1;33', str)
36
43
  end
37
44
 
38
- def out
39
- self.output ||= $stdout
40
- end
41
-
42
45
  private
43
46
 
44
47
  def color(code, str)
45
- log(out.isatty ? "\033[#{code}m#{str}\033[0m" : str)
48
+ log(@out.tty? ? "\033[#{code}m#{str}\033[0m" : str)
46
49
  end
47
50
  end
48
51
  end
@@ -1,62 +1,87 @@
1
+ require 'wopen3'
2
+
1
3
  module Overcommit
4
+ # Utility functions for general use.
2
5
  module Utils
3
6
  class << self
4
- @@hooks = []
5
-
6
- def register_hook(hook)
7
- @@hooks << hook
8
- end
9
-
10
- def run_hooks(*args)
11
- @@hooks.each { |hook| hook.new.run(*args) }
7
+ def script_path(script)
8
+ File.join(OVERCOMMIT_HOME, 'libexec', script)
12
9
  end
13
10
 
14
- def hook_name
15
- File.basename($0).tr('-', '_')
11
+ # Returns an absolute path to the root of the repository.
12
+ def repo_root
13
+ @repo_root ||=
14
+ begin
15
+ result = `git rev-parse --show-toplevel`.chomp
16
+ result if $?.success?
17
+ end
16
18
  end
17
19
 
18
- def load_hooks
19
- require File.expand_path("../hooks/#{hook_name}", __FILE__)
20
- rescue LoadError
21
- log.error "No hook definition found for #{hook_name}"
22
- exit 1
20
+ # Shamelessly stolen from:
21
+ # stackoverflow.com/questions/1509915/converting-camel-case-to-underscore-case-in-ruby
22
+ def snake_case(str)
23
+ str.gsub(/::/, '/').
24
+ gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2').
25
+ gsub(/([a-z\d])([A-Z])/, '\1_\2').
26
+ tr('-', '_').
27
+ downcase
23
28
  end
24
29
 
25
- def script_path(script)
26
- File.join(File.expand_path('../../hooks/scripts', $0), script)
30
+ # Converts a string containing underscores/hyphens/spaces into CamelCase.
31
+ def camel_case(str)
32
+ str.split(/_|-| /).map { |part| part.sub(/^\w/) { |c| c.upcase } }.join
27
33
  end
28
34
 
29
- def absolute_path(path)
30
- File.join(File.expand_path('../../..', __FILE__), path)
35
+ # Returns a list of supported hook types (pre-commit, commit-msg, etc.)
36
+ def supported_hook_types
37
+ Dir[File.join(OVERCOMMIT_HOME, 'lib', 'overcommit', 'hook', '*')].
38
+ select { |file| File.directory?(file) }.
39
+ map { |file| File.basename(file, '.rb').gsub('_', '-') }
31
40
  end
32
41
 
33
- # File.expand_path takes one more '..' than you're used to... we want to
34
- # go two directories up from the caller (which will be .git/hooks/something)
35
- # to the root of the git repo.
36
- def repo_path(path)
37
- File.join(File.expand_path('../../..', $0), path)
42
+ # Returns a list of supported hook classes (PreCommit, CommitMsg, etc.)
43
+ def supported_hook_type_classes
44
+ supported_hook_types.map do |file|
45
+ file.split('-').map { |part| part.capitalize }.join
46
+ end
38
47
  end
39
48
 
40
- # Shamelessly stolen from:
41
- # http://stackoverflow.com/questions/1509915/converting-camel-case-to-underscore-case-in-ruby
42
- def underscorize(str)
43
- str.gsub(/::/, '/').
44
- gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2').
45
- gsub(/([a-z\d])([A-Z])/, '\1_\2').
46
- tr('-', '_').
47
- downcase
49
+ # Returns whether a command can be found given the current environment path.
50
+ def in_path?(cmd)
51
+ exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']
52
+ ENV['PATH'].split(File::PATH_SEPARATOR).each do |path|
53
+ exts.each do |ext|
54
+ exe = File.join(path, "#{cmd}#{ext}")
55
+ return true if File.executable?(exe)
56
+ end
57
+ end
58
+ false
48
59
  end
49
60
 
50
- # Get a list of staged Added, Copied, or Modified files (ignore renames
51
- # and deletions, since there should be nothing to check).
52
- def modified_files
53
- `git diff --cached --name-only --diff-filter=ACM --ignore-submodules=all`.split "\n"
61
+ # Wrap external subshell calls. This is necessary in order to allow
62
+ # Overcommit to call other Ruby executables without requiring that they be
63
+ # specified in Overcommit's Gemfile--a nasty consequence of using
64
+ # `bundle exec overcommit` while developing locally.
65
+ def command(command)
66
+ with_environment 'RUBYOPT' => nil do
67
+ Wopen3.system(command)
68
+ end
54
69
  end
55
70
 
56
71
  private
57
72
 
58
- def log
59
- Logger.instance
73
+ # Calls a block of code with a modified set of environment variables,
74
+ # restoring them once the code has executed.
75
+ def with_environment(env, &block)
76
+ old_env = {}
77
+ env.each do |var, value|
78
+ old_env[var] = ENV[var.to_s]
79
+ ENV[var.to_s] = value
80
+ end
81
+
82
+ yield
83
+ ensure
84
+ old_env.each { |var, value| ENV[var.to_s] = value }
60
85
  end
61
86
  end
62
87
  end