Git到底把代码存在哪里:理解工作区、暂存区与提交历史

举报
yd_232225224 发表于 2026/09/15 20:57:51 2026/09/15
【摘要】 很多人第一次学习Git时,会把它理解成一个“保存代码版本的工具”:git add .git commit -m "完成某项功能"但一旦需要撤销修改,问题就出现了:git add之后,代码去了哪里?git commit保存的是差异还是完整文件?reset、restore和revert有什么区别?为什么文件已经提交,还能恢复到以前的状态?为什么切换分支会改变本地文件?HEAD究竟是什么?这些问题...

很多人第一次学习Git时,会把它理解成一个“保存代码版本的工具”:

git add .
git commit -m "完成某项功能"

但一旦需要撤销修改,问题就出现了:

  • git add之后,代码去了哪里?
  • git commit保存的是差异还是完整文件?
  • resetrestorerevert有什么区别?
  • 为什么文件已经提交,还能恢复到以前的状态?
  • 为什么切换分支会改变本地文件?
  • 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

然后明确回答三个问题:

  1. 哪些修改只存在于工作区?
  2. 哪些修改已经进入暂存区?
  3. 哪些修改已经成为提交?

接着再选择命令:

不想提交某个已暂存文件
→ 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就不再是一组容易混淆的命令,而会变成一套清晰、可预测的版本管理模型。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。