对比SVN学习GIT版本管理工具
 

2009-12-11 作者:刘旭晖 来源:刘旭晖的blog

 

因为近期工作需要,要掌握git的使用方法,所以决心花点时间学习一下它的各种使用方法,就当是花点时间磨刀吧。所以写这篇文档的目的主要还是为了自己能够系统的学习和理解GIT应用的方方面面,因为之前对SVN算是比较熟悉,所以决定以概念对比的方式来整理这篇文章,尽管,有些地方两者无法直接比较 8 )此外,主要的目的还是为了方便自己积累相关的git使用技巧。文中理解有偏差的地方,还请大家指正。

1 概述和参考资料

1.1 GIT相关

http://www.kernel.org/pub/software/scm/git/docs/

这里面包括了 tutorial 基本的操作 / core-tutorial 底层命令的使用 / user’s manual 完整的用户手册以及其它各种资料,如果你看完了,我的这篇文档你也就不用看了。

1.2 SVN相关
http://svnbook.red-bean.com/ 这里是一份online的svn book

或者也可以看tortoiseSVN的帮助文档

2 仓库的组织结构及相关概念

仓库的组织管理形式这部分,应该说是版本管理工具设计上最核心的内容。对于仓库的内部管理机制,我了解得很少,只能从外部的表象上做一些简单的比较。

SVN属于中心式的仓库管理,完整的仓库数据,统一维护在服务器端的(当然,服务器也可以就是你的本机了)仓库中,对于客户端来说,本地取得的数据不是完整的仓库,只是仓库中特定版本的部分或全部数据,同时,客户端还负责维护本地数据的变更情况,在客户端并不拥有仓库完整的历史数据。本地的工作树和仓库是相对独立的。

对于Git来说,应该属于分布式的仓库管理,倒不是说仓库的内容分散在不同的server上,只是对仓库而言,没有中心仓库之说,所有的仓库都是平等的。对于一个仓库的不同工作拷贝,每个都拥有完整的历史数据,工作树和仓库基本是合二为一的。

在SVN中,从仓库checkout的一个工作树,每个子目录下都维护着自己的.svn目录,记录着该目录中文件的修改情况以及和服务器端仓库的对应关系。所以SVN可以局部checkout部分路径下的内容,而不用checkout整个分支。

Git仓库中,项目根目录下的.git目录统一管理了所有的仓库数据和当前工作树的相关信息。

在SVN中,默认采用FSFS的数据库格式,任何提交都是一个版本的递增,所谓分支,tag等概念都只是仓库中不同路径上的一个对象或索引而已,和普通的路径并没有本质的区别。在工作树中,可以同时checkout多个分支的内容。

在Git中,其内部的对象层级依赖关系或许和SVN类似,但是其工作树的视图表现形式和SVN完全不同。工作树永远是一个完整的分支,不同的分支由不同的head索引去构建,你不可能在工作树中同时获得多个分支的内容。

3 基本操作

3.1 仓库创建初始化

在SVN中,仓库本身的管理和日常应用,使用的是两套不同的命令。仓库的创建和备份维护等使用的命令是 svnadmin, 使用svnadmin create来创建一个新的仓库

在git中,创建一个新的仓库,可以在一个空目录下,使用git init来实现,它将创建一个.git目录用来维护仓库数据。

在SVN中,创建仓库的地方并不是你日常使用的仓库的地方,你需要在别的地方checkout出特定的仓库路径作为你的日常工作的目录。在git中,仓库所在的目录也就是你的日常工作目录,没有服务器端和客户端之分。(严格的说 .git目录才是仓库,.git目录外的地方是你的工作目录,对于bare project来说,只有git目录下的内容,工作目录离得内容还是要checkout出来的)

3.2 Checkout仓库

在SVN中,使用SVN checkout(co)来checkout本地或远程仓库的代码

而对于git来说,尽管也有checkout命令,但是由于你需要在本地拥有仓库,所以通常从服务器上checkout代码的第一步是使用git clone来获取一个仓库的拷贝,默认的git clone操作同时还会checkout一份远程仓库上当前active的分支

在SVN中,其仓库的管理形式决定了你可以只checkout仓库中特定路径/分支下的子目录,而不是整个仓库,而git只能checkout整个分支。

3.3 将文件纳入版本管理

在SVN中,使用SVN add,这样在以后的commit过程中,每次在提交数据之前,svn都会自动根据这些add过的对象的修改情况,构建一个commit tree。

在git中,因为存在index的概念,要将一个文件纳入版本管理的范畴,首先是要用git-update-index –-add将文件纳入index的监控范围,只有更新到index中的内容才会在commit的时候被提交。另外,文件本身的改动并不会自动更新到index中,每次的任何修改都必须重新更新到index中去才会被提交。 当然,通常会用git add这样的封装脚本来调用git-update-index

3.4 检查当前状态

SVN Status 可以显示当前working tree的文件修改状态

在git中 git status 命令显示当前index的状态和working tree的状态。

3.5 提交文件

Git commit操作在git命令中属于相对简单的,需要注意的一点就是上面提到的,只有index中的内容才会比提交。

3.6 删除文件

在使用Svn rm删除一个目录的时候,因为每个目录下都存在.svn目录,记录了这个目录于服务器端仓库相关的信息,所以在commit之前,目录里的其它文件会被删除,但是目录及其子目录并不会被真正删除,只有commit以后,目录才会被删除。

在git中,同样,使用git rm 删除文件。但是git对目录的处理有些奇怪,如果某个目录下的所有文件都被删除以后,该目录就会被自动删除,也就是说你无法保留一个空的目录。你也无法添加一个空目录到仓库里。也就是说git 自动忽略空目录,不知道这样做的目的是什么?

3.7 查看log

svn log命令基本上就是用来查看版本提交时的所填写的log信息

git log可以做的事情会多很多,毕竟git log是对底层核心命令的再包装,通过它,不仅可以查看log信息,还可以输出特定版本的具体变更内容等等信息。

3.8 版本回溯

在SVN中,不提供任何从仓库中删除对象的机制,任何的修改都会导致版本的递增,所以,如果想丢弃一个修改,你需要做的事是反向diff你的修改,再提交一个新的版本。

在git中提供了重置committed tree对象索引的机制,所以,你可以通过例如git-reset这样的操作将当前分支的版本恢复到以前的某个状态。经常看见的例子就是回溯一个版本,然后修改内容,再次提交。不过这样做搞不好很容易出问题。包括在git-push之类的操作时会被reject,需要强行push之类的。

如果只是想放弃一个修改,git的文档推荐使用git-revert操作,这个操作基本上和SVN的思路是一样的了,就是提交一个新的版本将需要revert的版本的内容再反向修改回去,版本会递增,不影响之前提交的内容。

3.9 放弃当前修改

在SVN中,使用SVN revert对目录或文件操作都可以将当前工作树上特定路径的修改恢复到服务器上的版本,放弃当前的修改。

Git中,对特定文件使用不带其它参数的git checkout命令可以将文件恢复到index中的状态,如果你想恢复的特定的版本,那么类似: git checkout HEAD file这样的操作,将文件恢复到HEAD tree即最近一次提交的状态。

不过git checkout有个问题,不知道是否是故意这样设计的,就是即使用git rm删除的内容,如果没有提交,git checkout以后也会恢复,包括它在index中的状态。这点有些不理解。 理论上index上已经记录这个删除操作,不应该恢复才对。

Git中还有一种办法,可以快速彻底的放弃自从上次commit以来的所有变更,git reset –hard HEAD

3.10 代码合并

git merge能够自动记住以前merge过的位置和状态,这个比较容易理解,因为通过每个分支的head commit可以跟踪它的对象索引关系。另外,因为其对象管理机制的原因,只能以commit为单位,merge整个分支的所有修改。不能有选择的merge部分路径下的修改。Merge的时候要求index和HEAD是一致的,如果merge成功,内容会直接commit,而工作树上的修改仍会保持。(如果失败,会在工作树上将需要merge的内容和你已有的修改合并,大概不是你所希望的,所以最好不要这样做)

merge特定分支的特定版本之前的所有修改,可以通过merge那个版本对应的rev来实现,merge某一段版本区间的修改,考虑到commit需要完整的代码树关系,估计靠git merge来做是没有办法了,需要自己diff / patch代码来实现

SVN的Merge操作不会记住它的merge历史,换句话说,你可以多次merge同一份代码,但是他的好处是你可以自由的选择merge哪一部分、哪一段版本之间的代码,应该说他基本等同于是diff和patch的组合。不过因为SVN没有index的概念,所以merge的操作会和当前working tree上的修改合并在一起。

关于历史信息方面,因为svn的merge实际是patch文件内容本身,所以,不同分支上的历史信息不会在merge以后的主干上体现出来,而git的merge,如果没有冲突的话,实际是merge commit树的继承关系,所以,所有的历史信息在merge以后的commit中都能够被索引到。

3.11 获取单纯的代码

在svn中,如果不需要任何历史信息,只想要某个版本纯粹的代码(经常会有这种需求,这样做本地数据比较小) 那么,使用svn export命令即可以实现。

在git中,似乎没有这样的命令,不过,由于git的本地仓库信息完全维护在project根目录的.git目录下,(不像svn一样,每个子目录下都有单独的.svn目录)。所以,只要clone,checkout然后删除.git目录就可以了。

4 协同工作和权限控制

4.1 远程提交

对于SVN来说,由于是中心式的仓库管理形式,所以并不存在特殊的远程提交的概念,所有的commit操作都可以认为是对远程仓库的更新动作。

在git中,因为有本地仓库和remote仓库之分,所以也就区别于commit 操作,存在额外的push命令,用于将本地仓库的数据更新到远程仓库中去。这种工作模式应该是大多数开源项目的维护者的工作模式之一。

git push 可以选择需要提交的更新的分支以及制定该分支在远程仓库上的名字。

4.2 远程更新

在SVN中,因为只有一个中心仓库,所以所谓的远程更新,也就是svn update

对于git来说,别人的改动是存在于远程仓库上的,所以git checkout命令尽管在某些功能上和svn中的update类似(例如取仓库特定版本的内容),但是在远程更新这一点上,还是不同的,不属于git checkout的功能涵盖范围

Git使用git fetch和git pull来完成远程更新任务,fetch操作只是将远程数据库的object拷贝到本地,然后更新remotes head的refs,git pull 的操作则是在git fetch的基础上对当前分支外加merge操作。

4.3 多分枝协同工作

SVN中,我很喜欢的一个功能就是switch,使用Switch可以在同一个工作树上,对不同的模块checkout不同分支上的代码。 举个例子: 我从主干上checkout了整个内核树,然后使用switch命令将其中一个或几个驱动的目录或文件切换到我的个人分支或其它人的分支上去,这样,我可以使用一个update命令同时从几个不同的来源更新特定的文件,而我在工作树上对switch过的文件做的修改会自动提交到我的个人分支上,而不是主干的路径上。这样我的修改不会影响主干的内容,而同时又能随时更新主干上的最新内容。不仅方便工作,也有利于权限控制。一切都是自动的,方便!

在Git中,尽管也可以使用checkout命令checkout 特定分支的特定文件到当前分支的工作树上, 但是,这只是简单的更新当前工作树的文件内容而已,这些文件并不会被关联到他的来源上去,也就是说你做的任何修改,还是针对当前分支的。

对于多分枝协同工作,我所见到的常见的工作模式是fetch远程更新,然后merge到当前分支。这对于维护会冲突的不同版本和快速切换局部分支显然还是有所不足的。

这种情况或许和git的分布式仓库结构和整体设计思路有关,或许这样有利于保持所有开发者之间的代码的同步,但是总觉得这是个遗憾,这方面没有深入的再去研究,或许通过borrow object的方式可以部分实现类似SVN的switch的功能?(也或许要实现多分枝协同工作,在Git中还有其它不同思路的更巧妙的办法?) 哪位高手知道解决办法的还请不吝赐教!

git submodules 看起来是为了解决类似多个有依赖关系的模块的协同工作问题。不过用起来似乎有不少限制和麻烦。

4.4 权限控制

对于git协同工作时的权限控制,还没有仔细研究,不知道能否像SVN那样,通过Apache的用户账号形式,对每一个用户精确控制到文件级别的读写权限。 目前初步查找了一下,看来似乎没有这样的功能,不知道设计的初衷是什么,对于小组开发来说或许会比较麻烦?

这个与开源精神应该没有太直接的关系才对,因为很多时候,其实权限控制的目的倒不是纯粹为了限制对代码的Access,主要还是为了减少代码冲突,减少误操作等情况的发生。

5 GIT常见问题和操作

5.1 恢复丢失的版本

丢失版本最常见的问题就是 比如使用了 git reset –hard HEAD^之类的操作,结果发现丢弃的版本还想恢复回来,但是已经没有任何分支能够reference到这个commit了。幸运的是,git 对各个分支的head还有一份log记录叫做reflog,你可以在.git/logs/refs/head/ 目录下看到他们。 通过 git reflog 可以显示变更历史。使用类似 master@{1} master@{“2 days ago”}之类的格式,就能索引到你想要的commit。例如对应于git reset –hard HEAD^ 使用 git reset --hard HEAD@{1}即可恢复到reset之前的commit上。

5.2 修改最近一次commit的内容

如果只是想对最近一次的提交做一些变更,但是不想在commit tree上递增版本的话,可以使用git commit –amend来实现,在现有的基础上做任何你想做的变更,然后用带–amend参数的commit命令提交即可。基本上,这个操作近似等同于于以下操作:

$ git reset --soft HEAD^

$ ... do something else to come up with the right tree ...

$ git commit -c ORIG_HEAD

和git reset之类的操作类似,对于已经push的内容,最好是不要做这些回滚的操作,因为实际上,原先commit的head还是存在的,新的head和以前的head没有继承关系,在协同工作的时候容易产生一些问题(我猜想主要是merge相关的操作吧,因为merge是根据对象的继承关系来自动判断需要merge的内容的,对于已经merge过的分支被回滚以后,可能无法自动区别识别出这部分内容应该如何处理)


火龙果软件/UML软件工程组织致力于提高您的软件工程实践能力,我们不断地吸取业界的宝贵经验,向您提供经过数百家企业验证的有效的工程技术实践经验,同时关注最新的理论进展,帮助您“领跑您所在行业的软件世界”。
资源网站: UML软件工程组织