A platform-independent hooks manager written in Go to support shared hook repositories and per-repository Git hooks, checked into the working repository. This implementation is the Go port and successor of the original implementation (see Migration).
To make this work, the installer creates run-wrappers for Githooks that are
installed into the .git/hooks
folders on request (by default). There's more
to the story though. When one of the Githooks
run-wrappers executes, Githooks starts up and tries to find matching hooks in
the .githooks
directory under the project root, and invoke them one-by-one.
Also it searches for hooks in configured shared hook repositories.
This Git hook manager supports:
- Running repository checked-in hooks.
- Running shared hooks from other Git repositories (with auto-update). See these containerized example hook repositories.
- Git LFS support.
- No it works on my machine by running hooks over containers and automatic build/pull integration of container images (optional).
- Command line interface.
- Fast execution due to compiled executable. (even 2-3x faster from
v2.1.1
) - Fast parallel execution over threadpool.
- Ignoring non-shared and shared hooks with patterns.
- Automatic Githooks updates: Fully configurable for your own company by url/branch and deploy settings.
- Bonus: Platform-independent dialog tool for user prompts inside your own hooks.
Table of Content (click to expand)
- Layout and Options
- Execution
- Supported Hooks
- Git Large File Storage (Git LFS) Support
- Shared Hook Repositories
- Layout of Shared Hook Repositories
- Ignoring Hooks and Files
- Trusting Hooks
- Disabling Githooks
- Environment Variables
- Log & Traces
- Installing or Removing Run-Wrappers
- Running Hooks in Containers
- Running Hooks/Scripts Manually
- User Prompts
- Installation
- Uninstalling
- YAML Specifications
- Migration
- Dialog Tool
- Tests and Debugging
- Changelog
- FAQ
- Acknowledgements
- Authors
- Support & Donation
- License
Take this snippet of a Git repository layout as an example:
/
├── .githooks/
│ ├── commit-msg/ # All commit-msg hooks.
│ │ ├── validate # Normal hook script.
│ │ └── add-text # Normal hook script.
│ │
│ ├── pre-commit/ # All pre-commit hooks.
│ │ ├── .ignore.yaml # Ignores relative to 'pre-commit' folder.
│ │ ├── 01-validate # Normal hook script.
│ │ ├── 02-lint # Normal hook script.
│ │ ├── 03-test.yaml # Hook run configuration.
│ │ ├── docs.md # Ignored in '.ignore.yaml'.
│ │ └── final/ # Batch folder 'final' which runs all in parallel.
│ │ ├── 01-validate # Normal hook script.
│ │ └── 02-upload # Normal hook script.
│ │
│ ├── post-merge # An executable file.
│ │
│ ├── post-checkout/ # All post-checkout hooks.
│ │ ├── .all-parallel # All hooks in this folder run in parallel.
│ │ └── ...
│ ├── ...
│ ├── .images.yaml # Container image spec for use in e.g `03-test.yaml`.
│ ├── .ignore.yaml # Main ignores.
│ ├── .shared.yaml # Shared hook configuration.
│ ├── .envs.yaml # Environment variables passed to shared hooks.
│ └── .lfs-required # LFS is required.
└── ...
All hooks to be executed live under the .githooks
top-level folder, that
should be checked into the repository. Inside, we can have directories with the
name of the hook (like commit-msg
and pre-commit
above), or a file matching
the hook name (like post-merge
in the example). The filenames in the directory
do not matter, but the ones starting with a .
(dotfiles) will be excluded by
default. All others are executed in lexical order according to the Go function
Walk
rules. Subfolders as e.g.
final
get treated as parallel batch and all hooks inside are by default
executed in parallel over the thread pool. See
Parallel Execution for details.
You can use the command line helper (a globally
configured Git alias alias.hooks
), that is git hooks list
, to list all hooks
and their current state that apply to the current repository. For this
repository this looks like the following.
If a file is executable, it is directly invoked, otherwise it is interpreted
with the sh
shell. On Windows that mostly means dispatching to the bash.exe
from https://gitforwindows.org.
All parameters and standard input are forwarded from Git to the hooks. The
standard output and standard error of any hook which Githooks runs is captured
together1 and printed to the standard
error stream which might or might not get read by Git itself (e.g. pre-push
).
Hooks can also be specified by a run configuration in a corresponding YAML file, see Hook Run Configuration.
Hooks related to commit
events (where it makes sense, not post-commit
) will
also have a ${STAGED_FILES}
or ${STAGED_FILES_FILE}
environment variable
set. By default, STAGED_FILES
contains the list of staged and changed files
according to git diff --cached --diff-filter=ACMR --name-only
. File paths are
separated by a newline \n
. If you want to iterate in a shell script over them,
and expect spaces in paths, you might want to set the IFS
like this:
IFS="
"
for file in ${STAGED_FILES}; do
echo "$file"
done
The ACMR
filter in the git diff
will include staged files that are added,
copied, modified or renamed.
To enable the STAGED_FILES_FILE
variable which contains the path to the file
containing the paths to all staged files (separated by null-chars \0
, e.g.
<path>\0<path>\0
) use
git config githooks.exportStagedFilesAsFile true
and read this file in bash
with something like:
#!/bin/bash
while read -rd $'\\0' file; do
echo "$file"
done < "$STAGED_FILES_FILE"
1⏎ Note: This caveat is basically there because standard output and error might get interleaved badly and so far no solution to this small problem has been tackled yet. It is far better to output both streams in the correct order, and therefore send it to the error stream because that will not conflict in anyway with Git (see fsmonitor-watchman, unsupported right now.). If that poses a real problem for you, open an issue.
Each supported hook can also be specified by a configuration file
<hookName>.yaml
where <hookName>
is any
supported hook name. An example might look like the
following:
# The command to run.
# - if it contains path separators and is relative, it its evaluated relative to
# the worktree of the repository where this config resides.
cmd: "dist/command-of-${env:USER}.exe"
# The arguments given to `cmd`.
args:
- "-s"
- "--all"
- "${env:GPG_PUBLIC_KEY}"
- "--test ${git-l:my-local-git-config-var}"
# If you want to make sure your file is not
# treated always as the newest version. Fix the version by:
version: 1
All additional arguments given by Git to <hookName>
will be appended last onto
args
. All environment and Git config variables in args
and cmd
are
substituted with the following syntax:
${env:VAR}
: An environment variableVAR
.${git:VAR}
: A Git config variableVAR
which corresponds togit config 'VAR'
.${git-l:VAR}
: A Git config variableVAR
which corresponds togit config --local 'VAR'
.${git-g:VAR}
: A Git config variableVAR
which corresponds togit config --global 'VAR'
.${git-s:VAR}
: A Git config variableVAR
which corresponds togit config --system 'VAR'
.
Not existing environment variables or Git config variables are replaced with the
empty string by default. If you use ${!...:VAR}
(e.g ${!git-s:VAR }
) it will
trigger an error and fail the hook if the variable VAR
is not found. Escaping
the above syntax works with \${...}
.
Sidenote: You might wonder why this configuration is not gathered in one single YAML file for all hooks. The reason is that each hook invocation by Git is separate. Avoiding reading this total file several times needs time and since we want speed and only an opt-in solution this is avoided.
Githooks defines the environment variables in this table on hooks invocation.
As in the example, all discovered hooks in subfolders
<batchName>
, e.g. <repoPath>/<hooksDir>/<hookName>/<batchName>/*
where
<hooksDir>
is either
.githooks
for repository checked-in hooks orgithooks
,.githooks
or.
for shared repository hooks,
are assigned the same batch name <batchName>
and processed in parallel. Each
batch is a synchronisation point and starts after the one before has finished.
The threadpool uses by default as many threads as cores on the system. The
number of threads can be controlled by the Git configuration variable
githooks.numThreads
set anywhere, e.g. in the local or global Git
configuration.
If you place a file .all-parallel
inside <hooksDir>/<hookName>
, all
discovered hooks inside <hooksDir>/<hookName>
are assigned to the same batch
name all
resulting in executing all hooks in one parallel batch.
You can inspect the computed batch name by running
git hooks list --batch-name
.
The supported hooks are listed below. Refer to the Git documentation for information on what they do and what parameters they receive.
It is receommended to use --maintained-hooks
options during install
(1, 2) to
only select the hooks which are really needed, since executing the Githooks
manager for all hooks might slow down Git operations (especially for
reference-transaction
).
applypatch-msg
pre-applypatch
post-applypatch
pre-commit
pre-merge-commit
prepare-commit-msg
commit-msg
post-commit
pre-rebase
post-checkout
(non-zero exit code is wrapped to 1)post-merge
pre-push
pre-receive
update
post-receive
post-update
reference-transaction
push-to-checkout
pre-auto-gc
post-rewrite
sendemail-validate
post-index-change
The hook fsmonitor-watchman
is currently not supported. If you have a use-case
for it and want to use it with this tool, please open an issue.
If the user has installed Git Large File Storage
(git-lfs
) by calling git lfs install
globally or locally for a repository
only, git-lfs
installs 4 hooks when initializing (git init
) or cloning
(git clone
) a repository:
post-checkout
post-commit
post-merge
pre-push
Since Githooks overwrites the hooks in <repoPath>/.git/hooks
, it will also run
all Git LFS hooks internally if the git-lfs
executable is found on the
system path. You can enforce having git-lfs
installed on the system by placing
a <repoPath>/.githooks/.lfs-required
file inside the repository, then if
git-lfs
is missing, a warning is shown and the hook will exit with code 1
.
For some post-*
hooks this does not mean that the outcome of the git command
can be influenced even tough the exit code is 1
, for example post-commit
hooks can't fail commits. A clone of a repository containing this file might
still work but would issue a warning and exit with code 1
, a push - however -
will fail if git-lfs
is missing.
It is advisable for repositories using Git LFS to also have a pre-commit hook
(e.g. examples/lfs/pre-commit
) checked in which enforces a correct
installation of Git LFS.
The hooks are primarily designed to execute programs or scripts in the
<repoPath>/.githooks
folder of a single repository. However there are
use-cases for common hooks, shared between many repositories with similar
requirements and functionality. For example, you could make sure Python
dependencies are updated on projects that have a requirements.txt
file, or an
mvn verify
is executed on pre-commit
for Maven projects, etc.
For this reason, you can place a .shared.yaml
file (see
specs) inside the <repoPath>/.githooks
folder, which
can hold a list of repositories which contain common and shared hooks.
Alternatively, you can have shared repositories set by multiple
githooks.shared
local or global Git configuration variables, and the hooks in
these repositories will execute for all local projects where Githooks is
installed. See git hooks shared for configuring
all 3 types of shared hooks repositories.
Below are example values for these setting.
$ git config --global --get-all githooks.shared # shared hooks in global config (for all repositories)
https://github.com/shared/hooks-python.git
git@github.com:shared/repo.git@mybranch
$ cd myrepo
$ git config --local --get-all githooks.shared # shared hooks in local config (for specific repository)
ssh://user@github.com/shared/special-hooks.git@v3.3.3
/opt/myspecialhooks
Here are some shared hook repositories to get you started with:
Application of the hooks:
- Markdown2PDF, with CI.
They are all fully containerized so you do not have to worry about requirements
except docker
.
A example config <repoPath>/.githooks/shared.yaml
(see
specs):
version: 1
urls:
- ssh://user@github.com/shared/special-hooks.git@otherbranch
- git@github.com:shared/repo.git@mybranch
The install script offers to set up shared hooks in the global Git config. but you can do it any time by changing the global configuration variable.
Supported URL for shared hooks are:
-
All URLs Git supports such as:
ssh://github.com/shared/hooks-maven.git@mybranch
and also the shortscp
formgit@github.com:shared/hooks-maven.git
git://user@github.com/shared/hooks-python.git
file:///local/path/to/bare-repo.git@mybranch
All URLs can include a tag specification syntax at the end like
...@<tag>
, where<tag>
is a Git tag, branch or commit hash. Thefile://
protocol is treated the same as a local path to a bare repository, see next point. -
Local paths to bare and non-bare repositories such as:
/local/path/to/checkout
(gets used directly)/local/path/to/bare-repo.git@mybranch
(gets cloned internally)
Note that relative paths are relative to the path of the repository executing the hook. These entries are forbidden for shared hooks configured by
.githooks/.shared.yaml
per repository because it makes little sense and is a security risk.
Shared hooks repositories specified by URLs and local paths to bare
repository will be checked out into the <installPrefix>/.githooks/shared
folder (~/.githooks/shared
by default), and are updated automatically after a
post-merge
event (typically a git pull
) on any local repositories. Any other
local path will be used directly and will not be updated or modified.
Additionally, the update of shared hook repositories can also be triggered on
other hook names by setting a comma-separated list of additional hook names in
the Git configuration parameter githooks.sharedHooksUpdateTriggers
on any
configuration level.
You can also manage and update shared hook repositories using the
git hooks shared update
command.
By default, Githooks will fail if any configured shared hooks are not
available and you need to update them by running git hooks update
.
By using
git hooks config skip-non-existing-shared-hooks --help
you can disable this behavior locally/globally or by environment variable
GITHOOKS_SKIP_NON_EXISTING_SHARED_HOOKS
(see
env. variables) which makes Githooks skip non-existing
shared hooks.
The layout of these shared repositories is the same as above, with the exception
that the hook folders (or files) can be at the project root as well, to avoid
the redundant .githooks
folder.
If you want the shared hook repository to use Githooks itself (e.g. for
development purposes by using hooks from <sharedRepo>/.githooks
) you can
furthermore place the shared hooks inside a <sharedRepo/githooks
subfolder.
In that case the <sharedRepo>/.githooks
folder is ignored when other users use
this shared repository.
The priority to find hooks in a shared hook repository is as follows: consider hooks
- in
<hooksDir> := <sharedRepo>/githooks
, if it does not exist, consider hooks - in
<hooksDir> := <sharedRepo>/.githooks
, if it does not exist consider hooks - in
<hooksDir> := <sharedRepo>
as the last fallback.
Each of these directories can be of the same format as the normal .githooks
folder in a single repository.
You can get the root directory of a configured shared repository with namespace
<namespace>
by running git hooks shared root ns:<namespace>
. This might be
helpful in scripts if you have common shared functionality inside this shared
repository you want to use.
A shared repository can optionally have a namespace associated with it. The name
can be stored in a file .namespace
in any possible hooks directory
<hooksDir>
of the shared repository, see
layout. The namespace comes into play
when ignoring/disabling certain hooks. See
ignoring hooks. The namespace name must not contain
white spaces (\s
) or slashes /
.
The following namespaces names are reserved internally:
gh-self
: for hooks in the repository where Githooks runs (if no.namespace
is existing).gh-self-repl
: for original Git hooks which were replaced by Githooks during install.
The .ignore.yaml
(see specs) files allow excluding
files
- from being treated as hook scripts or
- hooks from being run.
You can ignore executing all sorts of hooks per Git repository by specifying
patterns or explicit paths which match against a hook's (file's)
namespace path. Note: Dot-files, e.g. .myfile
are always ignored.
Each hook either in the current repository <repoPath>/.githooks/...
or inside
a shared hooks repository has a so called namespace path.
A namespace path consists of the name of the hook prefixed by a namespace , e.g. :
<namespacePath> := ns:<namespace>/<relPath> = "ns:core-hooks/pre-commit/check-numbers.py"
where <relPath> = pre-commit/check-numbers.py
is the relative path to the
hook. Each shared repository can provide its own
namespace.
A namespace will be used when the hook belongs
to a shared hook repository and will have a default unique value if it is not
defined. You can inspect all namespace paths by inspecting ns-path:
in the
output of git hooks list in the current
repository. All ignore entries in .ignore.yaml
(patterns or paths) will match
against these namespace paths.
Disabling works like:
# Disable certain hooks by a pattern in this repository:
# User ignore pattern stored in `.git/.githooks.ignore.yaml`:
$ git hooks ignore add --pattern "pre-commit/**" # Store: `.git/.githooks.ignore.yaml`:
# or stored inside the repository:
$ git hooks ignore add --repository --pattern "pre-commit/**" # Store: `.githooks/.ignore.yaml`:
# Disable certain shared hooks (with namespace 'my-shared-super-hooks')
# by a glob pattern in this repository:
$ git hooks ignore add --repository --pattern "my-shared-super-hooks://pre-commit/**"
In the above example, one of the .ignore.yaml
files
should contain a glob pattern **/*.md
to exclude the pre-commit/docs.md
Markdown file. Patterns can contain double star syntax to match multiple
directories, e.g. **/*.txt
instead of *.txt
.
The main ignore file <repoPath>/<hookDir>/.ignore.yaml
applies to all hooks.
Any additional <repoPath>/<hookDir>/<hookName>/.ignore.yaml
file inside
<hookDir>
will be accumulated to the main file and patterns not starting with
ns:
are made relative to the folder <hookName>
. You can also manage
.ignore.yaml
files using
git hooks ignore [add|remove] --help
. Consult
this command documentation for further information on the pattern syntax.
To try and make things a little bit more secure, Githooks checks if any new
hooks were added we haven't run before, or if any of the existing ones have
changed. When they have, it will prompt for confirmation (trust prompt) whether
you accept those changes or not, and you can also disable specific hooks to skip
running them until you decide otherwise. The trust prompt is always fatal
meaning that failing to answer the prompt, or any other prompt error, will
result in a failing Git hook. To make the runner
non-interactive, see
user prompts. If a hook is still active and untrusted after
the prompt, Githooks will fail by default. This is useful to be sure that
all hooks get executed. However, you can disabled this behavior by skipping
active, untrusted hooks with
git hooks config skip-untrusted-hooks --enable
or by setting GITHOOKS_SKIP_UNTRUSTED_HOOKS
(see
env. variables).
The accepted checksums are maintained in the
<repoPath>/.git/.githooks.checksum
directory, per local repository. You can
however use a global checksum directory setup by making an absolute symbolic
link with name .githooks.checksum
inside the template directory
(init.templateDir
) which gets installed in each clone.
If the repository contains a <repoPath>/.githooks/trust-all
file, it is marked
as a trusted repository. Consult
git hooks trust --help
. On the first
interaction with hooks, Githooks will ask for confirmation that the user trusts
all existing and future hooks in the repository, and if she does, no more
confirmation prompts will be shown. This can be reverted by running
git hooks config trust-all --reset
command. This is a per-repository setting. Consult
git hooks config trust-all --help
for more information.
You can also trust individual hooks by using
git hooks trust hooks --help
.
To disable running any Githooks locally or globally, use the following:
# Disable Githooks completely for this repository:
$ git hooks disable # Use --reset to undo.
# or
$ git hooks config disable --set # Same thing... Config: `githooks.disable`
# Disable Githooks globally (for all repositories):
$ git hooks disable --global # Use --reset to undo.
# or
$ git hooks config disable --set --global # Same thing... Config: `githooks.disable`
Also, as mentioned above, all hook executions can be bypassed with a non-empty
value in the GITHOOKS_DISABLE
environment variable.
All of these environment variables are either defined during Githooks runner executing or affect its behavior. These should mostly only be used locally and not globally be defined.
Environment Variables | Effect |
---|---|
GITHOOKS_OS (defined by Githooks) |
The operating system. See Exported Environment Variables. |
GITHOOKS_ARCH (defined by Githooks) |
The system architecture. See Exported Environment Variables. |
STAGED_FILES (defined by Githooks) |
All staged files. Only set in pre-commit , prepare-commit-msg and commit-msg hook. |
GITHOOKS_CONTAINER_RUN (defined by Githooks) |
If a hook is run over a container, this variable is set and true |
GITHOOKS_DISABLE |
If defined, disables running hooks run by Githooks, except git lfs and the replaced old hooks. |
GITHOOKS_RUNNER_TRACE |
If defined, enables tracing during Githooks runner execution. A value of 1 enables more output. |
GITHOOKS_LOG_LEVEL |
A value debug , info , warn , error or disable sets the log level during Githooks runner execution. |
GITHOOKS_SKIP_NON_EXISTING_SHARED_HOOKS=true |
Skips on true and fails on false (or empty) for non-existing shared hooks. See Trusting Hooks. |
GITHOOKS_SKIP_UNTRUSTED_HOOKS=true |
Skips on true and fails on false (or empty) for untrusted hooks. See Trusting Hooks. |
You can pass arguments to shared hooks currently by specifying a
.githooks/.envs.yaml
file which will export
environment variables when running the shared hooks selected by its
namespace:
envs:
mystuff:
# All these variables are exported
# for shared hook namespace `mystuff`.
- "MYSTUFF_CHECK_DEAD_CODE=1"
- "MYSTUFF_STAGE_ON_FORMAT=1"
sharedA:
# All these variables are exported
# for shared hook namespace `sharedA`.
- "SHAREDA_ABC=1"
- "SHAREDA_TWEET=1"
You can see how the Githooks runner
is been called by setting the environment
variable GITHOOKS_RUNNER_TRACE
to a non empty value.
GITHOOKS_RUNNER_TRACE=1 git <command> ...
You can install and uninstall run-wrappers inside a repository with
git hooks install
. or
git hooks uninstall
. This installs and
uninstalls wrappers from ${GIT_DIR}/hooks
as well as sets and unsets local
Githooks-internal Git configuration variables.
To install run-wrappers for only selective hooks, use --maintained-hooks
, e.g.
cd repository
git hook install \
--maintained-hooks "!all, pre-commit, pre-merge-commit, prepare-commit-msg, commit-msg, post-commit" \
--maintained-hooks "pre-rebase, post-checkout, post-merge, pre-push"
Note: Git LFS hooks is properly taken care of when --maintained-hooks
is
used. That is, when you don't select a Git LFS hooks in --maintained-hooks
,
the missing Git LFS hooks will be installed too.
You can run hooks containerized over a container manager such as docker
. This
relieves the maintainer of a Githooks shared repo from dealing with "It works
on my machine!"
To enable containerized hook runs set the Git config variable either locally or globally with
git hooks config enable-containerized-hooks [--global] --set true
or use the environment variable GITHOOKS_CONTAINERIZED_HOOKS_ENABLED=true
.
Optionally set the container manager (default is docker
) like
git hooks config container-manager-types [--global] --set "podman,docker"
The container manager types can be a list from [docker
, podman
] where the
first valid one is used to run the hooks.
Running a hook in a container is achieved by specifying the image reference
(image name) inside a hook run configuration, e.g.
<hooksDir>/pre-commit/myhook.yaml
. This works for normal repositories as well
as for shared Githooks repositories.
For a shared repository, the file sharedRepo/githooks/pre-commit/checkit.yaml
might look like
version: 3
cmd: ./myscripts/checkit.sh
args:
image:
reference: "my-shellcheck:1.2.0"
which will launch the command ./myscript/checkit.sh
in a container
my-shellcheck:1.2.0
. The current Git repository where this hook is launched is
mounted as the current working directory and the relative path
./myscript/checkit.sh
will be mangled to a path in the mounted read-only
volume of this shared Githooks repo sharedRepo
which is cached inside
<installDir>/shared
.
Note: When running a hook script or command over a container, you will not have access to the same environment variables as on your host system. All Githooks environment variables are forwarded however to the container run.
Note: The images you run must be rootless
(contain a USER
statement) and
this user must have user/group id 1000
(Todo: We can loosen this requirement
if really needed). See the
example.
This manager is strongly preferred due to better security and less hassle with volume mounts.
The containers are run with the following flags to podman
:
--userns=keep-id:uid=1000,gid=1000
: User namespace mapping. Maps the user/group id of the user running Githooks (your host user) to the container user/group id1000
. This means a host user with user/group id e.g. 4000 will be seen inside the container as user/group id 1000. This also works for all volume mounts which will have1000:1000
permission inside the container.
The containers are run with the following flags to docker
:
--user:<uid>:<gid>
: The container is run as the same user id and group id as the user which runs Githooks (your host user). See the note below why this is the case.
Note: Running commands in containers which modify files on writable volumes
has some caveats and quirks with permissions which are host system dependent.
Hongli Lai summarized these troubles in a
very good article.
Long story short if the images are run with the docker
manager, you should
use
MatchHostFsOwner
which counter acts these permission problems neatly by installing
this into your hook's sidecar container.
To have this containerized functionality neatly integrated, Githooks provides a
way for specifying image pull and build options in an opt-in file
<hooksDir>/.images.yaml
(see <hooksDir>
definition), e.g.
version: 1
images:
koalaman/shellcheck:latest:
# will pull the image reference according to this dictionary key.
my-shellcheck:1.2.0:
pull: # optional
reference: myimages/${namespace}-shellcheck:v0.9.0
${namespace}-my-shellcheck:1.3.0:
build:
dockerfile: ./.githooks/docker/Dockerfile
stage: myfinalstage
context: ./.githooks/docker
This file will be acted upon when shared hooks are updated, e.g.
git hooks shared update
or when this happens automatically.
You can trigger the image pull/build procedure by running
git hooks images update [--config ...]
inside a normal repo a
which configures such a file in
a/.githooks/.images.yaml
or in a normal repository b
which configures to use
a sharedRepo
in .shared.yaml
which configures it in
sharedRepo/githooks/.images.yaml
. If this shared repo sharedRepo
has a
namespace banana
configured,
git hooks images update
in b
will trigger
- a pull of image
koalaman/shellcheck:latest
, - a pull of image
myimages/banana-shellcheck:v0.9.0
and tagging it withmy-shellcheck:1.2.0
, - and a build of an image
banana-my-shellcheck:1.3.0
of stagemyfinalstage
in the respective Dockerfile./.githooks/docker/Dockerfile
where the build context is set to.githooks/docker
.
Note: All paths in the build specification build:
are relative to the
repository root where this .images.yaml
is located.
All built images are automatically labeled with githooks-version
to make them
easy to retrieve, e.g.
docker images --filter label=githooks-version
or to easily delete all of them by
docker rmi $(docker images -f "label=githooks-version" -q)
Pruning Of Older Images: If a shared repository is updated from
git hooks shared update
it might come with new images references in
.images.yaml
. Githooks does not yet detect which references are no longer
needed after the pull/build procedure nor does it offer a way yet to prune older
images (just use the above).
The command git hooks exec
helps to launch executables and
run configuration the same as Githooks does when run
normally. This features simplifies executing add-on scripts/executables
distributed in shared hook repositories (and also locally with ns:gh-self
).
For example execute the following add-on 'format-all' script in this shared repository with:
git hooks exec --containerized \
ns:githooks-docs/scripts/format-docs-all.yaml -- \
--force \
--dir ./
This will launch the specified container and run the script.
Note: You need to have configured this shared repository inside your repo
where you use Githooks and it needs to be available with
git hooks shared update
.
Githooks shows user prompts during installation, updating (automatic or manual),
uninstallation and when executing hooks (the runner
executable).
The runner
might get executed over a Git GUI or any other environment where no
terminal is available. In this case all user prompts are shown as GUI dialogs
with the included platform-independent dialog tool. The GUI
dialog fallback is currently only enabled for the runner
.
Githooks distinguishes between fatal and non-fatal prompts.
-
A fatal prompt will result in a complete abort if
- The prompt could not be shown (terminal or GUI dialog).
- The answer returned by the user is incorrect (terminal only) or the user canceled the GUI dialog.
-
A non-fatal prompt always has a default answer which is taken in the above failing cases and the execution continues. Warning messages might be shown however.
The runner
will show prompts, either in the terminal or as GUI dialog, in the
following cases:
- Trust prompt: The user is required to trust/untrust a new/changed hook: fatal.
- Update prompts: The user is requested to accept a new update if automatic
updates are enabled (
git hooks update --enable-check
): non-fatal.- Various other prompts when the updater is launched: non-fatal.
User prompts during runner
execution are sometimes not desirable (server
infrastructure, docker container, etc...) and need to be disabled. Setting
git hooks config non-interactive-runner --enable --global
will:
- Take default answers for all non-fatal prompts. No warnings are shown.
- Take default answer for a fatal prompt if it is configured: The only fatal
prompt is the trust prompt which can be configured to pass by executing
git hooks config trust-all --accept
.
Launch the below shell command. It will download the release from Github and launch the installer.
Note: All downloaded files are checksum & signature checked.
curl -sL https://raw.githubusercontent.com/gabyx/githooks/main/scripts/install.sh | bash
See the next sections on different install options.
Note: Use bash -s -- -h
above to show the help message of the bootstrap
script and bash -s -- -- <options>
to pass arguments to the installer
(cli installer
), e.g. bash -s -- -- -h
to show the help.
Githooks is inside nixpkgs
, so you can
access it by pkgs.githooks
in your flake.nix
.
To install the Githooks derivation at a different version <version>
, add the
following to your inputs
in your flake.nix
:
inputs = {
githooks = {
url = "github:gabyx/githooks?dir=nix&ref=v<version>";
inputs.nixpkgs.follows = "nixpkgs";
};
}
You should never install a major version upgrade as Githooks should be uninstalled completely before. The uninstaller on any version however should work backward-compatible.
and then use it in your packages, e.g. here with home-manager by doing:
{ lib, pkgs, inputs, ...}:
let
githooks = inputs.githooks.packages."${pkgs.system}".default;
in {
home.packages = [githooks]
}
The installer will:
-
Download the current binaries if
--update
is not given. Optionally it can use a deploy settings file to specify where to get the binaries from. (default is this repository here.) -
Verify the checksums and signature of the downloaded binaries.
-
Launch the current installer which proceeds with the next steps. If
--update
is given the newest Githooks is downloaded and installed directly. -
Find the install mode relevant hooks directory
<hooksDir>
:-
Use the directory given with
--hooks-dir <dir>
on the command line. -
Use
git config --get githooks.pathForUseCoreHooksPath
if Githooks is already installed. -
Use the following template directory if
--hooks-dir-use-template-dir
is given:- Use
GIT_TEPMLATE_DIR
if set and add/hooks
- Use Git config value
init.templateDir
if set and add/hooks
- Use
<install-dir>/templates/hooks
.
Note: This will silently make all new repositories with
git init
orgit clone
directly use Githooks, this is similar to thecentralized
install mode. - Use
-
-
Write all Githooks run-wrappers into the hooks directory
<hooksDir>
and- Set
core.hooksPath
forcentralized
install mode (--centralized
).
- Set
-
Offer to enable automatic update checks.
-
Offer to find existing Git repositories on the file system (disable with
--skip-install-into-existing
)-
Make them use Githooks by either setting
core.hooksPath
(or install run-wrappers if<repo-git-dir>/hooks/githooks-contains-run-wrappers
exists). -
Offer to add an intro README in their
.githooks
folder.
-
-
Install/update run-wrappers into all registered repositories: Repositories using Githooks get registered in the install folders
registered.yaml
file on their first hook invocation. -
Offer to set up shared hook repositories.
This installer can install Githooks in one of 2 ways:
-
Manual: To use Githooks in a repo, it must be installed (default behavior) with
git hooks install
. -
Centralized: Using the Git
core.hooksPath
global variable (set by passing the--centralized
parameter to the install script). All repositories will use Githooks by default.
Read the details about the differences between these 2 approaches below.
This is the default installation mode.
In this mode, you decide yourself when to use Githooks in a repository simply by doing one of the following with the same effect:
- Run
git hooks install
orgit hooks uninstall
to install run wrappers explicitly.
This means that Githooks might not run if you forget to install the hooks.
To install Githooks on your system, simply execute cli installer
. It will
guide you through the installation process. Check the cli installer --help
for
available options. Some of them are described below:
Its advised to only install Githooks for a selection of the supported hooks by
using --maintained-hooks
as
curl -sL https://raw.githubusercontent.com/gabyx/githooks/main/scripts/install.sh | bash -s -- -- \
--maintained-hooks "!all, pre-commit, pre-merge-commit, prepare-commit-msg, commit-msg, post-commit" \
--maintained-hooks "pre-rebase, post-checkout, post-merge, pre-push"
You can still overwrite selectively for a repository by installing another set of hooks. Missing Git LFS hooks will always be placed if necessary.
If you want, you can try out what the script would do first, without changing anything by using:
curl -sL https://raw.githubusercontent.com/gabyx/githooks/main/scripts/install.sh | bash -s -- -- \
--dry-run
Optionally, you can also pass the hooks directory to which you want to install
the Githooks run-wrappers by appending --hook-dir <path>
to the command above,
for example:
curl -sL https://raw.githubusercontent.com/gabyx/githooks/main/scripts/install.sh | bash -s -- -- \
--hooks-dir /home/public/myhooks
You have the option to install Githooks centralized which will use the
run-wrappers globally by setting the global core.hooksPath
additionally. For
this, run the command below.
curl -sL https://raw.githubusercontent.com/gabyx/githooks/main/scripts/install.sh | bash -s -- -- \
--centralized
If you want to install from another Git repository (e.g. from your own or your
companies fork), you can specify the repository clone url as well as the branch
name (default: main
) when installing with:
curl -sL https://raw.githubusercontent.com/gabyx/githooks/main/scripts/install.sh | bash -s -- -- \
--clone-url "https://server.com/my-githooks-fork.git" \
--clone-branch "release"
The installer always maintains a Githooks clone inside <installDir>/release
for its automatic update logic. The specified custom clone URL and branch will
then be used for further updates in the above example (see
update mechanics).
Because the installer always downloads the latest release (here from another
URL/branch), it needs deploy settings to know where to get the binaries from.
Either your fork has setup these settings in their Githooks release (you
hopefully downloaded) already or you can specify them by using
--deploy-api <type>
or the full settings file --deploy-settings <file>
. The
<type>
can either be gitea
( or github
which is not needed since it can be
auto-detected from the URL) and it will automatically download and verify
the binaries over the implemented API. Credentials will be collected over
git credential
to access the
API. [@todo].
The installation depends on how you use Githooks in CI. The general approach is to run functionality or hooks over Githooks in CI only containerized. Doing it without containerization will be brittle and non-robust and requires you to have all needed tools installed in the running environment, also potentially the tools used in shared hook repositories.
There are generally two scenarios how you would use Githooks in CI.:
-
Run functionality in hook repositories (local and shared repos): This can be done by using
git hooks exec --containerized ...
. The followinggit hooks exec --containerized \ ns:githooks-shell/scripts/check-shell-all.yaml -- --force --dir "."
would run the config
scripts/check-shell-all.yaml
(see hook run configuration) from the hook repositorygithooks-shell
containerized. -
Run hooks containerized directly, e.g. pre-commit over a crafted
git commit
, to check all staged files in that commit with all hooks specified in the respective repository. This repository does exactly that intests/test-lint.sh
. -
Run 1. or 2. but with
nix-shell
support if hooks are setup like this. No containers needed, no nested container troubles. Not implemented yet.
Warnings: Running a containerized hook or script in CI might mean that a
container starts as a nested container since your CI already uses a top-level
container which itself has access to docker
or podman
. Nested containers are
kind of
tricky and mind-boggling.
Githooks will launch a container with two mounts
- the workspace (the repository on which Githooks runs, e.g.
/data/repo
) bind mounted to/mnt/workspace
and - the shared hook repository (
~/.githooks/shared
) bind mounted to/mnt/shared
,
inside the hook container.
These mounts can be influenced with the env. variable
GITHOOKS_CONTAINER_RUN_CONFIG_FILE
, see below.
Some nomenclature for the next explanations: the host is considered a VM (your
CI instance) and the top-level container T
is your CI container you started on
this VM for a dedicated CI job (C
has access to a container manager). The
nested container N
is a launched container from Githooks.
Adjusting the mounts is needed because for the nested container N
(inside
C
), the mounts might not work because the source paths (e.g. /data/repo
in -v /data/repo:/mnt/workspace
) are interpreted where the container manager
service runs. E.g. for docker
, when you mount the docker
socket into the
top-level container to have connect to the docker instance (running on the host,
generally not the best security practice!), then that would be the host and
the paths would not exist (e.g. there is no /data/repo
on the host). If you
have a full container manager (podman
preferred) inside container T
, it is
different. In that case, the mount paths are interpreted inside container T
.
The mounts might still not work because the paths cannot be mounted further into
a nested container N
due to restrictions what you can mount to nested
containers - if the path comes from a bind mount from the host (VM) into C
it
does not work (AFAIK). In that case you can workaround this by
GITHOOKS_CONTAINER_RUN_CONFIG_FILE
which is the path to a YAML file which
modifies the mounts:
version: 1
# Tell Githooks where the workspace will be in the nested container.
# (optional, default `/mnt/workspace`)
workspace-path-dest: /tmp/ci-job-1/build/repo
# Tell Githooks where the shared repository checkouts are in the nested container.
# (optional, default: `/mnt/shared`)
shared-path-dest: /tmp/ci-job-1/githooks-install/.githooks/shared
# Do not auto-mount the workspace (bind mount), do it yourself with args.
# (optional, default: true)
auto-mount-workspace: false
# Do not auto-mount the shared (bind mount), do it yourself with args.
# (optional, default: true)
auto-mount-shared: false
# Additional arguments to `docker run` or `podman run`.
args: ["-v", "gh-test-tmp:/tmp"]
The above will mount a volume gh-test-tmp
volume to /tmp
in N
where
Githooks will find the workspace under /tmp/ci-job-1/build/repo
and the shared
repository checkouts in /tmp/ci-job-1/githooks-install/.githooks/shared
.
Note: You can use whatever docker run
or podman run
accepts. To mention
is --volumes-from
where you can mount the same volumes from another containers
id.
The repository Markdown2PDF
contains a CI setup in
.gitlab/pipeline.yaml
for Gitlab
For Gitlab this boils down to following pipeline step which uses dockerized hooks:
format:
stage: <your-stage-name>
image: docker:24
rules:
- *defaults-rules
services:
- docker:24-dind
variables:
GITHOOKS_INSTALL_PREFIX: "$CI_BUILDS_DIR/githooks"
script:
- apk add git jq curl bash just findutils parallel
- just format
where just format
will call the following function:
function ci_setup_githooks() {
mkdir -p "$GITHOOKS_INSTALL_PREFIX"
printInfo "Install Githooks."
curl -sL "https://raw.githubusercontent.com/gabyx/githooks/main/scripts/install.sh" |
bash -s -- -- --non-interactive --prefix "$GITHOOKS_INSTALL_PREFIX"
git hooks config enable-containerized-hooks --global --set
printInfo "Pull all shared Githooks repositories."
git hooks shared update
}
which then enables you to call side-car scripts in the hook repository, e.g. as demonstrated which will run over containers the same as in non-CI use cases:
function run_format_shared_hooks() {
printInfo "Run all formats scripts in shared hook repositories."
git hooks exec --containerized \
ns:githooks-shell/scripts/format-shell-all.yaml -- --force --dir "."
git hooks exec --containerized \
ns:githooks-configs/scripts/format-configs-all.yaml -- --force --dir "."
git hooks exec --containerized \
ns:githooks-docs/scripts/format-docs-all.yaml -- --force --dir "."
git hooks exec --containerized \
ns:githooks-python/scripts/format-python-all.yaml -- --force --dir "."
}
You can use this hook manager also without a global installation. For that you
can clone this repository anywhere (e.g. <repoPath>
) and build the executables
with Go by running githooks/scripts/build.sh --prod
. You can then use the
hooks by setting core.hooksPath
(in any suitable Git config) to the checked in
run-wrappers in <repoPath>/hooks
like so:
git clone https://github.com/gabyx/githooks.git githooks
cd githooks
githooksRepo=$(pwd)
scripts/build.sh
Then, to globally enable them for every repo:
git config --global core.hooksPath "$gihooksRepo/hooks"
or locally enable them for a single repo only:
cd repo
git config --local core.hooksPath "$githooksRepo/hooks"
You can also run the installation in non-interactive mode with the command
below. This will determine an appropriate template directory (detect and use the
existing one, or use the one passed by --template-dir
, or use a default one),
install the hooks automatically into this directory, and enable periodic update
checks.
The global install prefix defaults to ${HOME}
but can be changed by using the
options --prefix <installPrefix>
:
curl -sL https://raw.githubusercontent.com/gabyx/githooks/main/scripts/install.sh | bash -s -- -- \
--non-interactive [--prefix <installPrefix>]
It's possible to specify which template directory should be used, by passing the
--template-dir <dir>
parameter, where <dir>
is the directory where you wish
the templates to be installed.
curl -sL https://raw.githubusercontent.com/gabyx/githooks/main/scripts/install.sh | bash -s -- -- \
--template-dir "/home/public/.githooks-templates"
By default the script will install the hooks into the ~/.githooks/templates/
directory.
On a server infrastructure where only bare repositories are maintained, it is best to maintain only server hooks. This can be achieved by installing with:
curl -sL https://raw.githubusercontent.com/gabyx/githooks/main/scripts/install.sh | bash -s -- -- \
--maintained-hooks "server"
The global template directory then only contains the following run-wrappers for Githooks:
pre-push
pre-receive
update
post-receive
post-update
reference-transaction
push-to-checkout
pre-auto-gc
which get deployed with git init
or git clone
automatically. See also the
setup for bare repositories.
If you want to use Githooks with bare repositories on a server, you should setup the following to ensure smooth operations (see user prompts):
cd bareRepo
# Install Githooks into this bare repository
git hooks install
# Automatically accept changes to all existing and new
# hooks in the current repository.
# This makes the fatal trust prompt pass.
git hooks config trust-all-hooks --accept
# Don't do global automatic updates, since the Githooks updater
# might get invoked in parallel on a server.
git hooks config update --disable-check
Note: A user cannot change bare repository Githooks by pushing changes to a bare
repository on the server. If you use shared hook repositories in you bare
repository, you might consider disabling shared hooks updates by
git hooks config disable-shared-hooks-update --set
.
In this approach, the install script installs the hook run-wrapper into a common
location (~/.githooks/templates/
by default).
To make Githooks available inside a repository, you must install it with
cd repo
git hooks install
which will simply set the core.hooksPath
to the common location where Githooks
maintains its run-wrappers. If you want a partial maintained hooks set with
git hooks install --maintained-hooks ...
, it will switch to install Githooks
run-wrappers inside this sole repository.
You have the possibility to install the Githooks run-wrappers into a Git
template directory (e.g. GIT_TEMPLATE_DIR
or init.templateDir
or the default
Git template directory from the Git install) by specifying
--hooks-dir-use-template-dir
. This option is discouraged and only available
for backward compatibility and is not really needed and can be covered with the
below centralized
install mode.
In this approach, the install script installs the hook run-wrapper into a common
location (~/.githooks/templates/
by default) and sets the global
core.hooksPath
variable to that location. Git will then, for all relevant
actions, check the core.hooksPath
location, instead of the default
${GIT_DIR}/hooks
location.
This approach works more like a blanket solution, where all repositories2 will start using the Githooks run-wrappers (thus launching Githooks), regardless of their location.
2⏎ Note: It is possible to override
the behavior for a specific repository, by setting a local core.hooksPath
variable with value ${GIT_DIR}/hooks
, which will revert Git back to its
default behavior for that specific repository. You don't need to initialize
git lfs install
, because they presumably be already in ${GIT_DIR}/hooks
from
any git clone/init
. This does NOT work when using this inside a Git
worktree.
You can update the Githooks any time by running
git hooks update
or one of the install commands above with --update
.
It then downloads the binaries (GPG signed + checksummed) and dispatches to the new installer to install the new version. It will update itself and simply overwrite the template run-wrappers with the new ones, and if you opt-in to install into existing or registered local repositories, those will get overwritten too.
You can also enable automatic update checks during the installation, that is
executed once a day after a successful commit. It checks for a new version
where you can then call git hooks update
your-self (*previous to version 3
this was automatic which was removed).
Automatic updates can be enabled or disabled at any time by running the command below.
# enable with:
$ git hooks update --enable-check # `Config: githooks.updateCheckEnabled`
# disable with:
$ git hooks update --disable-check
The update mechanism works by tracking the tags on the Git branch (chosen at
install time) which is checked out in <installDir>/release
. Normally, if there
are new tags (versions) available, the newest tag (version) is installed.
However, prerelease version tags (e.g. v1.0.0-rc1
) are
generally skipped. You can disable this behavior by setting the global Git
config value githooks.updateCheckUsePrerelease = true
. Major version updates
are never automatically installed an need the consent of the user.
If the annotated version tag or the commit message it points to
(git tag -l --format=%(contents) <tag>
) contains a trailing header which
matches the regex ^Update-NoSkip: *true
, than this version will not be
skipped. This feature enables to enforce an update to a specific version. In
some cases this is useful (serialization changes etc.).
The single-line commit trailers ^Update-Info: *(.*)
(multiple ones allowed) on
the annotated tag is used to assemble a small changelog during update, which is
presented to the user. The single line can contain important information/links
to relevant fixes and changes.
You can also check for updates at any time by executing
git hooks update
or using
git hooks config update-check [--enable|--disable]
command to enable or disable the automatic update checks.
If you want to get rid of this hook manager, you can execute the uninstaller
<installDir>/bin/uninstaller
by
git hooks uninstaller
or
curl -sL https://raw.githubusercontent.com/gabyx/githooks/main/scripts/install.sh | bash -s -- --uninstall
This will delete the run-wrappers installed in the template directory, optionally the installed hooks from the existing local repositories, and reinstates any previous hooks that were moved during the installation.
You can find YAML examples for hook ignore files .ignore.yaml
and shared hooks
config files .shared.yaml
here.
Migrating from the sh
implementation here
is easy, but unfortunately we do not yet provide an migration option during
install (PRs welcome) to take over Git configuration values and other not so
important settings.
However, you can take the following steps for your old .shared
and .ignore
files inside your repositories to make them work directly with a new install:
-
Convert all entries in
.ignore
files to a pattern in a YAML file.ignore.yaml
(see specs). Each old glob pattern needs to be prepended by**/
(if not already existing) to make it work correctly (because of namespaces), e.g. a pattern.*md
becomes**/.*md
. Disable shared repositories in the old version need to be reconfigured, by using ignore patterns. Check if the ignore is working by running[git hooks list](docs/cli/git_hooks_list.md)
. -
Convert all entries in
.shared
files to an url in a YAML file.shared.yaml
here. -
It's heartly recommended to first uninstall the old version, to get rid of any old settings.
-
Install the new version.
Trusted hooks will be needed to be trusted again. To port Git configuration
variables use the file githooks/hooks/gitconfig.go
which contains all used Git
config keys.
Githooks provides it's own platform-independent dialog tool dialog
which
is located in <installDir>/bin
. It enables the use of native GUI dialogs
such as:
- options dialog
- entry dialog
- file save and file selection dialogs
- message dialogs
- system notifications
inside of hooks and scripts. See the screenshots.
Why another tool?: At the moment of writing there exists no proper
platform-independent GUI dialog tool which is bomb-proof in it's output and
exit code behavior. This tool should really enable proper and safe usage
inside hooks and other scripts. You can even report the output in json
format
(use option --json
). It was heavily inspired by
zenity and features some of the same
properties (no cgo
, cancellation through context
). You can use this dialog
tool independently of Githooks.
Test it out! 🎉: Please refer to the documentation of the tool.
cd githooks
go mod download
go mod vendor
cd githooks/apps/dialog
go build ./...
./dialog --help
The dialog tool has the following dependencies:
macOS
:osascript
which is provided by the system directly.Unix
: A dialog tool such aszenity
(preferred),qarma
ormatedialog
.Windows
: Common Controls 6 which is provided by the system directly.
Running the integration tests with Docker:
cd githooks
bash tests/test-alpine.sh # and other 'test-XXXX.sh' files...
Run certain tests only:
bash tests/test-alpine.sh --seq {001..120}
bash tests/test-alpine.sh --seq 065
There is a docker development container for debugging purposes in
.devcontainer
. VS Code can be launched in this remote docker container with
the extension ms-vscode-remote.remote-containers
. Use
Remote-Containers: Open Workspace in Container...
and
Remote-Containers: Rebuild Container
.
Once in the development container: You can launch the VS Code tasks:
[Dev Container] go-delve-installer
- etc...
which will start the delve
debugger headless as a server in a terminal. You
can then attach to the debug server with the debug configuration
Debug Go [remote delve]
. Set breakpoints in the source code to trigger them.
- Finish deploy settings implementation for Gitea and others.
For upgrading from v1.x.x
to v2.x.x
consider the
braking change documentation.
- Shell on Windows shows weird characters: Githooks outputs UTF-8 characters
(emojis etc.). Make sure you have the UTF-8 codepage active by doing
chcp.com 65001
(either incmd.exe
orgit-bash.exe
, also from an integrated terminal in VS Code). You can make it permanent by putting this into the startup scripts of your shell, e.g. (.bashrc
). Consider using Windows Terminal.
- Original Githooks implementation in
sh
by Viktor Adam.
- Gabriel Nützi (
Go
implementation) - Viktor Adam (Initial
sh
implementation) - Matthijs Kooijman (suggestions & discussions)
- and community.
When you use Githooks and you would like to say thank you for its development and its future maintenance: I am happy to receive any donation which will be distributed equally among all contributors.
MIT