找回密码
 立即注册
首页 业界区 业界 git寻根——^和~的区别

git寻根——^和~的区别

甦忻愉 2025-5-29 16:36:13
一. 引子
在git操作中,我们可以使用checkout命令检出某个状态下文件,也可以使用reset命令重置到某个状态,这里所说的“某个状态”其实对应的就是一个提交(commit).
我们可以把一个git仓库想象成一棵树,每个commit就是树上的一个节点。家家都有一本自己的祖谱。祖谱记录了一个家族的生命史,它不仅记录着该家族的来源、迁徙的轨迹,还包罗了该家族生息、繁衍、婚姻、文化、族规、家约等历史文化的全过程。类似的,每个git仓库都有一本自己的祖谱,仓库中commit ID的繁衍,HEAD指针的迁徙,分支的增加、更新,同样的记录着一个仓库从无到有的点点滴滴。
在git中,我们其实可以通过^和~来定位某个具体的commit,而不用每次都去敲繁琐的hash值。为了便于大家理解,先把结论放在前面:

  • “^”代表父提交,当一个提交有多个父提交时,可以通过在”^”后面跟上一个数字,表示第几个父提交,”^”相当于”^1”.
  • ~相当于连续的个”^”.
  • checkout只会移动HEAD指针,reset会改变HEAD的引用值。
使用git log –graph 命令,可以查看自己仓库的当前分支提交ID的树状图,如下图所示。
1.jpeg

使用git log –pretty=raw命令,可以查看commit之间的父子关系,如下图所示,需要注意的是最开始的commit是没有父提交的。
2.jpeg

二. 困惑
在使用git的过程中,你也许会有很多的困惑。
在使用reset或checkout命令的时候,需要一个参数,但是每次都输入commit hash值是一件比较麻烦的事情。首先你得去查询下日志,然后再用键盘将前面几位hash值输入。有时候你一次还搞不定,突然开个小差,暗恋下女神,想一想基友,都容易把hash值遗忘或弄错。肿么办???
又话说突然间,一堆带有hash值的符号出现在生活中,HEAD^1~4,~3^2,我擦!这是TMD玩意儿?不懂啊,使用过程中,HEAD和引用各种乱窜,根本不听从我的指挥,哎呀,妈呀!我成了git的奴隶,从此生活不再美好。肿么办???
不,生活还要继续,要和git做朋友。做朋友当然先要摸清楚朋友的性情和脾气咯,有了好友,生活才会充满希望。
三. 解惑
古有“射人先射马,擒贼先擒王”,今有“git仓库顺藤摸瓜”。既然commit形成的树状图,表明了各个commit之间的关系,那么我们也可以顺着这棵树去查询commit的值。一般情况下,一个commit都会有一个父提交,那么通过^这个表达式,就可以访问到其父提交的ID值;使用~也可以达到同样的功效哦。
我们知道每提交一次,HEAD就会自动移到版本库中最近的一次提交。那么HEAD^就代表了最近一次提交的父提交,HEAD~也是同样的道理;但是如果你想当然的认为^和~的用法相同,那就错了,其实它们的区别还是蛮大的。
四. 详解
我们来通过一个具体的例子,来讲解一下^和~的用法区别,同时在checkout或reset的过程中,看看HEAD和引用的变化。
查看HEAD和引用的值
我们可以通过命令来查看HEAD和引用的值,也可以通过当前仓库下的.git目录去访问。当前分支为master时,我们查看HEAD的值,命令如下:
  1. $ cat .git/HEAD
  2. ref: refs/heads/master
复制代码
然后,我们可以查看master引用的值
  1. $ cat .git/refs/heads/master
  2. 3b0370b.......  # hash code
复制代码
master分支上初始化,并提交一次 
在master分支上新建一个提交”c1”,生成commit ID 973c,这时候master引用指向973c,HEAD指向master引用。
  1. $ git init
  2. Initialized empty Git repository
  3. $ echo c1 >> a
  4. $ git add a
  5. $ git commit
  6. [master (root-commit) 973c5dd] c1
  7. 1 files changed, 1 insertions(+), 0 deletions(-)
  8. create mode 100644 a
  9. $ git log --oneline
  10. 973c5dd c1
复制代码
对应的图如下所示:
3.jpeg

基于master新建br1分支,并提交两次
接下来在master分支基础上新建分支”br1”,并在”br1”上提交”c2”,commit ID为1c73,这时候HEAD指向br1,br1引用指向”c2”对应提交1c73.
  1. $ git checkout -b br1
  2. Switched to a new branch 'br1'
  3. $ echo c2 >> b
  4. $ git add b
  5. $ git commit
  6. [br1 1c7383c] c2
  7. 1 file changed, 1 insertion(+)
  8. create mode 100644 b
  9. $ git log --oneline
  10. 1c7383c c2
  11. 973c5dd c1
复制代码
对应的图如下所示:
4.png

在分支”br1”上,提交”c3”,commit ID为4927,此时HEAD指向br1,br1引用指向”c3”对应提交4927.
  1. $ echo c3 >> b
  2. $ git commit -a -m "c3"
  3. [br1 4927c6c] c3
  4. 1 file changed, 1 insertion(+)
  5. $ git log --oneline
  6. 4927c6c c3
  7. 1c7383c c2
  8. 973c5dd c1
复制代码
对应的图如下所示:
5.png
  
 
切换到master分支,基于master分支新建br2分支,并提交两次
我们先切回到master分支,然后新建分支br2,先后提交”c4”和”c5”,对应的ID分别是”86ba”和”063f”,这时候HEAD指向br2,br2引用指向”c5”的对应提交063f.git 命令如下:
  1. $ git chechout master
  2. Switched to branch 'master'
  3. $ git checkout -b br2
  4. Switched to a new branch 'br2'
  5. $ echo c4 >> c
  6. $ git add c
  7. $ git commit -m "c4"
  8. [br2 86ba564] c4
  9. 1 file changed, 1 insertion(+)
  10. create mode 100644 c
  11. $ git log --oneline
  12. 86ba564 c4
  13. 973c5dd c1
  14. $ echo c5 >> c
  15. $ git commit -a -m "c5"
  16. [br2 063f6e6] c5
  17. 1 file changed, 1 insertion(+)
  18. $ git log --oneline
  19. 063f6e6 c5
  20. 86ba564 c4
  21. 973c5dd c1
复制代码
对应的图如下所示:
6.png
  
 
切换到master分支,基于master分支创建br3分支,并提交两次
这个操作同分支br2上类似,先从br2分支切换到master分支,然后新建分支br3,分别提交”c6”和”c7”,对应的ID分别是”50f1”和”4f9c”,这时候HEAD指向br3,br2引用指向”c7”的对应提交4f9c,git 命令如下:
  1. $ git chechout master
  2. Switched to branch 'master'
  3. $ git checkout -b br3
  4. Switched to a new branch 'br3'
  5. $ echo c6 >> d
  6. $ git add d
  7. $ git commit -m "c6"
  8. [br3 50f14f6] c6
  9. 1 file changed, 1 insertion(+)
  10. create mode 100644 d
  11. $ git log --oneline
  12. 50f14f6 c6
  13. 973c5dd c1
  14. $ echo c7 >> c
  15. $ git commit -a -m "c7"
  16. [br2 4f9ca79] c7
  17. 1 file changed, 1 insertion(+)
  18. $ git log --oneline
  19. 4f9ca79 c7
  20. 50f14f6 c6
  21. 973c5dd c1
复制代码
对应的图如下所示:
7.png

切换到master分支,合并br1,br2和br3分支
先切换到master分支,然后合并br1 br2 br3,会新生成一个提交3b03.
  1. $ git checkout master
  2. $ git merge br1 br2 br3
  3. 3 files changed, 6 insertions(+)
  4. create mode 100644 b
  5. create mode 100644 c
  6. create mode 100644 d
  7. $ git log --oneline
  8. 3b0370b Merge braches 'br1', 'br2' and 'br3'
  9. 4f9ca79 c7
  10. 50f14f6 c6
  11. 063f6e6 c5
  12. 86ba564 c4
  13. 4927c6c c3
  14. 1c7383c c2
  15. 973c5dd c1
复制代码
这时候,运用git log –oneline –graph查看生成的树状图,如下所示.
8.jpeg

从上图分析,在第1条红线上的commit顺序是: 3b03→4927→1c73→973c
第2条红线上的commit顺序是:3b03→063f→86ba→973c
第3条黄线上的commit顺序是:3b03→4f9c→50f1→973c
这3条线的从左至右的顺序非常重要,因为HEAD^1对应的就是第1条红线的提交4927,HEAD^2对应的是第2条绿线的063f提交,HEAD^3对应的是第3条黄线的4f9c提交。3b03没有第4个父提交,因此也没有第4条线,这时候访问HEAD^n(n>3)都会报错。
因此从任何一条线上,我们都可以追溯到”c1”的commit,但是每条线上的中间节点,只能通过这条线上的节点去访问。
操作同上类似,最后的状态如下,这时候HEAD指向master,master引用指向”c8”的对应提交3b03.
对应的图如下所示:
9.png

我们再来看看3b03对应节点的父提交,如下图所示:
10.jpeg

 
从图得知,3b03一共有三个父提交,分别是4927,063f,4f9c.
reset与checkou的区别
在master分支上,当前提交为3b03,使用git reset –hard HEAD^,将master重置到HEAD的父提交;该命令也可以写成git reset –hard HEAD^1
  1. $ git reset --hard HEAD^
  2. HEAD is now at 4927c6c c3
复制代码
对应的图如下所示:
11.png
  1. <br>这时候,HEAD还是指向master分支,但是master引用的commit值已经变成了4927,即3b03的第一个父提交的ID.
复制代码
  1. [/code]然后,我们再重置到”c8”的commit”3b03”,git reset –hard 3b03,然后使用命令git checkout HEAD~ ,git 操作如下:
  2. [code]$ git reset --hard 3b03
  3. HEAD is now at 3b0370b Merge branches 'br1', 'br2' and 'br3'
  4. $ git checkout HEAD~
  5. HEAD is now at 4927c6c... c3
复制代码
对应的图如下所示:
12.jpeg

这时候,HEAD指向了commit 4927,即3b03的第一个父提交ID,但是master引用还是对应的3b03.
从上面的测试,我们可以得出以下结论:

  • HEAD^,HEAD^1和HEAD~三个表达式都是代表了HEAD的父提交
  • reset 的时候,HEAD不变,但是HEAD指向的引用值会变成相应的值;checkout 的时候,HEAD直接变成值,但原来引用中保存的值不变。
^n和~n的区别

(|HEAD)^n,指的是HEAD的第n个父提交(HEAD有多个父提交的情况下),如果HEAD有N个父提交,那么n取值为n < = N.
(|HEAD)~n,指的是HEAD的第n个祖先提交,用一个等式来说明就是:(|HEAD)~n = (|HEAD)^^^….(^的个数为n).我们通过例子来验证一下吧。
我们沿用上面演示用的仓库,先检出到master分支,再使用git checkout HEAD^2,看看我们检出了哪个commit
  1. $ git checkout master
  2. $ git checkout HEAD^2
  3. HEAD is now at 063f6e6... c5
复制代码
我们发现”c5”对应的commit值063f正是3b03第二个父提交的commit 对应的图如下所示:
13.jpeg

现在再切回master分支,git checkout master
然后使用git checkout HEAD^3,那么按照规律,就应该检出3b03的第三个父提交的commit,即”c7”的commit值4f9c.
  1. $ git checkout master
  2. Previous HEAD position was 063f6e6... c5
  3. Switched to branch 'master'
  4. $ git checkout HEAD^3
  5. HEAD is now at 4f9ca79... c7
复制代码
对应的图如下所示:
14.jpeg

果然没错,一切都在我们的预料之中!
现在验证下HEAD~的用法,切换到master分支,然后git checkout HEAD~2
  1. $ git checkout master
  2. $ git checkout HEAD~2
  3. HEAD is now at 1c7383c... c2
复制代码
15.png

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

  • “^”代表父提交,当一个提交有多个父提交时,可以通过在”^”后面跟上一个数字,表示第几个父提交,”^”相当于”^1”.
  • ~相当于连续的个”^”.
  • checkout只会移动HEAD指针,reset会改变HEAD的引用值。
现在看到^和~两个符号,再也不会彷徨和害怕了,因为我们知道了它们之间的关系及区别,从此我们过上了幸福的生活。
  
  
  
  
  
  
  
 
  
  

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
您需要登录后才可以回帖 登录 | 立即注册