在开发和维护软件项目时 svn和git是目前最常见的两种版本控制系统,但是二者的设计思路和适用范围又有很大不同,下面是对二者使用介绍和区别的整理。

SVN简介

Subversion(svn)是一个开源的版本控制系統,在2000年由CollabNet Inc开发,现在发展成为Apache软件基金会的一个项目。关键字:商业集中式

  • checkout
    将远程服务器的svn目录检出到本地目录。
  • switch
    将当前目录切换至另一个svn分支。
  • add
    向版本库中添加新的追踪文件。
  • commit
    将本地修改提交到远程仓库。
  • update
    从远程仓库拉取最新提交并合并到本地。
  • merge
    合并其他分支的指定提交至本地目录。
  • revert
    在本地目录回滚指定提交或回滚到某一次提交。
  • copy
    拷贝一个文件或目录到版本库,相当于创建分支。
  • status
    查看本地当前目录的修改状态。
  • clean-up
    递归清理工作拷贝,删除未完成的操作锁定。

Git简介

git也是一个开源的版本控制系统,最初版本于2005年由大名鼎鼎的Linus Torvalds开发完成,最早是为了管理Linux内核开发而开发的一个开放源码的版本控制软件,目前由软件自由保护组织(SFC)维护。关键字:敏捷开源分布式

  • clone
    git clone命令用于从远程仓库获取完整仓库到本地,对应svn的checkout操作。
  • checkout
    • git checkout "branch"切换到某一个分支,对应svn的switch操作。
    • git checkout -- "file"放弃工作区对应文件的修改,对应svn的revert本地文件的操作。

      注意这里的checkout相当于只检出指定文件,丢弃本地修改只是该操作的”副作用”。

  • add
    git add命令用于添加指定目录或文件到暂存区,使用-A参数时会同时跟踪指定目录或文件,此时对应svn的add操作。
  • commit
    git commit命令用于将暂存区的内容提交到仓库区。
  • pull
    git pull命令用于拉取远程仓库的提交,并与本地分支合并,对应svn的update操作。
  • push
    git push命令用于将本地仓库的提交推送到远程仓库。可以简单理解为git commit + git push等价于svn commit
  • merge
    git merge命令用于将某一分支的所有commit合并到当前分支。

    如果需要像svn一样只merge指定的提交可以使用git cherry-pick命令

  • branch
    git branch命令是一系列分支相关操作,包括查看、新建、删除本地(远程)分支。
  • reset
    使用git reset命令可用于回退工作区、暂存区、本地仓库的修改。当用于回退当前分支的指针到指定commit时,该操作十分危险,因为在撤销commit的时候会修改提交历史,并且是不可逆的撤销,reset操作本身也是不可追溯的。

    在多人协作开发场景下要注意,尽可能不要使用reset命令来回退公共分支,因为之后其他人拉取远程仓库时也会把自己的本地仓库回退到之前的版本,这会导致额外的工作量甚至造成冲突。reset正确的实践方式是只作用于本地(还没被推送至远程仓库)的改动,如果需要回滚一个公有的commit,应当使用git revert,因为它正是为了这个场景而设计的。

  • revert
    使用git revert命令用于撤消对仓库提交历史的更改,与reset命令不同的是,revert操作将反转指定提交的更改,并创建一个新的”还原提交”。

    revert命令相对于reset命令来说更安全,因为它不会重写历史(Rewriting History),通过创建新的commit来回滚可以避免丢失历史记录,保证了项目的每一次修改的历史记录的完整性。

  • tag
    git branch命令是一系列tag相关操作,包括查看、新建、删除本地(远程)tag。
  • status
    git status命令用于显示当前工作区和暂存区相对于上次commit的修改,对应svn的status操作。
  • log
    git log命令用于显示当前分支当前目录的版本历史,对应svn的log操作。
  • clean
    git clean -f命令用于从工作区删除未追踪的文件,如在build后清理工作区。

    git clean -f命令经常和git reset --hard一起被执行,reset仅仅影响已追踪的文件,因此需要git clean来单独清理未追踪的文件,这两个命令相结合可以让工作区回滚到一个特定的commit的确切状态,但是这也是git中唯一一个具有潜在威胁的永久地删除提交的命令,所以要谨慎使用。

二者比较

  • git是分布式的版本控制系统,而svn是集中式版本控制系统,这是二者最大的区别。在集中式版本控制系统中,版本库是存放于中央服务器的,保存着所有文件的修订历史版本,所以svn必须联网才能工作;在分布式版本控制系统中,每个克隆(clone)的版本库都是一个完整备份,包含所有历史版本,并且互相之间是平等的,不存在中央服务器(即去中心化),这也是git受开源社区(如github)欢迎并迅速发展起来的原因之一,在离线环境下,仍然可以使用git。

    尽管git的分布式带来了很多好处,但是向每个人同步最新的提交互相推送就很麻烦了,所以在实际项目中常常会约定一个中间节点作为远程仓库(上游仓库),这个节点可以是私有的服务器,也可以使用互联网上其他厂商搭建的托管平台(如github、gitlab)。所有人都从远程仓库拉取历史版本,然后所有人的修改也都推送到远程仓库来同步给其他人。但这样做并不意味着变成集中式版本控制系统了,这样只是为了方便多人协作,本质上远程仓库和自己本地仓库仍然是互相平等的,保存的内容也没有任何区别,将它作为中间节点是基于共识和约定的,但是没有这个中间节点同样可以进行协作,只是比较麻烦而已。

  • 从分支(branch)这个角度来看,分支和tag在svn中的概念很弱,本质只是版本库中和原文件夹拥有相同修改历史的另一份文件夹拷贝(通过copy命令创建),如果有人想要创建新的svn分支或tag,也必须在中央仓库中创建,因为svn以文件夹为基本单元来管理,所以分支管理和文件夹管理没什么区别,svn可以对任意文件夹进行权限管理,感觉svn的设计有种大道至简的哲学;而在git的设计中分支是一等公民,但是分支非常轻量级,一个分支仅仅是对一个commit的引用,并且每个工作成员可以在离线状态下任意在自己的本地版本库创建、修改和删除分支,完全不需要担心影响其他人员,并且分支的切换也相当快速。但是权限管理方面git比较弱,只能以分支为基本单元进行简单的管理。
  • 在内部实现方面,大多数的版本控制系统(如cvs、svn)都更在乎仓库内具体文件内容的差异,但是git在设计上更关注仓库的整体性是否有改变,这是git的最重要的设计哲学之一。例如修改svn管理的文件并提交之后,svn中央仓库只会生成两个版本所修改文件之间的差异数据;修改git管理的文件并提交后,git仓库默认会直接将变化后的文件直接拷贝为新的blob,并被本次提交产生的快照所引用,而对于没有发生改变的文件,快照会引用其旧的blob。所以一旦需要查看某版本直接将版本快照引用的blob覆盖到本地即可,而其他版本控制系统需要根据差异数据做merge才能得到对应版本的文件,因此git切换分支的速度非常快,这是一种空间换时间的策略。在整个仓库的视角下,可以认为git的diff颗粒度更大,将单个文件作为最小操作单元,而svn的diff颗粒度小到字节层面。

    如果git仅使用这种全量保存的策略,显而易见会随着时间的推移占用大量的存储空间,git为此也做了许多优化:首先git仓库中commit、tag、tree、blob的内容要经过压缩后,才会被保存为git对象(object),并保存在文件系统中;另外git提供了git gc指令,作用是只保存当前最新版本的所有完整文件(因为新版本使用较多),之前版本只保存相对于该最新版本的diff,达到优化存储空间的目的。git本身也会在变动文件过多时按照上述策略自动权衡时间和空间利用率进行优化存储,保证存储空间和加载速度的平衡。

  • svn有一个根据提交时间递增数字的全局版本号,这是因为svn是集中式版本控制系统,理所当然容易实现版本号的连续性;而git使用40位长的哈希值作为版本号,因为每个人的提交都是各自独立完成的,没有先后之分(即使提交有先后之分,也由于push/pull的方向和时机而不同)。另外git版本库可以被任何人克隆,如果没有安全性的设计,克隆版本库后伪造提交数据再克隆给其他人,那将是版本控制的灾难。使用SHA1摘要作为版本号,将版本号也作为校验码,同时解决了版本号唯一性和提交数据的安全性这两个问题。一但数据被伪造,将造成和SHA1摘要的版本号不符。

    git可以使用从左面开始任意长度的字串作为简化版本号,只要该简化的版本号不产生歧义,一般采用7位的短版本号。