Git独特之处

Git是一个分布式版本控制系统,首先分布式意味着Git不仅仅在服务端有远程仓库,同时会在本地也保留一个完整的本地仓库(.git/文件夹),这种分布式让Git拥有下面几个特点:

1.直接记录快照,而非差异比较

在文件存储方面,Git有别于其他版本控制系统(如CVS、Subversion),Git本身关心文件的整体性是否有改变,而不是文件内容的差异。

2.近乎所有操作都是本地执行

在 Git 中的绝大多数操作都只需要访问本地文件和资源,一般不需要来自网络上其它计算机的信息。 如果你习惯于所有操作都有网络延时开销的集中式版本控制系统,Git 在这方面会让你感到速度之神赐给了 Git 超凡的能量。 因为你在本地磁盘上就有项目的完整历史,所以大部分操作看起来瞬间完成。

3.Git 保证完整性

Git 中所有数据在存储前都计算校验和,然后以校验和来引用。 这意味着不可能在 Git 不知情时更改任何文件内容或目录内容。 这个功能建构在 Git 底层,是构成 Git 哲学不可或缺的部分。 若你在传送过程中丢失信息或损坏文件,Git 就能发现。

4.Git 一般只添加数据

你执行的 Git 操作,几乎只往 Git 数据库中增加数据。 很难让 Git 执行任何不可逆操作,或者让它以任何方式清除数据。 同别的 VCS 一样,未提交更新时有可能丢失或弄乱修改的内容;但是一旦你提交快照到 Git 中,就难以再丢失数据,特别是如果你定期的推送数据库到其它仓库的话。

内部原理

这里讨论内部原理主要是讨论Git如何对文件进行存储的,就是上面第一点提到的,“直接记录快照,而非差异比较”

CVS、Subversion这类系统将保存的信息看作是一组基本文件和每个文件随时间逐步累积的差异,如下图所示:

这种记录文件差异的方式占用存储空间小,每次提交只需要保存差异部分,但是版本切换的时候会比较耗时。

Git 不按照以上方式对待或保存数据。 反之,Git 更像是把数据看作是对小型文件系统的一组快照。 每次你提交更新,或在 Git 中保存项目状态时,它主要对当时的全部文件制作一个快照并保存这个快照的索引。 为了高效,如果文件没有修改,Git 不再重新存储该文件,而是只保留一个链接指向之前存储的文件。 Git 对待数据更像是一个 快照流,如下图所示:

实践

git目录介绍

通过git init命令在本地初始化一个git仓库,查看.git/文件夹的结构:

首先简要介绍各个文件/目录的左右:

  • HEAD 存储HEAD所指向的分支;
  • config 保存项目的特有配置信息,对应git-config命令;
  • description 该文件仅供 GitWeb 程序使用,我们无需关心;
  • hooks 包含客户端或服务端的钩子脚本,暂不关心;
  • info 目录包含一个全局性排除(global exclude)文件;
  • objects 存储所有数据内容;
  • refs 存储指向数据(分支)的提交对象的指针。

保存数据

创建两个新文件file1.txt和file2.txt

echo 'file1' > file1.txt
echo 'file2' > file2.txt

首先介绍一个新的命令:git hash-object -w

-w 选项指示 hash-object 命令存储数据对象;若不指定此选项,则该命令仅返回对应的键值。该命令输出一个长度为 40 个字符的校验和。 这是一个 SHA-1 哈希值——一个将待存储的数据外加一个头部信息(header)一起做 SHA-1 校验运算而得的校验和。

通过该命令存储file1.txt和file2.txt:

$ git hash-object -w file1.txt
e2129701f1a4d54dc44f03c93bca0a2aec7c5449 $ git hash-object -w file2.txt
6c493ff740f9380390d5c9ddef4af18697ac9375

这样就把这两个文件保存到git仓库中了,这时候再看看objects文件夹,会发现多出了两个文件夹:



可以看到这两个文件分别以校验合的前2个字符作为目录,后38个字符作为文件名存储,同时并对文件做了压缩,那么要怎么验证这个文件就是我们之前保存的呢?

这里要介绍另一个命令:git cat-file -p 校验和

该命令用于查看git压缩后文件的内容:

$ git cat-file -p e2129701f1a4d54dc44f03c93bca0a2aec7c5449
file1

没错,就是我们之前保存的文件。

从根本上来讲,Git是一个内容寻址的文件系统,其次才是一个版本控制系统。所谓“内容寻址的文件系统”,意思是根据文件内容的hash码来定位文件。这就意味着同样内容的文件,在这个文件系统中会指向同一个位置,不会重复存储。

Git对象

Git常见对象包含三种:数据对象、树对象、提交对象

  • 数据对象(blob):Git将每个纳入版本控制的文件定义为一个数据对象;
  • 树对象(tree):纳入版本控制中的文件夹或者提交快照;
  • 提交对象(commit):每次提交commit对应的记录;

现在我们将上面创建的两个文件提交到本地master分支:

$ git add file1.txt file2.txt
$ git commit -m "add file1 file2"

现在我们通过git log 来查看此次提交的信息:

可以看到提交的commit对象的校验和是:

c0d0c95a9fc80b0dc461abc2312ab8777453e2e0

我们可以用git cat-file -t命令来查看其对象类型:



可以看到这里确实是一个commit对象。

接着我们用git cat-file -p来查看该commit的内容:



我们可以看到commit对象包含了一条tree对象记录,提交人的信息和备注。

紧接着我们继续用git cat-file -p查看这条tree的内容:



可以看到,tree中保存着当前commit中的所有文件的引用,同时包含了文件名的映射。

其中文件模式为 100644,表明这是一个普通文件。 其他选择包括:100755,表示一个可执行文件;120000,表示一个符号链接。

到这里我们可以知道一个commit的完整引用关系如下图所示

接着修改file1.txt再提交一个commit:

git log查看最新一条commit的校验和:

看到是:

fb6c9721e4402987f5615960f26200fd648e1802

这里我们再用git cat-file -p来查看其内容:

发现这条commit多了一个parent记录,该parent就是上一个commit的引用,所以,完整的commit关系图如下所示:

commit对象除了第一个对象,其他对象都会在内部指向其上一个的commit对象,形成一条链,这条链就是我们常说的分支。可以看到上图中的file2.txt的校验和是相同的,我们没有对它进行修改,它们指向的是同一个文件,而file1.txt的校验和是不同的,哪怕只是修改了一个字节,这都会保存为2个独立的文件。

手动构建提交

由前面的知识我们知道了commit对象和tree对象的关系,那么现在我们通过手动构建tree对象和commit对象来实现一个提交。

构建tree对象

tree对象它能解决文件名保存的问题,也允许我们将多个文件组织到一起。 Git 以一种类似于 UNIX 文件系统的方式存储内容,但作了些许简化,所有内容均以树对象和数据对象的形式存储,如下图所示:

我们先修改一下file1.txt:

$ echo 'version 2' >> file1.txt

同时新建一个文件new.txt

$ echo "new file" >> new.txt

这里介绍一个底层命令:git update-index

该命令可以将文件加入暂存区,相当于使用git add命令:

这个时候用git status查看发现file1.txt已经加入到暂存区了:

现在我们将new.txt也用同样的方法加入到暂存区,不过由于new.txt没有纳入版本控制,这里需要--add参数:

接着我们要创建一个tree对象,可以通过 write-tree 命令将暂存区内容写入一个树对象,如果某个树对象此前并不存在的话,当调用 write-tree 命令时,它会根据当前暂存区状态自动创建一个新的树对象:

这里返回了创建的tree对象,我们用git cat-file 来查看其内容:

现在我们要创建一个commit对象,和这个tree对象关联起来,可以通过调用 commit-tree 命令创建一个提交对象,为此需要指定一个树对象的 SHA-1 值,以及该提交的父提交对象(如果有的话):

关联的tree对象的SHA-1值 = 7b4959adb1aa1b8daeb40a874bcbce114168ee70

父commit对象的SHA-1值 = fb6c9721e4402987f5615960f26200fd648e1802

现在commit对象就已经创建好了,我们可以用git cat-file来查看其内容:

但这时时候用git log来查看,会发现并没有出现此commit,这是因为我们当前分支(HEAD)指向并没有改变,这里我们需要将分支往后移动一个commit就可以了,有下面几种方法:

  • git merge 将新创建的commit,merge到当前分支

  • 修改HEAD的指向:

    首先查看./git/HEAD的内容:

    可以看到HEAD指向的是refs/heads/master这个分支,我们继续查看这个文件的内容:



    这里名指定了当前所在分支,只需要把之前创建的commit对象的HASH-1替换到这里即可。

其实上面两个方法最终所做的事情是一样的。

包文件

这里就有一个问题?

如果一个文件是100MB,然后对它进行了一个小小的修改,是不是在本地就会出现另一个100MB的文件?

确实是,这就是git快照的存储方式,每次提交都会保存一个完整的文件,这也是git分支操作能如此快速的原因之一。那么如果 Git 只完整保存其中一个,再保存另一个对象与之前版本的差异内容,岂不更好?

事实上 Git 可以那样做。 Git 最初向磁盘中存储对象时所使用的格式被称为“松散(loose)”对象格式。 但是,Git 会时不时地将多个这些对象打包成一个称为“包文件(packfile)”的二进制文件,以节省空间和提高效率。 当版本库中有太多的松散对象,或者你手动执行 git gc 命令,或者你向远程服务器执行推送时,Git 都会这样做。 要看到打包过程,你可以手动执行 git gc 命令让 Git 对对象进行打包:

这个时候再查看 objects 目录,你会发现大部分的对象都不见了,与此同时出现了一对新文件:

我们可以看到生成了.idx(索引)和.pack(包)文件,包文件包含了刚才从文件系统中移除的所有对象的内容。 索引文件包含了包文件的偏移信息,我们通过索引文件就可以快速定位任意一个指定对象。

Git 打包对象时,会查找命名及大小相近的文件,并只保存文件不同版本之间的差异内容。 你可以查看包文件,观察它是如何节省空间的。 git verify-pack 这个底层命令可以让你查看已打包的内容:

总结

理解Git的内部存储主要是理解commit、tree和blob三者之间的关系,通过对Git存储原理的学习,也可以让我们在日常工作中更好地使用Git。Git是一个很强大的版本控制系统,还有很多很多的功能,需要我们在使用过程中去慢慢学习。

引用:

Git-内部原理-底层命令和高层命令

Git内部原理浅析的更多相关文章

  1. Git详解之九:Git内部原理

    Git 内部原理 不管你是从前面的章节直接跳到了本章,还是读完了其余各章一直到这,你都将在本章见识 Git 的内部工作原理和实现方式.我个人发现学习这些内容对于理解 Git 的用处和强大是非常重要的, ...

  2. git内部原理

    Git 内部原理 无论是从之前的章节直接跳到本章,还是读完了其余章节一直到这——你都将在本章见识到 Git 的内部工作原理 和实现方式. 我们发现学习这部分内容对于理解 Git 的用途和强大至关重要. ...

  3. Git详解之九 Git内部原理

    以下内容转载自:http://www.open-open.com/lib/view/open1328070620202.html Git 内部原理 不管你是从前面的章节直接跳到了本章,还是读完了其余各 ...

  4. git内部原理-第一篇

    本人计划写一些关于<git内部原理>的文章 计划每周一篇

  5. Git 内部原理 - (7)维护与数据恢复 (8) 环境变量 (9)总结

    维护与数据恢复 有的时候,你需要对仓库进行清理 - 使它的结构变得更紧凑,或是对导入的仓库进行清理,或是恢复丢失的内容. 这个小节将会介绍这些情况中的一部分. 维护 Git 会不定时地自动运行一个叫做 ...

  6. Git 内部原理--初探 .git

    说到Git大家应该都非常熟悉,几乎每天都会用到它.在日常使用过程中,我们貌似并不需要关注其内部的原理,只需要记住那几个常用的命令,就可以说自己是会Git的人了.可是,事实真的是这样子的吗?今天我们就来 ...

  7. Git内部原理(1)

    Git本质上是一套内容寻址文件系统,在此之上提供了VCS的用户界面. Git底层命令(plumbing) vs 高层命令(porcelain) Git的高层命令包括checkout.branch.re ...

  8. Git 内部原理 - (1)底层命令和高层命令 (2Git 对象

    文章摘选自git官网,这里复制下来表示我已阅读并学习过一次这些内容: 无论是从之前的章节直接跳到本章,还是读完了其余章节一直到这——你都将在本章见识到 Git 的内部工作原理和实现方式. 我们发现学习 ...

  9. Git内部原理探索

    目录 前言 Git分区 .git版本库里的文件/目录是干什么的 Git是如何存储文件信息的 当我们执行git add.git commit时,Git背后做了什么 Git分支的本质是什么 HEAD引用 ...

随机推荐

  1. Linux中vim编辑命令

    vim 功能 : 一个强大的文本编辑器   语法格式 :vim [ 选项 ] / 路径 / 文本文件名 命令格式: vi [ 选项 ] [ 文件名 ]   +num 打开某个文件直接跳转到 num 行 ...

  2. python 连接数据库,查询结果写入数据到excel

    使用Python链接数据库查询数据,并将查询结果写入到Excel中,实现方法上主要有两步,第一,查数据,第二,写Excel. 一.导入需要的包 import time import xlwt from ...

  3. canopy聚类算法的MATLAB程序

    canopy聚类算法的MATLAB程序 凯鲁嘎吉 - 博客园 http://www.cnblogs.com/kailugaji/ 1. canopy聚类算法简介 Canopy聚类算法是一个将对象分组到 ...

  4. 6.3 使用Spark SQL读写数据库

    Spark SQL可以支持Parquet.JSON.Hive等数据源,并且可以通过JDBC连接外部数据源 一.通过JDBC连接数据库 1.准备工作 ubuntu安装mysql教程 在Linux中启动M ...

  5. golang数据结构和算法之CircularBuffer环形缓冲队列

    慢慢练语法和思路, 想说的都在代码及注释里. CircularBuffer package CircularBuffer const arraySize = 10 type CircularBuffe ...

  6. No implementation for org.apache.maven.model.path.PathTranslator was bound.

    2019-12-17 10:19:19,884 [ 688476] INFO - #org.jetbrains.idea.maven - org.apache.maven.model.resoluti ...

  7. jacoco统计自动化代码覆盖率

    jacoco统计自动化代码覆盖率 1. 简介 1.1. 什么是Jacoco Jacoco是一个开源的代码覆盖率工具,可以嵌入到Ant .Maven中,并提供了EclEmma Eclipse插件,也可以 ...

  8. getpatch

    import time import os import math import sys import os,os.path,shutil import numpy as np import cv2 ...

  9. MySQL学习笔记2——DML

    DML(数据操作语言,它是对表记录的操作(增,删,改)!) 1.插入数据 *INSERT INTO 表名(列名1,列名2,...) VALUES(列值1,列值2,...); >在表名后给出要插入 ...

  10. luoguP2597 [ZJOI2012]灾难

    题意 这题思路好奇怪啊 见到有向无环图显然是要拓朴排序,不妨按照被吃向吃连边,那么\(x\)灭绝当且仅当x的入点都灭绝,于是考虑怎样x的入点都灭绝 比如4号节点,它灭绝当且仅当2和3灭绝,2和3灭绝当 ...