本文是Frontend Masters课程《深入理解Git》(Git In-depth)笔记。
本文是第一部分,Git基础(Git Foundations)。
本文在Windows下使用git bash输入命令进行测试。
Git的核心为键值存储系统(key-value store)。
由于键是由内容产生的,这种系统也被称为内容可寻址存储系统(content addressable storage system)。
Git将压缩的数据存储在blob中,同时将元数据存储在blob的header中。blob由四部分组成:
先用git hash-object
生成“Hello, World!\n"的SHA1值,再用openssl sha1
生成”blob 14\0Hello, World!\n"SHA1值。这两次生成的SHA1值相同。说明git hash-object
在内容之前增加“blob标识符”、“内容的大小”和“\0”,然后求SHA1值。
echo
返回到stdin中的字符串自动在末尾补”\n“):
$ echo 'Hello, World!' | git hash-object --stdin
8ab686eafeb1f44702738c8b0f24f2567c36da6d
由于无法在Windows环境的git bash中输入”\0"。因此在nodejs中执行openssl sha1
//file: "hash.js"
const { exec } = require("child_process")
const cp = exec("openssl sha1", (error, stdout, stderr) => console.log(stdout))
cp.stdin.end("blob 14\0Hello, World!\n")
$ node hash.js
(stdin)= 8ab686eafeb1f44702738c8b0f24f2567c36da6d
在理解了git hash-object
的基本工作原理后,尝试在.git文件夹中生成blob对象。
用git init
初始化生成.git文件夹。hooks文件夹中的内容较多,删除以便容易看清.git文件夹结构。
$ git init
Initialized empty Git repository in E:/learn/git/sample/.git/
$ rm -r .git/hooks
使用cmd //c tree .git //f
展示.git文件夹的结构,在.git/object下只有pack和info两个空文件夹。
$ cmd //c tree .git //f
E:\LEARN\GIT\SAMPLE\.GIT
│ description
│ HEAD
│ config
├─info
│ exclude
├─refs
│ ├─heads
│ └─tags
└─objects
├─pack
└─info
用git hash-object -w
生成blob并存放在.git文件夹内,存放的具体位置由blob的SHA1值的前2位决定,blob的文件名为SHA1的后38位。
$ echo 'Hello, World!' | git hash-object -w --stdin
8ab686eafeb1f44702738c8b0f24f2567c36da6d
内容“Hello, World!\n"生成的blob的SHA1值为:8ab686eafeb1f44702738c8b0f24f2567c36da6d,存放在.git/objects/8a,文件名为:b686eafeb1f44702738c8b0f24f2567c36da6d
$ cmd //c tree .git //f
E:\LEARN\GIT\SAMPLE\.GIT
│ description
│ HEAD
│ config
├─info
│ exclude
├─refs
│ ├─heads
│ └─tags
└─objects
├─pack
├─info
└─8a
b686eafeb1f44702738c8b0f24f2567c36da6d
用git cat-file -t
可以显示Git对象的类型。以8ab6开头的SHA1值对应的Git对象的类型为blob。在Git中可以用SHA1值的前若干位(最少4位)来确定对象。Git对象的类型除了blob外还有tree和commit。
$ git cat-file -t 8ab6
blob
用git cat-file -p
可以显示Git对象的内容。
$ git cat-file -p 8ab6
Hello, World!
类型为blob的Git对象的SHA1值由内容唯一确定,无论是内容来自字符串还是文件。因此blob对象中只包含内容信息,不包含文件名和文件路径信息。这样的好处是重复内容的文件不会再.git中重复存储。文件名和文件路径信息存储在tree对象中。
$ git init
Initialized empty Git repository in E:/learn/git/sample/.git/
$ echo 'Hello, World!' > hello.txt
$ git hash-object -w hello.txt
8ab686eafeb1f44702738c8b0f24f2567c36da6d
初始化生成.git文件夹。生成包含内容"Hello, World!\n"的文件"hello.txt"。
$ git init
Initialized empty Git repository in E:/learn/git/sample/.git/
$ echo 'Hello, World!' > hello.txt
通过git status
以及cmd //c tree .git //f
查看Git状态和.git文件夹结构。Git的状态为有1个未跟踪的文件"hello.txt"。在.git/objects下没有Git对象。
$ git status
On branch master
No commits yet
Untracked files:
(use "git add ..." to include in what will be committed)
hello.txt
nothing added to commit but untracked files present (use "git add" to track)
$ cmd //c tree .git //f
E:\LEARN\GIT\SAMPLE\.GIT
│ description
│ HEAD
│ config
├─hooks
├─info
│ exclude
├─refs
│ ├─heads
│ └─tags
└─objects
├─pack
└─info
添加hello.txt文件
$ git add hello.txt
通过git status
以及cmd //c tree .git //f
查看Git状态和.git文件夹结构。Git的状态为有1个未提交的新文件"hello.txt"。此时.git/objects下有一个SHA1值为8ab686eafeb1f44702738c8b0f24f2567c36da6d的Git对象,此对象包含了"hello.txt"文件的内容。说明blob在git add
时生成。
$ git status
On branch master
No commits yet
Changes to be committed:
(use "git rm --cached ..." to unstage)
new file: hello.txt
$ cmd //c tree .git //f
E:\LEARN\GIT\SAMPLE\.GIT
│ description
│ HEAD
│ config
│ index
├─hooks
├─info
│ exclude
├─refs
│ ├─heads
│ └─tags
└─objects
├─pack
├─info
└─8a
b686eafeb1f44702738c8b0f24f2567c36da6d
复制并添加文件
$ mkdir copies
$ cat hello.txt > copies/hello-copy.txt
$ git add copies/hello-copy.txt
通过git status以及cmd //c tree .git //f查看Git状态和.git文件夹结构。Git的状态为有2个未提交的新文件"hello.txt"和"copies/hello-copy.txt"。此时.git/objects下有1个SHA1值为8ab686eafeb1f44702738c8b0f24f2567c36da6d的Git对象。由于"hello.txt"和"copies/hello-copy.txt"的内容相同,所以在git add copies/hello-copy.txt
之后,并不会增加新的Git对象。
$ git status
On branch master
No commits yet
Changes to be committed:
(use "git rm --cached ..." to unstage)
new file: copies/hello-copy.txt
new file: hello.txt
$ cmd //c tree .git //f
E:\LEARN\GIT\SAMPLE\.GIT
│ description
│ HEAD
│ config
│ index
├─hooks
├─info
│ exclude
├─refs
│ ├─heads
│ └─tags
└─objects
├─pack
├─info
└─8a
b686eafeb1f44702738c8b0f24f2567c36da6d
查看.git/objects下唯一的Git对象,类型为"blob",内容为"Hello, World!"。
$ git cat-file 8ab6 -t
blob
$ git cat-file 8ab6 -p
Hello, World!
在tree中包含两类指针,一类指向blob,一类指向其他tree,指针用SHA1值表示。tree中还包含了其他信息:
tree是Git对象的一种,有其对应的SHA1值,存储在.git/objects下。每一个tree所包含的文件夹结构(所有文件和文件夹的路径名称)以及所有文件内容,决定了tree的SHA1值。不同的两个tree其所包含的文件夹结构或文件内容必然不同。
正常情况下,不被其他任何tree指向的tree为根,代表了某一时刻工作区的文件夹结构和文件内容,但却无法表示是具体的哪一时刻,及其变化历史。
commit代表了具体时刻的工作区文件夹结构和内容,及其变化历史。commit中的内容包括:
commit的SHA1值由以上所有信息共同决定。
在之前操作的基础上执行git commit
,并用cmd //c tree .git //f
查看.git文件夹结构。在.git/objects下,新增了3个Git对象。
$ git commit -m "Initial commit"
[master (root-commit) fec787b] Initial commit
2 files changed, 2 insertions(+)
create mode 100644 copies/hello-copy.txt
create mode 100644 hello.txt
$ cmd //c tree .git //f
E:\LEARN\GIT\SAMPLE\.GIT
│ description
│ HEAD
│ config
│ index
│ COMMIT_EDITMSG
├─hooks
├─info
│ exclude
├─refs
│ ├─heads
│ │ master
│ └─tags
├─objects
│ ├─pack
│ ├─info
│ ├─8a
│ │ b686eafeb1f44702738c8b0f24f2567c36da6d
│ ├─8c
│ │ 0dd4898dc9d0eb8a55eb7773c4ee433577e135
│ ├─50
│ │ d594ce398b77c1fb612ae7ee8745587a3dedc6
│ └─fe
│ c787b35bc3163f9f821fa13795c97a622321f8
└─logs
│ HEAD
└─refs
└─heads
master
在git commit -m "Initial commit"
的返回中可以看到,最新生成的commit对象的SHA1值以fec787b开头。查看Git对象"fec7"的类型和内容。"fec7"的类型为commit,内容包含:当前根的指针、作者、提交者、message以及时间,由于"fec7"是第一个commit因此内容中不包含指向parent的指针。
$ git cat-file fec7 -t
commit
$ git cat-file fec7 -p
tree 50d594ce398b77c1fb612ae7ee8745587a3dedc6
author LI Chuan 1617198205 +0800
committer LI Chuan 1617198205 +0800
Initial commit
顺着当前根tree的指针我们可以遍历到当前时刻根tree所包含的其他所有的tree和blob。
$ git cat-file 50d5 -t
tree
$ git cat-file 50d5 -p
040000 tree 8c0dd4898dc9d0eb8a55eb7773c4ee433577e135 copies
100644 blob 8ab686eafeb1f44702738c8b0f24f2567c36da6d hello.txt
$ git cat-file 8c0d -t
tree
$ git cat-file 8c0d -p
100644 blob 8ab686eafeb1f44702738c8b0f24f2567c36da6d hello-copy.txt
$ git cat-file 8ab6 -t
blob
$ git cat-file 8ab6 -p
Hello, World!
当新增一个文件并提交后,在.git/objects下新增了3个Git对象,分别为1个commit、1个tree和1个blob。
$ echo 'A new file.' > new.txt
$ git add new.txt
$ git commit -m "Add new.txt"
[master 46752eb] Add new.txt
1 file changed, 1 insertion(+)
create mode 100644 new.txt
$ cmd //c tree .git //f
E:\LEARN\GIT\SAMPLE\.GIT
│ description
│ HEAD
│ config
│ COMMIT_EDITMSG
│ index
├─hooks
├─info
│ exclude
├─refs
│ ├─heads
│ │ master
│ └─tags
├─objects
│ ├─pack
│ ├─info
│ ├─8a
│ │ b686eafeb1f44702738c8b0f24f2567c36da6d
│ ├─8c
│ │ 0dd4898dc9d0eb8a55eb7773c4ee433577e135
│ ├─50
│ │ d594ce398b77c1fb612ae7ee8745587a3dedc6
│ ├─fe
│ │ c787b35bc3163f9f821fa13795c97a622321f8
│ ├─de
│ │ baded8eaa1bdcc5bfc7e898cec32592e427583
│ ├─ea
│ │ 2680dd7373ed7f9deca127698849709834edca
│ └─46
│ 752eb236fa0459f018d0c2933f1642df2a4f8d
└─logs
│ HEAD
└─refs
└─heads
master
分别查看新增的commit、tree和blob。新增加的commit的内容中多了指向上一个(parent)commit的指针。这样在在一个分支的情况下,通过commit可以在时间上向上(parent)遍历到所有commit。在某一个时刻,可以通过commit指向的根tree遍历到此时工作区的所有文件夹结构和文件内容。
$ git cat-file 4675 -t
commit
$ git cat-file 4675 -p
tree ea2680dd7373ed7f9deca127698849709834edca
parent fec787b35bc3163f9f821fa13795c97a622321f8
author LI Chuan 1617199399 +0800
committer LI Chuan 1617199399 +0800
Add new.txt
$ git cat-file ea26 -t
tree
$ git cat-file ea26 -p
040000 tree 8c0dd4898dc9d0eb8a55eb7773c4ee433577e135 copies
100644 blob 8ab686eafeb1f44702738c8b0f24f2567c36da6d hello.txt
100644 blob debaded8eaa1bdcc5bfc7e898cec32592e427583 new.txt
$ git cat-file deba -t
blob
$ git cat-file deba -p
A new file.
查看此时的.git/HEAD和.git/refs/heads/master。master中的内容为指向master分支中的最新一个commit的指针,而HEAD则直接指向master。
$ cat .git/HEAD
ref: refs/heads/master
$ cat .git/refs/heads/master
46752eb236fa0459f018d0c2933f1642df2a4f8d
blob、tree和commit为Git的基础,其他大部分功能都建立在这三者之上。blob存储了工作空间的内容,tree存储了工作空间的结构,commit存储了工作空间的变化历史。