Contenuti

Versionamento e git hooks

In questa pagina vorrei riassumere il metodo che uso per autogenerare un file con i dati della versione corrente del software. Il sistema si appoggia al software di versionamento git ed a GNU make. Utilizzo questo metodo prevalentemente per i miei firmware C/C++ per microcontrollore, ma gli stessi concetti possono essere adattati per altri software o flussi di progetto.

Cosa sono i git hooks?

Si tratta di una serie di script che vengono automaticamente lanciati da git quando si verificano eventi particolari (i.e. un commit, un merge…) 1. Gli script sono memorizzati nella directory .git/hooks, quindi sono personalizzabili per ciascun repository locale.

Script personalizzato

Non appena git prende atto di un cambio di stato nella directory attuale, tutto quel che deve fare è aggiornare il file di versione. Nel caso di un progetto C/C++ predispongo una regola version nel makefile, che si occupa di formattare adeguatamente le stringhe nel file version.git.h.

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

Questo codice va inserito nei file post-checkout, post-commit e post-merge all’interno della suddetta cartella .git/hooks. I nomi sono abbastanza autoesplicativi riguardo alla loro funzione. Sembra non essere contemplata l’azione di pull: in realtà viene invocato lo script relativo al merge in caso di fast-forward o, ovviamente, di merge automatico. In caso di conflitti l’hook viene richiamato al termine della risoluzione manuale (i.e. quando si fa il commit, ed alla fine sempre di un merge si tratta) 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

Preparare il Makefile

Segue come ho deciso di implementare la regola relativa alla versione nei miei makefile. Per tenere traccia delle commit più rilevanti solitamente applico dei tag nella forma v.m, con v (version) ed m (major) interi positivi, che metto in tripletta con il numero di commit effettuate dall’ultimo tag (minor). Si trovano divesi spunti interessanti sul web 3.

Attenzione: git describe --long --dirty --tags fallisce se non è presente alcun tag nella storia del repository.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
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)

Si nota che le varie informazioni sono inserite come macro, non tipizzate. Per poterne fruire ho sempre a portata di mano le macro di stringification 4, come in questo esempio.

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

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

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

Altri casi d’uso

$\LaTeX$

Se si usa un makefile per compilare documenti $\LaTeX$5, è sufficiente modificare lievemente la regola di cui sopra. Una strategia è quella di aggiungere comandi personalizzati che richiamino i valori di interesse. La scelta dei nomi dei comandi è arbitraria.

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

# Popolare TEX con i file da impaginare
# ...
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)

Infine, includere il file nel documento principale con \input{version.git.tex} per poterli richiamare dove serve.

Python

Qui un makefile non c’è, quindi bisogna spostare tutta la regola all’interno degli script di hook. Il concetto però è sempre lo stesso: lo script recupera da git le informazioni di versione e le gira in un file formattato in accordo al linguaggio usato. In definitiva… quel che segue. Occhio a lanciarlo con bash, perché non tutte le shell supportano l’IFS6.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
#!/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