大家好,我是程序员柏风,AI 最全的 Vibecoding 学习地图 http://coderoadmap.cn 作者。
Git 进阶
这一课你将能做到
- 用
git switch/git branch创建、切换、删除分支,并理解 HEAD 指针 - 区分 fast-forward 合并与三方合并,会用
--no-ff保留分支历史 - 用
git stash临时存稿,用git tag打版本号,用cherry-pick挑提交
为什么要学
分支是 Git 的灵魂,也是它碾压 SVN 的根本原因:Git 的分支只是一个指向 commit 的指针(41 字节),创建一个分支的代价是「零」。这让我们可以为每个 bug、每个功能开一条独立分支,互不干扰地并行开发。
不理解分支模型,后面学的 GitHub PR、Code Review、Git Flow 全都立不住。而面试官问你「HEAD 是什么」「merge 和 rebase 区别」时,答的就是这一课的内容。
先建立图像
分支 = 指针,HEAD = 当前位置标记:
main 分支
▼
A ── B ── C ────── D
\
E ── F ← feature 分支
▲
HEAD(你正站在这里)
合并的两种形态:
【fast-forward】main 直接快进到 feature(没有合并 commit)
A─B─C─D ─合并后─> A─B─C─D─E─F main 移到 F
【三方合并 / --no-ff】产生一个合并 commit M
A─B─C─D───M
\ /
E─F
--no-ff 强制走三方合并,好处是历史里能看出「这是一条功能分支」。
跟着做
步骤 1:创建并切换分支
git branch # 列出本地分支,* 号是当前
git switch -c feature/login # 创建并切换(推荐新语法)
# 等价老命令:git checkout -b feature/login
# 在新分支上改代码、提交
echo "// 登录功能" >> src/Login.java
git add .
git commit -m "feat: 实现用户登录"
switch 是 Git 2.23 引入的新命令,专职「切分支」;restore 专职「改文件」,把老 checkout 的职责拆开了。
步骤 2:查看分支图
git log --oneline --graph --all
# * 3f4a2b1 (HEAD -> feature/login) feat: 实现用户登录
# * d1e5c6a (main) fix: 修复首页空指针
--graph --all 让你同时看到所有分支的分叉结构。
步骤 3:合并分支(fast-forward)
git switch main
git merge feature/login
# Fast-forward
# src/Login.java | 1 +
如果 main 在你开发期间没有新提交,Git 直接把 main 指针快进到 feature 最新位置,不产生合并 commit。
步骤 4:强制三方合并(保留分支痕迹)
git switch main
git merge --no-ff feature/login -m "merge: 合入登录功能"
团队开发推荐 --no-ff,因为历史里能清楚看到「这一坨是一个功能」。
步骤 5:删除分支
git branch -d feature/login # 安全删除(已合并)
git branch -D feature/login # 强制删除(未合并也删,危险)
步骤 6:临时存稿 stash
正在改代码,线上突然报 bug 要你切回 main 处理?不想 commit 半成品:
git stash # 把当前改动收起来
git switch main # 去修 bug ...
git switch feature/login
git stash pop # 恢复刚才的改动
常用变体:git stash list 看列表、git stash show -p 看内容、git stash drop 丢弃。
步骤 7:打标签与 cherry-pick
# 给当前 commit 打版本号
git tag -a v1.0.0 -m "首个正式版"
git tag # 列出所有 tag
git push origin v1.0.0 # 推送标签到远程
# 把别的分支的某个 commit 单独挑过来
git cherry-pick 3f4a2b1
cherry-pick 常用于「这个修复只要那一个 commit,不要整条分支」。
讲清楚要点
- 分支只是指针:
git branch xxx只是创建了一个 41 字节的文件指向当前 commit,没有任何文件拷贝。这就是 Git 分支「廉价」的本质。 - HEAD 是「你当前在哪」的标记:通常 HEAD 指向某个分支名,分支名再指向 commit。
git switch就是移动 HEAD。 - fast-forward vs 三方合并:能快进就快进(无新 commit);如果 main 和 feature 都各自前进了,Git 会生成一个合并 commit 把两条线拧一起(三方合并)。
stash是栈结构:先进后出。pop取出并删除,apply只取出不删除,可同时存多份。- tag 是打在某个 commit 上的永久铭牌:和分支的区别是分支会随提交前移,tag 永远不动,常用于标记发布版本。
你可能会懵的地方
-
git switch和git checkout到底用哪个? 新代码统一用switch(切分支)和restore(改文件),语义清晰。checkout是老命令,一个命令干两件事,容易混淆,但所有老教程都在用它,你得认识。 -
fast-forward 是不是「丢了分支信息」? 是。历史变成一条直线,看不出哪些 commit 属于哪个功能。团队开发建议
git merge --no-ff,强制生成合并 commit。 -
detached HEAD 是什么鬼? 当 HEAD 不指向任何分支、直接指向某个 commit 时(比如
git switch --detach a1b2c3d或切到 tag),就进入「游离头指针」状态。此时提交的 commit 没有分支引用,切走后可能丢。解决:立刻git switch -c 新分支给它安个家。 -
stash 能跨分支用吗? 能。stash 是全局的,不绑定分支。在 A 分支 stash,切到 B 分支 pop,改动会落到 B 的工作区。
-
tag 和 branch 有什么本质区别? branch 是可移动的指针,tag 是固定在某个 commit 上的名字。branch 用来开发,tag 用来标记里程碑。
错法对照
| 错法 | 问题 | 正解 |
|---|---|---|
| 在 main 上直接改代码 | 污染主干,回滚困难 | 任何改动都先 switch -c feature/xxx |
用 -D 强删未合并分支 | 未合并工作丢失 | 默认用 -d,确认无用再 -D |
git stash 后忘记 pop | 改动「消失」了 | git stash list 随时查,处理完立刻 pop |
| detached HEAD 下直接提交 | 提交没人引用,切走就丢 | 先 git switch -c 新分支 再提交 |
| 打完 tag 忘记 push | 远程看不到版本 | git push origin --tags |
卡点急救
git switch报错「Your local changes would be overwritten」:当前改动与目标分支冲突。先git stash收起来,切换后再git stash pop;或先 commit。- 误删了分支:
git reflog找到该分支最后一次 commit 的哈希,然后git branch 分支名 哈希即可复活。 - detached HEAD 下已经提交了东西:立刻
git switch -c temp-save把当前位置固定成分支,再决定合到哪里。
面试常问
-
HEAD 是什么? HEAD 是一个指针,指向「当前所在的分支」(更精确说是指向当前分支名)。
git switch移动 HEAD,git commit让 HEAD 指向的分支前进一格。detached HEAD 就是 HEAD 直接指向 commit 而非分支。 -
git merge的 fast-forward 和三方合并区别? 目标分支自分支点之后没有新提交时,Git 直接快进指针(无合并 commit);如果双方都前进了,Git 找共同祖先做三方合并,生成一个合并 commit。--no-ff可强制后者。 -
git cherry-pick什么时候用? 当另一个分支上有某个 commit(比如热修复)你需要,但不想合并整条分支时,用 cherry-pick 把那个 commit 单独复制到当前分支。注意它生成的是新 commit(哈希不同)。
自测三问
- 创建分支的开销为什么接近零?分支文件存在哪?
- fast-forward 什么时候发生?怎么强制禁用它?
git stash pop和git stash apply的区别?
小结一句话
分支是廉价指针、HEAD 是当前位置,掌握 switch / merge / stash / tag / cherry-pick,你就拥有了并行开发的超能力。
延伸
- Pro Git:分支
- 下一课:GitHub 入门——把本地仓库推上云端,理解 origin / remote / push / pull。