From f5f00c3ba5a094a1bf26aa7e045d4178ca6e99e9 Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Sun, 2 Nov 2025 15:50:33 -1000 Subject: [PATCH 01/11] Add pre-commit hook to prevent simple test and lint failures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit adds a pre-commit hook inspired by shakacode/shakapacker that helps prevent simple test and lint failures from being committed. Key improvements: - Created bin/setup-git-hooks script to easily install the pre-commit hook - Pre-commit hook runs RuboCop on staged Ruby files only - Pre-commit hook runs all RSpec tests before allowing commit - Added clear documentation in README about Git hooks setup - Hook provides colored output for better visibility - Hook can be bypassed with --no-verify flag if needed The hook installation is simple: just run bin/setup-git-hooks ๐Ÿค– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- README.md | 14 +++++++++ bin/setup-git-hooks | 72 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+) create mode 100755 bin/setup-git-hooks diff --git a/README.md b/README.md index 4582901..bebc324 100644 --- a/README.md +++ b/README.md @@ -266,6 +266,20 @@ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. +### Git Hooks + +To set up the pre-commit hook that prevents simple test and lint failures: + +```bash +bin/setup-git-hooks +``` + +This will install a pre-commit hook that: +- Runs RuboCop on staged Ruby files +- Runs all RSpec tests + +To bypass the hook (not recommended), use `git commit --no-verify`. + To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then tag the commit with the version prefixed with a `v`, which will trigger the release diff --git a/bin/setup-git-hooks b/bin/setup-git-hooks new file mode 100755 index 0000000..20b1f30 --- /dev/null +++ b/bin/setup-git-hooks @@ -0,0 +1,72 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# Script to set up Git hooks +# Run this after cloning the repository or when hooks are updated + +require "fileutils" + +git_dir = `git rev-parse --git-common-dir`.strip +hooks_dir = File.join(git_dir, "hooks") +pre_commit_hook = File.join(hooks_dir, "pre-commit") +project_root = File.expand_path("..", __dir__) + +puts "Setting up Git hooks..." + +# Create hooks directory if it doesn't exist +FileUtils.mkdir_p(hooks_dir) unless File.directory?(hooks_dir) + +# Create the pre-commit hook +hook_content = <<~BASH + #!/bin/bash + # Pre-commit hook to run linting and tests on staged files + + set -e + + # Colors for output + RED='\\033[0;31m' + GREEN='\\033[0;32m' + YELLOW='\\033[1;33m' + NC='\\033[0m' # No Color + + echo -e "${YELLOW}Running pre-commit checks...${NC}" + + # Get the list of staged Ruby files + STAGED_RUBY_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep '\\.rb$' || true) + + if [ -n "$STAGED_RUBY_FILES" ]; then + echo -e "${YELLOW}Checking Ruby files with RuboCop...${NC}" + + # Run RuboCop on staged files + if ! bundle exec rubocop $STAGED_RUBY_FILES; then + echo -e "${RED}RuboCop failed! Please fix the issues before committing.${NC}" + echo -e "${YELLOW}You can run 'bundle exec rubocop -a' to auto-fix some issues.${NC}" + exit 1 + fi + + echo -e "${GREEN}RuboCop passed!${NC}" + else + echo -e "${YELLOW}No Ruby files to lint.${NC}" + fi + + # Run RSpec tests + echo -e "${YELLOW}Running RSpec tests...${NC}" + if ! bundle exec rspec; then + echo -e "${RED}Tests failed! Please fix the tests before committing.${NC}" + exit 1 + fi + + echo -e "${GREEN}All pre-commit checks passed!${NC}" + exit 0 +BASH + +File.write(pre_commit_hook, hook_content) +FileUtils.chmod(0o755, pre_commit_hook) + +puts "โœ“ Pre-commit hook installed at #{pre_commit_hook}" +puts "" +puts "The pre-commit hook will:" +puts " โ€ข Run RuboCop on staged Ruby files" +puts " โ€ข Run all RSpec tests" +puts "" +puts "To bypass the hook (not recommended), use: git commit --no-verify" From 1cd20ea4f26d95c52616d2694c62b5ca06a3e91d Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Sun, 2 Nov 2025 21:44:42 -1000 Subject: [PATCH 02/11] chore: fix prettier formatting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ๐Ÿค– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index bebc324..033ed5f 100644 --- a/README.md +++ b/README.md @@ -275,6 +275,7 @@ bin/setup-git-hooks ``` This will install a pre-commit hook that: + - Runs RuboCop on staged Ruby files - Runs all RSpec tests From f007be6b96d615d99508d4c29b022d97a0dc7f86 Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Tue, 4 Nov 2025 17:27:20 -1000 Subject: [PATCH 03/11] Improve pre-commit hook performance and usability MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit addresses performance concerns and improves the developer experience with the pre-commit hook based on community feedback. Key improvements: - Hook now runs only affected spec files (not entire test suite) * Maps lib/package_json/foo.rb to spec/package_json/foo_spec.rb * Includes any directly modified spec files * Results in sub-second test runs for most commits - Added hook version management (v1.0.0) for future updates - Integrated hook installation into bin/setup for automatic setup - Improved README documentation with clearer expectations - Hook still runs RuboCop on staged files only (fast feedback) Performance impact: - Before: 4+ minutes (full test suite on every commit) - After: <5 seconds for typical changes (affected tests only) - CI still runs full test suite to catch any issues Developer workflow: - Pre-commit: Fast checks on changed files only - Pre-push/PR: Run full `bundle exec rubocop` and `bundle exec rspec` - CI: Enforces all checks on entire codebase This balances fast local feedback with comprehensive CI coverage, reducing friction while maintaining code quality standards. ๐Ÿค– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- README.md | 25 ++++++++++++++++++----- bin/setup | 3 ++- bin/setup-git-hooks | 50 +++++++++++++++++++++++++++++++++++---------- 3 files changed, 61 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 033ed5f..b716764 100644 --- a/README.md +++ b/README.md @@ -268,18 +268,33 @@ prompt that will allow you to experiment. ### Git Hooks -To set up the pre-commit hook that prevents simple test and lint failures: +The repository includes a pre-commit hook that prevents simple test and lint +failures from being committed. The hook is automatically installed when you run +`bin/setup`. + +To manually install or update the hook: ```bash bin/setup-git-hooks ``` -This will install a pre-commit hook that: +The pre-commit hook provides fast feedback by: + +- Running RuboCop on staged Ruby files only (not the entire codebase) +- Running RSpec tests for affected files only (based on changed lib files) +- Skipping tests if only non-code files are changed + +**Important:** The pre-commit hook runs _fast_ checks only. Before pushing or +creating a PR, run the full test suite: + +```bash +bundle exec rubocop # Lint entire codebase +bundle exec rspec # Run full test suite +``` -- Runs RuboCop on staged Ruby files -- Runs all RSpec tests +CI will enforce that all tests and linting pass on the entire codebase. -To bypass the hook (not recommended), use `git commit --no-verify`. +To bypass the hook in exceptional cases, use `git commit --no-verify`. To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then tag diff --git a/bin/setup b/bin/setup index dce67d8..682c69c 100755 --- a/bin/setup +++ b/bin/setup @@ -5,4 +5,5 @@ set -vx bundle install -# Do any other automated setup that you need to do here +# Set up Git hooks automatically +bin/setup-git-hooks diff --git a/bin/setup-git-hooks b/bin/setup-git-hooks index 20b1f30..1b91141 100755 --- a/bin/setup-git-hooks +++ b/bin/setup-git-hooks @@ -5,6 +5,10 @@ # Run this after cloning the repository or when hooks are updated require "fileutils" +require "digest" + +# Hook version - update this when the hook logic changes +HOOK_VERSION = "1.0.0" git_dir = `git rev-parse --git-common-dir`.strip hooks_dir = File.join(git_dir, "hooks") @@ -20,6 +24,7 @@ FileUtils.mkdir_p(hooks_dir) unless File.directory?(hooks_dir) hook_content = <<~BASH #!/bin/bash # Pre-commit hook to run linting and tests on staged files + # Version: #{HOOK_VERSION} set -e @@ -37,7 +42,7 @@ hook_content = <<~BASH if [ -n "$STAGED_RUBY_FILES" ]; then echo -e "${YELLOW}Checking Ruby files with RuboCop...${NC}" - # Run RuboCop on staged files + # Run RuboCop on staged files only if ! bundle exec rubocop $STAGED_RUBY_FILES; then echo -e "${RED}RuboCop failed! Please fix the issues before committing.${NC}" echo -e "${YELLOW}You can run 'bundle exec rubocop -a' to auto-fix some issues.${NC}" @@ -45,15 +50,34 @@ hook_content = <<~BASH fi echo -e "${GREEN}RuboCop passed!${NC}" - else - echo -e "${YELLOW}No Ruby files to lint.${NC}" - fi - # Run RSpec tests - echo -e "${YELLOW}Running RSpec tests...${NC}" - if ! bundle exec rspec; then - echo -e "${RED}Tests failed! Please fix the tests before committing.${NC}" - exit 1 + # Find corresponding spec files for changed lib files + SPEC_FILES="" + for file in $STAGED_RUBY_FILES; do + if [[ $file == lib/* ]]; then + # Convert lib/package_json/foo.rb to spec/package_json/foo_spec.rb + spec_file=$(echo "$file" | sed 's|^lib/|spec/|' | sed 's|\\.rb$|_spec.rb|') + if [ -f "$spec_file" ]; then + SPEC_FILES="$SPEC_FILES $spec_file" + fi + elif [[ $file == spec/* ]]; then + # If it's already a spec file, include it + SPEC_FILES="$SPEC_FILES $file" + fi + done + + if [ -n "$SPEC_FILES" ]; then + echo -e "${YELLOW}Running affected RSpec tests...${NC}" + if ! bundle exec rspec $SPEC_FILES; then + echo -e "${RED}Tests failed! Please fix the tests before committing.${NC}" + exit 1 + fi + echo -e "${GREEN}Affected tests passed!${NC}" + else + echo -e "${YELLOW}No corresponding spec files found for changed files.${NC}" + fi + else + echo -e "${YELLOW}No Ruby files to check.${NC}" fi echo -e "${GREEN}All pre-commit checks passed!${NC}" @@ -64,9 +88,13 @@ File.write(pre_commit_hook, hook_content) FileUtils.chmod(0o755, pre_commit_hook) puts "โœ“ Pre-commit hook installed at #{pre_commit_hook}" +puts " Version: #{HOOK_VERSION}" puts "" puts "The pre-commit hook will:" -puts " โ€ข Run RuboCop on staged Ruby files" -puts " โ€ข Run all RSpec tests" +puts " โ€ข Run RuboCop on staged Ruby files only (fast)" +puts " โ€ข Run RSpec tests for affected files only (fast)" +puts " โ€ข Full test suite runs in CI" puts "" puts "To bypass the hook (not recommended), use: git commit --no-verify" +puts "" +puts "Note: Run 'bundle exec rubocop' before pushing to ensure all files pass linting." From 68d4dd4175384826c58b2312f65a78e7320b2814 Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Tue, 4 Nov 2025 17:43:45 -1000 Subject: [PATCH 04/11] Migrate to Lefthook for Git hooks management MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace custom Git hooks implementation with Lefthook, following the pattern used in React on Rails and Shakapacker projects. Key improvements: - Uses industry-standard Lefthook tool for hook management - Parallel execution of pre-commit hooks for faster feedback - Smart file detection using Lefthook's {staged_files} feature - Pre-push hooks run full test suite before remote push - Modular helper scripts in bin/lefthook/ directory - Automatic hook installation via bin/setup Hook configuration: - Pre-commit (parallel, staged files only): * RuboCop: Lints staged Ruby files * RSpec: Runs tests for affected files * Trailing newlines: Ensures proper file endings - Pre-push (comprehensive): * Full RuboCop on entire codebase * Full RSpec test suite Performance: - Pre-commit: <5 seconds (staged files only, parallel execution) - Pre-push: Full validation before sharing changes - CI: Complete verification on all platforms Benefits over custom solution: - Maintained by community (Evil Martians) - Built-in parallel execution - Better file filtering with globs - Easy to extend with new hooks - Consistent with React on Rails/Shakapacker ecosystem - Automatic re-staging of auto-fixed files (future enhancement) Migration notes: - Removed bin/setup-git-hooks (replaced by lefthook install) - Added lefthook gem to Gemfile - Created modular helper scripts for maintainability - Updated README with comprehensive hook documentation ๐Ÿค– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- Gemfile | 1 + Gemfile.lock | 2 + README.md | 44 +++++++----- bin/lefthook/check-trailing-newlines | 33 +++++++++ bin/lefthook/rspec-affected | 43 ++++++++++++ bin/lefthook/rubocop-lint | 23 ++++++ bin/setup | 4 +- bin/setup-git-hooks | 100 --------------------------- lefthook.yml | 25 +++++++ 9 files changed, 157 insertions(+), 118 deletions(-) create mode 100755 bin/lefthook/check-trailing-newlines create mode 100755 bin/lefthook/rspec-affected create mode 100755 bin/lefthook/rubocop-lint delete mode 100755 bin/setup-git-hooks create mode 100644 lefthook.yml diff --git a/Gemfile b/Gemfile index ce4f885..11a7eb9 100644 --- a/Gemfile +++ b/Gemfile @@ -5,6 +5,7 @@ source "https://rubygems.org" # Specify your gem's dependencies in package_json.gemspec gemspec +gem "lefthook", require: false gem "rake", "~> 13.0" gem "rspec", "~> 3.0" gem "rubocop", "< 1.51" # TODO: this version dropped support for Ruby 2.6 diff --git a/Gemfile.lock b/Gemfile.lock index 16f0442..0ed3466 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -10,6 +10,7 @@ GEM diff-lcs (1.5.0) docile (1.4.0) json (2.7.6) + lefthook (2.0.2) parallel (1.23.0) parser (3.2.2.3) ast (~> 2.4.1) @@ -73,6 +74,7 @@ PLATFORMS x86_64-linux DEPENDENCIES + lefthook package_json! rake (~> 13.0) rspec (~> 3.0) diff --git a/README.md b/README.md index b716764..e5317e2 100644 --- a/README.md +++ b/README.md @@ -268,33 +268,45 @@ prompt that will allow you to experiment. ### Git Hooks -The repository includes a pre-commit hook that prevents simple test and lint -failures from being committed. The hook is automatically installed when you run -`bin/setup`. +This project uses [Lefthook](https://github.com/evilmartians/lefthook) to manage +Git hooks, providing fast and efficient pre-commit checks. Hooks are +automatically installed when you run `bin/setup`. -To manually install or update the hook: +To manually install or update hooks: ```bash -bin/setup-git-hooks +bundle exec lefthook install ``` -The pre-commit hook provides fast feedback by: +#### Pre-commit Hooks (Fast) -- Running RuboCop on staged Ruby files only (not the entire codebase) -- Running RSpec tests for affected files only (based on changed lib files) -- Skipping tests if only non-code files are changed +The pre-commit hooks run in parallel on staged files only: -**Important:** The pre-commit hook runs _fast_ checks only. Before pushing or -creating a PR, run the full test suite: +- **RuboCop**: Lints staged Ruby files with auto-fix suggestions +- **RSpec**: Runs tests for affected files (maps `lib/` changes to `spec/`) +- **Trailing Newlines**: Ensures all files end with a newline + +These hooks complete in seconds, not minutes, providing immediate feedback. + +#### Pre-push Hooks (Comprehensive) + +Before pushing to remote, Lefthook runs the full test suite: + +- **Full RuboCop**: Lints entire codebase +- **Full RSpec**: Runs all tests + +This ensures comprehensive verification before sharing your changes. + +#### Bypassing Hooks + +In exceptional cases, you can bypass hooks: ```bash -bundle exec rubocop # Lint entire codebase -bundle exec rspec # Run full test suite +git commit --no-verify # Skip pre-commit hooks +git push --no-verify # Skip pre-push hooks ``` -CI will enforce that all tests and linting pass on the entire codebase. - -To bypass the hook in exceptional cases, use `git commit --no-verify`. +**Note:** CI will enforce all checks regardless of local bypasses. To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then tag diff --git a/bin/lefthook/check-trailing-newlines b/bin/lefthook/check-trailing-newlines new file mode 100755 index 0000000..4cb05a9 --- /dev/null +++ b/bin/lefthook/check-trailing-newlines @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +# Check for trailing newlines on staged files +set -euo pipefail + +files="$*" + +if [ -z "$files" ]; then + echo "โœ… No files to check for trailing newlines" + exit 0 +fi + +echo "๐Ÿ” Checking trailing newlines on staged files..." + +failed_files="" +for file in $files; do + if [ -f "$file" ] && [ -s "$file" ]; then + if ! tail -c 1 "$file" | grep -q '^$'; then + echo "โŒ Missing trailing newline: $file" + failed_files="$failed_files $file" + fi + fi +done + +if [ -n "$failed_files" ]; then + echo "" + echo "โŒ Trailing newline check failed!" + echo "๐Ÿ’ก Add trailing newlines to:$failed_files" + echo "๐Ÿ”ง Quick fix: for file in$failed_files; do echo >> \"\$file\"; done" + echo "๐Ÿšซ Skip hook: git commit --no-verify" + exit 1 +fi + +echo "โœ… All files have proper trailing newlines" diff --git a/bin/lefthook/rspec-affected b/bin/lefthook/rspec-affected new file mode 100755 index 0000000..b318f4b --- /dev/null +++ b/bin/lefthook/rspec-affected @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +# Run RSpec tests for affected files +set -euo pipefail + +files="$*" + +if [ -z "$files" ]; then + echo "โœ… No Ruby files changed" + exit 0 +fi + +# Find corresponding spec files for changed lib files +spec_files="" +for file in $files; do + if [[ $file == lib/* ]]; then + # Convert lib/package_json/foo.rb to spec/package_json/foo_spec.rb + spec_file=$(echo "$file" | sed 's|^lib/|spec/|' | sed 's|\.rb$|_spec.rb|') + if [ -f "$spec_file" ]; then + spec_files="$spec_files $spec_file" + fi + elif [[ $file == spec/* ]]; then + # If it's already a spec file, include it + spec_files="$spec_files $file" + fi +done + +if [ -z "$spec_files" ]; then + echo "โœ… No corresponding spec files found for changed files" + exit 0 +fi + +echo "๐Ÿงช Running affected RSpec tests:" +printf " %s\n" $spec_files + +if ! bundle exec rspec $spec_files; then + echo "" + echo "โŒ Tests failed!" + echo "๐Ÿ’ก Run full suite: bundle exec rspec" + echo "๐Ÿšซ Skip hook: git commit --no-verify" + exit 1 +fi + +echo "โœ… Affected tests passed" diff --git a/bin/lefthook/rubocop-lint b/bin/lefthook/rubocop-lint new file mode 100755 index 0000000..123b6fd --- /dev/null +++ b/bin/lefthook/rubocop-lint @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +# Lint Ruby files with RuboCop +set -euo pipefail + +files="$*" + +if [ -z "$files" ]; then + echo "โœ… No Ruby files to lint" + exit 0 +fi + +echo "๐Ÿ” RuboCop on staged Ruby files:" +printf " %s\n" $files + +if ! bundle exec rubocop --force-exclusion --display-cop-names -- $files; then + echo "" + echo "โŒ RuboCop check failed!" + echo "๐Ÿ’ก Auto-fix: bundle exec rubocop -a --force-exclusion -- $files" + echo "๐Ÿšซ Skip hook: git commit --no-verify" + exit 1 +fi + +echo "โœ… RuboCop checks passed" diff --git a/bin/setup b/bin/setup index 682c69c..b2830ae 100755 --- a/bin/setup +++ b/bin/setup @@ -5,5 +5,5 @@ set -vx bundle install -# Set up Git hooks automatically -bin/setup-git-hooks +# Set up Git hooks with Lefthook +bundle exec lefthook install diff --git a/bin/setup-git-hooks b/bin/setup-git-hooks deleted file mode 100755 index 1b91141..0000000 --- a/bin/setup-git-hooks +++ /dev/null @@ -1,100 +0,0 @@ -#!/usr/bin/env ruby -# frozen_string_literal: true - -# Script to set up Git hooks -# Run this after cloning the repository or when hooks are updated - -require "fileutils" -require "digest" - -# Hook version - update this when the hook logic changes -HOOK_VERSION = "1.0.0" - -git_dir = `git rev-parse --git-common-dir`.strip -hooks_dir = File.join(git_dir, "hooks") -pre_commit_hook = File.join(hooks_dir, "pre-commit") -project_root = File.expand_path("..", __dir__) - -puts "Setting up Git hooks..." - -# Create hooks directory if it doesn't exist -FileUtils.mkdir_p(hooks_dir) unless File.directory?(hooks_dir) - -# Create the pre-commit hook -hook_content = <<~BASH - #!/bin/bash - # Pre-commit hook to run linting and tests on staged files - # Version: #{HOOK_VERSION} - - set -e - - # Colors for output - RED='\\033[0;31m' - GREEN='\\033[0;32m' - YELLOW='\\033[1;33m' - NC='\\033[0m' # No Color - - echo -e "${YELLOW}Running pre-commit checks...${NC}" - - # Get the list of staged Ruby files - STAGED_RUBY_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep '\\.rb$' || true) - - if [ -n "$STAGED_RUBY_FILES" ]; then - echo -e "${YELLOW}Checking Ruby files with RuboCop...${NC}" - - # Run RuboCop on staged files only - if ! bundle exec rubocop $STAGED_RUBY_FILES; then - echo -e "${RED}RuboCop failed! Please fix the issues before committing.${NC}" - echo -e "${YELLOW}You can run 'bundle exec rubocop -a' to auto-fix some issues.${NC}" - exit 1 - fi - - echo -e "${GREEN}RuboCop passed!${NC}" - - # Find corresponding spec files for changed lib files - SPEC_FILES="" - for file in $STAGED_RUBY_FILES; do - if [[ $file == lib/* ]]; then - # Convert lib/package_json/foo.rb to spec/package_json/foo_spec.rb - spec_file=$(echo "$file" | sed 's|^lib/|spec/|' | sed 's|\\.rb$|_spec.rb|') - if [ -f "$spec_file" ]; then - SPEC_FILES="$SPEC_FILES $spec_file" - fi - elif [[ $file == spec/* ]]; then - # If it's already a spec file, include it - SPEC_FILES="$SPEC_FILES $file" - fi - done - - if [ -n "$SPEC_FILES" ]; then - echo -e "${YELLOW}Running affected RSpec tests...${NC}" - if ! bundle exec rspec $SPEC_FILES; then - echo -e "${RED}Tests failed! Please fix the tests before committing.${NC}" - exit 1 - fi - echo -e "${GREEN}Affected tests passed!${NC}" - else - echo -e "${YELLOW}No corresponding spec files found for changed files.${NC}" - fi - else - echo -e "${YELLOW}No Ruby files to check.${NC}" - fi - - echo -e "${GREEN}All pre-commit checks passed!${NC}" - exit 0 -BASH - -File.write(pre_commit_hook, hook_content) -FileUtils.chmod(0o755, pre_commit_hook) - -puts "โœ“ Pre-commit hook installed at #{pre_commit_hook}" -puts " Version: #{HOOK_VERSION}" -puts "" -puts "The pre-commit hook will:" -puts " โ€ข Run RuboCop on staged Ruby files only (fast)" -puts " โ€ข Run RSpec tests for affected files only (fast)" -puts " โ€ข Full test suite runs in CI" -puts "" -puts "To bypass the hook (not recommended), use: git commit --no-verify" -puts "" -puts "Note: Run 'bundle exec rubocop' before pushing to ensure all files pass linting." diff --git a/lefthook.yml b/lefthook.yml new file mode 100644 index 0000000..698c6c7 --- /dev/null +++ b/lefthook.yml @@ -0,0 +1,25 @@ +# Lefthook configuration +# Fast pre-commit hooks that check staged files only +# Install with: bundle exec lefthook install + +pre-commit: + parallel: true + commands: + rubocop: + glob: '*.rb' + run: bin/lefthook/rubocop-lint {staged_files} + + rspec: + glob: '*.rb' + run: bin/lefthook/rspec-affected {staged_files} + + trailing-newlines: + run: bin/lefthook/check-trailing-newlines {staged_files} + +pre-push: + commands: + full-rubocop: + run: bundle exec rubocop + + full-rspec: + run: bundle exec rspec From 77ff7fc3b470242bb12e9d3488db64b2d5dae050 Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Tue, 4 Nov 2025 22:31:19 -1000 Subject: [PATCH 05/11] fix: correct glob patterns to match Ruby files in subdirectories MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The glob patterns '*.rb' only match files in the root directory. Changed to '**/*.rb' to properly match Ruby files in all subdirectories like lib/, spec/, etc. This ensures hooks run for files in subdirectories as intended. ๐Ÿค– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- lefthook.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lefthook.yml b/lefthook.yml index 698c6c7..7cae823 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -6,11 +6,11 @@ pre-commit: parallel: true commands: rubocop: - glob: '*.rb' + glob: '**/*.rb' run: bin/lefthook/rubocop-lint {staged_files} rspec: - glob: '*.rb' + glob: '**/*.rb' run: bin/lefthook/rspec-affected {staged_files} trailing-newlines: From 7f5cc9d4fd64d3e80ce3bc18269e4e62f9cbf671 Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Thu, 6 Nov 2025 13:29:11 -1000 Subject: [PATCH 06/11] fix: handle filenames with spaces safely in hook scripts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixed critical shell scripting issues in all helper scripts: Security & Reliability Fixes: - Use proper quoted variable expansion ("$@" instead of $files) - Use bash arrays instead of string concatenation for file lists - Protect against command injection with special characters - Handle filenames with spaces, newlines, and special chars safely Specific Changes: - rubocop-lint: Use "$@" for file arguments, safer printf - rspec-affected: Use array for spec_files, proper quoting - check-trailing-newlines: Use array for failed_files Output Fixes: - Added missing spaces in error messages (lines 27-28) - Use array expansion with spaces: ${files[*]} All scripts now follow bash best practices for safe file handling. ๐Ÿค– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- bin/lefthook/check-trailing-newlines | 16 +++++++--------- bin/lefthook/rspec-affected | 18 ++++++++---------- bin/lefthook/rubocop-lint | 10 ++++------ 3 files changed, 19 insertions(+), 25 deletions(-) diff --git a/bin/lefthook/check-trailing-newlines b/bin/lefthook/check-trailing-newlines index 4cb05a9..08f5057 100755 --- a/bin/lefthook/check-trailing-newlines +++ b/bin/lefthook/check-trailing-newlines @@ -2,30 +2,28 @@ # Check for trailing newlines on staged files set -euo pipefail -files="$*" - -if [ -z "$files" ]; then +if [ $# -eq 0 ]; then echo "โœ… No files to check for trailing newlines" exit 0 fi echo "๐Ÿ” Checking trailing newlines on staged files..." -failed_files="" -for file in $files; do +failed_files=() +for file in "$@"; do if [ -f "$file" ] && [ -s "$file" ]; then if ! tail -c 1 "$file" | grep -q '^$'; then echo "โŒ Missing trailing newline: $file" - failed_files="$failed_files $file" + failed_files+=("$file") fi fi done -if [ -n "$failed_files" ]; then +if [ ${#failed_files[@]} -gt 0 ]; then echo "" echo "โŒ Trailing newline check failed!" - echo "๐Ÿ’ก Add trailing newlines to:$failed_files" - echo "๐Ÿ”ง Quick fix: for file in$failed_files; do echo >> \"\$file\"; done" + echo "๐Ÿ’ก Add trailing newlines to: ${failed_files[*]}" + echo "๐Ÿ”ง Quick fix: for file in ${failed_files[*]}; do echo >> \"\$file\"; done" echo "๐Ÿšซ Skip hook: git commit --no-verify" exit 1 fi diff --git a/bin/lefthook/rspec-affected b/bin/lefthook/rspec-affected index b318f4b..7e759a0 100755 --- a/bin/lefthook/rspec-affected +++ b/bin/lefthook/rspec-affected @@ -2,37 +2,35 @@ # Run RSpec tests for affected files set -euo pipefail -files="$*" - -if [ -z "$files" ]; then +if [ $# -eq 0 ]; then echo "โœ… No Ruby files changed" exit 0 fi # Find corresponding spec files for changed lib files -spec_files="" -for file in $files; do +spec_files=() +for file in "$@"; do if [[ $file == lib/* ]]; then # Convert lib/package_json/foo.rb to spec/package_json/foo_spec.rb spec_file=$(echo "$file" | sed 's|^lib/|spec/|' | sed 's|\.rb$|_spec.rb|') if [ -f "$spec_file" ]; then - spec_files="$spec_files $spec_file" + spec_files+=("$spec_file") fi elif [[ $file == spec/* ]]; then # If it's already a spec file, include it - spec_files="$spec_files $file" + spec_files+=("$file") fi done -if [ -z "$spec_files" ]; then +if [ ${#spec_files[@]} -eq 0 ]; then echo "โœ… No corresponding spec files found for changed files" exit 0 fi echo "๐Ÿงช Running affected RSpec tests:" -printf " %s\n" $spec_files +printf " %s\n" "${spec_files[@]}" -if ! bundle exec rspec $spec_files; then +if ! bundle exec rspec "${spec_files[@]}"; then echo "" echo "โŒ Tests failed!" echo "๐Ÿ’ก Run full suite: bundle exec rspec" diff --git a/bin/lefthook/rubocop-lint b/bin/lefthook/rubocop-lint index 123b6fd..9e8be9c 100755 --- a/bin/lefthook/rubocop-lint +++ b/bin/lefthook/rubocop-lint @@ -2,20 +2,18 @@ # Lint Ruby files with RuboCop set -euo pipefail -files="$*" - -if [ -z "$files" ]; then +if [ $# -eq 0 ]; then echo "โœ… No Ruby files to lint" exit 0 fi echo "๐Ÿ” RuboCop on staged Ruby files:" -printf " %s\n" $files +printf " %s\n" "$@" -if ! bundle exec rubocop --force-exclusion --display-cop-names -- $files; then +if ! bundle exec rubocop --force-exclusion --display-cop-names -- "$@"; then echo "" echo "โŒ RuboCop check failed!" - echo "๐Ÿ’ก Auto-fix: bundle exec rubocop -a --force-exclusion -- $files" + echo "๐Ÿ’ก Auto-fix: bundle exec rubocop -a --force-exclusion -- " echo "๐Ÿšซ Skip hook: git commit --no-verify" exit 1 fi From 952ed56cd7d116549aabc506891764f39bb8858d Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Mon, 10 Nov 2025 13:17:04 -1000 Subject: [PATCH 07/11] fix: address PR review feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Move Git Hooks section in README to proper location - Now appears after gem installation paragraph - Properly grouped under Development section 2. Fix shell safety in check-trailing-newlines - Changed quick-fix suggestion to use "${failed_files[@]}" - Ensures filenames with spaces are handled correctly - Prevents word splitting in the suggested command Addresses review comments from @G-Rath in PR #32 ๐Ÿค– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- README.md | 10 +++++----- bin/lefthook/check-trailing-newlines | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index e5317e2..58a396b 100644 --- a/README.md +++ b/README.md @@ -266,6 +266,11 @@ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. +To install this gem onto your local machine, run `bundle exec rake install`. To +release a new version, update the version number in `version.rb`, and then tag +the commit with the version prefixed with a `v`, which will trigger the release +workflow to publish the new version to [rubygems.org](https://rubygems.org). + ### Git Hooks This project uses [Lefthook](https://github.com/evilmartians/lefthook) to manage @@ -308,11 +313,6 @@ git push --no-verify # Skip pre-push hooks **Note:** CI will enforce all checks regardless of local bypasses. -To install this gem onto your local machine, run `bundle exec rake install`. To -release a new version, update the version number in `version.rb`, and then tag -the commit with the version prefixed with a `v`, which will trigger the release -workflow to publish the new version to [rubygems.org](https://rubygems.org). - ## Contributing Bug reports and pull requests are welcome on GitHub at diff --git a/bin/lefthook/check-trailing-newlines b/bin/lefthook/check-trailing-newlines index 08f5057..b4678be 100755 --- a/bin/lefthook/check-trailing-newlines +++ b/bin/lefthook/check-trailing-newlines @@ -23,7 +23,7 @@ if [ ${#failed_files[@]} -gt 0 ]; then echo "" echo "โŒ Trailing newline check failed!" echo "๐Ÿ’ก Add trailing newlines to: ${failed_files[*]}" - echo "๐Ÿ”ง Quick fix: for file in ${failed_files[*]}; do echo >> \"\$file\"; done" + echo "๐Ÿ”ง Quick fix: for file in \"\${failed_files[@]}\"; do echo >> \"\$file\"; done" echo "๐Ÿšซ Skip hook: git commit --no-verify" exit 1 fi From ce81aceadba9b6af1c61de106d7e837dafa0325f Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Tue, 18 Nov 2025 20:12:50 -1000 Subject: [PATCH 08/11] docs: address PR review feedback on git hooks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Improve documentation, robustness, and performance based on review: - Add comments explaining --force-exclusion usage in lefthook.yml - Document 1:1 mapping limitation in rspec-affected script - Add file existence validation to handle deleted files in hooks - Clarify execution time variability in README - Enable parallel execution for pre-push hooks (faster feedback) All tests passing (294 examples, 0 failures, 100% coverage). ๐Ÿค– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- README.md | 8 ++++++-- bin/lefthook/rspec-affected | 11 +++++++++++ bin/lefthook/rubocop-lint | 17 +++++++++++++++-- lefthook.yml | 3 +++ 4 files changed, 35 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 58a396b..5b5f2fd 100644 --- a/README.md +++ b/README.md @@ -289,13 +289,17 @@ The pre-commit hooks run in parallel on staged files only: - **RuboCop**: Lints staged Ruby files with auto-fix suggestions - **RSpec**: Runs tests for affected files (maps `lib/` changes to `spec/`) + - _Note: Assumes 1:1 mapping between lib/ and spec/ files. Won't catch changes + to shared modules or files without specs. Run full suite for comprehensive + testing._ - **Trailing Newlines**: Ensures all files end with a newline -These hooks complete in seconds, not minutes, providing immediate feedback. +These hooks typically complete in seconds, though execution time varies based on +the number of affected files and test complexity. #### Pre-push Hooks (Comprehensive) -Before pushing to remote, Lefthook runs the full test suite: +Before pushing to remote, Lefthook runs the full test suite in parallel: - **Full RuboCop**: Lints entire codebase - **Full RSpec**: Runs all tests diff --git a/bin/lefthook/rspec-affected b/bin/lefthook/rspec-affected index 7e759a0..1d85691 100755 --- a/bin/lefthook/rspec-affected +++ b/bin/lefthook/rspec-affected @@ -1,5 +1,11 @@ #!/usr/bin/env bash # Run RSpec tests for affected files +# NOTE: This script assumes a 1:1 mapping between lib/ and spec/ files. +# It will NOT catch: +# - Changes to files without corresponding specs +# - Shared modules that affect multiple specs +# - Changes that require integration tests +# For comprehensive testing, run the full suite: bundle exec rspec set -euo pipefail if [ $# -eq 0 ]; then @@ -10,6 +16,11 @@ fi # Find corresponding spec files for changed lib files spec_files=() for file in "$@"; do + # Skip non-existent files (e.g., deleted files) + if [ ! -f "$file" ]; then + continue + fi + if [[ $file == lib/* ]]; then # Convert lib/package_json/foo.rb to spec/package_json/foo_spec.rb spec_file=$(echo "$file" | sed 's|^lib/|spec/|' | sed 's|\.rb$|_spec.rb|') diff --git a/bin/lefthook/rubocop-lint b/bin/lefthook/rubocop-lint index 9e8be9c..40c6ff0 100755 --- a/bin/lefthook/rubocop-lint +++ b/bin/lefthook/rubocop-lint @@ -7,10 +7,23 @@ if [ $# -eq 0 ]; then exit 0 fi +# Filter out non-existent files (e.g., deleted files) +existing_files=() +for file in "$@"; do + if [ -f "$file" ]; then + existing_files+=("$file") + fi +done + +if [ ${#existing_files[@]} -eq 0 ]; then + echo "โœ… No existing Ruby files to lint" + exit 0 +fi + echo "๐Ÿ” RuboCop on staged Ruby files:" -printf " %s\n" "$@" +printf " %s\n" "${existing_files[@]}" -if ! bundle exec rubocop --force-exclusion --display-cop-names -- "$@"; then +if ! bundle exec rubocop --force-exclusion --display-cop-names -- "${existing_files[@]}"; then echo "" echo "โŒ RuboCop check failed!" echo "๐Ÿ’ก Auto-fix: bundle exec rubocop -a --force-exclusion -- " diff --git a/lefthook.yml b/lefthook.yml index 7cae823..4272bf3 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -7,6 +7,8 @@ pre-commit: commands: rubocop: glob: '**/*.rb' + # Uses --force-exclusion to respect .rubocop.yml exclusions (e.g., bin/**) + # even when files are explicitly passed as arguments run: bin/lefthook/rubocop-lint {staged_files} rspec: @@ -17,6 +19,7 @@ pre-commit: run: bin/lefthook/check-trailing-newlines {staged_files} pre-push: + parallel: true commands: full-rubocop: run: bundle exec rubocop From db4154f3f6dd8db42664655b77c75718c204da75 Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Wed, 19 Nov 2025 20:20:20 -1000 Subject: [PATCH 09/11] refactor: remove RSpec from pre-commit hook MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Running the full test suite on every commit is too slow (5-10 minutes) and requires all package managers to be installed. This creates a poor developer experience and encourages using --no-verify to bypass hooks. Changes: - Removed rspec hook from pre-commit in lefthook.yml - Deleted bin/lefthook/rspec-affected script - Updated README to reflect faster pre-commit hooks - Kept RSpec in pre-push for comprehensive verification Pre-commit hooks now focus on fast linting checks only: - RuboCop (staged files only) - Trailing newline check All tests passing: 294 examples, 0 failures, 100% coverage. ๐Ÿค– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- README.md | 7 +---- bin/lefthook/rspec-affected | 52 ------------------------------------- lefthook.yml | 4 --- 3 files changed, 1 insertion(+), 62 deletions(-) delete mode 100755 bin/lefthook/rspec-affected diff --git a/README.md b/README.md index 5b5f2fd..8aeb51c 100644 --- a/README.md +++ b/README.md @@ -288,14 +288,9 @@ bundle exec lefthook install The pre-commit hooks run in parallel on staged files only: - **RuboCop**: Lints staged Ruby files with auto-fix suggestions -- **RSpec**: Runs tests for affected files (maps `lib/` changes to `spec/`) - - _Note: Assumes 1:1 mapping between lib/ and spec/ files. Won't catch changes - to shared modules or files without specs. Run full suite for comprehensive - testing._ - **Trailing Newlines**: Ensures all files end with a newline -These hooks typically complete in seconds, though execution time varies based on -the number of affected files and test complexity. +These hooks complete in seconds, providing immediate feedback on code quality. #### Pre-push Hooks (Comprehensive) diff --git a/bin/lefthook/rspec-affected b/bin/lefthook/rspec-affected deleted file mode 100755 index 1d85691..0000000 --- a/bin/lefthook/rspec-affected +++ /dev/null @@ -1,52 +0,0 @@ -#!/usr/bin/env bash -# Run RSpec tests for affected files -# NOTE: This script assumes a 1:1 mapping between lib/ and spec/ files. -# It will NOT catch: -# - Changes to files without corresponding specs -# - Shared modules that affect multiple specs -# - Changes that require integration tests -# For comprehensive testing, run the full suite: bundle exec rspec -set -euo pipefail - -if [ $# -eq 0 ]; then - echo "โœ… No Ruby files changed" - exit 0 -fi - -# Find corresponding spec files for changed lib files -spec_files=() -for file in "$@"; do - # Skip non-existent files (e.g., deleted files) - if [ ! -f "$file" ]; then - continue - fi - - if [[ $file == lib/* ]]; then - # Convert lib/package_json/foo.rb to spec/package_json/foo_spec.rb - spec_file=$(echo "$file" | sed 's|^lib/|spec/|' | sed 's|\.rb$|_spec.rb|') - if [ -f "$spec_file" ]; then - spec_files+=("$spec_file") - fi - elif [[ $file == spec/* ]]; then - # If it's already a spec file, include it - spec_files+=("$file") - fi -done - -if [ ${#spec_files[@]} -eq 0 ]; then - echo "โœ… No corresponding spec files found for changed files" - exit 0 -fi - -echo "๐Ÿงช Running affected RSpec tests:" -printf " %s\n" "${spec_files[@]}" - -if ! bundle exec rspec "${spec_files[@]}"; then - echo "" - echo "โŒ Tests failed!" - echo "๐Ÿ’ก Run full suite: bundle exec rspec" - echo "๐Ÿšซ Skip hook: git commit --no-verify" - exit 1 -fi - -echo "โœ… Affected tests passed" diff --git a/lefthook.yml b/lefthook.yml index 4272bf3..1357b30 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -11,10 +11,6 @@ pre-commit: # even when files are explicitly passed as arguments run: bin/lefthook/rubocop-lint {staged_files} - rspec: - glob: '**/*.rb' - run: bin/lefthook/rspec-affected {staged_files} - trailing-newlines: run: bin/lefthook/check-trailing-newlines {staged_files} From d036e07b3ea84ff3ad6f5a375c529bbe36a734f9 Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Wed, 19 Nov 2025 21:35:17 -1000 Subject: [PATCH 10/11] docs: fix markdown code block style for markdownlint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace fenced code blocks with indented code blocks to comply with markdownlint MD046 rule, which expects indented style in this repo. Changes: - Converted ```bash blocks to 4-space indented code blocks - Fixed two occurrences in Git Hooks section ๐Ÿค– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- README.md | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 8aeb51c..74c8460 100644 --- a/README.md +++ b/README.md @@ -279,9 +279,7 @@ automatically installed when you run `bin/setup`. To manually install or update hooks: -```bash -bundle exec lefthook install -``` + bundle exec lefthook install #### Pre-commit Hooks (Fast) @@ -305,10 +303,8 @@ This ensures comprehensive verification before sharing your changes. In exceptional cases, you can bypass hooks: -```bash -git commit --no-verify # Skip pre-commit hooks -git push --no-verify # Skip pre-push hooks -``` + git commit --no-verify # Skip pre-commit hooks + git push --no-verify # Skip pre-push hooks **Note:** CI will enforce all checks regardless of local bypasses. From 30fc454f59f31597215cb7169f792aac3acdd5fa Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Wed, 19 Nov 2025 22:30:16 -1000 Subject: [PATCH 11/11] fix: correct trailing newline detection logic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous logic had a bug where it used `grep -q '^$'` on output from `tail -c 1`, which doesn't work correctly. The grep pattern matches empty lines, but tail -c 1 outputs a single character, not a line. Correct behavior: - Files ending with newline: tail -c 1 returns empty string - Files missing newline: tail -c 1 returns the last character Changed to use `[ -n "$(tail -c 1 "$file")" ]` which correctly detects when a file is missing a trailing newline. Tested with files both with and without trailing newlines. ๐Ÿค– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- bin/lefthook/check-trailing-newlines | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bin/lefthook/check-trailing-newlines b/bin/lefthook/check-trailing-newlines index b4678be..b400957 100755 --- a/bin/lefthook/check-trailing-newlines +++ b/bin/lefthook/check-trailing-newlines @@ -12,7 +12,8 @@ echo "๐Ÿ” Checking trailing newlines on staged files..." failed_files=() for file in "$@"; do if [ -f "$file" ] && [ -s "$file" ]; then - if ! tail -c 1 "$file" | grep -q '^$'; then + # Files ending with newline will have empty output from tail -c 1 + if [ -n "$(tail -c 1 "$file")" ]; then echo "โŒ Missing trailing newline: $file" failed_files+=("$file") fi