Git 内部原理--初探 .git
说到Git大家应该都非常熟悉,几乎每天都会用到它。在日常使用过程中,我们貌似并不需要关注其内部的原理,只需要记住那几个常用的命令,就可以说自己是会Git的人了。可是,事实真的是这样子的吗?今天我们就来聊聊那些不太被关注到的内部原理。
引言
首先我们要明白的一点就是,Git是一个内容寻址(content-addressable)文件系统,并在此之上提供了一个版本控制系统的用户界面。
还有一点需要明白的就是,我们日常使用的命令(checkout
、commit
)对用户更加友好,因此被称为高层(porcelain)命令
;还有一些早期设计的命令(hash-object
、write-tree
)被设计成能以UNIX命令行的风格连接在一起,抑或藉由脚本调用来实现功能,这些命令被称为底层(plumbing)命令
。这里,我们便通过底层命令来实现几个常见的高层命令,以达到初步了解Git内部原理的效果。
Git 仓库的初始化
在我们学习Git的时候,我们最开始接触到的一定是Git仓库的配置。在这里,我们也通过Git的初始化,来看看Git的初始化的时候都干了些啥。
frends-MacBook:GitTest frendguo$ git init
Initialized empty Git repository in /Users/frendguo/SourceCode/Demo/GitTest/.git/
frends-MacBook:GitTest frendguo$ ls -al
total 0
drwxr-xr-x 3 frendguo staff 96 Jan 13 22:51 .
drwxr-xr-x 4 frendguo staff 128 Jan 13 22:51 ..
drwxr-xr-x 9 frendguo staff 288 Jan 13 22:51 .git
当我们输入git init
后,Git会帮我们生成一个.git的文件夹。它的结构是这样子的:
.git/
├── config
├── description
├── HEAD
├── hooks
│ ├── applypatch-msg.sample
│ ├── commit-msg.sample
│ ├── post-update.sample
│ ├── pre-applypatch.sample
│ ├── pre-commit.sample
│ ├── prepare-commit-msg.sample
│ ├── pre-push.sample
│ ├── pre-rebase.sample
│ └── update.sample
├── info
│ └── exclude
├── objects
│ ├── info
│ └── pack
└── refs
| ├── heads
| └── tags
对于一个全新的git init
版本库,这将是你看到的默认结构。description 文件仅供 GitWeb 程序使用,我们无需关心。config
文件包含项目特有的配置选项。info
目录包含一个全局性排除(global exclude)文件,用以放置那些不希望被记录在 .gitignore
文件中的忽略模式(ignored patterns)。hooks
目录包含客户端或服务端的钩子脚本(hook scripts)。
HEAD
文件、(尚待创建的)index 文件
,和 objects 目录
、refs 目录
。这些条目是 Git 的核心组成部分。objects 目录
存储所有数据内容;refs 目录
存储指向数据(分支)的提交对象的指针;HEAD
文件指示目前被检出的分支;index
文件保存暂存区信息。
既然初始化仓库只是在本地创建几个文件/文件夹,那我们是否可以通过创建文件/文件夹来初始化Git仓库呢?答案是当然可以!
frends-MacBook:GitTest frendguo$ ls -al
total 0
drwxr-xr-x 2 frendguo staff 64 Jan 13 23:20 .
drwxr-xr-x 4 frendguo staff 128 Jan 13 22:51 ..
frends-MacBook:GitTest frendguo$ git status
fatal: not a git repository (or any of the parent directories): .git
当前GitTest
文件夹并不是一个Git仓库,在使用git status
查询Git仓库状态时,bash会提醒这不是一个Git仓库。
于是我们在这个文件夹下创建写文件/文件夹:
frends-MacBook:GitTest frendguo$ mkdir -p .git/objects .git/refs/heads/ .git/refs/tags
frends-MacBook:GitTest frendguo$ echo "ref:refs/heads/master" >> .git/HEAD
frends-MacBook:GitTest frendguo$ git status
On branch master
No commits yet
nothing to commit (create/copy files and use "git add" to track)
只是简单的创建几个文件/文件夹,就可以非常简单的创建一个Git仓库。所以,如果想要在本地备份一个版本库,就可以直接拷走.git 文件夹。
git add/commit 背后的那些事
创建好Git仓库后,我们便要开始向仓库提交一些东西了。待我们在工作区的内容编辑完成后,我们就需要将内容提交到暂存区了,对于平常的使用中,我们直接git add .
就可以将当前文件夹下所有的文件/文件夹全都提交到暂存区。那这条命令背后到底干了些啥呢?接下来我们便来探索下其背后的故事。
Git 对象
前面提到过Git是一个内容寻址系统
。也就是说Git的核心其实就是一个简单的键值对数据库,对于添加到Git仓库任意的对象,Git都有一个唯一的键来与之对应。这里可以通过底层命令hash-object
来演示效果。
frends-MacBook:GitTest frendguo$ echo "hello git" | git hash-object -w --stdin
8d0e41234f24b6da002d962a26c2495ea16a425f
其中参数-w
表示将对象写到Git的数据库当中。--stdin
表示接受标准输入,这里接收到的就是hello git
。对于返回值8d0e41234f24b6da002d962a26c2495ea16a425f
就是前文提到的键。
那么这个键是怎么计算得来的呢?—hash。它通过将头部信息(其中包含对象类型和对象大小)和原始信息拼接起来,然后计算HASH值,得到一个40位的序列。并将前两位作为文件夹,后面38位作为文件名将文件信息保存到Git仓库中。
上文中的hello git
保存到Git中如下所示:
frends-MacBook:GitTest frendguo$ find .git/objects
.git/objects
.git/objects/8d
.git/objects/8d/0e41234f24b6da002d962a26c2495ea16a425f
这里再介绍一个底层命令cat file
。我们可以通过这个命令从Git中取数据,比如我们要看下8d0e41234f24b6da002d962a26c2495ea16a425f
的文件类型,我们可以这么做:
frends-MacBook:GitTest frendguo$ git cat-file -t 8d0e41234f24b6da002d962a26c2495ea16a425f
blob
-t
表示查看其类型。blob
是Git中对象的一种类型,表示数据对象(BinaryLargeOBject)。与之对应的还有tree
(树对象,下文会提到)、commit
(提交对象,见下文)。
-p
表示需要查看该键对应的内容。如(其他用法可以通过man git-cat-file
来查看):
frends-MacBook:GitTest frendguo$ git cat-file -p 8d0e41234f24b6da002d962a26c2495ea16a425f
hello git
接下来,我们向数据库中添加本地已存在的文件:
frends-MacBook:GitTest frendguo$ echo "version 1" >> test.md
frends-MacBook:GitTest frendguo$ ls
test.md
frends-MacBook:GitTest frendguo$ git hash-object -w test.md
83baae61804e65cc73a7201a7252750c76066a30
修改test.md
文件并再次保存到Git中:
frends-MacBook:GitTest frendguo$ echo "version 2" > test.md
frends-MacBook:GitTest frendguo$ git hash-object -w test.md
1f7a7a472abf3dd9643fd615f6da379c4acb3e3a
查看objects/中的文件:
frends-MacBook:GitTest frendguo$ find .git/objects/ -type f
.git/objects//1f/7a7a472abf3dd9643fd615f6da379c4acb3e3a
.git/objects//83/baae61804e65cc73a7201a7252750c76066a30
.git/objects//8d/0e41234f24b6da002d962a26c2495ea16a425f
可以看到包括之前的hello git
,总共有三个文件保存到了Git仓库中。我们可以通过非常简单的方法将其恢复。
frends-MacBook:GitTest frendguo$ git cat-file -p 8d0e41234f24b6da002d962a26c2495ea16a425f > test.md
frends-MacBook:GitTest frendguo$ cat test.md
hello git
然而在这个简单的文件系统中,我们并没有保存文件名。那么Git是怎么保存文件名的呢?--答案就是树对象
。
树对象
接下来我们便来讨论讨论树对象。它不仅能保存文件名,还能将多个文件组织到一起。我们先来看看树对象怎么来创建吧~
通常,Git会根据某一时刻暂存区(存放在.git/index
中)所表示的状态来创建并记录一个对应的树对象,如此重复便能创建一系列的树对象。因而,在创建树对象之前,我们需要先将文件加入到暂存区。这里需要用到另一个底层命令update-index
,这个命令就是将文件加到暂存区中。
frends-MacBook:GitTest frendguo$ git ls-files --stag
frends-MacBook:GitTest frendguo$ git update-index --add --cacheinfo 10644 8d0e41234f24b6da002d962a26c2495ea16a425f hello
frends-MacBook:GitTest frendguo$ git ls-files --stag
100644 8d0e41234f24b6da002d962a26c2495ea16a425f 0 hello
其中,git ls-files --stag
是用来显示暂存区中的文件信息的。--add
是因为hello
文件不在暂存区中。--cacheinfo
是因为该内容是在Git仓库中,不在当前文件夹下。10644
在UNIX系统中用来表示该文件模式为普通文件。
这个时候,我们用git status
看看Git仓库的状态:
frends-MacBook:GitTest frendguo$ git status
On branch master
No commits yet
Changes to be committed:
(use "git rm --cached <file>..." to unstage)
new file: hello
Changes not staged for commit:
(use "git add/rm <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
deleted: hello
可以看到hello
文件已经加到暂存区中了。比较奇怪的是下面那个deleted: hello
。这个更改是在为加到暂存区的更改,因为本地路径中不存在该文件,而暂存区中存在该文件,所以Git认为是人为的将该文件删除了。我们可以通过检出(checkout)来将文件恢复到本地。
frends-MacBook:GitTest frendguo$ git checkout hello
frends-MacBook:GitTest frendguo$ ls -l
total 8
-rw-r--r-- 1 frendguo staff 10 Jan 14 00:41 hello
这个时候的状态就像我们使用了git add hello
一样。
frends-MacBook:GitTest frendguo$ git status
On branch master
No commits yet
Changes to be committed:
(use "git rm --cached <file>..." to unstage)
new file: hello
这里也可以联想到我们在将工作区的内容恢复到暂存区的内容的时候,我们也会使用检出(checkout)命令。(这个命令的内部实现,我们以后再探讨。)
嘿,别走了,我们这就创建树对象。这里需要用到write-tree
这个底层命令,它的作用就是将暂存区的内容写成一个树对象。
frends-MacBook:GitTest frendguo$ git ls-files --stag
100644 8d0e41234f24b6da002d962a26c2495ea16a425f 0 hello
frends-MacBook:GitTest frendguo$ git write-tree
66eea8c80abea0e9836aab458e48ab9a379186e5
frends-MacBook:GitTest frendguo$ git cat-file -t 66eea8c80abea0e9836aab458e48ab9a379186e5
tree
frends-MacBook:GitTest frendguo$ git cat-file -p 66eea8c80abea0e9836aab458e48ab9a379186e5
100644 blob 8d0e41234f24b6da002d962a26c2495ea16a425f hello
可以看到我们的树对象创建成功了,里面的内容就是暂存区的内容--一个hello文件。接下来我们创建一个新的树对象。
frends-MacBook:GitTest frendguo$ echo "version 1" > test.md
frends-MacBook:GitTest frendguo$ git update-index --add test.md
frends-MacBook:GitTest frendguo$ git ls-files --stag
100644 8d0e41234f24b6da002d962a26c2495ea16a425f 0 hello
100644 83baae61804e65cc73a7201a7252750c76066a30 0 test.md
frends-MacBook:GitTest frendguo$ git write-tree
3071bf0ff03f10445bb9c43e194ae990944006f4
frends-MacBook:GitTest frendguo$ git cat-file -p 3071bf0ff03f10445bb9c43e194ae990944006f4
100644 blob 8d0e41234f24b6da002d962a26c2495ea16a425f hello
100644 blob 83baae61804e65cc73a7201a7252750c76066a30 test.md
这里的树对象包含两个文件hello
和test.md
。现在我们将原来那个树对象添加到暂存区,可以通过read-tree
将树对象的内容读取到暂存区。
frends-MacBook:GitTest frendguo$ git read-tree --prefix=FirstTree \ 66eea8c80abea0e9836aab458e48ab9a379186e5
frends-MacBook:GitTest frendguo$ git ls-files
FirstTree/hello
hello
test.md
frends-MacBook:GitTest frendguo$ git write-tree
ac3c7527fcc566453b513c8706d9f36ba138980a
frends-MacBook:GitTest frendguo$ git cat-file -p ac3c7527fcc566453b513c8706d9f36ba138980a
040000 tree 66eea8c80abea0e9836aab458e48ab9a379186e5 FirstTree
100644 blob 8d0e41234f24b6da002d962a26c2495ea16a425f hello
100644 blob 83baae61804e65cc73a7201a7252750c76066a30 test.md
可以看到新的树对象包含了最开始我们创建的那个树对象和两个文件,如果我们把他们检出,是不是就能在工作目录看到根目录下有一个子目录和两个文件了。
frends-MacBook:GitTest frendguo$ ls
hello test.md
frends-MacBook:GitTest frendguo$ rm -rf hello test.md
frends-MacBook:GitTest frendguo$ ls -l
frends-MacBook:GitTest frendguo$ git checkout .
frends-MacBook:GitTest frendguo$ ls -l
total 16
drwxr-xr-x 3 frendguo staff 96 Jan 14 01:05 FirstTree
-rw-r--r-- 1 frendguo staff 10 Jan 14 01:05 hello
-rw-r--r-- 1 frendguo staff 10 Jan 14 01:05 test.md
果然是这样子的。是不是想起了我们使用的文件系统也是这种树形的结构呢。
提交对象
现在我们有三个树对象,分别表示了项目开发周期中不同时期的快照。如果我们想要重用这些快照,那么就必须记住三个树对象的SHA-1值。而且,我们并没有足够的信息来表明是谁,什么时候创建的快照以及为什么会有这个快照。因此,大佬们就想出来了一个提交对象
用来保存这些信息。
我们先用commit-tree
命令来创建一个提交对象,这个过程中,我们必须要指定树对象的SHA-1值,以及提交的父对象(如果存在的话)。
frends-MacBook:GitTest frendguo$ echo "first commit" | git commit-tree \ 66eea8c80abea0e9836aab458e48ab9a379186e5
1417b00016aa48b2e24518c5e1f8737b66b6a993
frends-MacBook:GitTest frendguo$ git cat-file -t 1417b00016aa48b2e24518c5e1f8737b66b6a993
commit
再来看看这个提交对象里面有些什么内容:
frends-MacBook:GitTest frendguo$ git cat-file -p 1417b00016aa48b2e24518c5e1f8737b66b6a993
tree 66eea8c80abea0e9836aab458e48ab9a379186e5
author frendguo <frendguo@live.cn> 1547399734 +0800
committer frendguo <frendguo@live.cn> 1547399734 +0800
first commit
通过查看提交对象的内容,我们可以知道,提交对象的格式:它先指定一个顶层的树对象,代表当前项目的快照;然后是作者/提交者的信息(根据user.name
和user.email
来设置);留空一行,就是提交的注释了(表示为什么会有这个快照)。(这里需要提到的是提交者和作者的信息,它们大多数情况下都是一样的,但在一些情况下(比如:cherry-pick),为了保证版权,就需要保留作者的信息。)
接下来我们再根据其他两个树对象创建两个提交。
frends-MacBook:GitTest frendguo$ echo "second commit" | git commit-tree 3071bf0ff03f10445bb9c43e194ae990944006f4 -p 1417b00016aa48b2e24518c5e1f8737b66b6a993
088bb9b3d9aa4c48e28a5c27672bb75e110fba64
frends-MacBook:GitTest frendguo$ echo "third commit" | git commit-tree ac3c7527fcc566453b513c8706d9f36ba138980a -p 088bb9b3d9aa4c48e28a5c27672bb75e110fba64
8a94cd783dfb4fdf45e836f7a0ae292f49593606
这个时候我们就可以用git log --stat
来查看Git的提交历史了。这里的--stat
是表示查看与当前HEAD
不一致的路径。
frends-MacBook:GitTest frendguo$ git log --stat 8a94cd783dfb4fdf45e836f7a0ae292f49593606
commit 8a94cd783dfb4fdf45e836f7a0ae292f49593606
Author: frendguo <frendguo@live.cn>
Date: Mon Jan 14 01:25:16 2019 +0800
third commit
FirstTree/hello | 1 +
1 file changed, 1 insertion(+)
commit 088bb9b3d9aa4c48e28a5c27672bb75e110fba64
Author: frendguo <frendguo@live.cn>
Date: Mon Jan 14 01:24:07 2019 +0800
second commit
test.md | 1 +
1 file changed, 1 insertion(+)
commit 1417b00016aa48b2e24518c5e1f8737b66b6a993
Author: frendguo <frendguo@live.cn>
Date: Mon Jan 14 01:15:34 2019 +0800
first commit
hello | 1 +
1 file changed, 1 insertion(+)
截止到这里,我们算是用底层命令基本完成了git add
和git commit
的功能。
Git 引用
之所以说基本完成了,那是因为我们并不能像高层命令那样提交后,直接使用git log
就能查看提交日志。
这里不得不提到的就是HEAD
。就像前文说到的那样,HEAD
表示现在被检出的分支。
frends-MacBook:GitTest frendguo$ cat .git/HEAD
ref:refs/heads/master
frends-MacBook:GitTest frendguo$ cat .git/refs/heads/master
cat: .git/refs/heads/master: No such file or directory
尝试将最新的提交对象的SHA-1值给refs/heads/master
?
frends-MacBook:GitTest frendguo$ echo "8a94cd783dfb4fdf45e836f7a0ae292f49593606" > .git/refs/heads/master
frends-MacBook:GitTest frendguo$ git log
commit 8a94cd783dfb4fdf45e836f7a0ae292f49593606 (HEAD -> master)
Author: frendguo <frendguo@live.cn>
Date: Mon Jan 14 01:25:16 2019 +0800
third commit
commit 088bb9b3d9aa4c48e28a5c27672bb75e110fba64
Author: frendguo <frendguo@live.cn>
Date: Mon Jan 14 01:24:07 2019 +0800
second commit
commit 1417b00016aa48b2e24518c5e1f8737b66b6a993
Author: frendguo <frendguo@live.cn>
Date: Mon Jan 14 01:15:34 2019 +0800
first commit
于是我们把最后一步走完了,现在就跟高层命令一致了。
这里强烈不建议直接修改引用文件。如果想要修改,请使用update-ref
命令来完成修改。
frends-MacBook:GitTest frendguo$ git update-ref refs/heads/master 8a94cd783dfb4fdf45e836f7a0ae292f49593606
通过修改master
分支,可以看到分支的本质其实就是一个引用(或者说一个指针,一个提交对象的键值)。所以当我们在使用git branch
命令的时候,其内部其实就是通过git update-ref
来实现的。而标签引用和分支有点异曲同工之妙,本文就不再赘述了。有兴趣的可以自己探索。
总结
到这里,初探.git就结束了。本文中通过使用底层命令来实现几个简单的高层命令的功能来探索Git的内部原理。
参考
- Pro Git
Git 内部原理--初探 .git的更多相关文章
- Git 内部原理之 Git 对象哈希
在上一篇文章中,将了数据对象.树对象和提交对象三种Git对象,每种对象会计算出一个hash值.那么,Git是如何计算出Git对象的hash值?本文的内容就是来解答这个问题. Git对象的hash方法 ...
- Git 内部原理 - (3) Git 引用 (4)包文件
Git 引用 我们可以借助类似于 git log 1a410e 这样的命令来浏览完整的提交历史,但为了能遍历那段历史从而找到所有相关对象,你仍须记住 1a410e 是最后一个提交. 我们需要一个文件来 ...
- Git详解之九:Git内部原理
Git 内部原理 不管你是从前面的章节直接跳到了本章,还是读完了其余各章一直到这,你都将在本章见识 Git 的内部工作原理和实现方式.我个人发现学习这些内容对于理解 Git 的用处和强大是非常重要的, ...
- git内部原理
Git 内部原理 无论是从之前的章节直接跳到本章,还是读完了其余章节一直到这——你都将在本章见识到 Git 的内部工作原理 和实现方式. 我们发现学习这部分内容对于理解 Git 的用途和强大至关重要. ...
- Git详解之九 Git内部原理
以下内容转载自:http://www.open-open.com/lib/view/open1328070620202.html Git 内部原理 不管你是从前面的章节直接跳到了本章,还是读完了其余各 ...
- git内部原理-第一篇
本人计划写一些关于<git内部原理>的文章 计划每周一篇
- Git 内部原理 - (7)维护与数据恢复 (8) 环境变量 (9)总结
维护与数据恢复 有的时候,你需要对仓库进行清理 - 使它的结构变得更紧凑,或是对导入的仓库进行清理,或是恢复丢失的内容. 这个小节将会介绍这些情况中的一部分. 维护 Git 会不定时地自动运行一个叫做 ...
- Git内部原理(1)
Git本质上是一套内容寻址文件系统,在此之上提供了VCS的用户界面. Git底层命令(plumbing) vs 高层命令(porcelain) Git的高层命令包括checkout.branch.re ...
- Git 内部原理 - (1)底层命令和高层命令 (2Git 对象
文章摘选自git官网,这里复制下来表示我已阅读并学习过一次这些内容: 无论是从之前的章节直接跳到本章,还是读完了其余章节一直到这——你都将在本章见识到 Git 的内部工作原理和实现方式. 我们发现学习 ...
随机推荐
- putty-gns3
hcl-cloud用的就是这个putty http://forum.gns3.net/topic5016.html File comment: Compiled PuTTY 0.62 for wind ...
- bc-win32-power-echo-vim-not-work
http://gnuwin32.sourceforge.net/packages.html linux ok, but win32 not ok [root@130-255-8-100 ~]# ech ...
- Cocos2d中的Menu使用
学习cocos2d-x中的菜单主要须要了解:菜单(CCMenu)和菜单项(CCMenuItem)以及CCMenuItem的详细子类. a. 以下来学习一下相关的类. 1. CCMenu 菜单,是CCL ...
- ios开发之核心动画四:核心动画-Core Animation--CABasicAnimation基础核心动画
#import "ViewController.h" @interface ViewController () @property (weak, nonatomic) IBOutl ...
- Redis主从高可用缓存
nopCommerce 3.9 大波浪系列 之 使用Redis主从高可用缓存 一.概述 nop支持Redis作为缓存,Redis出众的性能在企业中得到了广泛的应用.Redis支持主从复制,HA,集 ...
- Android菜鸟的成长笔记(27)——SurfaceView的使用
前面有关自定义View中进行了绘图,但View的绘图机制存在如下缺陷: 1.View缺乏双缓冲机制. 2.当程序需要更新View上的图像时,程序必须重绘View上显示的整张图片. 3.新线程无法直接更 ...
- 如何去掉windows2003的自动锁定(每离开一会都会出现这个界面,不想让它出现)
http://zhidao.baidu.com/link?url=SOCv57C-hX_3f0Xl0J0RFIVXpowXk73zdQd2B-wMUzYOm5E_N397bw_UkX4uLPlAiWQ ...
- 如何使用Name对象,包括WorkspaceNames和DatasetNames
转自chanyinhelv原文 如何使用Name对象,包括WorkspaceNames和DatasetNames 第一原文链接 该博主还有很多有关arcgis二次开发的不错的文章. 如何使用Name对 ...
- Opencv 使用Stitcher类图像拼接生成全景图像
Opencv中自带的Stitcher类可以实现全景图像,效果不错.下边的例子是Opencv Samples中的stitching.cpp的简化,源文件可以在这个路径里找到: \opencv\sourc ...
- eclipse配置本地服务
1.下载安装eclipse 2.下载tomcat文件,并解压 3.下载tomcat插件 com.sysdeo.eclipse.tomcat_3.3.0 将com.sysdeo.eclipse.tomc ...