上一回我们聊了 Git 内部的一些基本知识。这次接着聊一聊我们经常会用到的一些东西,比如 branch、tag,等等。
Branch 是什么?Tag 呢?
来不及解释了,直接上 git cat-file 吧。怎么回事,master 分支怎么是一个 commit 对象?
我们已经知道,.git 目录下面存放了仓库的信息,其中哈希对象全都保存在 objects 目录下。那我们再去翻翻其他的目录,会发现 .git/refs/heads 目录下有一个 master 文件,这刚好是 Git 仓库的分支名,这是不是巧合?
<img src="https://pic2.zhimg.com/v2-f94cc15071b97e2d02463702fd8dfcc5_b.png" data-rawwidth="1000" data-rawheight="680" class="origin_image zh-lightbox-thumb" width="1000" data-original="https://pic2.zhimg.com/v2-f94cc15071b97e2d02463702fd8dfcc5_r.png">不妨看看这个文件里面是什么东西。
<img src="https://pic4.zhimg.com/v2-bdb6378e677a8341f7265bc872683baf_b.png" data-rawwidth="1300" data-rawheight="450" class="origin_image zh-lightbox-thumb" width="1300" data-original="https://pic4.zhimg.com/v2-bdb6378e677a8341f7265bc872683baf_r.png">
是不是很眼熟?这不是第三次提交的 commit hash 吗?
<img src="https://pic4.zhimg.com/v2-7f62b1c66c4c58e1a84551c7ef65486f_b.png" data-rawwidth="1440" data-rawheight="920" class="origin_image zh-lightbox-thumb" width="1440" data-original="https://pic4.zhimg.com/v2-7f62b1c66c4c58e1a84551c7ef65486f_r.png">
原来如此,分支其实就是 commit 对象的引用。等等,这里还出现了一个 HEAD,看起来应该跟 master 一样,也是个引用。
<img src="https://pic4.zhimg.com/v2-b9b90240cb48ab8f08e3b0dadcdf5fe3_b.png" data-rawwidth="1280" data-rawheight="700" class="origin_image zh-lightbox-thumb" width="1280" data-original="https://pic4.zhimg.com/v2-b9b90240cb48ab8f08e3b0dadcdf5fe3_r.png">
我们注意到,.git/refs 目录下还有一个 tags 目录,这里应该就是保存标签信息的地方吧。而且看起来标签跟分支一样,也是 commit 对象的引用。创建一个标签试试。
<img src="https://pic2.zhimg.com/v2-8bd1aa05784543cfe256d13a776212ed_b.png" data-rawwidth="1280" data-rawheight="760" class="origin_image zh-lightbox-thumb" width="1280" data-original="https://pic2.zhimg.com/v2-8bd1aa05784543cfe256d13a776212ed_r.png">
同时在 .git/refs/tags 果然出现了一个 third 文件。
<img src="https://pic2.zhimg.com/v2-cc6c74894d1db7b93c50f1e20a2d2675_b.png" data-rawwidth="1000" data-rawheight="660" class="origin_image zh-lightbox-thumb" width="1000" data-original="https://pic2.zhimg.com/v2-cc6c74894d1db7b93c50f1e20a2d2675_r.png">
它的内容也不出所料,就是第三次提交的 commit hash。
<img src="https://pic2.zhimg.com/v2-8bb47668d4cbb68f26f687664884ae45_b.png" data-rawwidth="1260" data-rawheight="450" class="origin_image zh-lightbox-thumb" width="1260" data-original="https://pic2.zhimg.com/v2-8bb47668d4cbb68f26f687664884ae45_r.png">
现在,master 分支和 third 标签都是第三次提交 commit 对象的引用。当然,还有那个神秘的 HEAD。
<img src="https://pic4.zhimg.com/v2-64bbc9808b419899155e895897baddd3_b.png" data-rawwidth="1700" data-rawheight="960" class="origin_image zh-lightbox-thumb" width="1700" data-original="https://pic4.zhimg.com/v2-64bbc9808b419899155e895897baddd3_r.png">
标签和分支都是 commit 对象的引用,那它们有什么不同?别急,我们进行一次提交后再看看。
<img src="https://pic2.zhimg.com/v2-682cf332ab3cce8ad97e7c98da4619e5_b.png" data-rawwidth="1500" data-rawheight="1240" class="origin_image zh-lightbox-thumb" width="1500" data-original="https://pic2.zhimg.com/v2-682cf332ab3cce8ad97e7c98da4619e5_r.png">
看!提交过后,master 分支以及 HEAD 都指向了最新的这个 commit,而 third 标签并没有变化,仍然是第三次提交的引用。这么看来,标签和分支都是对 commit 对象的引用,但标签的引用不会变,而分支的引用会在提交时发生变化。
Git 命令 reset 做了什么?
既然标签和分支都是内容为 commit hash 的文本,我们能不能手工去修改它们呢?答案是:可以。比如,我们用文本编辑器打开 .git/refs/heads/master 和 .git/refs/tags/third,把它们的内容分别改成第三次和第二次提交的 commit hash。
<img src="https://pic3.zhimg.com/v2-bd0d15bfb5d2ab34a1713e4e0688f3f2_b.png" data-rawwidth="1500" data-rawheight="960" class="origin_image zh-lightbox-thumb" width="1500" data-original="https://pic3.zhimg.com/v2-bd0d15bfb5d2ab34a1713e4e0688f3f2_r.png">
并不意外,third 标签指向了第二次提交,master 指向第三次提交,而第四次提交好像没有存在过。但是,第四次提交新增的文件并没有消失,它处于 staged 状态。
<img src="https://pic4.zhimg.com/v2-345cecc7aa9c8918e6ccad4ccaea5e03_b.png" data-rawwidth="1100" data-rawheight="580" class="origin_image zh-lightbox-thumb" width="1100" data-original="https://pic4.zhimg.com/v2-345cecc7aa9c8918e6ccad4ccaea5e03_r.png">
我们把 .git/refs/heads/master 的内容改回第四次提交的 commit hash,然后执行 git reset --soft b026e2a,效果是一样的。
<img src="https://pic4.zhimg.com/v2-c919b4dfea66cc84d72b1e92281da567_b.png" data-rawwidth="1100" data-rawheight="620" class="origin_image zh-lightbox-thumb" width="1100" data-original="https://pic4.zhimg.com/v2-c919b4dfea66cc84d72b1e92281da567_r.png">
很明显,git reset 所做的事情就是修改 master 分支对 commit 对象的引用。当然,git reset 还有很多参数,区别在于对仓库文件状态的处理,但它们共同点,都是修改 .git/refs/heads 目录下,当前分支对应的引用文件。
至于标签,Git 并没有提供修改的命令。如果你发现自己创建了错误的标签,除了删除重建,也可以像我们刚才那样,手动篡改——不仅是内容,标签的名字也可以改。
Git 命令 reflog 有什么用?
如果我们去看 .git/refs/objects 目录,会发现第四次提交的哈希对象还在,并没有因为 git reset 操作而消失(即使加了 --hard 参数也是如此)。然而通过 git log 提供的信息无法追溯到它,也就是说,第四次提交所产生的哈希对象,成为了仓库历史中的孤岛。
<img src="https://pic2.zhimg.com/v2-50717dac34a9a445ef3dfc28c2a4aef5_b.png" data-rawwidth="1200" data-rawheight="720" class="origin_image zh-lightbox-thumb" width="1200" data-original="https://pic2.zhimg.com/v2-50717dac34a9a445ef3dfc28c2a4aef5_r.png">
对于这座孤岛,我们要找到它,首先要知道它叫什么。有熟悉的同学应该反应过来了,git reflog。
<img src="https://pic1.zhimg.com/v2-98cb44eb9de6862644a43cfe4cb91c88_b.png" data-rawwidth="1700" data-rawheight="500" class="origin_image zh-lightbox-thumb" width="1700" data-original="https://pic1.zhimg.com/v2-98cb44eb9de6862644a43cfe4cb91c88_r.png">
了解了前面的知识,再看 git reflog 这个命令的名字,它的含义就很清楚了——打印分支引用的变更日志。既然如此,当然也能找到孤岛的信息,毕竟它曾在分支中留下过脚印。不用多说大家也知道了,引用变更日志一定也保存在 .git 的某个地方。没错,对于 master 分支,这个文件就是 .git/logs/refs/heads/master。
<img src="https://pic2.zhimg.com/v2-d39a8809581ac9c8b377685ef94bd60d_b.png" data-rawwidth="1000" data-rawheight="700" class="origin_image zh-lightbox-thumb" width="1000" data-original="https://pic2.zhimg.com/v2-d39a8809581ac9c8b377685ef94bd60d_r.png">
打开看看。每一行都记录了引用变更前后的 commit hash,以及造成变更的操作。也就是说,git reflog 命令是对这个文件的内容进行格式化的输出。当然了,只有通过 Git 命令进行的操作会记录在案,我们手动篡改的行为并没有在这里留下任何痕迹。
<img src="https://pic1.zhimg.com/v2-7fad0dca6081aa7501aee76c1fa250ec_b.png" data-rawwidth="2294" data-rawheight="800" class="origin_image zh-lightbox-thumb" width="2294" data-original="https://pic1.zhimg.com/v2-7fad0dca6081aa7501aee76c1fa250ec_r.png">
大家一定还有印象,23a057c 这个哈希对象是有 parent 的,为什么会掉队呢?在 Git 的设计中,每一个 commit 对象只知道自己的 parent 是谁,而不清楚谁会把自己当做 parent。因此,仓库历史只能从 HEAD 或 branch 出发,顺着 parent 链回溯到最初的 commit,对于 HEAD 或 branch 之后的 commit 对象,就选择性地无视了。
顺便提一句,很多 Git 入门教程都会告诉我们,git pull 等于依次执行了 git fetch 和 git merge 两个命令。如果只执行 git fetch,也相当于把远端的 commit 对象拿到本地,以孤岛的形式存在。当然了,也是会留下脚印的,否则 git merge 就没办法操作了。
Git 命令 checkout 到底检出了什么?
有了孤岛的 commit hash,就可以上岸了吧。没错,这就是 git checkout。说到这里,有的同学可能困扰过,git checkout 这个命令既可以检出分支,又可以检出标签,还可以检出 commit hash,感觉好乱。看了上一篇以及本篇前面的内容,大家应该很清楚了,其实它做的事情只有一件,就是检出某个 commit 对象(你可以试试检出其他类型的哈希对象,并不会成功)。好了,上岸吧。
<img src="https://pic3.zhimg.com/v2-389401c7784c02e57d17ee757094af6e_b.png" data-rawwidth="1700" data-rawheight="840" class="origin_image zh-lightbox-thumb" width="1700" data-original="https://pic3.zhimg.com/v2-389401c7784c02e57d17ee757094af6e_r.png">
Git 明确的告知我们,这是一座孤岛。你所做的任何操作都有可能是徒劳,除非,你把这座孤岛变成另一个分支。通过 git log 可以看到,HEAD 和 master 这回终于不一样了。master 的引用还在第三次提交,而 HEAD 则始终指向当前所在的 commit。
<img src="https://pic4.zhimg.com/v2-bfbe57c2fdf7e95e76fd0c4e74192e23_b.png" data-rawwidth="1600" data-rawheight="1160" class="origin_image zh-lightbox-thumb" width="1600" data-original="https://pic4.zhimg.com/v2-bfbe57c2fdf7e95e76fd0c4e74192e23_r.png">
查看分支的命令 git branch 也反馈了相同的信息。
<img src="https://pic1.zhimg.com/v2-af62d1dd8f8caadbfe895d80c4fd98dc_b.png" data-rawwidth="1100" data-rawheight="420" class="origin_image zh-lightbox-thumb" width="1100" data-original="https://pic1.zhimg.com/v2-af62d1dd8f8caadbfe895d80c4fd98dc_r.png">
不过,我们还是可以通过 git checkout 命令回到 master 分支,再执行 git reset 命令,让 23a057c 重新回到可追溯的历史中。
Stash 是什么?
关于 git stash 命令怎么用,什么时候用,这里不赘述,我们只看它做了些什么。创建一个文件,加入 stash。
<img src="https://pic1.zhimg.com/v2-cc2fcd356cc1e3e7888199ada5c31b70_b.png" data-rawwidth="1600" data-rawheight="860" class="origin_image zh-lightbox-thumb" width="1600" data-original="https://pic1.zhimg.com/v2-cc2fcd356cc1e3e7888199ada5c31b70_r.png">
操作完之后,.git/objects 目录下多出来三个哈希对象。
<img src="https://pic4.zhimg.com/v2-5c57a1d959650f37cb64e02a4c606d43_b.png" data-rawwidth="1000" data-rawheight="720" class="origin_image zh-lightbox-thumb" width="1000" data-original="https://pic4.zhimg.com/v2-5c57a1d959650f37cb64e02a4c606d43_r.png">
其中两个都是 commit 对象,什么情况?
<img src="https://pic1.zhimg.com/v2-0bcd4e6e4ce582d607bb187f29337960_b.png" data-rawwidth="1200" data-rawheight="640" class="origin_image zh-lightbox-thumb" width="1200" data-original="https://pic1.zhimg.com/v2-0bcd4e6e4ce582d607bb187f29337960_r.png">
先看这个 tree 对象 cd49f69,记录文件树状态的,应该没有什么疑问,就不多解释了。
<img src="https://pic3.zhimg.com/v2-65c3d0d463fd8e8b86aaec1fec52439e_b.png" data-rawwidth="1500" data-rawheight="500" class="origin_image zh-lightbox-thumb" width="1500" data-original="https://pic3.zhimg.com/v2-65c3d0d463fd8e8b86aaec1fec52439e_r.png">
而两个 commit 对象就有点意思了。它们都包含 tree 对象 cd49f69,也拥有相同的 parent,第四次提交 23a057c。令人不解的是,26cd970 还有第二个 parent——1d1ecb1。
<img src="https://pic1.zhimg.com/v2-3cb9018286e1788dd9a70346eb273d68_b.png" data-rawwidth="1280" data-rawheight="940" class="origin_image zh-lightbox-thumb" width="1280" data-original="https://pic1.zhimg.com/v2-3cb9018286e1788dd9a70346eb273d68_r.png">
不过我们发现,.git/refs 目录多了一个 stash 文件,它一定指向这两个 commit 对象中的一个。
<img src="https://pic3.zhimg.com/v2-a227a98b94e3fcda4b3cdf5ed33dfc1a_b.png" data-rawwidth="1280" data-rawheight="940" class="origin_image zh-lightbox-thumb" width="1280" data-original="https://pic3.zhimg.com/v2-a227a98b94e3fcda4b3cdf5ed33dfc1a_r.png">
没错,stash 也是一个引用,它指向的正是拥有两个 parent 的 commit 对象 26cd970。
这两个 commit 对象和 master 分支引用的对象形成了一种奇特的关系。
<img src="https://pic3.zhimg.com/v2-856d8ad4789fa9159b481369b1403c66_b.png" data-rawwidth="1000" data-rawheight="560" class="origin_image zh-lightbox-thumb" width="1000" data-original="https://pic3.zhimg.com/v2-856d8ad4789fa9159b481369b1403c66_r.png">
继续 git stash pop,26cd970 虽然还存在,但 stash 已经不再指向 26cd970。仓库状态也回到了执行 git stash 之前。
<img src="https://pic2.zhimg.com/v2-5e9fceb4d62b4e4f3b45a7f889930081_b.png" data-rawwidth="1500" data-rawheight="850" class="origin_image zh-lightbox-thumb" width="1500" data-original="https://pic2.zhimg.com/v2-5e9fceb4d62b4e4f3b45a7f889930081_r.png">
为什么会这样?别着急,看完下一篇,你就会知道 stash 到底在玩什么花样。
小结
这次我们介绍了 branch、tag,还有 reset、reflog、checkout 等命令,又卖了一把 stash。原本只是想写一篇 rebase 操作指南,没想到一不小心铺垫了这么多。下一篇终于可以写 merge 和 rebase 了。