Presubmit.Hooks (presubmit v0.2.2)

Copy Markdown View Source

Installs and removes the git hooks that run mix presubmit.

Two hooks work together so that the change set checked is the commit that is about to exist:

  • prepare-commit-msg learns from git whether the commit amends HEAD (source commit with HEAD's SHA) and records HEAD^ as the base in .git/presubmit_base, or removes that file otherwise.
  • commit-msg runs mix presubmit --staged --message-file "$1", adding --base <recorded> when amending, so rules see HEAD^ → index — the amended commit — rather than the delta since HEAD, which could never satisfy a rule whose other half lives in the original commit.

Git reports the message source as message rather than commit when --amend is combined with -m or -F, so that form of amend is not detected and the delta since HEAD is what gets checked; amend with the editor or --no-edit, or run mix presubmit --staged --base HEAD^.

A hook blocks the commit only on presubmit's verdict. It points PRESUBMIT_VERDICT at a file under the git directory, and mix presubmit writes pass or fail there once its rules have run (Presubmit.CLI). When there is no verdict, presubmit did not run: Mix stopped first (stale or unresolvable dependencies, a project that does not load), or presubmit hit a configuration error or crashed. That is a fault in the checkout or the tool, not in the commit, so the hook prints presubmit could not run: <the error>; commit allowed, CI still checks and lets the commit through. Presubmit 0.1.0 writes no verdict; its report (a line starting Examining) shows that it ran, and then its exit status is the verdict.

The hooks also step aside, with a note, while a merge is in progress or when amending a merge commit (the change set would be someone else's branch), and when mix is not on PATH (GUI clients often lack the developer's shell environment). A rule that crashes is reported but does not block the commit (--on-error warn). CI remains the backstop for all of these.

Hooks are installed for the project in the current directory; in a subdirectory project of a larger repository the scripts cd there first. A hook that was not installed by this module is never overwritten, and installation is refused when core.hooksPath points hooks elsewhere.

Summary

Functions

The hooks installed by default: prepare-commit-msg and commit-msg.

The hooks this module knows how to install.

Installs the named hooks for the project at project_dir (the directory containing mix.exs), into its repository's hooks directory.

Which of the known hooks are currently installed by this module.

The script installed for hook, for a project at project_rel relative to the repository root ("." for a root project).

Removes every hook this module installed. Foreign hooks are left alone.

Types

hook()

@type hook() :: String.t()

Functions

default()

@spec default() :: [hook()]

The hooks installed by default: prepare-commit-msg and commit-msg.

hooks()

@spec hooks() :: [hook()]

The hooks this module knows how to install.

install(project_dir, hooks \\ ["prepare-commit-msg", "commit-msg"])

@spec install(Path.t(), [hook()]) :: {:ok, [Path.t()]} | {:error, String.t()}

Installs the named hooks for the project at project_dir (the directory containing mix.exs), into its repository's hooks directory.

Returns the paths written, or {:error, message} when core.hooksPath redirects hooks or a target is a hook that was not installed by this module.

installed(project_dir)

@spec installed(Path.t()) :: [hook()]

Which of the known hooks are currently installed by this module.

script(hook, project_rel \\ ".")

@spec script(hook(), String.t()) :: String.t()

The script installed for hook, for a project at project_rel relative to the repository root ("." for a root project).

uninstall(project_dir)

@spec uninstall(Path.t()) :: {:ok, [Path.t()]}

Removes every hook this module installed. Foreign hooks are left alone.