57

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
  • 48,968
  • 59
  • 172
  • 213
  • Reverse operation - [Preserving Git history while merging files](https://stackoverflow.com/q/46611465) – Pang Mar 30 '22 at 10:06

6 Answers6

42

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
  • 26,728
  • 1
  • 20
  • 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
13

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
  • 211,314
  • 129
  • 689
  • 1,509
  • Thanks -- worked like a charm, including repeating 3 times to split one large file into 4 (last iteration can just use `git mv $1 $2` to rename the original file) – jessexknight May 29 '23 at 18:45
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
  • 507,862
  • 82
  • 626
  • 550
  • 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
6

try git blame -C -C b.php

max
  • 33,369
  • 7
  • 73
  • 84
0

Just an FYI, I've published an NPM package (which you could call directly via the npx command, which does the splitting (duplicating) of the file for you. It's a bit slower than a shell script, but if you're working with npm, it's easily executable, without the need of creating the file or for checking in the file in your git repo, in order to share it with your team mates.

https://www.npmjs.com/package/swgh

You would do

npx swgh myFile.txt myDuplicatedFileWithHistory.someOtherExtensionIfYouWantTo
0

this version is my iteration on Lukas' answer

it is improved by adding specific files by name, and uses git reset --soft, so there can be files in the working directory, but they won't be affected by git-split.sh.

#!/bin/sh

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

git mv $1 $2
git add $2
git commit -m "renamed: $1 -> $2"
git branch temp-git-split
git reset HEAD~1 --soft
git mv $2 temp-git-split-file
git commit -m "renamed: $1 -> temp-git-split-file"
git merge temp-git-split
git add temp-git-split-file
git add $2
git rm $1
git commit -m "merging history"
git branch -d temp-git-split
git mv temp-git-split-file $1
git commit -m "renamed: temp-git-split-file -> $1"
Eric
  • 16,397
  • 8
  • 68
  • 76