12k
All articles

超越 commit 和 push 的 5 个 Git 命令

超越 commit 和 push 的 5 个 Git 命令:stash、reflog、cherry-pick、rebase -i 和 bisect,用于恢复、整理历史和查找 bug。

OpenReplay Team
OpenReplay Team
超越 commit 和 push 的 5 个 Git 命令

大多数开发者的日常离不开 addcommitpushpull,但 Git 真正的威力在于那些日常循环从不触及的恢复、历史编辑和调试命令。

你往往是以一种痛苦的方式认识其余那些命令的。一次 reset --hard 吞掉了一下午的工作,或者某个 bug 藏在 200 个提交中的某处,却没人说得清是哪一个搞坏的。下面这五个命令解决的正是基础命令无法应对的问题:暂存做了一半的工作、恢复你以为已经丢失的提交、在分支之间搬运单个修复、在提 PR 之前清理混乱的历史,以及精确定位引入 bug 的那个提交。每个命令都配有一个记忆触发点、准确的语法,以及那个最容易让人踩坑的注意事项。

核心要点

  • git stash 会把你做了一半的工作暂存起来,并交还一个干净的工作树,让你无需创建临时提交就能切换上下文。
  • git reflog 记录了 HEAD 指向过的每一个位置,因此在一次糟糕的 reset 或 rebase 之后,你可以找到丢失提交的哈希值,并用 git reset --hard <ref> 将其恢复。
  • git cherry-pick <hash> 会把单个提交复制到你当前的分支上,非常适合在不合并其他所有内容的前提下,把一个 hotfix 搬到发布分支。
  • 诸如 git rebase -igit reset --hard 这类重写历史的命令,在本地未推送的工作上是安全的,在共享分支上则很危险;当出错时,git reflog 就是你的恢复途径。
  • git bisect 会对你的历史做二分查找,用 log₂(n) 步找出引入 bug 的确切提交。

git stash:暂存工作,快速切换上下文

使用场景: 当一份 bug 报告到来,而你的工作树还是一团做了一半的乱麻,你需要立刻跳到别的分支。git stash 会把你未提交的改动停放到一个安全的地方,并把一个干净的工作树交还给你,与你最后一次提交保持一致。不需要创建那种一次性的 “WIP” 提交。

git stash                  # shelve tracked changes, clean the tree
git stash --include-untracked   # also shelve new, untracked files
git stash list             # see all stashes: stash@{0}, stash@{1}, ...
git stash pop              # reapply the latest stash and delete it
git stash apply stash@{1}  # reapply a specific stash, keep it in the list

注意事项: popapply 并不能互换。git stash pop 会应用你暂存的改动并删除该 stash;git stash apply 会应用改动但保留 stash,如果重新应用可能产生冲突,这种方式更安全。如果 pop 遇到合并冲突,stash 不会被丢弃。先解决冲突,然后再确认一下,不要想当然地以为它已经消失了。

git reflog:恢复你以为已经丢失的工作

使用场景: 当一次糟糕的 reset --hard、一次搞砸的 rebase,或者一个被删除的分支让提交凭空消失、恐慌开始蔓延时。git reflog 记录了 HEAD 在你本地仓库中占据过的每一个位置,所以那个”丢失”的提交几乎总是还存在。你只需要它的哈希值。

git reflog
# ab12cd3 HEAD@{0}: reset: moving to HEAD~1
# ef45gh6 HEAD@{1}: commit: the work you thought vanished
git reset --hard ef45gh6   # restore HEAD to that commit
# or, to inspect it on a new branch first:
git switch -c recovery ef45gh6

找到你想要的那个状态对应的 ref,然后用 git reset --hard <ref> 跳回去,或者先在一个新分支上检出它,安全地检查一番。这就是工具箱里那个”糟了,我的工作丢了”的救星。

注意事项: reflog 严格属于本地,且每个克隆各自独立。它不会同步到远端,在新克隆的仓库里也不存在,所以重新 clone 来”找回工作”是没用的。此外,条目会随着时间被垃圾回收清理掉,所以要尽早恢复,不要拖延。

git cherry-pick:在分支之间搬运单个提交

使用场景: 当某个 hotfix 提交需要落到发布分支上,但你不想合并它所在的整个特性分支时。git cherry-pick <hash> 会把某一个特定提交复制到你当前的分支上,其余内容一概不带。

git switch release-2.4
git cherry-pick 9f3c1a7        # apply one commit here
git cherry-pick 9f3c1a7 3b8e2d0   # apply several, in order

注意事项: cherry-pick 会创建一个新的提交,带有新的哈希值。原提交既不会被保留也不会被移动。如果同一个提交之后又通过常规合并进入这个分支,你的历史中就可能出现重复的改动。要有意识地使用它,而不要把它当作合并的日常替代品。

git rebase -i:在提 PR 之前清理历史

使用场景: 当你的特性分支里有五个 “fix typo” 和 “wip” 提交,而你希望在发起 pull request 之前拥有一份整洁、便于评审的历史时。git rebase -i <base-branch> 会打开一个编辑器,你可以在其中重新排序、压缩、编辑或丢弃提交。

git rebase -i main
# pick   a1b2c3d Add feature scaffold
# squash e4f5g6h fix typo
# reword h7i8j9k Wire up handler
# drop   k0l1m2n debug logging

把一个提交标记为 squash 会将它合并进上面那个提交,reword 只修改它的提交信息,而 drop 会把它彻底排除在结果之外。如果你只需要把整个分支压缩成一份暂存的改动,git merge --squash <branch> 是它的姊妹工具。它会把合并后的改动放入暂存区而不创建提交,所以你仍需自己执行 git commit

注意事项,以及黄金法则: 只要历史还在本地,就可以放心重写;但绝不要 rebase 那些你已经推送到共享分支上的提交。重写共享历史会迫使其他所有人陷入痛苦的对账工作,Pro Git 称这一风险为”变基的风险”

git bisect:找出引入 bug 的那个提交

使用场景: 当某个功能上周还好用、现在坏了,而你完全不知道是 200 个提交中的哪一个造成的时候。git bisect 会在历史中执行二分查找:标记一个坏提交和一个好提交,测试 Git 检出的那些中点,它就能在 log₂(n) 步内精确定位第一个坏提交。

git bisect start
git bisect bad                 # current commit is broken
git bisect good v2.3.0         # this older tag worked
# Git checks out a midpoint — test it, then tell Git:
git bisect good                # this one is fine
git bisect bad                 # this one is broken
# ...repeat until Git reports "<hash> is the first bad commit"
git bisect reset               # return HEAD to where you started

你可以把整个查找过程自动化:git bisect run <script> 会根据脚本的退出状态自动把每个提交标记为好或坏,无需任何手动输入。每次会话结束时都要执行 git bisect reset,把 HEAD 送回查找开始前的位置。

注意事项: bisect 假定每个检出的提交都能干净地构建并运行你的测试。如果某个提交因无关原因编译失败,就会扭曲结果。对这类提交要用 git bisect skip 标记,而不要靠猜测标成好或坏。

为那些破坏性命令准备一张安全网

这些命令中有两个会重写历史,而对二者的规则是相同的:git rebase -igit reset --hard 在本地未推送的工作上是安全的,在共享分支上则很危险。当你需要在别人已经拉取过的分支上做一次安全的撤销时,优先选择 git revert,它会记录一个新的提交来反转改动,而不改动已有历史。而当本地的重写出了问题时,git reflog 就是那个能把提交找回来的逃生舱。

这五个命令之所以配得上一席之地,是因为它们各自回答了 commitpush 无法回答的问题:存下这个、找回那个、搬走一个、整理一遍、揪出 bug。把每个命令与它的触发场景关联起来,一旦情形出现就立刻取用。

常见问题

git reset --hard 和 git revert 有什么区别?

git reset --hard 会回退 HEAD 并丢弃提交,从而重写历史,这使得它在任何别人已经拉取过的分支上都很危险。而 git revert 则是记录一个新的提交来反转目标改动,同时保持已有历史不变。只在本地未推送的工作上使用 reset --hard;在共享分支上则用 revert 作为安全的撤销方式。如果一次 reset 出了问题,git reflog 可以通过哈希值恢复被丢弃的提交。

git reset --hard 删掉了一个提交,我该如何恢复?

运行 git reflog,它记录了 HEAD 在你本地仓库中占据过的每一个位置。找到你想要的那个状态对应的条目,记下它的哈希值,然后运行 git reset --hard <hash> 来恢复它,或者先用 git switch -c recovery <hash> 在一个新分支上检查它。reflog 属于本地且每个克隆各自独立,所以重新 clone 并不能恢复这些工作;而且条目会随时间被垃圾回收清理掉,因此要尽早恢复,不要拖延。

在切换分支这件事上,git switch 是否取代了 git checkout?

git switch 和 git restore 在 Git 2.23(2019 年 8 月)中被引入,用于拆分 git checkout 过于臃肿的职责:switch 负责在分支之间移动,restore 负责恢复文件。这两个命令的手册页多年来一直带有实验性警告,但从 Git 2.55.0 起该警告已被移除,因此它们现在就是这些操作的推荐命令。git checkout 仍然可用,也没有被废弃,所以现有的工作流依然有效。

什么时候应该用 git cherry-pick 而不是合并?

当你只需要把某一个提交带到另一个分支上时(例如把单个 hotfix 搬到发布分支),而又不想引入源分支其余的历史,就用 git cherry-pick。合并会带来整个分支的改动,对于隔离单个修复而言是错误的工具。请注意,cherry-pick 会创建一个带有新哈希值的新提交,因此如果同样的改动之后又通过常规合并到来,你的历史中就可能出现重复的改动。

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — self-hosted, with full data ownership.

Star on GitHub

We use cookies to improve your experience. By using our site, you accept cookies.