Git将两个文件合并为一个,并保留历史记录

11
假设您在git仓库中有两个文件,比如 A.txtB.txt。是否可以将这两个文件合并成第三个文件A+B.txt,同时删除原始的A.txtB.txt,并提交所有更改,以使历史记录得以保留?也就是说,如果我运行命令git log --follow A+B.txt,我会知道内容来源于A.txtB.txt文件吗?我尝试过将文件分别放在两个不同的分支中,然后将它们合并成一个新文件(同时删除旧文件),但没有成功。

你可以尝试将 A.txt 重命名为 A+B.txt,将 B.txt 的更改内容加入其中并删除 B.txt,最后提交更改。 - Derek
1
为什么不在创建A+B.txt时,在提交信息中注明它是A.txt和B.txt的连接呢? - user2031271
3个回答

12
长话短说,答案是“是的”!
全文参考雷蒙德·陈(Raymond Chen)的文章将两个文件合并成一个同时保留行历史记录
想象一下你有两个文件:fruitsveggies

git blame for both fruits and veggies

The naïve way of combining the files would be to do it in a single commit, but you'll lose line history on one of the files (or both)

You could tweak the git blame algorithms with options like -M and -C to get it to try harder, but in practice, you don’t often have control over those options (eg. the git blame may be performed on a server)

The trick is to use a merge with two forked branches

  • In one branch, we rename veggies to produce.
  • In the other branch, we rename fruits to produce.
git checkout -b rename-veggies
git mv veggies produce
git commit -m "rename veggies to produce"
git checkout -
git mv fruits produce
git commit -m "rename fruits to produce"

Then merge the first into the second

git merge -m "combine fruits and veggies" rename-veggies

This will generate a merge conflict - that's okay - now take the changes from each branch's Produce file and combine into one - here's a simple concatenation (but resolve the merge conflict however you please):

cat "produce~HEAD" "produce~rename-veggies" >produce
git add produce
git merge --continue

The resulting produce file was created by a merge, so git knows to look in both parents of the merge to learn what happened.

git blame for produce

And that’s where it sees that each parent contributed half of the file, and it also sees that the files in each branch were themselves created via renames of other files, so it can chase the history back into both of the original files.

Each line should be correctly attributed to the person who introduced it in the original file, whether it’s fruits or veggies. People investigating the produce file get a more accurate history of who last touched each line of the file.

For best results, your rename commit should be a pure rename. Resist the temptation to edit the file’s contents at the same time you rename it. A pure rename ensure that git’s rename detection will find the match. If you edit the file in the same commit as the rename, then whether the rename is detected as such will depend on git’s “similar files” heuristic.

查看完整文章以获得完整的逐步分解和更多解释


最初,我认为这可能是使用情况之一,需要使用git merge-file这样的工具来执行以下操作:

>produce echo #empty
git merge-file fruits produce veggies --union -p > produce
git rm fruits veggies
git add produce
git commit -m "combine fruits and veggies"

然而,这只是帮助模拟合并差异算法来比较两个不同文件的结果。当提交时,最终输出与手动更新文件并手动提交更改的结果相同。

将每个分支中的两个文件合并,以便合并时不会出现冲突。否则,每次变基都会重新引发冲突。 - Bruno Martinez
太棒了!但是如果你要将文件A合并到文件B中,而不重新命名文件B,你会怎么做?我是否需要先将目标文件重命名为其他名称,然后在单独的提交中进行操作?还是有更好的方式可以实现? - Hubro
@Hubro,诀窍在于你必须同时重命名A和B,这样它们都被视为父级,就没有单一的获胜者了。 - KyleMit
要查看完整的行历史记录,例如在拆分+重新连接文件后,请使用git blame -C40。要在TortoiseGitBlame窗口中实现这一点,可以通过设置“检测移动或复制的行”=“来自修改的文件”来完成。 - jifb

5
简短的回答是“不”(或许甚至是Mu)。在Git中,“历史记录”就是一系列提交。不存在所谓的“文件历史记录”:要么你有一个提交,要么没有,而该提交具有一个或多个父提交,或者没有。这意味着“文件历史记录”并不存在,然而git log --follow却存在。这是自相矛盾的:如果文件历史记录不存在,那么git log --follow如何生成文件历史记录呢?(但如果需要通过git blame获取合并文件的有用合成line历史记录,请参见KyleMit的答案。)
答案是 git log --follow 是有欺骗性的。它并没有真正找到文件的历史记录。它通过更改正在查找的文件(单个)名称来查找历史记录并构建子历史记录。它逐个提交地查看每个提交,并针对其父提交运行(加速,限制)git diff --find-renames。如果差异指出父提交中的文件X.txt被重命名为子提交中的A.txt,并且您正在运行git log --follow A.txt,则git log中的代码现在开始查找X.txt

由于没有代码可以同时查找多个文件,因此您无法让这个特定的欺骗方式适应您所需的情况,即从查找一个特定文件转变为查找多个文件。(实际上有两个问题。一个是由于内部实现相当有限,2git log --follow只能一次查看一个文件。另一个问题是重命名检测不包括“组合检测”:Git将执行复制查找的“分割检测”,启用了--find-copies--find-copies-harder。后者非常计算密集,而且两者在这里都是朝着错误的方向工作,尽管简单地通过反转差异的顺序就可以使它做正确的事情。)


1这意味着--follow默认情况下根本不查看合并差异。另请参见`git log --follow --graph`跳过提交

2也称为“廉价的黑客”。


1
看起来这个程序要创建相同的文件名并从一个分支合并到另一个分支,这样Git在分配逐行属性时会尝试查看合并的父级(参见下文)。 - KyleMit
2
@KyleMit:这真是个好的技巧。 它并没有让一个文件拥有两个文件历史记录 - 实际上根本没有真正的文件历史记录,git log在这里没有什么用处。 但它确实使得合并提交和其后的提交的git blame更加有用。 blame(或annotate)命令从commit历史中合成了line history,这使它能够更好地完成工作。 - torek

0

Raymond Chen所写并由KyleMit引用的文章是最好的答案。以下是一种解决方案,最终只保留了一半的行历史记录,但我将其保留供参考/教育。

不要合并分支,只需使用cherry-pick拉取提交即可。这仍会导致冲突需要解决,但结果将是一个单独的提交而没有合并提交,并且未来操作的历史记录更简单(以一个文件的行历史为代价)。

git checkout -b temp
git mv A.txt AB.txt
git commit -am "moving B to AB"
git switch main
git mv B.txt AB.txt
git commit -am "moving A to AB"
git cherry-pick temp

解决冲突

git add AB.txt
git cherry-pick --continue

AB.txt将保留责任历史记录

git blame AB.txt

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接