overcommit 0.75.0 → 0.76.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.
- checksums.yaml +4 -4
- data/config/default.yml +56 -0
- data/lib/overcommit/cli.rb +7 -1
- data/lib/overcommit/configuration.rb +27 -34
- data/lib/overcommit/configuration_loader.rb +2 -2
- data/lib/overcommit/configuration_validator.rb +17 -3
- data/lib/overcommit/exceptions.rb +3 -0
- data/lib/overcommit/gem_plugin.rb +165 -0
- data/lib/overcommit/git_config.rb +18 -0
- data/lib/overcommit/git_repo.rb +13 -2
- data/lib/overcommit/hook/base.rb +1 -1
- data/lib/overcommit/hook/commit_msg/spell_check.rb +5 -8
- data/lib/overcommit/hook/pre_commit/bundle_audit.rb +6 -1
- data/lib/overcommit/hook/pre_commit/line_endings.rb +1 -2
- data/lib/overcommit/hook/pre_commit/yaml_syntax.rb +1 -1
- data/lib/overcommit/hook/prepare_commit_msg/replace_branch.rb +52 -3
- data/lib/overcommit/hook_context/base.rb +4 -1
- data/lib/overcommit/hook_context/commit_msg.rb +10 -4
- data/lib/overcommit/hook_context/helpers/stash_unstaged_changes.rb +25 -3
- data/lib/overcommit/hook_loader/base.rb +43 -11
- data/lib/overcommit/hook_loader/built_in_hook_loader.rb +2 -1
- data/lib/overcommit/hook_loader/plugin_hook_loader.rb +61 -18
- data/lib/overcommit/hook_runner.rb +1 -1
- data/lib/overcommit/hook_signer.rb +9 -29
- data/lib/overcommit/signature.rb +71 -0
- data/lib/overcommit/subprocess.rb +2 -1
- data/lib/overcommit/utils/messages_utils.rb +14 -0
- data/lib/overcommit/utils.rb +64 -6
- data/lib/overcommit/version.rb +1 -1
- data/lib/overcommit.rb +2 -0
- data/libexec/overcommit-parent-command.js +19 -0
- metadata +4 -1
|
@@ -17,6 +17,28 @@ module Overcommit::Hook::PrepareCommitMsg
|
|
|
17
17
|
#
|
|
18
18
|
# Similarly, a replacement text of `[\1][\2]` would result in `[123][topic]`.
|
|
19
19
|
#
|
|
20
|
+
# The hook does not prepend the text if the commit message already starts
|
|
21
|
+
# with it, or with an autosquash prefix (`fixup! `, `squash! `, `amend! `).
|
|
22
|
+
# This matters when you remove `message` or `commit` from
|
|
23
|
+
# `skipped_commit_types`: `git commit -m '[#123] Fix'`, `--amend`, and
|
|
24
|
+
# `--fixup` then keep one prefix.
|
|
25
|
+
#
|
|
26
|
+
# === Ticket IDs
|
|
27
|
+
#
|
|
28
|
+
# This config prepends `[PROJ-123] ` on a branch such as
|
|
29
|
+
# `task/PROJ-123/fix-login`, also for `-m` and `--amend` commits:
|
|
30
|
+
#
|
|
31
|
+
# PrepareCommitMsg:
|
|
32
|
+
# ReplaceBranch:
|
|
33
|
+
# enabled: true
|
|
34
|
+
# branch_pattern: '\A.*?([A-Z][A-Z0-9]+-\d+).*\z'
|
|
35
|
+
# replacement_text: '[\1] '
|
|
36
|
+
# skipped_commit_types: ['template', 'merge', 'squash']
|
|
37
|
+
# on_warn: pass # do not warn on branches without a ticket ID
|
|
38
|
+
#
|
|
39
|
+
# It keeps `template` skipped, because a commit template can start with a
|
|
40
|
+
# `#` comment line, and the prefix would turn that line into the subject.
|
|
41
|
+
#
|
|
20
42
|
# == When to run this hook
|
|
21
43
|
#
|
|
22
44
|
# You can configure this to run only for specific types of commits by setting
|
|
@@ -31,6 +53,10 @@ module Overcommit::Hook::PrepareCommitMsg
|
|
|
31
53
|
class ReplaceBranch < Base
|
|
32
54
|
DEFAULT_BRANCH_PATTERN = /\A(\d+)-(\w+).*\z/.freeze
|
|
33
55
|
|
|
56
|
+
# `git rebase --autosquash` finds these commits only when the prefix starts
|
|
57
|
+
# the subject.
|
|
58
|
+
AUTOSQUASH_PREFIX = /\A(?:fixup|squash|amend)! /.freeze
|
|
59
|
+
|
|
34
60
|
def run
|
|
35
61
|
return :pass if skip?
|
|
36
62
|
|
|
@@ -40,10 +66,14 @@ module Overcommit::Hook::PrepareCommitMsg
|
|
|
40
66
|
|
|
41
67
|
return :warn unless branch_pattern.match?(Overcommit::GitRepo.current_branch)
|
|
42
68
|
|
|
43
|
-
Overcommit::Utils.log.debug("Writing #{commit_message_filename} with #{new_template}")
|
|
44
|
-
|
|
45
69
|
modify_commit_message do |old_contents|
|
|
46
|
-
|
|
70
|
+
if keep_message?(old_contents)
|
|
71
|
+
Overcommit::Utils.log.debug("Keeping #{commit_message_filename} unchanged")
|
|
72
|
+
old_contents
|
|
73
|
+
else
|
|
74
|
+
Overcommit::Utils.log.debug("Writing #{commit_message_filename} with #{new_template}")
|
|
75
|
+
"#{new_template}#{old_contents}"
|
|
76
|
+
end
|
|
47
77
|
end
|
|
48
78
|
|
|
49
79
|
:pass
|
|
@@ -57,6 +87,25 @@ module Overcommit::Hook::PrepareCommitMsg
|
|
|
57
87
|
end
|
|
58
88
|
end
|
|
59
89
|
|
|
90
|
+
# Compare bytes: String methods such as `lstrip` raise on a message that is
|
|
91
|
+
# not valid in the default encoding, for example an amended Latin-1 commit.
|
|
92
|
+
def keep_message?(contents)
|
|
93
|
+
message = contents.b.lstrip
|
|
94
|
+
message.match?(AUTOSQUASH_PREFIX) || starts_with_new_template?(message)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Strip the template, because users type the prefix with or without the
|
|
98
|
+
# trailing space of `replacement_text`. Without that space, a template that
|
|
99
|
+
# ends in a word character, such as `PROJ-12`, must not match `PROJ-123`.
|
|
100
|
+
# A blank template never matches, so a whitespace-only `replacement_text`
|
|
101
|
+
# is still prepended as before.
|
|
102
|
+
def starts_with_new_template?(message)
|
|
103
|
+
template = new_template.b.strip
|
|
104
|
+
return false if template.empty? || !message.start_with?(template)
|
|
105
|
+
|
|
106
|
+
!(template.match?(/\w\z/) && message[template.size]&.match?(/\w/))
|
|
107
|
+
end
|
|
108
|
+
|
|
60
109
|
def branch_pattern
|
|
61
110
|
@branch_pattern ||=
|
|
62
111
|
begin
|
|
@@ -99,7 +99,10 @@ module Overcommit::HookContext
|
|
|
99
99
|
return @input_string if defined?(@input_string)
|
|
100
100
|
|
|
101
101
|
@input_mutex.synchronize do
|
|
102
|
-
|
|
102
|
+
unless defined?(@input_string)
|
|
103
|
+
@input.binmode if @input.respond_to?(:binmode)
|
|
104
|
+
@input_string = @input.read
|
|
105
|
+
end
|
|
103
106
|
end
|
|
104
107
|
|
|
105
108
|
@input_string
|
|
@@ -14,7 +14,7 @@ module Overcommit::HookContext
|
|
|
14
14
|
commit_message.strip.empty?
|
|
15
15
|
end
|
|
16
16
|
|
|
17
|
-
# User commit message
|
|
17
|
+
# User commit message with Git's comment handling and no verbose diff.
|
|
18
18
|
def commit_message
|
|
19
19
|
commit_message_lines.join
|
|
20
20
|
end
|
|
@@ -27,9 +27,11 @@ module Overcommit::HookContext
|
|
|
27
27
|
end
|
|
28
28
|
|
|
29
29
|
def commit_message_lines
|
|
30
|
-
raw_commit_message_lines.
|
|
31
|
-
|
|
32
|
-
reject
|
|
30
|
+
lines = raw_commit_message_lines.take_while { |line| !line.start_with?('diff --git') }
|
|
31
|
+
if cleanup_mode == 'strip'
|
|
32
|
+
lines.reject! { |line| line.start_with?(comment_character) }
|
|
33
|
+
end
|
|
34
|
+
lines
|
|
33
35
|
end
|
|
34
36
|
|
|
35
37
|
def comment_character
|
|
@@ -48,6 +50,10 @@ module Overcommit::HookContext
|
|
|
48
50
|
|
|
49
51
|
private
|
|
50
52
|
|
|
53
|
+
def cleanup_mode
|
|
54
|
+
@cleanup_mode ||= Overcommit::GitConfig.commit_cleanup_mode
|
|
55
|
+
end
|
|
56
|
+
|
|
51
57
|
def raw_commit_message_lines
|
|
52
58
|
::IO.readlines(commit_message_file)
|
|
53
59
|
end
|
|
@@ -17,6 +17,7 @@ module Overcommit::HookContext
|
|
|
17
17
|
# editors/tools which watch for file changes.
|
|
18
18
|
if !initial_commit? && unstaged_changes?
|
|
19
19
|
stash_changes
|
|
20
|
+
restore_staged_changes if @changes_stashed
|
|
20
21
|
|
|
21
22
|
# While running hooks make it appear as if nothing changed
|
|
22
23
|
restore_modified_times
|
|
@@ -82,7 +83,7 @@ module Overcommit::HookContext
|
|
|
82
83
|
stash_message = "Overcommit: Stash of repo state before hook run at #{Time.now}"
|
|
83
84
|
result = Overcommit::Utils.with_environment('GIT_LITERAL_PATHSPECS' => '0') do
|
|
84
85
|
Overcommit::Utils.execute(
|
|
85
|
-
%w[git -c commit.gpgsign=false stash save --
|
|
86
|
+
%w[git -c commit.gpgsign=false stash save --quiet] + [stash_message]
|
|
86
87
|
)
|
|
87
88
|
end
|
|
88
89
|
|
|
@@ -95,6 +96,19 @@ module Overcommit::HookContext
|
|
|
95
96
|
end
|
|
96
97
|
|
|
97
98
|
@changes_stashed = `git stash list -1`.include?(stash_message)
|
|
99
|
+
@stash_commit = `git rev-parse stash@{0}`.strip if @changes_stashed
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Restore the staged tree explicitly: older Git versions' --keep-index
|
|
103
|
+
# leave deleted files and the old paths of renamed files in the working tree.
|
|
104
|
+
# read-tree updates both the index and working tree without moving HEAD.
|
|
105
|
+
def restore_staged_changes
|
|
106
|
+
result = Overcommit::Utils.execute(%w[git read-tree --reset -u stash@{0}^2])
|
|
107
|
+
unless result.success?
|
|
108
|
+
raise Overcommit::Exceptions::HookSetupFailed,
|
|
109
|
+
"Unable to restore staged changes for #{hook_script_name} hook run:" \
|
|
110
|
+
"\nSTDOUT:#{result.stdout}\nSTDERR:#{result.stderr}#{stash_recovery_message}"
|
|
111
|
+
end
|
|
98
112
|
end
|
|
99
113
|
|
|
100
114
|
# Restores the file modification times for all modified files to make it
|
|
@@ -116,7 +130,7 @@ module Overcommit::HookContext
|
|
|
116
130
|
unless result.success?
|
|
117
131
|
raise Overcommit::Exceptions::HookCleanupFailed,
|
|
118
132
|
"Unable to cleanup working tree after #{hook_script_name} hooks run:" \
|
|
119
|
-
"\nSTDOUT:#{result.stdout}\nSTDERR:#{result.stderr}"
|
|
133
|
+
"\nSTDOUT:#{result.stdout}\nSTDERR:#{result.stderr}#{stash_recovery_message}"
|
|
120
134
|
end
|
|
121
135
|
|
|
122
136
|
# Hard-resetting a staged submodule removal results in the index being
|
|
@@ -136,9 +150,17 @@ module Overcommit::HookContext
|
|
|
136
150
|
unless result.success?
|
|
137
151
|
raise Overcommit::Exceptions::HookCleanupFailed,
|
|
138
152
|
"Unable to restore working tree after #{hook_script_name} hooks run:" \
|
|
139
|
-
"\nSTDOUT:#{result.stdout}\nSTDERR:#{result.stderr}"
|
|
153
|
+
"\nSTDOUT:#{result.stdout}\nSTDERR:#{result.stderr}#{stash_recovery_message}"
|
|
140
154
|
end
|
|
141
155
|
end
|
|
156
|
+
|
|
157
|
+
def stash_recovery_message
|
|
158
|
+
return '' unless @stash_commit
|
|
159
|
+
|
|
160
|
+
"\nYour changes are preserved in Git stash #{@stash_commit}.\n" \
|
|
161
|
+
"Release any file locks and use a clean working tree to recover on a new branch:\n" \
|
|
162
|
+
" git stash branch overcommit-recovery #{@stash_commit}\n"
|
|
163
|
+
end
|
|
142
164
|
end
|
|
143
165
|
end
|
|
144
166
|
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'overcommit/utils/messages_utils'
|
|
4
|
+
|
|
3
5
|
module Overcommit::HookLoader
|
|
4
6
|
# Responsible for loading hooks from a file.
|
|
5
7
|
class Base
|
|
@@ -22,27 +24,57 @@ module Overcommit::HookLoader
|
|
|
22
24
|
|
|
23
25
|
private
|
|
24
26
|
|
|
27
|
+
def validate_line_aware_config(hook_config)
|
|
28
|
+
pattern = hook_config['message_pattern']
|
|
29
|
+
unless pattern.is_a?(Regexp) && pattern.names.include?('file')
|
|
30
|
+
raise Overcommit::Exceptions::HookLoadError,
|
|
31
|
+
'`message_pattern` must be a Regexp with a named `file` capture group'
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
warning_pattern = hook_config['warning_message_type_pattern']
|
|
35
|
+
unless warning_pattern.nil? || warning_pattern.is_a?(Regexp) || warning_pattern.is_a?(String)
|
|
36
|
+
raise Overcommit::Exceptions::HookLoadError,
|
|
37
|
+
'`warning_message_type_pattern` must be a Regexp or String'
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def create_line_aware_command_hook_class(hook_base)
|
|
42
|
+
Class.new(hook_base) do
|
|
43
|
+
def run
|
|
44
|
+
result = execute(command, args: applicable_files)
|
|
45
|
+
return :pass if result.success?
|
|
46
|
+
|
|
47
|
+
output = result.stdout.split("\n")
|
|
48
|
+
return [:fail, result.stderr] if output.empty?
|
|
49
|
+
|
|
50
|
+
Overcommit::Utils::MessagesUtils.extract_messages(
|
|
51
|
+
output,
|
|
52
|
+
@config['message_pattern'],
|
|
53
|
+
Overcommit::Utils::MessagesUtils.create_type_categorizer(
|
|
54
|
+
@config['warning_message_type_pattern']
|
|
55
|
+
)
|
|
56
|
+
)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
25
61
|
attr_reader :log
|
|
26
62
|
|
|
27
63
|
# Load and return a {Hook} from a CamelCase hook name.
|
|
28
64
|
def create_hook(hook_name)
|
|
29
65
|
hook_type_class = Overcommit::Hook.const_get(@context.hook_class_name)
|
|
30
66
|
hook_base_class = hook_type_class.const_get(:Base)
|
|
31
|
-
hook_class = hook_type_class.const_get(hook_name)
|
|
32
|
-
unless hook_class < hook_base_class
|
|
67
|
+
hook_class = hook_type_class.const_get(hook_name, false)
|
|
68
|
+
unless hook_class.is_a?(Class) && hook_class < hook_base_class
|
|
33
69
|
raise Overcommit::Exceptions::HookLoadError,
|
|
34
70
|
"Class #{hook_name} is not a subclass of #{hook_base_class}."
|
|
35
71
|
end
|
|
36
72
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
raise Overcommit::Exceptions::HookLoadError,
|
|
43
|
-
"Unable to load hook '#{hook_name}': #{e}",
|
|
44
|
-
e.backtrace
|
|
45
|
-
end
|
|
73
|
+
hook_class.new(@config, @context)
|
|
74
|
+
rescue LoadError, NameError => e
|
|
75
|
+
raise Overcommit::Exceptions::HookLoadError,
|
|
76
|
+
"Unable to load hook '#{hook_name}': #{e}",
|
|
77
|
+
e.backtrace
|
|
46
78
|
end
|
|
47
79
|
end
|
|
48
80
|
end
|
|
@@ -6,7 +6,8 @@ module Overcommit::HookLoader
|
|
|
6
6
|
def load_hooks
|
|
7
7
|
@config.enabled_builtin_hooks(@context).map do |hook_name|
|
|
8
8
|
underscored_hook_name = Overcommit::Utils.snake_case(hook_name)
|
|
9
|
-
require
|
|
9
|
+
require File.join(Overcommit::HOOK_DIRECTORY, @context.hook_type_name,
|
|
10
|
+
"#{underscored_hook_name}.rb")
|
|
10
11
|
create_hook(hook_name)
|
|
11
12
|
end
|
|
12
13
|
end
|
|
@@ -5,11 +5,11 @@ require 'digest'
|
|
|
5
5
|
module Overcommit::HookLoader
|
|
6
6
|
# Responsible for loading hooks that are specific to the repository Overcommit
|
|
7
7
|
# is running in.
|
|
8
|
-
class PluginHookLoader < Base
|
|
8
|
+
class PluginHookLoader < Base # rubocop:disable Metrics/ClassLength
|
|
9
9
|
def load_hooks
|
|
10
|
-
|
|
10
|
+
check_for_unverified_plugins if @config.verify_signatures?
|
|
11
11
|
|
|
12
|
-
hooks = plugin_paths.map do |plugin_path|
|
|
12
|
+
hooks = load_gem_hooks + plugin_paths.map do |plugin_path|
|
|
13
13
|
require plugin_path
|
|
14
14
|
|
|
15
15
|
hook_name = Overcommit::Utils.camel_case(File.basename(plugin_path, '.rb'))
|
|
@@ -22,9 +22,9 @@ module Overcommit::HookLoader
|
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
def update_signatures
|
|
25
|
-
log.success('
|
|
25
|
+
log.success('All plugin signatures have been verified') if unverified_plugins.empty?
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
unverified_plugins.each do |plugin|
|
|
28
28
|
plugin.update_signature!
|
|
29
29
|
log.warning "Updated signature of plugin #{plugin.hook_name}"
|
|
30
30
|
end
|
|
@@ -32,6 +32,34 @@ module Overcommit::HookLoader
|
|
|
32
32
|
|
|
33
33
|
private
|
|
34
34
|
|
|
35
|
+
def gem_plugins
|
|
36
|
+
@gem_plugins ||= @config.enabled_gem_hooks(@context).
|
|
37
|
+
map { |name| name.split('::').first }.uniq.sort.map do |namespace|
|
|
38
|
+
Overcommit::GemPlugin.new(namespace, @config, @context)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def load_gem_hooks
|
|
43
|
+
sources = gem_plugins.map { |plugin| [plugin, plugin.hook_paths] }
|
|
44
|
+
sources.flat_map do |plugin, paths|
|
|
45
|
+
hook_module = Overcommit::Hook.const_get(@context.hook_class_name)
|
|
46
|
+
if hook_module.const_defined?(plugin.namespace, false)
|
|
47
|
+
namespace = hook_module.const_get(plugin.namespace, false)
|
|
48
|
+
unless namespace.instance_of?(Module)
|
|
49
|
+
raise Overcommit::Exceptions::HookLoadError,
|
|
50
|
+
"Gem namespace #{plugin.namespace} conflicts with an existing hook"
|
|
51
|
+
end
|
|
52
|
+
else
|
|
53
|
+
hook_module.const_set(plugin.namespace, Module.new)
|
|
54
|
+
end
|
|
55
|
+
plugin.activate
|
|
56
|
+
paths.zip(plugin.hook_names).map do |path, name|
|
|
57
|
+
require path
|
|
58
|
+
create_hook(name)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
35
63
|
def plugin_paths
|
|
36
64
|
directory = File.join(@config.plugin_directory, @context.hook_type_name)
|
|
37
65
|
Dir[File.join(directory, '*.rb')].sort
|
|
@@ -47,20 +75,20 @@ module Overcommit::HookLoader
|
|
|
47
75
|
@config.enabled_ad_hoc_hooks(@context)
|
|
48
76
|
end
|
|
49
77
|
|
|
50
|
-
def
|
|
51
|
-
(plugin_hook_names + ad_hoc_hook_names).
|
|
52
|
-
map { |hook_name| Overcommit::HookSigner.new(hook_name, @config, @context) }
|
|
53
|
-
|
|
78
|
+
def unverified_plugins
|
|
79
|
+
signers = (plugin_hook_names + ad_hoc_hook_names).
|
|
80
|
+
map { |hook_name| Overcommit::HookSigner.new(hook_name, @config, @context) }
|
|
81
|
+
(gem_plugins + signers).reject(&:signature_verified?)
|
|
54
82
|
end
|
|
55
83
|
|
|
56
|
-
def
|
|
57
|
-
return if
|
|
84
|
+
def check_for_unverified_plugins
|
|
85
|
+
return if unverified_plugins.empty?
|
|
58
86
|
|
|
59
87
|
log.bold_warning "The following #{@context.hook_script_name} plugins " \
|
|
60
88
|
'have been added, changed, or had their configuration modified:'
|
|
61
89
|
log.newline
|
|
62
90
|
|
|
63
|
-
|
|
91
|
+
unverified_plugins.each do |signer|
|
|
64
92
|
log.warning " * #{signer.hook_name} in #{signer.hook_path}"
|
|
65
93
|
end
|
|
66
94
|
|
|
@@ -74,16 +102,12 @@ module Overcommit::HookLoader
|
|
|
74
102
|
raise Overcommit::Exceptions::InvalidHookSignature
|
|
75
103
|
end
|
|
76
104
|
|
|
77
|
-
def
|
|
78
|
-
hook_module = Overcommit::Hook.const_get(@context.hook_class_name)
|
|
79
|
-
hook_base = hook_module.const_get('Base')
|
|
80
|
-
|
|
105
|
+
def create_git_hook_class(hook_base)
|
|
81
106
|
# Implement a simple class that executes the command and returns pass/fail
|
|
82
107
|
# based on the exit status
|
|
83
|
-
|
|
108
|
+
Class.new(hook_base) do
|
|
84
109
|
def run
|
|
85
110
|
result = @context.execute_hook(command)
|
|
86
|
-
|
|
87
111
|
if result.success?
|
|
88
112
|
:pass
|
|
89
113
|
else
|
|
@@ -91,6 +115,25 @@ module Overcommit::HookLoader
|
|
|
91
115
|
end
|
|
92
116
|
end
|
|
93
117
|
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def create_ad_hoc_hook(hook_name)
|
|
121
|
+
hook_module = Overcommit::Hook.const_get(@context.hook_class_name)
|
|
122
|
+
hook_base = hook_module.const_get('Base')
|
|
123
|
+
|
|
124
|
+
hook_config = @config.for_hook(hook_name, @context.hook_class_name)
|
|
125
|
+
hook_class =
|
|
126
|
+
if hook_config['message_pattern']
|
|
127
|
+
validate_line_aware_config(hook_config)
|
|
128
|
+
create_line_aware_command_hook_class(hook_base)
|
|
129
|
+
else
|
|
130
|
+
create_git_hook_class(hook_base)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# A later load may configure the same hook to use a different command format.
|
|
134
|
+
if hook_module.const_defined?(hook_name, false)
|
|
135
|
+
hook_module.send(:remove_const, hook_name)
|
|
136
|
+
end
|
|
94
137
|
|
|
95
138
|
hook_module.const_set(hook_name, hook_class).new(@config, @context)
|
|
96
139
|
rescue LoadError, NameError => e
|
|
@@ -197,7 +197,7 @@ module Overcommit
|
|
|
197
197
|
end
|
|
198
198
|
|
|
199
199
|
def load_hooks
|
|
200
|
-
require
|
|
200
|
+
require File.join(Overcommit::HOOK_DIRECTORY, @context.hook_type_name, 'base.rb')
|
|
201
201
|
|
|
202
202
|
@hooks += HookLoader::BuiltInHookLoader.new(@config, @context, @log).load_hooks
|
|
203
203
|
|
|
@@ -45,7 +45,7 @@ module Overcommit
|
|
|
45
45
|
'must be tracked by Git in order to be signed'
|
|
46
46
|
end
|
|
47
47
|
|
|
48
|
-
File.
|
|
48
|
+
File.expand_path(command.first.to_s, Overcommit::Utils.repo_root)
|
|
49
49
|
end
|
|
50
50
|
end
|
|
51
51
|
end
|
|
@@ -58,25 +58,17 @@ module Overcommit
|
|
|
58
58
|
file.start_with?(Overcommit::Utils.repo_root)
|
|
59
59
|
end
|
|
60
60
|
|
|
61
|
-
# Return whether
|
|
62
|
-
#
|
|
61
|
+
# Return whether this exact combination of hook code and configuration
|
|
62
|
+
# has been explicitly trusted before.
|
|
63
63
|
#
|
|
64
64
|
# @return [true,false]
|
|
65
|
-
def
|
|
66
|
-
signature
|
|
65
|
+
def signature_verified?
|
|
66
|
+
signature_store.include?(signature)
|
|
67
67
|
end
|
|
68
68
|
|
|
69
|
-
#
|
|
69
|
+
# Remember the current signature without forgetting older approvals.
|
|
70
70
|
def update_signature!
|
|
71
|
-
|
|
72
|
-
result = Overcommit::Utils.execute(
|
|
73
|
-
%W[git config #{scope}] + [signature_config_key, signature]
|
|
74
|
-
)
|
|
75
|
-
|
|
76
|
-
unless result.success?
|
|
77
|
-
raise Overcommit::Exceptions::GitConfigError,
|
|
78
|
-
"Unable to write to local repo git config: #{result.stderr}"
|
|
79
|
-
end
|
|
71
|
+
signature_store.add(signature)
|
|
80
72
|
end
|
|
81
73
|
|
|
82
74
|
private
|
|
@@ -103,20 +95,8 @@ module Overcommit
|
|
|
103
95
|
File.read(hook_path)
|
|
104
96
|
end
|
|
105
97
|
|
|
106
|
-
def
|
|
107
|
-
|
|
108
|
-
result = Overcommit::Utils.execute(
|
|
109
|
-
%W[git config #{scope} --get] + [signature_config_key]
|
|
110
|
-
)
|
|
111
|
-
|
|
112
|
-
if result.status == 1 # Key doesn't exist
|
|
113
|
-
return ''
|
|
114
|
-
elsif result.status != 0
|
|
115
|
-
raise Overcommit::Exceptions::GitConfigError,
|
|
116
|
-
"Unable to read from local repo git config: #{result.stderr}"
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
result.stdout.chomp
|
|
98
|
+
def signature_store
|
|
99
|
+
@signature_store ||= Overcommit::Signature.new(signature_config_key)
|
|
120
100
|
end
|
|
121
101
|
|
|
122
102
|
def signature_config_key
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'digest'
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
|
|
6
|
+
module Overcommit
|
|
7
|
+
# Repository-local approvals, indexed by hook identity and SHA256 signature.
|
|
8
|
+
# These files are outside Git's object database so fetching cannot add trust.
|
|
9
|
+
class Signature
|
|
10
|
+
def initialize(config_key)
|
|
11
|
+
@config_key = config_key
|
|
12
|
+
scope = Overcommit::GitConfig.signature_scope
|
|
13
|
+
root = if scope == '--worktree'
|
|
14
|
+
Overcommit::Utils.git_path('overcommit-signatures')
|
|
15
|
+
else
|
|
16
|
+
File.join(Overcommit::Utils.git_dir, 'overcommit-signatures')
|
|
17
|
+
end
|
|
18
|
+
@directory = File.join(root, Digest::SHA256.hexdigest(config_key))
|
|
19
|
+
migrate_legacy_signature(scope)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def include?(signature)
|
|
23
|
+
File.read(path(signature)) == contents(signature)
|
|
24
|
+
rescue Errno::ENOENT
|
|
25
|
+
false
|
|
26
|
+
rescue SystemCallError => e
|
|
27
|
+
raise Overcommit::Exceptions::SignatureError, "Unable to read signature history: #{e}"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def recorded?
|
|
31
|
+
File.directory?(@directory)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def add(signature)
|
|
35
|
+
FileUtils.mkdir_p(@directory)
|
|
36
|
+
File.write(path(signature), contents(signature), mode: 'w', perm: 0o600)
|
|
37
|
+
rescue SystemCallError => e
|
|
38
|
+
raise Overcommit::Exceptions::SignatureError, "Unable to write signature history: #{e}"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def path(signature)
|
|
44
|
+
File.join(@directory, signature)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def contents(signature)
|
|
48
|
+
"#{@config_key}\n#{signature}\n"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Preserve an existing approval before removing its legacy Git config key.
|
|
52
|
+
# Removing the key means deleting history files actually revokes trust.
|
|
53
|
+
def migrate_legacy_signature(scope)
|
|
54
|
+
result = Overcommit::Utils.execute(%W[git config #{scope} --get] + [@config_key])
|
|
55
|
+
return if result.status == 1
|
|
56
|
+
|
|
57
|
+
check_config_result(result)
|
|
58
|
+
signature = result.stdout.chomp
|
|
59
|
+
add(signature) if signature.match?(/\A[0-9a-f]{64}\z/)
|
|
60
|
+
result = Overcommit::Utils.execute(%W[git config #{scope} --unset-all] + [@config_key])
|
|
61
|
+
check_config_result(result) unless result.status == 5 # Already removed by another process.
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def check_config_result(result)
|
|
65
|
+
return if result.success?
|
|
66
|
+
|
|
67
|
+
raise Overcommit::Exceptions::GitConfigError,
|
|
68
|
+
"Unable to migrate local repo git config signature: #{result.stderr}"
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -35,7 +35,8 @@ module Overcommit
|
|
|
35
35
|
process.start
|
|
36
36
|
if options[:input]
|
|
37
37
|
begin
|
|
38
|
-
process.io.stdin.
|
|
38
|
+
process.io.stdin.binmode
|
|
39
|
+
process.io.stdin.write(options[:input])
|
|
39
40
|
rescue StandardError
|
|
40
41
|
# Silently ignore if the standard input stream of the spawned
|
|
41
42
|
# process is closed before we get a chance to write to it. This
|
|
@@ -37,6 +37,20 @@ module Overcommit::Utils
|
|
|
37
37
|
end
|
|
38
38
|
end
|
|
39
39
|
|
|
40
|
+
def create_type_categorizer(warning_pattern)
|
|
41
|
+
return nil if warning_pattern.nil?
|
|
42
|
+
|
|
43
|
+
lambda do |type|
|
|
44
|
+
warning =
|
|
45
|
+
if warning_pattern.is_a?(Regexp)
|
|
46
|
+
type && warning_pattern.match?(type)
|
|
47
|
+
else
|
|
48
|
+
type&.include?(warning_pattern)
|
|
49
|
+
end
|
|
50
|
+
warning ? :warning : :error
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
40
54
|
private
|
|
41
55
|
|
|
42
56
|
def extract_file(match, message)
|
data/lib/overcommit/utils.rb
CHANGED
|
@@ -166,12 +166,7 @@ module Overcommit
|
|
|
166
166
|
return if Process.ppid.zero?
|
|
167
167
|
|
|
168
168
|
if OS.windows?
|
|
169
|
-
|
|
170
|
-
'powershell.exe', '-NoProfile', '-NonInteractive', '-Command',
|
|
171
|
-
"(Get-CimInstance Win32_Process -Filter 'ProcessId = #{Process.ppid}').CommandLine",
|
|
172
|
-
])
|
|
173
|
-
command = result.stdout.strip
|
|
174
|
-
command if result.success? && !command.empty?
|
|
169
|
+
windows_parent_command
|
|
175
170
|
elsif OS.cygwin?
|
|
176
171
|
# Cygwin's `ps` command behaves differently than the traditional
|
|
177
172
|
# Linux version, but a comparable `procps` is provided to compensate.
|
|
@@ -294,6 +289,69 @@ module Overcommit
|
|
|
294
289
|
|
|
295
290
|
private
|
|
296
291
|
|
|
292
|
+
def windows_parent_command
|
|
293
|
+
native_windows_parent_command
|
|
294
|
+
rescue LoadError, StandardError
|
|
295
|
+
# win32ole is optional and absent from newer Ruby distributions.
|
|
296
|
+
# Use Script Host when it is unavailable or the COM query is denied.
|
|
297
|
+
scripted_windows_parent_command
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def scripted_windows_parent_command
|
|
301
|
+
begin
|
|
302
|
+
result = execute([
|
|
303
|
+
'cscript.exe', '//nologo', '//B', '//U',
|
|
304
|
+
File.expand_path('../../libexec/overcommit-parent-command.js', __dir__),
|
|
305
|
+
Process.ppid.to_s,
|
|
306
|
+
])
|
|
307
|
+
if result.success?
|
|
308
|
+
command = result.stdout.dup.force_encoding('UTF-16LE').encode('UTF-8').
|
|
309
|
+
delete_prefix("\uFEFF").strip
|
|
310
|
+
return command.empty? ? nil : command
|
|
311
|
+
end
|
|
312
|
+
rescue Errno::ENOENT, ChildProcess::LaunchError, EncodingError
|
|
313
|
+
# Preserve PowerShell when Script Host is disabled or unavailable.
|
|
314
|
+
end
|
|
315
|
+
powershell_windows_parent_command
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def powershell_windows_parent_command
|
|
319
|
+
result = execute([
|
|
320
|
+
'powershell.exe', '-NoProfile', '-NonInteractive', '-Command',
|
|
321
|
+
<<~POWERSHELL,
|
|
322
|
+
$parentId = #{Process.ppid}
|
|
323
|
+
for ($level = 0; $level -lt 4; $level++) {
|
|
324
|
+
$process = Get-CimInstance Win32_Process -Filter "ProcessId = $parentId"
|
|
325
|
+
if (!$process) { break }
|
|
326
|
+
if ($process.Name -ine 'env.exe') { $process.CommandLine; break }
|
|
327
|
+
$parentId = $process.ParentProcessId
|
|
328
|
+
}
|
|
329
|
+
POWERSHELL
|
|
330
|
+
])
|
|
331
|
+
command = result.stdout.strip
|
|
332
|
+
command if result.success? && !command.empty?
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
# Avoid starting PowerShell for every query when Ruby provides COM support.
|
|
336
|
+
def native_windows_parent_command
|
|
337
|
+
require 'win32ole'
|
|
338
|
+
service = WIN32OLE.connect('winmgmts:root/cimv2')
|
|
339
|
+
parent_id = Process.ppid
|
|
340
|
+
# Git for Windows' /usr/bin/env launcher remains between Git and Ruby.
|
|
341
|
+
4.times do
|
|
342
|
+
processes = service.ExecQuery(
|
|
343
|
+
'SELECT Name, CommandLine, ParentProcessId FROM Win32_Process ' \
|
|
344
|
+
"WHERE ProcessId = #{parent_id}"
|
|
345
|
+
)
|
|
346
|
+
process = processes.to_enum.first
|
|
347
|
+
return unless process
|
|
348
|
+
return process.CommandLine unless process.Name.to_s.casecmp?('env.exe')
|
|
349
|
+
|
|
350
|
+
parent_id = process.ParentProcessId
|
|
351
|
+
end
|
|
352
|
+
nil
|
|
353
|
+
end
|
|
354
|
+
|
|
297
355
|
# Log debug output.
|
|
298
356
|
#
|
|
299
357
|
# This is necessary since some specs indirectly call utility functions but
|