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
git describe
Typical output
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
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:
git describe # annotated tags only git describe --tags # include lightweight tags
Marking dirty working trees
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
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
# 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
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
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
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
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
# 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
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.