扫描下方海报二维码,试听课程:
html
(课程详细大纲,请参见文末)git
前言: 以前听过公司大佬分享过 Git 原理以后就想来本身总结一下,最近一忙起来就拖得久了,原本想塞更多的干货,可是不喜欢拖过久,因此先出一版足够入门的;github
Git 是当前流行的分布式版本控制管理工具,最初由 Linux Torvalds (Linux 之父) 创造,于 2005 年发布。面试
Git,这个词其实源自英国俚语,意思大约是 “混帐”。Linux 为何会以这样自嘲的名字来命名呢?这其中还有一段儿有趣的历史能够说一说:算法
如下摘自:安全
https://www.liaoxuefeng.com/wiki/896043488029600/896202815778784bash
Git 的诞生:服务器
不少人都知道,Linus 在 1991 年建立了开源的 Linux,今后,Linux 系统不断发展,已经成为最大的服务器系统软件了。app
Linus 虽然建立了 Linux,但 Linux 的壮大是靠全世界热心的志愿者参与的,这么多人在世界各地为 Linux 编写代码,那 Linux 的代码是如何管理的呢?分布式
事实是,在 2002 年之前,世界各地的志愿者把源代码文件经过 diff 的方式发给 Linus,而后由 Linus 本人经过手工方式合并代码!
你也许会想,为何 Linus 不把 Linux 代码放到版本控制系统里呢?不是有 CVS、SVN 这些免费的版本控制系统吗?
由于 Linus 坚决地反对 CVS 和 SVN,这些集中式的版本控制系统不但速度慢,并且必须联网才能使用。
有一些商用的版本控制系统,虽然比 CVS、SVN 好用,但那是付费的,和 Linux 的开源精神不符。
不过,到了 2002 年,Linux 系统已经发展了十年了,代码库之大让 Linus 很难继续经过手工方式管理了,社区的弟兄们也对这种方式表达了强烈不满
因而 Linus 选择了一个商业的版本控制系统 BitKeeper,BitKeeper 的东家 BitMover 公司出于人道主义精神,受权 Linux 社区无偿使用这个版本控制系统。
安定团结的大好局面在 2005 年就被打破了,缘由是 Linux 社区牛人汇集,难免沾染了一些梁山好汉的江湖习气。
开发 Samba 的 Andrew 试图破解 BitKeeper 的协议(这么干的其实也不仅他一个),被 BitMover 公司发现了(监控工做作得不错!),因而 BitMover 公司怒了,要收回 Linux 社区的无偿使用权。
Linus 能够向 BitMover 公司道个歉,保证之后严格管教弟兄们,嗯,这是不可能的。
然而实际状况是:Linus 花了两周时间本身用 C 写了一个分布式版本控制系统,这就是 Git!
一个月以内,Linux 系统的源码已经由 Git 管理了!牛是怎么定义的呢?你们能够体会一下。
Git 迅速成为最流行的分布式版本控制系统,尤为是 2008 年,GitHub 网站上线了,它为开源项目免费提供 Git 存储,无数开源项目开始迁移至 GitHub,包括 jQuery,PHP,Ruby 等等。
历史就是这么偶然,若是不是当年 BitMover 公司威胁 Linux 社区,可能如今咱们就没有免费而超级好用的 Git 了。
无论是集中式的 CVS、SVN 仍是分布式的 Git 工具,实际上都是一种版本控制系统,咱们能够经过他们很方便的管理咱们的文件、代码等,咱们能够先来畅想一下若是本身来设计这么一个系统,你会怎么设计?
摁,这不由让我想起了以前写毕业论文的日子,我先在一个开阔的空间建立了一个文件夹用于保存个人各类版本,而后开始了个人 “毕业论文版本管理”,参考下图:
这好像暴露了我写毕业论文愉快的经历..但无论怎么样,我在用一个粗粒度版本的制度,在对个人毕业论文进行着管理
摁,我经过不停在原基础上迭代出新的版本的方式,不只保存了我各个版本的毕业论文,还有这清晰的一个路径
完美?NO!问题是:
每一次的迭代都更改了什么东西,我如今彻底看不出来了!
当我在迭代个人超级无敌怎么样都不改的版本的时候,忽然回想起好像以前版本 1.0 的第一节内容和 2.0 版本第三节的内容加起来才是最棒的
我须要打开多个文档并建立一个新的文档,仔细对比文档中的不一样并为个人新文档添加新的东西,好麻烦啊…
到最后文件多起来的时候,我甚至都不知道是个人 “超级无敌版” 是最终版,仍是 “打死都不改版” 是最终版了;
更为要命的是,我保存在个人桌面上,没有备份,意味着我本地文件手滑删除了,那我就…我就…就…
而且可能问题还远不止于此,因此往往想起,就不自觉对 Linux 膜拜了起来。
Git 采用与 CSV/SVN 彻底不一样的处理方式,前者采用分布式,然后面两个都是集中式的版本管理。
先说集中式版本控制系统,版本库是集中存放在中央服务器的,而干活的时候,用的都是本身的电脑,因此要先从中央服务器取得最新的版本,而后开始干活,干完活了,再把本身的活推送给中央服务器。
中央服务器就比如是一个图书馆,你要改一本书,必须先从图书馆借出来,而后回到家本身改,改完了,再放回图书馆。
集中式版本控制系统最大的毛病就是必须联网才能工做,若是在局域网内还好,带宽够大,速度够快
可若是在互联网上,遇到网速慢的话,可能提交一个10M的文件就须要5分钟,这还不得把人给憋死啊。
那分布式版本控制系统与集中式版本控制系统有何不一样呢?
首先,分布式版本控制系统根本没有 “中央服务器”,每一个人的电脑上都是一个完整的版本库
这样,你工做的时候,就不须要联网了,由于版本库就在你本身的电脑上。
既然每一个人电脑上都有一个完整的版本库,那多我的如何协做呢?
比方说你在本身电脑上改了文件 A,你的同事也在他的电脑上改了文件 A,这时,大家俩之间只需把各自的修改推送给对方,就能够互相看到对方的修改了。
和集中式版本控制系统相比,分布式版本控制系统的安全性要高不少,由于每一个人电脑里都有完整的版本库
某一我的的电脑坏掉了没关系,随便从其余人那里复制一个就能够了。而集中式版本控制系统的中央服务器要是出了问题,全部人都无法干活了。
在实际使用分布式版本控制系统的时候,其实不多在两人之间的电脑上推送版本库的修改,由于可能大家俩不在一个局域网内,两台电脑互相访问不了,也可能今天你的同事病了,他的电脑压根没有开机。
所以,分布式版本控制系统一般也有一台充当 “中央服务器” 的电脑,但这个服务器的做用仅仅是用来方便 “交换” 你们的修改,没有它你们也同样干活,只是交换修改不方便而已。
固然,Git 的强大还远不止此。
首先,让咱们来建立一个空的项目目录,并进入该目录。
$ mkdir git-demo-project$ cd git-demo-project复制代码
若是咱们打算对该项目进行版本管理,第一件事就是使用 git init
命令,进行初始化。
$ git init复制代码
git init
命令只会作一件事,就是在项目的根目录下建立一个 .git
的子目录,用来保存当前项目的一些版本信息,咱们能够继续使用 tree -a
命令查看该目录的完整结构,以下:
$ tree -a.└── .git ├── HEAD ├── branches ├── config ├── description ├── hooks │ ├── applypatch-msg.sample │ ├── commit-msg.sample │ ├── fsmonitor-watchman.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 │ ├── .DS_Store │ ├── info │ └── pack └── refs ├── heads └── tags复制代码
config 是仓库的配置文件,一个典型的配置文件以下,咱们建立的远端,分支都在等信息都在配置文件里有表现;fetch
操做的行为也是在这里配置的:
[core] repositoryformatversion = 0 filemode = false bare = false logallrefupdates = true symlinks = false ignorecase = true[remote "origin"] url = git@github.com:yanhaijing/zepto.fullpage.git fetch = +refs/heads/*:refs/remotes/origin/*[branch "master"] remote = origin merge = refs/heads/master[branch "dev"] remote = origin merge = refs/heads/dev复制代码
Git 能够经过一种算法能够获得任意文件的 “指纹”(40 位 16 进制数字),而后经过文件指纹存取数据,存取的数据都位于 objects 目录。
例如咱们能够手动建立一个测试文本文件并使用 git add .
命令来观察 .git
文件夹出现的变化:
$ touch test.txt$ git add .复制代码
git add .
命令就是用于把当前新增的变化添加进 Git 本地仓库的,在咱们使用后,咱们惊奇的发现 .git
目录下的 objects/
目录下多了一个目录:
$ tree -a.├── .git│ ├── HEAD│ ├── branches│ ├── config│ ├── description│ ├── hooks│ │ ├── 节省篇幅..省略..│ ├── index│ ├── info│ │ └── exclude│ ├── objects│ │ ├── .DS_Store│ │ ├── e6│ │ │ └── 9de29bb2d1d6434b8b29ae775ad8c2e48c5391│ │ ├── info│ │ └── pack│ └── refs│ ├── heads│ └── tags└── test.txt复制代码
咱们可使用 git hash-object test.txt
命令来看看刚才咱们建立的 test.txt
的 “文件指纹”:
$ git hash-object test.txte69de29bb2d1d6434b8b29ae775ad8c2e48c5391复制代码
这时候咱们能够发现,新建立的目录 e6
实际上是该文件哈希值的前两位,这实际上是 Git 作的一层相似于索引同样的东西,而且默认采用 16 进制的两位数来当索引,是很是合适的。
objects 目录下有 3 种类型的数据:
Blob;
Tree;
Commit;
文件都被存储为 blob 类型的文件,文件夹被存储为 tree 类型的文件,建立的提交节点被存储为 Commit 类型的数据;
通常咱们系统中的目录(tree),在 Git 会像下面这样存储:
而 Commit 类型的数据则整合了 tree 和 blob 类型,保存了当前的全部变化,例如咱们能够再在刚才的目录下新建一个目录,并添加一些文件试试:
$ mkdir test$ touch test/test.file$ tree -a.├── .git│ ├── HEAD│ ├── branches│ ├── config│ ├── description│ ├── hooks│ │ ├── 节省篇幅..省略..│ ├── index│ ├── info│ │ └── exclude│ ├── objects│ │ ├── .DS_Store│ │ ├── e6│ │ │ └── 9de29bb2d1d6434b8b29ae775ad8c2e48c5391│ │ ├── info│ │ └── pack│ └── refs│ ├── heads│ └── tags├── test│ └── test.file└── test.txt复制代码
提交一个 Commit 再观察变化:
$ git commit -a -m "test: 新增测试文件夹和测试文件观察.git文件的变化"[master (root-commit) 30d51b1] test: 新增测试文件夹和测试文件观察.git文件的变化 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 test.txt$ tree -a.├── .git│ ├── COMMIT_EDITMSG│ ├── HEAD│ ├── branches│ ├── config│ ├── description│ ├── hooks│ │ ├── 节省篇幅..省略..│ ├── index│ ├── info│ │ └── exclude│ ├── logs│ │ ├── HEAD│ │ └── refs│ │ └── heads│ │ └── master│ ├── objects│ │ ├── .DS_Store│ │ ├── 30│ │ │ └── d51b1edd2efd551dd6bd52d4520487b5708c0e│ │ ├── 5e│ │ │ └── fb9bc29c482e023e40e0a2b3b7e49cec842034│ │ ├── e6│ │ │ └── 9de29bb2d1d6434b8b29ae775ad8c2e48c5391│ │ ├── info│ │ └── pack│ └── refs│ ├── heads│ │ └── master│ └── tags├── test│ └── test.file└── test.txt复制代码
首先咱们能够观察到咱们提交了一个 Commit 的时候在第一句话里面返回了一个短的像是哈希值同样的东西:[master (root-commit) 30d51b1]
中 的 30d51b1
对应的咱们也能够在 objects 找到刚才 commit 的对象,咱们可使用 git cat-file -p
命令输出一下当前文件的内容:
$ git cat-file -p 30d5tree 5efb9bc29c482e023e40e0a2b3b7e49cec842034author 我没有三颗心脏 <wmyskxz@wmyskxzdemacbook-pro.local> 1565742122 +0800committer 我没有三颗心脏 <wmyskxz@wmyskxzdemacbook-pro.local> 1565742122 +0800test: 新增测试文件夹和测试文件观察.git文件的变化</wmyskxz@wmyskxzdemacbook-pro.local></wmyskxz@wmyskxzdemacbook-pro.local>复制代码
咱们发现这里面有提交的内容信息、做者信息、提交者信息以及 commit message,固然咱们能够进一步看到提交的内容具体有哪些:
$ git cat-file -p 5efb100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 test.txt复制代码
咱们再试着提交一个 commit 来观察变化:
$ touch test/test2.file$ git commit -a -m "test: 新增长一个 commit 以观察变化."[master 9dfabac] test: 新增长一个 commit 以观察变化. 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 test/test.file create mode 100644 test/test2.file$ git cat-file -p 9dfabactree c562bfb9441352f4c218b0028148289f1ea7d7cdparent 30d51b1edd2efd551dd6bd52d4520487b5708c0eauthor 龙滔 <longtao@longtaodemacbook-pro.local> 1565878699 +0800committer 龙滔 <longtao@longtaodemacbook-pro.local> 1565878699 +0800test: 新增长一个 commit 以观察变化.</longtao@longtaodemacbook-pro.local></longtao@longtaodemacbook-pro.local>复制代码
能够观察到这一次的 commit 多了一个 parent 的行,其中的 “指纹” 和上一次的 commit 如出一辙
当咱们提交两个 commit 以后咱们的 Git 仓库能够简化为下图:
说明:其中由于咱们 test 文件夹新增了文件,也就是出现了变化,因此就被标识成了新的 tree 类型的对象;
refs 目录存储都是引用文件,如本地分支,远端分支,标签等
refs/heads/xxx 本地分支
refs/remotes/origin/xxx 远端分支
refs/tags/xxx 本地tag
引用文件的内容都是 40 位长度的 commit
$ cat .git/refs/heads/master9dfabac68470a588a4b4a78742249df46438874a复制代码
这就像是一个指针同样,它指向了你的最后一次提交(例如这里就指向了第二次提交的 commit),咱们补充上分支信息,如今的 Git 仓库就会像下图所示:
HEAD 目录下存储的是当前所在的位置,其内容是分支的名称:
$ cat HEADref: refs/heads/master复制代码
咱们再补充上 HEAD 的信息,如今的 Git 仓库以下图所示:
您也在上面了解到了,在 Git 中分支是一种十分轻便的存在,仅仅是一个指针罢了
咱们在普遍的使用分支中,不可避免的会遇到新建立分支的合并,这时候不管是选择 merge 仍是 rebase,都有可能发生冲突,咱们先来看一下冲突是如何产生的:
图上的状况,并非移动分支指针就可以解决问题的,它须要一种合并策略。
首先咱们须要明确的是谁与谁的合并,是 2,3 与 4, 5, 6 两条线的合并吗?
其实并非的,真实合并的其实只有 3 和 6,由于每一次的提交都包含了项目完整的快照,即合并只是 tree 与 tree 的合并。
这可能提及来有点绕,咱们能够先来想一个简单的算法,用来比较 3 和 6 的不一样。
若是咱们只是单纯的比较 3 和 6 的信息,其实并无意义,由于它们之间并不能确切的表达出当前的冲突状态。
所以咱们须要选取它们两个分支的分歧点(merge base)做为参考点,进行比较。
首先咱们把 1 做为基础,而后把 一、三、6 中全部的文件作一个列表,而后依次遍历这个列表中的文件。
咱们如今拿列表中的一个文件进行举例,把在提交在 一、三、6 中的该文件分别称为版本一、版本三、版本6,可能出现以下几种状况:
1. 版本 一、版本 三、版本 6 的 “指纹” 值都相同:这种状况则说明没有冲突;
2. 版本 3 or 版本 6 至少有一个与版本 1 状态相同(指的是指纹值相同或都不存在):
这种状况能够自动合并,好比版本 1 中存在一个文件,在版本 3 中没有对该文件进行修改,而版本 6 中删除了这个文件,则以版本 6 为准就能够了;
3. 版本 3 or 版本 6 都与版本 1 的状态不一样:
这种状况复杂一些,自动合并策略很难生效了,因此须要手动解决;
在解决完冲突后,咱们能够将修改的内容提交为一个新的提交,这就是 merge。
能够看到 merge 是一种不修改分支历史提交记录的方式,这也是咱们经常使用的方式。
可是这种方式在某些状况下使用起来不太方便,好比咱们建立了一些提交发送给管理者,管理者在合并操做中产生了冲突,还须要去解决冲突,这无疑增长了他人的负担。
而咱们使用 rebase 能够解决这种问题。
假设咱们的分支结构以下:
rebase 会把从 Merge Base 以来的全部提交,以补丁的形式一个一个从新打到目标分支上。
这使得目标分支合并该分支的时候会直接 Fast Forward(能够简单理解为直接后移指针),即不会产生任何冲突。提交历史是一条线,这对强迫症患者可谓是一大福音。
其实 rebase 主要是在 .git/rebase-merge 下生成了两个文件,分别为 git-rebase-todo 和 done 文件,这两个文件的做用光看名字就大概可以看得出来。
git-rebase-todo 中存放了 rebase 将要操做的 commit,而 done 存放正操做或已操做完毕的 commit
好比咱们这里,git-rebase-todo 存放了 四、五、6 三个提交。
首先 Git 会把 4 这个 commit 放入 done,表示正在操做 4,而后将 4 以补丁的方式打到 3 上,造成了新的 4
这一步是可能产生冲突的,若是有冲突,须要解决冲突以后才能继续操做。 接着按一样的方式把 五、6 都放入 done,最后把指针移动到最新的提交 6
上,就完成了 rebase 的操做。
从刚才的图中,咱们就能够看到 rebase 的一个缺点,那就是修改了分支的历史提交。
若是已经将分支推送到了远程仓库,会致使没法将修改后的分支推送上去,必须使用 -f 参数(force)强行推送。
因此使用 rebase 最好不要在公共分支上进行操做。
简单说就是压缩提交,把屡次的提交融合到一个 commit 中,这样的好处不言而喻,咱们着重来讨论一下实现的技术细节
仍是以咱们上面最开始的分支状况为例,首先,Git 会建立一个临时分支,指向当前 feature 的最新 commit。
而后按照上面 rebase 的方式,变基到 master 的最新 commit 处。
接着用 rebase 来 squash 之,压缩这些提交为一个提交。
最后以 fast forward 的方式合并到 master 中。
可见此时 master 分支多且只多了一个描述了此次改动的提交,这对于大型工程,保持主分支的简洁易懂有很大的帮助。
说明:想要了解更多的诸如 checkout、cherry-pick 等操做的话能够看看参考文章的第三篇,这里就不作细致描述了。
经过上面的了解,其实咱们已经大体的掌握了 Git 中的基本原理
咱们的 Commit 就像是一个链表节点同样,不只有自身的节点信息,还保存着上一个节点的指针
而后咱们以 Branch 这样轻量的指针保存着一条又一条的 commit 链条
不过值得注意的是,objects 目录下的文件是不会自动删除的,除非你手动 GC,否则本地的 objects 目录下就保留着你当前项目完整的变化信息
因此咱们一般都会看到 Git 上面的项目一般是没有 .git 目录的,否则仅仅经过 .git 目录理论上就能够还原出你的完整项目!
https://www.liaoxuefeng.com/wiki/896043488029600/896202780297248
https://yanhaijing.com/git/2017/02/08/deep-git-3
https://coding.net/help/doc/practice/git-principle.html
END
《21天互联网Java进阶面试训练营(分布式篇)》详细目录,扫描图片末尾的二维码,试听课程