Skip to content

About

Git Flow hooks for my GO projects

Resources

Stars

1 star

Watchers

1 watching

Forks

Repository files navigation

Git Flow hooks

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:

Pre-Requisites

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.

Installation

Linux

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/null

Add 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 update

Then install gitflow-hooks with:

sudo apt install gitflow-hooks

If you use Homebrew, you can install gitflow-hooks with:

brew install gildas/tap/gitflow-hooks

macOS

You can get gitflow-hooks from Homebrew with:

brew install gildas/tap/gitflow-hooks

You can also download the archive for your platform from the releases page and install it manually.

Windows

You can install gitflow-hooks with scoop:

scoop bucket add gildas https://github.com/gildas/scoop-bucket
scoop install gitflow-hooks

You can also download the archive for your platform from the releases page and install it manually.

Source code

You can also build and install the hook-it application with cargo from this project's folder:

make install

which 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).

Usage

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/repo

Use -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/repo

To inject the hooks in all the repositories found in a folder, use --recursive, optionally with --exclude regular expressions:

hook-it --recursive --exclude archive ~/Projects

While 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.

Configuration

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 false

The 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 false

You can also change the prefix used to tag the new release/hotfix (default is "v"):

git config gitflow.prefix.versiontag v

When 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/chart

The default location is: chart/

You can turn off the chart feature with:

git config --bool gitflow.bump-chart false

The 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 false

You can also change the default name and location of the Dockerfile:

git config gitflow.path.dockerfile path/to/Dockerfile

The 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 false

Custom pre-commit hook

You 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:

  1. 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
  2. the pre-commit of the repository's own hooks folder (e.g., .githooks/pre-commit), see Repositories with their own hooks folder;

  3. pre-commit.local in the hooks folder (e.g., .git/hooks/pre-commit.local). If the repository already has a pre-commit hook that is not ours, hook-it saves it as pre-commit.local before replacing it (a symlink stays a symlink to the same target, e.g., .githooks/pre-commit, which is never modified) (and stops if a different pre-commit.local already exists). If gitflow.path.pre-commit pointed to that hook, it now points to pre-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.

Repositories with their own hooks folder

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 .githooks

git 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:

  1. saves that folder in gitflow.path.custom-hooks (here, .githooks),
  2. points core.hooksPath to the git flow hooks folder (.git/hooks),
  3. 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 from gitflow.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-hooks

Shared git flow configuration

With 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).

Version file

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/file

The 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.json

When 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/.

Rust

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.toml

When 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.0 does not match ^1.2.3 for a 0.x crate, or after a major bump). Only inline tables whose requirement matches the previous version (optionally with ^, =, or ~) are updated;
  • update Cargo.lock with cargo update --workspace (offline first), if Cargo.lock is 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 true

When 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.

Supported version bumps

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 false

Starting 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).

Tag messages

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.

Verbosity

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 minor

or turn on verbose output in the repository:

git config --bool gitflow.verbose true

When 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.

Usage in Target Repository

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 xxx

Similarly, starting a hotfix can be done as follows:

git flow hotfix start xxx

Where 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:

  • major will update the version to 2.0.0
  • minor will update the version to 1.3.0
  • patch will 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/).
  • 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).
  • Node.js:

    • package.json.
  • Salesforce:

    • sfdx-project.json: the version is read from versionNumber (e.g., "1.2.3.NEXT"), then both versionNumber and versionName are updated. If patch bumps are not supported (gitflow.version-patch-supported is false), the trailing .0 is removed from versionName (e.g., 1.3.0 becomes 1.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 version written in PostInstall.cls and in the loggers follows the same rule as versionName.

If you use Pull Requests, you should publish the release once to create a Pull Request:

git flow release publish

As 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 finish

Note: 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.4
git flow release start major

You 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

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-thing

If 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 publish

Once the Pull Request is merged, finish the bugfix:

git flow bugfix finish

As 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.

TODO

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.

Acknowledgments

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...

About

Git Flow hooks for my GO projects

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages