<audio id="audio" title="06 | 代码回滚，你真的理解吗？" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/92/bc/926b17ecd2e841a914c26b96a90910bc.mp3"></audio>

## 什么是代码回滚？

在我正式开始今天的分享前，先给你讲两个核心概念：

<li>
包回滚是指，线上运行的系统，从现在的版本回滚到以前稳定的老版本。
</li>
<li>
代码回滚是指，Git分支的指针（游标），从指向当前有问题的版本改为指向一个该分支历史树上没问题的版本，而这个版本可以是曾经的commit，也可以是新建的commit。
</li>

## 你是不是也遇到了问题？

在日常的代码管理中，困扰开发工程师最多，也是他们向我咨询得最多的问题就是：代码回滚的问题。这些问题，有的只是影响个人开发，而有的涉及了整个团队。我把这些问题进行了整理汇总，你可以看看是否也遇到过类似的问题？

<li>
<p>今天上午我在自己的开发环境上拉了一条新分支，提交了5个commit，最新提交的3个commit 我不想要了，那我该怎么退回到这3个commit之前的那个commit？<br />
答：参考我在下面即将分享的“个人分支回滚”的内容。</p>
</li>
<li>
<p>我本地的分支通过 reset --hard 的方式做了代码回滚，想通过push的方式让远端的分支也一起回滚，执行push命令时却报错，该怎么办？<br />
答：如果不加 -f 参数，执行 reset --hard 后，push会被拒绝，因为你当前分支的最新提交落后于其对应的远程分支。push 时加上 -f 参数代表强制覆盖。</p>
</li>
<li>
<p>线上产品包已经回滚到昨天的版本了，我清清楚楚地记得昨天我把发布分支上的代码也reset --hard到对应的commit了，怎么那几个有问题的commit今天又带到发布分支上了？真是要命！<br />
答：集成分支不能用 reset --hard 做回滚，应该采用集成分支上新增commit的方式达到回滚的目的。</p>
</li>
<li>
<p>我刚刚在GitLab上接纳了一个合并请求（Merge Request），变更已经合入到master上了，但现在我发现这个合并出来的commit有较大的质量问题，我必须把master回滚到合并之前，我该怎么办？<br />
答：可以在GitLab上找到那<!-- [[[read_end]]] -->个合并请求，点击revert按钮。</p>
</li>
<li>
<p>刚刚线上A产品V6.2的包有问题，我已经把A的产品包回退到V6.1 版本了，请问发布分支上的代码也要回滚到V6.1对应的commit吗？<br />
答：你可以在下文“哪些情况下需要回滚代码？”和“哪些情况下包的回滚无需回滚代码？”中找到答案。</p>
</li>
<li>
<p>产品包的回滚可以在我们公司持续交付云平台上执行，平台能不能也提供代码一键回滚的功能？这样我们回滚代码能相对轻松一些。<br />
答：针对已上线发布的版本，我认为持续交付平台提供一键回滚的方式还是有必要的。这么做可以规范集成分支上线后代码回滚的行为，也能减少人为失误。具体做法可以参考我在下面给你分享的“集成分支上线后回滚”的内容。</p>
</li>

上面这六个问题，除了前两个问题外，剩下的四个问题都可能影响到整个团队，因此回滚代码时须站在团队的立场，采用合适的方式进行回滚。

接下来，我就一一为你解答这些问题。

## 哪些情况下需要回滚代码？

在代码集成前和集成后，都有可能需要回滚代码。

**第一种情况：开发人员独立使用的分支上，如果最近产生的commit都没有价值，应该废弃掉，此时就需要把代码回滚到以前的版本。** 如图1所示。

<img src="https://static001.geekbang.org/resource/image/2c/5c/2c8109c6e46be8137cb4bf7fdaacce5c.png" alt="" />

**第二种情况：代码集成到团队的集成分支且尚未发布，但在后续测试中发现这部分代码有问题，且一时半会儿解决不掉，为了不把问题传递给下次的集成，此时就需要把有问题的代码从集成分支中回滚掉。** 如图2所示。

<img src="https://static001.geekbang.org/resource/image/d9/33/d9947d8f6f7f746414a18b81be5f3733.png" alt="" />

**第三种情况：代码已经发布到线上，线上包回滚后发现是新上线的代码引起的问题，且需要一段时间修复，此时又有其他功能需要上线，那么主干分支必须把代码回滚到产品包V0529对应的commit。** 如图3所示。

<img src="https://static001.geekbang.org/resource/image/1d/e5/1db632b6217ed01a9ebb7c640ae09ee5.png" alt="" />

## 哪些情况下包的回滚无需回滚代码？

<li>
线上回滚后，查出并不是因为源代码有问题。
</li>
<li>
下次线上发布，就是用来修复刚才线上运行的问题。
</li>

## 代码回滚必须遵循的原则

集成分支上的代码回滚坚决不用 reset --hard 的方式，原因如下：

<li>
集成分支上的 commit 都是项目阶段性的成果，即使最近的发布不需要某些 commit 的功能，但仍然需要保留这些 commit ，以备后续之需。
</li>
<li>
开发人员会基于集成分支上的commit 拉取新分支，如果集成分支采用 reset 的方式清除了该 commit ，下次开发人员把新分支合并回集成分支时，又会把被清除的 commit 申请合入，很可能导致不需要的功能再次被引入到集成分支。
</li>

## 三种典型回滚场景及回滚策略

在上面的内容中，我给你提到了个人分支回滚、集成分支上线前的回滚，以及集成分支上线后的回滚，这三种需要代码回滚的场景，它们具有一定的代表性。

现在，我就先以表1 的形式，针对不同场景为你归纳不同的处理策略。后面的章节中，我再为你具体介绍每种场景的处理步骤。

<img src="https://static001.geekbang.org/resource/image/26/72/26f06b78c368b7717d06ab03f3bb4872.png" alt="" />

**第一，个人分支回滚**

<img src="https://static001.geekbang.org/resource/image/2c/5c/2c8109c6e46be8137cb4bf7fdaacce5c.png" alt="" />

针对图1的情况：

<li>
feature-x分支回滚前HEAD指针指向 C6 。
</li>
<li>
在个人工作机上，执行下面的命令：
</li>

```
$ git checkout feature-x   
$ git reset --hard  C3的HASH值

```

如果feature-x已经push到远端代码平台了，则远端分支也需要回滚：

```
$ git push -f origin  feature-x

```

**第二，集成分支上线前回滚**

<img src="https://static001.geekbang.org/resource/image/d9/33/d9947d8f6f7f746414a18b81be5f3733.png" alt="" />

针对图2中集成分支上线前的情况说明：

<li>
假定走特性分支开发模式，上面的commit都是特性分支通过merge request合入 master 产生的commit。
</li>
<li>
集成后，测试环境中发现C4和C6的功能有问题，不能上线，需马上回滚代码，以便 C5 的功能上线。
</li>
<li>
团队成员可以在 GitLab 上找到C4和C6合入master的合并请求，然后点击 revert 。如图4所示。
</li>

<img src="https://static001.geekbang.org/resource/image/77/a1/7734d20cb2ddf59f2237584f7c382fa1.png" alt="" />

回滚后master分支变成如图5所示，C4’是revert C4产生的commit，C6’是revert C6产生的commit。通过revert操作，C4 和 C6 变更的内容在 master 分支上就被清除掉了，而 C5 变更的内容还保留在 master 分支上。

<img src="https://static001.geekbang.org/resource/image/00/f0/00a1d5aa42c53d4d355e297ca5f221f0.png" alt="" />

**第三，集成分支上线后回滚**

<img src="https://static001.geekbang.org/resource/image/1d/e5/1db632b6217ed01a9ebb7c640ae09ee5.png" alt="" />

我先跟你说明一下图3中的具体情况：

<li>
C3打包并上线，生成线上的版本V0529，运行正确。之后C6也打包并上线，生成线上版本V0530，运行一段时间后发现有问题。C4和C5并没有单独打包上线，所以没有对应的线上版本。
</li>
<li>
项目组把产品包从V0530回滚到V0529，经过定位，V0530的代码有问题，但短时间不能修复，于是，项目组决定回滚代码。
</li>
<li>
C4和C5没有单独上过线，因此从线上包的角度看，不能回滚到C4或C5，应该回滚到C3。
</li>
<li>
考虑到线上包可以回滚到曾发布过的任意一个正确的版本。为了适应线上包的这个特点，线上包回滚触发的代码回滚我们决定不用 一个个revert C4、C5和C6的方式，而是直接创建一个新的commit，它的内容等于 C3 的内容。
</li>
<li>
具体回滚步骤：
</li>

```
$ git fetch origin  
$ git checkout master
$ git reset --hard  V0529         # 把本地的master 分支的指针回退到 V0529，此时暂存区(index)里就指向 V0529里的内容了。
$ git reset --soft  origin/master  # --soft使得本地的master 分支的指针重新回到 V05javascript:;30，而暂存区(index)变成 V0529的内容。
$ git commit -m "rollback to V0529"  # 把暂存区里的内容提交，这样一来新生成的commit的内容和 V0529 相同。 
$ git push origin  master        # 远端的master也被回滚。

```

回滚后如图6所示。

<img src="https://static001.geekbang.org/resource/image/1c/46/1c05958045f89f8dae034fd2f5461f46.png" alt="" />

C3’的内容等于 C3，master分支已清除C4、C5和C6的变更。

现在master又回到了正确的状态，其他功能可以继续上线。

如果要修复C4、C5和C6的问题，可以在开发分支上先revert 掉C3’ ，这样被清除的几个commit的内容又恢复了。

## 总结

代码回滚在持续交付中与包回滚一样，也是不可缺少的一项活动。但它并不是简单地去执行Git的reset或revert命令就可以搞定的事情。

除了开发的个人分支上存在回滚的情况外，我们还会遇到集成分支上需要回滚的情况；对于集成分支的回滚，又可以分为上线前和上线后两种情况；因为紧急程度和上线情况的不同，我们必须采用不同的回滚策略。

我围绕着开发工程师在代码管理中，最常遇到的6个问题，分别为你介绍了代码回滚的概念，梳理了需要回滚及不需要回滚的情况，分析了回滚的类别及其不同的回滚策略，提炼了回滚原则，希望能对你的实际工作有所帮助，保持正确的回滚姿势。

## 思考题

那么，接下来就是留给你的思考题了。

<li>
集成分支上线前，如果发现新提交的5个commit有3个需要回滚，请问，除了点击合并请求中的revert按钮这种方法外，还可以怎么做？
</li>
<li>
采用特性分支开发的一个项目，每个特性分支合入到 master 时都会产生一个合并的commit，而且该项目是禁止直接向master做push操作的。可是该项目的master分支却存在多个非合并产生的commit，请问这些commit很可能是怎么产生的？
</li>
<li>
持续交付平台如果要提供一键代码回滚的功能，每次回滚都要生成一个新的commit吗？即使以前已经产生过同内容的commit了，也要重建新的commit么？
</li>

欢迎你给我留言。


