Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,43 @@ 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
Git hooks, providing fast and efficient pre-commit checks. Hooks are
automatically installed when you run `bin/setup`.

To manually install or update hooks:

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
- **Trailing Newlines**: Ensures all files end with a newline

These hooks complete in seconds, providing immediate feedback on code quality.

#### Pre-push Hooks (Comprehensive)

Before pushing to remote, Lefthook runs the full test suite in parallel:

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

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.

## Contributing

Bug reports and pull requests are welcome on GitHub at
Expand Down
32 changes: 32 additions & 0 deletions bin/lefthook/check-trailing-newlines
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#!/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
# 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
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"
34 changes: 34 additions & 0 deletions bin/lefthook/rubocop-lint
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#!/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

# 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" "${existing_files[@]}"

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 -- <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
24 changes: 24 additions & 0 deletions lefthook.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# 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'
# 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}

trailing-newlines:
run: bin/lefthook/check-trailing-newlines {staged_files}

pre-push:
parallel: true
commands:
full-rubocop:
run: bundle exec rubocop

full-rspec:
run: bundle exec rspec