路线图

04-Git 日常与进阶操作

星辉 2026-07-02 阅读 6 min 1,173 字 路线图
04-Git 日常与进阶操作 封面

本章把 Git 操作分成三层讲:基础操作(每天都在用)、进阶操作(关键时刻救命)、危险操作(理解了才敢用)。每个操作都配实际命令示例和适用场景。

基础操作#

add / commit / log / diff#

这四个是 Git 的"吃喝拉撒",但很多人用了多年也没用对。Git 的命令分两层:porcelain(给人用的上层命令,如 git add git log)和 plumbing(给脚本用的底层命令,如 git cat-file git rev-parse)。日常用 porcelain 就够,写自动化脚本时才需要 plumbing。

bash
# add:选择性 stage(比 git add . 更可控)
git add -p                    # 按 hunk 选择性 stage,最推荐的工作方式
git add -u                    # 只 stage 已跟踪文件的修改(不包含新文件)

# commit:写好 message
git commit -m "feat(auth): add OAuth2 login with Google"
# 多行 message
git commit -m "feat(auth): add OAuth2 login" -m "Body: 详细说明"
# 修改最近一次 commit(未 push 时安全)
git commit --amend

# log:定制查看历史
git log --oneline --graph --all   # 图形化看全部分支
git log -p <file>                 # 看某个文件的完整改动历史
git log --author="zhangsan" --since="2 weeks ago"
git log -S "function_name"        # 搜索增删了某字符串的 commit(pickaxe)

# diff:看清差异
git diff                        # 工作区 vs 暂存区
git diff --staged               # 暂存区 vs HEAD(下次 commit 会写什么)
git diff main...feature         # feature 分支相对 main 分叉点的改动(三点)
git diff main..feature          # main 和 feature 的当前差异(两点)

git diff main...feature(三点)和 git diff main..feature(两点)容易混。三点是"feature 自分叉以来的改动"——这是 PR review 时最想看的;两点是"两个分支当前的差异"——包含 main 上的新提交,会污染 diff。

commit message 写法#

写好 commit message 是 Git 工程化的第一步。推荐用 Conventional Commits 规范(详见 06 章),简单说就是:

text
<type>(<scope>): <description>

[optional body]

[optional footer]

例:

text
feat(auth): add OAuth2 login with Google

Integrate Google OAuth2 as an alternative login method.
Closes #123

rebase vs merge#

这是 Git 最容易引发"宗教战争"的话题。其实两者没有对错,只是不同的历史整理策略,关键是团队统一。

merge:保留完整历史#

bash
git checkout main
git merge feature/login

历史长这样:

text
*   abc1234 Merge branch 'feature/login' into main
|\
| * def5678 feat: add login page
| * ghi9012 feat: add auth service
* | jkl3456 fix: something on main
|/
* mno7890 initial commit

特点:保留分支创建和合并的完整轨迹。多人协作的分支、已 push 的分支,用 merge 安全。

rebase:线性历史#

bash
git checkout feature/login
git rebase main                 # 把 feature 的提交"嫁接"到 main 最新点之后
git checkout main
git merge feature/login         # 此时是 fast-forward,无合并提交

历史长这样:

text
* def5678 feat: add login page
* ghi9012 feat: add auth service
* jkl3456 fix: something on main
* mno7890 initial commit

特点:历史线性、干净。但 rebase 会重写 commit hash——原本的 def5678 ghi9012 被替换成新的 hash,老的变孤儿。

黄金法则#

永远不要 rebase 已经 push 到远端、其他人在基于此工作的分支。rebase 重写 hash 后强制 push,其他人的本地分支会和远端不一致,解决起来非常麻烦。

bash
# 安全:rebase 自己的本地分支
git fetch origin
git rebase origin/main          # 把自己的提交接在最新 main 之后

# 危险:rebase 已 push 的共享分支
git push --force-with-lease origin feature/shared
# 除非确认没人基于此分支工作,否则别这么做

--force-with-lease 比 --force 安全——它会检查远端状态,如果远端分支有新提交(说明有人在用),会拒绝 push。

交互式 rebase:整理本地提交#

提交 PR 前清理"wip""fix typo"这类噪音 commit:

bash
git rebase -i HEAD~4

编辑器会出现:

text
pick abc1234 feat: add user model
pick def5678 wip
pick ghi9012 fix typo
pick jkl3456 feat: add user controller

改成:

text
pick abc1234 feat: add user model
squash def5678 wip              # 合入上一个,保留 message
fixup ghi9012 fix typo          # 合入上一个,丢弃 message
pick jkl3456 feat: add user controller

保存后历史变成两个干净的 commit。这是 PR 前整理提交的标配操作。

适用场景对比#

场景推荐理由
同步主分支最新代码到 feature 分支rebase历史线性,PR diff 干净
合并 feature 分支到 mainmerge(或 squash merge)保留分支轨迹,不重写已 push 历史
整理本地未 push 的提交interactive rebase清理噪音 commit
多人共用的长生命分支merge绝不 rebase 已 push 的共享分支

squash merge:第三种选择#

除了 merge 和 rebase,现代平台(GitHub/GitLab)还提供 squash merge——把 feature 分支的所有 commit 压成一个新 commit 合到 main:

bash
# GitHub/GitLab UI 上选 "Squash and merge"
# 等价于:
git checkout main
git merge --squash feature/login
git commit -m "feat(auth): add OAuth2 login (#123)"

优势:主线历史线性干净(一个 PR = 一个 commit),开发者 PR 过程中的 "wip"/"fix typo" 不进主线,只需关注 PR title 是否符合 Conventional Commits 规范。这是多数团队的最佳实践——squash merge + PR title 作为 commit message 源。

cherry-pick:精准移植#

cherry-pick 把某个 commit 的改动应用到当前分支,最典型的场景是 hotfix 同步:

bash
# 在 main 上修了一个 bug
git log main --oneline
# abc1234 fix(api): fix null pointer in getUserById

# 需要同步到 release/v1.x 分支
git checkout release/v1.x
git cherry-pick abc1234

cherry-pick 会创建一个新 commit(hash 不同),内容是同一个改动的"复制品"。

不要滥用 cherry-pick:如果你频繁需要 cherry-pick,说明分支策略有问题——通常是发版分支和主分支的同步机制没设计好。cherry-pick 不传递历史,同一个 commit 在两个分支上以不同 hash 存在,之后合并时可能制造混乱。

stash:暂存当前改动#

写到一半要切分支处理紧急 bug,但又不想提交半成品:

bash
git stash                       # 把工作区改动暂存
git stash -u                    # 包含未跟踪文件
git stash save "WIP: 重构登录"   # 加说明(旧语法)
git stash push -m "WIP: 重构登录" # 加说明(新语法)

git checkout main
# 处理紧急 bug...

git checkout feature/refactor
git stash pop                   # 取回最近的 stash(并删除 stash 记录)
git stash list                  # 列出所有 stash
git stash apply stash@{2}       # 取回指定 stash(不删除记录)

stash 是个栈,可以 push 多个。但不要长期堆积——stash list 看到 10 个 stash 时,大概率已经没人记得里面是什么了。stash 是临时操作,不是长期保存机制。

stash 的高级用法#

bash
# 只 stash 某个文件
git stash push -m "only config" -- config.yaml

# stash 但保留暂存区
git stash --keep-index

# 从 stash 创建分支(stash 内容冲突无法 pop 时用)
git stash branch recover-branch stash@{0}

# 清空所有 stash(谨慎)
git stash clear

stash vs commit:如果改动需要保留超过一天,直接创建临时 commit 比 stash 更安全——commit 不会因为 stash clear 误操作而丢失。

reflog:救命操作#

reflog 记录所有 HEAD 和分支的移动历史,即使 git reset --hard 也能找回。

bash
git reflog
# abc1234 HEAD@{0}: reset: moving to HEAD~1          ← 刚才的 reset
# def5678 HEAD@{1}: commit: feat: add user model      ← 被 reset 掉的 commit
# ghi9012 HEAD@{2}: checkout: moving from main to dev

# 找回被 reset 的 commit
git checkout def5678            # 临时查看
git checkout -b recover/login   # 创建新分支保存

reflog 默认保留 90 天(可配置 gc.reflogExpire)。只要 reflog 还在,几乎所有"误操作"都能恢复。这是 Git 比 SVN 安全得多的根本原因——SVN 撤销 commit 需要服务器管理员介入,Git 自己就能找回。

适用场景:

  • git reset --hard 误操作
  • git branch -D 误删分支
  • git checkout 切走后忘了之前在 detached HEAD 提交的内容
  • rebase 中途放弃,想回到 rebase 前的状态

bisect:二分定位 bug#

线上发现一个 bug,不知道是哪次提交引入的。git bisect 用二分查找快速定位:

bash
git bisect start
git bisect bad                  # 标记当前 commit 是坏的(有 bug)
git bisect good v1.2.0          # 标记 v1.2.0 是好的(无 bug)

# Git 自动 checkout 到中间的 commit,你测试后告诉 Git 状态
git bisect good                 # 这个 commit 是好的
# Git 继续 checkout 下一个中间 commit
git bisect bad                  # 这个 commit 是坏的
# ... 重复几次

git bisect reset                # 结束 bisect,回到原分支

如果有自动化测试脚本能判断好坏,可以自动化:

bash
git bisect start HEAD v1.2.0
git bisect run npm test         # 自动跑测试,根据退出码判断好坏

log N 个 commit 中定位引入 bug 的那个,bisect 只需要 log N 步。1000 个 commit 的历史,10 步就能定位。这是 Git 给调试工作带来的最大杠杆。

bisect 实战技巧#

  • 先缩小 good/bad 范围:如果能确定"上周五还是好的",用日期 git bisect good HEAD@{2 weeks ago} 比找 tag 更方便
  • 跳过无法构建的 commit:git bisect skip 跳过编译不过的中间 commit
  • 用脚本自动化:git bisect run ./test_bug.sh 让脚本判断好坏,适合"复现 bug 需要 5 分钟"的场景——自动跑 10 轮不用人工盯
  • 结合单元测试:先写一个能复现 bug 的测试,bisect run 这个测试——失败就是 bad,通过就是 good

reset 三种模式#

git reset 是 Git 里最危险也最强大的命令之一。三种模式对应三种"撤回程度":

模式HEAD 移动暂存区工作区适用场景
--soft✓ 移到指定 commit不变不变撤销 commit 但保留所有改动(重新 commit)
--mixed(默认)✓ 移到指定 commit重置为 HEAD不变撤销 commit + unstage(重新 add + commit)
--hard✓ 移到指定 commit重置为 HEAD重置为 HEAD彻底丢弃改动(危险!)
bash
# 场景 1:刚 commit,想改 commit message 或重新组织改动
git reset --soft HEAD~1
# 此时改动还在暂存区,重新 commit 即可

# 场景 2:想撤销最近的 commit 和 stage,但保留工作区改动
git reset HEAD~1                # 等价于 --mixed
# 改动回到工作区,需要重新 git add

# 场景 3:彻底放弃最近的所有改动(谨慎!)
git reset --hard HEAD~1
# 工作区和暂存区都被重置,未 commit 的改动永久丢失
# (除非 reflog 还能找回)

--hard 是 Git 里少数几个能真正"丢数据"的命令。用之前三思:工作区有没有未 commit 的改动?想清楚再用。

reset vs revert#

git revert 不是 reset 的反义词,它创建一个新 commit 来撤销指定 commit 的改动:

bash
# 已 push 到 main 的 commit,不能用 reset(会重写历史)
# 用 revert 创建一个"反向 commit"
git revert abc1234
git push

历史里会多出一个"Revert: xxx"的 commit,但原 commit 仍然在历史中。这是已 push 的共享分支上撤销改动的正确方式。

批量 revert:

bash
# 撤销一个范围的 commit
git revert abc1234..HEAD          # 创建多个 revert commit
# 或用 --no-commit 合并成一个
git revert --no-commit abc1234..HEAD
git commit -m "revert: rollback abc1234..HEAD"

三种 reset 的决策树#

text
要撤销最近的 commit?
  │
  ├── 改动还要不要?
  │     ├── 要,重新组织后再 commit → git reset --soft HEAD~1
  │     ├── 要,重新 add 再 commit   → git reset --mixed HEAD~1
  │     └── 不要,彻底丢弃           → git reset --hard HEAD~1(危险!)
  │
  └── 已经 push 到共享分支?
        └── 不能用 reset,用 git revert <commit>

实战要点#

  • 养成 git add -p 习惯。这是 Git 区别于其他 VCS 的杀手锏,能让 commit 历史变得语义清晰、可回滚、可 cherry-pick。
  • rebase 整理本地,merge 合并到主干。这个组合是大多数团队的最佳实践。
  • --force-with-lease 替代 --force。前者会检查远端状态,防止覆盖别人的提交。
  • reflog 是后悔药。任何误操作先别慌,git reflog 看一眼,大概率能找回。
  • bisect 在大仓库里是调试神器。配合自动化测试脚本,定位 bug 的效率提升一个数量级。
  • reset --hard 前先 git stash。多一层保险,没坏处。

小结#

Git 操作可以分三层理解:基础操作(add/commit/log/diff)是日常工具,进阶操作(rebase/cherry-pick/stash/reflog/bisect)是工程师的武器库,危险操作(reset --hard/force push)需要理解机制才敢用。

记住一个原则:Git 几乎不会真正丢数据,只要 reflog 还在,误操作都能找回。这是因为 Git 的对象是不可变的——即使 ref 移走了,commit 对象仍然存在于 .git/objects/ 里,直到 GC 才会被清理(默认 90 天后才 GC)。所以不要怕用 Git——多试、多练、多看 reflog,这是掌握 Git 的唯一路径。下一章讲怎么把这些操作组织成团队协作的分支策略。