From 16bd3d85974499b978c28d38b0c55fa55e4de693 Mon Sep 17 00:00:00 2001 From: Michiel Scholten Date: Sat, 22 Feb 2020 11:21:25 +0100 Subject: [PATCH] Script to move a file preserving Git history, rewriting history in the process --- bin/gitmv | 111 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100755 bin/gitmv diff --git a/bin/gitmv b/bin/gitmv new file mode 100755 index 0000000..82a7ea5 --- /dev/null +++ b/bin/gitmv @@ -0,0 +1,111 @@ +#!/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] = <...> <...> +# +# 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 +# @url https://gist.github.com/tupy/79ec62fee769d4049db2 +# + +function usage() { + echo "usage: `basename $0` [-d/--dry-run] [-v/--verbose] = <...> <...>" + [ -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`"