Turn Git History into a Version with git describe

A bug report says "it broke in the build from Tuesday" and you have nothing more precise than a branch name; git describe is the fix for that. It turns a commit into a label such as v1.0-1-gf2b2caf, showing which tag it follows, how many commits separate it, and which abbreviated commit is the exact source.

Allow about ten minutes. You need Git 2.43.0 or a compatible Git installation and a repository with at least one reachable tag. The examples use the Ubuntu package git-man version 1:2.43.0-1ubuntu7.3, whose installed manual describes Git 2.43.0.

Every command here is read-only: nothing needs elevated privileges, and nothing creates or changes a tag.

1. Check the installed command

Run these checks in the repository whose version you want to report:

$ git --version
git version 2.43.0
$ git rev-parse --show-toplevel
/path/to/project

If the second command fails, change into a Git working tree before continuing. git describe accepts a commit-ish, such as a branch name, tag, commit ID or HEAD; omit it and Git describes HEAD.

Checkpoint: The first line should show Git, and the second command should print the top-level directory of the intended repository.

2. Read the nearest annotated tag

Start with the default form:

$ git describe
v1.0-1-gf2b2caf

Read the result left to right:

If HEAD points directly at the tag, the output is just the tag name:

$ git describe v1.0
v1.0

By default, git describe only considers annotated tags. This trips people up when a release process creates lightweight tags with git tag VERSION: a lightweight tag can be useful, but it is not picked up by the default search.

3. Include lightweight tags when required

Use --tags when any tag under refs/tags, lightweight included, is an acceptable version source:

$ git describe --tags
v1.0-light

Use --all if branches and remote-tracking references should be candidates too:

$ git describe --all
heads/main-1-gf2b2caf

The exact result depends on the refs in your repository. Do not treat a branch-derived label as a release number without deciding that is acceptable for your build or deployment process.

Checkpoint: Choose the reference policy before this goes in a script. Use the default for annotated releases, --tags for all tags, and --all only when non-tag refs are deliberately part of the naming scheme.

4. Ask for only the closest tag

For a release display that should skip the commit count and hash, set the abbreviation length to zero:

$ git describe --tags --abbrev=0 HEAD
v1.0-light

This returns the closest matching tag name, not necessarily an exact tag on the supplied commit: a commit several changes past a release still reports that release's tag. Use --exact-match when anything else must fail:

$ git describe --exact-match HEAD
fatal: no tag exactly matches '...'

The diagnostic and full object ID can vary, so check the exit status in a script rather than matching the text:

if version=$(git describe --exact-match HEAD 2>/dev/null); then
    printf 'release tag: %s\n' "$version"
else
    printf 'HEAD is not exactly tagged\n' >&2
    exit 1
fi

5. Mark locally modified source

A build made from an edited working tree should not look identical to one made from committed source. Add --dirty:

$ git describe --tags --dirty
v1.0-light-1-gf2b2caf-dirty

With no local modification, the output matches describing HEAD plainly. The suffix is a warning label, not a commit: it does not say which file changed and does not make the source reproducible. For a different suffix, pass it explicitly, for example --dirty=-local.

Tip: Do not treat this as a full build-integrity check. Generated files, ignored files and Git's exact status rules all affect whether the work tree counts as dirty. If reproducibility matters, record the commit ID and build inputs separately.

6. Handle repositories with no usable tag

Without a matching tag, plain git describe fails. Use --always if a unique abbreviated commit ID is an acceptable fallback:

$ git describe --always HEAD
6b4ba6a

That fallback is an object-name abbreviation, not a release version. A script that needs a version should decide whether to reject an untagged commit or label it clearly. One practical pattern:

version=$(git describe --tags --always --dirty)
printf 'build version: %s\n' "$version"

When a plausible tag seems to be missing from the result, try --debug. It prints the search information to standard error while keeping the description on standard output, useful for troubleshooting, but avoid sending both streams into a value another tool expects to contain only the version.

7. Verify the result before publishing it

Check the described object directly. Replace DESCRIBED with the value you intend to inspect:

$ git rev-parse DESCRIBED^{commit}
f2b2caf28a4bb1dedcb98245ce8a70e2e8b2dad5
$ git log -1 --oneline DESCRIBED
f2b2caf second

A long-form description normally has the shape tag-count-gobject. The count matches commits that would appear in a range from the tag to the described commit. The abbreviated part grows as the repository gains objects, so do not assume it stays seven characters forever.

For merge-heavy history, --first-parent makes the search follow only the mainline parent at merges, which can stop a tag on a merged topic branch becoming the apparent base of a release description:

$ git describe --tags --first-parent HEAD

Use this only when your project treats first-parent history as its release line. It changes which tags are eligible, so record the option in the build script rather than adding it casually.

Done means