rails_git_hooks 0.6.1 → 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: '039dc0fb243e8ce670d6cadeb3c3e398cc6441276953c9a0c1e8f8541864256e'
4
- data.tar.gz: 26a46c6776054ec41554c60ad4ea06f48c14f703cd9c396735f2888b6b0f329b
3
+ metadata.gz: 3b990407e471e8f6dddf8df8741a8ac6de92955bc44470b1b45a19b93644b542
4
+ data.tar.gz: ed3f378f82db9a1dc81dcf58d90f02d12da0ff32c05697ecc2f1dbb92f8db2ca
5
5
  SHA512:
6
- metadata.gz: 589952d05b4bbc277efc18f59a02bdd51ba2edeae20b26fb1b093186bdd57413a4949e25987b291b546b06f29b2c81cff394901cd6c6dd9527a1fd1c0f3f0101
7
- data.tar.gz: d79a92159cd66cf640b1fc80a29ade822fd6e3215be50915aec2609301f7edd71a13b1b83252e4b94adbb95feed759f1dfb78eb80e1f23abd800419992a6a24f
6
+ metadata.gz: 0b1aa8a31af84e697e05f56d94ff3427a0afa82e0dfabe4de21e5f0ec865f7ba6c7ddd0537f058e852281a415f68b26a063fd6ef8e71f0d0f1a0b7f9c718aa04
7
+ data.tar.gz: 966f4fbd5dcfac4ce1e4a9ad1d8a3f8209f0d2885030ece2d916c5598e2324aeac348f7bc8e9690e60bd3676fc995e264e0c2e2b1fd80e51cd1563d4cf3bb4d0
data/CHANGELOG.md CHANGED
@@ -4,11 +4,27 @@ 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.1] (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]
8
24
 
9
25
  ### Changed
10
26
 
11
- - **Default install** now installs **commit-msg** and **pre-commit** only (Jira ticket prefix + default-branch protection + RuboCop). Pre-push remains opt-in: `rails_git_hooks install pre-push`.
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`.
12
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.
13
29
 
14
30
  ## [0.6.0]
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,63 +41,57 @@ Then:
38
41
 
39
42
  ```bash
40
43
  bundle install
41
- bundle exec rails_git_hooks install
42
44
  ```
43
45
 
44
- This installs **commit-msg** (Jira ticket prefix) and **pre-commit** (blocks commits on `master`/`main` + RuboCop on staged `.rb` files) by default.
45
-
46
- **2. Set your Jira project key**
46
+ ### 2. Install hooks
47
47
 
48
- Replace the default by passing your project key at install time or via env:
48
+ From your project root (inside the git repo):
49
49
 
50
50
  ```bash
51
- rails_git_hooks install --jira MYPROJ
52
- # or
53
- export GIT_HOOKS_JIRA_PROJECT=MYPROJ
54
- rails_git_hooks install
51
+ bundle exec rails_git_hooks install
55
52
  ```
56
53
 
57
- Default is `APD` if not set. For manual install: replace `JIRA_PROJECT_KEY` in the commit-msg script with your key (e.g. `APD`).
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
58
57
 
59
- To also run the full test suite before push:
60
- `rails_git_hooks install pre-push`
58
+ ```bash
59
+ # Run tests before every push
60
+ rails_git_hooks install pre-push
61
61
 
62
- > **Tip:** If the pre-commit hook doesn’t run, make it executable: `chmod +x .git/hooks/pre-commit`
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
+ ```
68
+
69
+ **Tip:** If a hook doesn’t run, make it executable: `chmod +x .git/hooks/<hook-name>`
63
70
 
64
71
  ---
65
72
 
66
- ## Commands
73
+ ## Command reference
67
74
 
68
- Run from your project root (inside a git repo).
75
+ All commands are run from the project root.
69
76
 
70
77
  | Command | Description |
71
78
  |---------|-------------|
72
- | `rails_git_hooks install [HOOK...] [--jira PROJECT]` | Install hooks. No args = install default (commit-msg + pre-commit). |
79
+ | `rails_git_hooks install [HOOK...]` | Install hooks. No arguments = install default (commit-msg + pre-commit). |
73
80
  | `rails_git_hooks list` | List available hook names. |
74
- | `rails_git_hooks disable HOOK [HOOK...] [whitespace-check]` | Disable hooks (use `*` for all) or the whitespace-check. |
75
- | `rails_git_hooks enable HOOK [HOOK...] [whitespace-check]` | Re-enable hooks or enable whitespace-check. |
76
- | `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. |
77
84
 
78
- **Examples**
85
+ **Common examples:**
79
86
 
80
87
  ```bash
81
- # Install everything with custom Jira key
82
- rails_git_hooks install --jira MYPROJ
83
-
84
- # Install only specific hooks
85
- rails_git_hooks install pre-commit commit-msg --jira APD
86
-
87
- # Temporarily disable pre-commit (e.g. for a quick fix)
88
- rails_git_hooks disable pre-commit
89
-
90
- # Disable all hooks
91
- rails_git_hooks disable *
92
-
93
- # Turn them back on
94
- rails_git_hooks enable pre-commit
95
-
96
- # Enable rejection of trailing whitespace and conflict markers in staged files (off by default)
97
- 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
98
95
  ```
99
96
 
100
97
  Disabled state is stored in `.git/rails_git_hooks_disabled` and persists until you run `enable`.
@@ -105,45 +102,48 @@ Disabled state is stored in `.git/rails_git_hooks_disabled` and persists until y
105
102
 
106
103
  ### commit-msg — Jira ticket prefix
107
104
 
108
- 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.
109
106
 
110
- **Example**
107
+ **Example:**
111
108
 
112
109
  - Branch: `task/APD-1234/fix-bug`
113
- - You run: `git commit -m 'fix bug'`
110
+ - Command: `git commit -m 'fix bug'`
114
111
  - Result: **`[APD-1234] fix bug`**
115
112
 
116
- Set the Jira project key at install time with `--jira PROJECT` or `GIT_HOOKS_JIRA_PROJECT`.
117
-
118
113
  ---
119
114
 
120
- ### 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()`).
121
121
 
122
- 1. **Blocks commits on `master` / `main`** — You must commit from a feature branch; direct commits to the default branch are rejected.
123
- 2. **Runs RuboCop** on staged `.rb` files. If there are offenses, the commit is aborted.
124
- 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):**
125
123
 
126
- 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`.
127
129
 
128
130
  ---
129
131
 
130
132
  ### pre-push — Run tests before push
131
133
 
132
- Runs `bundle exec rspec` before every `git push`. If the test suite fails, the push is aborted so you don’t break CI.
133
-
134
- 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`.
135
135
 
136
136
  ---
137
137
 
138
138
  ## Manual installation (without the gem)
139
139
 
140
- 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/`.
141
141
 
142
- | File | Notes |
143
- |------|--------|
144
- | [commit-msg](hooks/commit-msg) | Replace `JIRA_PROJECT_KEY` in the script with your Jira project key (e.g. `APD`). |
145
- | [pre-commit](hooks/pre-commit) | Requires the `rubocop` gem in the repo where you use it. |
146
- | [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. |
147
147
 
148
148
  ---
149
149
 
@@ -151,10 +151,10 @@ If you don’t want to use the gem, copy the scripts from `hooks/` into `.git/ho
151
151
 
152
152
  ```bash
153
153
  bundle install
154
- bundle exec rake # run specs (default task)
154
+ bundle exec rake # run specs
155
155
  bundle exec rake build # build the gem
156
- bundle exec rake install # install the gem locally
157
- 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/
158
158
  ```
159
159
 
160
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,32 +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
- # Default: Jira commit-msg + pre-commit (default-branch protection + RuboCop).
7
- # Use install(*available_hooks) or pass names for more.
8
- DEFAULT_HOOKS = %w[commit-msg pre-commit].freeze
9
-
10
- def initialize(git_dir: nil, jira_project: nil)
7
+ def initialize(git_dir: nil)
11
8
  @git_dir = git_dir || find_git_dir
12
- @jira_project = jira_project || ENV['GIT_HOOKS_JIRA_PROJECT'] || 'APD'
13
9
  end
14
10
 
15
11
  def install(*hook_names)
16
12
  target_dir = File.join(@git_dir, 'hooks')
17
13
  raise GitHooks::Error, "Not a git repository or .git/hooks not found: #{@git_dir}" unless Dir.exist?(target_dir)
18
14
 
19
- hooks = hook_names.empty? ? DEFAULT_HOOKS : hook_names
15
+ copy_shared_files(target_dir)
16
+
17
+ hooks = hook_names.empty? ? Constants::DEFAULT_HOOKS : hook_names
20
18
  installed = []
21
19
 
22
20
  hooks.each do |name|
23
- src = File.join(HOOKS_DIR, name)
21
+ src = File.join(Constants::HOOKS_DIR, name)
24
22
  next unless File.file?(src)
25
23
 
26
24
  dest = File.join(target_dir, name)
27
- content = read_template(name)
28
- content = content.gsub('JIRA_PROJECT_KEY', @jira_project) if name == 'commit-msg'
29
- write_hook(dest, content)
25
+ write_hook(dest, read_template(name))
30
26
  make_executable(dest)
31
27
  installed << name
32
28
  end
@@ -34,14 +30,16 @@ module GitHooks
34
30
  installed
35
31
  end
36
32
 
37
- def available_hooks
38
- 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)) }
39
35
  end
40
36
 
41
- DISABLED_FILE = 'rails_git_hooks_disabled'
37
+ def available_hooks
38
+ self.class.available_hook_names
39
+ end
42
40
 
43
41
  def disabled_file_path
44
- File.join(@git_dir, DISABLED_FILE)
42
+ File.join(@git_dir, Constants::DISABLED_FILE)
45
43
  end
46
44
 
47
45
  def disabled_hooks
@@ -71,24 +69,51 @@ module GitHooks
71
69
  hook_names
72
70
  end
73
71
 
74
- WHITESPACE_CHECK_FILE = 'rails_git_hooks_whitespace_check'
75
-
76
72
  def enable_whitespace_check
77
- path = File.join(@git_dir, WHITESPACE_CHECK_FILE)
78
- File.write(path, '')
73
+ enable_feature_flag('whitespace-check')
79
74
  end
80
75
 
81
76
  def disable_whitespace_check
82
- path = File.join(@git_dir, WHITESPACE_CHECK_FILE)
83
- FileUtils.rm_f(path)
77
+ disable_feature_flag('whitespace-check')
84
78
  end
85
79
 
86
80
  def whitespace_check_enabled?
87
- 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')
88
94
  end
89
95
 
90
96
  private
91
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
+
92
117
  def find_git_dir
93
118
  dir = Dir.pwd
94
119
  loop do
@@ -102,8 +127,21 @@ module GitHooks
102
127
  end
103
128
  end
104
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
+
105
143
  def read_template(name)
106
- path = File.join(HOOKS_DIR, name)
144
+ path = File.join(Constants::HOOKS_DIR, name)
107
145
  File.read(path)
108
146
  end
109
147
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module GitHooks
4
- VERSION = '0.6.1'
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.1
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