Skip to content
概述
本章介绍 Git 中用于整理提交历史、暂存工作现场和回退仓库状态的三类操作:变基(rebase)、储藏(stash)与重置(reset)。同时还会涉及安全撤销方案 git revert 以及用于找回丢失提交的引用日志 reflog。在掌握了分支、合并与远程协同之后,这些命令可以帮助你更灵活地在本地处理提交记录,并在需要时恢复误操作。
变基与合并的工作原理
git merge 会将两个分支的最新提交与它们的共同祖先进行一次三方合并,生成一个新的合并提交。合并提交有两个父提交,历史图不再保持线性。
git rebase 的思路与之完全不同:它会提取当前分支上所有本地独有的提交,将它们暂存起来,把分支指针移动到目标分支的最新提交上,再将暂存的提交逐个重新应用上去。最终结果是历史变成一条直线,就好像这些提交始终是基于目标分支的最新状态开发的。
二者最根本的差别在于对历史的处理方式:
- merge 保留完整的历史分支与合并点,反映真实的并行开发过程。
- rebase 重写提交历史,使提交记录呈线性,消除合并轨迹。
如果团队需要保持清晰的线性历史以便于审查和回溯,则变基更为合适。但需要注意:一旦分支已经推送到远程仓库,变基会改写提交的 SHA-1 值,导致其他协作者的历史出现分叉,因此 rebase 更适合尚未共享的本地分支。
基本用法与冲突处理
将当前分支(例如 feature)变基到 main 分支顶端:
bash
git checkout feature
git rebase mainGit 执行的操作如下:
- 找到
feature与main的共同祖先提交。 - 将
feature上相对于该祖先的所有提交保存为补丁。 - 将
feature指针重置到main的最新提交。 - 逐个应用之前保存的补丁。
如果补丁无法干净地应用,rebase 过程会暂停,并在冲突文件中标注冲突区域。解决冲突的流程与合并冲突类似:
bash
# 手动编辑冲突文件后
git add resolved-file.js
git rebase --continue如果中途想放弃整个变基操作,可以执行:
bash
git rebase --abort这样做会将分支恢复为执行 rebase 之前的状态。
注意:每次解决冲突后执行 git rebase --continue,Git 会继续应用下一个补丁,直到所有提交应用完毕。
交互式变基:压缩、编辑与拆分提交
交互模式的变基允许在应用每个提交前修改它的行为。命令格式为:
bash
git rebase -i HEAD~3该命令会打开一个文本列表,列出最近三个提交,每行以 pick 开头,后面跟着提交的简短哈希和说明。将 pick 修改为其他命令,即可执行对应操作。常用命令包括:
pick(p):使用该提交(默认行为)。reword(r):使用该提交但修改其提交信息。edit(e):使用该提交但在该提交处暂停,允许通过git commit --amend等方式修改内容或拆分为多个提交。squash(s):将该提交合并到前一个提交中,并合并提交信息。fixup(f):类似 squash,但直接丢弃本提交的说明信息。drop(d):完全删除该提交。
压缩提交 是交互式变基最常见的用法。假设要将最近三个提交压缩为一个:
bash
git rebase -i HEAD~3然后在打开的编辑器中把后两个提交的 pick 改为 squash(或 s):
text
pick a1b2c3d feat: add user module
squash e4f5g6h fix: correct validation
squash i7j8k9l chore: update tests保存退出后,Git 会将这些提交压缩到第一个提交里,并让你编辑最终合并后的提交信息。
拆分提交 需要将 pick 改为 edit。Git 会暂停在目标提交之后,此时可以执行 git reset HEAD^ 将提交的内容放回工作区(这会撤销该提交但保留修改),然后重新分多次 git add 和 git commit 形成多个新提交,最后执行 git rebase --continue 继续后续流程。
变基操作会改变所有受影响提交的 SHA-1 和时间戳,任何基于旧提交的本地指针(比如另一个分支或标签)都会失效,后续需要手动调整。
储藏:创建与查看
当正在一个分支上修改文件,突然需要切换到其他分支处理紧急任务,但又不想现在就提交半完成的工作时,可以使用储藏功能。git stash 会把工作目录和暂存区中的修改暂存到一个名为储藏栈的数据结构中,然后将工作区恢复到 HEAD 提交的状态。
创建储藏:
bash
git stash push -m "WIP: refactor auth logic"-m 参数为储藏添加描述信息,方便后续识别。不带 -m 的 git stash 会使用默认描述(基于当前提交信息)。
查看储藏列表:
bash
git stash list输出类似:
text
stash@{0}: On feature/auth: WIP: refactor auth logic
stash@{1}: On main: quick fix for loggingstash@{0} 是最新储藏,索引从零开始递增。
储藏的应用与删除
从储藏栈中取出修改有两种方式:
git stash apply [stash@{n}]:将指定储藏的修改应用到工作目录,但该储藏仍旧保留在栈中。不指定参数时默认应用最近一个储藏(stash@{0})。git stash pop [stash@{n}]:应用指定储藏并将其从栈中删除。不加参数等同于对stash@{0}执行pop。
如果应用储藏时产生冲突,Git 会像合并冲突一样在文件中标记冲突区域。此时即使使用 pop,储藏也不会被自动删除,需要手动解决冲突后执行 git stash drop 来清理。
删除储藏:
bash
git stash drop stash@{2} # 删除指定储藏
git stash clear # 清空整个储藏栈储藏栈是本地的,不会被推送到远程仓库,因此只适合临时性地保存工作进展。
重置的三种模式
git reset 的核心行为是将当前分支的 HEAD 移动到指定的提交,并根据模式参数对暂存区和工作目录施加不同的影响。三种模式的区别如下:
--soft:仅移动HEAD指针,暂存区和工作目录均保持不变。最新提交之后的所有更改仍然保留在暂存区中。--mixed(默认):移动HEAD的同时,将暂存区重置为目标提交的状态,工作目录保持不变。最新提交的更改会从暂存区撤出,但保留在工作目录中。--hard:移动HEAD,同时将暂存区和工作目录都强制重置为目标提交的状态。所有未提交的修改将被不可恢复地丢弃。
通过一个具体示例可以看清三者的差异。假设当前仓库有三个提交:
text
A -- B -- C (HEAD -> main)执行以下三种 reset 并配合 git status 和文件变化来观察效果:
git reset --soft HEAD~1- HEAD 回退到 B 提交。
- 提交 C 引入的更改仍然存在于暂存区(
git status显示有变更已暂存待提交)。 - 工作目录的文件内容与执行命令前一致。
git reset --mixed HEAD~1(等同git reset HEAD~1)- HEAD 回退到 B。
- 暂存区被清空,但提交 C 的更改还在工作目录中,文件显示为“未暂存的修改”。
- 可以继续编辑这些修改,或重新执行
git add。
git reset --hard HEAD~1- HEAD 回退到 B。
- 暂存区和工作目录完全恢复到 B 提交时的状态。
- 提交 C 的更改彻底丢失,
git status显示干净的工作树。但如果之前通过git reflog记录了 C 的哈希,仍有机会找回。
--hard 是破坏性最强的操作,任何未提交的修改都会直接消失。适用场景包括放弃本地实验性修改,或者将仓库恢复到某个已知良好状态。
回退:通过新提交撤销更改
git revert 的设计目标是在不修改历史的前提下取消某个提交引入的改动。它通过分析指定提交的差异,反向应用这些更改并生成一个新提交,原来的提交依然保留在历史中。
例如撤销最近一次提交:
bash
git revert HEADGit 会打开编辑器让你确认新的提交信息,默认说明类似于 “Revert ‘原提交信息’”。保存退出后,仓库中多了一个反向提交,记录文件回到了原提交之前的状态。
revert 与 reset 的关键差异在于历史是否被改写:
- revert 保留原有提交,增加新的“撤销”提交,历史不断向前延伸。适合已经推送到远程仓库的分支。
- reset 移动分支指针,使部分提交从历史中消失。如果这些提交已推送给他人,强制推送会造成合作者的本地历史混乱,一般仅适用于本地未共享的提交。
实际使用中,如果某次提交引入了缺陷,用 revert 撤销更加安全,团队的每个成员只需正常拉取即可获得修复,无需处理分叉的历史。
利用 reflog 找回丢失的提交
Git 将 HEAD 每一次移动的记录都保存在引用日志(reflog)中。即使某个提交因为 reset --hard 或分支删除操作而不再被任何分支引用,只要它存在于 reflog 中,就可以被找回。
查看 reflog:
bash
git reflog输出会列出每次导致 HEAD 移动的操作(如 commit、reset、checkout 等),以及操作前后的提交哈希。例如:
text
a1b2c3d HEAD@{0}: reset: moving to HEAD~1
e4f5g6h HEAD@{1}: commit: feat: add new module假设执行了一次 git reset --hard HEAD~1 导致最新的提交 e4f5g6h 丢失,通过 reflog 找到该哈希后,可以基于它新建一个分支或直接 reset 回去:
bash
git branch recovery-branch e4f5g6h或者使用 git reset --hard e4f5g6h 让 HEAD 回到该提交。reflog 记录默认保留 90 天(不可达对象的清理周期),只要在期限内都可以恢复。
历史改写与协同约束
变基和重置都会改写提交历史(修改 SHA-1、提交顺序或内容)。如果一个分支已经推送到远程仓库,并且有其他协作者基于它进行开发,再对已推送的历史进行 rebase 或 reset 并强制推送,就会导致协作者的本地历史与远程记录产生分叉,他们拉取时将遇到冲突甚至丢失提交。
因此一个基本的协同规则是:已推送到共享仓库的分支,不要进行变基或重置操作。如果必须撤销某个已推送提交,应使用 git revert 生成反向提交并推送。这是唯一不会破坏他人劳动的撤销手段。
当同一分支确实需要清理本地提交历史(比如在特性分支合并前执行 rebase -i 压缩中间提交),应确保该分支尚未推送到公共仓库,或者与其他开发者明确沟通,约定好之后共同执行强制拉取重建本地历史。在大部分日常开发流程中,保持已推送历史的不可变性是降低协作风险的基础。
参考链接
- [1] https://git-scm.com/docs/git-rebase
- [2] https://git-scm.com/book/en/v2/Git-Branching-Rebasing
- [5] https://git-scm.com/book/en/v2/Git-Tools-Rewriting-History
- [6] https://git-scm.com/docs/git-stash
- [8] https://git-scm.com/docs/git-reset
- [9] https://git-scm.com/book/en/v2/Git-Tools-Reset-Demystified
- [10] https://git-scm.com/docs/git-revert
- [12] https://git-scm.com/docs/git-reflog
