底层概念
Git 的组成概念主要是由三个区域和三个对象来完成,这是 Git 的工作原理与组成机制。
三大分区
Git 是分布式的,具备有远程仓库与本地仓库两个概念,但 Git 本质上就是一个数据库,本质的作用就是存储代码。
而在开发人员的**本地仓库 **系统中,根据数据管理流程可大致分为三个分区:
工作区 Working Directory
其实就是肉眼可见,可直接编辑的一个项目文件夹,即==开发人员当前正在开发的项目工作空间==就是工作区。
Git 系统不会管工作区中任何文件内容的修改,而只是监控暂存区。
工作区更像是一个
沙盒
空间,即你在工作区中不管如何修改,Git 都不会将其作为项目版本的更新迭代,除非你手动添加到暂存区中。
暂存区 Stage/Index
暂存区是 Git 系统内部帮助开发人员管理项目中临时修改的文件的区域,用于临时存放你的改动,事实上它只是一个文件,保存即将提交到文件列表的信息。外部不可见,属于 Git 系统版本库内部的一个工作区。
在内部,它会==存放上一次开发人员 git add
添加之后的数据,可以理解为是一个项目上一个版本的记录节点==。
在向版本库 commit
提交更新时,它会比较暂存区与工作区两者之中包含的文件细节差异,并给出相关更新提示。
暂存区是 Git 与 其他版本控制系统 最大的区别所在。
版本库 Commit History
其实就是==项目中的 .git
隐藏文件夹==。也叫本地仓库区。
安全存放数据的位置,这里面有你提交到所有版本的数据。其中HEAD指向最新放入仓库的版本。
当使用 git init
命令初始化一个项目时,就会创建一个项目的版本库,即产生一个 .git
隐藏文件夹。
在 .git
版本库中存了很多东西,其中最重要的就是 暂存区 Stage/Index
,也叫**索引区
**,以及 Git 为我们自动创建的主分支 master 和对应 HEAD 指针。
版本库中==存放已经 git commit
提交的数据==,在 push
的时候,就会把版本库中所记录下的所有暂存区的数据一次性 push
到远程 Git 仓库中。
在实际开发中,一般开发人员当天编写完代码执行
add添加到暂存区之后,会等到暂存区中的代码体量确定为一个完整版本时,才
commit提交到版本库中记录作为一个版本节点,最后一次性将版本库中记录的当前版本代码
push到远程仓库中
。
远程仓库 Remote
Remote:远程仓库,托管代码的服务器,可以简单的认为是你项目组中的一台电脑用于远程数据交换
Git 项目分区结构
- Directory:使用Git管理的一个目录,也就是一个仓库,包含我们的工作空间和Git的管理空间。
- WorkSpace:需要通过Git进行版本控制的目录和文件,这些目录和文件组成了工作空间。
- .git:存放Git管理信息的目录,初始化仓库的时候自动创建。
- Index/Stage:暂存区,或者叫待提交更新区,在提交进入repo之前,我们可以把所有的更新放在暂存区。
- Local Repo:本地仓库,一个存放在本地的版本库;HEAD会只是当前的开发分支(branch)。
- Stash:隐藏,是一个工作状态保存栈,用于保存/恢复WorkSpace中的临时状态。
数据传递流程/转换关系
由此以上三个概念,可以得出 Git 本地仓库到远程仓库的具体工作流程:
三大分区的转换关系如下:
git diff 分区对比
Git 提供了 git diff
指令用于查看三大分区之间保存的文件内容不同的对比分析。
diff >> different 不同的
工作区 vs 暂存区
$git diff
工作区 vs 版本库
$gir diff head
暂存区 vs 版本库
$git diff --cached
执行如下:
— : 表示减少了某个文件
+++ : 表示新增了某个文件
Git 的工作流程
按照 工作区 >> 暂存区 >> 版本库
的流程可将 Git 的工作流程划分为如下阶段:
1、在工作目录添加、修改文件
2、将需要进行版本管理的文件放入暂存区域
3、将暂存区域的文件提交到 Git 本地仓库
因此,Git 管理的文件有 3 种状态:已修改(Modified)、已暂存(Staged)、已提交(Committed)
.git 目录结构
当使用 git init
指令初始化一个项目仓库之后,就会在当前项目根目录下创建一个 .git
隐藏文件夹,内部存储的是当前项目的一些 Git 相关的重要信息。
hooks 目录
作用:包含客户端与服务端的钩子脚本。
-
Git 本身也是一个可拔插的程序【可自定义扩展】,它为用户交互仓库过程时提供了一些相对应的钩子函数来执行,同时开发人员也可以来自主编写这些钩子函数。
-
.sample
脚本文件
info 目录
作用:包含一个全局性排除文件。可自定义某些不需要 Git 管理的文件。
- exclude 排除文件
logs
目录
作用:保存日志信息。【其中包含 refs
被引用的分支及 HEAD 分支指针信息】。
- refs:引用的分支 log 信息
- HEAD 分支指针
objects 目录
作用:存储所有的数据内容(tree 树对象、blob 数据对象、commit 提交对象)
refs 目录
作用:存放指向数据的提交对象的指针(分支)。
-
heads
:当前 Git 项目所处的分支每当
git checkout -b
创建一个分支时,都会在refs/heads
目录下创建一个对应的分支文件,每个文件代表一个分支的提交,而分支文件内部保存的是最后一次提交对象的 hash10461@HKJ-PC MINGW64 ~/Desktop/SkillsWork/Git/example3/.git/refs/heads (GIT_DIR!) $ ll total 2 -rw-r--r-- 1 10461 197609 41 Nov 25 14:13 develop -rw-r--r-- 1 10461 197609 41 Nov 25 11:03 master 10461@HKJ-PC MINGW64 ~/Desktop/SkillsWork/Git/example3/.git/refs/heads (GIT_DIR!) $ cat develop 13f3587365844abfa7ec8599020cf3532dd9f9e5 10461@HKJ-PC MINGW64 ~/Desktop/SkillsWork/Git/example3/.git/refs/heads (GIT_DIR!) $ git cat-file -p 13f3587365844abfa7ec8599020cf3532dd9f9e5 tree 635dac506a927cf1b53c284bfd9151d382bbc39e parent dca2ba83991210551a75cc58c8587405ff1702c3 author huang <1046129660@qq.com> 1637809396 +0800 committer huang <1046129660@qq.com> 1637809396 +0800 deleted README.md
master
- 其他分支
-
当前引用的 head 分支指针
-
remote/origin :.git 仓库的分支来源
-
当前项目所创建的 tags 标签
config 配置文件
-
[core] :核心配置
-
[remote “origin”] :远程仓库来源地址
-
[branch “develop”]:分支信息
- merge:
develop
分支的合并分支
- merge:
-
description
本地仓库描述信息文件
index 暂存区
作用:暂存区数据信息
HEAD 分支指针
作用:一个指针,会指向 git checkout
切换的分支。
ref: refs/heads/develop
-
随着 commit 对象的迭代,HEAD 指针也会随之同步指向最后一次提交的 commit 对象。HEAD 指针就是分支。
-
当执行
git checkout -b
切换分支时,HEAD 中的内容也会随之指向为切换的分支。
显示 HEAD 信息
git show HEAD
FETCH_HEAD
作用:一个临时的 refs
引用指向,用于跟踪刚刚从远程仓库中 fetch > pull
获取的内容。
ORIG_HEAD
操作记录
作用:当使用 reset
、merge
、rebase
等风险操作时,Git 会将 HEAD 原来所指向 commit 对象的 sha-1 值存放于 ORIG_HEAD
文件中。它可以让我们找到最近一次风险操作的 HEAD 位置。
packed-refs
作用:打包的 tags
标签及标头信息,用于高效的数据库访问。
COMMIT_EDITMSG
作用:存储上一次 commit
提交时 -m
的备注信息。
三个对象
Git 底层提供了三个对象来分别对应 Git 工作流程所需的数据及操作对象。在我们指定一些 Git 操作时,内部就是通过这三个对象来完成的一些底层操作。
主要有如下:
blob 存储对象
Git 的核心部分就是一个简单的键值对数据库。开发人员可以向该数据库中插入任意类型的数据内容,一般保存在 objects
目录下,Git 会根据插入的内容生成一个唯一 hash 值作为唯一标识,通过该唯一标识可以在任意时刻再次检索该内容。
写入数据的流程则是由 Git 提供的 blob 对象来实现的。
- blob 对象是用来存储文本内容的。即把一个包含具体文本内容的文件作为一个 blob 对象存储在
git
系统中。
blob 对象的作用:Git 之所以能够作为版本控制系统,是因为 Git 会把文件的每次修改结果都作为一个对象保存起来。
常见的 blob 操作
创建一个文件并写入
通过
git
命令创建一个文本文件,并生成一个 blob 对象,最终插入到objects
目录下保存。
- objects/ 目录就是 commit 提交之后所在的版本库信息
$ echo "Hello World" | git hash-object -w --stdin
- ‘Hello World’ : 文本内容
git hash-object
:git 底层命令,可以根据传入的文本内容返回表示这些内容的键值【唯一 hash 值】-w
:使hash-object
命令存储数据,若不指定-w
参数则仅返回键值而不存储内容--stdin
:指定从标准输入设备如键盘来读取内容,若不指定--stdin
参数则需要指定一个输入文件的路径
生成的 hash 值:
# ce013625030ba8dba906f756967f9e9ca394464a
查看 Git 是如何存储数据的
find .git/objects -type f
# .git/objects/ce/013625030ba8dba906f756967f9e9ca394464a
Git 存储数据内容的方式:blob 对象的组织结构很像是一个文件系统。
一个文件【blob 对象】对应一个内容。由其生成的 hash 值来标识子目录和文件名。
键:文件夹名【以 hash-object 生成的 hash 值前 2 位】
值:文件名【以 hash-object 生成的 hash 值后 38 位】,内部存储压缩后的文件内容
在 Git 中一个 blob 对象表示一次 commit 提交的记录。
通过键值查看数据内容
可以通过之前生成的 hash 键值来反向解析出存储的内容。
$ git cat-file -p ce013625030ba8dba906f756967f9e9ca394464a
# hello
- -p 选项:可指示该命令自动判断内容的类型,并为我们显示格式友好的内容
通过键值查看数据内容的类型
$ git cat-file -t ce013625030ba8dba906f756967f9e9ca394464a
# blob
- -t :检索出数据内容的类型。
对一个文件进行简单的版本控制
创建一个新文件并将其存入数据库【工作区 >> 版本库】
$ echo 'version v1' > test.txt
$ git hash-object -w test.txt
git hash-object
会读取test.txt
文件中的内容并存储到objects
子目录下作为一个 blob 对象存储。
往
test.txt
中追加内容
$ echo 'version v2' > test.txt
$ git hash-object -w test.txt
# 若使用 vim 编辑 test.txt 文件之后,Git 并不会自动管理该文件内容的更新,而是需要手动提交一次 hash-object 才会插入到对应的 objects 子目录下存储。
查看存储的数据库内容
$ find .git/objects/
.git/objects/
.git/objects/f9
.git/objects/f9/14ca8b4497a44a7ea594e2d9579245688e608d
.git/objects/fb
.git/objects/fb/35247ad55b9e611657af32ea64f610abcee907
- 每一次追加内容,都会生成一个对应的 blob 对象,代表文件每一次的 commit 提交。
回退版本
首先查看 test.txt 的文件内容
$ cat test.txt
#version v1
#version v2
- Git 之所以把每次修改的结果都保存为一个blob对象,是方便版本回撤,即能把一个文件退回到任何时刻。
$ git cat-file -p f914ca8b4497a44a7ea594e2d9579245688e608d > test.txt
#version v1
直接解析出某次 commit 提交的版本记录内容来覆盖源数据。这样,下次 Git 再从 test.txt 文件中读取的就会是上一次版本的内容。
注意:blob 对象只对应项目中一个文件的改动,而一次版本的记录包含项目中多个文件的改动。
以上操作都是直接从工作区 >> 版本库,而真实的 Git 应该涉及到 暂存区。
tree 树对象
前提问题:
-
人为来记录
objects/
下每个文件的不同版本对应的 Sha-1 哈希值并不现实 -
且在 Git 中 文件名并没有被保存,而只是保存了文件的内容
这样也就无法对具体版本的文件进行索引
为了能更方便的检索出文件版本的改动记录,Git 采用树结构管理 hash 索引,对应压缩文件进行版本控制的。
概念
树 (tree) 对象,它能解决文件名保存的问题,也允许我们将多个文件组织到一起。Git 采用一种类似于 Unix 文件系统的方式存储内容。所有内容均以 tree 树对象和 blob 数据对象的形式存储,其中==树对象对应了 Git 文件系统中的目录项,blob 数据对象则对应具体内容==。
一个树对象中可以包含一条或多条记录,每条记录都会包含一个指向 blob 数据对象或子树对象的 SHA-1 指针,以及响应的模式、类型、文件名信息。一个树对象可以包含若干个 blob 数据对象和若干个 tree 树对象。
理解:树对象用来索引 Git 项目中每个文件产生的 blob 数据对象内容,从而达到快速查找的作用。
构建树对象
update-index 创建暂存区
通过 git update-index
指令可以==为某个文件的版本创建一个暂存区,表示一个文件的临时版本记录==。
在暂存区中,每次
update-index
都表示一个文件需要更新版本,故后来者会覆盖之前该文件在暂存区的记录内容。
$git update-index --add --cacheinfo 100644 [文件的hash值] test.txt
-
update-index
:表示为某个[文件名]
创建一个暂存区 -
--add
:因为此前该文件并不存在暂存区中,首次加入需要--add
-
--cacheinfo
:因为将要添加的文件位于 Git 数据库中,而不是位于当前 workspace 工作区目录中,所以需要--cacheinfo
-
100644
: 文件类型的标识-
Git 提供了如下几种文件标识:
100644:表示一个普通文本文件
100755:表示一个可执行文件
120000:表示一个符号链接
-
-
[hash值]
:对应记录在.git/objects/
版本库中的某个文件内容的 hash 值。【hash-object
生成的】-
故要想将文件保存在暂存区中,先在版本库中有记录,再根据某个文件记录塞入到暂存区中持续更新。
-
-
test.txt
:对应要塞入暂存区的工作区文件名
查询暂存区
该文件在暂存区生成的 hash 索引:
$git ls-files -s
#100644 cc628ccd10742baea8241c5924df992b5c019f71 0 new.txt
#100644 ce013625030ba8dba906f756967f9e9ca394464a 0 test.txt
文件标识–暂存区 hash 索引–undefined–文件名
write-tree 生成树对象
之后再通过 write-tree
将暂存区中的所有文件版本记录一并打包并写入到 .git/objects 版本库中,作为项目最新的版本。
$git write-tree
即:暂存区记录着工作区每一个文件的最新版本,而树对象记录着每一个项目的最新版本【包含多个 blob 文件】。
也可以说,一个树对象代表暂存区某一时刻的快照。
故,暂存区与工作区 文件同步。
此时,.git/objects
版本库中至少有三个文件:
.git/objects/92/0512d27e4df0c79ca4a929bc5d4254b3d05c4c # 工作区项目的第一个版本记录 【树对象】
.git/objects/a6/dwa891d9w1a9d1w98d45w98d4w98d4w98dw8d7 # new.txt 的第一个版本记录 【blob 对象】
.git/objects/ce/013625030ba8dba906f756967f9e9ca394464a # test.txt 的第一个版本记录 【blob 对象】
- blob 对象:文件版本
- tree 树对象:项目版本【可能包含多个 blob 或 tree】
若项目中多个文件进行了更新:
-
若执行了
hash-object
指令,则会在版本库中各自生成对应的版本记录 -
若执行了
update-index
指令,则在暂存区中覆盖之前的版本记录update-index
指令前提是得有.git/objects
生成的 hash 记录
示例
Git 从创建文件 > 塞入暂存区 > 构建项目版本记录流程如下:
$echo "hello" > test.txt
$git hash-object -w test.txt
$find .git/objects #ce/013625030ba8dba906f756967f9e9ca394464a
#---------------------------- 编写一个文件,并存入到版本库中【生成相对应的 hash 值】,作为该文件的第一个版本记录
$git update-index --add --cacheinfo 100644 ce013625030ba8dba906f756967f9e9ca394464a test.txt
$git ls-files -s #100644 ce013625030ba8dba906f756967f9e9ca394464a 0 test.txt
#---------------------------- 将写入到版本库中的某个文件版本记录存入到暂存区中,表示该文件的临时最新版本记录
$git write-tree
#---------------------------- 将暂存区中的所有文件版本记录一并打包为项目版本记录到版本库中,作为项目最新的版本
$find .git/objects -type f
#.git/objects/92/0512d27e4df0c79ca4a929bc5d4254b3d05c4c 工作区项目的第一个版本
#.git/objects/ce/013625030ba8dba906f756967f9e9ca394464a test.txt 文件的第一个版本
图解
Git 三大分区工作流程
工作区 > 暂存区 > 版本库 图解如下:
read-tree 合并树对象
前面提到,tree 树对象中不仅可以包含 blob 数据对象,也可以包含 tree 子树对象,即通过 read-tree
指令来实现。
$git read-tree --prefix=bak [树对象的 hash 值]
--prefix=bak
:表示新生树对象额外引用的一个暂存区引用,会与暂存区中其他文件记录同名【bak = 别名】。
通过引用某个树对象在
.git/objects
版本库记录的 hash 值索引,可以将该树对象包含到某个新生的树对象中。此时该新生树对象则不仅持有其他 blob 对象,还持有这个子 tree 树对象的引用。且不会管该子 tree 树对象的内容。
之后再通过 git write-tree
将包含子 tree 树对象的新生树对象写入到版本库中。
示例
假设版本库中有如下记录:
$find .git/objects -type f
#cc/628ccd10742baea8241c5924df992b5c019f71 # 树对象
#ce/013625030ba8dba906f756967f9e9ca394464a # blob 对象【new.txt】
#d7/fd677e7af85c7ef0d36f5181e11fed163b25a2 # blob 对象【test.txt】
假设此时暂存区中有如下记录:
$git ls-files -s
#100644 cc628ccd10742baea8241c5924df992b5c019f71 0 new.txt
#100644 ce013625030ba8dba906f756967f9e9ca394464a 0 test.txt
合并树对象:
$git read-tree --prefix=bak cc628ccd10742baea8241c5924df992b5c019f71
$git write-tree
查看暂存区:
#100644 cc628ccd10742baea8241c5924df992b5c019f71 0 bak/new.txt
#100644 ce013625030ba8dba906f756967f9e9ca394464a 0 bak/test.txt
#100644 cc628ccd10742baea8241c5924df992b5c019f71 0 new.txt
#100644 ce013625030ba8dba906f756967f9e9ca394464a 0 test.txt
图解
解析树对象
Git 根据某一时刻暂存区(即 Index 区域)所表示的状态创建并记录一个对应的树对象,如此重复便可依次记录(某个时间段内)一系列的树对象。
其实树对象就是对暂存区内操作的抽象,这棵树对象相当于就是快照。当我们的工作区有任何更改同步到暂存区时。便会调用 write-tree
命令。
通过 write-tree
命令向暂存区内容写入一个树对象。它会根据当前暂存区状态自动创建一个新的树对象。即每一次同步都产生一棵树对象。且该命令会返回一个 hash 指向树对象。
在 Git 中每一个文件(数据)都对应一个 hash (blob),每一个树对象都对应一个 hash(tree)。
总结:树对象就是项目快照。
查看树对象
$git cat-file -p master^{tree} / [树对象的 hash]
master^{tree}
:表示master
分支上最新的提交所指向的 tree 树对象
由此可以看出,树对象就像是某个待选择的记录目标,由 HEAD 切换分支来指向并提交 tree 树对象所包含的项目版本。
[树对象的 hash]
:查看指定 tree 树对象所包含的内容
$ git cat-file -p 84a225d8da142ada585856c613ed573a5f389132
#040000 tree d7fd677e7af85c7ef0d36f5181e11fed163b25a2 bak
#100644 blob cc628ccd10742baea8241c5924df992b5c019f71 new.txt
#100644 blob ce013625030ba8dba906f756967f9e9ca394464a test.txt
commit 提交对象
前提问题
现在有两个树对象(执行了两次 write-tree),分别代表了我们想要跟踪的不同项目快照。然而问题依旧,若想重用这些快照,你必须记住所有两个树对象的 SHA-1 哈希值。并且,你也完全不知道是谁、在什么时刻保存了这些快照。
而这些以上,正是提交对象(commit object)能为你保存的基本信息。
概念
提交对象的直接作用就是 对某个树对象进行包裹,并包含某次提交该树对象的一些基本信息。
树对象才是项目真正的一次版本记录,提交对象只是对某次树对象的提交操作封装了一些信息而已,提交对象所包含的信息直接面向于开发人员。
构建提交对象
可以通过 commit-tree
来生成一个提交对象,内部包含对应的快照(树对象),最后 commit 对象会存入到本地版本库中,它返回一个 commit 提交对象的 hash 值:
$echo 'first commit' | git commit-tree [树对象的 hash]
#6e4e6320333c3c0824ccebe9dcfeb26439f0e743
作用:对某个树对象【项目快照】进行一次提交,并备注提交的信息。
echo 'first commit'
:本次提交的备注信息[树对象的 hash]
:提交的具体树对象【项目快照】等价于
git commit -m 'first commit'
查看提交对象
$git cat-file -p [提交对象的 hash]
$git cat-file -t [提交对象的 hash]
# commit
返回:
tree 84a225d8da142ada585856c613ed573a5f389132 # 提交对象指向的 tree 树对象 hash
author huang <1046129660@qq.com> 1637681081 +0800 # 项目作者信息
committer huang <1046129660@qq.com> 1637681081 +0800 # 本次提交者信息
first commit # 本次提交的备注信息
提交对象的格式很简单:
它先指定一个顶层树对象,代表当前项目快照;然后是作者/提交者信息(依据你的 user.name 和 user.email 配置来设定,外加一个时间戳);留空一行,最后是提交注释
提交对象的父子关系
commit
提交对象最有趣的地方在于它具备父子关系,表示为自顶向下的链式结构。
每一次提交操作之后,若是第一次提交则作为根提交记录存在,若不是,则会在每次提交之后都保存指向上一次的提交对象的 hash 。
之所以保存上一次提交对象的 hash 索引的目的就是为了==版本回滚==。
而 commit
提交对象具备的父子关系就是为了==能更好的根据提交对象的 hash 索引来指定项目回滚到具体的某个版本==。
指令
echo 'second commit' | git commit-tree [树对象的 hash] -p [上一次提交对象的 hash]
作用:提交某个树对象【项目快照】的同时保存上一次的提交记录,便于版本回滚。
再次查看:
tree 84a225d8da142ada585856c613ed573a5f389132
parent 6e4e6320333c3c0824ccebe9dcfeb26439f0e743 # 上一次 commit 对象提交的记录
author huang <1046129660@qq.com> 1637681654 +0800
committer huang <1046129660@qq.com> 1637681654 +0800
second commit
parent
:指向上一次 commit 对象提交的记录。
图解
由此图可以看出,每次 commit
对象提交都会指向一个 tree 树对象【项目快照】,这是 Git 内部所管理的,外部无需关心;而每次又有新的提交后,都会产生一个 parent
变量来指向上一次的提交,便于版本回滚,并形成自顶向下的链式结构;而版本回滚的操作,就是将当前 commit
对象指向的引用变更到某个想要切换的 commit
对象即可获取到对应的 tree
树对象【项目快照】,完成版本回滚的操作。
高层概念
高层命令就是对 Git 底层命令一系列操作的抽象
基本流程
init 初始化仓库
$git init
作用:初始化后,在当前目录下会出现一个名为 .git
的隐藏目录,所有 Git 需要的数据和资源都存放在这个目录中。不过目前,仅仅是按照既有的结构框架初始化好了里边所有的文件和目录,还没有开始跟踪管理项目中的任何一个文件。
add 添加到暂存区
$git add [目录文件名 | ./(当前目录所有修改的文件)]
-
git add
是以下两个底层命令的结合: >>
git hash-object -w ...
# 将修改的文件添加到本地版本库作为版本记录,并生成一个 hash。有 n 个修改的文件,则会执行 n 次。
>>
git update-index
# 将版本库中的文件版本记录存入到暂存区,并创建一个对应的树对象。
作用:将当前工作区中改动的文件添加到暂存区中作为文件版本记录。
真实的内部流程:
先通过
hash-object
将修改的文件内容生成一个 hash 值存储到.git/objects
版本库中,再从版本库中提取该文件执行update-index
放入到暂存区中。当每一次git add
之后,都会在版本库中生成新的文件版本记录【增量】,而暂存区中则是该文件的最新记录【覆盖】。
注:至于那些工作区未曾改动的文件,由于文件内容不变,hash 不变,故不会生成版本记录到版本库中,也就不会存入暂存区。
commit 提交到本地版本库
$git commit
# 输入注释信息
作用:将当前暂存区的内容添加到本地版本库中。
表示要进入 vim 编辑器页面输入多行的注释提交信息
注意:注释内容不可以写在带有
#
注释的区域,否则会被 Git 当成注释信息来处理,不会进行提交。
真实的内部流程:
之后,当用户
git commit
执行提交操作时,Git 会参照暂存区中的内容执行write-tree
生成一个树对象并存入版本库中,提取当前 HEAD 指针指向的树对象,执行commit-tree
,注入一些提交信息,最后封装成commit
提交对象,存入版本库中。
总结:一个完整的 Git 流程至少包含一个 blob 数据对象、一个 tree 树对象、一个 commit 提交对象。
参数附带
-m 单行注释
$git commit -m "注释信息"
-m
:表示要输入的message
单行注释信息。
git commit
是以下两个底层命令的结合: >>
git write-tree
# 将指定的封装了暂存区信息的 树对象写入到版本库中,并返回一个 hash,代表版本记录。
>>
git commit-tree
# 提取一个树对象,并注入相对应的提交信息,封装成一个 commit 提交对象,存入到本地版本库中,待 push。
-a 简写
$git commit -a -m '注释信息'
-a
:表示跳过git add
命令的输入。
注意:-a
的前提是该文件是已跟踪状态,即之前执行过 git add
存入暂存区才可以。
表面上看似是跳过了暂存区的步骤,实际上 Git 内部还是会帮你搞定
git add
跟git commit
两条命令的执行。
工作流程图解
文件状态管理
概念
工作区目录下的所有文件都不外乎两种状态:已跟踪、未跟踪。
已跟踪的文件是指==本来就纳入到 Git 版本控制管理的文件==,在上次快照中有它们的记录,工作一段时间后,它们的状态可能是已提交、已修改、已暂存。
未跟踪的文件是==指在 git init 之前就存在的文件==,在初始化之后未将该文件纳入到 Git 版本控制管理中。它们既没有上次更新时的快照,也不在当前的暂存区。
初次克隆某个仓库时,工作区目录下的所有文件属于已跟踪文件,且状态是已提交;在编辑过某些文件后,Git 将这些文件都标记为已修改。我们逐步把修改过的文件放到暂存区域,直到最后一次性提交所有这些暂存起来的文件。
文件状态变化周期
untracked
:未跟踪 >>> 添加了文件 【unmodified
】unmodified
:未修改 >>> 编辑了文件【modified
】modified
:已修改 >>> 存入暂存区【staged
】staged
:已暂存 >>>commit
提交【unmodified
】
检查当前文件状态
$git status
作用:确定当前文件处于什么状态。(已跟踪(已提交、已暂存、已修改) 未跟踪)
克隆仓库后的文件
如果在克隆仓库之后立即执行此命令,会看到类似这样的输出:
$> git status
On branch master
nothing to commit, working directory clean
表示所在的工作区相当干净。换句话说,所有已跟踪文件在上次提交后都未被修改过。此外,上面的信息还表明,当前目录下没有出现任何处于未跟踪的新文件,否则 Git 会在这里列出来。最后,该命令还显示了当前所在的分支是 master,这是默认的分支名称,实际上是可以修改的。
未跟踪文件
如果创建一个新文件 README,保存退出后执行 git status
会看到 README 文件出现在未跟踪列表中:
$> git status
On branch master
Untracked files: # 未跟踪文件
(use "git add <file>..." to include in what will be committed)
README
nothing added to commit but untracked files present (use "git add" to track) # 使用 git add 标记为跟踪
在状态报告中可以看到新建的 README 文件出现在
Untracked files
下面。未跟踪的文件意味着 Git 在之前的快照(提交)中没有这些文件;Git 不会自动将之纳入跟踪范围,除非你明确告诉 Git “我需要跟踪该文件”,因而不用担心把临时文件什么的也归入版本管理。
跟踪新文件(暂存)
执行以下命令:
$git add [filename]
$git status
Changes to be committed:
(use "git rm --cached <file>..." to unstage)
(use "git reset <file>..." to unstage)
new file: README.md
可以看到:
README 文件已被跟踪,并处于暂存状态。
只要是在
Change to be committed
这句话下面的都表示处于暂存状态【下一步是提交】,如果此时提交,那么该文件此时此刻的版本将被留存在历史记录中。在
git add
后面可以指明要跟踪的文件和文件路径。如果是目录的话,就说明要递归跟踪该目录下的所有文件。译注:其实
git add
的潜台词是把目标文件快照放入暂存区域,即add file to staged area
,同时未曾跟踪过的文件标记为已跟踪。
暂存已修改文件
现在 README 文件都已暂存,下次提交时就会一并记录到仓库。假设此时,你想要在 README 里再加条注释,重新编辑存盘后,准备好提交。不过且慢,再运行 git status 看看:
$git status
On branch master
Changes to be committed: # 告诉我们下一步应该要提交此文件。
(use "git reset HEAD <file>..." to unstage)
new file: README
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: README
README 文件出现了两次!一次算已修改,一次算已暂存,这怎么可能呢?好吧,实际上 Git 只不过暂存了你运行 git add 命令时的版本,如果现在提交,那么提交的是添加注释前的版本,而非当前工作目录中的版本。所以,运行了 git add 之后又作了修订的文件,需要重新运行 git add 把最新版本重新暂存起来:
$ git add README
$ git status
On branch master
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
new file: README
小结:已暂存:绿色,已修改:红色,已提交:啥都没有。
查看暂存和未暂存文件
实际上 git status
的显示比较简单,仅仅只是列出了修改过的文件,如果要看具体修改了什么地方,可以通过 git diff
命令来查看。
修改后未暂存
$git diff
diff --git a/README.md b/README.md
index d905d9d..2dfd801 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,4 @@
-e
# 修改的内容 ∨
+wdwdhwuhdwi
+
+
+dwadawe
作用:查看工作区目录下所有修改过,但未暂存的文件(未执行
git add
)。
暂存后未提交
$git diff --staged / --cached
diff --git a/README.md b/README.md # 暂存后未提交文件
new file mode 100644 # 模文件式
index 0000000..d905d9d # 文件的暂存区索引
--- /dev/null
+++ b/README.md
@@ -0,0 +1 @@
+e
作用:查看暂存区中未提交的文件(未执行
git commit
)。
被删除文件的状态
当从工作区目录中删除了某个文件,再执行 git status
查看状态时,会提示如下信息:
$rm README.md
$git status
On branch master
Changes not staged for commit:
(use "git add/rm <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
deleted: README.md
no changes added to commit (use "git add" and/or "git commit -a")
可以看到,任何工作区的文件改动,包括修改、删除、增加,在 Git 眼中,都是修改状态。只不过提示的信息不同:
new file
:新增的文件modified
:修改的文件deleted
:删除的文件
此时,由于文件状态是符合 Git 流程的,所以还是可以执行 git add
、git commit
等一系列操作。
$git add
On branch master
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
deleted: README.md # 此时由 未跟踪的红色 转为 已跟踪的绿色
$git commit -m 'deleted README.md'
[master 13f3587] deleted README.md
1 file changed, 4 deletions(-)
delete mode 100644 README.md
此时,再查看暂存区与版本库信息:
$ git ls-files -s
# 未输出任何信息
$ find .git/objects/ -type f
.git/objects/13/f3587365844abfa7ec8599020cf3532dd9f9e5
.git/objects/2d/fd8014f9ce0d377fdc4694016aa957a51f7a69
.git/objects/63/5dac506a927cf1b53c284bfd9151d382bbc39e
.git/objects/65/91a864d2dd3365486bab5ebedc443bf03a5008
.git/objects/76/4c56f52bb309aa0cc5bb8c7a70dadeb45e3094
可以看到,暂存区中的记录随着工作区文件的删除而删除,但版本库中的信息,却是随着
git commit
操作的执行而新增了一条树对象【项目记录】,只不过这条新增的树对象之中并未包含任何表示文件内容的 blob 数据对象。由此,这个被删除的文件并不会再纳入到 Git 版本管理中了,因为没有任何树对象指向该文件。
被重命名文件的状态
当为工作区目录某个文件重命名之后,再执行 git status
查看状态时,会提示如下信息:
$mv REAMDE.md REAME
$git status
On branch master
Changes not staged for commit:
(use "git add/rm <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
deleted: README.md
Untracked files:
(use "git add <file>..." to include in what will be committed)
README
no changes added to commit (use "git add" and/or "git commit -a")
可以看到,工作区目录文件的重命名操作,在 Git 内部是先删除源文件,再新增一个文件。
rm
指令相当于是一次性执行了以下三条命令:
mv README.txt README
rm README.txt
git add README
故还需要为重命名的文件执行一次 git add
表示将重新跟踪重命名的文件:
$git add README
$git status
On branch master
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
renamed: README.md -> README
于此,重命名文件的状态在 Git 中表示
renamed
。在内部的细节流程中,重命名文件的操作需要进行一套完整的add > commit
流程。