rabbitt-githooks 1.3.0 → 1.3.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +5 -13
  2. data/Gemfile.lock +29 -19
  3. data/Rakefile +20 -0
  4. data/bin/githooks +2 -1
  5. data/bin/githooks-runner +10 -2
  6. data/lib/githooks.rb +11 -7
  7. data/lib/githooks/action.rb +67 -53
  8. data/lib/githooks/cli.rb +9 -6
  9. data/lib/githooks/commands/config.rb +26 -19
  10. data/lib/githooks/core_ext.rb +2 -1
  11. data/lib/githooks/core_ext/array/extract_options.rb +5 -1
  12. data/lib/githooks/core_ext/array/min_max.rb +4 -4
  13. data/lib/githooks/core_ext/array/select_with_index.rb +1 -1
  14. data/lib/githooks/core_ext/colorize.rb +30 -0
  15. data/lib/githooks/core_ext/object.rb +8 -0
  16. data/lib/githooks/core_ext/pathname.rb +7 -6
  17. data/lib/githooks/core_ext/string.rb +1 -1
  18. data/lib/githooks/core_ext/string/sanitize.rb +51 -0
  19. data/lib/githooks/error.rb +1 -0
  20. data/lib/githooks/hook.rb +21 -26
  21. data/lib/githooks/repository.rb +31 -35
  22. data/lib/githooks/repository/config.rb +16 -24
  23. data/lib/githooks/repository/diff_index_entry.rb +3 -2
  24. data/lib/githooks/repository/file.rb +14 -7
  25. data/lib/githooks/runner.rb +38 -36
  26. data/lib/githooks/section.rb +13 -19
  27. data/lib/githooks/system_utils.rb +132 -47
  28. data/lib/githooks/version.rb +1 -1
  29. data/lib/tasks/dev.task +14 -0
  30. data/rabbitt-githooks.gemspec +18 -15
  31. metadata +58 -33
  32. data/.gitignore +0 -34
  33. data/.hooks/commit-messages.rb +0 -29
  34. data/.hooks/formatting.rb +0 -44
  35. data/.rubocop.yml +0 -87
  36. data/lib/githooks/core_ext/process.rb +0 -9
  37. data/lib/githooks/core_ext/string/strip_empty_lines.rb +0 -9
  38. data/lib/githooks/terminal_colors.rb +0 -62
  39. data/thoughts.txt +0 -56
@@ -16,8 +16,9 @@ with this program; if not, write to the Free Software Foundation, Inc.,
16
16
  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17
17
  =end
18
18
 
19
+ require_relative 'core_ext/object'
19
20
  require_relative 'core_ext/array'
21
+ require_relative 'core_ext/colorize'
20
22
  require_relative 'core_ext/numbers'
21
23
  require_relative 'core_ext/string'
22
24
  require_relative 'core_ext/pathname'
23
- require_relative 'core_ext/process'
@@ -1,5 +1,9 @@
1
1
  class Array
2
- def extract_options
2
+ def extract_options!
3
3
  last.is_a?(Hash) ? pop : {}
4
4
  end
5
+
6
+ def extract_options
7
+ last.is_a?(Hash) ? last : {}
8
+ end
5
9
  end
@@ -19,17 +19,17 @@ with this program; if not, write to the Free Software Foundation, Inc.,
19
19
  require_relative '../numbers/infinity'
20
20
 
21
21
  class Array
22
- def min(&block)
22
+ def minimum(&_block)
23
23
  collection = block_given? ? collect { |obj| yield obj } : self
24
- collection.inject(Infinity) do |min, num|
24
+ collection.inject(Infinity) do |min, num| # rubocop:disable Style/EachWithObject
25
25
  min = num < min ? num : min
26
26
  min
27
27
  end
28
28
  end
29
29
 
30
- def max(&block)
30
+ def maximum(&_block)
31
31
  collection = block_given? ? collect { |obj| yield obj } : self
32
- collection.inject(0) do |max, num|
32
+ collection.inject(0) do |max, num| # rubocop:disable Style/EachWithObject
33
33
  max = num > max ? num : max
34
34
  max
35
35
  end
@@ -5,7 +5,7 @@ class Array
5
5
  if regexp.is_a? Regexp
6
6
  collection << [index, node] if node =~ regexp
7
7
  elsif block_given?
8
- collection << [index, node] if yield(node)
8
+ collection << [index, node] if block.call(node)
9
9
  end
10
10
  end
11
11
  end
@@ -0,0 +1,30 @@
1
+ require 'colorize'
2
+
3
+ module Colorize
4
+ module ClassMethods
5
+ unless respond_to? :disable_colorization_without_tty_detection
6
+ alias_method :disable_colorization_without_tty_detection, :disable_colorization
7
+ end
8
+
9
+ def disable_colorization(value = nil)
10
+ # disable colorization when we don't have a tty on STDOUT
11
+ return true unless value || STDOUT.tty?
12
+ disable_colorization_without_tty_detection(value)
13
+ end
14
+ end
15
+
16
+ module InstanceMethods
17
+ def success!
18
+ light_green
19
+ end
20
+
21
+ def failure!
22
+ light_red
23
+ end
24
+
25
+ def unknown!
26
+ light_yellow
27
+ end
28
+ alias_method :warning!, :unknown!
29
+ end
30
+ end
@@ -0,0 +1,8 @@
1
+ class Object
2
+ def deep_dup
3
+ Marshal.load(Marshal.dump(self))
4
+ rescue TypeError => e
5
+ raise e unless e.message.include? "can't dump"
6
+ dup
7
+ end
8
+ end
@@ -3,10 +3,9 @@ require 'pathname'
3
3
  if RUBY_ENGINE == 'jruby'
4
4
  class Pathname
5
5
  def realpath(basedir = nil)
6
- unless (path = java_realpath(basedir)).exist?
7
- fail Errno::ENOENT, path.to_s
6
+ java_realpath(basedir).tap do |path|
7
+ fail Errno::ENOENT, path.to_s unless path.exist?
8
8
  end
9
- path
10
9
  end
11
10
 
12
11
  def realdirpath(basedir = nil)
@@ -14,11 +13,13 @@ if RUBY_ENGINE == 'jruby'
14
13
  end
15
14
 
16
15
  def java_realpath(basedir = nil)
17
- if basedir && !@path.start_with?('/')
18
- path = self.class.new(basedir).realpath + @path
16
+ # rubocop:disable ElseAlignment, EndAlignment
17
+ path = if basedir && @path[0] != '/'
18
+ Pathname.new(basedir).realpath.join(@path)
19
19
  else
20
- path = @path.to_s
20
+ @path.to_s
21
21
  end
22
+ # rubocop:enable ElseAlignment, EndAlignment
22
23
 
23
24
  self.class.new java.io.File.new(path.to_s).canonical_path
24
25
  end
@@ -1,3 +1,3 @@
1
1
  require_relative 'string/git_option_path_split'
2
2
  require_relative 'string/inflections'
3
- require_relative 'string/strip_empty_lines'
3
+ require_relative 'string/sanitize'
@@ -0,0 +1,51 @@
1
+ require_relative '../array/extract_options'
2
+
3
+ class String
4
+ def strip_empty_lines!
5
+ replace(strip_empty_lines)
6
+ end
7
+
8
+ def strip_empty_lines
9
+ split(/\n/).reject { |s| s !~ /\S/ }.join("\n")
10
+ end
11
+
12
+ def strip_non_printable!
13
+ replace(strip_non_printable)
14
+ end
15
+
16
+ def strip_non_printable
17
+ gsub(/[^[:print:] \n\t\x1b]/, '')
18
+ end
19
+
20
+ def strip_colors!
21
+ replace(strip_colors)
22
+ end
23
+
24
+ def strip_colors
25
+ gsub(/\x1b\[\d+(?:;\d+)?m/, '')
26
+ end
27
+
28
+ def sanitize!(*methods)
29
+ options = methods.extract_options!
30
+
31
+ map = {
32
+ strip: :strip!,
33
+ empty_lines: :strip_empty_lines!,
34
+ non_printable: :strip_non_printable!,
35
+ colors: :strip_colors!
36
+ }
37
+
38
+ methods = map.keys if methods.empty? || methods.include?(:all)
39
+ methods -= Array(options.delete(:except)) if options[:except]
40
+
41
+ methods.collect(&:to_sym).each do |method|
42
+ send(map[method]) if map[method]
43
+ end
44
+
45
+ self
46
+ end
47
+
48
+ def sanitize(*methods)
49
+ dup.sanitize!(*methods)
50
+ end
51
+ end
@@ -1,5 +1,6 @@
1
1
  module GitHooks
2
2
  class Error < StandardError
3
+ class CommandExecutionFailure < GitHooks::Error; end
3
4
  class NotAGitRepo < GitHooks::Error; end
4
5
  class Registration < GitHooks::Error; end
5
6
  class TestsFailed < GitHooks::Error; end
data/lib/githooks/hook.rb CHANGED
@@ -17,6 +17,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
17
17
  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18
18
  =end
19
19
 
20
+ require_relative 'repository'
20
21
  require_relative 'system_utils'
21
22
 
22
23
  module GitHooks
@@ -27,24 +28,22 @@ module GitHooks
27
28
  @__mutex__ = Mutex.new
28
29
 
29
30
  class << self
30
- def instances # rubocop:disable TrivialAccessors
31
- @__phases__
32
- end
33
- alias_method :phases, :instances
31
+ attr_reader :__phases__
32
+ alias_method :phases, :__phases__
34
33
 
35
- def instance(phase = 'pre-commit')
34
+ def instance(phase = 'pre-commit') # rubocop:disable AbcSize
36
35
  phase = phase.to_s
37
36
  unless VALID_PHASES.include? phase
38
- valid_phases = VALID_PHASES.collect(&:inspect).join(', ')
39
- fail ArgumentError, "Hook phase (#{phase.inspect}) must be one of #{valid_phases}"
37
+ fail ArgumentError, "Hook phase (#{phase}) must be one of #{VALID_PHASES.join(', ')}"
40
38
  end
41
39
 
42
- return phases[phase] if phases[phase]
43
-
44
- @__mutex__.synchronize do
45
- return phases[phase] if phases[phase]
46
- phases[phase] = new(phase)
40
+ unless phases[phase]
41
+ @__mutex__.synchronize {
42
+ return phases[phase] if phases[phase]
43
+ phases[phase] = new(phase)
44
+ }
47
45
  end
46
+ phases[phase]
48
47
  end
49
48
  private :instance
50
49
 
@@ -57,8 +56,8 @@ module GitHooks
57
56
  end
58
57
 
59
58
  def register(phase, &block)
60
- fail ArgumentError, 'Missing required block to #register' unless block_given?
61
- self[phase].instance_eval(&block)
59
+ fail ArgumentError, 'expected block, received none' unless block_given?
60
+ instance(phase).instance_eval(&block)
62
61
  end
63
62
  end
64
63
 
@@ -66,7 +65,7 @@ module GitHooks
66
65
  attr_accessor :args, :staged, :untracked, :tracked
67
66
 
68
67
  def initialize(phase)
69
- @phase = phase
68
+ @phase = phase.to_s
70
69
  @sections = {}
71
70
  @commands = []
72
71
  @args = []
@@ -85,35 +84,31 @@ module GitHooks
85
84
  @repository = Repository.new(path)
86
85
  end
87
86
 
88
- def manifest(options = {})
87
+ def manifest
89
88
  @manifest ||= Manifest.new(self)
90
89
  end
91
90
 
92
91
  def run
93
92
  # only run sections that have actions matching files in the manifest
94
- runable_sections = sections.select { |section| !section.actions.empty? }
95
- runable_sections.collect { |section| section.run }.all?
93
+ sections.reject { |s| s.actions.empty? }.collect(&:run).all?
96
94
  end
97
95
 
98
96
  def method_missing(method, *args, &block)
99
- command = find_command(method)
100
- return super unless command
97
+ return super unless command = find_command(method) # rubocop:disable AssignmentInCondition
101
98
  command.execute(*args, &block)
102
99
  end
103
100
 
104
101
  def setup_command(name, options = {})
105
- name = name.to_s.to_sym
106
-
107
102
  @commands << SystemUtils::Command.new(
108
- name,
109
- path: options.delete(:path),
110
- aliases: options.delete(:aliases) || options.delete(:alias)
103
+ name.to_sym,
104
+ chdir: options.delete(:chdir),
105
+ bin_path: options.delete(:bin_path)
111
106
  )
112
107
  end
113
108
  private :setup_command
114
109
 
115
110
  def find_command(name)
116
- @commands.select { |command| command.aliases.include? name.to_s }.first
111
+ @commands.select { |command| command.name == name.to_s }.first
117
112
  end
118
113
 
119
114
  def sections
@@ -17,12 +17,12 @@ with this program; if not, write to the Free Software Foundation, Inc.,
17
17
  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18
18
  =end
19
19
 
20
- require 'ostruct'
21
- require 'singleton'
22
- require 'open3'
23
-
24
20
  module GitHooks
25
21
  class Repository # rubocop:disable ClassLength
22
+ extend SystemUtils
23
+
24
+ command :git
25
+
26
26
  autoload :Config, 'githooks/repository/config'
27
27
  autoload :File, 'githooks/repository/file'
28
28
  autoload :Limiter, 'githooks/repository/limiter'
@@ -38,11 +38,11 @@ module GitHooks
38
38
  }.freeze unless defined? CHANGE_TYPE_SYMBOLS
39
39
 
40
40
  CHANGE_TYPES = CHANGE_TYPE_SYMBOLS.invert.freeze unless defined? CHANGE_TYPES
41
-
42
41
  DEFAULT_DIFF_INDEX_OPTIONS = { staged: true } unless defined? DEFAULT_DIFF_INDEX_OPTIONS
43
42
 
44
43
  @__instance__ = {}
45
44
  @__mutex__ = Mutex.new
45
+
46
46
  def self.instance(path = Dir.getwd)
47
47
  path = Pathname.new(path).realpath
48
48
  strpath = path.to_s
@@ -70,12 +70,8 @@ module GitHooks
70
70
  @config ||= Repository::Config.new(root_path)
71
71
  end
72
72
 
73
- def git_command(*args)
74
- git.execute(*args.flatten)
75
- end
76
-
77
73
  def get_root_path(path)
78
- git_command('rev-parse', '--show-toplevel', path: path).tap do |result|
74
+ git('rev-parse', '--show-toplevel', chdir: path).tap do |result|
79
75
  unless result.status.success? && result.output !~ /not a git repository/i
80
76
  fail Error::NotAGitRepo, "Unable to find a valid git repo in #{path}"
81
77
  end
@@ -83,30 +79,33 @@ module GitHooks
83
79
  end
84
80
 
85
81
  def stash
86
- git_command(%w( stash -q --keep-index -a)).status.success?
82
+ git(*%w( stash -q --keep-index -a)).status.success?
87
83
  end
88
84
 
89
85
  def unstash
90
- git_command(%w(stash pop -q)).status.success?
86
+ git(*%w(stash pop -q)).status.success?
91
87
  end
92
88
 
93
- def manifest(options = {})
89
+ def manifest(options = {}) # rubocop:disable AbcSize
94
90
  ref = options.delete(:ref)
95
-
96
91
  return staged_manifest(ref: ref) if options.delete(:staged)
97
92
 
98
- files = unstaged_manifest(ref: ref)
93
+ manifest_list = unstaged_manifest(ref: ref)
99
94
 
100
- tracked_manifest(ref: ref).each do |file|
101
- files << file unless files.index { |f| f.path.to_s == file.path.to_s }
102
- end if options.delete(:tracked)
95
+ if options.delete(:tracked)
96
+ tracked_manifest(ref: ref).each_with_object(manifest_list) do |file, list|
97
+ list << file unless list.include?(file)
98
+ end
99
+ end
103
100
 
104
- untracked_manifest(ref: ref).each do |file|
105
- files << file unless files.index { |f| f.path.to_s == file.path.to_s }
106
- end if options.delete(:untracked)
101
+ if options.delete(:untracked)
102
+ untracked_manifest(ref: ref).each_with_object(manifest_list) do |file, list|
103
+ list << file unless list.include?(file)
104
+ end
105
+ end
107
106
 
108
- files.sort_by! { |f| f.path.to_s }
109
- files.uniq { |f| f.path.to_s }
107
+ manifest_list.sort!
108
+ manifest_list.uniq { |f| f.path.to_s }
110
109
  end
111
110
 
112
111
  def staged_manifest(options = {})
@@ -119,18 +118,18 @@ module GitHooks
119
118
  end
120
119
 
121
120
  def tracked_manifest(*)
122
- files = git_command('ls-files', '--exclude-standard').output.strip.split(/\s*\n\s*/)
121
+ files = git('ls-files', '--exclude-standard').output.strip.split(/\s*\n\s*/)
123
122
  files.collect { |path| DiffIndexEntry.from_file_path(path, true).to_repo_file }
124
123
  end
125
124
 
126
125
  def untracked_manifest(*)
127
- files = git_command('ls-files', '--others', '--exclude-standard').output.strip.split(/\s*\n\s*/)
126
+ files = git('ls-files', '--others', '--exclude-standard').output.strip.split(/\s*\n\s*/)
128
127
  files.collect { |path| DiffIndexEntry.from_file_path(path).to_repo_file }
129
128
  end
130
129
 
131
130
  private
132
131
 
133
- def diff_index(options = {})
132
+ def diff_index(options = {}) # rubocop:disable AbcSize
134
133
  options = DEFAULT_DIFF_INDEX_OPTIONS.merge(options)
135
134
 
136
135
  if $stdout.tty? && !options[:staged]
@@ -141,21 +140,18 @@ module GitHooks
141
140
  cmd << (options.delete(:ref) || 'HEAD')
142
141
  end
143
142
 
144
- cmd.compact!
145
-
146
- raw_output = git_command(*cmd.compact).output.strip
147
- raw_output.split(/\n/).collect { |data| DiffIndexEntry.new(data).to_repo_file }
148
- end
149
-
150
- def git
151
- @git ||= SystemUtils::Command.new('git')
143
+ git(*cmd.compact.compact).output_lines.collect do |diff_data|
144
+ DiffIndexEntry.new(diff_data).to_repo_file
145
+ end
146
+ rescue
147
+ exit! 1
152
148
  end
153
149
 
154
150
  def while_stashed(&block)
155
151
  fail ArgumentError, 'Missing required block' unless block_given?
156
152
  begin
157
153
  stash
158
- yield
154
+ block.call
159
155
  ensure
160
156
  unstash
161
157
  end
@@ -17,18 +17,14 @@ with this program; if not, write to the Free Software Foundation, Inc.,
17
17
  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18
18
  =end
19
19
 
20
- require 'ostruct'
21
- require 'singleton'
22
- require 'open3'
23
-
24
20
  module GitHooks
25
21
  class Repository
26
22
  class Config # rubocop:disable ClassLength
27
23
  OPTIONS = {
28
- 'path' => { type: :path, multiple: false },
29
- 'script' => { type: :path, multiple: false },
30
- 'pre-run-execute' => { type: :path, multiple: true },
31
- 'post-run-execute' => { type: :path, multiple: true }
24
+ 'path' => { type: :path, multiple: false },
25
+ 'script' => { type: :path, multiple: false },
26
+ 'pre-run-execute' => { type: :path, multiple: true },
27
+ 'post-run-execute' => { type: :path, multiple: true },
32
28
  }
33
29
 
34
30
  def initialize(path = Dir.getwd)
@@ -49,7 +45,7 @@ module GitHooks
49
45
  send(option.gsub('-', '_'))
50
46
  end
51
47
 
52
- def set(option, value, options = {}) # rubocop:disable CyclomaticComplexity, MethodLength
48
+ def set(option, value, options = {}) # rubocop:disable CyclomaticComplexity, MethodLength, PerceivedComplexity, AbcSize
53
49
  unless OPTIONS.keys.include? option
54
50
  fail ArgumentError, "Unexpected option '#{option}': expected one of: #{OPTIONS.keys.join(', ')}"
55
51
  end
@@ -82,11 +78,11 @@ module GitHooks
82
78
 
83
79
  if overwrite && !self[option].nil? && !self[option].empty?
84
80
  puts "Overwrite requested for option '#{option}'" if GitHooks.verbose
85
- unset(option, repo_path: repo, global: global)
81
+ unset(option, repo_chdir: repo, global: global)
86
82
  end
87
83
 
88
84
  option = "githooks.#{repo}.#{option}"
89
- git_command(global ? '--global' : '--local', var_type, add_type, option, value, path: repo).tap do |result|
85
+ git(global ? '--global' : '--local', var_type, add_type, option, value, chdir: repo).tap do |result|
90
86
  puts "Added option #{option} with value #{value}" if result.status.success?
91
87
  end
92
88
  end
@@ -95,10 +91,10 @@ module GitHooks
95
91
  repo = options.delete(:repo_path) || repo_path
96
92
  global = (opt = options.delete(:global)).nil? ? false : opt
97
93
  option = "githooks.#{repo}"
98
- git_command(global ? '--global' : '--local', '--remove-section', option, path: repo)
94
+ git(global ? '--global' : '--local', '--remove-section', option, chdir: repo)
99
95
  end
100
96
 
101
- def unset(option, *args) # rubocop:disable Style/CyclomaticComplexity
97
+ def unset(option, *args) # rubocop:disable CyclomaticComplexity, MethodLength, PerceivedComplexity, AbcSize
102
98
  unless OPTIONS.keys.include? option
103
99
  fail ArgumentError, "Unexpected option '#{option}': expected one of: #{OPTIONS.keys.join(', ')}"
104
100
  end
@@ -111,9 +107,9 @@ module GitHooks
111
107
  value_regex = args.first
112
108
 
113
109
  if options.delete(:all) || value_regex.nil?
114
- git_command(global ? '--global' : '--local', '--unset-all', option, path: repo)
110
+ git(global ? '--global' : '--local', '--unset-all', option, chdir: repo)
115
111
  else
116
- git_command(global ? '--global' : '--local', '--unset', option, value_regex, path: repo)
112
+ git(global ? '--global' : '--local', '--unset', option, value_regex, chdir: repo)
117
113
  end.tap do |result|
118
114
  puts "Unset option #{option.git_option_path_split.last}" if result.status.success?
119
115
  end
@@ -140,16 +136,16 @@ module GitHooks
140
136
  @repository.root_path
141
137
  end
142
138
 
143
- def git_command(*args)
139
+ def git(*args)
144
140
  args = ['config', *args].flatten
145
- @repository.git_command(*args)
141
+ @repository.git(*args)
146
142
  end
147
143
 
148
- def config(path = nil) # rubocop:disable MethodLength, CyclomaticComplexity
144
+ def config(path = nil) # rubocop:disable CyclomaticComplexity, MethodLength, PerceivedComplexity, AbcSize
149
145
  path ||= repo_path
150
146
 
151
- raw_config = git_command('--list', path: path).output.split("\n")
152
- raw_config.sort.uniq.inject({}) do |hash, line|
147
+ raw_config = git('--list', chdir: path).output.split("\n")
148
+ raw_config.sort.uniq.each_with_object({}) do |line, hash|
153
149
  key, value = line.split(/\s*=\s*/)
154
150
  key_parts = key.git_option_path_split
155
151
 
@@ -168,10 +164,6 @@ module GitHooks
168
164
  hash
169
165
  end
170
166
  end
171
-
172
- def git
173
- @repository.git
174
- end
175
167
  end
176
168
  end
177
169
  end