VCS版本控制图解指引-A Visual Guide to Version Control

以下为我们翻译的”VCS(Version Control System)的图解说明”所参考的说明版本控制文章, 这篇文章采用图解说明,清楚且容易了解,征得作者的同意,在此将他的文章翻译为中文, 原文的网址为

http://betterexplained.com/articles/a-visual-guide-to-version-control/

版本控制(又称Revision Control或Source Control) 让您随时追踪档案的内容变动.为什么你要关心这档事? 这样当你搞砸时可以很简单的回复前一工作版本.你也许自己编造了版本控制系统, 却没发现你在用如下”天才”的档案名称. 你有这样的档案吗? (但愿..你没有完全和下面相同的…..)

  • KalidAzadResumeOct2006.doc
  • KalidAzadResumeMar2007.doc
  • instacalc-logo3.png
  • instacalc-logo4.png
  • logo-old.png

这就是为何我们都用“Save As”. 你想要新档不会将旧档盖掉. 这是常发生的问题, 解决方法经常像这样:

  • 做个独立备份拷贝 例如:Document.old.txt.
  • 如果我们够聪明, 我们加个版本号码或日期: 例如:Document_V1.txt, DocumentMarch2007.txt
  • 我们甚至会用共享目录, 这样其他人可以不用e-mail传档即可看到或编辑档案. 希望他们记得重取档案名再存回去.

所以呢…为什么我们需要Version Control System (VCS)?

运用分享的目录/命名系统对于典型的项目或一次完成的论文还可以. 可是对于软体开发的项目呢? 没有什么机会行得通的…
你可以想像在分享的档案夹中的Windows source code像“Windows2007-Latest-UPDATED!!”给大家来编辑吗? 那么每位程式设计师只会在各自的子档案夹工作? 不可能这么做的.
大型的, 同时多人参与编辑, 快速变更的项目需要一个版本控制系统(Version Control System )- “file database”的专业说法, 来追踪所有变更以避免混乱. 一个好的版本控制系统(VCS)应可满足以下需求:

  • 备份与复原. 档案在编辑时被储存, 你可以跳回之前的每一时刻所编辑的版本. 如你需要回溯到2007年2月23日的档案…没问题.
  • 同步. 让大家分享档案且随时可取得最新的版本.
  • 短期的版本回复. 随意玩弄档案或搞砸时? (你就是有可能如此, 不是吗?). 就把所做的变更扔掉吧, 并回到版本库(repository)中所知的”好”版本.
  • 长期的版本回复. 有时我们真的搞得太糟了. 假设你是在一年前做了这个不良的修改,产生了bug. 这系统应能帮你跳到那个版本, 并了解当时做了什么变动.
  • 追踪变动. 当更新档案时, 你可以留下为何变更的纪录(此要存在VCS,不是在档案里). 这样可以容易的看出这个档案随著时间的演进与原因.
  • 追踪负责人. VCS可以标出每个变更的人名,已示负责.
  • 沙盒(SandBoxing), 或自我的保险. 正在做大幅的变动吗? 你可以暂时到独立的区域做此变动, 测试, 将问题解开, 然后才将你的变动提交(check in)回去.
  • 分支与合并. 更大的沙盒(sandbox). 你可将你的程式码拷贝一份branch(分支)到独立的区域做修改, 并独立追踪在上面的变更. 过些时候, 再将你的工作成果合并到主要共同开发的区域.
分享的目录虽然快又简单, 但无法做到以上所说的这些功能.

学习相关用语

大多数的版本控制系统牵涉以下的观念, 有可能用词有点不同.
基本设定

  • 版本库(Repository or repo): 储存档案的数据库.
  • 伺服器: 版本库所在的计算机.
  • 局端(Client): 与版本库连线的计算机.
  • 工作组合或工作备份(Working Set/Working Copy): 你局端(Client)上的档案目录,与你工作编辑变更的地方.
  • 主开发线(Trunk/Main): 在版本库里存放程式码的主要地方. 将程式码想像为一个族谱(family tree)–”trunk”即为主支.

基本动作

  • 新增: 将一个档案第一次放进版本库, 也就是说开始用版本控制来追踪此档案
  • 修订版本(Revision): 一个档案位于哪个版本 (v1, v2, v3, etc.).
  • 版头(Head): 存在版本库的最新版本.
  • 签出(Check out): 从版本库下载一个档案.
  • 签入/提交(Check in): 如档案内容变动时,将档案上载到版本库. 此档案将获得新的版本号码, 别人将可”check out”最近的版本.
  • 签入/提交说明: 说明做了什么变动的简短讯息.
  • 变更历史纪录: 从一个档案成立起所经历的所有变更列表.
  • 更新/同步: 将你局端计算机中的档案与版本库的最新资讯做同步. 此可让您撷取到所有最新的档案.
  • 回复(Revert): 抛弃你局端计算机上的变更并叫出上次从版本库签出最新的档案版本.

进阶动作

  • 分支(Branch): 制作分开的档案/档案夹备份以便独立运作(解bug, 测试等等). 分支可说是动词(分支源码)也可说是名词(在哪一个”分支”?).
  • 差异分析(Diff)/变更/差异部分(Delta): 找出两的档案的差异. 可帮忙看出版本间的变动内容.
  • 合并/补钉(Merge/Patch): 将一个档案的变更运用到另一个, 使其为最新内容. 譬如, 你可以合并一个分支到另一个分支或是主开发线做功能的整合. (在Microsoft, 此称为 Reverse Integrate and Forward Integrate)
  • 冲突(Conflict): 当一个档案的变更与其他变更发生冲突时, 两者无法同时采用各自的变更.
  • 解决(Resolve): 解决变更的冲突问题并签入(check in)正确版本
  • 上锁(Locking): “掌控” 某档案至你解锁(unlock)此档案其他人才能编辑此档. 有的版本控制系统有这样的功能以避免同一档案中变更的冲突.
  • 强力解锁(Breaking the lock): 强迫解锁某一个档案以便编辑. 这个功能也许在某人把哪个档案锁起来后度假去了(或当Halo 3游戏发行那天”请病假”).
  • 签出编辑: 签出(Checking out)一档案的“可编辑”版本. 有些版本控制系统原始设定即有可编辑的档案,有的需要清楚的指令.
典型的剧情可发展如下:
Alice 新增一个档案(list.txt)到版本. 她将此档案签出,做了些变动(把 “milk”放到列表中),然后将此签入/提交, 同时写签入的说明 (”加入需要的项目.”). 隔天早上, Bob 更新 他计算机的工作备份, 看到最新list.txt的版本, 此含有“milk”. 他可以浏览变更历史纪录是做差异分析 看到Alice在前一天加了 “milk” .

视觉化案例

在此故意给您高阶的说明:大多的教学指引都给你一堆文字指令. 让我们走过高阶的观念,不在指令语法上打转( Subversion手册 永远都在那里, 不用担心). 有时去试探到底有什么可能性总是不错的.

签入/提交(Checkins)

最简单的情境是签入一个档案(list.txt)且花时间去修改它.

每次我们签入一个档案的新版, 我们就会得到一个新修订版(new revision-每个revision的内容含多个档案at different version) (r1, r2, r3, etc.). 在Subversion你将会做:

svn add list.txt
(modify the file)
svn ci list.txt -m "Changed the list"

此 -m 旗标(flag) 使用来做此签入的说明.

签出与编辑

实作上, 你有可能不是一直签入档案. 你应该是签出, 编辑签入. 这样的周期看起来如下:

如果你不喜欢你做的变更, 想重头来的话, 你可以回复到前一版本再重新来过(或就此停止). 当签出时, 原始设定让你取得最近的修订版本(revision). 如果你想要的话, 你可以指定所要的修订版(revision). 在Subversion, 执行:

svn co list.txt (get latest version)
...edit file...
svn revert list.txt (throw away changes)

svn co -r2 list.txt (check out particular version)

差异(Diffs)

主开发线(trunk)有档案变更的历史纪录. 差异是你编辑(editing)的变更内容 : 想像你可以 “剥下” 他们然后将他们放入一个档案:

例如, 从 r1 到 r2, 我们加入eggs (+Eggs). 想像剥出红标然后将其放入r1, 以变成r2.
又如要从 r2到 r3, 我们加入 Juice (+Juice). 由 r3 到 r4, 我们去掉 Juice 并加入 Soup (-Juice, +Soup).

大多数的版本控制系统 储存差异(DIffs)而非 所有档案的内容. 如此可以省磁碟空间: 4个版本并非指有 4个复制; 我们有1个复制和4个小小的差异. 很简洁, 是吧?

SVN, 我们如下做两个修订版(revisions)的差异:

svn diff -r3:4 list.txt

差异帮助我们看到变更内容(”你如何再次修正bug?”) 甚至用到一个branch和其他的比较.
加码问题: r1 到 r4的差异(diff)是?

+Eggs
+Soup

请注意“Juice”甚至完全没出现 — 若直接从 r1 跳到 r4 根部不需要此变更, 因为 Juice 被Soup盖掉了.

分支(Branching)

分支让我们拷贝程式码到一个独立的档案夹随意修改运用:

譬如, 我们可以开一个分支来实验新想法: 疯狂的事情像加入Rice和 Eggo waffles. 要看你用的是什么版本控制系统,有的系统在你开分支时会变动版本的号码. 好了, 现在我们有一个分支, 我们可以变更我们的程式码并实验我们的怪怪想法. (“嗯..waffles? 我不知道老板会怎么想. 打赌用Rice应该安全才对) 由于我们是在分开的分支上行事, 我们可以独立修改或测试, 看看这些修改会不会伤害其他的部份. 且在分支里的修改都有版本控管的历史纪录.

在Subversion,你可以很简单地将一个目录做个copy另取名字, 就可以开一个branch了.

svn copy http://path/to/trunk http://path/to/branch

如此分支不是个太艰深的观念: 假装你已拷贝你的程式码到另一个目录. 你有可能已将学校的项目的程式码做了分支, 确保你有一个安全的环境尝试失败版本以便如果搞砸了可以回复没问题的版本.

合并(Merging)

分支听起来很简单,对吧? 不过, 不见得喔 — 解决如何将一个分支的变更合并到另一个可以说很伤脑筋.

让我们假设要将 “Rice” 从我们实验性的分支并入主开发线(mainline). 我们要如何做呢? 做 r6 和 r7的差异分析(diff)然后将差异并到主线?

错错错. 我们只想取用在分支上做的变动. 意思是我们做 r5和r6的diff, 然后将diff送到主开发线 (main trunk):

如果你做r6 和 r7的差异分析, 我们将漏掉 “Bread” (译者注:因为r6和r7的diff将为-Bread, +Rice, 将这个diff加到r7, r8将少了Bread),而”Bread”应该在主开发线上. 这是精巧之处 — 想像从实验性branch“剥出”的变更(+Rice)并加进去主线. 主线上也许有别的变更, 如此做就没问题了-我们只是插入Rice功能.

在Subversion, 合并和差异分析非常相近. 请在主线上, 执行以下指令 :

svn merge -r5:6 http://path/to/branch

此在实验性的分支上所做的指令diffs r5-r6并将此diff加到目前所在. 不幸地, Subversion并没有简单的方法来追踪已做过那些合并,如果你不小心, 有可能会重并同样的变更. merge tracking为SVN改善计画中的功能, 目前我们劝你做好变更记录,提醒自己”r5-r6变动已合并到主开发线了”. (译者注:SVN1.5后已经开始支援merge tracking功能)

冲突(Conflicts)

大部分的版本控制系统可以自动合并一个档案不同地方的变更. 当有些变更显然无法黏合时冲突就出现了: Joe想要去掉eggs以cheese替代(-eggs, +cheese), 而Sue 想要以hot dog 替代eggs(-eggs, +hot dog).

此时竞赛产生: 若 Joe 先签入, 系统可自然接受此变更 (而后Sue就无法做她要的变更了).

当变更的地方相同且有像这样的冲突时, 版本控制系统将可能呈报冲突讯息(conflict)而不让你签入— 此时如果你是Sue, 你可以决定是否要签入一个解决(resolves)两难的新版本. 处理方法可为:

  • 重新导入你的变更. 先同步到最近的版本(r4)然后再到此最新版做你要的变更: 到此已经有cheese的列表加入hot dog.
  • 以你的变更去覆盖别人的变更. 将最新的版本(r4)签出,将你的版本内容拷贝过去 , 再签入. 结果, 这将把cheese换成hot dog.

虽然冲突不会常常发生, 但蛮恼人的. 通常我采用先同步到最近版本再将我的变更加进去.

标籤(Tagging)

谁曾想过版本控制系统竟然有Web 2.0的观念? 很多系统让你在任一想要的版本贴标籤tag(label)以方便参考. 如此你可以说参考”“Release 1.0″而不用指出在系统内特有的建置号码(build number):

在Subversion, 标籤跟分支编辑方法类似,只要你想要做即可执行; 此功能存在乃为了清楚之后所有版本的发展, 所以你可以完全清楚看出在 version 1.0的发布版本中内容为何. 且且就终止于此. 没有别的地方可去了. (译注: 也就是说标籤具有”冻结”的作用, 不会看到不相干的资讯)

(in trunk)
svn copy http://path/to/revision http://path/to/tag

实作的案例: 管理Windows开发程式

我们猜想Windows开发可运用其自己的分享档案夹来管理它的开发程式, 不过事实并非如此. 那么, 它是如何做的呢?

  • 有个主开发线, 此处存有稳定建置的Windows.
  • 每一个功能群组(网路, 使用者介面, 多媒体播放器, 等等.) 有他们自己的分支发展他们各自的功能. 相较于主开发线这些分支为正在开发中且较不稳定的发展内容.

你在新功能的分支开发,然后用“Reverse Integrate (RI)” 并回主线. 之后, 你 “Forward Integrate”且从主线拿到最近的变更并入你的分支:

 

假设我们在 Media Player 10 和 IE 6. Media Player 团队在他们的分支做了version 11. 当其完成并通过测试, 产生从10到11的patch, 此被主线采用 (就像 “Rice” 的例子, 不过复杂些 ). 此为reverse integration, 从branch到trunk. IE团队也可同样执行.

之后, Media Player 团队可以取得其他团队最近更新的程式码, 如IE的. 如此, Media Player forward integrates 并从主线获得最近的patches整合回他们的branch. 这有点像之前的例子去拉主线的”Bread”到实验性的branch, 但, 也是比较复杂.

所以这就是所谓的RI和FI. 哼哼, 这样的安排可以让变更先在分支互相协调, 也同时让新的程式码不干扰主线的稳定度. 酷吧 ?

事实上, 可以有很多层次的分支和副分支, 配合品管度量来决定你何时要做RI. 不过要知道: 分支帮助您处理复杂度. 现在你知道一个大型的软体项目基本上是如何组织了吧.

重点提示

我的目标是分享有关版本控管系统的高阶想法. 以下为基础要点: :

  • 使用版本控制. 认真告诉你,这是个好东西,就算你不是写OS那么大的东西. 就算单人用也值得.
  • 从小处著手(Take it slow). 我也是现在才开始为我的项目了解如何做分支和合并. 才开始要运用这样的功能来管理. 如果你的项目还小, 分支和合并可能不是个问题. 维护大项目的人应该对分支和补丁的追踪很有经验了.
  • 保持学习. 可以参考很多指引: SVNCVSRCSGitPerforce 或更多你正在使用的系统资料. 重要的是把观念搞清楚并意识到每个系统有它自己的行话和哲理. 也建议参考Eric Sink的 detailed version control guide.

以上是基础资料 — 随著时间我将分享我从做my projects学到的东西. 现在你已了解一般的版本控制系统(VCS),也可以看看 an illustrated guide to distributed version control (DVCS分布式版本控制系统图解说明).

译注: 如有兴趣了解简便安装与管理Subversion的工具, 欢迎参考 CodeBeamer+Subversion实务操作手册

发表评论

电子邮件地址不会被公开。 必填项已用 * 标注

*

您可以使用这些 HTML 标签和属性: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>