如何让我的C代码自动打印出它的Git版本散列?

Is there an easy way to write C code that can access its Git version hash?

I wrote software in C to collect scientific data in a laboratory setting. My code records the data it collects in a .yaml file for later analysis. My experiments change from day-to-day and I often have to modify the code. To keep track of revisions, I use a git repository.

I would like to be able to include the Git revision hash as a comment in my .yaml data files. That way, I could look at the .yaml file and know exactly what code was used to generate the data shown in that file. Is there an easy way to do this automatically?

#0

If you are using a make-based build, you can put this in the Makefile:

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

(See man git describe for what the switches do)

then add this to your CFLAGS:

-DVERSION=\"$(GIT_VERSION)\"

Then you can just reference the version directly in the program as though it was a #define:

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

By default this just prints an abbreviated git commit id, but optionally you can tag particular releases with something like:

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

then it will print out:

Version: v1.1-2-g766d

which means, 2 commits past v1.1, with a git commit id beginning with "766d".

If there are uncommitted changes in your tree, it will append "-dirty".

There is no dependency scanning so you have to do an explicit make clean to force the version to be updated. This can be solved however.

The advantages are that it is simple and doesn't require any extra build dependencies like perl or awk. I have used this approach with GNU automake and with Android NDK builds.

#1

In my program, I hold the git version number and the date of the build in a separate file, called version.c, which looks like this:

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

There is also a header file, which looks like this:

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

Both the header file and the C file are generated by a Perl script which looks like this:

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

Here hash_to_c_file does all the work of creating version.c and version.h and make_date_time makes a string as shown.

In the main program, I have a 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);
}

I'm not that knowledgeable about git, so I'd welcome comments if there is a better way to do this.

#2

I ended up using something very similar to @Kinopiko's answer, but I used awk instead of perl. This is useful if your stuck on windows machines which have awk installed by nature of mingw, but not perl. Here's how it works.

My makefile has a line in it that invokes git, date, and awk to create a c file:

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

Everytime I compile my code, the awk command generates a version.c file that looks like this:

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

I have a static version.h file that looks like this:

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

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


#endif /* VERSION_H_ */

The rest of my code can now access the build-time and the git hash by simply including the version.h header. To wrap it all up, I tell git to ignore version.c by adding a line to my .gitignore file. This way git isn't constantly giving me merge conflicts. Hope this helps!

#3

Your program can shell out to git describe, either at runtime or as part of the build process.

#4

There are two things that you can do:

  • You can make Git to embed some version information in the file for you.

    The simpler way is to use ident attribute, which means putting (for example)

    *.yaml    ident
    

    in .gitattributes file, and $Id$ in the appropriate place. It would be automatically expanded to SHA-1 identifier of the contents of the file (blob id): this is NOT file version, or the last commit.

    Git does support $Id$ keyword in this way to avoid touching files which were not changed during branch switching, rewinding branch etc. If you really want Git to put commit (version) identifier or description in the file, you can (ab)use filter attribute, using clean/ smudge filter to expand some keyword (e.g. $Revision$) on checkout, and clean it up for commit.

  • You can make build process to do that for you, like Linux kernel or Git itself does.

    Take a look at GIT-VERSION-GEN script and its use in Git Makefile, or for example how this Makefile embeds version information during generation / configuration of gitweb/gitweb.cgi file.

    GIT-VERSION-GEN uses git describe to generate version description. It needs to work better that you tag (using signed / annotated tags) releases / milestones of your project.

#5

When I need to do this, I use a tag, like RELEASE_1_23. I can decide what the tag can be without knowing the SHA-1. I commit then tag. You can store that tag in your program anyway that you like.

#6

Based on the answer by njd27, I'm using the a version with dependency scanning, in combination with a version.h file with default values for when the code is built in a different way. All files that include version.h will be rebuilt.

It also includes the revision date as a separate define.

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

#7

I also use git to track changes in my scientific code. i didn't want to use an external program because it limits portability of the code (if someone would want to make changes on MSVS for example).

my solution was to use only the main branch for the calculations and make it output the build time using preprocessor macros __DATE__ and __TIME__. that way i can check it with git log and see which version i'm using. ref: http://gcc.gnu.org/onlinedocs/cpp/Standard-Predefined-Macros.html

another elegant way to solve the problem is to include git log into the executable. make an object file out of git log and include it into the code. this time the only external program you use is objcopy but there is less coding. ref: http://www.linuxjournal.com/content/embedding-file-executable-aka-hello-world-version-5967 and Embed data in a C++ program

#8

What you need to do is to generate a header file (eg using echo from cmd line) something like this:

#define GIT_HASH \
"098709a0b098c098d0e"

To generate it use something like this:

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

Might need to play with the quotes and backslashes a bit to get it to compile, but you get the idea.

#9

Yet another variation based on Makefile and 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)

File git_commit_filename.h will end up with a single line containing static const char *GIT_COMMIT_SHA="";

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

#10

This is a solution for CMake project that works for Windows and Linux, without the need for any other programs (e.g. script languages) to be installed.

The git hash is written to a .h file by a script, which is a bash script when compiling on Linux or a Windows batch script when compiling on Windows, and an if-clause in CMakeLists.txt picks the script corresponding to the platform the code is compile on.

The following 2 scripts are saved in the same directory as 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

In CMakeLists.txt the follwoing lines are added

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 the code the generated file is included with #include <my_project/githash.h> and the git hash can be printed to the terminal with std::cout << "Software version: " << kGitHash << std::endl;, or written to a yaml (or any) file in a similar fashion.

#11

You can see how I did it for memcached in the original commit.

Basically, tag occasionally, and make sure the thing you deliver comes from make dist or similar.

推荐文章

在字符串生成器中内联追加双引号时出现问题

在字符串生成器中内联追加双引号时出现问题

推荐文章

如何从YACC获取AST?

如何从YACC获取AST?

推荐文章

objective-c中的二进制到十进制

objective-c中的二进制到十进制

推荐文章

字符串解析到模型(RegEx model binder?)

字符串解析到模型(RegEx model binder?)

推荐文章

使用UStream API创建视频库

使用UStream API创建视频库

推荐文章

Windows Server 2003上未激发WPF contextmenu

Windows Server 2003上未激发WPF contextmenu

推荐文章

ZK:来自arrayList的Creatin列表框

ZK:来自arrayList的Creatin列表框

推荐文章

MongoDB setShardVersion失败,仅mongodump

MongoDB setShardVersion失败,仅mongodump

推荐文章

将值推送到数组的末尾

将值推送到数组的末尾

推荐文章

Android画布中的移位图

Android画布中的移位图

推荐文章

什么决定了NavigationCommands.BrowseBack是否调用页构造函数?

什么决定了NavigationCommands.BrowseBack是否调用页构造函数?

推荐文章

访问libltdl的模块引用计数

访问libltdl的模块引用计数

推荐文章

MYSQL计算关联的记录数

MYSQL计算关联的记录数

推荐文章

OpenGl ES iPhone,奇怪的线路

OpenGl ES iPhone,奇怪的线路

推荐文章

如何在Matlab中提取图像分割后的背景值?

如何在Matlab中提取图像分割后的背景值?

推荐文章

PyCUDA:C/C++包括什么?

PyCUDA:C/C++包括什么?