我知道如何将父提交称为HEAD^
。
但是是否有可能以类似的方式引用子提交?
我知道如何将父提交称为HEAD^
。
但是是否有可能以类似的方式引用子提交?
正如Tim Biegeleisen在评论中指出的那样,当你按分支名称进行检查时,这会将您置于该分支的末端。这在Git中是定义上发生的,因为Git与大多数版本控制系统不同。在Git中,“分支”一词有些模糊:我们有分支名称,其被定义为“指向表示该分支末端的提交的标签”,以及在提交DAG中存在的分支和合并结构(我喜欢称之为“DAGlets”)。有关此内容,请参见“分支”到底指什么?。然而,所有这些的一个关键结果是,提交经常同时位于多个分支上。
HEAD
|
v
A <- B <- C <- D <-- master
master
上(HEAD
指向master
),并且master
指向提交D
。git commit
——意味着我们创建一个新的提交E
,其父提交是D
,Git会立即将master
也指向E
: HEAD
|
v
A <- B <- C <- D <- E <-- master
A--B--C--D--E <- (HEAD) master
D
创建一个新的分支,关于孩子们的所有这些内容仍然是正确的。假设此时我们这样做:git checkout -b feature master~1
feature
的新分支标签,直接指向提交D
。提交E
——master
的顶端——仍然存在,但现在HEAD
将指向feature
,而feature
将指向D
: E <-- master
/
A--B--C--D <-- (HEAD) feature
提交 A
到 D
现在同时存在于两个分支: master
和 feature
。
如果我们现在在 feature
上进行新的提交,会得到如下结果:
E <-- master
/
A--B--C--D--F <-- (HEAD) feature
也就是说,F
现在是 feature
分支上最新的提交:HEAD
没有改变,而 feature
现在指向 F
。提交 E
和 F
分别位于一个分支上,提交 A
到 D
仍然在两个分支上。
如果我们不想要 F
,现在可以:
git checkout master
切换回 master
分支(并提交 E
)然后:
git branch -D feature
feature
分支并忘记关于提交 F
的所有内容。1现在提交 A
到 D
只在一个分支 master
上。
1如果您改变主意,提交记录通常会在存储库中停留一段时间。通过“reflogs”,Git通常会记住“废弃”提交的ID至少30天。有一个用于HEAD
的reflog,并保留提交F
的原始SHA-1哈希值,因此您可以使用git reflog
查找F
并将其取回。
对于分支名称feature
也有一个reflog,但当我们执行git branch -D feature
时,我们让Git将其丢弃了。
在Git中,“分离头部”充当一种类似于未命名的分支。让我们使用之前相同的 A
到 E
(尚未添加 F
),但只需使用 git checkout master~1
而不是 git checkout -b feature master~1
,并绘制它。现在,不再是 HEAD
指向 feature
并且 feature
指向提交 D
,而是直接将 HEAD
指向提交 D
:
E <-- master
/
A--B--C--D <-- HEAD
HEAD
包含某个提交的原始提交哈希(ID,a123456...
),而不是保存分支名称并让分支名称指向最新提交。尽管如此,在这种情况下,我们可以添加新的提交,就像之前创建提交 F
一样。由于我们原来的提交 F
仍然存在,我也会将其绘制出来,并使用 G
代替这个新提交。 E <-- master
/
A--B--C--D--G <-- HEAD
\
F [abandoned]
feature
)一样,当提交D
是当前提交时,它在当前分支上没有子提交(即使在master上有一个提交E
,它有提交D
作为父提交,而且还有一个被放弃的提交F
作为子提交!)。
在Git中,处于分支状态和处于分离HEAD模式之间的关键区别在于,当你处于分支状态时,HEAD
文件包含了分支的名称。也就是说,它"指向"分支名称,而分支名称指向提交,就像上面的图示那样。当你处于分离HEAD状态时,HEAD
文件直接指向提交……也就是分支的尖端。没有分支名称;当前提交是匿名分支的尖端。
作为分支的尖端,它自动没有子提交。
git log --children
,或者等效地,git rev-list --children
(git log
和git rev-list
本质上是相同的命令,只是输出格式不同)。为了避免在只想获取哈希ID时烦扰于--pretty=format
和--no-patch
,我们可以使用git rev-list
。git rev-list
的工作方式是从某些提交点(通常是分支的末端或多个分支的末端)开始向后工作,跟随父指针。默认情况下,它只打印出每个提交的哈希ID:$ git rev-list master
f8f7adce9fc50a11a764d57815602dcb818d1816
8213178c86d5583ff809c582d6727ad17b6a0bed
[snip]
--parents
选项,它会打印每个提交及其父级的ID(为了避免需要[snip]
,我将添加-n 2
以在打印两项后停止):$ git rev-list --parents -n 2 master
f8f7adce9fc50a11a764d57815602dcb818d1816 8213178c86d5583ff809c582d6727ad17b6a0bed 08df31eeccfe1576971ea4ba42570a424c3cfc41
8213178c86d5583ff809c582d6727ad17b6a0bed 2a96d39824464c28f2f45f2f4a4d53d7c390c9eb
这里master
的提示是一次合并,有两个父节点,因此在一行上打印了三个哈希值。
使用--children
告诉git rev-list
做一些有趣(且技术难度较高)的事情:它不会为每个提交打印父节点,而是遍历整个链,找到父节点,然后反转父/子关系。请记住,我们最初绘制图形的方式是这样的:
A <- B <- C <- D <-- master
提交 C
只知道它的父提交,而不知道它的子提交。 rev-list
命令可以从我们提供的尖端 master
开始向后遍历到根提交 A
,然后完成这个过程后,它可以反转 它所遵循的所有箭头(我为了一个原因加粗了这个短语):
A -> B -> C -> D
C
的ID,后跟提交D
的ID,全部显示在一行上。如果现在在Git本身的Git存储库中执行此操作,将会得到以下结果:$ git rev-list --children -n 2 master
f8f7adce9fc50a11a764d57815602dcb818d1816
8213178c86d5583ff809c582d6727ad17b6a0bed f8f7adce9fc50a11a764d57815602dcb818d1816
git rev-list
实际上遍历了 43807 个提交才得到这个结果。3 这是目前分支 master
上的提交数量。我们没有限制遍历范围,所以 Git 遍历了从 master
可达的每个提交,翻转了结果遍历中的所有箭头,并最终打印出两个提交哈希值及其相应的反向箭头子 ID:分支 master
本身的哈希值 (f8f7adc...
) 和主线上 "紧挨着" 分支 master
的提交的哈希值 (master^1
或 8213178...
)。
F
或提交G
时询问master
,这两个提交都不包含在master
分支中,那么此git rev-list
将永远无法到达当前提交。git rev-list --count master
这只是简单地给出了在rev-list遍历中访问的提交计数。另一种方法是运行:
git rev-list master | wc -l
这个命令会在标准输出中列出每个提交,将输出导向wc
程序,并指示wc
计算行数。但是使用git rev-list
进行计数大约快两倍。
我在 Git 自身的 Git 存储库中,执行了以下操作:
$ git checkout master
$ git checkout HEAD^
现在git status
显示HEAD detached at 8213178
。我们想要找到8213178
的(唯一)子节点,即master
指向的提交。因此我们尝试这样做:
$ git rev-list --children -n 1 HEAD
8213178c86d5583ff809c582d6727ad17b6a0bed
好的,那个失败了!但是出了什么问题呢?
还记得我之前加粗的那句话吗:git rev-list
会反转它所遵循的所有箭头。可惜,它根本没有遵循任何箭头来到达HEAD
!它从HEAD
(8213178...
)开始。这是它唯一需要的修订版本(-n 1
),所以它也在那里停止,并打印出HEAD
的ID并完成。
使用-n 2
至少使其跟随一个箭头-一个父链接-但这并不真正有帮助:
$ git rev-list --children -n 2 HEAD
8213178c86d5583ff809c582d6727ad17b6a0bed
2a96d39824464c28f2f45f2f4a4d53d7c390c9eb 8213178c86d5583ff809c582d6727ad17b6a0bed
HEAD
开始,并沿着箭头到达HEAD^
(2a96d39...
),因此它能够反转那个箭头:它告诉我们8213178...
是2a96d39...
的子节点。但我们想知道的是:哪些节点以8213178...
为父节点?为了让Git发现这一点,我们必须从8213178...
之外的某个地方开始。master
的末端。我们知道这一点,因为我们感兴趣的是“在通向master
末端的路上的HEAD的子节点”。next
末端的路上的HEAD的子节点”,或者“在通向pu
末端的路上的HEAD的子节点”,或者任何其他分支的子节点。我们甚至可以要求任何分支上的子节点。git rev-list --children [other options] --branches
--branches
标志表示“所有分支”;--branches=abc*
表示“所有以 abc
开头的分支”;等等。
重点在于,我们必须告诉 Git 从哪里开始。我们不能从 HEAD 开始。我们可以停在那里,但我们不能从那里开始。停在 HEAD 可以加快速度——不需要查看超过 43,000 个提交——所以我们可以尝试使用 HEAD..master
:
$ git rev-list --children HEAD..master
f8f7adce9fc50a11a764d57815602dcb818d1816
08df31eeccfe1576971ea4ba42570a424c3cfc41 f8f7adce9fc50a11a764d57815602dcb818d1816
1ecc6b291c162b9fc7b59a3251c4cbbcf3b07b84 08df31eeccfe1576971ea4ba42570a424c3cfc41
6cbec0da471590a2b3de1b98795ba20f274d53fa 1ecc6b291c162b9fc7b59a3251c4cbbcf3b07b84
8e4571e57a1a3cc6f1318b3da8612b2e3c8e1252 6cbec0da471590a2b3de1b98795ba20f274d53fa
c81d2836753a268be07346d362ffab3c6a5e14a9 8e4571e57a1a3cc6f1318b3da8612b2e3c8e1252
[12 more lines snipped]
哇,这里发生了什么?答案有点棘手,但与master
是合并提交有关。让我们先看一下这个相对简单的表达式:
$ git rev-list --count HEAD..master
18
HEAD
只是master^1
,也就是master
的第一个父提交,实际上在HEAD..master
中有18个可到达的提交。这是因为master^2
上有17个提交,而这些提交不在master^1
上。加上合并本身,你就得到了这18个提交。通常情况下,准确地绘制它很困难,因为Git的提交DAG非常混乱,但是一个简化的图片看起来像这样“浴缸里的HEAD”: HEAD
|
v
...--x--x---------------x--o <-- master
\ /
o--o--o--o--o--o
HEAD..master
的意思是Git应该从HEAD
开始向后划掉(用x
表示)提交,同时从master
开始向后获取提交。因此,这将获取master
本身,并试图获取master^1
(即HEAD
提交),但这会被划掉,然后尝试(并成功)获取master^2
以及沿着底部行的所有提交,在重新加入被划掉的提交的顶部行之前停止。
这里的诀窍在于我们必须让git rev-list
检查HEAD
提交本身,以便跟随任何指向HEAD
的箭头。然后,我们可以使用grep
或类似工具来挑选出包含HEAD
提交的行,并使用--children
使该行列出具有HEAD
作为父级的提交:
$ git rev-list --children master | grep "^$(git rev-parse HEAD)"
grep "^$(git rev-parse HEAD)"
实现——这里的帽字符是grep表示“行首”的符号,因此与Git本身无关。HEAD
的父提交处终止遍历来加快这个过程:git rev-list --children master ^HEAD^@
诱人的做法是尝试将此与-n
和/或--reverse
结合使用,但由于以下两个原因而注定会失败:
HEAD
提交可以位于此遍历中的任何位置,具体取决于后面跟随HEAD
的DAGlet结构-n
限制在反转列表之前进行,因此-n 1
始终只获取master
提交。因此,我们可以在此停止并宣布胜利,使用git rev-list --children
来反转箭头,使用master
来获取正确的选择起点,可选择使用HEAD^
或HEAD^@
以停止遍历以加快git rev-list
步行速度,并且-至关重要-使用grep
来挑选出所需的行,然后查看所有子提交ID。
rev-list
命令还支持一个标志,--ancestry-path
,正是我们需要的。
通常情况下,如我上面所述,git rev-list X..Y
的意思是 git rev-list Y ^X
:查找所有从提交 Y
可达的提交,但排除所有从提交 X
可达的提交。如果 DAGlet(由 X..Y
选择)中没有合并,则该列表(至少在正确顺序下打印)以第一个在提交 X
后的提交结束或开始。问题出现在存在合并时: ^X
部分会删除提交 X
及其祖先,但未删除不是提交 X
的子孙节点的 Y
的祖先。再次查看“浴缸里的 HEAD”图,这次我将在浴缸另一侧添加几个提交,并实际上进行另一个分支与合并:
HEAD
|
v
...--x--x---------------x--o--o---o <-- master
\ / \ /
o--o--o--o--o--o o
HEAD
|
v
...--x--x---------------x--o--o---o <-- master
\ / \ /
x--x--x--x--x--x o
o
都是 master分支末端的祖先 和 HEAD的后代。这正是 --ancestry-path
的作用:它的意思是“对于我们明确排除的任何提交,也排除那些没有这些提交作为祖先的提交”。 (也就是说,Git 反转了条件:它不能轻易地测试“是后代”,但是它可以测试“是祖先”。如果 D 是 A 的后代,则根据定义,A 是 D 的祖先,因此通过测试“非祖先”,它可以推断出“非后代”。)HEAD
的提交,我们就得到了一个合适的“下一个提交”。请注意,有时会有两个或更多这样的提交。例如,让我们向前移动一次提交,将 HEAD 拖到浴缸的边缘: HEAD
|
v
...--x--x---------------x--x--o---o <-- master
\ / \ /
x--x--x--x--x--x o
HEAD
|
v
...--x--x---------------x--x--x---o <-- master
\ / \ /
x--x--x--x--x--x o
o--o
/ \
...--x--x o <-- branch
\ /
o--o
git rev-list
来获取该范围内每个提交的ID列表(使用--boundary
、^X^@
语法或显式添加起始点X
,如果您想在像X..Y
这样的范围中包括提交X
)。然后,您可能已将要访问的每个提交的ID保存在文件中,因此在遍历“苯环”DAGlet时不会错过任何一个提交。
或者,您可以标记两个提交并在它们之间工作。例如,git bisect
的工作方式就是这样。
4好的,显然这取决于你如何限制问题。这就是为什么定义你想要做什么是如此重要的原因!
git log
(或者在这种情况下更好的是git rev-list
)与--chlidren
选项可以工作,但不能以这种方式和这个案例中使用(即"detached HEAD"并查看当前HEAD)。我会在回答中解释原因——在评论中没有足够的空间来进行解释。 - torek列出所有分离 HEAD 的子节点
使用此别名:
# Get all children of current or specified commit-ish
children = "!bash -c 'c=${1:-HEAD}; set -- $(git rev-list --all --not \"$c\"^@ --children | grep $(git rev-parse \"$c\") ); shift; echo $*' -"
git children
将列出游离 HEAD 的 所有 子提交。
列出特定的子提交
如果你只想要某个分支祖先中的子提交:
根据 this question 的答案,我在我的 .gitconfig
中编写了这个别名。
# Get the child commit of the current commit.
# Use $1 instead of 'HEAD' if given. Use $2 instead of curent branch if given.
child = "!bash -c 'git log --format=%H --reverse --ancestry-path ${1:-HEAD}..${2:\"$(git rev-parse --abbrev-ref HEAD)\"} | head -1' -"
git child HEAD
<tip>
,其中tip是一个commit-ish,通常是包含分离头的分支名称。git child HEAD branchname
它默认为给出HEAD的子级(除非给出另一个commit-ish参数),通过向当前分支的尖端追踪祖先一步来实现(除非给出第二个参数作为另一个commit-ish)。但请注意,对于分离头,“当前分支”未定义。%h
而不是%H
。git rev-list
,它可以跟踪到有趣的提交的祖先路径,因此:git rev-list --ancestry-path --all --reflog --parents ^HEAD | grep `git rev-parse HEAD`
git rev-list --boundary --ancestry-path --all --reflog --children ^HEAD \
| grep ^-$(git rev-parse HEAD)
HEAD
上,因此没有子节点。因此,能够导航到任何一个 父节点 通常是令人满意的。您能详细说明一下为什么您认为需要这个功能吗? - Tim Biegeleisen