Build the mental model
A branch label moves forward with every new commit — that's the entire point of a branch. A tag does the opposite: it marks one specific commit permanently, and never moves again no matter how many commits get added afterward.
Tags exist for moments that matter — the exact commit shipped to production, the exact commit a customer is running, the exact commit that closed out a milestone.
| Tag type | What it is |
|---|---|
| Lightweight tag | A named pointer to a commit, nothing more — `git tag <name>` |
| Annotated tag | A full object with its own author, date, message, and can be signed — `git tag -a <name> -m "..."` |
For anything worth keeping or sharing with a team, annotated tags are the better default — they leave a record of who tagged what and why.
Once tags mark meaningful points in history, a team needs a shared naming scheme so a tag name communicates something by itself. That's what semantic versioning solves: a version number written as MAJOR.MINOR.PATCH, like v2.4.1.
- MAJOR — a breaking change that could break code depending on you
- MINOR — a new feature added in a backward-compatible way
- PATCH — a bug fix that changes no behavior anyone relies on
Convention, not enforcement
Git enforces none of this. You could tag a single typo fix v99.0.0 and Git would not object — semantic versioning is a convention humans and tools agree to follow, not a rule Git checks.
- Tag
- A named pointer to one specific commit that never moves, used to mark meaningful points like releases.
- Semantic Versioning
- A version-numbering convention, MAJOR.MINOR.PATCH, where each segment signals a specific kind of change — breaking, feature, or fix.
TAG VS BRANCH: WHO MOVES
------------------------
TAG VS BRANCH: WHO MOVES
-------------------------
main (branch) ---------------------------> moves forward
|
o---o---o---o---o---o <- new commits keep landing here
^
|
v1.0.0 (tag) <- stays pinned to this one commit
forever, even as main moves onConnect it to a real scenario
Tag the commit
On the commit you want to mark, run `git tag v1.0.0` for a lightweight tag or `git tag -a v1.1.0 -m "message"` for an annotated one.
List tags
`git tag` alone lists every tag in the repo, alphabetically.
Inspect a tag
`git show v1.0.0` prints the commit it points to; for annotated tags it also shows the tag's own author, date, and message first.
Push the tag
Push it explicitly with `git push origin v1.1.0`, or push everything with `git push --tags`.
Tags don't push automatically
A plain `git push` sends commits, not tags. Teammates won't see a new tag until you push it explicitly.
Try the working example
git init -q -b main
git config user.name "Thuta Learner"
git config user.email "learner@example.com"
export GIT_AUTHOR_NAME="Thuta Learner"
export GIT_AUTHOR_EMAIL="learner@example.com"
export GIT_COMMITTER_NAME="Thuta Learner"
export GIT_COMMITTER_EMAIL="learner@example.com"
echo "console.log('v1');" > app.js
git add app.js
GIT_AUTHOR_DATE="2026-01-01T09:00:00" GIT_COMMITTER_DATE="2026-01-01T09:00:00" \
git commit -m "Initial release logic"
echo "console.log('v1.1');" >> app.js
git add app.js
GIT_AUTHOR_DATE="2026-01-01T10:00:00" GIT_COMMITTER_DATE="2026-01-01T10:00:00" \
git commit -m "Add greeting feature"
echo "console.log('fix');" >> app.js
git add app.js
GIT_AUTHOR_DATE="2026-01-01T11:00:00" GIT_COMMITTER_DATE="2026-01-01T11:00:00" \
git commit -m "Fix off-by-one bug"
git log --oneline
git tag v1.0.0 HEAD~2
git tag -a v1.1.0 -m "Add greeting feature and bug fix"
git tag
git show v1.0.0 --stat
git show v1.1.0 --stat$ git log --oneline
fba08e9 Fix off-by-one bug
7744f86 Add greeting feature
9b07fe7 Initial release logic
$ git tag
v1.0.0
v1.1.0
$ git show v1.0.0 --stat
commit 9b07fe7c5e1dc5e087a0f2c53bf8bd78caf6e93f
Author: Thuta Learner <learner@example.com>
Date: Thu Jan 1 09:00:00 2026 +0900
Initial release logic
app.js | 1 +
1 file changed, 1 insertion(+)
$ git show v1.1.0 --stat
tag v1.1.0
Tagger: Thuta Learner <learner@example.com>
Date: Thu Jan 1 11:30:00 2026 +0900
Add greeting feature and bug fix
commit fba08e984fb5ba335e158b32b88bdcd5f003ea7b
Author: Thuta Learner <learner@example.com>
Date: Thu Jan 1 11:00:00 2026 +0900
Fix off-by-one bug
app.js | 1 +
1 file changed, 1 insertion(+)5-minute try-it
In a repo of your own, make two commits, then tag the earlier one as v1.0.0 (lightweight) and the later one as v1.1.0 (annotated, with a message describing what changed). Run `git tag` to confirm both exist, then use `git show` on each and note the difference in what gets printed for a lightweight tag versus an annotated one.
One important caution
Tags don't move — if you tag the wrong commit, you have to delete the tag (`git tag -d`) and re-create it; there's no 'update in place'.
A plain `git push` does not send tags to the remote; forgetting `git push --tags` means teammates never see the tag you created.
Git Documentation: git-tag — Git & GitHub