¿Cómo puedo hacer que mi código C imprima automáticamente el hash de la versión de Git?

84

¿Existe una manera fácil de escribir código C que pueda acceder a su hash de versión Git?

Escribí software en C para recopilar datos científicos en un entorno de laboratorio. Mi código registra los datos que recopila en un archivo .yaml para su posterior análisis. Mis experimentos cambian día a día y, a menudo, tengo que modificar el código. Para realizar un seguimiento de las revisiones, uso un repositorio de git.

Me gustaría poder incluir el hash de revisión de Git como comentario en mis archivos de datos .yaml. De esa manera, podría mirar el archivo .yaml y saber exactamente qué código se usó para generar los datos que se muestran en ese archivo. ¿Existe una manera fácil de hacer esto automáticamente?

AndyL
fuente
1
El uso de ganchos de confirmación previa (consulte book.git-scm.com/5_git_hooks.html ) sería otra forma de hacerlo.
Yktula

Respuestas:

39

En mi programa, tengo el número de versión de git y la fecha de la compilación en un archivo separado, llamado version.c, que se ve así:

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

También hay un archivo de encabezado, que se ve así:

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

Tanto el archivo de encabezado como el archivo C son generados por un script Perl que se ve así:

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

Aquí hash_to_c_filehace todo el trabajo de crear version.cy version.hy make_date_timehace que una cadena como se muestra.

En el programa principal, tengo una rutina

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

No tengo tanto conocimiento sobre git, por lo que agradecería los comentarios si hay una mejor manera de hacer esto.

Stefan Majewsky
fuente
1
El script de Perl es parte del script de compilación, que es una "compilación de un paso" para todo.
12
Esto es bueno hasta donde llega, pero tenga en cuenta que informará el hash de la última confirmación en la rama, no el hash del código que se está compilando. Si hay cambios no confirmados, esos no serán evidentes.
Phil Miller
1
git diff por defecto comprueba las diferencias entre su espacio de trabajo y el índice. Es posible que también desee probar git diff --cached para las diferencias entre el índice y HEAD
Karl
6
Todos esos 'const char * name = "value";' las construcciones podrían cambiarse sensiblemente a 'const char name [] = "value";', que ahorra 4 bytes por elemento en una máquina de 32 bits y 8 bytes por elemento en una máquina de 64 bits. Por supuesto, en estos días de GB de memoria principal, eso no es un gran problema, pero todo ayuda. Tenga en cuenta que no es necesario cambiar ninguno de los códigos que utilizan los nombres.
Jonathan Leffler
1
Los cambié como sugieres. El tamaño de mi programa con const char []: 319356 bytes (despojados). El tamaño de mi programa con const char *: 319324 bytes (despojado). Entonces, su idea no parece ahorrar bytes, pero aumenta el número total en 32. No tengo idea de por qué. En la "version.c" original hay tres cadenas, pero una se omitió en la respuesta anterior. Si miras la primera edición, todavía está ahí.
163

Si está utilizando una compilación basada en make, puede poner esto en el Makefile:

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

(Ver man git describe lo que hacen los interruptores)

luego agregue esto a sus CFLAGS:

-DVERSION=\"$(GIT_VERSION)\"

Luego, puede hacer referencia a la versión directamente en el programa como si fuera una #define:

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

De forma predeterminada, esto solo imprime un ID de confirmación de git abreviado, pero opcionalmente puede etiquetar lanzamientos particulares con algo como:

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

luego se imprimirá:

Version: v1.1-2-g766d

lo que significa, 2 confirmaciones pasadas v1.1, con un ID de confirmación de git que comienza con "766d".

Si hay cambios no confirmados en su árbol, agregará "-dirty".

No hay escaneo de dependencias, por lo que debe hacer un análisis explícito make cleanpara forzar la actualización de la versión. Sin embargo, esto se puede solucionar .

Las ventajas son que es simple y no requiere ninguna dependencia de compilación adicional como perl o awk. He usado este enfoque con GNU automake y con compilaciones de Android NDK.

ndyer
fuente
6
+1 Personalmente, prefiero que el archivo MAKE genere un archivo de encabezado que contenga en #define GIT_VERSION ...lugar de ponerlo en la línea de comando con la -Dopción; elimina el problema de la dependencia. Además, ¿por qué el doble subrayado? Técnicamente, es un identificador reservado.
Dan Moulding
8
Cada uno por su cuenta, como digo, las ventajas son que tiene pocas partes móviles y son comprensibles. Lo he editado para eliminar los guiones bajos.
ndyer
Debe agregarse que si usa gengetopt, uno puede agregar esto directamente al gengetopt en el Makefile: gengetopt --set-version = $ (GIT_VERSION)
Trygve
1
La primera declaración debe estar entre comillas GIT_VERSION := "$(shell git describe --abbrev=4 --dirty --always --tags)", no funciona sin comillas.
Abel Tom
11

Terminé usando algo muy similar a la respuesta de @ Kinopiko, pero usé awk en lugar de perl. Esto es útil si está atascado en máquinas con Windows que tienen awk instalado por naturaleza de mingw, pero no perl. Así es como funciona.

Mi archivo MAKE tiene una línea que invoca git, date y awk para crear un archivo 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 

Cada vez que compilo mi código, el comando awk genera un archivo version.c que se ve así:

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

Tengo un archivo version.h estático que se ve así:

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

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


#endif /* VERSION_H_ */

El resto de mi código ahora puede acceder al tiempo de compilación y al hash de git simplemente incluyendo el encabezado version.h. Para terminar, le digo a git que ignore version.c agregando una línea a mi archivo .gitignore. De esta manera, git no me genera conflictos de combinación constantemente. ¡Espero que esto ayude!

AndyL
fuente
Un apéndice ... esto funcionará en Matlab: mathworks.com/matlabcentral/fileexchange/32864-get-git-info
AndyL
1
No creo que FORCEsea ​​una buena idea ya que makefile nunca quedará satisfecho (cada vez que hagas que hagas un nuevo encabezado). En su lugar, puede agregar dependencia a los archivos git relevantes en la fórmula $(MyLibs)/version.c : .git/COMMIT_EDITMSG .git/HEAD . El archivo COMMIT_EDITMSGcambia cada vez que realiza una confirmación y HEADcambia cada vez que explora el historial, por lo tanto, su archivo se actualiza cada vez que es relevante.
Kamil S Jaron
9

Su programa puede pagar git describe, ya sea en tiempo de ejecución o como parte del proceso de compilación.

bdonlan
fuente
4
De git help describe: "Mostrar la etiqueta más reciente a la que se puede acceder desde una confirmación": esto no es lo que pide la pregunta. Sin embargo, estoy de acuerdo con el resto de tu respuesta. Para ser correcto, el comando debería ser git rev-parse HEAD.
Mike Mazur
5
@mikem, git describees lo que utilizan la mayoría de los otros proyectos, porque también incluye información de etiquetas legible por humanos. Si no está exactamente en una etiqueta, se agrega al número de confirmaciones desde la etiqueta más cercana y el hash de revisión abreviado.
bdonlan
7

Hay dos cosas que puede hacer:

  • Puede hacer que Git incruste información de la versión en el archivo por usted.

    La forma más sencilla es usar ident atributo , que significa poner (por ejemplo)

    *.yaml    ident
    

    en .gitattributesarchivo y $Id$en el lugar apropiado. Se expandirá automáticamente al identificador SHA-1 del contenido del archivo (id de blob): esta NO es la versión del archivo ni la última confirmación.

    Git admite la palabra clave $ Id $ de esta manera para evitar tocar archivos que no se cambiaron durante el cambio de rama, rebobinar la rama, etc. filteratributo, usando el filtro limpio / manchado para expandir alguna palabra clave (por ejemplo, $ Revision $) al finalizar la compra, y limpiarla para confirmarla.

  • Puede hacer que el proceso de compilación lo haga por usted, como lo hace el kernel de Linux o el propio Git.

    Eche un vistazo al script GIT-VERSION-GEN y su uso en Git Makefile , o por ejemplo, cómo este Makefile incrusta información de versión durante la generación / configuración del gitweb/gitweb.cgiarchivo.

    GIT-VERSION-GEN usa git describe para generar la descripción de la versión. Debe funcionar mejor que etiquete (utilizando etiquetas firmadas / anotadas) lanzamientos / hitos de su proyecto.

Jakub Narębski
fuente
4

Cuando necesito hacer esto, uso una etiqueta , como RELEASE_1_23. Puedo decidir cuál puede ser la etiqueta sin conocer el SHA-1. Me comprometo y luego etiqueto. Puede almacenar esa etiqueta en su programa de la forma que desee.

Brian D Foy
fuente
4

Según la respuesta de njd27, estoy usando una versión con escaneo de dependencias, en combinación con un archivo version.h con valores predeterminados para cuando el código se compila de una manera diferente. Se reconstruirán todos los archivos que incluyan version.h.

También incluye la fecha de revisión como una definición separada.

# 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
fuente
1
Supongo que ha pasado GIT_VERSION y GIT_DATE a través de CFLAGS para que version.h pueda usarlos. ¡Frio!
Jesse Chisholm
2

También uso git para rastrear cambios en mi código científico. No quería usar un programa externo porque limita la portabilidad del código (si alguien quisiera hacer cambios en MSVS, por ejemplo).

mi solución fue usar solo la rama principal para los cálculos y hacer que genere el tiempo de compilación usando macros de preprocesador __DATE__y __TIME__. de esa manera puedo verificarlo con git log y ver qué versión estoy usando. ref: http://gcc.gnu.org/onlinedocs/cpp/Standard-Predefined-Macros.html

otra forma elegante de resolver el problema es incluir git log en el ejecutable. crea un archivo objeto de git log e inclúyelo en el código. esta vez el único programa externo que usa es objcopy pero hay menos codificación. ref: http://www.linuxjournal.com/content/embedding-file-executable-aka-hello-world-version-5967 e incrustar datos en un programa C ++

kirill_igum
fuente
1
¡El uso de macros de preprocesador es muy inteligente! Gracias.
AndyL
4
pero si compruebo una versión anterior, luego la compilo, me guiará a la confirmación incorrecta.
Sebastian Mach
2

Lo que debe hacer es generar un archivo de encabezado (por ejemplo, usando eco de la línea cmd) algo como esto:

#define GIT_HASH \
"098709a0b098c098d0e"

Para generarlo usa algo como esto:

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

Es posible que necesite jugar un poco con las comillas y las barras diagonales inversas para que se compile, pero entiendes la idea.

Igor Zevaka
fuente
Solo me pregunto, ¿no cambiaría el hash de git cada vez que hace eso y, por lo tanto, cambia file.h, y luego confirma los cambios en la fuente?
Jorge Israel Peña
@ Blaenk ... eso es lo que yo también estaba pensando. Pero la idea de bdonlan de que el programa pregunte en tiempo de ejecución parece solucionar este problema.
AndyL
6
Bueno, este archivo tendría que estar bajo .gitignore y generarse cada vez que construya el proyecto.
Igor Zevaka
Alternativamente, puede incluir una versión básica de este archivo y establecer una --assume-unchangedgit update-index --assume-unchanged
marca
1

Otra variación más basada en Makefile y 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)

El archivo git_commit_filename.h terminará con una sola línea que contiene el carácter estático const * GIT_COMMIT_SHA = "";

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

Larytet
fuente
1

Esta es una solución para el proyecto CMake que funciona para Windows y Linux, sin la necesidad de instalar ningún otro programa (por ejemplo, lenguajes de script).

El hash de git se escribe en un archivo .h mediante un script, que es un script bash cuando se compila en Linux o un script por lotes de Windows cuando se compila en Windows, y una cláusula if en CMakeLists.txt selecciona el script correspondiente a la plataforma se compila el código.

Los siguientes 2 scripts se guardan en el mismo directorio que 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

En CMakeLists.txt se agregan las siguientes líneas

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)

En el código, se incluye el archivo generado #include <my_project/githash.h>y el hash de git puede imprimirse en la terminal std::cout << "Software version: " << kGitHash << std::endl;o escribirse en un archivo yaml (o cualquier otro) de manera similar.

Adrian
fuente
0

Puedes ver cómo lo hice para memcached en la confirmación original .

Básicamente, etiquete ocasionalmente y asegúrese de que lo que entrega provenga de make disto similar.

Dustin
fuente