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
Usage
git rebasetags <rebase args>
Installation
Get the script from github and make it executable.
To do it in one step, paste the following on your terminal
sudo wget https://raw.githubusercontent.com/nachoparker/git-rebasetags/master/git-rebasetags -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.
Details
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 README.md‘ ).
#!/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! Details: A new bash shell will be spawned in the case of interactive rebasing. Tags will be restored upon exiting that shell. Usage git-rebasetags <rebase args> More at https://ownyourbits.com/2017/08/14/rebasing-in-git-without-losing-tabs """ 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() NTAGS = len(TAGS) TAGHASHES = {} for line in TAGS: TAGHASHES[line.split()[1].split('/')[2]] = line.split()[0] TAGCOMMENTS = {} 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') pty.spawn('/bin/bash') else: break # 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 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # 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"
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.
One more question, I’m guessing this script was intended for lightweight tags, not annotated tags, correct? Thanks.
I use the following as a post-rewrite hook:
#! /bin/sh
[ “$1” = “rebase” ] || exit 0
set -e
once=false
shopt -s lastpipe
while read o n
do
git tag –points-at $o | while read t
do
$once || { echo >&2; once=true; }
echo “Rewrite tag: $t” >&2
git tag -f “$t” $n >/dev/null
done
done