overcommit 0.5.0 → 0.6.0

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