Meine git-hooks
Generelle Struktur
Da ich einige Skripte für viele bis fast alle Projekte benötigen kann, mag ich es einen globalen Ordner für git-hooks zu haben.
Wenn man die Dokumentation anguckt, dann gibt es die Möglichkeit mit
git config --global core.hooksPath ~/.githooks/
alle hooks in meinem Homeverzeichnis zu haben. Dieses hat jetzt nur ein Problem. Ich verliere all hooks, welche ich local haben möchte.
Die beste Alternative, welche ich dazu bis jetzt gefunden habe war:
Ach, dann mach in dem entsprechendem Repo einfach ein ´git config core.hooksPath .git/hooks/´
Aber das möchte ich nicht machen. Ich verliere dann meine anderen globalen Hooks, sobald ich ein Skript Lokal haben möchte.
Meine Möglichkeit das zu umgehen sieht also das folgende vor:
#!/bin/bash
for hook in applypatch-msg pre-applypatch post-applypatch pre-commit pre-merge-commit prepare-commit-msg commit-msg post-commit pre-rebase post-checkout post-merge pre-push pre-receive update post-receive post-update push-to-checkout pre-auto-gc post-rewrite sendemail-validate fsmonitor-watchman p4-pre-submit post-index-change
do
if [ ! -e ${hook} ]; then
echo "#!/bin/bash" > ${hook}
echo "" >> ${hook}
echo 'GIT_BASE_DIR=$(git rev-parse --show-toplevel)' >> ${hook}
echo "exitcode=0" >> ${hook}
echo "" >> ${hook}
echo "if [ -x \${GIT_BASE_DIR}/.git/hooks/${hook} ]; then" >> ${hook}
echo " \$GIT_BASE_DIR/.git/hooks/${hook} \$@ || exitcode=\$?" >> ${hook}
echo "fi" >> ${hook}
echo "" >> ${hook}
echo "# Please insert your checks here." >> ${hook}
echo "" >> ${hook}
echo "exit \${exitcode}" >> ${hook}
chmod a+x ${hook}
fi
done
Dieses Skript erstellt für jeden möglichen hook ein neues Skript, welches zuerst im lokalen .git/hooks Verzeichnis nachguckt ob ein Skript existiert und es ausführt.
Ein paar Sachen auf die dabei zu achten sind:
- Git kann Parameter übergeben (die meisten Skripte)
- Git kann Informationen in das Skript reinpipen (pre-push, pre-receive, post-receive)
- Git kann eine Ausgabe erwarten (pre-receive, update, post-receive, post-update, fsmonitor-watchman)
Meine Lösung dafür ist:
- $@ übergibt alle Parameter des Skriptes an das Unterskript
- Siehe pre-push
- Ich benutze sie einfach nicht. Hier sind es hauptsächlich Server-Skripte, welche bei mir nicht eingesetzt werden.
Damit habe ich jetzt globale Skripte, welche keinen Unterschied zu localen Skripten darstellen. Nur kann ich sie noch erweitern und genau das habe ich für einige auch gemacht.
commit-msg
Rechtschreibung
Meinen commit-msg hook habe ich von hier übernommen. Ich habe es nur um das grep erweitert um die Kommentare von der Prüfung auszuschließen.
ASPELL=$(which aspell)
if [ $? -ne 0 ]; then
echo "Aspell not installed - unable to check spelling" >&2
else
WORDS=$(grep -v -e "^#" "$1" | $ASPELL list )
fi
if [ -n "$WORDS" ]; then
echo -e "\e[1;33mPossible spelling errors found in commit message. Use git commit --amend to change the message.\nPossible mispelled words: " $WORDS ".\e[0m" >&2
fi
Ein einfaches Skript, welches guckt, ob die Commit Message Rechtschreibfehler enthält, da sich ja doch ab und zu welche einschleichen.
pre-commit
Standartskripte
Hier benutzte ich das Beispiel-Skript, da es eine vernünftige Grundfunktionalität besitzt, welche ich eigentlich gerne überall hätte. Nur den letzten Befehl habe ich etwas überarbeitet indem ich das exec entfernt habe um danach noch weiter schreiben zu können.
if git rev-parse --verify HEAD >/dev/null 2>&1
then
against=HEAD
else
# Initial commit: diff against an empty tree object
against=$(git hash-object -t tree /dev/null)
fi
# Cross platform projects tend to avoid non-ASCII filenames; prevent
# them from being added to the repository. We exploit the fact that the
# printable range starts at the space character and ends with tilde.
if [ "$(git config --bool hooks.allownonascii)" != "true" ] &&
# Note that the use of brackets around a tr range is ok here, (it's
# even required, for portability to Solaris 10's /usr/bin/tr), since
# the square bracket bytes happen to fall in the designated range.
test $(git diff --cached --name-only --diff-filter=A -z $against |
LC_ALL=C tr -d '[ -~]\0' | wc -c) != 0
then
echo -e "\e[1;31mCommiting on master is not allowed!\e[0m"
echo -e "To allow commit on master for this repo execute:\n"
echo -e "\tgit config --local --add hooks.allownonascii true\n"
exitcode=1
fi
# If there are whitespace errors, print the offending file names and fail.
git diff-index --check --cached $against -- || exitcode=$?
Master schützen
Es gibt allerdings noch eine kleine Erweiterung. Ich möchte nicht unbedingt auf master commiten, da ich denke ein jeder kann und sollte mit feature und development branches arbeiten, also verbiete ich es mir einfach generell.
# Check where out HEAD is at
if [ "$(git symbolic-ref --short HEAD)" == "master" ]; then
if [ "$(git config --bool --get hooks.allowcommitonmaster)" != "true" ]; then
echo -e "\e[1;31mCommiting on master is not allowed!\e[0m"
echo -e "To allow commit on master for this repo execute:\n"
echo -e "\tgit config --local --add hooks.allowcommitonmaster true\n"
exitcode=1
fi
fi
In manchen Repositories ist es allerdings unerlässlich und damit ich den Befehl dafür niemals vergesse schreibe ich ihn gleich in die Fehlermeldung mit rein.
Python linter
Da ich in letzter Zeit viel in Python programmiere, habe ich zuerst angefangen für viele Projekte einen Linter im CI-File zu installieren, aber ich würde den check auch gerne schon vorher haben. Also filtere ich alle python Dateien raus, welche im Commit vorkommen, und sollte dieses nicht leer sein, dann werden nur diese durch den linter durchgeschickt:
# Do a linting for all modified python files.
pythonfiles=$(git diff --name-only --cached | grep .py | xargs)
if [ "${pythonfiles}" != "" ]; then
# color output, dont print gobal score, and use numcpu threads
pylint --persistent=n --jobs=0 --score=n --output-format=colorized ${pythonfiles} || exitcode=$?
fi
pre-push
Skript-Probleme
Bash übergibt Standartmäßig alle nicht genutzten Parameter der Standarteingabe an das aufgerufene Skript. Was für unseren Standartworkflow genau das ist, was wir benötigen. Sollte man jedoch einmal selbst auch die Eingaben verarbeiten wollen benötigt man etwas mehr. Ich kann jetzt leider nicht die Eingaben einfach doppelt verarbeiten, sondern muss sie in einer temporären Variable speichern. Dieses sieht dann folgendermaßen aus:
#!/bin/bash
OPERANDS=$(cat)
GIT_DIR=$(git rev-parse --show-toplevel)
if [ -x ${GIT_DIR}/.git/hooks/pre-push ]; then
echo "${OPERANDS}" | $GIT_DIR/.git/hooks/pre-push || exitcode=$?
fi
testing
Was ich nett fand war die Möglichkeit teure pre-commit skripte erst bei pre-push auszuführen. Aber das muss ich erst noch ausbauen und die Programmierumgebung/Sprache entdecken.
Irgendwas in der Richtung wird da aber noch kommen.
Server abstellen
Es gibt auf meiner Arbeit einen git Server, welcher obsolet geworden ist, da die meisten Projekte auf einen anderen umgezogen sind. Eigentlich habe ich alle Repositories umgestellt, aber falls es nicht der Fall sein sollte, dann wird hier verhindert, auf genau diesen zu puschen. (Beispiel auf den Umzug von github zu gitlab erstellt)
if [[ $2 =~ "github.com:gumulka88" ]]; then
echo -e "\e[1;31mThou shall not push to $2!\e[0m"
echo "Change url to the new server with:"
newurl=$(echo "$2" | sed 's/github.com:gumulka88/gitlab.com:gumulka/g')
echo -e "\n\tgit remote set-url $1 ${newurl}\n"
exitcode=1
fi
README und LICENSE
Ich mache häufiger mal ein ganz kleines Repo um irgendein Skript zu speichern, welches ich ich erstellt habe, und Monate später komme ich darauf zurück und denke mir:
Was wollte ich noch einmal damit genau und wie kann ich es ändern?
oder aber:
Was waren noch mal die Vorbedingungen für diesen Code?
Hier wäre es doch super, wenn ich eine README angelegt hätte. Oder aber ich finde irgendwo im Netz ein tolles Projekt, welches aber leider nicht mehr weiter entwickelt wird und keine Lizenz hat und es rechtlich schwierig ist, das einfach so zu benutzen im Firmenumfeld. Also lass mich doch dafür sorgen, dass wenigstens ich immer eine Lizenz habe. Das muss nicht beim ersten Commit passieren, wohl aber, wenn ich es veröffentliche.
Ich habe schon die Variable GIT_BASE_DIR, welche auf mein Stammverzeichnis zeigt. Jetzt muss ich nur noch gucken, ob die Dateien existieren.
for checking in README LICENSE; do
lower=$(echo "${checking}" | tr '[:upper:]' '[:lower:]')
if [ "$(ls $GIT_BASE_DIR | grep -i ${checking})" == "" ] ; then
if [ "$(git config --bool --get hooks.pushwithout${lower})" != "true" ]; then
echo -e "\e[1;31mYou must have a ${checking}!\e[0m"
echo -e "To allow pushing without a ${checking} for this repo execute:\n"
echo -e "\tgit config --local --add hooks.pushwithout${lower} true\n"
exitcode=1
fi
fi
done