Git - 1 工作原理

前情概要:Git - 0 Linus再封神

Git可以管理本地仓库版本与远程仓库版本.
这篇文章,先讲本地仓库的原理, 远程仓库跟本地仓库的原理是一样的.

0010 Git的三种状态与三个区

三种状态

Git有三种状态,在你于某个文件夹中执行git init在此文件夹创建了仓库之后.
此文件夹下的文件,就只有三个状态了:

  • modified(已修改)-已经修改了文件,但没有保存到数据库中.
  • staged(已暂存)-对一个已修改文件的当前版本做了标记,使之包括在下次提交的快照中.
  • committed(已提交)-数据已经安全的存放在本地数据库中.

什么意思,接下来就会详细解释.保证任何人都能听得懂.

三个区

由上面三种状态,我们就会知道,Git项目会有三个区(或者阶段):
工作区,暂存区仓库区(.git目录).

三个区

  • Git 仓库:这个就是保存各种文件版本的数据库,可以向这个数据库中拉取各个文件版本或把更新后的文件推入数据库进行记录。这是 Git 用来保存项目的元数据对象数据库的地方,是 Git 最重要的部分,从其他计算机克隆仓库时,拷贝的就是这里的数据。已经推入到这个数据库中的文件对应的状态是 已提交 (committed) 。

Git仓库就是项目目录下隐藏的.git文件夹,是执行git commit之后版本代码存放的地方.

  • 暂存区域:这个区域用来存储对当前已修改过并且作了版本标记的文件,在同一段时间内位于暂存区尚未提交的所有文件都属于同一个当前的版本,这些标记使得对应文件被包含在下次提交的快照中。这个区域是一个文件,保存了下次将提交的文件列表信息,一般位于 Git 仓库目录中。在这个区域的文件状态是 已暂存 (staged) 。

暂存区就是执行git add之后,代码存放的地方.

  • 工作目录:这个区域就是开发人员写代码的地方,对于已经修改并保存的文件,都会存储在这个区域,等待转移到暂存区然后提交。它是对项目的某个版本独立提取出来的内容。那些从 Git 仓库的压缩数据库中提取出来的文件,就是放在这个区域所在的磁盘上供你使用或修改。在这个区域的文件状态是 已修改 (modified) 。

就是项目文件夹,你写代码的地方.

Git 工作三部曲

  • 在工作目录修改文件;- 写代码
  • 将修改的文件对应的文件快照上传到暂存区。- git add
  • 提交更新,找到暂存区域的文件,将快照永久性存储到 Git 仓库目录。- git commit

完整的Git工作流程:
image

Git文件状态变化的生命周期

版本控制就是对文件的版本控制,要对文件进行修改、提交等操作,首先要知道文件当前在什么状态,不然可能会提交了现在还不想提交的文件,或者要提交的文件没提交上。

工作目录中的每一个文件都不外乎这两种状态:已追踪(tracked)和未追踪(untracked)。

  1. 已追踪(tracked):已追踪的文件指的是已经被纳入到版本控制的文件。已追踪的这些文件又可能处于已修改(modified),未修改(unmodified),已暂存(staged)的状态。

当你克隆了一个远程仓库到本地的时候,工作目录中所有的文件都是已追踪的文件,而且都是处于未修改的状态。

  1. 未追踪(untracked):此文件在项目文件夹中, 但并没有加入到git库.未追踪的文件表示不被纳入版本控制的文件,除了已追踪的文件之外所有的文件都是未追踪文件。例如当你克隆了一个项目到本地之后,你在本地的项目中新添加了一个文件,那么这个文件就是未追踪的状态。

image

  • 未追踪的文件(Untracked)在文件夹中, 但并没有加入到git库, 不参与版本控制. 通过git add 状态变为Staged.
  • 未修改的文件(Unmodified)文件已经入库, 未修改, 即版本库中的文件快照内容与文件夹中完全一致. 这种类型的文件有两种去处, 如果它被修改, 而变为Modified. 如果使用git rm移出版本库, 则成为Untracked文件.
  • 已修改(modified)的文件仅仅是修改, 并没有进行其他的操作. 这个文件也有两个去处, 通过git add可进入暂存staged状态, 使用git checkout 则丢弃修改过, 返回到unmodify状态, 这个git checkout即从库中取出文件, 覆盖当前修改!
  • 已暂存的文件(staged) 执行git commit则将修改同步到库中, 这时库中的文件和本地文件又变为一致, 文件为Unmodify状态. 执行git reset HEAD filename取消暂存, 文件状态为Modified

这里提前放上一张工作流的图片,接下来会解释
image

image

0011 Git操作及.git内容变化

涉及到的命令:

  • git init 创建(初始化)新的Git代码仓库.
  • git add 添加文件到暂存区
  • git commit 把暂存区内容提交到仓库.
  • git status 显示有变更的文件状态
  • git log 显示当前分支的版本历史

工作日常用的整理在这里了:Git常用命令清单 这么多命令记不住,记录下来用的时候查就行了.

创建新本地仓库

$ mkdir learnGit
$ cd learnGit
$ git init

创建完后,会发现文件夹里会有个隐藏的.git文件夹,进去看看.

$ ls -l .git/
 branches/
 config
 description
 HEAD
 hooks/
 info/
 objects/
 refs/

不用关心的:

description文件仅供 GitWeb 程序使用,我们无需关心.
config 文件包含项目特有的配置选项.
info 目录包含一个全局性排除(global exclude)文件, 用以放置那些不希望被记录在 .gitignore
文件中的忽略模式(ignored patterns).
hooks 目录包含客户端或服务端的钩子脚本(hook scripts)暂时用不到不管.

剩下的四个部分非常重要:
HEAD 文件(尚待创建的)index 文件,和 objects 目录refs 目录
它们都是Git 的核心组成部分。

  • objects 目录存储所有数据内容;
  • refs 目录存储指向数据(分支、远程仓库和标签等)的提交对象的指针;
  • HEAD 文件指向目前被检出的分支;
  • index 文件保存暂存区信息。

我们将详细地逐一检视这四部分,来理解 Git 是如何运转的。

  • 刚刚init之后,objects下两个文件夹 info, pack都是空的.
  • refs下两个文件夹 heads, tags也都是空的.
  • HEAD文件中的内容是 ref: refs/heads/master 意思是当前分支是在master.

操作之前,复习复习一下,三个区.
image

文件添加到暂存区

我们可以用git ls-files -s来看index文件中的内容

我在这个新仓库里写了两个文件, Firstfile.c 和 ReadME.md.

在第1章已经用git status看过了,再看一遍.
tony@ubuntu:~/learnGit$ git status 

位于分支 master
尚无提交
未跟踪的文件:
  (使用 "git add <文件>..." 以包含要提交的内容)
	Firstfile.c
	ReadME.md
提交为空,但是存在尚未跟踪的文件(使用 "git add" 建立跟踪)

然后执行:
$ git add .  - 把所有文件添加到暂存区.

tony@ubuntu:~/learnGit$ git status
位于分支 master
尚无提交
要提交的变更:
  (使用 "git rm --cached <文件>..." 以取消暂存)
	新文件:   Firstfile.c
	新文件:   ReadME.md

未跟踪的文件 这几个字消失了,再看看.git里面有什么变化.

branches/
config
description
HEAD
hooks/
index
info/
objects/
refs/

变化:

  • index - 暂存区索引文件出现.且objects下多出两个文件夹.
  • objects/77/
  • objects/f5/
  • objects多出的两个文件夹下各有一个文件.
ls -lF ./objects/f5/
总用量 4
-r--r--r-- 1 tony tony 60 10月 17 22:46 5dedc2c1c08962615aababc2ff08571a2c2f7f

ls -lF ./objects/77/
总用量 4
-r--r--r-- 1 tony tony 136 10月 17 22:46 fd941d361f3f486002a8f569a124466e794d38
  • 我们来看一下暂存区index的变化:
tony@ubuntu:~/learnGit$ git ls-files -s 
100644 77fd941d361f3f486002a8f569a124466e794d38 0	Firstfile.c
100644 f55dedc2c1c08962615aababc2ff08571a2c2f7f 0	ReadME.md

可以看到暂存区index里 存放了文件的hash(下章会讲)跟文件名.

将暂存区内容commit提交到仓库

  • 第一次commit为根提交:
tony@ubuntu:~/learnGit$ git commit -m "first commit"
[master (根提交) f8f9cf4] first commit
 2 files changed, 13 insertions(+)
 create mode 100644 Firstfile.c
 create mode 100644 ReadME.md

tony@ubuntu:~/learnGit$ git status
位于分支 master
无文件要提交,干净的工作区

看下log:
tony@ubuntu:~/learnGit$ git log 
commit f8f9cf4b3c915992c04577073db03c58cac5806a (HEAD -> master)
Author: Tony <xxx@qq.com>
Date:   Mon Oct 17 23:05:20 2022 +0800

    first commit

  • git commit执行之后 .git仓库中文件的变化
  • 多了个log文件夹.
├── logs
│ ├── HEAD
│ └── refs
│     └── heads
│        └── master

目前里面 HEAD 和 /refs/heads/master 中的内容 一模一样,都是第一次提交的信息.
tony@ubuntu:~/learnGit$ cat .git/logs/refs/heads/master 
0000000000000000000000000000000000000000 f8f9cf4b3c915992c04577073db03c58cac5806a Tony <xxx@qq.com> 1666019120 +0800	commit (initial): first commit

  • objects文件夹比执行add又多了两个文件夹
├── objects
│   ├── 24
│   │   └── 3f0ddd146da71bc5fe67206b1d35ecac81a9ec - 它是个tree对象(下章就会讲)
│   ├── 77
│   │   └── fd941d361f3f486002a8f569a124466e794d38 - blob对象
│   ├── f5
│   │   └── 5dedc2c1c08962615aababc2ff08571a2c2f7f - blob对象
│   ├── f8
│   │   └── f9cf4b3c915992c04577073db03c58cac5806a - commit对象
  • refs/heads/路径下多了个master文件
tony@ubuntu:~/learnGit$ cat .git/refs/heads/master 
f8f9cf4b3c915992c04577073db03c58cac5806a

注意:这里/refs/heads/master的内容,就是objects里,commit对象文件夹名f8 + 该文件夹下的文件名f9cf4b3c915992c04577073db03c58cac5806a.
变化就是这么多了.

0100 Git数据存储原理

Git本质上讲是一个内容寻址(content-addressable)文件系统,并在此基础上提供了一个版本控制系统的用户界面.Git的核心,是一个简单的键值对数据库(key-value data store). --Pro git

这一节的学习链接放这里了:
http://shafiul.github.io/gitbook/index.html 看第一章The Git Object Model和第七章里的内容.
https://git-scm.com/book/en/v2/Git-Internals-Plumbing-and-Porcelain pro git第10章内部原理讲的挺好.

Sha-1校验和

这一节用到的git底层命令 git cat-file -t 查看hash(校验和)的对象类型
git cat-file -p 打印hash(校验和)的内容.

在上面0011章节中,看到了add,commit操作后,出现好多很长的十六进制数字,一些文件如.git/ref/heads/master文件,.git/logs/refs/heads/master文件中都有.这些数字到底是什么?
这些40位十六进制数字校验和.

校验和–checksum是对一组数据(通常是一个文件)进行算法-加密哈希函数运算得到的结果。通过比较你手头文件和原始文件的校验和,能够确保你对原始文件的拷贝是真的并且不存在错误。
校验和通常也被称之为哈希值哈希和哈希码,或简称为哈希–hash

这些文件名是这么算出来的:
hash(校验和) = sha1(header + content) - content就是文件内容.
header = type(对象类型) +content.length +'\0'

解释起来,就是文件内容的二进制,再加上个头,然后利用sha-1算法进行加密运算,得出来的结果就是校验和.
header中的对象类型有四类:blob,tree,commit,tag四种.

git用的sha1哈希算法只用五个32位寄存器,所以最后压缩完毕之后,就是160位二进制.
这160位二进制,每4位转成16进制,就是40位十六进制数了.

我们来用命令看一下,0011章节中出现的hash值.
说明一下,objects目录下,就是把40位hash值的前两位拿去当文件夹的目录名,剩下38位作为文件名.

  • 先看这些hash的对象类型吧:
tony@ubuntu:~/learnGit$ git cat-file -t 243f0ddd146da71bc5fe67206b1d35ecac81a9ec
tree
tony@ubuntu:~/learnGit$ git cat-file -t 77fd941d361f3f486002a8f569a124466e794d38
blob
tony@ubuntu:~/learnGit$ git cat-file -t f55dedc2c1c08962615aababc2ff08571a2c2f7f
blob
tony@ubuntu:~/learnGit$ git cat-file -t f8f9cf4b3c915992c04577073db03c58cac5806a
commit
先知道这些类型,接下来介绍Git的数组存储方式.
  • 再看看这些hash是不是我们文件里写的内容:
tony@ubuntu:~/learnGit$ git cat-file -p 243f0ddd146da71bc5fe67206b1d35ecac81a9ec
100644 blob 77fd941d361f3f486002a8f569a124466e794d38	Firstfile.c
100644 blob f55dedc2c1c08962615aababc2ff08571a2c2f7f	ReadME.md
(注:这个tree类型存放两个指针,指向两个blob类型的对象. 注意哦, 文件名存放在Tree对象里!)

tony@ubuntu:~/learnGit$ git cat-file -p 77fd941d361f3f486002a8f569a124466e794d38
#include <stdio.h>

int main(void)
{
   int len = 0;
   
   len =  printf("This is my first file for git\n");
   printf("Length of printf %d\n", len);
   return len;

}
(注:完全就是我们Firstfile.c里的内容,一个空格一个标点都没变.)

tony@ubuntu:~/learnGit$ git cat-file -p f55dedc2c1c08962615aababc2ff08571a2c2f7f
# ReadMe

This Repo is used for learning Git.
(注:ReamME.md文件里的内容也是)

tony@ubuntu:~/learnGit$ git cat-file -p f8f9cf4b3c915992c04577073db03c58cac5806a
tree 243f0ddd146da71bc5fe67206b1d35ecac81a9ec
author Tony <xxx@qq.com> 1666019120 +0800
committer Tony <xxx@qq.com> 1666019120 +0800

first commit
(注:commit类型里存放的是上面那个tree对象与作者跟注释内容,tree对象里有两个blob对象,
blob对象存放我们写的文件内容.)
  • 注意: 我们的文件名,是存放在Tree对象中的. 而Blob对象,是不存文件名,只存文件内容的!先记住!

  • 由此我们可以看到我们写的文件全部变成了40位16进制hash值了,去查看会发现是乱码,被压缩过了.Git就是这样存放数据的.

  • 而且一旦文件内容发生一点点的变化(一个标点符号,一个空白行等),hash就会变.

Git对象(The Objects)

The Git Object Model讲的很清楚,英文也不难,想看可以到这里看.不想看英文的小伙伴,可以看看我写的.一样的内容.

每个对象都有三个部分组成 - 类型,大小,内容.
大小就是文件内容的长度了,内容取决于你这个对象是什么类型的.
类型分四类: blob tree commit tag

  • blob对象,只用来存数据,其他都不存,连文件名都不存.通常来说就是个写代码或文字的文件.
  • tree对象,基本上来说就是个目录吧.指向其他的tree对象,或者blob对象.有点像子目录和文件.
  • commit对象指向一个tree对象,用来记录项目某次提交时的状态.(上面那个commit对象里存的就是tree对象的hash值,还有提交时写的注释内容跟提交人,时间戳等等内容.)
  • tag对象,以特殊的方式,标记某个版本.

几乎所有的Git功能都是使用这四个简单的对象类型来完成的。它就像是在你本机的文件系统之上构建一个小的文件系统。这个小型的文件系统就是 .git/objects目录。

与SVN的区别
Git与你熟悉的大部分版本控制系统的差别是很大的。也许你熟悉Subversion、CVS、Perforce、Mercurial 等等, 他们使用"增量文件系统"(Delta Storage systems), 就是说它们存储每次提交(commit)之间的差异。Git正好与之相反, 它会把你的每次提交的文件的全部内容(snapshot)都会记录下来。这会是在使用Git时的一个很重要的理念。

  • Blob对象

Blob对象
Blob对象就是一堆二进制数据,它只存储文件里的内容,它不指向任何其他内容,也没有任何其它属性,连文件名都不存储.
前面看过的我们自己写的两个Blob对象:

tony@ubuntu:~/learnGit$ git cat-file -p 77fd941d361f3f486002a8f569a124466e794d38
#include <stdio.h>

int main(void)
{
   int len = 0;
   
   len =  printf("This is my first file for git\n");
   printf("Length of printf %d\n", len);
}

tony@ubuntu:~/learnGit$ git cat-file -p f55dedc2c1c08962615aababc2ff08571a2c2f7f
# ReadMe

This Repo is used for learning Git.

由于Blob对象是完全由它文件里的数据内容所决定,所以若是两个相同内容的文件在同一个目录下,或者在多个不同仓库版本中,他们会共用一个Blob对象,就是会用同一个hash值(还记得hash值怎么算的了吗).就是说,同样一份数据内容git只存储一个blob对象,只存一次.
Blob对象完全跟它的路径在哪,是否重命名过等都没有关系.只由文件里的内容决定.它只存文件里的内容!

  • Tree对象

一个tree对象有一串(bunch)指向blob对象或是其它tree对象的指针,它一般用来表示内容之间的目录层次关系.
我们的文件名, 是存在Tree对象中的.
Tree对象

Tree对象的内容,之前看过了的:

tony@ubuntu:~/learnGit$ git cat-file -p 243f0ddd146da71bc5fe67206b1d35ecac81a9ec
100644 blob 77fd941d361f3f486002a8f569a124466e794d38	Firstfile.c
100644 blob f55dedc2c1c08962615aababc2ff08571a2c2f7f	ReadME.md

Tree对象包含一个条目列表,每个条目都有一个模式,对象类型,SHA1名称和文件名称,并按名称排序.它表示单个目录树的内容.
一个tree对象可以指向(reference): 一个包含文件内容的blob对象, 也可以是其它包含某个子目录内容的其它tree对象. Tree对象、blob对象和其它所有的对象一样,都用其内容的SHA1哈希值来命名的;只有当两个tree对象的内容完全相同(包括其所指向所有子对象)时,它的名字才会一样,反之亦然。这样就能让Git仅仅通过比较两个相关的tree对象的名字是否相同,来快速的判断其内容是否不同。tree对象存储的是指针(tree和blob的SHA哈希值),不存储真正的对象。tree对象可以理解为就是一个目录,目录里包含子目录(tree的SHA值)和文件(blob的SHA值).而SHA值所对应的真正的对象文件存在 .git/objects下面。

  • Commit对象

Commit就是提交, "commit对象"指向一个"tree对象", 这个tree对象就是本次提交所对应的目录树,里面包括这次提交时工作区里面所有的目录和文件的指针,有时也叫做快照。"commit对象"还带有相关的描述信息.

image

Commit对象Hash内容:

tony@ubuntu:~/learnGit$ git cat-file -p f8f9cf4b3c915992c04577073db03c58cac5806a
tree 243f0ddd146da71bc5fe67206b1d35ecac81a9ec
author Tony <xxx@qq.com> 1666019120 +0800
committer Tony <xxx@qq.com> 1666019120 +0800

first commit

提交(commit)由以下的部分组成:

一个 tree对象: tree对象的SHA1签名, 代表着目录在某一时间点的内容.

父提交 (parent(s)): 提交(commit)的SHA1签名代表着当前提交前一步的项目历史. 合并的提交(merge commits)可能会有不只一个父对象. 如果一个提交没有父对象, 那么我们就叫它"根提交"(root commit), 它就代表着项目最初的一个版本(revision). 每个项目必须有至少有一个"根提交"(root commit)。Git就是通过父提交把每个提交联系起来,也就是我们一般所说的提交历史。父提交就是当前提交上一版本。

作者 : 做了此次修改的人的名字, 还有修改日期.

提交者(committer): 实际创建提交(commit)的人的名字, 同时也带有提交日期. TA可能会和作者不是同一个人; 例如作者写一个补丁(patch)并把它用邮件发给提交者, 由他来创建提交(commit).

提交说明 :用来描述此次提交.

一个提交(commit)本身并没有包括任何信息来说明其做了哪些修改; 所有的修改(changes)都是通过与父提交(parents)的内容比较而得出的。
一般用 git commit 来创建一个提交(commit), 这个提交(commit)的父对象一般是当前分支(current HEAD), 同时把存储在当前索引(index)的内容全部提交.

commit是使用频率最高的对象,一般在使用Git时,我们直接接触的就是commit。我们 commit代码, merge代码, pull / push代码,重置版本库,查看历史,切换分支这些在开发流程中的基本操作都是直接和commit对象打交道。

  • Tag对象

一个标签对象包括一个对象名, 对象类型, 标签名, 标签创建人的名字("tagger"), 还有一条可能包含有签名(signature)的消息. 你可以用 git cat-file -p 命令来查看这些信息。

现在我们的git对象库里还没有一个tag对象,我们先用 git tag -m <msg> <tagname> [<commit>] 命令创建一个tag

$git tag -m 'create tag from demo' v1.0 #基于当前HEAD建立一个tag,所以tag指向的就是HEAD的引用
$git tag
v1.0
$git cat-file -p v1.0
object e6361ed35aa40f5bae8bd52867885a2055d60ea2
type commit
tag v1.0
tagger tianle <tianle@dangdang.com> 1494406971 +0800

create tag from demo

image

Tag对象就是里程碑的作用,一般在我们正式发布代码是需要建立一个里程碑。
好了,至此我们就把Git的4种对象类型介绍完了。他们是 blob , tree , commit , tag 。这部分非常重要,理解了Git对象模型是理解Git使用流程和各种Git命令的基础。
要想会用Git,这部分必须弄清楚,否则永远不会用,只能照猫画虎死记硬背命令,这样一旦命令的结果的预想的不一致,就懵了。

对象模型(The Object Model)

项目结构:

$>tree
.
|-- README
`-- lib
    |-- inc
    |   `-- tricks.rb
    `-- mylib.rb

2 directories, 3 files

它的对象模型如下:
image
每个目录都创建了 tree对象, 每个文件都创建了一个对应的 blob对象 . 最后有一个 commit对象 来指向根tree对象(root of trees), 这样我们就可以追踪项目每一项提交内容。除了第一个commit,每个commit对象都有一个父commit对象,父commit就是上一次的提交(历史 history),这样就形成了一条提交历史链.Git就是通过这种方式组成了git版本库.

我们自己写的例子的数据结构是这样的:
img

Git对象,以及其数据存储原理已经讲完了.
后面会根据原理,再来看看,Git的工作过程,各个部分数据的变化.

posted @ 2022-10-17 20:29  道阻且长行则将至Go  阅读(77)  评论(0编辑  收藏  举报