Git 核心概念与实用技巧
从 Commit、Branch 和 HEAD 开始理解 Git 的工作模型,并介绍 Merge、Rebase、Cherry-pick、Reflog、Worktree 等日常开发中真正实用的技巧。
Git 大概是程序员每天都在使用,却又很容易「会用但没有真正理解」的工具之一。
很多人从这样的流程开始接触 Git:
git add .
git commit -m "update"
git push代码成功上传,于是 Git 好像就是一个高级版的「代码网盘」——直到 merge 冲突、rebase 后 commit 全变、reset --hard 丢代码、HEAD detached、push --force 覆盖他人提交,才发现 Git 背后有一套非常明确的模型。
核心结论
Commit 是历史节点,Branch 是指针,HEAD 表示当前位置。 Git 的大多数操作,本质上都只是在修改这张 Commit 图:创建新 Commit、移动 Ref、重新组织图的结构。
1Git 管理的是一系列快照
我们平时看到的是文件和文件夹;Git 更关心的是一条不断增长的 Commit 链(更准确说是 Commit 图)。
每一次 Commit 都是项目在某一时刻的一个快照,同时记录:
- 当前项目内容(tree + blob)
- 作者、时间、Commit Message
- 父 Commit(merge 时可以有多个父节点)
- 唯一 SHA 哈希
发生分支后,图会从一条线变成 DAG(有向无环图):
思维转变
不要把 Git 想象成「保存了很多文件版本」,而应把它想象成一张不断增长的 Commit 图。理解这一点之后,Merge、Rebase、Cherry-pick,甚至 Reset 都会直观很多。
2Working Tree、Index 与 Repository
Git 初学者最容易困惑的是:git add 到底在干什么?
一个文件从修改到进入历史,经过三个区域:
| 区域 | 含义 | 常见命令 |
|---|---|---|
| Working Tree | 磁盘上正在编辑的文件 | 直接改文件 |
| Index / Staging Area | 下一次 Commit 准备包含的内容 | git add, git restore --staged |
| Repository | 已持久化的 Commit 历史 | git commit, git log |
git diff # 工作区 vs 暂存区
git diff --staged # 暂存区 vs 最后一次 commit暂存区让你精确控制一次 Commit 的粒度——只 stage 相关改动,而不是把半成品一股脑 commit 进去。
最值得养成的习惯:git add -p
git add -pGit 会逐块展示 diff,让你决定哪些修改进入下一次 Commit。同一文件里同时修了 Bug、格式化了代码、写了未完成的新功能?完全可以拆成多个 Atomic Commit,而不是 git add . 一锅炖。
3Branch 其实只是一个指针
很多人第一次接触 Branch,会把它想象成「Git 复制了一份完整项目」。实际上没有。
创建 git switch -c feature 只是多了一个指向同一 Commit 的名字;在 feature 上继续提交,才会分叉:
Branch 非常轻量——创建几十个 Branch 并不意味着复制几十份项目。这也是 Git 鼓励频繁开分支的原因。
4HEAD 又是什么?
如果 Branch 是指向 Commit 的指针,HEAD 就是「你现在站在哪里」:
HEAD → feature → Commit E执行 git switch main 时,HEAD 改为指向 main,Working Tree 更新为 main 当前 Commit 的内容。
Detached HEAD
有时 HEAD 不指向 Branch,而直接指向某个 Commit(例如 checkout 某个 tag 或 git bisect 中间节点)。此时若继续 Commit,新提交没有 Branch 名称帮你保存位置——切走之前记得 git switch -c 建分支,或用 git cherry-pick 摘走改动。
5Merge 到底做了什么?
假设 feature 从 main 的 B 分出,各自前进:
在 main 上 git merge feature 后,常见结果是产生 Merge Commit M,它拥有两个父节点——真实分支历史被保留:
- Fast-forward:
main直接追上feature,历史仍是一条直线 - 三方合并:出现 merge commit,图上能看到功能分支从哪分出、在哪汇合
6Rebase 到底做了什么?
同样的起点,在 feature 上执行 git rebase main:
关键:C ≠ C',D ≠ D'。Rebase 不是「把 C、D 移动过去」,而是以 E 为新起点,把 C、D 所代表的修改重新播放一遍,生成新 Commit。父节点变了,Hash 自然改变。
不要随便 Rebase 已共享的历史
Alice 和 Bob 都拥有 Commit C;Alice 把 C rebase 成 C' 后,对 Git 来说 C 与 C' 是两个完全不同的节点,Bob 本地仍是 C——历史开始分叉。
可以大胆 Rebase 自己尚未 push 的 feature 分支(PR 前 git rebase -i main 整理历史);已共享的 main / release / develop 应谨慎得多。
强调:历史实际上是怎么发生的?
保留分叉与汇合的真实形状,适合公共分支、需要审计「功能从哪条分支合入」的场景。不改已有 Commit 的 Hash。
我偏好的实际工作流:开发时在 feature 上 rebase 整理自己的历史 → PR 时用 merge 合入 main。个人历史干净,又不必频繁改写公共分支。
6.1Interactive Rebase
开发过程中出现 fix / fix again / rename variable 很正常;共享前可以整理:
git rebase -i HEAD~5| 指令 | 作用 |
|---|---|
pick | 保留该 Commit |
reword | 修改 Commit Message |
squash | 合并到上一个 Commit,保留两条 message |
fixup | 合并到上一个 Commit,丢弃本条 message |
drop | 删除该 Commit |
PR 前把「fix typo」「wip」压成 1~2 个语义清晰的 Commit,Review 体验会好很多。
7Cherry-pick:只拿走一个 Commit
并不希望合并整个 Branch,只想要其中一个修改:
git cherry-pick FGit 把 F 所表示的修改效果应用到当前 Branch,并创建新 Commit F'(F ≠ F')。适合:把 Bug Fix 摘到 release 分支、从实验分支恢复某个独立改动。
8Commit 应该怎么写?
好的 Git 历史应该能回答:这个修改为什么存在?
Fix weapon chamber state after magazine removal
Add environment openness detection for third-person camera比 update / fix / final2 有意义得多。Message 不必写成论文,但应能独立描述这一笔修改的意图。
8.1Atomic Commit
衡量标准不是「改了几个文件或多少行」,而是:这个 Commit 能否表达一个独立、完整、可理解的修改?
Fix shotgun spread calculation ← 可能改五个文件,仍是一个合理 Commit
Fix spread + Refactor save + UI ← 最好拆成三个 CommitAtomic Commit 让 Review、Revert、Cherry-pick、git bisect 都更有价值。
8.2Conventional Commits
Prop
Type
格式:<type>(<scope>): <subject>,例如 feat(inventory): add drag-and-drop slot reorder。
需要自动生成 Changelog、Semantic Versioning 的项目非常有用;个人项目不必机械遵守——比前缀更重要的是 Message 是否真正说明了修改意图。fix: update 并不会因为有 fix: 就成为好 Commit。
8.3git commit --amend
刚 Commit 完发现少文件、Message 拼错、漏了一处改动:
git add .
git commit --amend它会替换上一次 Commit,产生新 Hash。已 Push 且被他人依赖的 Commit 不要随便 Amend(同 Rebase 一样)。
9Reset、Restore 与 Revert
主要处理文件内容和暂存状态:
git restore Player.cpp # 放弃工作区修改
git restore --staged Player.cpp # 从暂存区撤出,保留工作区改动10Stash:临时寄存改动
改到一半要切分支修 Bug,还不想 Commit:
git stash push -m "WIP: inventory UI refactor"
git stash list
git stash pop # 应用并删除最新一条
git stash apply stash@{0} # 应用但保留 stashgit stash push --keep-index # 只 stash 未 stage 的改动
git stash push -u # 包含未跟踪的新文件
git stash branch fix stash@{0} # 从 stash 建分支,避免 pop 冲突丢改动Stash 本质是 Git 内部的临时 Commit;长期当「草稿箱」容易忘,尽量 small commit 或 WIP 分支。
11git reflog:Git 的后悔药
手滑 git reset --hard 后,B、C 看起来「消失」了——但很多情况下:
git reflog仍能看到 HEAD 曾经指向的位置:
HEAD@{0} reset: moving to A
HEAD@{1} commit: C
HEAD@{2} commit: Bgit reset --hard HEAD@{1} # 回到 C| 误操作 | 常见恢复 |
|---|---|
错 reset --hard | git reflog 找之前的 HEAD |
| 错 merge(未 push) | git reset --hard ORIG_HEAD |
| 删了分支 | git reflog 或 git branch recover <sha> |
| Message 写错(未 push) | git commit --amend |
Reflog 不是永久备份,但知道它存在,能避免不少惨剧。
12git bisect:让 Git 帮你找 Bug
1000 个 Commit 前正常,现在有 Bug——二分查找大约只需 次判断:
标记范围:
git bisect start
git bisect bad # 当前版本有问题
git bisect good v1.2.0 # 已知正常的 tag/commitGit checkout 中间节点,测试后标记 git bisect good 或 git bisect bad,重复直到定位第一个 bad Commit。
结束并回到原分支:
git bisect reset有自动化测试时:git bisect run npm test(exit 0 = good,非 0 = bad)。这也是 Atomic Commit 很有价值的又一原因。
13git worktree:同时打开多个 Branch
传统工作流一次只能 checkout 一个 Branch;git worktree 允许同一 Repository 拥有多个 Working Tree:
git worktree add ../Game-feature feature
git worktree add ../Game-hotfix hotfix尤其适合:主功能做到一半突然修 Hotfix、同时编译两个 Branch、多个 Coding Agent 各占目录互不干扰。
14冲突:为什么会冲突,怎么解
冲突发生在同一段内容的同一区域,两个 Branch 改成了不同样子,Git 无法自动决定保留哪边:
<<<<<<< HEAD
const MAX = 100;
=======
const MAX = 256;
>>>>>>> feature-xgit add <file>git merge --continue 或 git rebase --continue减少冲突:小步 Commit、频繁同步 main;大重构单独 Branch;约定模块 ownership。
15Fetch、Pull 与 Force Push
git fetch origin只把远程新信息拿回来(更新 origin/main 等 remote-tracking 分支),不修改当前 Branch 和工作区。重要操作前先 fetch,看清远程发生了什么,再决定 merge 还是 rebase。
16一个简单而实用的 Git 工作流
对个人项目和小团队,不必把 Git Flow 搞得极其复杂:
main 创建 Feature Branchgit add -p)git fetch + rebase main,git rebase -i 整理历史中途被叫去修 Bug?git stash 寄存当前改动,修完再 stash pop 回来。
Release / Develop 等长期 Branch 是否需要,应根据项目规模决定——越简单、越清晰、越容易执行的流程,通常越好。
17几条值得长期遵守的原则
收束
- 先理解 Commit Graph,再学 Git 命令。
- Branch 很廉价,大胆创建。
- 一个 Commit 尽量表达一个完整意图(Atomic Commit)。
- 自己的历史可以整理,共享历史不要随意改写。
- 公共历史出错优先 Revert,而不是 Reset。
- 用
git add -p控制 Commit 边界。 - 出事故时先看
git reflog。 - 需要同时工作在多个 Branch 时用
git worktree。 - Force Push 优先
--force-with-lease,且避开公共 Branch。 - 不要把 Git 当成只会
add → commit → push的上传工具。
Git 的命令很多,但底层模型并没有想象中那么复杂。大多数操作都可以归结为三件事:创建新的 Commit、移动 Ref、重新组织 Commit Graph。当这个模型真正建立起来之后,Git 才会从一个需要死记命令的工具,变成一个可以预测其行为的版本控制系统。
18延伸阅读
- Pro Git(中文版)
- Conventional Commits
git help <command>— 本地最权威的说明
CONTINUE EXPLORING