Git doesn't have file history.
Git stores commits, and commits are history. They are the only history there is. (I say it's not file history because it's commit history.) Each commit has a parent commit, or if the commit is a merge, two parents (or potentially more than two if it's an octopus merge).
Other than having a parent, each commit is a stand-alone snapshot of all the files that are in that commit. There's no history here: it's just a snapshot. If you want to see what happened between the previous commit and the current commit, you have Git extract the previous commit (snapshot O for Old) and the current commit (snapshot N for New) and run diff O N. That's what changed: whatever is different between O and N.
You can ask Git to synthesize a file history, but it does so by a horrible hack: it looks for one particular changed file, in each commit, as it goes back through commit history. It prints commits where that commit changes the file when compared to that commit's parent. If the file name changes—if the commit renamed the file—and you have used --follow
, Git changes which (single) file name it's looking for, so now it's looking under the previous name.
If you have a history consisting of a sequence of commits:
(history starts here, at a root commit)
|
v
o--o--<branches and merges...>--o <-- end
and a second history:
o--o--<branches and merges...>--o <-- end
o--o--...--o <-- end2
^
|
(we want to replace this one)
in a single repository, you can write a "replacement" commit object (using git replace
) that is just like the second root commit that we want to replace, except for one thing: it has, as its parent commit, the commit to which end
points.
This replacement commit effectively splices the two histories together.
Repeat this as desired for as many splices as you would like to add, for as many separate commit chains as you have in a single repository. Then you can run git filter-branch
over this repository, which copies every commit, but follows the replacements. This has the effect of cementing the grafts in place. See What does git filter-branch with no arguments do? or Rebase entire git branch onto orphan branch while keeping commit tree intact for example.