rails_git_hooks 0.7.0 → 0.7.2
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/CHANGELOG.md +33 -6
- data/README.md +72 -115
- data/lib/rails_git_hooks/checks/base.rb +55 -0
- data/lib/rails_git_hooks/checks/commit_msg/jira_prefix.rb +31 -0
- data/lib/rails_git_hooks/checks/commit_msg.rb +10 -0
- data/lib/rails_git_hooks/checks/post_checkout/bundle_install.rb +16 -0
- data/lib/rails_git_hooks/checks/post_checkout/db_migrate.rb +16 -0
- data/lib/rails_git_hooks/checks/post_checkout.rb +11 -0
- data/lib/rails_git_hooks/checks/post_merge/bundle_install.rb +16 -0
- data/lib/rails_git_hooks/checks/post_merge/db_migrate.rb +16 -0
- data/lib/rails_git_hooks/checks/post_merge.rb +11 -0
- data/lib/rails_git_hooks/checks/pre_commit/debugger_check.rb +43 -0
- data/lib/rails_git_hooks/checks/pre_commit/default_branch.rb +20 -0
- data/lib/rails_git_hooks/checks/pre_commit/json_format_check.rb +30 -0
- data/lib/rails_git_hooks/checks/pre_commit/migrations_check.rb +37 -0
- data/lib/rails_git_hooks/checks/pre_commit/rubocop.rb +30 -0
- data/lib/rails_git_hooks/checks/pre_commit/whitespace_check.rb +31 -0
- data/lib/rails_git_hooks/checks/pre_commit/yaml_format_check.rb +31 -0
- data/lib/rails_git_hooks/checks/pre_commit.rb +16 -0
- data/lib/rails_git_hooks/checks/pre_push/run_tests.rb +24 -0
- data/lib/rails_git_hooks/checks/pre_push.rb +10 -0
- data/lib/rails_git_hooks/checks/shared/bundle_install_check.rb +28 -0
- data/lib/rails_git_hooks/checks/shared/db_migrate_check.rb +28 -0
- data/lib/rails_git_hooks/checks.rb +10 -0
- data/lib/rails_git_hooks/cli.rb +17 -90
- data/lib/rails_git_hooks/config/constants.rb +26 -0
- data/lib/rails_git_hooks/config/defaults.yml +190 -0
- data/lib/rails_git_hooks/config/defaults_loader.rb +42 -0
- data/lib/rails_git_hooks/core/check_definition.rb +63 -0
- data/lib/rails_git_hooks/core/check_result.rb +41 -0
- data/lib/rails_git_hooks/core/error.rb +5 -0
- data/lib/rails_git_hooks/install/installer.rb +79 -0
- data/lib/rails_git_hooks/runtime/check_registry.rb +33 -0
- data/lib/rails_git_hooks/runtime/dependency_checker.rb +51 -0
- data/lib/rails_git_hooks/runtime/file_matcher.rb +23 -0
- data/lib/rails_git_hooks/runtime/override_config.rb +131 -0
- data/lib/rails_git_hooks/runtime/policy_resolver.rb +36 -0
- data/lib/rails_git_hooks/runtime/repository.rb +69 -0
- data/lib/rails_git_hooks/runtime/runner.rb +80 -0
- data/lib/rails_git_hooks/runtime.rb +25 -0
- data/lib/rails_git_hooks/version.rb +1 -1
- data/lib/rails_git_hooks.rb +14 -6
- data/templates/hooks/commit-msg +7 -17
- data/templates/hooks/post-checkout +13 -0
- data/templates/hooks/post-merge +13 -0
- data/templates/hooks/pre-commit +7 -21
- data/templates/hooks/pre-push +7 -17
- metadata +41 -52
- data/lib/rails_git_hooks/constants.rb +0 -21
- data/lib/rails_git_hooks/installer.rb +0 -156
- data/templates/shared/commit_msg/jira_prefix.rb +0 -20
- data/templates/shared/pre_commit/debugger_check.rb +0 -48
- data/templates/shared/pre_commit/default_branch.rb +0 -9
- data/templates/shared/pre_commit/rubocop_check.rb +0 -24
- data/templates/shared/pre_commit/whitespace_check.rb +0 -25
- data/templates/shared/pre_push/run_tests.rb +0 -9
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GitHooks
|
|
4
|
+
class CheckResult
|
|
5
|
+
attr_reader :status, :messages, :reason
|
|
6
|
+
|
|
7
|
+
def initialize(status:, messages: [], reason: nil)
|
|
8
|
+
@status = status.to_sym
|
|
9
|
+
@messages = Array(messages).compact
|
|
10
|
+
@reason = reason&.to_sym
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.pass(messages: [])
|
|
14
|
+
new(status: :pass, messages: messages, reason: :pass)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.warn(messages:, reason: :warning)
|
|
18
|
+
new(status: :warn, messages: messages, reason: reason)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.fail(messages:, reason: :failure)
|
|
22
|
+
new(status: :fail, messages: messages, reason: reason)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def with_status(status)
|
|
26
|
+
self.class.new(status: status, messages: messages, reason: reason)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def pass?
|
|
30
|
+
status == :pass
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def warn?
|
|
34
|
+
status == :warn
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def fail?
|
|
38
|
+
status == :fail
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
|
|
5
|
+
module GitHooks
|
|
6
|
+
class Installer
|
|
7
|
+
def initialize(git_dir: nil)
|
|
8
|
+
if git_dir
|
|
9
|
+
@git_dir = git_dir
|
|
10
|
+
@repo = nil
|
|
11
|
+
else
|
|
12
|
+
@repo = Repository.new
|
|
13
|
+
@git_dir = @repo.git_dir
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def install
|
|
18
|
+
target_dir = File.join(@git_dir, 'hooks')
|
|
19
|
+
raise GitHooks::Error, "Not a git repository or .git/hooks not found: #{@git_dir}" unless Dir.exist?(target_dir)
|
|
20
|
+
|
|
21
|
+
copy_runtime(target_dir)
|
|
22
|
+
|
|
23
|
+
hooks = hooks_enabled_in_config.select { |name| self.class.available_hook_names.include?(name) }
|
|
24
|
+
hooks.each_with_object([]) do |name, installed|
|
|
25
|
+
dest = File.join(target_dir, name)
|
|
26
|
+
File.write(dest, File.read(File.join(Constants::HOOKS_DIR, name)))
|
|
27
|
+
File.chmod(0o755, dest)
|
|
28
|
+
installed << name
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.available_hook_names
|
|
33
|
+
Dir.children(Constants::HOOKS_DIR).select { |name| File.file?(File.join(Constants::HOOKS_DIR, name)) }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def available_hooks
|
|
37
|
+
self.class.available_hook_names
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def repo_for_config
|
|
43
|
+
@repo_for_config ||= begin
|
|
44
|
+
root = File.dirname(@git_dir)
|
|
45
|
+
Struct.new(:root, :config_path, :local_config_path).new(
|
|
46
|
+
root,
|
|
47
|
+
File.join(root, Constants::CONFIG_FILE),
|
|
48
|
+
File.join(root, Constants::CONFIG_FILE_LOCAL)
|
|
49
|
+
)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def hooks_enabled_in_config
|
|
54
|
+
override_config = OverrideConfig.new(repo: repo_for_config)
|
|
55
|
+
effective = override_config.effective_config(CheckRegistry.all)
|
|
56
|
+
effective.each_with_object([]) do |(section_name, check_configs), out|
|
|
57
|
+
hook_name = Constants::SECTION_TO_HOOK[section_name]
|
|
58
|
+
next unless hook_name && self.class.available_hook_names.include?(hook_name)
|
|
59
|
+
|
|
60
|
+
out << hook_name if check_configs.values.any? { |cfg| cfg['enabled'] == true }
|
|
61
|
+
end.uniq
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def copy_runtime(target_dir)
|
|
65
|
+
runtime_dir = File.join(target_dir, Constants::RUNTIME_DIR_NAME)
|
|
66
|
+
FileUtils.rm_rf(runtime_dir)
|
|
67
|
+
FileUtils.mkdir_p(runtime_dir)
|
|
68
|
+
|
|
69
|
+
Dir.glob(File.join(Constants::RUNTIME_SOURCE_DIR, '**', '*')).each do |src|
|
|
70
|
+
next unless File.file?(src)
|
|
71
|
+
|
|
72
|
+
rel = src.sub(%r{\A#{Regexp.escape(Constants::RUNTIME_SOURCE_DIR)}/}, '')
|
|
73
|
+
dest = File.join(runtime_dir, rel)
|
|
74
|
+
FileUtils.mkdir_p(File.dirname(dest))
|
|
75
|
+
File.write(dest, File.read(src))
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GitHooks
|
|
4
|
+
class CheckRegistry
|
|
5
|
+
CHECK_CLASSES = [
|
|
6
|
+
Checks::PreCommit::DefaultBranch,
|
|
7
|
+
Checks::PreCommit::DebuggerCheck,
|
|
8
|
+
Checks::PreCommit::YAMLFormatCheck,
|
|
9
|
+
Checks::PreCommit::JSONFormatCheck,
|
|
10
|
+
Checks::PreCommit::MigrationsCheck,
|
|
11
|
+
Checks::PreCommit::WhitespaceCheck,
|
|
12
|
+
Checks::PreCommit::RuboCop,
|
|
13
|
+
Checks::CommitMsg::JiraPrefix,
|
|
14
|
+
Checks::PrePush::RunTests,
|
|
15
|
+
Checks::PostCheckout::BundleInstall,
|
|
16
|
+
Checks::PostCheckout::DbMigrate,
|
|
17
|
+
Checks::PostMerge::BundleInstall,
|
|
18
|
+
Checks::PostMerge::DbMigrate
|
|
19
|
+
].freeze
|
|
20
|
+
|
|
21
|
+
def self.all
|
|
22
|
+
CHECK_CLASSES.map(&:definition)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.for(hook_name)
|
|
26
|
+
all.select { |definition| definition.hook == hook_name.to_sym }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.find!(key)
|
|
30
|
+
all.find { |definition| definition.key == key } || raise(GitHooks::Error, "Unknown check: #{key}")
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GitHooks
|
|
4
|
+
class DependencyChecker
|
|
5
|
+
def initialize(repo:)
|
|
6
|
+
@repo = repo
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def check(config)
|
|
10
|
+
dependencies = config.fetch('dependencies', {})
|
|
11
|
+
missing = []
|
|
12
|
+
|
|
13
|
+
Array(dependencies['executables']).each do |name|
|
|
14
|
+
missing << "missing executable: #{name}" unless executable_available?(name)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
Array(dependencies['libraries']).each do |name|
|
|
18
|
+
missing << "missing library: #{name}" unless library_available?(name)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
Array(dependencies['files']).each do |name|
|
|
22
|
+
path = File.expand_path(name, @repo.root)
|
|
23
|
+
missing << "missing file: #{name}" unless File.exist?(path)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
return CheckResult.pass if missing.empty?
|
|
27
|
+
|
|
28
|
+
hint = config['install_hint']
|
|
29
|
+
messages = missing.dup
|
|
30
|
+
messages << hint if hint && !hint.empty?
|
|
31
|
+
CheckResult.fail(messages: messages, reason: :missing_dependency)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def executable_available?(name)
|
|
37
|
+
return File.executable?(File.expand_path(name, @repo.root)) if name.include?(File::SEPARATOR)
|
|
38
|
+
|
|
39
|
+
ENV.fetch('PATH', '').split(File::PATH_SEPARATOR).any? do |dir|
|
|
40
|
+
File.executable?(File.join(dir, name))
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def library_available?(name)
|
|
45
|
+
require name
|
|
46
|
+
true
|
|
47
|
+
rescue LoadError
|
|
48
|
+
false
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GitHooks
|
|
4
|
+
module FileMatcher
|
|
5
|
+
FLAGS = File::FNM_PATHNAME | File::FNM_DOTMATCH | File::FNM_EXTGLOB
|
|
6
|
+
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def filter(paths, include_patterns:, exclude_patterns:)
|
|
10
|
+
filtered = if include_patterns.empty?
|
|
11
|
+
paths
|
|
12
|
+
else
|
|
13
|
+
paths.select { |path| matches_any?(path, include_patterns) }
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
filtered.reject { |path| matches_any?(path, exclude_patterns) }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def matches_any?(path, patterns)
|
|
20
|
+
Array(patterns).any? { |pattern| File.fnmatch?(pattern, path, FLAGS) }
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
require 'yaml'
|
|
5
|
+
|
|
6
|
+
require_relative '../config/defaults_loader'
|
|
7
|
+
|
|
8
|
+
module GitHooks
|
|
9
|
+
class OverrideConfig
|
|
10
|
+
BOOLEAN_OPTIONS = %w[enabled quiet].freeze
|
|
11
|
+
POLICY_OPTIONS = %w[on_fail on_warn on_missing_dependency].freeze
|
|
12
|
+
|
|
13
|
+
def initialize(repo:)
|
|
14
|
+
@repo = repo
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def load
|
|
18
|
+
main = load_file(@repo.config_path)
|
|
19
|
+
local = load_file(@repo.local_config_path)
|
|
20
|
+
deep_merge(main, local)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def config_for(definition)
|
|
24
|
+
base = DefaultsLoader.config_for(definition.hook_section, definition.config_name) || definition.default_config
|
|
25
|
+
data = load
|
|
26
|
+
section = data.fetch(definition.hook_section, {})
|
|
27
|
+
merged = deep_merge(base, section.fetch('ALL', {}))
|
|
28
|
+
deep_merge(merged, section.fetch(definition.config_name, {}))
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def effective_config(registry)
|
|
32
|
+
registry.group_by(&:hook_section).transform_values do |definitions|
|
|
33
|
+
definitions.to_h { |definition| [definition.config_name, config_for(definition)] }
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def set_option(definition, option, value)
|
|
38
|
+
data = load_file(@repo.config_path)
|
|
39
|
+
section = data[definition.hook_section] ||= {}
|
|
40
|
+
section[definition.config_name] ||= {}
|
|
41
|
+
|
|
42
|
+
normalized = normalize_value(option, value)
|
|
43
|
+
base = DefaultsLoader.config_for(definition.hook_section, definition.config_name) || definition.default_config
|
|
44
|
+
default_value = base[option]
|
|
45
|
+
|
|
46
|
+
if normalized == default_value
|
|
47
|
+
section[definition.config_name].delete(option)
|
|
48
|
+
else
|
|
49
|
+
section[definition.config_name][option] = normalized
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
cleanup_empty_nodes!(data, definition)
|
|
53
|
+
write(data)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def init
|
|
57
|
+
return if File.exist?(@repo.config_path)
|
|
58
|
+
|
|
59
|
+
File.write(@repo.config_path, <<~YAML)
|
|
60
|
+
# rails_git_hooks overrides
|
|
61
|
+
#
|
|
62
|
+
# Example:
|
|
63
|
+
# PreCommit:
|
|
64
|
+
# RuboCop:
|
|
65
|
+
# enabled: true
|
|
66
|
+
# on_fail: fail
|
|
67
|
+
YAML
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def load_file(path)
|
|
73
|
+
return {} unless File.exist?(path)
|
|
74
|
+
|
|
75
|
+
deep_stringify(YAML.safe_load(File.read(path), aliases: true) || {})
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def write(data)
|
|
79
|
+
if data.empty?
|
|
80
|
+
FileUtils.rm_f(@repo.config_path)
|
|
81
|
+
else
|
|
82
|
+
File.write(@repo.config_path, YAML.dump(data))
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def cleanup_empty_nodes!(data, definition)
|
|
87
|
+
section = data.fetch(definition.hook_section, {})
|
|
88
|
+
section.delete(definition.config_name) if section.fetch(definition.config_name, {}).empty?
|
|
89
|
+
data.delete(definition.hook_section) if section.empty?
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def normalize_value(option, value)
|
|
93
|
+
case option
|
|
94
|
+
when *BOOLEAN_OPTIONS
|
|
95
|
+
parse_boolean(value)
|
|
96
|
+
when *POLICY_OPTIONS
|
|
97
|
+
value.to_s
|
|
98
|
+
else
|
|
99
|
+
value
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def parse_boolean(value)
|
|
104
|
+
return value if [true, false].include?(value)
|
|
105
|
+
|
|
106
|
+
case value.to_s
|
|
107
|
+
when 'true', 'yes', 'on', '1' then true
|
|
108
|
+
when 'false', 'no', 'off', '0' then false
|
|
109
|
+
else
|
|
110
|
+
raise GitHooks::Error, "Invalid boolean value: #{value.inspect}"
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def deep_merge(base, override)
|
|
115
|
+
base.merge(override) do |_key, old, new|
|
|
116
|
+
old.is_a?(Hash) && new.is_a?(Hash) ? deep_merge(old, new) : new
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def deep_stringify(value)
|
|
121
|
+
case value
|
|
122
|
+
when Hash
|
|
123
|
+
value.each_with_object({}) { |(key, nested), out| out[key.to_s] = deep_stringify(nested) }
|
|
124
|
+
when Array
|
|
125
|
+
value.map { |nested| deep_stringify(nested) }
|
|
126
|
+
else
|
|
127
|
+
value
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GitHooks
|
|
4
|
+
class PolicyResolver
|
|
5
|
+
VALID_FAIL_POLICIES = %w[fail warn pass].freeze
|
|
6
|
+
VALID_WARN_POLICIES = %w[warn fail pass].freeze
|
|
7
|
+
|
|
8
|
+
def resolve(result, config)
|
|
9
|
+
policy = policy_for(result, config)
|
|
10
|
+
return result if policy == result.status.to_s
|
|
11
|
+
|
|
12
|
+
result.with_status(policy.to_sym)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def policy_for(result, config)
|
|
18
|
+
case result.reason
|
|
19
|
+
when :missing_dependency
|
|
20
|
+
validate(config.fetch('on_missing_dependency', 'warn'), VALID_FAIL_POLICIES)
|
|
21
|
+
else
|
|
22
|
+
case result.status
|
|
23
|
+
when :fail then validate(config.fetch('on_fail', 'fail'), VALID_FAIL_POLICIES)
|
|
24
|
+
when :warn then validate(config.fetch('on_warn', 'warn'), VALID_WARN_POLICIES)
|
|
25
|
+
else 'pass'
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def validate(value, valid_values)
|
|
31
|
+
return value if valid_values.include?(value)
|
|
32
|
+
|
|
33
|
+
raise GitHooks::Error, "Invalid policy value: #{value.inspect}"
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'open3'
|
|
4
|
+
|
|
5
|
+
module GitHooks
|
|
6
|
+
class Repository
|
|
7
|
+
attr_reader :root, :git_dir
|
|
8
|
+
|
|
9
|
+
def initialize(start_dir = Dir.pwd)
|
|
10
|
+
@root, @git_dir = resolve_paths(start_dir)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def config_path
|
|
14
|
+
File.join(root, Constants::CONFIG_FILE)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def local_config_path
|
|
18
|
+
File.join(root, Constants::CONFIG_FILE_LOCAL)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def hook_runtime_dir(hooks_dir)
|
|
22
|
+
File.join(hooks_dir, Constants::RUNTIME_DIR_NAME)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def git(*args)
|
|
26
|
+
stdout, status = Open3.capture2e('git', *args, chdir: root)
|
|
27
|
+
[stdout, status]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def git_output(*args)
|
|
31
|
+
stdout, status = git(*args)
|
|
32
|
+
raise GitHooks::Error, stdout.strip unless status.success?
|
|
33
|
+
|
|
34
|
+
stdout
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def current_branch
|
|
38
|
+
git_output('rev-parse', '--abbrev-ref', 'HEAD').strip
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def staged_files
|
|
42
|
+
git_output('diff', '--cached', '--name-only').split("\n").map(&:strip).reject(&:empty?)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def changed_files(ref1, ref2)
|
|
46
|
+
git_output('diff', '--name-only', ref1.to_s, ref2.to_s).split("\n").map(&:strip).reject(&:empty?)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def resolve_paths(start_dir)
|
|
52
|
+
dir = File.expand_path(start_dir)
|
|
53
|
+
loop do
|
|
54
|
+
git_entry = File.join(dir, '.git')
|
|
55
|
+
if File.directory?(git_entry)
|
|
56
|
+
return [dir, git_entry]
|
|
57
|
+
elsif File.file?(git_entry)
|
|
58
|
+
git_dir = File.expand_path(File.read(git_entry).strip.sub(/\Agitdir: \s*/, ''), dir)
|
|
59
|
+
return [dir, git_dir]
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
parent = File.dirname(dir)
|
|
63
|
+
raise GitHooks::Error, 'Not inside a git repository' if parent == dir
|
|
64
|
+
|
|
65
|
+
dir = parent
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GitHooks
|
|
4
|
+
class Runner
|
|
5
|
+
def initialize(repo:, hook_name:, argv:, stdin:)
|
|
6
|
+
@repo = repo
|
|
7
|
+
@hook_name = hook_name.to_sym
|
|
8
|
+
@argv = argv
|
|
9
|
+
@stdin = stdin
|
|
10
|
+
@overrides = OverrideConfig.new(repo: repo)
|
|
11
|
+
@dependencies = DependencyChecker.new(repo: repo)
|
|
12
|
+
@policy = PolicyResolver.new
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def run
|
|
16
|
+
failed = false
|
|
17
|
+
|
|
18
|
+
CheckRegistry.for(@hook_name).each do |definition|
|
|
19
|
+
config = @overrides.config_for(definition)
|
|
20
|
+
next unless config['enabled']
|
|
21
|
+
|
|
22
|
+
context = {
|
|
23
|
+
repo: @repo,
|
|
24
|
+
argv: @argv,
|
|
25
|
+
stdin: @stdin,
|
|
26
|
+
applicable_files: applicable_files_for(config)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
raw_result = @dependencies.check(config)
|
|
30
|
+
raw_result = definition.klass.new(config: config, context: context).run if raw_result.pass?
|
|
31
|
+
final_result = @policy.resolve(raw_result, config)
|
|
32
|
+
|
|
33
|
+
print_result(definition, final_result, quiet: config['quiet'])
|
|
34
|
+
failed ||= final_result.fail?
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
failed ? 1 : 0
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def applicable_files_for(config)
|
|
43
|
+
return [] unless config['file_based']
|
|
44
|
+
|
|
45
|
+
FileMatcher.filter(
|
|
46
|
+
modified_files,
|
|
47
|
+
include_patterns: Array(config['include']),
|
|
48
|
+
exclude_patterns: Array(config['exclude'])
|
|
49
|
+
)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def modified_files
|
|
53
|
+
@modified_files ||= case @hook_name
|
|
54
|
+
when :pre_commit then @repo.staged_files
|
|
55
|
+
when :post_checkout
|
|
56
|
+
argv[2] == '1' ? @repo.changed_files(argv[0], argv[1]) : []
|
|
57
|
+
when :post_merge
|
|
58
|
+
@repo.changed_files('ORIG_HEAD', 'HEAD')
|
|
59
|
+
else []
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def print_result(definition, result, quiet:)
|
|
64
|
+
return if result.pass? && quiet
|
|
65
|
+
return if result.pass? && result.messages.empty?
|
|
66
|
+
|
|
67
|
+
case result.status
|
|
68
|
+
when :warn
|
|
69
|
+
warn ''
|
|
70
|
+
warn "Warning (#{definition.config_name}):"
|
|
71
|
+
when :fail
|
|
72
|
+
warn ''
|
|
73
|
+
warn "Commit rejected (#{definition.config_name}):"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
result.messages.each { |message| warn " #{message}" } unless result.pass?
|
|
77
|
+
warn '' unless result.pass?
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'config/constants'
|
|
4
|
+
require_relative 'core/error'
|
|
5
|
+
require_relative 'core/check_result'
|
|
6
|
+
require_relative 'core/check_definition'
|
|
7
|
+
require_relative 'runtime/repository'
|
|
8
|
+
require_relative 'runtime/file_matcher'
|
|
9
|
+
require_relative 'runtime/dependency_checker'
|
|
10
|
+
require_relative 'runtime/policy_resolver'
|
|
11
|
+
require_relative 'runtime/override_config'
|
|
12
|
+
require_relative 'checks'
|
|
13
|
+
require_relative 'runtime/check_registry'
|
|
14
|
+
require_relative 'runtime/runner'
|
|
15
|
+
|
|
16
|
+
module GitHooks
|
|
17
|
+
module Runtime
|
|
18
|
+
module_function
|
|
19
|
+
|
|
20
|
+
def execute(hook_name, argv: [], stdin: '')
|
|
21
|
+
repo = Repository.new
|
|
22
|
+
Runner.new(repo: repo, hook_name: hook_name, argv: argv, stdin: stdin).run
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
data/lib/rails_git_hooks.rb
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative 'rails_git_hooks/version'
|
|
4
|
-
require_relative 'rails_git_hooks/
|
|
5
|
-
require_relative 'rails_git_hooks/
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
4
|
+
require_relative 'rails_git_hooks/core/error'
|
|
5
|
+
require_relative 'rails_git_hooks/config/constants'
|
|
6
|
+
require_relative 'rails_git_hooks/core/check_result'
|
|
7
|
+
require_relative 'rails_git_hooks/core/check_definition'
|
|
8
|
+
require_relative 'rails_git_hooks/runtime/repository'
|
|
9
|
+
require_relative 'rails_git_hooks/runtime/file_matcher'
|
|
10
|
+
require_relative 'rails_git_hooks/runtime/dependency_checker'
|
|
11
|
+
require_relative 'rails_git_hooks/runtime/policy_resolver'
|
|
12
|
+
require_relative 'rails_git_hooks/runtime/override_config'
|
|
13
|
+
require_relative 'rails_git_hooks/checks'
|
|
14
|
+
require_relative 'rails_git_hooks/runtime/check_registry'
|
|
15
|
+
require_relative 'rails_git_hooks/runtime/runner'
|
|
16
|
+
require_relative 'rails_git_hooks/install/installer'
|
|
17
|
+
require_relative 'rails_git_hooks/runtime'
|
data/templates/hooks/commit-msg
CHANGED
|
@@ -1,23 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
repo_root = Dir.pwd
|
|
10
|
-
git_dir = File.join(repo_root, '.git')
|
|
11
|
-
git_dir = File.expand_path(File.read(git_dir).strip.sub(/\Agitdir: \s*/, ''), repo_root) if File.file?(git_dir)
|
|
12
|
-
disabled_file = File.join(git_dir, 'rails_git_hooks_disabled')
|
|
13
|
-
if File.exist?(disabled_file)
|
|
14
|
-
disabled = File.read(disabled_file).split("\n").map(&:strip)
|
|
15
|
-
exit 0 if disabled.include?('*') || disabled.include?('commit-msg')
|
|
4
|
+
hooks_dir = File.dirname(File.expand_path(__FILE__))
|
|
5
|
+
begin
|
|
6
|
+
require 'bundler/setup'
|
|
7
|
+
rescue LoadError, StandardError
|
|
8
|
+
nil
|
|
16
9
|
end
|
|
17
10
|
|
|
18
|
-
|
|
19
|
-
end
|
|
20
|
-
RailsGitHooks.const_set(:GIT_DIR, git_dir.freeze)
|
|
21
|
-
hooks_dir = File.dirname(File.expand_path(__FILE__))
|
|
11
|
+
require File.join(hooks_dir, 'rails_git_hooks', 'runtime')
|
|
22
12
|
|
|
23
|
-
|
|
13
|
+
exit GitHooks::Runtime.execute(:commit_msg, argv: ARGV, stdin: $stdin.read)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
hooks_dir = File.dirname(File.expand_path(__FILE__))
|
|
5
|
+
begin
|
|
6
|
+
require 'bundler/setup'
|
|
7
|
+
rescue LoadError, StandardError
|
|
8
|
+
nil
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
require File.join(hooks_dir, 'rails_git_hooks', 'runtime')
|
|
12
|
+
|
|
13
|
+
exit GitHooks::Runtime.execute(:post_checkout, argv: ARGV, stdin: $stdin.read)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
hooks_dir = File.dirname(File.expand_path(__FILE__))
|
|
5
|
+
begin
|
|
6
|
+
require 'bundler/setup'
|
|
7
|
+
rescue LoadError, StandardError
|
|
8
|
+
nil
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
require File.join(hooks_dir, 'rails_git_hooks', 'runtime')
|
|
12
|
+
|
|
13
|
+
exit GitHooks::Runtime.execute(:post_merge, argv: ARGV, stdin: $stdin.read)
|