从内存与磁盘出发,彻底搞懂 Git 的存储原理
从内存与磁盘出发,彻底搞懂 Git 的存储原理
导读:各个社区里讲解 Git 的文章有很多,往往过于深入底层原理,劝退一众初学者,对小白友好的文章又大多以书桌、柜子等来类比,虽简单易懂但过渡并不自然。我认为其实如果你明白“内存是临时工作区、磁盘是永久存储区”,就能很容易理解 Git。这篇文章就用内存与磁盘的类比,带你彻底搞懂 Git 到底是什么。
前言:为什么是“内存与磁盘”?
刚翻开 Git 教程时,很多人会在“概念海”中晕头转向——工作区、暂存区、本地仓库、远程仓库、commit、push、pull、merge、rebase……
其实,Git 的设计思路和计算机最基本的存储架构如出一辙:
| 计算机 | Git |
|---|---|
| 内存(RAM) | 工作目录(Working Directory) |
| 剪贴板 | 暂存区(Staging Area) |
| 本地硬盘 | 本地仓库(Local Repository) |
| 网盘 | 远程仓库(Remote Repository) |
从 Git 视角,未提交的修改等同于未持久化的内存数据。
※ 提前说明:这个类比是从 Git 的视角出发的。相对于 Git 自己的数据库(
.git/目录),工作目录中的文件就像“未保存的内存”——你任意修改,但 Git 不会认为它们是“已提交的版本”。至于你的工作目录文件实际存在操作系统磁盘上,那是另一个层面的事,不影响我们理解 Git 的存储层次。后面第九部分会专门讨论这个类比的边界。
Git 的四层架构和计算机的存储层次结构有着惊人的结构相似性。Git 的创造者 Linus Torvalds 也是 Linux 内核的开发者,长年与操作系统存储体系打交道——这种分层缓冲的思维模式,很可能潜移默化地影响了他。当然,Git 的每个设计都有其具体的工程动机,如:性能、完整性、分布式协作等(我们知道 Linux 内核有上万个贡献者,需要离线工作和快速合并),但这种相似性对于我们理解 Git 来说,是一个非常有用的思维桥梁。我认为理解了这一点,Git 就不再是散乱如沙的命令,而是一个顺理成章的存储系统。
一、我们每天都在用的“内存”和“磁盘”
1.1 内存(RAM):速度快但短暂
内存是电脑用来临时存放正在处理的数据的地方。它的特点是:
- 速度快:CPU 可以直接读写,延迟极低
- 容量有限:通常 8GB、16GB、32GB
- 价格:高昂
- 断电即失:关机或断电后,所有数据都会消失
你打开一个 Word 文档开始写论文或者工作报告,这些文字就存在内存里。如果不幸的是,你忘记保存电脑没电关机了,那文字就都没了——因为它们没有被正式存储下来,能正式存储数据的就是磁盘。
1.2 磁盘(SSD/HDD):速度慢但“永恒”
磁盘是用来永久存储数据的地方。它的特点是:
- 速度慢:比内存慢几个数量级
- 容量大:通常 256GB、512GB、1TB 甚至更大
- 价格:便宜
- 持久保存:断电后数据不会丢失
当你点击 Word 的「保存」按钮时,数据就从内存写入了磁盘。即使关机,下次打开文件还在。
1.3 一个关键操作:保存
在内存和磁盘之间,有一个最核心的操作——保存(Save):
内存(编辑中)─── 保存 ───▶ 磁盘(永久存储)
这个“保存”操作做了两件事:
- 把内存中的数据复制一份到磁盘
- 在磁盘上创建一个不可变的快照(文件)
这个模式很重要,因为 Git 的核心操作 git commit 本质上也是一次“保存”。
二、Git 的四层存储架构
Git 的存储系统可以分为四个区域,它们和计算机存储体系有着非常匹配的对应关系。
2.1 示意图
2.2 四个区域详解
第一层:工作目录 ≈ 内存(RAM)
像内存一样,工作目录是你正在工作的地方。你在里面新建文件、修改代码、删除内容——所有操作都是实时的、即时的,但也都是临时的。
关键:
- 文件状态是最新的
- 修改不会自动保存——如果你不做任何 Git 操作就关闭了编辑器,修改仍然在文件里(因为操作系统帮你保存到了磁盘),但 Git 不知道你改了什么
# 你在"内存"中修改了文件
vim 你的文件名
# 此时 Git 还不知道你改了
# 就像你在内存中改了数据,但还没点"保存"
注:工作目录就是你能直接看到的项目文件夹。
第二层:暂存区 ≈ 剪贴板 / 缓存区
这是一个中间缓冲地带,用来标记你的哪些修改准备提交。就像剪贴板——你复制了一段内容到剪贴板,确认无误后再粘贴到目标位置。
它的存在是非常重要的,比如你同时修改了 3 个文件,但这时领导说哪些改好了先交上去看一看,此时只有其中 1 个修改完了,另外 2 个还待补充。如果没有暂存区,你只能把目前的文件同时提交,要么先不提交等最后做完一起交,没有中间选项,而暂存区给了你选择权,就可以只选择提交 1 个文件了,等领导看完那个文件,这时你就已经把另外两个改完了,过程十分丝滑顺畅。
# 只把 file1.py 加入暂存区(准备提交)
git add file1.py
# file2.py 和 file3.py 的修改留在工作目录(不提交)
关键:
- 暂存区仍然在“内存层面”——从类比角度它是临时的中间状态。(注:暂存区实际存储在
.git/index文件中,后文 5.4 节会详述。) - 你可以反复
git add,每次都会更新暂存区的快照 git status命令就是对比「工作目录」和「暂存区」的差异
# 把修改从"内存"复制到"缓存区"
git add src/main.py
# 查看状态:哪些在缓存区(绿色),哪些还在内存(红色)
git status
注:
git add不是添加新文件的意思,而是把这个文件的修改加入待提交清单,可以理解为把这个东西先放进购物车,之后买不买再说。
第三层:本地仓库 ≈ 本地磁盘
打开文件资源管理器到你git过的项目文件夹中,你能看到.git/ 目录下的所有内容,是 Git 的核心数据库,存储了项目的完整版本历史。
就像磁盘是你的电脑的永久存储一样,本地仓库是 Git 的永久存储。每次 git commit,就相当于一次保存到磁盘的操作。
关键:
- 数据是持久化的——即使你删除了整个项目文件,只要
.git目录还在,所有历史记录都还在 - 每次 commit 都会生成一个不可变的快照(就像磁盘上保存的文件,除非你主动删除,否则不会改变)
- commit 是原子操作——要么完整保存,要么不保存,不会出现“保存了一半”的情况
# 把暂存区的内容"保存到磁盘"
git commit -m "feat: 添加用户登录功能"
这个命令到底做了什么?
- 读取暂存区中的文件快照
- 为每个文件生成一个唯一的哈希值(SHA-1)
- 将文件内容压缩后存入
.git/objects/目录 - 创建一个 commit 对象,记录:谁提交的、什么时候提交的、提交信息是什么、上一个 commit 是谁
- 更新分支指针(比如
main)指向这个新的 commit
这就像你点击 Word 的“保存”按钮后,操作系统做了这些事:
- 读取内存中的文档内容
- 将数据写入磁盘的特定扇区
- 更新文件的修改时间
- 更新文件系统的目录索引
注:
git commit其实就是 Git 世界的“Ctrl+S”。但和 Word 不同的是,Git 每次保存都会创建一个新版本,而不是覆盖旧版本,所以这意味着你可以随时回到任何一个历史版本。
第四层:远程仓库 ≈ 网盘
远程仓库就是你托管在 GitHub、GitLab、Gitee 等服务器上的仓库副本。
就像百度网盘、夸克网盘等一样,远程仓库是你的代码的云端备份和团队共享空间。
关键:
- 不在你本地电脑上——即使你的电脑坏了,代码还在
- 可以多人同时访问——团队成员可以各自从远程仓库拉取代码、推送修改
- 是协作的核心——没有远程仓库,Git 就只是一个本地的版本控制工具
# 把本地"磁盘"的数据同步到"网络磁盘"
git push origin main
# 从"网络磁盘"同步最新数据到本地
git pull origin main
注:
push—— 传,pull—— 取
三、一次完整的 Git 工作流
接下来我将用一个完整的例子说明具体要怎么操作。
3.1 使用 winget 命令下载安装 Git (推荐)
winget 是微软官方的 Windows 包管理器,在 Windows 10 1809 版本及更高版本中已经内置了,无需额外安装。
打开 CMD (命令提示符) 或 PowerShell,输入以下命令并回车:
winget install --id Git.Git -e --source winget
执行后,winget 就会自动下载并安装 Git。等待安装完成,然后重启 CMD 就可以正常使用了。
注意:
winget默认安装在C:\Program Files\Git路径下。
3.2 在 Gitee 上新建仓库 (其他平台同理)

-
新建仓库,填写仓库名称,确定路径(网页会自动生成与你的仓库名称相关的内容)等。
-
选择公开还是私有
⚠️不想开源公布一定记得选择私有!!!
-
最后三个选项根据需要选择即可。
-
点击创建。

根据需要选择 HTTPS 方式或SSH方式。
这两种方式就像是去仓库取东西时的两种不同“通行证”。
简单来说:SSH 更方便(一劳永逸),HTTPS 更通用(但需要密码或令牌)。
以下是详细的对比:
🔑 SSH(推荐:一劳永逸)
- 原理:你在电脑上生成一对“钥匙”(公钥和私钥)。你把公钥给 Gitee 保管,私钥留在自己电脑里。
- 体验:
- 免密操作:配置好后,你每次
git push或git pull时,Git 会自动用私钥验证身份,不需要输入密码。 - 更安全:基于密钥加密,比密码更难破解。
- 免密操作:配置好后,你每次
- 缺点:第一次配置稍微麻烦一点(需要生成密钥并复制到 Gitee 设置里)。
- 地址格式:通常以
git@开头,例如:git@gitee.com:lynli/my-warehouse.git
🔒 HTTPS(通用:每次都要验证)
- 原理:就像登录网页一样,每次操作都需要提供账号和密码。
- 体验:
- 需要验证:每次推送代码时,Git 都会弹窗让你输入用户名和密码(或者私人令牌)。
- 容易受阻:现在很多平台(包括 Gitee 和 GitHub)为了安全,已经不再支持直接用“登录密码”操作了,必须使用“私人令牌”代替密码,这有时候会让人困惑。
- 优点:不需要配置 SSH Key,在任何电脑上只要有账号密码(或令牌)就能直接克隆代码。
- 地址格式:通常以
https://开头,例如:https://gitee.com/lynli/my-warehouse.git
❓ 该怎么选?
强烈建议你选择 SSH。
虽然第一次配置需要多花 2 分钟,但以后你再也不用担心输错密码、令牌过期或者每次都要弹窗输入密码的烦恼了。
3.3 在电脑终端里配置你的远程仓库
将你打算提交的本地电脑的目录下点击鼠标右键选择 Open git bash here。
进行以下操作:
-
- 配置用户名
git config --global user.name "username"
-
- 配置邮箱
git config --global user.email address@gmail.com
注意:
如果用户名包含空格(如Zhang San),需要加引号;否则可加可不加。
邮箱地址通常不含空格,一般不需要加引号。
-
- 初始化当前目录为 Git 仓库
git init
-
- 更新远程地址
根据 3.2 中你选择的 HTTPS 方式或 SSH 方式进行远程连接。
如果你决定使用 SSH:
只需要在 Git Bash 里运行这行命令,把远程地址换一下即可:
git remote add origin git@gitee.com:lynli/my-warehouse.git
(注意:把后面 @ 那一串换成你的 SSH 地址)
-
- 将当前内容全部提交到暂存区
git add .
-
- 将当前暂存区内容提交到本地仓库
git commit -m "这里可以备注你的提交内容/更新内容"
-
- 将当前本地仓库内容提交到远程仓库
git push -u origin main
# 或
# git push origin master
Gitee 等平台新建仓库的默认分支名已逐渐从
master改为main。若远程是main,而本地是master,直接推送可能失败或创建多余分支。
可以先用git branch -M main重命名本地分支,或根据远程实际分支名推送。
到目前为止,你的代码已经经历了完整的四层旅程:
内存(当前目录下已编辑好的文件)→ 缓存(git add)→ 磁盘(git commit)→ 网络磁盘(git push)
如果后续增加了文件内容,可以重复上方 5、6、7代码继续提交。
3.4 一个更复杂的场景:选择性提交
你同时改了 3 个文件,但只想提交其中 2 个:
# 修改了三个文件
vim index.js # 修改了登录逻辑(已完成)
vim style.css # 修改了样式(已完成)
vim config.js # 修改了配置(还没改完,不想提交)
# 只把完成的两个文件加入缓存
git add index.js style.css
# 确认状态
git status
# 输出:
# Changes to be committed: ← 在缓存区(绿色,会提交)
# index.js
# style.css
# Changes not staged for commit: ← 还在内存(红色,不提交)
# config.js
# 提交(只有缓存区的两个文件会被"保存到磁盘")
git commit -m "feat: 完成登录和样式修改"
这就是暂存区存在的意义——让你精确控制每次提交的内容。
四、反向操作——从磁盘恢复到内存
理解了保存,我们再来看看恢复。这也是很多新手容易困惑的地方。
4.1 撤销工作目录的修改:从磁盘覆盖内存
# 你在 index.js 里改了一堆东西,改崩了,想恢复到上次 commit 的状态
git restore index.js
小贴士:你可能在网上看到过
git checkout -- <file>的写法。Git 2.23(2019 年)将git checkout拆分成了两个语义更清晰的命令:
git restore <file>:恢复工作目录中的文件(替代git checkout -- <file>)git switch <branch>:切换分支(替代git checkout <branch>)
git checkout至今仍然可用(兼容旧版本),但推荐使用新命令,语义更明确,不容易混淆。本文后续统一使用新命令。
发生了什么? Git 从本地仓库(磁盘)中取出上次保存的版本,覆盖工作目录(内存)中的文件。
本地仓库(磁盘)─── 覆盖 ───▶ 工作目录(内存)
就像你在 Word 里改崩了,选择“不保存”关闭文件,然后重新打开——文件就恢复到了上次保存的状态。
⚠️ 注意:这个操作会丢弃工作目录中的所有未提交修改,不可恢复!
4.2 撤销暂存区的修改:从缓存退回内存
# 你 git add 了,但后悔了,想从缓存区撤出来
git restore --staged index.js
# 或者旧版写法
git reset HEAD index.js
发生了什么? 文件的修改从暂存区(缓存)退回到工作目录(内存),但修改内容本身不会丢失——只是不再被标记为“准备提交”。
暂存区(缓存)─── 退回 ───▶ 工作目录(内存)
就像你把一段文字复制到了剪贴板,但后来觉得不对,又清空了剪贴板——文字本身还在原文档里,只是剪贴板里没了。
4.3 撤销 commit:从磁盘退回缓存或内存
这是三种不同程度的“撤销保存”:
# 方式一:软撤销 —— commit 撤销,但修改保留在缓存区
git reset --soft HEAD~1
# 方式二:混合撤销(默认)—— commit 撤销,修改退回工作目录(未暂存)
git reset --mixed HEAD~1
# 等同于
git reset HEAD~1
# 方式三:硬撤销 —— commit 撤销,修改全部丢弃 ⚠️ 危险!
git reset --hard HEAD~1
用内存/磁盘的类比来理解:
| 命令 | 类比 | 修改保留在哪? |
|---|---|---|
--soft |
撤销了「保存」操作,但文件还在「剪贴板」里 | 暂存区(缓存) |
--mixed |
撤销了「保存」操作,文件退回「编辑器」里,但没放在剪贴板 | 工作目录(内存,未暂存) |
--hard |
撤销了「保存」操作,并且清空了编辑器 | 哪都不在,彻底丢失 |
⚠️ 注意:
git reset --hard是最危险的操作之一!它会永久删除你的修改。在使用之前,请确保你真的不需要这些修改了。
补充:如果已经 push 到远程了怎么办?
如果你已经把 commit 推送到了远程仓库,并且别人可能已经基于它工作了,不要用 git reset(因为会改写历史,导致队友混乱)。这时候应该用 git revert:
# 创建一个"反向修改"的新 commit,安全地撤销某个 commit
git revert <commit-hash>
git push origin main
git revert 不会删除任何历史,只是增加一个新的 commit 把改动撤销回去。这是团队协作中最安全的做法。
4.4 git stash:内存休眠
有时候你正在开发一个功能,改了一半,突然需要切换到另一个分支去修一个紧急 bug。但你的修改还没完成,不想 commit,也不想丢弃。怎么办?
# 把当前工作目录和暂存区的修改"冻结"
git stash
# 输出:Saved working directory and index state WIP on main
# 此时工作目录是干净的,你可以自由切换分支
git switch bugfix/urgent-fix
# ... 修完 bug,提交 ...
# 切回来,"唤醒"之前冻结的修改
git switch main
git stash pop
# 输出:Dropped refs/stash@{0} (abc123...)
类比:git stash 就像你把电脑休眠了——内存中的所有数据被保存到磁盘上的一个临时文件,电脑关机后可以随时恢复到休眠前的状态。
工作目录 + 暂存区 ── stash ──▶ .git/refs/stash(临时磁盘空间)
│
stash pop
▼
工作目录 + 暂存区(恢复原状)
五、深入 Git 底层——磁盘上到底存了什么?
前面我们用类比理解了 Git 的操作逻辑,现在让我们打开 .git 目录,看看“磁盘”上到底存了什么。
5.1 .git 目录结构
your-project/
├── .git/
│ ├── objects/ ← 所有数据的"磁盘扇区"(核心存储)
│ ├── refs/ ← 分支和标签的"指针"
│ │ ├── heads/ ← 本地分支
│ │ └── remotes/ ← 远程分支
│ ├── HEAD ← 当前所在分支的指针
│ ├── index ← 暂存区(二进制文件)
│ ├── config ← 仓库配置
│ └── logs/ ← 操作日志
├── src/
├── README.md
└── ...
5.2 objects 目录:Git 的「硬盘扇区」
.git/objects/ 是 Git 存储所有数据的地方。每个文件、每次提交、每个目录结构,都会被存储为一个对象(Object),用 SHA-1 哈希值作为唯一标识。
注:Git 目前仍默认使用 SHA-1,但正在向更安全的 SHA-256 迁移。文章对此不做深入讨论。
Git 有三种核心对象类型:
Blob 对象:文件内容
Blob(Binary Large Object)存储的是文件的内容,不包含文件名。
# 查看某个文件对应的 blob 对象
git hash-object src/main.py
# 输出:a1b2c3d4e5f6...(40 位 SHA-1 哈希)
# 这个哈希值对应的文件存储在
# .git/objects/a1/b2c3d4e5f6...
SHA-1 的输出是 160 位(20 字节),用十六进制表示就是 40 个字符。
关键特性:Git 是内容寻址的——相同内容的文件,无论文件名是什么,都只存储一份。这意味着如果你把一个文件复制了 100 份(内容相同),Git 只会存储一个 blob 对象。
就像磁盘上的文件去重功能——系统发现两个文件内容完全一样,就只存一份,用两个指针指向它。
Tree 对象:目录结构
Tree 对象存储的是目录结构——一个目录下有哪些文件,以及它们对应的 blob 或 tree。
tree (abc123)
├── blob (def456) → "README.md" 的内容
├── blob (789abc) → "index.js" 的内容
└── tree (def789) → src/ 目录
├── blob (111222) → "main.py" 的内容
└── blob (333444) → "utils.py" 的内容
你可以把 tree 想象成文件系统中的文件夹——它记录了目录下有哪些条目,以及每个条目指向哪里。
Commit 对象:提交记录
Commit 对象是 Git 版本控制的核心——它把一切串联起来。
commit (xyz789)
├── tree: abc123 ← 指向当前版本的目录结构
├── parent: aaa111 ← 指向上一个 commit(形成链表)
├── author: 张三 ← 谁写的
├── committer: 张三 ← 谁提交的
└── message: "feat: 添加登录功能" ← 提交信息
关键:每个 commit 都指向一个 tree(目录结构快照),同时指向上一个 commit(parent)。这样,所有的 commit 就形成了一条链表——这就是 Git 的「版本历史」。
commit3 (最新) ──→ tree3 ──→ blob3a, blob3b, tree3c
│
└── parent ──→ commit2 ──→ tree2 ──→ blob2a, blob2b
│
└── parent ──→ commit1 ──→ tree1 ──→ blob1a
就像磁盘上的文件版本——每次保存都创建一个新版本,但旧版本不会被删除,你可以随时回溯。
5.3 HEAD 和分支:指针的指针
HEAD 是一个特殊的指针,它指向你当前所在的分支。
HEAD ──→ main ──→ commit3 (最新的 commit)
当你切换分支时,HEAD 就指向另一个分支:
git switch develop
# HEAD ──→ develop ──→ commit5
分支本质上就是一个指向某个 commit 的可移动指针。当你创建新 commit 时,分支指针就自动向前移动:
# 当前在 main 分支,HEAD → main → commit3
git commit -m "new feature"
# 现在 HEAD → main → commit4(main 指针向前移动了)
用磁盘类比:HEAD 就像你当前打开的文件夹路径,分支就像书签——标记了你在版本历史中的位置。
5.4 .git/index:暂存区的真面目
暂存区不是一个抽象概念,它是一个真实的二进制文件——.git/index。你可以用 git ls-files --stage 命令查看它的内容:
git ls-files --stage
# 输出:
# 100644 a1b2c3d4... 0 README.md
# 100644 e5f6g7h8... 0 src/main.py
每一行记录了:
- 文件权限(100644 = 普通文件)
- blob 哈希值(指向 objects 目录中的具体对象)
- 暂存区编号(用于 merge 冲突)
- 文件路径
所以暂存区的本质就是一个清单——上面列出了:下次 commit 时应该包含哪些文件的哪个版本。
六、那些让人困惑的操作,用类比重新理解
6.1 git clone = 从网盘下载整个文件夹
git clone https://github.com/someone/project.git
这就像你从百度网盘下载了一个文件夹到本地。它会:
- 下载所有文件内容(blob 对象)
- 下载所有目录结构(tree 对象)
- 下载所有版本历史(commit 对象)
- 创建分支指针和 HEAD
所以 git clone 下载的不是当前版本的文件,而是整个仓库的完整历史。
6.2 git pull = 下载 + 合并
git pull origin main
git pull 其实是两个操作的组合:
git pull = git fetch + git merge
git fetch:从远程仓库下载最新的 commit 和对象,但不修改工作目录(就像从网盘下载了文件,但放在了「收件箱」,还没有打开)git merge:把下载的内容和当前工作目录合并(就像打开收件箱里的文件,和当前文档合并)
6.3 git merge = 把两份文档合并成一份
git merge feature-branch
当你和同事同时修改了同一个文件的不同部分,git merge 会尝试自动合并。如果修改了同一行,就会产生冲突(Conflict)——Git 不知道该保留哪个版本,需要你手动决定。
<<<<<<< HEAD(你的修改)
背景色:白色
=======
背景色:黑色
>>>>>>> feature-branch(同事的修改)
就像两个人同时编辑同一份 Word 文档的不同段落,Word 可以自动合并;但如果两个人改了同一段话,Word 就会让你手动选择保留哪个。
6.4 git rebase = 整理你的保存历史
git rebase main
rebase 的意思是变基——把你的 commit 历史重新排列,让它看起来像是在最新的 main 分支上依次提交的。
# rebase 前:
main: A ── B ── C
\
feature: D ── E
# rebase 后:
main: A ── B ── C
\
feature: D' ── E'(D 和 E 被重新创建,基于 C)
用磁盘类比:就像你有一份文档的历史版本 v1、v2、v3,然后你发现 v2 和 v3 其实是基于旧版本的。你可以把 v2 和 v3 的修改重新应用到最新版本上,得到一个更干净的历史。
⚠️ 注意:
rebase会改写历史(创建新的 commit 对象)。如果你已经 push 到远程仓库并且别人已经基于这些 commit 工作了,不要 rebase——否则会造成混乱。黄金法则:只对本地未 push 的 commit 做 rebase。
6.5 git log = 查看保存历史
git log --oneline --graph
# 输出:
# * abc1234 (HEAD -> main) feat: 添加深色模式
# * def5678 fix: 修复登录 bug
# * ghi9012 init: 初始化项目
这就像你查看 Word 文档的版本历史或操作系统的文件修改记录——每次保存都有一条记录,你可以看到谁在什么时候做了什么修改。
七、常见场景实战
场景一:改崩了,想回到上一个版本
# 查看历史,找到想回到的版本
git log --oneline
# 方式一:只是想看看旧版本(不修改分支,随时可以回来)
git checkout abc1234 # abc1234 是某个 commit 的哈希(此处用 checkout 是因为要进入"分离 HEAD"状态)
# 看完了,切回来
git switch main
# 方式二:确定要回到旧版本(会丢弃之后的修改)
git reset --hard abc1234
切换到某个历史 commit 时,只能用
git checkout <commit-hash>,因为git switch只接受分支名。Git 会提示你处于'分离 HEAD'状态——这意味着你不在任何分支上,你的新修改不会属于任何分支。
场景二:commit 信息写错了
# 修改最后一次 commit 的信息
git commit --amend -m "正确的提交信息"
这就像你保存了一个文件,然后发现文件名写错了——你可以重命名(修改 commit 信息),但文件内容不变。
场景三:提交到了错误的分支
# 你在 main 分支上做了修改并 commit 了,但其实应该在 feature 分支上
# 1. 创建 feature 分支并切换过去(把 commit 带走)
git branch feature
git switch feature
# 2. 把 main 分支退回到上一个 commit
git reset --hard HEAD~1
# 现在 feature 分支上有你的 commit,main 分支上没有了
场景四:不小心 push 了敏感信息
# 假设你不小心把密码 commit 并 push 了
# 1. 从历史中彻底删除这个文件
git filter-branch --force --index-filter \
'git rm --cached --ignore-unmatch config/passwords.yml' \
--prune-empty --tag-name-filter cat -- --all
# 2. 强制推送(覆盖远程历史)
git push origin --force --all
⚠️ 严重警告:一旦密码被 push 到公开仓库,即使你删除了,别人可能已经看到了。正确的做法是立即更换密码,而不是仅仅从 Git 历史中删除。
# 安装 git-filter-repo
pip install git-filter-repo
# 使用示例:从历史中彻底删除某个文件
git filter-repo --path config/passwords.yml --invert-paths
⚠️ 注意:
git filter-branch已被 Git 官方标记为过时,不过这个命令仍然有效,但推荐使用git-filter-repo工具。
八、Git 存储的几个“反直觉”事实
事实一:Git 存储的是快照,不是差异
很多人以为 Git 只存储每次修改的差异,但实际上,Git 默认存储的是完整的文件快照——每次 commit 都会保存当时每个文件的完整内容。
不过,Git 底层会通过 git gc 把松散对象打包为 packfile,使用差异压缩来节省磁盘空间。所以逻辑上是快照,物理上依然高效。
事实二:Git 几乎不会丢失数据
只要一个 commit 被创建了(即使你后来 reset 了),它的对象仍然存在于 .git/objects/ 中,直到垃圾回收清理它。你可以用 git reflog 找到所有曾经存在过的 commit:
git reflog
# 输出:
# abc1234 HEAD@{0}: reset: moving to HEAD~1
# def5678 HEAD@{1}: commit: 添加新功能
# ghi9012 HEAD@{2}: commit: 修复 bug
即使你 reset --hard 了,只要你知道 commit 的哈希值,就能恢复:
git checkout def5678 # 恢复到"丢失"的 commit
这就像你删除了一个文件,但它还在回收站里——只要没被清空,就能恢复。
事实三:分支只是指针,创建和删除几乎没有成本
创建一个分支只是在 .git/refs/heads/ 下创建了一个包含 40 字符哈希值的文件。所以 Git 的分支操作是瞬间完成的,无论项目有多大。
# 创建分支:只是写了一个 41 字节的文件
git branch feature-xyz
# 删除分支:只是删除了一个文件
git branch -d feature-xyz
事实四:.git 目录可能比你的项目文件还大
对于长期维护的项目,.git 目录可能非常大(几百 MB 甚至几 GB),因为它存储了完整的版本历史。这就是为什么 GitHub 有文件大小限制,也有 .gitignore 文件的存在——让你排除不需要版本控制的文件(如编译产物、依赖包等)。
九、类比的边界——“内存与磁盘”不能表示的地方
前面我们用“内存与磁盘”的类比把 Git 的核心概念讲了一遍。这个类比在宏观层面非常有效——它能帮你快速建立直觉,理解每个操作的本质。但到了细节层面,这个类比有些地方是不对应的。
你可以跳过这一节,不影响基本使用,但如果你好奇“为什么我的工作目录文件没丢”,这一节就是为你准备的。
9.1 不一致之一:工作目录的文件其实在磁盘上
你的项目文件夹(工作目录)里的文件,是操作系统帮你持久化到磁盘上的。即使你断电重启,文件还在——这和内存“断电即失”的特性完全不同。
从 Git 的视角来看,工作目录中的文件是游离在版本控制之外的——它们存在,但 Git 不认为它们是已保存的版本。只有 git add + git commit 之后,Git 才认可这个版本已被记录。
所以更准确的说法是:
工作目录 = 操作系统的磁盘,但相对于 Git 来说是“未保存的内存”
本地仓库 = Git 自己的磁盘(.git/objects/)
9.2 不一致之二:磁盘保存是覆盖式的,Git commit 是追加式的
当你用 Word 保存文件时,新版本会覆盖旧版本——旧版本就没了(除非有版本历史功能)。但 Git 的 commit 是追加式的——每次 commit 都创建一个全新的快照,旧版本永远不会被覆盖。
传统磁盘保存: v1 → v2 → v3(v1 和 v2 被覆盖,不可恢复)
Git commit: v1 → v2 → v3(v1 和 v2 仍然存在,随时可回溯)
这意味着 Git 的“磁盘”更像是一种追加写入日志(Append-Only Log),而不是传统意义上的覆盖式存储:
- 你可以随时回到任何一个历史版本(
git checkout <hash>) - 即使你
git reset了,旧版本的数据仍然在.git/objects/中(直到垃圾回收) - 这也是为什么
.git目录会越来越大——它从不主动删除旧数据
9.3 不一致之三:CPU 缓存是自动管理的,暂存区是手动管理的
在真实的计算机体系中,CPU 缓存(L1/L2/L3)的读写是硬件自动完成的——你不需要(也无法)手动控制哪些数据进入缓存、哪些被淘汰。但 Git 的暂存区需要你手动操作:
git add file.js # 手动把文件加入暂存区
git add file2.js # 再手动加一个
git commit # 手动触发"保存"
没有 git add,你的修改就永远不会进入暂存区;没有 git commit,暂存区的内容永远不会被保存。每一步都需要你显式地告诉 Git。
9.4 不一致之四:内存中的数据可以原地修改,Git 的对象是不可变的
在真实的内存中,你可以随时修改任何一个字节:
data = [1, 2, 3]
data[0] = 999 # 原地修改,内存中的值直接变了
但 Git 中的对象(blob、tree、commit)一旦创建就是不可变的——你不能修改一个已有的 blob 对象,只能创建一个新的。每个对象的内容和它的 SHA-1 哈希值是一一绑定的:内容变了,哈希就变了,就是一个全新的对象。
修改前:blob(abc123) → "Hello World"
修改后:blob(def456) → "Hello Git" ← 这是一个全新的对象,abc123 仍然存在
这种不可变性是 Git 能够保证数据完整性的基础——任何篡改都会导致哈希值不匹配,从而被立即发现。
9.5 不一致之五:内存和磁盘之间没有「分支」概念
在操作系统中,内存就是内存,磁盘就是磁盘,没有「分支」的概念。但 Git 的本地仓库可以有无数个分支,每个分支指向不同的 commit,代表不同的开发线。
main: A → B → C → E
\
feature: D → F
这种分支模型是 Git 作为版本控制工具的核心能力,在内存/磁盘的类比中找不到对应物。
不过,如果你把分支理解为书签——标记了你在版本历史中的不同位置——就比较好理解了。就像你在看一本厚书时,可以在不同章节夹上不同的书签,随时翻到任意一个书签的位置。
9.6 一张表总结所有不一致
| 维度 | 真实的内存/磁盘 | Git |
|---|---|---|
| 数据持久性 | 内存断电即失 | 工作目录的文件不会丢失 |
| 写入方式 | 覆盖式(新版本替换旧版本) | 追加式(旧版本永远保留) |
| 缓存管理 | 硬件自动管理 | 手动 git add |
| 数据可变性 | 内存中的数据可原地修改 | Git 对象创建后不可变 |
| 分支概念 | 不存在 | 核心功能 |
| 寻址方式 | 物理地址 / 虚拟地址 | 内容寻址(SHA-1 哈希) |
十、最佳实践——如何用好 Git
10.1 频繁 commit,小步提交
就像你应该经常按 Ctrl+S 一样,你应该频繁 commit。每次只做一个小改动,就 commit 一次。这样:
- 如果出错了,可以精确地回退到任何一个点
- commit 信息可以清晰地描述每个改动
- 团队协作时,减少合并冲突
# 好的实践:每个小改动一个 commit
git add login.js && git commit -m "feat: 添加登录表单验证"
git add api.js && git commit -m "feat: 添加登录 API 调用"
git add style.css && git commit -m "style: 调整登录页面样式"
# 不好的实践:一堆改动一个 commit
git add . && git commit -m "做了很多修改" # ← 太模糊了
10.2 写好 commit 信息
Commit 信息就像文件的「备注」——你以后回看的时候,需要知道这个版本改了什么。推荐使用 Conventional Commits 规范:
<type>(<scope>): <subject>
type:
feat: 新功能
fix: 修复 bug
docs: 文档修改
style: 格式调整(不影响功能)
refactor: 重构(不新增功能,不修复 bug)
test: 测试相关
chore: 构建/工具链相关
10.3 善用 .gitignore
.gitignore 就像你告诉操作系统这些文件不要备份:
# 依赖包(可以重新安装,不需要版本控制)
node_modules/
# 编译产物(可以重新生成)
dist/
build/
# 环境配置(包含敏感信息)
.env
.env.local
# 操作系统生成的文件
.DS_Store
Thumbs.db
# IDE 配置(每个人不同)
.vscode/
.idea/
10.4 提交前检查
# 提交前看看自己到底要提交什么
git diff # 工作目录 vs 暂存区
git diff --cached # 暂存区 vs 最新 commit
git diff HEAD # 工作目录 vs 最新 commit
# 确认无误后再提交
git commit -m "feat: xxx"
就像你在点击 Word 的“保存”之前,应该先检查一下文档内容是否正确。
总结
速查表
| 你想做什么 | Git 命令 | 类比 |
|---|---|---|
| 编辑文件 | 直接修改 | 在内存中修改数据 |
| 标记准备提交 | git add <file> |
复制到剪贴板/缓存 |
| 查看当前状态 | git status |
检查内存和缓存的差异 |
| 查看具体改动 | git diff |
对比内存和缓存的差异 |
| 保存版本 | git commit -m "..." |
保存到磁盘(Ctrl+S) |
| 上传到云端 | git push |
上传到网盘 |
| 从云端下载 | git pull |
从网盘下载 |
| 撤销文件修改 | git restore <file> |
从磁盘恢复到内存 |
| 撤销暂存 | git restore --staged <file> |
从缓存退回内存 |
| 撤销 commit(本地,未 push) | git reset HEAD~1 |
撤销保存,文件退回编辑器(未暂存) |
| 撤销 commit(已 push,安全方式) | git revert <commit> |
创建一个反向 commit,不删历史 |
| 查看历史 | git log |
查看保存记录 |
| 切换版本 | git checkout <hash> |
打开历史版本的文件 |
| 临时冻结修改 | git stash |
电脑休眠 |
| 恢复冻结 | git stash pop |
从休眠唤醒 |
FAQ 速查——常见问题
Q1:我改崩了,怎么回到上一个版本?
A1:如果你想丢弃所有未暂存的文件修改(工作区的改动),相当于把所有改了一半但没保存的文件全部还原。用 git restore . 。
A2:如果已经 commit 了,用 git reset --hard HEAD~1 回到上一个版本。
(⚠️ 会丢掉之后的所有修改)
Q2:我不小心
git add了不想提交的文件,怎么办?
A:用 git restore --staged <文件名> 把它从暂存区撤回来,文件本身的修改不会丢。
Q3:commit 之后想再加一个文件进去,不想新建 commit,可以吗?
A:可以。先用 git add 把那个文件加入暂存区,然后运行 git commit --amend --no-edit。这会把这个文件合并到上一次 commit 里,不产生新的 commit 记录。
Q4:怎么撤销已经 push 到远程的 commit?
A:如果是个人分支且确定没人基于它工作,可以用 git reset --hard <正确的commit> 然后 git push --force。否则,请用 git revert <要撤销的commit>,它会生成一个新的反向 commit,安全无害。
Q5:我 fork 了别人的仓库后仓库更新了内容,怎么同步原仓库的新更新?
A:先添加原仓库为上游 git remote add upstream <原仓库URL>,然后 git fetch upstream,再合并 git merge upstream/main(或使用 git pull upstream main 一步完成)。
Git 的设计哲学其实非常朴素:分层存储,精确控制,永不丢失。它和操作系统的存储体系一样,都是把数据按照 临时 → 缓冲 → 永久 → 远程 的层次组织起来。
理解了内存与磁盘的类比,就不需要死记硬背 Git 的命令了——每次操作,你只需要问自己一个问题:
我现在要把数据从哪一层移到哪一层?
答案就呼之欲出了。
📢 声明:本文借助AI辅助工具进行资料整理与初稿生成,所有内容均经过作者本人的详细核对、修改与编排,文责自负。

浙公网安备 33010602011771号