Tags — marking a specific commit as meaningful, permanently

A branch pointer moves forward with every commit. A tag is the opposite — a pointer that's meant to stay fixed on one exact commit forever, marking "this specific point in history matters."

Intermediate

4 min read

The problem: branches move, but some points in history need to stay fixed

main:  A -- B -- C -- D -- E -- ...
                 ^
                 this exact commit was what got deployed as "v1.0" —
                 but main keeps moving forward past it

The branching lesson established that a branch is a pointer that moves forward with every new commit — which is exactly right for ongoing work, but wrong for marking a specific, permanent point in history ("this exact commit is what version 1.0 actually was"). If that information only lived in a person's memory or a separate spreadsheet, it would be disconnected from the actual repository and easy to lose track of.

git tag: a pointer that stays fixed, forever

git tag v1.0.0
git tag v1.0.0 a3f8e21    -- tag a specific commit, not necessarily the current one

A tag is a named pointer to one specific commit, and unlike a branch, it doesn't move — main keeps advancing with every new commit, but v1.0.0 stays pointed at exactly the commit it was created on, permanently. This is the direct answer to "which exact commit was version 1.0" — instead of relying on memory or an external record, the answer lives directly in the repository's own history, queryable the same way any other reference is.

Two kinds of tags: lightweight vs. annotated

git tag v1.0.0                                    -- lightweight: just a name, nothing else
git tag -a v1.0.0 -m "First stable release"          -- annotated: has its own message, author, date

A lightweight tag is just a name pointing at a commit — nothing more. An annotated tag is a real object in Git's database with its own message, author, and timestamp, similar in spirit to a commit itself — git show v1.0.0 on an annotated tag shows that message and metadata, not just the commit it points to. Annotated tags are the generally recommended default for anything meant to mark a real release, specifically because they carry that extra context (why this point matters, who marked it) directly in the repository.

Semantic versioning: the convention behind tag names like v1.2.3

v1.2.3
 | | |
 | | +-- PATCH: backward-compatible bug fixes only
 | +---- MINOR: new functionality, backward-compatible
 +------ MAJOR: breaking changes

vMAJOR.MINOR.PATCH (semantic versioning, or "semver") is the widely-used convention for what a version number itself communicates — incrementing PATCH promises "safe to upgrade, nothing should break"; incrementing MINOR promises new capability without breaking existing usage; incrementing MAJOR explicitly signals that something incompatible changed and upgrading requires care. This directly parallels the API design and versioning lesson's discussion of what counts as a breaking change — semver is essentially that same distinction, expressed as a tagging convention.

Tags don't push automatically — a real, common surprise

git push origin main       -- pushes commits, but NOT tags
git push origin v1.0.0       -- pushes one specific tag
git push origin --tags        -- pushes every local tag that isn't already on the remote

Unlike commits on a branch, tags are not included by a plain git push — this genuinely surprises people the first time: creating a tag locally and expecting it to show up on GitHub after an ordinary push, only to find it isn't there. Tags have to be pushed explicitly, either individually or all at once with --tags.

Checking out a tag: a fixed point, not a place to keep working

git checkout v1.0.0

Checking out a tag puts the working directory in exactly the state that commit was in — but since a tag doesn't move the way a branch does, committing while "on" a tag leaves those new commits unattached to any branch, in what's called a detached HEAD state (a genuinely real trap for anyone unfamiliar with it). Checking out a tag is for looking at or building from a fixed historical point — reproducing exactly what version 1.0 was, for instance — not for a place to continue ongoing work; new work belongs on a real branch, created explicitly from that tag if needed.

Further reading

Check your understanding

A quick comprehension check — not tracked, not graded, just for you.

1. What's the key structural difference between a tag and a branch?

2. Why might an annotated tag be preferred over a lightweight tag for marking an actual release?

3. In semantic versioning (vMAJOR.MINOR.PATCH), what does incrementing MAJOR signal, as opposed to PATCH?

4. Does a plain git push send local tags to the remote along with commits?