模板:内容

版本控制可视化指南

版本控制(又称修订控制,又称源代码控制)让你能够随时间跟踪你的文件。为什么要在意?因为当你搞砸时,你可以轻松回到之前可用的版本。

你可能已经捣鼓出了自己的版本控制系统,却没意识到它有个这么极客的名字。你有像这样的文件吗?(希望不是完全一样的这些)。

  • KalidAzadResumeOct2014.doc
  • KalidAzadResumeMar2015.doc
  • instacalc-logo3.png
  • instacalc-logo4.png
  • logo-old.png

这就是我们用“另存为(Save As)”的原因。你想要新文件,又不希望毁掉旧文件。这是个常见问题,通常的解决方式如下:

  • 创建一个单一备份副本(Document.old.txt)。
  • 如果我们够聪明,会添加一个版本号或日期:Document_V1.txt、DocumentMarch2015.txt
  • 我们甚至可能使用一个共享文件夹这样其他人就能查看和编辑文件,而无需通过电子邮件发送。希望他们在保存后重新命名文件。

那么为什么我们需要版本控制系统(Version Control System,VCS)?

我们的共享文件夹/命名系统对于课堂项目或一次性论文来说没问题。但软件项目?绝不可能。

你觉得 Windows 源代码是放在一个像“Windows2007-Latest-UPDATED!!”这样的共享文件夹里,谁都能改?每个程序员只是在不同的子文件夹里工作?不可能。

有许多作者参与、变化迅速的大型项目需要一个版本控制系统(极客说法即“文件数据库”)来跟踪变更并避免一片混乱。一个好的版本控制系统能做到以下几点:

  • 备份与还原。文件在编辑时会被保存,你可以跳转到任意时间点的状态。需要 2007 年 2 月 23 日的那个文件?没问题。
  • 同步。让人们共享文件并保持最新版本。
  • 短期撤销。瞎改文件结果搞砸了?(这很像你干的事,不是吗?)。丢弃你的更改,回到数据库里“最后一次已知良好”的版本。
  • 长期撤销。有时我们搞砸得很严重。假设你一年前做了一次更改,而且有个 bug。跳回旧版本,看看那天做了什么更改。
  • 跟踪更改。随着文件更新,你可以留下说明更改原因的消息(存储在 VCS 中,而非文件里)。这样很容易看出文件如何随时间演变,以及为什么。
  • 跟踪所有权。VCS 会给每次更改打上做出更改者的名字标签。有助于指责风暴(互相推卸责任的开会)记功。
  • 沙箱化,或给自己买个保险。要做大改动?你可以在隔离区域做临时更改,在“签入”你的更改前先测试并排除问题。
  • 分支与合并。一个更大的沙盒。你可以分支将代码副本复制到独立区域并隔离修改(分别跟踪更改)。之后,你可以合并将你的工作合并回公共区域。

共享文件夹快捷简单,但比不上这些功能。

学习术语

大多数版本控制系统都涉及以下概念,尽管标签可能不同。

基本设置

  • 仓库(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:

Alice添加(adds)a file (list.txt) to the仓库(repository). She签出(checks 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, Bob更新(updates)his local working set and sees the latest revision oflist.txt, which contains “milk”. He can browse the变更日志(changelog)或者diff看到 Alice 前一天放了“milk”。

可视化示例

本指南刻意保持高层视角:大多数教程会向你扔一堆文本命令。我们先覆盖高层概念,而不陷在语法里(Subversion 手册一直都在,别担心)。有时候see what’s possible.

签入(Checkins)

最简单的场景是检入一个文件(list.txt)并随着时间修改它。

version control checkin

每次我们检入一个新版本,就会得到一个新修订号(r1, r2, r3 等)。在 Subversion 中你会这样做:

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

这个-m标志是本次检入要使用的消息。

签出与编辑(Checkouts and Editing)

实际上,你可能不会一直检入文件。你可能得check out, edit and check in。循环看起来像这样:

version control checkout

如果你不喜欢自己的改动想重来,可以revert到上一个版本并重新开始(或停止)。检出时,默认会获取最新修订版。如果需要,你可以指定某个特定修订版。在 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)拥有随着文件演进而产生的changes历史。差异(diff)是你编辑时所做的改动:想象你能把它们“剥”下来并应用到文件上:

version control diff

例如,要从 r1 到 r2,我们加 eggs(+Eggs)。想象把那张红色贴纸剥下来贴到 r1 上,就得到了 r2。

而从 r2 到 r3,我们加 Juice(+Juice)。从 r3 到 r4,我们移除 Juice 并加 Soup(-Juice, +Soup)。

大多数版本控制系统store diffs rather than full copies of the file。这节省磁盘空间:一个文件的 4 个修订版不意味着我们有 4 份拷贝;我们只有 1 份拷贝和 4 个小差异。挺巧妙吧?在 SVN 中,我们对文件的两次修订做差异是这样:

svn diff -r3:4 list.txt

差异帮我们注意到改动(“你到底是怎么修好那个 bug 的?”)甚至能把它们从一个分支应用到另一个分支。

Bonus question:r1 到 r4 的差异是什么?

+Eggs
+Soup

注意“Juice”甚至没参与——从 r1 直接跳到 r4 不需要那个改动,因为 Juice 被 Soup 覆盖了。

分支(Branching)

分支(Branch)让我们将代码复制到一个独立的文件夹中,这样就能单独折腾它:

version control 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):

version control merge

如果我们对 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)。

version control conflict

这时候就像一场竞赛:如果 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”而不是某个特定的构建号:

version control tag

在 Subversion 中,标签(tag)只是你约定不去编辑的分支;它们留作纪念,这样你就能确切看到 1.0 版本里包含了什么。因此它们止于一个桩(stub)——没有后续了。

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

真实案例:管理 Windows 源代码

我们猜 Windows 是用共享文件夹管理的,但事实并非如此。所以这是怎么做到的?

  • 有一个main line带有 Windows 的稳定构建版本。
  • 每个小组(网络(Networking)、用户界面(User Interface)、媒体播放器(Media Player)等)has its own branch以开发新特性。这些正处于开发中,不如主分支稳定。

你在自己的分支中开发新特性,然后“反向集成(Reverse Integrate, RI)”把它们弄进 Main。之后,你“正向集成(Forward Integrate)”把 Main 里最新的改动带入你的分支:

version control branch example

假设我们在 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.

关键要点

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,试试这篇图解分布式版本控制指南.

本系列其他文章

  1. 版本控制可视化指南
  2. 分布式版本控制入门(图解)
  3. 学习 Git 时的顿悟时刻

加入 45 万月度读者

喜欢这篇文章?还有更多内容能帮你建立持久、直观的数学理解。加入通讯以获取额外内容和最新更新。
社交分享