This repository contains git hooks for git flow to help deploy releases and hotfixes in my Go, Rust, Node.js, and Salesforce repositories.
Here are some sample repositories for GitHub, BitBucket, and GitLab:
The hooks from this repository work with git-flow-next (brew install git-flow-next on macOS) or git flow AVH Edition, which is not maintained anymore. The original git-flow does not handle git hooks. Make sure to install one of these!
The version of the target repository is assumed to follow the semver specifications.
You can grab the latest binary package on the Downloads pages.
On Debian/Ubuntu distributions, you can use my package repository. First download the signing key:
curl -fsSL https://gildas.github.io/apt/gildas-archive-keyring.gpg | \
sudo tee /usr/share/keyrings/gildas-archive-keyring.gpg >/dev/nullAdd this source:
echo "deb [arch=amd64,arm64 signed-by=/usr/share/keyrings/gildas-archive-keyring.gpg] https://gildas.github.io/apt stable main" | sudo tee /etc/apt/sources.list.d/gildas.list
sudo apt updateThen install gitflow-hooks with:
sudo apt install gitflow-hooksIf you use Homebrew, you can install gitflow-hooks with:
brew install gildas/tap/gitflow-hooksYou can get gitflow-hooks from Homebrew with:
brew install gildas/tap/gitflow-hooksYou can also download the archive for your platform from the releases page and install it manually.
You can install gitflow-hooks with scoop:
scoop bucket add gildas https://github.com/gildas/scoop-bucket
scoop install gitflow-hooksYou can also download the archive for your platform from the releases page and install it manually.
You can also build and install the hook-it application with cargo from this project's folder:
make installwhich runs cargo install --path .. Run make help to see the other recipes (build, test, lint, fmt, ...). make lint checks the Rust code with clippy, and the syntax of the hooks (plus shellcheck, if installed).
The hooks are embedded in the binary, so hook-it is the only file to ship to users, and it runs from anywhere (Linux, macOS, and Windows). Point it at the target repository:
hook-it /path/to/repoUse -v to display more information and --noop (or --dry-run) to see what would be done without modifying anything. Check all the options with hook-it --help.
hook-it checks that the repository is valid and has git-flow already; if not, it tries to initialize git-flow in the repository. With git-flow-next, it uses the branch names from gitflow.branch.master and gitflow.branch.develop if they are set anywhere in your git config (e.g., git config --global gitflow.branch.develop dev), instead of git-flow-next's defaults (main and develop).
You can also choose the branches on the command line, which takes precedence over the git config:
hook-it --main master --develop dev /path/to/repo(--master and --dev work as well.) The main branch must already exist, locally or on origin (unless the repository has no branch at all), so a typo does not create a new production branch; the develop branch is created from the main branch if needed. If git-flow ends up using other branches than the requested ones, hook-it stops with an error.
These flags are only used when hook-it initializes git-flow. If the repository already uses git-flow with other branches, hook-it leaves them alone and displays a warning; run git flow init -f in the repository to change them.
It also checks that your installed git-flow is git-flow-next or the AVH edition, and stops if it is not the case.
By default, hook-it looks for the kind of language used in the repository (Go, Rust, Salesforce, or Node.js) and copies the appropriate hooks. It searches in the root of the repository, then in the src/, backend/, and src/backend/ folders.
You can also tell hook-it where to look for the code with (it can be repeated):
hook-it --src source_folder /path/to/repoTo inject the hooks in all the repositories found in a folder, use --recursive, optionally with --exclude regular expressions:
hook-it --recursive --exclude archive ~/ProjectsWhile editing the hooks, --hooks-dir hooks copies them from that folder instead of the embedded ones. Run make install again to embed the new versions.
By default, the hooks will prevent you from:
- committing anything to the master branch
- committing anything that has unresolved merge conflicts
- committing code that is not linted (Go with golangci-lint and staticcheck, Node.js and Salesforce LWC/Aura with the project's ESLint, Rust with Clippy when turned on, see Rust). Missing linters are skipped with a warning.
- force Pull Request usage
In case you do not want either of these features, you can turn them off with:
git config --bool gitflow.allow-master-commit true
git config --bool gitflow.allow-conflict-commit true
git config --bool gitflow.must-pass-lint false
git config --bool gitflow.use-pull-request falseThe hooks will also:
- format the code with gofmt for Go changes, with rustfmt for Rust changes (skipped if rustfmt is not installed), and with the project's Prettier for Node.js and Salesforce changes (skipped if Prettier is not installed)
Similarly, you can turn off the formatting feature with:
git config --bool gitflow.prettify falseYou can also change the prefix used to tag the new release/hotfix (default is "v"):
git config gitflow.prefix.versiontag vWhen using Pull Requests, if the origin is on GitHub, the scripts will rely on the Github's CLI (gh) and will fail if the tool is not present.
If the origin is on BitBucket, the scripts will rely on the Bitbucket's CLI (bb) and will fail if the tool is not present. You can configure which Bitbucket profile is used in your git config (git config bitbucket.cli.profile myprofile)
If the origin is on GitLab, the scripts will reply on the GitLab's CLI (glab) and will fail if the tool is not present.
The scripts will also bump the Helm Chart version if it is present. You can configure the location of the chart with:
git config gitflow.path.chart path/to/chartThe default location is: chart/
You can turn off the chart feature with:
git config --bool gitflow.bump-chart falseThe scripts will also update the OCI annotations (version and created) in the Dockerfile if present.
You can turn off the Dockerfile feature with:
git config --bool gitflow.bump-dockerfile falseYou can also change the default name and location of the Dockerfile:
git config gitflow.path.dockerfile path/to/DockerfileThe scripts will also bump the Appveyor version if it is present.
You can turn off the Appveyor feature with:
git config --bool gitflow.bump-appveyor falseYou can run your own pre-commit hook as well. It runs after the checks of the git flow pre-commit hook, and only if they pass. If it fails, the commit fails.
The hooks run the first custom pre-commit hook they find:
-
the script given by
gitflow.path.pre-commit, its path is relative to the root of the repository:git config gitflow.path.pre-commit scripts/pre-commit
-
the
pre-commitof the repository's own hooks folder (e.g.,.githooks/pre-commit), see Repositories with their own hooks folder; -
pre-commit.localin the hooks folder (e.g.,.git/hooks/pre-commit.local). If the repository already has a pre-commit hook that is not ours,hook-itsaves it aspre-commit.localbefore replacing it (a symlink stays a symlink to the same target, e.g.,.githooks/pre-commit, which is never modified) (and stops if a differentpre-commit.localalready exists). Ifgitflow.path.pre-commitpointed to that hook, it now points topre-commit.local.
The script of 1. must be executable, otherwise the commit fails. A custom hook cannot be a git flow pre-commit hook (it would run itself forever), the commit fails as well. When gitflow.path.pre-commit is set, the commit also fails if the script does not exist. Like git, the hooks ignore the hooks of 2. and 3. that are not executable, with a warning (e.g., a disabled pre-commit hook saved by hook-it).
While a merge is in progress, the git flow checks are skipped, but the custom hook still runs.
Some repositories commit their hooks, in a .githooks folder for instance, so the whole team shares them, and each developer turns them on with:
git config core.hooksPath .githooksgit then only runs the hooks of that folder, and the git flow pre-commit hook would never run. So, when core.hooksPath points to another folder (in the repository config or in your global config), hook-it:
- saves that folder in
gitflow.path.custom-hooks(here,.githooks), - points
core.hooksPathto the git flow hooks folder (.git/hooks), - installs a small forwarder in the git flow hooks folder for every other git hook (
commit-msg,pre-push,post-checkout, ...). It runs the hook of the same name fromgitflow.path.custom-hooks, if it exists.
Like git with core.hooksPath, a relative gitflow.path.custom-hooks is relative to the folder where the hook runs: the root of the working tree, or the .git folder for the hooks that receive a push (pre-receive, update, ...).
If you move or copy the repository, run hook-it again: it recognizes its own hooks folder from the previous location and keeps gitflow.path.custom-hooks.
The git flow pre-commit hook runs the pre-commit of that folder after its own checks (see Custom pre-commit hook). The other hooks of the folder run as before, and hooks added later to the folder (e.g., by a teammate) run as well, without running hook-it again.
hook-it never overwrites a hook of the git flow hooks folder that is not ours (e.g., a ctags post-checkout): it displays a warning if the custom folder has a hook with the same name, as that hook will not run. Move the file away and run hook-it again.
The procedure for a developer of such a repository is then:
git clone <url> && cd <repo>
git config core.hooksPath .githooks # the usual procedure of the repository, if any
hook-it .If something sets core.hooksPath again later (e.g., husky on npm install), the git flow pre-commit hook does not run anymore; run hook-it again, it saves the new folder and takes core.hooksPath over again.
To stop forwarding, give the folder back to git:
git config core.hooksPath "$(git config gitflow.path.custom-hooks)"
git config --unset gitflow.path.custom-hooksWith git-flow-next, git flow init --shared writes the git flow configuration (branches, prefixes, ...) in a .gitflow file at the root of the repository, to commit, so the whole team uses the same branches. That file only carries the branch configuration, not the hooks: each developer still runs hook-it after cloning.
git flow init and the hooks ignore .gitflow, so when the local config has no git flow configuration and the repository has a .gitflow file, hook-it copies its branch configuration into the local config instead of initializing git flow with the default branches. The other settings of .gitflow (e.g., gitflow.path.pre-commit, gitflow.allow-master-commit) are ignored with a warning, as they would let a repository run its own scripts on commit, or turn the checks off, without you opting in; set them yourself if you trust them. --main and --develop do not change the shared branches (hook-it only warns if they differ).
By default, the scripts look for the file carrying the version of the repository:
- Go: the Go file that contains a line like
var VERSION = "1.2.3". It can be in a subfolder (e.g.,src/backend/version.go); the shallowest match wins; - Rust: the shallowest
Cargo.toml, which is the workspace root in a workspace (see Rust); - Node.js:
package.json; - Salesforce:
sfdx-project.json.
You can point the scripts to another file with:
git config gitflow.path.versionfile path/to/version/fileThe path is relative to the root of the repository. If the file cannot be found, starting a release or a hotfix fails.
In Go and Rust repositories, the scripts also bump the package.json of an embedded web client, if any (e.g., src/frontend/package.json); the shallowest one wins. You can point the scripts to another one with:
git config gitflow.path.packagejson path/to/package.jsonWhen searching, the scripts only consider the files known to git (tracked, or new and not ignored), and they skip hidden folders (e.g., .local/), vendor/, and node_modules/.
In a single crate, the version is read from the [package] section of Cargo.toml.
In a workspace, the version is read from the [workspace.package] section of the root Cargo.toml, and the members inherit it with version.workspace = true:
[workspace.package]
version = "1.2.3"If the members of the workspace carry their own versions instead, point the scripts to the Cargo.toml of the member that carries the version of the repository:
git config gitflow.path.versionfile crates/app/Cargo.tomlWhen bumping the version, the scripts also:
- bump the version requirements of the workspace's own crates, like
core = { path = "crates/core", version = "1.2.3" }. Otherwise cargo cannot resolve the workspace anymore (e.g.,1.3.0does not match^1.2.3for a0.xcrate, or after a major bump). Only inline tables whose requirement matches the previous version (optionally with^,=, or~) are updated; - update
Cargo.lockwithcargo update --workspace(offline first), ifCargo.lockis tracked by git. Only the entries of the workspace members change, the dependencies stay untouched. If cargo is not installed, the scripts warn you to run the command yourself.
When committing, the scripts format the staged .rs files with rustfmt, using the edition from Cargo.toml and the project's rustfmt.toml. Only the staged files are formatted, not the modules they declare; if a file is partially staged, the unstaged changes stay out of the commit.
Clippy is turned off by default: it builds the whole workspace, which can make every commit slow. You can turn it on with:
git config --bool gitflow.use-clippy trueWhen turned on, the scripts run cargo clippy --workspace --all-targets -- -D warnings whenever .rs files are staged, and the commit fails if Clippy reports anything. Note that Clippy checks the working copy, including unstaged changes. gitflow.must-pass-lint false turns it off as well.
All the bump types (major, minor, patch) are allowed by default. Some repositories do not want some of them; for example, a Salesforce package that does not use patch numbers. You can forbid a bump type with:
git config --bool gitflow.version-major-supported false
git config --bool gitflow.version-minor-supported false
git config --bool gitflow.version-patch-supported falseStarting a release or a hotfix with a forbidden bump type (e.g., git flow hotfix start patch) then fails. Giving an explicit version number (e.g., git flow hotfix start 1.2.4) is always allowed.
The scripts also refuse to bump a version that is not made of exactly 3 components (major.minor.patch).
When finishing a release or a hotfix, you can customize the tag message with:
git config gitflow.release.finish.message "Release %tag%"
git config gitflow.hotfix.finish.message "Hotfix %tag%"The placeholder is replaced by the version, without its prefix. You can change the placeholder with:
git config gitflow.message.versiontag "%tag%"With git-flow-next, the custom message is used only when git-flow-next would use its default message (Tagging version ...), so a message given with -m on the command line is kept.
By default, the hooks only display their results, warnings, and errors. To see what they are doing, either set the VERBOSE environment variable:
VERBOSE=1 git flow release start minoror turn on verbose output in the repository:
git config --bool gitflow.verbose trueWhen gitflow.verbose is used, the hooks display a warning reminding you that verbose output is on.
With VERBOSE=2, the hooks also display tracing information, like the name of each hook as it runs.
Once the hooks are initialized, everything can be done in the target repository folder.
Starting a new release can be done simply:
git flow release start xxxSimilarly, starting a hotfix can be done as follows:
git flow hotfix start xxxWhere xxx is the new version you are releasing/hotfixing, it is mandatory. If xxx is one of major, minor, patch, the scripts will bump the corresponding component of the current version (from the repository code) according to the semver recommandations.
For example, if the current version is 1.2.3:
majorwill update the version to 2.0.0minorwill update the version to 1.3.0patchwill update the version to 1.2.4
The scripts will stage and commit the bumped files in a single commit (Bumped to version x.y.z).
Depending on the language, the scripts will bump the following files:
-
Go:
- version.go (or any file that contains the line:
var VERSION = "1.2.3", even in a subfolder, see Version file); - package.json as well, if the repository contains one (for example, a Go backend with an embedded web client in
src/frontend/).
- version.go (or any file that contains the line:
-
Rust:
- Cargo.toml: the version in
[package], or in[workspace.package]for a workspace, and the version requirements of the workspace's own crates; - Cargo.lock, if it is tracked by git;
- package.json as well, if the repository contains one (for an embedded web client).
- Cargo.toml: the version in
-
Node.js:
- package.json.
-
Salesforce:
- sfdx-project.json: the version is read from
versionNumber(e.g.,"1.2.3.NEXT"), then bothversionNumberandversionNameare updated. If patch bumps are not supported (gitflow.version-patch-supportedisfalse), the trailing.0is removed fromversionName(e.g.,1.3.0becomes1.3); - package.json, if present at the root of the repository;
- the post-install Apex class (PostInstall.cls), if present: the line
String PACKAGE_VERSION = '...';, so the post-install script knows which version is being installed; - the loggers, if present, so the version they report matches the package:
- the Aura logger (logger.js): the line
var version = '...';, - the LWC logger (genforceLogger.js): the line
version = '...';.
- the Aura logger (logger.js): the line
The version written in PostInstall.cls and in the loggers follows the same rule as
versionName. - sfdx-project.json: the version is read from
If you use Pull Requests, you should publish the release once to create a Pull Request:
git flow release publishAs indicated in the command line result instruction, while merging the Pull Request, the approver should not delete the release branch.
When the release is ready, simply finish it:
git flow release finishNote: The finish process will fail if there is no Pull Request (asuming you use the Pull Request usage feature) or the current Pull Request has not been properly merged.
Examples:
git flow release start 12.3.4git flow release start majorYou do not need to repeat the version when finishing the release/hotfix if you are on their branch.
As stated earlier, if the repository has a "chart" folder, the scripts will update the "appVersion" accordingly as well. They will also bump the chart version according to the same rules used for the application version.
Features and bugfixes work the same way, features with git flow feature ..., and bugfixes with git flow bugfix .... Both start from the develop branch and are merged back into it:
git flow bugfix start fix-the-thingIf you use Pull Requests, publish the bugfix to create a Pull Request against the develop branch. Its description lists the commits of the branch:
git flow bugfix publishOnce the Pull Request is merged, finish the bugfix:
git flow bugfix finishAs with releases and hotfixes, the finish process fails if there is no Pull Request or if the Pull Request has not been merged yet. Before finishing, the scripts fetch the develop branch from the origin so the merged Pull Request is included.
The hooks should log their work and maybe display some nicer progress.
Maybe having some in-repository code that would allow the hooks to update to the current version of this repository.
Thanks to Peter van der Does and Jasper N. Brouwer for their git flow hooks examples (resp. petervanderdoes/gitflow-avh, jaspernbrouwer/git-flow-hooks) that inspired me.
Of course, we wouldn't have any git flow without Vincent Driessen and his inspiring blog post A successful Git branching model about 10 years ago...