git原理浅析
共 46113字,需浏览 93分钟
·
2021-06-06 14:46
git 历史
linux
之父 Linus Torvalds
大家应该都知道,而 git
也是由 Linus
开发的。从 1991
年发布了第一版的 linux
内核,Linux 内核开源项目有着众多的参与者,但绝大多数的 Linux
内核维护工作都花在了提交补丁和保存归档的繁琐事务上(1991
-2002
年间)。到 2002
年,整个项目组开始启用一个专有的分布式版本控制系统 BitKeeper
来管理和维护代码,之前市面上也有其他的版本管理系统,比如 CSV
、SVN
,但是 Linus
觉得它们很蠢,直到有了 BitKeeper
才开始使用版本管理系统。
至于为什么又自己开发了 git
,看完下边对 Linus
的采访就明白了。
你为什么要开发 Git?
Torvalds:我从来没有想过去做版本控制软件,因为在我看来那是计算机世界里最无聊的事了(如果数据库除外的话 ;^),我天生就不喜欢 source-control management (SCM)。但是 BitKeeper(BK) 的诞生改变了我对版本控制的认识。BK 在大多数方面是正确的,在本地保存一个仓库的副本,分布式合并确实是一大创新。这个分布式版本控制的创新完美地解决了 SCM 的通病:“谁可以修改代码”的难题。BK 告诉我们,你只要给每个人一个仓库,问题就解决了。但是 BK 也存在一些问题,技术上的问题(例如重命名很麻烦)还不算什么,它最大的坏处是不开源,很多人因为这个不使用它。所以即使我们有几个核心维护者使用 BK——开源项目可以免费使用——但它也没有普及。虽然它帮助过我们开发内核,但依然有不少痛点没有解决。
当 Tridge 违反 BK 的使用协议反编译 BK 的时候,我们到达了紧急关头。我花了几个周(还是几个月来着?)试图调解 Tridge 和 Larry McVoy(注:他是 Bitkeeper 的 老大),最后也没有成功。我意识到我不能继续使用 BK 了,但我真的不想回到没有 BK 的黑暗时代。遗憾的是,我们想用其他 SCM 来代替它,却没有找到能在远程方面工作得好的。现有的软件不能满足我对远程方面的需求,我又担心整个流程和代码的完整性,所以最后我决定自己写一个。
总结就是,本来 BK
免费给他们用,但是有 linux
内核有成员开始反编译 BK
,BK
就不让他们用了,然后 Linus
就用了几周的时间自己写了一个,git
就此诞生。。。然后 linus
就专心又去搞 linux
了,把 git
交给团队成员 Junio Hamano
进行后期的迭代维护。
git 原理浅析
首先在一个空文件夹中执行 git init
命令初始化 git
仓库,然后会自动生成一个隐藏文件夹 .git
,目录树如下。
.git
├── HEAD
├── config
├── description
├── hooks
│ ├── applypatch-msg.sample
│ ├── commit-msg.sample
│ ├── fsmonitor-watchman.sample
│ ├── post-update.sample
│ ├── pre-applypatch.sample
│ ├── pre-commit.sample
│ ├── pre-merge-commit.sample
│ ├── pre-push.sample
│ ├── pre-rebase.sample
│ ├── pre-receive.sample
│ ├── prepare-commit-msg.sample
│ └── update.sample
├── info
│ └── exclude
├── objects
│ ├── info
│ └── pack
└── refs
├── heads
└── tags
下边依次分析下上边的文件。
description 文件
description
文件仅供 GitWeb
程序使用,一般用不到。
info文件夹
info
目录包含一个全局性排除(global exclude)文件, 用以放置那些不希望被记录在 .gitignore
文件中的忽略模式(ignored patterns),和 .gitignore
文件是 一个作用。
config 文件
默认的配置文件,打开后显示的是下边的内容。
[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
ignorecase = true
precomposeunicode = true
主要是当前仓库的一些配置,git
除了在这里有配置文件,还存在于 ~/.gitconfig
,打开后看一下。
[http]
proxy = socks5://127.0.0.1:1080
[https]
proxy = socks5://127.0.0.1:1080
[user]
name = windliang
email = 6489178757@qq.com
/etc/gitconfig
也是 git
的一个配置文件,但由于没有配置过这个文件,所以我电脑里这个文件不存在。
git
为我们提供了 config
命令用来配置上边的文件。
git config --list
是展示配置文件中已有的配置项,输出如下
http.proxy=socks5://127.0.0.1:1080
https.proxy=socks5://127.0.0.1:1080
user.name=windliang
user.email=6489178757@qq.com
core.repositoryformatversion=0
core.filemode=true
core.bare=false
core.logallrefupdates=true
core.ignorecase=true
core.precomposeunicode=true
可以看到就是把之前两个配置文件的内容按一定的格式输出。
上边讲到配置文件分布在三个文件中,git
为我们提供了三个参数 --local
,--global
,--system
,分别处理 git
当前仓库下的 config
文件、 ~/.gitconfig
、以及/etc/gitconfig
,如果存在同名的配置项,当前仓库下的配置文件优先级最高,其次是~/.gitconfig
,/etc/gitconfig
优先级最低。
举几个例子。
比如我们只想查看当前仓库下配置文件的配置项,可以执行 git config --local --list
,输出如下。
core.repositoryformatversion=0
core.filemode=true
core.bare=false
core.logallrefupdates=true
core.ignorecase=true
core.precomposeunicode=true
在 ~/.gitconfig
中增加一个配置项,git config --global alias.ss status
,执行后再打开 ~/.gitconfig
,可以看到就增加了一个配置项。
[http]
proxy = socks5://127.0.0.1:1080
[https]
proxy = socks5://127.0.0.1:1080
[user]
name = windliang
email = 6489178757@qq.com
[alias]
ss = status
通过上边 alias
的配置,下次如果我们想执行 git status
只需要输入 git ss
就可以了,也就是别名。
如果想删除某个配置项,可以添加 --unset
参数,比如执行 git config --global --unset alias.ss
。
也可以单独查看某个配置项,例如输入 git config --global user.name
,输出如下
windliang
底层命令和上层命令
我们经常使用的命令其实是上层命令(porcelain commands),参考下边的表格。
git-add git-rebase git-cherry
git-am git-reset git-count-objects
git-archive git-revert git-difftool
git-bisect git-rm git-fsck
git-branch git-shortlog git-get-tar-commit-id
git-bundle git-show git-help
git-checkout git-stash git-instaweb
git-cherry-pick git-status git-merge-tree
git-citool git-submodule git-rerere
git-clean git-tag git-rev-parse
git-clone git-worktree git-show-branch
git-commit gitk git-verify-commit
git-describe git-config git-verify-tag
git-diff git-fast-export git-whatchanged
git-fetch git-fast-import gitweb
git-format-patch git-filter-branch git-archimport
git-gc git-mergetool git-cvsexportcommit
git-grep git-pack-refs git-cvsimport
git-gui git-prune git-cvsserver
git-init git-reflog git-imap-send
git-log git-relink git-p4
git-merge git-remote git-quiltimport
git-mv git-repack git-request-pull
git-notes git-replace git-send-email
git-pull git-annotate git-svn
git-push git-blame
其实还有我们没有用过的底层命令(plumbing commands),多数底层命令并不面向最终用户,它们更适合作为新工具的组件和自定义脚本的组成部分。
git-apply git-for-each-ref git-receive-pack
git-checkout-index git-ls-files git-shell
git-commit-tree git-ls-remote git-upload-archive
git-hash-object git-ls-tree git-upload-pack
git-index-pack git-merge-base git-check-attr
git-merge-file git-name-rev git-check-ignore
git-merge-index git-pack-redundant git-check-mailmap
git-mktag git-rev-list git-check-ref-format
git-mktree git-show-index git-column
git-pack-objects git-show-ref git-credential
git-prune-packed git-unpack-file git-credential-cache
git-read-tree git-var git-credential-store
git-symbolic-ref git-verify-pack git-fmt-merge-msg
git-unpack-objects git-daemon git-interpret-trailers
git-update-index git-fetch-pack git-mailinfo
git-update-ref git-http-backend git-mailsplit
git-write-tree git-send-pack git-merge-one-file
git-cat-file git-update-server-info git-patch-id
git-diff-files git-http-fetch git-sh-i18n
git-diff-index git-http-push git-sh-setup
git-diff-tree git-parse-remote git-stripspace
下边用底层命令来进行 git
的相关操作,以便对 git
原理有个更深的了解。
objects 文件夹
这个文件夹顾名思义,就是存储对象的。git
主要有三种对象,blob
对象,tree
对象,commit
对象。和文件有关的东西都会存到这个文件夹中,相当于一个键值对的数据库。
blob 对象
首先新建一个 test.txt
,写入 hello world
,echo 'hello world' > test.txt
。
然后执行 git hash-object -w test.txt
命令,得到
3b18e512dba79e4c8300dd08aeb37f8e728b8dad
hash-object
命令会返回生成对象的键值,-w
会把该对象写入数据库,也就是 objects
文件夹中。
键值其实就是【头部信息】加上【文件原始内容】做了 SHA-1
得到的 40
位的哈希值,其中「头部信息」指的是 对象类型+空格+数据的字节数+空字节
。
我们来看一下 objects
文件夹的变化。
objects
├── 3b
│ └── 18e512dba79e4c8300dd08aeb37f8e728b8dad
├── info
└── pack
可以看到多了一个文件夹 3b
和一个文件 18e512dba79e4c8300dd08aeb37f8e728b8dadf
,组合起来刚好就是我们得到的键值。
通过指令 git cat-file -p 3b18e512d
看一下该文件的内容。
hello world
cat-file
可以解码刚刚生成的对象,-p
参数会自动选择内容的编码,3b18e512d
是键值的前几位。
然后我们修改一下文件的内容,echo 'hello world 2' > test.txt
,再次执行 git hash-object -w test.txt
。
再看一下 objects
文件夹。
objects
├── 3b
│ └── 18e512dba79e4c8300dd08aeb37f8e728b8dad
├── d0
│ └── e1e95455754bd31d56260d19a7774fd7aebe5d
├── info
└── pack
可以看到我们又多了一个对象,此时我们把本地的 test.txt
文件删除,rm test.txt
。
然后看一下之前写的内容还在不在,git cat-file -p d0e1e954557
。
hello world 2
可以看到还是能取到之前的内容,git
把之前所有的内容都存了起来,这就是简单的版本管理。但有个问题就是,当前我们只存了键值,并没有存文件名,这种类型的对象我们叫做「数据对象」blob object
,通过 git cat-file -t
命令加上 SHA-1
的键值前几位,就能查看该对象的内部类型。git cat-file -t d0e1e954557
。
blob
tree 对象
tree
对象记录了文件名以及文件之间的关系,相当于就是文件夹的作用,可以理解为下边的图。
下边演示如何用底层命令生成一个 tree
对象。
生成 tree
对象之前,我们需要将文件加入到暂存区。
新建一个空项目,然后 git init
,新建文件 test.txt
。
执行 git update-index --add test.txt
,这个命令会生成相应的对象存入 objects
文件夹中,并将 test.txt
加入暂存区,
可以执行 git status
看一下。
On branch master
No commits yet
Changes to be committed:
(use "git rm --cached <file>..." to unstage)
new file: test.txt
同时可以看到 objects
文件夹中多了一个文件。
objects
├── 95
│ └── d09f2b10159347eece71399a7e2e907ea3df4f
├── info
└── pack
暂存区有了文件以后,就可以生成一个 tree
对象了,执行 git write-tree
,生成当前暂存区的一个快照,返回值如下:
f03546f10f086a5cbc7b8580632ca6db2ba9411d
会返回 tree
对象的 key
值,看一下 objects
文件夹,新生成了一个文件。
objects
├── 95
│ └── d09f2b10159347eece71399a7e2e907ea3df4f
├── f0
│ └── 3546f10f086a5cbc7b8580632ca6db2ba9411d
├── info
└── pack
然后我们通过 git cat-file -p f0354
看一下 tree
对象的内容。
100644 blob 95d09f2b10159347eece71399a7e2e907ea3df4f test.txt
可以看到当前 tree
对象包含有一个 blob
对象,文件名是 test.txt
,100644
表示普通文件,还包括:100755
,表示一个可执行文件;120000
,表示一个符号链接。
有了 tree
对象我们就有了文件名,以及文件之间的关系,但是更改文件后我们可能还需要一些注释,如果是多人合作,还需要指明是谁生成的快照。因此我们需要将当前 tree
对象再包装一层,生成 commit
对象。
commit 对象
执行 echo 'first commit' | git commit-tree f0354
将之前的 tree
对象包装成一个 commit
对象。f0354
是之前 tree
对象的 key
,返回值如下:
84340eaeccbc0854bdec82f6b07f05eb01bd4dcd
同样的给我们返回了 commit
对象的 key
,同时看一下 objects
文件夹,也会多一个文件。
objects
├── 84
│ └── 340eaeccbc0854bdec82f6b07f05eb01bd4dcd
├── 95
│ └── d09f2b10159347eece71399a7e2e907ea3df4f
├── f0
│ └── 3546f10f086a5cbc7b8580632ca6db2ba9411d
├── info
└── pack
我们看一下这个 commit
对象的内容,git cat-file -p 8434
。
tree f03546f10f086a5cbc7b8580632ca6db2ba9411d
author windliang <6489178757@qq.com> 1594200559 +0800
committer windliang <6489178757@qq.com> 1594200559 +0800
first commit
此外 git commit-tree
还有一个 -p
参数,用来指定当前 commit
对象的父 commit
对象。
比如我们修改一下文件,再生成新的 tree
对象,依次执行下边的命令。
echo 'hello world2' > test2.txt
git update-index --add test2.txt
git write-tree
此时就得到了一个 tree
对象。
ab00cb505b3f955ab5fb245b7ca155a5820d2cd4
接下再生成新的 commit
对象,并且指定父 commit
对象,echo 'second commit' | git commit-tree ab00 -p 8434
。
44d318147e6b7bf2c3a5268018390440b2beae56
然后我们通过这个 commit
对象的 key
查看一下 log
,git log 44d3
。
commit 44d318147e6b7bf2c3a5268018390440b2beae56
Author: windliang <6489178757@qq.com>
Date: Wed Jul 8 17:47:31 2020 +0800
second commit
commit 84340eaeccbc0854bdec82f6b07f05eb01bd4dcd
Author: windliang <6489178757@qq.com>
Date: Wed Jul 8 17:29:19 2020 +0800
first commit
因为设置了父 commit
对象,所以第一次的提交也可以看到。
refs 文件夹,HEAD 文件
heads
我们刚刚执行 git log
命令的时候,写的是 git log 44d3
,多加了 commit
对象的 key
值 44d3
,写起来很麻烦,我们可以给它起一个别名,这个别名就是我们一直用的分支了。
我们将 commit
对象的 key
值写入 .git/refs/heads/master
文件中
echo 44d318147e6b7bf2c3a5268018390440b2beae56 > .git/refs/heads/master
然后执行 git log master
就可以看到 log
了。
commit 44d318147e6b7bf2c3a5268018390440b2beae56 (HEAD -> master)
Author: windliang <6489178757@qq.com>
Date: Wed Jul 8 17:47:31 2020 +0800
second commit
commit 84340eaeccbc0854bdec82f6b07f05eb01bd4dcd
Author: windliang <6489178757@qq.com>
Date: Wed Jul 8 17:29:19 2020 +0800
first commit
可以省略 master
,直接执行 git log
,默认查询的就是当前分支的 log
。
我们也可以给另外一个 commit
对象创建一个别名,换句话说创建一个新的分支。
echo 84340eaeccbc0854bdec82f6b07f05eb01bd4dcd > .git/refs/heads/dev
然后执行 git checkout dev
会发现可以成功的切换分支,说明分支创建成功了。
这里我们直接操控了文件,git
其实给我提供了一个命令,会更加安全
git update-ref refs/heads/dev 84340eaeccbc0854bdec82f6b07f05eb01bd4dcd
回想一下我们之前创建分支的命令,会执行 git branch fix
,注意到我们并没有指定 commit
对象的 key
值,为什么可以成功创建分支呢?
HEAD
文件!它里边始终保存着最新的 commit
对象的 key
值,当有新的 commit
的时候它会更新,当切换分支的时候它也会更新。
打开 HEAD
文件可以看一下。
ref: refs/heads/master
他保存了一个引用,refs/heads/master
文件保存的就是当前分支最新的 commit
对象的 key
值。
如果我们切换分支,git checkout dev
,可以看到 HEAD
中的值也会相应的变化。
ref: refs/heads/dev
tags
上边介绍了 blob
对象,tree
对象,commit
对象。commit
对象是包装了 tree
对象,还有个 tag
对象,通常是对 commit
对象的包装。
标签的话主要分为附注标签和轻量标签。可以像创建分支那样创建一个轻量标签:
git update-ref refs/tags/v1.0 44d318147e6b7bf2c3a5268018390440b2beae56
轻量标签的话相当于就是对 commit
的一个引用,没有创建新的对象。
我们再来创建一个附录对象,可以添加一些注释。
git tag -a v1.1 84340eaeccbc0854bdec82f6b07f05eb01bd4dcd -m 'test tag'
看一下新创建对象的 key
值,cat .git/refs/tags/v1.1
。
b518af197d2a925b668043edf1af88b82664e19f
然后查看一下该对象,git cat-file -p b518
。
object 84340eaeccbc0854bdec82f6b07f05eb01bd4dcd
type commit
tag v1.1
tagger windliang <6489178757@qq.com> 1594208408 +0800
test tag
我们顺便看一下这个对象的类型,git cat-file -t b518
。
tag
另外要注意的是,标签对象并非必须指向某个 commit
对象,它可以对任意类型的 git
对象打标签。
remotes
如果有远程仓库,并对其执行过推送操作,git
会记录下最近一次推送操作时的分支,并保存在 refs/remotes
目录下。
$ git remote add origin git@github.com:wind-liang/test.git
$ git push -u origin master
Enumerating objects: 6, done.
Counting objects: 100% (6/6), done.
Delta compression using up to 12 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (6/6), 467 bytes | 467.00 KiB/s, done.
Total 6 (delta 0), reused 0 (delta 0)
To github.com:wind-liang/test.git
* [new branch] master -> master
Branch 'master' set up to track remote branch 'master' from 'origin'.
然后查看 remotes
里的文件,cat .git/refs/remotes/origin/master
44d318147e6b7bf2c3a5268018390440b2beae56
这个值就是最新的 commit
对象的 key
值。远程引用和分支(位于 refs/heads
目录下的引用)之间最主要的区别在于,远程引用是只读的。虽然可以 git checkout
到某个远程引用,但是 Git
并不会将 HEAD
引用指向该远程引用。
objects/pack
前边我们讲了每一个文件都作为一个对象存到 objects
目录下,如果只修改了文件的某一行,然后进行提交,依旧会新生成一个 object
。如果 git
只保存其中一个,再保存另一个对象与之前版本的差异内容,不是能省些空间吗?
事实上 Git
可以那样做。但 Git
最初向磁盘中存储对象时所使用的格式被称为「松散(loose)」对象格式。Git
会时不时地将多个这些对象打包成一个称为“包文件(packfile)”的二进制文件,以节省空间和提高效率。当版本库中有太多的松散对象,或者手动执行 git gc
命令,或者你向远程服务器执行推送时,Git
都会这样做。
Git
打包对象时,会查找命名及大小相近的文件,并只保存文件不同版本之间的差异内容。可以找一个项目看一下 pack
下的目录。
pack
├── pack-0efa08fc30b8d98dff42203c71c6afe0533ce468.idx
├── pack-0efa08fc30b8d98dff42203c71c6afe0533ce468.pack
├── pack-19571818da01df976f52298facf362dc93d61026.idx
├── pack-19571818da01df976f52298facf362dc93d61026.pack
├── pack-725ab685133ed6e35083c5b3dcaf02ebc238489c.idx
├── pack-725ab685133ed6e35083c5b3dcaf02ebc238489c.pack
├── pack-7c01d29f0cb068c617aa49471cbf9f6eb1cb2156.idx
├── pack-7c01d29f0cb068c617aa49471cbf9f6eb1cb2156.pack
├── pack-a59c4fce103fb83cec0f513ab32cd92e6122e7a4.idx
├── pack-a59c4fce103fb83cec0f513ab32cd92e6122e7a4.pack
├── pack-db0d185ea0e7d96bbad911bb371c67869d8599b0.idx
├── pack-db0d185ea0e7d96bbad911bb371c67869d8599b0.pack
├── pack-f07ea07e30bb0aa4dfbb1fcb08da4cd5e5e5f793.idx
└── pack-f07ea07e30bb0aa4dfbb1fcb08da4cd5e5e5f793.pack
可以是两种类型,一种是打包文件,另一种就是索引文件,用来记录不同版本之间的差异。更详细的可以看一下 Git 内部原理 - 包文件。
packed-refs文件
执行 gc
以后,会将 refs
文件夹中的引用打包到这个文件中。
index
当我们执行了 git add
或者上边讲到的 git update-index --add
命令,我们就会发现 .git
目录下增加了一个 index
文件。这个文件存储的东西就是我们常说的「暂存区」。它主要存储了每个文件的索引,也就是在 objects
目录下生成的对象的 SHA-1
哈希值。还有生成 tree
对象的一些信息,比如文件名以及文件之间的关系,为下一步生成 tree
对象做准备。
hooks 文件夹
这里主要是 git
为我们提供了一些钩子函数,把下边的 .sample
去掉,当前钩子就会生效。可以编辑各个钩子文件,就可以在执行 push
、commit
等操作时完成一些自己想要的一些动作。
hooks
├── applypatch-msg.sample
├── commit-msg.sample
├── fsmonitor-watchman.sample
├── post-update.sample
├── pre-applypatch.sample
├── pre-commit.sample
├── pre-merge-commit.sample
├── pre-push.sample
├── pre-rebase.sample
├── pre-receive.sample
├── prepare-commit-msg.sample
└── update.sample
通过钩子,可以实现提交代码前自动格式化代码、规范化 commit-msg
等功能,还可以做到当远程仓库 github
更新后,让服务器端自动拉取最新项目,实现一些 web
项目的自动更新。
COMMIT_EDITMSG
存储最后一次提交的信息内容。git commit
命令之后打开的编辑器就是在编辑此文件,退出编辑器保存后,git
会把此文件内容写入 commit
记录。一般直接在 commit
命令后添加 -m
选项,附加提交信息。
ORIG_HEAD 文件
相当于 HEAD
文件的一个备份,会指向 HEAD
之前的一个 commit
对象。当执行一些危险的操作,比如 git rebase
等,需要先记录 ORIG_HEAD
再执行其他的操作。
FETCH_HEAD 文件
FETCH_HEAD
记录了 fetch
时候远程分支的 key
值,也就是 commit
对象的 SHA-1
哈希值。当执行 git pull
的时候相当于先执行 git fetch
,然后执行 git merge FETCH_HEAD
,也就是和拉取下来的远程分支合并。
打开 FETCH_HEAD
文件,第一行就是 FETCH_HEAD
的值,用于 merge
,其它行是同时拉取下来的分支。
d6a81fdb23503d5e85cb8f74ea77cd4ab20e0659 branch 'master' of ssh://git.github.com/ed-f2e/test
5ce98b8e6f832382417c5a1ef55f1f1ca303f86d not-for-merge branch 'foodsafetab-20200701' of ssh://git.github.com/ed-f2e/test
d46bec5fea5af996d75497b05592802ef31fe63b not-for-merge branch 'overview-20200601' of ssh://git.github.com/ed-f2e/test
005ef114aa680401681f582b8f71dfe020417989 not-for-merge branch 'visual-20200622' of ssh://git.github.com/ed-f2e/test
关键字 not-for-merge
,表明 git pull
时只 fetch
,不 merge
。
logs 文件夹
记录了操作信息,git reflog
命令以及像 HEAD@{1}
形式的路径会用到。如果删除此文件夹,那么依赖于 reflog
的命令就会报错。
文件夹总
基本上把 .git
目录总结完了,下边汇总一下。
.
├── COMMIT_EDITMSG // git commit 时候编辑的文件
├── FETCH_HEAD // git fetch 保存从远程仓库抓取下来的 commit 对象的键值
├── HEAD // 保存当前 commit 对象的键值
├── ORIG_HEAD // 执行危险操作时 HEAD 的备份
├── config // 当前 git 仓库的相关配置,优先级最高
├── description // 仅供 GitWeb 程序使用
├── hooks // 保存 git 的所有钩子
│ ├── applypatch-msg.sample
│ ├── commit-msg
│ ├── commit-msg.git-flow
│ ├── commit-msg.sample
│ ├── fsmonitor-watchman.sample
│ ├── post-update.sample
│ ├── pre-applypatch.sample
│ ├── pre-commit
│ ├── pre-commit.git-flow
│ ├── pre-commit.mt-eslint-check
│ ├── pre-commit.sample
│ ├── pre-merge-commit.sample
│ ├── pre-push
│ ├── pre-push.git-flow
│ ├── pre-push.sample
│ ├── pre-rebase.sample
│ ├── pre-receive.sample
│ ├── prepare-commit-msg.sample
│ └── update.sample
├── index // 暂存区,保存对象的索引和 tree 对象的相关信息
├── info
│ └── exclude // 和 .gitignore 一样的作用
├── logs // 记录历史的一些操作,git reflog 命令依赖于此目录
├── objects // git 的数据库,存放所有对象
│ ├── 0f
│ │ ├── 01ae962d3a527bfd692175ee5600501eef43fb
│ │ ├── 5ef114aa680401681f582b8f71dfe020417989
│ │ ├── 71685e674a8190ba2f94391cae5f99ea4854fb
│ │ └── cef7d6ec44ae5ffabf2514bd19ccb1ec303eb4
│ ├── 34
│ │ ├── 621a5fb9cdb057a09c559586702a4d20388c71
│ │ └── 6e1d53b34578f0bfcef11dd2d59dc25072e79e
│ ├── 55
│ │ ├── 137e5e9f4e37218033af62bc9fcc2fded5545b
│ │ ├── c0d6f9e2d8df065ccb512d45cbbd61d327e7fd
│ │ └── eeddf196be55abc4292f562b8bd4fb19bbb2d4
│ ├── info
│ └── pack
│ ├── pack-679830bc13d16192a07ddaa0f51f49b0163b7578.idx
│ └── pack-679830bc13d16192a07ddaa0f51f49b0163b7578.pack
├── packed-refs // 执行 gc 以后,将 refs 中的引用进行打包
└── refs // 对 commit 对象的引用
├── heads //所有分支
│ ├── master
├── remotes // 远程分支所对应的 key 值
│ └── origin
│ ├── HEAD
│ └── master
└── tags // 所有标签
对象总结
主要包括 blob
对象,tree
对象,commit
对象,还有 tag
对象。通过 commit
对象以链表的形式连接在了一起。
可以看到第一次 commit
的时候,创建了 README.md
,index.html
,js
文件夹以及 index.js
。
第二次 commit
的时候,仅仅修改了 index.html
,其他文件仍旧指向原来的对象。并且用当前 commit
包装了一个 tag
对象。
第三次 commit
的时候,增加了 index.css
文件,其他文件仍旧指向原来的对象。此外当前 commit
对象是当前操作的对象,所以 HEAD
指向当前 commit
对象,另外 mater
分支也指向当前 commit
对象。
换一种眼光看命令
这一节回顾一下 git
经常用的命令和上边介绍的文件的一些关系。为了方便监测每个命令改变了哪些文件,我们在 .git
目录中再执行一次 git init
,也就是将 .git
目录看作我们的另外一个项目,操作如下:
新建一个目录,learnGit
,在里边新建 index.html
,README.md
,js
文件夹,js
文件夹中新建 index.js
。目录结构如下:
.
├── README.md
├── index.html
└── js
└── index.js
然后初始化当前目录为 git
仓库。
learnGit % git init
Initialized empty Git repository in /Users/learnGit/.git/
此时就会自动生成 .git
目录,进入 .git
目录再执行一次 git init
,git add .
,git commit -m "init"
,来监测后续 .git
目录的变化情况。然后回到我们的根目录learnGit
中进行下边的实验。
git add .
此时会发现每个文件会生成一个对象,因此 objects
文件夹中多了 3
个文件(如果是 mac
系统会发现多了 4
个文件,原因是系统自动生成了一个 .DS_Store
文件,这里就不考虑了),也就是 3
个 blob
对象。此外,增加了 index
文件,也就是暂存区,会存储每个 commit
对象的索引,以及生成 tree
对象的相关信息。
create mode 100644 index
create mode 100644 objects/4d/7a16a7949cf8206f6f910535fd6811d4a5e3d2
create mode 100644 objects/94/a127e7307c6562a2bdbf2d156589572c31963e
create mode 100644 objects/c3/b573586becc940e02cd0914ef2eaf6d1ff7a28
相当于执行了 git hash-object -w 文件名
生成对象,以及 git update-index --add 文件名
命令,将文件加入暂存区。
git commit -m
执行 git commit -m "first"
,文件变化情况如下。
create mode 100644 COMMIT_EDITMSG
create mode 100644 logs/HEAD
create mode 100644 logs/refs/heads/master
create mode 100644 objects/74/900affe800f97c02e9cad8a9b2304e21f0a412 //commit 对象
create mode 100644 objects/bc/4a821da58aa317d1790199b98b1e1b638baebb //tree 对象
create mode 100644 objects/d2/eb8f97312f90fe39586e6deefb6b41b4d8340f //tree 对象
create mode 100644 refs/heads/master
会发现 objects
文件夹中多了三个文件,其中两个是 tree
对象,因为我们的目录有两个文件夹。另一个就是包装了 tree
对象的 commit
对象。
增加了 COMMIT_EDITMSG
,也就是 commit
时候写的提交信息,在这里的话里边内容就是 "first"。
logs
目录发生了一些变化,reflog
命令依赖这里的文件。
自动为我们创建了 mater
分支,因此增加了 refs/heads/master
文件,里边的内容就是我们刚刚生成的 commit
对象的 hash
值,也就是 74900affe800f97c02」9cad8a9b2304e21f0a412
。
git push
我们先执行 git remote add origin git@github.com:wind-liang/learnGit.git
添加一个远程仓库。此时 config
文件多了三行,记录了远程仓库。
[remote "origin"]
url = git@github.com:wind-liang/learnGit.git
fetch = +refs/heads/*:refs/remotes/origin/*
记录了远程仓库的名字 origin
,以及 url
地址,还有就是执行 fetch
时候的默认操作,从远程取回所有分支的更新,可以看下一节fetch
命令的介绍。
格式:git push <远程主机名> <本地分支名>:<远程分支名>
。
我们执行 git push origin master
,可以省略远程分支名,默认和本地分支名一致。
create mode 100644 logs/refs/remotes/origin/master
create mode 100644 refs/remotes/origin/master
logs
就不说了。会发现本地新建了一个远程分支 origin/master
,里边内容就是我们刚刚推送的本地 mater
分支指向的 commit
对象的 hash
值,也就是 74900affe800f97c02e9cad8a9b2304e21f0a412
。
此时我们修改 index.html
,然后执行 git add .
加到暂存区。看一下文件的变化。
index | Bin 396 -> 377 bytes
objects/07/51aaed4e8f37c1f84eb7780ca08989029ec504 | Bin 0 -> 247 bytes
此时会多一个对象,也就是新一版的 index.html
,以及 index
文件会发生变化。
接着执行 git commit -m "second"
,会根据暂存区的信息生成当前的树对象以及 commit
对象,objects
文件夹中应该会增加两个对象,一个 tree
对象,一个 commit
对象。
COMMIT_EDITMSG | 2 +-
index | Bin 377 -> 396 bytes
logs/HEAD | 1 +
logs/refs/heads/master | 1 +
objects/63/53b5966f6bbf3f6a840b1261030519b67fdf51 | Bin 0 -> 150 bytes
objects/af/f7b5ebae94eba99e5fa0ef245595c21686562b | Bin 0 -> 154 bytes
refs/heads/master | 2 +-
refs/heads/master
也会更新,指向最新的 commit
对象。
如果我们想把当前改变再推送到远程仓库,又需要执行 git push origin master
,有些长。git
为我们提供了 --set-upstream
参数,简写是 -u
,可以让本地分支关联都某个远程分支,这样的话如果下次想把当前分支推送到远程,只需要执行 git push
就可以了。
我们执行一下 git push -u origin master
,看一下哪些文件会变化。
config | 3 +++
logs/refs/remotes/origin/master | 1 +
refs/remotes/origin/master | 2 +-
看一下 config
文件。
[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
ignorecase = true
precomposeunicode = true
[remote "origin"]
url = git@github.com:wind-liang/learnGit.git
fetch = +refs/heads/*:refs/remotes/origin/*
[branch "master"]
remote = origin
merge = refs/heads/master
可以看到,它记录了本地 mater
分支和远程仓库 origin
中的 refs/heads/mater
关联。除了在 git push
起作用,git fetch
和 git pull
的默认操作也会依赖这里的配置,可以继续看下边的小节。
此外,这里建立的远程分支必须要和本地分支同名,因为在 Git 2.0
之后 git push
不加任何参数的话,默认模式为 simple
,推送当前分支到upstream
分支上,必须保证本地分支与 upstream
分支同名,不然的话 git push
是没有用的。
比如我们将 mater
分支和远程仓库的 dev
分支关联,执行 git branch -u origin/dev
,再执行 git push
就会得到下边的提示。
fatal: The upstream branch of your current branch does not match
the name of your current branch. To push to the upstream branch
on the remote, use
git push origin HEAD:dev
To push to the branch of the same name on the remote, use
git push origin HEAD
To choose either option permanently, see push.default in 'git help config'.
还有其他的模式,nothing, current, upstream, matching
,一般就用默认的 simple
,这里就不介绍了。
git fetch
为了更详细的看 git fetch
命令的作用。我新建了另一个远程仓库 origin2
,关联到了当前本地仓库,并且在远程仓库中添加了 index2.txt
。
同时在原来 origin
的远程仓库中,在 mater
分支新增了 index.css
文件。增加了 dev
分支,并且修改了 index.html
。
当前本地仓库的配置文件如下:
[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
ignorecase = true
precomposeunicode = true
[remote "origin"]
url = git@github.com:wind-liang/learnGit.git
fetch = +refs/heads/*:refs/remotes/origin/*
[branch "master"]
remote = origin
merge = refs/heads/master
[remote "origin2"]
url = git@github.com:wind-liang/learnGit2.git
fetch = +refs/heads/*:refs/remotes/origin2/*
git fetch
的命令格式为 git fetch <远程主机名> <分支名>
。
我们不加参数,只执行 git fetch
看一下效果。
FETCH_HEAD | 2 ++
logs/refs/remotes/origin/dev | 1 +
logs/refs/remotes/origin/master | 1 +
objects/08/f4162aa74066285166b9b46f845ea648941943 | Bin 0 -> 151 bytes
objects/36/619ae122d6e01b7c82838247754173c1b6930a | Bin 0 -> 177 bytes
objects/88/688aebd84396af325f55f82a4bdf0e283a8e4a | Bin 0 -> 251 bytes
objects/b1/be33b8a27e1fcfa2c9f01b0fcb76a28b091071 | 2 ++
objects/be/5678cb94b6da6f94cad10077739a850cd893b5 | Bin 0 -> 38 bytes
objects/ff/383d0247ea2c27dd2d44753dd5b18ca4ecfeaf | Bin 0 -> 177 bytes
refs/remotes/origin/dev | 1 +
refs/remotes/origin/master | 2 +-
11 files changed, 8 insertions(+), 1 deletion(-)
默认抓取了远程仓库 origin
的两个分支。由于远程仓库新增了 index.css
文件,并且修改了 dev
分支中的 index.html
,所以是 2
个 blob
对象,2
个 tree
对象,2
个 commit
对象,所以 objects
文件中增加了 6
个对象。
FETCH_HEAD
记录了两个分支指向的最新 commit
对象的 hash
值。
b1be33b8a27e1fcfa2c9f01b0fcb76a28b091071 branch 'master' of github.com:wind-liang/learnGit
08f4162aa74066285166b9b46f845ea648941943 not-for-merge branch 'dev' of github.com:wind-liang/learnGit
refs/remotes/origin/dev
和 refs/remotes/origin/master
分别记录了分支所对应的 commit
对象。
git merge
git fetch
仅仅把远程分支拉取了下来,我们还需要通过 git merge
将远程分支的内容和本地内容进行合并。
我们将本地的 mater
分支和远程的 mater
分支进行合并。
首先可以执行 git diff origin/mater
看一下和远程仓库代码的区别。
然后可以执行 git merge origin/master
将刚刚拉下来的远端分支和当前分支合并。
ORIG_HEAD | 1 +
index | Bin 396 -> 468 bytes
logs/HEAD | 1 +
logs/refs/heads/master | 1 +
refs/heads/master | 2 +-
5 files changed, 4 insertions(+), 1 deletion(-)
可以看到 index
文件进行了更新,也就是更新了暂存区。refs/heads/master
文件进行了更新,也就是将 mater
分支指向了最新的 commit
对象。查看 refs/heads/master
文件中的内容是 b1be33b8a27e1fcfa2c9f01b0fcb76a28b091071
,和我们刚刚 FETCH_HEAD
中远端 mater
分支的 commit
对象的 hash
值一致。新增的 ORIG_HEAD
文件是 HEAD
的备份。
这种合并方式属于 Fast Forward
,合并的时候直接将 mater
分支指向了最新的提交。是因为要合并过来的分支是之前 mater
分出去的,并且分出去之后 mater
分支没有再产生新的 commit
对象,也就是下面的情况。
------------ origin/master
/
-----master
这种情况合并的话,直接把 mater
指向 origin/master
即可。
还有另外一种情况,如下图。
------------ origin/master
/
-------------master
分出去以后,mater
分支又进行了几次提交,此时我们再执行 git merge origin/master
看一下会是什么情况。
Merge remote-tracking branch 'origin/master'
# Please enter a commit message to explain why this merge is necessary,
# especially if it merges an updated upstream into a topic branch.
#
# Lines starting with '#' will be ignored, and an empty message aborts
# the commit.
~
此时会进入一个编辑文件,让我们填写 commit
对象的信息,填写退出后,文件变化如下。
ORIG_HEAD | 2 +-
index | Bin 468 -> 468 bytes
logs/HEAD | 1 +
logs/refs/heads/master | 1 +
objects/09/1d34a18801e88da14b609d29ac6d6ee8ea9079 | Bin 0 -> 209 bytes
objects/dd/c0e866456daf833034119e6a797eb63614cb4a | Bin 0 -> 178 bytes
refs/heads/master | 2 +-
相比之前的 Fast Forward
模式,这里我们相当于多进行了一个 commit
操作,增加了 tree
对象和 commit
对象。
而且这个 commit
对象比较特殊,它有两个 parent
对象, 通过命令 git cat-file -p 091d
来看一下。
tree ddc0e866456daf833034119e6a797eb63614cb4a
parent b9ba3b492e1fad20acd003ea4dad463a012b357a
parent 5f4cfe0f32a1580924a1b5a2d2673d6e6b13639c
author windliang <6489178757@qq.com> 1595149755 +0800
committer windliang <6489178757@qq.com> 1595149755 +0800
Merge remote-tracking branch 'origin/master'
所以最后合并后的情况相当于下边的样子:
------------ origin/master
/ \
- master
------------- /
git pull
在 dev
分支下执行 git pull
命令。
理解了 git fetch
和 git merge
,git pull
就好说了。它相当于先执行 git fetch
拉取下了所有分支,然后再执行 git merge FETCH_HEAD
。FETCH_HEAD
就是当前分支跟踪的远程分支的 commit
对象的 HASH
值。它怎么知道当前分支追踪的远程分支是哪个呢?就是我们之前 git push
设置的。
[branch "dev"]
remote = origin
merge = refs/heads/dev
这样的话,如下所示,FETCH_HEAD
文件第一行存储的是远端 dev
分支的 commit
对象的 HASH
值,做为 FETCH_HEAD
的引用,用于接下来的 git merge FETCH_HEAD
操作。
cc28c96446ecf074671a0521c917025d5942ef9c branch 'dev' of github.com:wind-liang/learnGit
0932e6eb2fc387f7eeb94147a98cf16f8a0c27b8 not-for-merge branch 'master' of github.com:wind-liang/learnGit
如果我们执行git branch -u origin/master
,让当前分支 dev
去追踪远程仓库的 master
分支。此时再执行 git fetch
。FETCH_HEAD
第一行记录的就是远程仓库 mater
分支了。
a2837cd3f45d3f9ebe7d5d6a3f90ff633938a40b branch 'master' of github.com:wind-liang/learnGit
79d8411ee2466e7e6f361a18601b81d5b7a98156 not-for-merge branch 'dev' of github.com:wind-liang/learnGit
上边是 git pull/fetch
的默认操作,git pull
的完整格式为 git pull <远程主机名> <远程分支名>:<本地分支名>
。如果不指定本地分支名,则默认为当前分支。
如果我们指定了远程的分支,执行git pull origin mater
,就相当于先执行 git fetch origin master
,此时就不会拉取所有分支,FETCH_HEAD
就指向这个唯一拉下来的分支了。
总结
主要从两个角度介绍了 git
,一方面介绍了 .git
目录中每个文件的作用以及相关的底层命令,另一方面介绍了常用的一些命令对 .git
目录的影响。花了不少时间总结下来,自己对 git
有了更深的理解,希望对大家也能够有所帮助。
参考链接:
[Git维基百科](https://en.wikipedia.org/wiki/Git)
[Git 10 周年访谈:Linus Torvalds 讲述背后故事](https://linuxstory.org/10-years-of-git-an-interview-with-git-creator-linus-torvalds/)
[10 Years of Git: An Interview with Git Creator Linus Torvalds](https://www.linux.com/news/10-years-git-interview-git-creator-linus-torvalds/)
[Torvalds on Version Control: Git good, SVN terrible!](https://www.zensoftware.nl/nl/3412-2/)
[探秘git隐藏文件夹](https://cloud.tencent.com/developer/article/1565474)
[图解git原理的几个关键概念](https://tonybai.com/2020/04/07/illustrated-tale-of-git-internal-key-concepts/)
[Git 内部原理 - 底层命令与上层命令]([https://git-scm.com/book/zh/v2/Git-%E5%86%85%E9%83%A8%E5%8E%9F%E7%90%86-%E5%BA%95%E5%B1%82%E5%91%BD%E4%BB%A4%E4%B8%8E%E4%B8%8A%E5%B1%82%E5%91%BD%E4%BB%A4](https://git-scm.com/book/zh/v2/Git-内部原理-底层命令与上层命令))
[Which are the plumbing and porcelain commands?](https://stackoverflow.com/questions/39847781/which-are-the-plumbing-and-porcelain-commands)
[Git 是怎样生成 diff 的:Myers 算法](https://cjting.me/2017/05/13/how-git-generate-diff/)
[Git 原理入门](http://www.ruanyifeng.com/blog/2018/10/git-internals.html)
[.git文件夹探秘,理解git运作机制](https://developer.aliyun.com/article/716483)
[Use of index and Racy Git problem](https://github.com/git/git/blob/master/Documentation/technical/racy-git.txt?spm=a2c6h.12873639.0.0.592469418daUIL&file=racy-git.txt)
[Git index format](https://github.com/git/git/blob/master/Documentation/technical/index-format.txt?spm=a2c6h.12873639.0.0.592469418daUIL&file=index-format.txt)
[Refs and the Reflog](https://www.atlassian.com/git/tutorials/refs-and-the-reflog)
[Git中的push和pull的默认行为](Git中的push和pull的默认行为)