skip to content
月与羽

Git 重写历史

/ 9 min read

黄金法则:不要重写公共历史!

在开始之前,必须强调最重要的原则:永远不要重写已经推送到公共/共享分支(如 mainmasterdevelop)的历史

  • 原因:重写历史会改变 Commit 的 ID (SHA-1)。如果你的团队成员已经基于旧的历史进行了开发,你的强制推送 (git push --force) 会使他们的本地仓库与远程仓库产生严重分歧,导致协作混乱和代码丢失。
  • 例外:只重写你个人私有分支尚未推送到远程的本地提交。

核心工具与常见场景

以下是重写历史最常用的几个命令,按使用场景和复杂度排序。

1. 修改最后一次提交 (git commit --amend)

这是最简单、最常用的历史重写场景。

  • 用途

    • 修改最后一次提交的说明 (Commit Message)。
    • 向最后一次提交中添加被遗漏的文件。
  • 如何操作

    1. 仅修改 Message

      Terminal window
      git commit --amend

      这会打开一个编辑器,让你重新编辑提交信息。

    2. 添加遗漏的文件

      Terminal window
      # 假设你忘记添加 file3.txt
      git add file3.txt
      git commit --amend --no-edit # --no-edit 表示不修改 Message,直接把新文件加进去
  • 原理:它并不会真的“修改”上一个提交,而是创建一个包含新内容/新消息的全新提交,并用它来替换掉旧的提交。

2. 交互式变基 (git rebase -igit rebase --interactive)

这是最强大、最灵活的历史重写工具,可以处理一系列的提交。

  • 用途

    • 合并(压缩)多个提交为一个。
    • 修改某个历史提交的消息。
    • 删除某个提交。
    • 重新排序提交。
    • 拆分一个大提交为多个小提交。
  • 如何操作

    1. 启动交互式 Rebase。你需要指定一个“基底”提交,表示你要重写从这个基底之后的所有提交。

      Terminal window
      # 重写最近的 3 个提交
      git rebase -i HEAD~3
      # 重写自某个特定 commit_id 以来的所有提交(不包括该 commit_id)
      git rebase -i <commit_id>
    2. Git 会打开一个编辑器,列出你选定范围内的所有提交,类似这样:

      pick a3322e1 feat: add user login
      pick 9a1b4d3 fix: correct typo
      pick 2c5e6f7 wip
    3. 你需要修改每行前面的命令 (pick) 来告诉 Git 如何操作。常用命令有:

      • p, pick: 保留该提交(默认)。
      • r, reword: 保留该提交,但修改提交消息。
      • e, edit: 保留该提交,但 Rebase 会在该提交处暂停,让你进行修改(比如添加/删除文件、拆分提交等)。
      • s, squash: 将该提交合并到前一个提交中,并合并它们的提交消息。
      • f, fixup: 与 squash 类似,但会丢弃该提交的消息。
      • d, drop: 直接删除该提交。
    4. 保存并关闭编辑器,Git 会按照你的指令一步步执行。

  • 常见示例

    • 合并后两个提交 (fixwip) 到第一个提交中

      pick a3322e1 feat: add user login
      s 9a1b4d3 fix: correct typo
      f 2c5e6f7 wip

      保存后,Git 会让你重新编辑合并后的提交消息。

    • 修改中间提交的消息

      pick a3322e1 feat: add user login
      r 9a1b4d3 fix: correct typo
      pick 2c5e6f7 wip

      保存后,Git 会让你先重新编辑 9a1b4d3 的消息,然后继续。

3. 从整个历史中移除文件 (git filter-repo)

当你不小心把一个大文件、密码或私钥提交到了仓库历史中,即使在后续的提交中删除了它,它依然存在于 Git 的历史记录里。这时需要重写整个仓库的历史。

  • 传统工具git filter-branch。功能强大但语法复杂,运行缓慢,且容易出错。官方已不推荐使用

  • 现代推荐工具git-filter-repo。它是一个第三方脚本,速度极快,用法更简单、更安全。

  • 如何操作 (git-filter-repo)

    1. 安装 git-filter-repo

    2. 执行命令从所有历史中删除指定文件或文件夹。

      Terminal window
      # 从历史中删除一个文件
      git filter-repo --path path/to/your/secret.txt --invert-paths
      # 从历史中删除一个文件夹
      git filter-repo --path path/to/your/large-folder/ --invert-paths
  • 警告:这是一个非常彻底的重写操作,会改变仓库中的所有 Commit ID。执行前务必备份仓库。

4. 彻底丢弃某些提交 (git reset --hard)

git reset 也可以用来重写历史,但它的逻辑是移动分支指针,从而“丢弃”指针移动路径上的提交。

  • 用途

    • 在本地彻底放弃最近的几次提交,回到某个历史状态。
  • 如何操作

    Terminal window
    # 假设你的提交历史是 C -> B -> A (HEAD)
    # 你想彻底丢弃 A 和 B,回到 C 的状态
    git reset --hard <commit_id_of_C>
    # 或者,丢弃最近的 2 个提交
    git reset --hard HEAD~2
  • 危险性--hard 标志会同时重置工作区和暂存区,所有未提交的本地修改都会丢失。它丢弃的提交如果没有被其他分支引用,最终会被 Git 垃圾回收机制清理掉。


重写历史后的关键一步:推送

由于本地历史已经被重写,它与远程分支的历史已经“分叉”。你不能使用 git push,必须强制推送。

  • 不安全的强制推送 (git push --force)

    Terminal window
    git push origin your-branch --force

    它会粗暴地用你的本地分支覆盖远程分支。如果在你上次 pull 之后,有其他人向远程分支推送了新的提交,--force 会将他们的提交也一并抹去!

  • 更安全的强制推送 (git push --force-with-lease)

    Terminal window
    git push origin your-branch --force-with-lease

    这是强烈推荐的方式。它在推送前会检查:如果远程分支的历史和你本地记录的远程分支历史不一样(意味着有新人推送了代码),推送就会失败。这给了你一个机会先 pull(或者 rebase)最新的代码,解决冲突后再强制推送,避免覆盖他人的工作。

总结

操作场景推荐命令危险级别描述
修改最后一次提交git commit --amend简单快捷,用于修正笔误或补充文件。
整理多个提交git rebase -i功能强大,用于合并、修改、重排、删除一系列提交。
彻底移除历史文件git-filter-repo非常高全局操作,用于从所有历史中清除敏感数据或大文件。
放弃最近的提交git reset --hard快速回滚到某个历史版本,会丢弃之后的提交和本地修改。

核心要点

  1. 备份:在进行复杂历史重写(尤其是 rebase -ifilter-repo)前,先备份你的分支或整个仓库。
  2. 私有:只在私有、未分享的分支上重写历史。
  3. 强制推送:重写后需要强制推送,优先使用 --force-with-lease
  4. 清晰:重写历史的目的是为了让历史记录更清晰、更有逻辑性,而不是为了炫技。