这是 Git 手册系列的第二篇。初级篇解决了"能用",本篇解决"用得对":多人协作不打架、冲突不慌、历史能改得干净。


目录

  • 分支模型:选一个然后坚持
  • merge 与 rebase:到底用哪个
  • 冲突的正确解法
  • reset 的三种模式(soft / mixed / hard)
  • stash:临时收摊子
  • cherry-pick:挑一个提交带走
  • 修改历史:amend 与交互式 rebase
  • reflog:你的后悔药
  • tag 与发布
  • 中级常见错误

分支模型:选一个然后坚持

个人 / 小团队:Trunk-Based(主干开发)

  • main 是唯一长期分支,随时可发布
  • 功能分支从 main 切出,活 1~3 天,合并后立即删除
  • 配合 PR/MR 做代码评审
git switch -c feature/xxx main
# 开发、提交
git push -u origin feature/xxx   # 提 PR,评审通过后合并
git switch main && git pull
git branch -d feature/xxx

需要同时维护多个发布版本:Git Flow

  • main:线上版本;develop:集成分支
  • feature/*release/*hotfix/* 各司其职
  • 重,只有确实需要多版本并行(如 SaaS + 私有化部署)才用

结论:90% 的团队用 Trunk-Based 就够了。分支模型越简单,协作成本越低。


merge 与 rebase:到底用哪个

两者都用来"把 B 分支的改动合进 A 分支",区别在于历史形态:

merge(保留真实历史,产生合并提交):
main:   A---B-------M
              \   /
feature:       C---D

rebase(把 feature 的提交"搬家"到 main 最新处,历史成一条直线):
main:   A---B
feature:         C'--D'   (C、D 被重放为新提交)

经验法则:

  • 公共分支(main、develop)上用 merge,保留协作痕迹。
  • 自己的功能分支同步 main 最新代码时用 rebase,保持历史干净:
git switch feature/xxx
git fetch origin
git rebase origin/main      # 解决冲突后 git rebase --continue

⚠️ 黄金规则:永远不要 rebase 已经 push 且别人在用的分支。rebase 会改写提交 id,别人的本地历史会对不上。


冲突的正确解法

冲突的本质:同一文件的同一区域,两边都改了,Git 不知道该听谁的。

git merge feature/xxx
# CONFLICT (content): Merge conflict in src/app.js

处理流程:

git status                 # 1. 看哪些文件冲突

打开冲突文件,会看到标记:

<<<<<<< HEAD
const timeout = 3000;
=======
const timeout = 5000;
>>>>>>> feature/xxx

<<<<<<< HEAD======= 是当前分支的内容,=======>>>>>>> 是合入方的内容。手动编辑成正确结果,删掉标记:

const timeout = 5000;

然后:

git add src/app.js         # 2. 标记为已解决
git commit                 # 3. 完成合并提交

中途想放弃:git merge --abort(rebase 对应 git rebase --abort),一切回到合并前。

降低冲突概率的三个习惯:

  1. 功能分支短命,尽快合并
  2. 开始干活前先 git pull / git rebase origin/main
  3. 公共文件(配置文件、路由表)的改动尽早沟通

reset 的三种模式(soft / mixed / hard)

git reset 把 HEAD 移动到某个提交,三种模式决定"工作区和暂存区怎么办":

模式提交历史暂存区工作区
--soft回退保留保留
--mixed(默认)回退清空保留
--hard回退清空清空
git reset --soft HEAD~1      # 撤销上次提交,改动还在暂存区(改提交内容用)
git reset HEAD~1             # 撤销上次提交,改动回到工作区(重新组织提交用)
git reset --hard HEAD~1      # 撤销上次提交,改动全部丢弃(危险!)

HEAD~1 表示"上一条提交",HEAD~3 表示往前数 3 条。

安全准则--hard 之前先 git stash 或确认改动有备份;只 reset 还没 push 的提交。


stash:临时收摊子

场景:正在 feature 分支改一半,线上突然有 bug 要你切分支修。

git stash                    # 把未提交的改动收起来,工作区变干净
git switch hotfix/urgent     # 去修 bug
# …修完、提交…
git switch feature/xxx       # 回来
git stash pop                # 恢复刚才收起的改动

常用操作:

git stash list               # 看有几个 stash
git stash show -p            # 看最近一次 stash 的内容
git stash push -m "登录页改了一半"   # 带备注存
git stash apply stash@{1}    # 恢复指定的(pop 会删掉,apply 保留)
git stash drop stash@{0}     # 删除

cherry-pick:挑一个提交带走

场景:在 develop 上修的一个 bug,main 上也需要,但不想合并整个 develop。

git log --oneline develop        # 找到那个提交的 id,比如 a1b2c3d
git switch main
git cherry-pick a1b2c3d          # 把它复制过来(生成新提交 id)

可以连续挑多个:git cherry-pick A B C,或挑一个区间 git cherry-pick A..B(不含 A)。


修改历史:amend 与交互式 rebase

amend:修补最近一次提交

# 刚提交完发现漏了个文件 / 提交信息写错了
git add forgotten.js
git commit --amend               # 编辑器里顺便改 message
git commit --amend --no-edit     # 只补内容不改 message

交互式 rebase:整理最近 N 条提交

git rebase -i HEAD~4

编辑器里列出最近 4 条提交,把每行开头的 pick 改成:

指令作用
pick保留这条提交
reword保留,但改提交信息
squash合并进上一条(保留两条的信息供编辑)
fixup合并进上一条(丢弃本条信息)
drop删除这条提交

典型场景:PR 之前把 修复 typo再改一下真的改好了 这类碎提交压成一条干净的。

⚠️ 同样的黄金规则:只改写未 push 的历史。如果已 push 且确认只有自己在用,改写后需要 git push --force-with-lease(比 --force 安全:远程有别人新提交时会拒绝)。


reflog:你的后悔药

几乎所有"我完了"的瞬间,git reflog 都能救:

git reflog
a1b2c3d HEAD@{0}: reset: moving to HEAD~1      ← 手滑的操作
e4f5g6h HEAD@{1}: commit: 重要的提交            ← 以为丢了的提交

reflog 记录 HEAD 的每一次移动(commit、reset、rebase、切换分支……),保留约 90 天。找回方式:

git reset --hard e4f5g6h     # 直接回到那个状态
# 或
git branch rescue e4f5g6h    # 开个分支把那个提交捞回来

只要不是 git clean 删掉的未跟踪新文件,Git 里几乎没有真正丢失的提交。


tag 与发布

git tag v1.2.0                   # 轻量标签
git tag -a v1.2.0 -m "发布 1.2.0:新增支付模块"   # 附注标签(推荐)
git push origin v1.2.0           # 推送单个标签
git push --tags                  # 推送所有标签
git tag -d v1.2.0                # 删本地
git push origin :v1.2.0          # 删远程

版本号遵循语义化版本 主.次.修(semver):破坏性变更升主版本,新功能升次版本,修 bug 升修订号。


中级常见错误

1. 在公共分支上 rebase / amend 了已 push 的提交 队友 pull 时会历史错乱。解:公共分支只用 merge;改写前先确认没有别人基于它工作。

2. 解决冲突时直接用"接受我方/接受对方"整文件覆盖 可能把别人无关的改动一起吞掉。逐块(hunk)检查。

3. git reset --hard 之后发现误删了改动 先别慌,git reflog 找回。养成 hard 之前 git stash 的习惯。

4. stash 堆成山从来不清理 git stash list 定期看一眼,用完就 pop/drop。

5. cherry-pick 之后又 merge 原分支,出现重复改动 偶尔会发生,合并时 Git 通常能识别;冲突了耐心解一次即可。


本篇速查表

目的命令
同步 main 到功能分支git rebase origin/main
放弃合并git merge --abort
撤销提交保留改动git reset --soft/--mixed HEAD~1
彻底回退(危险)git reset --hard <id>
暂存半成品git stash / git stash pop
复制单个提交git cherry-pick <id>
改最近提交git commit --amend
整理历史git rebase -i HEAD~n
找回丢失提交git reflog
安全强推git push --force-with-lease
打标签git tag -a v1.0.0 -m "..."

到这一层,你已经能应付团队协作的全部日常。下一篇《高级篇》进入 Git 的内部世界:对象模型、引用、HEAD 的本质、bisect 二分排错、worktree、submodule/subtree 与钩子——理解了"为什么",命令就再也背不丢。