GIT copy file preserving history

I have a somewhat confusing question in GIT. Lets say, I have a file dir1/A.txt committed and git preserves a history of commits

Now I need (for some reasons) to copy the file into dir2/A.txt (not move but copy). I know that there is a git mv command but I need dir2/A.txt to have the same history of commits as dir1/A.txt, and dir1/A.txt to still remain there.

I'm not planning to update A.txt once the copy is created and all the future work will be done on dir2/A.txt

I know it sounds confusing, I'll add that this situation is on java based module (mavenized project) and we need to create a new version of code so that our customers will have the ability to have 2 different versions in runtime, the first version will be removed eventually when the alignment will be done. We can use maven versioning of course, I'm just newbie in GIT and curious about what git can provide here.

Answers


Unlike subversion, git does not have a per-file history. If you look at the commit data structure, it only points to the previous commits and the new tree object for this commit. No explicit information is stored in the commit object which files are changed by the commit; nor the nature of these changes.

The tools to inspect changes can detect renames based on heuristics. E.g. "git diff" has the option -M that turns on rename detection. So in case of a rename, "git diff" might show you that one file has been deleted and another one created, while "git diff -M" will actually detect the move and display the change accordingly (see "man git diff" for details).

So in git this is not a matter of how you commit your changes but how you look at the committed changes later.


All you have to do is move it in parallel to two different locations, merge, and then move one copy back to the original location, and you are done.

You can then run git blame on either copy, with no special options, and see the best historical attributions for both. You can also run git log on the original and see a full history.

In detail, suppose you want to copy foo/ in your repository to bar/. Do this (note I'm using -n to commit to avoid any rewriting, etc. by hooks):

git mv foo bar
git commit -n
SAVED=`git rev-parse HEAD`
git reset --hard HEAD^
git mv foo foo-magic
git commit -n
git merge $SAVED # This will generate conflicts
git commit -a -n # Trivially resolved like this
git mv foo-magic foo
git commit -n

Note: I used git 2.1.4 on Linux

Why this works

You will end up with a revision history like this:

  ORIG_HEAD
    /    \
SAVED  ALTERNATE
    \    /
    MERGED
      |
   RESTORED

When you ask git about the history of 'foo', it will (1) detect the rename from 'foo-magic' between MERGED and RESTORED, (2) detect that 'foo-magic' came from the ALTERNATE parent of MERGED, and (3) detect the rename from 'foo' between ORIG_HEAD and ALTERNATE. From there it will dig into the history of 'foo'.

When you ask git about the history of 'bar', it will (1) notice no change between MERGED and RESTORED, (2) detect that 'bar' came from the SAVED parent of MERGED, and (3) detect the rename from 'foo' between ORIG_HEAD and SAVED. From there it will dig into the history of 'foo'.

It's that simple. You just need to force git into a merge situation where you can accept two traceable copies of the file(s), and we do this with a parallel move of the original (which we soon revert).


Simply copy the file, add and commit it:

cp dir1/A.txt dir2/A.txt
git add dir2/A.txt
git commit -m "Duplicated file from dir1/ to dir2/"

Then the following commands will show the full pre-copy history:

git log --follow dir2/A.txt

To see inherited line-by-line annotations from the original file use this:

git blame -C -C -C dir2/A.txt

Git does not track copies at commit-time, instead it detects them when inspecting history with e.g. git blame and git log.

Most of this information comes from the answers here: Record file copy operation with Git


For completeness, I would add that, if you wanted to copy an entire directory full of controlled AND uncontrolled files, you could use the following:

git mv old new
git checkout HEAD old

The uncontrolled files will be copied over, so you should clean them up:

git clean -fdx new

I've slightly modified Peter's answer here to create a reusable, non-interactive shell script called git-split.sh:

#!/bin/sh

if [[ $# -ne 2 ]] ; then
  echo "Usage: git-split.sh original copy"
  exit 0
fi

git mv "$1" "$2"
git commit -n -m "Split history $1 to $2 - rename file to target-name"
REV=`git rev-parse HEAD`
git reset --hard HEAD^
git mv "$1" temp
git commit -n -m "Split history $1 to $2 - rename source-file to temp"
git merge $REV
git commit -a -n -m "Split history $1 to $2 - resolve conflict and keep both files"
git mv temp "$1"
git commit -n -m "Split history $1 to $2 - restore name of source-file"

In my case, I made the change on my hard drive (cut/pasted about 200 folders/files from one path in my working copy to another path in my working copy), and used SourceTree (2.0.20.1) to stage both the detected changes (one add, one remove), and as long as I staged both the add and remove together, it automatically combined into a single change with a pink R icon (rename I assume).

I did notice that because I had such a large number of changes at once, SourceTree was a little slow detecting all the changes, so some of my staged files look like just adds (green plus) or just deletes (red minus), but I kept refreshing the file status and kept staging new changes as they eventually popped up, and after a few minutes, the whole list was perfect and ready for commit.

I verified that the history is present, as long as when I look for history, I check the "Follow renamed files" option.


Need Your Help

How to work with android xml in graphical layout using eclipse?

android eclipse android-layout

I have an iMac with 4GB of RAM and dual core cpu but when I try to work with android xml putting text, images, button, and playing with alignments in Relative layout it work so slow and finally if ...

Error with Win32 Window Wrapper (Incorrect Parameter)

c++ winapi this wrapper

I am writing a window wrapper for win32 to make gui creation easier. I have an abstractdisplay class, a displayclass class, and a displayclass. In the end, the window does not show up. After some