浅析git

git是什么

简单来讲,Git,它是一个快速的 分布式版本控制系统 (Distributed Version Control System,简称 DVCS)git

同传统的 集中式版本控制系统 (Centralized Version Control Systems,简称CVCS) 不一样,Git的分布式特性使得开发者间的协做变得更加灵活多样。程序员

这时候咱们会想到:算法

  1. 什么又是版本控制呢?
  2. 什么是分布式什么是集中式?

咱们带着问题往下走。数据库

版本控制

版本控制是一种记录一个或若干文件内容变化,以便未来查阅特定版本修订状况的系统。安全

好比:有一位程序员他可能须要保存一个代码文件的全部的修订版本,这样就能够服务器

  • 将某个文件回溯到以前的状态
  • 甚至将整个项目都回退到过去某个时间点的状态
  • 比较文件的变化细节,查出最后是谁修改了哪一个地方,从而找出致使怪异问题出现的缘由

这时候采用版本控制就是一个很是明智的选择,使用版本控制系统一般还意味着,就算你乱来一气把整个项目中的文件改的改删的删,你也照样能够轻松恢复到原先的样子。 但额外增长的工做量却微乎其微。app

版本控制的成长

儿童:人们经过复制整个项目的方式来保存不一样的版本,或许还会更名加上备份时间以示区别。好处就是简单,可是特别容易犯错,一不当心会写错文件或者覆盖意想外的文件。分布式

少年:人们为了上面的问题,好久之前就开发了许多种本地版本控制系统,大可能是采用某种简单的数据库来记录文件的历次更新差别,好比其中比较流行的 RCS网站

青年:人们又遇到一个问题,如何让在不一样系统上的开发者协同工做? 因而,集中化的版本控制系统( CVCS)应运而生。 这类系统,诸如 CVSSubversion ,都有一个单一的集中管理的服务器,保存全部文件的修订版本,而协同工做的人们都经过客户端连到这台服务器,取出最新的文件或者提交更新。如今,每一个人均可以在必定程度上看到项目中的其余人正在作些什么。 而管理员也能够轻松掌控每一个开发者的权限,而且管理一个 CVCS设计

事分两面,有好有坏。 这么作最显而易见的缺点是中央服务器的单方面故障。 若是关机一小时,那么在这一小时内,谁都没法提交更新,也就没法协同工做。 若是中心数据库所在的磁盘发生损坏,又没有作恰当备份,毫无疑问你将丢失全部数据——包括项目的整个变动历史,只剩下人们在各自机器上保留的单独快照。

壮年:因而分布式版本控制系统面世了。 在这类系统中,像 GitMercurial 等,客户端并不仅提取最新版本的文件快照,而是把代码仓库完整地镜像下来。 这么一来,任何一处协同工做用的服务器发生故障,过后均可以用任何一个镜像出来的本地仓库恢复。 由于每一次的克隆操做,实际上都是一次对代码仓库的完整备份。

许多这类系统均可以指定和若干不一样的远端代码仓库进行交互。籍此,你就能够在同一个项目中,分别和不一样工做小组的人相互协做。 你能够根据须要设定不一样的协做流程,好比层次模型式的工做流,而这在之前的集中式系统中是没法实现的。

git诞生史记

不少人都知道, Linus 在1991年建立了开源的 Linux ,今后,Linux 系统不断发展,已经成为最大的服务器系统软件了。

Linus 虽然建立了 Linux,但 Linux 的壮大是靠全世界热心的志愿者参与的,这么多人在世界各地为 Linux 编写代码,那 Linux 的代码是如何管理的呢?

事实是,在2002年之前,世界各地的志愿者把源代码文件经过 diff 的方式发给 Linus,而后由 Linus 本人经过手工方式合并代码!

你也许会想,为何 Linus 不把 Linux 代码放到版本控制系统里呢?不是有 CVSSVN这些免费的版本控制系统吗?由于 Linus 坚决地反对 CVSSVN,这些集中式的版本控制系统不但速度慢,并且必须联网才能使用。有一些商用的版本控制系统,虽然比 CVSSVN 好用,但那是付费的,和 Linux 的开源精神不符。

不过,到了2002年,Linux 系统已经发展了十年了,代码库之大让 Linus 很难继续经过手工方式管理了,社区的弟兄们也对这种方式表达了强烈不满,因而 Linus 选择了一个商业的版本控制系统 BitKeeperBitKeeper 的东家 BitMover 公司出于人道主义精神,受权 Linux 社区无偿使用这个版本控制系统。

安定团结的大好局面在2005年就被打破了,缘由是 Linux 社区牛人汇集,难免沾染了一些梁山好汉的江湖习气。开发 SambaAndrew 试图破解 BitKeeper 的协议(这么干的其实也不仅他一个),被 BitMover 公司发现了(监控工做作得不错!),因而 BitMover 公司怒了,要收回 Linux 社区的无偿使用权。

Linus 能够向 BitMover 公司道个歉,保证之后严格管教弟兄们,嗯,这是不可能的。实际状况是这样的:

Linus 花了两周时间本身用 C 写了一个分布式版本控制系统,这就是 Git!一个月以内,Linux 系统的源码已经由 Git 管理了!牛是怎么定义的呢?你们能够体会一下。

Git 迅速成为最流行的分布式版本控制系统,尤为是2008年,GitHub 网站上线了,它为开源项目免费提供 Git 存储,无数开源项目开始迁移至 GitHub,包括 jQueryPHPRuby等等。

历史就是这么偶然,若是不是当年 BitMover 公司威胁 Linux 社区,可能如今咱们就没有免费而超级好用的 Git 了。

git的优势

在集中式系统中,每一个开发者就像是链接在集线器上的节点,彼此的工做方式大致相像。 而在 Git 中,每一个开发者同时扮演着节点和集线器的角色——也就是说,每一个开发者既能够将本身的代码贡献到其余的仓库中,同时也能维护本身的公开仓库,让其余人能够在其基础上工做并贡献代码。 由此,Git 的分布式协做能够为你的项目和团队衍生出种种不一样的工做流程。

  • 速度快

  • 简单的设计,易用

  • 对非线性开发模式的强力支持(容许成千上万个并行开发的分支)

  • 彻底分布式

  • 有能力高效管理相似 Linux 内核同样的超大规模项目(速度和数据量)

git实现原理

从根本上来说 Git 是一个内容寻址 (content-addressable) 文件系统,并在此之上提供了一个版本控制系统的用户界面,Git 的核心部分是一个简单的键值对数据库 (key-value data store) 。 你能够向该数据库插入任意类型的内容,它会返回一个键值,经过该键值能够在任意时刻再次检索 (retrieve) 该内容。

初始化的git目录

当在一个新目录或已有目录执行 git init 时,Git 会建立一个 .git 目录。 这个目录包含了几乎全部 Git 存储和操做的对象。 如若想备份或复制一个版本库,只需把这个目录拷贝至另外一处便可。

$ ls -F1
HEAD
config*
description
hooks/
info/
objects/
refs/

这是一个全新的 git init 版本库,这将是你看到的默认结构。

  • description 文件仅供 GitWeb 程序使用,咱们无需关心。
  • config 文件包含项目特有的配置选项。
  • info 目录包含一个全局性排除(global exclude)文件,用以放置那些不但愿被记录在 .gitignore 文件中的忽略模式(ignored patterns)
  • hooks 目录包含客户端或服务端的钩子脚本 (hook scripts)
  • objects 目录存储全部数据内容。
  • refs 目录存储指向数据(分支)的提交对象的指针
  • HEAD 文件指示目前被检出的分支
  • index 文件保存暂存区信息。

git对象模型

全部用来表示项目历史信息的文件,是经过一个40个字符的 (40-digit) “对象名”来索引的,对象名看起来像这样:

6ff87c4664981e4397625791c8ea3bbb5f2279a3

你会在Git里处处看到这种“40个字符”字符串。每个“对象名”都是对“对象”内容作 SHA1 哈希计算得来的,( SHA1 是一种密码学的哈希算法)。这样就意味着两个不一样内容的对象不可能有相同的“对象名”。

这样作会有几个好处:

  • Git 只要比较对象名,就能够很快的判断两个对象是否相同。
  • 由于在每一个仓库 (repository) 的“对象名”的计算方法都彻底同样,若是一样的内容存在两个不一样的仓库中,就会存在相同的“对象名”下。
  • Git 还能够经过检查对象内容的 SHA1 的哈希值和“对象名”是否相同,来判断对象内容是否正确。

对象

每一个对象 (object) 包括三个部分:类型,大小和内容。大小就是指内容的大小,内容取决于对象的类型,有四种类型的对象:"blob""tree""commit""tag"

  • “blob” 用来存储文件数据,一般是一个文件。

  • “tree” 有点像一个目录,它管理一些“tree”或是 “blob”(就像文件和子目录)。

  • 一个“commit”只指向一个"tree",它用来标记项目某一个特定时间点的状态。它包括一些关于时间点的元数据,如时间戳、最近一次提交的做者、指向上次提交 (commits) 的指针等等。

  • 一个 “tag” 是来标记某一个提交 (commit) 的方法。

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

Blob对象

image

一个 blob 一般用来存储文件的内容。

Tree 对象

image

一个 tree 对象能够指向一个包含文件内容的 blob 对象, 也能够是其它包含某个子目录内容的其它 tree 对象,它通常用来表示内容之间的目录层次关系。 Tree 对象、blob 对象和其它全部的对象同样,都用其内容的 SHA1 哈希值来命名的;只有当两个 tree 对象的内容彻底相同(包括其所指向全部子对象)时,它的名字才会同样,反之亦然。这样就能让Git 仅仅经过比较两个相关的 tree 对象的名字是否相同,来快速的判断其内容是否不一样。

Commit对象

commit 对象指向一个 tree 对象,而且带有相关的描述信息。

image

一个提交 commit 由如下的部分组成:

  • 一个 tree 对象:tree 对象的 `SHA1签名, 表明着目录在某一时间点的内容。

  • 父对象 (parent(s)): 提交 (commit) 的SHA1签名表明着当前提交前一步的项目历史。合并的提交 (merge commits) 可能会有不仅一个父对象。若是一个提交没有父对象,那么咱们就叫它“根提交" (root commit) ,它就表明着项目最初的一个版本 (revision)。 每一个项目必须有至少有一个“根提交"(root commit)。

  • 做者 (author) :作了这次修改的人的名字,还有修改日期。

  • 提交者(committer):实际建立提交(commit)的人的名字, 同时也带有提交日期。
  • 注释:用来描述这次提交。

注意:一个提交(commit)自己并无包括任何信息来讲明其作了哪些修改; 全部的修改(changes)都是经过与父提交(parents)的内容比较而得出的。 值得一提的是, 尽管git能够检测到文件内容不变而路径改变的状况, 可是它不会去显式(explicitly)的记录文件的改名操做(能够看一下 git diff )。

通常用 git commit 来建立一个提交 (commit), 这个提交 (commit) 的父对象通常是当前分支 (current HEAD) ,同时把存储在当前索引 (index) 的内容所有提交。

对象模型:

若是咱们把它提交 (commit) 到一个 Git 仓库中, 在 Git 中它们也许看起来就以下图:

image

你能够看到:每一个目录都建立了 tree对象 (包括根目录), 每一个文件都建立了一个对应的 blob对象。最后有一个 commit 对象 来指向根 tree 对象 (root of trees) , 这样咱们就能够追踪项目每一项提交内容.

标签对象:

image

一个标签对象包括一个对象名(SHA1签名), 对象类型, 标签名, 标签建立人的名字(tagger), 还有一条可能包含有签名(signature)的消息.


回到咱们的问题


强大的git分支

有人把 Git 的分支模型称为它的必杀技特性,也正由于这一特性,使得它 从众多版本控制系统中脱颖而出。

Git 保存的不是文件的变化或者差别,而是一系列不一样时刻的文件快照。

在进行提交操做时,Git 会保存一个提交对象(commit object)。知道了 Git 保存数据的方式,该提交对象会包含一个指向暂存内容快照的指针。 但不只仅是这样,该提交对象还包含了做者的姓名和邮箱、提交时输入的信息以及指向它的父对象的指针。首次提交产生的提交对象没有父对象,普通提交操做产生的提交对象有一个父对象,而由多个分支合并产生的提交对象有多个父对象,

当使用 git commit 新建一个提交对象前,Git 会先计算每个子目录的校验和(40 个字符长度 SHA-1 字串),而后在 Git 仓库中将这些目录保存为树(tree)对象。以后 Git 建立的提交对象,除了包含相关提交信息之外,还包含着指向这个树对象(项目根目录)的指针,如此它就能够在未来须要的时候,重现这次快照的内容了。

Git 中的分支,其实本质上仅仅是个指向 commit 对象的可变指针。Git 会使用 master 做为分支的默认名字。在若干次提交后,你其实已经有了一个指向最后一次提交对象的 master 分支,它在每次提交的时候都会自动向前移动。

Git 是如何知道你当前在哪一个分支上工做的呢?其实答案也很简单,它保存着一个名为 HEAD 的特别指针。在 Git 中,它是一个指向你正在工做中的本地分支的指针,咱们能够将 HEAD 想象为当前分支的别名。

因为 Git 中的分支实际上仅是一个包含所指对象校验和的文件,因此建立和销毁一个分支就变得很是廉价。说白了,新建一个分支就是向一个文件写入 41 个字节(外加一个换行符)那么简单,固然也就很快了。

大多数版本控制系统它们管理分支大多采起备份全部项目文件到特定目录的方式,因此根据项目文件数量和大小不一样,可能花费的时间也会有至关大的差异,快则几秒,慢则数分钟。

而 Git 的实现与项目复杂度无关,它永远能够在几毫秒的时间内完成分支的建立和切换。同时,由于每次提交时都记录了祖先信息(parent 对象),未来要合并分支时,寻找恰当的合并基础(译注:即共同祖先)的工做其实已经天然而然地摆在那里了,因此实现起来很是容易。Git 鼓励开发者频繁使用分支,正是由于有着这些特性做保障。

分支的新建与合并

  1. 新建分支并进入

$ git checkout -b iss53

image

  1. 根据需求写代码并提交
$ git commit -a -m 'new text'

image

  1. 接到线上问题须要而且修改bug
$ git checkout master
$ git checkout -b hotfix
$ git commit -a -m 'fixed bug'

image

  1. 合并修改完bug的代码进master(暂无冲突)
$ git checkout master
$ git merge hotfix

image

  1. 解决问题后删除hotfix分支并返回原来的iss53分支继续工做
$ git branch -d hotfix
$ git checkout iss53
$ git commit -a -m 'finished'

image

  1. 合并iss53分支进主分支
$ git checkout master
$ git merge iss53

请注意,此次合并操做的底层实现,并不一样于以前 hotfix 的并入方式。由于此次你的开发历史是从更早的地方开始分叉的。因为当前 master 分支所指向的提交对象(C4)并非 iss53 分支的直接祖先,Git 不得不进行一些额外处理。就此例而言,Git 会用两个分支的末端(C4 和 C5)以及它们的共同祖先(C2)进行一次简单的三方合并计算。

此次,Git 没有简单地把分支指针右移,而是对三方合并后的结果从新作一个新的快照,并自动建立一个指向它的提交对象(C6)。这个提交对象比较特殊,它有两个祖先(C4 和 C5)。

image

image

有时候合并操做并不会如此顺利。若是在不一样的分支中都修改了同一个文件的同一部分,Git 就没法干净地把二者合到一块儿。若是你在解决问题 #53 的过程当中修改了 hotfix 中修改的部分,将会出现问题。

Git 做了合并,但没有提交,它会停下来等你解决冲突。

任何包含未解决冲突的文件都会以未合并 unmerged 的状态列出。Git 会在有冲突的文件里加入标准的冲突解决标记,能够经过它们来手工定位并解决这些冲突。

rebase 变基

最容易的整合分支的方法是 merge 命令,它会把两个分支最新的快照(C3 和 C4)以及两者最新的共同祖先(C2)进行三方合并,合并的结果是产生一个新的提交对象(C5)。:

image

可是,若是你想让 experiment分支历史看起来像没有通过任何合并同样,还有另一个选择:你能够把在 C3 里产生的变化补丁在 C4 的基础上从新打一遍。在 Git 里,这种操做叫作变基 (rebase)。有了 rebase 命令,就能够把在一个分支里提交的改变移到另外一个分支里重放一遍。

$ git checkout experiment
$ git rebase master

它的原理是回到两个分支最近的共同祖先,根据当前分支(也就是要进行变基的分支 experiment )后续的历次提交对象(这里只有一个 C3),生成一系列文件补丁,而后以基底分支(也就是主干分支 master)最后一个提交对象(C4)为新的出发点,逐个应用以前准备好的补丁文件,最后会生成一个新的合并提交对象(C3'),从而改写 experiment 的提交历史,使它成为 master 分支的直接下游

image

简单讲他就是把你的 experiment 分支里的每一个提交 commit 取消掉,而且把它们临时 保存为补丁 patch (这些补丁放到".git/rebase"目录中),而后把 experiment 分支更新 到最新的 origin 分支,最后把保存的这些补丁应用到 experiment 分支上。

如今的 C3' 对应的快照,其实和普通的三方合并,即上个例子中的 C5 对应的快照内容如出一辙了。虽然最后整合获得的结果没有任何区别,但变基能产生一个更为整洁的提交历史。若是视察一个变基过的分支的历史记录,看起来会更清楚:仿佛全部修改都是在一根线上前后进行的,尽管实际上它们本来是同时并行发生的。

rebase 的过程当中,也许会出现冲突 conflict。在这种状况,Git 会中止 rebase 并会让你去解决 冲突;在解决完冲突后,用 git-add 命令去更新这些内容的索引 index, 而后,你无需执行 git-commit ,只要执行:

$ git rebase --continue
这样git会继续应用 apply 余下的补丁。在任什么时候候,你能够用 --abort 参数来终止 rebase 的行动,而且 experiment 分支会回到 rebase 开始前的状态。

$ git rebase --abort

git merge 应该只用于为了保留一个有用的,语义化的准确的历史信息,而但愿将一个分支的整个变动集成到另一个 branch 时使用 rebase。这样造成的清晰版本变动图有着重要的价值。

全部其余的状况都是以不一样的方式使用 rebase 的适合场景:经典型方式,三点式,interactivecherry-picking

咱们使用变基的目的:是想要获得一个能在远程分支上干净应用的补丁 — 好比某些项目你不是维护者,但想帮点忙的话,最好用变基:先在本身的一个分支里进行开发,当准备向主项目提交补丁的时候,根据最新的 origin/master 进行一次变基操做而后再提交,这样维护者就不须要作任何整合工做(其实是把解决分支补丁同最新主干代码之间冲突的责任,化转为由提交补丁的人来解决。),只需根据你提供的仓库地址做一次快进合并,或者直接采纳你提交的补丁。

须要注意,合并结果中最后一次提交所指向的快照,不管是经过变基,仍是三方合并,都会获得相同的快照内容,只不过提交历史不一样罢了。变基是按照每行的修改次序重演一遍修改,而合并是把最终结果合在一块儿。

有趣的变基

  • 我在不一样的topic之间来回切换,这样会致使个人历史中不一样topic互相交叉,逻辑上组织混乱;
  • 咱们可能须要多个连续的commit来解决一个bug;
  • 我可能会在commit中写了错别字,后来又作修改;
  • 甚至咱们在一次提交时纯粹就是由于懒惰的缘由,我可能吧不少的变动都放在一个commit中作了提交。

  • rebase能够合并commit

  • rebase能够用来修改commit信息

  • rebase能够用来拆分commit

git rebase -i HEAD~3

变基也能够放到其余分支进行,并不必定非得根据分化以前的分支。

image

image

image

变基的风险

要用它得遵照一条准则:

不要在公共分支上使用rebase。

“No one shall rebase a shared branch” — Everyone about rebase

若是你遵循这条金科玉律,就不会出差错。

在进行变基的时候,实际上抛弃了一些现存的提交对象而创造了一些相似但不一样的新的提交对象。若是你把原来分支中的提交对象发布出去,而且其余人更新下载后在其基础上开展工做,而稍后你又用 git rebase 抛弃这些提交对象,把新的重演后的提交对象发布出去的话,你的合做者就不得不从新合并他们的工做,这样当你再次从他们那里获取内容时,提交历史就会变得一团糟。

注意rebase每每会重写历史,全部已经存在的commits虽然内容没有改变,可是commit自己的hash都会改变。

结论:只要你的分支上须要rebase的全部commits历史尚未被push过(好比上例中rebase时从分叉处开始有两个commit历史会被重写),就能够安全地使用git rebase来操做。

上述结论可能还须要修正:对于再也不有子分支的branch,而且由于rebase而会被重写的commits都尚未push分享过,能够比较安全地作rebase

思考下它的功能吧 git pull --rebase

相关文章
相关标签/搜索