首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

从撤销 rebase 谈谈 git 原理

这是我在我的知识星球中分享的文章,欢迎加入星球阅读更多文章并与我交流。假设我们有两个分支,a 和 b,它们的提交都有一个相同的父提交(master 指向的那次提交)。如图所示:现在我们在分支 a 上,然后 rabase 到分支 b 上。如图所示:平时开发中经常遇到这种情况,假设分支 a 和 b 是两个独立的 feature 分支,但是不小心被我们错误的 rebase 了。现在相当于两个 feature 分支中原本独立的业务被揉起来了,当然是我们不想看到的结果,那么如何撤销呢?一种方案是利用 reflog 命令。利用 reflog 撤销变基

我们先不考虑原理,直接上解决方案,首先输入 ,你会看到如下图所示的日志:最后的输出其实是最早的操作,我们逐条分析下:

HEAD@: 这里我们创建了初始的提交

HEAD@:检出了分支 a

HEAD@:在分支 a 上做了一次提交,注意 master 分支没有变动

HEAD@:从分支 a 回到分支 master,相当于向后退了一次

HEAD@:检出了分支 b

HEAD@:在分支 b 上做了一次提交,注意 master 分支没有变动

HEAD@:这一步开始变基到分支 a,首先切换到分支 a 上

HEAD@:把分支 b 对应的那次提交变基到分支 a 上

HEAD@:变基结束,因为是在 b 上发起的变基,所以最后还切回分支 b

如果我们想撤销此次 rebase,只要输入以下命令就可以了:

git reset --hard HEAD@{3}

此时再看,已经“恢复”到 rebase 前的状态了。的是不是感觉很神奇呢,先别着急,后面会介绍这么做的原理。git 工作原理简介

为了搞懂 git 是如何工作的,以及这些命令背后的原理,我想有必要对 git 的模型有基础的了解。首先,每一个 git 目录都有一个名为 的隐藏目录,关于 git 的一切都存储于这个目录里面(全局配置除外)。这个目录里面有一些子目录和文件,文件其实不重要,都是一些配置信息,后面会介绍其中的 HEAD 文件。子目录有以下几个:

info:这个目录不重要,里面有一个 exclude 文件和 文件的作用相似,区别是这个文件不会被纳入版本控制,所以可以做一些个人配置。

hooks:这个目录很容易理解, 主要用来放一些 git 钩子,在指定任务触发前后做一些自定义的配置,这是另外一个单独的话题,本文不会具体介绍。

objects:用于存放所有 git 中的对象,下面单独介绍。

logs:用于记录各个分支的移动情况,下面单独介绍。

refs:用于记录所有的引用,下面单独介绍。

本文主要会介绍后面三个文件夹的作用。git 对象

git 是面向对象的!

git 是面向对象的!

git 是面向对象的!没错,git 是面向对象的,而且很多东西都是对象。我举个简单的例子,来帮助大家理解这个概念。假设我们在一个空仓库里,编辑了 2 个文件,然后提交。此时都会有那些对象呢?首先会有两个数据对象,每个文件都对应一个数据对象。当文件被修改时,即使是新增了一个字母,也会生成一个新的数据对象。其次,会有一个树对象用来维护一系列的数据对象,叫树对象的原因是它持有的不仅可以是数据对象,还可以是另一个树对象。比如某次提交了两个文件和一个文件夹,那么树对象里面就有三个对象,两个是数据对象,文件夹则用另一个树对象表示。这样递归下去就可以表示任意层次的文件了。最后则是提交对象,每个提交对象都有一个树对象,用来表示某一次提交所涉及的文件。除此以外,每一个提交还有自己的父提交,指向上一次提交的对象。当然,提交对象还会包含提交时间、提交者姓名、邮箱等辅助信息,就不多说了。假设我们只有一个分支,以上知识点就足够解释 git 的提交历史是如何计算的了。它并不存储完整的提交历史,而是通过父提交的对象不断向前查找,得出完整的历史。注意开头那张图片,分支 b 指向的提交是 ,不妨来看下它是何方神圣:

git cat-file -t 9cbb015

git cat-file -p 9cbb015

这里我们使用 命令,其中 参数打印对象的类型, 参数会智能识别类型,并打印其中的内容。输出结果如图所示:可见 是一个提交对象,里面包含了树对象、父提交对象和各种配置信息。我们可以再打印树对象看看:这表示本次提交只修改了 begin 这个文件,并且输出了 begin 这个文件对于的数据对象。git 引用

既然 git 是面向对象的,那么有没有指正呢?还真是有的,分支和标签都是指向提交对象的指针。这一点可以验证:

cat .git/refs/heads/a

所有的本地分支都存储在目录下,每一个分支对应一个文件,文件的内容如图所示:可见,刚好是本文第一张图中分支 a 所指向的提交。我们已经搞明白了 git 分支的秘密,现在有了所有分支的记录,又有了每次提交的父提交对象,就能够得出像 SourceTree 或者文章开头第一张图那样的提交状态了。至于标签,它其实也是一种引用,可以理解为不能移动的分支。只能永远指向某个固定的提交。最后一个比较特殊的引用是 HEAD,它可以理解为指针的指针,为了证明这一点,我们看看 文件:它的内容记录了当前指向哪个分支,其实是一个文件,这个文件的内容是分支 b 指向的那个提交对象。理解这一点非常重要,否则你会无法理解和的区别。这两个命令都会改变 HEAD 的指向,区别是 不改变 HEAD 指向的分支的指向,而 会。举个例子, 在分支 b 上执行以下两个命令都会让 HEAD 指向 这次提交(分支 a 指向的提交):

git checkout a

git reset --hard a

但 仅改变 HEAD 的指向,不会改变分支 b 的指向。而 不仅会改变 HEAD 的指向,还因为 HEAD 指向分支 ,就把 b 也指向 这次提交。git 日志

在 目录中,有一个文件夹和一个 HEAD 文件,每当 HEAD 引用改变了指向的位置,就会在 中添加了一个记录。而 这个目录中则有多个文件,每个文件对应一个分支,记录了这个分支 的指向位置发生改变的情况。当我们执行 的时候,其实就是读取了 这个文件。撤销 rebase 的原理

首先我们要排除一个误区,那就是 git 会维护每次提交的提交对象、树对象和数据对象,但并不会维护每次提交时,各个分支的指向。在介绍分支的那一节中我们已经看到,分支仅仅是一个保留了提交对象的文件而已,并不记录历史信息。即使在上一节中,我们知道分支的变化信息会被记录下来,但也不会和某个提交对象绑定。也就是说,git 中并不存在某次提交时的分支快照那么我们是如何通过 reset 来撤销 rebase 的呢,这里还要澄清另一个事实。前文曾经说过,某个时刻下你通过 SourceTree 或者 看到的分支状态,其实是由所有分支的列表、每个分支所指向的提交,和每个提交的父提交共同绘制出来的。首先 下的文件告诉我们有多少分支,每个文件的内容告诉我们这个分支指向那个提交,有了这个提交不断向前追溯就绘制出了这个分支的提交历史。所有分子的提交历史也就组成了我们看到的状态。但我们要明确:不是所有提交对象都能看到的,举个例子如果我们把某个分支向前移一次提交,那个分支的提交线就会少一个节点,如果没有别的提交线包含这个节点,这个节点就看不到了。所以在 rebase 完成后,我们以为看到了下面这样的提交线:

实际上是这样的:

master 分支上依然有分叉,原来 这次提交依然存在,只不过没有分支的提交线包含它,所以无法看到而已。但是通过 ,我们可以找回 HEAD 头的每一次移动,所以能看到这次提交。当我们执行这个命令时:

git reset --hard HEAD@{3}

再看一次 的输出:其实是它左侧这次提交的缩写,所以上述命令等价于:

git reset --hard 9cbb015

前文说过, 不仅会移动 HEAD,还会移动 HEAD 所指向的分支,所以这个命令的执行结果就是让 HEAD 和分支 b 同时指向 这个提交,看起来像是撤销了 rebase。但别忘了,分支 a 的上面还是有一次提交的,9d0618e 这次提交仅仅是没有分支指向它,所以不显示而已。但它真实的存在着,严格意义上来说,我们并没有真正的撤销此次 rebase。广告我是张星宇,喜欢探索问题的本质,讨厌一切不说人话的描述。正在学习前端,励志成为一名全栈工程师。博客、Github、微博、简书、掘金等平台的 ID 都是bestswifter,如果觉得本文有帮助,欢迎加入我的知识星球并持续与我互动,我会不定期分享自己的收获。

  • 发表于:
  • 原文链接http://kuaibao.qq.com/s/20180116G0ZHSC00?refer=cp_1026
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券