git是基于快照和对象图的版本控制系统:工作区是可编辑文件目录,暂存区(.git/index)登记待提交变更,版本库(.git目录)存储所有commit、tree、blob对象及分支指针。

git 不是“保存多个文件副本”的工具,也不是“自动备份文件夹”的替代品——它是一套基于快照(snapshot) 和对象图(object graph) 的版本控制系统。理解这点,才能避开绝大多数初学者踩坑的起点。
git 为什么不用“差异存储”而用“快照”
Git 每次git commit,不是记录“这个文件比上一次多了三行、删了一行”,而是对工作目录当前所有已暂存的文件,生成一个完整快照,并把快照内容按 SHA-1 哈希后存为 blob 对象。
这意味着:
- 即使你只改了一个字节,Git 也会为该文件生成新的
blob(但未修改的文件会复用旧blob引用,不重复存储) -
git log -p看到的“差异”,是 Git 在读取两个快照后现场计算出来的,不是预先存好的 - 所以
git checkout或git reset极快:它只是把指定快照对应的所有blob写回工作区,不涉及逐行打补丁
常见误解:以为 git diff 是在查“历史存档里的差量”,其实它根本没存差量——它是在比对两个任意树对象(tree)里各 blob 的内容。
工作区、暂存区、版本库三者到底谁管什么
这不是三个“文件夹”,而是三种**状态视角**:- 工作区(Working Directory):你看到的、能直接编辑的那些文件
- 暂存区(Staging Area / Index):一个临时中转站,由
git add把工作区的变更“登记”进来,准备打包成下一次提交;它本质是一个二进制文件(.git/index),记录了“下次 commit 要包含哪些 blob + 路径 + 权限” - 版本库(Repository):即
.git目录,里面存着所有commit、tree、blob、tag对象,以及分支指针(如refs/heads/main)
容易出错的点:
-
git commit只提交暂存区内容,和工作区当前是否干净无关 -
git checkout main会重置工作区 + 暂存区,但git reset --hard main效果一样,区别在于后者还移动 HEAD 指针 -
git add -u只登记已跟踪文件的修改/删除,不会把新文件加进来;git add .会递归加入所有(包括未跟踪的新文件)
裸仓库(bare repo)为什么不能有工作区
git init --bare 创建的仓库,内部结构只有 objects/、refs/、HEAD 等,没有 worktree,也没有 index。
这是因为:
- 它专为远程协作设计,只负责收发对象,不做开发
- 没有工作区 → 不可能执行
git status、git add、git commit这类需要操作文件系统的命令 - 所有推送(
git push)来的commit,只是被写入objects/并更新refs/heads/xxx指针,不触发任何文件检出
如果你误在裸仓库里运行 git checkout,会报错:fatal: this operation must be run in a work tree。
分布式 ≠ 多个完全一样的副本
每个克隆(git clone)确实包含全部提交历史,但:
- 分支指针(
refs/heads/*)是本地的,远程分支(如origin/main)只是对远程refs/heads/main的最后一次快照引用,不是实时同步的镜像 -
git fetch只更新远程引用(origin/*),不改变本地分支或工作区 -
git pull = git fetch + git merge,但 merge 行为取决于当前分支设置(merge.ff、rebase等),不是简单“拉过来覆盖”
最常被忽略的一点:你本地的 main 分支和远程的 main 分支,从 Git 角度看是两个完全独立的指针,即使它们指向同一个 commit —— 同名不等于同源,靠的是人为约定和协作规范来对齐。
快照模型、三层状态分离、裸仓无工作区、远程引用非实时镜像——这四点不厘清,后面所有分支策略、rebase 冲突、reflog 恢复,都会变成玄学操作。











