Skip to content

关于 Revert 的那些“坑”

起因很简单,我需要把最近的几个 commit 给撤销掉,但又不能用 reset --hard 这种暴力手段,因为分支已经推到远程了。我想当然地以为 git revert 应该很轻松,结果...呵呵。

这篇笔记记录了我从抓狂到“原来如此”的全过程。

踩坑一:我以为 Revert 是删除,结果它是在“添乱”?

一开始,我的目标是回退到 ed44ae7 这个 commit:

8f5ff83 (HEAD -> main) stage
9b23011 stage
ed44ae7 Overhaul for production deployment

我下意识地想把 8f5ff839b23011 这俩哥们从历史上抹掉。差点就 reset --hard 了。

还好理智战胜了冲动。我记起来 revert 才是团队协作的安全选择。但它的工作方式和我想的完全不一样。

顿悟 #1:git revert 不是删除,而是用一个新的 commit 来“抵消”一个旧的 commit。 你的历史不会减少,反而会增加。这保证了历史的完整性,别人拉代码时不会被搞得一团糟。

踩坑二:说好的一起回滚,你怎么只滚了一个就停了?

知道了原理,我开始操作。我想回滚 8f5ff839b23011,于是我找到了它们的父提交 ed44ae7,然后敲下了自以为很帅的命令:

bash
# 意思是从 ed44ae7 之后,到 8f5ff83 为止,全给我 revert 了!
git revert ed44ae7..8f5ff83

然后,诡异的事情发生了。它弹了个编辑器让我确认第一次 revert 的信息,然后...就没然后了。git log 一看,只多了一个 Revert "stage" 的提交。

我当时就???剩下那个呢?被你吃了?

顿悟 #2:git revert <范围> 不是原子操作! 它是一个循环,会从最新的 commit 开始一个一个地 revert。如果在 revert 第一个 commit 时就遇到了冲突,整个过程就会立即暂停,等你来擦屁股。

我的救星 git status 证实了这一点,它告诉我正处于冲突状态。

解决方案

  1. 手动解决冲突文件里的 <<<<<<<>>>>>>>
  2. git add . 告诉 Git 我搞定了。
  3. git revert --continue 让它继续去处理下一个。
  4. 如果不想干了,git revert --abort 可以随时跑路。

踩坑三:终极 BOSS - cannot lock ref 'HEAD'

正当我以为掌握了一切,准备再次大展拳脚时,一个更诡异的错误出现了:

hint: Waiting for your editor to close the file...
fatal: cannot lock ref 'HEAD': is at b548b33... but expected 8f5ff83...

这是什么鬼?我明明什么都没干,就是等编辑器关掉而已,HEAD 怎么自己“叛变”了?

我查了半天,差点以为是 Git 坏了。

顿悟 #3:元凶是后台的“小动作”,通常来自 IDE! 在我用命令行执行 revert,Git 暂停并等待我关闭编辑器时,我的 VS Code 也没闲着。它自带的 Git 插件在后台默默地执行 git fetch 之类的操作来刷新状态。这个后台操作和我的前台操作发生了竞态条件,把 HEAD 的引用文件搞乱了。

终极解决方案(我的新圣经): 既然问题出在“等待编辑器”这个环节,那我就不给你等待的机会!

  1. 先用 git revert --abort 清理战场。

  2. --no-commit (或 -n) 参数!

    bash
    git revert -n ed44ae7..8f5ff83

    这个 -n 参数是真正的宝藏!它的作用是:计算出所有 revert 需要的更改,把它们放到暂存区,但是不自动提交!

  3. 自己手动提交。 因为没有自动提交,也就没有打开编辑器、没有暂停等待。后台的小动作根本没机会捣乱。

    bash
    # 看一眼状态,所有更改都已经在暂存区了
    git status
    
    # 然后,帅气地一次性提交!
    git commit -m "Revert: 回滚了两个 stage 提交,修复了某某问题"

我的最终笔记 & 新的工作流

以后再也不一个一个 revert 然后处理冲突了。我的新流程是:

  1. 想好要 revert 的范围 <start>..<end>
  2. 直接运行 git revert -n <start>..<end>
  3. 处理可能出现的冲突(如果有的话,一次性处理完)。
  4. git add .
  5. git commit -m "一个清晰的理由"
  6. git push 收工。

这个流程不仅避免了所有诡异的错误,还把一堆 revert 操作合并成了一个干净的 commit。完美。


管理私有开发库和公开发布库

最近有个需求,需要将一个内部开发的项目开源,但又不想把所有凌乱的、包含内部信息的 commit 历史都公开出去。解决方案就是用两个远程仓库

核心思路:本地仓库同时连接两个远程库,一个私有(origin),一个公开(public)。向 origin 推送完整的开发历史,向 public 只推送“净化”过的历史。

最推荐的策略:合并提交 (Squash) 这个方法最简单安全。在私有库里随意提交,发布时,将一个版本的所有开发 commit 合并成一个干净的 commit,再推到公开库。

操作流程:

  1. 准备工作:在本地仓库添加公开库为远程。

    bash
    # public 是我们给公开库起的名字
    git remote add public git@github.com:your-username/my-project-public.git
  2. 合并历史:这是将一个分支上所有 commit 合并成一个的终极大法!

    bash
    # 假设你在 main 分支
    # --root 表示从第一个 commit 开始 rebase
    git rebase -i --root

    在打开的编辑器里,保留第一行的 pick,把其他所有行的 pick 都改成 s (squash 的缩写)。保存后,再为这个“巨型”commit 写一个新的、清晰的 commit message。

  3. 推送到公开库

    bash
    # 将当前分支强制推送到 public 远程的 main 分支
    # --force 是必须的,因为公开库的历史被我们完全重写了
    git push public HEAD:main --force

警告:这个操作尤其适合项目首次开源。如果公开库已经有别人在用,强制推送前一定要和所有人沟通好!

版本管理:如何表示开发中的版本

除了 v1.0 这样的正式版,我们怎么表示一个正在开发中 (WIP) 的版本呢?

  • 功能分支 (Feature Branches)最推荐的方式。分支名本身就说明了一切。

    • feature/user-login:开发新功能。
    • bugfix/login-error:修复 bug。
    • wip/refactor-database:明确告知这是个未完成的大改造。
  • 开发分支 (develop):对于有明确发布周期的项目,可以维护一个 develop 分支作为所有功能合并的主开发线,它永远代表“下一个版本”的开发状态。

  • 预发布版本号 (Pre-release Tags):对于库和包来说,这是最专业的做法。

    • 1.2.0-alpha.1 (内部测试版)
    • 1.2.0-beta.2 (公开测试版)
    • 1.2.0-rc.1 (发布候选版)

核心命令辨析:reset, checkout 的爱恨情仇

git resetgit checkout 功能强大,但也容易混淆。

git reset:回滚到最后一次提交

如果你想撤销最后一次提交,reset 是你的好朋友,但它有三种不同的“脾气”:

  1. git reset --soft HEAD~1最温柔。撤销 commit,但所有代码改动都保留在暂存区(像已经 git add 了一样)。最适合修改 commit message 或把更多改动加进去。

  2. git reset HEAD~1 (默认模式是 --mixed)有点脾气。撤销 commit,代码改动保留在工作区,但需要重新 git add。适合“我再想想这些代码要不要”的场景。

  3. git reset --hard HEAD~1极度暴力。撤销 commit,代码改动全部永久删除。工作区和暂存区都变得干干净净。

    用前请三思,数据无价!

git checkout 的“身份危机”

checkout 曾经是个“瑞士军刀”,既能切换分支,又能恢复文件,容易让人迷惑。

  • 切换分支git checkout develop
  • 创建并切换git checkout -b feature/new-idea
  • 恢复文件git checkout -- config.yml (丢弃对 config.yml 的修改)

为了让命令更清晰,Git 2.23 版本后引入了两个新命令:

  • git switch <分支>:专门用来切换分支。
  • git restore <文件>:专门用来恢复文件。

建议:新项目或新习惯可以拥抱 switchrestore,它们的意图更明确。但 checkout 无处不在,理解它的全部功能仍然是必备技能。