简单地说,git 究竟是怎样的一个系统呢?请注意接下来的内容非常重要,若你理解了 git 的思想和基本工作原理,用起来就会知其所以然,游刃有余。
在学习 git 时,请尽量理清你对其它版本管理系统已有的认识,如 cvs、subversion 或 perforce, 这样能帮助你使用工具时避免发生混淆。
尽管 git 用起来与其它的版本控制系统非常相似, 但它在对信息的存储和认知方式上却有很大差异,理解这些差异将有助于避免使用中的困惑。
git 初始化代码仓库执行完成了 git init 命令,究竟做了什么呢?
执行完成如下命令之后,我们可以得到下图所示的内容,右侧的就是 git 为我们创建的代码仓库,其中包含了用于版本管理所需要的内容。
# 左边执行
$ mkdir git-demo
$ cd git-demo && git init
$ rm -rf .git/hooks/*.sample
# 右边执行
$ watch -n 1 -d find .
我们这里可以一起看下生成的 .git 目录的结构如何:
➜ tree .git
.git
├── head
├── config
├── description
├── hooks
├── info
│ └── exclude
├── objects
│ ├── info
│ └── pack
└── refs
├── heads
└── tags
.git/config - 当前代码仓库本地的配置文件
本地配置文件(.git/config)和全局配置文件(~/.gitconfig)通过执行如下命令,可以将用户配置记录到本地代码仓库的配置文件中去git config user.name demogit config user.email demo@demo.com➜ cat .git/config
[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
ignorecase = true
precomposeunicode = true
[user]
name = demo
email = demo@demo.com
.git/objects - 当前代码仓库代码的存储位置
blob 类型commit 类型tree 类型# 均无内容
➜ ll .git/objects
total 0
drwxr-xr-x 2 escape staff 64b nov 23 20:39 info
drwxr-xr-x 2 escape staff 64b nov 23 20:39 pack
➜ ll .git/objects/info
➜ ll .git/objects/pack
.git/info - 当前仓库的排除等信息
➜ cat ./.git/info/exclude
# git ls-files --others --exclude-from=.git/info/exclude
# lines that start with '#' are comments.
# for a project mostly in c, the following would be a good set of
# exclude patterns (uncomment them if you want to use them):
# *.[oa]
# *~.git/hooks - 当前代码仓库默认钩子脚本
./.git/hooks/commit-msg.sample
./.git/hooks/pre-rebase.sample
./.git/hooks/pre-commit.sample
./.git/hooks/applypatch-msg.sample
./.git/hooks/fsmonitor-watchman.sample
./.git/hooks/pre-receive.sample
./.git/hooks/prepare-commit-msg.sample
./.git/hooks/post-update.sample
./.git/hooks/pre-merge-commit.sample
./.git/hooks/pre-applypatch.sample
./.git/hooks/pre-push.sample
./.git/hooks/update.sample
.git/head - 当前代码仓库的分支指针
➜ cat .git/head
ref: refs/heads/master
.git/refs - 当前代码仓库的头指针
# 均无内容
➜ ll .git/refs
total 0
drwxr-xr-x 2 escape staff 64b nov 23 20:39 heads
drwxr-xr-x 2 escape staff 64b nov 23 20:39 tags
➜ ll .git/refs/heads
➜ ll .git/refs/tags
.git/description - 当前代码仓库的描述信息
➜ cat .git/description
unnamed repository; edit this file 'description' to name the repository.
add 之后发生了什么执行完成了 git add 命令,究竟做了什么呢?
执行完成如下命令之后,我们可以得到下图所示的内容,我们发现右侧新增了一个文件,但是 git 目录里面的内容丝毫没有变化。这是因为,我们现在执行的修改默认是放在工作区的,而工作区里面的修改不归 git 目录去管理。
而当我们执行 git status 命令的时候,git 又可以识别出来现在工作区新增了一个文件,这里怎么做到的呢?—— 详见[理解 blob 对象和 sha1]部分
而当我们执行 git add 命令让 git 帮助我们管理文件的时候,发现右侧新增了一个目录和两个文件,分别是 8d 目录、index 和 0e41.. 文件。
# 左边执行
$ echo hello git > helle.txt
$ git status
$ git add hello.txt
# 右边执行
$ watch -n 1 -d find .
我们这里重点看下,生成的 8d 这个目录以及下面的文件。而其名称的由来是因为 git 对其进行了一个叫做 sha1 的 hash 算法,用于将文件内容或者字符串变成这么一串加密的字符。
# 查看 objects 的文件类型
$ git cat-file -t 8d0e41
blob
# 查看 objects 的文件内容
$ git cat-file -p 8d0e41
hello git
# 查看 objects 的文件大小
$ git cat-file -s 8d0e41
10
# 拼装起来
blob 10\\0hello git
现在我们就知道了,执行 git add 命令将文件从工作区添加到暂存区里面,git 会把帮助我们生成一些 git 的对象,它存储的是文件的内容和文件类型并不存储文件名称。
微信搜索公众号:架构师指南,回复:架构师 领取资料 。
为了验证我们上述的说法,我们可以添加同样的内容到另一个文件,然后进行提交,来观察 .git 目录的变化。我们发现,右侧的 objects 目录并没有新增目录和文件。这就可以证明,blob 类型的 object 只存储的是文件的内容,如果两个文件的内容一致的话,则只需要存储一个 object 即可。
话说这里 object 为什么没有存储文件名称呢?这里因为 sha1 的 hash 算法计算哈希的时候,本身就不包括文件名称,所以取什么名称都是无所谓的。那问题来了,就是文件名的信息都存储到哪里去了呢?—— 详见[理解 blob 对象和 sha1]部分
# 左边执行
$ echo hello git > tmp.txt
$ git add tmp.txt
# 右边执行
$ watch -n 1 -d find .
理解 blob 对象和 sha1了解 git 的 blob 对象和 sha1 之前的关系和对应计算!
hash 算法是把任意长度的输入通过散列算法变化成固定长度的输出,根据算法的不同,生成的长度也有所不同。
hash 算法:
md5 - 128bit - 不安全 - 文件校验sha1 - 160bit(40位) - 不安全 - git 存储sha256 - 256bit- 安全 - docker 镜像sha512 - 512bit - 安全但是,当我们使用工具对上述文件内容进行 sha1 计算的时候,会发现并没有我们在 .git 目录里面看到的那样,这是为什么呢?
➜ echo hello git | shasum
d6a96ae3b442218a91512b9e1c57b9578b487a0b -
这里因为 git 工具的计算方式,是使用类型 长度 \\0 内容的方式进行计算的。这里,我们算了下文件内容只有九位,但是这里是十位,这里因为内容里面有换行符的存在导致的。现在我们就可以使用 git cat-file 命令来拼装 git 工具存储的完整内容了。
➜ ls -lh hello.txt
-rw-r--r-- 1 escape staff 10b nov 23 21:12 hello.txt
➜ echo blob 10\\0hello git | shasum
8d0e41234f24b6da002d962a26c2495ea16a425f -
# 拼装起来
blob 10\\0hello git
当我们使用 cat 命令来查看 object 对象里面的内容的时候,发现看着像是一串乱码。其实这是 git 工具将文件的原始内容进行一个压缩,然后再存储到 object 对象里面。奇怪的是,我们发现压缩之后的内容反而比原始内容还大!
这是因为其进行了压缩,存储了一些压缩相关的信息。上例所示的比原始文件大,是因为我们创建的内容实在是太小了。当我们常见一个比较大的文件时,就会看到压缩之后的文件大小远小于原始文件的。
➜ cat .git/objects/8d/0e41234f24b6da002d962a26c2495ea16a425f
xkor04`hwh,6a%
➜ ls -lh .git/objects/8d/0e41234f24b6da002d962a26c2495ea16a425f
-r--r--r-- 1 escape staff 26b nov 23 21:36 .git/objects/8d/0e41234f24b6da002d962a26c2495ea16a425f
➜ file .git/objects/8d/0e41234f24b6da002d962a26c2495ea16a425f
.git/objects/8d/0e41234f24b6da002d962a26c2495ea16a425f: vax coff executable not stripped - version 16694
其实,我们这里也是可以通过 python 代码来获取二进制 object 对象的内容的。
import zlib
contents = open('0e41234f24b6da002d962a26c2495ea16a425f', 'rb').read()
zlib.decompress(contents)
聊聊工作区和暂存区聊聊工作区和暂存区,以及文件如何在工作区和缓存区之间同步的问题。
之前的章节我们也聊到了,当我们执行 git status 命令的时候,git 工具怎么知道我们有一个文件没有追踪,以及文件名的信息都存储到哪里去了?
这一切的答案,都要从工作区和索引区讲起。git 根据其存储的状态不同,将对应状态的“空间”分为工作区、暂存区(也可称为索引区)和版本区三类。具体示例,可以参考下图。
而更加深层次的理解,就要从执行 git add 命令后生成相关的 object 对象,但是其存储的是文件的类容、大小和内容,并不包含文件名称的信息。而文件名称相关的信息就包含在生成的 index 文件(索引文件)里面。
当我们直接查看 index 文件里面的内容,发现使我们无法理解的乱码,但是通过基本的输出,我们可以看到其文件名称。要想查看 index 文件的内容,可以通过 git 提供的相关命令进行查看。
# 左边执行
$ echo file1 > file1.txt
$ git add file1.txt
$ cat .git/index
$ git ls-files # 列出当前暂存区的文件列表信息
$ git ls-files -s # 列出当前暂存区文件的详细信息
# 右边执行
$ watch -n 1 -d tree .git
当添加文件的时候,文件或目录会从工作区流向暂存区,加之一些其他操作,会导致工作区和暂存区是会有一定差别的。这就会导致,当我们执行 git status 的结果就是两者的差别。
经过如下操作,会使工作区和暂存区和的内容不一致了,通过命令我们也是可以查看区别的。当我们使用 add 命令将新文件添加到暂存区的时候,会发现这下就一致了。
# 左边执行
$ git status
$ echo file2 > file2.txt
$ git ls-files -s
$ git status
$ git add file2.txt
$ git ls-files -s
$ git status
# 右边执行
$ watch -n 1 -d tree .git
如果我们这里去修改一个文件的话,很显然这个时候我们的工作区和暂存区又不一致了。当我们使用命令去查看文件状态的时候,发现一个文件被修改了,而 git 是怎么知道的呢?咳咳,就是通过查找 index 文件的内容,找到对应文件名称以及其内部引用的 object 对象,与工作区的文件内容进行对比而来的。
# 左边执行
$ git ls-files -s
$ echo file.txt > file1.txt
$ git status
# 右边执行
$ watch -n 1 -d tree .git
而这个时候,我们再使用 git add 命令将其修改内容保存至暂存区的话,会发现对应文件的 object 的 blob 对象的引用值发生改变了。这时可以发现,objects 目录下面有三个对象了,其中 file1.txt 占了两个,但是文件却只有两个。通过命令查看对应 blob 对象的内容,发现各有不同。
# 左边执行
$ git ls-files -s
$ git add file1.txt
$ git ls-files -s
# 右边执行
$ watch -n 1 -d tree .git
理解 commit 提交原理执行完成了 git commit 命令,究竟做了什么呢?
git 仓库中的提交记录保存的是你的目录下所有文件的快照,就像是把整个目录复制,然后再粘贴一样,但比复制粘贴优雅许多!git 希望提交记录尽可能地轻量,因此在你每次进行提交时,它并不会盲目地复制整个目录。条件允许的情况下,它会将当前版本与仓库中的上一个版本进行对比,并把所有的差异打包到一起作为一个提交记录。git 还保存了提交的历史记录。这也是为什么大多数提交记录的上面都有父节点的原因。
当我们使用 add 命令将工作区提交到暂存区,而暂存区其实保存的是当前文件的一个状态,其中包括有哪些目录和文件,以及其对应的大小和内容等信息。但是我们最终是需要将其提交到代码仓库(本地)的,而其命令就是 git commit 了。
而当我们执行 git commit 命令的时候,究竟都发生了什么呢?可以看到当提交之后,.git 目录中生成了两个信息的 object 对象,其中 logs 和 refs 目录都有新的文件生成。通过如下操作,我们可以查看到其提交的类型和对应内容。
# 左边执行
$ git commit -m 1st commit
$ git cat-file -t 6e4a700 # 查看 commit 对象的类型
$ git cat-file -p 6e4a700 # 查看 commit 对象的内容
$ git cat-file -t 64d6ef5 # 查看 tree 对象的类型
$ git cat-file -p 64d6ef5 # 查看 tree 对象的内容
# 右边执行
$ watch -n 1 -d tree .git
这样我们就理解了,当我们执行 git commit 命令之后,会生成一个 commit 对象和一个 tree 对象。commit 对象内容里面包含了一个 tree 对象和相关提交信息,而 tree 对象里面则包含了这次我们提交版本里面的文件状态(文件名称和 blob 对象),这样我们就知道了这次提交的变动了。
我们这次提交之后,处理 objects 目录发生变动之外,还有一些其他的变化。比如 logs 和 refs 的目录有所变化。我们查看 refs 目录里面的内容,发现其指向了 6e4a70 这个 commit 对象,即当前 master 分支上面最新的提交就是这个 6e4a70 了。
而这个 6e4a70 这个 commit 对象,有一个 head 的指向,就是 .git 目录下的 head 文件。其实质就是一个指针,其永远指向我们当前工作的分支,即这里我们工作在 master 分支上。当我们切换分支的时候,这个文件的指向也会随机改变的。
# 左边执行
$ cat .git/refs/heads/master
$ cat .git/head
# 右边执行
$ watch -n 1 -d tree .git
平板电视研发企业厦华电子发布2021年报
同轴喇叭与套装喇叭有什么不同
动动力电池产业成绩斐然,中国企业引领全球市场
张云勇:新基建要求5G网络加速覆盖
大地震!权威媒体炮轰比特币交易所蔑视中国法律,或将全面封杀!
探究Git基本原理(上)
泄水泵站监控解决方案
电视行业靠价格战“脱困”难,高端产品该如何争夺利润之地
无人驾驶汽车技术原理与实现
三相负载中性线和零线上电流计算公式
歼15引擎起火!遭鸟群撞击起火连人带机安全着陆,歼15撞机到降落全过程解析
日本推出全新的可调焦眼镜,将搭载液晶透镜
小米5S何时才能升级安卓7.0?官方的回应令人期待
变压器与EMI的关系解决方案
科大讯飞:已与奇瑞、广汽等几十家合作伙伴构建汽车AI星火生态
VR红色文化教育可视化,带你重走历史长河
新基建将会对数据中心产业产生重大影响
【干货】拓尔微按摩椅专题期刊发布
一加5什么时候上市?一加5什么配置?一加5的最新消息:8G运存 全面屏
一文详解变频器和PLC/PCA系统