Git中的HEAD ^和HEAD?有什么区别?


What's the difference between HEAD^ and HEAD~ in Git?

当我在Git中指定祖先提交对象时,我在HEAD^HEAD~之间感到困惑。

两者都有"编号"版本,例如HEAD^3HEAD~2

在我看来,它们看起来非常相似或相同,但是波浪号和插入符号之间是否有任何区别?


经验法则

  • 大多数时候使用~-可以追溯到很多代,通常是您想要的
  • 在合并提交上使用^-因为它们有两个或多个(直接)父级

口诀:

  • Tilde ~的外观几乎是线性的,并且想要沿直线向后移动
  • 插入符^提示道路上有趣的树或叉子部分

波浪号

git rev-parse文档的"指定修订"部分将~定义为

~, e.g. master~3
A suffix ~ to a revision parameter means the commit object that is the nth generation ancestor of the named commit object, following only the first parents. [For example,] ~3 is equivalent to ^^^ which is equivalent to ^1^1^1

您可以进行任何提交的父母,而不仅仅是HEAD。您还可以追溯几代人:例如,master~2表示master分支尖端的祖父母,在合并提交时偏爱第一个父级。

插入符号

Git历史是非线性的:有向无环图(DAG)或树。对于只有一个父对象的提交,rev~rev^表示同一意思。插入符选择器对于合并提交很有用,因为每个提交都是两个或多个父母的孩子,并且会压缩从生物学借来的语言。

HEAD^表示当前分支的尖端的第一个直接父代。 HEAD^HEAD^1的缩写,您也可以根据需要寻址HEAD^2等。 git rev-parse文档的同一部分将其定义为

^, e.g. HEAD^, v1.5.1^0
A suffix ^ to a revision parameter means the first parent of that commit object. ^ means the nth parent ([e.g.] ^ is equivalent to ^1). As a special rule, ^0 means the commit itself and is used when is the object name of a tag object that refers to a commit object.

例子

这些说明符或选择器可以任意链接,例如,英语中的topic~3^2是合并提交的第二个父级,该合并提交是分支topic当前尖端的曾祖父母(返回三代)。

git rev-parse文档的上述部分通过概念性的git历史记录跟踪了许多路径。时间通常向下流动。提交D,F,B和A是合并提交。

Here is an illustration, by Jon Loeliger. Both commit nodes B and C are parents of commit node A. Parent commits are ordered left-to-right.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
G   H   I   J
 \ /     \ /
  D   E   F
   \  |  / \
    \ | /   |
     \|/    |
      B     C
       \   /
        \ /
         A

A =      = A^0
B = A^   = A^1     = A~1
C = A^2
D = A^^  = A^1^1   = A~2
E = B^2  = A^^2
F = B^3  = A^^3
G = A^^^ = A^1^1^1 = A~3
H = D^2  = B^^2    = A^^^2  = A~2^2
I = F^   = B^3^    = A^^3^
J = F^2  = B^3^2   = A^^3^2

运行以下代码以创建一个git存储库,其历史记录与引用的插图匹配。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#! /usr/bin/env perl

use strict;
use warnings;
use subs qw/ postorder /;
use File::Temp qw/ mkdtemp /;

my %sha1;
my %parents = (
  A => [ qw/ B C /               ],
  B => [ qw/     D E F /         ],
  C => [ qw/         F /         ],
  D => [ qw/           G H /     ],
  F => [ qw/               I J / ],
);

sub postorder {
  my($root,$hash) = @_;
  my @parents = @{ $parents{$root} || [] };
  postorder($_, $hash) for @parents;
  return if $sha1{$root};
  @parents = map"-p $sha1{$_}", @parents;
  chomp($sha1{$root} = `git commit-tree @parents -m"$root" $hash`);
  die"$0: git commit-tree failed" if $?;
  system("git tag -a -m '$sha1{$root}' '$root' '$sha1{$root}'") == 0 or die"$0: git tag failed";
}

$0 =~ s!^.*/!!;  # / fix Stack Overflow highlighting
my $repo = mkdtemp"repoXXXXXXXX";
chdir $repo or die"$0: chdir: $!";
system("git init") == 0               or die"$0: git init failed";
chomp(my $tree = `git write-tree`);      die"$0: git write-tree failed" if $?;

postorder 'A', $tree;
system"git update-ref HEAD   $sha1{A}"; die"$0: git update-ref failed" if $?;
system"git update-ref master $sha1{A}"; die"$0: git update-ref failed" if $?;

# for browsing history - http://blog.kfish.org/2010/04/git-lola.html
system"git config alias.lol  'log --graph --decorate --pretty=oneline --abbrev-commit'";
system"git config alias.lola 'log --graph --decorate --pretty=oneline --abbrev-commit --all'";

它仅在git lolgit lola的新的一次性仓库中添加别名,因此您可以按以下方式查看历史记录:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ git lol
*   29392c8 (HEAD -> master, tag: A) A
|\
| * a1ef6fd (tag: C) C
| |
|  \
*-. \   8ae20e9 (tag: B) B
|\ \ \
| | |/
| | *   03160db (tag: F) F
| | |\
| | | * 9df28cb (tag: J) J
| | * 2afd329 (tag: I) I
| * a77cb1f (tag: E) E
*   cd75703 (tag: D) D
|\
| * 3043d25 (tag: H) H
* 4ab0473 (tag: G) G

请注意,在您的计算机上,SHA-1对象名称与上面的名称不同,但是标签允许您按名称处理提交并检查您的理解。

1
2
3
4
5
6
$ git log -1 --format=%f $(git rev-parse A^)
B
$ git log -1 --format=%f $(git rev-parse A~^3~)
I
$ git log -1 --format=%f $(git rev-parse A^2~)
F

git rev-parse文档中的"指定修订版本"包含了很多信息,值得深入阅读。另请参阅Pro Git书中的Git工具-版本选择。

家长提交顺序

git自己的历史记录中的提交89e4fcb0dd是合并提交,如git show 89e4fcb0dd所示,并带有显示直接祖先对象名称的Merge标头行。

1
2
3
4
5
6
commit 89e4fcb0dd01b42e82b8f27f9a575111a26844df
Merge: c670b1f876 649bf3a42f b67d40adbb
Author: Junio C Hamano <[email protected]>
Date:   Mon Oct 29 10:15:31 2018 +0900

    Merge branches 'bp/reset-quiet' and 'js/mingw-http-ssl' into nd/config-split […]

我们可以要求git rev-parse依次显示89e4fcb0dd的直系父母,以确认顺序。

1
2
3
4
$ git rev-parse 89e4fcb0dd^1 89e4fcb0dd^2 89e4fcb0dd^3
c670b1f876521c9f7cd40184bf7ed05aad843433
649bf3a42f344e71b1b5a7f562576f911a1f7423
b67d40adbbaf4f5c4898001bf062a9fd67e43368

查询不存在的第四父级会导致错误。

1
2
3
4
5
$ git rev-parse 89e4fcb0dd^4
89e4fcb0dd^4
fatal: ambiguous argument '89e4fcb0dd^4': unknown revision or path not in the working tree.
Use '--' to separate paths from revisions, like this:
'git <command> [<revision>...] -- [<file>...]'

如果只想提取父母,请使用漂亮的格式%P表示完整的哈希

1
2
$ git log -1 --pretty=%P 89e4fcb0dd
c670b1f876521c9f7cd40184bf7ed05aad843433 649bf3a42f344e71b1b5a7f562576f911a1f7423 b67d40adbbaf4f5c4898001bf062a9fd67e43368

%P(对于缩写父母)。

1
2
$ git log -1 --pretty=%p 89e4fcb0dd
c670b1f876 649bf3a42f b67d40adbb


http://www.kernel.org/pub/software/scm/git/docs/git-rev-parse.html上的插图(Jon Loeliger)很好地描述了HEAD^HEAD~之间的差异。 。

本文档对于初学者可能有点晦涩,因此我复制了以下插图:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
G   H   I   J
 \ /     \ /
  D   E   F
   \  |  / \
    \ | /   |
     \|/    |
      B     C
       \   /
        \ /
         A
A =      = A^0
B = A^   = A^1     = A~1
C = A^2
D = A^^  = A^1^1   = A~2
E = B^2  = A^^2
F = B^3  = A^^3
G = A^^^ = A^1^1^1 = A~3
H = D^2  = B^^2    = A^^^2  = A~2^2
I = F^   = B^3^    = A^^3^
J = F^2  = B^3^2   = A^^3^2


~^本身都引用提交的父级(~~^^都引用祖父母提交,等等。)但是当它们与数字一起使用时,它们的含义不同:

  • ~2表示层次结构中的两个级别,如果提交具有多个父级,则通过第一个父级

  • ^2表示提交具有多个父项的第二个父项(即,因为它是合并项)

这些可以组合,所以HEAD~2^3表示HEAD的祖父母提交的第三父提交。


我的两分钱

enter image description here


这是从http://www.paulboxley.com/blog/2011/06/git-caret-and-tilde逐字记录的很好解释:

ref~ is shorthand for ref~1 and means the commit's first parent. ref~2 means the commit's first parent's first parent. ref~3 means the commit's first parent's first parent's first parent. And so on.

ref^ is shorthand for ref^1 and means the commit's first parent. But where the two differ is that ref^2 means the commit's second parent (remember, commits can have two parents when they are a merge).

The ^ and ~ operators can be combined.

enter image description here


^格式允许您选择提交的第n个父级(与合并相关)。 ~格式允许您选择始终在第一个父项之后的第n个祖先提交。有关一些示例,请参见git-rev-parse的文档。


值得注意的是,git还具有用于跟踪"从何处来" /"现在要回去"的语法-例如,HEAD@{1}将引用您跳转到新位置的位置提交位置。

基本上HEAD@{}变量捕获HEAD移动的历史记录,您可以使用命令git reflog通过查看git的引用日志来决定使用特定的head。

例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
0aee51f HEAD@{0}: reset: moving to HEAD@{5}
290e035 HEAD@{1}: reset: moving to HEAD@{7}
0aee51f HEAD@{2}: reset: moving to HEAD@{3}
290e035 HEAD@{3}: reset: moving to HEAD@{3}
9e77426 HEAD@{4}: reset: moving to HEAD@{3}
290e035 HEAD@{5}: reset: moving to HEAD@{3}
0aee51f HEAD@{6}: reset: moving to HEAD@{3}
290e035 HEAD@{7}: reset: moving to HEAD@{3}
9e77426 HEAD@{8}: reset: moving to HEAD@{3}
290e035 HEAD@{9}: reset: moving to HEAD@{1}
0aee51f HEAD@{10}: reset: moving to HEAD@{4}
290e035 HEAD@{11}: reset: moving to HEAD^
9e77426 HEAD@{12}: reset: moving to HEAD^
eb48179 HEAD@{13}: reset: moving to HEAD~
f916d93 HEAD@{14}: reset: moving to HEAD~
0aee51f HEAD@{15}: reset: moving to HEAD@{5}
f19fd9b HEAD@{16}: reset: moving to HEAD~1
290e035 HEAD@{17}: reset: moving to HEAD~2
eb48179 HEAD@{18}: reset: moving to HEAD~2
0aee51f HEAD@{19}: reset: moving to HEAD@{5}
eb48179 HEAD@{20}: reset: moving to HEAD~2
0aee51f HEAD@{21}: reset: moving to HEAD@{1}
f916d93 HEAD@{22}: reset: moving to HEAD@{1}
0aee51f HEAD@{23}: reset: moving to HEAD@{1}
f916d93 HEAD@{24}: reset: moving to HEAD^
0aee51f HEAD@{25}: commit (amend): 3rd commmit
35a7332 HEAD@{26}: checkout: moving from temp2_new_br to temp2_new_br
35a7332 HEAD@{27}: commit (amend): 3rd commmit
72c0be8 HEAD@{28}: commit (amend): 3rd commmit

一个例子可能是我做了local-commits a-> b-> c-> d,然后我放弃了2次提交以检查我的代码-git reset HEAD~2-然后在那之后我想将HEAD移回d -git reset HEAD@{1}


简单地:

  • ~指定祖先
  • ^指定父母

合并时可以指定一个或多个分支。然后一个提交有两个或两个以上的父母,然后^可用于指示父母。

假设您在分支A上,并且还有两个分支:B和C。

在每个分支上,最后三个提交是:

  • 答:A1,A2,A3
  • B:B1,B2,B3
  • C:C1,C3,C3

如果现在在分支A上,则执行以下命令:

1
git merge B C

然后您将三个分支合并在一起(这里您的合并提交有三个父级)

~表示第一个分支中的第n个祖先,因此

  • HEAD~表示A3
  • HEAD~2表示A2
  • HEAD~3表示A1

^表示第n个父对象,因此

  • HEAD^表示A3
  • HEAD^2表示B3
  • HEAD^3表示C3

~^彼此相邻的下一个用法是在先前字符指定的提交的上下文中。

注意1:

  • HEAD~3始终等于:HEAD~~~HEAD^^^(每个表示A1),

通常:

  • HEAD~n始终等于:HEAD~...~(n倍~)和:HEAD^...^(n倍^)。

注意2:

  • HEAD^3HEAD^^^不同(第一个表示C3,第二个表示A1),

通常:

  • HEAD^1HEAD^相同,
  • 但对于n> 1:HEAD^n始终与HEAD^...^不同(n倍~)。

TLDR

?是您大部分时间想要的,它引用过去对当前分支的提交

^引用父母(git-merge创建第二个父母或更多)

A?总是和A ^相同

A ~~始终与A ^^相同,依此类推

A?2与A ^ 2不同,

因为?2是~~的简写

^ 2并非简写形式,它表示第二个父级


HEAD ^^^与HEAD?3相同,选择HEAD之前的第三次提交

HEAD ^ 2指定合并提交中的第二个头


  • HEAD?指定"分支"上的第一个父对象

  • HEAD ^允许您选择提交的特定父项

一个例子:

如果要遵循侧分支,则必须指定类似

1
master~209^2~15


HEAD?和HEAD ^之间差异的实际示例

HEAD^ VS HEAD~


~这表示父级的父级。

^如果它有两个或多个父级,例如合并提交,我们可以选择第二个或另一个父级。

因此,如果只有一个东西((HEAD?或HEAD ^)),则结果相同。


简而言之,对于第一级亲子关系(祖先,继承,世系等),HEAD ^和HEAD?都指向同一提交,即(位于)HEAD(提交)上方的一个父级。

此外,HEAD ^ = HEAD ^ 1 = HEAD?= HEAD?1。但是HEAD ^^!= HEAD ^ 2!= HEAD?2。 HEAD ^^ = HEAD?2。继续阅读。

除了第一级的育儿之外,事情变得更加棘手,特别是如果工作分支/主分支已经合并(来自其他分支)。插入符号还存在语法问题,HEAD ^^ = HEAD?2(它们等效),但是HEAD ^^!= HEAD ^ 2(它们完全是两个不同的东西)。

每个/插入符号都指HEAD的第一父代,这就是为什么将在一起的插入号等同于代字号表达式的原因,因为它们严格地基于连接的插入号上的数字来引用第一父代(第一父代)的第一父代等。或波浪号后面的数字(无论哪种方式,它们都代表相同的意思),即与第一个父母住在一起并世世代代。

HEAD?2(或HEAD ^^)指的是提交,它是层次结构中当前提交(HEAD)的上一级/上一级的提交,表示HEAD的祖父母提交。

另一方面,HEAD ^ 2并不是指第一个父母的第二个父母的提交,而只是指第二个父母的提交。这是因为插入符号表示提交的父级,并且后面的数字表示引用哪个/哪个父提交(在插入符后没有数字的情况下,第一个父级[因为它是该数字的简写形式为1,表示第一个父级])。与插入符号不同,此后的数字并不表示层次结构向上另一级,而是表示在层次结构中侧身有多少个层次,需要找到正确的父级(提交)。与代字号表达式中的数字不同,该字符在层次结构中仅是一个父级,而不管(立即)插入号中的数字如何。对于整个层次结构中的父母,插入号的尾数不是向上,而是侧向计数(在向上的父级水平上,等于连续的插入号的数量)。

所以HEAD ^ 3等于HEAD提交的第三个父对象(不是曾祖父母,这就是HEAD ^^^ AND HEAD?3会是...)。