GitTags vs Branches

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

Text
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 stayed
How they are stored — under the hood

Branches and tags in .git/refs/

Bash
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 git log by default

As decoration: (tag: v1.0.0)

As decoration: (HEAD -> main)

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

v1.0.0, v2.3.1, release-2024-01

main, feature/auth, hotfix/login-fix

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

Bash
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
Commits in detached HEAD are not protected
If you do not create a branch before switching away, orphaned commits will be deleted by Git's garbage collector after approximately 30 days (or sooner if you run `git gc`). Use `git switch -c my-branch` to rescue them.
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

Text
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 D

Confirm that tag and branch point to the same commit

Bash
# 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 — v1.0.0

No

Working on a new feature

No

Yes — feature/search

Fixing a critical bug in production

After fix — v1.0.1

During fix — hotfix/login-crash

Preserving a "before refactor" snapshot

Yes — pre-big-refactor

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 — git checkout v1.0.0 to inspect

No

Long-lived parallel development line

No

Yes — develop, release/2.x

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

Text
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 branch
Tags work across all branches
A tag applied to any commit is visible from any branch that can reach that commit. `git describe` will find the nearest tag regardless of which branch you are currently on.
Quick 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.

Both can coexist with the same name — but avoid it
Technically you could have both a branch named `release` and a tag named `release`. Git handles them in separate namespaces (`refs/heads/` vs `refs/tags/`). However, this is confusing to humans. Avoid naming tags and branches the same thing.