GitIntroduction to Tags

Git Tags: An Introduction

Every software project reaches moments worth preserving — a stable release, a significant milestone, the version that shipped to production. Git branches are great for ongoing work, but they move every time you commit. Tags are different: they are permanent, immutable pointers that stay locked to one exact commit forever. Once you tag a commit, that label always means the same thing, no matter how many more commits accumulate on top.

Tags vs Branches — the core distinction

Both tags and branches are references stored in .git/refs/, and both ultimately point to a commit hash. The critical difference is behaviour over time.

How branches move but tags stay fixed

Text
BEFORE a new commit:

  A───B───C   ◀── main (branch)
          │
         v1.0.0  (tag)

AFTER git commit:

  A───B───C───D   ◀── main  (branch moved forward)
          │
         v1.0.0  (tag still points to C — it never moves)

A branch ref file is updated by Git every time you make a new commit on that branch. A tag ref file is written once and never touched again. That immutability is what makes tags suitable for version markers: v2.3.0 will always mean exactly the code that was in commit 3f8a2c9, no matter what else happens in the repository.

Two types of tags

Git has two distinct tag flavours. Understanding the difference is important before you start tagging releases.

Lightweight tags

A lightweight tag is the simplest thing Git can do: it creates a file under .git/refs/tags/ that contains a single commit SHA, and nothing else. No author, no date, no message. It is essentially a branch that never moves.

Creating a lightweight tag

Bash
git tag v1.0-lw

# Inspect what was stored — just a SHA:
cat .git/refs/tags/v1.0-lw
# 3f8a2c9d1b4e6a0f2c8d...
Annotated tags

An annotated tag creates a full Git tag object in the object database. That object stores the tagger name, email address, timestamp, a freeform message, and optionally a GPG cryptographic signature. The tag object then points to the commit (or tree or blob). Annotated tags are first-class Git objects, just like commits and trees.

Creating an annotated tag

Bash
git tag -a v1.0.0 -m "Release 1.0.0 — first stable version"

# The tag object itself has its own SHA:
cat .git/refs/tags/v1.0.0
# a4b9c3e...   (this is the TAG object SHA, not the commit SHA)

git cat-file -p v1.0.0
# object 3f8a2c9d1b4e6a0f...   ← the commit
# type commit
# tag v1.0.0
# tagger Alice <alice@example.com> 1700000000 +0000
#
# Release 1.0.0 — first stable version
Comparing lightweight, annotated tags, and branches

Feature

Lightweight Tag

Annotated Tag

Branch

Stored as

Ref file only (SHA)

Tag object + ref file

Ref file only (SHA)

Moves when you commit?

No — immutable

No — immutable

Yes — advances with each commit

Stores author/date?

No

Yes

No (commit has author)

Stores a message?

No

Yes

No

Supports GPG signing?

No

Yes

No

Appears in git describe?

Yes (with --tags flag)

Yes (by default)

No

Pushed automatically?

No — must push explicitly

No — must push explicitly

No — must push explicitly

Best use case

Local bookmarks, quick markers

Official releases, milestones

Ongoing parallel work

When to use tags
  • Version releasesv1.0.0, v2.3.1, v3.0.0-beta.1. The most common use case by far.

  • Milestones — marking a commit that passed QA, a commit sent to a client for review, etc.

  • Point-in-time snapshots — "this is the codebase as of the conference demo" or "before the great refactor".

  • Integration anchors — marking the last commit that was successfully deployed to production.

  • Hotfix base points — tag the production commit before applying an emergency fix so you can always return to it.

How tags fit into the Git object model

Git stores four types of objects: blobs (file content), trees (directory listings), commits (snapshots with metadata), and tag objects (annotated tags). Every object is content-addressed — identified by the SHA-1 (or SHA-256 in newer Git) hash of its content.

Git object model with an annotated tag

Text
  tag object (v1.0.0)
  ├── type: tag
  ├── object: → commit SHA
  ├── tagger: Alice <alice@example.com>
  ├── date: 2024-03-15 14:30:00
  └── message: "Release 1.0.0"
          │
          ▼
  commit object
  ├── tree: → root tree SHA
  ├── parent: → previous commit SHA
  ├── author: Alice
  └── message: "Final tweaks for release"
          │
          ▼
  tree object
  ├── blob: src/index.ts
  ├── blob: README.md
  └── tree: src/
              │
              ▼
          blob objects (actual file contents)

A lightweight tag skips the tag object entirely — the ref file just points straight to the commit SHA. An annotated tag adds an indirection layer: the ref points to the tag object, which in turn points to the commit.

Semantic versioning with Git tags

The overwhelming convention in open-source and commercial software is to prefix tag names with a lowercase v, then follow Semantic Versioning (SemVer): MAJOR.MINOR.PATCH.

SemVer tag naming convention

Text
v1.0.0       ← first stable release
v1.0.1       ← patch: backward-compatible bug fix
v1.1.0       ← minor: backward-compatible new feature
v2.0.0       ← major: breaking change

Pre-release identifiers:
v2.0.0-alpha.1
v2.0.0-beta.3
v2.0.0-rc.1   ← release candidate

Build metadata (rarely used in tags):
v1.0.0+20240315
  • MAJOR — increment when you make incompatible API changes.

  • MINOR — increment when you add functionality in a backward-compatible manner.

  • PATCH — increment when you make backward-compatible bug fixes.

  • Pre-release versions (alpha, beta, rc) have lower precedence than the release version.

  • The v prefix is a convention, not part of SemVer proper, but it is almost universal in Git repos.

Tagging a release with annotated tag and SemVer

Bash
# Make sure you are on the right commit
git log --oneline -5

# Create the annotated tag
git tag -a v2.1.0 -m "Release 2.1.0

Changes:
- Added dark mode support
- Fixed login redirect bug
- Improved build performance by 30%"

# Verify
git show v2.1.0

# Push to remote so everyone can see it
git push origin v2.1.0
Tag names are global
Tag names must be unique across the entire repository. You cannot have two tags named `v1.0.0`. If you need to move a tag (e.g., you tagged the wrong commit), you must delete the old one and recreate it — or use the force flag, with caution.
Always use annotated tags for releases
Use lightweight tags only for temporary, local bookmarks. For any release or milestone that you push to a remote, always use annotated tags (`git tag -a`). They carry authorship and a message, they show up correctly in `git describe`, and tools like GitHub automatically create release pages from them.
Quick reference

Essential tag commands at a glance

Bash
# Create lightweight tag on current commit
git tag v1.0-lw

# Create annotated tag on current commit
git tag -a v1.0.0 -m "Release 1.0.0"

# Create annotated tag on a specific commit
git tag -a v0.9.0 abc1234 -m "Pre-release snapshot"

# List all tags
git tag

# Show tag details
git show v1.0.0

# Push a tag to remote
git push origin v1.0.0

# Push all local tags to remote
git push origin --tags

# Delete a local tag
git tag -d v1.0-lw

# Delete a remote tag
git push origin --delete v1.0-lw