51

I want to take a function out of one file and put it into another, but keep the blame history.

cp a.php b.php

vim b.php
# delete everything but 1 function

vim a.php
# delete the 1 function

git add a.php b.php
git commit

But if I run git blame b.php I only see it blaming to this new commit.

Paul Tarjan
  • 46,930
  • 58
  • 166
  • 211
  • Reverse operation - [Preserving Git history while merging files](https://stackoverflow.com/q/46611465) – Pang Mar 30 '22 at 10:06

4 Answers4

38

The general rule to maintaining blame history is to make a separate move commit first before any edits. It has been my experience that this allows git blame to work without the need for the -C option. So in the case of splitting the file up into new files, this can be done in two commits:

  1. Duplicate the original to the new destinations, making sure to delete the original
  2. Remove the extra sections from the duplicated files

In the example provided, this would be:

cp a.php b.php
mv a.php c.php
git add a.php b.php c.php
git commit
vim b.php  # delete everything but 1 function
vim c.php  # delete the 1 function
git add b.php c.php
git commit
vine77
  • 24,260
  • 1
  • 19
  • 12
  • The behavior of `git add` on a nonexistent file changed sometime around Git 1.9. You need either `git rm` or `git add -A` to reflect removed files. – Damian Yerrick Jun 25 '16 at 00:35
  • Thanks for the comment @DamianYerrick. I don't believe it should matter either way if you specify the exact files to stage though. (The change was that, as of git 2.0, "`git add ` is the same as `git add -A `" in that it includes removals according to the [release notes](https://git.kernel.org/cgit/git/git.git/plain/Documentation/RelNotes/2.0.0.txt)). – vine77 Jun 27 '16 at 18:37
9

I've slightly modified Peter's answer to another question 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"
REV=`git rev-parse HEAD`
git reset --hard HEAD^
git mv $1 temp
git commit -n -m "Split history $1 to $2"
git merge $REV
git commit -a -n -m "Split history $1 to $2"
git mv temp $1
git commit -n -m "Split history $1 to $2"

It simply copies the source file into a new file, and both files have the same history. An explanation why this works can be seen in that other answer

Lukas Eder
  • 196,412
  • 123
  • 648
  • 1,411
6

Perhaps this previous SO question could be informative:

How does git track source code moved between files?

To paraphrase the accepted answer: essentially, Git doesn't actually "store" moved code; when generating things like blames for moved code, that's done ex post facto by examining the state of the entire repository from commit to commit.

Community
  • 1
  • 1
Amber
  • 477,764
  • 81
  • 611
  • 541
  • 2
    This answer sounds like a "no," but really it's a "sometimes." The delete appears to be what triggers Git to look at a file for history beyond other files birthdates. Splitting off one function but keeping the rest of a file as OP did might not work. But I just split one file in half, deleting the original and giving it two new names, and the blame is correctly assigned throughout both new files. – Potatoswatter Dec 18 '12 at 02:49
  • Woops, that was only after editing. After committing it apparently lost the blame for one of the new files. Still possibly a maybe? – Potatoswatter Dec 18 '12 at 03:01
  • @Potatoswatter Try splitting each file in a separate branch, then merging the branches in. I think that should get git to recognize the "multiple copy". – DylanYoung Jan 29 '19 at 14:37
5

try git blame -C -C b.php

max
  • 32,545
  • 7
  • 69
  • 82