这篇文章是对 Pro Git一书Git 内部原理章节的总结。理解了Git的数据类型和内部原理有助于我们更好的、效率更高的使用Git。
对底层命令和高层命令的介绍Pro Git 10.1 Git 内部原理 - 底层命令和高层命令介绍的简明易懂,这里就不再做赘述。
当我们在一个目录执行git init
命令时,Git会在目录创建一个.git目录。这个目录几乎包括了所有Git操作和存储的对象。如想要复制或备份一个版本库,复制或备份此目录即可。其目录结构如下所示:
目录&文件 | 作用 |
---|---|
config | config文件包含项目特有的配置选项 |
description | description 文件仅供 GitWeb 程序使用 |
HEAD | HEAD 文件内容指向目前被检出的分支的引用 |
hooks | hooks 目录包含客户端或服务端的钩子脚本(hook scripts) |
info | info 目录包含一个全局性排除(global exclude)文件,用以放置那些不希望被记录在 .gitignore 文件中的忽略模式(ignored patterns) |
objects | objects 目录存储所有数据内容 |
resf | refs 目录存储指向数据(分支)的提交对象的指针(也就是说refs目录是存放指针的,指针指向objects目录中的数据) |
Git中有四种对象类型,分别为blob、tree、commit和tag对象类型,每个对象都有一个SHA-1值(指针)指向自身实例,SHA-1是在创建对象时生成,通过这个SHA-1来访问对象实例,对象内容存储在.git/objects目录。
类型 | 说明 |
---|---|
blob,数据对象 | 可存储较大内容的文本,可看作是操作系统中文件的内容 |
tree,树对象 | 可存储多个tree和blob对象,可看作是操作系统中的目录 |
commit,提交对象 | 可存储一个tree对象和其它信息(提交人、提交日期、父commit对象和提交说明等) |
tag,标签对象 | tag对象存储一个SHA-1(SHA-1可以指向任意对象,不仅仅是commit对象)和tag创建人、创建时间、tag说明等信息,创建annotated(附注标签)类型标签就是在创建一个tag对象 |
我们将工作目录的改动提交本地仓库通常会使用高层命令git add
和git commit
,这节将使用底层命令来创建Git对象方式来替代git add
和git commit
命令功能。通过这样的方式来理解Git对象和add和commit命令的运作原理。为了演示我们在本地初始化一个Git仓库,并完成以下操作:
新建一个fileA.txt文件,写入一些内容提交到仓库。
新建fileB文件,修改fileA文件提交到仓库
将包含fielA.txt和fileB.txt文件的bak目录提交仓库
使用底层命令完成这个步骤操作如下:
git hash-object -w fileA.txt
命令将fileA.txt文件内容写到blob对象,并返回SHA-1值。不指定-w选项只会返回SHA-1值不会将fileA.txt文件内容写入blob对象。Administrator@USER-20171018VX MINGW64 /e/gitObject (master)
$ git hash-object -w fileA.txt
warning: LF will be replaced by CRLF in fileA.txt.
The file will have its original line endings in your working directory.
78981922613b2afb6025042ff6bd878ac1994e85
查看78981922613b2afb6025042ff6bd878ac1994e85指向blob对象内容
Administrator@USER-20171018VX MINGW64 /e/gitObject (master)
$ git cat-file -p 78981922613b2afb6025042ff6bd878ac1994e85
a
将blob对象写tree对象
将blob对象写入tree对象首先要将blob对象加载到暂存区,然后将暂存区内容写入一个树对象。
将blob对象添加到暂存区
Administrator@USER-20171018VX MINGW64 /e/gitObject (master)
$ git update-index --add --cacheinfo 10064 78981922613b2afb6025042ff6bd878ac1994e85 fileA.txt
查看当前暂存区中的内容
Administrator@USER-20171018VX MINGW64 /e/gitObject (master)
$ git ls-files -s
100644 78981922613b2afb6025042ff6bd878ac1994e85 0 fileA.txt
将暂存区内容写入一个树对象
Administrator@USER-20171018VX MINGW64 /e/gitObject (master)
$ git write-tree
80adf9e9282dd6167e39f7aa89324ee785709d32
查看80adf9e9282dd6167e39f7aa89324ee785709d32指向tree对象内容(格式)。100644为文件模式,blob对象有三种文件模式,其中100644表示一个普通文件,100755表示一个可执行文件,120000表示一个符号链接。blob表示对象类型。7898192表示指向blob对象的SHA-1,fileA.txt表示文件名。
Administrator@USER-20171018VX MINGW64 /e/gitObject (master)
$ git cat-file -p 80adf9e9282dd6167e39f7aa89324ee785709d32
100644 blob 78981922613b2afb6025042ff6bd878ac1994e85 fileA.txt
将tree对象写入commit对象
使用git commit-tree
命令将tree对象写入commit对象。
Administrator@USER-20171018VX MINGW64 /e/gitObject (master)
$ echo "first commit" | git commit-tree 80adf9e9282dd6167e39f7aa89324ee785709d32
d8527dfb5e0d44cfea240365cc8a90955bd54a56
查看d8527dfb5e0d44cfea240365cc8a90955bd54a56指向commit对象内容。tree是我们写入的80adf9eSHA-1值,author表示这次提交的作者,comitter表示这次提交的提交人,first commit是提交说明。
Administrator@USER-20171018VX MINGW64 /e/gitObject (master)
$ git cat-file -p d8527dfb5e0d44cfea240365cc8a90955bd54a56
tree 80adf9e9282dd6167e39f7aa89324ee785709d32
author zhangxy <928839929@qq.com> 1536224278 +0800
committer zhangxy <928839929@qq.com> 1536224278 +0800
first commit
目前我们使用了Git底层命令来完成了add和commit命令的工作,现在Git内部数据结构如下图所示:
新建fileB.txt文件,修改fileA.txt文件,将fielA和fileB文件分别写入blob对象并读入暂存区。暂存区内容如下:
$ git ls-files -s
100644 f70f10e4db19068f79bc43844b49f3eece45c4e8 0 fileA.txt
100644 61780798228d17af2d34fce4cfbdf35556832472 0 fileB.txt
将暂存区内容写入一个tree对象
Administrator@USER-20171018VX MINGW64 /e/gitObject (master)
$ git write-tree
38056b9ac03f611728742ba8570cc41446089d1e
将tree对象写入一个commit对象,选项-p表示为当前commit对象设置一个父commit对象(即上次提交产生的commit对象)。
Administrator@USER-20171018VX MINGW64 /e/gitObject (master)
$ echo "second commit" | git commit-tree 38056b9ac03f611728742ba8570cc41446089d1e -p d8527df
f999b8b2232c708674d345ecccae5611e812625e
查看637eea644d2d867b7afe03307ae50dc60d1489db commit对象内容
Administrator@USER-20171018VX MINGW64 /e/gitObject (master)
$ git cat-file -p f999b8b2232c708674d345ecccae5611e812625e
tree 38056b9ac03f611728742ba8570cc41446089d1e
parent d8527dfb5e0d44cfea240365cc8a90955bd54a56
author zhangxy <928839929@qq.com> 1536285649 +0800
committer zhangxy <928839929@qq.com> 1536285649 +0800
second commit
新建fileB文件,修改fileA文件提交到仓库后Git内部数据结构
fileA文件为第一次提交中的版本(7898192指向的blob对象内容)。我们需要将7898192写入一个tree对象并加载到暂存区,用这个tree对象表示bak目录,这个tree对象已经在第一步时候产生了,SHA-1为80adf9e。
使用read-tree命令将80adf9e tree对象读入到暂存区,命名为bak。 –prefix 选项表示将一个已有的树对象作为子树读入暂存区。
Administrator@USER-20171018VX MINGW64 /e/gitObject (master)
$ git read-tree --prefix=bak 80adf9e9282dd6167e39f7aa89324ee785709d32
Administrator@USER-20171018VX MINGW64 /e/gitObject (master)
$ git ls-files -s
100644 78981922613b2afb6025042ff6bd878ac1994e85 0 bak/fileA.txt
100644 f70f10e4db19068f79bc43844b49f3eece45c4e8 0 fileA.txt
100644 61780798228d17af2d34fce4cfbdf35556832472 0 fileB.txt
将当前暂存区写入一个tree对象
Administrator@USER-20171018VX MINGW64 /e/gitObject (master)
$ git write-tree
d5622fe8e764ab9de57688510cef24408e54f61d
将d5622fe8e764ab9de57688510cef24408e54f61d tree对象写入一个commit对象
Administrator@USER-20171018VX MINGW64 /e/gitObject (master)
$ echo "third commit" | git commit-tree d5622fe8e764ab9de57688510cef24408e54f61d -p f999b8b
d8e38ed05df88cbd2069c585cfe5b61b563f82c3
查看d8e38ed05df88cbd2069c585cfe5b61b563f82c3 commit对象内容
Administrator@USER-20171018VX MINGW64 /e/gitObject (master)
$ git cat-file -p d8e38ed05df88cbd2069c585cfe5b61b563f82c3
tree d5622fe8e764ab9de57688510cef24408e54f61d
parent f999b8b2232c708674d345ecccae5611e812625e
author zhangxy <928839929@qq.com> 1536285932 +0800
committer zhangxy <928839929@qq.com> 1536285932 +0800
third commit
将包含fileA.txt文件的bak目录提交仓库后Git内部数据结构
查看提交历史记录
Administrator@USER-20171018VX MINGW64 /e/gitObject (master)
$ git log --oneline d8e38ed
d8e38ed third commit
f999b8b second commit
d8527df first commit
现在我们向本地仓库提交了三次,我们想给第一次和第二次提交分别打上名称为v0.1和v0.2的标签。在这之前我们需要了解tag对象分为两种类型:
- annotated(附注标签)类型:tag包含的SHA-1指向一个tag对象,对象中包括指向Git对象的SHA-1值、tag说明等信息
- lightweight(轻量标签)类型:tag只包含commit对象的SHA-1
使用lightweight类型tag对象给第一次提交打上v0.1标签
Administrator@USER-20171018VX MINGW64 /e/gitObject (master)
$ git update-ref refs/tags/v0.1 d8527df
查看v0.1标签内容我们可以看出v0.1tag引用指向了第一次提交产生的commit对象。
Administrator@USER-20171018VX MINGW64 /e/gitObject (master)
$ cat .git/refs/tags/v0.1
d8527dfb5e0d44cfea240365cc8a90955bd54a56
使用annotated类型tag对象给第一次提交打上v0.2标签。Pro Git书中也没有给出如何用底层命令创建一个annotated类型的tag对象,所以就用git tag -a
命令吧!
Administrator@USER-20171018VX MINGW64 /e/gitObject (master)
$ git tag -a v0.2 f999b8b -m "第二次提交"
查看v0.1标签内容我们可以看出查看v0.2tag引用也指向了一个tag对象。查看这个tag对象内容我们发现tag对象和commit对象很相似,但是tag对象可以指向任何Git对象,但通常是一个commit对象。commit对象只能指向一个tree对象。tag对象可以看作是一个永远不会移动的分支引用,只会指向这个对象,只不过给这个对象起个别名而已。
Administrator@USER-20171018VX MINGW64 /e/gitObject (master)
$ cat .git/refs/tags/v0.2
ffb9ac6edf5b2048eaed0ac090850ea7eaaffbb3
Administrator@USER-20171018VX MINGW64 /e/gitObject (master)
$ git cat-file -p ffb9ac6edf5b2048eaed0ac090850ea7eaaffbb3
object f999b8b2232c708674d345ecccae5611e812625e
type commit
tag v0.2
tagger zhangxy <928839929@qq.com> 1536288673 +0800
第二次提交
Administrator@USER-20171018VX MINGW64 /e/gitObject (master)
$ git cat-file -t ffb9ac6edf5b2048eaed0ac090850ea7eaaffbb3
tag
查看提交历史发现我们将第一次和第二次提交打上了v0.1和v0.2标签。
Administrator@USER-20171018VX MINGW64 /e/gitObject (master)
$ git log --oneline d8e38ed
d8e38ed third commit
f999b8b (tag: v0.2) second commit
d8527df (tag: v0.1) first commit
Git中有四种引用类型,分别为tag、HEAD、heads和remotes引用。
类型 | 说明 |
---|---|
tag,标签引用 | .git/refs/tags目录中每个文件代表一个标签,标签的内容是一个SHA-1值,如果标签类型是轻量标签,则SHA-1指向的是一个commit对象,如果标签类型是附注标签,则SHA-1只指向一个tag对象 |
HEAD,当前分支引用 | .git/HEAD文件存储一个指向当前捡出分支引用的路径,即HEAD表示当前分支 |
heads,分支引用 | .git/refs/heads目录中每个文件代表一个本地分支,文件中存储的是指向分支最后一次提交的SHA-1 |
remotes,远程引用 | .git/refs/remotes目录中每个文件代表远程仓库分支在本地的引用,文件中存储的是指向远程仓库分支最新一次提交的SHA-1 |
我们在 给commit提交打标签 我们已经看到tags引用保存在.git/refs/tags/,目录中的文件保存的SHA-1根据tag对象类型的不同指向一个commit对象或tag对象。
现在查看提交历史都需要显示指定最后一次提交的SHA-1值(git log --oneline d8e38ed
),这样很麻烦,可以通过底层命令将最新的一次提交的SHA-1值写入到refs/heads目录中的一个文件里,通过文件名称来引用最新一次提交。
Administrator@USER-20171018VX MINGW64 /e/gitObject (master)
$ git update-ref refs/heads/master d8e38ed
Administrator@USER-20171018VX MINGW64 /e/gitObject (master)
$ cat .git/refs/heads/master
d8e38ed05df88cbd2069c585cfe5b61b563f82c3
我们可以看到heads引用实质上就是指向最新一次提交的引用。在使用高层命令commit将文件提交到仓库时,commit命令会自动为我们将refs/heads/master文件内容更新成最新一次提交的SHA-1值。现在我们可以使用git log --oneline master
来查看master分支的提交历史。当我们从远程仓库clone一个仓库到本地,master分支会自动被创建并指向最新一次提交。
HEAD引用基本就是 Git 分支的本质:一个指向某一系列提交之首的指针或引用。 若想在第二个提交上创建一个分支,可以这么做git update-ref refs/heads/test f999b8b
。
$ git log --oneline --all
d8e38ed (HEAD -> master) third commit
f999b8b (tag: v0.2, test) second commit
d8527df (tag: v0.1) first commit
Git允许我们捡出分支、在分支上工作、合并分支,假设我们使用git checkout -b dev
创建并捡出dev分支,这其中就涉及到一个问题:Git怎么知道我在那个分支上工作?答案就是HEAD引用,HEAD引用指向当前分支,.git/HEAD文件保存这HEAD引用的内容。
Administrator@USER-20171018VX MINGW64 /e/gitObject (master)
$ cat .git/HEAD
ref: refs/heads/master
我们使用底层命令symbolic-ref来改变.git/HEAD文件内容。其实我们在执行git checkout dev
命令时,此命令就会使用底层命令symbolic-ref来改变.git/HEAD文件内容来达到切换分支的目的。当我们从远程仓库clone一个仓库到本地或在本地初始化一个仓库,HEAD引用会默认指向master分支。
Administrator@USER-20171018VX MINGW64 /e/gitObject (master)
$ git symbolic-ref HEAD refs/heads/dev
Administrator@USER-20171018VX MINGW64 /e/gitObject (dev)
$ cat .git/HEAD
ref: refs/heads/dev
当我们使用git clone https://gitee.com/xxxx/xxxx.git
将远程仓库克隆到本地,本会有个远程分支在本地的引用,通过origin/master(远程仓库名称/分支名称)方式来访问。这些remote引用内容存储在.git/refs/remotes/目录。
gitObject表示远程仓库名称,master表示远程仓库中分支名称。
Administrator@USER-20171018VX MINGW64 /d/gitObject (master)
$ find .git/refs/remotes/
.git/refs/remotes/
.git/refs/remotes/gitObject
.git/refs/remotes/gitObject/master
远程分支引用的内容存储的是SHA-1值,这SHA-1值指向了和远程分支最后一次推送或拉取的一串提交对象中的最新提交对象。
Administrator@USER-20171018VX MINGW64 /d/gitObject (master)
$ cat .git/refs/remotes/gitObject/master
50b73681a4ab043a8dc7b4ed85e7a51b997fbc39
git fectch
和git pull
命令都会改变远程引用的内容。一般情况下我们会将远程仓库分支推送的最新提交通过git fecth
命令抓取到本地仓库的远程引用分支,然后再使用git merge origin/master
命令合并到自己本地的其它分支,在将合并后的分支使用git pull
命令推送到远程分支。远程引用就像远程仓库的一个书签,记录了远程仓库每个分支最新的提交对象。注意:远程引用并不自动和远程仓库的分支自动同步,我们需要使用git fecth
命令手工同步。如果你想查看远程仓库的最新情况(有哪些分支,每个分支的提交历史)之前最好先执行git fetch
命令。
假设现在远程仓库master分支有一个新提交,使用git fetch
拉取这次新提交到origin/mastger分支。那么现在本地Git仓库的远程分支和本地分支提交历史如下图