- 在windows机器上开发得差不多了之后,打包传送到开发机编译,在开发机上解决编译错误。
[缺点] 浪费时间在打包解包,机器间传输代码。
- 在windows机器上开发之后,check in代码进分支,在开发机上check out或者update后,进行编译,解决编译错误。相当于把svn作为一种机器间通信方式。
[缺点] 提交进svn的代码甚至都没有编译过,我只能说,svn不是这样用的。
- 在linux开发机上安装samba, 在windows环境下开发,开发机上编译。
[优点] 在windows环境编译,很便捷;可以实时在开发机上编译。
[缺点] 如果不用命令行,TortoiseSVN在samba下慢得不能忍。
- 完全在Linux开发机上开发,编译。
[优点] 反馈更敏捷,命令行都已经提交完代码了,乌龟svn可能还在龟速更新。
[缺点] 缺少图形界面, svn操作完全是命令行的,不太直观。
通常大多数习惯前三种模式。在windows环境使用TortoiseSVN, 各种操作都封装的很好,使用起来也很傻瓜。最近在linux环境下开发,发现svn命令行用熟了比TortoiseSVN更好用,最重要的是反应速度更快。
我们的svn工作模式
SVN并行开发管理策略
总的原则:trunk保证相对稳定。分支合并到主干时将冲突降至最低。
(1) trunk用于集成、测试、发布,可以提交fixbug代码,但不允许直接提交新特性。
(2) 特性在分支上开发,在编译、测试通过后才能合并到主干。
(3) 特性分支确定一个负责人,负责每天执行从trunk到分支的合并。合并回trunk前,先执行一次trunk到dev的合并,然后在trunk上使用复兴分支。
(4) 特性分支的存在时间不能太长,不超过一周为宜。合入主干后不能继续使用。
相册特性开发-采用特性分支
特性分支: 为了使新特性的开发不影响测试或发布,把新特性开发放在分支上进行管理,分支的生命周期取决于特性的开发周期,该策略的特点是:
1) trunk用于集成、测试、发布
2) 新特性放在分支上开发
3) 分支负责人定期(如每天)把trunk的变更同步至分支
4) 分支合并回trunk前先执行trunk到分支的合并,然后使用复兴分支
5) 分支合并回trunk后,分支生命周期结束,清理分支。
第三方代码管理-采用供应商分支
这里的供应商分支通常是指对第三方源代码的管理,供应商分支的管理通常步骤如下:
1) 创建供应商分支目录,导入供应商代码,供应商代码的维护在此分支上进行
2) 将供应商分支拷贝(branch/tag方式)到trunk或开发分支的某个目录下
3) trunk或开发分支不对供应商分支代码做修改
4) trunk或开发分支确认使用供应商分支的新版本时,把供应商分支合并到trunk或开发分支
可能会有人存在疑问,为什么要对供应商内容做管理?
供应商代码和自己的产品代码可以看着是两个产品线,供应商的代码可能随时会发生变化,为了不让供应商代码的变更影响自己产品的正常研发,就有必要使用分支独立管理供应商代码。
供应商分支还可以被另外一种方案所替代,那就是:外部引用(svn:externals)
分支合并到主干策略
如果说到svn使用最痛苦的一点,那莫过于主干合分支,或者分支合主干时爆发的各种莫名其妙的冲突狂潮,如“树冲突”,“文件冲突”,“文件丢失”等等。解决起来费时费力,还容易出错,效率很低。有没有较好的解决方法,可以让日子好过一点呢?答案是有的,需要遵循下面的方法。
冲突产生的根源
svn版本管理原理
svn生成的每个版本只记录增量修改,每个文件都有一个根源版本。
根源版本
根源版本,即祖先版本,在合并的时候是比较重要的一个概念,类似于C++中基类和派生类的关系,这里所说的根源就是一个基础版本。
如上图所示,hello.h的v1版本是v2版本的根源,而v2则是v3、v4的根源。
当进行分支合并时,SVN会沿着分支和trunk追溯共同的根源版本,然后计算其差异,如果没有共同的根源,则会报树冲突,就只能使用忽略根源的方式进行合并了。
冲突1:丢失文件及可避免的树冲突(可以避免)
当多人协作开发时,如果分支间合并缺乏时序性,就有可能导致合并时丢失文件。
举个简单例子来说:
如上图所示:
A和B两人同时在2.0-dev分支上开发,A新增一个a.cpp,并提交到SVN版本库上,产生123版本;然后B基于A提交的a.cpp做修改,提交后生成一个新版本124;
如果B此时想把自己修改的版本124merge到trunk,那么问题就来了,因为124版本的根源版本是123,只合并124过来,实际上就相当于只合并124的变更内容,而此时trunk上还没有a.cpp,这时SVN就会认为trunk上的a.cpp被删除了(因为找不到),会提示用户一个树冲突;
【由于是从2.0-dev合并到trunk, trunk是主合并方,分支是从的关系(在SVN中,trunk的东西称着mine,而分支是theirs),所以处理树冲突的时候优先用mine来解决】
当124合并到trunk时,由于找不到a.cpp,SVN会报一个树冲突,用trunk解决冲突,实际上也就是不会把a.cpp合并过来,这就会导致合并时出现丢失文件的现象。
冲突2:文本冲突(无法避免)
当A、B两人同时修改一个文件的同一行时,工具不能判断到底是选择A的修改or选择B的修改,或者是A和B的修改都要,这个事情就会报一个冲突,这种冲突也就是文本冲突。
冲突3:树冲突 (无法避免)
当A、B同时改了一个文件,A删除了该文件,而B修改了该文件,这时由于二者的修改无法进行合并,所以只能让用户二选一,要么删除该文件、要么保留该文件,这种冲突是树冲突。
树冲突产生的场景比较多,以下A和B的任何一种操作组合都会导致树冲突。
|
推荐的合并实践
1) 创建分支时,在日志中写明主干当前版本号。
2) 创建分支后,每天同步一次主干到分支, 并在日志中写明合入的主干当前版本号,便于定位问题。合并命令的用法见小节《常用svn命令》
每天合并问题最容易定位与解决,这也应了敏捷开发中小步快跑的思想,以及持续集成的初衷都是一样的;如果这个频率不能保证,每周也至少2-3次,否则解决冲突与定位问题的代价就大了。
3)合并回主干前,先执行一次主干到分支的合并,然后在主干上使用复兴分支。复兴分支使用了合并跟踪技术(svn1.5以上),能够不指定合并版本范围就将分支中的改动自动合入主干。
4)特性分支的存在时间不能太长,不超过一周为宜。合入主干后不能继续使用,否则当下次从trunk合到此分支时,会引入大量的树冲突。
svn常见使用流程讲解
命令行下字符编码的问题:目前相册源程序文件使用的都是gbk编码,而svn提交时默认使用utf8编码。我们希望在终端使用GBK编码,包括编译调试,但是使用svn时切换到utf8编码。所以需要如下两个封装的命令
命令 |
在.bashrc中加入下列两行,并且执行source ~/.bashrc alias svncoding="export LC_ALL=\"en_US.UTF-8\"" alias gcccoding="export LC_ALL=\"en_US.ISO-8859-1\"" |
作用 |
编码方式切换 |
示例 |
svncoding切换到svn需要的编码(utf-8) gcccoding切换到开发、编译需要的编码(ISO-8859-1编码无法表示编码,但是开发机对应的默认中文显示用gbk。原因何在?开发机/etc/sysconfig/language里写的是RC_LANG="en_US.UTF-8",secureCRT中使用默认设置(windows默认GBK),看来是以终端编码为准) |
结果 |
命令 |
在.bashrc中加入下列两行,并且执行source ~/.bashrc export PHOTO_REPO= ” https://tc-svn.tencent.com/isd/isd_qzoneplatformdev_rep/QzonePhoto_proj” |
作用 |
代表相册代码svn库地址,便于快捷输入,减少输入量 |
示例 |
echo $PHOTO_REPO/trunk |
结果 |
https://tc-svn.tencent.com/isd/isd_qzoneplatformdev_rep/QzonePhoto_pro/trunk |
命令 |
svn info TRUNK_URL |
作用 |
获取svn库的最新信息,如trunk的当前版本号 |
示例 |
svn info $PHOTO_REPO/trunk |
结果 |
Path: trunk URL: https://tc-svn.tencent.com/isd/isd_qzoneplatformdev_rep/QzonePhoto_proj/trunk Repository Root: https://tc-svn.tencent.com/isd/isd_qzoneplatformdev_rep Repository UUID: df2599cb-4e79-6a4a-84f3-a1dd8b66c2e7 Revision: 166688 <- 最新版本号 Node Kind: directory Last Changed Author: reanshen Last Changed Rev: 166688 Last Changed Date: 2013-04-12 19:07:17 +0800 (Fri, 12 Apr 2013) |
命令 |
svn copy TRUNK_URL BRANCH_URL –m ”Create branch from trunk %REVISION%” |
作用 |
创建分支 |
示例 |
svn copy $PHOTO_REPO/trunk $PHOTO_REPO/branches/photo_xxxx -m “Create branch from trunk 166688” |
结果 |
Committed revision 166690. |
命令 |
svn co SVN_URL[@REVISION] [PATH] |
作用 |
把svn库中的代码检出到本地,进行开发,编译 SVN_URL: 想要检出的代码URL路径 REVISION: 想要检出的版本号 PATH: 想要将检出代码存放在何处。不填,则将URL最后一段作为文件夹名 |
示例 |
|
结果 |
检出代码到当前目录下的photo_xxxx下 |
命令 |
svn commit [PATH] –m “comments” |
作用 |
提交开发的代码 |
示例 |
svn commit . –m “add test cases” |
结果 |
提交了当前目录内的修改 |
命令 |
svn merge TRUNK_URL [–dry-run] |
作用 |
将主干改动同步到分支。svn1.5引入“合并跟踪”技术,让我们不再需要每次自己查找哪些版本需要同步到分支。svnbook中称这种合并技术为sync-merge。对于之前需要手动跟踪版本合并范围,效率的提升不是一点点。 前置条件:本地代码库保证无任何未提交改动(用svn status查看);使用svn update更新到分支最新版本。 --dry-run选项:可以尝试合并,但不修改本地代码。可以借此先看看冲突多不多。 不加—dry-run选项: 进行真实的合并,修改本地代码,需要进行提交。 |
示例 |
svn merge $PHOTO_REPO/trunk |
结果 |
将主干上次合并后的改动自动同步到分支 |
命令 |
svn merge –reintegrate BRANCH_URL |
作用 |
将分支代码合并进主干。 前置条件:主干中所有的更改都已合并入分支。主干的本地代码库无未提交的代码,并且使用svn update更新到最新版本。 |
示例 |
svn merge –reintegrate $PHOTO_REPO/branches/photo_xxxx |
结果 |
分支中所有的改动均合入主干。 无冲突时,OK,顺利合并; 有冲突时,如果是文本冲突,有两种选择。
会用vimdiff打开左中右三个垂直并列窗口 左:base版本(分支svn库中的版本) 中:mine版本(有本地修改的版本) 右:theirs版本(主干上的版本)
如果是树冲突,用svn status可以看出来。 $ svn status A + C code/bar.c > local edit, incoming delete upon update Summary of conflicts: Tree conflicts: 1 如何解决树冲突,视乎具体情况而定。 例如:主干上该文件被删除,则在分支上先使用svn delete删掉文件,再用svn resolve –accept working 解决冲突。 |
命令 |
svn delete BRANCH_URL –m “remove branch, reintegrated with trunk in r%REVISION%” |
作用 |
分支合入主干后就不能继续使用,否则再次从主干合入分支会造成大量树冲突。 清理掉svn数据库中的分支,让代码库更整洁一些,避免隐患。 |
示例 |
svn delete $PHOTO_REPO/branches/photo_xxxx \ -m "remove branch, reintegrated with trunk in r16666" |
结果 |
Committed revision 166702. 分支从版本库中删除 |
命令 |
svn copy BRANCH_URL@LAST_REVISION BRANCH_URL –m “resurrect branch photo_xxxx@%LAST_REVISION” |
作用 |
这时候有人问了,“万一我需要看某个分支改了哪些东西,删掉分支后我怎么看呢”? 不用担心,分支一旦出现就永远不会消失,我们只是让它隐藏而已。真的需要时,可以轻易让它复活。如此命令所示,其中分支最后存在的版本号可以通过svn log branches父目录获得 |
示例 |
svn copy $PHOTO_REPO/branches/photo_xxxx@166701 $PHOTO_REPO/branches/photo_xxxx -m "resurrect branch photo_xxxx@166701" |
结果 |
复活分支 |
大体的svn工作流程如上所示,下面是其他一些通用操作。
命令 |
svn update |
作用 |
将svn库中的代码更新到本地代码库 |
示例 |
svn up |
结果 |
命令 |
svn resolve –accept [working | theirs-full | mine-full |base] filename |
作用 |
冲突解决 --accept是个选择器 base: 选择上次最新checkout的版本(未作本地修改的) mine-full: 选择仅包含自己本地修改的版本,忽略来自服务器的代码改动 theirs-full: 选择仅包含来自服务器的代码改动,忽略自己本地的改动 working: 选择手动merge完成后的版本 |
示例 |
|
结果 |
命令 |
svn status [-v] |
作用 |
查看本地文件状态,可以看到当前目录下所有的改动 -v: 可以显示所有的文件信息,包括未修改的。 -u: 可以用”*”标识有更新的文件 |
示例 |
svn status |
结果 |
? scratch.c (不在版本控制中) A stuff/loot (新加文件,未提交) A stuff/loot/new.c D stuff/old.c (删除文件,未提交) M bar.c (修改文件,未提交) C cc.c (有冲突未解决的) |
命令 |
svn log . –stop-on-copy –l N [-v] |
作用 |
查看svn记录 --stop-on-copy: 在分支从主干分出时停止 -l N: 显示最多N个版本的Log -v: 显示详细的改动(文件改动) |
示例 |
svn log . –stop-on-copy –l 1 -v |
结果 |
------------------------------------------------------------------------ r164717 | philchen| 2013-03-29 09:22:23 +0800 (Fri, 29 Mar 2013) | 2 lines Changed paths: M /QzonePhoto_proj/branches/photobuild_xxxx/application/qqphoto/comm/photo_log/include/monitor_id_define.h |
命令 |
svn diff [filename] |
作用 |
未指定filename: 查看当前目录的所有更改 指定filename: 查看指定文件的修改 |
示例 |
svn diff |
结果 |
如果未配置diff-cmd, 默认使用svn自带的diff工具,输出格式如下 Index: bar.c =================================================================== --- bar.c (revision 3) [‘-‘标识前一版本的] +++ bar.c (working copy) [‘+’标识当前本地文件的] @@ -1,6 +1,7 @@[解释:’-‘标识前一版本,”1,6”指显示从第一行开始,共显示6行;’+’标识当前版本,”1,7”指显示从第一行开始,共显示7行] #include<stdlib.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; } 如果配置了diff-cmd, 就使用配置的。详见”svn配置”一节。 |
命令 |
svn add FOO |
作用 |
把FOO(文件,目录,符号链接)加入到版本库。添加目录时目录下面的所有东西都被加进来,除非文件的后缀在svn::ignore里面。 |
示例 |
svn add newproj/ |
结果 |
命令 |
svn delete FOO |
作用 |
把FOO(文件,目录,符号链接)从版本库中删除。文件或者符号链接会立即从文件系统中删除。 |
示例 |
svn del newproj/ |
结果 |
svn配置
配置diff工具(vimdiff) |
作用:在命令行近似于ui方式较直观显示差异 |
打开~/.subversion/config, 编译diff-cmd选项。 diff-cmd = /usr/local/py/diffwrap.py diffwrap.py内容如下 #!/usr/bin/env python import sys import os DIFF = "/usr/local/bin/vimdiff" # Subversion provides the paths we need as the last two parameters. LEFT = sys.argv[-2] RIGHT = sys.argv[-1] # Call the diff command (change the following line to make sense for # your diff program). cmd = [DIFF, LEFT, RIGHT] os.execv(cmd[0], cmd) 当使用一个外部的diff命令时,Subversion会生成一个非常复杂的命令行。第一个参数就是具体的--diff-cmd,然后就是具体的 --extensions (尽管使用空白的 --符号时会忽略扩展),或者如果没有指定--extensions或者--extensions为空的话,就加上‘-u’参数。第三和第四个参数,Subversion会传递一个“-L”还有第一个文件的标签(例如,“"project_issues.html (revision 11209)”)。第五个和第六个就是另一个“-L”和第二个文件的标签。第七和第八个参数分别是第一个和第二个文件的名称(例 如,“.svn/text-base/project_issues.html.svn-base”和“.svn/tmp /project_issues.html.tmp”)。 |
配置merge工具(vimdiff) |
|||||||||||||||||||||||
作用:在命令行近似于ui方式较直观显示差异 左:base版本 中:merged编辑区 右:来自外部的版本 |
|||||||||||||||||||||||
打开~/.subversion/config, 编译merge-tool-cmd选项。 merge-tool-cmd=”/usr/local/py/mymerge.py” 然后编辑mymerge.py
|
|||||||||||||||||||||||
vimdiff用法 窗口焦点切换,即切换当前窗口 CTRL-w h 跳转到左边的窗口 CTRL-w j 跳转到下面的窗口 CTRL-w k 跳转到上面的窗口 CTRL-w l 跳转到右边的窗口 CTRL-w t 跳转到最顶上的窗口 CTRL-w b 跳转到最底下的窗口 CTRL-w w 跳转到另一个窗口 CTRL-w CTRL-w 跳转到另一个窗口,同CTRL-w w 光标移动 移动光标,切分窗口会同步移动,使用:set noscrollbind命令可取消同步 ]c 跳到下一个不同的地方 [c 跳到上一个不同的地方 上下文折叠 默认情况下,vimdiff会将文件中不同之处上下6行之外的相同文本折叠隐藏,可通过 :set diffopt=context:3 修改显示的上下文行数。 zo 打开折叠 zc 关闭折叠 文件合并 dp 将当前窗口光标位置处的内容复制到另一窗口 do 将另一窗口光标位置处的内容复制到当前窗口 diffupdate 重新比较两个文件,如果手动修改文件的话有时不会自动同步 文件操作 yy 复制当前行 nyy 复制当前行开始的n行 dd 删除当前行 ndd 删除当前行开始的n行 p 粘贴 u 撤销 CTRL-r 重复(即取消撤销) wa 全部保存 wqa 全部保存后退出 qa 全部退出 qa! 全部强制退出,不保存文件修改 |