
版本控制(又称修订控制,又称源代码控制)让你能够随时间跟踪你的文件。为什么要在意?因为当你搞砸时,你可以轻松回到之前可用的版本。
你可能已经捣鼓出了自己的版本控制系统,却没意识到它有个这么极客的名字。你有像这样的文件吗?(希望不是完全一样的这些)。
- KalidAzadResumeOct2014.doc
- KalidAzadResumeMar2015.doc
- instacalc-logo3.png
- instacalc-logo4.png
- logo-old.png
这就是我们用“另存为(Save As)”的原因。你想要新文件,又不希望毁掉旧文件。这是个常见问题,通常的解决方式如下:
- 创建一个单一备份副本(Document.old.txt)。
- 如果我们够聪明,会添加一个版本号或日期:Document_V1.txt、DocumentMarch2015.txt
- 我们甚至可能使用一个共享文件夹这样其他人就能查看和编辑文件,而无需通过电子邮件发送。希望他们在保存后重新命名文件。
So Why Do We Need A Version Control System (VCS)?
我们的共享文件夹/命名系统对于课堂项目或一次性论文来说没问题。但软件项目?绝不可能。
你觉得 Windows 源代码是放在一个像“Windows2007-Latest-UPDATED!!”这样的共享文件夹里,谁都能改?每个程序员只是在不同的子文件夹里工作?不可能。
有许多作者参与、变化迅速的大型项目需要一个版本控制系统(极客说法即“文件数据库”)来跟踪变更并避免一片混乱。一个好的版本控制系统能做到以下几点:
- 备份与还原。文件在编辑时会被保存,你可以跳转到任意时间点的状态。需要 2007 年 2 月 23 日的那个文件?没问题。
- 同步。让人们共享文件并保持最新版本。
- 短期撤销。瞎改文件结果搞砸了?(这很像你干的事,不是吗?)。丢弃你的更改,回到数据库里“最后一次已知良好”的版本。
- 长期撤销。有时我们搞砸得很严重。假设你一年前做了一次更改,而且有个 bug。跳回旧版本,看看那天做了什么更改。
- 跟踪更改。随着文件更新,你可以留下说明更改原因的消息(存储在 VCS 中,而非文件里)。这样很容易看出文件如何随时间演变,以及为什么。
- 跟踪所有权。VCS 会给每次更改打上做出更改者的名字标签。有助于
指责风暴(互相推卸责任的开会)记功。 - 沙箱化,或给自己买个保险。要做大改动?你可以在隔离区域做临时更改,在“签入”你的更改前先测试并排除问题。
- 分支与合并。一个更大的沙盒。你可以分支将代码副本复制到独立区域并隔离修改(分别跟踪更改)。之后,你可以合并将你的工作合并回公共区域。
共享文件夹快捷简单,但比不上这些功能。
Learn the Lingo
大多数版本控制系统都涉及以下概念,尽管标签可能不同。
基本设置
- 仓库(Repository,repo):存储文件的数据库。
- 服务器:存储 repo 的计算机。
- 客户端:连接到 repo 的计算机。
- 工作集/工作副本:你的本地文件目录,你在其中进行更改。
- 主干/主线:repo 中代码的主要位置。把代码想象成家谱——主干是主线。
基本操作
- 添加:首次将文件放入 repo,即用版本控制(Version Control)开始跟踪它。
- 修订版本:文件所处的版本(v1、v2、v3 等)。
- 头(最新版本):repo 中的最新修订版。
- 检出:从仓库(Repository)下载一个文件。
- 检入:将文件上传到仓库(Repository)(如果它已更改)。该文件会获得一个新的修订号,人们可以“检出(Check out)”最新的那个。
- 检入信息:描述更改内容的简短消息。
- 变更日志/历史:文件自创建以来所经历更改的列表。
- 更新/同步:将你的文件与仓库(Repository)中的最新版本同步。这让你能获取所有文件的最新修订版。
- 回退:丢弃你的本地更改,并从仓库(Repository)重新加载最新版本。
高级操作
- 分支:为私人使用(修复 bug、测试等)创建文件/文件夹的独立副本。Branch(分支)既可以是动词(“branch the code”,对代码进行分支),也可以是名词(“Which branch is it in?”,它在哪个分支里?)。
- 差异/更改/增量:找出两个文件之间的差异。用于查看修订版之间发生了什么变化。
- 合并(或补丁):将一个文件的更改应用到另一个文件,使其保持最新。例如,你可以将一个分支的特性合并(Merge)到另一个分支中。(在微软(Microsoft)这被称为反向积分(Integrate)与正向积分(Integrate))
- 冲突:当对某个文件的待定更改相互矛盾时(两种更改无法同时应用)。
- 解决:修复相互矛盾的更改,并检入(Check in)正确的版本。
- Locking:取得对某个文件的控制权,在解锁之前任何人都不能编辑它。某些版本控制系统使用这种方式来避免冲突。
- Breaking the lock:强制解锁一个文件以便你能编辑它。如果有人锁定了文件然后去度假(或者“请病假”赶在 Halo 3 发售那天),可能就需要这样做。
- Check out for edit:检出一个文件的“可编辑”版本。某些 VCS(版本控制系统)默认文件可编辑,另一些则需要显式命令。
And a typical scenario goes like this:
Aliceaddsa file (list.txt) to therepository. Shechecks it out, makes a change (puts “milk” on the list), and checks it back in with a checkin message (“Added required item.”). The next morning, Bobupdateshis local working set and sees the latest revision oflist.txt, which contains “milk”. He can browse thechangelogordiffto see that Alice put “milk” the day before.
Visual Examples
This guide is purposefully high-level: most tutorials throw a bunch of text commands at you. Let’s cover the high-level concepts without getting stuck in the syntax (theSubversion 手册is always there, don’t worry). Sometimes it’s nice tosee what’s possible.
Checkins
The simplest scenario is checking in a file (list.txt) and modifying it over time.

Each time we check in a new version, we get a new revision (r1, r2, r3, etc.). In Subversion you’d do:
svn add list.txt
(modify the file)
svn ci list.txt -m "Changed the list"
The-mflag is the message to use for this checkin.
Checkouts and Editing
In reality, you might not keep checking in a file. You may have tocheck out, edit and check in. The cycle looks like this:

If you don’t like your changes and want to start over, you canrevertto the previous version and start again (or stop). When checking out, you get the latest revision by default. If you want, you can specify a particular revision. In Subversion, run:
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
The trunk has a history ofchangesas a file evolves. Diffs are the changes you made while editing: imagine you can “peel” them off and apply them to a file:

For example, to go from r1 to r2, we add eggs (+Eggs). Imagine peeling off that red sticker and placing it on r1, to get r2.
And to get from r2 to r3, we add Juice (+Juice). To get from r3 to r4, we remove Juice and add Soup (-Juice, +Soup).
Most version control systemsstore diffs rather than full copies of the file. This saves disk space: 4 revisions of a file doesn’t mean we have 4 copies; we have 1 copy and 4 small diffs. Pretty nifty, eh? In SVN, we diff two revisions of a file like this:
svn diff -r3:4 list.txt
Diffs help us notice changes (“How did you fix that bug again?”) and even apply them from one branch to another.
Bonus question:what’s the diff from r1 to r4?
+Eggs
+Soup
Notice how “Juice” wasn’t even involved — the direct jump from r1 to r4 doesn’t need that change, since Juice was overridden by Soup.
Branching
分支(Branch)让我们将代码复制到一个独立的文件夹中,这样就能单独折腾它:

例如,我们可以为列表创建一条分支,用来放一些新的、实验性的点子:比如 Rice 或 Eggo 华夫饼这类疯狂的东西。根据版本控制系统的不同,创建分支(副本)可能会改变修订号。
既然有了分支,我们就可以修改代码并解决其中的问题。(“嗯……华夫饼?我不知道老板会怎么想。米饭是稳妥的选择。”)。由于我们在独立的分支中,可以隔离地进行修改和测试,知道我们的改动不会伤害任何人。而且我们的分支历史也处于版本控制之下。
在 Subversion 中,你只需将一个目录复制到另一个位置就能创建分支。
svn copy http://path/to/trunk http://path/to/branch
所以分支并不是一个很难的概念:Pretend you copied your code into a different directory.你大概在学校项目里也分支过自己的代码,确保有一个“保险”版本可以在事情搞砸时回退。
Merging
分支听起来很简单,对吧?其实不然——搞清楚如何将一个分支的改动合并到另一个分支可能会很棘手。
假设我们想把实验分支里的“Rice”特性合并到主线(mainline)中。该怎么做呢?对 r6 和 r7 做差异(diff)然后应用到主线?
Wrongo.我们只想应用那些改动that happened in the branch!。也就是说,我们对 r5 和 r6 做差异,然后应用到主干(trunk):

如果我们对 r6 和 r7 做差异,就会丢失主线里的“Bread”特性。这是一个微妙的点——想象从实验分支中“剥下”那些改动(+Rice)并加到主线中。主线可能还有其他改动,这没关系——我们只是想插入 Rice 特性。
在 Subversion 中,合并(merge)和差异非常接近。在主干内部,运行命令:
svn merge -r5:6 http://path/to/branch
该命令对实验分支中的 r5-r6 做差异并应用到当前位置。不幸的是,Subversion 没有简便的方法来追踪哪些合并已经应用过,所以如果你不小心,可能会把同样的改动应用两次。这是一个计划中的特性,但目前的建议是保留一条变更日志(changelog)消息,提醒自己已经把 r5-r6 合并进主线了。
Conflicts
很多时候,版本控制系统(VCS)可以自动合并对同一文件不同部分的改动。Conflicts当出现的改动不协调时就会产生:Joe 想去掉 eggs 并换成 cheese(-eggs, +cheese),而 Sue 想用 hot dog 替换 eggs(-eggs, +hot dog)。

这时候就像一场竞赛:如果 Joe 先提交(check in),那他的改动就会生效(而 Sue 就无法提交她的改动了)。
当改动像这样重叠且互相矛盾时,VCS 可能会报告一个conflict并且不让你提交——得由你来提交一个更新的版本以resolves这个困境。几种做法:
- Re-apply your changes。同步到最新版本(r4)并将你的更改重新应用到该文件:在已有奶酪的列表中添加热狗。
- Override their changes with yours。检出最新版本(r4),覆盖你的版本,然后检入你的版本。实际上,这移除了奶酪并用热狗替换了它。
冲突(conflict)不常发生,但可能很烦人。通常我会更新到最新版本,然后重新应用我的改动。
Tagging
谁能想到一个版本控制系统还能符合 Web 2.0 规范?许多系统让你可以标记(tag)(标注)任意修订版以便引用。这样你就可以说“Release 1.0”而不是某个特定的构建号:

在 Subversion 中,标签(tag)只是你约定不去编辑的分支;它们留作纪念,这样你就能确切看到 1.0 版本里包含了什么。因此它们止于一个桩(stub)——没有后续了。
(in trunk)
svn copy http://path/to/revision http://path/to/tag
Real-life example: Managing Windows Source Code
我们猜 Windows 是用共享文件夹管理的,但事实并非如此。所以这是怎么做到的?
- 有一个main line带有 Windows 的稳定构建版本。
- 每个小组(网络(Networking)、用户界面(User Interface)、媒体播放器(Media Player)等)has its own branch以开发新特性。这些正处于开发中,不如主分支稳定。
你在自己的分支中开发新特性,然后“反向集成(Reverse Integrate, RI)”把它们弄进 Main。之后,你“正向集成(Forward Integrate)”把 Main 里最新的改动带入你的分支:

假设我们在 Media Player 10 和 IE 6。Media Player 团队在自己的分支里做出了 11 版。当就绪并测试通过后,会有一个从 10–11 的补丁被应用到 Main(就像“Rice”的例子,只是复杂一点)。这是一个reverse integration,从分支到主干。IE 团队也可以做同样的事。
之后,Media Player 团队可以获取其他团队(比如 IE)的最新代码。这种情况下,Media Playerforward integrates并把 Main 里的最新补丁弄进自己的分支。这就像把“Bread”特性拉进实验分支,不过同样更复杂。
所以是 RI 和 FI。遵命。这种安排让改动在各分支间渗透,同时把新代码挡在主线之外。酷吧?
In reality, there’s many layers of branches and sub-branches, along with quality metrics that determine when you get to RI. But you get the idea: branches help manage complexity. Now you know the basics of how one of the largest software projects is organized.
Key Takeaways
My goal was to share high-level thoughts about version control systems. Here are the basics:
- Use version control.说真的,这是件好事,即便你不是在写操作系统(OS)。单是为了备份就值得了。
- Take it slow.我现在才刚开始研究我项目中的分支(Branch)和合并(Merge)。只要先学会使用版本控制,然后从那里继续就好。如果是小项目,分支/合并可能不是问题。大项目通常有经验丰富的维护者来跟踪分支和补丁(Patch)。
- Keep Learning.有很多关于SVN, CVS, RCS, Git, Perforce或你正在使用的任何系统的指南。重要的是要了解这些概念并意识到每个系统都有自己的术语和理念。Eric Sink 也有一篇详细的版本控制指南。
These are the basics — as time goes on I’ll share specific lessons I’ve learned from我的项目. Now that you’ve figured out a regular VCS,试试这篇图解分布式版本控制指南.