所有232 展开类目

git寻根——^和~的区别 ƕ

2013/05/13
嘘...

二. 困惑

在使用git的过程中,你也许会有很多的困惑。

在使用reset或checkout命令的时候,需要一个<commit>参数,但是每次都输入commit hash值是一件比较麻烦的事情。首先你得去查询下日志,然后再用键盘将前面几位hash值输入。有时候你一次还搞不定,突然开个小差,暗恋下女神,想一想基友,都容易把hash值遗忘或弄错。肿么办???

又话说突然间,一堆带有hash值的符号出现在生活中,HEAD^1~4,<commit>~3^2,我擦!这是TMD玩意儿?不懂啊,使用过程中,HEAD和引用各种乱窜,根本不听从我的指挥,哎呀,妈呀!我成了git的奴隶,从此生活不再美好。肿么办???

不,生活还要继续,要和git做朋友。做朋友当然先要摸清楚朋友的性情和脾气咯,有了好友,生活才会充满希望。

三. 解惑

古有“射人先射马,擒贼先擒王”,今有“git仓库顺藤摸瓜”。既然commit形成的树状图,表明了各个commit之间的关系,那么我们也可以顺着这棵树去查询commit的值。一般情况下,一个commit都会有一个父提交,那么通过<commit>^这个表达式,就可以访问到其父提交的ID值;使用<commit>~也可以达到同样的功效哦。

我们知道每提交一次,HEAD就会自动移到版本库中最近的一次提交。那么HEAD^就代表了最近一次提交的父提交,HEAD~也是同样的道理;但是如果你想当然的认为^和~的用法相同,那就错了,其实它们的区别还是蛮大的。

四. 详解

我们来通过一个具体的例子,来讲解一下^和~的用法区别,同时在checkout或reset的过程中,看看HEAD和引用的变化。

查看HEAD和引用的值

我们可以通过命令来查看HEAD和引用的值,也可以通过当前仓库下的.git目录去访问。当前分支为master时,我们查看HEAD的值,命令如下:

$ cat .git/HEAD
ref: refs/heads/master

 然后,我们可以查看master引用的值

$ cat .git/refs/heads/master
3b0370b.......  # hash code

master分支上初始化,并提交一次 

在master分支上新建一个提交”c1”,生成commit ID 973c,这时候master引用指向973c,HEAD指向master引用。

$ git init
Initialized empty Git repository
$ echo c1 >> a
$ git add a
$ git commit
[master (root-commit) 973c5dd] c1
 1 files changed, 1 insertions(+), 0 deletions(-)
 create mode 100644 a
$ git log --oneline
973c5dd c1 

对应的图如下所示:

 

基于master新建br1分支,并提交两次

接下来在master分支基础上新建分支”br1”,并在”br1”上提交”c2”,commit ID为1c73,这时候HEAD指向br1,br1引用指向”c2”对应提交1c73.

$ git checkout -b br1
Switched to a new branch 'br1'
$ echo c2 >> b
$ git add b
$ git commit
[br1 1c7383c] c2
 1 file changed, 1 insertion(+)
 create mode 100644 b
$ git log --oneline
1c7383c c2
973c5dd c1

对应的图如下所示:

在分支”br1”上,提交”c3”,commit ID为4927,此时HEAD指向br1,br1引用指向”c3”对应提交4927.

$ echo c3 >> b
$ git commit -a -m "c3"
[br1 4927c6c] c3
 1 file changed, 1 insertion(+)
$ git log --oneline
4927c6c c3
1c7383c c2
973c5dd c1

对应的图如下所示:

切换到master分支,基于master分支新建br2分支,并提交两次

我们先切回到master分支,然后新建分支br2,先后提交”c4”和”c5”,对应的ID分别是”86ba”和”063f”,这时候HEAD指向br2,br2引用指向”c5”的对应提交063f.git 命令如下:

$ git chechout master
Switched to branch 'master'
$ git checkout -b br2
Switched to a new branch 'br2'
$ echo c4 >> c
$ git add c
$ git commit -m "c4"
[br2 86ba564] c4
 1 file changed, 1 insertion(+)
 create mode 100644 c
$ git log --oneline
86ba564 c4
973c5dd c1
$ echo c5 >> c
$ git commit -a -m "c5"
[br2 063f6e6] c5
 1 file changed, 1 insertion(+)
$ git log --oneline
063f6e6 c5
86ba564 c4
973c5dd c1

对应的图如下所示:

切换到master分支,基于master分支创建br3分支,并提交两次

这个操作同分支br2上类似,先从br2分支切换到master分支,然后新建分支br3,分别提交”c6”和”c7”,对应的ID分别是”50f1”和”4f9c”,这时候HEAD指向br3,br2引用指向”c7”的对应提交4f9c,git 命令如下:

$ git chechout master
Switched to branch 'master'
$ git checkout -b br3
Switched to a new branch 'br3'
$ echo c6 >> d
$ git add d
$ git commit -m "c6"
[br3 50f14f6] c6
 1 file changed, 1 insertion(+)
 create mode 100644 d
$ git log --oneline
50f14f6 c6
973c5dd c1
$ echo c7 >> c
$ git commit -a -m "c7"
[br2 4f9ca79] c7
 1 file changed, 1 insertion(+)
$ git log --oneline
4f9ca79 c7
50f14f6 c6
973c5dd c1

对应的图如下所示:

切换到master分支,合并br1,br2和br3分支

先切换到master分支,然后合并br1 br2 br3,会新生成一个提交3b03.

$ git checkout master
$ git merge br1 br2 br3
 3 files changed, 6 insertions(+)
 create mode 100644 b
 create mode 100644 c
 create mode 100644 d
$ git log --oneline
3b0370b Merge braches 'br1', 'br2' and 'br3'
4f9ca79 c7
50f14f6 c6
063f6e6 c5
86ba564 c4
4927c6c c3
1c7383c c2
973c5dd c1

这时候,运用git log –oneline –graph查看生成的树状图,如下所示.

从图得知,3b03一共有三个父提交,分别是4927,063f,4f9c.

reset与checkou的区别

在master分支上,当前提交为3b03,使用git reset –hard HEAD^,将master重置到HEAD的父提交;该命令也可以写成git reset –hard HEAD^1

$ git reset --hard HEAD^
HEAD is now at 4927c6c c3

对应的图如下所示:

这时候,HEAD还是指向master分支,但是master引用的commit值已经变成了4927,即3b03的第一个父提交的ID.

然后,我们再重置到”c8”的commit”3b03”,git reset –hard 3b03,然后使用命令git checkout HEAD~ ,git 操作如下:

$ git reset --hard 3b03
HEAD is now at 3b0370b Merge branches 'br1', 'br2' and 'br3'
$ git checkout HEAD~
HEAD is now at 4927c6c... c3

对应的图如下所示:

这时候,HEAD指向了commit 4927,即3b03的第一个父提交ID,但是master引用还是对应的3b03.
从上面的测试,我们可以得出以下结论:
    1. HEAD^,HEAD^1和HEAD~三个表达式都是代表了HEAD的父提交
  2. reset <commit>的时候,HEAD不变,但是HEAD指向的引用值会变成相应的<commit>值;checkout <commit>的时候,HEAD直接变成<commit>的值,但原来引用中保存的值不变。

^和~的区别
(<commit>|HEAD)^n,指的是HEAD的第n个父提交(HEAD有多个父提交的情况下),如果HEAD有N个父提交,那么n取值为n < = N.

(<commit>|HEAD)~n,指的是HEAD的第n个祖先提交,用一个等式来说明就是:(<commit>|HEAD)~n = (<commit>|HEAD)^^^….(^的个数为n).我们通过例子来验证一下吧。

我们沿用上面演示用的仓库,先检出到master分支,再使用git checkout HEAD^2,看看我们检出了哪个commit.

$ git checkout master
$ git checkout HEAD^2
HEAD is now at 063f6e6... c5
我们发现”c5”对应的commit值063f正是3b03第二个父提交的commit 。对应的图如下所示:

现在再切回master分支,git checkout master

然后使用git checkout HEAD^3,那么按照规律,就应该检出3b03的第三个父提交的commit,即”c7”的commit值4f9c.

$ git checkout master
Previous HEAD position was 063f6e6... c5
Switched to branch 'master'
$ git checkout HEAD^3
HEAD is now at 4f9ca79... c7

对应的图如下所示:

果然没错,一切都在我们的预料之中!

现在验证下HEAD~的用法,切换到master分支,然后git checkout HEAD~2

$ git checkout master
$ git checkout HEAD~2
HEAD is now at 1c7383c... c2
对应的图如下所示:

这时候HEAD悄然来到了”c2”的commit 1c73,因此,HEAD~2 相当于HEAD的第一个父提交的第一个父提交。即HEAD~2 = HEAD^^ = HEAD^1^1, 符合预期!好开心的哟!


五.总结
   1.“^”代表父提交,当一个提交有多个父提交时,可以通过在”^”后面跟上一个数字,表示第几个父提交,”^”相当于”^1”.

   2.~<n>相当于连续的<n>个”^”.

   3.checkout只会移动HEAD指针,reset会改变HEAD的引用值。

现在看到^和~两个符号,再也不会彷徨和害怕了,因为我们知道了它们之间的关系及区别,从此我们过上了幸福的生活。
 
 

 

3 条评论   |   账号登录: è域账号     Ƹ淘宝账号     ǔ新浪微博
iGNU
iGNU 发表于: 2016-05-31 11:29:19
@hustwolf
如果是merge A into B操作的话,那么parent 1是B,而parent 2是A。
沙弥小_116
沙弥小_116 发表于: 2015-08-18 09:38:36
也有一个问题,这个导图蛮好看的,用的什么工具啊?
hustwolf
hustwolf 发表于: 2015-03-27 18:56:51
有一个问题,假如一个commit有多个parent,这多个parent是根据什么算法形成顺序的。即HEAD^1 , HEAD^2 这确定的parent commit是怎么算的? 难道是根据这些parent的commit时间?时间差最小,就越是主线?
loading...