前言
今天总结一个小知识点,虽然不难,但是对新手有很强的迷惑性,了解一下也挺好。我们在使用 Git 回退到版本的时候,可能见过这种写法 git reset --hard HEAD~
,有时候也会遇到这种写法 git reset --hard HEAD^
,这两个语句都是将代码库还原到上一个版本,但是只差了一个符号,他们究竟有什么区别呢?这里先给出结论:HEAD~
和 HEAD^
含义不同,功能一样!
HEAD
HEAD
这个词在 git
使用过程中经常出现,作用很像是数据结构中指向二叉树根节点root
的指针。有个 root
指针我们就可以对二叉树进行任意操作,它是二叉树的根基。而 git
中的 HEAD
概念也类似一个指针,它指向是当前分支的“头”,通过这个头节点可以追寻到当前分支之前的所有提交记录。
git
的提交记录之间的关系很像一棵树,或者说是一张图,通过当前的提交记录指向上一次提交记录串联起来,形成一个头结构,而在 git
中我们常常说的切换分支,只不过是 git
客户端帮你把要操作的那条路径的头节点,存储到了 HEAD
文件中。
HEAD
在 git
版本控制中代表头节点,也就是分支的最后一次提交,同时也是一个文件,通常在版本库中 repository/.git/HEAD
,其中保存的一般是 ref: refs/heads/master
这种分支的名字,而本质上就是指向一次提交的 hash
值,一般长成这个样子 ce11d9be5cc7007995b607fb12285a43cd03154b
。
HEAD~ 和 HEAD^
在 HEAD
后面加 ^
或者 ~
其实就是以 HEAD
为基准,来表示之前的版本,因为 HEAD
被认为是当前分支的最新版本,那么 HEAD~
和 HEAD^
都是指次新版本,也就是倒数第二个版本,HEAD~~
和 HEAD^^
都是指次次新版本,也就是倒数第三个版本,以此类推。
这个说法在之前的总结 《git checkout/git reset/git revert/git restore常用回退操作》 中提到过,但是并未展开说,今天就来测试一下。
HEAD 后面 ~ 和 ^ 的区别
其实 HEAD~
和 HEAD^
的作用是相同的,这两者的区别出现在重复使用或者加数字的情况,下面来分情况说明一下。
HEAD~
和 HEAD^
后面都加1
加上参数1之后变成了 HEAD~1
和 HEAD^1
,其实这就是他们本来的面貌,在参数为 1 的情况下可以省略,HEAD~1
表示回退一步,参数1表示后退的步数,默认推到第一个父提交上,而HEAD^1
表示后退一步,直接后退到第n个父提交上,数字1表示是第一个父提交。
这里引入一个父提交的概念,也就是在最新提交之前的最近的提交我称它为父提交,但是父提交会有两个吗?实际上会的,直接的父提交可能会有很多,分支合并是产生父提交的一种常见原因,两个分支合并到一起时,这两个分支的原 HEAD
都会成为合并后最新提交的父提交。
理解了这个概念,我们发现虽然数字是一样的,但是含义却不相同,HEAD~1
中指的是后退的步数,HEAD^1
指的是退到第几个父提交上。
HEAD~
和 HEAD^
后面都加0
这是一种比较特殊的情况, 加上参数0之后变成了 HEAD~0
和 HEAD^0
,其实他们指向的节点没有改变,还是代表了 HEAD
,只要了解这种情况就行了,我还没有见过谁这样写过。
HEAD~
和 HEAD^
后面都加大于1的数字
这时就会发现两者的不同了,比如我们把数字都定为2,那么 HEAD~2
代表后退两步,每一步都后退到第一个父提交上,而 HEAD^2
代表后退一步,这一步退到第二个父提交上,如果没有第二个父提交就会报出以下错误:
fatal: ambiguous argument ‘HEAD^2’: unknown revision or path not in the working tree.
Use ‘–’ to separate paths from revisions, like this:
‘git[ …] – [ …]’
具体示例
上面说了几种加数字的情况,如果是第一次接触可能还是不太明白,没关系,我可以实际操作一下,看个具体的例子就明白了。
准备工作
下面是一个测试代码库的分支结构,一共有 dev1
、dev2
、dev3
、dev4
四个分支,最终合并到 dev1
分支,提交记录如下:
1 | albert@home-pc MINGW64 /d/gitstart (dev1) |
也许有颜色标记会看得更清楚一些,所以截个图放在这:
刚看这种图的时候要注意一点,记录列表中的先后关系不代表提交时间的先后,如果习惯于看SVN的记录以后,很容易在看日志信息时加上时间因素,但是这个时间因素在 git
查看记录时变得不再明显,比如上面记录中的 e330eac
在图形上要比 f82b57b
更接近 HEAD 提交 ce11d9b
,但是因为处在不同的分支上,在合并之前他俩的修改时间还真不一定是哪个更早一些。
树形记录
在 git
的提交记录图上,我们可以确定当前提交的父提交(所依赖的提交)是哪一个或者哪几个,但是不能确定任意两个提交的时间先后,为了能更清楚的看清分支提交的依赖关系,还是看下面这个树形图更方便一些。
1 | graph TB |
查看命令
在验证 HEAD~
和 HEAD^
之前我们先学习一个命令 git rev-parse HEAD
这个命令可以显示出 HEAD
对应的提交的 hash 值,加上 --short
参数就可以显示出长度为7位的短 hash,用起来比较方便,测试如下:
1 | albert@home-pc MINGW64 /d/gitstart (dev1) |
开始测试
下面可以用 git rev-parse --short
命令来测试 HEAD
后面跟不同参数时对应的提交是哪一个了,测试如下:
HEAD~
、HEAD^
、HEAD~1
、HEAD^1
1 | albert@home-pc MINGW64 /d/gitstart (dev1) |
测试后发现,这四种写法结果是一样的,都是指向 HEAD
的第一个父提交,这和我们前面说的观点一致。
HEAD~~
、HEAD^^
、HEAD~2
、HEAD^2
1 | albert@home-pc MINGW64 /d/gitstart (dev1) |
这次我们发现,前三个表示方法是一样的,指向同一个提交记录,但是最后一个与他们不同,这时根据前面提到定义来看就行了,HEAD~~
实际上是 HEAD~1~1
的简写,而~
后的数字就是指的后退的步数,所以 HEAD~~
等价于 HEAD~2
,属于一种合并计算。
HEAD^^
是 HEAD^1^1
的简写,而 ^
后面的数字表示后退一步到第几个父提交上,因为数字是1,所以 HEAD^^
表示退一步到第一个父提交上,在退一步到第一个父提交上,这时与 HEAD~~
的作用是相同的。
HEAD^2
就有些不同了,它表示后退一步到第二个父提交上,所以对照树形图是第二排的第二个节点。
~ 和 ^ 混合使用
看了上面的例子对于 ~
和 ^
的使用应该有些明白了,它俩其实可以组合使用的,比如想退到第5排、第2个节点上,也就是 ca4db4a
上,简单来看需要第一步到第一个父提交上,在退一步到第一个父提交上,然后退一步到第二个父提交上,最后退一步到第一个父提交上。
那么我们根据需求可以写成 HEAD^1^1^2^1
,测试一下看看 hash 是否正确:
1 | albert@home-pc MINGW64 /d/gitstart (dev1) |
测试发现没有问题,其实还可以合并啊,我们知道1是可以省略的,所以可以简写成 HEAD^^^2^
,另外多个 ^
还可以写成 ~n
的形式,所以这个节点还可以表示成 HEAD~2^2^
的样子,测试如下,结果是一样的。
1 | albert@home-pc MINGW64 /d/gitstart (dev1) |
关于 git reset 的一点思考
刚学习 git reset
的命令时一直认为是一个回退命令,其实学习一段时间之后发现,这个命令其实很符合它的名字,就是一个重置(reset)命令,通过 git reset
命令可以修改 HEAD
指向不同的提交,这个提交甚至都不必是当前分支上的某次提交,测试后发现,只要是版本库中合法提交都可以使用这个命令进行设置,相应的版本库的内容也会发生对应的变化,从这一点来看,它真的太强大了,它可以使你正在开发的 dev
分支瞬间变成 master
分支。
总结
HEAD~
后面加数字表示后退的步数,每次后退都默认退到第一个父提交上,HEAD~2
表示连退两步。HEAD^
后面加数字表示只退一步,但是这一步后退到数字表示的父提交上,HEAD^2
表示退一步到第二个父提交上。git
在查看多分支提交记录时,日志的先后顺序不代表提交时间的先后顺序。git reset
命令是一个重置HEAD
的命令,可以指挥版本库指向任何一个合法提交。
俗话说:人不犯我,我不犯人;可俗话又说:先下手为强,后下手遭殃!
俗话说:宁为玉碎,不为瓦全;可俗话又说:留得青山在,不怕没柴烧!
…
其实只要你变成了那个成功的“俗话”,你说的就是金科玉律,警世哲理!2020-5-31 14:51:49