繁体   English   中英

有没有办法引用当前分离的HEAD的子提交?

[英]Is there a way to refer to a child commit of the current detached HEAD?

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

但是有可能以类似的方式引用子提交吗?

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

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

正如Tim Biegeleisen在评论中指出的那样 ,当你通过分支机构名称结账时,这会让你处于分支的顶端 通过 Git中的定义发生,因为Git与大多数版本控制系统不同。 在Git中,术语“分支”有些含糊不清:我们有分支名称 ,它们被定义为“指向提交的标签,表示该分支的提示”,我们有分支和合并结构 (我喜欢称之为提交DAG中的“DAGlets”)。 看看“分支”究竟是什么意思? 了解更多相关信息。 但是,所有这些的一个关键结果是提交通常一次在多个分支上

缺少的信息

当你在提交DAG中的某个地方指向某个提交,并询问有关子提交时,你正在做出一个假设。 具体来说,您假设一个特定的分支或一组分支 要了解其工作原理,让我们看一下分支如何发展。

分支在运动中

根据定义,分支的尖端是它的结束:在该分支上不再有提交。 如果你运行git commit并创建一个新的,分支名称会自动指向你刚才提交的新提交。 分支现在有一个新的提示最多提交,并且 - 这是真正的踢球者 - 你现在在那个提交 ,当然没有孩子:这是一个新的无子女提交。 它只能通过添加新的提交来获取新的子项。

这在视觉上是有道理的:这里我们处于某个分支的顶端,在分支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现在都在两个分支上: master feature

如果我们现在在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分支并忘记提交F所有内容。 1现在提交AD只在一个分支上, master


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

有人支持分支名称引用日志feature为好,但是当我们做git branch -D feature ,我们提出的Git把它扔掉。


匿名分支在运动中

在Git中,“独立的HEAD”充当一种未命名的分支。 让我们像以前一样使用相同的A through- E (不添加F ),但只需使用git checkout master~1而不是git checkout -b feature master~1 ,并绘制它。 现在,而不是HEAD指向feature并具有提交D feature点,我们得到的是HEAD直接指向提交D

           E   <-- master
          /
A--B--C--D     <-- HEAD

这是一个分离的头什么: HEAD包含原始提交哈希(标识时, a123456...而不是抱着一个分支的名字,让分支名点东西)的一些承诺,尖端,最犯。

尽管如此,在这种情况下,我们可以添加新的提交,就像我们之前做出提交F一样。 由于我们原来的F实际上仍在那里,我也会在那里画出来,并使用G代替这个新的提交:

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

这一切都说明的是,就当你像一个名为分支的feature ,当提交D是最新的, 它对当前分支无子女 (即使有一个提交E有承诺对主D的父-并且,就此而言,作为一个孩子,还有一个被遗弃的提交F !)。

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

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

作为分支的一角,它自动没有孩子。

恢复缺失的信息

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

实际上,这是一个合理的反对意见。 但是,在转移前你吗? 你所说的只是:“我目前有一个独立的HEAD,并希望找到一个孩子提交。”

假设你用这种方式重新解释这个问题:“我有一个分离的HEAD指向一些提交。我想在使用branch(branch-name) B时找到当前提交的子提交。”

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

BVengerov的回答中 ,前面讨论的那个是使用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]我将在打印2个内容后添加-n 2以停止):

$ git rev-list --parents -n 2 master
f8f7adce9fc50a11a764d57815602dcb818d1816 8213178c86d5583ff809c582d6727ad17b6a0bed 08df31eeccfe1576971ea4ba42570a424c3cfc41
8213178c86d5583ff809c582d6727ad17b6a0bed 2a96d39824464c28f2f45f2f4a4d53d7c390c9eb

(这里的master的尖端是合并,有两个父母,因此三个哈希印在一条很长的线上)。

使用--children告诉git rev-list做一些有趣的事情(技术上很难):不是为每次提交打印父母 ,而是--children它本来走过的整个链,找到父母,然后扭转父/子关系。 请记住,我们最初绘制的图形如下:

A <- B <- C <- D   <-- master

Commit C只知道它的提交,而不知道它的子代。 rev-list命令可以从我们给它的提示, master ,回到root commit 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可以到达的每个提交,反转了生成的walk中的所有箭头,最后,打印了两个提交哈希及其附加的反向箭头子ID: master本身( f8f7adc... ),以及在主线( master^18213178... )上提交“just before”master的提交。


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

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 status表示HEAD detached at 8213178 我们想找到8213178的(单个)子8213178 ,它是提交master节点。 所以我们试试这个:

$ git rev-list --children -n 1 HEAD
8213178c86d5583ff809c582d6727ad17b6a0bed

嗯,这是一个半身像! 但出了什么问题?

记住我之前加粗的短语: git rev-list将反转它所遵循的所有箭头 唉,它根本没有箭头到达 HEAD 始于 HEAD8213178... )。 这是它需要的唯一修改( -n 1 )所以它也停在那里,并打印出HEAD的ID并完成。

使用-n 2使它至少遵循一个箭头 - 一个父链接 - 但它并没有真正有用:

$ git rev-list --children -n 2 HEAD
8213178c86d5583ff809c582d6727ad17b6a0bed
2a96d39824464c28f2f45f2f4a4d53d7c390c9eb 8213178c86d5583ff809c582d6727ad17b6a0bed

这一次,它再次从HEAD开始,然后按箭头指向HEAD^2a96d39... ),因此它能够反转箭头:它告诉我们8213178...8213178...的孩子2a96d39... 但我们想知道:什么节点有8213178...作为父母? 并获得Git的发现是,我们必须从某个地方开始超过 8213178...

只有一个正确的地方可以开始

开始的地方是master的小费。 我们之所以知道这一点,是因为我们感兴趣的是“HEAD的孩子们在路上走向了master的尖端”。

如果我们想要的,我们可以问,而不是“HEAD的是,导致顶端的道路上孩子next ”,或“HEAD的是,导致顶端的道路上孩子pu ”,或任何其他的科。 或者我们甚至可以在任何一个分支上要求孩子:

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

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

这里的要点是我们必须告诉Git从哪里开始 我们不能 HEAD 开始 我们可以在那里停下来 ,但我们无法那里开始 停止在HEAD可以加快速度 - 没有必要查看超过HEAD..master次提交 - 所以我们可以尝试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 ,也有其实在18个到达提交HEAD..master 这是因为在master^2上有17个提交,它们也不在master^1 添加合并本身,您将获得这18个提交。 准确地绘制它一般很难,因为Git的提交DAG非常混乱,但简化的图片看起来像这样的“浴缸里的头”:

                       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然后沿底行的所有提交,停止一次它重新加入提交获取x-ed的顶行。

什么有效

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

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

这将遍历所有43千加承诺,找到我们所关心的一切,很多事情我们不这样做,然后提取该开头的一行提交我们关心的,这是一个开始,当前提交ID ( 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 walk,并且 - 至关重要 - 使用grep来挑选所需的行,然后查看所有子提交ID。

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

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

通常,如上所述, git rev-list X..Y “表示” git rev-list Y ^X :查找从提交Y可到达的所有提交,排除从提交X可到达的所有提交。 如果X..Y选择的X..Y中没有合并,则此列表 - 如果以正确的顺序打印,至少结束或从提交X 的第一次提交开始。 合并发生的问题: ^X部分扔出去提交X和它的祖先,但没有折腾出的祖先Y不在的后代X 再看看“浴缸中的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

所有剩余的o s为主机 HEAD的后代 的尖端的两个祖先 这正是--ancestry-path所做的:它意味着“对于我们明确排除的任何提交,也排除那些没有那些提交作为祖先的提交”。 (也就是说,Git反转条件:它不能如此轻易地测试“是后代”,但它可以测试“是祖先”。如果DA的后代,那么根据定义, AD的祖先,所以通过测试“不是祖先”它可以推断出“不是后代”。)

如果我们以某种拓扑排序顺序列出结果提交,然后选择一个“最接近” 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,我认为它类似于苯环或苯基

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

如果我们移到第一行,我们将如何重新审视最后一行? 如果我们移到最底行,我们将如何再次访问最上一行?

真正正确的方式

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

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


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

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

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

这里这里也提出类似的问题。

列出所有独立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的所有子项。

列出一个特定的孩子

如果您只想要特定分支的祖先中的孩子:

根据这个问题的答案,我在.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通过查看记录的亲子关系,即时获取子信息。 基线提交查找器是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)

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM