Git到底把代码存在哪里:理解工作区、暂存区与提交历史
很多人第一次学习Git时,会把它理解成一个“保存代码版本的工具”:
git add .
git commit -m "完成某项功能"
但一旦需要撤销修改,问题就出现了:
git add之后,代码去了哪里?git commit保存的是差异还是完整文件?reset、restore和revert有什么区别?- 为什么文件已经提交,还能恢复到以前的状态?
- 为什么切换分支会改变本地文件?
HEAD究竟是什么?
这些问题看似分散,本质上都可以通过Git的三个区域解释清楚:
工作区
↓ git add
暂存区
↓ git commit
本地仓库
理解这三个区域后,大部分Git命令就不再需要死记硬背。
一、Git管理的不是文件夹,而是版本关系
假设我们创建了一个文件:
print("Hello")
然后提交:
git add main.py
git commit -m "创建程序入口"
之后将文件修改为:
print("Hello Git")
再次提交:
git add main.py
git commit -m "修改输出内容"
Git会形成一条提交历史:
提交A:创建程序入口
↓
提交B:修改输出内容
每次提交都包含一个指向前一个提交的关系,因此多个提交可以组成一条历史链。
分支并不是复制出来的一整套代码,而是一个指向某次提交的可移动标记。执行新提交后,当前分支会向前移动,指向最新提交。
二、Git更接近保存快照,而不是只保存差异
很多人认为Git每次提交只保存“本次修改了哪些行”。从使用感受上看,这种理解并不奇怪,因为git diff展示的就是文件差异。
但从逻辑模型上看,Git的每次提交都对应项目在某个时刻的一份文件快照。
例如:
提交A
├── main.py
└── config.json
提交B
├── main.py
├── config.json
└── user.py
如果某个文件与上次提交完全相同,Git不会无意义地重复保存相同内容,而是复用已有对象。
因此,可以同时使用两个角度理解Git:
- 从用户查看修改的角度,它擅长计算版本之间的差异;
- 从提交数据的角度,每个提交代表项目的一份完整快照。
这也是Git能够快速切换版本的重要原因。
三、工作区是什么
工作区就是我们平时直接编辑的项目目录。
在编辑器中修改、创建或删除文件,首先改变的都是工作区。
例如,已经提交的main.py内容为:
print("Hello")
我们将其修改为:
print("Hello Git")
此时Git会提示文件发生变化:
git status
可能显示:
modified: main.py
这表示工作区中的main.py与Git当前记录的版本不同。
此时修改还没有进入暂存区,也没有成为新的提交。
四、暂存区是什么
暂存区也称为索引,是工作区和下一次提交之间的准备区域。
执行:
git add main.py
并不是把文件“上传”到某个远程位置,而是把main.py当前内容放入暂存区,作为下一次提交的候选版本。
可以将暂存区理解成一张“下次提交内容清单”。
假设我们同时修改了三个文件:
main.py
config.json
README.md
但只希望本次提交前两个文件,就可以执行:
git add main.py config.json
git commit -m "更新程序配置"
此时README.md仍然留在工作区,不会进入这次提交。
暂存区的价值在于:一次提交不必包含工作区中的所有修改,而可以只选择逻辑上属于同一件事的部分。
五、为什么修改同一个文件后会同时出现两种状态
考虑以下过程。
首先修改文件:
print("Version 1")
然后执行:
git add main.py
此时暂存区保存了:
print("Version 1")
接着不提交,而是继续修改工作区文件:
print("Version 2")
这时,同一个文件同时存在两个不同版本:
暂存区:Version 1
工作区:Version 2
执行git status时,可能会同时看到:
Changes to be committed
Changes not staged for commit
这不是Git状态混乱,而是因为:
- 第一次修改已经进入暂存区;
- 第二次修改还停留在工作区。
如果此时直接提交,提交的是Version 1。如果希望提交最新内容,需要再次执行:
git add main.py
六、本地仓库保存什么
执行git commit后,Git会根据暂存区创建新的提交。
一次提交通常包含:
- 当前项目的文件快照;
- 作者和提交者信息;
- 提交时间;
- 提交说明;
- 父提交标识;
- 能够唯一标识内容的哈希值。
执行:
git log --oneline
可能看到:
6a31d82 修复登录状态判断
481bd0a 增加用户登录功能
d910f43 初始化项目
前面的字符串是提交标识的缩写。
每个提交都指向其父提交,因此可以沿着父提交不断向前追溯项目历史。
七、HEAD到底是什么
HEAD表示当前检出位置。
在正常情况下,HEAD会指向当前分支,而当前分支再指向某次提交:
HEAD
↓
main分支
↓
提交C
创建新提交后,main分支向前移动:
提交A ← 提交B ← 提交C ← 提交D
↑
main
↑
HEAD
因此,HEAD通常可以理解为“当前所在分支的最新提交”。
常见写法包括:
HEAD 当前提交
HEAD~1 当前提交的上一个提交
HEAD~2 当前提交向前两个提交
如果直接切换到某个具体提交,而不是切换到分支,就可能进入分离HEAD状态:
HEAD
↓
提交B
main
↓
提交D
此时可以查看或测试旧版本,但如果要在这个位置继续开发,最好创建一个新分支,否则新提交可能在之后变得难以找到。
八、三个区域可以看成三份状态
理解Git时,可以把以下三者想象成三份项目快照:
HEAD:最近一次提交的内容
暂存区:准备在下次提交的内容
工作区:当前磁盘上的实际内容
多数Git命令,本质上都在比较或移动这三份状态。
例如:
git diff
默认比较:
工作区 与 暂存区
它显示尚未暂存的修改。
而:
git diff --staged
比较:
暂存区 与 HEAD
它显示下次提交将包含哪些变化。
如果两个命令都没有输出,通常说明工作区、暂存区和HEAD中的受跟踪文件内容一致。
九、git add真正做了什么
git add最常见的作用是把工作区内容更新到暂存区。
git add main.py
对应的变化是:
工作区 ──复制内容──> 暂存区
它不会创建提交,也不会修改已有提交。
git add还可以暂存文件删除操作。假设删除了一个已跟踪文件,再执行:
git add deleted-file.txt
Git会把“删除该文件”加入暂存区。
因此,git add并不只是“添加新文件”,更准确的含义是“将当前变化加入下一次提交”。
十、git commit真正做了什么
git commit会读取暂存区,并创建一份新的提交快照:
暂存区 ──创建提交──> 本地仓库
提交完成后,当前分支指向新提交。
需要注意的是,git commit不会自动提交所有工作区修改。它只提交已经放入暂存区的内容。
所以,下面的流程非常重要:
git status
git diff --staged
git commit -m "准确描述本次修改"
提交前查看暂存区差异,可以避免把调试代码、临时配置或无关修改误提交进去。
十一、git restore用于恢复文件内容
git restore主要用于恢复工作区或暂存区中的文件。
丢弃工作区修改
假设main.py已经被修改,但尚未加入暂存区:
git restore main.py
默认会使用暂存区中的版本覆盖工作区文件。
这意味着尚未保存到其他位置的工作区修改可能丢失,因此执行前应先确认差异:
git diff main.py
取消暂存
如果文件已经执行过git add,但不希望它进入下次提交,可以执行:
git restore --staged main.py
这会将文件从暂存状态恢复为未暂存状态,但通常不会删除工作区中的修改。
对应关系是:
暂存区恢复到HEAD状态
工作区修改继续保留
“取消暂存”和“丢弃修改”是两件不同的事情,不能混淆。
十二、git reset用于移动分支位置
git reset的核心作用是将当前分支指向另一个提交,同时根据模式决定是否更新暂存区和工作区。
常见模式包括:
--soft;--mixed;--hard。
soft模式
git reset --soft HEAD~1
它会移动当前分支,但保留暂存区和工作区内容。
可以理解为:
撤销提交
保留暂存
保留文件修改
适合刚提交后发现提交说明不合适,或者希望重新整理提交内容的情况。
mixed模式
git reset --mixed HEAD~1
--mixed也是git reset的常见默认模式。
它会移动当前分支,并更新暂存区,但保留工作区文件。
可以理解为:
撤销提交
取消暂存
保留文件修改
hard模式
git reset --hard HEAD~1
它会同时修改:
- 当前分支;
- 暂存区;
- 工作区。
可以理解为:
撤销提交
取消暂存
丢弃工作区修改
--hard可能造成未提交内容丢失,因此使用前必须确认目标提交和当前工作区状态。
十三、git revert为什么更适合撤销公共提交
假设某次错误提交已经被其他人获取:
提交A ← 提交B ← 错误提交C
如果直接使用reset让分支回到提交B,就会改写分支历史。其他人的仓库中可能仍然保留提交C,后续同步时容易产生冲突。
git revert不会删除提交C,而是创建一个新提交D,用相反的修改抵消C:
提交A ← 提交B ← 提交C ← 提交D
撤销C
命令形式为:
git revert 提交标识
它具有两个优点:
- 原有提交历史仍然保留;
- 其他协作者可以正常获取新的撤销提交。
因此,可以使用一个简单原则:
尚未共享的本地提交:可以考虑reset
已经共享的公共提交:优先考虑revert
十四、reset、restore和revert的区别
| 命令 | 主要作用 | 是否移动分支 | 是否创建新提交 | 是否可能影响工作区 |
|---|---|---|---|---|
git restore |
恢复文件或取消暂存 | 否 | 否 | 是 |
git reset |
重置分支、暂存区或工作区 | 是 | 否 | 取决于模式 |
git revert |
创建反向提交撤销历史修改 | 否 | 是 | 通常通过新提交体现 |
不要只把它们都记成“撤销命令”,而要先判断自己究竟想改变什么:
- 想恢复某个文件:考虑
restore; - 想让本地分支回到某个提交:考虑
reset; - 想安全撤销已经共享的提交:考虑
revert。
十五、分支为什么创建得很快
在Git中,分支本质上只是一个指向提交的标记。
假设当前历史为:
提交A ← 提交B ← 提交C
↑
main
创建新分支:
git switch -c feature
结果只是多了一个新的指针:
提交A ← 提交B ← 提交C
↑
main、feature
Git不需要复制整个项目目录,所以创建分支非常快。
当在feature分支上产生新提交时:
提交A ← 提交B ← 提交C ← 提交D
↑ ↑
main feature
两个分支只是指向不同的提交位置。
十六、切换分支为什么会改变文件
执行:
git switch feature
Git会把工作区和暂存区调整为feature分支所指向提交的状态。
因此,切换分支后,本地文件可能增加、减少或改变内容。
如果工作区存在未提交修改,而且这些修改会被目标分支覆盖,Git通常会拒绝切换,并提示先提交、暂存或处理当前修改。
这是Git保护本地工作的一种方式。
十七、git stash适合临时保存修改
开发到一半时,如果需要紧急切换分支处理其他任务,但当前修改还不适合提交,可以使用:
git stash push -m "暂存正在开发的功能"
它会把当前修改临时保存起来,并让工作区恢复到相对干净的状态。
处理完其他任务后,可以查看暂存记录:
git stash list
再恢复修改:
git stash apply
如果希望恢复后同时删除对应暂存记录,可以使用:
git stash pop
需要注意,stash是临时工具,不适合作为长期保存代码的方式。重要工作仍然应该通过清晰的提交和分支管理。
十八、未跟踪文件为什么不会自动进入提交
新建文件后,Git可能显示:
Untracked files
这表示文件存在于工作区,但Git尚未开始跟踪它。
只有执行:
git add new-file.py
它才会进入暂存区,并有机会出现在下一次提交中。
这样设计是为了防止构建产物、临时文件、日志和本地配置被自动加入版本历史。
对于长期不需要跟踪的文件,可以在忽略规则中声明:
build/
*.log
local-config.json
需要注意,忽略规则主要针对尚未跟踪的文件。一个已经被Git跟踪的文件,不会因为后来加入忽略规则就自动停止跟踪。
十九、误操作后为什么有时还能恢复
Git通常会记录HEAD和分支指针近期移动过的位置。
可以使用:
git reflog
查看本地引用操作记录,例如:
HEAD@{0}: reset: moving to HEAD~1
HEAD@{1}: commit: 完成功能开发
HEAD@{2}: commit: 初始化模块
如果误执行reset导致提交在普通日志中消失,仍然可能通过引用日志找到原提交标识,再创建分支进行恢复。
例如:
git branch recovery 提交标识
不过,引用日志并不是永久备份。无法访问的对象可能在后续清理过程中被删除。因此,不能因为存在恢复机制,就随意执行破坏性命令。
二十、一个更安全的Git操作顺序
执行撤销或历史修改操作前,可以按照以下顺序检查:
git status
git diff
git diff --staged
git log --oneline --decorate -n 10
然后明确回答三个问题:
- 哪些修改只存在于工作区?
- 哪些修改已经进入暂存区?
- 哪些修改已经成为提交?
接着再选择命令:
不想提交某个已暂存文件
→ git restore --staged
想丢弃某个未暂存文件的修改
→ git restore
想重新整理尚未共享的本地提交
→ git reset
想撤销已经共享的提交
→ git revert
如果仍然不确定,可以先创建一个临时分支保存当前位置:
git branch backup-before-change
这相当于给当前提交增加一个容易找到的标记。
二十一、常见误区
1. git add等于上传代码
git add只更新本地暂存区,不会与远程仓库通信。
2. git commit会提交所有修改
git commit默认只提交暂存区中的内容,未暂存修改仍留在工作区。
3. 工作区文件就是Git保存的唯一副本
Git仓库中保存着已提交对象,工作区只是当前检出版本的可编辑表现形式。
4. 分支是一份完整代码副本
分支主要是指向提交的引用,并不是把整个项目复制一遍。
5. reset和revert效果完全相同
reset移动分支位置,可能改写历史;revert通过新提交抵消旧修改,保留原有历史。
6. 文件加入忽略规则后就不再被跟踪
如果文件已经被提交,忽略规则不会自动解除跟踪。
7. 提交越大越方便
包含大量无关修改的提交虽然省事,却难以审查、撤销和定位问题。一次提交最好表达一个完整、明确的逻辑变化。
结语
Git最重要的不是记住大量命令,而是理解三个区域:
HEAD:上一次提交的项目状态
暂存区:下一次准备提交的状态
工作区:当前正在编辑的实际文件
git add把工作区内容放入暂存区,git commit根据暂存区创建新提交,git restore恢复文件或取消暂存,git reset移动分支并选择性更新其他区域,git revert则用新提交安全地撤销已有修改。
当你能够判断一个操作改变的是工作区、暂存区还是提交历史时,Git就不再是一组容易混淆的命令,而会变成一套清晰、可预测的版本管理模型。
- 点赞
- 收藏
- 关注作者
评论(0)