Skip to content

Instantly share code, notes, and snippets.

@emiller
Last active November 20, 2024 13:20
Show Gist options
  • Save emiller/6769886 to your computer and use it in GitHub Desktop.
Save emiller/6769886 to your computer and use it in GitHub Desktop.
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`"
@SQA777
Copy link

SQA777 commented Jun 14, 2021

I find this script very useful. Moved some tracked files to different directories w/o losing the files' Git logs.

However, when I tried to move a file from one directory to another, it didn't quite complete the move.

I get this message: "WARNING: Ref 'refs/heads/master' is unchanged"

Then Git shows the file has been moved to the new directory but Git status shows a pending "Deleted" status of the file in the old directory.

BTW, the Git repo had no pending commits for any of the tracked files when I invoked this script.

If I commit the changes that this script made to Git, the file in the new directory has no logs anymore for some reason. So I backed out the change by doing a git reset --hard <prev. commit hash>

Has anyone else encountered this problem?

@ivayloc
Copy link

ivayloc commented May 1, 2022

From what I understand this script should also move folders with files, when I run it bash git-mv-with-history src=apps\product-catalog\ this error is displayed:

git-mv-with-history: line 63: git: command not found
BusyBox v1.29.3 (2019-01-24 07:45:07 UTC) multi-call binary.

Usage: basename FILE [SUFFIX]

Strip directory path and .SUFFIX from FILE
git-mv-with-history: line 110: git: command not found

@DryreL
Copy link

DryreL commented Aug 4, 2022

git: 'rewrite-history' is not a git command. See 'git --help'.

@sekgobela-kevin
Copy link

This script knows its job. Thank you!

@yalamala1970
Copy link

I have been looking for this kind of script. We have some SQL scripts stored as ".txt" files in many directories. I am new to bash script. Is there any way I can rename all files with '.txt' extension to '.sql' in batch?

@Foadsf
Copy link

Foadsf commented Nov 20, 2024

Thanks for this script! However, it's worth noting that Git now officially recommends against using git filter-branch due to performance and safety concerns. From the official Git documentation:

WARNING: git filter-branch has a plethora of pitfalls that can produce non-obvious manglings of the intended history rewrite [...] Please use an alternative history filtering tool such as git filter-repo.

I've created a PowerShell prototype that uses git-filter-repo instead. It's still experimental but aims to:

  • Work cross-platform (Windows/Linux/macOS)
  • Use Git's built-in rename detection
  • Preserve file history properly using git-filter-repo

Would love to get feedback and suggestions for improvement if anyone wants to test it out!

@docbill
Copy link

docbill commented Nov 20, 2024

Anything like this with a date of 2013-09-29, I could consider more inspiration, not something one should consider using. Although, you can probably just drop it in chatgpt with a copy of this discussion, and chatgpt will output something ready to review and then debug.

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