在未接触git前,当需要修改已稳定运行的代码时,我们一般都是先把文件copy一份,在copy的文件上进行修改,比如a.py 修改成a1.py, 或者b.py修改成b_new.py等等,这个非常不便于管理及无法对修改历史进行追踪.
因而我们看看git在文件内容发生变化时是如何重新命名的.
新建一个空目录git-test
在目录下执行以下操作
$ git init
Initialized empty Git repository in /home/duan/hc-work/git-test/.git/
重点关注objects部分有无变化
$ tree .git/objects
├── objects
│ ├── info
│ └── pack
现在来看一下git add之后,看看有哪些变化:
$ echo hello > hello.txt
$ git add hello.txt
$ tree .git
这里面多了一个叫index的文件及.git/object多了一个文件
$ tree .git/objects/
.git/objects/
├── ce
│ └── 013625030ba8dba906f756967f9e9ca394464a
├── info
└── pack
采用git cat-file 来分别查看文件名为ce013625030ba8dba906f756967f9e9ca394464a的文件类型与文件内容,注意这里可以简写,只要不产生文件名冲突即可. 其次注意objects目录的下一级目录是以id的前两个字符命名的。
$ git cat-file -t ce01362503
blob
$ git cat-file -p ce01362503
hello
git是如何生成文件名013625030ba8dba906f756967f9e9ca394464a的,根据git说明操作如下:
$ wc -c hello.txt
6 hello.txt
$ sha1sum < <(printf "blob 6\000";cat hello.txt)
ce013625030ba8dba906f756967f9e9ca394464a
以上就是blob对象的文件名计算,通过文件长度+文件内容合并在一起计算sha1 id作为文件名.
再来看下带目录结构add时的变化:
$ mkdir -p a/b
$ ls
a hello.txt
$ echo 'hahaha!!!' > a/b/c.txt
$ git add .
$ tree .git/
├── objects
│ ├── 56
│ │ └── d90ab53ecd048a1f75f7e69ea3b276a4903725
│ ├── ce
│ │ └── 013625030ba8dba906f756967f9e9ca394464a
Git 并没有一个专门的暂存目录来存储一些代表文件变化的对象(Blobs),它使用一个叫做index的文件, 这里可以看出,index并不存储tree对象,只存储blob对象。
.git/index是一个二进制,感兴趣的话,可以从网上查到写入格式.
现在我们可以用命令行来展示index中的文件内容.
$ git ls-files -s
100644 56d90ab53ecd048a1f75f7e69ea3b276a4903725 0 a/b/c.txt
100644 ce013625030ba8dba906f756967f9e9ca394464a 0 hello.txt
$ git cat-file -t 56d90ab53ec
blob
$ git cat-file -p 56d90ab53ec
hahaha!!!
$ git commit -m 'first commit.'
[master (root-commit) bc9bbb6] first commit.
2 files changed, 2 insertions(+)
create mode 100644 a/b/c.txt
create mode 100644 hello.txt
提交之后,发现index并没有变化,也就是暂存区不变.
$ git ls-files -s
100644 56d90ab53ecd048a1f75f7e69ea3b276a4903725 0 a/b/c.txt
100644 ce013625030ba8dba906f756967f9e9ca394464a 0 hello.txt
但是objects多了几个文件
$ tree .git/objects/
.git/objects/
├── 56
│ └── d90ab53ecd048a1f75f7e69ea3b276a4903725
├── 57
│ └── 57ab9a5a5f1d2d78a1a3a8c1c8d7203b30947b
├── 9f
│ └── 07ce23710cc70e4ba9ef4ce0c62d132e87eda4
├── bc
│ └── 9bbb6ed25afbb48282557f489c6a5dbc71a282
├── ce
│ └── 013625030ba8dba906f756967f9e9ca394464a
├── d1
│ └── 8191dffd2b34ef45974cddcd3a6d05f9f747b9
├── info
└── pack
8 directories, 6 files
首先看到提交id 为bc9bbb6对应文件类型与内容
逐一分析这6个文件.
commit 文件对象
$ git cat-file -t bc9bbb6ed25a
commit
$ git cat-file -p bc9bbb6ed25a
tree d18191dffd2b34ef45974cddcd3a6d05f9f747b9
author dxp <cicido@126.com> 1678361997 +0800
committer dxp <cicido@126.com> 1678361997 +0800
first commit.
以上是commit文件对象的内容,commit id生成类似, 首先文件内容是确定的,包括tree id及作者信息及提交信息。
commit id计算
$ git cat-file -p bc9bbb6ed25a | wc -c
154
(base) duan@A5-14:~/hc-work/git-test$ sha1sum < <(printf "commit 154\000";git cat-file -p bc9bbb6ed25a)
bc9bbb6ed25afbb48282557f489c6a5dbc71a282 -
也是commit文件长度+commit内容生成sha1 id, 并以此id作为commit文件对象的文件名
tree文件对象
$ git cat-file -t d18191dff
tree
$ git cat-file -p d18191dff
040000 tree 9f07ce23710cc70e4ba9ef4ce0c62d132e87eda4 a
100644 blob ce013625030ba8dba906f756967f9e9ca394464a hello.txt
tree id 计算:
$ git cat-file -p d18191dff | wc -c
118
(base) duan@A5-14:~/hc-work/git-test$ sha1sum < <(printf "tree 118\000";git cat-file -p d18191dff)
d1662fd9ccfd6f7d4aee1d24c5b453f9ebf56d7b -
至此,commit, tree, blob对象的文件名id生成已全部清晰,都是由文件长度+文件内容生成.
递归分析子tree id对象
$ git cat-file -t 9f07ce23710
tree
$ git cat-file -p 9f07ce23710
040000 tree 5757ab9a5a5f1d2d78a1a3a8c1c8d7203b30947b b
$ git cat-file -p 5757ab9a5a
100644 blob 56d90ab53ecd048a1f75f7e69ea3b276a4903725 c.txt
至此,通过tree文件内容能建立起整个代码目录结构,id生成过程是一个自下而上的过程.
blob文件对象
参照前面的blob文件id分析.
另外执行命令git write-tree可以得到最顶层的tree id,并用此id作为commit文件中的tree id.
$ git write-tree
d18191dffd2b34ef45974cddcd3a6d05f9f747b9
这个id刚好是提交对象中的tree-id. commit利用此id建立一个commit文件对象.
由此可见,git中共有四类对象blob, tree, commit, tag, tag对象将在后面进行介绍.
blob为文件对象,对应代码文件.
tree对目录对象,可以建立起整个代码目录结构。
commit对象,用于标记以当前提交时间整个代码目录与文件的结构。一旦生成提交对象,则可以通过此对象id,索引出整个代码结构及当前代码内容.
这样就可以一个commit id来代表某个时间结点时的所有目录与文件,代表了一个版本库.