rabbitt-githooks 1.2.7

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 (44) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +34 -0
  3. data/.rubocop.yml +73 -0
  4. data/Gemfile +21 -0
  5. data/Gemfile.lock +45 -0
  6. data/LICENSE.txt +339 -0
  7. data/README.md +4 -0
  8. data/Rakefile +19 -0
  9. data/bin/githooks +15 -0
  10. data/bin/githooks-runner +17 -0
  11. data/hooks/commit-messages.rb +29 -0
  12. data/hooks/formatting.rb +53 -0
  13. data/lib/githooks.rb +89 -0
  14. data/lib/githooks/action.rb +150 -0
  15. data/lib/githooks/cli.rb +93 -0
  16. data/lib/githooks/commands/config.rb +107 -0
  17. data/lib/githooks/core_ext.rb +23 -0
  18. data/lib/githooks/core_ext/array.rb +3 -0
  19. data/lib/githooks/core_ext/array/extract_options.rb +5 -0
  20. data/lib/githooks/core_ext/array/min_max.rb +37 -0
  21. data/lib/githooks/core_ext/array/select_with_index.rb +13 -0
  22. data/lib/githooks/core_ext/numbers.rb +1 -0
  23. data/lib/githooks/core_ext/numbers/infinity.rb +19 -0
  24. data/lib/githooks/core_ext/pathname.rb +27 -0
  25. data/lib/githooks/core_ext/process.rb +7 -0
  26. data/lib/githooks/core_ext/string.rb +3 -0
  27. data/lib/githooks/core_ext/string/git_option_path_split.rb +6 -0
  28. data/lib/githooks/core_ext/string/inflections.rb +67 -0
  29. data/lib/githooks/core_ext/string/strip_empty_lines.rb +9 -0
  30. data/lib/githooks/error.rb +10 -0
  31. data/lib/githooks/hook.rb +159 -0
  32. data/lib/githooks/repository.rb +152 -0
  33. data/lib/githooks/repository/config.rb +170 -0
  34. data/lib/githooks/repository/diff_index_entry.rb +80 -0
  35. data/lib/githooks/repository/file.rb +125 -0
  36. data/lib/githooks/repository/limiter.rb +55 -0
  37. data/lib/githooks/runner.rb +317 -0
  38. data/lib/githooks/section.rb +98 -0
  39. data/lib/githooks/system_utils.rb +109 -0
  40. data/lib/githooks/terminal_colors.rb +63 -0
  41. data/lib/githooks/version.rb +22 -0
  42. data/rabbitt-githooks.gemspec +49 -0
  43. data/thoughts.txt +56 -0
  44. metadata +175 -0
@@ -0,0 +1,152 @@
1
+ # encoding: utf-8
2
+ =begin
3
+ Copyright (C) 2013 Carl P. Corliss
4
+
5
+ This program is free software; you can redistribute it and/or modify
6
+ it under the terms of the GNU General Public License as published by
7
+ the Free Software Foundation; either version 2 of the License, or
8
+ (at your option) any later version.
9
+
10
+ This program is distributed in the hope that it will be useful,
11
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ GNU General Public License for more details.
14
+
15
+ You should have received a copy of the GNU General Public License along
16
+ with this program; if not, write to the Free Software Foundation, Inc.,
17
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18
+ =end
19
+
20
+ require 'ostruct'
21
+ require 'singleton'
22
+ require 'open3'
23
+
24
+ module GitHooks
25
+ class Repository
26
+ autoload :Config, 'githooks/repository/config'
27
+ autoload :File, 'githooks/repository/file'
28
+ autoload :Limiter, 'githooks/repository/limiter'
29
+ autoload :DiffIndexEntry, 'githooks/repository/diff_index_entry'
30
+
31
+ CHANGE_TYPE_SYMBOLS = {
32
+ added: 'A', copied: 'C',
33
+ deleted: 'D', modified: 'M',
34
+ renamed: 'R', retyped: 'T',
35
+ unknown: 'U', unmerged: 'X',
36
+ broken: 'B', untracked: '?',
37
+ any: '*'
38
+ }.freeze unless defined? CHANGE_TYPE_SYMBOLS
39
+
40
+ CHANGE_TYPES = CHANGE_TYPE_SYMBOLS.invert.freeze unless defined? CHANGE_TYPES
41
+
42
+ DEFAULT_DIFF_INDEX_OPTIONS = { staged: true, ref: 'HEAD' } unless defined? DEFAULT_DIFF_INDEX_OPTIONS
43
+
44
+ @__instance__ = {}
45
+ @__mutex__ = Mutex.new
46
+ def self.instance(path = Dir.getwd)
47
+ path = Pathname.new(path).realpath
48
+ strpath = path.to_s
49
+ return @__instance__[strpath] if @__instance__[strpath]
50
+
51
+ @__mutex__.synchronize do
52
+ return @__instance__[strpath] if @__instance__[strpath]
53
+ @__instance__[strpath] = new(path)
54
+ end
55
+ end
56
+
57
+ def self.method_missing(method, *args, &block)
58
+ return super unless instance.public_methods.include? method
59
+ instance.public_send(method, *args, &block)
60
+ end
61
+
62
+ attr_reader :root_path
63
+
64
+ def initialize(path = Dir.getwd)
65
+ @root_path = get_root_path(path)
66
+ end
67
+ protected :initialize
68
+
69
+ def config
70
+ @config ||= Repository::Config.new(root_path)
71
+ end
72
+
73
+ def git_command(*args)
74
+ git.execute(*args.flatten)
75
+ end
76
+
77
+ def get_root_path(path)
78
+ git_command('rev-parse', '--show-toplevel', path: path).tap do |result|
79
+ unless result.status.success? && result.output !~ /not a git repository/i
80
+ fail Error::NotAGitRepo, "Unable to find a valid git repo in #{path}"
81
+ end
82
+ end.output.strip
83
+ end
84
+
85
+ def stash
86
+ git_command(%w( stash -q --keep-index -a)).status.success?
87
+ end
88
+
89
+ def unstash
90
+ git_command(%w(stash pop -q)).status.success?
91
+ end
92
+
93
+ def manifest(options = {})
94
+ ref = options.delete(:ref) || 'HEAD'
95
+ unstaged = options.delete(:unstaged)
96
+ untracked = options.delete(:untracked)
97
+
98
+ return staged_manifest(ref: ref) unless unstaged || untracked
99
+
100
+ [].tap do |files|
101
+ files.push(*unstaged_manifest(ref: ref)) if unstaged
102
+ files.push(*untracked_manifest) if untracked
103
+ end
104
+ end
105
+
106
+ def staged_manifest(options = {})
107
+ diff_index(options.merge(unstaged: false))
108
+ end
109
+ alias_method :commit_manifest, :staged_manifest
110
+
111
+ def unstaged_manifest(options = {})
112
+ diff_index(options.merge(unstaged: true))
113
+ end
114
+
115
+ def untracked_manifest
116
+ files = git_command('ls-files', '--others', '--exclude-standard').output.strip.split(/\s*\n\s*/)
117
+ files.collect { |path| DiffIndexEntry.from_file_path(path).to_repo_file }
118
+ end
119
+
120
+ private
121
+
122
+ def diff_index(options = {})
123
+ options = DEFAULT_DIFF_INDEX_OPTIONS.merge(options)
124
+
125
+ cmd = %w(diff-index -C -M -B)
126
+ cmd << '--cached' unless options[:unstaged]
127
+ cmd << options.delete(:ref) || 'HEAD'
128
+
129
+ raw_output = git_command(*cmd).output.strip
130
+ raw_output.split(/\n/).collect { |data| DiffIndexEntry.new(data).to_repo_file }
131
+ end
132
+
133
+ def git
134
+ @git ||= SystemUtils::Command.new('git')
135
+ end
136
+
137
+ def while_stashed(&block)
138
+ fail ArgumentError, 'Missing required block' unless block_given?
139
+ begin
140
+ stash
141
+ yield
142
+ ensure
143
+ unstash
144
+ end
145
+ end
146
+
147
+ def run_while_stashed(cmd)
148
+ while_stashed { system(cmd) }
149
+ $? == 0
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,170 @@
1
+ # encoding: utf-8
2
+ =begin
3
+ Copyright (C) 2013 Carl P. Corliss
4
+
5
+ This program is free software; you can redistribute it and/or modify
6
+ it under the terms of the GNU General Public License as published by
7
+ the Free Software Foundation; either version 2 of the License, or
8
+ (at your option) any later version.
9
+
10
+ This program is distributed in the hope that it will be useful,
11
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ GNU General Public License for more details.
14
+
15
+ You should have received a copy of the GNU General Public License along
16
+ with this program; if not, write to the Free Software Foundation, Inc.,
17
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18
+ =end
19
+
20
+ require 'ostruct'
21
+ require 'singleton'
22
+ require 'open3'
23
+
24
+ module GitHooks
25
+ class Repository::Config # rubocop:disable ClassLength
26
+ OPTIONS = {
27
+ 'path' => { type: :path, multiple: false },
28
+ 'script' => { type: :path, multiple: false },
29
+ 'pre-run-execute' => { type: :path, multiple: true },
30
+ 'post-run-execute' => { type: :path, multiple: true }
31
+ }
32
+
33
+ def initialize(path = Dir.getwd)
34
+ @repository = Repository.instance(path)
35
+ end
36
+
37
+ OPTIONS.keys.each do |name|
38
+ method_name = name.gsub(/-/, '_')
39
+ class_eval(<<-EOS, __FILE__, __LINE__ + 1)
40
+ def #{method_name}(options = {})
41
+ result = get('#{name}', options)
42
+ OPTIONS['#{name}'][:multiple] ? [result].flatten.compact : result
43
+ end
44
+ EOS
45
+ end
46
+
47
+ def [](option)
48
+ send(option.gsub('-', '_'))
49
+ end
50
+
51
+ def set(option, value, options = {}) # rubocop:disable CyclomaticComplexity, MethodLength
52
+ unless OPTIONS.keys.include? option
53
+ fail ArgumentError, "Unexpected option '#{option}': expected one of: #{OPTIONS.keys.join(', ')}"
54
+ end
55
+
56
+ repo = options.delete(:repo_path) || repo_path
57
+ global = (opt = options.delete(:global)).nil? ? false : opt
58
+ var_type = "--#{OPTIONS[option][:type]}"
59
+ add_type = OPTIONS[option][:multiple] ? '--add' : '--replace-all'
60
+ overwrite = !!options.delete(:overwrite)
61
+
62
+ if option == 'path'
63
+ new_path = Pathname.new(value)
64
+ errors = []
65
+ errors << 'path must be a real location' unless new_path.exist?
66
+ errors << 'path must be a directory' unless new_path.directory?
67
+ errors << 'path must have a hooks directory in it' unless (new_path + 'hooks').exist?
68
+
69
+ if errors.size > 0
70
+ puts "Unable to change githooks path for [#{repo}]:"
71
+ errors.each { |error| puts " #{error}" }
72
+ fail ArgumentError
73
+ end
74
+ else
75
+ fail ArgumentError unless Pathname.new(value).executable?
76
+ end
77
+
78
+ value = Pathname.new(value).realpath.to_s
79
+
80
+ if overwrite && !self[option].nil? && !self[option].empty?
81
+ puts "Overwrite requested for option '#{option}'" if GitHooks.verbose
82
+ unset(option, repo_path: repo, global: global)
83
+ end
84
+
85
+ option = "githooks.#{repo}.#{option}"
86
+ git_command(global ? '--global' : '--local', var_type, add_type, option, value, path: repo).tap do |result|
87
+ puts "Added option #{option} with value #{value}" if result.status.success?
88
+ end
89
+ end
90
+
91
+ def remove_section(options = {})
92
+ repo = options.delete(:repo_path) || repo_path
93
+ global = (opt = options.delete(:global)).nil? ? false : opt
94
+ option = "githooks.#{repo}"
95
+ git_command(global ? '--global' : '--local', '--remove-section', option, path: repo)
96
+ end
97
+
98
+ def unset(option, *args)
99
+ unless OPTIONS.keys.include? option
100
+ fail ArgumentError, "Unexpected option '#{option}': expected one of: #{OPTIONS.keys.join(', ')}"
101
+ end
102
+
103
+ options = args.extract_options
104
+ repo = options.delete(:repo_path) || repo_path
105
+ global = (opt = options.delete(:global)).nil? ? false : opt
106
+ option = "githooks.#{repo}.#{option}"
107
+
108
+ value_regex = args.first
109
+
110
+ if options.delete(:all) || value_regex.nil?
111
+ git_command(global ? '--global' : '--local', '--unset-all', option, path: repo)
112
+ else
113
+ git_command(global ? '--global' : '--local', '--unset', option, value_regex, path: repo)
114
+ end.tap do |result|
115
+ puts "Unset option #{option.git_option_path_split.last}" if result.status.success?
116
+ end
117
+ end
118
+
119
+ def get(option, options = {})
120
+ unless OPTIONS.keys.include? option
121
+ fail ArgumentError, "Unexpected option '#{option}': expected one of: #{OPTIONS.keys.join(', ')}"
122
+ end
123
+
124
+ repo = options[:repo_path] || repo_path
125
+ githooks = list(options)['githooks']
126
+
127
+ githooks[repo][option] if githooks && githooks[repo] && githooks[repo][option]
128
+ end
129
+
130
+ def list(options = {}) # rubocop:disable MethodLength, CyclomaticComplexity
131
+ repo = options.delete(:repo_path) || repo_path
132
+ global = (opt = options.delete(:global)).nil? ? false : opt
133
+
134
+ config_list = git_command('--list', global ? '--global' : '--local', path: repo).output.split(/\n/)
135
+ config_list.inject({}) do |hash, line|
136
+ key, value = line.split(/\s*=\s*/)
137
+ key_parts = key.git_option_path_split
138
+
139
+ ptr = hash[key_parts.shift] ||= {} # rubocop:disable IndentationWidth
140
+ while key_parts.size > 1 && (part = key_parts.shift)
141
+ ptr = ptr[part] ||= {} # rubocop:disable IndentationWidth
142
+ end
143
+
144
+ key = key_parts.shift
145
+ case ptr[key]
146
+ when nil then ptr[key] = value
147
+ when Array then ptr[key] << value
148
+ else ptr[key] = [ptr[key], value].flatten
149
+ end
150
+
151
+ hash
152
+ end
153
+ end
154
+
155
+ private
156
+
157
+ def repo_path
158
+ @repository.root_path
159
+ end
160
+
161
+ def git_command(*args)
162
+ args = ['config', *args].flatten
163
+ @repository.git_command(*args)
164
+ end
165
+
166
+ def git
167
+ @repository.git
168
+ end
169
+ end
170
+ end
@@ -0,0 +1,80 @@
1
+ require 'ostruct'
2
+ require 'pathname'
3
+
4
+ module GitHooks
5
+ class Repository::DiffIndexEntry < OpenStruct
6
+ DIFF_STRUCTURE_REGEXP = %r{
7
+ ^:
8
+ (?<original_mode>\d+)\s
9
+ (?<new_mode>\d+)\s
10
+ (?<original_sha>[a-f\d]+)\s
11
+ (?<new_sha>[a-f\d]+)\s
12
+ (?<change_type>.)
13
+ (?:(?<score>\d+)?)\s
14
+ (?<file_path>\S+)\s?
15
+ (?<rename_path>\S+)?
16
+ }xi unless defined? DIFF_STRUCTURE_REGEXP
17
+
18
+ def self.from_file_path(file_path)
19
+ file_path = Pathname.new(file_path)
20
+ new(
21
+ [
22
+ 0,
23
+ file_path.stat.mode.to_s(8),
24
+ 0x0,
25
+ 0x0,
26
+ '?',
27
+ file_path.to_s
28
+ ].join(' ').prepend(':')
29
+ )
30
+ end
31
+
32
+ def initialize(entry)
33
+ unless entry =~ DIFF_STRUCTURE_REGEXP
34
+ fail ArgumentError, 'Unable to parse incoming diff entry data: #{entry}'
35
+ end
36
+ super parse_data(entry)
37
+ end
38
+
39
+ def parse_data(entry) # rubocop:disable MethodLength
40
+ data = Hash[DIFF_STRUCTURE_REGEXP.names.collect(&:to_sym).zip(
41
+ entry.match(DIFF_STRUCTURE_REGEXP).captures
42
+ )]
43
+
44
+ {
45
+ from: FileState.new(
46
+ data[:original_mode].to_i(8),
47
+ data[:original_sha],
48
+ data[:file_path].nil? ? nil : Pathname.new(data[:file_path])
49
+ ),
50
+ to: FileState.new(
51
+ data[:new_mode].to_i(8),
52
+ data[:new_sha],
53
+ data[:rename_path].nil? ? nil : Pathname.new(data[:rename_path])
54
+ ),
55
+ type: Repository::CHANGE_TYPES[data[:change_type]],
56
+ score: data[:score].to_i
57
+ }
58
+ end
59
+
60
+ def to_repo_file
61
+ Repository::File.new(self)
62
+ end
63
+
64
+ class FileState
65
+ attr_reader :mode, :sha, :path
66
+
67
+ def initialize(mode, sha, path)
68
+ @mode, @sha, @path = mode, sha, path
69
+ end
70
+
71
+ def inspect
72
+ "#<#{self.class.name.split('::').last} mode=#{mode.to_s(8)} path=#{path.to_s.inspect} sha=#{sha.inspect}>"
73
+ end
74
+
75
+ def to_path
76
+ Pathname.new(@path)
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,125 @@
1
+ # encoding: utf-8
2
+ =begin
3
+ Copyright (C) 2013 Carl P. Corliss
4
+
5
+ This program is free software; you can redistribute it and/or modify
6
+ it under the terms of the GNU General Public License as published by
7
+ the Free Software Foundation; either version 2 of the License, or
8
+ (at your option) any later version.
9
+
10
+ This program is distributed in the hope that it will be useful,
11
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ GNU General Public License for more details.
14
+
15
+ You should have received a copy of the GNU General Public License along
16
+ with this program; if not, write to the Free Software Foundation, Inc.,
17
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18
+ =end
19
+
20
+ require 'ostruct'
21
+ require 'delegate'
22
+
23
+ # allow for reloading of class
24
+ unless defined? DiffIndexEntryDelegateClass
25
+ DiffIndexEntryDelegateClass = DelegateClass(GitHooks::Repository::DiffIndexEntry)
26
+ end
27
+
28
+ module GitHooks
29
+ class Repository::File < DiffIndexEntryDelegateClass
30
+ def initialize(entry)
31
+ unless entry.is_a? Repository::DiffIndexEntry
32
+ fail ArgumentError, "Expected a Repository::DiffIndexEntry but got a '#{entry.class.name}'"
33
+ end
34
+ @file = entry
35
+ end
36
+
37
+ def __getobj__ # rubocop:disable TrivialAccessors
38
+ @file
39
+ end
40
+
41
+ def inspect
42
+ attributes = [:name, :path, :type, :mode, :sha, :score].collect do |name|
43
+ "#{name}=#{attribute_value(name).inspect}"
44
+ end
45
+ "#<#{self.class.name} #{attributes.join(' ')} >"
46
+ end
47
+
48
+ def path
49
+ to.path || from.path
50
+ end
51
+
52
+ def name
53
+ path.basename.to_s
54
+ end
55
+
56
+ # rubocop:disable CyclomaticComplexity
57
+ def attribute_value(attribute)
58
+ case attribute
59
+ when :name then name
60
+ when :path then path.to_s
61
+ when :type then type
62
+ when :mode then to.mode
63
+ when :sha then to.sha
64
+ when :score then score
65
+ else fail ArgumentError,
66
+ "Invalid attribute type '#{attribute}' - expected: :name, :path, :type, :mode, :sha, or :score"
67
+ end
68
+ end
69
+
70
+ def match(type, _match)
71
+ value = attribute_value(type)
72
+ return _match.call(value) if _match.respond_to? :call
73
+
74
+ case type
75
+ when :name then _match.is_a?(Regexp) ? value =~ _match : value == _match
76
+ when :path then _match.is_a?(Regexp) ? value =~ _match : value == _match
77
+ when :type then _match.is_a?(Array) ? _match.include?(value) : _match == value
78
+ when :mode then _match & value == _match
79
+ when :sha then _match == value
80
+ when :score then _match == value
81
+ end
82
+ end
83
+ # rubocop:enable CyclomaticComplexity
84
+
85
+ def fd
86
+ case type
87
+ when :deleted, :deletion then nil
88
+ else path.open
89
+ end
90
+ end
91
+
92
+ def realpath
93
+ case type
94
+ when :deleted, :deletion then path
95
+ else path.realpath
96
+ end
97
+ end
98
+
99
+ def contains?(string_or_regexp)
100
+ if string_or_regexp.is_a?(Regexp)
101
+ contents =~ string_or_regexp
102
+ else
103
+ contents.include? string_or_regexp
104
+ end
105
+ end
106
+
107
+ def grep(regexp)
108
+ lines(true).select_with_index { |line|
109
+ line =~ regexp
110
+ }.collect { |num, line|
111
+ [num + 1, line] # line numbers start from 1, not 0
112
+ }
113
+ end
114
+
115
+ def contents
116
+ return unless fd
117
+ fd.read
118
+ end
119
+
120
+ def lines(strip_newlines = false)
121
+ return [] unless fd
122
+ strip_newlines ? fd.readlines.collect(&:chomp!) : fd.readlines
123
+ end
124
+ end
125
+ end