本篇文章从零开始,带你完成 Git 的安装与配置,深入理解工作区、暂存区、版本库三个核心概念,并掌握添加、修改、回退、撤销、删除等 Git 基本操作,为后续深入学习打下坚实基础。
Git 基本操作
1.1创建 Git 本地仓库
要提前说的是,仓库是进⾏版本控制的⼀个⽂件⽬录 。我们要想对⽂件进⾏版本控制,就必须先创建⼀个仓库出来。创建⼀个 Git 本地仓库对应的命令为 git init,注意命令要在⽂件⽬录下执⾏,例如:


我们发现,当前⽬录下多了⼀个 .git 的隐藏⽂件, .git ⽬录是 Git 来跟踪管理仓库的,不要⼿动
修改这个⽬录⾥⾯的⽂件,不然改乱了,就把 Git 仓库给破坏了。其中包含 Git 仓库的诸多细节,

.git 目录的核心组成,用这个表说明情况,
| 文件/目录 | 一句话说明 |
|---|---|
objects/ |
存放所有历史版本的文件内容(最核心) |
refs/ |
记录分支和标签指向哪个版本 |
HEAD |
标记你当前在哪个分支上 |
config |
当前仓库的配置(用户名、远程地址等) |
hooks/ |
某些操作时自动执行的脚本(如提交前自动检查) |
日常开发时,谁在服务谁
| Git 命令 | 读/写了 .git 的哪个部分 |
|---|---|
git commit |
新增 objects/,更新 refs/heads/(分支最新指向) |
git checkout |
读 refs/heads/,改 HEAD(切换分支) |
git branch |
增删 refs/heads/ 中的分支文件 |
git config |
读写 config |
git push |
读 objects/ + refs/,发送给远程仓库 |
1.2配置 Git
当安装 Git 后⾸先要做的事情是设置你的 ⽤⼾名称 和 e-mail 地址,这是⾮常重要的。配置命令为:
cpp
git config [--global] user.name "Your Name"
git config [--global] user.email "email@example.com"
# 把 Your Name 改成你的昵称
# 把 email@example.com 改成邮箱的格式,只要格式正确即可。

其中 --global 是⼀个可选项。 如果使⽤了该选项,表⽰这台机器上所有的 Git 仓库都会使⽤这个配置。如果你希望在不同仓库中使⽤不同的 name 或 e-mail ,可以不要 --global 选项,但要注意的是,执⾏命令时必须要在仓库⾥
查看配置命令为:
cpp
git config -l

删除对应的配置命令为:
cpp
git config --global --unset user.name
git config --global --unset user.email

1.3⼯作区、暂存区、版本库
工作区(Working Directory):即你电脑上存放项目代码和文件的实际目录,也就是你能直接看到和编辑的那些文件所在的文件夹。你在 IDE 中打开、修改、保存的文件,都位于工作区中。
暂存区(Stage / Index):也叫索引(Index),位于
.git/index文件中。它是一个临时存储区域,用于记录你下一次要提交的内容。当你执行git add时,文件的快照就会被写入暂存区。版本库(Repository):即项目根目录下的
.git隐藏文件夹。它不算工作区,而是 Git 的"数据仓库"。版本库中存储了所有提交历史、分支信息、标签以及完整的对象数据库(objects/)。只要.git目录完好无损,整个项目的所有历史版本就都还在。
三者之间的关系与数据流转:
cpp
工作区 (Working Directory) ── git add ──> 暂存区 (Stage / Index) ── git commit ──> 版本库 (Repository / .git)
简单来说:工作区 是你干活的地方,暂存区 是你整理准备提交的地方,版本库是 Git 永久保存所有历史版本的地方。
我们用一张图来直观理解:
| 区域 | 对应位置 | 核心命令 |
|---|---|---|
| 工作区 | 项目目录(不含 .git) |
git status 查看修改 |
| 暂存区 | .git/index |
git add 写入暂存区 |
| 版本库 | .git/ |
git commit 永久保存 |
补充:三个区域的核心区别
| 对比维度 | 工作区 | 暂存区 | 版本库 |
|---|---|---|---|
| 位置 | 项目根目录(不含 .git) |
.git/index |
.git/ 目录 |
| 内容来源 | 你自己修改编辑的文件 | git add 添加的快照 |
git commit 提交的对象 |
| 是否被 Git 追踪 | 部分可能未被追踪 | 即将被提交 | 已永久保存 |
| 误操作是否可恢复 | 未追踪的文件丢失较难恢复 | 可用 git restore --staged 撤回 |
几乎永久可恢复 |
补充:常用场景对应关系(这个里面的有些格式后面我会慢慢一一讲解)
| 场景 | 涉及区域 | 命令 |
|---|---|---|
| 修改了一个文件 | 工作区 | 直接在 IDE 或编辑器中修改 |
| 准备提交某个文件 | 工作区 → 暂存区 | git add 文件名 |
| 把所有修改都准备提交 | 工作区 → 暂存区 | git add . |
| 正式提交保存 | 暂存区 → 版本库 | git commit -m "信息" |
| 查看当前状态 | 三个区域对比 | git status |
| 查看工作区与暂存区的差异 | 工作区 vs 暂存区 | git diff |
| 查看暂存区与版本库的差异 | 暂存区 vs 版本库 | git diff --cached |
| 撤销工作区的修改 | 工作区 | git restore 文件名 |
| 撤销暂存区的添加 | 暂存区 | git restore --staged 文件名 |
下⾯这个图展⽰了⼯作区、暂存区和版本库之间的关系:
图片补充:
| 概念 | 准确含义 |
|---|---|
HEAD |
指向当前所在分支的指针。例如 HEAD 指向 master,说明你在 master 分支上。它本质上是一个引用,而不是一个独立的数据存储区域。 |
master |
分支名,指向该分支最新的一次提交。当你在 master 分支上执行 git commit 时,master 指针会自动更新,指向新提交。 |
| 暂存区(Stage) | 本质上是一个文件列表,记录了下一次 commit 要包含哪些文件的哪些版本,位于 .git/index。 |
| 版本库(Repository) | 就是 .git 目录本身,包含了 objects、refs、HEAD、config 等全部 Git 元数据。 |
- 图中左侧为⼯作区,右侧为版本库。Git 的版本库⾥存了很多东西,其中最重要的就是暂存区。
- 在创建 Git 版本库时,Git 会为我们⾃动创建⼀个唯⼀的 master 分⽀,以及指向 master 的⼀个指针叫 HEAD。(分⽀和HEAD的概念后⾯再说)
- 当对⼯作区修改(或新增)的⽂件执⾏ git add 命令时,暂存区⽬录树的⽂件索引会被更新。
- 当执⾏提交操作 git commit 时,master 分⽀会做相应的更新,可以简单理解为暂存区的⽬录树才会被真正写到版本库中。
由上述描述我们便能得知:通过新建或粘贴进⽬录的⽂件,并不能称之为向仓库中新增⽂件,⽽只是在⼯作区新增了⽂件。必须要通过使⽤ git add 和 git commit 命令才能将⽂件添加到仓库中进⾏管理!!
1.4添加⽂件--场景⼀
在包含 .git 的⽬录下新建⼀个 ReadMe ⽂件,我们可以使⽤ git add 命令可以将⽂件添加到暂存
区:
- 添加⼀个或多个⽂件到暂存区: git add file1 file2 ...
- 添加指定⽬录到暂存区,包括⼦⽬录: git add dir
- 添加当前⽬录下的所有⽂件改动到暂存区: git add .
再使⽤ git commit 命令将暂存区内容添加到本地仓库中:
- 提交暂存区全部内容到本地仓库中: git commit -m "message"
- 提交暂存区的指定⽂件到仓库区: git commit file1 file2 ... -m "message"
注意 git commit 后⾯的 -m 选项,要跟上描述本次提交的 message,由⽤⼾⾃⼰完成,这部分内
容绝对不能省略,并要好好描述,是⽤来记录你的提交细节,是给我们⼈看的。
例如:

git commit 命令执⾏成功后会告诉我们,1个⽂件被改动(就是我们新添加的ReadMe⽂件),插
⼊了两⾏内容(ReadMe有两⾏内容)。
我们还可以多次 add 不同的⽂件,⽽只 commit ⼀次便可以提交所有⽂件,是因为需要提交的⽂件是通通被 add 到暂存区中,然后⼀次性 commit 暂存区的所有修改。如:

截⾄⽬前为⽌,我们已经更够将代码直接提交⾄本地仓库了。我们可以使⽤ git log 命令,来查看
下历史提交记录:
bash
git log 显示完整提交历史
命令 :git log
作用 :查看当前分支的提交历史
输出内容 :commit哈希值、Author、Date、提交注释

通过
git log可以查看当前仓库的完整提交历史,每条记录都包含一个由 Git 自动生成的 SHA-1 哈希值(40位十六进制字符串)作为提交的唯一标识,以及提交的作者、时间和注释信息。所有提交按时间倒序排列,最近的提交显示在最上面。图片中显示该仓库当前在master分支上,共有 2 次提交记录:最早的一次是first file,最近的一次是add 3 files,最新提交的哈希值为9f0865d...。日常使用中,推荐使用git log --oneline以更简洁的方式查看提交历史,每个提交只显示哈希值的前 7 位和注释信息,更加清爽易读。
该命令显⽰从最近到最远的提交⽇志,并且可以看到我们 commit 时的⽇志消息。
如果嫌输出信息太多,看得眼花缭乱的,可以试试加上**--pretty=oneline 参数:**
git log --oneline 精简显示,每行一个提交(最常用)

查看 .git ⽂件
先来看看我们的 .git 的⽬录结构:

需要注意的地方
| 现象 | 说明 |
|---|---|
objects/ 里的目录名是 2位十六进制(如 9f/、e7/) |
这就是 commit 对象哈希值的前两位 |
index 文件出现 |
说明暂存区有内容(你执行过 git add) |
logs/ 目录出现 |
Git 开始记录操作日志,方便 git reflog 恢复历史 |
COMMIT_EDITMSG 文件 |
是你最后一次 commit 时写的提交信息,临时文件 |
refs/heads/master 文件 |
记录 master 分支当前指向哪个 commit |
HEAD 文件 |
记录当前在哪个分支上 |
通过**
tree .git/**可以清晰地看到 Git 版本库在磁盘上的物理存储结构。所有 Git 对象(commit、tree、blob)都存储在**
.git/objects/**目录下,每个对象以其完整 40 位 SHA-1 哈希值的前 2 位作为子目录名、后 38 位作为文件名进行存放。图片中出现的
e7/和9f/目录分别对应两次提交的 commit 对象,而30/、39/、d3/、e6/等目录则对应 tree 对象和 blob 对象,它们是记录目录结构和文件实际内容的载体。此外,
.git/目录中的index文件代表暂存区的内容,logs/目录记录操作日志以支持git reflog恢复历史,refs/heads/master文件记录**master** 分支当前指向的提交,HEAD文件则标记当前所在的分支。本质上,.git/ 目录就是 Git 仓库的完整数据库,每次 git commit 都会在其中新增至少 3 个对象,确保所有历史版本都得到完整保存。
1. index就是我们的暂存区,add 后的内容都是添加到这⾥的。
2.HEAD 就是我们的默认指向 master 分⽀的指针:

⽽默认的 master 分⽀,其实就是:

9f0865dc0a82c076237e642e8da035d7e7056cea是什么东西呢?保存的就是当前最新
的 commit id
3. objects为 Git 的对象库,⾥⾯包含了创建的各种版本库对象及内容。当执⾏ git add 命令时,暂存区的⽬录树被更新,同时⼯作区修改(或新增)的⽂件内容被写⼊到对象库中的⼀个新的对象中,就位于 ".git/objects" ⽬录下,让我们来看看这些对象有何⽤处:

三种基本用法
| 命令 | 含义 |
|---|---|
git cat-file -t 对象哈希 |
查看对象的类型(commit / tree / blob / tag) |
git cat-file -s 对象哈希 |
查看对象的大小(字节数) |
git cat-file -p 对象哈希 |
查看对象的内容(美化打印) |
其中 -p 就是你刚才想用的,意为 pretty-print(美化打印)。
查找 object 时要将 commit id 分成2部分,其前2位是⽂件夹名称,后38位是⽂件名称。
找到这个⽂件之后,⼀般不能直接看到⾥⾯是什么,该类⽂件是经过 sha (安全哈希算法)加密过的⽂件,好在我们可以使⽤ git cat-file 命令来查看版本库对象的内容:

这就是我们最近⼀次的提交
解释如下:
| 行内容 | 名词 | 含义 |
|---|---|---|
tree 30e20ee... |
tree(目录树对象) | 记录这次提交里有哪些文件、文件名是什么、分别对应哪个 blob 对象,相当于一个目录快照 |
parent e747883... |
parent(父提交) | 指向这次提交的上一次提交,把两次提交串成一条历史链。第一次提交没有这一行 |
author hjq <...> 1784817876 +0800 |
author(作者) | 这个文件的原始作者 是谁,以及提交的时间 |
committer hjq <...> 1784817876 +0800 |
committer(提交者) | 把这次提交录入到仓库的人。通常和 author 一样,只会在打补丁、转交提交等场景下不同 |
add 3 files |
commit message(提交注释) | 你写的那句提交说明,git commit -m "这里的内容" |
Git 会自动给时间戳加上时区,你这里的 +0800 表示东八区,也就是北京时间。author 和 committer 一般都是同一个人,不会特意区分。但在开源项目里(比如 Linux 内核),如果一个人写补丁、另一个人负责合入,那这两个字段就会不同:author 是原作者,committer 是合入者。

这就是第一次提交 first file 的信息,相关解释如下:
其中,还有⼀⾏ tree 30e20ee76836a7edd4b370121927bc6668ecc3d5 **,我们使⽤同样的⽅
法,看看结果,**继续深入:查看 tree 对象

在看 ReadMe 对应 390ca7a5098bcfa36a43dbf8ba5aa206dfa092ee:

这是我们对ReadMe做的修改,被git记录了下来
总结⼀下,在本地的 git 仓库中,有⼏个⽂件或者⽬录很特殊
- index: 暂存区, git add 后会更新该内容。
- HEAD: 默认指向 master 分⽀的⼀个指针。
- refs/heads/master: ⽂件⾥保存当前 master 分⽀的最新 commit id 。
- objects: 包含了创建的各种版本库对象及内容,可以简单理解为放了 git 维护的所有修改。
1.5添加⽂件--场景⼆
和大家学习到这⾥,我们已经清楚了如何向仓库中添加⽂件,并且对于⼯作区、暂存区、版本库也有了⼀定的认识。那么我们再展⽰⼀种添加⽂件的场景,能加深对⼯作区、暂存区、版本库的理解,⽰例如下:

提交后发现打印了 1 file changed, 0 insertions(+), 0 deletions(-) ,意思是只
有⼀个⽂件改变了,这时我们提出了疑问,不是新增了两个⽂件吗?再来回忆下, git add 是将⽂件添加到暂存区, git commit 是将暂存区的内容添加到本地仓库中。由于我们并没有使⽤ git add file5 ,file5 就不在暂存区中维护,所以我们 commit 的时候其实只是把已经在暂存区的 file4 提交了,⽽遗漏了⼯作区的 file5。
如何提交 file5 呢?
很简单,再次add , commit 即可。
修改⽂件
Git ⽐其他版本控制系统设计得优秀,因为 Git 跟踪并管理的是修改,⽽⾮⽂件。
什么是修改?⽐如你新增了⼀⾏,这就是⼀个修改,删除了⼀⾏,也是⼀个修改,更改了某些字符,也是⼀个修改,删了⼀些⼜加了⼀些,也是⼀个修改,甚⾄创建⼀个新⽂件,也算⼀个修改。
让我们将 ReadMe ⽂件进⾏⼀次修改:常用选项速查

此时,仓库中的 ReadMe 和我们⼯作区的 ReadMe 是不同的,如何查看当前仓库的状态呢?
git status 命令⽤于查看在你上次提交之后是否有对⽂件进⾏再次修改。

上⾯的结果告诉我们,ReadMe 被修改过了,但还没有完成添加与提交。⽬前,我们只知道⽂件被修改了,如果能知道具体哪些地⽅被修改了,就更好了。有人会说,我刚改的我知道呀!可是,你还记得你三天前写了什么代码吗?或者没写?

cpp
diff --git a/ReadMe b/ReadMe
a/ReadMe:旧版本(暂存区里的版本)
b/ReadMe:新版本(工作区里的版本)
index 390ca7a..2a33abb 100644
390ca7a:旧版本(暂存区)的 blob 哈希值(前7位)
2a33abb:新版本(工作区)的 blob 哈希值(前7位)
100644:文件权限(普通文件)
--- a/ReadMe
+++ b/ReadMe
---:旧文件(暂存区)
+++:新文件(工作区)
@@ -1,3 +1,5 @@
-1,3:旧文件从第 1 行开始,共 3 行
+1,5:新文件从第 1 行开始,共 5 行
hello git
hello git
+
+hello world
没有 - 号的行:新旧版本共有的内容(hello git)
+ 号的行:新增的内容(hello world)
**git diff file 命令⽤来显⽰暂存区和⼯作区⽂件的差异,显⽰的格式正是Unix通⽤的diff格式。**也可以使⽤ git diff HEAD -- file 命令来查看版本库和⼯作区⽂件的区别。知道了对 ReadMe 做了什么修改后,再把它提交到本地仓库就放⼼多了。

git add 之后,就没有看到上⾯ no changes added to commit (use "git add"and/or "git commit -a") 的消息了。接下来让我们继续 git commit 即可:

在
master分支上提交成功,提交的哈希值是e87a95c,有 1 个文件被修改,新增了 2 行内容

我现在在
master分支上,file5是一个未被追踪的新文件(从未被git add过)除了
file5之外,没有其他需要提交的内容
补充:file5 需要提交吗?
根据我的实际需求,有三种处理方式:
需要提交 file5 :执行
git add file5将 file5 添加到暂存区,再执行git commit -m "add file5"提交即可。不需要提交 file5 :暂时不管它,它会一直显示在
git status的未追踪文件列表中;如果确定不需要这个文件,可以直接删除:rm file5。以后再说:保持现状,file5 仍然是一个未被追踪的文件,不会影响其他操作。下次提交时再决定是否要把它纳入版本管理。
**核心原则:**Git 只会提交被 git add 过的文件。工作区中未被追踪的文件不会随 git commit 被保存到版本库中
总结一下:
git add之后,暂存区就有了内容,git status不会再提示"no changes added to commit";git commit把暂存区的内容提交到了版本库,但file5没有被git add,所以它还在工作区,不会随本次提交被保存。
1.6版本回退
之前我们也提到过,Git 能够管理⽂件的历史版本,这也是版本控制器重要的能⼒。如果有⼀天你发现之前前的⼯作做的出现了很⼤的问题,需要在某个特定的历史版本重新开始,这个时候,就需要版本回退的功能了。
执行
git reset命令用于回退版本,可以指定退回到某一次提交。需要理解的是,"回退"本质上只是将版本库中的内容进行回退,而工作区或暂存区是否同步回退,则由命令参数决定。
git reset 的命令语法格式为:
cpp
git reset [--soft | --mixed | --hard] [HEAD]
三种参数的区别:
--mixed(默认选项,可不带该参数):将暂存区 回退到指定版本,工作区 文件保持不变。适用于想撤销git add但又不想丢掉工作区改动的场景。
--soft:工作区和暂存区的内容都不变,只将版本库回退到指定版本。适用于想重新调整提交信息或合并多次提交的场景。
--hard:将暂存区和工作区都回退到指定版本。使用前务必确认工作区没有未提交的代码,因为该参数会丢弃工作区的所有修改且不可恢复,请谨慎使用。
HEAD 的写法说明:
直接写
commit id:表示回退到指定的某次提交
HEAD:当前版本
HEAD^:上一个版本
HEAD^^:上上一个版本(以此类推,^的个数代表回退的步数)
也可以用 ~ 加数字表示:
HEAD~0或HEAD~:当前版本
HEAD~1:上一个版本
HEAD~2:上上一个版本以此类推
使用建议: 在日常开发中,
--soft适合合并提交,--mixed适合撤销暂存,--hard仅在确认丢弃所有本地改动时使用。如果不确定,建议先用git status和git diff查看当前状态,避免误操作。
为了便于表述,⽅便测试回退功能,我们先做⼀些准备⼯作:更新3个版本的 ReadMe,并分别进⾏3次提交,如下所⽰:
第⼀次修改提交

master:在 master 分支上提交
87e130d:本次提交的哈希值(前7位)
1 file changed:有 1 个文件被修改
2 insertions(+):新增了 2 行内容
第⼆次修改提交

第三次修改提交

查看历史提交记录

现在,如果我们在提交完 version3 后, 发现 version 3 编写错误,想回退到 version2,重新基于
version 2 开始编写。由于我们在这⾥希望的是将⼯作区的内容也回退到 version 2 版本,所以需
要⽤到 --hard 参数,⽰例如下:


我们惊奇的发现,此时 ReadMe ⽂件的内容,已经回退到 version2 了!,当前,我们再次⽤ git
log 查看⼀下提交⽇志,发现 HEAD 指向了version2,如下所⽰:

到这⾥⼀般回退功能就演⽰完了,但现在如果我后悔了,想再回到 version 3 怎么办?我们可以继续使⽤git reset 命令,回退到 version 3 版本,但我们必须要拿到 version 3 的 commit
id 去指定回退的版本。
但我们看到了 git log 并不能打印出 version 3 的 commit id ,运⽓好的话我们可以从终端
上去找找之前的记录,运⽓不好的话 commit id 已经被我们搞丢了。
但是,**Git 还提供了⼀个 git reflog 命令能补救⼀下,**该命令⽤来记录本地的每⼀次命令。这样,你就可以很方便地找到你的所有操作记录了。

但问题来了:d79e182 是啥东西?这个是 add version3 的 commit id(部分)。没错,Git 版本回退的时候,也可以使用部分 commit id 来代表目标版本。
例如,现在你想回到 version3,只需要执行:
cpp
git reset --hard d79e182
Git 会自动识别出这是哪个完整的 commit id,并帮你完成回退。
补充说明
| 你的 commit id | 对应版本 | 说明 |
|---|---|---|
d79e182 |
add version3 |
最新的一次提交(第三次) |
ec57baa |
add version2 |
第二次提交(当前 HEAD 指向这里) |
87e130d |
add version1 |
第一次提交 |
你当前的 HEAD 指向 ec57baa(version2),但通过 git reflog 可以看到 d79e182(version3)依然存在,只是因为 HEAD 移动了,git log 默认不显示它而已。
说白了就是:git reflog****记录了本地仓库的所有操作历史,是找回"丢失"提交的利器。即使git log看不到某个提交,只要git reflog里还有记录,就能用git reset --hard <commit-id>恢复回来。而且 Git 支持使用 commit id 的前几位作为简写,只要不重复即可。
回退到v3

查看⼯作区

查看log

可往往是理想很丰满,现实很⻣感。在实际开发中,由于⻓时间的开发了,导致 commit id 早就找不到了,可突然某⼀天,我⼜想回退到 version3,那该如何操作呢?貌似现在不可能了。
值得说的是,Git 的版本回退速度⾮常快,因为 Git 在内部有个指向当前分⽀(此处是master)的
HEAD 指针, refs/heads/master ⽂件⾥保存当前 master 分⽀的最新 commit id 。当我们
在回退版本的时候,Git 仅仅是给 refs/heads/master 中存储⼀个特定的version,可以简单理解
成如下⽰意图:

1.7撤销修改
如果我们在我们的⼯作区写了很⻓时间代码,越写越写不下去,觉得⾃⼰写的实在是垃圾,想恢复到上⼀个版本
1.情况⼀:对于⼯作区的代码,还没有 add
向ReadMe中新增⼀⾏代码

直接删除代码


⾟亏我们⼯作效率不⾼,才写了⼀⾏代码就发现不⾏了,要是你写了3天,⼀直都没有提交,该怎么删掉呢?你⾃⼰都忘了⾃⼰新增过哪些,有同学说,我可以 git diff xxx ⼀下,看看差别在删啊,
那你肯定⼜要花3天时间删代码了,并且很⼤的概率还会改出bug。⼀周过去了,你怎么向你的⽼板交代呢?
Git 其实还为我们提供了更好的⽅式,我们可以使⽤ git checkout -- file 命令让⼯作区的
⽂件回到最近⼀次 add 或 commit 时的状态。 要注意 git checkout -- file 命令中的
-- 很重要,切记不要省略,⼀旦省略,该命令就变为其他意思了,后⾯我们再说。⽰例如下:
向ReadMe中新增⼀⾏代码

恢复到上⼀次 add 或 commit

2.情况⼆:已经 add ,但没有 commit
add 后还是保存到了暂存区呢?怎么撤销呢?
向ReadMe中新增⼀⾏代码

add 存⼊暂存区

回忆⼀下学过的 git reset 回退命令,该命令如果使⽤ --mixed 参数,可以将暂存区
的内容退回为指定的版本内容,但⼯作区⽂件保持不变。那我们就可以回退下暂存区的内容了。⽰例如下:
--mixed 是 git reset 的默认参数,使用时可以省略。该参数的作用是:将暂存区的内容回退到指定版本,但工作区的内容保持不变。
例如,执行以下命令可以将 ReadMe 文件从暂存区移除(即撤销 git add):
cpp
git reset HEAD -- ReadMe
注意 :
--mixed是默认选项,所以可以不写。--用于明确区分参数和文件路径,防止因文件名与分支名重名而产生歧义。
执行后,Git 会提示
其中 M 表示 ReadMe 文件已被修改(Modified),但此时该修改仅存在于工作区,暂存区中已经没有该文件的记录了。
⽤ git status 查看⼀下,发现现在暂存区是⼲净的,⼯作区有修改。

还记得如何丢弃工作区中未暂存的修改,将文件恢复到上一次提交的状态?
执行命令
cpp
git checkout -- ReadMe
作用 :丢弃工作区中
ReadMe文件的所有修改,将其恢复到暂存区或版本库中的状态。
命令格式说明
cpp
git checkout -- <文件名>
--:分隔符,用于明确区分分支名和文件名(防止文件名与分支名重名时产生歧义)
<文件名>:要恢复的文件


总结:git checkout -- ReadMe的作用是丢弃工作区中指定文件的修改,将其恢复到最近一次git add或git commit时的状态。
3.情况三:已经 add ,并且也 commit 了
不要担⼼,我们可以 git reset --hard HEAD^ 回退到上⼀个版本!不过,这是有条件的,就是
你还没有把⾃⼰的本地版本库推送到远程。还记得Git是分布式版本控制系统吗?我们后⾯会讲到远程版本库,⼀旦你推送到远程版本库,就真的后果有点严重。
向ReadMe中新增⼀⾏代码

提交

回退

1.8删除⽂件
在 Git 中,删除也是⼀个修改操作,我们实战⼀下, 如果要删除 file5 ⽂件,怎么搞呢?如果你这样
做了

但这样直接删除是没有⽤的,反⽽徒增烦恼, git status 命令会⽴刻告诉你哪些⽂件被删除了:
但会发现我这个没显示:原因如下:
因为你删的
file5从来没有被 Git 追踪过(你之前只git add和git commit过file1、file2、file3、file4和ReadMe,file5只是创建了,从未add过)。Git 的
status只关心它认识的、曾经管理过的文件。对它来说,file5是个"局外人",进进出出 Git 根本不管。

对比file1

此时,⼯作区和版本库就不⼀致了,要删⽂件,⽬前除了要删⼯作区的⽂件,还要清除版本库的⽂
件。
⼀般⾛到这⾥,有两种可能:
- 确实要从版本库中删除该⽂件
- 不⼩⼼删错了
对第⼆种情况,很明显误删,需要使⽤ git 来进⾏恢复,很简单,我们刚学过(删除也是修改):

有人会说那file5呢?我的答案是它无法恢复在这里看

为什么 git checkout -- file5 会报错?
在上面的操作中,我们执行 git checkout -- file5 时,遇到了这样一个错误:
cpp
error: pathspec 'file5' did not match any file(s) known to git
翻译过来就是:file5 这个文件不在 Git 的"花名册"里,Git 根本不认识它,所以没法帮你恢复。
根本原因在于:
git checkout -- 文件名只能恢复被 Git 追踪过的文件 。所谓的"被追踪",是指该文件曾经被git add过并提交到了版本库中,Git 的版本库里存有它的历史记录,因此才能将其恢复。
回顾我们的操作:
cpp
rm file5 file5 从未被 git add 过,是未被追踪的文件
git checkout -- file5 报错!Git 不认识它
而 file1 就不一样了:
cpp
rm file1 file1 之前被 git add 和 git commit 过,是被追踪的文件
git checkout -- file1 成功恢复!
因为 file1 之前被 git add 并提交过,Git 的版本库里存有它的副本,所以能顺利恢复。
结论 :
git checkout -- 文件名只能恢复那些曾经被 Git 追踪过的文件。对于从未被git add过的文件,删除后就真的消失了------Git 也无能为力。所以,当你创建一个新文件并打算长期保留时,记得及时git add和git commit,让它进入 Git 的"保护名单"。一句话说明白:没有被 Git 追踪过的文件,删了就真的没了,Git 也没办法。
对于第⼀种情况,很明显是没有删完,我们只删除了⼯作区的⽂件。这时就需要使⽤ git rm 将⽂
件从暂存区和⼯作区中删除,并且 commit :

rm vs git rm 对比
| 对比维度 | rm file |
git rm file |
|---|---|---|
| 工作区 | 删除 | 删除 |
| 暂存区 | 保留(索引中仍有该文件记录) | 删除(索引记录同步移除) |
| 是否自动暂存删除操作 | 否,需要手动 git add 或 git commit -a |
是,删除操作已自动进入暂存区 |
| 后续操作 | 需要额外执行 git add 或 git commit -a |
直接执行 git commit 即可完成删除 |
| Git 状态提示 | Changes not staged for commit: deleted: file |
Changes to be committed: deleted: file |
一句话讲明白
rm file 只删工作区,暂存区里还有残留记录,需要再处理一次才能提交删除;git rm file 一次性把工作区和暂存区都删干净,直接 commit 就能完成删除操作。简单说:rm 只管删文件,git rm 既删文件又告诉 Git "我要删它了"。
1.9Git reset 三种模式
先回顾一下 Git 的三个区域:

-
工作区:就是你电脑上打开文件夹看到的那些文件,能直接改
-
暂存区 :
git add之后文件放的地方,相当于"候车室" -
版本库 :
git commit之后存下来的版本,每一次提交就是一张"照片"
开始我们统一在 ReadMe 里写 git world,三个区域都一样。然后执行回退,把版本库从 git world 退回到 git,看三个区域分别怎么变。
2.三种模式
1. git reset --soft HEAD~1
cpp
git reset --soft HEAD~1
变完啥样,用这个表格告诉你:
| 区域 | 变成啥了 |
|---|---|
| 版本库 | git |
| 暂存区 | 还是 git world |
| 工作区 | 还是 git world |
用人话说: 就动了指针,其他啥也没碰。原来在暂存区的东西还在,不用重新
git add,直接git commit就能再来一次。啥时候用: 我自己的理解是------比如写了一上午代码,零零碎碎 commit 了四五次,想把这些合成一个有意义的提交,就用这个。课上老师举的例子是"合并提交记录"。
2. git reset --mixed HEAD~1(默认)
不写参数的时候默认就是这个,git reset HEAD~1 就是它。
cpp
git reset HEAD~1
变完啥样,表格告诉你:
| 区域 | 变成啥了 |
|---|---|
| 版本库 | git |
| 暂存区 | git |
| 工作区 | 还是 git world |
用人话说: 指针移了,暂存区也清了,但工作区的修改还在。相当于
git add被撤回来了,文件变成"改了但还没 add"的状态。啥时候用: 这个我踩过坑。有一次我
git add .把所有文件都扔进去了,后来发现有个文件不应该提交,就用这个把暂存区清空,重新挑着 add。老师讲的时候说这叫"撤销暂存"。注意**:** 工作区的修改不会丢,放心用。
3. git reset --hard HEAD~1
cpp
git reset --hard HEAD~1
变完啥样,表格如下:
| 区域 | 变成啥了 |
|---|---|
| 版本库 | git |
| 暂存区 | git |
| 工作区 | git |
用人话说: 三个区域全部同步回退。工作区里还没提交的修改,直接没掉。
这个有一个坑,后面细说。
啥时候用: 老师说一般是"彻底放弃所有改动,强制回到某个历史版本"的时候用。我自己是觉得,除非你确定当前改的东西完全不要了,否则别碰它。
3.三个模式对比
| 参数 | 版本库 | 暂存区 | 工作区 | 一句话人话 |
|---|---|---|---|---|
--soft |
回退 | 不变 | 不变 | 只挪指针,啥也不动 |
--mixed |
回退 | 回退 | 不变 | 撤销 add,修改还在 |
--hard |
回退 | 回退 | 回退 | 全部回去,没提交的没了 |
4.注意要点
先说 --hard 的教训。
有一次我改了代码,没 commit,但是我执行了 git reset --hard,改的东西全没了。还好我可以用 git reflog 找回。后面就如果,非要用 --hard 的话,先执行 git stash:
bash
git stash 把现在的修改临时存起来
git reset --hard HEAD~1
git stash pop 需要的时候再恢复
虽然多敲两行,但保,再说一个容易忽略的点:
git reset只影响本地仓库。如果代码已经 push 到远程了,本地 reset 完再 push 会报错,得用git push -f。这个-f在小组项目里要小心,会把别人的提交覆盖掉。
5.简单小结
自己整理了一遍之后,总结了三句话:
想合并提交 -->
--soft想撤销
git add-->--mixed(默认)
--hard少用,非要用先stash
思考题:万一不小心
git reset --hard了,除了git reflog还有别的办法吗?
有,但有限。
git reflog是最常用的恢复方式,如果它也不行,还可以试试git fsck --lost-found,这个命令会找出所有"丢失"的提交对象,不过找出来的东西比较乱,需要自己一个一个翻。另外,如果你用的是 VS Code、IDEA 这类编辑器,可以看看编辑器的本地历史记录,有时候能找回单个文件的内容。再不行就只能靠操作系统的回收站或者数据恢复工具了,但那个成功率就看运气了。
不过说实话,与其想着怎么恢复,不如养成好习惯:执行
git reset --hard之前,先git stash把当前修改存起来,或者干脆手动复制一份备份。再好的恢复手段,也不如提前做好预防。