Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -73,6 +74,7 @@ PLATFORMS
x86_64-linux

DEPENDENCIES
lefthook
package_json!
rake (~> 13.0)
rspec (~> 3.0)
Expand Down
42 changes: 42 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,48 @@ 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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you put this section after the existing paragraph ("To install this gem onto your local machine")? since that isn't part of this subheader


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 hooks:

```bash
bundle exec lefthook install
```

#### Pre-commit Hooks (Fast)

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/`)
- **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
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.

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
Expand Down
31 changes: 31 additions & 0 deletions bin/lefthook/check-trailing-newlines
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#!/usr/bin/env bash
# Check for trailing newlines on staged files
set -euo pipefail

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 "$@"; do
if [ -f "$file" ] && [ -s "$file" ]; then
if ! tail -c 1 "$file" | grep -q '^$'; then
echo "❌ Missing trailing newline: $file"
failed_files+=("$file")
fi
fi
done

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 "🚫 Skip hook: git commit --no-verify"
exit 1
fi

echo "✅ All files have proper trailing newlines"
41 changes: 41 additions & 0 deletions bin/lefthook/rspec-affected
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
#!/usr/bin/env bash
# Run RSpec tests for affected files
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
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"
21 changes: 21 additions & 0 deletions bin/lefthook/rubocop-lint
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#!/usr/bin/env bash
# Lint Ruby files with RuboCop
set -euo pipefail

if [ $# -eq 0 ]; then
echo "✅ No Ruby files to lint"
exit 0
fi

echo "🔍 RuboCop on staged Ruby files:"
printf " %s\n" "$@"

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 "🚫 Skip hook: git commit --no-verify"
exit 1
fi

echo "✅ RuboCop checks passed"
3 changes: 2 additions & 1 deletion bin/setup
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ set -vx

bundle install

# Do any other automated setup that you need to do here
# Set up Git hooks with Lefthook
bundle exec lefthook install
25 changes: 25 additions & 0 deletions lefthook.yml
Original file line number Diff line number Diff line change
@@ -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