C programming
Git
version control
code automation
software development

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

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Embedding the current Git revision into a C binary is a practical way to make builds traceable. When a user reports a bug or a crash dump references a version string, the commit hash gives you an exact path back to the source that produced the executable.

The general idea is simple: ask Git for the current revision during the build and inject that value into the compiled program. The main design choice is whether you pass it as a compiler definition or generate a header file.

The Smallest Working Approach

For quick experiments, you can pass the hash directly on the compiler command line.

bash
gcc main.c -DGIT_HASH=\"$(git rev-parse --short HEAD)\" -o app

Then in C:

c
1#include <stdio.h>
2
3#ifndef GIT_HASH
4#define GIT_HASH "unknown"
5#endif
6
7int main(void) {
8    printf("version: %s\n", GIT_HASH);
9    return 0;
10}

This is enough for a simple manual build, but it becomes fragile as soon as the project grows. Build systems cache object files, compile multiple targets, and need reliable rebuild triggers when the Git revision changes.

A Better Pattern: Generate a Header

For larger projects, generating a header file is usually cleaner. It keeps version metadata in one place and avoids repeating compile flags across targets.

bash
printf '#define GIT_HASH "%s"\n' "$(git rev-parse --short HEAD)" > version.h

Then include it in your code:

c
1#include <stdio.h>
2#include "version.h"
3
4void print_version(void) {
5    printf("build revision: %s\n", GIT_HASH);
6}

This approach also makes it easy to expose more metadata later, such as the build date, branch name, or dirty working-tree state.

Include Dirty State for Real Debugging

A plain commit hash can be misleading if the binary was built from a modified checkout. Appending a dirty marker avoids that ambiguity.

bash
HASH=$(git describe --always --dirty)
printf '#define GIT_HASH "%s"\n' "$HASH" > version.h

Now a locally modified build might show something like a1b2c3d-dirty, which is much more honest during debugging.

Makefile Example

A Make-based project can automate header generation like this:

make
1VERSION_HEADER := version.h
2GIT_HASH := $(shell git describe --always --dirty 2>/dev/null || echo unknown)
3
4$(VERSION_HEADER):
5	printf '#define GIT_HASH "%s"\n' "$(GIT_HASH)" > $(VERSION_HEADER)
6
7main.o: main.c $(VERSION_HEADER)
8	$(CC) -c main.c -o main.o
9
10app: main.o
11	$(CC) main.o -o app

This keeps the generated file as an explicit dependency, which makes the build graph easier to reason about.

CMake Example

CMake projects often use execute_process plus configure_file so the version string is generated during configuration.

cmake
1execute_process(
2  COMMAND git describe --always --dirty
3  WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
4  OUTPUT_VARIABLE GIT_HASH
5  OUTPUT_STRIP_TRAILING_WHITESPACE
6)
7
8configure_file(
9  ${CMAKE_SOURCE_DIR}/version.h.in
10  ${CMAKE_BINARY_DIR}/generated/version.h
11  @ONLY
12)

Template file:

c
#define GIT_HASH "@GIT_HASH@"

This works well when your build already has a configuration step.

What to Print at Runtime

A hash alone is useful, but many teams print a fuller version string:

c
printf("app %s (%s)\n", APP_VERSION, GIT_HASH);

That combines a human-friendly release number with a machine-precise source revision. It is a good compromise for command-line tools and service startup logs.

Common Pitfalls

One common mistake is generating the hash only once and then expecting it to update automatically after every new commit. If the generated header is not rebuilt when HEAD changes, the binary can report a stale revision.

Another issue is assuming .git is always available. Source tarballs, export archives, and some CI environments may build without full Git metadata. Always provide a fallback such as "unknown" or inject the revision from CI variables.

Developers also forget about dirty state. If local edits are present, printing only the last commit hash can send debugging in the wrong direction.

Finally, avoid recomputing the hash in every compilation command if a generated header or configured file can centralize the value. Centralization makes builds easier to maintain and reason about.

Summary

  • Injecting the Git revision into a C binary makes builds traceable and easier to debug.
  • Passing -DGIT_HASH=... works for small builds, but a generated header is usually cleaner.
  • 'git describe --always --dirty is often more useful than a plain short hash.'
  • Build systems should treat the generated version metadata as an explicit dependency.
  • Always provide a fallback for environments that do not have .git metadata available.

Course illustration
Course illustration

All Rights Reserved.