Elytra
BlogGit

Git 核心概念与实用技巧

从 Commit、Branch 和 HEAD 开始理解 Git 的工作模型,并介绍 Merge、Rebase、Cherry-pick、Reflog、Worktree 等日常开发中真正实用的技巧。

教程
GPT 5.6+1
gitversion-controlrebasemergecherry-pickworktreereflogbisectstash

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 哈希
Loading diagram…

发生分支后,图会从一条线变成 DAG(有向无环图):

Loading diagram…

思维转变

不要把 Git 想象成「保存了很多文件版本」,而应把它想象成一张不断增长的 Commit 图。理解这一点之后,Merge、Rebase、Cherry-pick,甚至 Reset 都会直观很多。

2Working Tree、Index 与 Repository

Git 初学者最容易困惑的是:git add 到底在干什么?

一个文件从修改到进入历史,经过三个区域:

Loading diagram…
区域含义常见命令
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 -p

Git 会逐块展示 diff,让你决定哪些修改进入下一次 Commit。同一文件里同时修了 Bug、格式化了代码、写了未完成的新功能?完全可以拆成多个 Atomic Commit,而不是 git add . 一锅炖。

3Branch 其实只是一个指针

很多人第一次接触 Branch,会把它想象成「Git 复制了一份完整项目」。实际上没有。

Loading diagram…

创建 git switch -c feature 只是多了一个指向同一 Commit 的名字;在 feature 上继续提交,才会分叉:

Loading diagram…

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 分出,各自前进:

Loading diagram…

在 main 上 git merge feature 后,常见结果是产生 Merge Commit M,它拥有两个父节点——真实分支历史被保留:

Loading diagram…
  • Fast-forward:main 直接追上 feature,历史仍是一条直线
  • 三方合并:出现 merge commit,图上能看到功能分支从哪分出、在哪汇合

6Rebase 到底做了什么?

同样的起点,在 feature 上执行 git rebase main:

Loading diagram…

关键: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

7Cherry-pick:只拿走一个 Commit

并不希望合并整个 Branch,只想要其中一个修改:

Loading diagram…
git cherry-pick F

Git 把 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    ← 最好拆成三个 Commit

Atomic 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}        # 应用但保留 stash

11git reflog:Git 的后悔药

手滑 git reset --hard 后,B、C 看起来「消失」了——但很多情况下:

git reflog

仍能看到 HEAD 曾经指向的位置:

HEAD@{0}  reset: moving to A
HEAD@{1}  commit: C
HEAD@{2}  commit: B
git reset --hard HEAD@{1}   # 回到 C
误操作常见恢复
错 reset --hardgit 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——二分查找大约只需 log⁡2(1000)≈10\log_2(1000) \approx 10 次判断:

标记范围:

git bisect start
git bisect bad              # 当前版本有问题
git bisect good v1.2.0      # 已知正常的 tag/commit

Git 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
→ main
→ feature

尤其适合:主功能做到一半突然修 Hotfix、同时编译两个 Branch、多个 Coding Agent 各占目录互不干扰。

14冲突:为什么会冲突,怎么解

冲突发生在同一段内容的同一区域,两个 Branch 改成了不同样子,Git 无法自动决定保留哪边:

<<<<<<< HEAD
const MAX = 100;
=======
const MAX = 256;
>>>>>>> feature-x
打开文件,理解两边意图
手工改成最终结果,删掉冲突标记
git 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 搞得极其复杂:

Loading diagram…
从最新 main 创建 Feature Branch
开发并提交 Atomic Commits(善用 git add -p)
必要时 git fetch + rebase main,git rebase -i 整理历史
Push → Pull Request → Review / CI → Merge → 删除 Feature Branch

中途被叫去修 Bug?git stash 寄存当前改动,修完再 stash pop 回来。

Release / Develop 等长期 Branch 是否需要,应根据项目规模决定——越简单、越清晰、越容易执行的流程,通常越好。

17几条值得长期遵守的原则

收束

  1. 先理解 Commit Graph,再学 Git 命令。
  2. Branch 很廉价,大胆创建。
  3. 一个 Commit 尽量表达一个完整意图(Atomic Commit)。
  4. 自己的历史可以整理,共享历史不要随意改写。
  5. 公共历史出错优先 Revert,而不是 Reset。
  6. 用 git add -p 控制 Commit 边界。
  7. 出事故时先看 git reflog。
  8. 需要同时工作在多个 Branch 时用 git worktree。
  9. Force Push 优先 --force-with-lease,且避开公共 Branch。
  10. 不要把 Git 当成只会 add → commit → push 的上传工具。

Git 的命令很多,但底层模型并没有想象中那么复杂。大多数操作都可以归结为三件事:创建新的 Commit、移动 Ref、重新组织 Commit Graph。当这个模型真正建立起来之后,Git 才会从一个需要死记命令的工具,变成一个可以预测其行为的版本控制系统。

18延伸阅读

CONTINUE EXPLORING

沿着这篇文章继续

在地图中查看

On this page