Menu for Per-Arne's Git Good!
Courses and project Per-Arne's Git Good!

Tags: fixed names for important versions

Module 2: Branching and merging · 10 min

The interactive lesson needs JavaScript. The text below is the complete lesson.

After this lesson you can

  • mark a release with git tag -a v1.0 -m and inspect it with git show apply
  • explain that a tag stays put, while a branch moves on every commit understand
  • tell annotated from lightweight tags, and choose the right kind for a release evaluate
  • interpret the answer from git describe understand

Understand: A bookmark that does not move

When a version is finished, you want to be able to find exactly that one again. A commit ID is hard to remember. A tag gives the commit a fixed name, for example v1.0.

Here is the difference from a branch. When you keep committing, main moves. The tag v1.0 stays where it was set.

There are two kinds of tags. A lightweight tag is just a name for a commit, a bit like a branch that never moves. It suits private bookmarks, like try-1 here.

An annotated tag is an object of its own in Git. It stores who made it, when, and a message. Use annotated tags for releases, with git tag -a.

Watch: Mark a release

The project is ready for its first release. We create an annotated tag with a message. Watch the graph: v1.0 appears on the commit we are on.

$ git tag -a v1.0 -m "First release"

git show first shows the tag itself: who made it, when, and the message. Below comes the commit it points to.

$ git show v1.0

Now we keep working and commit. Watch the graph: main moves, and v1.0 stays put.

$ echo "Open 8-16" >> README.md && git add README.md && git commit -m "Add opening hours"

The log confirms it. The tag is on the previous commit, and main points to the new one.

$ git log --oneline --decorate

Now we create a lightweight tag, without -a and without a message. It is just a name for the commit we are on.

$ git tag try-1

git describe answers with v1.0, a 1 and a short ID: one commit after v1.0. Notice that it skipped try-1, even though it is closer. By default, git describe uses only annotated tags.

$ git describe

Tasks

  1. Mark the commit you are on as release v1.0, with an annotated tag.
  2. See what the tag v1.0 contains.
  3. Make a new commit on main that changes README.md.
  4. Show the history with the names on the commits.
  5. Create a lightweight tag called try-1 on the commit you are on.
  6. Ask Git to describe the commit you are on from the tags.

Practise: Transfer: tag history after the fact

A repository with three commits on main. None of them is tagged yet.

Tasks

  1. Mark the very first commit as release v0.1. Others must be able to see who released it, when and why.
  2. A bug has been found in v0.1. Create a branch hotfix-0.1 that starts at v0.1, and be on it.

Check yourself: Recall it

What sets an annotated tag apart from a lightweight tag?

  • The annotated tag moves with the branch
  • The annotated tag is an object of its own with who made it, a date and a message
  • Only lightweight tags can be shown with git show

git describe prints v1.0-3-g4f2a1c9. What does it mean?

  • Version 1.0.3
  • Three commits after v1.0, on a commit whose ID starts with 4f2a1c9
  • There are three tags called v1.0

The teacher must be able to see who marked the hand-in and when. A classmate suggests git tag hand-in. What do you answer?

  • Fine, every tag records who and when
  • Use git tag -a hand-in -m "Hand-in 1" instead, because it records who, when and a message
  • Use a branch called hand-in instead, so the mark follows later work

Review: As long as nobody moves the tag, why does v1.0 mean exactly the same files many years from now?

  • Git locks the files in the commit, so nobody can edit them
  • The tag stores its own copy of all the files
  • The commit ID is computed from the content. If anything changes, you get a new commit with a new ID.

Common misconceptions

  • “A tag moves with the branch.” It stays on the commit where it was set.
  • “Lightweight and annotated tags are the same.” Only an annotated tag stores who, when and a message, and git describe uses only those by default.

Read more

Two kinds of tag, two kinds of object

A lightweight tag is just a name in refs/tags/ that points straight at a commit, like a branch that never moves. An annotated tag is an object of its own in the Git database. It records the object it tags, the tag name, who made it, when, and a message. The tag name then points to that tag object.

$ git tag -a v1.0 -m "First release"
$ git tag try-1
$ git cat-file -t v1.0
tag
$ git cat-file -t try-1
commit
$ git cat-file -p v1.0
object 69966c17bf0d690babd2d565d9b9a7358820e38a
type commit
tag v1.0
tagger Student <student@example.invalid> 1799744400 +0100
First release

The object line is the tagged commit. That is why git rev-parse v1.0 prints the ID of the tag object, while git rev-parse v1.0^{commit} follows it to the commit. Use the second form in scripts and checks.

Annotated tags can also be signed with git tag -s, so others can verify with git tag -v that the release really comes from you. If you leave out -m, Git opens your editor for the message.

Use Kind
Releases, hand-ins, anything others rely on annotated: git tag -a
Private, temporary bookmarks lightweight: git tag name

Several commands look only at annotated tags by default, among them git describe and git push --follow-tags.

Tagging an older commit

A tag goes on HEAD unless you name another commit:

$ git log --oneline
15cecfd Add opening hours
69966c1 Add contact info
eff28a0 Initial snapshot
$ git tag -a v0.1 -m "Early release" eff28a0

HEAD~2 would work just as well as the ID.

Listing and inspecting

$ git tag -l 'v*' -n
v1.0            First release
$ git show --no-patch v1.0
tag v1.0
Tagger: Student <student@example.invalid>
Date:   Tue Jan 12 10:00:00 2027 +0100
First release
commit 69966c17bf0d690babd2d565d9b9a7358820e38a
Author: Student <student@example.invalid>
Date:   Tue Jan 12 10:00:00 2027 +0100
    Add contact info

-n adds the first line of each message. For a lightweight tag, git show jumps straight to the commit, because there is no tag object to show. git log --oneline --decorate shows tags next to the commits.

Reading git describe

$ git describe
v1.0-1-g15cecfd

Git walks back from HEAD to the nearest annotated tag and prints three parts: the tag, the number of commits since it, and g (for git) followed by the abbreviated ID of the current commit. On a tagged commit the answer is just v1.0. Build scripts use this to stamp a version into a program.

  • --tags also counts lightweight tags.
  • If there is no tag to start from, the command fails with a message that begins with fatal:. With --always you get the short ID instead.

A tag should never move

Creating a tag that already exists fails:

fatal: tag 'v1.0' already exists

-f would move it, but anyone who already fetched the tag keeps the old one, and git fetch refuses to overwrite an existing tag without being forced. Two people would then mean different code by v1.0. If a release was wrong, make a new one, for example v1.0.1. Many projects number versions as MAJOR.MINOR.PATCH, called semantic versioning.

Sharing tags

Tags live in your repository. Pushing a branch does not send them:

$ git push origin v1.0                  # one named tag
 * [new tag]         v1.0 -> v1.0
$ git push --follow-tags origin main    # the branch, plus annotated tags it reaches
$ git push origin --tags                # every tag, lightweight ones included

Prefer naming the tag or --follow-tags: --tags also publishes private bookmarks like try-1. git ls-remote --tags origin shows what the server has. A line ending in ^{} is the commit an annotated tag points to:

b31f5cff91109e1958c1d82922501178bb6c608b    refs/tags/v1.0
69966c17bf0d690babd2d565d9b9a7358820e38a    refs/tags/v1.0^{}

Deleting is two separate steps: git tag -d try-1 removes the local tag, and git push origin --delete v1.0 removes it from the server.

Working from a tag

A tag is not a branch, so git switch v1.0 fails with fatal: a branch is expected, got tag 'v1.0'. Use git switch --detach v1.0 to look around, or git switch -c hotfix-1.0 v1.0 to start a branch there for a fix.

Official documentation