git, OYB software

Rebasing in git without losing tags

Rewriting commits, squashing them, eliminating them, forgeting blobs or just rebasing from master to make future merges less painful. Git comes equiped with all we need to rewrite history.

But! doing so will have the nasty side effect of changing our commit hashes, and our tags will be now referencing commits that are out of tree. As we want to make changes further and further away in the branch history, manually adjusting the tags to the new hashes can become really tedious.

Enter git rebasetags


git rebasetags <rebase args>


Get the script from github and make it executable.

To do it in one step, paste the following on your terminal

sudo wget -O /usr/local/bin/git-rebasetags
sudo chmod +x /usr/local/bin/git-rebasetags

If you don’t like installing to /usr/local/bin  using sudo,  just copy git-rebasetags  wherever you like. It will work as long as the file is in the $PATH with execute permissions.


In case we are doing interactive rebasing, we will be presented with a bash shell where we can make any manual changes and finish (or abort) the rebase operation. Whenever we exit that shell, our tags will be reassigned to the new hashes.

Matching is performed based on the commit line. This means that, if we change it, we will still have to do manual adjustments, and the same goes whenever we have different commits with the same comment ( for example ‘Update‘ ).

#!/usr/bin/env python3

Perform a git rebase operation, restoring original tags

Copyleft 2017 by Ignacio Nunez Hernanz <nacho _a_t_ ownyourbits _d_o_t_ com>
GPL licensed (see LICENSE file in repository root).
Use at your own risk!

    A new bash shell will be spawned in the case of interactive rebasing.
    Tags will be restored upon exiting that shell.

    git-rebasetags <rebase args>

More at

import sys
import pty
import os
from subprocess import run, PIPE

# Save tag-comment information
TAGS = run(['git', 'show-ref', '--tags'], stdout=PIPE).stdout.decode('utf-8').splitlines()

for line in TAGS:
    TAGHASHES[line.split()[1].split('/')[2]] = line.split()[0]

for tag, commit in TAGHASHES.items():
    OUT = run(['git', '--no-pager', 'show', '-s', '--format=%s', commit], stdout=PIPE)
    TAGCOMMENTS[tag] = OUT.stdout.decode('utf-8').rstrip()

# Interactive rebase
pty.spawn(['/bin/bash', '-c', 'git rebase ' + ' '.join(sys.argv[1:])])
pty.spawn(['/bin/bash', '-c', 'reset'])

while True:
    if os.path.exists('.git/rebase-merge'):
        print('\nRebase in progress. Type \'git rebase --abort\' to cancel. Type \'exit\' when you are done')

# Restore tags after rebase, matching comments
print('Restoring tags...')

OUT = run(['git', 'log', '--oneline'], stdout=PIPE).stdout.decode('utf-8').splitlines()

count = 0
for line in OUT:
    for tag, comment in TAGCOMMENTS.items():
        if comment == ' '.join(line.split()[1:]):
            commit = line.split()[0]
            print('tag ' + tag + ' -> ' + commit)
            run(['git', 'tag', '-d', tag], stdout=PIPE)
            run(['git', 'tag', tag, commit])
            count += 1

if count < NTAGS:
    print('WARNING: ' + str(NTAGS - count) + 'tags without a match')

# License
# This script is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
# This script is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this script; if not, write to the
# Free Software Foundation, Inc., 59 Temple Place, Suite 330,
# Boston, MA  02111-1307  USA

Keep in mind that we are still changing history, so forced pushes, merges, conflicts and such niceties will happen.
Remember to keep a checked out copy of the repo before trying this, and use with care.

Extra: using git-filter-branch

If you only want to do an action that can be automated with git-filter-branch, then you can use the –tag-name-filter option to restore tags to the each commit after the operation

git filter-branch --tag-name-filter cat --index-filter "git rm --cached --ignore-unmatch file.blob"



Author: nachoparker

Humbly sharing things that I find useful [ github dockerhub ]


  1. Hi. This script looks great. I had some trouble with it on Windows 7 (I know…). Seems to have trouble importing pty in python. I made some tweaks, but it ends up not running that interactive rebase in the background and checking for its completion, just runs synchronously in one thread, which messes up if there are conflicts, etc. I’ll see if I can do this on a later version of windows or another OS.

  2. One more question, I’m guessing this script was intended for lightweight tags, not annotated tags, correct? Thanks.

  3. I use the following as a post-rewrite hook:
    #! /bin/sh

    [ “$1” = “rebase” ] || exit 0

    set -e

    shopt -s lastpipe

    while read o n
    git tag –points-at $o | while read t
    $once || { echo >&2; once=true; }
    echo “Rewrite tag: $t” >&2
    git tag -f “$t” $n >/dev/null

Leave a Reply

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