(我发现你的问题有点不集中,因为我不确定git log --graph 输出的哪些特定方面令人困惑,所以这会很长,我担心:我会尽量涵盖一切都很重要。)
knittl noted in a comment 杂散的右括号只是前一行的环绕。通常git log 通过寻呼机运行其输出,智能寻呼机可以通过在文本上为您提供一个左右滚动的“窗口”来解决这个问题,这样右括号就会从右侧消失终端窗口(或您使用的任何终端模拟器)。
除此之外,让我们具体看看309a287 和3f7475,但从这个开始:
我也对为什么似乎有三个分支(三个垂直平行线)感到有些困惑,因为我只有一个主分支和一个附加分支。
显然,您有两个名字:master 和 blackforest。但是您也有两个不同的存储库:
Merge branch 'master' of [snipped]
截断的部分将是一个 URL;这个特定的消息 Merge branch '<name>' of <url>(可能以 into <name> 结尾)是 git pull 在将合并提交消息传递给 git merge 时制造的。1 所以这个 @987654336 @ 输出意味着您运行了 git pull,它在从其他 Git 存储库获得的提交上调用了 git merge。
我们实际上可以看到该提交:它是合并提交e9415f4... 的第二个父级。所以如果我们只取这四行:
| * commit e9415f49f953ca4fe53bd9631e4afea94c3ba4ba (HEAD -> master, ori...
| |\ Merge: 9a1ff1f 3f74752
| | * commit 3f7475269c1134aef39a06f13ae11d74c9496542
| | |\ Merge: 309a287 3d41b51
我们可以看到。这是您自己的神秘提交之一,在这种情况下,3f74752...。它的提交消息以Merge pull request #1 from 开头。 GitHub 和其他类似的托管网站会生成带有此类消息的提交。
因此,您(或其他人)必须在 GitHub 等网络托管网站上提交 this。您或他们必须在托管在那里的存储库中进行此提交。这是第二个 Git 存储库,它有自己的分支名称,因此根据它有多少分支名称,可能有数千或数百万个分支。 p>
它们的 分支只有在您允许它们时才会影响您的 存储库。你让你的 Git 调用他们的 Git 并从他们那里获得任何新的提交。你的 Git 将这些提交添加到你的存储库数据库中,它会相当努力地保留它所见过的每一个提交。如果您随后 git merge他们的 次提交之一,您将获得从您自己的分支名称(无论您现在在哪个分支上)直接访问此提交的权限。
记住,git pull 表示 运行 git fetch,然后运行第二个 Git 命令。第二个 Git 命令默认为 git merge。 git merge 命令有时需要合并消息——只要它进行真正的合并——当第二个命令是 git merge 时,git pull 会提供一条消息。 (如果您告诉git pull 改为运行git rebase,则不需要合并消息,因此git pull 不提供。)
Git 与分支无关
了解正在发生的事情的方法是意识到 Git 最终真正关心的是提交。它不太关心分支。大多数时候,它根本不关心文件——文件只是提交的一些麻烦事。 ? 当然,如果不是为了提交中的文件,我们根本不会使用 Git,但这只是 我们 关心文件:那不是 Git。
一旦我们看到 commits 对 Git 很重要,我们就可以看到 branch names 是如何进入图片的。然后有可能——尽管仍然有点飞跃——从 branch names 到我们真正所说的 branch 的基本问题(参见What exactly do we mean by "branch"?):
Git 中的分支名称 是指向一个特定提交的指针。 也就是说,它通过名称标识一个提交哈希ID。其他 Git 名称,例如标签名称,可以完成相同的工作,但分支名称还有一个特殊属性:它们自动移动,因此它们总是将 last 提交命名为在分支中。
换句话说,根据定义,存储在分支名称下的哈希 ID 是分支中的最后一次提交。然后我们只需要实现一个更关键的事情:每个提交都存储了一些先前提交的哈希 ID。
提交本身就是图表的形式。给定一些提交(由某个哈希 ID 标识),Git 可以进入该提交并从该提交中提取其直接前任的哈希 ID。或者,对于合并提交,我们有两个,而不是一个直接的前任。2 但是,无论哪种方式,我们最终都会得到这样的结果:
... <-F <-G <-H
其中H 是某个链中last 提交的哈希ID。或者,一条链可能在合并提交处结束:
...--I--J
\
M
/
...--K--L
或者之后有一些额外的提交:
...--I--J
\
M--N
/
...--K--L
但在所有情况下,链在分支提示提交处结束,这样做是因为分支名称指向该提交:
...--G--H <-- master
\
I--J <-- feature
所有通过提示的提交都在分支上,因此在这种情况下,通过H 的提交同时在master 和feature 上,并且I--J 都在feature 一个人。
2一个合并提交实际上可以有两个以上的父节点,但我们在这里并不需要担心这一点。
git log 必须将事物线性化
假设我们有一个这样的图表:
...--I--J
\
M--N <-- master
/
...--K--L
其中N 是最新提交,其父M 是具有两个父J 和L 的合并。通过横向绘制此图,最新的提交在右侧,我们可以表明在提交的上排或下排(分别为I-J 和K-L)上完成的任何工作都没有严格按照 其他行,但仅在其自己的行内。
请注意,Git 通过从提示提交开始查找提交,如分支名称(此处为 master 导致提交 N)然后向后工作。当它向后工作时,git log 需要从每个提交移到其父级或父级。从N 到M 这很容易,因为只有一个父母。不过,从M 回来,git log 真的应该同时访问两个提交 J 和 L ......但它不能。
特别是git log,必须将内容垂直打印出来。 tip 提交 N 将首先出现,因此位于终端窗口的顶部。下面是提交M。如果git log 可以做我要画的东西,它可能会像这样显示合并的左右“边”:
N <information>
|
M <information>
/ \
J L <information about J> <information about L>
| |
I K <information about I> <information about K>
: :
但是git log 做不到,所以它是近似的。它会选择J 和L 中具有较晚提交者日期 的提交,并首先将其发布:
N <information>
|
M <information>
/ \
| L <information about L>
| |
J | <information about J>
: :
这与实际输出非常接近,但略有不同:
N <information about N>
|
M <information about M>
|\
| L <information about L>
| |
J | <information about J>
: :
这就是您在自己的git log 输出中看到的最多的内容。
“第一父母”很重要
最后可能看起来特别奇怪的是:
| * commit 309a287f9c39d219d719efbcc872a176f7644b19
| |\ Merge: dafe938 806e855
| |/ Author:
|/| Date: Sun May 17 17:42:46 2020 +0100
| |
| | Merge branch 'blackforest'
| |
* | commit 806e8558cd7b24658a998b2ee5d19500e608b77d
为什么git log 会做出这种时髦的向右突出,然后又向左摆动的事情?也就是为什么要画这个:
| *
| |\
| |/
|/|
: :
何时:
| *
|/|
: :
可能会?
这里的答案是,在 Git 中,合并提交的 first-parent-ness 很重要。在我自己的水平图纸中:
...--I--J
\
M--N <-- master
/
...--K--L
我试图让J <-M 连接看起来比L <-M 连接更重要。这是因为从某种意义上说,这里没有任何更重要的东西:J 和 L 都是 M 的父级,当我们进行合并时,如果我们不使用-X ours 或 -X theirs,在解决冲突的同时提交也没有什么比这更重要的了。
但是任何合并提交的 first 父级是特殊的,原因与任何正常的非合并提交的 first and only 父级是特殊的相同:它是我们所做的提交的直接沿袭,一次一个提交。
考虑我们如何进行正常的日常非合并提交。我们开始:
git checkout master
这让我们得到了这样的结果:
...--G--H <-- master (HEAD)
也就是说,特殊名称 HEAD 现在附加到分支名称 master。 当前分支现在是master。 当前提交是master的提示提交,即提交H:它是我们工作树中提交H的内容,我们可以继续努力。
如果我们确实做了一些工作,并且git add 和git commit,我们会得到一个新的提交,我们称之为I。新提交将提交 H 作为其父提交:
...--G--H [master used to point to H]
\
I
而git commit 的最后一步是将I 的实际哈希ID 写入名称 master:
...--G--H [master used to point to H]
\
I <-- master (HEAD)
之后我们可以再次将它们画成一条直线:
...--G--H--I <-- master (HEAD)
当我们重复这个过程时,分支会增长。这又是那个模棱两可的词,branch:这一次它意味着 一系列提交,以特定的指定提交结束 和 名称master,两者同时,depending on what we want it to mean at that moment。
...--G--H--I--J <-- master (HEAD)
如果我们现在有其他分支:
...--G--H--I--J <-- master (HEAD)
\
K----L <-- feature
并运行git merge feature,我们得到一个新的合并提交M。合并提交扩展了master,因为HEAD 附加到master:
...--G--H--I--J--M <-- master (HEAD)
\ /
K----L <-- feature
即使名称 feature 不存在,我们也可以这样做,只要我们能够以某种方式找到提交 L。也就是说,如果我们像这样提交K 和L,然后完全删除名称feature,同时确保Git 不会清除提交,我们会: p>
...--G--H--I--J <-- master (HEAD)
\
K----L [unnamed]
然后我们可以运行git merge <em>hash-of-L</em>,我们会得到和以前一样的结果:
...--G--H--I--J--M <-- master (HEAD)
\ /
K----L
提交L 现在再次成为可查找名称:它是M 的第二个 父级。
更常见的是,我们可能会将feature 合并到master,生成M,然后删除名称feature,让我们保持相同的状态。
把这一切放在一起
Git 不太关心名称。 Git 只关心提交。 Git 使用名称来find提示提交,然后向后工作;如果它可以找到任何给定的提交,则提交保留。
但是在所有情况下,提交的第一父级都很重要。对于普通(非合并)提交,第一个父级是唯一的父级。对于合并提交,第一个父提交是 在我们进行合并时作为提示的提交。
如果我们使用git log --first-parent——有或没有--graph——git log 将在到达合并提交时忽略除第一个父级之外的所有内容。也就是说,给定以合并提交M 结尾的图形片段,没有--first-parent 的git log 将显示:
- 提交 M;那么
- 按某种顺序提交 J 或 L;那么
- 以某种顺序提交 I 或 J 或 K 或 L,但跳过之前显示的任何提交;
以此类推,直到它显示所有可以通过从 M 开始并向后工作来找到的提交。它一次显示每个提交,并且它使用的 order 是您可以控制的,在git log 输出中使用各种对提交进行排序 选项:
git log --author-date-order
使用作者日期时间戳而不是提交者日期时间戳(每个提交都有两个日期和时间戳)。或者:
git log --topo-order
使用git log --graph 要求的排序,因此git log --graph 启用该排序约束。
添加--first-parent 告诉git log,当它退出提交M 时,它应该仅查看提交J,而不是提交L。提交 L 是 second 父级,因此 git log 应该将其从提交访问列表中删除。结果将是git log --first-parent master 将显示M,然后是J,然后是I,然后是H,然后是G,以此类推。
这种特殊控制顺序的原因是git log 使用priority queue 遍历图一次提交。你给git log一些开始提交:
git log master
或:
git log --all
例如,git log 计算出这些提交的哈希 ID。如果你给了一个分支名称,比如master,那就是一个提交:该分支的尖端提交。队列中现在有一个条目。
那么,只要队列不为空,git log就会执行一个循环:
- 将最前面的(最高优先级)条目从队列中取出。
- 显示该提交。 (有一些选项可能不在此处显示,但我们没有使用这些选项中的任何一个。)
- 如果我们尚未显示它们并且它们尚未在队列中,则将此提交的父项放入队列中。使用
--first-parent 选项,仅将 first 父级放入队列。
- 重复。
所以我们的git log master 和--first-parent 在任何时候在队列中都不会有超过一个提交:它从一个开始,删除它以达到零,显示提交,然后放入一个父级。如果没有--first-parent,它会从队列中的一个提交(M)开始,将其删除(队列为空),显示M,并将两个 提交插入队列:J 和L。现在优先级很重要。
默认优先级是较晚提交日期的提交具有更高的优先级。如果我们在提交J 之后提交L,我们将显示的下一个提交是L。不管L 是第一个还是第二个父母都是如此。
git log --graph 代码将确保从M 连接到L(它的第二个父级)的行 从右下角开始。这就是我们在这里看到的:
| * commit 309a287f9c39d219d719efbcc872a176f7644b19
| |\ Merge: dafe938 806e855
| |/ Author:
|/| Date: Sun May 17 17:42:46 2020 +0100
| |
| | Merge branch 'blackforest'
| |
* | commit 806e8558cd7b24658a998b2ee5d19500e608b77d
从309a287... 到806e855... 的连接器从向下和向右开始,然后折回以加入左侧线。 309a287... 的 第二个 父级是 806e855...。 (第一个父母是dafe938...。)
到达806e855... 的行来自提交3d41b51...,这是一个普通的单亲提交。 那个提交是从提交c501f9fc...中找到的,这也是一个普通的单亲提交,并且是——或者至少是,你的Git最后一次检查——将 Git 存储库中分支 blackforest 的提交提示提交到 origin(在 GitHub 或任何地方)。
(您的图表也因git stash 的两次提交而有些混乱。这些提交不在任何分支 上,但可以通过名称refs/stash 访问。请注意,其中一个其中从技术上讲是一个合并提交——但git merge 没有做出这个提交;git stash 做到了;如果你把它当作一个普通的合并来对待,大多数 Git 命令都不会有什么意义,因为他们会认为git merge 成功了。只有git stash 自己知道以后如何拆开它。)