分布式版本控制系统:Git

Git无疑是目前最流行的分布式版本控制系统。本文简要总结了Git的历史、几个比较重要的概念以及应用。

本文的示例和图片均来自《Pro Git》这本书,该书可在下面的站点在线阅读:

https://git-scm.com/book/en/v2

Git的诞生

Git是Linux kernel的创始人Linus Torvalds于2005年开发的。记得Linux Torvalds在一次访谈中说,他花了2周的时间完成了Git的开发。 速度之快实在惊人。

最开始(1991~2002)Linux kernel是通过提交补丁和文件归档的方式进行维护的。到2002年,开始使用分布式版本控制系统BitKeeper来管理和维护代码。但是到了2005年,Linux kernel社区与BitKeeper公司之间的关系破裂,BitKeeper公司收回了Linux kernel社区免费使用BitKeeper的权利。于是Linux kernel社区就基于BitKeeper的经验开发了自己的分布式版本控制系统:Git。

分布式

Git是一种分布式的版本控制系统,在本地有完整的历史信息(存储在.git目录中),所以在Git中绝大多数操作只需要访问本地的文件,因此速度极快。即使在没有网络连接的情况下,也可以正常提交代码,因为只是提交到本地的仓库(repository)中。这是SVN这种集中式版本控制系统无法做到的。

快照(snapshot)

许多其他版本控制系统(如:CVS、SVN、Mercurial)是存储一组文件以及每个文件随时间逐步积累的一个个改变,如下图所示:

而Git是通过快照(snapshot)的方式存储数据,如下图(虚线框表示文件内容与上一个版本相比没有发生改变)。这种方式使得Git在处理分支时非常高效,后面会进一步阐述。

三个区域和三种状态

Git项目有三个区域:Git仓库、工作目录和暂存区域。Git仓库目录就是.git目录,存储项目元数据(metadata)和各种对象(objects)的地方,是Git最重要的部分。

关于Git objects的机制,可以参考下面的链接:

https://git-scm.com/book/en/v2/Git-Internals-Git-Objects

工作目录就是从Git仓库中提取出来的内容,我们可以直接查看和修改的就是工作目录中的文件。

暂存区域是包含在.git目录中的一个索引文件,存储将要进入下一次提交(commit)的信息。

相对应的,Git中每个文件可能处于三种状态之一:已提交(committed)、已修改(modified)和已暂存(staged)。已提交表示数据已经保存在了git仓库中了。已修改表示数据已经被修改,但还没有保存到git仓库中。已暂存表示数据已经修改并且已经标记,会包含在下一次提交的快照中。

三个区域与三种状态的关系图示如下:

分支(branch)和提交对象(commit object)

每当执行"git commit"命令时,会产生一个提交对象,该对象中包括指向当前项目快照的指针以及parent commit对象的指针。当然,对于首次提交(commit),是不存在parent commit对象的。

项目快照其实就是每一个目录(包括所有子目录)以及文件的校验和构成的树形结构。

当多次提交之后,每个提交对象包含一个指向其parent commit对象的指针(有时会有多个parent commit对象),图示如下:

Git的分支,本质上就是指向提交对象的可变指针。默认分支的名字是master。

master分支并不是一个特殊的分支,它与其它分支没有什么区别。只是git init命令默认会创建master分支而已。

例如,当用“git branch testing”创建一个分支后,分支testing和master就指向同一个提交对象:

注意:HEAD是一个特殊的指针,它指向当前所在的本地分支,因为"git branch testing"只是创建了testing分支,但并没有切换过去,所以HEAD还是指向master分支。当执行“git checkout testing”之后,HEAD指针就会指向testing分支:

可以通过一条命令创建分支并切换过去:

git checkout -b testing

如果在testing分支上稍作改动,再提交一次,那么HEAD分支就随着向前移动:

由此可见,在Git中创建分支只是创建一个可以移动的指针(初始指向上游分支(默认就是当前分支)的最后一个commit对象);而切换分支就使简单的移动HEAD指针而已。所以Git中处理分支是非常高效的

git commit --amend

这条命令用于修改最近的一次提交。之所以把这个命令单独拿出来讲,是因为它很实用。例如假设你刚提交了一个修改,但刚提交后你发现有一个拼写错误,于是你又修改了一次,重新提交了一次:

$git commit -m "fix a typo"

这样自然可以解决问题,但当你查看历史时,会发现有两次提交。这时--amend选项就派上用场了。修改完拼写错误后,用下面的命令提交,那么第二次提交会替代第一次提交,所以最终只会看到一个提交。

$git commit --amend

注意,这条命令只能修改最近的一次提交,意味着从上次提交到目前,你还没有做任何修改。

分支merge

Git允许并行开发,自然就会涉及到分支的合并。假设你正在一个分支上修复一个minor bug,突然有一个critical bug需要马上修复。所以你需要暂停当前分支上的工作,重新基于master创建一个分支来修复critical bug。当critical bug完成之后,就可以合并修改到master分支。下图中,hotfix就是修复critical bug的分支,iss53就是minor bug的分支。合并hotfix到master的命令如下:

$git checkout master

$git merge hotfix

合并之后,就可以将hotfix分支删除了:

$git branch -d hotfix

hotfix分支可以很容易地合并到master中,因为master分支只是fast-forward到hotfix的最近的commit对象。

合并完hotfix分支之后,回到之前的修复minor bug的分支(iss53)继续工作,工作完之后,同样需要合并iss53和master分支。但这时与之前的合并稍有不同,因为master对应的commit对象并不是iss53的直接祖先了,所以这时合并,master不会是简单的fast-forward了,而是采用三方合并(three-way merge)的机制。所谓三方,就是两个分支最新的快照,以及他们共同的祖先。图示如下:

合并命令与之前的相同。这次合并之后,会生成一个新的快照,并自动创建一个新的提交对象指向该快照,如下图所示:

注意,如果合并过程中发生冲突(conflict),那么需要手动解决冲突,然后运行命令git add来标记冲突已解决。

rebase

通常在整合多分支时,采用merge的方式,如上节所述。其实还有另一种方式,就是rebase。用一句话概括,rebase就是将一个分支上的所有修改都移到另一个分支上。

同样,以例子来说明。假设目前有下面的分支:

如果按照上节的merge方式将experiment分支的修改合并到master中,就用如下命令:

$git checkout master

$git merge experiment

合并之后,图示如下:

如果采用rebase的方式,则命令如下:

$git checkout experiment

$git rebase master

其原理也很简单,首先找到两个分支的共同祖先(该例中master和experiment的共同祖先就是C2),提取当前分支(experiment)相对于该共同祖先的历次修改,保存为临时文件。然后将当前的分支指向目标分支master,最后在目标分支上依次重新应用所有的修改。rebase之后图示如下。

最后切换到master分支,进行一次快速合并即可:

$git checkout master

$git merge experiment

也许有人会问,为什么rebase之后还要做merge?道理很简单,rebase只是对分支experiment进行操作,master分支没有任何改变,所以需要对master分支进行一次合并。由于rebase之后,experiment分支对于master来说已经没有了分叉,看来就是一条直线,所以master只需进行fast-forward的merge。

rebase的好处显而易见,它使分支看起来更加干净整洁,因为它将分叉变成了一条直线。所以rebase之后,合并分支到master就很容易。从另一方面看,由于rebase改写了分支的历史,也招来一些批评,所以要谨慎使用。这里有一条原则:不要对你仓库之外的分支进行rebase。意思就是说,如果你已经将分支push到远程服务器上,已经分享给别人之后,就不能再对分支进行rebase了。因为别人可能已经基于你的分支进一步做了一些修改,这时如果你对分支做了rebase,那么别人不得不重新整合你的修改。对别人会造成很大的困扰。

Github

Github无疑是目前最流行的源代码托管提供商。这里只是简单描述如何参与到一个开源项目中去。也就是如何向一个开源项目贡献自己的修改。

大致流程如下(假设你已经有了一个Github帐号,并设置好了SSH公钥):

1、fork目标项目;

在项目的首页,有一个按钮“Fork”,点击该按钮即可。

2、创建一个分支;

3、在分支完成你要做的修改;

4、将分支push到Github上;

5、创建一个合并请求(pull request);

在Github页面上有一个“Compare & pull request”按钮,点击该按钮,并填写标题以及描述即可。

6、讨论,然后继续修改;

7、项目owner合并或者关闭你的pull request。

数据恢复

在Git中, 任何数据只要commit到了仓库(repository)中,那么任何无意中删除的数据都可以找回来。秘诀就是使用"git reflog"命令。每一次commit操作,git都会记录下HEAD改变时的值。所以通过git reflog可以知道曾经做过的任何操作。

所谓数据丢失,其实就是某个commit不可达(unreachable),所以所谓数据恢复,就是让这些不可达的commit重新变成可达(reachable)。所以数据恢复的方法就是先通过git reflog获得需要恢复的commit的hash值,然后创建一个分支来指向这个commit:

假设需要恢复的commit的hash值前几位是484a592

$git branch recover-branch 484a592

更详细的信息,可以查看下面的链接:

https://git-scm.com/book/en/v2/Git-Internals-Maintenance-and-Data-Recovery

--END--

本文中的内容,都摘自《Pro Git》这本书,当然做了一定的总结和加工。如果读者有时间并且也有兴趣,鼓励自行在线阅读这本书:

https://git-scm.com/book/en/v2/

由于时间和精力有限,本文只是总结并列出了我认为比较重要的一些方面,欢迎留言补充和纠正。

  • 发表于:
  • 原文链接http://kuaibao.qq.com/s/20180501G0XDT300?refer=cp_1026
  • 腾讯「云+社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 yunjia_community@tencent.com 删除。

扫码关注云+社区

领取腾讯云代金券