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 stillv1-1-g<C>, becausev3is not before C. - The name doesn't tell you which later tags contain the commit. C is in
v2, but its name only mentionsv1.
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 towardsv2.
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.