How to Write a Custom Overcommit PreCommit Git Hook in 4 Steps

This post summarizes the first ever Ruby talk I gave at a RubySG meetup

*Follow the instructions here to install Overcommit before reading the tutorial*

Despite Overcommit being an opinionated git hook manager, it is fully configurable and extendable as well. In this tutorial, I will walk you through on how you can write a pre-commit hook (with tests) that enforces a maximum length for each line in your files through the use of the APIs provided by the Overcommit gem.

Step 1. Putting the files in the right directory

By default, Overcommit looks for your custom hooks in the .git-hooks folder which we can configure by overriding plugin_directory in your .overcommit.yml settings file. For pre-commit hooks, you’ll want to put all your hooks under the following directory:

# <your_app>/.git-hooks/pre_commit/line_length.rb

Step 2. Write your custom hook

# <your_app>/.git_hooks/pre_commit/line_length.rb

module Overcommit::Hook::PreCommit
  class LineLength < Base
    # Overcommit expects you to override this method which will be called
    # everytime your pre-commit hook is run.
    def run
      # Create two arrays to hold our error and warning messages.
      error_lines = []
      warning_lines = []

      # The `config` attribute is a hash of
      # your settings based on your `.overcommit.yml` file.
      max_line_length = config['max']

      # For pre-commit hooks, `applicable_files` is one of the methods that
      # Overcommit provides which returns an array of files that have been
      # modified for the particular commit.
      applicable_files.each do |file|
        # `modified_lines` is another method provided by Overcommit. It will
        # return an array containing the index of lines in the file which have
        # been modified for the particular commit.
        modified_lines_num = modified_lines(file)

        # Loop through each file with the index., 'r').each_with_index do |line, index|
          # Check if the length of line is greater than our desired length.
          if line.length > max_line_length
            message = format("#{file}:#{index + 1}: Line is too long [%d/%d]",
              line.length, max_line_length)

            # If the line is included in our modified lines, we will add it to
            # `error_lines`, else add it to `warning_lines`.
            if modified_lines_num.include?(index + 1)
              error_lines << message
              warning_lines << message

      # Overcommit expects 1 of the 3 as return values, `:fail`, `:warn` or `:pass`.
      # If the hook returns `:fail`, the commit will be aborted with our message
      # containing the errors.
      return :fail, error_lines.join("\n") if error_lines.any?

      # If the hook returns `:warn`, the commit will continue with our message
      # containing the warnings.
      return :warn, "Modified files have lints (on lines you didn't modify)\n" <<
        warning_lines.join("\n") if warning_lines.any?


Step 3. Enable and configure your pre-commit hook in the settings file

    enabled: true  # All custom hooks are disabled by default.
    description: Checking for length of lines  # Prints a description when your hook is run.
    max: 89  # Setting the max length to 89 chars. Accessed through the `config` attribute in our hook.
      - '**/*.rb'  # Rubocop is already checking for the length of lines in Ruby.
      - '**/*.gif' # However, we are interested in ensuring that the line of lines
      - '**/*.jpg' # in our HAML and YAML files follow the same rules as well.
      - '**/*.png'

Step 4. Writing tests for your pre-commit hook

# spec/overcommit/pre_commit/line_length_spec.rb

require 'spec_helper'
require 'overcommit'
require 'overcommit/hook/pre_commit/base'
require Rails.root.join('.git-hooks/pre_commit/line_length')

describe Overcommit::Hook::PreCommit::LineLength do
  let(:config) do
    # Load our settings file and initialize an instance of `Overcommit::Configuration`.'.overcommit.yml'))

  # The context which the hook is running in. For pre-commit hooks, it will be
  # an instance of `Overcommit::HookContext::Precommit`. However, we would not be
  # needing this in our tests so we will just create a double for it.
  let(:context) { double('context') }

  # Path to our fixture.
  let(:staged_file) { '<your_app>/spec/overcommit/fixtures/test_file.txt' }

  # Create an instance of our hook passing in our configuration and context.
  subject! {, context) }

  before do
    # Stub `applicable_files` to return our fixture.
    allow(subject).to receive(:applicable_files).and_return([staged_file])

  context 'when file contains line which are too long' do
    context 'when all the lines have been modified' do
      before do
        # Stub `modified_lines` to return the scenario where all 3 lines have been
        # modified.
        allow(subject).to receive(:modified_lines).and_return([1, 2, 3])

      it 'should return the right status and error message' do
        expect( eq(
            "#{staged_file}:1: Line is too long [99/89]\n" <<
            "#{staged_file}:3: Line is too long [99/89]"

    context 'when only the second line has been modified' do
      # Assert that `` to return `:warn` with our warning lines.

  context 'when file do not contain line which are too long' do
    # Assert that `` to return `:pass`
# spec/overcommit/fixtures/test_file.txt

This line is purposely written so that it will be more than 89 characters and fail pre_commit hook
This line is short
This line is purposely written so that it will be more than 89 characters and fail pre_commit hook

That is all in writing a custom Overcommit pre-commit hook. If you’re interested to learn more about the API that Overcommit provides, I urge you to look into Overcommit’s library of hooks to see how other hooks are implemented.