【问题标题】:Is there a way to refer to a child commit of the current detached HEAD?有没有办法引用当前分离的 HEAD 的子提交?
【发布时间】:2016-08-27 15:12:36
【问题描述】:

我知道如何将父提交称为HEAD^

但是是否可以以类似的方式引用子提交?

【问题讨论】:

  • 在 Git 中签出一个分支时,通常会登陆该分支的 HEAD,因此没有子分支。因此,能够导航到任何 parent 通常是令人满意的。您能否详细说明您认为需要此功能的原因?

标签: git version-control parent-child git-checkout git-revision


【解决方案1】:

答案是“否”和“是”,或者“这个问题没有意义”,这取决于你的意思

您的问题专门针对“当前分离的 HEAD 的子提交”。这就是为什么这个问题没有意义,至少在没有额外信息的情况下是没有意义的。

作为Tim Biegeleisen noted in a comment,当您按分支名称结帐时,这会将您置于分支的提示。这在 Git 中按定义发生,因为 Git 与大多数版本控制系统不同。在 Git 中,术语“分支”有点含糊:我们有分支 names,它们被定义为“指向表示该分支尖端的提交的标签”,我们有 branch-在提交 DAG 中合并结构(我喜欢称之为“DAGlets”)。有关更多信息,请参阅What exactly do we mean by "branch"?。然而,所有这一切的一个关键结果是提交通常一次在多个分支上

缺失的信息

当您指向提交 DAG 中某处的某个提交并询问子提交时,您是在做出假设。具体来说,您假设一个特定的分支或一组分支。要了解它是如何工作的,让我们看看分支是如何演变的。

运动中的分支

根据定义,分支的尖端是它的结束:该分支上不再有提交。如果你运行 git commit 并创建一个新的,分支 name 会自动指向你刚刚提交的新提交。该分支现在有了一个新的最尖端提交,而且——这是真正的关键——你现在在那个提交中,它当然没有子提交:这是一个新的无子提交。它只能通过添加新的提交来获得新的子节点。

这在视觉上是有道理的:我们在某个分支的顶端,在分支master 中提交D

                        HEAD
                         |
                         v
A <- B <- C <- D   <-- master

我们在分支masterHEAD 指向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 将指向featurefeature 将指向D

           E   <-- master
          /
A--B--C--D     <-- (HEAD) feature

提交AD 现在位于两个 分支上:masterfeature

如果我们现在在 feature 上进行新的提交,我们会得到:

           E    <-- master
          /
A--B--C--D--F   <-- (HEAD) feature

也就是说,F 现在是feature 的最尖端提交:HEAD 根本没有改变,feature 现在指向F。提交 EF 分别在一个分支上,而提交 AD 仍然在两个分支上。

如果我们毕竟不想要F,我们现在可以:

git checkout master

切换回master(并提交E)然后:

git branch -D feature

删除feature 分支并忘记提交F1 现在提交AD 仅在一个分支上,master


1如果您改变主意,提交往往会在存储库中逗留一段时间。 Git 通常会通过“reflogs”记住“放弃”提交的 ID 至少 30 天。 HEAD 有一个 reflog,它保留了提交 F 的原始 SHA-1 哈希,因此您可以使用 git reflog 查找 F 并将其取回。

分支名称 feature 的 reflog,但是当我们使用 git branch -D feature 时,我们让 Git 将其丢弃。


运行中的匿名分支

在 Git 中,“分离的 HEAD”充当一种 未命名 分支。让我们像以前一样使用A-through-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 HEAD 包含一些提交的原始提交哈希(ID,a123456... 事物),而不是持有一个分支的名称并让分支名称指向最尖端的提交。

尽管如此,在这种情况下,我们可以添加新的提交,就像我们之前提交 F 所做的那样。由于我们原来的F 仍然在其中,所以我也将它画进去,并使用G 代替这个新的提交:

           E    <-- master
          /
A--B--C--D--G   <-- HEAD
          \
           F    [abandoned]

所有这一切的意思是,就像当您在像feature 这样的命名 分支上时,当提交D 是当前的时,它在当前分支上没有子级 (尽管在 master 上有一个提交 E,它有一个提交 D 作为父级——而且,就此而言,还有一个废弃的提交 F 作为一个孩子仍然在那里!)。

分离的 HEAD 只是一个匿名分支

在 Git 中,在分支上处于分离 HEAD 模式之间的主要区别在于,当您在分支上时,HEAD 文件包含分支的名称。也就是说,它“指向”分支名称,分支名称指向提交,如上图所示。当你有一个分离的 HEAD 时,HEAD 文件直接指向提交......这是分支提示。没有分支名称;当前提交是匿名分支的尖端。

作为一个分支的尖端,它自动没有孩子。

恢复丢失的信息

你可能会反对所有这一切:在一个分支上,所以我分离的 HEAD 应该被认为是我之前所在的分支的一部分!

事实上,这是一个合理的反对意见。 但是你之前在哪个分支你说的只是:“我目前有一个分离的 HEAD,想找到一个子提交。”

假设您以这种方式重新表述问题:“我有一个分离的 HEAD 指向某个提交。我想在使用分支(分支名称)时找到当前提交的子提交 B em>。”

现在我们有足够的信息!由于 Git 的工作方式,我们必须从 B 的尖端开始——分支名称 B 指向的提交——然后从 向后工作B 直到我们到达当前提交(如果有的话)。2有几种内置方法可以做到这一点。

之前在BVengerov's answer 中讨论过的一个是使用git log --children,或等效的git rev-list --childrengit loggit rev-list 本质上是相同的命令,但输出格式不同)。让我们使用git rev-list 来避免在我们只想获取哈希ID 时不得不对--pretty=format--no-patch 大惊小怪。

正如我们刚刚提到的,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

Commit C 只知道它的 parent 提交,而不知道它的子提交。 rev-list 命令可以从我们给它的提示 master 返回到 root 提交 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 这是分支 @987654435 上的提交次数@ 在这一刻。我们没有限制遍历,所以 Git 遍历了从 master 到达的每个提交,反转了结果遍历中的所有箭头,最后,打印了两个带有反向箭头子 ID 的提交哈希:master本身(f8f7adc...),以及主线上提交“就在”之前的 master(master^18213178...)。


2如果当前提交实际上不是分支 B 尖端的祖先——例如,在我们上面的图表中,如果我们询问 @ 987654441@ 当我们提交 F 或提交 G 时,它们都不包含在 master 分支中——那么这个 git rev-list永远到达当前提交。 p>

3为了找到这个计数,我跑了:

git rev-list --count master

它只是给出了在 rev-list walk 中访问的提交的计数。另一种方式是运行:

git rev-list master | wc -l

列出标准输出上的每个提交,该输出通过管道传输到wc 程序,wc 指示计算行数。但是让git rev-list 进行计数的速度大约是原来的两倍。


简单的方法行不通

我在 Git 本身的 Git 存储库中,我做到了:

$ git checkout master
$ git checkout HEAD^

现在git statusHEAD 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 的孩子”或“在通往@987654475 尖端的道路上的 HEAD 的孩子” @",或任何其他分支。或者我们甚至可以在 any 分支上要求孩子:

git rev-list --children [other options] --branches

--branches 标志表示“所有分支”; --branches=abc* 表示“名称以abc 开头的所有分支”;等等。

这里的重点是我们必须告诉 Git 从哪里开始。我们不能从 HEAD 开始。我们可以停止那里,但我们不能开始那里。停在 HEAD 可以加快速度——无需查看超过 43000 次提交——所以我们可以试试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-ing) 提交,同时从后面的 master 接受提交。所以这需要 master 本身,并尝试使用 master^1HEAD 提交),但它会被 x-ed 出来,并尝试(并成功)使用 master^2,然后是底行的所有提交,一旦它重新加入提交被删除的第一行,就会停止。

什么有效

这里的诀窍是我们必须git rev-list 来检查HEAD 提交本身,以便它遵循任何导致HEAD 的箭头。然后我们可以使用grep 或类似的方法来选择具有HEAD 提交的行,并使用--children 以便该行列出以HEAD 作为其父提交的提交:

$ git rev-list --children master | grep "^$(git rev-parse HEAD)"

这会遍历所有 43,000 多次提交,找到我们关心的所有内容和许多我们不关心的内容,然后提取以我们确实关心的提交开头的一行,这是以当前提交 ID 开头的那个(grep "^$(git rev-parse HEAD)"——这里的帽子字符是 grep 的“行首”符号,因此与 Git 本身无关)。

我们可以通过在 HEAD 的任何父节点处终止 walk 来加快速度:

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。

但这是 Git,所以还有另一种方法

rev-list 命令还支持一个标志 --ancestry-path,这正是我们所需要的。

通常,如上所述,git rev-list X..Y“意味着”git rev-list Y ^X:查找从提交 Y 可访问的所有提交,不包括从提交 X 可访问的所有提交。如果X..Y 选择的 DAGlet 中没有合并,则此列表(如果以正确的顺序打印,至少)以第一个提交 提交X 结束或开始。当有 合并时会出现问题:^X 部分丢弃了提交 X 及其祖先,但未能丢弃不是 X 后代的 Y 的祖先。再看一下“浴缸中的 HEAD”图,虽然这次我会在浴缸的另一边添加一些提交,实际上,在其中进行另一个分支和合并:

                       HEAD
                        |
                        v
...--x--x---------------x--o--o---o   <-- master
         \                /    \ /
          o--o--o--o--o--o      o

我们想要的是“x out”不在“你在这里”点(后代)右侧的提交。也就是说,我们想要这个:

                       HEAD
                        |
                        v
...--x--x---------------x--o--o---o   <-- master
         \                /    \ /
          x--x--x--x--x--x      o

所有剩余的os 都是master 的tip 的祖先 HEAD 的后代。这正是--ancestry-path 所做的:它意味着“对于我们明确排除的任何提交,也排除将这些提交作为祖先的提交”。 (也就是说,Git 颠倒了条件:它不能那么容易地测试“是的后代”,但它可以 测试“是的祖先”。如果 DA,那么根据定义,AD的一个祖先,所以通过测试“不是祖先”可以推断出“不是后代” -of”。)

如果我们以某种拓扑排序的顺序列出生成的提交,然后选择一个“最接近”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

我们应该访问剩下的两个提交中的哪一个?假设我们选择“最上面”的那个。我们会去参观较低的吗?我们可以尝试选择较低的,这将使我们全部访问。 (我不会为此建议一种方法。)

现在考虑这个 DAGlet,我认为它类似于苯环或phenyl group

          o--o
         /    \
...--x--x      o   <-- branch
         \    /
          o--o

如果我们移到顶行,我们将如何重新访问底行?如果我们移动到底行,我们将如何重新访问顶行?

真正正确的方法

完整问题的唯一真正解决方案4 是在离开命名分支提示之前标记提交以访问。也就是说,如果您的目标是访问从“我现在所在的位置”到“过去的某个时间点”的某个提交范围内的每个提交(或“每个提交”的一些合理定义的子集),您应该首先标记在整个范围之外。一个简单的方法是运行 git rev-list 以获取该范围内每个提交 ID 的列表(使用 --boundary^X^@ 语法或显式添加起点 X,如果需要将提交X 包含在X..Y 之类的范围内)。然后,您可能会将要访问的每个提交的 ID 保存在一个文件中,因此您在遍历“苯环”DAGlet 时不会错过一些。

或者,您可以标记两个提交,然后在它们之间工作。例如,git bisect 就是这样工作的。


4嗯,显然这取决于你如何约束问题。这就是为什么定义您想要做的事情如此重要!

【讨论】:

    【解决方案2】:

    git log --reverse --children -n1 HEAD 应该可以工作。

    git log --reverse --children -n1 HEAD --pretty=format:"%H" --no-patch 如果您只想查看提交的哈希值。

    herehere 提出了类似的问题。

    【讨论】:

    • 使用git log(或者,在这里可能更好,git rev-list)和--chlidren 可以工作,但不能以这种方式和这种情况下(“分离的HEAD”并查看当前的HEAD) .我会留下答案的原因 - 评论中没有空间。
    【解决方案3】:

    Git 通过查看记录的亲子关系即时获取孩子信息。基线提交查找器是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)
    

    【讨论】:

      【解决方案4】:

      列出分离的 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&lt;tip&gt;,其中tip 是一个commit-ish,通常是包含分离头的分支名称。

      例如:git child HEAD branchname

      它默认给 HEAD 的子节点(除非给出了另一个 commit-ish 参数),方法是按照祖先向当前分支的尖端迈出一步(除非另一个 commit-ish 作为第二个参数给出)。但是请注意,对于分离的头部,“当前分支”是未定义的。

      如果您想要短哈希形式,请使用 %h 而不是 %H

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2017-07-20
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-06-19
        • 2011-11-13
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多