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/
由于时间和精力有限,本文只是总结并列出了我认为比较重要的一些方面,欢迎留言补充和纠正。
领取专属 10元无门槛券
私享最新 技术干货