Gitgit describe

git describe

git describe turns a commit into a human-readable name based on the nearest tag. It's the command behind most build version strings — when you see something like v1.2.0-14-g2414721-dirty in a binary's --version output, that string almost certainly came from git describe.

The basic command

Bash
git describe

Typical output

Text
v1.2.0-14-g2414721

Read that as "14 commits past tag v1.2.0, current HEAD is abbreviated SHA 2414721." The g is a literal prefix for "git" (left over from the days of CVS/Subversion co-existence — it disambiguates from other VCS systems).

Parsing the format

The three pieces

Text
v1.2.0  -  14  -  g  2414721
  ^         ^        ^      ^
  tag      commits   "git"  abbreviated SHA of HEAD
           since the tag

If HEAD is exactly on the tag:
v1.2.0
(no suffix at all)
Lightweight tags with --tags

By default git describe only considers annotated tags (the kind made with git tag -a and git tag -s). Lightweight tags (plain git tag v1.2) are ignored unless you ask for them:

Bash
git describe              # annotated tags only
git describe --tags       # include lightweight tags
Why the default excludes lightweight tags
Annotated tags carry a message, an author, and a date, and are the convention for releases. Lightweight tags are essentially named bookmarks. Defaulting to annotated tags only means `git describe` gives you *release* versions instead of stray local labels.
Marking dirty working trees

Bash
git describe --dirty
# v1.2.0-14-g2414721-dirty   (if working tree has uncommitted changes)
# v1.2.0-14-g2414721         (if working tree is clean)

# Customise the suffix
git describe --dirty="*"
# v1.2.0-14-g2414721*

Add --dirty to every CI version string. If a developer accidentally builds with uncommitted changes, the -dirty suffix lights up the bug report and saves you hours of head-scratching later.

Controlling the SHA length

Bash
git describe --abbrev=0        # drop the commit suffix entirely; only the tag
# v1.2.0   (closest tag, even if HEAD isn't exactly on it)

git describe --abbrev=4        # short SHAs (4 hex chars)
# v1.2.0-14-g2414

git describe --abbrev=12       # longer SHAs (12 hex chars)
# v1.2.0-14-g2414721abcd
Filtering which tags count

Bash
# Only consider tags that look like a semver release
git describe --match "v[0-9]*"

# Combine with --tags to include lightweight ones
git describe --tags --match "release-*"

# Negative match: anything NOT a release-candidate
git describe --match "v*" --exclude "*-rc*"

Useful when you have many tag namespaces (e.g., v1.2.0, nightly-2026-05-19, internal/exp-feature) and only one of them represents production releases.

Requiring an exact tag

Bash
git describe --exact-match
# Succeeds with the tag name if HEAD is exactly on a tag.
# Fails with non-zero exit otherwise.

# Common idiom in CI to detect a tagged release build:
if git describe --exact-match --tags >/dev/null 2>&1; then
  echo "Building tagged release"
else
  echo "Building development snapshot"
fi
Describing any commit, not just HEAD

Bash
git describe 1f9ab2c
git describe HEAD~5
git describe origin/main
git describe v1.0..v2.0    # describes the tip of the range
The build-versioning use case

This is the canonical reason git describe exists. In CI, you bake the version into the artifact:

A CI build step

Bash
VERSION=$(git describe --tags --always --dirty)
echo "Building version: $VERSION"

# Pass it to your build tool
node build.js --app-version "$VERSION"
docker build --build-arg VERSION="$VERSION" .
ldflags="-X main.version=$VERSION"   # Go
cargo build --release -- --features "version=$VERSION"

Examples of values $VERSION might take

Text
v2.1.0                          # exact tagged release
v2.1.0-3-g8af1c12                # 3 commits past v2.1.0
v2.1.0-3-g8af1c12-dirty          # ... with uncommitted changes
8af1c12                          # before any tags exist (--always)

--always is the safety net: if there are no tags at all, git describe would normally fail. --always falls back to just the abbreviated SHA so your build script never crashes.

Other useful flags
  • --candidates=N — consider the N most recent tags when picking the closest one (default 10). Bump it on busy repos with many tags.

  • --first-parent — when walking ancestry, follow only first parents. Critical for mainline-only versioning in repos that merge feature branches.

  • --long — always include the commit suffix, even when HEAD is exactly on a tag. Gives you consistent output shape for parsing.

  • --contains <commit> — invert the question: which tags contain this commit? Handy for "in which release did this bug fix ship?"

Two especially useful invocations

Bash
# Always include the suffix — easier to parse in scripts
git describe --long --tags --always --dirty
# v1.2.0-0-g2414721 (HEAD is exactly v1.2.0, but suffix is still there)

# Find which release contains a bug-fix commit
git describe --contains a1b2c3d
# v1.3.0~12
describe walks ancestry, not all tags
`git describe` finds the closest reachable tag in the *ancestry* of HEAD. If a tag exists on a sibling branch, `git describe` won't see it. That's usually what you want — your version should reflect *this branch's* history, not unrelated tags.
Tip
For a build-friendly version string that works in every repository state, use:
git describe --tags --always --dirty --long --match="v[0-9]*". It gives consistent output shape, ignores junk tags, falls back to SHAs in fresh repos, and screams loudly when someone ships from a dirty tree.