Linux实用工具-SVN使用总结
后记:此文是我做为程序员初入职后,面对的第一个专业问题 如何对项目代码进行版本控制管理? 的非正式总结。
显然获取知识的过程并非如书本顺畅地一起呵成,本文记述的就是本人从最开始的一无所知(其实也略有所闻),到最后的轻车熟路的一点过程。
我一直相信 授人以鱼,不如授人以渔 所以这里尽量保留了获取知识的过程,所有没有过于整理,希望能为初入此坑的朋友带来一点参考。
SVN做为版本控制,对于比较专业的开发中,显得有点过时,但其中包含的思想(操作流程、获取知识的过程),不会过时。
同为程序员、学习者、为生活而奋斗的战友、共勉!
带着问题学习 (自 [2009-05-10 日])
1、什么是SVN?
2、大致的原理?
3、据我所知的命令有
-
svn checkout -
svn update刚开始工作的时候如果没有使用
svn update那么如果之后使用得了commit会怎么样?如果之后才使用svn update那么会怎么样?自己的修改还存在么? -
svn commit发生冲突的时候
commit会把自己的修改擦掉吗?
4、我只想知道的功能
- 怎样建立最初的svn工作环境?
- 导出服务器的工作目录到本机?
- 更新服务器的修改以便与本机的工作拷贝同步?
- 提交自己的修改到服务器,并且添加一些说明性的信息?
- 更新本机的拷贝到服务器中一个特定的版本?
- 查看服务器上所有可用的版本的信息?
- 如何处理冲突?
- 从服务器捡出一个指定的版本到独立的一个工作拷贝?
- 在本地工作目录增加、删除了一个文件或者目录,如何让它与服务器的也同步?
- 一般程序员利用svn工作的日常步骤到底是怎么样的?
- 怎样更改文件的名字以及移动工作目录?
5、我好奇的功能
svn checkoutsvn updatesvn copysvn addsvn deletesvn commitsvn cat
前面都是废话,这里是正题,慢慢地更新吧。
开始都是最简单的,往后随着应用以及兴趣,会更新。
如下的描述主要针对 Subversion 1.1 。
一些“术语”
何为工作拷贝?
一个工作拷贝就是你本地机器的一个普通的目录,保存一些文件,你可以任意的编辑、编译它们,你的工作拷贝是你的私有工作区。
Subversion 不会自动把你的修改与其他人的合并,也不会把你的修改展示给别人。当你确定要“发布”自己的修改的时候, SVN 会提供相应命令,这样你才能把你的工作和别人的工作合并,并使别人看到你的修改。
通常你的工作拷贝的每一个文件夹里有一个以 .svn 为名的文件夹,它用来帮助 Subversion 来识别哪个文件作个修改,以及哪个文件已经过期等等。
何为分支?
分支( =branches= )就是一份拷贝,对主干( =trunk= )上的拷贝,你把它赋予分支的意义,那么它就是分支。在分支上面工作目的是为了让自己的工作不要影响到别人。
我们需要注意的是在 SVN 中,版本号是全局的,无论分支还是主干,其版本号是唯一全局按序递增的,同一个版本号不可能在多个分支中出现。
一、最初的步骤
SVN存储所有版本控制数据到一个中心版本库。
1、新建一个版本库
$svnadmin create /path/to/repos
$ls /path/to/repos
conf/ dav/ db/ format hooks/ locks/ README.txt
该命令建立一个新目录 /path/to/repos ,包含了一个 Subversion 版本库。请确定这个目录在你的本机上。
2、建立需要导入到版本库的文件和目录
$svn import /tmp/project file:///path/to/repos -m "initial import"
这里, /tmp/project 是你要导入的目录,为了使今后使用清楚,应该包含三个顶级子目录即 branches , tags , 和 trunk 。典型目录结构类似如下:
/tmp/project/branches/
/tmp/project/tags/
/tmp/project/trunk/
foo.c
bar.c
Makefile
...
这样,你就可以继续工作了。如 checkout 等。
二、基本命令
获取帮助
$svn help
有关svn所有的信息,都可以通过这条命令获取。
关于获取工作拷贝
得到一个工作拷贝
$svn checkout htttp://svn.example.com/repos/calc
这样你就有了一个 /calc 的个人拷贝,它是从: http://svn.example.com/repos/calc 提取出来的。
得到一个工作拷贝并放到你的新目录中
$svn checkout http://svn.example.com/repos/calc subv
这样工作目录(拷贝) calc 将放到你的新目录( subv )中。
SVN 可以有多种不同方式访问的 URL 形式:
file:/// 用来访问本地的
http:// 用来访问SVN的Apache的WebDAV协议
https:// 同上,不过用ssl加密
svn:// 用来访问SVN自定义的协议的的
svn+ssh:// 同上,不过用ssh封装
关于历史版本
展示当前目录历史信息
$svn log
这将展示项目各个版本的历史信息,每条记录信息包括谁、在什么时候、改了多少,以及相应的描述修改的日志信息。日志根据时间逆序排列。
展示指定文件的历史
$svn log foo.c
这将展示文件 foo.c 的历史信息。
详细展示指定文件的历史
$svn log --verbose foo.c
这样除了打印你所添加的信息之外还打印一些更为详细的信息比如哪些文件变了等。
按特定顺序(或指定版本)展示历史信息
$svn log -r 5:9
或
$svn log --revision 9:5
或
$svn log -r 8
这里用了 --revision (即 -r )选项,前两个分别按照时间顺序和逆序显示版本5和版本9之间的历史;最后一个显示版本8的历史。
注意有时候得到的是空信息,如: svn log -r 2
那不是错,那是说明当前目录在指定版本到现在没被修改过,可以用版本库顶级目录做为参数来查看。
如: svn log -r 2 http://svn.collab.net/repos/svn
查看最新的版本信息
$svn log -r HEAD
这样,显示的是已经提交的最新修改的信息。
使用带 --revision 的 svn update 和 svn checkout 来回到过去
$svn checkout --revision 2
$svn update --revision 2
这会覆盖当前的目录版本吗?还是可以指定 URL 来提取一个独立的版本?
得到一个指定时间的拷贝
$svn checkout --revision {2002-09-08}
或
$svn checkout --revision {15:30}
或
$svn checkout --revision {20020908T1530}
等等。
这里将会提取出离指定时间最近的版本,需要注意的是,假设指定了 2002-09-08 很有可能 2002-09-07 的某个时间更近,因为默认是以日期的0点开始的。
显示一段时间的版本历史信息
$svn log -revision {2002-09-07}:{2009-09-08}
这会找到这个时间的所有历史版本,也可版本号和时期混用表示时间段,不过不推荐这样。
关于工作拷贝更新
$svn update
这样,将会把你的工作拷贝更新为服务器上最新的版本,看到其他人的修改。不用你自己指定, SVN 会识别那些文件需要更新(可能出现的覆盖问题后面有解决方法)。该命令的输出信息将告诉你哪些文件被做了哪些修改到你的工作拷贝。
SVN 对 update 和 commit 可能产生问题的解决:
- 若在工作拷贝里没做修改,且服务器版本库在工作拷贝版本之后也没被提交过其他修改;那么
svn commit不做任何事,svn update不做任何事。 - 若在工作拷贝里做过修改,但服务器版本库在修改前工作拷贝的版本后没被提交过其他修改;那么
svn commit会成功地提交(更新服务器版本库),svn update不做任何事。 - 若在工作拷贝里没做修改,但服务器版本库在工作拷贝版本之后被提交过其他修改;那么
svn commit不做任何事,svn update会把你的工作拷贝版本更新为版本库中最新的。 - 若工作拷贝里做过修改,且服务器版本库在修改前工作拷贝的版本后被提交过其他修改;那么
svn commit首先会失败并要求update,svn update会合并版本库和本地的修改,如果有冲突会询问用户去解决(,之后就继续了??网上答案说是需要手动commit)。
关于工作拷贝修改
你可以使用任何你喜欢的编辑工具编辑文件,但是你不可以在修改目录结构时不通知 Subversion 。
为版本库添加一个新文件
$touch foo
$svn add foo
这里, foo 是待添加的文件。当然需要 commit 才会使版本库目录结构改变。先用普通 linux 命令在目录中建立了一个 foo 文件,然后通知 svn 要添加这个文件。如果 foo 是一个目录,那么 svn 默认会将 foo 目录下的所有文件也递归地添加进去(若仅添加目录不递归添加下面的文件需要使用 -N 参数)。
在版本库删除一个文件
$svn delete foo
这里, foo 是待删除的文件。当然需要 commit 才会使版本库目录结构改变,不需要非得用 linux 普通命令将 foo 文件删除再进行,只用 svn delete foo ,意思是版本库中“逻辑上”把这个文件删除了,版本库中没有了这文件,而实际在普通的文件系统中还有这个文件,可以重新再添加进去,但是如果 commit 之后文件就从文件系统删除了。
在版本库拷贝一个文件
$svn copy foo bar
这里,建立一个 foo 的拷贝文件: bar 。当然需要 commit 才会使版本库目录结构改变。不需要利用普通 linux 命令 cp 事先建立一个 bar 文件,会自动复制一份在文件系统中。
在版本库移动一个文件
$svn move foo bar
这里的例子相当于把文件 foo 重新命名为 bar 了。当然需要 commit 才会使版本库目录结构改变。不需要事先利用普通 linux 命令 mv 。
关于检查修改
检查自己的修改
$svn status
这个命令不会和版本库通信,其显示的信息是告诉你你的当前工作拷贝将对服务器上的版本库作怎样的修改,在当前工作拷贝的顶级目录运行,默认将递归检查子目录上所有的文件,该命令和 update 不一样,但有些共通之处(指出了变化)。
检查自己对某个文件的修改
$svn status stuff/fish.c
和上面的命令一样,不过指定了一个特定的文件项,那么就仅显示该文件项的信息。
提交你的修改
直接提交某个文件的修改
$svn commit button.c
这样,你就会把自己对 button.c 的修改提交到服务器上去,提交后会使版本库的版本号加1,以后可通过指定号码恢复(其他人的工作拷贝需要他们自己请求更新,才会把服务器上新版本更新到本地,看到你的修改,可能出现的覆盖问题后面有解决方法).
直接把所有修改的信息提交到版本库
$svn commit
由于既没有 --message (或 -m ),也没有 --file 来描述修改的信息,所以 SVN 会启用一个你喜欢的编辑器来编辑日志描述消息。如果写描述时想要取消提交,那么直接关闭编辑器,不要保存,然后在提示中选a即 abort ,(如果选的是c即 continue ,那么应该就会提交没有描述信息的修改);如果你保存了日志,那么只需要简单删掉所有文本,再次保存。
提交时增加描述修改的日志信息
$svn commit --message "add some infomation"
这里, --message 可以简化为 -m ,后面接的是描述信息的字符串。如果描述信息很短,就可以这样直接再命令行中指定。如果信息多,就需要指定文件,见后面。
提交时指定描述修改的日志文件
$svn commit --file logmsg
这里,利用了 --file ,后面接的就是相应的日志文件。
典型的工作周期
下面给出基于上述命令,在SVN版本控制工具中的基本工作流程。
1、更新你的工作拷贝
$svn update
2、做出修改
$svn add
$svn delete
$svn copy
$svn move
3、检验修改
$svn status
$svn diff
$svn revert
4、合并别人的修改到你的工作拷贝
$svn update
$svn resolved
5、提交你的修改
$svn commit
三、进阶命令
关于分支( 自 [2010-01-06 三] )
分支的创建
创建分支方法1
步骤如下:
$cd calc
$svn copy trunk branches/my-calc-branch
$svn commit -m "建立一个分支"
这样,将建立一个 calc/trunk 主干的拷贝(分支)。这里的拷贝不是拷贝整个目录是“廉价”的拷贝,只是做了一个链接,但是效果和拷贝的一样,以后的修改只在本分支上能体现出来。
创建分支方法2
$svn copy http://svn.example.com/repos/calc/trunk \
http://svn.example.com/repos/calc/branches/my-calc-branch \
-m "创建一个分支"
这里,和前面的方法没有什么区别,这里是一个即时的拷贝,直接在服务器上面操作,不用将数据取出再提交了。(网络上有人说 reversion 是分支创建时的版本, HEAD 是分支最新版本)
分支的合并
查看主干线项目的修改
$svn diff -r 343:344 http://svn.example.com/repos/calc/trunk
这样将在屏幕上输出主干线上 343 和 344 版本的区别,你可以知道主干线上做了什么修改进而确定是否应该将这些修改合并到自己的分支中,保持尽量和主干线的最新修改一致。
合并主干线上指定版本的修改到当前目录所在分支
$svn merge -r 343:344 http://svn.example.com/repos/calc/trunk
这里,假设你当前所在的工作目录就是你的分支目录,这样会将主干版本 343 到 344 上的修改直接应用( patch )到你当前的分支上面了,而不是仅仅打印到屏幕上面,保持你的分支和主干线不会脱离太远。可以这样提交:
$svn commit -m "将主干上面343到344版本上做的修改应用到本分支上了"
合并的结果和对 diff 的输出进行 patch 是一样的,但是 svn merge 命令比 patch 命令更强, patch 只能支持指定文件, svn merge 还可以支持目录等。如果你没有指定目标, svn 就会假定你要合并修改到当前目录,或当前目录的同名文件上。
合并主干线上指定版本的修改到指定的目录分支
$svn merge -r 343:344 http://svn.example.com/repos/calc/trunk my-calc-branch
这样,就将主干线上( http://svn.example.com/repos/calc/trunk )指定的 343 到 344 的修改合并到分支 my-calc-branch 上了。和 svn update 一样, svn merge 也会产生冲突不过两者冲突文件的名字会不同这样有利区分。
合并两个指定目录上面的修改到目标分支
$svn merge http://svn.example.com/repos/calc/trunk http://svn.example.com/repos/calc/branch1 my-calc-branch
这样,会将主线( trunk )到 branch1 上面进行的修改应用到 my-calc-branch 。 我们一定要注意记录合并的信息,防止重复合并修改造成错误。
模拟合并,仅输出并不实际操作
$svn merge --dry-run -r 343:344 http://svn.example.com/repos/calc/trunk
这样仅仅输出合并信息,并不实际合并操作我们这样再结合 svn status 可以判断当前的合并是不是重复或者错误的合并。我们也可以利用和 svn merge 一样的参数的 svn diff 来检测是否应该合并。
取消修改
$svn revert
如果你喜欢合并的修改,你可以使用 svn commit 来提交,如果不喜欢,就用 svn revert 来恢复修改。
分支合并常见操作
合并分支到主干
将分支上的修改合并到主干的步骤如下:
1、确认版本
$svn log --verbose --stop-on-copy \
http://svn.example.com/repos/calc/branches/my-calc-branch
这里, stop-on-copy 将只打印 my-calc-branch 分支的版本,更多信息查看 svn help log 这里是 341 。
2、更新版本
$cd calc/trunk
$svn update
这里进入主干目录得工作拷贝,然后更新成为最新得主干,假设版本号为 405 。
3、执行合并
$svn merge -r 341:405 http://svn.example.com/repos/calc/branches/my-calc-branch
这里,通过比较不同,会将分支合并到主干目录得工作拷贝上面。(问题:假设分支没有合并过主干的修改,那么主干在分支之后不就白做修改了?)
4、提交
$svn commit -m "Merged my-calc-branch changes r345:405 into trunk"
这样,会提交合并的结果到服务器,完成把分支合并到主干上,版本号是 406 。(问题:是不是分支必须要先将主干的最新修改给合并进来?)
再次合并分支到主干
假设又在分支上工作了一段时间,想要进行再次合并到主干,步骤如下
-
1、确认版本
$cd calc/trunk $svn log这里,首先在主干上查看上次分支合并到主干上得相关日志版本,可防止重复合并,发现是
406。 -
2、更新版本
$cd calc/trunk $svn update这里,合并前先把主干更新一下,假设
HEAD版本是480。 -
3、执行合并
$svn merge -r 406:480 http://svn.example.com/repos/calc/branches/my-calc-branch这样,就把分支上面的修改合并到主干上了。(问题:假设主干期间变化了,分支没有合并主干的信息行吗?会不会把主干的修改信息给冲掉了?)
-
4、提交
$svn commit -m "Merged my-calc-branch changes r406:480 into trunk"这样,就完成了第二次合并分支到主干的工作。
其它
最大的问题:是不是必须要在分支合并到主干前,保证期间主干上的修改分支都合并过才行?
冲突处理( 自 [2011-01-01 六])
预测冲突
$svn status -u
产生冲突
$svn update
当你修改之后, update ,别人在你修改期间提交了,那么可能就会产生冲突。
假设 filename 文件产生冲突之后, svn 会在你的目录下额外生成三个不受 svn 版本控制的文件,它们分别是:
filename.mine: 你更新(update)前的文件,它没有冲突,只有你最近的修改内容。filename.rOLDREV: 你修改前的没有冲突的文件,它只含有你工作前,上次更新的内容不含任何修改。filename.rNewREV: 别人修改的没有冲突的最新文件,它是在你工作时候别人更新到服务器的最新版本。
这里的 OLDREV 和 NewREV 表示版本号,分别是你工作前未修改的 base 版本,和服务器上最新的 HEAD 版本。
有了这三个文件,你使用 svn commit 的时候无法提交,除非把这三个文件删掉。(但是如果你单纯地删掉这三个文件的话,你提交的文件一定就是错的了,因为你提交的文件是含有冲突标记的文件,它包含了所有的冲突内容)
解决冲突
解决冲突三种方法:
- 手动合并冲突。
- 利用某一个临时文件来覆盖你的当前工作文件。
- 运行
svn revert <filename>放弃所做的修改。
处理冲突常用的操作
假设更新或者合并的时候产生冲突,冲突的文件是 filename, 即,在你上次下载工作拷贝以及提交之前,你和其它人同时修改了 filename 文件。
这个时候,将会产生前面所述的 filename (带有双方修改以及冲突标记), filename.mine (只有你的修改), filename.rOLDREV (你工作前的版本) filename.rNewREV (包含别人修改的最新版本)。
手动解决冲突
-
1、假设更新之后产生了冲突
这时候查看工作文件的内容如下
$cat filename输入之后,输出:
......some content...... <<<<<<<.mine ......content1...... ======== ......content2...... >>>>>>>.r2 ......some content......这里,小于号大于号和等号是冲突标记,用来标记产生冲突的位置,在你解决冲突之后一定要确保这些内容被删除了。
处于
<<<<<<<.mine=和=======之间的内容(......content1......)是你的有冲突的最新修改,处于
=======和>>>>>>>.r2之间的内容(......content2......)是当前提交到服务器的最新版本(假设版本号是2)和你的最新修改冲突的部分。 -
2、选择某一个修改方案
手动删除上述有冲突的内容和冲突标记。
这里,假设我们选择了
content1的内容,那么修改后的文件内容应该是:......some content...... ......content1...... ......some content...... -
3、告诉
svn解决冲突了$svn resolved filename这样,
svn应该会删掉三个临时文件了,然后你就可以svn commit提交了。记住如果运行这个命令就会删除临时文件,然后就能提交了,即使你的文件实际仍有冲突!
选择冲突中的一个方案覆盖另一个方案
因为发生冲突的方案都独立保存到临时文件中,选择利用某一个临时文件来覆盖你的当前工作文件,即可实现选择对应的修改方案。
假设决定取消自己的修改,只需要依次运行如下:
$cp filename.r2 filename
$svn resolved filename
这样就解决冲突了,实际相当于把你自己刚才的修改给放弃了,然后删掉临时文件。
使用 svn revert 撤销自己的修改
$svn revert filename
这样,就可以了,结果是取消了自己的修改,这里不用运行 svn resolved 了。
四、其他
这里记录一些杂乱整理不全的信息。
一旦 commit , 这个文件在本地文件系统中就会被删除(针对文件删除?)。
另外, SVN 的许多命令都有简写/别名,例如 checkout 可简写为 co , commit 可简写为 ci ,等等。
在添加两个有用的命令, svn info = 和 = svn ann
参考资料:
- 《使用Subersion进行版本控制》