git,代码管理之神器,众猿之良友。身出名门,构思巧妙,应用于各大小项目之间,受关注度颇为广泛
coder,苦猿也,终日代码为伴,左java, 右python, 仰观架构,俯视bug,神游于指令之间,精驰于算法内外,偶有心得,敲键不止,神神惶惶,战战兢兢,代码所得之难如此,稍有误删,前功尽弃,嚎啕戚然。
今有git作为辅助,众猿皆喜,心无挂碍,意有所托,代码无常损失再无畏也,今以拙笔,概述git原理,助众友猿,善用此神器
注:本文写作过程中,引用参考了不少优秀文章,比我描述更为清楚的,大有人在,参考的资料,都放在ref小节
1. git目录解析
古语有云:一生二,二生三,三生万物。三,万物之始也,git也不能免其道
打开一个git项目,我们会发现其中有个.git目录,展开.git目录,还会发现一个index文件。示例如下:
├── .git │ ├── HEAD │ ├── config │ ├── description │ ├── hooks │ ├── index │ ├── info │ ├── objects │ └── refs ├── a.txt ├── b.txt
这些目录非常重要,用于记录项目文件,提交记录,日志等,实现代码的提交,回滚,分支等重要工作。git根据用途,把这些目录划分为三种:
- working directory。工作目录,即项目文件本身。如图中的.git目录之外的文件,都属于working directory
- staging area,指的是.git/index文件。这是个二进制文件,记录着文件提交过程中的临时信息,保证数据提交前后的完整性
- repository。即.git目录下除index的所有文件,其中记录着文件提交后的信息,包括项目文件的修改历史,日志等
一个完整的git提交流程,被划分为三个步骤:
- 在working directory修改了某些文件
- 把修改的文件提交到staging area
- 执行commit,把staging area中的文件永久更新到repository
git提交过程如下:
2.新增文件
让我们来一步步的执行文件提交, 进而了解git的执行原理。虽然我们常用的指令是add, commit,但这两个指令是众多底层指令的封装,并不能很好的观察文件的变化。
在这里我们采用底层指令,来逐步展示文件的提交过程
让我们准备一个git项目。首先在任意路径创建一个目录gittest。并生成两个文件
mkdir gittest cd gittest echo $"File A">a.file mkdir subdir echo $"File B">subdir/b.file
然后把目录初始化为git项目
git init .
生成后的文件结构为:
$ tree -a . ├── .git │ ├── HEAD │ ├── config │ ├── description │ ├── hooks │ │ ├── applypatch-msg.sample │ │ ├── commit-msg.sample │ │ ├── fsmonitor-watchman.sample │ │ ├── post-update.sample │ │ ├── pre-applypatch.sample │ │ ├── pre-commit.sample │ │ ├── pre-push.sample │ │ ├── pre-rebase.sample │ │ ├── pre-receive.sample │ │ ├── prepare-commit-msg.sample │ │ └── update.sample │ ├── info │ │ └── exclude │ ├── objects │ │ ├── info │ │ └── pack │ └── refs │ ├── heads │ └── tags ├── a.file └── subdir └── b.file
2.1 创建blob object
我们现在要把a.file和subdir/b.file提交到git的repository之中。在repository中,文件并不是直接拷贝过去,而是生成一种叫做blob object的文件,repository 管理的是blob object。
blob object和项目文件是一对一的方式,一个项目文件,就有一个blob object
$ git hash-object -w a.file subdir/b.file 6d8b18077cc99abd8dda05a6062c646406abb2d4 c512b6c64656b87ea8caf37a32bc5a562d797745
该命令生成了blob object文件,并拷贝到.git目录下,另外输出了两个哈希值,我们来看看.git/objects目录:
$ tree .git/objects .git/objects ├── 6d │ └── 8b18077cc99abd8dda05a6062c646406abb2d4 ├── c5 │ └── 12b6c64656b87ea8caf37a32bc5a562d797745 ├── info └── pack
文件的哈希值前两位被当作目录,后面位数被当作blob object的文件名
我们来检查下blob object文件的内容
$ git cat-file -p 6d8b18077cc99abd8dda05a6062c646406abb2d4 File A
内容和我们的a.file一摸一样
2.2 提交到staging area
生成了blob object之后,要把这项变动通知到staging area中,也就是往.git/index文件中写入内容。
我们首先来看下.git/index的内容
$ git ls-files --stage
空空如也
现在我们来空瓶装好酒,把blob file记录到index中
$ git update-index --add --cacheinfo 100644 6d8b18077cc99abd8dda05a6062c646406abb2d4 a.file $ git update-index --add --cacheinfo 100644 c512b6c64656b87ea8caf37a32bc5a562d797745 subdir/b.file
再来看看我们的酒瓶
$ git ls-files --stage 100644 6d8b18077cc99abd8dda05a6062c646406abb2d4 0 a.file 100644 c512b6c64656b87ea8caf37a32bc5a562d797745 0 subdir/b.file
再用git status看看
$ git status On branch master No commits yet Changes to be committed: (use "git rm --cached <file>..." to unstage) new file: a.file new file: subdir/b.file
是不是我们在git add之后,看到的熟悉输出呢?
没错,git add其实一共就干了两件事,首先把要提交的文件生成blob object, 其次把改动记录在.git/index文件中
2.3 创建tree object
现在staging area已经更新了,下一步要往repository写入了。在写入之前,得先创建tree object。
$ git write-tree 9a61991471b755f7aa66f95a8d26c03fcb80ad59
生成了一个类似前面blob object的哈希值。我们来看看.git/objects下面新增了什么内容
$ tree .git/objects .git/objects ├── 08 │ └── d38f10799ada5673b3b9879e085bb7ffce8cd3 ├── 6d │ └── 8b18077cc99abd8dda05a6062c646406abb2d4 ├── 9a │ └── 61991471b755f7aa66f95a8d26c03fcb80ad59 ├── c5 │ └── 12b6c64656b87ea8caf37a32bc5a562d797745 ├── info └── pack
可以看到,新增了两个文件夹,把目录名和文件名拼凑在一起,可以对应两个哈希值:
- 9a61991471b755f7aa66f95a8d26c03fcb80ad59
- 08d38f10799ada5673b3b9879e085bb7ffce8cd3
9a61991471b755f7aa66f95a8d26c03fcb80ad59是刚生成的tree object。那另外一个呢?
我们看一下9a61991471b755f7aa66f95a8d26c03fcb80ad59的文件内容:
$ git cat-file -p 9a61991471b755f7aa66f95a8d26c03fcb80ad59 100644 blob 6d8b18077cc99abd8dda05a6062c646406abb2d4 a.file 040000 tree 08d38f10799ada5673b3b9879e085bb7ffce8cd3 subdir
神不神奇?里面竟然有一个08d38f10799ada5673b3b9879e085bb7ffce8cd3。而且从第二列我们可以看出,类型也是一个tree object
tree object其实对应着文件目录。当我们提交文件到git中时,除了文件本身提交,文件的组织关系(即目录)也要提交。比如例子中,b.file属于subdir, 那么subdir这个文件目录也要提交。除了subdir, 当前的目录,也会生成tree object提交
2.4 提交到repository
现在tree object也有了,万事俱备,该提交到repository了
$ echo "first commit"| git commit-tree 9a61991471b755f7aa66f95a8d26c03fcb80ad59 51a5e5fcbc1a1a6d2d0853417e195cfd6ccac6a6
commit-tree后面的参数是顶层目录的tree object
commit-tree执行后,输出了一个新的哈希值51a5e5fcbc1a1a6d2d0853417e195cfd6ccac6a6。聪明的你已经意识到了,会不会是另一种新的object呢?
我们看看.git/objects目录吧
$ tree .git/objects .git/objects ├── 08 │ └── d38f10799ada5673b3b9879e085bb7ffce8cd3 ├── 51 │ └── a5e5fcbc1a1a6d2d0853417e195cfd6ccac6a6 ├── 6d │ └── 8b18077cc99abd8dda05a6062c646406abb2d4 ├── 9a │ └── 61991471b755f7aa66f95a8d26c03fcb80ad59 ├── c5 │ └── 12b6c64656b87ea8caf37a32bc5a562d797745 ├── info └── pack
还真是。这种新的object被保存到了objects目录下,这是一个commit object。每次提交,git都会生成一个commit object。
我们再来看看具体内容:
$ git cat-file -p 51a5e5fcbc1a1a6d2d0853417e195cfd6ccac6a6 tree 9a61991471b755f7aa66f95a8d26c03fcb80ad59 author gerald <xixisese@gmail.com> 1632362835 +0800 committer gerald <xixisese@gmail.com> 1632362835 +0800 first commit
里面记录着我们的tree object。作者,提交时间,以及我们的日志。
commit object是提交时生成的object,记录着顶层tree object, 提交者,提交时间,提交日志等信息
至此,commit object生成,我们的提交工作就完成了,我们可以用git log查看我们刚才提交情况:
$ git log --stat 51a5e5fcbc1a1a6d2d0853417e195cfd6ccac6a6 commit 51a5e5fcbc1a1a6d2d0853417e195cfd6ccac6a6 Author: gerald <xixisese@gmail.com> Date: Thu Sep 23 10:07:15 2021 +0800 first commit a.file | 1 + subdir/b.file | 1 + 2 files changed, 2 insertions(+)
2.6 小结:
回顾前面的提交流程,我们小结如下:
- git的提交过程,就是生成blob object, tree object和commit object的过程
- 提交过程中,数据流向是从working directory, 到staging area, 再到repository
3. 修改文件
前面我们新增了两个文件,现在我们修改b.file, 再看一次与新增文件有什么不同
首先我们修改文件b.file
$ echo $"File B has modified" >> subdir/b.file $ cat subdir/b.file File B File B has modified
接下来我们提交b.file
- 创建新的blob object
$ git hash-object -w subdir/b.file 4b031c51add476b2f8c7a0f3b26239d01b60359f
- 更新到staging area
git update-index subdir/b.file
- 创建新的tree object
$ git write-tree 0cfd8b8e3fc7dce82d64f7b2ba8ed26f522f03f3
查看tree object的内容:
$ git cat-file -p 0cfd8b8e3fc7dce82d64f7b2ba8ed26f522f03f3 100644 blob 6d8b18077cc99abd8dda05a6062c646406abb2d4 a.file 040000 tree 7676ddb3411cfb76a5e59edc3af8ca7547ee3410 subdir $ git cat-file -p 7676ddb3411cfb76a5e59edc3af8ca7547ee3410 100644 blob 4b031c51add476b2f8c7a0f3b26239d01b60359f b.file
可以看到tree object依然包含a.file,虽然a.file并没有改变。也就是说,每次新的tree object都包含整个项目文件。
项目文件夹改变后,会生成新的tree object,如图,b.file的内容改变后,除了a.file对应的blob object保持不变外,b.file对应blob boject, subdir和当前路径对应的tree object都新生成了
- 提交tree object到repository
$ echo "second commit"| git commit-tree 0cfd8b8e3fc7dce82d64f7b2ba8ed26f522f03f3 -p 51a5e5fcbc1a1a6d2d0853417e195cfd6ccac6a6 024000dad0c2baedccc57a39d8f195abbde93be0
提交的时候,和之前一样,会生成哈希值为51a5e5fcbc1a1a6d2d0853417e195cfd6ccac6a6的commit object。这里多了一个-p参数,指定了当前commit的parent。
git中,除了第一个commit外,每一个commit都有parent, parent指向上一次的commit。这样就形成一条完整的commit链,回溯提交历史。
开发者可以指定的任意commit作为parent, 产生分叉, 在其基础上开展的后续提交,会成为一个独立的分支。分支的内容,我们后续讲解。
下图中,箭头代表parent,颜色不一的commit object,因为parent不同,就组成了不同的分支。
查看本例中的commit object, 注意其中多了parent字段:
$ git cat-file -p 024000dad0c2baedccc57a39d8f195abbde93be0 tree 0cfd8b8e3fc7dce82d64f7b2ba8ed26f522f03f3 parent 51a5e5fcbc1a1a6d2d0853417e195cfd6ccac6a6 author gerald <xixisese@gmail.com> 1632367201 +0800 committer gerald <xixisese@gmail.com> 1632367201 +0800 second commit
查看提交日志,我们能看到一条完整的提交链:
$ git log --stat 024000dad0c2baedccc57a39d8f195abbde93be0 commit 024000dad0c2baedccc57a39d8f195abbde93be0 Author: gerald <xixisese@gmail.com> Date: Thu Sep 23 11:20:01 2021 +0800 second commit subdir/b.file | 1 + 1 file changed, 1 insertion(+) commit 51a5e5fcbc1a1a6d2d0853417e195cfd6ccac6a6 Author: gerald <xixisese@gmail.com> Date: Thu Sep 23 10:07:15 2021 +0800 first commit a.file | 1 + subdir/b.file | 1 + 2 files changed, 2 insertions(+)
4. 引入reference
reference我们在下一章介绍branch的时候介绍,这里先提一下。
细心的朋友会发现,我们在运行git log的时候,会加上指定的commit id。而我们平时使用git时,直接就git log就可以。为什么呢?
这里就是我们上面说的commit链了,在git中,所有的commit形成了一颗树,或者叫做一条链,链上有各种分叉,每一条分叉都是独立的提交链
那么,当我们执行git log的时候,看的是哪一条分叉呢?我们需要一个默认分叉
我们把最新的commit id设置为默认分叉:
$ echo 024000dad0c2baedccc57a39d8f195abbde93be0 > .git/refs/heads/master
现在一切正常了:
$ git log commit 024000dad0c2baedccc57a39d8f195abbde93be0 (HEAD -> master) Author: gerald <xixisese@gmail.com> Date: Thu Sep 23 11:20:01 2021 +0800 second commit commit 51a5e5fcbc1a1a6d2d0853417e195cfd6ccac6a6 Author: gerald <xixisese@gmail.com> Date: Thu Sep 23 10:07:15 2021 +0800 first commit
Ref
- https://medium.com/hackernoon/understanding-git-index-4821a0765cf
- https://medium.com/hackernoon/https-medium-com-zspajich-understanding-git-data-model-95eb16cc99f5
- https://medium.com/hackernoon/understanding-git-branching-2662f5882f9
- https://hackernoon.com/reset-101-ba05d9e3f2c7
- https://git-scm.com/book/en/v2
回复 agodelo 取消回复