管理提交历史:Rebase, Reset, Revert, Cherry-pick, Reflog
Git 提供了强大的工具来修改和管理提交历史。理解这些工具的原理和适用场景对于维护清晰、整洁的项目历史至关重要。 ⚠️ 警告: 其中一些命令(特别是git rebase 和 git reset)会重写提交历史。在对已推送到共享仓库的分支使用这些命令时,必须极其谨慎,否则会给协作者带来严重问题。请务必理解其原理和潜在风险。
变基 (Rebase): 线性化提交历史
git rebase 是整合来自不同分支的更改的两种主要方法之一(另一种是 git merge)。
merge: 将两个分支的历史连接在一起,并创建一个新的合并提交 (Merge Commit)。如果分支有分叉,历史记录会呈现非线性。rebase: 获取当前分支(例如feature)上独有的提交,将它们暂存,然后将当前分支指向目标基底分支(例如main)的最新提交,最后逐一重新应用之前暂存的提交。结果是feature分支看起来像是直接从main的最新状态开发出来的,历史记录保持线性。
rebase 与 merge 的区别:rebase 创建线性历史,而 merge 保留完整历史记录和分支结构
核心场景:
- 保持特性分支与主线同步: 在开发特性分支时,主线 (
main) 可能已经更新。在合并回main之前,先将特性分支变基到main的最新提交上:这样做可以使后续合并回main时通常能进行快进合并 (Fast-forward),保持主线历史整洁。 - 清理本地提交历史: 在将本地的一系列零散提交(“fix typo”, “wip” 等)推送到共享仓库前,使用交互式变基 (
git rebase -i) 将它们合并 (squash/fixup)、编辑 (reword/edit) 或重新排序 (reorder),形成更清晰、更有意义的提交记录。
feature 变基到 main):
初始状态:
A, B, C 的内容被重新应用,生成了新的提交 A', B', C',它们的提交哈希值 (SHA-1) 会改变。
Rebase 黄金法则: 永远不要对已经推送到公共/共享仓库并且可能被他人使用的分支执行实用技巧:rebase操作! 因为rebase通过创建新提交来重写历史。如果其他人已经基于旧的提交进行了开发,你的变基会使他们的本地仓库与远程仓库产生严重分歧,合并时会非常混乱。变基主要用于清理你自己的、尚未分享的本地提交历史。
git pull --rebase
这相当于 git fetch 后跟 git rebase。它会先获取远程更新,然后将你本地尚未推送的提交变基到更新后的远程分支之上,避免了 git pull (默认是 merge) 可能产生的合并提交。
交互式变基 (git rebase -i): 精雕细琢提交历史
交互式变基是 rebase 的一个强大模式,允许你在变基过程中对一系列提交进行精细控制。
操作: git rebase -i <base-commit>
<base-commit> 是你想要修改的提交范围的父提交。例如,修改最近 3 个提交:git rebase -i HEAD~3。
执行后,Git 会打开一个编辑器,列出从 <base-commit> 之后到当前 HEAD 的所有提交,每行一个,格式类似:
pick 改为 squash, reword, edit, drop 等),或者调整行的顺序来重新排序提交。保存并关闭编辑器后,Git 会按照你的指示执行操作。
常用命令:
pick(p): 保留该提交不变。reword(r): 保留该提交,但暂停让你修改提交信息。edit(e): 保留该提交,但暂停让你修改提交内容(例如添加文件、修改代码),修改后git commit --amend,然后git rebase --continue。squash(s): 将该提交合并到前一个提交中。Git 会暂停让你编辑合并后的提交信息(默认包含两个提交的信息)。fixup(f): 类似squash,但丢弃该提交的提交信息,直接使用前一个提交的信息。常用于合并小的修复提交。drop(d): 完全移除该提交。- 重新排序: 直接在编辑器中调整行的顺序。
重置 (Reset): 回溯提交历史 (需谨慎!)
git reset 是一个强大的命令,用于将当前分支的 HEAD 指针移动到指定的历史提交。根据不同的模式,它还会影响暂存区 (Index) 和工作目录 (Working Directory)。
⚠️ 警告: git reset 会修改当前分支的历史。绝对不要对已推送到公共仓库的分支使用 reset 来移除提交,这会破坏协作者的仓库历史。它主要用于修改本地的历史记录。
三种主要模式:
-
git reset --soft <commit>:- 移动
HEAD指针到<commit>。 - 暂存区 和 工作目录 保持不变。
- 效果:
<commit>之后的所有提交内容都变成已暂存 (staged) 状态。 - 场景: 合并本地多个零散提交。例如,
git reset --soft HEAD~3将最近 3 个提交的内容放入暂存区,然后git commit -m "合并成一个有意义的提交"。
- 移动
-
git reset --mixed <commit>(默认模式):- 移动
HEAD指针到<commit>。 - 重置暂存区以匹配
<commit>的状态。 - 工作目录 保持不变。
- 效果:
<commit>之后的所有提交内容都变成未暂存 (unstaged) 的更改。 - 场景: 撤销错误的
git add。例如,git add .后发现加错了文件,git reset HEAD <file>将文件移出暂存区;git reset(不带 commit) 撤销所有暂存。撤销最近一次提交并将更改放回工作目录:git reset HEAD~1。
- 移动
-
git reset --hard <commit>:- 移动
HEAD指针到<commit>。 - 重置暂存区以匹配
<commit>。 - 重置工作目录以匹配
<commit>。 - 效果:彻底丢弃
<commit>之后的所有提交,以及工作目录和暂存区中所有未提交的更改。 - ⚠️ 极度危险! 丢失的更改很难恢复(除非用
reflog)。 - 场景: 彻底放弃某个提交之后的所有工作和本地修改,回到一个已知的干净状态。例如,
git reset --hard origin/main使本地分支与远程完全一致(会丢失本地未推送的提交和未提交的更改)。
- 移动
Git reset 的三种模式:—soft 只移动 HEAD,—mixed 重置暂存区,—hard 同时重置工作目录
撤销 (Revert): 安全地“撤销”提交
与reset 修改历史不同,git revert <commit> 通过创建一个新的提交来撤销指定 <commit> 所引入的更改。原始的错误提交仍然保留在历史中,但其效果被新提交抵消了。
关键点:
revert不会重写历史,而是追加历史。- 它是安全的操作,可以用于撤销已推送到共享分支上的错误提交。
main 分支上的某个已发布的提交 (a1b2c3d) 引入了一个 Bug。
reset vs revert 总结:
- 修改本地私有历史: 使用
reset(通常--soft或--mixed) 或rebase -i。 - 撤销公共共享历史: 使用
revert。
拣选 (Cherry-pick): 精准复制代码提交
git cherry-pick <commit-hash> 允许你选择一个或多个来自其他分支的提交,并将这些提交引入的代码变更应用到你当前所在的分支。它会创建一个新的提交,包含被拣选提交的变更内容。
cherry-pick 操作允许你选择指定的提交,并将其应用到当前分支
核心场景:
- 紧急修复 (Hotfix): 在
develop分支修复了一个线上 Bug (提交 B),需要立即将这个修复应用到main分支,但不希望引入develop上的其他未完成特性。 - 功能回传 (Backporting): 将某个在新版本分支 (
v2.0) 中开发的小功能或优化 (提交 C),选择性地应用到旧的、仍在维护的版本分支 (v1.1) 上。
cherry-pick复制的是变更内容,不是提交本身,因此会在当前分支创建新的提交,具有不同的哈希值。- 如果被拣选的提交依赖于其父提交的更改,直接拣选可能会失败或引入错误。
- 过度使用
cherry-pick可能意味着分支策略混乱。优先考虑merge或rebase。
引用日志 (Reflog): Git 的时光机与安全网
Git 在本地记录了你的HEAD 和分支指针在过去一段时间内的移动轨迹。git reflog 命令可以查看这份日志,即使某些提交因为 reset 或 rebase 而从正常的分支历史中“消失”了。
核心场景 (本地恢复):
- 恢复误删的分支: 不小心
git branch -D my-feature。 - 撤销灾难性的
reset --hard:git reset --hard HEAD~3后发现删多了。
reflog 是你本地仓库的最后一道防线,用于从本地操作失误中恢复。它记录的是本地指针的移动,默认只保留一段时间(通常 90 天)。
Git reflog 记录了 HEAD 和分支指针的移动历史,可以用来恢复意外删除的提交