1. Introduction

Git tagging can be a very convenient feature for many use cases:

  • mark special commits
  • refer to commits with readable names
  • meta-organization
  • layering

However, the synchronization between local and remote tags isn’t always implicit. So, removing a local tag might not necessarily mean it’s deleted remotely.

In this tutorial, we explore ways to synchronize and delete local and remote Git tags. First, we acquire remote repositories via different means and monitor how the tags are replicated. After that, we explore ways to push tags upstream. Finally, we discuss how to delete both a tag from both the local and remote repositories.

For clarity, we use localrepo $ and remoterepo $ as the respective prompts when running commands in one or the other repository type.

We tested the code in this tutorial on Debian 12 (Bookworm) with GNU Bash 5.2.15. Unless otherwise specified, it should work in most POSIX-compliant environments.

2. Remote Tags to Local Repository

As already mentioned, the link between local and remote tags depends on our preferences and workflow. There doesn’t need to be a one-to-one relationship.

2.1. Clone and Fetch With Tags

To demonstrate, let’s clone a remote repository:

$ git clone https://github.com/git/git
Cloning into 'git'...
remote: Enumerating objects: 361226, done.
remote: Counting objects: 100% (1051/1051), done.
remote: Compressing objects: 100% (500/500), done.
remote: Total 361226 (delta 667), reused 824 (delta 549), pack-reused 360175
Receiving objects: 100% (361226/361226), 231.90 MiB | 16.66 MiB/s, done.
Resolving deltas: 100% (271697/271697), done.

This operation automatically names the remote as origin. So, let’s check the metadata of origin:

$ git remote show origin
* remote origin
  Fetch URL: https://github.com/git/git
  Push  URL: https://github.com/git/git
  HEAD branch: master
  Remote branches:
    jch    tracked
    maint  tracked
    master tracked
    next   tracked
    seen   tracked
    todo   tracked
  Local branch configured for 'git pull':
    master merges with remote master
  Local ref configured for 'git push':
    master pushes to master (up to date)

At this point, we can see also –list the available –tags:

$ git tag --list
gitgui-0.10.0
gitgui-0.10.1
gitgui-0.10.2
gitgui-0.11.0
[...]

Evidently, a cloned repository includes full tag information.

2.2. Fetch Without Tags

Now, let’s retry the cloning manually.

First, we initialize a new repository and add a remote manually:

$ git init && git remote add origin https://github.com/git/git

After that, we issue a manual fetch command:

$ git fetch
remote: Enumerating objects: 361198, done.
remote: Counting objects: 100% (1970/1970), done.
remote: Compressing objects: 100% (1419/1419), done.
remote: Total 361192 (delta 667), reused 1743 (delta 549), pack-reused 359222
Receiving objects: 100% (361192/361192), 231.53 MiB | 8.45 MiB/s, done.
Resolving deltas: 100% (271657/271657), done.
[...]

Here, we can already see a discrepancy in the number of objects when compared to the full clone we did earlier.

To verify, let’s perform the fetch with the –tags option:

$ git fetch --tags
remote: Enumerating objects: 34, done.
remote: Counting objects: 100% (14/14), done.
remote: Total 34 (delta 14), reused 14 (delta 14), pack-reused 20
Unpacking objects: 100% (34/34), 50.88 KiB | 1.50 MiB/s, done.
From https://github.com/git/git
 * [new tag]               junio-gpg-pub -> junio-gpg-pub
 * [new tag]               v1.4.4.5      -> v1.4.4.5

Notably, two tags were added that didn’t exist before.

In summary, remote add –fetch, clone, and fetch –tags ensure all tags are present, regardless of any references to them. On the other hand, the default fetch operation only acquires referenced tags.

3. Push Local Tags to Remote

Conversely, if we have local tags, [push]ing doesn’t necessarily synchronize them with the remote repository.

3.1. Set Up Repositories

To begin with, let’s create the remoterepo repository:

$ mkdir remoterepo && git init remoterepo

Then, we create and commit a new file to it:

$ echo 'content' > file && git add file && git commit --all --message 'init commit'

Although it’s in a local directory, we can use remoterepo as a remote for a new localrepo repository by just cloning it:

$ git clone remoterepo localrepo
Cloning into 'localrepo'...
done.
$ cd localrepo

Now, let’s verify origin for localrepo:

$ git remote show origin
* remote origin
Fetch URL: /home/baeldung/remoterepo
Push  URL: /home/baeldung/remoterepo
  HEAD branch: master
  Remote branch:
    master tracked
  Local branch configured for 'git pull':
    master merges with remote master
  Local ref configured for 'git push':
    master pushes to master (up to date)

Thus, we have remoterepo as the origin of localrepo with direct access to both repositories.

3.2. Tagging

Since it’s cloned, localrepo* currently has all tags that are available in *remoterepo. However, there aren’t any at this stage, as we can verify via the tag –list and the log subcommand:

localrepo $ git tag --list
localrepo $ git log --all --decorate --oneline --graph
* 2355cc8 (HEAD -> master, origin/master, origin/HEAD) major edits
* 3a5377c minor edit
* 5a43687 init commit

So, let’s create a local tag and verify we can see it:

localrepo $ git tag minor 3a5377c
localrepo $ git log --all --decorate --oneline --graph
* 2355cc8 (HEAD -> master, origin/master, origin/HEAD) major edits
* 3a5377c (tag: minor) minor edit
* 5a43687 init commit

Of course, remoterepo doesn’t include these changes yet. Let’s push them:

localrepo $ git push
Everything up-to-date
localrepo $ git push --tags
Total 0 (delta 0), reused 0 (delta 0), pack-reused 0
To /home/baeldung/remoterepo
 * [new tag]         minor -> minor

Critically, we need the –tags flag to push local tags remotely.

Now, we see the tag in remoterepo as well:

remoterepo $ git tag --list
minor
remoterepo $ git log --all --decorate --oneline --graph
* 2355cc8 (HEAD -> master) major edits
* 3a5377c (tag: minor) minor edit
* 5a43687 init commit

As we can see, the repositories are in sync.

4. Delete Remote Tag

We can now check ways to delete a remote tag while leaving the local one.

4.1. push Empty Reference

The push subcommand enables linking between different refs (references):

$ git push origin <SOURCE_REF>:<DEST_REF>

So, *one way to destroy a remote tag is by [push]ing an empty reference to it*:

$ git push origin :refs/tags/<TAG>

In our case, we can replace TAG with minor:

localrepo $ git push origin :refs/tags/minor
To /home/baeldung/remoterepo
 - [deleted]         minor

Although we can omit refs/tags/ as a prefix, doing so might result in a branch deletion due to the common namespace.

Even so, this syntax is fairly obscure.

4.2. Use –delete With push

Alternatively, we can use the –delete flag of the push subcommand:

localrepo $ git push --delete origin minor
To /home/baeldung/remoterepo
 - [deleted]         minor

Further, we can append all tags we want deleted to the end of the command.

Let’s verify the remote repository tags:

remoterepo $ git tag --list
remoterepo $ git log --all --decorate --online --graph
* 2355cc8 (HEAD -> master) major edits
* 3a5377c minor edit
* 5a43687 init commit

As expected, the minor tag is gone.

4.3. Delete Local Tag

In both cases above, the local tag remains intact:

localrepo $ git tag --list
minor
localrepo $ git log --all --decorate --oneline --graph
* 2355cc8 (HEAD -> master, origin/master, origin/HEAD) major edits
* 3a5377c (tag: minor) minor edit
* 5a43687 init commit

So, if we want to completely remove the tag so it doesn’t get pushed upstream, we also –delete it from the local repository:

localrepo $ git tag --delete minor
Deleted tag 'minor' (was 3a5377c)

Interestingly, we can delete all tags, if necessary:

localrepo $ git tag --delete $(git tag)

Either way, even if we push local changes, the remote repository won’t lose any tags. Still, the local tags are now gone.

5. Summary

In this article, we talked about tag synchronization and ways to delete remote and local tags.

In conclusion, how we delete a tag depends on the requirements, but generally involves a push for remote tags.