Git 中存在合并冲突时保留两个二进制文件

Keep both binary files when there is a merge conflict in Git

免责声明

我意识到我正在将 git 用于并非真正设计的用途。但我 所以 接近让它做我想做的事。如果你有更好的想法让我知道...

TL;DR

我想在合并冲突时保留两个二进制文件。我看到了一个答案 here 但我不认为它解决了我的具体问题,至少,我无法弄清楚它是如何完成的。

问题

我在一个 master 分支上有数百个小的 (~4kb) 二进制文件——每个文件都是一个 sheet 音乐文件。每首乐曲在完成之前都需要经过各个阶段:格式化、添加和弦、修改歌词、由#1 人员修改、由#2 人员修改等。使用一些批处理文件,我可以简单轻松地编写和解析提交消息生成一种报告。 Git 似乎是一个很好的解决方案,可以通过编程方式跟踪每首歌曲的状态。同样重要的是,我保留每首歌曲的完整更改历史记录并能够轻松查看历史记录(tortoisegit 使我能够做到这一点 - 右键单击​​文件并选择“git show日志").

每次修改文件时,都会提交更改(即每次提交都表示一个已更改的文件)。假设我有两首歌,A 和 B(实际上有 400 多首)。有多个提交表示歌曲 A 的更改,多个提交表示歌曲 B 的更改,并且更改分布在整个 master 分支上,如下所示:

A1 - A2 - B1 - A 3 - B2

现在假设用户对歌曲 A 和 B 进行了更改并将其推送到远程,但我也在处理歌曲 A 和 B 并尝试在我的基础上进行更改,如下所示:

远程:
A1 - A2 - B1 - A3 - B2 - 他们的 B3 - 他们的 A4
|| ||
本地: V V
A1 - A2 - B1 - A3 - B2 - 我的 B3 - 我的 A4

经典的合并冲突场景,对吧?

我怎样才能得到类似下面的结果?

远程:
A1 - A2 - B1 - A3 - B2 - 他们的 B3 - 我的 B3 - 他们的 A 4 - 我的 A4

本地:
A1 - A2 - B1 - A3 - B2 - 他们的 B3 - 我的 B3 - 他们的 A 4 - 我的 A4

我已经尝试了所有可以在网上找到的组合和可能的解决方案(在此过程中我学到了很多关于 git 的知识)但似乎无法破解这个。感谢像您这样的编程向导的任何帮助。我希望问题足够清楚。

您需要重命名其中之一或两者。同一目录下的两个文件不能重名。发生冲突时,有3种选择。

# 1. rename theirs only
# 1.1 choose our version for B3
git checkout --ours B3
# 1.2 rename their version to their.B3 for example
git show FETCH_HEAD:B3 > their.B3
# 1.3 add changed files
git add B3 their.B3
# 1.4 end the merge process
git merge --continue

# 2. rename ours only
# 2.1 choose their version for B3
git checkout --theirs B3
# 1.2 rename our version to our.B3 for example
git show HEAD:B3 > our.B3
# 1.3 add changed files
git add B3 our.B3
# 1.4 end the merge process
git merge --continue

# 3. rename both
# 3.1 rename theirs to their.B3
git show FETCH_HEAD:B3 > their.B3
# 3.2 rename ours to our.B3
git show HEAD:B3 > our.B3
# 3.3 add changed files
git add our.B3 their.B3
# 3.4 remove B3
git rm B3
# 3.5 end the merge process
git merge --continue

让我们先介绍一些背景信息。

Git commits store snapshots of your all files.1 也就是说,每个提交都有一个完整的副本构成存储在该提交中的每个文件的字节。任何一个特定提交中的文件的名称都带有嵌入的斜杠,例如 path/to/file.ext。每个提交中的副本都以特殊的、只读的、Git-only 格式存储。提交中的副本是去重的(因此,如果你进行一个新的提交,只是重复使用一些以前的文件,你实际上不会得到一个新的副本)——这是因为它实际上是不可能的在文件存储后更改。但是您计算机上的大多数程序无法使用内部 Git-only 文件,因此要 使用 这些文件,您必须将它们解压缩,这就是 git checkout或者(因为 Git 2.23)git switch 对你有用。

您的计算机通常会将这些文件保存在目录(或文件夹,如果您喜欢该术语)中,这样您最终会得到 path 包含 to 其中包含 file.ext。有些计算机在文件名大小写方面存在一些问题,例如,它们无法同时存储 README.TXT readme.txt。但是,即使 Linux 系统通常没有这个问题,实际上也无法将您的 B 他们的 B 存储在一个名称下 B。同样,Git 不能使用一个 name 在一个提交中存储两个不同的 files:任何给定提交中的每个文件都必须有自己独特的名字。

最初,这不是问题:您和您的同事只有一个 A 和一个 B 等等。如果只有一个人 更改 它,您和他们将选择更新的 AB,并且更新的文件进入较新的提交。您的计算机可以使用的工作树副本只是一个副本;当您进行新提交时,新提交会根据需要存储新的(或重新使用旧的)冻结的 Git 化版本。但是当你们都改变其中之一时......好吧,这将我们带到合并和合并冲突中。


1这是提交的 data 部分。提交还存储 元数据 ,这是有关提交本身的信息,例如提交人、时间和原因:您的日志消息。在元数据中,每个提交也存储其 parent 提交的哈希 ID。对于合并提交,提交存储每个父项的 ID(复数)。但在这里,我们最感兴趣的只是数据。


合并发现差异

当你 运行:

git checkout somebranch       # or git switch somebranch
git merge otherbranch

Git找到最佳共同祖先提交,然后运行两个git diff查看什么文件 改变了(以及如何),以及什么文件 他们 改变了(以及如何)。对于文本文件,Git 将“以及如何”部分转换为文本差异,然后尝试合并这两个差异。由于您的文件是二进制文件,Git 不能这样做,甚至可能不会尝试。 :-)

你的工作树,唉,只能容纳文件B一份。但是 Git 并不是真的 使用 您的工作树副本。那只是为了。 Git 正在内部使用 committed 份冻结格式的副本。 Git 将这些存储在 Git 所谓的 索引 暂存区 或(现在很少) 缓存。通常,索引只包含任何一个文件的一个冻结格式副本。但是在合并冲突期间,Git 扩展了索引。

此时,Git 有文件 B 三个 个活动副本,而不是一个或两个。 B 有这三个不同的副本——你们都从合并基础共享的副本,然后是你的版本和你试图合并的两个提交的版本——这就是为什么有三个副本,事实上 三份副本是 Git 表示合并冲突的方式。

此时,您的工作是安排索引仅保存每个文件的 一个 副本,准备提交。

您将必须重命名至少一个文件

所以你现在有一个合并冲突,三个副本一些二进制文件——我将继续称它为B——在三个提交中,现在复制到 Git 的索引中。2 Git 为您提供了一种方便的方法来提取这三个副本中的两个:

git checkout --ours B

和:

git checkout --theirs B

这两个命令将文件的两个非合并基础副本从索引复制到您的工作树。所以:

git checkout --ours B; mv B "My B"; git add "My B"
git checkout --theirs B; mv B "Their B"; git add "Their B"
git rm --cached B

将告诉 Git 首先提取您的 B 版本,然后您将其重命名并添加为 My B。然后你 Git 提取他们的 B 版本,重命名并添加它。最后,你告诉 Git 解决文件 B 三个版本之间冲突的正确方法是删除文件 B.

您由此做出的新提交将有两个重命名的 B 副本作为其快照的一部分,并且根本没有原始 B


2从技术上讲,Git 的索引中的内容实际上并不是 副本。它是 Git 使用的去重冻结格式 blob 对象 的哈希 ID。但是您可以将其视为副本;它像一个一样工作。