Skip to main content

What are Git tags and when to use them?

Git tags are fixed references to specific commits, used to mark release points like v1.0.0.

Theory

TL;DR

  • Tags are like a sticky note on a commit: they stay on that exact spot forever, unlike branches that move forward with every new commit
  • Two types: lightweight (just a pointer to a commit) and annotated (a full Git object with message, author, date)
  • Annotated tags are for releases; lightweight tags are for quick local bookmarks
  • Tags are not pushed automatically - you have to push them explicitly

Quick example

bash
# Lightweight tag (just a pointer) git tag v1.0 # Annotated tag (full object with metadata) git tag -a v1.1.0 -m "Stable release v1.1.0" # List all tags git tag # v1.0 # v1.1.0 # Push a specific tag to remote git push origin v1.1.0

After git tag -a, Git creates a tag object storing who tagged it, when, and why. Lightweight tags skip all that and just point to a commit SHA.

Key difference: tags vs branches

A branch pointer moves forward every time you commit. HEAD on main today is not the same commit as HEAD on main last week. A tag never moves. v1.0.0 points to the same commit in six months as it does today. That immutability is exactly what makes tags useful for releases - you can always get back to exactly what shipped.

When to use tags

  • Marking releases: annotated tags for versions like v2.0.0 with a message describing what changed
  • Hotfixes on old versions: tag the base commit before creating a hotfix branch (git checkout -b hotfix v1.0.0)
  • CI/CD triggers: push a tag to kick off a production deploy pipeline
  • Local bookmarks: lightweight tags work fine for "I need to come back here" moments during development

Do not tag work-in-progress. That is what branches are for.

How it works internally

Git stores tags in .git/refs/tags/. A lightweight tag is just a file containing the commit SHA. An annotated tag creates a separate tag object in the Git database with its own SHA, which then points to the commit. When you run git checkout v1.0.0, Git puts you in detached HEAD state - HEAD points directly to the commit hash, not to any branch name.

Common mistakes

Forgetting to push tags. Running git push after creating a tag does nothing for the tag. Your teammates pull and see no tags at all.

bash
# Wrong git tag v1.0.0 git push # tag stays local only # Right git push origin v1.0.0 # or push all local tags at once git push --tags

Committing in detached HEAD after a tag checkout. If you check out a tag and start committing, those commits belong to no branch. Switch away and they are gone.

bash
# Wrong git checkout v1.0.0 # ... make changes, commit ... git checkout main # commits are now dangling # Right git checkout -b hotfix-v1.0.0 v1.0.0 # now you are on a branch, commits are safe

Using lightweight tags for releases. A lightweight tag has no message, no author, no date. git show v1.0 just dumps the raw commit. Annotated tags give you context that is searchable and auditable. Teams that use lightweight tags for releases usually regret it six months later when they can not tell who tagged what or why.

Deleting a shared tag. Local delete (git tag -d v1.0.0) does not remove the tag from the remote. Remote delete (git push origin :refs/tags/v1.0.0) breaks CI pipelines and anyone who pinned to that tag. Coordinate with the team before touching published tags.

Real-world usage

  • npm: npm version patch automatically creates a v1.2.3 commit and tag in one step
  • React on GitHub: tags like v18.2.0 mark the exact commit before each npm publish
  • Linux kernel: tags like v6.1 and v6.2 mark merge windows for downstream distro builds
  • Docker Hub: a tag push can trigger an image build automatically
  • semantic-release: reads existing tags to compute the next version number

Follow-up questions

Q: What is the difference between a lightweight and an annotated tag?
A: A lightweight tag is a file containing a commit SHA. An annotated tag is a Git object with its own SHA, containing the tagger name, email, date, and message. Use git tag -a for anything shared or release-related.

Q: How do you push all tags at once?
A: git push --tags pushes all local tags. git push origin --follow-tags (Git 2.16+) pushes only annotated tags reachable from the current branch, which is the safer default for most workflows.

Q: What is detached HEAD when checking out a tag?
A: HEAD points directly to a commit SHA instead of a branch name. Any commits you make from that state belong to no branch and can be lost when you switch away.

Q: Can you move a tag to a different commit?
A: Locally: git tag -d v1.0.0 then recreate it. On a remote: delete with git push origin :refs/tags/v1.0.0, then push again. Both are disruptive on shared repos. Avoid it on anything already published.

Q: In a monorepo with dozens of packages, how do you tag individual package releases without tagging the whole repo?
A: Tools like Changesets or Lerna handle this. They create per-package tags in the format @org/package@v1.0.0 and run the tagging logic in CI, so you never manually tag individual packages.

Examples

Basic: Creating and inspecting an annotated tag

bash
git checkout main git pull origin main # Create annotated tag for a release git tag -a v2.0.0 -m "Major update: new auth system" # Inspect the tag object git show v2.0.0 # tag v2.0.0 # Tagger: Jane Dev <jane@example.com> # Date: Mon Oct 14 11:30:00 2024 # Major update: new auth system # # commit abc1234... # Push the tag to remote git push origin v2.0.0

The git show output includes tagger metadata you do not get from a lightweight tag. That matters when you are debugging what exactly shipped and who created the release.

Intermediate: Tag-triggered deploy in a Node.js project

bash
# Typical Express/Node workflow git checkout main git pull origin main # run tests first # npm version creates a commit + tag automatically npm version 4.18.2 -m "Release Express 4.18.2" # creates tag v4.18.2 # Push the commit and the tag together git push origin main --follow-tags # In your CI config (GitHub Actions): # on: # push: # tags: # - 'v*' # The deploy job only runs when a version tag is pushed

The --follow-tags flag pushes the branch commit and any reachable annotated tags in one shot. No separate step to remember, no accidentally leaving the tag local while the code is already on the remote.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.

Finished reading?