Embedding Git Commit Information in Go Binaries

by | May 25, 2022

Embedding Git commit information in Go Binaries is essential for identifying exact software versions, especially when working with development builds.

So it’s good if software is able to tell you its version, for example by calling it with --version. For release versions, maintaining this information by hand is feasible, but in order for development versions to show exact revision information, you need some automation, otherwise updating it will be forgotten, leaving you with wrong information.

This blog post shows different techniques that can be used to embed a Git commit hash into programs written in the Go programming language. Finally, it will also show a Git feature that is useful for other languages as well.

Linker Flags

One effective way to add Git commit information in Go Binaries is by using linker flags. Just define a string variable somewhere in the code like this:

var Commit string

This variable can then be set to a specific value by specifying extra linker flags:

go build -ldflags="-X main.Commit=$(git rev-parse HEAD)"

Note that main specifies the package name that contains the variable, so if you want to set a variable in a different package, you have to specify the import path of that package instead.

go generate

Another option is to use go generate, which allow you to run arbitrary commands to update auto-generated files. This can be used to generate Go source code later included in the build, or, starting from Go 1.16, you can also write arbitrary files that can then be embedded into the binary using the embed package. So consider the following Go code:

//go:generate sh -c "printf %s $(git rev-parse HEAD) > commit.txt"
//go:embed commit.txt
var Commit string

The //go:generate directive instructs Go to run a command that writes the current commit hash to a file called commit.txt (the extra printf %s is only there to strip the trailing newline character, this could also be done by stripping it when using the variable) and the //go:embed directive instructs it to set the variable Commit to the contents of this file.

However, these commands are not run automatically, so to build the binary, you now have to invoke two commands:

go generate
go build

But if you happen to use go generate anyways to generate other files, this gives you a nice method of adding Git version information basically for free. Also keep in mind that you have to run this every time. Otherwise you might end up using a file from a previous build with outdated information.

Build Information (Go 1.18+)

Go 1.18 added a nice feature: when building a Go project from within a Git checkout, some commit information is added to the binary automatically when running go build without requiring any additional flags (but you can control the behavior in more detail with the -buildvcs flag if you want to). This makes it a very nice solution as it requires no additional build scripts that supply the additional information.

This information can be read from the Go code like this:

import "runtime/debug"

var Commit = func() string {
    if info, ok := debug.ReadBuildInfo(); ok {
        for _, setting := range info.Settings {
            if setting.Key == "vcs.revision" {
                return setting.Value
            }
        }
    }

    return ""
}()

The amount of information available is quite limited though. In case of Git, the following attributes are available besides vcs.revision:

  • vcs.time: Timestamp of the commit.
  • vcs.modified: Set to true (as a string) if the binary was built from a working directory containing uncommitted changes.

git archive

For situations where you’re not building from a Git checkout, there is a convenient Git feature that allows you to substitute placeholders in files when exporting the repository, for example as a .tar.gz file. The placeholders look like this:

const Commit = "$Format:%H$"

This variable will be set to the full commit hash, for a list of available placeholders, you can check the git-log(1) man page. This replacement is not done automatically, but only on files that have the attribute export-subst set in .gitattributes (next to .gitignore) like this:

/commit.go export-subst

When this repository is now exported using git archive, the resulting file looks something like this:

$ git archive HEAD | tar -x --to-stdout commit.go  
package main

const Commit = "31fbe21f80912bb2040027c3239ce6c86f7e109c"

Of course, this substitution only takes place when exporting an archive, which also includes downloads from GitHub. In all other cases, the variable still includes the template string, so any code using such a variable should be aware that this can happen, check if the replacement happened, and treat the commit information as unavailable otherwise.

If you want to see a full working example combining the last two techniques, you can have a look at this pull request for Icinga DB which among other things adds Git commit information to the output of icingadb --version.

If you want to learn more about how to work efficiently with Git, read our blog post Resolving Git Merge Conflicts Easily and Accurately, which shows you how to solve common problems quickly and accurately.

Embedding Git commit information in Go binaries is not only a best practice, but also a necessity for maintaining software transparency. By using techniques like linker flags go generate, or leveraging Go’s built-in VCS support, developers can ensure their binaries always reflect the exact state of their codebase. Start integrating these methods into your workflow today and take control of your build process!

You May Also Like…

Releasing Icinga Director v1.11.2

Releasing Icinga Director v1.11.2

We are pleased to announce the release of Icinga Director version 1.11.2, which addresses several important bug fixes...

Releasing Icinga Web v2.12.2

Releasing Icinga Web v2.12.2

Today we’re announcing the general availability of Icinga Web v2.12.2. You can find all issues related to this release...

Code Reviews – How do they work?

Code Reviews – How do they work?

We at Icinga / NETWAYS (yes, that’s the order) held an internal event recently. It’s name was Knowledge Days and I got...

Subscribe to our Newsletter

A monthly digest of the latest Icinga news, releases, articles and community topics.