Skip to content

Instantly share code, notes, and snippets.

@ilanKeshet
Forked from emiller/git-mv-with-history
Last active January 3, 2023 16:37
Show Gist options
  • Star 10 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save ilanKeshet/bf4251b21919d341cf4431f89e77a8a5 to your computer and use it in GitHub Desktop.
Save ilanKeshet/bf4251b21919d341cf4431f89e77a8a5 to your computer and use it in GitHub Desktop.
git utility to move/rename file or folder and retain history with it.
#!/bin/bash
#
# credit: foked from https://gist.github.com/emiller/6769886 emiller/git-mv-with-history
#
# 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] -s/--start-commit 'SHA1' <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
#
# Change log:
# @content added --start-commit parameter limiting the range of operation. if not set, revision range defaults to 'HEAD..HEAD' i.e. noop
# @author ilankeshet
# @date 2017-01-31
#
function usage() {
echo "usage: `basename $0` [-d/--dry-run] [-v/--verbose] -s/--start-commit 'SHA1' <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=""
startCommit="HEAD"
repo=$(basename `git rev-parse --show-toplevel`)
while [[ $1 =~ ^\- ]]; do
case $1 in
-d|--dry-run)
dryrun=1
;;
-s|--start-commit)
shift
[ -z "$1" ] && echo "no start commit provided" && exit 1;
startCommit="$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: starting from $startCommit"
echo -e "$filter"
fi
[ $dryrun -eq 0 ] && git filter-branch -f --tree-filter "`echo -e $filter`" "${startCommit}..HEAD"
@ilanKeshet
Copy link
Author

added an option to specify commit range

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment