Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
git utility to move/rename file or folder and retain history with it.
#!/bin/bash
#
# git-mv-with-history -- move/rename file or folder, with history.
#
# Moving a file in git doesn't track history, so the purpose of this
# utility is best explained from the kernel wiki:
#
# Git has a rename command git mv, but that is just for convenience.
# The effect is indistinguishable from removing the file and adding another
# with different name and the same content.
#
# https://git.wiki.kernel.org/index.php/GitFaq#Why_does_Git_not_.22track.22_renames.3F
#
# While the above sucks, git has the ability to let you rewrite history
# of anything via `filter-branch`. This utility just wraps that functionality,
# but also allows you to easily specify more than one rename/move at a
# time (since the `filter-branch` can be slow on big repos).
#
# Usage:
#
# git-rewrite-history [-d/--dry-run] [-v/--verbose] <srcname>=<destname> <...> <...>
#
# After the repsitory is re-written, eyeball it, commit and push up.
#
# Given this example repository structure:
#
# src/makefile
# src/test.cpp
# src/test.h
# src/help.txt
# README.txt
#
# The command:
#
# git-rewrite-history README.txt=README.md \ <-- rename to markdpown
# src/help.txt=docs/ \ <-- move help.txt into docs
# src/makefile=src/Makefile <-- capitalize makefile
#
# Would restructure and retain history, resulting in the new structure:
#
# docs/help.txt
# src/Makefile
# src/test.cpp
# src/test.h
# README.md
#
# @author emiller
# @date 2013-09-29
#
function usage() {
echo "usage: `basename $0` [-d/--dry-run] [-v/--verbose] <srcname>=<destname> <...> <...>"
[ -z "$1" ] || echo $1
exit 1
}
[ ! -d .git ] && usage "error: must be ran from within the root of the repository"
dryrun=0
filter=""
verbose=""
repo=$(basename `git rev-parse --show-toplevel`)
while [[ $1 =~ ^\- ]]; do
case $1 in
-d|--dry-run)
dryrun=1
;;
-v|--verbose)
verbose="-v"
;;
*)
usage "invalid argument: $1"
esac
shift
done
for arg in $@; do
val=`echo $arg | grep -q '=' && echo 1 || echo 0`
src=`echo $arg | sed 's/\(.*\)=\(.*\)/\1/'`
dst=`echo $arg | sed 's/\(.*\)=\(.*\)/\2/'`
dir=`echo $dst | grep -q '/$' && echo $dst || dirname $dst`
[ "$val" -ne 1 ] && usage
[ ! -e "$src" ] && usage "error: $src does not exist"
filter="$filter \n\
if [ -e \"$src\" ]; then \n\
echo \n\
if [ ! -e \"$dir\" ]; then \n\
mkdir -p ${verbose} \"$dir\" && echo \n\
fi \n\
mv $verbose \"$src\" \"$dst\" \n\
fi \n\
"
done
[ -z "$filter" ] && usage
if [[ $dryrun -eq 1 || ! -z $verbose ]]; then
echo
echo "tree-filter to execute against $repo:"
echo -e "$filter"
fi
[ $dryrun -eq 0 ] && git filter-branch -f --tree-filter "`echo -e $filter`"
@silverdr

This comment has been minimized.

Copy link

silverdr commented Jun 9, 2014

I still have to test it but if this really works "as advertised" then you're my saviour man!

@silverdr

This comment has been minimized.

Copy link

silverdr commented Jun 9, 2014

BTW, I forked it and corrected the comment lines to reflect the current filename of this utility but I see no way to make a pull request.. so you may want to correct it yourself..

@reiabreu

This comment has been minimized.

Copy link

reiabreu commented Jul 2, 2014

Worked perfectly. Thank you!

@ankur-gupta

This comment has been minimized.

Copy link

ankur-gupta commented Feb 11, 2015

This is an excellent tool. Very nicely written! I did not believe how well it worked in so many cases. Thank you very much. This should be a part of git.

I did find one case in which this script did not work: when has a space in it. Inspite of using "" or \ or other tricks it still fails. This is most likely in line #82: for arg in $@; do.

@amedranogil

This comment has been minimized.

Copy link

amedranogil commented Jun 1, 2015

I had commented line 89 :

[ ! -e "$src"   ] && usage "error: $src does not exist" 

because I had to rename old (no longer existing) folder in history.
It worked perfectly! THANKS!

@fvilpoix

This comment has been minimized.

Copy link

fvilpoix commented Aug 27, 2015

Absolutly great tool, thank you !

@KimJejun

This comment has been minimized.

Copy link

KimJejun commented Nov 25, 2015

great!

@nitu1234

This comment has been minimized.

Copy link

nitu1234 commented Dec 8, 2015

Great!!! It worked...Thannks a lot

@yonimor

This comment has been minimized.

Copy link

yonimor commented Jan 7, 2016

Thanks, great job!

@yuri-1218inc

This comment has been minimized.

Copy link

yuri-1218inc commented Mar 8, 2016

Amazing. Sensational script, and after an hour or so of looking through blogs, etc., to get this done right... it is done right.

@appleboy

This comment has been minimized.

Copy link

appleboy commented Apr 13, 2016

Nice work.

@fspafford

This comment has been minimized.

Copy link

fspafford commented May 2, 2016

Would this work if the new file or folder were in a different Git repository?

@docbill

This comment has been minimized.

Copy link

docbill commented Jul 29, 2016

Nice idea, but REALLY REALLY slow. I have a repo with 12000 commits and it took it several hours. I have found an alternative method to complete the same update in a couple of minutes, which I will harden into a script very similar to yours.

@Mukarr

This comment has been minimized.

Copy link

Mukarr commented Aug 4, 2016

Once I have rewritten the history with git-mv-with-history <srcname>=<destname>, I am unable to commit as
git status says
Your branch and 'origin/master' have diverged, and have 1004 and 1004 different commits each, respectively.

@jarzuaga

This comment has been minimized.

Copy link

jarzuaga commented Nov 10, 2016

I have the same iisue as @Mukarr. @docbill would you mind sharing your alternative method?

@pvuaccruent

This comment has been minimized.

Copy link

pvuaccruent commented Nov 14, 2016

@Mukarr & @jarzuaga. Whenever you use "git filter-branch", history is rewritten, so that is expected behavior. Either push those new commits to a new repo, or force push it to overwrite an existing one.

@ilanKeshet

This comment has been minimized.

Copy link

ilanKeshet commented Jan 31, 2017

Many thanks 👍

@emiller, everybody
file-git-mv-with-history gist fork which supports setting revision range https://gist.github.com/ilanKeshet/bf4251b21919d341cf4431f89e77a8a5#file-git-mv-with-history

git-rewrite-history [-d/--dry-run] [-v/--verbose] -s/--start-commit 'SHA1' <srcname>=<destname> <...> <...>

@rbhatti4

This comment has been minimized.

Copy link

rbhatti4 commented May 12, 2017

Awesome. What I needed. Thanks

@gitmvissue

This comment has been minimized.

Copy link

gitmvissue commented Sep 25, 2017

Thank you for this script. It creates .git-rewrite directory? what do i need to do with this? Also is force push the only option to push in this changes? "git status" only shows that .git-rewrite is added.

@marcaurele

This comment has been minimized.

Copy link

marcaurele commented Oct 5, 2017

You don't really loose the history of a file when moving it. If you use --follow parameter you'll get the history commits from before the move: git log --follow <file_name> . Github does not use this parameter for file history, that's sad.

@yashrajsingh

This comment has been minimized.

Copy link

yashrajsingh commented Dec 18, 2017

Hm, the script was not working for me. I was getting this error:

/usr/local/git/libexec/git-core/git-filter-branch: line 370: eval: -e: invalid option
eval: usage: eval [arg ...]
tree filter failed: -e 

To fix this, from the Last line of this script remove the -e, i.e Change this echo -e $filter to echo $filter

Also, this utility doesn't work for moving folders.

@manu-technology

This comment has been minimized.

Copy link

manu-technology commented Jan 6, 2018

git mv old_folder new_folder
Worked great for me.
git status displayed all my files as renamed

Post commit I could view the history for any file on github and gitgui tool :)

@alexrosano

This comment has been minimized.

Copy link

alexrosano commented Jan 12, 2018

Amazing script! Thanks for writing this.

@IanDarwin

This comment has been minimized.

Copy link

IanDarwin commented Feb 2, 2018

Thanks, works nicely (though I too had to remove the "-e").

@dcpc007

This comment has been minimized.

Copy link

dcpc007 commented Feb 16, 2018

@docbill hi, have you some details on the method you used on big repo ?
@emiller is there some "range" filter possible ? Or limit to a branch only ?

@hemikak

This comment has been minimized.

Copy link

hemikak commented Apr 17, 2018

Cannot merge into a branch after running the script:
xxx and yyy are entirely different commit histories.

@IlariExove

This comment has been minimized.

Copy link

IlariExove commented May 24, 2018

git log --follow is easier solution than this script, in my opinion.

@rquadling

This comment has been minimized.

Copy link

rquadling commented Sep 10, 2018

Unfortunately git log --follow is not a default position. So, if you are merging multiple repos into 1, then EVERY file is renamed in that process. This mechanism rewrites the history so that it was ALWAYS in the new location.

@fuyunv

This comment has been minimized.

Copy link

fuyunv commented Oct 20, 2018

Thanks. it helps me a lot.

@kfordaccela

This comment has been minimized.

Copy link

kfordaccela commented Jan 23, 2019

Is there a way to clean up the renames, essentially it's making my repository much bigger than it should be, I posted something on stackoverflow trying to get some help with the same question.

https://stackoverflow.com/questions/54318225/remove-history-for-files-not-in-masterhead

After doing a rename, I think that I should mention you should merge current state and then follow something like the following to ensure that the directory get's delete throughout history after the rename.

https://stackoverflow.com/questions/10067848/remove-folder-and-its-contents-from-git-githubs-history

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.