Automatic Semantic Versioning in Git

Most software that goes beyond small scripts has some sort of version number scheme. Very popular seems to be the Semantic Versioning schema as described by Tom Preston-Werner on a dedicated website. Let me quote the summary he gives there:

Given a version number MAJOR.MINOR.PATCH, increment the:

  1. MAJOR version when you make incompatible API changes,
  2. MINOR version when you add functionality in a backwards compatible manner, and
  3. PATCH version when you make backwards compatible bug fixes.

Additional labels for pre-release and build metadata are available as extensions to the MAJOR.MINOR.PATCH format.

(Tom Preston-Werner, Semantic Versioning 2.0.0)

Thus, a version number always consists of exactly three numbers separated by dots. (I will ignore the possibility for appended labels such as beta, alpha or rc (release candidate) in this post.)

When using git for version control of source code, the natural thing to do is to tag commits with these version numbers to document which commit corresponds to which version. Typically, a v is added before the number, so a tag might have the name v3.42.5, for example.

Now, when creating a tag, one could just use git describe or git log to check for the last tag and type the new version tag by hand. This is not a lot of work and doesn’t need to be done very often, but I wanted to automate the progress anyway.

Adding a custom git command

git directly provides a tool for the user to automate small repetitive tasks, by using aliases. These can be defined either by something like git config --global alias.ls 'log --pretty=format:\""%C(yellow)%h%Cred%d %Creset%s%Cblue [%cn]\"" --decorate', or it can be added directly to the ~/.gitconfig file under [alias]. In the example above, it just uses git internal commands, but using a ““bang”” (!) before the commands allows to also use any command line tool we want.

In principle, it is possible to define and call a complete bash function in there, but since the problem at hand requires more than just a few lines of code, I instead saved my code it in an separate .sh file:

#!/usr/bin/bash

git-tag-version() {
    MAJOR_VERSION=0
    MINOR_VERSION=0
    PATCH_VERSION=0

    LATEST_VERSION_TAG=`git tag -l ""v[0-9]*\.[0-9]*\.[0-9]*"" | tail -n 1`
    if [[ -z $LATEST_VERSION_TAG ]]
    then
        echo ""None of the tags matches the pattern 'vX.Y.Z', please create the first manually.""
        exit 1
    fi

    LATEST_VERSION=`expr $LATEST_VERSION_TAG : 'v\([0-9]*\.[0-9]*\.[0-9]*\)'`

    MAJOR_VERSION=`echo $LATEST_VERSION | cut -d '.' -f1`
    MINOR_VERSION=`echo $LATEST_VERSION | cut -d '.' -f2`
    PATCH_VERSION=`echo $LATEST_VERSION | cut -d '.' -f3`

    # increase version based on arguments
    if [[ $# -ne 1 ]]
    then
        echo ""Exactly one argument is required!""
        exit 1
    else
        key=$1
        case $key in
            '-M'|'--major')
                MAJOR_VERSION=`expr $MAJOR_VERSION + 1`
                MINOR_VERSION=0
                PATCH_VERSION=0
                shift
                ;;
            '-m'|'--minor')
                MINOR_VERSION=`expr $MINOR_VERSION + 1`
                PATCH_VERSION=0
                shift
                ;;
            '-p'|'--patch')
                PATCH_VERSION=`expr $PATCH_VERSION + 1`
                shift
                ;;
            *)
                echo ""$key is not a valid argument!""
                exit 1
        esac
    fi

    VERSION=""$MAJOR_VERSION.$MINOR_VERSION.$PATCH_VERSION""

    echo ""v$VERSION""
    git tag -a ""v$VERSION"" -m ""Version $VERSION""
}

git-tag-version $@

This script extracts the latest git tag which fits the format vX.Y.Z. It then expects exactly one parameter, which determines if the new version increases the X, Y or Z. If it is not the patch number (Z) that gets increased, all lower order numbers are set back to zero. Finally, a git tag with this new version number is created.

I put this file in my ““scripts”” code folder, made it executable for me with chmod u+x and linked it into my ~/.local/bin folder with ln -s. This folder is automatically part of the PATH variable, which means I can now simply type git-tag-version.sh -m into the console and get a new tag with automatically increased minor version number.

To make it even better, I can now use the git alias function, I mentioned earlier. Just saying git config --global alias.tagver '!git-tag-version.sh' makes it possible to now call git tagver -m inside any git repository, that uses the semantic versioning scheme (without the additional labels).

Possible improvements

As mentioned, semantic versioning allows for additional labels, after the ““core”” version number. It is possible to append a - followed by a pre-release identifier and/or a + followed by a build identifier (this is similar to what git describe does btw.). I don’t think there is reason to add a build identifier as a git tag, since this should be equivalent to the commit hashs git already uses, but the pre-release identifiers might be worth an extension of this script in the future.

git  bash