综述:从根本上来讲 Git 是一套内容寻址 (content-addressable) 文件系统,在此之上提供了一个 VCS 用户界面。
-
1. 安装tree
工具,用于查看文件目录结构,可以有助于对于Git的学习理解。
brew install tree
安装好之后,可以在任意目录下输入tree
命令,就可以看到目录以树形结构进行展示,层级关系一目了然。
2. 新建一个干净的文件夹,执行ls -a
命令来显示所有文件(包括隐藏文件),可以看到两个文件:
. ..
不难联想到当我们想返回当前文件的上级目录,通常都会用cd ..
命令,而这里就有..
这个隐藏文件,所以可以理解为..
其实是对上级目录的一个标识文件。
3. 在当前目录执行git init
命令,然后再ls -a
查看文件情况。
. .. .git
多出来一个隐藏文件.git
,所有 Git 存储和操作的内容都位于该目录下。如果你要备份或复制一个库,基本上将这一目录拷贝至其他地方就可以了。
4. cd到.git
文件里,然后用tree
命令显示当前目录结构。一些文件的大概作用我做了简要注释。接下来重点展开的文件,我标注了*号标记。
$ tree
.
├── HEAD //* 文件指向当前的分支,这个文件后面解释
├── config //项目特有的配置选项
├── description //仅供 GitWeb 程序使用,不作深究
├── hooks //客户端或服务端钩子脚本
│ ├── applypatch-msg.sample
│ ├── commit-msg.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
8 directories, 14 files
5. 了解git add
操作:模拟开发流程,在你的项目目录下创建一个新的文件,我这里以README.md
为例子。创建好之后在文件中随便写一点内容,然后执行git add .
操作,此时再进入.git
目录下执行tree
命令
$ tree
.
├── HEAD
├── config
├── description
├── hooks
│ ├── applypatch-msg.sample
│ ├── commit-msg.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
├── index
├── info
│ └── exclude
├── objects
│ ├── d0
│ │ └── d37ed92ba0227dc7c3b9a5cb2a8bfa6a04a6f1
│ ├── info
│ └── pack
└── refs
├── heads
└── tags
9 directories, 16 files
会发现在objects
目录下多出来了一些东西,这就是由我们刚刚的git add .
操作所生成的。那么这些文件代表什么呢。其实这个文件是Git为每份内容生成的一个文件,取得该内容与头信息的 SHA-1 校验和,创建以该校验和前两个字符为名称的子目录,并以 (校验和) 剩下 38 个字符为文件命名 (保存至子目录下)。
简单来说,就是git通过一种算法将我们提交的内容进行了计算,得到了一串数字,然后数字的前两位作为一个目录名,剩余的字符串作为文件名。
查看一下该文件的内容,可以通过 cat-file 命令。该命令是查看 Git 对象的瑞士军刀。传入 -p 参数可以让该命令输出数据内容的类型,注意完整的文件名是目录名+文件名:
$ git cat-file -p d0d37ed92ba0227dc7c3b9a5cb2a8bfa6a04a6f1
#GitTest
我们添加的内容就是存放在这个文件中了。
6. 了解git commit
: 在刚刚操作的基础上,执行git commit -m
操作,同样观察.git
目录结构, 我提交的描述是“。
$ tree
.
├── COMMIT_EDITMSG
├── HEAD
├── config
├── description
├── hooks
│ ├── applypatch-msg.sample
│ ├── commit-msg.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
├── index
├── info
│ └── exclude
├── logs
│ ├── HEAD
│ └── refs
│ └── heads
│ └── master
├── objects
│ ├── 5f
│ │ └── 3a2c5612b3687a8963fe76e46305b0c7ba5724
│ ├── cf
│ │ └── 0359c6428293e0b717f02292ef191ccc590592
│ ├── d0
│ │ └── d37ed92ba0227dc7c3b9a5cb2a8bfa6a04a6f1
│ ├── info
│ └── pack
└── refs
├── heads
│ └── master
└── tags
14 directories, 22 files
objects
目录下多了两个文件,我们一个个的来看:
$ git cat-file -p 5f3a2c5612b3687a8963fe76e46305b0c7ba5724
tree cf0359c6428293e0b717f02292ef191ccc590592
author liuyiyi <liuyiyi@lyydeMacBook-Pro.local> 1503989030 +0800
committer liuyiyi <liuyiyi@lyydeMacBook-Pro.local> 1503989030 +0800
first commit
这个文件的内容,就是我们git commit -m
中的描述信息,注意他有一个tree cf0359c6428293e0b717f02292ef191ccc590592
,这个是指向了这次提交时的项目根目录,什么意思看看就知道了:
$ git cat-file -p cf0359c6428293e0b717f02292ef191ccc590592
100644 blob d0d37ed92ba0227dc7c3b9a5cb2a8bfa6a04a6f1 GitTestREADME.md
我们进行commit时项目目录的文件存在状况就是只有一个文件GitTestREADME.md
,所以如果我们进行指定版本的代码回滚时,就会根据这个值来置回当时根目录中文件的存在状态。
剩下一个文件是什么的呢:
$ git cat-file -p cf0359c6428293e0b717f02292ef191ccc590592
100644 blob d0d37ed92ba0227dc7c3b9a5cb2a8bfa6a04a6f1 GitTestREADME.md
原来这就是上面我们在有关commit描述文件中发现的指向根目录的那个文件。
7. 继续添加新文件再执行一次git add.
和git commit -m
操作,一个命令指向完就观察一次.git
文件的变化状况, 我这里列出最终的结果状态, 过程就省略了:
$ tree
.
├── COMMIT_EDITMSG
├── HEAD
├── config
├── description
├── hooks
│ ├── applypatch-msg.sample
│ ├── commit-msg.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
├── index
├── info
│ └── exclude
├── logs
│ ├── HEAD
│ └── refs
│ └── heads
│ └── master
├── objects
│ ├── 58
│ │ └── 7a55b8f052d0a74bd592ffe7c8c5ff8ccc06c5
│ ├── 5f
│ │ └── 3a2c5612b3687a8963fe76e46305b0c7ba5724
│ ├── 92
│ │ └── a36d18afcce6bdda094c8360551051695fc842
│ ├── c9
│ │ └── c50f0b74a03f5be47d97770a2d11e23983d425
│ ├── cf
│ │ └── 0359c6428293e0b717f02292ef191ccc590592
│ ├── d0
│ │ └── d37ed92ba0227dc7c3b9a5cb2a8bfa6a04a6f1
│ ├── info
│ └── pack
└── refs
├── heads
│ └── master
└── tags
17 directories, 25 files
多出来三个文件,拿到文件内容,可以看到和之前第一次文件里存储的东西都是同样的,一个是git add
提交的内容,一个是git commit -m
的内容,一个是当前根目录。需要注意的是这个保存了git commit -m
内容的目录,它与第一次commit有一些差别:
$ git cat-file -p 92a36d18afcce6bdda094c8360551051695fc842
tree 587a55b8f052d0a74bd592ffe7c8c5ff8ccc06c5
parent 5f3a2c5612b3687a8963fe76e46305b0c7ba5724
author liuyiyi <liuyiyi@lyydeMacBook-Pro.local> 1503991626 +0800
committer liuyiyi <liuyiyi@lyydeMacBook-Pro.local> 1503991626 +0800
second commit
多了一个parent 5f3a2c5612b3687a8963fe76e46305b0c7ba5724
, 从字面就能理解是啥意思,指向它前一个commit,也可以理解为前一个节点, 可以求证一下:
$ git cat-file -p 5f3a2c5612b3687a8963fe76e46305b0c7ba5724
tree cf0359c6428293e0b717f02292ef191ccc590592
author liuyiyi <liuyiyi@lyydeMacBook-Pro.local> 1503989030 +0800
committer liuyiyi <liuyiyi@lyydeMacBook-Pro.local> 1503989030 +0800
first commit
由于第一次commit它是没有父节点的,所以没有parent, 而之后的commit都是有父节点的, 所以都会有parent这一行。
8. 回过头来讲讲HEAD, refs两个文件。HEAD众所周知是指向当前分支,但是它是怎么指向的呢,看看他的内容就知道了,要切记,HEAD本质上是一个文件!以上所有的东西都是文件!!
vim HEAD
ref: refs/heads/master
HEAD的内容是一个文件路径,所以其实不是说HEAD指向了当前分支,而是它指向了一个文件的路径.根据这个文件路径看看文件内容:
vim refs/heads/master
92a36d18afcce6bdda094c8360551051695fc842
这个路径下的文件内容,就是第二次commit的文件名称, 所以准确来说,这个文件指向了当前分支上的当前commit, 而HEAD文件内容是这个文件。如果文件进行回滚操作,refs/heads/master
文件中的值会随之改变,变为你回滚到的那个commit.
9. 创建一个新的分支develop
,观察.git
文件目录的变化:
└── refs
├── heads
│ ├── develop
│ └── master
└── tags
会发现refs/headers下面多了一个文件,名为develop,获取develop文件的内容,指向了master第二次commit内容的文件。
vim refs/heads/develop
92a36d18afcce6bdda094c8360551051695fc842
所以基于当前分支创建一个新的分支,其实本质上只是多了一个文件,而这个文件内容就是指向创建分之时最新的那个commit而已。创建分支的代价非常小。
10. 此时将分支切换到了develop
上,此时观察HEAD的变化,估计已经可以猜测到HEAD内容发生了怎么样的变化,文件内容将变refs/heads/develop
这个路径,也就是指向了当前分支develop
。