当我在Git中指定一个祖先提交对象时,我混淆了HEAD^和HEAD~。

两者都有“编号”版本,如HEAD^3和HEAD~2。

在我看来它们非常相似或相同,但是波浪号和插入符号之间有什么不同吗?


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

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


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


HEAD~指定“分支”上的第一个父节点。 HEAD^允许你选择一个特定的提交父节点

一个例子:

如果你想要遵循一个分支,你必须指定像这样的东西

master~209^2~15

经验法则

大多数情况下使用~来回溯几代,通常是你想要的 在合并提交时使用^,因为它们有两个或多个(直接)父节点

助记符:

波浪线~在外观上几乎是线性的,并且想要以直线向后走 卡瑞特表示一棵树的有趣部分或道路的岔路口

波浪号

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

<rev>~<n>,例如master~3 修订参数的后缀~<n>表示提交对象是命名提交对象的第n代祖先,仅在第一代父对象之后。例如,<rev>~3等价于<rev>^^^,它等价于<rev>^1^1^1…

你可以得到任何承诺的父母,而不仅仅是HEAD。您还可以通过代向后移动:例如,master~2表示主分支尖端的祖父级,在合并提交时倾向于第一个父级。

插入符号

Git历史是非线性的:有向无环图(DAG)或树。对于只有一个父节点的commit, rev~和rev^意味着同样的事情。插入符号选择器在合并提交中变得非常有用,因为每个插入符号都是两个或多个父节点的子节点——这是从生物学中借来的语言。

HEAD^表示当前分支顶端的第一个直接父结点。HEAD^是HEAD^1的缩写,你也可以在适当的时候称呼HEAD^2等等。git rev-parse文档的同一部分将其定义为

<rev>^,例如HEAD^, v1.5.1^0 对修订形参加上后缀^表示该提交对象的第一个父对象。^<n>表示第n个父元素([例如]<rev>^等同于<rev>^1)。作为一个特殊规则,<rev>^0表示提交本身,当<rev>是引用提交对象的标记对象的对象名时使用。

例子

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

前面提到的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. (N.B. The git log --graph command displays history in the opposite order.) 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存储库。

#! /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 lol和git lola添加别名,以便您可以查看历史

$ 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对象名称与上述名称不同,但标记允许您按名称定位提交,并检查您的理解。

$ 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文档中的“指定修订”包含大量信息,值得深入阅读。参见《Git工具-版本选择》一书。

父级提交顺序

从git自己的历史记录中提交89e4fcb0dd是一个合并提交,正如git show 89e4fcb0dd用merge标题行指示的那样,该标题行显示了直接祖先的对象名称。

提交89年e4fcb0dd01b42e82b8f27f9a575111a26844df 合并:c670b1f876 649bf3a42f b67d40adbb 作者:juno C Hamano <gitster@pobox.com> 日期:2018年10月29日星期一10:15:31 +0900 合并分支“bp/reset-quiet”和“js/mingw-http-ssl”到nd/config-split[…]

我们可以通过让git rev-parse按顺序显示89e4fcb0dd的直接父节点来确认顺序。

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

查询不存在的第四个父节点将导致错误。

$ 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来获取完整的哈希值

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

或者%p表示缩写parents。

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

HEAD^和HEAD~之间的区别可以通过http://www.kernel.org/pub/software/scm/git/docs/git-rev-parse.html上的插图(由Jon Loeliger绘制)很好地描述。

这个文档对于初学者来说可能有点晦涩,所以我复制了下面的插图:

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还有一个用于跟踪“从哪里来的”/“想要返回-现在”的语法——例如,HEAD@{1}将引用您跳转到新提交位置的位置。

基本上,HEAD@{}变量捕获了HEAD移动的历史,你可以使用git reflog命令查看git的reflogs来决定使用特定的HEAD。

例子:

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

一个例子可以是我做了本地提交a->b->c->d,然后我回去丢弃2个提交来检查我的代码- git重置HEAD~2 -然后在那之后我想把我的HEAD移回d - git重置HEAD@{1}。


~和^本身都是指提交的父节点(~~和^^都是指祖父节点提交,等等),但当它们与数字一起使用时,它们的含义有所不同:

~2表示在层次结构中向上两层,如果一个提交有多个父级,则通过第一个父级 ^2表示第二个父节点,其中提交有多个父节点(也就是说,因为它是一个merge)

这些可以组合起来,所以HEAD~2^3意味着HEAD的祖父提交的第三个父提交。


我的意见…


以下是从http://www.paulboxley.com/blog/2011/06/git-caret-and-tilde上逐字逐句摘录的一个很好的解释:

Ref ~是Ref ~1的简写,表示提交的第一个父对象。Ref ~2表示提交的第一个父对象的第一个父对象。Ref ~3表示提交的第一个父级的第一个父级的第一个父级。等等。 Ref ^是Ref ^1的简写,表示提交的第一个父对象。但两者的不同之处在于ref^2表示提交的第二个父对象(记住,当提交是一个merge时,它们可以有两个父对象)。 ^和~操作符可以组合使用。


简单地说:

~指定祖先 ^指定父母

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

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

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

A: a1, a2, a3 B: b1 b2 b3 C: c1 c3 c3

如果现在在分支A上执行命令:

git merge B C

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

and

~表示第一个分支中的第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次~)和to: HEAD^…^ (n乘以^)

注意2:

HEAD^3和HEAD^^^不一样(第一个表示C3,第二个表示A1),

和一般:

HEAD^1和HEAD^是一样的, 但是对于n > 1: HEAD^n总是不等于HEAD^…^ (n乘以^)


简单地说,对于亲子关系的第一级(祖先,继承,世系等),HEAD^和HEAD~都指向同一个提交,它位于HEAD(提交)之上的一个父级。

此外,HEAD^ = HEAD^1 = HEAD~ = HEAD~1。但是HEAD^^ != HEAD^2 != HEAD~2。然而头^^ =头~2。继续读下去。

在第一级亲子关系之外,事情变得更加棘手,特别是如果工作分支/主分支有合并(来自其他分支)。还有一个插入符号的语法问题,HEAD^^ = HEAD~2(它们是等价的)但是HEAD^^ != HEAD^2(它们是完全不同的两个东西)。

每个/插入符号指的是HEAD的第一个父代,这就是为什么串在一起的插入符号相当于波浪号表达式,因为它们指的是第一个父代的(第一个父代的)第一个父代,等等,严格基于连接插入符号上的数字或波浪号后面的数字(无论哪种方式,它们都意味着相同的事情),即保持第一个父代并向上x代。

HEAD~2(或HEAD^^)指的是在层次结构中当前提交(HEAD)上/上两级祖先的提交,这意味着HEAD的祖父级提交。

HEAD^2, on the other hand, refers NOT to the first parent's second parent's commit, but simply to the second parent's commit. That is because the caret means the parent of the commit, and the number following signifies which/what parent commit is referred to (the first parent, in the case when the caret is not followed by a number [because it is shorthand for the number being 1, meaning the first parent]). Unlike the caret, the number that follows afterwards does not imply another level of hierarchy upwards, but rather it implies how many levels sideways, into the hierarchy, one needs to go find the correct parent (commit). Unlike the number in a tilde expression, it is only one parent up in the hierarchy, regardless of the number (immediately) proceeding the caret. Instead of upward, the caret's trailing number counts sideways for parents across the hierarchy [at a level of parents upwards that is equivalent to the number of consecutive carets].

所以HEAD^3等于HEAD提交的第三个父结点(不是曾祖结点,即HEAD^^^ AND HEAD~3)。


TLDR

~是你大多数时候想要的,它引用过去提交到当前分支

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

A~总是等于A^ A~~总是和A^^一样,以此类推 A~2并不等于A^2, 因为~2是~~的缩写 虽然^2不是任何东西的缩写,但它意味着第二个父元素


HEAD~和HEAD^之间区别的一个实际例子:


~表示父母。

^如果它有两个或更多的父节点,比如合并提交。我们可以选择父节点中的第二个或另一个。

如果只有一个东西,比如(HEAD~或HEAD^),结果是一样的。


如果你想知道在你的命令中是输入HEAD^还是HEAD~,那就随便用:

它们都是同一个提交的名称——当前提交的第一个父文件。

master~和master^ -也是如此,这两个名字都是master的第一个父元素。

就像2 + 2和2 * 2都是4一样,它们是不同的方式,但答案是一样的。

这回答了一个问题:Git中HEAD^和HEAD~之间有什么区别?

如果你只是做了一个合并(所以你当前的提交有一个以上的父),或者你仍然对插入号和波浪号的工作原理感兴趣,请参阅其他答案(我不会在这里重复)以获得深入的解释,以及如何重复使用它们(例如head ~~~),或与数字一起使用(例如head ^2)。否则,我希望这个答案能帮你节省一些时间。


^ BRANCH选择器 git结帐头^2 通过移动到所选分支(在提交树上后退一步),选择(合并)提交的第二个分支。

~提交选择器 git结帐头~2 在默认/选择的分支上向后提交2次


将~和^相对引用定义为PARENT选择器是迄今为止我在互联网上看到的主要定义——包括官方的Git Book。是的,它们是PARENT选择器,但这种“解释”的问题是,它完全违背了我们的目标:如何区分两者……:)

另一个问题是当我们被鼓励使用^ BRANCH选择器进行COMMIT选择时(也就是HEAD^ === HEAD~)。 同样,是的,你可以这样使用它,但这不是它的设计目的。^ BRANCH选择器的向后移动行为是一个副作用,而不是它的目的。

只有在合并提交时,才可以将数字分配给^ BRANCH选择器。因此,只有在需要在分支机构之间进行选择时,才能充分利用其容量。 在fork中表达选择的最直接的方法是踏上所选的路径/分支——这是在提交树上后退一步。这只是一种副作用,而不是它的主要目的。


OP:当我在Git中指定一个祖先提交对象时,我混淆了HEAD^和HEAD~。

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

HEAD^(插入号)和HEAD~(波浪号)之间的区别在于它们如何从指定的起点向后遍历历史,在这种特殊情况下是HEAD。

波浪号~

<rev>~[<n>] = select <n>第一代祖先

插入符号 ^

<rev>^[<n>] = select <n>第一代祖先的第一个父母

*第一个父节点总是在merge的左边,例如,在被合并到的分支上提交。

把~和^连在一起

如下图所示,两个选择器~和^可以组合使用。还要注意,不使用HEAD作为起点,任何常规引用都可以使用,比如分支、标记甚至是提交散列。

此外,根据要选择的祖先,^和~可以互换使用,如下表所示。

来源:在这篇关于这个主题的博客文章中可以找到一个完整的纲要。