Featured image of post Git - preserve history when moving files

Git - preserve history when moving files

When moving files/directories around in a git repo, the version history of the file gets lost! Let’s fix that.

While working on my vSummary github repo, I came to a point where I had decided to change the folder structure of it. At this point I had about 60-ish commits to it. So I began by using the git mv command to move and rename directories then finished off with the usual commit and push commands. However, when I took a look at some of the individual files in the repo I noticed that the commit history for them were gone. While github still had all my commits, the history for individual files/directories seemed to have been erased with the restructuring commit. I did not expect that, but according to github, this is the expected behaviour: Why does github not track renames

Fortunately, I was able to fix this by rolling back my latest commit and using this gist script to preserve the history of my restructuring: https://gist.github.com/emiller/6769886.js

# 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"

repo=$(basename `git rev-parse --show-toplevel`)

while [[ $1 =~ ^\- ]]; do
  case $1 in


      usage "invalid argument: $1"


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\

[ -z "$filter" ] && usage

if [[ $dryrun -eq 1 || ! -z $verbose ]]; then
  echo "tree-filter to execute against $repo:"
  echo -e "$filter"

[ $dryrun -eq 0 ] && git filter-branch -f --tree-filter "`echo -e $filter`"

Once you are finished with your history rewrites, you can force a push like: git push origin master --force

Licensed under CC BY-NC-SA 4.0
comments powered by Disqus
Built with Hugo, using a modified version of the Stack theme.