Git教程 · 提交详解
- 1️⃣ 访问权限与时间戳
- 2️⃣ add命令与 commit 命令
- 3️⃣ 提交散列值
- 4️⃣ 提交历史
- 5️⃣ 一种特别的提交查看方法
- 6️⃣ 同一项目的多部不同历史
- 6.1 部分输出:-n
- 6.2 格式化输出:--format、--oneline
- 6.3 统计修改信息:--stat、 --shortstat
- 6.4 日志选项:--graph
- 7️⃣ 多次提交
- 7.1 status 命令
- 7.2 存储在暂存区中的快照
- 7.3 怎样的修改不该被提交
- 7.4 用.gitignore 忽略非版本控制文件
- 7.5 储藏
- 🌾 总结
在Git 中,提交无疑是最重要的概念了。Git 管理的是软件版本,而版本库中的版本是以 提交 的形式保存的。某一次的提交的覆盖范围通常是整个项目,即通过一次提交,该项目中的每个文件就都被存进了版本库中。
下面,我们可以通过 git log --stat -1
命令来看一下提交中究竟包含了哪些重要信息,其摘要如下所示。
commit 7a90822f510d4a3309dfbebda2fc9cf350ee013d (HEAD -> main, origin/main-xiaoshan)
Merge: 63f2bd3 dab5fc3
Author: xiaoshan<xiaoshan>
Date: Fri Feb 23 13:53:47 2024 +0800Merge branch 'main' of ...
如上所示,这段信息的第一行显示的是该提交的散列值 7a90822f…e013d, 紧随其后的是与作者相关的信息、该提交被创建的时间以及相应的注释信息。最后部分所说明的是哪些文件自上一版本以来发生了变化。针对每次提交,Git 都会为其 计算一个由40个字符组成的唯一编码,我们称之为 提交散列值 (commit hash
)。 只要知道这个散列值,我们就可以将项目中的文件从版本库中恢复到该提交被创建的那个时间点上。在Git 中,恢复到某一版本通常被称之为 检出(checkout) 操作。
1️⃣ 访问权限与时间戳
Git 会保存每个文件原有的访问权限(即 POSIX 文件权限,包含读、写、执行3种权限), 但不会保留文件的修改时间。因此在执行检出操作时,文件的修改时间会被设置为当前时间。
为什么不保存修改时间呢? 这主要是因为,如今的许多构建工具的重新生成项目动作都是靠这些文件的修改时间来触发的,即如果最后一次修改晚于我们最后一次构建的结果,我 们就进行一次新的建构过程。由于 Git 在进行检出操作时总是用当前时间来充当文件的修改时间,所以就能确保这些工具正确、顺畅地完成整个构建过程。
2️⃣ add命令与 commit 命令
通常,提交中会包含当前所有的修改,既有新增的文件也有被删除的文件。唯一例外的是在.gitignore
文件中列出的那些文件(后面详细讨论.gitignore
) 。
我们可以分 add
命令和 commit
命令这两步来创建一次提交。
- 注册修改
我们可以用add 命令来进行注册,将所做的修改纳入下次提交。在这里,你可以使用-all
参数,这表示我们会将所有修改都纳入在内。git add --all
- 创建提交
现在可以创建一次新的提交了。git commit
3️⃣ 提交散列值
乍看之下,40个字符的提交散列值的确有点长。毕竟,其他版本控制系统使用的都是简单的序列数(例如 Subversion) 或像1:17这样的版本名词(例如CVS)。
但是, Git的开发者选择了散列值这种形式也是有其充分理由的。
-
这样的提交散列值可以在本地生成。我们无需与其他计算机或中央服务器进行通信, 就可以随时随地创建新的提交。而且,由于提交散列值是根据文件内容及其元数据(即 作者、提交时间)计算出来的,因此两次不同的修改会得到相同提交散列值的概率是非常低的。毕竟,这里要面对的是2160种不同的值呢。
-
更为重要的是,提交散列值中的信息要比单纯一个软件版本的名称要多得多。这 也是该软件版本的汇总信息。正因为如此,我们才能通过 Git的
fsck
命令来查看版本库的完整性。如果其内容与相应的散列值不匹配,我们就得到如下错误报告。
> git fsck
error: shal mismatch 2b6c746e5e20a64032bac627f2729f72a9cba4ee
error: 2b6c746e5e20a64032bac627f2729f72a9cba4ee:
object corrupt or missing
当然,我们也可以在指定某个提交散列值时采用缩写形式。大致上只要指定几个足以识别该提交的字符就可以了。但如果指定的字符太少, Git 还是会报错的。
>git checkout 9acc5d5efecld2d62f7e98bcc3880cda762cb831
>git checkout 9acc
另外,我们也可以为某个提交起一个有意义的名称(例如 release-1.2.3)。这就是所谓的标记。
>git checkout release-1.2.3
4️⃣ 提交历史
版本库中所包含的并不仅仅是一个个独立的提交,它同时也存储了这些提交之间的关系。 每当我们修改了软件并确认提交时, Git 就会记下这个提交之前的版本。这些提交会形成一个关系图,该图反映了整个项目的开发过程。
有趣的是,每当有多个开发者同时在开发一款软件时,其中的分支在创建时会如提交关系图中的C 处这般开始,随后又会如G 处那样被合并。
5️⃣ 一种特别的提交查看方法
我们可以将一次提交看成一个已被冻结的版本层次,但也可以将其视为自上一次提交以 来项目中所纳入的一组修改。当然,我们也可以将它说成是一种差异集或一组修改。所以版本库实质上也是一部项目的修改历史。
通过 diff
命令,我们可以比较出两次提交之间的差异。
a. 两次提交
两次提交之间可以有一份完整的差异清单。我们在不用提交散列值的情况下,靠指定相关特定的符号名称(例如分支、标签、HEAD 等)也能获取到它。
>git diff 77d231f HEAD
b. 与上一次提交进行比较
通过在 diff 命令中使用 ^!
, 我们可以比较当前提交与上一次提交之间的差异。
>git diff 77d231f^!
C. 限制文件范围
我们还可以限制只显示哪些文件或目录之间的差异。
>git diff 77d231f 05bcfd1 -book/bisection/
d. 统计修改情况
或者我们可以通过 --stat
选项来显示每个文件中的修改数量。
>git diff --stat 77d231f 05bcfd1
6️⃣ 同一项目的多部不同历史
首先,我们需要适应 Git 的分布式架构。在(像 CVS 、Subversion 这样的)集中式版本控制系统中,通常会存在一个用于存储项目历史的中央服务器。但在 Git 中,每个开发者都有一个(有时是多个)属于自己的版本库克隆体。当开发者创建提交时,通常是在本地完成 这一动作的。而自此之后,他的版本库就有了一部不同于其他开发者版本库的历史,尽管这些人所克隆的是同一个项目。
这样一来,每个版本库都有一个属于它自己的故事。这些版本库之间可以通过 fetch
、pull
以及 push
命令来共享彼此的提交。除此之外,你也可以用 merge
命令将这些不同的历史重新合并在一起。
在许多项目中,我们通常会有一个用于存储官方历史的版本库(它通常位于项目服务器上), 我们称该版本库为项目版本库 (blessed repository
)。但这仅仅是一个习惯做法而已,单纯从技术角度来看的话,该项目的所有克隆体都是平等的。例如,如果主版本库被破坏了,它的另一个克隆体同样可以胜任它的工作。
当然, 一个规模非常大的项目可以会被分布在多个版本库中。在这种情况下,我们通常会有一个主版本库,该版本库中会依次存储其各个子项目的版本库,这些版本库通常称为子模块 (submodule
)。
我们可以用 log
命令来显示提交历史。
a. 简单的日志输出
> git log
commit 7a90822f510d4a3309dfbebda2fc9cf350ee013d (HEAD -> main, origin/main-xiaoshan)
Merge: 63f2bd3 dab5fc3
Author: xiaoshan<xiaoshan>
Date: Fri Feb 23 13:53:47 2024 +0800
...
b. 一些实用选项
> git log -n 3 #Only the last three commits
> git log --oneline #Only one line per commit
> git log --stat #Only show statistics
log 命令通常并不一定要显示出版本库中所有的提交。我们往往只需要它显示出当前提交的前几次提交即可。
在使用 log 命令时,我们可以通过一些选项来控制自己所需要显示的提交以及它们显示
的格式。下面我们来看几个常用的选项。
6.1 部分输出:-n
该选项通常用于限制输出。例如下面这个命令只显示该项目的最后3次提交:
> git log -n 3
6.2 格式化输出:–format、–oneline
对于日志的输出格式,我们可以用–format 选项来控制。例如, --format=fuller 选项可 以用来显示许多细节信息。而下面则是–oneline 选项所显示的概述信息。
> git log --oneline
2753f19 TODO indented for illustration.
e0ffbdb Note.
4200ba2 Section on different histories of the same project.
6.3 统计修改信息:–stat、 --shortstat
统计类选项也是很有用的: --stat 可用来显示被修改的那些文件。 --dirstat 则可以用来 显示那些包含被修改文件的目录。而 --shortstat 则用来显示项目中有多少文件被修改,以及新增或删除了多少文件。
> git log --shortstat --oneline
0a6f423df (HEAD -> master, origin/master-l) ...
50498817d (origin/master, origin/HEAD) ...26 files changed, 26 insertions(+), 26 deletions(-)
7cb257dd9 Merge branch 'dev-0222' into 'master'
f6e93ea33 附加信息增加用户初始角色 1 file changed, 4 insertions(+)
18a44beef Merge branch 'dev-0206pro' into 'master'
1f3656121 息维护调整 1 file changed, 2 insertions(+), 2 deletions(-)
...
6.4 日志选项:–graph
我们也可以通过–graph 选项来显示各提交之间的关系。
> git log --graph --oneline
* 0a6f423df (HEAD -> master, origin/master-l) ...
|\
| * 50498817d (origin/master, origin/HEAD) ...
| * 7cb257dd9 Merge branch 'dev-0222' into 'master'
| |\
| | * f6e93ea33 附加信息增加用户初始角色
| |/
| * 18a44beef Merge branch 'dev-0206pro' into 'master'
| |\
| | * 1f3656121 息维护调整
7️⃣ 多次提交
新的提交未必一定得包含工作区中所发生的所有修改。事实上在这一方面,Git 赋予了用
户完全的控制权。甚至,我们可以用它来摘取合并其中的一些修改,并将其纳入下一次提交中。
提交的产生通常被分为两个步骤。首先,我们要用 add 命令将所有相关的修改纳入到一 个缓存区 (buffer) 中。这个缓存区通常被叫做暂存区 (staging area) 或索引 (index) 。 接着,我们才能用 commit 命令将暂存区中的修改传送到版本库中。
7.1 status 命令
通过status
命令,我们可以查看当前工作区中所发生的修改,以及其中的哪些修改已经被注册到了暂存区中,以作为下次提交的内容。
> git status# On branch staging
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
#
# modified:bar.txt
#
# Changed but not updated:
# (use "git add <file>..."to update what will be committed)
# (use "git checkout --<file>..."to discard changes in ...
#
# modified: foo.txt
#
# Untracked files:
# (use "git add <file>..."to include in what will be committed)
#
# new.txt
no changes added to commit(use "git add" and/or "git commit -a")
这段输出可按以下几个小标题来显示。
- 被提交的修改 (
changes to be committed
): 这部分将列出那些将在下次提交中被纳入版本库中的、被修改的文件。 - 不会被更新的修改 (
changed but not updated
): 这部分将列出那些已被修改,但尚未被注册到下次提交中的文件。 - 未被跟踪的文件 (
untracked files
): 这部分将列出所有的新增文件。
除此之外,Git 还提供了相关的帮助提示,告诉我们应该用什么命令来重新改变这些状态。
例如,我们可以用以下命令将 blah.txt
移出暂存区。
git reset HEAD blah.txt
对于CVS 和 Subversion的用户来说,“更新” 这个术语可能会引起一些用法上的混淆。在这些系统中,“更新”通常指的是将版本库中所发生的修改回收到工作区中来。但在 Git 中,更新则 是指将工作区中的修改集合到暂存区中,两者的方向完全相反。
如果工作区中发生了很多修改,我们也可以在此使用 --short
选项,以便相关输出显得更紧凑一些。例如:
> git status --short
M blah.txt
M foo.txt
M bar.txt
?? new-file.txt
由于创建新的提交未必就一定会包含当前所有的更新,所以在这里,我们可以选择纳入所有文件,也可以只选取其中一些文件。
-
查看所发生的修改
> git status
在这里,位于“不会被更新的修改”和“未被跟踪的文件”这两个小标题下的内容会被 status 命令显示为尚未被注册为下次提交的文件。
-
收集相关修改
接下来,我们要用 add 命令将相关的修改添加到暂存区中。在这里,我们既可以单独指定文件的路径,也可以从其所有子目录中指定某一包含了新增文件与被修改文件的目录。 add 命令可以被调用多次,并且允许我们在指定路径时使用*
和?
等通配符。> git add foo.txt bar.txt #selected files > git add dir/ #a directory and everything underneath > git add. #current directory and everything underneath
如果还想进行进一步的控制,也可以在这里使用–interactive 选项,开启交互模式。然 后,就可以单独选取某个具体的代码片段了,在极端的情况下,我们甚至可以逐行将代码 注册到提交中。
-
创建提交
最后,我们通过一次提交来启用这些修改。> git commit
完成该操作后,暂存区就会被清空。工作区将不受提交影响。那些没有被 add 命令添加的文件依然留在工作区中。
选择性提交对于某些修改之间的隔离非常有用。例如:假设我们在项目中创建了一个新类, 但与此同时,我们也纠正了其他类中的一些错误。分多次提交这些修改有两个好处: 一则是被提 交的历史记录更清晰,二则使人们可以更方便地获取稍早前那个单一bug 的修复(即捡取)。
但我们需要记住的是,选择性提交在版本库中所创建软件版本从未确实在本地存在过。
因此,它们也从未测试过,在最坏的情况下甚至都不能编译。综合上述原因,我们会建议在可能的情况下尽量不要使用选择性提交。这种做法往往只能记录下我们想修复某个错误的信息,但这并不等于它能正确地修复这个错误。
7.2 存储在暂存区中的快照
关于暂存区,我们需要知道一件事:它的作用并不仅仅是为下次提交提供一份文件清单。 暂存区不仅要存储修改所发生的位置,同时也要存储修改的内容。为了达到这一目的,Git 必须要为那些被选出的文件生成一个快照。下面我们以下图为例来为此做一个说明。在第1 行中,工作区、暂存区以及版本库中的内容是相同的。然后,开发者对其工作区中的文件做了某些修改(第2行)。再然后,该开发者用add 命令将这些修改传到了暂存区中,但版本库此时仍未受到影响(第3行)。
接下来到了第4行中,开发者再次修改了文件。现在,上述3个区域中的内容都不相同了。 然后,我们用 commit 命令将第一次修改的内容传到版本库中(第5行号线)。这时候第二次修改的内容依然还留在工作区中。开发者需要继续通过add 命令将其转移到暂存区中来(第6行)。
我们所做的修改是通过 add 命令被注册到下次提交中的。在那之后,当工作区中发生
进一步修改时,我们就可以用diff 命令来一探究竟。
a. 暂存区中的是什么?
对于已经被 add 命令放入暂存区的那些修改,我们可以通过 --staged 选项来显示器暂存
的内容。下面命令所要显示的是当前版本库中 HEAD 提交与暂存区之间的不同之处。
> git diff --staged #staging vs.repository
b. 尚未被注册的又是什么?
在不带任何选项的情况下, diff 命令所显示的就是工作区中尚未被注册的本地修改,
换句话说,就是显示暂存区与工作区之间的不同之处。
> git diff #staging vs.workspace
7.3 怎样的修改不该被提交
事实上,有些特定的修改是我们确实不想提交的,其中包括以下几种。
- 为调试而做的实验性修改。
- 意外添加的修改。
- 尚未准备好的修改。
- 自动生成文件中所发生的修改。
reset
命令可用来重置暂存区。其 第一个参数为 HEAD, 表示的是我们 要将其重置为当前的 HEAD 版本。第 二个参数则用于指定要被重置的文件或目录。例如:
> git reset HEAD
或者:
> git reset HEAD foo.txt src/test/
在重置过程中,暂存区将会被重写。这在通常情况下不会是一个问题, 因为相同的修改很可能仍然会保留在 工作区中。但如果这个相同的文件在 add 命令之后已经被进一步修改过了,那么相关信息就很可能被丢掉了。
对于上述问题,Git 为我们提供了以下几种应对方法。
- 使用
reset
命令重置那些实验性的或者被意外修改的内容。 - 将我们不希望被提交的忽略文件列表写入
.gitignore
。 - 使用
stash
命令将我们希望日后再提交的修改内容暂时保存起来。
7.4 用.gitignore 忽略非版本控制文件
在一般情况下,对于那些自动生成的文件、由编辑器创建的或用于备份的临时文件,我 们不会希望将它们置于版本控制之下的。事实上,我们可以通过往项目根目录下的 .gitignore
文件中添加条目的方式让Git “看不见” 这些文件。我们可以在该文件中指定这些文件路径和 目录,并且可以使用 “*
”和“&
” 等通配符。关于路径的指定,我们需要知道:即使我们在 该文件中写的是 generated/
这样的简单路径,也会将所有包含这一名字的目录都包括在内, 例如 src/demo/generated
, 都会被彻底忽略。但如果我们在这类路径之前加了个1, 例如 /generated/
, 那么就只有这个确切的路径(相对于该项目的根目录而言)会被忽略了。
#
# Simple file path
#
somehow/simultaneous.txt
#
# Directories ending with a "/"
#
generated/
#
#File types as glob expressions
#
*.bak
#
# "!" marked exceptions."demo.bak"
# will be versioned,but "*.bak"'
# will be excluded.
#
!demo.bak
另外,我们也可以在项目的子目录中创建一个.gitignore
文件。这样一来,该文件就只能影响该目录下的文件和路径了。这在某些情况下可能是有用的,例如,如果我们的项目是用 多种编程语言来编写的,那么它们各自都应该有一个不同的配置文件。
但请注意,.gitignore
文件中的条目只能影响那些当前还未交由Git 来管理的文件。如果 其中的某个文件已经被现有版本包含了,那么 status
命令依然会显示该文件之上发生的所有 修改,并且它也一样可以通过 add
命令被注册到下次提交中。如果我们想忽略一个已经被版本化的文件,可以通过 update-index
命令的 --assume-unchanged
选项来做到这一点。
7.5 储藏
如果我们在某些事情进行到中间的时候,突然发现自己需要快速修复某个问题。这时候, 我们通常会希望立即着手去做相关的修改,但同时先不提交之前一直在做的事情。在这种情况下,我们可以用stash
命令先将这些修改保存在本地,日后再来处理。
我们通过 stash 命令将工作区和暂存区中的修改保存在一个被我们称之为储藏栈 (stash
stack) 的缓存区中。
> git stash
我们可以用 stash pop
命令将栈中所储藏的修改恢复到工作区中。
a. 恢复位于栈顶的被储藏修改
> git stash pop
b1. 储藏堆栈中有什么?
首先,我们要检查一下当前储藏了什么修改内容。
> git stash list
stash@{0}: WIP on master:297432e Mindmap updated.
stash@{1}: WIP on omaster:213e335 Introduction to workflow
b2. 恢复更早之前所储藏的修改
其次,我们还是要检查一下当前栈中储藏了什么修改内容。
> git stash pop stash@{1}
🌾 总结
-
版本库:项目的版本库通常驻留在其
.git
目录中。其中包含了以提交形式存储的项目历史。由于Git 是个分布式系统,所以同一个项目通常可能会有多个拥有不同历史的版本库。按照 Git 的设计,我们可以在必要时再将这些历史重新合并起来。 -
提交(通常也被称为版本、修订或修改集): 通过
commit
命令可以创建一次提交, 一 次提交通常存储了项目的某种确定状态,其中包含了该项目中所有文件的情况。每次提交中都包含了与作者和提交日期相关的元数据。特别地, Git中还存储了前/后提交之间的关系,这种关系将构成一个项目版本图,我们可以用log
命令将版本库中的这些提交显示出来。 -
提交散列值:提交散列值主要用于唯一标识提交。但同时它也是一种信息汇总,我们 可以用它来验证软件对象的存储完整性。通常一个提交散列值的长度是40个字符。
-
暂存区:暂存区(也称为索引)中所存储的是我们为下一次提交准备的内容,它以快照的形式保存了相关的文件内容。
-
添加生成快照:我们可以通过
add
命令在暂存区中创建一份被修改文件的快照。这样 如果我们再次修改了同一批文件,新增的修改就不会自动被纳入到下一次提交中了。 -
选择性提交:我们可以用
add
命令来指定哪些文件将会被存储在快照中,其余所有文 件将保持不变。 -
代码段选取:我们甚至可以通过
--interactive
选项来逐行(或者逐段)选取自己所需要的修改), 这种情况下只有被选取的那部分修改会以快照的形式被存储在暂存区中。 -
查看状态:
status
命令可以显示出哪些文件被纳入了下次提交,而哪些文件只是在本 地本修改了还尚未被注册到暂存区中。 -
重置暂存区:我们可以通过
git reset HEAD
命令将所有文件重置到当前的HEAD 版本。 -
.gitignore
文件:我们可以在这个文件中列出不需要被 Git 管理的文件目录。 -
储藏:我们可以通过
stash
命令将在工作区和暂存区中当前所做的修改储藏起来。之 后,再用git stash pop
命令将其恢复。
《【Git教程】(二)入门 ——关于工作区与版本库、版本提交、查看信息、克隆、推送与拉回的简单介绍 ~》
《【Git教程】(四)版本库 —— 存储系统,存储目录,提交对象及其命名、移动与复制~》