rails_git_hooks 0.7.0 → 0.7.1

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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +18 -6
  3. data/README.md +143 -77
  4. data/lib/rails_git_hooks/checks/base.rb +55 -0
  5. data/lib/rails_git_hooks/checks/commit_msg/jira_prefix.rb +31 -0
  6. data/lib/rails_git_hooks/checks/commit_msg.rb +10 -0
  7. data/lib/rails_git_hooks/checks/pre_commit/debugger_check.rb +43 -0
  8. data/lib/rails_git_hooks/checks/pre_commit/default_branch.rb +20 -0
  9. data/lib/rails_git_hooks/checks/pre_commit/json_format_check.rb +30 -0
  10. data/lib/rails_git_hooks/checks/pre_commit/migrations_check.rb +37 -0
  11. data/lib/rails_git_hooks/checks/pre_commit/rubocop.rb +30 -0
  12. data/lib/rails_git_hooks/checks/pre_commit/whitespace_check.rb +31 -0
  13. data/lib/rails_git_hooks/checks/pre_commit/yaml_format_check.rb +31 -0
  14. data/lib/rails_git_hooks/checks/pre_commit.rb +16 -0
  15. data/lib/rails_git_hooks/checks/pre_push/run_tests.rb +24 -0
  16. data/lib/rails_git_hooks/checks/pre_push.rb +10 -0
  17. data/lib/rails_git_hooks/checks.rb +6 -0
  18. data/lib/rails_git_hooks/cli.rb +78 -59
  19. data/lib/rails_git_hooks/config/constants.rb +20 -0
  20. data/lib/rails_git_hooks/config/defaults.yml +127 -0
  21. data/lib/rails_git_hooks/config/defaults_loader.rb +42 -0
  22. data/lib/rails_git_hooks/core/check_definition.rb +63 -0
  23. data/lib/rails_git_hooks/core/check_result.rb +41 -0
  24. data/lib/rails_git_hooks/core/error.rb +5 -0
  25. data/lib/rails_git_hooks/install/installer.rb +53 -0
  26. data/lib/rails_git_hooks/runtime/check_registry.rb +29 -0
  27. data/lib/rails_git_hooks/runtime/dependency_checker.rb +51 -0
  28. data/lib/rails_git_hooks/runtime/file_matcher.rb +23 -0
  29. data/lib/rails_git_hooks/runtime/override_config.rb +125 -0
  30. data/lib/rails_git_hooks/runtime/policy_resolver.rb +36 -0
  31. data/lib/rails_git_hooks/runtime/repository.rb +61 -0
  32. data/lib/rails_git_hooks/runtime/runner.rb +76 -0
  33. data/lib/rails_git_hooks/runtime.rb +25 -0
  34. data/lib/rails_git_hooks/version.rb +1 -1
  35. data/lib/rails_git_hooks.rb +14 -6
  36. data/templates/hooks/commit-msg +7 -17
  37. data/templates/hooks/pre-commit +7 -21
  38. data/templates/hooks/pre-push +7 -17
  39. metadata +30 -9
  40. data/lib/rails_git_hooks/constants.rb +0 -21
  41. data/lib/rails_git_hooks/installer.rb +0 -156
  42. data/templates/shared/commit_msg/jira_prefix.rb +0 -20
  43. data/templates/shared/pre_commit/debugger_check.rb +0 -48
  44. data/templates/shared/pre_commit/default_branch.rb +0 -9
  45. data/templates/shared/pre_commit/rubocop_check.rb +0 -24
  46. data/templates/shared/pre_commit/whitespace_check.rb +0 -25
  47. data/templates/shared/pre_push/run_tests.rb +0 -9
@@ -1,156 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'fileutils'
4
-
5
- module GitHooks
6
- class Installer
7
- def initialize(git_dir: nil)
8
- @git_dir = git_dir || find_git_dir
9
- end
10
-
11
- def install(*hook_names)
12
- target_dir = File.join(@git_dir, 'hooks')
13
- raise GitHooks::Error, "Not a git repository or .git/hooks not found: #{@git_dir}" unless Dir.exist?(target_dir)
14
-
15
- copy_shared_files(target_dir)
16
-
17
- hooks = hook_names.empty? ? Constants::DEFAULT_HOOKS : hook_names
18
- installed = []
19
-
20
- hooks.each do |name|
21
- src = File.join(Constants::HOOKS_DIR, name)
22
- next unless File.file?(src)
23
-
24
- dest = File.join(target_dir, name)
25
- write_hook(dest, read_template(name))
26
- make_executable(dest)
27
- installed << name
28
- end
29
-
30
- installed
31
- end
32
-
33
- def self.available_hook_names
34
- Dir.children(Constants::HOOKS_DIR).select { |f| File.file?(File.join(Constants::HOOKS_DIR, f)) }
35
- end
36
-
37
- def available_hooks
38
- self.class.available_hook_names
39
- end
40
-
41
- def disabled_file_path
42
- File.join(@git_dir, Constants::DISABLED_FILE)
43
- end
44
-
45
- def disabled_hooks
46
- path = disabled_file_path
47
- return [] unless File.exist?(path)
48
-
49
- File.read(path).split("\n").map(&:strip).reject(&:empty?)
50
- end
51
-
52
- def disable(*hook_names)
53
- path = disabled_file_path
54
- current = (disabled_hooks + hook_names).uniq
55
- File.write(path, "#{current.join("\n")}\n")
56
- hook_names
57
- end
58
-
59
- def enable(*hook_names)
60
- path = disabled_file_path
61
- return [] unless File.exist?(path)
62
-
63
- current = disabled_hooks - hook_names
64
- if current.empty?
65
- File.delete(path)
66
- else
67
- File.write(path, "#{current.join("\n")}\n")
68
- end
69
- hook_names
70
- end
71
-
72
- def enable_whitespace_check
73
- enable_feature_flag('whitespace-check')
74
- end
75
-
76
- def disable_whitespace_check
77
- disable_feature_flag('whitespace-check')
78
- end
79
-
80
- def whitespace_check_enabled?
81
- feature_flag_enabled?('whitespace-check')
82
- end
83
-
84
- def enable_rubocop_check
85
- enable_feature_flag('rubocop-check')
86
- end
87
-
88
- def disable_rubocop_check
89
- disable_feature_flag('rubocop-check')
90
- end
91
-
92
- def rubocop_check_enabled?
93
- feature_flag_enabled?('rubocop-check')
94
- end
95
-
96
- private
97
-
98
- def enable_feature_flag(name)
99
- file = Constants::FEATURE_FLAG_FILES[name]
100
- return unless file
101
-
102
- File.write(File.join(@git_dir, file), '')
103
- end
104
-
105
- def disable_feature_flag(name)
106
- file = Constants::FEATURE_FLAG_FILES[name]
107
- return unless file
108
-
109
- FileUtils.rm_f(File.join(@git_dir, file))
110
- end
111
-
112
- def feature_flag_enabled?(name)
113
- file = Constants::FEATURE_FLAG_FILES[name]
114
- file && File.exist?(File.join(@git_dir, file))
115
- end
116
-
117
- def find_git_dir
118
- dir = Dir.pwd
119
- loop do
120
- git = File.join(dir, '.git')
121
- return git if File.directory?(git)
122
-
123
- parent = File.dirname(dir)
124
- raise GitHooks::Error, 'Not inside a git repository' if parent == dir
125
-
126
- dir = parent
127
- end
128
- end
129
-
130
- def copy_shared_files(target_dir)
131
- return unless Dir.exist?(Constants::SHARED_DIR)
132
-
133
- Dir.glob(File.join(Constants::SHARED_DIR, '**', '*')).each do |src|
134
- next unless File.file?(src)
135
-
136
- rel = src.sub(%r{\A#{Regexp.escape(Constants::SHARED_DIR)}/}, '')
137
- dest = File.join(target_dir, rel)
138
- FileUtils.mkdir_p(File.dirname(dest))
139
- File.write(dest, File.read(src))
140
- end
141
- end
142
-
143
- def read_template(name)
144
- path = File.join(Constants::HOOKS_DIR, name)
145
- File.read(path)
146
- end
147
-
148
- def write_hook(path, content)
149
- File.write(path, content)
150
- end
151
-
152
- def make_executable(path)
153
- File.chmod(0o755, path)
154
- end
155
- end
156
- end
@@ -1,20 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Prepends [TICKET] to commit message when branch name contains a Jira-style ticket.
4
- # No config needed; works with any project key (2–5 letters + digits).
5
-
6
- commit_message_file = ARGV[0]
7
- branch_name = `git branch --no-color 2> /dev/null`[/^\* (.+)/, 1].to_s
8
- original_commit_message = File.read(commit_message_file).strip
9
-
10
- branch_ticket_pattern = /([A-Z]{2,5}-\d+)/i
11
- skip_if_already_prefixed = /\A\[[A-Z]{2,5}-\d+\]/i
12
-
13
- if (m = branch_name.match(branch_ticket_pattern))
14
- jira_ticket = m.captures.first
15
-
16
- unless original_commit_message.match?(skip_if_already_prefixed) || original_commit_message.include?(jira_ticket)
17
- message = "[#{jira_ticket}] #{original_commit_message}"
18
- File.open(commit_message_file, 'w') { |f| f.write message }
19
- end
20
- end
@@ -1,48 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Warns when staged files contain debugger statements (Ruby, JavaScript/TypeScript, Python).
4
- # Does not block the commit. Expects RailsGitHooks::GIT_DIR to be set by the loader.
5
-
6
- # [ extension => [ [ regex, label ], ... ] ]
7
- DEBUGGER_PATTERNS = {
8
- '.rb' => [
9
- [/\bbinding\.pry\b/, 'binding.pry'],
10
- [/\bbinding\.irb\b/, 'binding.irb'],
11
- [/\bdebugger\b/, 'debugger'],
12
- [/\bbyebug\b/, 'byebug']
13
- ],
14
- '.js' => [[/\bdebugger\s*;?/, 'debugger']],
15
- '.jsx' => [[/\bdebugger\s*;?/, 'debugger']],
16
- '.ts' => [[/\bdebugger\s*;?/, 'debugger']],
17
- '.tsx' => [[/\bdebugger\s*;?/, 'debugger']],
18
- '.mjs' => [[/\bdebugger\s*;?/, 'debugger']],
19
- '.cjs' => [[/\bdebugger\s*;?/, 'debugger']],
20
- '.py' => [
21
- [/\bbreakpoint\s*\(\s*\)/, 'breakpoint()'],
22
- [/\bpdb\.set_trace\s*\(\s*\)/, 'pdb.set_trace()'],
23
- [/\bipdb\.set_trace\s*\(\s*\)/, 'ipdb.set_trace()']
24
- ]
25
- }.freeze
26
-
27
- staged = `git diff --cached --name-only`.split("\n").map(&:strip).reject(&:empty?)
28
- warnings = []
29
-
30
- staged.each do |path|
31
- next unless File.file?(path)
32
-
33
- ext = File.extname(path)
34
- patterns = DEBUGGER_PATTERNS[ext]
35
- next unless patterns
36
-
37
- File.read(path).lines.each_with_index do |line, i|
38
- patterns.each do |regex, label|
39
- warnings << "#{path}:#{i + 1}: #{label}" if line.match?(regex)
40
- end
41
- end
42
- end
43
-
44
- unless warnings.empty?
45
- warn 'Warning (debugger check):'
46
- warnings.uniq.each { |e| warn " #{e}" }
47
- end
48
- # Does not exit 1 — commit is never blocked by this check
@@ -1,9 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Prevent commits on default branch (master/main).
4
-
5
- branch = `git rev-parse --abbrev-ref HEAD`.strip
6
- if %w[master main].include?(branch)
7
- warn "Commits on '#{branch}' are not allowed. Create a feature branch."
8
- exit 1
9
- end
@@ -1,24 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # RuboCop on staged .rb files (off by default; enable with rails_git_hooks enable rubocop-check).
4
- # Expects RailsGitHooks::GIT_DIR to be set by the loader.
5
-
6
- git_dir = RailsGitHooks::GIT_DIR
7
- rubocop_check_file = File.join(git_dir, 'rails_git_hooks_rubocop')
8
- if File.exist?(rubocop_check_file)
9
- require 'english'
10
- require 'rubocop'
11
-
12
- ADDED_OR_MODIFIED = /A|AM|^M/.freeze
13
-
14
- changed_files = `git status --porcelain`.split(/\n/)
15
- .select { |file_name_with_status| file_name_with_status =~ ADDED_OR_MODIFIED }
16
- .map { |file_name_with_status| file_name_with_status.split(' ')[1] }
17
- .select { |file_name| File.extname(file_name) == '.rb' }
18
- .join(' ')
19
-
20
- unless changed_files.empty?
21
- system("rubocop #{changed_files}")
22
- exit $CHILD_STATUS.to_s[-1].to_i
23
- end
24
- end
@@ -1,25 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Trailing whitespace / conflict markers (off by default; enable with rails_git_hooks enable whitespace-check).
4
- # Expects RailsGitHooks::GIT_DIR to be set by the loader.
5
-
6
- git_dir = RailsGitHooks::GIT_DIR
7
- whitespace_check_file = File.join(git_dir, 'rails_git_hooks_whitespace_check')
8
- if File.exist?(whitespace_check_file)
9
- staged = `git diff --cached --name-only`.split("\n").map(&:strip).reject(&:empty?)
10
- errors = []
11
- staged.each do |path|
12
- next unless File.file?(path)
13
-
14
- File.read(path).lines.each_with_index do |line, i|
15
- errors << "#{path}:#{i + 1}: trailing whitespace" if line.match?(/[ \t]\z/)
16
- stripped = line.strip
17
- errors << "#{path}:#{i + 1}: conflict marker" if stripped.start_with?('<<<<<<<', '=======', '>>>>>>>')
18
- end
19
- end
20
- unless errors.empty?
21
- warn 'Commit rejected (whitespace/conflict check):'
22
- errors.uniq.each { |e| warn " #{e}" }
23
- exit 1
24
- end
25
- end
@@ -1,9 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Runs the full test suite before push; aborts push if tests fail.
4
- # Uses bundle exec rspec. For Minitest, edit to use bundle exec rake test.
5
-
6
- require 'english'
7
-
8
- system('bundle exec rspec')
9
- exit $CHILD_STATUS.to_s[-1].to_i