Git核心功能探秘

来源:互联网 发布:淘宝修改店铺名称 编辑:程序博客网 时间:2024/05/19 21:43

        本文立足分布式版本控制工具Git基本功能,阐述这些功能的含义、应用场景,并注重和其他传统版本控制工具在相似功能上的横向比较,以使对Git不甚了解或者刚刚使用Git的读者对Git的基本功能,以及Git本身能有更深入的理解。

Git本质与特征

        Git最初是用于Linux操作系统内核开发使用的版本控制工具,在这里不再对其历史做过多介绍,你只需知道Git和大名鼎鼎的Linux是由同一个作者开发的。

        Git与我们经常使用的SVNCVS版本控制工具在使用方式上有很大不同,这种不同本质上是由Git的体系结构决定的。Git是分布式版本控制工具的典型代表。

        这里需要澄清一点,Git更准确的定位应该是对等式的分布式版本控制工具,因为SVNCVS版本控制工具实际上都是分布式系统,只不过客户端和服务器形成了一种常见的集中式体系结构,因此我们常常忽略其分布式系统的本质。相比之下,采用对等式体系结构的Git显得更加“分布式”。

        具有对等式体系结构的Git软件既有客户端又有服务器端的功能,并不区分两者。这样,参与开发的每个开发者的电脑上既是Git的客户端,又是版本库的提供者,即服务器端。每个开发者电脑上都有自己的版本库,每个开发者既可以从其他的某个远程版本库获取代码来更新自己的版本库,也可以把自己的版本库推到远程的某个版本库中,来更新远程的版本库。

        在本地的版本库和远程的版本库发生通信之前,传统的add操作、commit操作等都是针对本地自己的版本库的。因此可知Git至少比传统版本管理工具有如下优势:

1、每个开发者都有自己的本地版本库,这样避免了传统版本控制工具服务器节点因为崩溃而造成的数据损失;

2、传统的add操作commit操作等在本地库进行,因此速度更快。

        然而,对等式的Git也带来了一个显著的问题。假设有三个或更多的开发者共同开发一个软件,每人都安装Git且有自己的本地版本库,当某个人想更新自己的版本库,应该选择远程其他开发者的哪一个开发者的版本库来更新呢?

        面对这个问题,一般有三种解决方法,而这三种解决方法也正好形成了Git的三种典型应用方式:

1、类集中式:专门使用一个服务器作为Git主版本库,所有开发者自己的版本库与主版本库同步。这种方式适合已经用惯了SVN的开发团队,以及项目规模较大的开发团队。

2、管理员式:指定开发者中的一个(可以是开发经理)为主版本库,其他所有开发者与主版本库进行同步,拥有主版本库的开发者兼任配置管理员。这种方式避免了使用额外的配置服务器,但是拥有主版本库的开发者有事请假的话就有点儿小杯具了。

3、纯对等式:没有所谓主版本库的概念,采用这种方式一般团队规模较小并且每个开发者都有清晰的功能模块分工。本地版本库要从其他哪个远程版本库同步代码取决于依赖的代码是由哪个开发者开发的。这种方式下每个开发者的版本库都是整个项目的一部分,最终在项目结束时要把各个版本库mergerebase成一个统一的软件,这个工作量较大。

        在以下的论述中默认采用第一种类集中式的Git使用方式,讨论如何在Linux上搭建Git中心服务器。需要指出的是,如果你开发的是开源软件,则可以直接使用Github上的版本库,Github就相当于一个免费的开放版本托管库。

Git环境构建与通信

        Git环境构建比较简单,首先需要各个开发者电脑和主版本库服务器上安装Git软件即可,如果使用Eclipse 4.*开发软件,则开发者电脑上连Git软件都无需安装,因为Eclipse自带的Git插件已经足够了。

        开发环境中的主版本库服务器使用的是中标麒麟Linux操作系统,首先到Rpm Search网站上搜索并下载GitRPM安装包,git-1.7.8.2-2.el5.rf.i386.rpmperl-Git-1.7.8.2-2.el5.rf.i386.rpm,将其安装:

# rpm -Uvh git-1.7.8.2-2.el5.rf.i386.rpm perl-Git-1.7.8.2-2.el5.rf.i386.rpm

        为Git创建新用户并为指定密码:

# useradd git

# passwd git

        建立一个Git版本库目录/git并初始化一个空项目版本库ProjectA

# mkdir -p /git/ProjectA.git

# chown -R git:git /git

# su – git

$ cd /git/ProjectA.git

$ git –bare init

        在开发者电脑上创建本地版本库的过程类似。在创建完本地版本库后,首先要使用git add命令将代码加入到本地版本库的管理中(即stage),然后对加入的代码commit,即执行一次快照(snapshot)。对于一个项目中的各个代码来说,如果后续对文件进行了修改,则先进行add操作,然后执行commit。虽然在CVSSVN中也有相同的Addcommit操作,但是Git官方文档中首次对这两个操作进行了形象的比喻,add被喻为stage过程,就是搬上舞台,commit操作被喻为snapshot,即照相,而舞台指的就是本地的版本库。现有工程的所有代码只有加入版本库才能接受版本控制工具的管理,即搬上舞台(add)。而搬上舞台(add)的操作需要在作出修改后进行。搬上舞台(add)之后可以在合适的时候照相(commit),因为不照相就不会为以后留下任何痕迹,搬上舞台(add)操作便没有任何意义。而照相(commit)操作可以执行多次,只要你觉得这一刻有意义值得记录下来。

        经过addcommit操作,本地的版本库中有了项目版本信息,下面要将本地库中的信息更新到主版本库中。Git可以使用多种协议进行版本库之间的通信,其中githttphttps协议一般都是只读的,对远程版本库进行读写的开发操作一般选择ssh协议。很多使用Git的开发者会在使用ssh之前创建RSA密码对实现无密码的ssh操作,实际上我认为没有太大必要,因为所有开发者使用一个git密码进行ssh操作也未尝不可。

        首先为远程主版本库URI起一个名字:

# git remote add main_base ssh://10.1.50.4:/git/ProjectA.git

然后将本地版本库的master分支上的更新push到远程主版本库的对应分支:

# git push master main_base

        这里一个比较重要而又常常被忽略的问题是URI格式。每个开发者经常能看到URI格式,但是实际上URI格式定义还是比较复杂的,如果在格式上不注意,就很有可能无法与远程版本库通信,而又不容易找出原因,颇有点儿阴沟帆船的意味。比如,如果远程版本库是WindowsD盘的git/ProjectA.gitURI该如何写呢?写成如下形式是错误的:

ssh://10.1.50.4:D:/git/ProjectA.git

        正确的写法应该是这样滴:

ssh://10.1.50.4:/D/git/ProjectA.git

        令人遗憾的是,错误的写法貌似更加符合人们的习惯,可惜URI是一个中立的标准,不会为Windows特有的路径写法妥协。关于URI的详细格式参考RFC 3986

Git核心功能解惑

        上文中对Git addcommit操作作了解释,下面将继续论述Git中的其他核心功能的含义和应用场景。

Diff

        Git Diff和传统的Diff功能类似,查看版本间的不同,但是Git Diff功能更强大。Git Diff的主要功能包括:

1、查看指定文件和之前任意commit中的差异;

2、查看整个工程与之前任意commit中的差异;

3、查看任意两个分支的差异。

Reset

        Git Reset就是回滚的命令,这个功能很常用,可以将版本库中的指定分支回滚到指定的commit处。Git Reset在回滚时提供两个选项—soft--hard--soft是指回滚时保留当前还没有提交的更新,而--hard是指回滚到指定commit处,不保留任何更新。

        Git Reset的功能远比通常想象的要复杂,如果需要了解Git Reset的其他功能,可以查阅官方文档。

Stash

        Git Stash的功能是暂时保存上次commit之后到目前的所有工作,然后将版本库回滚到上次commit之后的状态,等到完成所需操作后再恢复保存的工作。

        Git Stash一个主要应用场景是上一个Commit中的bug修改。当我们在上一个commit的基础上继续工作时,突然发现一个上次的commit代码有个bug,因此我们要停止现有的工作修改这个Bug。但是我们不能再现在的工作基础上修改这个Bug,因为目前的工作只进行了一半,程序很可能无法运行,或者无法正常运行,所以修改完bug后无法测试。

        这时就需要Git Stash命令暂时保存上次提交到现在的工作,然后回滚到上次提交处,等Bug修改完并且测试通过后,再将暂时保存的信息取回并应用到版本库的当前分支中。

        Git Stash保存的工作内容在Git栈中,可以查看Git栈中暂存的工作内容,并恢复指定的工作内容,还可以删除Git栈内容(在恢复之前一般没人这么做)。

Branch

        Git Branch命令提供传统版本控制工具中都会提供的分支管理功能。在刚刚创建Git版本库时默认只有一个分支,即Master主分支。在一个分支中的各个commit在前一个commit的基础上建立,一个commit依赖之前的commit,一个分支下的commit就像一个单链表一样串连起来。如果在master分支的某个commit开始创建另一个分支branch,就意味着branch分支上的各个commitmaster分支上之后创建的commit并行进展,没有直接的依赖关系。

        Git的分支功能特别适合在一个项目上开发各个独立发布的子系统,或者功能特性,也非常方便基于一个项目开发衍生产品。分支也可以用来发布一个特性,来让开源社区开发者实现,之后合并到主分支;也可以是一个Bug修改分支。分支最常用的功能是开发一个新版本,但是之前的版本需要继续维护。

        Git有一个当前的工作分支,这个当前的工作分支就是HEAD所指向的分支。Git分支的功能包括分支的创建、工作分支的转换、分支合并等。此外,Git独有的功能是整合远程分支,也就是说Git不只是管理本地库中的各个分支,还包括远程的版本库分支,可以合并远程分支和本地分支。

Checkout

        Git中的Checkout可以说和CVS中的Checkout完全两码事。Git中的Checkout有两个功能:

1、创建一个分支,并转到新创建的分支下,例如:

$ git checkout -b newBranchName

2、回滚指定文件到指定的commit处,这与reset不同,reset是将整个分支回滚。例如下面将file文件回滚到master分支的第二个commit处:

$ git checkout master~2 file.exe

MergeRebase

        MergeRebase功能类似,都是用于将另一个分支中的所有更新应用于当前分支,唯一区别是分支合并后的Git分支结构有所不同。

        例如一个Master分支经过两次commit后,在其基础上创建了一个feature分支,用于开发软件的一个功能特性。之后masterfeature分支都各自经历了几次commitGit分支结构如下所示:

        假设当前分支为master分支,则git merge feature命令会把feature分支的所有更新与master分支合并,并在合并时commit一次,合并后保留之前的maserfeature分支,合并之后变成一个分支。merge后的Git分支结构如下所示:

        Rebase功能也是将feature分支的所有更新应用到当前的主分支上,与Merge不同的是,Rebasefeature分支的所有commit转换为master上的commit,这些commit将应用在feature分支出现之后master的所有commit之前。完成feature内容应用到master分支之后,feature分支将被删除,之后版本库将只有一个master分支,就好像feature分支从未出现过一样。Rebase后的Git分支结构如下所示:

        从合并之后的分支结构图中可以看出Rebase的合并更加简洁,Rebase也使得Git的结构变得更加简单。但是,这并不意味着Rebase可以取代Merge,而是RebaseMerge都有适合自己的使用场景。Rebase适合Feature分支工作量较小的场景,feature分支的开发的小功能将合并到主分支,之后也不需要在维护或者更新feature分支,这样rebase将保持Git版本库的结构简洁;但是,如果Feature分支工作量较大,commit版本较多,而且合并之后也有可能在Feature的各个commit之上修改Bug,在这种情况下适合Merge操作,而使用Rebase则会消除Feature分支,这显然不合适。

FetchPull

        Git FetchPull命令都是负责从远程版本库的某个分支上获取信息并同步到本地库的指定分支,这两个命令有些许差别。

Fetch命令从远程版本库的中获取指定分支的所有更新的commit,但是不会自动和本地库中的分支进行合并,而是由配置管理员手动选择这些远程更新的处理方式,merge或者rebase

        而Pull命令则可以理解为Fetch+Merge,当然,默认的合并命令merge也可以改为rebase,因此PullFetch更加简洁。Git colon命令实际上就是先创建本地库和主分支,然后再把远程分支Pull到本地库中。

Push

        Git push命令把你当前工作分支中的更新推到远程版本库的指定分支中。但是需要注意的是,如果你的这次push和上一次push之间远程分支被其他人更新过的话,你的这次push将被拒绝,你必须首先fetch或者pull,让自己的本地库更新并处理冲突,之后才能执行push操作。

Submodule

        Git Submodule功能是一项传统版本控制工具没有的功能,该功能实现了在一个项目中引入其他公开Git项目作为子项目。在一个项目中引入子项目的目的是该项目对子项目有依赖,但是所依赖的子项目也在开发过程中。如果没有submodule功能,就只能把所依赖的子项目代码做成类库并引入当前的项目中,但是如果所依赖的子项目升级或者更新时,我们不得不手动重新打包类库并替换之前的类库,同时执行pullpush操作同步远程和本地版本库。有了submodule命令,我们可以分别不受影响的开发项目和项目所依赖的子项目。

        实际上,从目前情况来看,普通开发者很少使用Submodule自己开发依赖库,即使要开发依赖库,也可以直接建立两个并列的版本库,或者两个分支。主要原因是传统的如SVN工具中没有类似的功能,该功能要广泛使用尚需时日。

        然而,Submodule真正的意义在于对开源的推动。虽然很少有人用该功能自己开发依赖库,但是却有越来越多的人从GitHub上引入子项目作为依赖,这个功能实际上也可以认为是一种更方便的使用GitHub上的第三方代码于自己的项目中的方法。以快速简便的方式直接引入并管理第三方代码,这个功能的确火了GitHub,更火了开源。

结论

        本文论述主要论述了两方面的内容:Git的本质特性和深入分析Git的各项功能含义。在论述Git的各项核心功能时,并没有罗列介绍具体功能的使用和命令格式,更侧重这些功能的含义、意义和使用场景。因为理解对于刚开始使用Git的开发者最重要的,只要深入理解了Git的各个功能,具体如何使用这些功能并不复杂,无非是几个命令。

        目前Git的使用者数量貌似还比不过传统的SVN那样多,但是可以预见,本地化操作、更少的远程的交互和丰富的命令,以及庞大的GitHub都是Git快速成为主流的保证,使用Git绝对是一种高端大气上档次的选择。

        现在先写下对Git的功能和使用方面的理解,以后有机会再研究一下Git内部实现。

参考资料

[1] Scott Chacon. Pro Git. Apress, 2009.

[2] Andrew Burgess. Geting Good with Git. Rockable Press, 2010.


原创粉丝点击