我正在自下而上阅读John Wiegley的Git。在任何其他名称的提交中... 他提到:
name1..name2
— 此别名和以下别名指示提交范围, 这对于 log 等命令非常有用,用于查看内容 发生在特定的时间段内。左边的语法 指从 name2 返回到的所有提交,但不是 包括,名称 1。如果省略name1
或name2
HEAD
,则在 它的位置。
master..
— 此用法等效于master..HEAD
。我正在添加它 在这里,即使上面已经暗示了,因为我使用这种 在查看对当前分支所做的更改时不断使用别名。
我在这里很困惑:HEAD
永远是master
的最后一次承诺,对吧? 那么master..HEAD
是什么意思呢?
HEAD
永远是master
的最后一次提交吧?
仅当您当前"在"分支master
.
让我们用几个例子来完全具体,以便你可以看到它是如何工作的。 我们将首先创建一个新的、完全空的 Git 存储库:
$ mkdir example && cd example
$ git init
Initialized empty Git repository in ...
$ echo just an example > README
$ git add README
$ git commit -m initial
[master (root-commit) a3de110] initial
1 file changed, 1 insertion(+)
create mode 100644 README
您将获得与我不同的哈希 ID,如果您将 Git 设置为使用不同的分支名称(例如,main
)创建新存储库,您将获得不同的分支名称,但无论哪种方式:
[<some name here> (root-commit) <abbreviated-hash>] initial
Line告诉你几件事:
这是一个根提交,在本例中是这个新存储库中的第一次提交。 第一次提交有点特殊,因为它始终是根提交(并且通常是唯一的根提交)。 根提交并不是特别有趣。 从某种意义上说,它们特别沉闷,因为它们是所有动作停止的地方。
当前分支名称是(在我的情况下)
master
。我的新提交的缩写哈希ID是
a3de110
。
任何提交的完整哈希 ID 都是它的"真实名称":这个名称在每个 Git 存储库中都有效,表示提交。由于我刚刚在我的存储库中进行了它,并且您没有我刚刚进行的提交,因此您没有此提交,因此您也没有此哈希 ID。 这些哈希 ID 是普遍唯一的!1
无论如何,让我们来看看现在HEAD
的名字:
$ git rev-parse --symbolic-full-name HEAD
refs/heads/master
$ git rev-parse HEAD
a3de1101707189f42f01b50fed47aa350398f49a
在这里我们看到,是的,我们在分支master
- 它的全名是refs/heads/master
- 它的完整哈希ID是那个丑陋的十六进制大数。
1我们可以证明这在数学上是不可能的(从鸽子洞原理中很明显),并且由于 Git 依赖于这种不可能性,我们已经证明了 Git 最终会崩溃。 然而,在实践中,Git 十年又十年地工作:实际的破损几率与你坐在键盘前并在仓库中工作时被闪电击中并死亡的几率相似(而且几乎总是小于)。这在理论上是可能的,只是在现实生活中发生的频率不够高,值得担心。
还没有足够的提交来引起人们的兴趣
由于只有一个提交的存储库是如此枯燥,让我们再进行几次提交。 让我们也做一些额外的分支名称:
$ echo one > file1
$ git add file1
$ git commit -m 'add a first file'
[master 0f553c5] add a first file
1 file changed, 1 insertion(+)
create mode 100644 file1
$ echo two >> file1
$ git add file1 && git commit -m 'add a second line to file1'
[master fbdddac] add a second line to file1
1 file changed, 1 insertion(+)
$ git switch -c b1
Switched to a new branch 'b1'
$ echo two > file2 && git add file2 && git commit -m 'add second file'
[b1 51a610e] add second file
1 file changed, 1 insertion(+)
create mode 100644 file2
$ git switch master
Switched to branch 'master'
$ git switch -c b2
Switched to a new branch 'b2'
$ echo three > file3 && git add file3 && git commit -m 'add different second file'
[b2 e683c47] add different second file
1 file changed, 1 insertion(+)
create mode 100644 file3
我们现在有三个分支名称(master
、b1
和b2
),目前b2
"在":
$ git log --all --decorate --oneline --graph
* e683c47 (HEAD -> b2) add different second file
| * 51a610e (b1) add second file
|/
* fbdddac (master) add a second line to file1
* 0f553c5 add a first file
* a3de110 initial
这显示了 Git 如何绘制图形(另请参阅 Pretty Git 分支图),如果我们使用git rev-parse
我们可以看到HEAD
指的是b2
,这意味着哈希以e683c...
开头的提交:
$ git rev-parse --symbolic-full-name HEAD
refs/heads/b2
$ git rev-parse HEAD
e683c472999de3d39c3e69d030b362df561b5711
$ git rev-parse b2
e683c472999de3d39c3e69d030b362df561b5711
现在我们有足够的东西可以使用
出于StackOverflow讨论的目的,我喜欢水平而不是垂直绘制图形。 Git 将最新的提交放在顶部;我把它们放在右边。 这是相同的图表,除了我没有使用丑陋的大哈希ID,而是使用单个字母来代表每个提交,并在右侧绘制较新的提交:
D <-- b1
/
A--B--C <-- master
E <-- b2 (HEAD)
最新的提交,add different second file
(e683c...
),位于底行。分支名称b2
(即refs/heads/b2
)指向此提交,或者换句话说,git rev-parse b2
打印出那个丑陋的大哈希 ID。
特殊名称HEAD
当前附加到名称b2
。 所以HEAD
意味着现在b2
。
如果我们运行:
git log master..b2
或:
git log master..HEAD
或:
git log master..
(所有这些目前的意思都是一样的)我们将看到提交e683c...
:
$ git log master..
commit e683c472999de3d39c3e69d030b362df561b5711 (HEAD -> b2)
Author: Chris Torek <chris.torek@gmail.com>
Date: Mon Dec 19 01:00:28 2022 -0800
add different second file
事实上,这正是我们所看到的。 如果我切换回master
:
$ git switch master
Switched to branch 'master'
这改变了HEAD
的附件:它现在附加到master
,所以现在master..HEAD
的意思与master..master
相同,这意味着没有提交,这就是我们将在git log
输出中看到的:
$ git log master..
$
有和没有历史记录的选择
在大多数 Git 命令中,两点语法A..B
"意味着":
- 从提交
B
开始,向后工作,列出你可以通过这种方式找到的所有提交,然后 - 从提交
A
开始,向后工作,列出您可以通过这种方式找到的所有提交。
我们用绿色"绘制"或"突出显示"第一组提交,然后用红色"绘制"(或重新突出显示)第二组提交中的所有内容。 现在仅以绿色突出显示的提交是我们选择的提交。
这适用于git log
、git cherry-pick
和其他一些 Git 命令。 需要一段时间才能习惯。 例如,给定我上面的图表,b1..b2
是什么意思? 好吧,让我们看一下图表:
D <-- b1
/
A--B--C <-- master
E <-- b2
(我把HEAD
拿出来,因为我们目前没有使用它)。 我们希望从b2
开始 - 提交E
,或e683c...
我刚刚创建的存储库中,并以绿色突出显示。 然后我们向后工作:E
向后指向C
,所以我们用绿色突出显示C
,然后向后移动一步以B
并以绿色突出显示,然后移回A
并突出显示绿色。 提交A
,作为我们的根提交,在它之前没有任何提交,所以这就是我们停止以绿色突出显示的地方。
现在我们以红色突出显示提交D
- 提交51a61...
。 无论如何,我们都不会列出它,但我们现在仍然"把它涂成红色"。 然后我们向后移动一跳,提交C
,这是D
之前的提交,并将那个涂成红色。这个"红色油漆"覆盖了之前的"绿色油漆",所以现在我们不打算显示提交C
。 然后我们向后移动另一个跃点以B
并将其标记为红色,依此类推。
最终结果是我们只显示提交E
。 这与master..b2
相同。 即使master
选择C
而b1
选择D
,它显示相同的原因是C
- 和更早的提交是git log b2
选择的唯一提交。 取消选择过程取消选择C
- 及更早,无论我们是从C
本身开始,还是从D
开始
。这让我们进入了 Git 中另一个令人困惑的事情。git log
命令通过执行"使用历史记录进行选择"来工作。 也就是说,我们运行:
git log b2
Git 使用分支名称b2
来查找提交E
并显示该提交,然后向后工作。 这向我们展示了从E
开始并向后工作发现的整个历史。
一些 Git 命令,git log
您将使用的最常见的命令,只做这种事情:选择历史记录。 也就是说,你告诉命令"从这里开始",他们这样做,但随后他们也从那里向后工作。 对于这些命令,范围语法b1..b2
或stop..start
或任何阻止它们一直工作到时间开始的东西。
其他 Git 命令,例如git cherry-pick
,不会执行这种"使用历史记录选择"。 如果运行:
git cherry-pick e683c472999de3d39c3e69d030b362df561b5711
Git 将找到提交e683c472999de3d39c3e69d030b362df561b5711
(假设您在这里有我的最后一次提交)并尝试将其复制到一个新的、据称经过改进的提交中,因为这就是git cherry-pick
的用途。阿拉伯数字默认情况下,Cherry-pick 选择不带历史记录,并将stop..start
与git cherry-pick
一起使用,可以复制范围内的所有提交。
所以你最终需要知道:这个命令默认选择有历史记录还是没有历史?但事实证明,当你使用 Git 时,这最终会"感觉很自然"。 只需在第一次使用新命令时进行一些实验:制作一个临时的"垃圾"克隆,看看会发生什么。
2一旦进行任何提交,它就不可更改,因此,如果您打算更改"提交在图形中的位置"或"该提交的日志消息"以外的内容,您可能需要git cherry-pick -n
,这意味着复制工作,但暂时不要提交。 Git 就是这样,如果你在这里搞砸了,有一些解决方法,要记住的主要事情是:如果你已经提交了某些东西,它现在在 Git 中,只要你还有仓库,你可能就可以把它找回来;如果你没有提交它不在 Git中,Git无法帮助你把它找回来。因此,进行大量小提交往往是个好主意。
但git diff
很奇怪
我们在 Git 中经常使用的一个命令是git diff
,和git log
一样,你可以运行:
git diff A..B
关于git diff
的事情是它拒绝遵守这里的所有正常规则。 两点语法并不意味着"范围内的每个提交"。相反,git diff A..B
的意思与git diff A B
完全相同。Git 作者/维护者决定鼓励人们使用第二种形式。 它还有另一个优点,因为它短了一个字符。 这确实意味着你不能使用隐含的HEAD
,因为:
git diff xyzzy
表示将名称xyzzy
选择的提交与当前工作树进行比较,而不是将xyzzy
选择的提交与当前提交进行比较。 您必须使用:
git diff xyzzy HEAD
对于后者。 这有点烦人,真的。 如果git diff
完全拒绝两点语法,那可能会很好。