Jak mogę sprawić, by mój kod C automatycznie drukował skrót wersji Git?

84

Czy istnieje łatwy sposób na napisanie kodu C, który może uzyskać dostęp do skrótu jego wersji Git?

Napisałem oprogramowanie w C do zbierania danych naukowych w warunkach laboratoryjnych. Mój kod zapisuje zebrane dane w pliku .yaml do późniejszej analizy. Moje eksperymenty zmieniają się z dnia na dzień i często muszę modyfikować kod. Aby śledzić zmiany, używam repozytorium git.

Chciałbym móc dołączyć skrót wersji Git jako komentarz w moich plikach danych .yaml. W ten sposób mogłem spojrzeć na plik .yaml i dokładnie wiedzieć, jaki kod został użyty do wygenerowania danych pokazanych w tym pliku. Czy istnieje łatwy sposób na zrobienie tego automatycznie?

AndyL
źródło
1
Używanie haków przed zatwierdzeniem (patrz book.git-scm.com/5_git_hooks.html ) byłoby innym sposobem na zrobienie tego.
Yktula

Odpowiedzi:

39

W swoim programie numer wersji git i datę kompilacji trzymam w osobnym pliku o nazwie version.c, który wygląda tak:

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

Istnieje również plik nagłówkowy, który wygląda następująco:

#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 */

Zarówno plik nagłówkowy, jak i plik C są generowane przez skrypt Perla, który wygląda następująco:

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_");

Tutaj hash_to_c_filewykonuje całą pracę tworzenia version.ci version.hi make_date_timesprawia, że ciąg, jak pokazano.

W programie głównym mam rutynę

#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);
}

Nie mam zbyt dużej wiedzy na temat git, więc mile widziane komentarze, jeśli istnieje lepszy sposób na zrobienie tego.

Stefan Majewsky
źródło
1
Skrypt Perl jest częścią skryptu budującego, który jest „jednoetapowym budowaniem” wszystkiego.
12
Jest to dobre na tyle, na ile jest to możliwe, ale pamiętaj, że zgłosi skrót ostatniego zatwierdzenia w gałęzi, a nie skrót kompilowanego kodu. Jeśli pojawią się niezatwierdzone zmiany, nie będą one widoczne.
Phil Miller
1
git diff domyślnie sprawdza różnice między obszarem roboczym a indeksem. Możesz także wypróbować git diff --cached, aby sprawdzić różnice między indeksem a HEAD
Karl
6
Wszystkie te 'const char * name = "wartość";' konstrukcje można rozsądnie zmienić na 'const char name [] = "value";', co oszczędza 4 bajty na element na komputerze 32-bitowym i 8 bajtów na element na komputerze 64-bitowym. To prawda, w dzisiejszych czasach GB pamięci głównej nie jest to duży problem, ale wszystko pomaga. Zauważ, że żaden kod używający nazw nie musi się zmieniać.
Jonathan Leffler
1
Zmieniłem je tak, jak sugerujesz. Rozmiar mojego programu z const char []: 319356 bajtów (pozbawiony). Rozmiar mojego programu z const char *: 319324 bajtów (pozbawiony). Więc twój pomysł nie wydaje się zapisywać żadnych bajtów, ale zwiększa całkowitą liczbę o 32. Nie mam pojęcia dlaczego. W oryginalnym pliku „version.c” występują trzy ciągi, ale jeden został pominięty w powyższej odpowiedzi. Jeśli spojrzysz na pierwszą edycję, nadal tam jest.
163

Jeśli używasz kompilacji opartej na marce, możesz umieścić to w pliku Makefile:

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

(Zobacz man git, opis, co robią przełączniki)

następnie dodaj to do swoich CFLAGS:

-DVERSION=\"$(GIT_VERSION)\"

Następnie możesz po prostu odwołać się do wersji bezpośrednio w programie, tak jakby była to #define:

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

Domyślnie to po prostu wyświetla skrócony identyfikator zatwierdzenia git, ale opcjonalnie możesz oznaczyć poszczególne wydania czymś takim jak:

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

wtedy wydrukuje:

Version: v1.1-2-g766d

co oznacza, że ​​2 zatwierdza się po wersji 1.1, a identyfikator zatwierdzenia git zaczyna się od „766d”.

Jeśli w twoim drzewie są niezatwierdzone zmiany, doda "-dirty".

Nie ma skanowania zależności, więc musisz zrobić jawnie, make cleanaby wymusić aktualizację wersji. To może być rozwiązany jednak.

Zaletą jest to, że jest prosty i nie wymaga żadnych dodatkowych zależności budowania, takich jak perl czy awk. Użyłem tego podejścia w GNU automake i kompilacjach Android NDK.

ndyer
źródło
6
+1 Osobiście wolę, aby makefile generował plik nagłówkowy, który zawiera, #define GIT_VERSION ...zamiast umieszczać go w linii poleceń z -Dopcją; eliminuje problem zależności. Poza tym, dlaczego podwójne podkreślenie? Technicznie jest to zarezerwowany identyfikator.
Dan Molding
8
Każdy do siebie - jak mówię zalety są takie, że ma niewiele ruchomych części i są zrozumiałe. Zmodyfikowałem go, aby usunąć podkreślenia.
ndyer
Należy dodać, że jeśli używasz gengetopt, możesz dodać to bezpośrednio do gengetopt w pliku Makefile: gengetopt --set-version = $ (GIT_VERSION)
Trygve
1
Pierwsze zdanie powinno być w cudzysłowie GIT_VERSION := "$(shell git describe --abbrev=4 --dirty --always --tags)", nie działa bez cudzysłowów.
Abel Tom
11

Skończyło się na tym, że użyłem czegoś bardzo podobnego do odpowiedzi @ Kinopiko, ale użyłem awk zamiast perl. Jest to przydatne, jeśli utknąłeś na komputerach z systemem Windows, na których zainstalowano awk z natury mingw, ale nie perl. Oto jak to działa.

Mój plik makefile zawiera wiersz, który wywołuje git, date i awk w celu utworzenia pliku ac:

$(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 

Za każdym razem, gdy kompiluję swój kod, polecenie awk generuje plik version.c, który wygląda następująco:

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

Mam statyczny plik version.h, który wygląda następująco:

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

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


#endif /* VERSION_H_ */

Reszta mojego kodu może teraz uzyskać dostęp do czasu kompilacji i skrótu git, po prostu dołączając nagłówek version.h. Aby zakończyć to wszystko, mówię gitowi, aby zignorował version.c, dodając linię do mojego pliku .gitignore. W ten sposób git nie daje mi ciągle konfliktów scalania. Mam nadzieję że to pomoże!

AndyL
źródło
Dodatek ... to zadziała w Matlabie: mathworks.com/matlabcentral/fileexchange/32864-get-git-info
AndyL
1
Nie sądzę, żeby FORCEto był dobry pomysł, ponieważ makefile nigdy nie będzie satysfakcjonujący (za każdym razem, gdy robisz nowy nagłówek). Zamiast tego możesz po prostu dodać zależności do odpowiednich plików git w formule $(MyLibs)/version.c : .git/COMMIT_EDITMSG .git/HEAD . Plik COMMIT_EDITMSGzmienia się za każdym razem, gdy wykonujesz zatwierdzenie i HEADzmienia się za każdym razem, gdy przeglądasz historię, dlatego plik jest aktualizowany za każdym razem, gdy ma znaczenie.
Kamil S Jaron
9

Twój program może zostać wyrzucony git describew czasie wykonywania lub jako część procesu kompilacji.

bdonlan
źródło
4
Od git help describe: „Pokaż najnowszy tag, do którego można dotrzeć z zatwierdzenia” - nie o to chodzi w pytaniu. Zgadzam się jednak z resztą twojej odpowiedzi. Aby było poprawne, polecenie powinno być git rev-parse HEAD.
Mike Mazur
5
@mikem git describejest tym , czego używa większość innych projektów, ponieważ zawiera również informacje o tagach czytelne dla człowieka. Jeśli nie znajdujesz się dokładnie na tagu, dodaje on liczbę zatwierdzeń od najbliższego tagu i skrócony hash rewizji.
bdonlan
7

Możesz zrobić dwie rzeczy:

  • Możesz zmusić Git do osadzenia niektórych informacji o wersji w pliku.

    Prostszym sposobem jest użycie ident atrybutu , co oznacza umieszczenie (na przykład)

    *.yaml    ident
    

    w .gitattributespliku iw $Id$odpowiednim miejscu. Zostałby automatycznie rozszerzony do identyfikatora SHA-1 zawartości pliku (identyfikator obiektu blob): NIE jest to wersja pliku ani ostatnie zatwierdzenie.

    Git obsługuje słowo kluczowe $ Id $ w ten sposób, aby uniknąć dotykania plików, które nie zostały zmienione podczas przełączania gałęzi, przewijania gałęzi itp. Jeśli naprawdę chcesz, aby Git umieścił identyfikator zatwierdzenia (wersji) lub opis w pliku, możesz (ab) użyć filteratrybut, używając filtra clean / smudge, aby rozwinąć niektóre słowa kluczowe (np. $ Revision $) przy pobieraniu i wyczyścić je przed zatwierdzeniem.

  • Można uczynić proces kompilacji to zrobić dla Ciebie, jak Linux kernel lub Git sam robi.

    Przyjrzyj się skryptowi GIT-VERSION-GEN i jego zastosowaniu w Git Makefile , lub na przykład, jak ten gitweb/gitweb.cgiplik Makefile osadza informacje o wersji podczas generowania / konfiguracji pliku.

    GIT-VERSION-GEN używa git opis do generowania opisu wersji. Powinno działać lepiej, aby oznaczać (używając podpisanych / opatrzonych adnotacjami znaczników) wydania / kamienie milowe projektu.

Jakub Narębski
źródło
4

Kiedy muszę to zrobić, używam tagu , na przykład RELEASE_1_23. Mogę zdecydować, jaki może być tag, nie znając SHA-1. Zatwierdzam, a następnie oznaczam. Możesz przechowywać ten tag w swoim programie tak, jak chcesz.

brian d foy
źródło
4

Opierając się na odpowiedzi udzielonej przez njd27, używam wersji ze skanowaniem zależności, w połączeniu z plikiem version.h z domyślnymi wartościami dla sytuacji, gdy kod jest budowany w inny sposób. Wszystkie pliki zawierające version.h zostaną odbudowane.

Zawiera również datę rewizji jako osobną definicję.

# 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
źródło
1
Zakładam, że masz GIT_VERSION i GIT_DATE przekazane przez CFLAGS, więc version.h może ich używać. Fajne!
Jesse Chisholm
2

Używam również git do śledzenia zmian w moim kodzie naukowym. nie chciałem używać zewnętrznego programu, ponieważ ogranicza to przenośność kodu (jeśli ktoś chciałby wprowadzić zmiany na przykład na MSVS).

moim rozwiązaniem było użycie tylko głównej gałęzi do obliczeń i sprawienie, by wyświetlał czas kompilacji za pomocą makr preprocesora __DATE__i __TIME__. w ten sposób mogę to sprawdzić za pomocą dziennika git i zobaczyć, której wersji używam. ref: http://gcc.gnu.org/onlinedocs/cpp/Standard-Predefined-Macros.html

innym eleganckim sposobem rozwiązania problemu jest dołączenie dziennika git do pliku wykonywalnego. zrobić plik obiektowy z dziennika git i dołączyć go do kodu. tym razem jedynym zewnętrznym programem jakiego używasz to objcopy, ale jest mniej kodowania. ref: http://www.linuxjournal.com/content/embedding-file-executable-aka-hello-world-version-5967 i Osadź dane w programie C ++

kirill_igum
źródło
1
Użycie makr preprocesora jest bardzo sprytne! Dziękuję Ci.
AndyL
4
ale jeśli pobiorę starszą wersję, a następnie ją skompiluję, poprowadzi mnie to do niewłaściwego zatwierdzenia.
Sebastian Mach
2

To, co musisz zrobić, to wygenerować plik nagłówkowy (np. Używając echo z linii cmd), coś takiego:

#define GIT_HASH \
"098709a0b098c098d0e"

Aby go wygenerować, użyj czegoś takiego:

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

Być może trzeba będzie trochę pobawić się cudzysłowami i odwrotnymi ukośnikami, aby go skompilować, ale masz pomysł.

Igor Zevaka
źródło
Zastanawiałem się tylko, czy za każdym razem, gdy to robi, a zatem zmienia plik.h, a następnie zatwierdza zmiany w źródle, zmieniłby się skrót git?
Jorge Israel Peña
@Blaenk .. też o tym myślałem. Ale pomysł bdonlana, by program pytał w czasie wykonywania, wydaje się ominąć ten problem.
AndyL
6
Cóż, ten plik musiałby być pod .gitignore i generowany za każdym razem, gdy budujesz projekt.
Igor Zevaka
Alternatywnie możesz dołączyć podstawową wersję tego pliku i ustawić --assume-unchanged na niej flagę ( git update-index --assume-unchanged)
Igor Zevaka
2

Jest to rozwiązanie dla projektu CMake, które działa w systemach Windows i Linux, bez konieczności instalowania jakichkolwiek innych programów (np. Języków skryptowych).

Skrót git jest zapisywany do pliku .h przez skrypt, który jest skryptem bash podczas kompilacji w systemie Linux lub skryptem wsadowym systemu Windows podczas kompilacji w systemie Windows, a klauzula if w CMakeLists.txt wybiera skrypt odpowiadający platformie kod jest kompilowany.

Następujące 2 skrypty są zapisywane w tym samym katalogu co CMakeLists.txt:

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

W CMakeLists.txt dodaje się następujące wiersze

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)

W kodzie zawarty jest wygenerowany plik, #include <my_project/githash.h>a skrót git może być wydrukowany na terminalu za pomocą std::cout << "Software version: " << kGitHash << std::endl;lub zapisany w pliku yaml (lub dowolnym) w podobny sposób.

Adrian
źródło
1

Kolejna odmiana oparta na Makefile i powłoce

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)

Plik git_commit_filename.h kończy się pojedynczą linią zawierającą statyczną stałą char * GIT_COMMIT_SHA = "";

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

Larytet
źródło
0

Możesz zobaczyć, jak zrobiłem to dla memcached w oryginalnym zatwierdzeniu .

Zasadniczo oznaczaj od czasu do czasu tagi i upewnij się, że dostarczana przesyłka pochodzi make distlub jest podobna.

Dustin
źródło