Contents

Versioning and Git Hooks

In this page, I’d like to summarize the method I use to auto-generate a file with the current version data of the software. The system relies on git version control and GNU make. I primarily use this method for my C/C++ firmware for microcontrollers, but the same concepts can be adapted for other software or project workflows.

What are git hooks?

Git hooks are a series of scripts that are automatically executed by git when certain events occur (e.g., a commit, a merge) 1. These scripts are stored in the .git/hooks directory, making them customizable for each local repository.

Custom script

As soon as git detects a state change in the current directory, all it needs to do is update the version file. In the case of a C/C++ project, I set up a version rule in the makefile, which takes care of properly formatting the strings in the version.git.h file.

1
2
3
4
if [[ -f "Makefile" || -f "makefile" ]]; then
    make version > /dev/null 2> /dev/null
    echo "Version updated!"
fi

This code should be placed in the post-checkout, post-commit, and post-merge files within the .git/hooks folder. The names are quite self-explanatory regarding their functions. Note that the action of pulling is not explicitly mentioned; in reality, the script for the merge is invoked in the case of a fast-forward or, of course, an automatic merge. In the case of conflicts, the hook is called after manual resolution (i.e., when you commit, and it’s always a merge at the end) 2.

1
2
3
4
5
6
    vim version_hook.sh
    chmod +x version_hook.sh
    cd .git/hooks
    ln -s ../../version_hook.sh post-checkout
    ln -s ../../version_hook.sh post-commit
    ln -s ../../version_hook.sh post-merge

Preparing the Makefile

Below is how I decided to implement the version rule in my makefiles. To keep track of the most relevant commits, I usually apply tags in the form v.m, with v (version) and m (major) as positive integers, and I put them in a triplet with the number of commits made since the last tag (minor). There are various interesting ideas on the web 3.

Note: git describe --long --dirty --tags will fail if there are no tags in the repository history.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
PROJ_NAME=project
GIT_DESCRIBE:=$(subst -, ,$(shell git describe --long --dirty --tags))
BRANCH  := $(shell git branch --show-current)
COMMIT  := $(strip $(word 3, $(GIT_DESCRIBE)))
VERSION := $(strip $(word 1, $(subst ., ,$(word 1, $(GIT_DESCRIBE)))))
MAJOR   := $(strip $(word 2, $(subst ., ,$(word 1, $(GIT_DESCRIBE)))))
MINOR   := $(strip $(word 2, $(GIT_DESCRIBE)))
DIRTY   := $(strip $(word 4, $(GIT_DESCRIBE)))
VERSION_FILE = inc/version.git.h

## ...
version:
	@echo "#pragma once" > $(VERSION_FILE)
	@echo "#define BRANCH $(BRANCH)" >> $(VERSION_FILE)
	@echo "#define COMMIT $(COMMIT)" >> $(VERSION_FILE)
	@echo "#define VERSION $(VERSION)" >> $(VERSION_FILE)
	@echo "#define MAJOR $(MAJOR)" >> $(VERSION_FILE)
	@echo "#define MINOR $(MINOR)" >> $(VERSION_FILE)
	@echo "#define DIRTY $(DIRTY)" >> $(VERSION_FILE)

The various pieces of information are added as macros but annot be directly used as strings. To use them, I have the stringification macros 4, as shown in this example.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// +++ Usually in utils.h +++
#define xstr(s) str(s)
#define str(s) #s
// ---          ---           ---

// +++        version.c       +++
#include "version.git.h"

const char[] VERSION_STR = xstr(VERSION);
// ---          ---           ---

Other Use Cases

$\LaTeX$

If you use a makefile to compile $\LaTeX$ documents 5, you just need to slightly modify the rule mentioned above. One strategy is to add custom commands that fetch the relevant values. The choice of command names is arbitrary.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
## ...
VERSION_FILE = version.git.tex

# Populate TEX with files to be typeset
# ...
TEX += VERSION_FILE
## ...

version:
	@echo -n "" > $(VERSION_FILE)
	@echo "\newcommand{\git_BRANCH}{$(BRANCH)}" >> $(VERSION_FILE)
	@echo "\newcommand{\git_COMMIT}{$(COMMIT)}" >> $(VERSION_FILE)
	@echo "\newcommand{\git_VERSION}{$(VERSION)}" >> $(VERSION_FILE)
	@echo "\newcommand{\git_MAJOR}{$(MAJOR)}" >> $(VERSION_FILE)
	@echo "\newcommand{\git_MINOR}{$(MINOR)}" >> $(VERSION_FILE)
	@echo "\newcommand{\git_DIRTY}{$(DIRTY)}" >> $(VERSION_FILE)

Finally, include the file in the main document with \input{version.git.tex} to use the values where needed.

Python

In this case, there’s no makefile, so you need to move the entire rule inside the hook script. However, the concept remains the same: the script retrieves version information from git and formats it into a file in accordance with the language used. Here’s how it looks. Note that you need to execute it with bash, as not all shells support IFS6.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#!/bin/bash
PROJ_NAME=project

GIT_DESCRIBE=$(git describe --long --dirty --tags)
IFS='-' read -r -a GIT_DESCRIBE <<< "$GIT_DESCRIBE"
BRANCH=$(git branch --show-current)
COMMIT=${GIT_DESCRIBE[2]}
IFS='.' read -r -a VERSION_A <<< "${GIT_DESCRIBE[0]}"
VERSION=${VERSION_A[0]}
MAJOR=${VERSION_A[1]}
MINOR=${GIT_DESCRIBE[1]}
DIRTY=${GIT_DESCRIBE[4]}
VERSION_FILE=version.py

echo -n "" > $VERSION_FILE
echo "BRANCH = \"$BRANCH\"" >> $VERSION_FILE
echo "COMMIT = \"$COMMIT\"" >> $VERSION_FILE
echo "VERSION = \"$VERSION

\"" >> $VERSION_FILE
echo "MAJOR = \"$MAJOR\"" >> $VERSION_FILE
echo "MINOR = \"$MINOR\"" >> $VERSION_FILE
echo "DIRTY = \"$DIRTY\"" >> $VERSION_FILE

References