rails_git_hooks 0.6.0 → 0.7.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ec622102778f161f06cc57a88c9a749044554a83a7a8c4abdbcc1e423eb287f3
4
- data.tar.gz: e03e9d4ca6b32fbb1c29fb40422096bdd013712e564df3b8bbefb6c358703d93
3
+ metadata.gz: 3b990407e471e8f6dddf8df8741a8ac6de92955bc44470b1b45a19b93644b542
4
+ data.tar.gz: ed3f378f82db9a1dc81dcf58d90f02d12da0ff32c05697ecc2f1dbb92f8db2ca
5
5
  SHA512:
6
- metadata.gz: 15701ccb101a859a300bec674cf5290fdaa9f86f6fa08555d07d64622b3536cc99b60085b057095753d3ac7d7ae5bbec46f650510b7ccfea7fbcb47b87a40170
7
- data.tar.gz: c5f315f7fd2ae74107c85f18dab6f344010394a3263fe3e8ab1dcdb4e5ae12f2332a34f15687ba07b08f0fe5e27ef37efa1e750743855d4e90b2a3d3d2b26ec1
6
+ metadata.gz: 0b1aa8a31af84e697e05f56d94ff3427a0afa82e0dfabe4de21e5f0ec865f7ba6c7ddd0537f058e852281a415f68b26a063fd6ef8e71f0d0f1a0b7f9c718aa04
7
+ data.tar.gz: 966f4fbd5dcfac4ce1e4a9ad1d8a3f8209f0d2885030ece2d916c5598e2324aeac348f7bc8e9690e60bd3676fc995e264e0c2e2b1fd80e51cd1563d4cf3bb4d0
data/CHANGELOG.md CHANGED
@@ -4,7 +4,30 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
6
6
 
7
- ## [0.6.0] (latest)
7
+ ## [0.7.0] (latest)
8
+
9
+ ### Added
10
+
11
+ - **Pre-commit debugger check** — Warns (does not block) when staged files contain debugger statements: Ruby (`binding.pry`, `debugger`, `byebug`, `binding.irb`), JavaScript/TypeScript (`.js`, `.jsx`, `.ts`, `.tsx`, `.mjs`, `.cjs` — `debugger`), Python (`breakpoint()`, `pdb.set_trace()`, `ipdb.set_trace()`). On by default with pre-commit.
12
+
13
+ ### Changed
14
+
15
+ - **Overcommit-style layout:** Hook templates live in `templates/hooks/` (was `lib/rails_git_hooks/templates/`). `rake sync_hooks` copies them to `hooks/` for manual install.
16
+ - **Constants:** Extracted `lib/rails_git_hooks/constants.rb` (GEM_ROOT, HOOKS_DIR, DEFAULT_HOOKS, FEATURE_FLAG_FILES). Installer and CLI use it. Single source of truth; no config file.
17
+ - Gemspec includes `templates/**/*`. Version set to 0.7.0.
18
+
19
+ ### Fixed
20
+
21
+ - **Hook scripts** — `RailsGitHooks::GIT_DIR` was set inside a `module` body, where the script’s local `git_dir` is out of scope (Ruby scope gate), causing `NameError` when hooks ran. All three hooks (commit-msg, pre-commit, pre-push) now set the constant via `RailsGitHooks.const_set(:GIT_DIR, git_dir.freeze)` from the script scope.
22
+
23
+ ## [0.6.1]
24
+
25
+ ### Changed
26
+
27
+ - **Default install** now installs **commit-msg** and **pre-commit** only (Jira ticket prefix + default-branch protection; RuboCop opt-in). Pre-push remains opt-in: `rails_git_hooks install pre-push`.
28
+ - README: quick start and commands table updated for default (commit-msg + pre-commit); Jira project key / `JIRA_PROJECT_KEY` for manual install; pre-push install instruction.
29
+
30
+ ## [0.6.0]
8
31
 
9
32
  ### Added
10
33
 
data/README.md CHANGED
@@ -1,34 +1,37 @@
1
- # Git Hooks
1
+ # Rails Git Hooks
2
2
 
3
3
  [![Gem Version](https://badge.fury.io/rb/rails_git_hooks.svg)](https://badge.fury.io/rb/rails_git_hooks)
4
4
  [![Build Status](https://github.com/NikitaNazarov1/rails_git_hooks/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/NikitaNazarov1/rails_git_hooks/actions/workflows/tests.yml?query=branch%3Amain)
5
5
  [![RuboCop](https://github.com/NikitaNazarov1/rails_git_hooks/actions/workflows/rubocop.yml/badge.svg?branch=main)](https://github.com/NikitaNazarov1/rails_git_hooks/actions/workflows/rubocop.yml?query=branch%3Amain)
6
6
 
7
- > Automate Jira ticket prefixes and RuboCop checks with git hooks. Built for Rails and Ruby projects.
7
+ Most useful git hooks for Rails and Ruby. Install only what you need. Turn hooks off anytime without uninstalling.
8
8
 
9
9
  ---
10
10
 
11
- ## What you get
11
+ ## What’s included
12
12
 
13
- | Hook | What it does |
14
- |------|--------------|
15
- | **commit-msg** | Prepends `[TICKET-123]` to commit messages when your branch name contains a Jira ticket. |
16
- | **pre-commit** | Blocks commits to `master`/`main` and runs RuboCop on staged `.rb` files. |
17
- | **pre-push** | Runs the full test suite (`bundle exec rspec`) before push; aborts push if tests fail. |
13
+ | Hook | Description |
14
+ |------|-------------|
15
+ | **commit-msg** | Adds `[TICKET-123]` to commit messages when your branch name contains a Jira ticket. |
16
+ | **pre-commit** | Blocks commits to `master`/`main`. Warns about debugger statements (Ruby, JS/TS, Python). Optional: RuboCop, trailing-whitespace/conflict checks. |
17
+ | **pre-push** | Runs `bundle exec rspec` before push and blocks push if tests fail. |
18
18
 
19
- Hooks can be disabled temporarily (e.g. for quick WIP commits or CI) without uninstalling.
19
+ - **Installed by default:** `commit-msg` and `pre-commit` (branch protection + debugger warnings; RuboCop and whitespace checks are off).
20
+ - **Optional:** `pre-push`, RuboCop, and whitespace/conflict checks — enable when you want them.
20
21
 
21
22
  ---
22
23
 
23
24
  ## Quick start
24
25
 
25
- **1. Install the gem**
26
+ ### 1. Install the gem
27
+
28
+ **Standalone:**
26
29
 
27
30
  ```bash
28
31
  gem install rails_git_hooks
29
32
  ```
30
33
 
31
- Or with Bundler — add to your `Gemfile`:
34
+ **With Bundler** — add to your `Gemfile`:
32
35
 
33
36
  ```ruby
34
37
  gem "rails_git_hooks"
@@ -38,56 +41,57 @@ Then:
38
41
 
39
42
  ```bash
40
43
  bundle install
41
- bundle exec rails_git_hooks install
42
44
  ```
43
45
 
44
- **2. (Optional) Set your Jira project key**
46
+ ### 2. Install hooks
47
+
48
+ From your project root (inside the git repo):
45
49
 
46
50
  ```bash
47
- rails_git_hooks install --jira MYPROJ
48
- # or
49
- export GIT_HOOKS_JIRA_PROJECT=MYPROJ
50
- rails_git_hooks install
51
+ bundle exec rails_git_hooks install
51
52
  ```
52
53
 
53
- Default is `APD` if not set.
54
+ This installs **commit-msg** and **pre-commit** (default-branch protection only). No Jira project key needed — the hook detects ticket IDs like `APD-123` or `PROJ-456` from the branch name.
55
+
56
+ ### 3. Optional: add more
57
+
58
+ ```bash
59
+ # Run tests before every push
60
+ rails_git_hooks install pre-push
61
+
62
+ # Run RuboCop on staged .rb files before commit
63
+ rails_git_hooks enable rubocop-check
64
+
65
+ # Reject trailing whitespace and conflict markers in staged files
66
+ rails_git_hooks enable whitespace-check
67
+ ```
54
68
 
55
- > **Tip:** If the pre-commit hook doesn’t run, make it executable: `chmod +x .git/hooks/pre-commit`
69
+ **Tip:** If a hook doesn’t run, make it executable: `chmod +x .git/hooks/<hook-name>`
56
70
 
57
71
  ---
58
72
 
59
- ## Commands
73
+ ## Command reference
60
74
 
61
- Run from your project root (inside a git repo).
75
+ All commands are run from the project root.
62
76
 
63
77
  | Command | Description |
64
78
  |---------|-------------|
65
- | `rails_git_hooks install [HOOK...] [--jira PROJECT]` | Install hooks. No args = install all. |
79
+ | `rails_git_hooks install [HOOK...]` | Install hooks. No arguments = install default (commit-msg + pre-commit). |
66
80
  | `rails_git_hooks list` | List available hook names. |
67
- | `rails_git_hooks disable HOOK [HOOK...] [whitespace-check]` | Disable hooks (use `*` for all) or the whitespace-check. |
68
- | `rails_git_hooks enable HOOK [HOOK...] [whitespace-check]` | Re-enable hooks or enable whitespace-check. |
69
- | `rails_git_hooks disabled` | Show which hooks are currently disabled. |
81
+ | `rails_git_hooks disable HOOK [...]` | Disable hooks or options (use `*` for all). |
82
+ | `rails_git_hooks enable HOOK [...]` | Re-enable hooks or enable optional checks. |
83
+ | `rails_git_hooks disabled` | Show currently disabled hooks. |
70
84
 
71
- **Examples**
85
+ **Common examples:**
72
86
 
73
87
  ```bash
74
- # Install everything with custom Jira key
75
- rails_git_hooks install --jira MYPROJ
76
-
77
- # Install only specific hooks
78
- rails_git_hooks install pre-commit commit-msg --jira APD
79
-
80
- # Temporarily disable pre-commit (e.g. for a quick fix)
81
- rails_git_hooks disable pre-commit
82
-
83
- # Disable all hooks
84
- rails_git_hooks disable *
85
-
86
- # Turn them back on
87
- rails_git_hooks enable pre-commit
88
-
89
- # Enable rejection of trailing whitespace and conflict markers in staged files (off by default)
90
- rails_git_hooks enable whitespace-check
88
+ rails_git_hooks install # default hooks
89
+ rails_git_hooks install pre-push # add pre-push
90
+ rails_git_hooks disable pre-commit # turn off pre-commit temporarily
91
+ rails_git_hooks disable * # turn off all
92
+ rails_git_hooks enable pre-commit # turn pre-commit back on
93
+ rails_git_hooks enable rubocop-check # run RuboCop on staged .rb files
94
+ rails_git_hooks enable whitespace-check # reject trailing ws & conflict markers
91
95
  ```
92
96
 
93
97
  Disabled state is stored in `.git/rails_git_hooks_disabled` and persists until you run `enable`.
@@ -98,45 +102,48 @@ Disabled state is stored in `.git/rails_git_hooks_disabled` and persists until y
98
102
 
99
103
  ### commit-msg — Jira ticket prefix
100
104
 
101
- If your branch name contains a Jira ticket (e.g. `task/APD-1234/fix-bug`), the hook prepends `[APD-1234] ` to the commit message unless it’s already there.
105
+ **What it does:** If the branch name contains a Jira-style ticket (e.g. `task/APD-1234/fix-bug`), the hook prepends `[APD-1234]` to the commit message unless the message already starts with that format. Works with any project key (2–5 letters + digits); no config.
102
106
 
103
- **Example**
107
+ **Example:**
104
108
 
105
109
  - Branch: `task/APD-1234/fix-bug`
106
- - You run: `git commit -m 'fix bug'`
110
+ - Command: `git commit -m 'fix bug'`
107
111
  - Result: **`[APD-1234] fix bug`**
108
112
 
109
- Set the Jira project key at install time with `--jira PROJECT` or `GIT_HOOKS_JIRA_PROJECT`.
110
-
111
113
  ---
112
114
 
113
- ### pre-commit — Default branch protection + RuboCop
115
+ ### pre-commit — Branch protection and optional checks
116
+
117
+ **Always on (when installed):**
118
+
119
+ - Blocks commits to `master` or `main`. You must commit from a feature branch.
120
+ - **Debugger check** — Warns (does not block) when staged files contain debugger statements: Ruby (`binding.pry`, `debugger`, `byebug`, `binding.irb`), JavaScript/TypeScript (`.js`, `.jsx`, `.ts`, `.tsx` — `debugger`), Python (`breakpoint()`, `pdb.set_trace()`, `ipdb.set_trace()`).
114
121
 
115
- 1. **Blocks commits on `master` / `main`** — You must commit from a feature branch; direct commits to the default branch are rejected.
116
- 2. **Runs RuboCop** on staged `.rb` files. If there are offenses, the commit is aborted.
117
- 3. **Trailing whitespace / conflict markers** (off by default) — When enabled, rejects commits that add trailing spaces/tabs or `<<<<<<<` / `=======` / `>>>>>>>` in staged files. Enable with: `rails_git_hooks enable whitespace-check`. Disable with: `rails_git_hooks disable whitespace-check`.
122
+ **Optional (off by default):**
118
123
 
119
- Requires the `rubocop` gem in your project. If the hook doesn’t run, ensure it’s executable: `chmod +x .git/hooks/pre-commit`.
124
+ 1. **RuboCop** Run RuboCop on staged `.rb` files; commit fails if there are offenses.
125
+ `rails_git_hooks enable rubocop-check` / `disable rubocop-check`. Requires the `rubocop` gem.
126
+
127
+ 2. **Whitespace & conflict markers** — Reject commits that add trailing spaces/tabs or `<<<<<<<` / `=======` / `>>>>>>>` in staged files.
128
+ `rails_git_hooks enable whitespace-check` / `disable whitespace-check`.
120
129
 
121
130
  ---
122
131
 
123
132
  ### pre-push — Run tests before push
124
133
 
125
- Runs `bundle exec rspec` before every `git push`. If the test suite fails, the push is aborted so you don’t break CI.
126
-
127
- Requires the `rspec` gem (or for Minitest, edit the hook to use `bundle exec rake test`). If the hook doesn’t run, ensure it’s executable: `chmod +x .git/hooks/pre-push`.
134
+ Runs `bundle exec rspec` before every `git push`. If the suite fails, the push is aborted. Not installed by default; add with `rails_git_hooks install pre-push`. For Minitest, edit the hook to use `bundle exec rake test`.
128
135
 
129
136
  ---
130
137
 
131
138
  ## Manual installation (without the gem)
132
139
 
133
- If you don’t want to use the gem, copy the scripts from `hooks/` into `.git/hooks`. The `hooks/` directory is synced from the gem templates via `rake sync_hooks` in development.
140
+ Copy the **entire** `hooks/` directory into your repo’s `.git/hooks/` (so the hook scripts and the `pre_commit/`, `commit_msg/`, `pre_push/` subdirs are all under `.git/hooks/`). Run `rake sync_hooks` to regenerate `hooks/` from `templates/hooks/` and `templates/shared/`.
134
141
 
135
- | File | Notes |
136
- |------|--------|
137
- | [commit-msg](hooks/commit-msg) | Replace `JIRA_PROJECT_KEY` in the script with your Jira project key (e.g. `APD`). |
138
- | [pre-commit](hooks/pre-commit) | Requires the `rubocop` gem in the repo where you use it. |
139
- | [pre-push](hooks/pre-push) | Runs `bundle exec rspec`; edit to use `bundle exec rake test` for Minitest. |
142
+ | Script | Notes |
143
+ |--------|--------|
144
+ | [commit-msg](hooks/commit-msg) | Jira-style ticket prefix; no config. |
145
+ | [pre-commit](hooks/pre-commit) | Branch protection + debugger warnings; optional RuboCop (requires `rubocop` gem). |
146
+ | [pre-push](hooks/pre-push) | Runs `bundle exec rspec`; edit for Minitest if needed. |
140
147
 
141
148
  ---
142
149
 
@@ -144,10 +151,10 @@ If you don’t want to use the gem, copy the scripts from `hooks/` into `.git/ho
144
151
 
145
152
  ```bash
146
153
  bundle install
147
- bundle exec rake # run specs (default task)
154
+ bundle exec rake # run specs
148
155
  bundle exec rake build # build the gem
149
- bundle exec rake install # install the gem locally
150
- bundle exec rake sync_hooks # copy templates to hooks/
156
+ bundle exec rake install # install locally
157
+ bundle exec rake sync_hooks # copy templates (hooks + shared subdirs) → hooks/
151
158
  ```
152
159
 
153
160
  ---
@@ -1,10 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'rails_git_hooks'
4
- require 'optparse'
5
4
 
6
5
  module GitHooks
7
6
  class CLI
7
+ FEATURE_FLAG_TOKENS = GitHooks::Constants::FEATURE_FLAG_FILES.keys.freeze
8
+
8
9
  def self.run(argv = ARGV)
9
10
  new.run(argv)
10
11
  end
@@ -33,14 +34,8 @@ module GitHooks
33
34
  private
34
35
 
35
36
  def run_install(args)
36
- jira = nil
37
- OptionParser.new do |opts|
38
- opts.on('--jira PROJECT', 'Jira project key (e.g. APD)') { |v| jira = v }
39
- end.parse!(args)
40
- hooks = args
41
-
42
- installer = Installer.new(jira_project: jira)
43
- installed = installer.install(*hooks)
37
+ installer = Installer.new
38
+ installed = installer.install(*args)
44
39
  puts "Installed hooks: #{installed.join(', ')}"
45
40
  rescue GitHooks::Error => e
46
41
  warn "Error: #{e.message}"
@@ -48,46 +43,52 @@ module GitHooks
48
43
  end
49
44
 
50
45
  def run_list
51
- dir = GitHooks::Installer::HOOKS_DIR
52
- hooks = Dir.children(dir).select { |f| File.file?(File.join(dir, f)) }
53
- puts "Available hooks: #{hooks.join(', ')}"
46
+ puts "Available hooks: #{Installer.available_hook_names.join(', ')}"
54
47
  end
55
48
 
56
- def run_disable(args) # rubocop:disable Metrics/AbcSize
57
- tokens = args.reject { |a| a.start_with?('-') }
49
+ def run_disable(args)
50
+ tokens = parse_tokens(args)
58
51
  if tokens.empty?
59
- warn 'Usage: rails_git_hooks disable HOOK [HOOK...] [whitespace-check]'
52
+ warn 'Usage: rails_git_hooks disable HOOK [HOOK...] [whitespace-check] [rubocop-check]'
60
53
  warn "Use '*' to disable all hooks."
61
54
  exit 1
62
55
  end
63
56
  installer = Installer.new
64
- hook_names = tokens - ['whitespace-check']
65
- installer.disable_whitespace_check if tokens.include?('whitespace-check')
57
+ hook_names, feature_flags = split_tokens(tokens)
58
+ feature_flags.each { |name| installer.public_send(:"disable_#{name.tr('-', '_')}") }
66
59
  installer.disable(*hook_names) if hook_names.any?
67
- disabled = hook_names + (tokens.include?('whitespace-check') ? ['whitespace-check'] : [])
68
- puts "Disabled: #{disabled.join(', ')}"
60
+ puts "Disabled: #{(hook_names + feature_flags).join(', ')}"
69
61
  rescue GitHooks::Error => e
70
62
  warn "Error: #{e.message}"
71
63
  exit 1
72
64
  end
73
65
 
74
66
  def run_enable(args)
75
- tokens = args.reject { |a| a.start_with?('-') }
67
+ tokens = parse_tokens(args)
76
68
  if tokens.empty?
77
- warn 'Usage: rails_git_hooks enable HOOK [HOOK...] [whitespace-check]'
69
+ warn 'Usage: rails_git_hooks enable HOOK [HOOK...] [whitespace-check] [rubocop-check]'
78
70
  exit 1
79
71
  end
80
72
  installer = Installer.new
81
- hook_names = tokens - ['whitespace-check']
82
- installer.enable_whitespace_check if tokens.include?('whitespace-check')
73
+ hook_names, feature_flags = split_tokens(tokens)
74
+ feature_flags.each { |name| installer.public_send(:"enable_#{name.tr('-', '_')}") }
83
75
  installer.enable(*hook_names) if hook_names.any?
84
- enabled = hook_names + (tokens.include?('whitespace-check') ? ['whitespace-check'] : [])
85
- puts "Enabled: #{enabled.join(', ')}"
76
+ puts "Enabled: #{(hook_names + feature_flags).join(', ')}"
86
77
  rescue GitHooks::Error => e
87
78
  warn "Error: #{e.message}"
88
79
  exit 1
89
80
  end
90
81
 
82
+ def parse_tokens(args)
83
+ args.reject { |a| a.start_with?('-') }
84
+ end
85
+
86
+ def split_tokens(tokens)
87
+ feature_flags = tokens & FEATURE_FLAG_TOKENS
88
+ hook_names = tokens - FEATURE_FLAG_TOKENS
89
+ [hook_names, feature_flags]
90
+ end
91
+
91
92
  def run_disabled
92
93
  installer = Installer.new
93
94
  list = installer.disabled_hooks
@@ -106,17 +107,17 @@ module GitHooks
106
107
  rails_git_hooks - Install git hooks for Jira commit prefix and RuboCop
107
108
 
108
109
  Usage:
109
- rails_git_hooks install [HOOK...] [--jira PROJECT_KEY]
110
- rails_git_hooks disable HOOK [HOOK...] [whitespace-check] (use * for all hooks)
111
- rails_git_hooks enable HOOK [HOOK...] [whitespace-check]
110
+ rails_git_hooks install [HOOK...]
111
+ rails_git_hooks disable HOOK [HOOK...] [whitespace-check] [rubocop-check] (use * for all hooks)
112
+ rails_git_hooks enable HOOK [HOOK...] [whitespace-check] [rubocop-check]
112
113
  rails_git_hooks disabled
113
114
  rails_git_hooks list
114
115
  rails_git_hooks --help
115
116
 
116
117
  Commands:
117
118
  install Install hooks into current repo's .git/hooks.
118
- disable Disable hooks or whitespace-check (trailing ws/conflict markers in pre-commit).
119
- enable Re-enable disabled hooks or enable whitespace-check.
119
+ disable Disable hooks or whitespace-check / rubocop-check (pre-commit options).
120
+ enable Re-enable disabled hooks or enable whitespace-check / rubocop-check.
120
121
  disabled List currently disabled hooks.
121
122
  list List available hook names.
122
123
 
@@ -125,8 +126,9 @@ module GitHooks
125
126
  rails_git_hooks disable pre-commit
126
127
  rails_git_hooks disable * # disable all hooks
127
128
  rails_git_hooks enable pre-commit
128
- rails_git_hooks enable whitespace-check # reject trailing ws/conflict markers (off by default)
129
- rails_git_hooks install commit-msg pre-commit --jira MYPROJ
129
+ rails_git_hooks enable whitespace-check # trailing ws/conflict markers (off by default)
130
+ rails_git_hooks enable rubocop-check # RuboCop on staged .rb files (off by default)
131
+ rails_git_hooks install commit-msg pre-commit
130
132
  HELP
131
133
  end
132
134
  end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GitHooks
4
+ # Paths and default config (single source of truth).
5
+ module Constants
6
+ GEM_ROOT = File.expand_path('../..', __dir__)
7
+ HOOKS_DIR = File.expand_path('templates/hooks', GEM_ROOT).freeze
8
+ SHARED_DIR = File.expand_path('templates/shared', GEM_ROOT).freeze
9
+
10
+ DISABLED_FILE = 'rails_git_hooks_disabled'
11
+
12
+ # Default hooks when install is run with no arguments.
13
+ DEFAULT_HOOKS = %w[commit-msg pre-commit].freeze
14
+
15
+ # Pre-commit feature flag file names (keys = CLI tokens).
16
+ FEATURE_FLAG_FILES = {
17
+ 'whitespace-check' => 'rails_git_hooks_whitespace_check',
18
+ 'rubocop-check' => 'rails_git_hooks_rubocop'
19
+ }.freeze
20
+ end
21
+ end
@@ -1,29 +1,28 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'fileutils'
4
+
3
5
  module GitHooks
4
6
  class Installer
5
- HOOKS_DIR = File.expand_path('templates', __dir__).freeze
6
-
7
- def initialize(git_dir: nil, jira_project: nil)
7
+ def initialize(git_dir: nil)
8
8
  @git_dir = git_dir || find_git_dir
9
- @jira_project = jira_project || ENV['GIT_HOOKS_JIRA_PROJECT'] || 'APD'
10
9
  end
11
10
 
12
11
  def install(*hook_names)
13
12
  target_dir = File.join(@git_dir, 'hooks')
14
13
  raise GitHooks::Error, "Not a git repository or .git/hooks not found: #{@git_dir}" unless Dir.exist?(target_dir)
15
14
 
16
- hooks = hook_names.empty? ? available_hooks : hook_names
15
+ copy_shared_files(target_dir)
16
+
17
+ hooks = hook_names.empty? ? Constants::DEFAULT_HOOKS : hook_names
17
18
  installed = []
18
19
 
19
20
  hooks.each do |name|
20
- src = File.join(HOOKS_DIR, name)
21
+ src = File.join(Constants::HOOKS_DIR, name)
21
22
  next unless File.file?(src)
22
23
 
23
24
  dest = File.join(target_dir, name)
24
- content = read_template(name)
25
- content = content.gsub('JIRA_PROJECT_KEY', @jira_project) if name == 'commit-msg'
26
- write_hook(dest, content)
25
+ write_hook(dest, read_template(name))
27
26
  make_executable(dest)
28
27
  installed << name
29
28
  end
@@ -31,14 +30,16 @@ module GitHooks
31
30
  installed
32
31
  end
33
32
 
34
- def available_hooks
35
- Dir.children(HOOKS_DIR).select { |f| File.file?(File.join(HOOKS_DIR, f)) }
33
+ def self.available_hook_names
34
+ Dir.children(Constants::HOOKS_DIR).select { |f| File.file?(File.join(Constants::HOOKS_DIR, f)) }
36
35
  end
37
36
 
38
- DISABLED_FILE = 'rails_git_hooks_disabled'
37
+ def available_hooks
38
+ self.class.available_hook_names
39
+ end
39
40
 
40
41
  def disabled_file_path
41
- File.join(@git_dir, DISABLED_FILE)
42
+ File.join(@git_dir, Constants::DISABLED_FILE)
42
43
  end
43
44
 
44
45
  def disabled_hooks
@@ -68,24 +69,51 @@ module GitHooks
68
69
  hook_names
69
70
  end
70
71
 
71
- WHITESPACE_CHECK_FILE = 'rails_git_hooks_whitespace_check'
72
-
73
72
  def enable_whitespace_check
74
- path = File.join(@git_dir, WHITESPACE_CHECK_FILE)
75
- File.write(path, '')
73
+ enable_feature_flag('whitespace-check')
76
74
  end
77
75
 
78
76
  def disable_whitespace_check
79
- path = File.join(@git_dir, WHITESPACE_CHECK_FILE)
80
- FileUtils.rm_f(path)
77
+ disable_feature_flag('whitespace-check')
81
78
  end
82
79
 
83
80
  def whitespace_check_enabled?
84
- File.exist?(File.join(@git_dir, WHITESPACE_CHECK_FILE))
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')
85
94
  end
86
95
 
87
96
  private
88
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
+
89
117
  def find_git_dir
90
118
  dir = Dir.pwd
91
119
  loop do
@@ -99,8 +127,21 @@ module GitHooks
99
127
  end
100
128
  end
101
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
+
102
143
  def read_template(name)
103
- path = File.join(HOOKS_DIR, name)
144
+ path = File.join(Constants::HOOKS_DIR, name)
104
145
  File.read(path)
105
146
  end
106
147
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module GitHooks
4
- VERSION = '0.6.0'
4
+ VERSION = '0.7.0'
5
5
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'rails_git_hooks/version'
4
+ require_relative 'rails_git_hooks/constants'
4
5
  require_relative 'rails_git_hooks/installer'
5
6
 
6
7
  module GitHooks
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # Git commit-msg hook: prepends [TICKET] when branch contains a Jira-style ticket.
6
+ # Logic in templates/shared/commit_msg/.
7
+
8
+ # Bootstrap: resolve .git dir and skip if hook is disabled
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')
16
+ end
17
+
18
+ module RailsGitHooks
19
+ end
20
+ RailsGitHooks.const_set(:GIT_DIR, git_dir.freeze)
21
+ hooks_dir = File.dirname(File.expand_path(__FILE__))
22
+
23
+ load File.join(hooks_dir, 'commit_msg', 'jira_prefix.rb')
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # Blocks commits on default branch (master/main). Warns on debugger statements (Ruby/JS/TS/Python).
6
+ # Optionally runs RuboCop and whitespace/conflict checks.
7
+ # Logic is in templates/shared/pre_commit/ for easier maintenance and adding new checks.
8
+
9
+ # Bootstrap: resolve .git dir and skip if hook is disabled
10
+ repo_root = Dir.pwd
11
+ git_dir = File.join(repo_root, '.git')
12
+ git_dir = File.expand_path(File.read(git_dir).strip.sub(/\Agitdir: \s*/, ''), repo_root) if File.file?(git_dir)
13
+ disabled_file = File.join(git_dir, 'rails_git_hooks_disabled')
14
+ if File.exist?(disabled_file)
15
+ disabled = File.read(disabled_file).split("\n").map(&:strip)
16
+ exit 0 if disabled.include?('*') || disabled.include?('pre-commit')
17
+ end
18
+
19
+ module RailsGitHooks
20
+ end
21
+ RailsGitHooks.const_set(:GIT_DIR, git_dir.freeze)
22
+ hooks_dir = File.dirname(File.expand_path(__FILE__))
23
+
24
+ load File.join(hooks_dir, 'pre_commit', 'whitespace_check.rb')
25
+ load File.join(hooks_dir, 'pre_commit', 'default_branch.rb')
26
+ load File.join(hooks_dir, 'pre_commit', 'debugger_check.rb')
27
+ load File.join(hooks_dir, 'pre_commit', 'rubocop_check.rb')
@@ -3,9 +3,9 @@
3
3
 
4
4
  #
5
5
  # Runs the full test suite before push. Push is aborted if tests fail.
6
- # Uses `bundle exec rspec`. For Minitest, edit to use `bundle exec rake test`.
6
+ # Logic in templates/shared/pre_push/.
7
7
 
8
- # Git runs hooks with CWD = repo root; find .git from here
8
+ # Bootstrap: resolve .git dir and skip if hook is disabled
9
9
  repo_root = Dir.pwd
10
10
  git_dir = File.join(repo_root, '.git')
11
11
  git_dir = File.expand_path(File.read(git_dir).strip.sub(/\Agitdir: \s*/, ''), repo_root) if File.file?(git_dir)
@@ -15,7 +15,9 @@ if File.exist?(disabled_file)
15
15
  exit 0 if disabled.include?('*') || disabled.include?('pre-push')
16
16
  end
17
17
 
18
- require 'english'
18
+ module RailsGitHooks
19
+ end
20
+ RailsGitHooks.const_set(:GIT_DIR, git_dir.freeze)
21
+ hooks_dir = File.dirname(File.expand_path(__FILE__))
19
22
 
20
- system('bundle exec rspec')
21
- exit $CHILD_STATUS.to_s[-1].to_i
23
+ load File.join(hooks_dir, 'pre_push', 'run_tests.rb')
@@ -0,0 +1,20 @@
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
@@ -0,0 +1,48 @@
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
@@ -0,0 +1,9 @@
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
@@ -0,0 +1,24 @@
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
@@ -0,0 +1,25 @@
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
@@ -0,0 +1,9 @@
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
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails_git_hooks
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nikita Nazarov
@@ -65,11 +65,18 @@ files:
65
65
  - bin/rails_git_hooks
66
66
  - lib/rails_git_hooks.rb
67
67
  - lib/rails_git_hooks/cli.rb
68
+ - lib/rails_git_hooks/constants.rb
68
69
  - lib/rails_git_hooks/installer.rb
69
- - lib/rails_git_hooks/templates/commit-msg
70
- - lib/rails_git_hooks/templates/pre-commit
71
- - lib/rails_git_hooks/templates/pre-push
72
70
  - lib/rails_git_hooks/version.rb
71
+ - templates/hooks/commit-msg
72
+ - templates/hooks/pre-commit
73
+ - templates/hooks/pre-push
74
+ - templates/shared/commit_msg/jira_prefix.rb
75
+ - templates/shared/pre_commit/debugger_check.rb
76
+ - templates/shared/pre_commit/default_branch.rb
77
+ - templates/shared/pre_commit/rubocop_check.rb
78
+ - templates/shared/pre_commit/whitespace_check.rb
79
+ - templates/shared/pre_push/run_tests.rb
73
80
  homepage: https://github.com/NikitaNazarov1/rails_git_hooks
74
81
  licenses:
75
82
  - MIT
@@ -1,32 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # frozen_string_literal: true
3
-
4
- #
5
- # Git commit-msg hook: if branch name contains a Jira ticket (e.g. APD-1234),
6
- # prepends "[APD-1234] " to commit messages unless already present.
7
- # JIRA_PROJECT_KEY is replaced at install time by git_hooks; for manual install, replace it with your project key.
8
-
9
- # Git runs hooks with CWD = repo root; find .git from here
10
- repo_root = Dir.pwd
11
- git_dir = File.join(repo_root, '.git')
12
- git_dir = File.expand_path(File.read(git_dir).strip.sub(/\Agitdir: \s*/, ''), repo_root) if File.file?(git_dir)
13
- disabled_file = File.join(git_dir, 'rails_git_hooks_disabled')
14
- if File.exist?(disabled_file)
15
- disabled = File.read(disabled_file).split("\n").map(&:strip)
16
- exit 0 if disabled.include?('*') || disabled.include?('commit-msg')
17
- end
18
-
19
- commit_message_file = ARGV[0]
20
- branch_name = `git branch --no-color 2> /dev/null`[/^\* (.+)/, 1].to_s
21
- original_commit_message = File.read(commit_message_file).strip
22
-
23
- pattern = /(JIRA_PROJECT_KEY-\d+)/i
24
-
25
- if (m = branch_name.match(pattern))
26
- jira_number = m.captures.first
27
-
28
- exit 0 if original_commit_message.include?(jira_number)
29
-
30
- message = "[#{jira_number}] #{original_commit_message}"
31
- File.open(commit_message_file, 'w') { |f| f.write message }
32
- end
@@ -1,58 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # frozen_string_literal: true
3
-
4
- #
5
- # Runs RuboCop on staged .rb files. Commit is aborted if RuboCop reports offenses.
6
-
7
- # Git runs hooks with CWD = repo root; find .git from here
8
- repo_root = Dir.pwd
9
- git_dir = File.join(repo_root, '.git')
10
- git_dir = File.expand_path(File.read(git_dir).strip.sub(/\Agitdir: \s*/, ''), repo_root) if File.file?(git_dir)
11
- disabled_file = File.join(git_dir, 'rails_git_hooks_disabled')
12
- if File.exist?(disabled_file)
13
- disabled = File.read(disabled_file).split("\n").map(&:strip)
14
- exit 0 if disabled.include?('*') || disabled.include?('pre-commit')
15
- end
16
-
17
- # Trailing whitespace / conflict markers check (disabled by default; enable with .git/rails_git_hooks_whitespace_check)
18
- whitespace_check_file = File.join(git_dir, 'rails_git_hooks_whitespace_check')
19
- if File.exist?(whitespace_check_file)
20
- staged = `git diff --cached --name-only`.split("\n").map(&:strip).reject(&:empty?)
21
- errors = []
22
- staged.each do |path|
23
- next unless File.file?(path)
24
-
25
- File.read(path).lines.each_with_index do |line, i|
26
- errors << "#{path}:#{i + 1}: trailing whitespace" if line.match?(/[ \t]\z/)
27
- stripped = line.strip
28
- errors << "#{path}:#{i + 1}: conflict marker" if stripped.start_with?('<<<<<<<', '=======', '>>>>>>>')
29
- end
30
- end
31
- unless errors.empty?
32
- warn 'Commit rejected (whitespace/conflict check):'
33
- errors.uniq.each { |e| warn " #{e}" }
34
- exit 1
35
- end
36
- end
37
-
38
- # Prevent commits on default branch (master/main)
39
- branch = `git rev-parse --abbrev-ref HEAD`.strip
40
- if %w[master main].include?(branch)
41
- warn "Commits on '#{branch}' are not allowed. Create a feature branch."
42
- exit 1
43
- end
44
-
45
- require 'english'
46
- require 'rubocop'
47
-
48
- ADDED_OR_MODIFIED = /A|AM|^M/.freeze
49
-
50
- changed_files = `git status --porcelain`.split(/\n/)
51
- .select { |file_name_with_status| file_name_with_status =~ ADDED_OR_MODIFIED }
52
- .map { |file_name_with_status| file_name_with_status.split(' ')[1] }
53
- .select { |file_name| File.extname(file_name) == '.rb' }
54
- .join(' ')
55
-
56
- system("rubocop #{changed_files}") unless changed_files.empty?
57
-
58
- exit $CHILD_STATUS.to_s[-1].to_i