How does "git describe" work?

Table of Contents

git describe <sha> names a commit after the nearest tag before it, plus the number of commits since that tag.

1. The output format

v1-57-g3f1c9a7e2b
│  └┬┘ └────┬────┘
│   │       └── "g" + the abbreviated commit hash (g = git)
│   └────────── 57 commits after the tag
└────────────── the nearest tag before the commit

If the commit is the tagged commit, the output is just the tag:

$ git describe v1
v1

2. A small example

Take a repository with one branch and two tags:

A ── B ── C ── D ── E ── F
     │              │
     v1             v2
Commit Nearest tag before it Commits since the tag git describe
A none   fatal: No names found
B v1 0 v1
C v1 1 v1-1-g<C>
D v1 2 v1-2-g<D>
E v2 0 v2
F v2 1 v2-1-g<F>

git describe only looks backwards in history, never forwards. That has two consequences:

  • Adding a tag later doesn't change the name of older commits. If we tag F as v3, C is still v1-1-g<C>, because v3 is not before C.
  • The name doesn't tell you which later tags contain the commit. C is in v2, but its name only mentions v1.

3. With branches

Now add a release branch that forks from v1:

main     A ── B ── C ── D ── E
              │              │
              v1             v2
              │
release       └── X ── Y
Commit Branch git describe
C main v1-1-g<C>
D main v1-2-g<D>
X release v1-1-g<X>
Y release v1-2-g<Y>
E main v2

D and Y both come out as v1-2-.... Their names look the same, but they are on different branches. Only the hash tells them apart.

One solution is to tag the first commit on main after a release, marking the start of the next development cycle. Here we tag C as v2-dev:

main     A ── B ── C ── D ── E
              │    │         │
              v1   v2-dev    v2
              │
release       └── X ── Y
Commit Branch git describe
C main v2-dev
D main v2-dev-1-g<D>
X release v1-1-g<X>
Y release v1-2-g<Y>
E main v2

Now the name alone tells us the branch:

  • v1-N-... means the release branch.
  • v2-dev-N-... means main, working towards v2.

Many projects follow this workflow.

4. Looking forwards: git describe --contains

--contains does the opposite. It names a commit after the nearest tag after it, i.e. the first tag that contains it.

Using the small example:

$ git describe --contains C
v2~2

C is 2 commits before v2, so it is in v2.

If no tag comes after the commit, it fails:

$ git describe --contains F
fatal: cannot describe 'F'

5. Useful flags

Same repository as the small example, with one extra lightweight tag nightly on C:

A ── B ── C ── D ── E ── F
     │    │         │
     v1   nightly   v2

All outputs are for commit D:

Command Output Use
git describe D v1-2-g<D> Full name: nearest annotated tag before D + commit count
git describe --abbrev=0 D v1 Only the nearest tag before D. Drops the -2-g<D> suffix
git describe --contains D v2~1 First tag after D that contains it. D is 1 commit before v2
git describe --tags D nightly-1-g<D> Also consider lightweight tags, so nightly wins
git describe --exact-match D fatal: no tag exactly matches ... Only succeeds if D itself is tagged
git describe --always A <A> Fall back to the abbreviated hash when no tag is found

git describe D and git describe --tags D may be confusing if you aren't aware of annotated tags and lightweight tags.

Author: Adithya

Created: 2026-10-16 Fri 02:10

Validate