DEV Community

Prasanth Vaaheeswaran
Prasanth Vaaheeswaran

Posted on • Originally published at Medium on

Under the sink: Removing remote git tags

TLDR; git push [remote-name] :refs/tags/[tag-name]

Job well done – move on. Butt wait, if you’re like me, you’re probably wondering: “wait what, what the hell is going on here?” So go ahead and picture a plumbers butt crack ‘cuz we goin’ to take a peek under the git sink.


Maybe not popular opinion, but one of my favorite parts about git is its commands. I think they’re pretty great, it’s one of the main reasons I us the terminal. However, I don’t stray too far away from the common git commands, which for me are the following:

git [push]|[pull]|[checkout]|[rebase]|[diff]|[log]

All (relatively) easy to understand and use. These are referred to as porcelain commands – they hide away the plumbing. The plumbing being all the parts tucked away that make git work and do the things behind the scenes we take for granted – sorta like the plumbing in your home.

So, when I needed to remove a tag that I had already pushed up to my remote, a task I’d done several times in the past, – I’d like to say I just remembered how to do it or figured it out on my own. I didn’t. I google it, in fact, I even remember googling it several times before. It’s just something about that command that didn’t sink in with me, probably because I never took the time to figure out why it worked. So, lets dive in.

git push [remote-name] :refs/tags/[tag-name]

Looks complex; lets break this down (lc;lbtd).

  • git
  • push
  • [remote-name] e.g origin

Okay.. so far so good…

  • :refs/tags/[tag-name] e.g :refs/tags/1.10.1

What – the – f%*#K? Okay I think I’ve identified where I need to focus.


Lets break this down further.

  • :
  • refs/tags/[tag-name]

Being slightly familiar with the internal file structure of the .git folder, which is in every repo, I can tell the second part looks like a path to the.git/refs folder. I also know the refs folder is where all the git references are stored. After reviewing the man pages for push (git help push), focusing on the options push accepts, I found out the colon is part of the refspec format. Meaning the entire part, :refs/tag/[tag-name], is a refspec. Before we discuss refspecs, lets go over what git references are.

Git references

As mentioned earlier, git references are stored in the /.git/refs folder. Here you will find several sub-folders, which git uses to group the references. But what are they?

$_: ls ./.git/refs/
heads remotes tags
Enter fullscreen mode Exit fullscreen mode

References in git, or “refs” as its commonly referred to in the documentation, refer to raw SHA-1 values. Branch names, tags , they are all just human friendly aliases for the full 40-character SHA-1 checksum (+header) hash. I won’t be going over what a content-addressable filesystem is or how that works, but know, git is one, and these hashes are how git is able to reference our data within it. Think of it as a key-value store, the ref being the key.

Git creates and updates these aliases all the time for us while we use it. Imagine having to type out the entire 40-character hash when you wanted to checkout or pull a branch? If you cat out a ref you will see this:

$_: cat ./.git/refs/heads/master 
Enter fullscreen mode Exit fullscreen mode

These folders also exist on the remote server. In fact if you ever want to backup your git repo, like really lo-fi, copy the.git folder.

The Refspec

A refspec is a format to specify the source and destination of, wait for it, references! Git automatically sets the refspec for each remote you add. If you are curious about what your refspecs are set to, take a look at this file ./.git/config in your repo.

$_: cat ./.git/config
        repositoryformatversion = 0
        filemode = true
        bare = false
        logallrefupdates = true
[remote "origin"]
        url =
        fetch = +refs/heads/*:refs/remotes/origin/*
[branch "master"]
        remote = origin
        merge = refs/heads/master
Enter fullscreen mode Exit fullscreen mode

Format: +<src>:<dst>

It’s important to understand that depending on if you’re pushing or pulling, the source and destination flip between local and remote. When you are pushing, the src is local and destination is remote, vice-versa if you are pulling.

Another important thing to know is, git expands the reference to its fully qualified name, it does this by using the destination set in the config for a remote (see how branch remote is set to “origin” in the above snippet). In fact git always makes this expansion. Ever try to push local branch that didn’t exist on your remote? :)

$_: git log -n 1 --oneline origin/master 
8d519a1 Remove vundle configs and set tab to 2
$_: git log -n 1 --oneline remotes/origin/master
8d519a1 Remove vundle configs and set tab to 2
$_: git log -n 1 --oneline refs/remotes/origin/master
8d519a1 Remove vundle configs and set tab to 2
Enter fullscreen mode Exit fullscreen mode


Oh and one more thing about the refspecs, pushing an empty <src> tells git to set the <dst> on the remote to nothing, which deletes  it.

So… git push origin :refs/tags/1.10.1

All the magic is in that refspec. Since pushing normally expands the refspec for you, which tries to match whatever is appropriate to what your current HEADis set to, here we are manually setting it to say: push nothing to the tag reference 1.10.1 at origin please. Which we now know deletes the tag on the remote.

Yeah I was definitely taking things for granted by not looking in to refspecs. I’m glad I did. The funny thing is, understanding whats going on here also explains some other errors that didn’t make sense to be before when I was trying to push or pull things. Nexy time I hit those, maybe I’ll write something up. I hope this also helps you.

Further reading


I didn’t cover what the + means in the refspec format. Its optional first of all, and tells git to update the reference even if it isn’t a fast-forward. I don’t want to go in to what that means, perhaps a another post for another day.

Top comments (0)