系列:版本控制基础
Git 安装与使用教程:Linux 和 Windows 从零开始版本管理 讲了 Git 的基础操作——add、commit、push、pull、branch。这些够你日常用了。
但这篇要聊的东西不一样——它们不是每天都要敲的命令,但需要用的时候不知道,会浪费你大量时间。我一共挑了十来个场景,每个都附上可复现的命令示例。
git stash:临时存一下手头的工作
你正写一半代码,突然要切分支修个线上问题,但当前的改动还没到可以 commit 的程度。这时候 stash 就是你的救星。
git stash # 暂存所有改动,工作区恢复干净git stash list # 看存了多少个git stash pop # 恢复最近一个并删除记录git stash apply # 恢复最近一个但保留记录git stash drop stash@{0} # 手动删掉某个 stash给 stash 加个名字,不然堆多了根本分不清:
git stash push -m "WIP: 修登录页样式"如果你只改了几个文件,想挑特定文件 stash:
git stash push -m "只存前端" src/components/Login.tsx什么时候用:频繁在分支间跳来跳去,改动还没成型不想 commit。
坑:别让 stash 堆几个月——它不跟分支走,时间一长很容易忘了是什么。我习惯每周 git stash list 看一圈,把没用的清掉。
git bisect:二分法定位 bug 是哪次提交引入的
有个 bug 你确定三个月前没有,但不知道哪次改动引入的。bisect 用二分查找帮你自动定位到具体哪个 commit。
git bisect startgit bisect bad HEAD # 告诉 git:当前版本有问题git bisect good abc1234 # 告诉 git:这个 commit 是好的Git 会自动跳到中间某个 commit,你测试一下:
# 如果这个版本有问题git bisect bad# 如果没问题git bisect good如此反复五六次,Git 就能定位到引入 bug 的具体 commit。整个过程通常不超过十分钟。
git bisect reset # 结束 bisect,回到原来的 HEAD自动化:如果可以写个脚本判断 bug 是否存在(比如跑个测试),那更省事:
git bisect run pytest tests/test_login.py什么时候用:一个稳定的功能突然坏了,但最近一个月改了几十个 commit,不知道是哪个。
git reflog:救回你刚刚”删除”的东西
reflog 是 Git 的后悔药——它记录了你本地仓库所有 HEAD 的移动历史。误 reset、误 rebase、误删分支之后,大多数情况能靠它救回来。
git reflog输出大概长这样:
abc1234 HEAD@{0}: commit: 修了一个 typodef5678 HEAD@{1}: rebase (finish): ...ghi9012 HEAD@{2}: reset: moving to HEAD~3jkl3456 HEAD@{3}: checkout: moving from main to feature-x每一条记录都有一个引用号。想跳回去:
git reset --hard HEAD@{2} # 回到 reset 之前的状态误删了分支:
git branch -D feature-x # 手滑删了# 从 reflog 里找 feature-x 最后指向的 commitgit reflog# 重建分支git branch feature-x abc1234注意:reflog 只存在本地,记录了最近 90 天的操作。push 到远程的不在 reflog 里。
git rebase -i:整理提交历史
rebase -i(交互式 rebase)可以对 commit 做任意操作——合并、拆分、重新排序、改 message。最有用的两个场景:
场景 1:合并零碎提交
你一天之内 commit 了五六次,都是修同一个功能的小改:
git rebase -i HEAD~6会弹出编辑器:
pick abc1111 开始做功能pick abc2222 又改了一点pick abc3333 修 bugpick abc4444 再修pick abc5555 删掉没用的代码pick abc6666 还是改回来吧改成:
pick abc1111 开始做功能squash abc2222 又改了一点squash abc3333 修 bugsquash abc4444 再修squash abc5555 删掉没用的代码squash abc6666 还是改回来吧保存退出后,六个 commit 合并成一个。
场景 2:重新排序
想让两个 commit 互换位置,直接编辑顺就行:
pick abc3333 修 bug ← 挪到前面pick abc1111 开始做功能场景 3:拆一个 commit
把 pick 改成 edit,这个 commit 应用后 Git 会暂停,让你:
git reset HEAD^ # 撤销这个 commit 但保留改动git add fileA.jsgit commit -m "第一部分"git add fileB.jsgit commit -m "第二部分"git rebase --continue # 继续原则:rebase 完 git log --oneline 应该是干净的、每个 commit 做一件明确的事。但已经 push 给别人用的分支别 rebase——会把别人的历史搞乱。
git cherry-pick:把单个 commit 搬到另一个分支
有时候你不需要 merge 整个分支——你只想要其中一个 commit。
git cherry-pick abc1234Git 会把这个 commit 的改动应用到当前分支上。如果想保留原始 commit 的 author 和时间:
git cherry-pick -x abc1234 # -x 会在提交信息里附带原始 commit hash连续搬多个:
git cherry-pick abc1234 def5678搬运一个范围:
git cherry-pick abc1234..def5678 # 左开右闭什么时候用:修 bug 的 commit 在 feature 分支上,想把它也搬到 main。或者从实验分支里提取一个工具函数。
git worktree:同时切多个分支
正常情况下一个 repo 同时只能在一个分支上工作。想同时修改两个分支,要么 stash 切来切去,要么 clone 两份。worktree 让你在一个 repo 里打开多个工作目录。
git worktree add ../project-hotfix hotfix-branch这会创建一个新目录 ../project-hotfix,里面是 hotfix-branch 的内容。你就可以同时写 feature 和修 hotfix,互不干扰。
git worktree list # 看所有 worktreegit worktree remove ../project-hotfix # 用完删掉如果有未提交的改动不想丢,可以基于一个 stash 创建临时 worktree:
git worktree add --detach ../temp-fixcd ../temp-fixgit stash pop# 干完活 git add && git commit# 然后回到原目录继续什么时候用:正在写一个大功能,突然线上出了个 hotfix。或者在编译/跑测试的间隙想切到其他分支继续干活。
git log 和 git diff 的高级用法
git log 不止 --oneline:
git log --oneline --graph --all # 可视化看全部分支结构git log --since="2 weeks ago" --author="张三" # 按时间和人过滤git log -S"函数名" # 找哪些 commit 增删了这个函数git log --grep="JIRA-123" # 按 commit message 搜索git log -- path/to/file # 只看某个文件的改动历史git diff 也很能打:
git diff --staged # 看暂存区改了什么git diff branchA...branchB # 只看 B 相对 A 的增量git diff --word-diff # 按单词显示差异,不是按行git show abc1234 # 看某个 commit 的完整改动git blame 不是用来追责的
git blame 看每行代码是谁、哪次 commit 引入的。名字不太好听,但它不是甩锅工具——是用来理解代码上下文。
git blame src/app.pygit blame -L 50,100 src/app.py # 只看 50-100 行git blame -w -C src/app.py # 忽略空白变化,检测行是否从其他文件移动过来的看到不明白的代码,blame 可以帮你定位到原始 commit,然后 git show 看那个 commit 的完整背景。
清理和优化仓库
git gc
Git 会自动 gc(垃圾回收),但如果你刚做了大量的 rebase 或删了大文件,可以手动跑:
git gc --aggressive # 深度压缩,少跑别经常跑。大型仓库 gc 一次要很久,Git 自己定期做就够了。
git clean
删掉所有未跟踪的文件(新建了还没 add 的文件):
git clean -n # 先看看会删啥,安全第一git clean -fd # 删文件 + 目录撤销某个文件的改动
还没 commit 的文件不想要了:
git checkout -- path/to/file # 恢复到最近一次 commit 的状态git restore path/to/file # 新写法,更语义化git commit —amend:改最后一个 commit
提完了发现忘记加一个文件,或者 commit message 写错了:
git add 忘掉的文件.pygit commit --amend -m "新的 commit message"如果什么都不改只想改消息:
git commit --amend --no-edit -m "更好的描述"绝对不要 amend 已经 push 到远程的 commit——除非只有你一个人用这个分支。否则同事 pull 的时候会报冲突。
常见踩坑场景
已经 push 的 commit 想撤回
git revert abc1234revert 创建一个新的”反向 commit”,把目标 commit 的改动撤销。比 reset + --force push 安全一万倍——它不改历史,同事不会炸。
rebase 搞砸了
git reflog # 找到 rebase 之前的 HEADgit reset --hard @{1} # 回到 rebase 前rebase 过程中也可以随时 git rebase --abort。
merge 完后悔了
还没 push 的话:
git reset --hard HEAD~1已经 push 了的话用 revert,但 revert merge commit 比较特殊:
git revert -m 1 abc1234-m 1 意思是”回到第一个 parent 的状态”,通常就是回到 main 分支在 merge 之前的状态。
不小心 commit 了敏感信息
比如把密码、API key 提交了:
- 如果刚 commit 还没 push:
git reset HEAD~1重新来。 - 已经 push 了:改密码,然后
git rebase -i把那个 commit 删掉,force push。 - 很久以前 commit 的:用
git filter-branch或 BFG Repo-Cleaner。但本质上是改历史,全组同事要重新 clone。
核心原则:密码一 push 就泄露了,改密码比删历史更重要。
总结
这些命令不是用来炫技的。把它们的场景串起来大概是:
- 遇到 bug 不知道什么时候引入的 →
git bisect - 手滑 reset/删分支/搞砸 rebase →
git reflog - commit 记录太乱想整理 →
git rebase -i - 正在写功能突然要修线上问题 →
git stash临时保存,或git worktree双开 - 想把一个 commit 搬到另一个分支 →
git cherry-pick - commit 之后发现少加了一个文件 →
git commit --amend - push 完发现搞错了 →
git revert,别reset --hard+--force
Git 真正厉害的地方不是命令本身,而是每条改动都有迹可循,随时能回退。理解了这一点,很多操作不用死记格式,知道它能做到就行了,需要的时候 git help <command> 查具体参数。