这里的问题是,就Git而言,deleting你删除的那一行是正确答案。
请记住,Git 的基本存储单元是提交。每个提交都有:
- 一些数据:所有文件的快照;和
- 一些元数据:关于提交的信息。这包括制作者、制作时间(日期和时间戳)以及原因(您或提交者的日志消息)。不过,对 Git 来说最后也是最重要的元数据是 parent 提交哈希 ID。
每个提交都有一个唯一的哈希 ID。这个哈希 ID 在你提交的那一刻被分配给提交。从那时起,该哈希 ID 将保留给 that 提交。只有该提交可以具有该 ID。1
同时,正如我们刚刚提到的,每个提交都可以在其元数据中存储一个哈希 ID。从技术上讲,每个提交都可以存储 Git 想要的任意数量的哈希 ID,但它们必须是已经存在的提交的哈希 ID。2 大多数提交只存储一个其他提交哈希 ID:父(单数) ) 的提交。 (合并提交存储两个,这就是使它们合并提交的原因,并且某人在一个新的、完全空的存储库中进行的第一次提交,不能有父提交——没有更早的提交可以引用——所以它只是没有。 t.)
那么,在你的情况下,你可能有一些更早的提交,或者没有。我们将只绘制一个假设您这样做的图表:
... <-F <-G <-H
哈希 ID 为 H(H 代表真实哈希 ID,看起来是随机的)的提交会记住其父级的哈希 ID,先前存在的提交 G,它会记住它的父F,等等。这些嵌入在每个提交的元数据中的向后指向的箭头是 Git 查找提交的方式——除了提交 H 本身,它是 last 提交。 p>
Git 查找任何分支的 last 提交的方式是分支 name,例如 master,保存提交的哈希 ID。所以为了使绘图更完整,让我们把它画进去。因为任何提交在我们完成后都不会改变,我们可以偷懒并停止绘制 那些 箭头作为箭头,只要我们记得他们向后指:
...--F--G--H <-- master
现在,让我们进行添加这个新文件file1.txt 的新提交。提交H 根本没有file1.txt — 它有一些其他文件,但没有file1.txt。我们git add file1.txt 并运行git commit 并提供日志消息。 Git 创建一个新的提交,它会获得一个新的唯一的大丑陋哈希 ID,但我们将称之为 I。 Git 将父级设置为H,以便I 指向H:
...--F--G--H <-- master
\
I
然后,作为git commit 的最后一步,Git 将I 的实际哈希ID 写入名称master:
...--F--G--H
\
I <-- master
(没有理由继续在单独的行上绘制I,所以我们不会。)
现在您编辑该文件,并按照通常的过程进行新的提交J。提交J 以I 作为其父级,Git 将J 的哈希ID 写入名称master:
...--F--G--H--I--J <-- master
这里没有什么可以合并,也就是说,你不能使用git merge 来做你想做的事。你有一个线性的提交链,以J 结尾。从J 回到I,从I 回到H,以此类推。
1从某种意义上说,哈希 ID 是在您提交之前 保留给该提交的——除了哈希 ID 本身取决于 确切的时间你做到了,一直到第二个。因此,如果您提前一秒或一秒后提交,它将具有不同的哈希 ID。在任何情况下,哈希 ID 都是唯一的:只有提交可以具有该哈希 ID。
如果 Git 无法提供唯一的哈希 ID,它不会让您进行提交!这实际上从未发生过,尽管这是理论上的可能性。另见How does the newly found SHA-1 collision affect Git?
2我们即将创建的新提交的哈希 ID 取决于其父提交的哈希 ID。因此,即使我们计算出如果其父提交是现有提交 X 的新提交将具有什么哈希 ID,对于任何 X,如果我们随后插入 this 在创建之前将提交的元数据散列到其中,它毕竟会获得一个 不同的 散列 ID。因此,提交不能可能引用自身,也不能允许在其中放置一些随机垃圾。因此,每次提交总是指一些较早的提交。
更简单地说,给定一个提交,你可以在时间上倒退到它的父级......但你只能在时间上倒退。你不能转发到它未来的孩子。
因此,您不能更改任何提交,也不能删除任何较早的提交而不删除所有后来的提交。 (Git 使删除提交变得特别困难。与 Mercurial 相比,您在其中运行 hg strip -r <rev> 并删除该提交 和 它的所有子项。您仍然无法选择子项,但是很容易取消提交。)
合并
Git 中的合并通常发生在我们有多个分支名称 时。让我们回到我们刚刚提交H 作为master 的最后一次提交的情况。 (我们可以使用git reset --hard HEAD~2 来实现这一点——这使得master 再次直接指向H,并且还设置了工作区——Git 的索引,以及我们可以看到文件的工作树——以反映提交@再次返回 987654364@。I 和 J 将继续存在,默认情况下,至少可以再检索 30 天。但我们会假装我们根本没有创建过 I 和 J。)所以我们有这个:
...--G--H <-- master
现在我们将创建一个或两个新分支。当我们这样做时,我们需要在我们的绘图中再添加一件事。如果只有一个分支名称master,那可能就是我们正在使用的分支。但是如果我们添加了dev 作为第二个名字呢?我们使用的是哪个名称?
Git 对此的回答是使用特殊名称HEAD。这个特殊名称通常附加到您的一个分支名称。 (它只能附加到一个或没有:永远不会超过一个。)我们将添加第二个分支名称 dev,但将 HEAD 附加到 master:
...--G--H <-- master (HEAD), dev
现在我们将以通常的方式创建新的提交 I 和 J。让我们把它们画进去:
I--J <-- master (HEAD)
/
...--G--H <-- dev
注意dev 没有移动:它仍然指向现有的提交H。名称 master 现在指向新提交 J。
现在,让我们在 dev 上创建两个提交。我们从git checkout dev 开始。这会将我们的 HEAD 附加到 dev,并提取提交 H 的内容以使用/打开:
I--J <-- master
/
...--G--H <-- dev (HEAD)
存储库中的 commits 没有改变!但是我们看到和使用的文件有,当前分支是dev,当前提交是H。3现在我们又做了两个新的提交。任何数字都是允许的,但两个使说明更容易:
I--J <-- master
/
...--G--H
\
K--L <-- dev (HEAD)
现在我们可以运行git merge。我们选择一个要使用的分支——git checkout master 或 git checkout dev——然后我们运行 git merge 并为其指定另一个分支的名称。4 让我们使用git checkout master 和git merge dev 所以HEAD 和当前提交标识 J 而不是 L:5
I--J <-- master (HEAD)
/
...--G--H
\
K--L <-- dev
Git 现在必须找到 both 分支上的 best 提交。在这种情况下,这很明显:它是提交H。我们从J 后退两步到达那里,我们从L 后退两步到达那里。如果底部的链更长,我们将不得不返回 3 或 4 步或许多步,但只要我们能够 to 提交H,提交H将是最好的共享提交。
Git 将这个共享的、最佳的提交称为 合并基础,我们和他们都从该提交开始。 合并基础提交是合并的关键。您(或 Git)通过查看图表来找到它,该图表显示了提交是如何连接的。
Git 现在将运行两个 git diff 操作:
-
git diff --find-renames <em>hash-of-H hash-of-J,找出在master 上我们 发生了什么变化,因为共享提交H;和
-
git diff --find-renames <em>hash-of-H hash-of-L,找出他们在dev 上更改了什么,因为共享提交H。
git merge 所做的是合并这些更改,然后将合并后的更改应用到提交H 中的快照——合并基础。这样我们就可以保留更改并添加更改。
这也是为什么合并大多是对称的。如果我们检查了dev,即提交L,然后运行git merge master,Git 仍然会发现共同提交H 作为合并基础。它将运行相同的两个git diff 命令(以另一个顺序,但谁在乎呢?)。然后它将这些差异组合成一个大的组合集,并将它们应用于来自提交H 的快照。结果是一样的。
如果我们的更改和它们的更改以某种方式重叠,Git 将声明一个合并冲突。在这种情况下,Git 不会自行完成合并。它会给你留下一个必须用手清理的烂摊子。没关系:您只需清理它,git add,然后提交(或运行 git merge --continue)即可完成工作。
为了完成这项工作,Git 将进行一个新的提交——我们称之为M,用于合并,因为我们巧妙地将之前的每个提交标记为H 到L——并更新当前分支名称像往常一样,我们检查的任何一个分支现在都以新的合并提交M 结束。为了将其标记为合并提交,Git 将其 两个 父级设置为 J,然后是 L,因为我们在开始时位于 J。所以我们可以得出结果:
I--J
/ \
...--G--H M <-- master (HEAD)
\ /
K--L <-- dev
我们进行了合并。合并时的快照是将来自H-vs-J 和来自H-vs-L 的组合更改应用到H 的结果。合并的 父母 像往常一样是上一个提交,以及我们在运行 git merge dev 时选择的另一个提交。
既然存在这种合并,尝试将L 甚至K 合并到master 中是不可能的。原因是L 和M 之间最好的共享 提交是提交L ...这已经是M 历史的一部分。如果我们沿着 bottom 行从M 后退,我们会到达L。历史记录(在 Git 中由提交组成,包括它们的连接)表明 L 已在此处合并。
3当你问 Git:HEAD 中有什么内容?你有两种表达方式。您可以问 Git:HEAD 中的 分支名称 是什么? 或者,您可以问:HEAD 中 commit 做了什么选择? 两个不同的问题得到两个不同的答案。在“分离的 HEAD”模式下,HEAD 没有附加到任何分支名称,第一个给你一个错误而不是答案。第二个问题几乎总是有效。
Git 还具有 未出生分支 的概念,当您开始使用一个完全没有提交的新的、完全空的存储库时,它需要它。在这种情况下,HEAD 存在,并且拥有一个分支名称,但分支名称本身不 存在并且无效。所以在这种特殊情况下,你可以问关于 HEAD 的“什么名字”问题,但不能问“什么 ID”问题:与分离的 HEAD 设置相反。
4事实上,git merge 通过提交哈希 ID 工作,所以我们可以给它任何我们想要的提交的哈希 ID。但通常我们——人类——按名字工作。
5合并结果一般每路都是一样的,只是先列出哪个父级。但是,如果我们对git merge 使用特定的标志参数,则合并结果可能会有所不同。
樱桃采摘
我们可以做一些事情。给定任何提交链——不管有没有像这样的分叉:
o--P--C--o--o <-- branch1
/
...--o--o
\
o--o--H <-- branch2 (HEAD)
或只是一个线性链,如:
...--o--o--P--C--o--o--H <-- branch (HEAD)
我们可以挑选出一些提交C,一个其父是P的孩子,并在其上运行git cherry-pick。 (通常你会在这里使用 C 的哈希 ID。)这样做会迫使 Git:
- 找到提交
P,C的父级:这很容易,因为C在其中包含P的哈希ID;
- 将
P 视为合并基础,将C 视为“他们的”提交,将当前提交H(由HEAD 选择)视为“我们的”提交,以及像往常一样进行全面的三向合并。
所以 Git 现在将比较 P 与 C 以查看“他们”做了什么,比较 P 与 H 以查看我们做了什么,并将这两组更改结合起来。然后,Git 会将合并的更改应用到P 中的快照。如果一切顺利,Git 将使用 C 的原始提交消息将生成的文件作为新的快照C'(提交C 的副本)提交,以此类推。它不会使这是一个合并提交,而只是一个普通的提交:
o--P--C--o--o <-- branch1
/
...--o--o
\
o--o--H--C' <-- branch2 (HEAD)
或:
...--o--o--P--C--o--o--H--C' <-- branch (HEAD)
从另一个分支中挑选提交往往更有意义,如上图所示;但是您可以从自己的历史记录中挑选一个提交,以重新应用相同的更改。如果在C 和C' 之间的某个提交 是在C 中发生的任何事情un-did 的提交,这将特别有用。6
6Git 有一个命令,git revert,可以进行此类提交。您将它指向某个孩子,Git 会执行与樱桃选择相同的三向合并,除了 merge base 这次是 C,而“他们的”提交是 @ 987654488@。 (和往常一样,ours / HEAD 提交是 HEAD 提交。) 练习:尝试按此顺序获取 C 与 P 的差异。如果您按顺序将这组更改与C 和HEAD 结合起来会发生什么?
请注意,所有这些操作都针对整个提交
您一开始想对一个文件大惊小怪。但是Git 在这里所做的一切——或者我们已经展示了Git 所做的——都是基于整个commits。那是因为提交确实是 Git 中的基本单元。确实,提交存储文件,但 Git 并不是真正关于 files。 Git 是关于提交。文件只是使提交有用的东西。
您可以从单个提交中提取单个文件,并处理它们:例如git diff,给定两个文件的名称,可以区分这两个文件。但这是使用 Git 的一种非典型方式。 Git 适用于一次提交操作。