How can I get my C code to automatically print out its Git version hash?
Asked Answered
S

14

99

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?

Speos answered 10/11, 2009 at 0:30 Comment(1)
Using pre-commit hooks (see book.git-scm.com/5_git_hooks.html ) would be another way to go about doing this.Epaulet
M
43

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.

Mutilate answered 10/11, 2009 at 2:18 Comment(11)
As long as there is a way to have the Perl script automatically run or you can always remember to run it, this looks like a really good/sophisticated answer.Furlana
The Perl script is part of the build script, which is a "one step build" for everything.Mutilate
This is good as far as it goes, but do keep in mind that it will report the hash of the latest commit on the branch, not the hash of the code being compiled. If there are uncommitted changes, those will not be apparent.Huberty
I use git diff and check the result for empty output. Do you have a better suggestion?Mutilate
git diff by default checks for differences between your workspace and the index. You may also want to try git diff --cached for differences between the the index and HEADShcherbakov
All those 'const char *name = "value";' constructs could sensibly be changed to 'const char name[] = "value";', which saves 4 bytes per item on a 32-bit machine and 8 bytes per item on a 64-bit machine. Granted, in these days of GB of main memory, that's not a big problem, but it all helps. Note that none of the code using the names needs to change.Shebeen
I changed them as you suggest. The size of my program with const char []: 319356 bytes (stripped). The size of my program with const char *: 319324 bytes (stripped). So your idea doesn't seem to save any bytes, but increase the total number by 32. I have no idea why. In the original "version.c" there are three strings, but one was omitted from the above answer. If you look at the first edit it is still there.Mutilate
Oh sorry it isn't there actually. But anyway there are three const char * strings.Mutilate
I do something similar, but generate the version file in a .phony rule in my makefile.Cis
In my Makefile, I added version.c and version.h to .INTERMEDIATE, and generate the contents in the Makefile as well. This means make will generate the version files for building and remove them afterwards.Shimmy
No need to #include version.h in version.cHumph
I
185

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.

Intermission answered 10/11, 2009 at 0:31 Comment(4)
+1 Personally, I prefer to have the makefile generate a header file that contains #define GIT_VERSION ... instead of putting it on the command line with the -D option; it eliminates the dependency problem. Also, why the double underscore? Technically that's a reserved identifier.Bickart
Each to their own - as I say the advantages are that it has few moving parts and they are understandable. I've edited it to remove the underscores.Intermission
It should be added, that if you use gengetopt one can add this directly to the gengetopt in the Makefile: gengetopt --set-version=$(GIT_VERSION)Antidote
The first statement should be with quotes GIT_VERSION := "$(shell git describe --abbrev=4 --dirty --always --tags)", does not work without quotes.Meditation
M
43

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.

Mutilate answered 10/11, 2009 at 2:18 Comment(11)
As long as there is a way to have the Perl script automatically run or you can always remember to run it, this looks like a really good/sophisticated answer.Furlana
The Perl script is part of the build script, which is a "one step build" for everything.Mutilate
This is good as far as it goes, but do keep in mind that it will report the hash of the latest commit on the branch, not the hash of the code being compiled. If there are uncommitted changes, those will not be apparent.Huberty
I use git diff and check the result for empty output. Do you have a better suggestion?Mutilate
git diff by default checks for differences between your workspace and the index. You may also want to try git diff --cached for differences between the the index and HEADShcherbakov
All those 'const char *name = "value";' constructs could sensibly be changed to 'const char name[] = "value";', which saves 4 bytes per item on a 32-bit machine and 8 bytes per item on a 64-bit machine. Granted, in these days of GB of main memory, that's not a big problem, but it all helps. Note that none of the code using the names needs to change.Shebeen
I changed them as you suggest. The size of my program with const char []: 319356 bytes (stripped). The size of my program with const char *: 319324 bytes (stripped). So your idea doesn't seem to save any bytes, but increase the total number by 32. I have no idea why. In the original "version.c" there are three strings, but one was omitted from the above answer. If you look at the first edit it is still there.Mutilate
Oh sorry it isn't there actually. But anyway there are three const char * strings.Mutilate
I do something similar, but generate the version file in a .phony rule in my makefile.Cis
In my Makefile, I added version.c and version.h to .INTERMEDIATE, and generate the contents in the Makefile as well. This means make will generate the version files for building and remove them afterwards.Shimmy
No need to #include version.h in version.cHumph
S
12

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!

Speos answered 3/12, 2009 at 23:20 Comment(2)
An addendum... this will work in Matlab:mathworks.com/matlabcentral/fileexchange/32864-get-git-infoSpeos
I do not think that FORCE is a good idea since makefile will never be satisfied (every time you make you do a new header). Instead, you can just add dependency to relevant git files in formula $(MyLibs)/version.c : .git/COMMIT_EDITMSG .git/HEAD . File COMMIT_EDITMSG changes every time you make a commit and HEAD changes every time you browse history, therefore your file get updated everytime when it is relevant.Plataea
E
8

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

Epidote answered 10/11, 2009 at 0:32 Comment(2)
From git help describe: "Show the most recent tag that is reachable from a commit" -- this is not what the question asks for. I agree with the rest of your answer, though. In order to be correct, the command should be git rev-parse HEAD.Impend
@mikem, git describe is what most other projects use, because it includes human-readable tag information as well. If you're not exactly on a tag, it appends on the number of commits since the nearest tag, and the abbreviated revision hash.Epidote
L
7

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.

Lusterware answered 10/11, 2009 at 9:37 Comment(0)
A
4

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.

Azalea answered 10/11, 2009 at 1:54 Comment(0)
T
4

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)
Tita answered 8/1, 2016 at 14:51 Comment(1)
I presume you have GIT_VERSION and GIT_DATE passed in via CFLAGS so version.h can use them. Cool!Coe
D
3

This is a solution for CMake projects 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. An if-clause in CMakeLists.txt is used to execute the script corresponding to the platform the code is compiled on.

Save the following 2 scripts 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 add the following lines at the beginning to define the custom target "write_git_hash"

if(WIN32)
  add_custom_target( write_git_hash
    get_git_hash.cmd
    COMMENT "Call batch script for writing git hash"
    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
  )
else()
  add_custom_target( write_git_hash
    ./get_git_hash.sh
    COMMENT "Call shell script for writing git hash"
    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
  )
endif()

Make sure to add the "include" folder to your project

project(my_project)
include_directories(include)

After you define the executable, add "write_git_hash" as a dependency

add_executable(my_project src/my_cpp_file.cpp)
add_dependencies(my_project write_git_hash)

This will trigger the generation of the githash.h when building the project.

Include the githash.h file in your C++ file with #include <my_project/githash.h>. You should now be able to print the git hash to the terminal with

std::cout << "Software version: " << kGitHash << std::endl;

or do with it whatever you like.

Defeasance answered 30/9, 2019 at 10:56 Comment(2)
Thanks for this answer, I've largely replicated it but am having difficulty getting CMake to generate githash.h BEFORE it attempts to compile. I get complaints that githash.h cannot be found at compile time then CMake generates githash.h afterwards. AARGHH.Regiment
@Regiment I updated the bit of code for the CMakeLists.txt file. It should now generate the githash.h file before it attempts to use it.Defeasance
T
2

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.

Tension answered 10/11, 2009 at 0:35 Comment(4)
Just wondering, wouldn't each time he does that and therefore changes file.h, and then commits the changes to the source, the git hash would change?Wynn
@Blaenk.. thats what I was thinking too. But bdonlan's idea of having the program ask at runtime seems to get around this problem.Speos
Well, this file would have to be under .gitignore and generated every time you build the project.Tension
Alternatively you can include a basic version of this file and set --assume-unchanged flag on it (git update-index --assume-unchanged)Tension
P
2

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

Patchwork answered 17/1, 2011 at 18:57 Comment(2)
The use of preprocessor macros is very clever! Thank you.Speos
but if I checkout an older version, then compile it, it will guide me to the wrong commit.Hamate
B
1

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

Berkeleian answered 29/6, 2017 at 6:11 Comment(0)
H
1

I wanted something similar for my projects based on CMake (which I usually use for C or C++). The idea was to only need to add the project (f.e. as submodule) and automatically get a tiny static library that you can link to your main project. The static library contains a simple C API to query versioning information during runtime. CMake dependencies ensure, that the generated source file (wich contains the versioning information) is always up to date. No need to re-run the configure step (cmake) every time your git status changes - it will be detected automatically. The project is FOSS under the MIT license.

I also created a draft (still on a branch ATM) of how to integrated with Doxygen. This requires to add a hook to the Doxygen run and sets the version in the Doxygen documentation to the one retrieved via Git.

You can find the project here:

... and an example project here:

The basic steps to setup the project are:

# add the project (or your fork) as submodule (f.e. in the folder modules):
git submodule add https://github.com/boon-code/c-git-version modules/c-git-version

The CMakeLists.txt could look like this:

cmake_minimum_required(VERSION 3.1 FATAL_ERROR)
project(Example C)

add_subdirectory(modules/c-git-version)

add_executable(example "${CMAKE_CURRENT_SOURCE_DIR}/src/main.c")
target_link_libraries(example c_git_version)

With a simple C file "main.c" that outputs the version information:

#include <stdio.h>
#include <stdlib.h>

#include "c_git_version.h"

int main(void)
{
        printf("Version: %s\nBranch: %s\nHash: %s\n",
                c_git_version(), c_git_branch(), c_git_hash());

        printf("Submodules:\n%s\n", c_git_submodules());

        return EXIT_SUCCESS;
}

The complete file tree of the project would look like this:

.
|-- CMakeLists.txt
|-- modules
|   `-- c-git-version
|       |-- CMakeLists.txt
|       |-- LICENSE
|       |-- README.md
|       |-- cmake
|       |   `-- GetGitVersion.cmake
|       |-- inc
|       |   `-- c_git_version.h
|       `-- src
|           `-- c_git_version.c.in
`-- src
    `-- main.c
Hedonics answered 1/7, 2023 at 21:8 Comment(2)
While this link may answer the question, it is better to include the essential parts of the answer here and provide the link for reference. Link-only answers can become invalid if the linked page changes. - From ReviewAuria
Well, yes, I can provide a simple example. But the point of the library is, that you can simply include it as submodule in your project without having to take care of the details. So I wouldn't post the content of the project, only the usage. Is this what you meant?Hedonics
S
0

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.

Santee answered 10/11, 2009 at 2:13 Comment(0)
I
0

This isn't for C code, but I'll post just to complete the answers. Maybe a Java guy can use it. If you are using Maven as your build system, I've successfully used Git commit ID plugin.

It is very easy to use the plugin, just add the code below to your pom.xml:

            <plugin>
                <groupId>io.github.git-commit-id</groupId>
                <artifactId>git-commit-id-maven-plugin</artifactId>
                <version>5.0.0</version>
                <executions>
                    <execution>
                        <id>get-the-git-infos</id>
                        <goals>
                            <goal>revision</goal>
                        </goals>
                        <phase>initialize</phase>
                    </execution>
                </executions>
                <configuration>
                    <generateGitPropertiesFile>true</generateGitPropertiesFile>
                    <generateGitPropertiesFilename>${project.build.outputDirectory}/git.properties</generateGitPropertiesFilename>
                    <includeOnlyProperties>
                        <includeOnlyProperty>^git.build.(time|version)$</includeOnlyProperty>
                        <includeOnlyProperty>^git.commit.id.(abbrev|full)$</includeOnlyProperty>
                    </includeOnlyProperties>
                    <commitIdGenerationMode>full</commitIdGenerationMode>
                </configuration>
            </plugin>
Infrasonic answered 3/10, 2023 at 22:21 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.