Git 使用指南之时光穿梭机

上一篇博文你已经了解了 Git 版本控制系统的基本概念,不同平台下 Git 的安装以及相关配置,以及 Git 版本库初识。并且在上一篇博文中我们已经成功地演示了如何使用 Git 管理一个既存项目(GitTestProject),下面我们将基于这个版本库为实例继续来看 Git 版本库日常管理的常规操作

叮咚~~,完成当前博文学习,即可达成 “初步项目版本管理” 成就!!!

版权说明: 本文思路以及内容主要来自廖雪峰老师的 Git 教程 (强烈推荐膜拜原文),并结合个人使用所作,只作为学习记录使用。如内容有侵权请联系删除,禁止转载!

更多 Git 相关内容,请关注博主 Git 博文系列:

之一 >>> Git 使用指南之初识

之二 >>> Git 使用指南之时光穿梭机

之三 >>> Git 使用指南之远程仓库

之四 >>> Git 使用指南之分支管理

之五 >>> Git 使用指南之 WorkFlow (工作流)

之六 >>> Git 使用指南之 Git 自定义

之七 >>> Git 使用指南之 HEAD 指针

之八 >>> Git 使用指南之 Git 中的黑魔法

| >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

上篇博文中 GitTestProject 版本库目录结构说明:

GitTestProject 版本库包含:Server 目录、Client 目录、以及 readme.txt 说明文档。

Server 目录下包含一个名为:service.py 的文件,其内容为:

1
# This is a Test!

Client 目录视作是一个仅包含 Git 占位文件(.gitignore)的空目录。

readme.txt 文件内容为:

1
2
Git is a version control system.
Git is free software.

==============================================================|


下面一起来看 Git 到底有多神奇:

File Modification In Repo

来看版本库中的文件修改 >>>

我们知道,Server 目录下包含一个名为:service.py 的文件,其内容如下:

1
# This is a Test!

到了工作时间,我们想要接着继续写 service.py 脚本了,我做了如下变更:

1
2
3
# Git is a distributed version control system.
# Git is free software.
# This is a Test!

通过上一篇博文知道,文件修改完成之后,想要 GitTestProject 版本库管理,还需要执行:添加(git add)&& 提交(git commit)的操作。

CMD 1 –>>> git status

事实上,在添加、提交变更之前,我们需要关注的是:Git 版本库(GitTestProject)是否已经实时地跟踪到了我们的修改(很慌,要是没有识别到修改怎么办?),这是很重要的!!!当然是可以的,毕竟 Git 就是干这个的。

git status 命令说明: 可以让我们 时刻监控到版本仓库中所有管理文件的当前状态,我们在 Git Bash 中运行查看一下 Git
监控信息:

1
2
3
4
5
6
7
8
9
$ git status
On branch master
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)

modified: Server/service.py

no changes added to commit (use "git add" and/or "git commit -a")

可以看到,Git 告诉我们 modified: Server/service.py(service.py 已经被修改),并且提醒我们 Changes not staged for commit(变更未提交)。

一般,一个文件的状态通常可以分为:

  • 不受版本控制的 untracked 状态
  • 受版本控制并且已修改的 modified 状态
  • 受版本控制已修改并提交到暂存区的 staged 状态
  • 从暂存区已经提交到本地仓库的 committed 状态
  • 提交到本地仓库未修改或者从远程仓库克隆下来的 unmodified 状态

CMD 2 –>>> git diff

上面我们知道,版本库确实检测到了 service.py 文件被变更了!但我们更加关注的是版本库检测到的 service.py 文件的修改,和我们手动修改是否一致?!!这时需要使用 git diff(git different) 命令来查看修改后文件和修改前文件的差异:

1
2
3
4
5
6
7
8
9
10
$ git diff Server/service.py
diff --git a/Server/service.py b/Server/service.py
index 68f5fe0..433af3b 100644
--- a/Server/service.py
+++ b/Server/service.py
@@ -1 +1,3 @@
-This is a Test!
+# Git is a distributed version control system.
+# Git is free software.
+# This is a Test!

可以看到 Git 已经跟踪了我们对 service.py 文件的修改,这下舒服了~,将其提交给版本库也就放心了。


CMD 3 –>>> git add && git commit -m

提交~提交 >>>

1
2
# 添加变更至暂存区(见下文 Git Stage 说明),等待 commit
$ git add Server/service.py

执行后同样没有任何输出(成功信号)。在执行 git commit 之前,我们再次运行 git status 查看一下当前仓库的状态(不放心再确认一下):

1
2
3
4
5
6
$ git status
On branch master
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)

modified: Server/service.py

Git 告诉我们将要被提交的修改是:Server/service.py(心里默念一句 NB),确定之后我们直接提交给版本库:

1
2
3
$ git commit -m 'Add help info'
[master 8622ab2] Add help info
1 file changed, 3 insertions(+), 1 deletion(-)

提交完成后,我们再次查看版本库状态:

1
2
3
$ git status
On branch master
nothing to commit, working tree clean

Git 监控信息显示:当前没有需要提交的变更,并且工作目录是干净(working tree clean)的,完美~


Time Shuttle

Git 实现时光穿梭(版本回滚) >>>

上述版本库中文件的修改操作,再练习一次,修改 service.py 文件如下:

1
2
3
4
5
# Git is a distributed version control system.
# Git is free software.
# This is a Test!

print ('Git is very useful!!!')

然后尝试添加 && 提交:

1
2
3
4
$ git add Server/service.py
$ git commit -m 'Add test code'
[master dd32d2d] Add test code
1 file changed, 2 insertions(+)

像这样,不断对文件进行修改,然后不断提交变更到版本库里。这就好像游戏存档一样:打通一部分关卡,存档一次,形成不同的存档库。一旦失败的话,可以通过存档回滚到最新的存档。

Git 也是一样,每当你觉得文件修改到一定程度(完成某项功能,或某个版本)的时候,就可以 “保存一个快照”,这个快照在 Git 中被称为一次 commit。一旦你把文件改乱了,或者误删了文件,还可以从最近的一个 commit 恢复(版本回滚),然后继续工作,而不是把几个月的工作成果全部丢失。


变更历史记录

在开始讲解 Git 版本库的回滚操作之前,我们先来回顾一下 service.py 一共有多少个 commit(版本、快照)被提交到 Git 仓库里:

明确了有多少、有哪些版本,才可以准确地在版本库中的不同版本间进行回滚

版本 1:ADD Project Base

1
2
3
# service.py 被提交到 GitTestProject,内容如下:

# This is a Test!

版本 2:Add help info

1
2
3
# Git is a distributed version control system.
# Git is free software.
# This is a Test!

版本 3:ADD test code

1
2
3
4
5
# Git is a distributed version control system.
# Git is free software.
# This is a Test!

print ('Git is very useful!!!')

截至目前为止,当前版本库中,我们执行过 3 次 commit 操作,形成了三个不同的版本。

  • 版本 1:ADD Project Base
  • 版本 2:Add help info
  • 版本 3:ADD test code

以上列出的就是当前版本库中的项目的 变更历史记录


CMD 4 –>>> git log

然后,实际工作中,我们怎么可能记得一个几千行的文件变更了多少次、每次都改了什么内容(不然要版本控制系统干什么,和原始版本控制有何区别?),所以需要版本控制系统可以告诉我们 项目的变更历史记录

git log 命令就是做这个事的,它会显示出版本库中:从最新到最远的提交日志

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ git log
commit 47f21abe1449214864c84c2abdf44168ee26df60 (HEAD -> master)
Author: staff_ming <staff_ming@xxxx.com>
Date: Fri Dec 21 11:20:04 2018 +0800

Add test code

commit 277b8bf5e9d974f73f1ace2dcd20fea2afd0296a
Author: staff_ming <staff_ming@xxxx.com>
Date: Fri Dec 21 11:02:34 2018 +0800

Add help info

commit 6a8c6bab3053e25a64241a22a56083c549bf79f2
Author: staff_ming <staff_ming@xxxx.com>
Date: Thu Dec 20 19:53:06 2018 +0800

ADD Project Base

如果嫌输出信息太多,看得眼花缭乱的,可以试试加上 --pretty=oneline 参数(清晰了很多):

1
2
3
4
$ git log --pretty=oneline
47f21abe1449214864c84c2abdf44168ee26df60 (HEAD -> master) ADD test code
277b8bf5e9d974f73f1ace2dcd20fea2afd0296a Add help info
6a8c6bab3053e25a64241a22a56083c549bf79f2 ADD Project Base

当然你也可以 查看单个文件的提交日志:

1
$ git log Server/service.py

| >>>>>>>>>>>>>>>>>>>>>>>> 历史变更记录解析 >>>>>>>>>>>>>>>>>>>>>>>>>>

你看到的一大串类似 47f21ab... 的是 commit id(版本号),和 SVN 不一样,Git 的 commit id 不是 1,2,3…… 递增的数字,而是一个非常大的数字(十六进制)。

为什么采用这种机制(SHA-1)?因为 Git 是分布式的版本控制系统,后面我们还要研究多人在同一个版本库里工作,如果大家都用 1,2,3…… 作为版本号,很容易就冲突了。

每提交一个新版本,实际上 Git 就会把它们自动串成一条时间线。如果使用可视化工具(Git GUI)查看 Git 历史,就可以更清楚地看到提交历史的时间线:

================================================================|

有了版本库时间线(变更历史记录线),接下来就可以启动时光穿梭机进行版本回滚了…

问题:假设 版本 3:ADD test code 有错误,我们想要将版本库回滚到上一个版本(版本 2:Add help info),怎么办??!


HEAD

开始回滚前,需要简单介绍一下 HEAD 的基本概念:

我们知道,要想实现版本回滚,Git 必须被指定要回滚到哪个版本。在 Git 中,用 HEAD 关键字表示当前版本,也就是最新的提交 47f21ab...(commit id)。上一个版本表示为:HEAD^,上上一个版本可以表示为:HEAD^^,很多人就要问了那如果要表示往上 90 个版本呢,难道要写 90 个 ^,当然不可能!版本数目较多时可以表示为:HEAD~90。


时空穿梭

有了以上的知识储备我们就可以进行版本库的 时光穿梭 了 >>>

CMD 5 –>>> git reset –hard

Git 版本库中的 “时光穿梭” 使用 git reset(版本库重置) 命令来实现其功能。

1 –> 回到过去

现在我们来看如何从当前版本【 Add test code 】回滚到上一版本【 Add help info 】,即:Add test code ->>> Add help info,指令如下:

1
2
$ git reset --hard HEAD^
HEAD is now at 8622ab2 Add help info

这里我们侧重看 git reset 实现功能,参数 --hard 会在后续补充说明,这里你只需知道它是版本指定相关参数即可。回滚后,我们来看 service.py 当前版本内容:

1
2
3
4
$ cat Server/service.py
# Git is a distributed version control system.
# Git is free software.
# This is a Test!

我们发现,果然 service.py 时空倒流到上一个版本了。


2 –> 穿梭到未来

如果你现在想重新回滚到 –>> 版本3:ADD test code 怎么办?

此时,我们查看一下当前版本库的提交日志信息:

1
2
3
$ git log --pretty=oneline
277b8bf5e9d974f73f1ace2dcd20fea2afd0296a (HEAD -> master) Add help info
6a8c6bab3053e25a64241a22a56083c549bf79f2 ADD Project Base

可以发现,之前最新的那个版本 【 ADD test code 】 已经看不到了!就好比你从现在回到了过去,但你又想回到现在(未来),你想通过 git log 察看现在时间节点在时间线上的版本标识,结果发现你已经找不到了。难道只能活在过去了么?

在给出办法之前你先祈祷吧…之前我们一直使用的 Git Bash 窗口你还没手贱关掉。我们可以找到先前 【 ADD test code 】 的 commit id47f21abe....,这样又找到了回去的时间节点版本标识(确实关闭也不要紧,是不是有点慌,哈哈。2333),下一小节【HEAD && Commit Id】中我们会给出 commit id 的查询方法)。如果你已经关闭,可以转至下一小节查询一下~

commit id 也可以作为 Git 版本库中时间线上的版本标识,在 Git 项目管理中这要比使用 HEAD 更加合理。

重新回滚到 –>> 版本3:Add test code 的指令如下:

1
2
$ git reset --hard 47f21abe
HEAD is now at 47f21abe ADD test code

注意:版本号(commit id)没必要写全,Git 会去自动检索的,当然也不能太少,否则可能无法和其它版本区别。

此时查看 service.py,发现我们已经回到了“未来”:

1
2
3
4
5
6
$ cat Server/service.py
# Git is a distributed version control system.
# Git is free software.
# This is a Test!

print ('Git is very useful!!!')

重新查看版本库变更历史记录如下:

1
2
3
4
git log --pretty=oneline
47f21abe1449214864c84c2abdf44168ee26df60 (HEAD -> master) ADD test code
277b8bf5e9d974f73f1ace2dcd20fea2afd0296a Add help info
6a8c6bab3053e25a64241a22a56083c549bf79f2 ADD Project Base

至此,你就可以自由地进行版本库的回滚了~


HEAD && Commit Id

区别于 SVN,在进行版本回滚的时候我们发现 Git 的版本回退速度非常快,这是由于 Git 内部的指针机制决定的。

Git 内部有个指向当前版本的指针,就是我们前面说的 Head。当你回滚版本的时候,Git 仅仅是把 HEAD 从指向 【 ADD test code 】 版本的地址:

移动指向 【 Add help info 】:

然后顺便把工作区的文件更新了。所以你让 HEAD 指向哪个版本号,你就把当前版本定位在哪(讲到这里很多人会想到程序设计语言中的指针)。

HEAD 所指向的版本地址就是: commit id。故版本库回滚时可以同时使用 HEAD && commit id 作为版本标识。

CMD 6 –>>> git reflog

“穿越到未来” 中找不到 commit id 的问题解决方法 >>>

前面我们在 穿越到未来 中提到 Git Bash 关掉后,表面看起来找不到最新版本的 commit id。实际上,Git 提供了指令 git reflog 用来记录你的每一次引用日志(HEAD 指针移动记录):

1
2
3
4
5
6
$ git reflog
47f21ab (HEAD -> master) HEAD@{0}: reset: moving to 47f21abe14492
277b8bf HEAD@{1}: reset: moving to HEAD^
47f21ab (HEAD -> master) HEAD@{2}: commit: ADD test code
277b8bf HEAD@{3}: commit: Add help info
6a8c6ba HEAD@{4}: commit (initial): ADD Project Base

从日志输出可以看出:Add test code 的 commit id 是 47f21ab


Git Stage

Git 和其他版本控制系统(如:SVN)的一个很大的不同之处就是有 暂存区(stage) 的概念。

这一小节,我们将从 Git 的本地数据管理入手,深入了解 Git 中引入的暂存区(Stage)的作用。

Git Local Data Management

Git 本地数据管理,大概可以分为三个区:工作区(Working Directory)、暂存区(Stage)、以及版本库(Repository)。

Working Directory

我们将当前程序开发所在目录称为:工作区(Working Directory),也就是我们的项目开发目录(如:GitTestProject)。该区域的文件会有状态的变化且状态由 Git 自动检测,如果程序中文件做任何操作(增、删、改),文件状态均会被检测到(类似于 SVN)。

Repository

工作区(GitTestProject)有一个隐藏目录 .git ,这个不算工作区,而是 Git 的 版本库(Repository)

Git 的版本库里存储了很多东西,其中最重要的就是称为 stage(或者叫:index)的 暂存区;还有 Git 为我们自动创建第一个分支 master,以及指向 master 的一个指针叫 HEAD(分支的概念后续会有专门的博文进行讲解,这里将其理解为版本库中的一条时间线)。

Stage

数据暂时存放的区域,可在工作区和版本库之间进行数据的友好交流。


Git 版本控制原理

当工作区检测到有文件发生变化时,那么意味着:我们在上一个版本之后再次对项目进行了变更。变更完成之后,我们可以将当前变更当做下一版本进行提交(生成一个新的版本或快照),那么就是执行 git add . 将所有文件提交到暂存区(stage),然后再执行 git commit -m 'another version' 提交到版本库的当前分支。原理图如下:

简单理解为:首先将需要提交的文件修改通通放到暂存区,然后一次性提交暂存区的所有修改即可。

下面我们完成一个实例来深入了解 Git 版本控制原理,详细过程如下所示:

1 ->> 在工作区修改文件 Server/service.py 内容为:

1
2
3
4
5
6
7
8
# Git is a distributed version control system.
# Git is free software.
# This is a Test!

print ('Git is very useful!!!')

# Git has a mutable index called stage.
# Git tracks changes.

2 ->> 在工作区添加文件 Client/request.py(内容随意),然后查看版本库当前状态:

1
2
3
4
5
6
7
8
9
10
11
12
$ 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: Server/service.py

Untracked files:
(use "git add <file>..." to include in what will be committed)
Client/request.py

no changes added to commit (use "git add" and/or "git commit -a")

Git 非常清楚地告诉我们,Server/service.py 脚本被修改了,而 Client/request.py 还从来没有被添加过,所以它的状态是 Untracked。

3)git add . 将所有修改文件提交到暂存区(stage),然后查看此时版本库状态:

1
2
3
4
5
6
7
$ git status
On branch master
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)

new file: Client/request.py
modified: Server/service.py

4)git commit 一次性把暂存区的所有修改提交到分支。

1
2
3
4
$ git commit -m 'understand how stage works'
[master 847e6ef] understand how stage works
2 files changed, 3 insertions(+)
create mode 100644 Client/request.py

5)提交后,如果工作区没有做任何修改,那么工作区就是 “干净” 的:

1
2
3
$ git status
On branch master
nothing to commit, working tree clean

此时版本库的暂存区(stage)内容会被提交的 master 分支。

看到这里,肯定会有很多看客老爷们会问:为啥要暂存区,通过工作区直接提交到本地仓库不就OK了?暂存区存在有什么作用? 下文会给出说明:


Data Flow In Git

根据上文 ,我们给出当开发者通过 git 变更数据时,各区之间可能的数据传递流程示意图:

我们可以通过对比三个区之间的数据差别,来验证以上流程的正确性,这可以借助之前学过的 git diff 命令来实现:

命令 作用
git diff 工作区 vs 暂存区
git diff head 工作区 vs 版本库
git diff –cached 暂存区 vs 版本库

开始实验一 >>>> :

上一个版本之后未进行任何变更(文件未修改,未 add,未 commit)时,执行 git diff 命令,结果都为空:

命令 结果
(工作区 vs 暂存区)git diff 无输出
(工作区 vs 版本库)git diff head 无输出
(暂存区 vs 版本库)git diff –cached 无输出

此时查看文件内容:

1
2
3
4
5
6
7
8
9
$ cat Server/service.py
# Git is a distributed version control system.
# Git is free software.
# This is a Test!

print ('Git is very useful!!!')

# Git has a mutable index called stage.
# Git tracks changes.

然后对 Server/service.py 进行如下变更,现在工作区内容发生变化,暂存区和版本库内容一致。

1
2
3
4
5
6
7
8
9
10
# Git is a distributed version control system.
# Git is free software.
# This is a Test!

print ('Git is very useful!!!')

# Git has a mutable index called stage.
# Git tracks changes.

# Git local data management test.

Result 1 >>>>:(工作区 vs 暂存区)git diff

1
2
3
4
5
6
7
8
9
10
11
$ git diff
diff --git a/Server/service.py b/Server/service.py
index 6bef824..5127360 100644
--- a/Server/service.py
+++ b/Server/service.py
@@ -6,3 +6,5 @@ print ('Git is very useful!!!')

# Git has a mutable index called stage.
# Git tracks changes.
+
+# Git local data management test.

Result 2 >>>>:(工作区 vs 版本库)git diff head

1
2
3
4
5
6
7
8
9
10
11
$ git diff head
diff --git a/Server/service.py b/Server/service.py
index 6bef824..5127360 100644
--- a/Server/service.py
+++ b/Server/service.py
@@ -6,3 +6,5 @@ print ('Git is very useful!!!')

# Git has a mutable index called stage.
# Git tracks changes.
+
+# Git local data management test.

Result 3 >>>>:(暂存区 vs 版本库)git diff –cached

1
2
$ git diff --cached
# 无输出

执行 git add 操作后,修改同步到暂存区,现在工作区和暂存区数据一致。

1
$ git add Server/service.py

Result 1 >>>>:(工作区 vs 暂存区)git diff

1
2
$ git diff
# 无输出

Result 2 >>>>:(工作区 vs 版本库)git diff head

1
2
3
4
5
6
7
8
9
10
11
$ git diff head
diff --git a/Server/service.py b/Server/service.py
index 6bef824..5127360 100644
--- a/Server/service.py
+++ b/Server/service.py
@@ -6,3 +6,5 @@ print ('Git is very useful!!!')

# Git has a mutable index called stage.
# Git tracks changes.
+
+# Git local data management test.

Result 3 >>>>:(暂存区 vs 版本库)git diff –cached

1
2
3
4
5
6
7
8
9
10
11
$ git diff --cached
diff --git a/Server/service.py b/Server/service.py
index 6bef824..5127360 100644
--- a/Server/service.py
+++ b/Server/service.py
@@ -6,3 +6,5 @@ print ('Git is very useful!!!')

# Git has a mutable index called stage.
# Git tracks changes.
+
+# Git local data management test.

执行 git commit 操作后,修改已经同步到版本库,三区数据再次保持一致。

命令 结果
(工作区 vs 暂存区)git diff 无输出
(工作区 vs 版本库)git diff head 无输出
(暂存区 vs 版本库)git diff –cached 无输出

开始实验二 >>>> :

1)对 Server/service.py 进行第一次修改:

1
2
3
4
5
6
7
8
# Git is a distributed version control system.
# Git is free software.
# This is a Test!

print ('Git is very useful!!!')

# Git has a mutable index called stage.
# Git tracks changes.

2)将其添加到暂存区:

1
2
3
4
5
6
7
$ git add Server/service.py
$ git status
On branch master
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)

modified: Server/service.py

3)对 Server/service.py 进行第二次修改:

1
2
3
4
5
6
7
8
# Git is a distributed version control system.
# Git is free software.
# This is a Test!

print ('Git is very useful!!!')

# Git has a mutable index called stage.
# Git tracks changes of files.

4)提交:

1
2
3
4
5
6
7
8
9
10
11
12
13
$ git commit -m 'git tracks changes'
[master 9046f26] git tracks changes
1 file changed, 1 insertion(+)

$ git status
On branch master
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)

modified: Server/service.py

no changes added to commit (use "git add" and/or "git commit -a")

我们发现还有一次修改未被提交,我们先回顾一下操作过程:

第一次修改 -> git add -> 第二次修改 -> git commit

当你用 git add 命令后,在工作区的第一次修改被放入暂存区,准备提交。但是,在工作区的第二次修改并没有放入暂存区,所以,git commit 只负责把暂存区的修改提交了,也就是第一次的修改被提交了,第二次的修改不会被提交。

Result 1 >>>>:(工作区 vs 暂存区)git diff

1
2
3
4
5
6
7
8
9
10
11
 $ git diff
diff --git a/Server/service.py b/Server/service.py
index 6bef824..a17dbf0 100644
--- a/Server/service.py
+++ b/Server/service.py
@@ -5,4 +5,4 @@
print ('Git is very useful!!!')

# Git has a mutable index called stage.
-# Git tracks changes.
+# Git tracks changes of files.

此时,由于第二次修改内容未同步至暂存区,所以工作区和暂存区数据不一致。

Result 2 >>>>:(工作区 vs 版本库)git diff head

1
2
3
4
5
6
7
8
9
10
11
$ git diff head
diff --git a/Server/service.py b/Server/service.py
index 6bef824..a17dbf0 100644
--- a/Server/service.py
+++ b/Server/service.py
@@ -5,4 +5,4 @@
print ('Git is very useful!!!')

# Git has a mutable index called stage.
-# Git tracks changes.
+# Git tracks changes of files

可见,第二次修改确实没有被提交。所以:如果不用 git add 到暂存区,那就不会加入到 commit 快照中。

Result 3 >>>>:(暂存区 vs 版本库)git diff –cached

1
2
$ git diff --cached
# 无输出

此时,第一次修改的暂存区内容已提交至版本库,所以暂存区和版本库内容是保持一致的。


事实上,Stage 的引入,确实赋予了 Git 更多灵活的特性。

Git Reset –Options

前面我们提到过,git reset 版本库重置命令可以根据历史变更记录(HEAD && Commit ID)来实现 “时光穿梭”(版本库重置)。

上文通过对 Git 本地数据管理的说明,相信你已经对 Git 分区工作原理有了深入的了解。掌握这一小节之后,通过 git reset 不同的参数搭配使用,可以在工作区,暂存区和版本库之间,轻松进行数据的来回切换。

Git Reset 有三种模式(参数):softmixedhard

下面分别来看使用上的区别:


Mode:hard

该模式下,Git 会使用 <HEAD or Commit ID> 所对应的版本重置 Stage 区和工作区(Working Directory)。命令格式:

1
$ git reset --hard <HEAD or Commit ID>

命令说明:git reset 后面加了 --hard 参数时,你的 stage区和工作目录里的内容会被完全重置为 <HEAD or Commit ID> 所对应的新位置相同的内容。

简言之:你的没有 commit 的修改会被全部擦掉。

Demo > > > >

例如,上一次 commit 之后又对文件做了一些修改:将修改后的 Server/service.py 文件 add 到 Stage;修改后的 Client/request.py 保留在工作区(未执行 add)。查看此时版本库状态:

1
2
3
4
5
6
7
8
9
10
$ git status
On branch master
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: Server/service.py

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: Client/request.py

然后,查看 git reset --hard 效果:

1
2
$ git reset --hard HEAD^
HEAD is now at bf2ead8 Git Local Data Managemant

可以发现,当前版本库的 HEAD 切到上一条 commit 的同时,工作目录里的新改动(Client/request.py)和已经 add 到 stage 区的新改动(Server/service.py)也一起全都消失了:

1
2
3
$ git status
On branch master
nothing to commit, working tree clean

Mode:soft

该模式下,Git 会使用 <HEAD or Commit ID> 所对应的版本进行重置,但注意,会保留 Stage 区和工作区(Working Directory)中的内容,并且会把重置 <HEAD or Commit ID> 所带来的新的差异放进暂存区。命令格式:

1
$ git reset --soft <HEAD or Commit ID>

命令说明:什么是「重置 HEAD 所带来的新的差异」?由于 HEAD 的移动(A –> C),版本 A 到 版本 C 之间的差异即为重置 HEAD 所带来的新的差异,会将这些由版本版本重置引发的差异放入暂存区。

简言之:保留工作目录和暂存区内容,并把重置 HEAD 所带来的新的差异放进暂存区。

可以想象到,当我们想合并「当前节点」与「reset 目标节点」之间不具太大意义的 commit 记录(可能是阶段性地频繁提交)時,可以考虑使用 Soft Reset 来让 commit 演进线图较为清晰点。

Demo > > > >

例如,上一次 commit 之后又对文件做了一些修改:将修改后的 Server/service.py 文件 add 到 Stage;修改后的 Client/request.py 保留在工作区(未执行 add)。查看此时版本库状态:

1
2
3
4
5
6
7
8
9
10
$ git status
On branch master
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: Server/service.py

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: Client/request.py

假设此时当前 commit 的改动内容是新增了 softtest.txt 文件。如果这时你执行:

1
2
3
4
5
6
7
8
9
10
11
12
$ git reset --soft HEAD^
$ git status
On branch master
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: Server/service.py
new file: softtest.txt

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: Client/request.py

可见,将版本库由 HEAD 重置为 HEAD^,重置 HEAD 所带来的差异为:新增了 softtest.txt 文件,故新增文件被存放到暂存区。


Mode:mixid

该模式下(默认,不加参数),Git 会使用 <HEAD or Commit ID> 所对应的版本进行重置,注意,会保留工作区(Working Directory)中的内容,但会清空暂存区(Stage)。命令格式:

1
2
3
$ git reset --mixed <HEAD or Commit ID>
# 或者
$ git reset <HEAD or Commit ID>

命令说明:保留工作区(Working Directory)中的内容,但会清空暂存区(Stage)。工作目录的修改、暂存区的内容以及由 reset 所导致的新的文件差异,都会被放进工作目录。

简言之:把所有差异都混合(mixed)放在工作目录中。

Demo > > > >

例如,上一次 commit 之后又对文件做了一些修改:将修改后的 Server/service.py 文件 add 到 Stage;修改后的 Client/request.py 保留在工作区(未执行 add)。查看此时版本库状态:

1
2
3
4
5
6
7
8
9
10
$ git status
On branch master
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: Server/service.py

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: Client/request.py

假设此时当前 commit 的改动内容是新增了 softtest.txt 文件。如果这时你执行不带参数的 reset:

1
2
3
4
5
6
7
8
9
10
11
 $ 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: Client/request.py
modified: Server/service.py

Untracked files:
(use "git add <file>..." to include in what will be committed)
softtest.txt

完美~


Undo Edit

如何撤销修改? >>>

3.2 Data Flow In Git 实验二 中我们对 Server/service.py 文件做了两次修改,第一次修改为正确修改已被提交(commit)。第二次修改未提交(add),提交(add)前我们突然发现修改为错误修改:

1
2
3
4
5
6
7
8
9
$ cat Server/service.py
# Git is a distributed version control system.
# Git is free software.
# This is a Test!

print ('Git is very useful!!!')

# Git has a mutable index called stage.
# Git tracks changes of files.

我们能想到的直接的纠正方法是手动把文件恢复到上一个版本的状态。但我们查看此时版本库状态:

1
2
3
4
5
6
7
8
9
$ git status
On branch master
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)

modified: Server/service.py

no changes added to commit (use "git add" and/or "git commit -a")

CMD 7 –>>> git checkout – file

我们发现,Git 提示可以通过 git checkout -- file 可以丢弃工作区的修改,指令如下:

1
$ git checkout -- Server/service.py

命令 git checkout -- Server/service.py 意思是:把 service.py 文件在工作区的修改全部撤销,这里有两种情况:

  • 第一种是 service.py 自修改后还没有被放到暂存区,撤销修改就回到和版本库一模一样的状态;
  • 第二种是 service.py 已经添加到暂存区后又作了修改,撤销修改就回到添加到暂存区后的状态。

简单地说:git checkout -- file 会让文件 file 回到上一次 git commitgit add 时的状态。

可以发现,丢弃工作区的修改之后 service.py 文件又恢复到之前的内容了:

1
2
3
4
5
6
7
8
9
$ cat Server/service.py
# Git is a distributed version control system.
# Git is free software.
# This is a Test!

print ('Git is very useful!!!')

# Git has a mutable index called stage.
# Git tracks changes.

注意:git checkout -- file 命令中的参数 -- 很重要,没有 --,就变成了 “切换到另一个分支” 的命令,后面的分支管理中会再次遇到 git checkout 命令。

还能怎么操作可以实现上述功能?!!上一小节介绍的 git reset 也可以实现上述撤消操作。如下:

1
2
$ git reset --hard HEAD
HEAD is now at 04aee1b git tracks changes

这里我们再来尝试 git checkout -- 的第二种情况(变更已经 add 到暂存区):将 service.py 文件内容修改为上述中第二次修改内容,如下:

1
2
3
4
5
6
7
8
# Git is a distributed version control system.
# Git is free software.
# This is a Test!

print ('Git is very useful!!!')

# Git has a mutable index called stage.
# Git tracks changes of files.

然后将上述修改添加至暂存区:

1
$ git add Server/service.py

添加后再进行修改,内容如下:

1
2
3
4
5
6
7
8
9
10
# Git is a distributed version control system.
# Git is free software.
# This is a Test!

print ('Git is very useful!!!')

# Git has a mutable index called stage.
# Git tracks changes of files.

# Git Second tracks changes of files.

随后,查看此时文件状态

1
2
3
4
5
6
7
8
9
10
$ git status
On branch master
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: Server/service.py

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: Server/service.py

此时,我们发现修改有误,需要撤销工作区中文件内容,指令如下:

1
$ $ git checkout -- Server/service.py

查看文件内容,发现撤销修改回到添加到暂存区后的状态(仅仅撤销了第二次修改的内容):

1
2
3
4
5
6
7
8
9
$ cat Server/service.py
# Git is a distributed version control system.
# Git is free software.
# This is a Test!

print ('Git is very useful!!!')

# Git has a mutable index called stage.
# Git tracks changes of files.

事实上,我们第一次已经 add 到 暂存区的内容也是有问题的,怎么办?

git reset HEAD file 可以帮我们直接清楚暂存区内容。

如果你想将当前文件的修改从暂存区移除去,可以做如下操作:

1
2
3
4
5
6
7
8
9
10
11
12
git reset HEAD Server/service.py
Unstaged changes after reset:
M Server/service.py

$ 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: Server/service.py

no changes added to commit (use "git add" and/or "git commit -a")

然后,我们对比一下,工作区和暂存区中 Server/service.py 文件差异:

1
2
3
4
5
6
7
8
9
10
11
$ git diff
diff --git a/Server/service.py b/Server/service.py
index 6bef824..a17dbf0 100644
--- a/Server/service.py
+++ b/Server/service.py
@@ -5,4 +5,4 @@
print ('Git is very useful!!!')

# Git has a mutable index called stage.
-# Git tracks changes.
+# Git tracks changes of files.

现在,上一小节的第二次修改也不想要了,怎么办?指令如下:

1
$ git checkout -- Server/service.py

当然了,使用 git reset 可以很容易实现上述“复杂”的过程,但这里主要是为了引入 git reset HEAD file,掌握这一方法可以很快清空暂存区。


CMD 8 –>>> git switch && git restore

我们知道,git checkout 命令身兼数职(切换分支、文件撤回…),从 Git 2.23 版本开始引入了两个新的命令来分担 git checkout 功能:

  • git switch:切换分支
  • git restore:文件撤回(检出)

git switch 功能这里不进行详细介绍,在介绍 Git 分支功能时再详解。

git restore 命令详解:

  1. git restore file>>> 指令使得在工作空间但是不在暂存区的文件撤销更改(内容恢复到没修改之前的状态);【另一种说法:用暂存区或者版本库中的文件覆盖本地文件的修改可以达到回退修改的目的】
  2. git restore –staged file>>> 的作用是将暂存区的文件从暂存区撤出,但不会更改文件的内容。【另一种说法:使用版本库中的文件覆盖暂存区的文件,达到回退 git add 命令的目的】

Delete File

前面我们接触的修改文件都是新建、修改等。那么,Git 中是如何管理文件删除的???

下面我们通过一个删除实例来看 Git 如何管理删除,先添加一个新文件 git_rm_test.txt 到 Git 并且提交:

1
2
3
4
5
6
$ touch git_rm_test.txt
$ git add git_rm_test.txt
$ git commit -m 'Add git_rm_test.txt'
[master 6516e48] Add git_rm_test.txt
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 git_rm_test.txt

然后我们删除 git_rm_test.txt 文件。这时 Git 会跟踪删除文件操作,工作区和版本库就不一致了。此时我们来看版本库状态,会发现 Git 已经知道了我们的删除操作:

1
2
3
4
5
6
7
8
9
$ git status
On branch master
Changes not staged for commit:
(use "git add/rm <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)

deleted: git_rm_test.txt

no changes added to commit (use "git add" and/or "git commit -a")

此时我们面临两个选择:一是确实要从版本库中删除该文件;那就用命令 git rm(add) file 删掉,并且 git commit 提交:

1
2
3
4
5
6
7
$ git rm git_rm_test.txt
rm 'git_rm_test.txt'

$ git commit -m 'rm git_rm_test.txt'
[master 4f15f79] rm git_rm_test.txt
1 file changed, 0 insertions(+), 0 deletions(-)
delete mode 100644 git_rm_test.txt

另一种情况是删错了,因为版本库里还有呢,所以可以很轻松地把误删的文件恢复到最新版本:

1
$ git checkout -- git_rm_test.txt
Author

Waldeinsamkeit

Posted on

2017-07-02

Updated on

2022-03-05

Licensed under

You need to set install_url to use ShareThis. Please set it in _config.yml.

Comments

You forgot to set the shortname for Disqus. Please set it in _config.yml.