Git 是分布式版本控制的事实标准,但多数人只会背命令,遇到冲突和误操作就慌。其实 Git 的所有命令都在操作同一套简单模型:三个区、三种对象、一堆指针。建立这套模型,比记一百条命令更有用。

为什么是分布式版本控制

版本控制系统的发展大致是三步:本地版本控制(靠复制目录或本地数据库记录差异)→ 集中式版本控制(CVCS,如 SVN,所有历史存在一台中央服务器上,客户端只取当前版本)→ 分布式版本控制(DVCS,如 Git)。

集中式的软肋很明显:中央服务器宕机,全员无法提交;中心磁盘损坏且没备份,历史全部丢失。分布式的解法是每个客户端都克隆完整的仓库——包括全部提交历史。任何一台机器挂了,随便找个克隆就能恢复。这也让 Git 的大部分操作(提交、看日志、切分支)都在本地完成,不依赖网络,速度极快。

三区模型:工作区、暂存区、仓库

日常操作只跟三个「区」打交道:

  • 工作区(Working Directory):你实际看到和编辑的文件。
  • 暂存区(Staging Area / Index):一个待提交清单,git add 把修改放进来。它让你可以精确控制「这次提交包含哪些改动」,而不是把一揽子修改全提交。
  • 本地仓库(Repository):.git 目录里保存的提交历史,git commit 把暂存区的内容固化成一个提交。

推送到远程仓库是第四步:git push 把本地提交同步到 GitHub 等远端。git status 本质上就是在汇报这三个区之间的差异,看不懂输出时先想清楚「哪个区和哪个区在比」。

对象模型直觉:blob、tree、commit

Git 不存「文件差异」,而是存内容快照,所有数据都是内容寻址的对象(以内容的 SHA-1 哈希为键),核心就三种:

  • blob:一个文件的内容。两个文件内容相同,在 Git 里只存一份。
  • tree:一个目录,记录文件名到 blob 或子 tree 的映射。
  • commit:一次提交,指向一个根 tree(即该时刻整个项目的快照),并记录父提交、作者、时间戳和提交信息。

提交通过父指针串成一条有向无环图——这就是「提交历史」的真相。理解这一点后,很多「魔法」都不再神秘:git commit 只是新建一个 commit 对象;哈希相同意味着内容完全一致;Git 天然去重,所以仓库比想象中省空间。

分支的本质:一个指针

分支不是什么代码副本,只是一个指向某个 commit 的可移动指针,创建分支就是写一个 41 字节的文件(40 位哈希 + 换行)。这就是 Git 分支「秒建、秒切、秒删」的原因——不像 SVN 那样复制整个目录树。

还有一个特殊指针叫 HEAD,指向当前所在的分支(或 commit)。提交时:新建 commit 对象 → 当前分支指针前移 → HEAD 跟着分支走。git checkout -b feature 等价于「新建指针 + HEAD 转过去」,两步都是改指针,所以瞬间完成。

merge vs rebase:场景化选择

分支合并有两条路,行为差异决定了适用场景:

  • merge:新建一个合并提交,把两条历史汇合。历史保留分叉结构,忠实记录「这些工作是并行做的」。git merge feature 不改写任何已有提交,绝对安全。
  • rebase:把当前分支的提交「摘下来」,逐个重放到目标分支最新提交之后。历史变成一条直线,干净易读;但重放会生成新的 commit 对象(哈希变了),等于改写了历史。

选择原则一句话:已推送(共享)的提交不 rebase,未推送的本地提交放心 rebase。典型工作流:

# 本地功能分支开发期间,同步主干最新代码:用 rebase 保持线性
git checkout feature
git rebase main

# 功能完成后合回主干:用 merge 保留一次清晰的合并记录
git checkout main
git merge --no-ff feature

变基过程中断想放弃:git rebase --abort;合并到一半想反悔:git merge --abort。

reset、revert、checkout 辨析

三个「撤销系」命令最容易混:

  • git reset:移动分支指针(回退历史)。三种模式:--soft 只动指针,改动留在暂存区;--mixed(默认)指针回退且清暂存区,改动留在工作区;--hard 连工作区一起清掉,未提交的改动直接丢失,慎用。reset 会改写历史,已推送的提交不要 reset。
  • git revert:不改写历史,而是新建一个「反向提交」来抵消指定提交的效果。因为只是新增提交,它是撤销已推送提交的唯一安全方式。
  • git checkout / git restore / git switch:在「区」之间搬运或切换。git checkout -- file(新语法 git restore file)丢弃工作区改动;git restore --staged file 把文件移出暂存区;git switch branch 切分支。

撤销操作决策树

遇到「想撤销」,按两个问题定位:

  1. 改动提交了吗?
    • 没提交,想丢弃工作区修改:git restore <file>。
    • 没提交,但已 add 进暂存区:git restore --staged <file>(退回工作区)。
  2. 提交推送了吗?
    • 没推送,想撤销最近提交但保留改动:git reset --soft HEAD~1(改动回暂存区)或 git reset --mixed HEAD~1(改动回工作区)。
    • 没推送,想彻底丢弃最近提交:git reset --hard HEAD~1。
    • 已推送:git revert <commit>,生成反向提交后正常 push。千万别对已推送历史 reset 后强推,除非你确定没有别人基于它工作。

误删了提交也别慌:git reflog 记录 HEAD 的每一次移动,找到丢失提交的哈希,git reset --hard <hash> 就能找回来——提交对象在仓库里会留存相当长的时间。

小结

  • Git 是分布式系统:每个克隆都是完整备份,大部分操作离线完成。
  • 三区模型:工作区 →(add)→ 暂存区 →(commit)→ 仓库 →(push)→ 远程。
  • 对象模型:commit 指向 tree,tree 指向 blob,历史是 commit 组成的有向无环图。
  • 分支和 HEAD 都是指针,这是 Git 轻量分支能力的根源。
  • merge 保历史、rebase 求线性;共享提交不 rebase。
  • 撤销先问「提交了吗、推送了吗」:未提交用 restore,未推送用 reset,已推送用 revert,误删用 reflog 找回。