深入理解Git:(1) Git基础

说明

本文是Frontend Masters课程《深入理解Git》(Git In-depth)笔记。
本文是第一部分,Git基础(Git Foundations)。
本文在Windows下使用git bash输入命令进行测试。

数据存储

Git的核心为键值存储系统(key-value store)。

  • 值(value)为数据(data)。
  • 键(key)为数据的哈希值(hash)。
  • 键的格式为SHA1。

由于键是由内容产生的,这种系统也被称为内容可寻址存储系统(content addressable storage system)。

blob

Git将压缩的数据存储在blob中,同时将元数据存储在blob的header中。blob由四部分组成:

  1. blob标识符
  2. 内容的大小
  3. \0
  4. 内容
用git hash-object生成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 add时生成blob

初始化生成.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 <file>..." 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 <file>..." 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 <file>..." 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

在tree中包含两类指针,一类指向blob,一类指向其他tree,指针用SHA1值表示。tree中还包含了其他信息:

  1. 指针的类型(type of pointer):blob或tree
  2. 文件(当指针指向blob时)或文件夹(当指整指向tree时)名称
  3. 文件属性(mode):executable file, symbolic link …

tree是Git对象的一种,有其对应的SHA1值,存储在.git/objects下。每一个tree所包含的文件夹结构(所有文件和文件夹的路径名称)以及所有文件内容,决定了tree的SHA1值。不同的两个tree其所包含的文件夹结构或文件内容必然不同。

正常情况下,不被其他任何tree指向的tree为根,代表了某一时刻工作区的文件夹结构和文件内容,但却无法表示是具体的哪一时刻,及其变化历史。

commit

commit代表了具体时刻的工作区文件夹结构和内容,及其变化历史。commit中的内容包括:

  1. 指向根的指针(1个)
  2. 指向其他commit(parent)的指针(0,1或若干个)
  3. 作者和提交者
  4. 时间
  5. message

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 <lichuan_172@163.com> 1617198205 +0800
committer LI Chuan <lichuan_172@163.com> 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 <lichuan_172@163.com> 1617199399 +0800
committer LI Chuan <lichuan_172@163.com> 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存储了工作空间的变化历史。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值