/overcommit

A fully configurable and extendable Git hook manager

Primary LanguageRubyMIT LicenseMIT

Gem Version Build Status Windows Build Status Coverage Status Maintainability Inline docs

Overcommit Logo

overcommit is a tool to manage and configure Git hooks.

In addition to supporting a wide variety of hooks that can be used across multiple repositories, you can also define hooks specific to a repository which are stored in source control. You can also easily add your existing hook scripts without writing any Ruby code.

Requirements

This project aims to support the following Ruby runtimes on both *nix and Windows:

  • Ruby 2.4+

Windows

If you are using Overcommit on Windows, make sure you include the ffi gem in your list of dependencies. Overcommit does not include the ffi gem by default since it significantly increases the install time for non-Windows platforms.

Dependencies

Some hooks have third-party dependencies. For example, to lint your SCSS files, you're going to need the scss_lint gem.

Depending on the hooks you enable/disable for your repository, you'll need to ensure your development environment already has those dependencies installed. Most hooks will display a warning if a required executable isn't available.

If you are using Bundler to manage your Ruby gem dependencies, you'll likely want to use the gemfile option to control which gem versions are available during your hook runs.

Installation

overcommit is installed via RubyGems. It is strongly recommended that your environment support running gem install without requiring root user privileges via sudo or otherwise. Using a Ruby version manager like rbenv or rvm is recommended.

Once you have an environment that allows you to install gems without sudo, run:

gem install overcommit

You can then run the overcommit command to install hooks into repositories.

mkdir important-project
cd important-project
git init
overcommit --install

After running overcommit --install, any existing hooks for your repository which Overcommit will replace will be backed up. You can restore everything to the way it was by running overcommit --uninstall.

Automatically Install Overcommit Hooks

If you want to use overcommit for all repositories you create/clone going forward, add the following to automatically run in your shell environment:

export GIT_TEMPLATE_DIR="$(overcommit --template-dir)"

The GIT_TEMPLATE_DIR provides a directory for Git to use as a template for automatically populating the .git directory. If you have your own template directory, you might just want to copy the contents of overcommit --template-dir to that directory.

Usage

Once you've installed the hooks via overcommit --install, they will automatically run when the appropriate hook is triggered.

The overcommit executable supports the following command-line flags:

Command Line Flag Description
-i/--install Install Overcommit hooks in a repository
-u/--uninstall Remove Overcommit hooks from a repository
-f/--force Don't bail on install if other hooks already exist--overwrite them
-l/--list-hooks Display all available hooks in the current repository
-r/--run Run pre-commit hook against all tracked files in repository
-t/--template-dir Print location of template directory
-h/--help Show command-line flag documentation
-v/--version Show version

Skipping Hooks

Sometimes a hook will report an error that for one reason or another you'll want to ignore. To prevent these errors from blocking your commit, you can include the name of the relevant hook in the SKIP environment variable, e.g.

SKIP=RuboCop git commit

If you would prefer to specify a whitelist of hooks rather than a blacklist, use the ONLY environment variable instead.

ONLY=RuboCop git commit

Use this feature sparingly, as there is no point to having the hook in the first place if you're just going to ignore it. If you want to ensure a hook is never skipped, set the required option to true in its configuration. If you attempt to skip it, you'll see a warning telling you that the hook is required, and the hook will still run.

Disabling Overcommit

If you have scripts that execute git commands where you don't want Overcommit hooks to run, you can disable Overcommit entirely by setting the OVERCOMMIT_DISABLE environment variable.

OVERCOMMIT_DISABLE=1 ./my-custom-script

Disabling Colorized Output

Overcommit automatically colorizes its output based on whether it is outputting to a TTY. However, you can manually enable/disable color by setting the OVERCOMMIT_COLOR environment variable.

OVERCOMMIT_COLOR=0 git commit

Continuous Integration

You can run the same set of hooks that would be executed in a pre-commit hook against your entire repository by running overcommit --run. This makes it easy to have the checks verified by a CI service such as Travis CI, including custom hooks you've written yourself.

The --run flag works by creating a pre-commit context that assumes all the files in your repository have changed, and follows the same rules as a normal pre-commit check. If any hook fails with an error, it will return a non-zero exit code.

Configuration

Overcommit provides a flexible configuration system that allows you to tailor the built-in hooks to suit your workflow. All configuration specific to a repository is stored in .overcommit.yml in the top-level directory of the repository.

When writing your own configuration, it will automatically extend the default configuration, so you only need to specify your configuration with respect to the default. In order to enable/disable hooks, you can add the following to your repo-specific configuration file:

PreCommit:
  RuboCop:
    enabled: true
    command: ['bundle', 'exec', 'rubocop'] # Invoke within Bundler context

Hook Options

Individual hooks expose both built-in configuration options as well as their own custom options unique to each hook. The following table lists all built-in configuration options:

Option Description
enabled If false, this hook will never be run
required If true, this hook cannot be skipped via the SKIP environment variable
quiet If true, this hook does not display any output unless it warns/fails
description Message displayed while hook is running.
requires_files If true, this hook runs only if files that are applicable to it have been modified. See include and exclude for how to specify applicable files.
include File paths or glob patterns of files that apply to this hook. The hook will only run on the applicable files when they have been modified. Note that the concept of modified varies for different types of hooks. By default, include matches every file until you specify a list of patterns.
exclude File paths or glob patterns of files that do not apply to this hook. This is used to exclude any files that would have been matched by include.
exclude_branches List of branch names or glob patterns of branches that this hook should not run against.
exclude_remotes PrePush hooks only. List of remote names that the hook should not run against.
include_remote_ref_deletions PrePush hooks only. By default, PrePush hooks will not run for pushes that delete a remote ref (i.e. branches or tags). Set to true to have the hook run even for deleted remote ref.
problem_on_unmodified_line How to treat errors reported on lines that weren't modified during the action captured by this hook (e.g. for pre-commit hooks, warnings/errors reported on lines that were not staged with git add may not be warnings/errors you care about). Valid values are report: report errors/warnings as-is regardless of line location (default); warn: report errors as warnings if they are on lines you didn't modify; and ignore: don't display errors/warnings at all if they are on lines you didn't modify (ignore is not recommended).
on_fail Change the status of a failed hook to warn or pass. This allows you to treat failures as warnings or potentially ignore them entirely, but you should use caution when doing so as you might be hiding important information.
on_warn Similar to on_fail, change the status of a hook that returns a warning status to either pass (you wish to silence warnings entirely) or fail (you wish to treat all warnings as errors).
required_executable Name of an executable that must exist in order for the hook to run. If this is a path (e.g. ./bin/ruby), ensures that the executable file exists at the given location relative to the repository root. Otherwise, if it just the name of an executable (e.g. ruby) checks if the executable can be found in one of the directories in the PATH environment variable. Set this to a specific path if you want to always use an executable that is stored in your repository. (e.g. RubyGems bin stubs, Node.js binaries, etc.)
required_library/required_libraries List of Ruby libraries to load with Kernel.require before the hook runs. This is specifically for hooks that integrate with external Ruby libraries.
command Array of arguments to use as the command. How each hook uses this is different, but it allows hooks to change the context with which they run. For example, you can change the command to be ['bundle', 'exec', 'rubocop'] instead of just rubocop so that you can use the gem versions specified in your local Gemfile.lock. This defaults to the name of the required_executable.
flags Array of arguments to append to the command. This is useful for customizing the behavior of a tool. It's also useful when a newer version of a tool removes/renames existing flags, so you can update the flags via your .overcommit.yml instead of waiting for an upstream fix in Overcommit.
env Hash of environment variables the hook should be run with. This is intended to be used as a last resort when an executable a hook runs is configured only via an environment variable. Any pre-existing environment variables with the same names as ones defined in env will have their original values restored after the hook runs. NOTE: Currently, only strings are accepted values. Boolean values will raise an error. WARNING: If you set the same environment variable for multiple hooks and you've enabled parallel hook runs, since the environment is shared across all threads you could accidentally have these separate hooks trample on each other. In this case, you should disable parallelization for the hook using the parallelize option.
parallelize Whether to allow this hook to be run concurrently with other hooks. Disable this if the hook requires access to a shared resource that other hooks may also access and modify (e.g. files, the git index, process environment variables, etc).
processors The number of processing units to reserve for this hook. This does not reserve CPUs, but indicates that out of the total number of possible concurrent hooks allowed by the global concurrency option, this hook requires the specified number. Thus in the typical case where concurrency is set to the number of available cores (default), and you have a hook that executes an application which itself creates 2 threads (or is otherwise scheduled on 2 cores), you can indicate that Overcommit should allocate 2 processors to the hook. Ideally this means your hooks won't put undue load on your available cores.
install_command Command the user can run to install the required_executable (or alternately the specified required_libraries). This is intended for documentation purposes, as Overcommit does not install software on your behalf since there are too many edge cases where such behavior would result in incorrectly configured installations (e.g. installing a Python package in the global package space instead of in a virtual environment).
skip_file_checkout Whether to skip this hook for file checkouts (e.g. git checkout some-ref -- file). Only applicable to PostCheckout hooks.
skip_if Array of arguments to be executed to determine whether or not the hook should run. For example, setting this to a value of ['bash', '-c', '! which my-executable'] would allow you to skip running this hook if my-executable was not in the bin path.

In addition to the built-in configuration options, each hook can expose its own unique configuration options. The AuthorEmail hook, for example, allows you to customize the regex used to check commit author emails via the pattern option—useful if you want to enforce that developers use a company email address for their commits. This provides incredible flexibility for hook authors as you can make your hooks sufficiently generic and then customize them on a per-project basis.

Hook Categories

Hook configurations are organized into categories based on the type of hook. So pre-commit hooks are located under the PreCommit option, and post-commit hooks are located under PostCommit. See the default configuration for a thorough example.

The ALL Hook

Within a hook category, there is a special type of hook configuration that applies to all hooks in the category. This configuration looks like a normal hook configuration, except it has the name ALL:

PreCommit:
  ALL:
    problem_on_unmodified_line: warn
    requires_files: true
    required: false
    quiet: false

  SomeHook:
    enabled: true

  ...

The ALL configuration is useful for when you want to DRY up your configuration, or when you want to apply changes across an entire category of hooks.

Note that array configuration options (like include/exclude) in the special ALL hook section are not merged with individual hook configurations if custom ones are defined for the hook. Any custom configuration option for include/exclude will replace the ALL hook's configuration. If you want to have a global list of default exclusions and extend them with a custom list, you can use YAML references, e.g.

PreCommit:
  ALL:
    exclude: &default_excludes
      - 'node_modules/**/*'
      - 'vendor/**/*'
  MyHook:
    exclude:
      - *default_excludes
      - 'another/directory/in/addition/to/default/excludes/**/*'

Again, you can consult the default configuration for detailed examples of how the ALL hook can be used.

Gemfile

You may want to enforce the version of Overcommit or other gems that you use in your git hooks. This can be done by specifying the gemfile option in your .overcommit.yml.

The gemfile option tells Overcommit to load the specified file with Bundler, the standard gem dependency manager for Ruby. This is useful if you would like to:

  • Enforce a specific version of Overcommit to use for all hook runs (or to use a version from the master branch that has not been released yet)
  • Enforce a specific version or unreleased branch is used for a gem you want to use in your git hooks

Loading a Bundler context necessarily adds a startup delay to your hook runs as Bundler parses the specified Gemfile and checks that the dependencies are satisfied. Thus for projects with many gems this can introduce a noticeable delay.

The recommended workaround is to create a separate Gemfile in the root of your repository (call it .overcommit_gems.rb), and include only the gems that your Overcommit hooks need in order to run. Generate the associated lock file by running:

bundle install --gemfile=.overcommit_gems.rb

...and commit .overcommit_gems.rb and the resulting .overcommit_gems.rb.lock file to your repository. Set your gemfile option to .overcommit_gems.rb, and you're all set.

Using a smaller Gemfile containing only the gems used by your Overcommit hooks significantly reduces the startup delay in your hook runs. It is thus the recommended approach unless your project has a relatively small number of gems in your Gemfile.

Plugin Directory

You can change the directory that project-specific hooks are loaded from via the plugin_directory option. The default directory is .git-hooks.

Quiet Hook Runs

If you prefer to have your hooks be completely silent unless there is a problem, you can set the top-level quiet option to true. Note that if you have many hooks or slow hooks this may not be desirable, as you don't get visual feedback indicating the general progress of the hook run.

Concurrency

Overcommit runs hooks in parallel by default, with a number of concurrent workers equal to the number of logical cores on your machine. If you know your particular set of hooks would benefit from higher/lower number of workers, you can adjust the global concurrency option. You can define single-operator mathematical expressions, e.g. %{processors} * 2, or %{processors} / 2.

concurrency: '%{processors} / 4'

Note that individual hooks can specify the number of processors they require with the processors hook option. See the hook options section for more details.

Signature Verification

You can disable manual verification of signatures by setting verify_signatures to false. See the Security section for more information on this option and what exactly it controls.

Built-In Hooks

Currently, Overcommit supports the following hooks out of the box—simply enable them in your .overcommit.yml.

Note: Hooks with a * are enabled by default.

Warning: This list represents the list of hooks available on the master branch. Please consult the change log to view which hooks have not been released yet.

CommitMsg

commit-msg hooks are run against every commit message you write before a commit is created. A failed hook prevents a commit from being created. These hooks are useful for enforcing policies on your commit messages, e.g. ensuring a task ID is included for tracking purposes, or ensuring your commit messages follow proper formatting guidelines.

PostCheckout

post-checkout hooks run after a successful git checkout, or in other words any time your HEAD changes or a file is explicitly checked out.

PostCommit

post-commit hooks run after a commit is successfully created. A hook failing in this case does not prevent the commit since it has already occurred; however, it can be used to alert the user to some issue.

PostMerge

post-merge hooks run after a git merge executes successfully with no merge conflicts. A hook failing in this case does not prevent the merge since it has already occurred; however, it can be used to alert the user to some issue.

PostRewrite

post-rewrite hooks run after a commit is modified by a git commit --amend or git rebase. A hook failing in this case does not prevent the rewrite since it has already occurred; however, it can be used to alert the user to some issue.

PreCommit

pre-commit hooks are run after git commit is executed, but before the commit message editor is displayed. If a hook fails, the commit will not be created. These hooks are ideal for syntax checkers, linters, and other checks that you want to run before you allow a commit to even be created.

WARNING: pre-commit hooks cannot have side effects

pre-commit hooks currently do not support hooks with side effects (such as modifying files and adding them to the index with git add). This is a consequence of Overcommit's pre-commit hook stashing behavior to ensure hooks are run against only the changes you are about to commit.

Without Overcommit, the proper way to write a pre-commit hook would be to extract the staged changes into temporary files and lint those files instead of whatever contents are in your working tree (as you don't want unstaged changes to taint your results). Overcommit takes care of this for you, but to do it in a generalized way introduces this limitation. See the thread tracking this issue for more details.

PrePush

pre-push hooks are run during git push, after remote refs have been updated but before any objects have been transferred. If a hook fails, the push is aborted.

PreRebase

pre-rebase hooks are run during git rebase, before any commits are rebased. If a hook fails, the rebase is aborted.

Repo-Specific hooks

Out of the box, overcommit comes with a set of hooks that enforce a variety of styles and lints. However, some hooks only make sense in the context of a specific repository.

For example, you can have a number of simple checks that run against your code to catch common errors. For example, if you use RSpec, you can make sure all spec files contain the line require 'spec_helper'.

Inside our repository, we can add the file .git-hooks/pre_commit/ensure_spec_helper.rb in order to automatically check our spec files:

module Overcommit::Hook::PreCommit
  class EnsureSpecHelper < Base
    def run
      errors = []

      applicable_files.each do |file|
        if File.read(file) !~ /^require 'spec_helper'/
          errors << "#{file}: missing `require 'spec_helper'`"
        end
      end

      return :fail, errors.join("\n") if errors.any?

      :pass
    end
  end
end

The corresponding configuration for this hook would look like:

PreCommit:
  EnsureSpecHelper:
    enabled: true
    description: 'Checking for missing inclusion of spec_helper'
    include: '**/*_spec.rb'

Adding Existing Git Hooks

You might already have hook scripts written which you'd like to integrate with Overcommit right away. To make this easy, Overcommit allows you to include your hook script in your configuration without writing any Ruby code. For example:

PostCheckout:
  CustomScript:
    enabled: true
    required_executable: './bin/custom-script'

So long as a command is given (either by specifying the command option directly or specifying required_executable) a special hook is created that executes the command and appends any arguments and standard input stream that would have been passed to the regular hook. The hook passes or fails based on the exit status of the command.

The script is executed as if Git were calling the hook directly. If you want to understand which arguments are passed to the script depending on the type of hook, see the git-hooks documentation.

Security

While Overcommit can make managing Git hooks easier and more convenient, this convenience can come at a cost of being less secure.

Since installing Overcommit hooks will allow arbitrary plugin code in your repository to be executed, you expose yourself to an attack where checking out code from a third party can result in malicious code being executed on your system.

As an example, consider the situation where you have an open source project. An attacker could submit a pull request which adds a post-checkout hook that executes some malicious code. When you fetch and checkout this pull request, the post-checkout hook will be run on your machine, along with the malicious code that you just checked out.

Overcommit attempts to address this problem by storing a signature of your configuration and all hook plugin code since the last time it ran. When the signature changes, a warning is displayed alerting you to which plugins have changed. It is then up to you to manually verify that the changes are not malicious, and then continue running the hooks.

The signature is derived from the contents of the plugin's source code itself and any configuration for the plugin. Thus a change to the plugin's source code or your local repo's .overcommit.yml file could result in a signature change.

Disabling Signature Checking

In typical usage, your plugins usually don't change too often, so this warning shouldn't become a nuisance. However, users who work within proprietary repositories where all developers who can push changes to the repository already have a minimum security clearance may wish to disable this check.

While not recommended, you can disable signature verification by setting verify_signatures to false in your .overcommit.yml file.

Regardless of whether you have verify_signatures disabled for your project, if you are running Overcommit for the first time you will need to sign your configuration with overcommit --sign. This needs to happen once so Overcommit can record in your local git repo's configuration (outside of source control) that you intend to enable/disable verification. This way if someone else changes verify_signatures you'll be asked to confirm the change.

Contributing

We love contributions to Overcommit, be they bug reports, feature ideas, or pull requests. See our guidelines for contributing to best ensure your thoughts, ideas, or code get merged.

Community

All major discussion surrounding Overcommit happens on the GitHub issues list.

Changelog

If you're interested in seeing the changes and bug fixes between each version of overcommit, read the Overcommit Changelog.

License

This project is released under the MIT license.

The Overcommit logo is adapted from the Git Logo by Jason Long, and is licensed under the Creative Commons Attribution 3.0 Unported License.