### 基本的工作周期 Subversion有许多特性、选项和华而不实的高级功能,但日常的工作中你只使用其中的一小部分,有一些只在特殊情况才会使用,在这一节里,我们会介绍许多你在日常工作中常见的命令。 典型的工作周期是这样的: - 更新你的工作拷贝 - **svn update** - 做出修改 - **svn add** - **svn delete** - **svn copy** - **svn move** - 检验修改 - **svn status** - **svn diff** - **svn revert** - 合并别人的修改到工作拷贝 - **svn update** - **svn resolved** - 提交你的修改 - **svn commit** ### 更新你的工作拷贝 当你在一个团队的项目里工作时,你希望更新你的工作拷贝得到所有其他人这段时间作出的修改,使用**svn update**让你的工作拷贝与最新的版本同步。 ~~~ $ svn update U foo.c U bar.c Updated to revision 2. ~~~ 这种情况下,其他人在你上次更新之后提交了对`foo.c`和`bar.c`的修改,因此Subversion更新你的工作拷贝来引入这些更改。 让我们认真检查**svn update**的输出,当服务器发送修改到你的工作拷贝,一个字母显示在每一个项目之前,来让你知道Subversion对你的工作拷贝做了什么操作: `U foo` 文件`foo`更新了(从服务器收到修改)。 `A foo` 文件或目录`foo`被添加到工作拷贝。 `D foo` 文件或目录`foo`在工作拷贝被删除了。 `R foo` 文件或目录`foo`在工作拷贝已经被替换了,这是说,`foo`被删除,而一个新的同样名字的项目添加进来,它们具有同样的名字,但是版本库会把它们看作具备不同历史的不同对象。 `G foo` 文件`foo`接收到版本库的更改,你的本地版本也已经修改,但改变没有互相影响,Subversion成功的将版本库和本地文件合并,没有发生任何问题。 `C foo` 文件`foo`的修改与服务器冲突,服务器的修改与你的修改交迭在一起,不要恐慌,这种冲突需要人(你)来解决,我们在后面的章节讨论这种情况。 ### 修改你的工作拷贝 现在你可以开始工作并且修改你的工作拷贝了,你很容易决定作出一个修改(或者是一组),像写一个新的特性,修正一个错误等等。这时可以使用的Subversion命令包括**svn add**、 **svn delete**、**svn copy**和**svn move**。如果你只是修改版本库中已经存在的文件,在你提交之前,不必使用上面的任何一个命令。你可以对工作备份作的修改包括: 文件修改 这是最简单的一种修改,你不必告诉Subversion你想修改哪一个文件,只需要去修改,然后Subversion会自动地探测到哪些文件已经更改了。 目录树修改 你可以“标记”目录或者文件为预定要删除、增加、复制或者移动,也许这些改动在你的工作拷贝马上发生,而版本库只在你提交的时候才发生改变。 修改文件,可以使用文本编辑器、字处理软件、图形程序或任何你常用的工具,Subverion处理二进制文件像同文本文件一样―效率也一样。 这些是常用的可以修改目录树结构的子命令(我们会在后面包括**svn import**和**svn mkdir**)。 ### 警告 你可以使用任何你喜欢的工具编辑文件,但你不可以在修改目录结构时不通知Subversion,需要使用**svn copy**、**svn delete**和**svn move**命令修改工作拷贝的结构,使用**svn add**增加版本控制的新文件或目录。 **svn add foo** 预定将文件、目录或者符号链`foo`添加到版本库,当你下次提交后,`foo`会成为其父目录的一个子对象。注意,如果`foo`是目录,所有foo中的内容也会预定添加进去,如果你只想添加`foo`本身,使用`--non-recursive`(`-N`)参数。 **svn delete foo** 预定将文件、目录或者符号链`foo`从版本库中删除掉,如果foo是文件,它马上从工作拷贝中删除,如果是目录,不会被删除,但是Subversion准备好删除了,当你提交你的修改,`foo`就会在你的工作拷贝和版本库中被删除。 **svn copy foo bar** 建立一个新的项目`bar`作为`foo`的复制品,当在下次提交时会将`bar`添加到版本库,这种拷贝历史会记录下来(按照来自`foo`的方式记录),**svn copy**并不建立中介目录。 **svn move foo bar** 这个命令与与运行**svn copy foo bar; svn delete foo**完全相同,`bar`作为`foo`的拷贝准备添加,`foo`已经预定要被删除,**svn move**不建立中介的目录。 **不通过工作拷贝修改版本库** 本章的前面曾经说过,为了使版本库反映你的改动,你应该提交所有改动。这并不完全正确―有一些方式*是*可以直接操作版本库的,当然只有子命令直接操作URL而不是本地拷贝路径时才可以实现,通常**svn mkdir**、**svn copy**、**svn move**、和 **svn delete**可以使用URL工作。 指定URL的操作方式有一些区别,因为在使用工作拷贝的运作方式时,工作拷贝成为一个“集结地”,可以在提交之前整理组织所要做的修改,直接对URL操作就没有这种奢侈,所以当你直接操作URL的时候,所有以上的动作代表一个立即的提交。 ### 检查你的修改 当你完成修改,你需要提交他们到版本库,但是在此之前,检查一下做过什么修改是个好主意,通过提交前的检查,你可以整理一份精确的日志信息,你也可以发现你不小心修改的文件,给了你一次恢复修改的机会。此外,这是一个审查和仔细察看修改的好机会,你可通过命令**svn status**、**svn diff**和**svn revert**精确地察看所做的修改。你可以使用前两个命令察看工作拷贝中的修改,使用第三个来撤销部分(或全部)的修改。 Subversion已经被优化来帮助你完成这个任务,可以在不与版本库通讯的情况下做许多事情,详细来说,对于每一个文件,你的的工作拷贝在`.svn`包含了一个“原始的”拷贝,所以Subversion可以快速的告诉你那些文件修改了,甚至允许你在不与版本库通讯的情况下恢复修改。 #### **svn status** 相对于其他命令,你会更多地使用这个**svn status**命令。 **CVS用户:控制另类的更新!** 你也许使用**cvs update**来看你做了哪些修改,**svn status**会给你所有你做的改变―而不需要访问版本库,并且不会在不知情的情况下与其他用户作的更改比较。 在Subversion,**update**只是做这件事―将工作拷贝更新到版本库的最新版本,你可以消除使用**update**察看本地修改的习惯。 如果你在工作拷贝的顶级目录运行不带参数的**svn status**命令,它会检测你做的所有的文件或目录的修改,以下的例子是来展示**svn status**可能返回的状态码(注意,`#`之后的不是**svn status**打印的)。 ~~~ L abc.c # svn已经在.svn目录锁定了abc.c M bar.c # bar.c的内容已经在本地修改过了 M baz.c # baz.c属性有修改,但没有内容修改 X 3rd_party # 这个目录是外部定义的一部分 foo.o # svn并没有管理foo.o ! some_dir # svn管理这个,但它可能丢失或者不完整 ~ qux # 作为file/dir/link进行了版本控制,但类型已经改变 I .screenrc # svn不管理这个,配置确定要忽略它 A + moved_dir # 包含历史的添加,历史记录了它的来历 M + moved_dir/README # 包含历史的添加,并有了本地修改 D stuff/fish.c # 这个文件预定要删除 A stuff/loot/bloo.h # 这个文件预定要添加 C stuff/loot/lump.c # 这个文件在更新时发生冲突 R xyz.c # 这个文件预定要被替换 S stuff/squawk # 这个文件已经跳转到了分支 ~~~ 在这种格式下,**svn status**打印五列字符,紧跟一些空格,接着是文件或者目录名。第一列告诉一个文件的状态或它的内容,返回代码解释如下: `A item` 文件、目录或是符号链`item`预定加入到版本库。 `C item` 文件`item`发生冲突,在从服务器更新时与本地版本发生交迭,在你提交到版本库前,必须手工的解决冲突。 `D item` 文件、目录或是符号链`item`预定从版本库中删除。 `M item` 文件`item`的内容被修改了。 `R item` 文件、目录或是符号链`item`预定将要替换版本库中的`item`,这意味着这个对象首先要被删除,另外一个同名的对象将要被添加,所有的操作发生在一个修订版本。 `X item` 目录没有版本化,但是与Subversion的外部定义关联,关于外部定义,可以看[“外部定义”一节]( "外部定义")。 ` item` 文件、目录或是符号链`item`不在版本控制之下,你可以通过使用**svn status**的`--quiet`(`-q`)参数或父目录的`svn:ignore`属性忽略这个问题,关于忽略文件的使用,见[“`svn:ignore`”一节]( "svn:ignore")。 `! item` 文件、目录或是符号链`item`在版本控制之下,但是已经丢失或者不完整,这可能因为使用非Subversion命令删除造成的,如果是一个目录,有可能是检出或是更新时的中断造成的,使用**svn update**可以重新从版本库获得文件或者目录,也可以使用**svn revert file**恢复原来的文件。 `~ item` 文件、目录或是符号链`item`在版本库已经存在,但你的工作拷贝中的是另一个。举一个例子,你删除了一个版本库的文件,新建了一个在原来的位置,而且整个过程中没有使用**svn delete**或是**svn add**。 `I item` 文件、目录或是符号链`item`不在版本控制下,Subversion已经配置好了会在**svn add**、**svn import**和**svn status**命令忽略这个文件,关于忽略文件,见[“`svn:ignore`”一节]("svn:ignore")。注意,这个符号只会在使用**svn status**的参数`--no-ignore`时才会出现―否则这个文件会被忽略且不会显示! 第二列说明文件或目录的属性的状态(更多细节可以看[“属性”一节]( "属性")),如果一个`M`出现在第二列,说明属性被修改了,否则显示空白。 第三列只显示空白或者`L`,`L`表示Subversion已经在`.svn`工作区域锁定了这个项目,当你的**svn commit**正在运行的时候―也许正在输入log信息,运行**svn status**你可以看到`L`标记,如果这时候Subversion并没有运行,可以推测Subversion发生中断并且已经锁定,你必须运行**svn cleanup**来清除锁定(本节后面将有更多论述)。 第四列只会显示空白或`+`,`+`的意思是一个有附加历史信息的文件或目录预定添加或者修改到版本库,通常出现在**svn move**或是**svn copy**时,如果是看到`A+`就是说要包含历史的增加,它可以是一个文件或是拷贝的根目录。`+`表示它是即将包含历史增加到版本库的目录的一部分,也就是说他的父目录要拷贝,它只是跟着一起的。 `M+`表示将要包含历史的增加,并且已经更改了。当你提交时,首先会随父目录进行包含历史的增加,然后本地的修改提交到更改后的版本。 第五列只显示空白或是`S`,表示这个目录或文件已经转到了一个分支下了(使用**svn switch**)。 如果你传递一个路径给**svn status**,它只给你这个项目的信息: ~~~ $ svn status stuff/fish.c D stuff/fish.c ~~~ **svn status**也有一个`--verbose`(`-v`)选项,它可以显示工作拷贝中的*所有*项目,即使没有改变过: ~~~ $ svn status --verbose M 44 23 sally README 44 30 sally INSTALL M 44 20 harry bar.c 44 18 ira stuff 44 35 harry stuff/trout.c D 44 19 ira stuff/fish.c 44 21 sally stuff/things A 0 stuff/things/bloo.h 44 36 harry stuff/things/gloo.c ~~~ 这是**svn status**的“加长形式”,第一列保持相同,第二列显示一个工作版本号,第三和第四列显示最后一次修改的版本号和修改人。 上面所有的**svn status**调用并没有联系版本库,只是与`.svn`中的元数据进行比较的结果,最后,是`--show-updates`(`-u`)参数,它将会联系版本库为已经过时的数据添加新信息: ~~~ $ svn status --show-updates --verbose M * 44 23 sally README M 44 20 harry bar.c * 44 35 harry stuff/trout.c D 44 19 ira stuff/fish.c A 0 stuff/things/bloo.h Status against revision: 46 ~~~ 注意这两个星号:如果你现在执行**svn update**,你的`README`和`trout.c`会被更新,这告诉你许多有用的信息―你可以在提交之前,需要使用更新操作得到文件`README`的更新,或者说文件已经过时,版本库会拒绝了你的提交。(后面还有更多关于此主题)。 #### **svn diff** 另一种检查修改的方式是**svn diff**命令,你可以通过不带参数的**svn diff***精确的*找出你所做的修改,这会输出统一区别格式:[[3](#)] ~~~ $ svn diff Index: bar.c =================================================================== --- bar.c (revision 3) +++ bar.c (working copy) @@ -1,7 +1,12 @@ +#include <sys/types.h> +#include <sys/stat.h> +#include <unistd.h> + +#include <stdio.h> int main(void) { - printf("Sixty-four slices of American Cheese...\n"); + printf("Sixty-five slices of American Cheese...\n"); return 0; } Index: README =================================================================== --- README (revision 3) +++ README (working copy) @@ -193,3 +193,4 @@ +Note to self: pick up laundry. Index: stuff/fish.c =================================================================== --- stuff/fish.c (revision 1) +++ stuff/fish.c (working copy) -Welcome to the file known as 'fish'. -Information on fish will be here soon. Index: stuff/things/bloo.h =================================================================== --- stuff/things/bloo.h (revision 8) +++ stuff/things/bloo.h (working copy) +Here is a new file to describe +things about bloo. ~~~ **svn diff**命令通过比较你的文件和`.svn`的“原始”文件来输出信息,预定要增加的文件会显示所有增加的文本,要删除的文件会显示所有要删除的文本。 输出的格式为*统一区别格式*(unified diff format),删除的行前面加一个`-`,添加的行前面有一个`+`,**svn diff**命令也打印文件名和**打补丁**需要的信息,所以你可以通过重定向一个区别文件来生成“补丁”: ~~~ $ svn diff > patchfile ~~~ 举个例子,你可以把补丁文件发送邮件到其他开发者,在提交之前审核和测试。 #### **svn revert** 假设你通过上面的diff输出发现你不小心用编辑器在`README`中输入了一些字符。 这是使用**svn revert**的好机会。 ~~~ $ svn revert README Reverted 'README' ~~~ Subversion把文件恢复到未修改的状态,叫做`.svn`目录的“原始”拷贝,应该知道**svn revert**可以恢复任何预定要做的操作,举个例子,你不再想添加一个文件: ~~~ $ svn status foo foo $ svn add foo A foo $ svn revert foo Reverted 'foo' $ svn status foo foo ~~~ ### 注意 **svn revert***`ITEM`*的效果与删除*`ITEM`*然后执行**svn update -r BASE***`ITEM`*完全一样,但是,如果你使用**svn revert**它不必通知版本库就可以恢复文件。 或许你不小心删除了一个文件: ~~~ $ svn status README README $ svn delete README D README $ svn revert README Reverted 'README' $ svn status README README ~~~ **看!没有网络!** 这三个命令(**svn status**、**svn diff**和 **svn revert**)都可以在没有网络的情况下工作,这让你在没有网络连接时的管理修改过程更加简单,像在飞机上旅行,乘坐火车往返或是在海滩上奋力工作时。 Subversion通过在`.svn`管理区域使用原始的版本缓存来做到这一点,这使得恢复本地版本而*不必访问网络*,这个缓存(叫做“text-base”)也允许Subversion可以根据原始版本生成一个压缩的增量(“区别”)提交―即使你有个非常快的网络,有这样一个缓存有极大的好处,非常的快,只向服务器提交修改的部分,这一点乍一看好像并不重要,但当你要提交一个400M大小的文件的修改时,你就会明白! ### 解决冲突(合并别人的修改) 我们可以使用**svn status -u**来预测冲突,当你运行**svn update**一些有趣的事情发生了: ~~~ $ svn update U INSTALL G README C bar.c Updated to revision 46. ~~~ `U`和`G`没必要关心,文件干净的接受了版本库的变化,文件标示为`U`表明本地没有修改,文件已经根据版本库更新。`G`标示合并,标示本地已经修改过,与版本库没有重迭的地方,已经合并。 但是`C`表示冲突,说明服务器上的改动同你的改动冲突了,你需要自己手工去解决。 当冲突发生了,有三件事可以帮助你注意到这种情况和解决问题: - Subversion打印`C`标记,并且标记这个文件已冲突。 - 如果Subversion认为这个文件是可合并的,它会置入*冲突标记*―特殊的横线分开冲突的“两面”―在文件里可视化的描述重叠的部分(Subversion使用`svn:mime-type`属性来决定一个文件是否可以使用上下文的,以行为基础合并,更多信息可以看[“`svn:mime-type`”一节]( "svn:mime-type"))。 - 对于每一个冲突的文件,Subversion放置三个额外的未版本化文件到你的工作拷贝: `filename.mine` 你更新前的文件,没有冲突标志,只是你最新更改的内容。(如果Subversion认为这个文件不可以合并,`.mine`文件不会创建,因为它和工作文件相同。) `filename.rOLDREV` 这是你的做更新操作以前的`BASE`版本文件,就是你在上次更新之后未作更改的版本。 `filename.rNEWREV` 这是你的Subversion客户端从服务器刚刚收到的版本,这个文件对应版本库的`HEAD`版本。 这里`OLDREV`是你的`.svn`目录中的修订版本号,`NEWREV`是版本库中`HEAD`的版本号。 举一个例子,Sally修改了`sandwich.txt`,Harry刚刚改变了他的本地拷贝中的这个文件并且提交到服务器,Sally在提交之前更新它的工作拷贝得到了冲突: ~~~ $ svn update C sandwich.txt Updated to revision 2. $ ls -1 sandwich.txt sandwich.txt.mine sandwich.txt.r1 sandwich.txt.r2 ~~~ 在这种情况下,Subversion*不*会允许你提交`sandwich.txt`,直到你的三个临时文件被删掉。 ~~~ $ svn commit --message "Add a few more things" svn: Commit failed (details follow): svn: Aborting commit: '/home/sally/svn-work/sandwich.txt' remains in conflict ~~~ 如果你遇到冲突,三件事你可以选择: - “手动”合并冲突文本(检查和修改文件中的冲突标志)。 - 用某一个临时文件覆盖你的工作文件。 - 运行**svn revert <filename>**来放弃所有的修改。 一旦你解决了冲突,你需要通过命令**svn resolved**让Subversion知道,这样就会删除三个临时文件,Subversion就不会认为这个文件是在冲突状态了。 ~~~ $ svn resolved sandwich.txt Resolved conflicted state of 'sandwich.txt' ~~~ #### 手工合并冲突 第一次尝试解决冲突让人感觉很害怕,但经过一点训练,它简单的像是骑着车子下坡。 这里一个简单的例子,由于不良的交流,你和同事Sally,同时编辑了`sandwich.txt`。Sally提交了修改,当你准备更新你的版本,冲突发生了,我们不得不去修改`sandwich.txt`来解决这个问题。首先,看一下这个文件: ~~~ $ cat sandwich.txt Top piece of bread Mayonnaise Lettuce Tomato Provolone <<<<<<< .mine Salami Mortadella Prosciutto ======= Sauerkraut Grilled Chicken >>>>>>> .r2 Creole Mustard Bottom piece of bread ~~~ 小于号、等于号和大于号串是冲突标记,并不是冲突的数据,你一定要确定这些内容在下次提交之前得到删除,前两组标志中间的内容是你在冲突区所做的修改: ~~~ <<<<<<< .mine Salami Mortadella Prosciutto ======= ~~~ 后两组之间的是Sally提交的修改冲突: ~~~ ======= Sauerkraut Grilled Chicken >>>>>>> .r2 ~~~ 通常你并不希望只是删除冲突标志和Sally的修改―当她收到三明治时,会非常的吃惊。所以你应该走到她的办公室或是拿起电话告诉Sally,你没办法从从意大利熟食店得到想要的泡菜。一旦你们确认了提交内容后,修改文件并且删除冲突标志。 ~~~ Top piece of bread Mayonnaise Lettuce Tomato Provolone Salami Mortadella Prosciutto Creole Mustard Bottom piece of bread ~~~ 现在运行**svn resolved**,你已经准备好提交了: ~~~ $ svn resolved sandwich.txt $ svn commit -m "Go ahead and use my sandwich, discarding Sally's edits." ~~~ 记住,如果你修改冲突时感到混乱,你可以参考subversion生成的三个文件―包括你未作更新的文件。你也可以使用第三方的合并工具检验这三个文件。 #### 拷贝覆盖你的工作文件 如果你只是希望取消你的修改,你可以仅仅拷贝Subversion为你生成的文件替换你的工作拷贝: ~~~ $ svn update C sandwich.txt Updated to revision 2. $ ls sandwich.* sandwich.txt sandwich.txt.mine sandwich.txt.r2 sandwich.txt.r1 $ cp sandwich.txt.r2 sandwich.txt $ svn resolved sandwich.txt ~~~ #### 下注:使用**svn revert** 如果你得到冲突,经过检查你决定取消自己的修改并且重新编辑,你可以恢复你的修改: ~~~ $ svn revert sandwich.txt Reverted 'sandwich.txt' $ ls sandwich.* sandwich.txt ~~~ 注意,当你恢复一个冲突的文件时,不需要再运行**svn resolved**。 现在我们准备好提交修改了,注意**svn resolved**不像我们本章学过的其他命令一样需要参数,在任何你认为解决了冲突的时候,只需要小心运行**svn resolved**,―一旦删除了临时文件,Subversion会让你提交这文件,即使文件中还存在冲突标记。 ### 提交你得修改 最后!你的修改结束了,你合并了服务器上所有的修改,你准备好提交修改到版本库。 **svn commit**命令发送所有的修改到版本库,当你提交修改时,你需要提供一些描述修改的*日志信息*,你的信息会附到这个修订版本上,如果信息很简短,你可以在命令行中使用`--message`(`-m`)选项: ~~~ $ svn commit --message "Corrected number of cheese slices." Sending sandwich.txt Transmitting file data . Committed revision 3. ~~~ 然而,如果你把写日志信息当作工作的一部分,你也许会希望通过告诉Subversion一个文件名得到日志信息,使用`--file`选项: ~~~ $ svn commit --file logmsg Sending sandwich.txt Transmitting file data . Committed revision 4. ~~~ 如果你没有指定`--message`或者`--file`选项,Subversion会自动地启动你最喜欢的编辑器(见[“config”一节]( "config")的`editor-cmd`部分)来编辑日志信息。 ### 提示 如果你使用编辑器撰写日志信息时希望取消提交,你可以直接关掉编辑器,不要保存,如果你已经做过保存,只要简单的删掉所有的文本并再次保存。 ~~~ $ svn commit Waiting for Emacs...Done Log message unchanged or not specified a)bort, c)ontinue, e)dit a $ ~~~ 版本库不知道也不关心你的修改作为一个整体是否有意义,它只检查是否有其他人修改了同一个文件,如果别人*已经*这样做了,你的整个提交会失败,并且提示你一个或多个文件已经过时了: ~~~ $ svn commit --message "Add another rule" Sending rules.txt svn: Commit failed (details follow): svn: Out of date: 'rules.txt' in transaction 'g' ~~~ 此刻,你需要运行**svn update**来处理所有的合并和冲突,然后再尝试提交。 我们已经覆盖了Subversion基本的工作周期,还有许多其它特性可以管理你得版本库和工作拷贝,但是只使用前面介绍的命令你就可以很轻松的工作了。 当然没有任何东西是在版本库里被删除了―只是在版本库的`HEAD`里消失了,你可以通过检出(或者更新你的工作拷贝)你做出删除操作的前一个修订版本来找回所有的东西。 Subversion使用内置区别引擎,缺省情况下输出为统一区别格式。如果你期望不同的输出格式,你可以使用`--diff-cmd`指定外置的区别程序,并且通过`--extensions`传递其他参数,举个例子,察看本地文件`foo.c`的区别,同时忽略空格修改,你可以运行**svn diff --diff-cmd /usr/bin/diff --extensions '-bc' foo.c**。 你也可以手工的删除这三个临时文件,但是当Subversion会给你做时你会自己去做吗?我们是这样想的。 如果你向他们询问,他们非常有理由把你带到城外的铁轨上。