1、什么是git
git的官方定义:Git is a fast, scalable, distributed revision control system with an unusually rich command set that provides both high-level operations and full access to internals.
可以看出,git是一个具有丰富命令集的版本控制系统,它的特点就是快速、可扩展以及分布式。
而git实际上是一个简单内容记录工具(the stupid content tracker)。从根本上来讲,git是一套内容寻址 (content-addressable) 文件系统,在此之上提供了一个版本控制系统的用户界面。
1.1 an unusually rich command set
在当前版本里,git一共有158个命令,但是常用命令只有10多个,其他命令可以视为底层命令。由于git最开始被设计的时候用来作为the stupid content tracker,只是现在被最广泛地用来作为revision control system,但是实际上git也可以被用来做其它事情。因此,git不仅一开始提供了功能齐全的底层命令,而且还提供了许多友好的高层命令。
git命令分为两种:
Procelain(高层命令)
user-friendly commands: init, add, commit, push, checkout, branch, merge, etc.
Plmbing(底层命令)
bunch of verbs that do low-level work: hash-object, update-index, write-tree, etc.
1.2 revision control system
版本控制系统,学名:software change and configuration management system(SCM)。它需要具有的特点如下:
记录和控制软件的变化点和版本号
能清楚知道什么东西被改了,被谁改了
在多人完成的大项目中,能更加容易的合作和管理文件
最重要的是,要有回滚功能
在实际生活中,版本控制就被经常用到,例如写论文时的版本控制。但是,这只是简单的本地版本控制。在多人的项目合作中,这就显得力不从心了。因为使用这种方法时,首先需要大量的拷贝工作,以保证能够回滚嘛;其次当多人更改同一项目出现写写冲突情况时,虽然Linux提供diff和patch命令,但是解决起来肯定极其痛苦。
因此,版本控制系统,核心就是让在不同系统上的开发者协同工作,需要解决的主要问题就是防止各种人一块写最后写乱套的情况。
2、git有何不同
2.1 集中式版本控制系统
首先,谈谈传统的集中式版本控制系统。
集中式版本控制系统中,版本库是集中存放在中央服务器(server),每个开发者都作为客户端(client)。协同工作的开发者都通过客户端连到中央服务器,取出(checkout)最新的文件或者提交(commit)更新。它的主要特点如下:
每次checkout拿到的都是当前版本库最新的一个快照状态(snapshot)。它并不知道版本库之前的历史状态信息。
每次commit都会更新中央版本库。
为了保证不丢失数据,在每次commit时都需要re-sync。
而集中式版本控制系统的缺点,则有如下几点:
1. 对于每次commit操作,都需要re-sync;
对于集中式版本控制系统,在多人协作中,可以认为如果commit越早,麻 烦也就越少。
从这个角度上看,可以认为git做了两步commit操作,第一次为commit,第二次为pull,只有pull才需要re-sync。
2. 单点故障
在没有commit之前所有文件都在于client端的本地文件,如果此时不小心删除掉了一些文件,除非重新访问server进行回滚操作,否则就真的删除掉
当server没有备份并且宕掉了,就算能恢复当前版本的数据,但是之前的历史数据信息是恢复不了的。
3. 每次工作的时候,需要联网;
4. 速度慢;
大部分时间都是需要client和server进行沟通协作的。
2.2 分布式版本控制系统
我们再来看看git,它是一个分布式版本控制系统。
与传统的集中式版本控制系统相比,git最大的不同在于,它是分布式版本控制系统。分布式意味着,不止服务器保存有version database,每个客户端也保存有version database。git的主要特点如下:
每次clone都真正的拷贝所有的数据。
在初始clone之后,大多数的操作都是在本地进行的。
而git版本控制系统的主要优点如下三点:
1. Fast:
除了第一次clone的时候慢,之后的操作都比较快。因为大多数操作都是在本地进行的,不需要联网。
2. Scalable:
你可以对本地的version database做任何你想做的事情,他并不会对其他人 造成任何影响。
3. Distributed:
在client端,肯定不法彻底避免单点故障,但是可以减少这样可能性。因为本地保存有version database,所以当本地不小心丢失文件时,往往可以本地出错本地解决。
在server坏掉的时候,可以根据client里面的version database进行恢复,甚至可以把client当成一个新的server。
3、git如何存储内部数据
从上一小节,我们知道git是一个分布式版本控制系统,无论是服务端还是客户端都拥护一份自己的version database。那么本小节,首先看看version database到底是什么。然后,分别深入看看git init、git add、git commit这个三个常用命令具体的工作过程。
version database
git的version database到底是什么?
1. 本质上,是一套内容寻址文件系统(Content Addressable Filesystem)。
2. 实现上,是一个简单的KV数据库
3. Key: sha-1 hash(everything is hashed)
20 bytes, 40 hex, 160 bit, 2.9e48 distinct keys
How would git handle a SHA-1 collision on a blob?
4.Value: binary files
Commits : actual git commits
Trees : directories(structure of file system)
Blobs : contents of files/data
git以一种类似UNIX文件系统但更简单的key-value方式来存储内容。key是对文件内容的哈希值。value则包含有commit object、tree object以及blob object三种对象。
所有内容以tree或blob对象存储,其中tree对象对应于UNIX中的目录,blob对象则大致对应于inodes或文件内容。commit对象则可以看作是对当前版本的一个快照。
一个单独的tree对象包含一条或多条tree记录,每一条记录含有一个指向blob或子tree对象的SHA-1指针,并附有该对象的权限模式、类型和文件名信息。
接下来一步一步,看来git flow里到底发生了什么:
3.1 git init命令
git init主要是创建了四个文件夹:.git、.git/refs/heads、.git/refs/tags以及.git/objects,和一个文件.git/HEAD,并把初始化./git/HEAD里面的内容为指向master branch的HEAD。
那么如何用普通的Linux命令实现git init呢?答案其实很简单:
1
2
3
4
5
6
|
mkdir .git
mkdir -p .git/refs/heads
mkdir -p .git/refs/tags
mkdir -p .git/objects
touch .git/HEAD
echo
"ref:refs/heads/master"
> .git/HEAD
|
以上便是git init, without git init的具体实现做法。其中,objects目录存储所有数据内容,refs目录存储指向数据(分支)的提交对象的指针,HEAD文件指向当前分支。
3.2 git add命令
git add做了两件事情:
第一,把文件写成blob object作为value,并把文件内容的hash值作为key;
第二,更新了index文件,把文件放入了暂存区(staging area)。
1
2
|
echo
"Hello World"
> hello.txt
git add .
|
上面的操作可以用下面底层语句实现:
1
2
3
|
echo
"Hello World"
| git hash-object -w --stdin
git update-index --add --cacheinfo 100644 557db03de997c86a4028e1ebd3a1ceb225be238 hello.txt
git checkout -- hello.txt
|
第一个命令中,参数-w指示hash-object命令存储(数据)对象,若不指定这个参数该命令仅仅返回键值。该命令输出长度为40个字符的SHA-1哈希值校验和,并创建以它的前2个字符为名称的子目录,剩下38个字符作为文件命名(保存至子目录下)。
第二个命令,update-index为一个单独文件创建一个index。更新完staging area后,由于仅仅Index里有,但是本地仍没有,如果git status,所以会显示为deleted。此时,使用第三条命令可以把repository进行checkout出来。
3.3 git commit命令
我们进行第一次git commit操作。
1
|
git commit -m
"First Commit"
|
此时会多出两个文件。
第一个是commit object,它实际上是对于跟踪项目内容的一次快照,里面的格式内容有:指明了该时间点项目快照的顶层树对象、作者/提交者信息以及提交注释信息;
第二个则是那个被指向tree object,它实际上一个文件根目录,里面指向一个blob object,而这个blob object便是之前git add时的内容。
虽然执行git log命令可以查看完整的历史信息并以此找到文件,那么问题来了,系统又是如何找到commit log的呢? 答案是:refs!
在.git/HEAD里存的是一个ref,ref是一个file,这个file里面又存了一个commit的hash值,从而便可以找到了当前的base版本。
我们尝试进行第二次git commit操作。 首先,git add一个简单的脚本文件hello.sh。
/*************hello.sh的具体内容如下:
1
2
3
4
|
!/bin/sh
echo
"Hello World"
**************/
git add hello.sh
|
此时,git add只会增加一个blob object,而不会增加多一个tree object。也就是说,在add的时候并不写目录,只是更新了暂存区,在index里会保存目录信息。因为在git commit操作前,可以进行多次git add操作。
1
|
git commit -m
"Second Commit"
|
此时,会多出三个文件。第一个是commit object,第二个是根目录的tree object,第三个是文件目录的tree object。
通过下图,可以发现commit object中还带有个parent指针,它指向上一次的commit object。最重要的是,它还更新自身的refs/heads/master信息;
通过命令git ls-files --stage查看staging area信息。你还可以发现,暂存区里只存储blob object,并不存储tree object和commit object。
4、branch/merge在git内部是如何工作
相对于git init、git add、git commit这三个命令,git branch和git merge则显得十分的小巧机智。结合下列几幅图,能更轻松的理解这两个命令。
4.1 branch操作
在git里,branch操作,实际上仅仅增添一个41-bytes的引用文件(在refs/heads/目录底下)。可以看出,这样的操作是极其的简单廉价的,示意图如下:
同理,在git里,checkout操作,实际上也仅仅是更改了引用文件的指针而已。
4.2 merge操作
最后,我们来分析一下,git里的merge操作。它的操作流程和示意图如下:
1. 计算出当前branch和公共branch的diff;
这步操作的速度很快。因为只需要去比较文件的hash值是否匹配即可。
2. 应用该diff去订正本地文件;
这步操作并不会覆盖分支里之前的版本数据。
3. 解决所有的冲突;
当merge操作完成后,示意图如下:
首先,可以发现,这是会新建一个mater处的commit object,而它有两个parent指针,分别指向之前两个branch的commit object。
其次,如果不小心做了一个bad merge,而又不想让他人知道时,其实只需要更改下master和feature指针的hash信息即可。
最后,通过branch和merge操作,你会发现在git的世界里,它会尽量不让你做删除的事情。
5、小结
Git is the stupid content tracker.
You can represent renames on top of git - git itself really doesn't care. In many ways you can just see git as a filesystem - it's content-addressable, and it has a notion of versioning, but I really really designed it coming at the problem from the viewpoint of a _filesystem_person (hey, kernels is what I do), and I actually have absolutely _zero_interest in creating a traditional SCM system. -- by Linus Torvalds
我们应该如何看待并理解git呢?本质上,git是一套文件寻址系统。
虽然git具有版本控制的功能,但是在Linus最初设计git的时候,完完全全是从一个文件系统的角度进行设计和实现这个产品的。所以说,git本质上仍然是一个文件系统,只是它很适合地被用作版本控制系统。