Chapter 9: Tagging Your Releases in Git
Chapter Objectives
By the end of this chapter, you will be able to:
- Define what Git tags are and understand their utility for marking specific points in history, especially releases.
- Differentiate between lightweight tags and annotated tags.
- Create lightweight tags using
git tag <tag-name>
. - Create annotated tags, including a tagging message, using
git tag -a <tag-name> -m "message"
. - List existing tags in your repository.
- Tag specific older commits in your project’s history.
- Share your tags with collaborators by pushing them to remote repositories.
- Delete tags both locally and from remote repositories.
- Check out the state of your project at a specific tag.
Introduction
Throughout the previous chapters, you’ve learned to navigate your project’s history using commit hashes and branch names. While branches are excellent for ongoing development and isolating work, they are designed to move as new commits are added. Sometimes, however, you need to mark a specific point in your history as being important and immutable—for example, a software release like version 1.0, a critical milestone, or a submitted assignment. This is where Git’s tagging feature comes into play.
Tags in Git allow you to assign a meaningful, human-readable name to a specific commit. Unlike branches, tags, once created, don’t typically move. This chapter will teach you how to create different types of tags, how to manage them, and how to share them with others by pushing them to remote repositories. Using tags effectively is a key practice for professional software development, making it easy to identify and retrieve specific versions of your project over time.
Theory
What is a Git Tag?
A Git tag is a reference that points to a specific commit in your repository’s history. Think of it like a permanent bookmark or a label you attach to a particular snapshot of your project. Once a commit is tagged, you can easily refer to that specific version of your code using the tag name instead of a long, cryptic SHA-1 commit hash.
Tags are commonly used to mark release points (e.g., v1.0, v2.0.1-beta). When you release version 1.0 of your software, you can tag the corresponding commit with v1.0. Later, if you need to check out the exact code for that release (perhaps to fix a bug specific to that version), you can easily do so using the tag.
Why Use Tags?
- Marking Releases: This is the most common use case. Tags like
v1.0.0
,v1.0.1
,v2.0-rc1
provide clear markers for released versions of software. - Identifying Milestones: You can tag important internal milestones or specific versions that were deployed to a testing environment.
- Historical Reference: Tags provide stable, human-readable names for specific points in history, making it easier to understand the evolution of a project.
- Reproducibility: Easily retrieve the exact state of your codebase for a particular release or version.
Types of Tags
Git supports two main types of tags:
Lightweight Tags:
- A lightweight tag is simply a pointer to a specific commit—like a branch that doesn’t move.
- It stores no extra information beyond the tag name and the commit it points to.
- It’s essentially a named SHA-1 hash.
- You create these if you just want a temporary label or don’t need the extra information stored by annotated tags.
Annotated Tags:
- Annotated tags are stored as full objects in Git’s database. They are checksummed and contain more information.
- They include:
- The tagger’s name and email.
- The date the tag was created.
- A tagging message (similar to a commit message, explaining what this tag represents).
- Optionally, they can be signed with GnuPG (GPG) for verification.
- It is generally recommended to use annotated tags for public releases or important milestones because they contain this valuable metadata.
%%{ init: { "theme": "base", "themeVariables": { "primaryColor": "#EDE9FE", /* Default node color - Light Purple */ "primaryTextColor": "#5B21B6", "primaryBorderColor": "#5B21B6", "lineColor": "#5B21B6", /* Arrow color - Purple */ "textColor": "#1F2937", /* General text */ "fontSize": "13px", "fontFamily": "Open Sans", "commitFill": "#DBEAFE", /* Commit Object - Light Blue */ "commitStroke": "#2563EB", "commitColor": "#1E40AF", "tagObjectFill": "#FEF3C7", /* Tag Object - Light Amber */ "tagObjectStroke": "#D97706", "tagObjectColor": "#92400E", "tagNameFill": "#D1FAE5", /* Tag Name - Light Green */ "tagNameStroke": "#059669", "tagNameColor": "#065F46" }, "flowchart": { "htmlLabels": true, "nodeSpacing": 50, "rankSpacing": 60, "curve": "basis" } } }%% graph LR; subgraph Lightweight Tag direction LR LW_TagName["<div style='padding:8px; text-align:center;'>🏷️<br><b>Tag Name</b><br>(e.g., v0.9-lw)</div>"]; LW_Commit["<div style='padding:8px; text-align:center;'>📦<br><b>Commit Object</b><br><code style='font-size:0.9em;'>SHA-1: a1b2c3d</code></div>"]; LW_TagName -- "Points directly to" --> LW_Commit; end subgraph Annotated Tag direction LR Anno_TagName["<div style='padding:8px; text-align:center;'>🏷️<br><b>Tag Name</b><br>(e.g., v1.0)</div>"]; Anno_TagObject["<div style='padding:5px; text-align:left; font-size:0.9em;'>📜<br><b>Tag Object</b> (SHA-1: t1a2g3)<br>- Tagger: Your Name<br>- Date: 2025-05-16<br>- Message: \<i>Version 1.0 release\</i><br>- Points to Commit: <code style='color:#1E40AF;'>e4f5g6h</code></div>"]; Anno_Commit["<div style='padding:8px; text-align:center;'>📦<br><b>Commit Object</b><br><code style='font-size:0.9em;'>SHA-1: e4f5g6h</code></div>"]; Anno_TagName -- "Points to" --> Anno_TagObject; Anno_TagObject -- "Contains pointer to" --> Anno_Commit; end style LW_TagName fill:#D1FAE5,stroke:#059669,stroke-width:1.5px,color:#065F46; style LW_Commit fill:#DBEAFE,stroke:#2563EB,stroke-width:1.5px,color:#1E40AF; style Anno_TagName fill:#D1FAE5,stroke:#059669,stroke-width:1.5px,color:#065F46; style Anno_TagObject fill:#FEF3C7,stroke:#D97706,stroke-width:1.5px,color:#92400E; style Anno_Commit fill:#DBEAFE,stroke:#2563EB,stroke-width:1.5px,color:#1E40AF; classDef default fill:#transparent,stroke:#1F2937,stroke-width:1px,color:#1F2937,font-family:'Open Sans';
Tag Naming Conventions
While Git is flexible, good tag names make your repository easier to understand. A common convention, especially for software releases, is Semantic Versioning (SemVer). SemVer uses a MAJOR.MINOR.PATCH
format (e.g., v1.0.0
, v2.3.1
).
MAJOR
version when you make incompatible API changes,MINOR
version when you add functionality in a backward-compatible manner, and- PATCH version when you make backward-compatible bug fixes.
- Often prefixed with a v (e.g., v1.0.0).
Other conventions exist, but the key is to be consistent within your project.
How Tags are Stored
Tags are stored as references in your Git repository, typically within the .git/refs/tags/
directory. An annotated tag creates a new tag object, while a lightweight tag is just a file in this directory whose content is the commit SHA-1.
Sharing Tags
By default, when you git push
, tags are not sent to the remote repository. You need to explicitly push tags if you want to share them with others or have them available on your remote server.
Command | Description | Example |
---|---|---|
git tag | Lists all existing tags in alphabetical order. | git tag |
git tag -l “pattern” (or –list) |
Lists tags that match the given glob pattern. | git tag -l “v1.*” |
git tag <tag-name> [commit-sha] | Creates a lightweight tag. If [commit-sha] is omitted, tags HEAD. | git tag v0.9-lw a1b2c3d git tag quick-label (tags HEAD) |
git tag -a <tag-name> -m “message” [commit-sha] | Creates an annotated tag with a tagging message. If [commit-sha] is omitted, tags HEAD. | git tag -a v1.0 -m “Release v1.0” git tag -a v1.0.1 abc1234 -m “Hotfix for v1.0” |
git show <tag-name> | Displays information about the tag and the commit it points to. For annotated tags, shows tagger info and message. | git show v1.0 |
git push <remote> <tag-name> | Pushes a specific tag to the specified remote repository. | git push origin v1.0 |
git push <remote> –tags | Pushes all local tags to the specified remote repository that are not already on the remote. | git push origin –tags |
git tag -d <tag-name> (or –delete) |
Deletes a tag from the local repository. | git tag -d old-tag |
git push <remote> –delete <tag-name> (or git push <remote> :refs/tags/<tag-name>) |
Deletes a tag from the specified remote repository. | git push origin –delete v0.9-beta |
git checkout <tag-name> | Switches the working directory to the state of the specified tag. Puts Git in a “detached HEAD” state. | git checkout v1.0 (To work from here: git switch -c new-branch-from-tag) |
git tag -f <tag-name> [commit-sha] (or –force) |
Forces creation or update of a tag, even if it already exists. Use with caution, especially if the tag is shared. | git tag -f -a v1.0 -m “Updated v1.0” new_commit_sha |
Practical Examples
Setup:
Let’s create a new repository for these examples or use an existing one with a few commits.
# Create a new project directory
mkdir my-tagged-project
cd my-tagged-project
git init
# Make a few commits
echo "Initial content for our app." > app.js
git add app.js
git commit -m "C1: Initial commit with app.js"
echo "function main() { console.log('Version 0.9'); }" >> app.js
git add app.js
git commit -m "C2: Add main function, work towards v0.9"
echo "console.log('App starting...'); main();" >> app.js
git add app.js
git commit -m "C3: Finalize v1.0 features"
Ensure your user.name
and user.email
are configured as per Chapter 2.
1. Listing Tags: git tag
The git tag
command, with no arguments, lists all existing tags in alphabetical order.
git tag
Expected Output: (No output yet, as we haven’t created any tags)
2. Creating a Lightweight Tag
Let’s create a lightweight tag v0.9-lw pointing to the second commit (“C2: Add main function, work towards v0.9”).
First, find the hash of the commit you want to tag (C2):
git log --oneline
Expected Output (hashes will vary):
<hash_C3> (HEAD -> main) C3: Finalize v1.0 features
<hash_C2> C2: Add main function, work towards v0.9
<hash_C1> C1: Initial commit with app.js
Let’s say <hash_C2> is a1b2c3d.
Create the lightweight tag:
git tag v0.9-lw a1b2c3d # Replace a1b2c3d with your actual hash for C2
If you omit the commit hash, git tag <tagname> will tag the commit that HEAD is currently pointing to.
List tags again:
git tag
Expected Output:
v0.9-lw
To see what a lightweight tag points to, git show
will show the commit information:
git show v0.9-lw
Expected Output: (Will show details of commit C2)
commit a1b2c3d...
Author: Your Name <youremail@example.com>
Date: ...
C2: Add main function, work towards v0.9
... diff output ...
Notice there’s no separate tagger information for a lightweight tag.
3. Creating an Annotated Tag
Annotated tags are generally preferred for releases. Let’s tag the latest commit (C3) as v1.0.
Create an annotated tag using the -a (annotate) and -m (message) flags:
git tag -a v1.0 -m "Version 1.0 release. Stable and ready."
# This tags the current HEAD commit (C3)
List tags:
git tag
Expected Output:
v0.9-lw
v1.0
View information about the annotated tag:
git show v1.0
Expected Output:
tag v1.0
Tagger: Your Name <youremail@example.com>
Date: Fri May 16 00:05:00 2025 +0300
Version 1.0 release. Stable and ready.
commit <hash_C3> (HEAD -> main, tag: v1.0)
Author: Your Name <youremail@example.com>
Date: ...
C3: Finalize v1.0 features
... diff output ...
Explanation of Output:
- It first shows the tag object’s information (tagger, date, tag message).
- Then, it shows the information for the commit that the tag points to (
<hash_C3>
).
4. Tagging an Older Commit (Annotated)
You can tag any commit in your history, not just the latest one. Let’s say we want to retroactively tag the first commit (C1) as v0.1-alpha.
Get the hash of the first commit (C1) from git log –oneline. Let’s assume it’s e4f5g6h.
Create an annotated tag for that commit:
git tag -a v0.1-alpha e4f5g6h -m "Alpha release of initial app structure"
List tags:
git tag
Expected Output:
v0.1-alpha
v0.9-lw
v1.0
5. Listing Tags with Patterns
If you have many tags, you can list them with a pattern using -l
or --list
:
git tag -l "v0.*"
Expected Output:
v0.1-alpha
v0.9-lw
6. Sharing Tags (Pushing to a Remote)
Tags are not automatically pushed to a remote repository when you git push
. You need to push them explicitly.
a. Setup for Remote:
Let’s quickly set up a bare “remote” repository like we did in previous chapters.
# In the parent directory of my-tagged-project:
# cd ..
mkdir central-tag-repo
cd central-tag-repo
git init --bare project-tags.git
cd ../my-tagged-project # Go back to our working repo
# Add this as a remote
git remote add origin ../central-tag-repo/project-tags.git
b. Pushing a Single Tag
To push a specific tag (e.g., v1.0):
git push origin v1.0
Expected Output:
Total 0 (delta 0), reused 0 (delta 0), pack-reused 0
To ../central-tag-repo/project-tags.git
* [new tag] v1.0 -> v1.0
c. Pushing All Tags
To push all your local tags that are not yet on the remote:
git push origin --tags
Expected Output (will push v0.1-alpha
and v0.9-lw
if not already pushed):
Total 0 (delta 0), reused 0 (delta 0), pack-reused 0
To ../central-tag-repo/project-tags.git
* [new tag] v0.1-alpha -> v0.1-alpha
* [new tag] v0.9-lw -> v0.9-lw
Tip: If you use
git push
without--tags
or a specific tag name, tags are not sent. If you want tags to be pushed whenever you push a branch that contains tagged commits, you can configurepush.followTags
totrue
(git config --global push.followTags true
). However, explicit pushing of tags (--tags
) is often preferred for clarity.
7. Deleting Tags
a. Deleting a Tag Locally
To delete a tag from your local repository:
git tag -d v0.9-lw
# or git tag --delete v0.9-lw
Expected Output:
Deleted tag 'v0.9-lw' (was <hash_of_commit_it_pointed_to>)
Verify: git tag
will no longer list v0.9-lw
.
b. Deleting a Tag Remotely
Deleting a tag locally does not remove it from remote servers. To delete a tag from a remote:
git push origin --delete v1.0
# Or the older, more cryptic syntax:
# git push origin :refs/tags/v1.0
Expected Output:
To ../central-tag-repo/project-tags.git
- [deleted] v1.0
Now, if someone else clones or fetches from origin, they won’t get the v1.0 tag (or it will be removed from their remote-tracking tags if they update).
Warning: Deleting tags, especially from a remote, should be done with caution if others might be using them. It’s a form of rewriting history for tags.
8. Checking Out Tags
You can check out a tag to see the state of your project at that specific point. This is useful for inspecting an old release or building a specific version.
git checkout v0.1-alpha
Expected Output:
Note: switching to 'v0.1-alpha'.
You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by switching back to a branch.
If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -c with the switch command. Example:
git switch -c <new-branch-name>
Or undo this operation with:
git switch -
Turn off this advice by setting config variable advice.detachedHead to false
HEAD is now at <hash_C1> C1: Initial commit with app.js
Explanation of “Detached HEAD”:
- When you check out a tag (or a commit hash directly),
HEAD
no longer points to a local branch but directly to a commit. This is called a “detached HEAD” state. - You can look around and make experimental changes.
- If you make new commits in this state, they don’t belong to any branch. If you switch to another branch before creating a new branch for these commits, they can become “orphaned” and eventually garbage collected (lost).
- If you need to make changes starting from a tag (e.g., a hotfix for an old release): It’s best practice to create a new branch from the tag immediately:
# After git checkout v0.1-alpha
git switch -c hotfix/v0.1.1_critical_fix
# Now you are on a new branch 'hotfix/v0.1.1_critical_fix'
# and can make commits safely on this branch.
To go back to your main line of development:
git switch main
OS-Specific Notes
The core Git tagging commands (git tag, git show <tag>, git push <remote> <tag>) are consistent and behave identically across Windows, macOS, and Linux. There are no significant OS-specific differences for the functionalities covered in this chapter.
If you were to delve into GPG signing of tags (an advanced topic not covered here), the setup and usage of GPG tools would be OS-specific, but the Git commands themselves (git tag -s …) would remain the same.
Common Mistakes & Troubleshooting Tips
Git Issue / Error | Symptom(s) | Troubleshooting / Solution |
---|---|---|
Forgetting to push tags | Tags exist locally but not on the remote. Collaborators/builds don’t see them. | Solution: Push explicitly: git push origin <tagname> or git push origin –tags. |
Trying to move a tag (without force) | Error when trying git tag <existing-tag> <new-commit>. | Solution: Tags are fixed. To “move”, delete old tag (local/remote), then create new tag (or use -f with caution, preferably create a new version like v1.0.1). |
Tag deleted locally, not remotely | Tag still exists on remote, may reappear locally on fetch. | Solution: Delete from remote too: git push origin –delete <tagname>. |
Confusing tag types | Lightweight tag used for release; git show <tag> lacks tagger info/message. | Solution: Prefer annotated tags (git tag -a) for releases/public markers. Lightweight for private/temporary labels. |
Committing in Detached HEAD | New commits made after git checkout <tagname> are not on any branch and can be lost. | Solution: After git checkout <tagname>, immediately create a new branch if making changes: git switch -c <new-branch> <tagname>. |
Tag name already exists | Error: tag ‘<tagname>’ already exists when trying to create a tag. | Solution: Choose a unique tag name. If you intend to replace/update the tag (use with caution), delete the old one first or use git tag -f <tagname>. |
Exercises
Use the my-tagged-project
repository from the Practical Examples.
1. Tagging Workflow:
- Ensure your project has at least three commits. If not, add a new commit to
app.js
on themain
branch. - Create a lightweight tag named
lw-checkpoint
on the second commit in your history. - Create an annotated tag named
v1.1
on the latest commit ofmain
, with the message “Version 1.1 – Added new feature X”. - List all your tags.
- Display the information for the
v1.1
annotated tag. - Push only the
v1.1
tag to yourorigin
remote. - Push all remaining local tags to
origin
.
2. Managing and Correcting Tags:
- Suppose you realize
v1.1
should have pointed to the commit before the latest one. - Delete the
v1.1
tag locally. - Delete the
v1.1
tag from theorigin
remote. - Find the commit hash of the commit before the current
HEAD
onmain
. - Re-create the annotated tag
v1.1
on that correct earlier commit with the same message “Version 1.1 – Added new feature X”. - Push the corrected
v1.1
tag toorigin
. - Check out the
v1.1
tag. What state is Git in? Create a new branch namedmaintenance/v1.1
from this tag. Switch back tomain
.
Summary
Tags are invaluable for marking specific, important points in your project’s history, especially for releases:
- Tags are named pointers to specific commits. Unlike branches, they generally don’t move.
- Two types of tags:
- Lightweight Tags (
git tag <name> [commit]
): Simple pointers, no extra metadata. - Annotated Tags (
git tag -a <name> -m "message" [commit]
): Full Git objects storing tagger info, date, message. Recommended for releases.
- Lightweight Tags (
git tag
: Lists all local tags. Use-l <pattern>
to filter.git show <tagname>
: Displays information about the tag and the commit it points to.- Tags can be created on any commit in history, not just
HEAD
. - Sharing Tags: Tags are not pushed by default.
git push <remote> <tagname>
: Pushes a specific tag.git push <remote> --tags
: Pushes all local tags not on the remote.
- Deleting Tags:
- Locally:
git tag -d <tagname>
. - Remotely:
git push <remote> --delete <tagname>
.
- Locally:
- git checkout <tagname>: Switches your working directory to the state of that tag, putting you in a “detached HEAD” state. Create a new branch if you intend to make commits from that point.Using tags consistently helps maintain a clear and understandable history for your projects, making version management and release tracking much more straightforward.
Further Reading
- Pro Git Book:
- Chapter 2.6 Git Basics – Tagging: https://git-scm.com/book/en/v2/Git-Basics-Tagging
- Official Git Documentation:
git-tag(1)
: https://git-scm.com/docs/git-tag
- Semantic Versioning (SemVer): https://semver.org/ (A widely adopted standard for version numbering that pairs well with tagging).
