问题在于,对于Git来说,删除你删除的行是正确答案。
请记住,Git的基本存储单位是提交。每个提交都包括:
- 一些数据:所有文件的快照;和
- 一些元数据:关于提交的信息。这包括谁制作了它、何时(日期和时间戳)以及为什么(您或提交者的日志消息)。然而,对于Git来说,最后且最重要的元数据是父提交哈希值。
每个提交都有一个唯一的哈希ID。该哈希ID在您进行提交时分配给提交。从那时起,该哈希ID保留给该提交。只有该提交可以具有该ID。同时,正如我们刚才提到的,每个提交都能够在其元数据中存储一个哈希ID。从技术上讲,每个提交可以存储Git想要的任意多个哈希ID,但它们必须是已经存在的提交的哈希ID。大多数提交仅存储另一个提交哈希ID:父提交(单数)。(合并提交存储两个哈希ID,这就是使它们成为合并提交的原因,而在新的完全空的存储库中第一个提交没有父提交——没有早期提交可供参考——因此它根本不存在。)
在您的情况下,您可能有一些早期提交,也可能没有。我们将只绘制一个假设您有早期提交的图形:
... <-F <-G <-H
提交的哈希值为
H
(
H
代表真实的哈希值,看起来是随机的)记得其父提交
G
的哈希值,而
G
又记得其父提交
F
的哈希值,依此类推。这些向后指向的箭头被嵌入到每个提交的元数据中,这就是Git如何“查找”提交的方式。但对于提交
H
本身,它是最后一个提交。
Git查找任何分支的最后一个提交的方法是:该分支的名称,例如
master
,保存了提交的哈希值。所以为了使绘画更加完整,让我们在图中添加这一点。由于提交后不能更改任何内容,我们可以懒惰一些,停止将那些箭头作为箭头绘制,只要我们记住它们指向后面即可。
...--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将的实际哈希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
,再到 H
,依此类推。
1 从某种意义上讲,在创建提交之前哈希 ID 已经为该提交保留了——不过哈希 ID 本身取决于您 创建提交的确切时间,精确到秒。因此,如果您早了一秒或晚了一秒进行提交,它将具有不同的哈希 ID。无论如何,哈希 ID 是唯一的:只有该提交才能具有该哈希 ID。
如果 Git 找不到唯一的哈希 ID,则不会允许您进行提交!虽然这是一种理论可能性,但实际上从未发生过。另请参见How does the newly found SHA-1 collision affect Git?
2 我们即将创建的新提交的哈希 ID 取决于其父提交的哈希 ID。因此,即使我们确定正在创建的新提交将具有的哈希 ID(如果其父提交是现有提交 X),对于任何 X,如果我们在创建提交之前将此哈希 ID 插入到提交的元数据中,则最终会得到一个不同的哈希 ID。因此,提交不能引用自身,也不允许随意添加任何内容。因此,每个提交都始终引用某个较早的提交。
简而言之,给定一个提交,您可以向后移动到其父提交……但您只能向后移动,不能向前移动到其未来的子提交。
由于这个原因,您无法更改任何提交,并删除任何较早的提交也会同时删除所有较新的提交。(Git使删除提交变得特别困难。与 Mercurial 相比,您需要运行hg strip -r <rev>
,它将删除该提交及其所有子提交。您仍然不能选择子提交,但很容易删除一个提交。)
合并
通常情况下,在Git中进行合并是指我们有多个分支名称。让我们回到这样一种情况,即当我们在
master
上仅有提交
H
作为最后一次提交时。(我们可以使用
git reset --hard HEAD~2
来实现这一点 - 这使得
master
直接指向
H
,并且还设置了工作区 - Git的索引和我们可以查看文件的工作树 - 以再次反映提交
H
。
I
和
J
将继续存在,并且默认情况下,它们可以在至少30天内被检索到。但是我们假装从未创建
I
和
J
)因此我们有以下内容:
...--G--H <-- master
现在我们将创建一个或两个新分支。 在这样做时,我们需要向我们的图纸中添加一件事情。 如果只有一个分支名称
master
,那可能是我们使用的分支。 但是如果我们添加了
dev
作为第二个名称呢?我们使用哪个
名称?
Git 的答案是使用特殊名称
HEAD
。 这个特殊的名称通常被
附加 到你的一个分支名称上。(它只能附加到一个或零个:永远不会超过一个。) 我们将添加第二个分支名称
dev
,但是保留
HEAD
附加到
master
上:
...
现在我们将像往常一样创建新的提交I
和J
。让我们把它们画出来:
I
/
...
请注意,
dev
没有移动:它仍然指向现有提交
H
。名称
master
现在指向新的提交
J
。
现在,让我们在
dev
上创建两个提交。我们首先执行
git checkout dev
。这将把我们的 HEAD 附加到
dev
,并提取提交
H
的内容以进行操作/使用。
I
/
...
代码库中的 提交 没有改变!但我们看到和使用的文件已经改变,当前分支 是 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现在需要找到两个分支上最好的提交。在这种情况下,很明显:它是提交H。我们从J回退两步到达那里, 从L回退两步也可以到达那里。如果底部的链更长,我们就需要回退3或4或其他数量的步骤,但只要我们能够到达提交H,提交H就是最佳的共享提交。
Git将此共享的最佳提交称为“合并基础”。它是我们和他们都开始的关键提交。您(或Git)通过查看显示提交连接方式的图形来查找它。
Git现在将运行两个git diff操作:
1. `git diff --find-renames hash-of-H hash-of-J`,以查找我们自己在主分支上对共享提交H之后做出的更改;
2. `git diff --find-renames hash-of-H hash-of-L`,以查找开发分支上对共享提交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
历史记录的一部分。如果我们沿着底部行从
M
向后退,我们会到达
L
。Git 中的历史记录包括提交及其连接,这意味着
L
已经在此处合并。
当您询问 Git:“HEAD 中有什么内容?”时,您有两种方式提问。您可以问 Git:“
HEAD
中有哪个分支名称?”或者您可以问:“
HEAD
选择哪个提交?”这两个不同的问题得到了两个不同的答案。在“分离 HEAD”模式下,即
HEAD
没有连接到任何分支名称时,第一个问题会导致错误而不是答案。第二个问题几乎总是有效的。
Git 还有一个“未出生的分支”概念,当您开始使用没有任何提交的全新空存储库时,它需要这个分支。在这种情况下,
HEAD
存在并持有一个分支名称,但分支名称本身不存在且无效。因此,在这种特殊情况下,您可以问关于 HEAD 的“名称”问题,但不能问“ID”问题:与分离的 HEAD 设置相反。
实际上,git merge 通过提交哈希 ID 运作,因此我们可以给它想要的任何提交的哈希 ID。但通常我们——人类——按名称工作。
合并结果通常是相同的,除了列出的第一个父提交不同。如果我们使用特定的标志参数来运行 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
的哈希值)。这样做会强制Git执行以下操作:
- 查找提交
P
,即C
的父提交:这很容易,因为C
中包含了P
的哈希值;
- 将
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中发生的任何更改,则特别有用。
6
6Git有一个命令git revert,用于创建这样的提交记录。你指定它的目标为某个子节点,Git执行与挑选某个提交相同的三路合并,但这次的“合并基础”是C,而“theirs”提交是P。(ours/HEAD提交像往常一样是当前的HEAD提交)。练习:尝试获取C与P的差异,并按此顺序进行。如果将此组更改与C与HEAD的更改组合,按照此顺序会发生什么?
请注意,所有这些操作都是针对整个提交记录的
您最初想要处理一个文件。但是Git所做的一切或我们所展示的Git所做的一切都是基于整个提交记录的。这是因为提交记录实际上是Git中的基本单元。提交记录存储文件是正确的,但是Git不是真正关于文件的。Git关注的是提交记录。文件只是使提交记录有用的东西。
您可以从单个提交记录中提取单个文件,并对其进行操作:例如,git diff可以仅针对两个文件的名称进行差异比较。但这种使用Git的方式是不典型的。Git针对以提交记录为单位的操作。
rebase
操作。 - Ashhar Hasan