Using Pre-Commit with StandardRB
Using pre-commit with StandardRB helps me keep my codebase formatted efficiently. Here's how I set that up.
When I was looking for a good resource on how to set up pre-commit to work with StandardRB, I could not find anything that did exactly what I needed. I wanted to use pre-commit to have StandardRB automatically fix any issues in staged files. Since other people might want to accomplish this same thing, I have consolidated this information for the public to see.
What was I trying to accomplish? #
If you are not familiar, StandardRB is "a linter & formatter built on RuboCop that provides an unconfigurable configuration to all of RuboCop's built-in rules," and pre-commit is "a framework for managing and maintaining multi-language pre-commit hooks."
Specifically, what I set out to build was a pre-commit hook managed by the pre-commit framework that runs standardrb --fix
before each of my commits, which will fix any formatting mistakes I made in my Ruby code and inform me of other issues that it cannot automatically fix. I also wanted this to only run on the Ruby files that were changed as part of the commit so the hook wouldn't take as long.
Hint: If you just want the code and do not care how I got there, I assembled it all in one place at the bottom of this article.
How did I accomplish it? #
First, I took the pattern established by the pre-commit-ruby gem for RuboCop and adapted it slightly for use by StandardRB. The result was two files.
First, my .pre-commit-config.yaml
:
repos:
- repo: local
hooks:
- id: standardrb
name: Check Ruby style with StandardRB
description: Enforce Ruby style guide with StandardRB
entry: bin/standardrb-wrapper.sh
language: script
types: ['ruby']
stages: [commit]
verbose: true
Then, my bin/standardrb-wrapper.sh
:
#!/bin/sh
bundle install >/dev/null
bundle exec standardrb --fix
This works fine, but because I was adding this to a fairly large repo, it would often take upwards of 10 seconds to run. That might not sound like much, but when it is taking that long for every commit, the time can add up. That's when I decided to try to figure out a way to make sure it only ran the fix on files that actually changed as part of that commit.
Since RuboCop is more widely used than StandardRB, I looked for resources that would help me run RuboCop only on modified files and I stumbled upon the article Running Rubocop Only On Modified Files. This article gave me what I needed to get going. Specifically, this command:
git ls-files -m | xargs ls -1 2>/dev/null | grep '\.rb$'
This command provides a list of the names of files that have been modified (git ls-files -m
) with the deleted files filtered out (xargs ls -1 2>/dev/null
) and then filtered down to just the Ruby files (grep '\.rb$'
), which is almost exactly the list I wanted to run the StandardRB fix on.
Unfortunately, the -m
in git ls-files -m
only gets unstaged files, but because this will run on files I am about to commit, they are staged. To get around this, I looked over the git diff
documentation to see if there were options to provide just the names of the staged files. Those options were --name-only
and --cached
(or its synonym --staged
).
So, I replaced git ls-files -m
with git diff --name-only --cached
and got this:
git diff --name-only --cached | xargs ls -1 2>/dev/null | grep '\.rb$'
This retrieves a list of non-deleted, staged Ruby files, which is exactly what I wanted. Now, I just needed to run the StandardRB fix on it. To do so, I once again followed the path of the Running Rubocop Only On Modified Files article and added a little bit to the end of that command, replacing the RuboCop fix with the StandardRB one:
git diff --name-only --cached | xargs ls -1 2>/dev/null | grep '\.rb$' | xargs bundle exec standardrb --fix
Then, I needed to plug that command into my shell script (bin/standardrb-wrapper.sh
):
#!/bin/sh
bundle install >/dev/null
git diff --name-only --cached | xargs ls -1 2>/dev/null | grep '\.rb$' | xargs bundle exec standardrb --fix
And there you have it: a pre-commit hook for StandardRB that only runs on the modified Ruby files.
Figuring this out took some effort, but it allowed me to keep my codebase formatted in an efficient way. If you would also like to have something like that in your codebase, I assembled all the code below for you.
Finished Code #
.pre-commit-config.yaml
repos:
- repo: local
hooks:
- id: standardrb
name: Check Ruby style with StandardRB
description: Enforce Ruby style guide with StandardRB
entry: bin/standardrb-wrapper.sh
language: script
types: ['ruby']
stages: [commit]
verbose: true
bin/standardrb-wrapper.sh
#!/bin/sh
bundle install >/dev/null
git diff --name-only --cached | xargs ls -1 2>/dev/null | grep '\.rb$' | xargs bundle exec standardrb --fix