Tags vs Branches
Tags and branches are both references stored in .git/refs/, and both ultimately resolve to a commit hash. Yet they serve fundamentally different purposes and behave very differently over time. Understanding this distinction is one of the most important conceptual leaps in mastering Git.
The core conceptual difference
Think of a branch as a sticky note on the whiteboard that someone moves forward every time a new idea is added. Think of a tag as a plaque bolted to the wall at a specific point, permanently marking that moment — no one moves it, ever.
Branches move; tags stay fixed
Timeline ────────────────────────────────────────────────────▶
Commits: A ── B ── C ── D ── E ── F ── G
│ │
[v1.0.0] [v1.1.0] ← tags: never move
(tag) (tag)
A ── B ── C ── D ── E ── F ── G
│
[main] ← branch: moves with each commit
After adding commit H:
A ── B ── C ── D ── E ── F ── G ── H
│ │ │
[v1.0.0] [v1.1.0] [main] ← main moved; tags stayedHow they are stored — under the hood
Branches and tags in .git/refs/
ls .git/refs/heads/ # branches # main # feature-auth # hotfix-login ls .git/refs/tags/ # tags # v1.0.0 # v1.1.0 # v2.0.0 # Both are just files with commit SHAs: cat .git/refs/heads/main # 9a8b7c6d... ← updated every commit cat .git/refs/tags/v1.0.0 # 3f8a2c9d... ← written once, never changed # The difference is behaviour, not storage format
Comprehensive comparison table
Dimension | Tag | Branch |
|---|---|---|
Purpose | Mark a permanent, named point in history | Represent ongoing parallel lines of work |
Moves when you commit? | Never — immutable pointer | Yes — advances to the new commit automatically |
HEAD can point to it? | Yes, but puts you in detached HEAD | Yes — normal state |
Can you commit "to" it? | No — commits land on branches or nowhere (detached) | Yes — making a commit advances the branch |
Stored metadata | Tagger, date, message (annotated) or none (lightweight) | None — branch ref just contains a SHA |
Appears in | As decoration: | As decoration: |
Pushed automatically? | No — must be pushed explicitly | No — must be pushed explicitly |
Deleted safely? | Yes, if local only; risky if pushed | Yes, if merged; use -D if unmerged |
Typical naming |
|
|
Git object type | Tag object (annotated) or raw ref (lightweight) | Raw ref (always) |
Suitable for releases? | Yes — primary use case | Rarely used for releases directly |
Suitable for development? | No | Yes — primary use case |
Can you commit to a tag?
No. Tags are immutable — you cannot commit "onto" a tag the way you can commit onto a branch. If you check out a tag and make commits, those commits go into detached HEAD state, where they have no branch to belong to. They will eventually be garbage-collected if you do not create a branch to contain them.
Attempting to commit on a checked-out tag
git checkout v1.0.0 # HEAD detached at v1.0.0 # Make a change and commit echo "test" >> file.txt git add file.txt git commit -m "This commit has nowhere to live" # [detached HEAD a4b9c3e] This commit has nowhere to live git log --oneline -2 # a4b9c3e (HEAD) This commit has nowhere to live # 3f8a2c9 (tag: v1.0.0) Final tweaks before release # Switch back to main WITHOUT saving a branch git switch main # Warning: you are leaving 1 commit behind, not connected to # any of your branches: # a4b9c3e This commit has nowhere to live # If you want to keep it by creating a new branch, this may be # done (now or later) by using: # # git branch <new-branch-name> a4b9c3e # The orphaned commit will be garbage collected eventually
Can a tag and a branch point to the same commit?
Yes, absolutely. It is common and completely valid for a tag and a branch to both point to the same commit at the same time. For example, when you tag a release, main and v1.0.0 both refer to the same commit. They simply diverge as soon as the next commit is pushed to main.
Tag and branch on the same commit
A ── B ── C ── D
↑ ↑
v1.0.0 main ← both point to commit D at release time
(tag) (branch)
After next commit (E):
A ── B ── C ── D ── E
↑ ↑
v1.0.0 main ← main moved; v1.0.0 stayed at DConfirm that tag and branch point to the same commit
# Just after tagging before any new commits
git rev-parse v1.0.0^{} # dereference annotated tag
# 9a8b7c6d...
git rev-parse main
# 9a8b7c6d... ← identical
# After a new commit on main:
git rev-parse v1.0.0^{}
# 9a8b7c6d... ← unchanged
git rev-parse main
# e1f2a3b4... ← different (main moved forward)When to use each
Situation | Use a tag | Use a branch |
|---|---|---|
Marking a software release | Yes — | No |
Working on a new feature | No | Yes — |
Fixing a critical bug in production | After fix — | During fix — |
Preserving a "before refactor" snapshot | Yes — | No |
Code review via Pull Request | No | Yes — branches are the basis for PRs |
Continuous integration trigger | Yes — CI can trigger on tag push | Yes — CI can trigger on branch push |
Rollback anchor point | Yes — | No |
Long-lived parallel development line | No | Yes — |
Tags in release workflows
In most team workflows, branches and tags work together in a complementary way. Branches carry the work; tags mark the milestones along the way.
Branches and tags cooperating in a release workflow
develop branch: A ── B ── C ── D ── E ── F
│ │
[v1.0.0] [v1.1.0] ← tags applied to commits on develop
main branch: A ──────── C ──────────── F
│ │
[v1.0.0] [v1.1.0] ← same tags visible after merge
hotfix branch: D ── X ── Y
│
[v1.0.1] ← patch tag on hotfix branchQuick decision guide
Asking "where are we going?" → use a branch.
Asking "where were we?" → use a tag.
Need to do work → branch.
Need to mark history → tag.
Will this reference move? → branch. Will it stay fixed? → tag.
Is this for internal development? → branch. Is this for external consumers (releases, changelogs, packages)? → tag.