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.

Bash
# 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.

Bash
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):

Bash
git log --oneline

Expected Output (hashes will vary):

Bash
<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:

Bash
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:

Bash
git tag

Expected Output:

Bash
v0.9-lw

To see what a lightweight tag points to, git show will show the commit information:

Bash
git show v0.9-lw

Expected Output: (Will show details of commit C2)

Bash
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:

Bash
git tag -a v1.0 -m "Version 1.0 release. Stable and ready."
# This tags the current HEAD commit (C3)

List tags:

Bash
git tag

Expected Output:

Bash
v0.9-lw
v1.0

View information about the annotated tag:

Bash
git show v1.0

Expected Output:

Bash
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:

Bash
git tag -a v0.1-alpha e4f5g6h -m "Alpha release of initial app structure"

List tags:

Bash
git tag

Expected Output:

Bash
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:

Bash
git tag -l "v0.*"

Expected Output:

Bash
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.

Bash
# 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):

Bash
git push origin v1.0

Expected Output:

Bash
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:

Bash
git push origin --tags

Expected Output (will push v0.1-alpha and v0.9-lw if not already pushed):

Bash
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 configure push.followTags to true (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:

Bash
git tag -d v0.9-lw
# or git tag --delete v0.9-lw

Expected Output:

Bash
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:

Bash
git push origin --delete v1.0
# Or the older, more cryptic syntax:
# git push origin :refs/tags/v1.0

Expected Output:

Bash
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.

Bash
git checkout v0.1-alpha

Expected Output:

Bash
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:
Bash
# 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:

Bash
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:

  1. Ensure your project has at least three commits. If not, add a new commit to app.js on the main branch.
  2. Create a lightweight tag named lw-checkpoint on the second commit in your history.
  3. Create an annotated tag named v1.1 on the latest commit of main, with the message “Version 1.1 – Added new feature X”.
  4. List all your tags.
  5. Display the information for the v1.1 annotated tag.
  6. Push only the v1.1 tag to your origin remote.
  7. Push all remaining local tags to origin.

2. Managing and Correcting Tags:

  1. Suppose you realize v1.1 should have pointed to the commit before the latest one.
  2. Delete the v1.1 tag locally.
  3. Delete the v1.1 tag from the origin remote.
  4. Find the commit hash of the commit before the current HEAD on main.
  5. Re-create the annotated tag v1.1 on that correct earlier commit with the same message “Version 1.1 – Added new feature X”.
  6. Push the corrected v1.1 tag to origin.
  7. Check out the v1.1 tag. What state is Git in? Create a new branch named maintenance/v1.1 from this tag. Switch back to main.

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.
  • 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>.
  • 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

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top