浅谈 git 底层工作原理
系统复习到这里也快差不多了,大概就剩下两三个 sections,这里学习一下 git 的 hashing 和对象。
当然,跳过问题也不大。
config 文件
这里还是会用 redux 的项目,先看一下基本信息:
➜ redux git:(master) cd .git
➜ .git git:(master) ls
COMMIT_EDITMSG config index objects
HEAD description info packed-refs
ORIG_HEAD hooks logs refs
➜ .git git:(master) cat config
[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
ignorecase = true
precomposeunicode = true
[remote "origin"]
url = https://github.com/reduxjs/redux.git
fetch = +refs/heads/*:refs/remotes/origin/*
[branch "master"]
remote = origin
merge = refs/heads/master
config 文件保存的就是所有的本地文件,如更新了本地的用户名,config 也会有对应的更新:
➜ .git git:(master) cat config
[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
ignorecase = true
precomposeunicode = true
[remote "origin"]
url = https://github.com/reduxjs/redux.git
fetch = +refs/heads/*:refs/remotes/origin/*
[branch "master"]
remote = origin
merge = refs/heads/master
[user]
name = GA
下面提一个简单的例子,默认的 git branch 看起来是这个样子的:
通过简单的配置:
[color]
ui = true
[color "branch"]
local = cyan
current = yellow bold
可以将 UI 改成下面这个样子:
具体的设定很多,可以看下 reference,如果真的感兴趣的话……有些配置真的可以搞得很风骚,但我确实是用的不太多……
ref
refs 下面有三个文件夹:
-
heads
保存所有的 head,每个文件都以分支的名称结尾,每个文件里面只包含最后一个 commit 的 hash 值:
❯ cd heads ❯ ls master new-branch ❯ cat master 49d04ce8e7ec22917d09eb50f61fe464f324b65f
-
remotes
如文件夹名称所示,这里会下载所有的 remotes,每一个 remote 是一个单独的文件夹,每个文件夹下面包含下载的远程分支。
❯ cd remotes ❯ ls origin ❯ cd origin ❯ ls HEAD ❯ cat HEAD ref: refs/remotes/origin/master
这也是为什么 git 知道分支已经不同了,正巧今天 redux 又做了一个 commit,可以做一个 demo:
❯ git checkout master Switched to branch 'master' Your branch is ahead of 'origin/master' by 2 commits. (use "git push" to publish your local commits) ❯ git fetch remote: Enumerating objects: 17, done. remote: Counting objects: 100% (17/17), done. remote: Compressing objects: 100% (17/17), done. remote: Total 17 (delta 9), reused 0 (delta 0), pack-reused 0 Unpacking objects: 100% (17/17), 15.73 KiB | 644.00 KiB/s, done. From https://github.com/reduxjs/redux 662f1ed1..1d0e692e master -> origin/master ❯ git status On branch master Your branch and 'origin/master' have diverged, and have 2 and 3 different commits each, respectively. (use "git pull" to merge the remote branch into yours) nothing to commit, working tree clean ❯ cat .git/refs/remotes/origin/master 1d0e692e5e9c6c030523791b107ca536ca2823fd ❯ git log --oneline 49d04ce8 (HEAD -> master, new-branch) add new message 11d94c7a add text to readme 662f1ed1 Merge pull request #4532 from jimhigson/patch-1 34308acd (tag: v5.0.0-alpha.6) Merge pull request #4531 from MahendraBishnoi29/patch-5 378c9276 Fix missing call to `getDefaultMiddleware` 9944978b update React Fragment link 8782a6e8 update tutorial link HTML & CSS url (#4528)
在拉取了远程的代码后,远程获得了一个新的 commit hash,这个在本地上是没有的,通过对比后,git 能够获得这样的信息:
通过对比 hash value,git 可以准确的判断,在 662f1ed1 之后,远程和我本地分别有了不同的 commits,而这就造成了不同的分支,因此它提出了 diverged branch 这个说法。
-
tags
tags 和 refs 相似。
里面除了保留远程的 tags 之外,还会保留本地的 tags。
❯ cd tags ❯ ls diff-tag v5.0.0-alpha.6 ❯ cat diff-tag 30635355a0f4fb6783420273f8d09c0893d52424
HEAD
HEAD 是一个文件追踪目前的 HEAD 指向什么地方,一般是一个 branch,或是一个 commit。
二者的区别在 Git 时间线管理 有提到过。
❯ cat .git/HEAD
ref: refs/heads/master
❯ git checkout new-branch
Switched to branch 'new-branch'
❯ cat .git/HEAD
ref: refs/heads/new-branch
❯ git checkout b05e4325
Note: switching to 'b05e4325'.
You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by switching back to a branch.
If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -c with the switch command. Example:
git switch -c <new-branch-name>
Or undo this operation with:
git switch -
Turn off this advice by setting config variable advice.detachedHead to false
HEAD is now at b05e4325 Release 5.0.0-alpha.5
❯ cat .git/HEAD
b05e43256ec2bed86e1180be978b41313cf6529d
objects
objects 包含了所有 git 的备份文件、commits 等,这些文件通常都是压缩且加密(目前 git 用的是 SHA-1 进行加密)的,加密过后的 40 位的值可以用来获取当时这个 commit 文件,文件包含内容如下:
❯ ls .git/objects
11 1b 23 30 45 63 78 96 b5 b9 df f4 pack
17 1d 26 31 49 67 7d a3 b7 dc ee info
❯ ls .git/objects/11
d94c7a0fb7ef4fdcefb6e701674c2449804abd
❯ ls .git/objects/pack
pack-6e1431c4d78908ed9d91bfa34c7d22b1da735487.idx pack-6e1431c4d78908ed9d91bfa34c7d22b1da735487.pack
❯ ls .git/objects/info
其加密的语法位 git hash-object <file>
,git 将输入的文件加密,并返回 hashed value 作为 key,这样之后可以回溯这时传过去的文件。
# 仅仅是将其输出到 console,用 -w flag 会将这个 command 会写到 objects 里
❯ echo 'hello' | git hash-object --stdin
ce013625030ba8dba906f756967f9e9ca394464a
❯ ls .git/objects
11 1b 23 30 45 63 78 96 b5 b9 df f4 pack
17 1d 26 31 49 67 7d a3 b7 dc ee info
# 下面的记录会写到 objects 中
❯ echo 'hello' | git hash-object --stdin -w
ce013625030ba8dba906f756967f9e9ca394464a
❯ ls .git/objects
11 1b 23 30 45 63 78 96 b5 b9 dc ee info
17 1d 26 31 49 67 7d a3 b7 ce df f4 pack
❯ ls .git/objects/ce
013625030ba8dba906f756967f9e9ca394464a
git 获取了 2 位字符用作文件夹的名称,随后存储了后 38 位作为单独文件的名称,这样通过 文件夹+文件
的方式,git 就能够获取 40 位 16 进制的值作为 key,去回溯当时提交的文件内容。
想要追溯加密的文件可以用 git cat-file <object-hash>
,如:
# -p 代表 pretty print
❯ git cat-file -p ce013625030ba8dba906f756967f9e9ca394464a
hello
# redux 成员做的 commit
❯ git cat-file -p b951537de944539df9e9526be0429234dc2b7aad
tree 67bdbc53959986be15daf89bc98534f4cdaa5fa1
parent 8782a6e8321cb97ce0e21f77874b360593f4363c
author browny <mahendrabrowny@gmail.com> 1682414114 +0530
committer GitHub <noreply@github.com> 1682414114 +0530
gpgsig -----BEGIN PGP SIGNATURE-----
wsBcBAABCAAQBQJkR5oiCRBK7hj4Ov3rIwAANKgIACwdeWIZ6u8W3YOBLNupe8jr
B/q/feV5W41G+3+0iCx+JcWtbdCl0H79OCvj0Xm/decgbbf0IO5RYdQ0lOuIXvQu
kXlNREGTIdlkh7gv2WoRRNjBWL6fQ5LybbmooOcJwCB4f47MMsj5HKnRsKl0XWYW
C8qn2eQbs09QST4vv4baldTFPVzW4ajL+KHCwXZG76Ou7OFbPzIR7RGg6QPPK0av
3P30HMeXSlDyxaHLkb6OHIEAyK+Yl+nDvLwdORFeu8wTpe2B0MNiZBqugzerBmwf
En8ZOht33YKRGwuXbW42qSI2Hi0BJNgUpCnDFaEDyUl2ezicQxhTQPKhWBXrEVw=
=qYQZ
-----END PGP SIGNATURE-----
update thunk link%
为了表示 git 会将当前文件 hash 进去,这里继续举个例子:
❯ echo "new file \n love it" > new.txt
❯ cat new.txt
new file
love it
❯ git hash-object new.txt
dcad5c179ec08ab9dbc57a9dc4be586fb3caaf15
❯ git hash-object new.txt -w
dcad5c179ec08ab9dbc57a9dc4be586fb3caaf15
❯ ls .git/objects/
11 1b 23 30 45 63 78 96 b5 b9 dc ee info
17 1d 26 31 49 67 7d a3 b7 ce df f4 pack
❯ ls .git/objects/dc
2c05886aa4658763804b3c295c3e3cadea3414 ad5c179ec08ab9dbc57a9dc4be586fb3caaf15
❯ git cat-file dcad5c179ec08ab9dbc57a9dc4be586fb3caaf15 -p
new file
love it
这也是为什么在之后的 commit 中删除文件,checkout 到之前的 commit 还是可以追溯回当时的版本的原因。
blob
使用命令行保存的二进制文件就是 blog,blob 中不包含文件名,只保存所有的内容。
每一个 blob 也会有自己的 hash value。
tree
tree 解决了文件名的问题——blob 是不保存文件名的,同时它可以存储多个文件——每个 blob 只保存单独文件的内容。git 中所有的文件都会被保存成 blob 或者 tree,tree 可以对标成文件夹,blob 对标为单独的一个文件。
假设保存的文件结构如下:
|- index.html
|- main.js
|- styles
| |- main.css
| |- index.css
对应的储存的 blog 结构如下:
每个 tree 都会保存对应 blob 和 tree 的引用,除此之外还会保存 mode、type 和文件名,如:
mode | type | hashed value | filename |
---|---|---|---|
100644 | blob | e03d902a8d2a53c889cc129bda1b0ce2cf718a67 | .babelrc.cjs |
查看当前 tree 的指令为 git cat-file -p <branch | commit hash>^{tree}
,如:
❯ git cat-file -p master^{tree}
100644 blob e03d902a8d2a53c889cc129bda1b0ce2cf718a67 .babelrc.cjs
040000 tree a2f9306f466054b9d6bac7fec0a5ea8b4eb13637 .codesandbox
100644 blob ed5699bd11a460bab0476e42d9d879ffd7d0580b .editorconfig
100644 blob efac9cb61b3e7b4501710a9c70ddf81c7e41d7bb .eslintignore
100644 blob 8f8944f970ee5c1ae83432ec7a1822a56d43cc4d .eslintrc.cjs
100644 blob 72cfe1819b98b78c1cb600bcbf46702673077b34 .git-blame-ignore-revs
100644 blob cc4ea460c9dc256ad6d38bd605006239d23b1541 .gitbook.yaml
commit
commit 的结构稍微复杂一些,它会包含更多的信息:tree(当前 repo 下的所有内容),parent,author,committer,message,如上面查看过的:
❯ git cat-file -p b951537de944539df9e9526be0429234dc2b7aad
tree 67bdbc53959986be15daf89bc98534f4cdaa5fa1
parent 8782a6e8321cb97ce0e21f77874b360593f4363c
author browny <mahendrabrowny@gmail.com> 1682414114 +0530
committer GitHub <noreply@github.com> 1682414114 +0530
gpgsig -----BEGIN PGP SIGNATURE-----
wsBcBAABCAAQBQJkR5oiCRBK7hj4Ov3rIwAANKgIACwdeWIZ6u8W3YOBLNupe8jr
B/q/feV5W41G+3+0iCx+JcWtbdCl0H79OCvj0Xm/decgbbf0IO5RYdQ0lOuIXvQu
kXlNREGTIdlkh7gv2WoRRNjBWL6fQ5LybbmooOcJwCB4f47MMsj5HKnRsKl0XWYW
C8qn2eQbs09QST4vv4baldTFPVzW4ajL+KHCwXZG76Ou7OFbPzIR7RGg6QPPK0av
3P30HMeXSlDyxaHLkb6OHIEAyK+Yl+nDvLwdORFeu8wTpe2B0MNiZBqugzerBmwf
En8ZOht33YKRGwuXbW42qSI2Hi0BJNgUpCnDFaEDyUl2ezicQxhTQPKhWBXrEVw=
=qYQZ
-----END PGP SIGNATURE-----
update thunk link%
其中 gpgsig
以下的部分是实际的 commit message,PGP SIGNATURE 是一个 e-verify 加密,redux 除了 git 之外应该还有其他的验证。
reference
-
git-config - Get and set repository or global options
-
Github merkle DAG
-
How is git commit sha1 formed
-
How is the Git hash calculated?
-
Git Internals - Git Objects