2010-01-27

Subversion 版本库整理实战

在使用 svnadmin dump, svnadmin load, svndumpfilter 等命令对 Subversion 版本库裁减,可真的不是 a piece of cake. 有很多技巧,窍门和陷阱。

这不,今天一个客户的电话,就涉及到了 svn 版本库裁减的好些问题:
  • svndumpfilter 命令后面的 include 或者 exclude 子语句,后面的多个路径用逗号分割可以么?
  • svndumpfilter 命令后面的 include 或者 exclude 子语句,后面的路径可以使用通配符么?如何使用?
  • svndumpfilter 命令涉及的路径非常多,在命令行写太复杂了,甚至可能超过 SHELL 对命令行长度的限制,该如何?
  • 重新整理的版本库为什么有很多空的提交,说是为了占位之用?
  • 重新整理后的版本库的路径可以改变么?

简要回答

客户的实际需求是将几十个GB的代码库,将某一个或某几个分支拆分出来形成新的版本库。但是在执行 svndumpfilter 后产生的过滤文件只有几十MB,而且在导入到新版本库后,版本库当中无内容,只有空的提交。 后来查看客户的命令,发现 svndumpfilter 命令的格式不正确,include 或者 exclude 子命令的参数是一个路径列表,这个路径列表应该用空格分隔,而不能用逗号。 还有:路径不支持通配符,因此需要一个一个的写,如果太多,可以放在一个文件中,用回车符分割。 在命令行中应该用空格分割包含或者排除的路径列表。看看 svndumpfilter 的这段代码:
if (subcommand->cmd_func != subcommand_help)
{
    opt_state.prefixes = apr_array_make(pool, os->argc - os->ind,
    sizeof(const char *));
    for (i = os->ind ; i< os->argc; i++)
    {
        const char *prefix;
        /* Ensure that each prefix is UTF8-encoded, in internal
           style, and absolute. */
        SVN_INT_ERR(svn_utf_cstring_to_utf8(&prefix, os->argv[i], pool));
        prefix = svn_path_internal_style(prefix, pool);
        prefix = svn_path_join("/", prefix, pool);
        APR_ARRAY_PUSH(opt_state.prefixes, const char *) = prefix;
    }
  • 将 os->argv 的数组转换为列表。即:用空格分割各个包含或者排除的路径
  • prefix 进行了整理,包括在前面增加 "/" 字符,就是说路径前面的 "/" 字符可有可无
如果包含或者排除的路径太长,可以将路径写入文件中,用  --targets target_file 指定。参见下面 svndumpfilter 的代码片段:
if (opt_state.targets_file)
{
    svn_stringbuf_t *buffer, *buffer_utf8;
    const char *utf8_targets_file;

    /* We need to convert to UTF-8 now, even before we divide
       the targets into an array, because otherwise we wouldn't
       know what delimiter to use for svn_cstring_split().  */

    SVN_INT_ERR(svn_utf_cstring_to_utf8(&utf8_targets_file,
    opt_state.targets_file, pool));

    SVN_INT_ERR(svn_stringbuf_from_file2(&buffer, utf8_targets_file,
 pool));
    SVN_INT_ERR(svn_utf_stringbuf_to_utf8(&buffer_utf8, buffer, pool));

    opt_state.prefixes = apr_array_append(pool,
    svn_cstring_split(buffer_utf8->data, "\n\r", TRUE, pool),
 opt_state.prefixes);
 }
其含义为:
  • 如果在 include 或者 exclude 语句包含路径列表太长,可以将包含路径写入文件,使用 --targets 命令指向该文件
  • 在该文件中,用回车键分割各个路径
  • 在该文件中,除了路径不能有其他参数

一个不太简单的例子

要求:一个版本库(包含 /trunk, /branches/1.x, /tags/1.0),只将其中的分支  /branches/1.x 导出

1. 先初始化示例版本库,包含主线 /trunk, 分支 /tags/1.0 和 /branches/1.x

/tmp/svn$ svnadmin create repos
/tmp/svn$ svn co file:///tmp/svn/repos work
取出版本 0。
/tmp/svn$ cd work/
/tmp/svn/work$ mkdir trunk branches tags
/tmp/svn/work$ svn add *
A         branches
A         tags
A         trunk
/tmp/svn/work$ svn ci -m "初始化项目目录"
增加           branches
增加           tags
增加           trunk

提交后的版本为 1。
/tmp/svn/work$ cd trunk
/tmp/svn/work/trunk$ mkdir src doc
/tmp/svn/work/trunk$ echo hello > readme.txt
/tmp/svn/work/trunk$ echo source file > src/hello.c
/tmp/svn/work/trunk$ svn add *
A         doc
A         doc/api.txt
A         readme.txt
A         src
A         src/hello.c
/tmp/svn/work/trunk$ svn ci -m "文件初始化"
增加           trunk/doc
增加           trunk/doc/api.txt
增加           trunk/readme.txt
增加           trunk/src
增加           trunk/src/hello.c
传输文件数据...
提交后的版本为 2。
/tmp/svn/work/trunk$ date >> readme.txt
/tmp/svn/work/trunk$ date >> src/hello.c
/tmp/svn/work/trunk$ svn st
M       src/hello.c
M       readme.txt
/tmp/svn/work/trunk$ svn ci -m "修改文件..."
正在发送       trunk/readme.txt
正在发送       trunk/src/hello.c
传输文件数据..
提交后的版本为 3。
/tmp/svn/work/trunk$ cd ..
/tmp/svn/work$ svn cp trunk branches/1.x
A         branches/1.x
/tmp/svn/work$ svn ci -m "创建分支 1.x"
增加           branches/1.x
增加           branches/1.x/doc
增加           branches/1.x/readme.txt
增加           branches/1.x/src
增加           branches/1.x/src/hello.c

提交后的版本为 4。
/tmp/svn/work$ svn cp trunk tags/1.0.0
A         tags/1.0
/tmp/svn/work$ svn ci -m "创建里程碑 1.0"
增加           tags/1.0
增加           tags/1.0/doc
增加           tags/1.0/readme.txt
增加           tags/1.0/src
增加           tags/1.0/src/hello.c

提交后的版本为 5。
/tmp/svn/work$ cd branches/1.x/
/tmp/svn/work/branches/1.x$ echo branch >> readme.txt
/tmp/svn/work/branches/1.x$ svn ci -m "分支中修改 readme.txt"
正在发送       1.x/readme.txt
传输文件数据.
提交后的版本为 6。
/tmp/svn/work/branches/1.x$ echo branch >> src/hello.c
/tmp/svn/work/branches/1.x$ svn ci -m "分支中修改 hello.c"
正在发送       1.x/src/hello.c
传输文件数据.
提交后的版本为 7。
/tmp/svn/work/branches/1.x$ cd ../trunk/
/tmp/svn/work/trunk$ ls
doc  readme.txt  src
/tmp/svn/work/trunk$ echo new api >> doc/api.txt
/tmp/svn/work/trunk$ svn ci -m "new 主线中增加"
正在发送       trunk/doc/api.txt
传输文件数据.
提交后的版本为 8。
/tmp/svn$ svn log file:///tmp/svn/repos
------------------------------------------------------------------------
r8 | jiangxin | 2010-01-27 11:00:18 +0800 (三, 2010-01-27) | 1 行

主线中增加 new api
------------------------------------------------------------------------
r7 | jiangxin | 2010-01-27 10:59:17 +0800 (三, 2010-01-27) | 1 行

分支中修改 hello.c
------------------------------------------------------------------------
r6 | jiangxin | 2010-01-27 10:59:05 +0800 (三, 2010-01-27) | 1 行

分支中修改 readme.txt
------------------------------------------------------------------------
r5 | jiangxin | 2010-01-27 10:58:33 +0800 (三, 2010-01-27) | 1 行

创建里程碑 1.0
------------------------------------------------------------------------
r4 | jiangxin | 2010-01-27 10:58:12 +0800 (三, 2010-01-27) | 1 行

创建分支 1.x
------------------------------------------------------------------------
r3 | jiangxin | 2010-01-27 10:57:42 +0800 (三, 2010-01-27) | 1 行

修改文件...
------------------------------------------------------------------------
r2 | jiangxin | 2010-01-27 10:57:15 +0800 (三, 2010-01-27) | 1 行

文件初始化
------------------------------------------------------------------------
r1 | jiangxin | 2010-01-27 10:55:58 +0800 (三, 2010-01-27) | 1 行

初始化项目目录
------------------------------------------------------------------------

2. 导出版本4到版本7的数据到导出文件

通过 log 可以看出和 branches/1.x 相关的提交只是从第4个版本到第7个版本,于是只导出这些版本到 dumpfile
/tmp/svn$ svnadmin dump /tmp/svn/repos -r4:7 > r4-7.dump
* 已转存版本 4。
警告: 版本 1 的参考数据比最旧的转存数据版本 (4)还旧。
警告:  装载这个转存到空的版本库会失败。

警告: 版本 2 的参考数据比最旧的转存数据版本 (4)还旧。
警告:  装载这个转存到空的版本库会失败。

警告: 版本 3 的参考数据比最旧的转存数据版本 (4)还旧。
警告:  装载这个转存到空的版本库会失败。

警告: 版本 2 的参考数据比最旧的转存数据版本 (4)还旧。
警告:  装载这个转存到空的版本库会失败。

警告: 版本 3 的参考数据比最旧的转存数据版本 (4)还旧。
警告:  装载这个转存到空的版本库会失败。

* 已转存版本 5。
* 已转存版本 6。
* 已转存版本 7。

3. 新版本库由 r4-8.dump 加载失败

从上一步导出的 r4-7.dump 导入到新版本库。
/tmp/svn$ svnadmin create newrepos
/tmp/svn$ svnadmin load newrepos < r4-7.dump
<<< 开始新的事务,基于原始版本 4
 * 正在增加路径: trunk ...完成。
 * 正在增加路径: trunk/doc ...完成。
 * 正在增加路径: trunk/doc/api.txt ...完成。
 * 正在增加路径: trunk/src ...完成。
 * 正在增加路径: trunk/src/hello.c ...完成。
 * 正在增加路径: trunk/readme.txt ...完成。
 * 正在增加路径: branches ...完成。
 * 正在增加路径: branches/1.x ...完成。
 * 正在增加路径: branches/1.x/doc ...完成。
 * 正在增加路径: branches/1.x/doc/api.txt ...完成。
 * 正在增加路径: branches/1.x/src ...完成。
 * 正在增加路径: branches/1.x/src/hello.c ...完成。
 * 正在增加路径: branches/1.x/readme.txt ...完成。
 * 正在增加路径: tags ...完成。

------- 提交新版本 1 (从原始版本 4 装载) >>>

<<< 开始新的事务,基于原始版本 5
svnadmin: 当前版本库不存在相对源版本 -2
 * 正在增加路径: tags/1.0 ...

/tmp/svn$ svn log file:///tmp/svn/newrepos
------------------------------------------------------------------------
r1 | jiangxin | 2010-01-27 10:58:12 +0800 (三, 2010-01-27) | 1 行

创建分支 1.x
------------------------------------------------------------------------
导入出错,这是为什么呢?因为在导出包含的 r5 ,是从 trunk 版本 2 创建里程碑 1.0,因为我们的导出是从版本4开始的,不包含版本2,因此导致 r5 的导出记录导入出错。

4. 对 r4-7.dump 过滤,只包含 branches 内容,再导入,成功

/tmp/svn$ svnadumpfilter --drop-empty-revs --renumber-revs include branches < r4-7.dump > branch_only.dump
包含 (以及丢弃空版本) 的前缀:
 '/branches'

版本 4 提交为 4。
跳过版本 5。
版本 6 提交为 5。
版本 7 提交为 6。

删除 1 个版本。

版本被重新编号如下:
 7 => 6
 6 => 5
 5 => (丢弃)
 4 => 4

丢弃 12 个节点:
 '/tags'
 '/tags/1.0'
 '/tags/1.0/doc'
 '/tags/1.0/readme.txt'
 '/tags/1.0/src'
 '/tags/1.0/src/hello.c'
 '/trunk'
 '/trunk/doc'
 '/trunk/doc/api.txt'
 '/trunk/readme.txt'
 '/trunk/src'
 '/trunk/src/hello.c'
/tmp/svn$ rm -rf newrepos
/tmp/svn$ svnadmin create newrepos
/tmp/svn$ svnadmin load newrepos < branch_only.dump

5. 新版本库中的路径由 branches/1.x 修改为 trunk,如何操作?

按照上面的步骤,创建的新版本库中的路径都是在 branches/1.x ,如下:
/tmp/svn$ svn log file:///tmp/svn/newrepos
------------------------------------------------------------------------
r3 | jiangxin | 2010-01-27 10:59:17 +0800 (三, 2010-01-27) | 1 行

分支中修改 hello.c
------------------------------------------------------------------------
r2 | jiangxin | 2010-01-27 10:59:05 +0800 (三, 2010-01-27) | 1 行

分支中修改 readme.txt
------------------------------------------------------------------------
r1 | jiangxin | 2010-01-27 10:58:12 +0800 (三, 2010-01-27) | 1 行

创建分支 1.x
------------------------------------------------------------------------
/tmp/svn$ svn ls -R file:///tmp/svn/newrepos
branches/
branches/1.x/
branches/1.x/doc/
branches/1.x/doc/api.txt
branches/1.x/readme.txt
branches/1.x/src/
branches/1.x/src/hello.c
如果创建过程中对导出文件进行进一步的处理,就可以实现新版本中,提交都在 /trunk 而非 branches 中: 查看导出文件中 Node-path 字段,同时输出行号:
/tmp/svn$ grep -n "^Node-path" branch_only.dump
23:Node-path: branches
32:Node-path: branches/1.x
41:Node-path: branches/1.x/doc
50:Node-path: branches/1.x/doc/api.txt
71:Node-path: branches/1.x/src
80:Node-path: branches/1.x/src/hello.c
102:Node-path: branches/1.x/readme.txt
142:Node-path: branches/1.x/readme.txt
173:Node-path: branches/1.x/src/hello.c
过滤掉 branches 目录创建相关内容,因为我们需要的是 branches/1.x 开始的内容
/tmp/svn$ head -22 branch_only.dump  > top
/tmp/svn$ tail -n +32 branch_only.dump > tail
/tmp/svn$ cat top tail > branch_only_strip.dump
/tmp/svn$ grep -n "^Node-path" branch_only_strip.dump
23:Node-path: branches/1.x
32:Node-path: branches/1.x/doc
41:Node-path: branches/1.x/doc/api.txt
62:Node-path: branches/1.x/src
71:Node-path: branches/1.x/src/hello.c
93:Node-path: branches/1.x/readme.txt
133:Node-path: branches/1.x/readme.txt
164:Node-path: branches/1.x/src/hello.c
对导出文件的内容进行字符串替换,将 branches/1.x 替换为 trunk
/tmp/svn$ sed -e "s@^\(Node-path: \|Node-copyfrom-path: \)branches/1.x@\1trunk@" branch_only_strip.dump > new_trunk.dump
/tmp/svn$ grep -n "^Node-path" new_trunk.dump
23:Node-path: trunk
32:Node-path: trunk/doc
41:Node-path: trunk/doc/api.txt
62:Node-path: trunk/src
71:Node-path: trunk/src/hello.c
93:Node-path: trunk/readme.txt
133:Node-path: trunk/readme.txt
164:Node-path: trunk/src/hello.c
使用字符串替换之后的导出文件 (new_trunk.dump),导入到新版本中
/tmp/svn$ rm -rf newrepos/
/tmp/svn$ svnadmin create newrepos
/tmp/svn$ svnadmin load newrepos < new_trunk.dump
<<< 开始新的事务,基于原始版本 4
 * 正在增加路径: trunk ...完成。
 * 正在增加路径: trunk/doc ...完成。
 * 正在增加路径: trunk/doc/api.txt ...完成。
 * 正在增加路径: trunk/src ...完成。
 * 正在增加路径: trunk/src/hello.c ...完成。
 * 正在增加路径: trunk/readme.txt ...完成。

------- 提交新版本 1 (从原始版本 4 装载) >>>

<<< 开始新的事务,基于原始版本 5
 * 正在修改路径: trunk/readme.txt ...完成。

------- 提交新版本 2 (从原始版本 5 装载) >>>

<<< 开始新的事务,基于原始版本 6
 * 正在修改路径: trunk/src/hello.c ...完成。

------- 提交新版本 3 (从原始版本 6 装载) >>>

/tmp/svn$ svn log file:///tmp/svn/newrepos

3 | jiangxin | 2010-01-27 10:59:17 +0800 (三, 2010-01-27) | 1 行

分支中修改 hello.c
------------------------------------------------------------------------
r2 | jiangxin | 2010-01-27 10:59:05 +0800 (三, 2010-01-27) | 1 行

分支中修改 readme.txt
------------------------------------------------------------------------
r1 | jiangxin | 2010-01-27 10:58:12 +0800 (三, 2010-01-27) | 1 行

创建分支 1.x
------------------------------------------------------------------------
/tmp/svn$ svn ls -R file:///tmp/svn/newrepos
trunk/
trunk/doc/
trunk/doc/api.txt
trunk/readme.txt
trunk/src/
trunk/src/hello.c
/tmp/svn$ exit
下面是相关版本库整理相关命令的手册,摘抄自:群英汇版本控制帮助中心

版本库整理相关命令

1. 导出 ── svnadmin dump

svnadmin dump
该命令将版本库导出到一个格式文件,该导出文件包含所有版本库历史信息,可以用于版本库备份,或导入其它版本库。
用法:
svnadmin dump REPOS_PATH [-r LOWER[:UPPER]] [--incremental] [-q]
参数:
  • REPOS_PATH必须是本地路径 如: /opt/svn/svnroot/repos2/
  • -r LOWER[:UPPER]
    导出从版本 LOWER 到版本 UPPER(或仅导出 LOWER 版本,如果UPPER不提供)的版本库历史。如果不提供该参数,则导出全部历史。
  • --incremental
    导出的第一个版本是和前一次版本的变更,用于增量备份和导入。如果不提供该参数,第一个导出版本是完整内容。
  • -q
    在标准错误输出不显示进度 (仅错误)
  • --help
    查看 svnadmin dump 命令的用法
示例:
$ svnadmin dump /opt/svn/svnroot/repos2 > dumpfile.txt

2. 导入 ── svnadmin load

svnadmin load
从标准输入读取版本库转存(导出)的格式文件,并导入到新版本库中。
用法:
svnadmin load [--ignore-uuid|--force-uuid] [--use-pre-commit-hook] [--use-post-commit-hook] [--parent-dir ARG] REPOS_PATH
参数:
  • REPOS_PATH
    必须是本地路径。如: /opt/svn/svnroot/repos2/。如果是空版本库(即刚刚用 svnadmin create 创建),则会用标准输入流中的 UUID 替换该库的 UUID。
  • --ignore-uuid
    即使目标版本库是空的,也不用标准输入流中的 UUID 替换版本库的 UUID。
  • --force-uuid
    即使目标版本库非空(含一次以上的提交),如果流中存在UUID,则设定为版本库的 UUID。
  • --use-pre-commit-hook
    提交版本前调用 pre-commit 钩子
  • --use-post-commit-hook
    提交版本后调用 post-commit 钩子
  • --parent-dir ARG
    加载到版本库指定的目录中,缺省加载到根
  • -q [--quiet]
    在标准错误输出不显示进度 (仅错误)
  • --help
    查看 svnadmin load 命令的用法
示例: $ svnadmin load --parent-dir new/subdir/for/project /opt/svn/svnroot/new_repos < dumpfile.txt

3. 裁减 ── svndumpfilter

svndumpfilter
从标准输入读取版本库转存(导出)的格式文件,并导入到新版本库中。
用法:
svndumpfilter help [include] [exclude]
svndumpfilter exclude PATH_PREFIX ... [OPTIONS ...]
svndumpfilter include PATH_PREFIX ... [OPTIONS ...]
子命令:
  • exclude 从标准输入中排除某个/某些路径下的内容,仅输出其它未指定路径下的内容。
  • include
    仅从标准输入中包含某个/某些路径下的内容,其它未指定的路径下的内容被抛弃。
  • help 查看帮助。后面提供 include 或者 exclude 参数,则输出 include 或 exclude 子命令的详细帮助。
参数:
  • PATH_PREFIX ...
    路径前缀。可以是空格分隔的多个前缀,将对 svn 导出文件中属于该前缀之下路径的文件或者目录进行相应的处理(忽略或者包含)。前缀如果不包含"/",将会自动添加一个"/"。
  • --drop-empty-revs
    删除因过滤而产生的空版本。
  • --renumber-revs
    过滤后重编余下的版本。
  • --skip-missing-merge-sources
    跳过缺少的合并源。
  • --targets ARG
    传递文件 ARG 的内容为额外的参数
  • --preserve-revprops
    不过滤版本属性。
  • --quiet
    不显示过滤的统计数据。
示例:
  • 将 svn 的 dump 文件 inputfile 中出现的以 /trunk/module1 或者 /trunk/module2 为前缀的文件和路径忽略,其余文件和目录转存到文件 filteredfile。
    $ svndumpfilter --drop-empty-revs --renumber-revs --skip-missing-merge-sources exclude /trunk/module1 /trunk/module2  < inputfile > filteredfile

4. 导入后目录降级

导入后目录降级
即导入到一个子目录中。如旧版本库 old_repos 中的 /trunk, /tags, /branches 等目录,导入到新库 new_repos 中的新路径为 repos1/trunk, repos1/tags, repos1/branches。
实现目录降级非常简单:
  • 在新版本库中创建要导入到的子目录。如在新版本库 new_repos 中创建目录 repos1:
    svn mkdir file:///opt/svn/svnroot/new_repos/repos1 -m "create new subdir for import"
  • 在使用 svnadmin load 导入时,提供参数 --parent-dir DIR_NAME。如导入到新库的 repos1 目录:
    svnadmin load --parent-dir repos1 /opt/svn/svnroot/new_repos < old_repos_dumpfile

5. 导入后目录升级

导入后目录升级
即 从旧版本的一个子目录的导出内容,导入到一个新版本库的根目录中。如旧版本库 old_repos 中的 repos1/trunk, repos1/tags, repos1/branches 等目录,导入到新库 new_repos 中的新路径为 trunk, tags, branches。
实现目录升级稍微复杂,需要对导出文件进行替换操作。
  • 将旧版本的 repos1 模块下的文件(repos1/trunk, repos1/tags, repos1/branches)导出:
    $ svnadmin dump /opt/svn/svnroot/old_repos | svndumpfilter include /repos1 --drop-empty-revs --renumber-revs > filteredfile
    版本被重新编号如下:
       10 => 7
       9 => 6
       8 => 5
       7 => 4
       6 => 3
       5 => 2
       4 => (丢弃)
       3 => 1
       2 => (丢弃)
       1 => (丢弃)
       0 => 0
  • 将导出文件 filteredfile 中的路径 repos1 替换为空。
    sed -e "s@^\(Node-path: \|Node-copyfrom-path: \)repos1/@\1@"  filteredfile > filteredfile.fixed
  • 用替换过的导出文件,导入到新库。如导入到 new_repos 版本库:
    svnadmin load /opt/svn/svnroot/new_repos < filteredfile.fixed
  • 删除新库中可能包含的 /repos1(!) 如果要避免在新库中出现 /repos1 空目录,在导出旧版本库时使用 --revision X:Y 参数。X 是创建 /repos1 目录后的下一个版本,Y是版本库最新版本。
blog comments powered by Disqus