
Git 基础:认识三大区域与文件修改提交流程
Git 工作区、暂存区和版本库的概念及关系,详细演示了使用 git add 和 git commit 将文件纳入管理的流程。通过实际操作展示了如何查看提交历史、理解.git 目录结构,以及利用 git status 和 git diff 监控文件修改状态并正确提交变更。

Git 工作区、暂存区和版本库的概念及关系,详细演示了使用 git add 和 git commit 将文件纳入管理的流程。通过实际操作展示了如何查看提交历史、理解.git 目录结构,以及利用 git status 和 git diff 监控文件修改状态并正确提交变更。



要理解 Git 的工作流程,必须先搞清楚它在哪几个地方'存放'和'处理'你的文件。Git 主要涉及以下三个区域:


.git 文件夹)下的 index 文件里(路径是 .git/index)。所以,有时候人们也把暂存区叫做'索引'(Index)。可以想象成你的'打包盒子',你把准备好一起提交的文件放进去。.git 文件夹。注意,这个 .git 文件夹不属于工作区!它是独立于工作区的版本库本身。版本库就像你的'仓库'或'档案室',所有经过 commit 保存的版本都安全地存放在这里,随时可以追溯或恢复。想象一下,它们之间的关系就像这样:
+--------------+ +--------------+ +--------------+
| 工作区 | ----> | 暂存区 | ----> | 版本库 |
| (Working Dir)| | (Staging Area)| | (Repository) |
+--------------+ +--------------+ +--------------+
(你的办公桌) (准备提交的区域) (历史版本仓库)
^ ^
|
|
+---------------------+---------------------+
(可以从版本库或暂存区恢复到工作区)
git add 把这些改动从工作区添加到暂存区。git add 累积起来的所有改动)都准备好作为一个完整的历史版本保存时,需要 git commit 把暂存区的内容提交到版本库。重点来了!
你仅仅在工作区新建了一个文件,或者修改了工作区里的文件,对于 Git 的版本库来说,它是不知道的!这些改动没有被 Git 跟踪起来。
通过新建或粘贴进目录的文件,并不能称之为向仓库中新增文件,而只是在工作区新增了文件。必须要通过使用 git add 和 git commit 命令才能将文件添加到仓库中进行管理!!
要让 Git 开始管理这些文件和改动,你必须完成从工作区 -> 暂存区 -> 版本库 的这个流程,也就是依次使用 git add 命令和 git commit 命令!
git add 和 git commit理解了'三区'的概念,我们就知道 Git 跟踪和保存文件的基本步骤是:
git add 命令将工作区的改动添加到暂存区。git commit 命令将暂存区的改动提交到版本库,形成一个新的历史版本。下面我们通过实际操作来学习这两个命令。
假设我们已经在 git init 过的项目文件夹里(也就是工作区),新建了一个 ReadMe 文件,并写了一些内容。
# 确保你在 Git 仓库目录下
user@host:~/gitcode$ pwd
/home/user/gitcode
# 使用 vim 或其他编辑器创建并编辑 ReadMe 文件
# 比如输入两行内容:
# hello bit
# hello git
user@host:~/gitcode$ vim ReadMe
# 查看 ReadMe 的内容(确认修改已完成)
user@host:~/gitcode$ cat ReadMe
hello bit
hello git
现在,ReadMe 文件在我们的工作区里。要让 Git 跟踪它,我们需要先把它加到暂存区。
第一步:将文件添加到暂存区 (git add)
使用 git add 命令指定要添加的文件名:
# 将 ReadMe 文件添加到暂存区
user@host:~/gitcode$ git add ReadMe
git add [文件名]:将指定文件的工作区改动添加到暂存区。git add [目录名]:将指定目录(包括子目录)下的所有改动都添加到暂存区。git add .:将当前目录下的所有改动(包括新增、修改、删除,但删除需要单独处理或使用特定命令)都添加到暂存区。这是最常用的方式,表示'把我当前目录下所有 Git 知道有变化的文件的改动都放进暂存区'。执行 git add ReadMe 后,ReadMe 文件的当前状态(包括内容和存在)就被放进了暂存区,它现在正安静地躺在你的'打包盒子'里,等待被提交。
第二步:将暂存区内容提交到版本库 (git commit)
现在暂存区里有了 ReadMe 文件。我们可以用 git commit 命令把暂存区里的所有内容作为一个新的版本提交到版本库。
# 提交暂存区的所有内容到版本库
# -m 后面跟着的是本次提交的'说明信息'
user@host:~/gitcode$ git commit -m "commit my first file"
git commit -m "你的提交信息":提交暂存区中的所有内容到版本库。-m 参数非常重要,后面跟着的是本次提交的日志消息(message)。这个消息是给你自己和未来的协作者看的,要清晰地说明这次提交'做了什么'。这部分内容绝不能省略,也务必好好描述!git commit [文件名 1] [文件名 2] ... -m "你的提交信息":提交暂存区中指定文件的内容到版本库(但通常我们是提交暂存区的全部内容,所以第一个命令更常用)。执行 git commit -m "commit my first file" 后,Git 会把你暂存区里所有的改动(这里就是新增的 ReadMe 文件内容)打包,生成一个唯一的版本号(commit id),然后永久地存储到版本库里。
Git 会给出提交成功的反馈信息:
[master (root-commit) c614289] commit my first file
# [分支信息] commit id 的前几位]
1file changed, 2 insertions(+)
# 本次提交涉及 1 个文件,新增了 2 行内容
create mode 100644 ReadMe
# ReadMe 文件被创建,权限模式是 100644
这说明你的 ReadMe 文件已经成功地作为你的第一个版本,被 Git 永久保存起来了。
理解多次 add,一次 commit:
你可能会想,如果我改了好几个文件,是不是要 add 一次就 commit 一次?不是的!
你可以多次使用 git add 命令,将不同时间修改好的、准备一起提交的文件或改动,陆续地添加到暂存区。当你觉得所有这次要一起保存的改动都进了暂存区后,最后只需执行一次 git commit 命令,Git 就会把暂存区里的所有内容,作为一个完整的、单一的版本提交到版本库。
例如,我们再创建并添加三个空文件 file1, file2, file3:
# 在工作区创建三个新文件
user@host:~/gitcode$ touch file1 file2 file3
# 逐个或批量添加到暂存区
user@host:~/gitcode$ git add file1
user@host:~/gitcode$ git add file2
user@host:~/gitcode$ git add file3
# 或者直接 git add . 把当前目录下的所有新增文件都加进去
# 一次性提交暂存区里所有待提交的内容(file1, file2, file3)
user@host:~/gitcode$ git commit -m "add 3 files"
[master 23807c5] add 3 files
# Git 生成了新的 commit id
3 files changed, 0 insertions(+), 0 deletions(-)
# 本次提交涉及 3 个文件,没有增删行(因为是空文件)
create mode 100644 file1
# file1 被创建
create mode 100644 file2
# file2 被创建
create mode 100644 file3
# file3 被创建
这次提交就包含了这三个文件。这再次印证了 commit 提交的是暂存区的内容。
git log每次 git commit 都会在版本库中留下一个永久的历史记录。我们可以使用 git log 命令来查看这些记录。
user@host:~/gitcode$ git log
commit 23807c536969cd886c4fb624b997ca575756eed6 (HEAD -> master)
# 最新提交的 commit id,以及 HEAD 和 master 分支的指向
Author: User <[email protected]>
# 作者信息
Date: Sat May 6 11:27:32 2023 +0800
# 提交日期和时间
add 3 files
# 提交时填写的消息(message)
commit c61428926f3853d4ec6dde904415b0e6c1dabcc6
# 上一个提交的 commit id
Author: User <[email protected]>
Date: Sat May 6 11:25:50 2023 +0800
commit my first file
git log 命令会按照提交时间的倒序(最近的提交在最上面)显示所有的提交记录。你可以看到每个提交的唯一 ID、作者、时间以及提交时填写的消息。
如果觉得输出信息太多,可以加上 --pretty=oneline 参数,让信息更简洁:
user@host:~/gitcode$ git log --pretty=oneline
23807c536969cd886c4fb624b997ca575756eed6 (HEAD -> master) add 3 files
# commit id 和提交信息
c61428926f3853d4ec6dde904415b0e6c1dabcc6 commit my first file
# 上一个 commit id 和提交信息
理解 Commit ID:
git log 中看到的那一长串字母和数字组合(比如 23807c5... 和 c614289...),就是每次提交的 Commit ID(版本号)。
它不是简单的 1, 2, 3 递增序号,而是 Git 使用 SHA1 这种加密算法,根据本次提交的内容(包括文件的改动、提交者、时间、父提交等信息)计算出来的一个唯一的哈希值。即使你在不同的电脑上提交相同的内容,生成的 Commit ID 也会是一样的。它是 Git 用来引用某个特定版本的'身份证号'。通常我们只需要使用它的前几位(一般是 7-8 位)就可以唯一标识一个提交了。
.git 目录变化为了帮助你更深刻地理解 Git 是如何工作的,我们可以再次 peek 一下 .git 目录里的变化。在我们执行了几次 add 和 commit 后,.git 目录会变得更'丰满'。
user@host:~/gitcode$ tree .git/
.git/
├── branches
├── COMMIT_EDITMSG # 最近一次 commit 的消息
├── config # 仓库配置文件
├── description
├── HEAD # 指向当前分支(如 master/main)
├── hooks # 钩子脚本目录
├── index # **就是暂存区的文件!git add 会修改它!**
├── info
│ └── exclude # Git 忽略文件配置
├── logs # 记录 HEAD 和分支的移动历史
│ ├── HEAD
│ └── refs
│ └── heads
│ └── master
├── objects # **Git 存放所有对象的地方(文件内容、目录树、提交等)!**
│ ├── 23# 根据对象 ID 前两位命名的文件夹
│ │ └── 807c536969cd886c4fb624b997ca575756eed6 # 最新 commit 对象
│ ├── 83
│ │ └── 0a8c9feefbdc098bbae2cdc25e5034ce1920d7 # 一个 tree 对象 (目录树快照)
│ ├── 8f
│ │ └── add50161b6fafa53ce7e79d278dc490240c946 # 可能是一个 blob 或 tree 对象
│ ├── 9c
│ │ └── 9e1f0f6bff3015df71a0963004476f5e6cfd54 # ReadMe 文件的 blob 对象 (内容)
│ ├── c6
│ │ └── 142892626f3853d4ec6dde904415b0e6c1dabcc6 # 第一个 commit 对象
│ ├── e6
│ │ └── 9de29bb2d1d6434b8b29ae775ad8c2e48c5391 # file1/file2/file3 的 blob 对象 (内容)
│ ├── info
│ └── pack # 优化存储的对象打包文件
└── refs # 存放指向 commit 的引用,如分支和标签
├── heads # 分支引用存放处
│ └── master # master 分支引用,里面存着 master 分支最新提交的 commit id
└── tags # 标签引用存放处
# 注意:你的 objects 目录下的文件夹和文件可能不同,因为它们是根据你的具体提交内容生成的。
这里面有几个关键文件/目录,和我们的操作密切相关:
.git/index: 这就是暂存区!每次执行 git add 命令,Git 都会更新这个文件,记录下当前暂存区的文件状态和对应的对象信息。.git/HEAD: 这是一个指针,默认情况下,它指向你当前所在的分支(比如 master 或 main)。# 查看 HEAD 指向
user@host:~/gitcode$ cat .git/HEAD
ref: refs/heads/master # 表示 HEAD 指向 refs/heads/master
.git/refs/heads/master: 这是一个文件,它里面保存着 master 分支最新一次提交的 commit id。# 查看 master 分支指向的 commit id
user@host:~/gitcode$ cat .git/refs/heads/master
23807c536969cd886c4fb624b997ca575756eed6 # 这就是我们上面 git log 看到的最新 commit id
.git/objects/: 这是 Git 的对象数据库,Git 存储所有版本数据的核心区域!每次 git add,Git 会把工作区的文件内容压缩并存入这里,生成一个 blob 对象;每次 git commit,Git 会根据暂存区的内容,生成一个 tree 对象(代表当时的目录结构)和一个 commit 对象(包含作者、时间、message,以及指向 tree 对象和父 commit 对象)。你看到的 objects 目录下以 commit id 前两位命名的文件夹里,就存放着这些对象。所以上图实际上还有一个文件夹作为逻辑上的中间层:对象库

Git 使用 SHA1 哈希值作为对象的 ID。你可以使用 git cat-file -p [对象 ID] 命令来查看 Git 对象库中某个对象的内容。
例如,查看最新的 commit 对象(使用上面 git log 或 .git/refs/heads/master 看到的 commit id):
user@host:~/gitcode$ git cat-file -p 23807c536969cd886c4fb624b997ca575756eed6
tree 830a8c9feefbdc098bbae2cdc25e5034ce1920d7 # 这个 commit 对应的目录树对象 ID
parent c61428926f3853d4ec6dde904415b0e6c1dabcc6 # 这个 commit 的父提交 ID(上一个版本)
author User <[email protected]> 1683343652 +0800 # 作者信息
committer User <[email protected]> 1683343652 +0800 # 提交者信息
# 这是本次提交的消息
add 3 files
再查看上面 commit 对象里指向的 tree 对象(使用 tree 后面的 ID):
user@host:~/gitcode$ git cat-file -p 830a8c9feefbdc098bbae2cdc25e5034ce1920d7
100644 blob 9c9e1f0f6bff3015df71a0963004476f5e6cfd54 ReadMe # ReadMe 文件,类型是 blob,这是它的对象 ID
100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 file1 # file1 文件,类型是 blob,这是它的对象 ID
100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 file2 # file2 文件
100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 file3 # file3 文件
这个 tree 对象记录了在 23807c5... 那个提交时,项目根目录下有哪些文件和文件夹,以及它们各自对应的对象 ID。这里可以看到 ReadMe, file1, file2, file3 都指向了各自的内容对象。
最后,我们查看 ReadMe 文件对应的 blob 对象(使用上面看到的 9c9e1f0f6bff3015df71a0963004476f5e6cfd54 ID):
user@host:~/gitcode$ git cat-file -p 9c9e1f0f6bff3015df71a0963004476f5e6cfd54
hello bit
hello git
# 这就是 ReadMe 文件在那个提交时的实际内容!
通过这个探索,我们可以看到 Git 如何在 objects 目录下存储文件的实际内容(blob 对象)、目录结构(tree 对象)以及提交信息(commit 对象),并且通过 Commit ID 将它们关联起来。每次 git add 和 git commit 都会在这个对象库中创建新的对象。这有助于我们理解 Git 是如何保存项目历史的。
总结 .git 内部的几个重要部分:
index: 暂存区的实际文件,git add 的结果保存在这里。HEAD: 指向当前所在分支(默认是 master 或 main)的指针。refs/heads/master: 一个文件,里面存储着 master 分支最新提交的 commit id。objects: Git 的对象数据库,存放所有 Git 管理的对象(文件内容、目录树、提交等)。在今后的学习中,试着将常用的 Git 命令和 .git 目录内部的变化联系起来,每次操作后都进行回想'这时候目录内部的操作',可以帮助你更深刻地理解 Git 的工作原理。
add 和 commit 的配合为了加深对工作区、暂存区、版本库以及 add 和 commit 命令关系的理解,我们再看一个例子:
# 1. 在工作区新增 file4 文件
user@host:~/gitcode$ touch file4
# 2. 将 file4 添加到暂存区(它现在进入了打包盒子)
user@host:~/gitcode$ git add file4
# 3. 紧接着,在工作区又新增了 file5 文件(它还在办公桌上)
user@host:~/gitcode$ touch file5
# 4. 提交修改(Git 会提交暂存区的内容)
user@host:~/gitcode$ git commit -m "add file"
[master 3d406c0] add file
1file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 file4
看提交结果,Git 告诉我们只有 1 file changed (file4)!这是怎么回事?我们明明新增了两个文件 (file4 和 file5) 啊?
原因: 回忆一下我们前面说的,git commit 提交的是暂存区里的内容。在执行 git commit 命令时:
file4 已经通过 git add file4 被添加到了暂存区。file5 虽然在工作区被创建了,但我们没有对它执行 git add file5,所以它还在工作区,没有进入暂存区。因此,git commit 时只看到了暂存区里的 file4,就把 file4 提交了,而完全忽略了还在工作区的 file5。
如何提交 file5 呢? 非常简单,按照流程来:先 git add file5 把 file5 加到暂存区,然后再 git commit 一次。
这个例子再次强调用 commit 提交的是暂存区,而不是工作区!在你 commit 之前,一定要确保所有想提交的改动都已经通过 git add 进入了暂存区。
git status 和 git diff除了新增文件,修改已有文件是更常见的操作。Git 在这方面设计得非常高效,因为它跟踪并管理的是文件的修改,而不是整个文件。
'修改'可以有很多种:在你文件中新增一行、删除一行、更改几个字符、甚至改变文件名等等,Git 都能识别为'修改'。
让我们来修改一下之前创建的 ReadMe 文件:
# 查看 ReadMe 的当前内容
user@host:~/gitcode$ cat ReadMe
hello bit
hello git
# 修改 ReadMe 文件,比如增加一行
# hello bit
# hello git
# hello world
user@host:~/gitcode$ vim ReadMe
# 再次查看修改后的内容
user@host:~/gitcode$ cat ReadMe
hello bit
hello git
hello world
现在,工作区里的 ReadMe 文件内容和版本库中最新提交的 ReadMe 内容已经不一样了。Git 怎么知道哪些文件被修改了呢?
git statusgit status 命令是使用 Git 过程中最最常用的命令之一!它用于查看你当前工作区和暂存区的状态,告诉你哪些文件有改动,这些改动处于哪个阶段(在工作区还是暂存区)。
# 查看 Git 仓库状态
user@host:~/gitcode$ git status
On branch master
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: ReadMe
no changes added to commit
(use "git add" and/or "git commit -a")
git status 的输出信息告诉我们:
On branch master: 你当前在 master 分支上(分支概念后面会讲)。Changes not staged for commit:: 工作区有改动,但这些改动还没有添加到暂存区(还没有 add)。modified: ReadMe: ReadMe 文件被修改了。no changes added to commit: 暂存区是空的,没有任何准备要提交的内容。这个状态说明:你修改了 ReadMe 文件,但 Git 只是知道它被改了,还没有把这个改动记录到暂存区,更没有提交到版本库。它还在你的'办公桌'上。
git diffgit status 告诉我们文件被修改了,但具体改了哪些地方呢?这就需要用到 git diff 命令。
git diff 命令默认情况下,是用来查看工作区与暂存区之间文件差异的。
# 查看 ReadMe 文件在工作区和暂存区之间的差异
user@host:~/gitcode$ git diff ReadMe
diff --git a/ReadMe b/ReadMe
index 9c9e1f0..4a97140 100644
--- a/ReadMe # Diff 比较的'旧'文件(暂存区或最新提交的版本)
+++ b/ReadMe # Diff 比较的'新'文件(工作区的文件)
@@ -1,2 +1,3 @@ # 表示在原文件的第 1 行开始的 2 行,和新文件的第 1 行开始的 3 行之间有差异
hello bit
-hellogit # 减号开头的行表示在旧版本有,在新版本被删除了
+hello git # 加号开头的行表示在旧版本没有,在新版本被增加了
+hello world # 加号开头的行表示在新版本被增加了
git diff 的输出使用了标准的 diff 格式:
--- a/ 开头表示比较的'旧'文件(通常是暂存区或上一个版本的)。+++ b/ 开头表示比较的'新'文件(通常是工作区当前的)。@@ ... @@ 之间的信息表示差异发生的位置。- 开头的行表示这行在旧版本有,但在新版本被删除了。+ 开头的行表示这行在旧版本没有,但在新版本被新增了。通过 git diff ReadMe,我们清楚地看到在工作区,我们在 hello git 后面新增了一行 hello world,并且虽然 hello git 本身内容没变,但因为上面的行删除了,这里显示好像是删了又加了(Git 认为行号变了也算改动,或者这里的 diff 算法是按行匹配)。但重点是,你能清晰地看到 hello world 是新增的。
另一个常用的 diff 比较:工作区与版本库最新提交的差异
如果你想直接看工作区的文件和版本库里最新提交的版本有什么不同,可以使用 git diff HEAD -- [文件名] 命令:
# 查看工作区 ReadMe 文件和版本库最新提交版本 ReadMe 文件的差异
user@host:~/gitcode$ git diff HEAD -- ReadMe
这个命令会比较你当前工作区的状态和 HEAD 指针指向的那个提交(也就是当前分支的最新版本)的文件状态。了解这个有助于你区分工作区和暂存区在 diff 命令中的不同作用。
现在我们知道了 ReadMe 被修改了,并且通过 git diff 确认了修改内容。接下来,按照流程,把这个修改也保存到版本库中。
第一步:将修改添加到暂存区 (git add)
虽然文件是修改,不是新增,但流程一样,还是要先 add 到暂存区:
user@host:~/gitcode$ git add ReadMe
执行 git add ReadMe 后,ReadMe 文件在工作区里的修改就被添加到了暂存区。暂存区里现在包含了 ReadMe 文件修改后的新内容。
再次查看状态 (git status):
现在 ReadMe 的修改已经在暂存区了,我们再看看状态:
user@host:~/gitcode$ git status
On branch master
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: ReadMe
这次的输出变了!ReadMe 文件出现在了 Changes to be committed: 这个区域下,并且前面显示 modified: ReadMe。这表明 ReadMe 文件在暂存区里有改动,这些改动已经准备好被提交到版本库了。
第二步:将暂存区内容提交到版本库 (git commit)
暂存区里有内容了,我们就可以执行 commit 命令来保存这个修改版本了:
# 提交暂存区的修改到版本库,并写上提交信息
user@host:~/gitcode$ git commit -m "add modify ReadMe file"
[master 94da695] add modify ReadMe file
1file changed, 2 insertions(+), 1 deletion(-)
# 这次修改的结果:1 个文件改动,增加了 2 行,删除了 1 行
提交成功!ReadMe 文件的这次修改已经被作为一个新的版本永久保存在版本库里了。
最后查看状态 (git status):
提交完成后,工作区和暂存区都应该是干净的(和版本库最新提交的状态一致)。
user@host:~/gitcode$ git status
On branch master
nothing to commit, working tree clean
nothing to commit, working tree clean:这说明当前工作区和暂存区都没有任何需要提交的改动,它们的状态是干净的,和版本库里最新的版本是同步的。这是一个很好的状态!
通过上面的学习,我们现在对 Git 的基本操作流程有了清晰的认识:
git status 查看工作区和暂存区的状态,了解哪些文件有改动,它们处于哪个阶段。git diff [文件名] 查看工作区里的文件相对于暂存区的具体修改内容。git add [文件名] 或 git add . 将工作区中准备提交的改动放入暂存区。git commit -m "有意义的提交信息" 将暂存区的内容作为一个新版本提交到版本库。git log 查看已经提交的版本历史。理解工作区 -> 暂存区 -> 版本库 这个流程,以及 git add 和 git commit 在其中扮演的角色,是掌握 Git 的关键。每次改动都需要经过 add 和 commit 两步(或类似操作)才能真正被 Git 永久记录下来。
接下来,我们将继续学习 Git 的其他常用操作,比如如何回退版本,如何忽略文件等等。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML 转 Markdown 互为补充。 在线工具,Markdown 转 HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML 转 Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online