Git 进阶工作流:从 add/commit/push 到团队协作的正确姿势
Git 入门只需要三条命令:
add、commit、push。但用了三年还是这三条命令,遇到合并冲突就删库重来,看到git log --graph像地铁线路图一样乱。这篇文章讲的是这三条命令之后的东西——分支策略、合并方式、rebase 的正确用法、以及一套真正能落地的团队工作流。
你的 Git 在哪个阶段?
| 阶段 | 特征 |
|---|---|
| 入门 | add、commit、push、pull 循环 |
| 进阶 | 会用 branch、merge、stash |
| 熟练 | rebase、cherry-pick、交互式 rebase |
| 精通 | reflog 救场、bisect 找 bug、submodule/subtree |
这篇文章的目标是把你从"入门"拉到"熟练"。
分支策略:Git Flow vs GitHub Flow
Git Flow(重流程,适合版本发布型项目)
main ●──●────────────────────●── v1.0 v2.0
\ /
develop ●──●──●──●──●──●──●
\ /
feature/login ●──●
\
release/1.0 ●──●
\
hotfix/bug ●──●
适合:有明确版本号的软件(App、桌面软件),发布周期以周或月计。
GitHub Flow(轻流程,适合持续部署)
main ──●──●──●──●──●──●──● (随时可部署)
\ \ \ \
feature1 ●──● ● ●
feature2 ●──●──●
适合:Web 应用、API 服务,每天部署多次。规则只有一条:所有功能开发在 feature 分支,合并前走 Pull Request。
怎么选?
个人项目或小团队 Web 项目,用 GitHub Flow。你想简单,分支越少越好。
Merge 的三种方式:你一直用错的那个按钮
GitHub PR 页面有三个合并按钮:
1. Merge Commit(默认)
main: A──B──C
\
feature: D──E
merge 后:
main: A──B──C────M
\ /
D──E
保留完整历史,但多人协作会产生大量 merge commit,"地铁线路图"就是这么来的。
2. Squash and Merge(推荐)
main: A──B──C
\
feature: D──E
squash merge 后:
main: A──B──C──S (S 是 D+E 的压缩)
一条 feature 分支的所有 commit 压缩成一个,main 分支历史是一条直线。缺点:丢失了 feature 分支上的中间 commit 细节。
3. Rebase and Merge
main: A──B──C
\
feature: D──E
rebase merge 后:
main: A──B──C──D'──E'
把 feature 分支的 commit 逐个搬到 main 顶部,保留每个 commit,但改变了 commit hash。
推荐策略:
| 场景 | 方式 |
|---|---|
| 功能开发 PR | Squash and Merge |
| Bug 修复(单 commit) | Rebase and Merge |
| 跨团队协作的大功能 | Merge Commit(保留完整上下文) |
我个人偏好:日常开发一律 Squash and Merge。main 分支历史干净,回滚一个 PR 就是回滚一个 commit。
Rebase 的正确姿势
Rebase 是最容易被用错的 Git 命令。记住核心原则:只 rebase 还没 push 过的本地 commit。
场景一:同步上游变更(最常用)
# 你在 feature 分支上开发了 3 天,main 已经被同事更新了 20 个 commit
git checkout feature
git fetch origin
git rebase origin/main
# 如果有冲突,解决后:
git add .
git rebase --continue
等价于把你 feature 分支上的 commit "摘下来",等 main 更新完再"贴回去"。效果是 history 变成一条直线。
如果用 merge:
git merge origin/main
会产生一个 "Merge branch 'main' into feature" 的 commit,纯粹噪音。
场景二:交互式 Rebase —— 整理本地 commit
你在本地开发时 commit 很随意:
WIP: 开始写
fix typo
增加接口
WIP: 临时保存
又改了个 bug
完成功能
提交 PR 前,用交互式 rebase 整理:
git rebase -i HEAD~6
进入编辑器:
pick a1b2c3d WIP: 开始写
pick e4f5g6h fix typo
pick i7j8k9l 增加接口
pick m0n1o2p WIP: 临时保存
pick q3r4s5t 又改了个 bug
pick u6v7w8x 完成功能
改成:
pick a1b2c3d 开始写
fixup e4f5g6h fix typo # 合并到上一个
pick i7j8k9l 增加接口
fixup m0n1o2p WIP: 临时保存 # 合并到上一个
fixup q3r4s5t 又改了个 bug # 合并到上一个
reword u6v7w8x 完成功能 # 保留但重新写 commit 消息
保存退出后,6 个零散的 commit 变成 3 个清晰的 commit。再 push 到远端。
fixup 的作用是把该 commit 的内容合并到上一个 commit,但丢弃它的 commit 消息。squash 会合并内容并让你重新编辑消息。reword 只改消息不改内容。
Git Stash:临时的后悔药
正在 feature 分支上写了一堆代码,突然线上出了 bug 要切到 hotfix 分支:
git stash # 暂存所有修改
git checkout main
git checkout -b hotfix/crash
# ... 修复、提交、合并 ...
git checkout feature
git stash pop # 恢复之前的工作
常用 stash 命令:
git stash # 暂存所有改动
git stash save "描述" # 附说明
git stash list # 查看 stash 列表
git stash pop # 恢复最近一个并删除
git stash apply stash@{1} # 恢复指定 stash,不删除
git stash drop stash@{2} # 删除指定 stash
Cherry-pick:精准搬运单个 commit
测试环境上的某个修复需要立刻搬到线上,但线上分支和测试分支差异很大,不能直接 merge。
# 找到那个 commit 的 hash
git log --oneline
# 切到目标分支
git checkout main
# 只搬运这一个 commit
git cherry-pick abc1234
如果是搬运一个范围:
git cherry-pick abc1234..def5678
Cherry-pick 会重新生成 commit,hash 会变。不要对已经 push 到公共分支的 commit 做 cherry-pick,会造成重复提交。
Git Reflog:救回"被删除"的 commit
git reset --hard 之后发现删错了,欲哭无泪?Reflog 是你最后的救命稻草。
git reflog
abc1234 HEAD@{0}: reset: moving to HEAD~3
def5678 HEAD@{1}: commit: 重要的功能
ghi9012 HEAD@{2}: commit: 另一个修改
看到那个 def5678 了吗?它还在 repo 里,只是没有分支指向它。
git checkout -b recovery def5678
被"删除"的 commit 复活了。Reflog 默认保留 90 天,足够你从任何灾难中恢复。
Git Bisect:自动定位引入 bug 的 commit
线上突然出 bug,你知道 2 周前是好的,但中间有 200 个 commit,不知道哪个引出的 bug。
git bisect start
git bisect bad HEAD # 当前版本有问题
git bisect good abc1234 # 2 周前版本没问题
Git 自动 checkout 到中间某个 commit。你测试:
git bisect good # 如果这个版本没问题
# 或
git bisect bad # 如果这个版本有问题
重复几次,Git 二分法快速定位到出问题的那一次 commit。200 个 commit,只需要测 log2(200) ≈ 8 次。
有自动化测试的话更快:
git bisect run python test_bug.py
全自动二分定位。
一套可落地的团队工作流
总结成每天的操作:
# 早上来,同步最新代码
git checkout main
git pull --rebase
# 开始新功能
git checkout -b feature/xxx
# ... 写代码,多次 commit ...
# 提交 PR 前整理 commit
git fetch origin
git rebase -i origin/main
# push
git push -u origin feature/xxx
# 创建 PR,等待 Code Review
# Review 意见修改后,继续 commit + push
# 最后选择 "Squash and Merge"
# 切回 main,拉最新,开始下一个功能
git checkout main
git pull --rebase
git branch -d feature/xxx
常见问题
Q:push 被拒绝,提示 "non-fast-forward"?
因为远端有新 commit 你本地没有。正确做法:
git fetch origin
git rebase origin/main
git push
千万别:git push -f(除非你完全确定自己在做什么)。
Q:冲突了怎么办?
Rebase 过程中冲突:
# Git 提示冲突
# 编辑冲突文件,解决冲突
git add .
git rebase --continue
# 如果解决不了,放弃本次 rebase
git rebase --abort
Merge 过程中冲突也一样,但 --abort 换成 git merge --abort。
Q:.git 目录太大了怎么办?
历史久远的仓库,git clone 会下载全部历史。清理大文件:
# 找出大文件
git rev-list --objects --all | git cat-file --batch-check='%(objecttype) %(objectname) %(objectsize) %(rest)' | awk '/^blob/ {print $3, $4}' | sort -rn | head -10
# 用 BFG Repo-Cleaner 或 git filter-branch 清理(谨慎操作)
Git 的精髓不在于记住多少命令,而在于理解每个操作对历史的改变。Squash 让 main 分支干净、Rebase 让 feature 分支跟上游同步、Reflog 让一切误操作可逆。掌握这三件武器,Git 就从"能用的版本管理"变成"好用的版本管理"。