工作以来,使用的版本控制工具一直是 SVN(Subversion)。在 Windows 下使用的是 SVN 的图形界面工具,在 Linux 系统下,使用的是 SVN 命令行。虽然接触 SVN 已经有几年历史了,但是很多时候为了完成稍微复杂一些的日常任务,总是不得不去网上查找,这显得不是那么 酷炫
。所以这次下定决心,好好学习 SVN 命令的基础用法。
本篇文章将覆盖日常工作流程中对 SVN 的基本用法,不涉及 SVN 服务器的配置以及仓库管理等高级话题。
SVN 基础理论
SVN 是一个版本控制工具,用来跟踪文件的版本变化。版本控制工具的核心概念是仓库(repository),它是系统数据的集中存储,通常以目录树的形式组织信息。客户端连接到仓库后,可以读写文件。
SVN 使用的是 copy-modify-merge
版本控制模型。客户端和 SVN 仓库建立连接后,可以创建项目文件的本地副本,每个人在自己的本地私有版本上进行修改,最后将自己的私有副本合并到 SVN 仓库。在合并的时候,可能会出现修改冲突,此时需要个人手动解决冲突问题。
SVN 同时也提供 lock-modify-unlock
的版本控制模型,主要是方便对非文本文件进行版本控制。
SVN 仓库在接受 SVN 客户端提交的修改(commit)后,会创建文件系统树的一个新状态,并用唯一的一个自然数标识,该数字称为 revision
。新创建的 SVN 仓库初始 revision 为 0,此后随着客户端的每次提交, SVN 仓库的 revision 将逐渐递增。注意,SVN 的 revision 是应用于整个仓库,而不是独立的文件。所以 foo.c
的 revision 为 5 真实含义为 svn 仓库为 revision 5 时 的 foo.c。
SVN 本地副本(working copy
)是本地系统中的目录树。当然 SVN 本地副本中也包含一些由 SVN 创建和维护的文件,每个本地副本下都包含一个子目录 .svn
,该目录也被称为 SVN 的 管理目录
。对于本地副本的每个文件,SVN 都会记录两个信息:
- 该文件基于的 revision
- 该文件上次从仓库更新的时间戳
通过以上信息,以及对比 SVN 仓库,本地副本的文件可以有以下四种状态:
Unchanged, and current
:文件本地未修改,SVN 仓库中的对应文件也未被修改Locally Changed, and current
:文件本地被修改,SVN 仓库中的对应文件未被修改。此时svn commit
可以提交修改,svn update
对文件没有影响Unchanged, and out of date
:文件本地未修改,SVN 仓库中的对应文件已经被修改。此时svn commit
无影响,svn update
将更新本地文件Locally changed, and out of date
:文件本地被修改,SVN 仓库中的对应文件也被修改。此时svn commit
将会失败,原因是out-of-date
。首先需要使用svn update
更新该文件,更新本地副本时 SVN 将会尝试合并公共修改和本地修改,如果出现冲突,需由用户手动解决冲突
需要注意,在 SVN 中 commit
和 update
是相互独立的。push 动作并不引发执行 pull,反之亦然。所以,每次你使用 svn commit
提交某个文件的修改后,你的本地仓库将处于 mixed revisions
状态:所提交的文件将处于最新的 revision,而其它文件可能还处于旧的 revision。mixed revision
在开发流程中有时也很有作用。
SVN 基本用法
接下来将介绍 SVN 的基本用法,这些知识足以让你完成日常工作中的绝大多数任务。SVN 命令的完整手册可以访问这里。
使用 svn help
命令可以获取 SVN 命令行的帮助信息,使用 svn help SUBCOMMAND
获取每个子命令的详细帮助信息,包括子命令的语法、选项和行为。
导入数据到仓库
svn import
命令是最快速的方法,用于把未版本化的文件拷贝到 SVN 仓库,在导入过程中将按需创建中间目录。当你本地已经有工作目录树,希望使用 SVN 进行版本跟踪,可以使用该命令:
1 | svn import /path/to/mytree http://svn.example.com/svn/repo/some/project -m "Initial import" |
执行上述命令,mytree 目录下的文件都将被拷贝到 SVN 仓库,而 SVN 仓库中的目录并不需要手动创建,import 命令将自动帮你完成这一过程。需要注意,import 命令完成后,本地目录并不会自动成为 working copy
,还是需要重新创建本地副本。
仓库布局惯例
尽管 SVN 允许你在仓库中创建任意的目录和文件,但是在漫长的开发岁月中,关于代码仓库的布局已经形成了一套传统,这些布局规则中的代码目录名称将为开发人员提供有价值的信息。
首先,仓库中的每个项目都有一个可标识的项目根目录(project root)。其次,该项目根目录下将包含以下子目录:
- trunk:项目开发主线
- branches:用于不同开发目的的分支
- tags:完成一些开发需求后的稳定版本
创建本地副本
使用 svn checkout
命令从 SVN 仓库中创建项目的本地副本,默认情况下,该命令将获取目录的最新版本以及该目录下的所有子目录。
1 | svn checkout http://svn.example.com/svn/repo/trunk |
使用该命令后,将在本地文件系统创建一个目录,目录名为 checkout URL
的最后一个组件名。当然你也可以显式指定本地目录的名称,如果该目录不存在,checkout 命令将帮你创建:
1 | svn checkout http://svn.example.com/svn/repo/trunk my-working-copy |
更新本地副本
通常一个项目会有多个人同时修改,为了使你的本地工作副本更新到 SVN 仓库的最新状态,可以使用 svn uddate
命令。
1 | $ svn update |
本地修改
在将本地工作副本更新到最新状态后,就可以开始你自己的修改了。通常存在两种形式的修改:
- 文件内容的修改
- 目录结构的修改
对文件内容的修改,不需要额外通知 SVN 什么信息,直接使用你的编辑器修改文件即可。但是对于目录结构的修改(包括添加/删除文件或目录,重命名文件或目录,移动文件或目录),需要使用 SVN 命令来进行操作。注意这些命令执行完毕后只在本地工作副本立即生效,只有 commit 后才会在 SVN 仓库生效。
svn add
:添加文件/目录到 SVN 仓库。如果添加的是目录,目录底下的所有内容都会被添加,可以使用--depth=empty
来添加目录本身svn delete
:从 SVN 仓库中删除文件/目录。如果删除是文件,该操作在本地工作副本下立即生效。如果删除的是目录,只有提交后才会从本地副本和 SVN 仓库中删除该目录svn copy FOO BAR
:创建一个 FOO 的副本,名为 BAR。除非使用 —parents 选项,否则不创建中间目录svn move FOO BAR
:将 FOO 重命名为 BAR,该命令等效于运行svn copy FOO BAR
,svn delete FOO
svn mkdir FOO
:创建一个新目录 FOO,该命令等效于运行mkdir FOO
,svn add FOO
SVN 支持直接修改仓库的内容,即不通过 commit 本地修改的方式。svn mkdir
/ svn copy
/ svn more
/ svn delete
都可以直接使用仓库 URL 作为参数,从而直接对仓库进行修改。
查看你的修改
查看整体修改
在提交修改之前,通常需要对本地修改进行确认。使用 svn status 命令可以查看整体修改。
1 | $ svn status |
该命令的输出会显示所有本地修改过的文件,文件前的字符显示了修改状态:
- ?:该文件/目录未在版本控制下
- A:该文件/目录即将新添加到 SVN 仓库
- C:文件处于冲突状态。这表明从仓库更新后对该文件的修改和本地修改出现冲突。在提交仓库之前必须手动解决冲突
- D:该文件/目录即将从 SVN 仓库删除
- M:文件内容被修改
使用 --verbose(-v)
选项,可以看到本地副本所有文件的状态,即使该文件未被修改,而且输出信息更详细:
1 | $ svn status -v |
该命令输出的第一列表示文件修改状态,第二列输出表示文件的当前 revision,第三列输出表示文件上次被修改后的 revision,第四列表示上次谁修改该文件,第五列表示文件名。
可以使用 --show-update(-u)
选项,查看自上次更新以来,哪些文件在 SVN 仓库中已经被修改过,即查看本地副本中的过期文件。
查看详细修改
使用 svn diff
命令可以查看文件内容的差异。在本地副本的顶级目录下运行该命令,可以查看本地副本下所有修改文件的差异。该命令的输出采用统一的 diff 文件格式,所以 svn diff
命令的输出结果可以应用于 patch 程序,这样就可以把你在本地副本上所做的修改共享给其他人,而不需要首先提交该修改。
1 | $ svn diff > patchfile |
svn diff
命令也可以使用外部程序来产生不同的 diff 文件格式,使用 --diff-cmd
传递外部程序的名称,使用 --extensions(-x)
来指定传递给该外部程序的选项参数。示例如下:
1 | $ svn diff --diff-cmd /usr/bin/diff -x "-i" foo.c |
修正错误
当使用 svn diff
命令检查所有修改后,如果你发现对某个文件的修改存在错误,一种解决办法是将文件恢复到原始状态,然后重新进行修改。在 SVN 中,可以使用 svn revert
命令达到该效果。
1 | $ svn status README |
除了对文件内容的还原,svn revert
也能够对文件的添加/删除进行还原。例如,取消对文件的添加:
1 | $ svn status new-file.txt |
解决冲突
当仓库中的某个文件出现了修改,而你在本地副本中也对该文件进行了修改,之后使用 svn update 命令对该文件进行更新时,该文件可能会出现冲突。
假设执行 svn update 命令出现如下输出:
1 | $ svn update |
对于标识为 U(updated,表示已更新)和标识为 G(merGed,表示已合并)的文件不需要关心,这些文件正常地从仓库中获取了更新。但是对于文件 bar.c
则出现了冲突,此时 SVN 将询问你采取何种措施来解决冲突。可采取的动作包括:
- e,edit:使用编辑器打开出现冲突的文件,编辑器由环境变量 EDITOR 指定
- df,diff-full: 显示该文件的 base revision 和当前内容的差异
- r,resolve:当编辑文件之后,告诉 SVN 你已经解决了冲突,SVN 应该接受文件的当前内容
- dc,display-conflict:显示文件的所有冲突区域,忽略已经成功合并的部分
- mc,mine-conflict:对于和本地修改出现冲突的仓库修改,全部采用本地修改。但是仍接受从仓库中获取的未冲突修改
- tc,theirs-conflict:对于和本地修改出现冲突的仓库修改,全部采用仓库修改,即忽略本地修改。但是仍保留本地文件中的未冲突修改。
- mf,mine-full:忽略所有从仓库中获取到的修改,同时全部保留本地修改
- tf,theirs-full:忽略所有本地修改,同时全部接受从仓库中获取的修改
- p,postone:将该文件保留为冲突状态,在 update 命令执行完毕之后再进行解决
- l,launch:启动外部程序来解决冲突,该方式需要做一些提前设置
- s,show all:显示交互式解决冲突时所有可使用的命令
交互式地查看冲突
使用 df(diff-full)
命令可以显示冲突区域对应的本地修改(相比较于 base revision):
1 | ... |
在这个例子中,diff 内容的第一行显示了对本地副本(即 Base Revision)的修改,这些修改没有出现冲突。
1 | -Just buy a sandwich. |
接下来的内容行则是出现冲突的区域,首先是你的修改:
1 | +Go pick up a cheesesteak. |
之后的内容行则是从服务器中收到的修改:
1 | +Bring me a taco! |
而如果使用 dc(diff-conflict)
命令则只显示冲突区域,而不是对文件的所有修改。而且该命令使用不同的格式来显示冲突区域,允许你更容易地比较冲突区域的差异。
交互式地解决冲突
在获取到冲突区域的详细信息后,就可以着手解决这些冲突,主要有两种方式:
- 使用编辑器修改冲突区域
- 直接选择本地修改或仓库修改
延迟解决冲突
当然,也可以使用 p(postone)
来推迟对该文件的冲突解决。如果你根本不想使用交互式方式来解决冲突,可以在使用 svn update
命令时传递 --non-interactive
选项,之后所有的冲突文件都会直接被标记为 C(Conflicted)
状态。
当你延迟解决冲突时,SVN 会做以下三件事,用于协助你解决冲突:
- update 命令执行过程中,会对冲突文件标记为 C,并始终记住该状态
- 如果该文件是可合并的,它将在文件中放置冲突标记符,用于可视化地展示冲突区域
- 对于每个冲突文件,都将创建 3 个额外的未版本化的文件
- filename.mine:在 update 流程之前本地副本中的该文件,它包含了所有的本地修改
- filename.rOLDREV:对应于 BASE revision 的该文件,即 update 流程之前该文件的未修改版本,OLDREV 表示 base revision 号
- filename.rNEWREV:对应于从仓库中获取到的新版本的该文件,NEWREV 对应于该文件的 revision 号(一般为 HEAD)
之后,在这些临时文件被删除之前,该冲突文件都不能被 commit。你需要使用 svn resolve
命令来解决冲突,同时使用 —accept
选项来指定解决冲突的方式,该选项有如下参数:
- base:选择该文件的 base revision,即不包含任何本地修改
- mine-full:选择只包含你的修改的文件版本
- theirs-full:选择从仓库中获取的文件版本,即忽略所有你的修改
- working:同时选择你的修改以及从 SVN 仓库中获取的修改,此时需要在本地副本的相应文件中手动合并冲突内容
当使用 svn resolve
命令解决冲突后,该文件对应的 3 个临时文件将被删除,该文件也不再处于冲突状态。
手动合并冲突
接下来将展示如何手动合并冲突。对于如下冲突文件
1 | $ cat sandwich.txt |
<、=、> 都是冲突标记符,并不是真实的冲突内容。在你解决冲突后、准备提交前,需要确保这些标记符从文件中被删除了。以下内容是冲突区域中你所做的修改:
1 | <<<<<<< .mine |
然后是冲突区域中对应的仓库修改:
1 | ======= |
根据你的实际情况,手动解决冲突内容,然后使用 svn resolve
命令告诉 SVN 冲突已经被解决。注意,使用该命令一定要确保冲突的确已经被解决,执行该命令后,临时文件将被删除。之后就可以对该文件提交成功(即使该文件仍然包含冲突标记符)。
1 | $ svn resolve --accept working sandwich.txt |
使用 svn revert
如果你想放弃你的修改,你可以使用 svn revert
命令将文件恢复成 base revision
。即使此时文件已经处于冲突状态,该命令也可以生效,而且执行该命令后冲突也会被解决。
所以当你 revert 一个冲突文件之后,不需要再使用 svn resolve
命令。
提交修改
使用 svn commit
命令向 SVN 仓库提交你的修改。提交修改时,需要添加日志信息,如果日志信息非常简短,可以直接使用 --message(-m)
选项。日志信息也可以保存在文件中,使用 --file(-F)
选项指定文件路径。
1 | $ svn commit -m "Corrected number of cheese slices." |
如果你没有显式地指定日志信息,SVN 将自动启动编辑器让你输入日志。
检查历史
以下几个命令可以让你从 SVN 仓库获取历史数据信息:
svn diff
:以行级别显示具体的差异svn log
:显示每个 revision 对应的日志信息,包括提交日志、日期、作者、修改路径等svn cat
:检索指定 revision 下的某文件,并显示文件内容svn annotate
:显示指定 revision 下的某文件,同时显示该文件每一行的最近一次修改信息svn list
:显示指定 revision 下的某目录内容
获取历史变化的具体信息
svn diff
主要有以下 3 种用途:
- 检查本地修改
- 将你的本地副本和仓库进行比较
- 比较仓库中不同 revision 变化
当不带任何选项运行 svn diff 命令时,将获取本地修改信息,它把当前本地副本中的文件和缓存在 .svn 目录下的原始文件信息进行对比:
1 | $ svn diff |
如果使用 –revision(-r) 选项指定单个 revision 号,将把本地副本和仓库中的指定 revision 进行比较。
1 | $ svn diff -r 3 rules.txt |
如果使用 –revision(-r) 选项指定两个 revision 号,中间用 :
隔开,将比较仓库中指定文件的两个版本的差异:
1 | $ svn diff -r 2:3 rules.txt |
如果要比较仓库中某个文件的某个 revision 和前一个 revision 之间的差异,可以使用 --change(-c)
的差异:
1 | $ svn diff -c 3 rules.txt |
最后,即使你没有本地副本,你也可以比较仓库中的版本差异,只需要在命令行中指定仓库的 URL:
1 | $ svn diff -c 5 http://svn.example.com/repos/example/trunk/text/rules.txt |
获取历史变化列表
使用 svn log
命令可以获取文件或目录的历史变化信息。该命令将显示谁修改了该文件/目录、在哪个 revision
发生了修改、修改时间、修改日志。默认日志信息以时间顺序逆序显示。
1 | $ svn log |
可以通过 --revision
选项获取指定 revision 之间或单个 revision 的日志信息:
命令 | 作用 |
---|---|
svn log -r 5:9 | 按时间顺序显示 revision 5 到 9 之间的日志信息 |
svn log -r 9:5 | 按时间顺序逆序显示 revision 5 到 9 之间的日志信息 |
svn log -r 8 | 获取 revision 为 8 时的日志信息 |
使用 --verbose(-v)
选项可以显示修改的文件信息。使用 --diff
选项,还可以同时显示每个 revision 之间的具体差异。
浏览仓库
使用 svn cat
命令可以显示指定 revision 的某文件,可以通过重定向命令将输出重定向到文件中:
1 | $ svn cat -r 2 rules.txt > rules.txt.v2 |
svn annotate
命令类似于 svn cat
命令,也可以显示文件内容,但是该命令使用表格形式进行输出。每一行不仅显示文件内容,同时还显示这一行最近一次修改的用户名、revision 号和时间戳(可选,使用 –verbose 选项)。
在该命令的输出中可能某些行没有属性,这是因为这些行已经在本地副本中被修改了。所以这也是另一种方式来查看本地副本中哪些内容被修改。可以指定 BASE revision
,从而忽略本地副本对这些行的修改:
1 | $ svn annotate rules.txt@BASE |
注意,svn blame
和 svn praise
等效于 svn annotate
。所以使用哪个命令,取决于你看到代码时的心情。
svn list
允许你直接显示 SVN 仓库中某目录的文件列表,而不需要先创建你的本地副本。使用 --verbose(-v)
选项可以获取更详细的信息。
1 | $ svn list http://svn.example.com/repo/project |
通过 svn update
命令的 --revision(-r)
选项来获取指定 revision 的仓库快照。当然你也可以使用 svn checkout
命令的 -r
选项来 checkout 指定 revision 的仓库快照。对于 SVN 新手可能会犯一个错误,不能通过 checkout 一个旧版本,然后再提交该旧版本来回退修改,因为 SVN 不允许你提交过期的文件。
1 | # Checkout the trunk from r1729. |
如果你仅仅想要仓库文件的副本,之后不需要通过 SVN 跟踪本地版本的变化,可以使用 svn export
创建本地副本,该命令创建的本地副本中不存在 .svn
管理目录。
1 | # Export the trunk from the latest revision. |
从中断中恢复
当 SVN 尝试更新你的本地副本时,SVN 会在一个私有的 to-do-list
中记录它将要采取的行动,然后按照该列表逐一完成每个任务,并且对工作副本中的相关部分进行加锁。当所有任务完成后,SVN 清空它的 to-do-list
并释放锁。该流程类似于一个日志型文件系统。
如果 SVN 操作中间被中断,该私有的 to-do-list
仍然存在工作副本中。所以如果执行某些 SVN 操作提示工作副本处于 locked 状态,只需要运行 svn cleanup
命令来纠正错误。svn status
的输出中 L 状态表示文件处于 locked 状态。
1 | $ svn status |
处理文件树冲突
之前介绍的冲突是关于文件内容的冲突,还有一种情况是目录结构出现冲突。对于目录结构冲突,在解决冲突之前 SVN 也不允许你执行提交操作。
例如,如果执行 svn update
命令出现如下结果:
1 | $ svn update |
在这个例子中,仓库中的 code/bar.c
被其它人重命名为 code/baz.c
,因此本地副本更新后出现文件树冲突(Tree conflict)。通过 svn status
命令得知 code/bar.c 本地已经修改,但是仓库中已经删除:
1 | $ svn status |
之后我们需要通过一些方法调查文件树的变动过程,该过程可以和他人沟通,也可以通过 SVN 的历史记录获取一些线索。通过 svn info
获取冲突文件的详细信息:
1 | $ svn info code/bar.c |
得知到文件树变动过程后,我们就需要着手解决冲突。在这个例子中,如果我们同意变动,并且决定将本地修改移植到新文件 code/baz.c,我们可以这样做:
首先基于旧文件产生 diff 文件:
1 | $ svn diff code/bar.c > PATCHFILE |
修改 diff 文件中的内容,从而将修改应用到新的文件 code/baz.c
1 | $ cat PATCHFILE |
将修改后的 diff 文件应用到新文件
1 | $ svn patch PATCHFILE |
之后删除本地中的 code/bar.c 并告诉 SVN 成功已经解决:
1 | $ svn delete --force code/bar.c |
如果我们不同意重命名,则只需要在本地副本中删除新文件 code/baz.c,而原来的 code/bar.c 已经被 SVN 自动设置为添加状态,不需要再运行 svn add 命令:
1 | $ svn delete --force code/baz.c |
以上,一个由于重命名而导致的文件树冲突就被解决了。