Wie kann ich meinen C-Code dazu bringen, seinen Git-Versions-Hash automatisch auszudrucken?

83

Gibt es eine einfache Möglichkeit, C-Code zu schreiben, der auf den Hash der Git-Version zugreifen kann?

Ich habe Software in C geschrieben, um wissenschaftliche Daten in einer Laborumgebung zu sammeln. Mein Code zeichnet die gesammelten Daten zur späteren Analyse in einer .yaml-Datei auf. Meine Experimente ändern sich von Tag zu Tag und ich muss oft den Code ändern. Um Revisionen zu verfolgen, verwende ich ein Git-Repository.

Ich möchte in der Lage sein, den Git-Revisions-Hash als Kommentar in meine .yaml-Datendateien aufzunehmen. Auf diese Weise konnte ich mir die .yaml-Datei ansehen und genau wissen, mit welchem ​​Code die in dieser Datei angezeigten Daten generiert wurden. Gibt es eine einfache Möglichkeit, dies automatisch zu tun?

AndyL
quelle
1
Die Verwendung von Pre-Commit-Hooks (siehe book.git-scm.com/5_git_hooks.html ) wäre eine weitere Möglichkeit, dies zu tun.
Yktula

Antworten:

38

In meinem Programm halte ich die Git-Versionsnummer und das Datum des Builds in einer separaten Datei namens " version.c, die folgendermaßen aussieht:

#include "version.h"
const char * build_date = "2009-11-10 11:09";
const char * build_git_sha = "6b54ea36e92d4907aba8b3fade7f2d58a921b6cd";

Es gibt auch eine Header-Datei, die so aussieht:

#ifndef VERSION_H
#define VERSION_H
extern const char * build_date; /* 2009-11-10 11:09 */
extern const char * build_git_sha; /* 6b54ea36e92d4907aba8b3fade7f2d58a921b6cd */
#endif /* VERSION_H */

Sowohl die Header-Datei als auch die C-Datei werden von einem Perl-Skript generiert, das folgendermaßen aussieht:

my $git_sha = `git rev-parse HEAD`;
$git_sha =~ s/\s+//g;
# This contains all the build variables.
my %build;
$build{date} = make_date_time ();
$build{git_sha} = $git_sha;

hash_to_c_file ("version.c", \%build, "build_");

Hier hash_to_c_fileerledigt man die ganze Arbeit des Erstellens version.cund version.hund make_date_timemacht einen String wie gezeigt.

Im Hauptprogramm habe ich eine Routine

#include "version.h"

// The name of this program.
const char * program_name = "magikruiser";
// The version of this program.
const char * version = "0.010";

/* Print an ID stamp for the program. */

static void _program_id_stamp (FILE * output)
{
    fprintf (output, "%s / %s / %s / %s\n",
             program_name, version,
             build_date, build_git_sha);
}

Ich kenne mich mit Git nicht so gut aus, daher würde ich Kommentare begrüßen, wenn es einen besseren Weg gibt, dies zu tun.

Stefan Majewsky
quelle
1
Das Perl-Skript ist Teil des Build-Skripts, das für alles ein "One-Step-Build" ist.
12
Dies ist soweit es gut ist, aber denken Sie daran, dass es den Hash des letzten Commits in der Verzweigung meldet, nicht den Hash des zu kompilierenden Codes. Wenn es nicht festgeschriebene Änderungen gibt, sind diese nicht ersichtlich.
Phil Miller
1
git diff prüft standardmäßig auf Unterschiede zwischen Ihrem Arbeitsbereich und dem Index. Sie können auch versuchen, git diff - zwischengespeichert für Unterschiede zwischen dem Index und HEAD
Karl
6
Alle diese 'const char * name = "value";' Konstrukte könnten sinnvollerweise in 'const char name [] = "value";' geändert werden, wodurch 4 Bytes pro Element auf einem 32-Bit-Computer und 8 Bytes pro Element auf einem 64-Bit-Computer gespeichert werden. Zugegeben, in diesen Tagen mit GB Hauptspeicher ist das kein großes Problem, aber alles hilft. Beachten Sie, dass sich keiner der Codes, die die Namen verwenden, ändern muss.
Jonathan Leffler
1
Ich habe sie geändert, wie Sie vorschlagen. Die Größe meines Programms mit const char []: 319356 Bytes (gestrippt). Die Größe meines Programms mit const char *: 319324 Bytes (entfernt). Ihre Idee scheint also keine Bytes zu sparen, sondern erhöht die Gesamtzahl um 32. Ich habe keine Ahnung warum. In der ursprünglichen "version.c" gibt es drei Zeichenfolgen, aber eine wurde in der obigen Antwort weggelassen. Wenn Sie sich die erste Bearbeitung ansehen, ist sie immer noch da.
160

Wenn Sie einen make-basierten Build verwenden, können Sie diesen in das Makefile einfügen:

GIT_VERSION := "$(shell git describe --abbrev=4 --dirty --always --tags)"

(Siehe man git beschreiben, was die Schalter tun)

Fügen Sie dies dann Ihren CFLAGS hinzu:

-DVERSION=\"$(GIT_VERSION)\"

Dann können Sie einfach direkt im Programm auf die Version verweisen, als wäre es ein #define:

printf("Version: %s\n", VERSION);

Standardmäßig wird nur eine abgekürzte Git-Commit-ID gedruckt. Optional können Sie jedoch bestimmte Releases mit folgenden Tags versehen:

git tag -a v1.1 -m "Release v1.1"

dann wird es ausgedruckt:

Version: v1.1-2-g766d

Das heißt, 2 Commits nach Version 1.1, wobei eine Git-Commit-ID mit "766d" beginnt.

Wenn in Ihrem Baum nicht festgeschriebene Änderungen vorhanden sind, wird "-dirty" angehängt.

Es gibt kein Scannen von Abhängigkeiten, daher müssen Sie eine explizite make cleanAktion ausführen, um die Aktualisierung der Version zu erzwingen. Dies kann jedoch gelöst werden .

Die Vorteile sind, dass es einfach ist und keine zusätzlichen Build-Abhängigkeiten wie Perl oder Awk erfordert. Ich habe diesen Ansatz mit GNU Automake und mit Android NDK Builds verwendet.

ndyer
quelle
5
+1 Ich persönlich möchte, dass das Makefile eine Header-Datei generiert, die diese enthält, #define GIT_VERSION ...anstatt sie mit der -DOption in die Befehlszeile zu setzen . Es beseitigt das Abhängigkeitsproblem. Auch warum der doppelte Unterstrich? Technisch gesehen ist das eine reservierte Kennung.
Dan Moulding
8
Jeder für sich - wie ich schon sagte, die Vorteile sind, dass es nur wenige bewegliche Teile hat und sie verständlich sind. Ich habe es bearbeitet, um die Unterstriche zu entfernen.
ndyer
Es sollte hinzugefügt werden, dass, wenn Sie gengetopt verwenden, dies direkt zu gengetopt im Makefile hinzugefügt werden kann: gengetopt --set-version = $ (GIT_VERSION)
Trygve
1
Die erste Anweisung sollte in Anführungszeichen stehen GIT_VERSION := "$(shell git describe --abbrev=4 --dirty --always --tags)"und funktioniert nicht ohne Anführungszeichen.
Abel Tom
11

Am Ende habe ich etwas verwendet, das der Antwort von @ Kinopiko sehr ähnlich ist, aber ich habe awk anstelle von perl verwendet. Dies ist nützlich, wenn Sie auf Windows-Computern stecken, auf denen awk von Natur aus mingw installiert ist, jedoch nicht perl. So funktioniert das.

Mein Makefile enthält eine Zeile, die git, date und awk aufruft, um eine AC-Datei zu erstellen:

$(MyLibs)/version.c: FORCE 
    $(GIT) rev-parse HEAD | awk ' BEGIN {print "#include \"version.h\""} {print "const char * build_git_sha = \"" $$0"\";"} END {}' > $(MyLibs)/version.c
    date | awk 'BEGIN {} {print "const char * build_git_time = \""$$0"\";"} END {} ' >> $(MyLibs)/version.c 

Jedes Mal, wenn ich meinen Code kompiliere, generiert der Befehl awk eine version.c-Datei, die folgendermaßen aussieht:

/* version.c */
#include "version.h"
const char * build_git_sha = "ac5bffc90f0034df9e091a7b3aa12d150df26a0e";
const char * build_git_time = "Thu Dec  3 18:03:58 EST 2009";

Ich habe eine statische version.h-Datei, die so aussieht:

/*version.h*/
#ifndef VERSION_H_
#define VERSION_H_

extern const char * build_git_time;
extern const char * build_git_sha;


#endif /* VERSION_H_ */

Der Rest meines Codes kann jetzt auf die Build-Zeit und den Git-Hash zugreifen, indem einfach der Header version.h eingefügt wird. Zum Abschluss fordere ich git auf, version.c zu ignorieren, indem ich meiner .gitignore-Datei eine Zeile hinzufüge. Auf diese Weise gibt mir Git nicht ständig Zusammenführungskonflikte. Hoffe das hilft!

AndyL
quelle
Ein Nachtrag ... das wird in Matlab funktionieren
AndyL
1
Ich halte das nicht für FORCEeine gute Idee, da Makefile niemals zufrieden sein wird (jedes Mal, wenn Sie einen neuen Header erstellen). Stattdessen können Sie einfach Abhängigkeiten zu relevanten Git-Dateien in der Formel hinzufügen $(MyLibs)/version.c : .git/COMMIT_EDITMSG .git/HEAD . Die Datei COMMIT_EDITMSGändert sich jedes Mal, wenn Sie einen Commit durchführen, und HEADjedes Mal, wenn Sie den Verlauf durchsuchen. Daher wird Ihre Datei jedes Mal aktualisiert, wenn dies relevant ist.
Kamil S Jaron
9

Ihr Programm kann git describeentweder zur Laufzeit oder als Teil des Erstellungsprozesses ausgeführt werden.

bdonlan
quelle
4
Von git help describe: "Das neueste Tag anzeigen, das über ein Commit erreichbar ist" - das ist nicht das, wonach die Frage fragt. Ich stimme jedoch dem Rest Ihrer Antwort zu. Um korrekt zu sein, sollte der Befehl lauten git rev-parse HEAD.
Mike Mazur
5
@mikem git describewird in den meisten anderen Projekten verwendet, da es auch lesbare Tag-Informationen enthält. Wenn Sie sich nicht genau in einem Tag befinden, werden die Anzahl der Commits seit dem nächsten Tag und der abgekürzte Revisions-Hash angehängt.
Bdonlan
7

Sie können zwei Dinge tun:

  • Sie können Git dazu bringen, einige Versionsinformationen für Sie in die Datei einzubetten.

    Der einfachere Weg ist die Verwendung von ident Attributen , dh Putten (zum Beispiel).

    *.yaml    ident

    in der .gitattributesDatei und $Id$an der entsprechenden Stelle. Es wird automatisch auf die SHA-1-Kennung des Dateiinhalts (Blob-ID) erweitert: Dies ist NICHT die Dateiversion oder das letzte Commit.

    Git unterstützt auf diese Weise das Schlüsselwort $ Id $, um zu vermeiden, dass Dateien berührt werden, die beim Wechseln der Verzweigung, Zurückspulen der Verzweigung usw. nicht geändert wurden filterAttribut: Verwenden Sie den Clean / Smudge-Filter, um ein Schlüsselwort (z. B. $ Revision $) beim Auschecken zu erweitern und es für das Festschreiben zu bereinigen.

  • Sie können einen Build-Prozess erstellen, der dies für Sie erledigt, wie dies der Linux-Kernel oder Git selbst tun.

    Sehen Sie sich das GIT-VERSION-GEN- Skript und seine Verwendung in Git Makefile an oder beispielsweise, wie dieses Makefile Versionsinformationen während der Generierung / Konfiguration von gitweb/gitweb.cgiDateien einbettet .

    GIT-VERSION-GEN verwendet git description, um eine Versionsbeschreibung zu generieren. Es muss besser funktionieren, wenn Sie Releases / Meilensteine ​​Ihres Projekts (mit signierten / kommentierten Tags) markieren.

Jakub Narębski
quelle
4

Wenn ich das tun muss, benutze ich ein Tag wie RELEASE_1_23. Ich kann entscheiden, was das Tag sein kann, ohne den SHA-1 zu kennen. Ich verpflichte mich dann tag. Sie können dieses Tag in Ihrem Programm speichern, wie Sie möchten.

brian d foy
quelle
4

Basierend auf der Antwort von njd27 verwende ich eine Version mit Abhängigkeitsscan in Kombination mit einer version.h-Datei mit Standardwerten, wenn der Code auf andere Weise erstellt wird. Alle Dateien, die version.h enthalten, werden neu erstellt.

Es enthält auch das Überarbeitungsdatum als separate Definition.

# Get git commit version and date
GIT_VERSION := $(shell git --no-pager describe --tags --always --dirty)
GIT_DATE := $(firstword $(shell git --no-pager show --date=short --format="%ad" --name-only))

# recompile version.h dependants when GIT_VERSION changes, uses temporary file version~
.PHONY: force
version~: force
    @echo '$(GIT_VERSION) $(GIT_DATE)' | cmp -s - $@ || echo '$(GIT_VERSION) $(GIT_DATE)' > $@
version.h: version~
    @touch $@
    @echo Git version $(GIT_VERSION) $(GIT_DATE)
PTT
quelle
1
Ich nehme an, Sie haben GIT_VERSION und GIT_DATE über CFLAGS übergeben, damit version.h sie verwenden kann. Cool!
Jesse Chisholm
2

Ich benutze git auch, um Änderungen in meinem wissenschaftlichen Code zu verfolgen. Ich wollte kein externes Programm verwenden, da es die Portabilität des Codes einschränkt (wenn jemand beispielsweise Änderungen an MSVS vornehmen möchte).

Meine Lösung bestand darin, nur den Hauptzweig für die Berechnungen zu verwenden und die Erstellungszeit mithilfe von Präprozessor-Makros __DATE__und auszugeben __TIME__. Auf diese Weise kann ich es mit Git Log überprüfen und sehen, welche Version ich verwende. Ref: http://gcc.gnu.org/onlinedocs/cpp/Standard-Predefined-Macros.html

Eine andere elegante Möglichkeit, das Problem zu lösen, besteht darin, das Git-Protokoll in die ausführbare Datei aufzunehmen. Erstellen Sie eine Objektdatei aus dem Git-Protokoll und fügen Sie sie in den Code ein. Dieses Mal ist das einzige externe Programm, das Sie verwenden, objcopy, aber es gibt weniger Codierung. ref: http://www.linuxjournal.com/content/embedding-file-executable-aka-hello-world-version-5967 und Daten in ein C ++ - Programm einbetten

kirill_igum
quelle
1
Die Verwendung von Präprozessor-Makros ist sehr clever! Danke dir.
AndyL
4
Wenn ich jedoch eine ältere Version auschecke und dann kompiliere, führt mich dies zum falschen Commit.
Sebastian Mach
2

Was Sie tun müssen, ist eine Header-Datei zu generieren (z. B. mit Echo aus der cmd-Zeile).

#define GIT_HASH \
"098709a0b098c098d0e"

Verwenden Sie Folgendes, um es zu generieren:

echo #define GIT_HASH \ > file.h
echo " > file.h
echo git status <whatever cmd to get the hash> > file.h
echo " > file.h

Möglicherweise müssen Sie ein wenig mit den Anführungszeichen und Backslashes spielen, damit sie kompiliert werden können, aber Sie haben die Idee.

Igor Zevaka
quelle
Ich frage mich nur, würde sich nicht jedes Mal, wenn er das tut und deshalb file.h ändert und dann die Änderungen an der Quelle festschreibt, der Git-Hash ändern?
Jorge Israel Peña
@Blaenk .. das habe ich auch gedacht. Aber bdonlans Idee, das Programm zur Laufzeit abfragen zu lassen, scheint dieses Problem zu umgehen.
AndyL
6
Nun, diese Datei müsste sich unter .gitignore befinden und jedes Mal generiert werden, wenn Sie das Projekt erstellen.
Igor Zevaka
Alternativ können Sie eine Basisversion dieser Datei --assume-unchangedgit update-index --assume-unchanged
einfügen
1

Noch eine Variation basierend auf Makefile und Shell

GIT_COMMIT_FILE=git_commit_filename.h

$(GIT_COMMIT_FILE): phony
    $(eval GIT_COMMIT_SHA=$(shell git describe --abbrev=6 --always 2>/dev/null || echo 'Error'))
    @echo SHA=$(GIT_COMMIT_SHA)
    echo -n "static const char *GIT_COMMIT_SHA = \"$(GIT_COMMIT_SHA)\";" > $(GIT_COMMIT_FILE)

Die Datei git_commit_filename.h enthält eine einzelne Zeile mit dem statischen const char * GIT_COMMIT_SHA = "";

Von https://gist.github.com/larytet/898ec8814dd6b3ceee65532a9916d406

Larytet
quelle
1

Dies ist eine Lösung für CMake-Projekte, die unter Windows und Linux funktioniert, ohne dass andere Programme (z. B. Skriptsprachen) installiert werden müssen.

Der Git-Hash wird von einem Skript in eine .h-Datei geschrieben. Dies ist ein Bash-Skript beim Kompilieren unter Linux oder ein Windows-Batch-Skript beim Kompilieren unter Windows. Eine if-Klausel in CMakeLists.txt wählt das Skript aus, das der Plattform entspricht Code wird am kompiliert.

Die folgenden 2 Skripte werden im selben Verzeichnis wie CMakeLists.txt gespeichert:

get_git_hash.sh:

#!/bin/bash
hash=$(git describe --dirty --always --tags)
echo "#ifndef GITHASH_H" > include/my_project/githash.h
echo "#define GITHASH_H" >> include/my_project/githash.h
echo "const std::string kGitHash = \"$hash\";" >> include/my_project/githash.h
echo "#endif // GITHASH_H" >> include/my_project/githash.h

get_git_hash.cmd:

@echo off
FOR /F "tokens=* USEBACKQ" %%F IN (`git describe --dirty --always --tags`) DO (
SET var=%%F
)
ECHO #ifndef GITHASH_H > include/my_project/githash.h
ECHO #define GITHASH_H >> include/my_project/githash.h
ECHO const std::string kGitHash = "%var%"; >> include/my_project/githash.h
ECHO #endif // GITHASH_H >> include/my_project/githash.h

In CMakeLists.txt werden die folgenden Zeilen hinzugefügt

if(WIN32)
  add_custom_target(
    run ALL
    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
    COMMAND get_git_hash.cmd
  )
else()
  add_custom_target(
    run ALL
    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
    COMMAND ./get_git_hash.sh
  )
endif()

include_directories(include)

In dem Code ist die generierte Datei enthalten, #include <my_project/githash.h>und der Git-Hash kann auf std::cout << "Software version: " << kGitHash << std::endl;ähnliche Weise auf das Terminal gedruckt oder in eine Yaml-Datei (oder eine beliebige Datei) geschrieben werden.

Adrian
quelle
0

Sie können sehen, wie ich es für memcached im ursprünglichen Commit gemacht habe .

Markieren Sie im Grunde gelegentlich und stellen Sie sicher, dass das, was Sie liefern, von make distoder ähnlichem stammt.

Dustin
quelle