|
| 1 | +--- |
| 2 | +layout: post |
| 3 | +title: "使用 git-svn 整理 SVN 版本库" |
| 4 | +--- |
| 5 | + |
| 6 | +SVN 本身提供了如下版本库整理工具: |
| 7 | + |
| 8 | +* svnadmin dump |
| 9 | +* svndumpfilter include |
| 10 | +* svndumpfilter exclude |
| 11 | +* svnadmin load |
| 12 | + |
| 13 | +其中 `svnadmin dump` 将整个版本库或部分提交导出为一个导出文件; `svndumpfilter` |
| 14 | +基于配置项的路径(SVN 1.7的 svndumpfilter 还支持通配符路径)对导出文件进行过滤, |
| 15 | +过滤结果保存在为新的导出文件; `svnadmin load` 将导出文件导入到另外的版本中, |
| 16 | +导入过程有两个选择——维持路径不变,或导入到某个路径之下。 |
| 17 | + |
| 18 | +相对于Git提供的用于提交整理的 `git filter-branch` 命令,SVN的版本库整理工具能做的实在不多。 |
| 19 | +而且用SVN的相关工具容错性太差,操作过程经常被中断,实在是步步惊心。 |
| 20 | + |
| 21 | +最近遇到的一个案例,需要将两个 SVN 版本库(bar 和 baz)的全部历史导入到另外一个 SVN 版本库(foo)中。 |
| 22 | +并要求版本库 bar 和 baz 的目录结构采用 foo 中统一规定的目录结构。 |
| 23 | + |
| 24 | +## 将 bar 和 baz 版本库转换为本地Git库 ## |
| 25 | + |
| 26 | +以 bar 为例,将两个版本库(bar 和 baz)转换为本地的 Git 版本库,以便使用强大的 |
| 27 | +`git filter-branch` 命令对提交中的文件路径进行逐一的修改。 |
| 28 | + |
| 29 | + $ git init git/bar |
| 30 | + $ cd git/bar |
| 31 | + $ git svn init --no-metadata file:///path/to/svn/bar |
| 32 | + $ git svn fetch |
| 33 | + |
| 34 | +说明: |
| 35 | + |
| 36 | +* SVN 版本库 bar 位于本机的路径 /path/to/svn/bar 下。 |
| 37 | +* 导出的 Git 版本库位于 git/bar 目录下。 |
| 38 | +* 因为版本库 bar 并未使用分支(未采用 trunk、branches、tags目录结构),因此执行 `git svn` 时并未使用 `-s` 等参数。 |
| 39 | + |
| 40 | +## 源版本库中文件名过长的问题 ## |
| 41 | + |
| 42 | +Windows和Linux下文件名长度限制不同,前者255个Unicode字符,后者为255个字节。 |
| 43 | +在此次转换中就遇到 bar 版本库中存在若干文件名超长的文件,导致无法在 Linux 平台上检出。 |
| 44 | +为避免后续操作中出现错误,对其进行重命名。 |
| 45 | + |
| 46 | + |
| 47 | +首先创建一个脚本 `rename.sh`,该脚本将提供给 `git filter-branch` 命令对版本库中超长文件名进行重命名操作。 |
| 48 | + |
| 49 | + #!/bin/sh |
| 50 | + |
| 51 | + git ls-files -s | \ |
| 52 | + sed \ |
| 53 | + -e "s#\(\t.*/file-name-is-too-long\).*\.pdf#\1-blahblah.pdf#" \ |
| 54 | + | GIT_INDEX_FILE=$GIT_INDEX_FILE.new git update-index --index-info && \ |
| 55 | + mv "$GIT_INDEX_FILE.new" "$GIT_INDEX_FILE" |
| 56 | + |
| 57 | +然后执行下面命令对版本库整理: |
| 58 | + |
| 59 | + $ cd git/bar |
| 60 | + $ git filter-branch --index-filter 'sh /path/to/rename.sh' |
| 61 | + |
| 62 | +## 删除空白提交 ## |
| 63 | + |
| 64 | +从SVN转换的Git版本库可能存在空白提交,例如一些仅修改了SVN属性的提交不被 `git-svn` 支持,转换成了空提交。 |
| 65 | +这些空提交会对后续操作造成干扰,执行如下命令删除空白提交: |
| 66 | + |
| 67 | + $ cd git/bar |
| 68 | + $ git filter-branch -f --commit-filter ' |
| 69 | + if [ "$(git rev-parse $GIT_COMMIT^^{tree} 2>/dev/null)" = "$(git rev-parse $GIT_COMMIT^{tree})" ]; |
| 70 | + then |
| 71 | + skip_commit "$@"; |
| 72 | + else |
| 73 | + git commit-tree "$@"; |
| 74 | + fi' HEAD |
| 75 | + |
| 76 | +## 向Git日志中添加MetaData ## |
| 77 | + |
| 78 | +执行 `git log` 操作可以看到转换后的提交保持了原有SVN提交的用户名和提交时间,还记录了对应SVN的提交编号信息。 |
| 79 | +但是后续操作(`git svn dcommit`)会改变Git提交,破坏其中包含的原有SVN提交的提交者和提交时间, |
| 80 | +因此需要用其他方法将这些信息记录下来,以便补救。 |
| 81 | + |
| 82 | +使用 `git filter-branch` 的 `--msg-filter` 过滤器逐一向提交插入原有SVN的提交者和提交时间的元信息。 |
| 83 | + |
| 84 | + $ cd git/bar |
| 85 | + $ git filter-branch -f --msg-filter ' |
| 86 | + cat && |
| 87 | + echo "From: REPO-NAME, author: $GIT_AUTHOR_NAME, date: $GIT_AUTHOR_DATE"' HEAD |
| 88 | + |
| 89 | + |
| 90 | +## 根据需要对版本库目录重新组织 ## |
| 91 | + |
| 92 | +`git filter-branch` 至少有两个过滤器可以逐一对提交进行重新组织。一个是 `--tree-filter` , |
| 93 | +一个是 `--index-filter` 。前者的过滤器脚本写起来简单,但执行起来较后者慢至少一个数量级。 |
| 94 | + |
| 95 | +根据路径转换的需求,编写过滤器脚本,如脚本 `transform.sh` : |
| 96 | + |
| 97 | + #!/bin/sh |
| 98 | + |
| 99 | + if test -z "$GIT_INDEX_FILE"; then |
| 100 | + GIT_INDEX_FILE=.git/index |
| 101 | + fi |
| 102 | + |
| 103 | + git ls-files -s | \ |
| 104 | + sed \ |
| 105 | + -e "s#\(\t\)#\1new-root/#" \ |
| 106 | + -e "s#\(\tnew-root\)\(/old-path-1/\)#\1/new-path-1/#" \ |
| 107 | + -e "s#\(\tnew-root\)\(/old-path-2/\)#\1/new-path-2/#" \ |
| 108 | + -e "s#\(\tnew-root\)\(/old-path-3/\)#\1/new-path-3/#" \ |
| 109 | + | GIT_INDEX_FILE=$GIT_INDEX_FILE.new git update-index --index-info && \ |
| 110 | + mv "$GIT_INDEX_FILE.new" "$GIT_INDEX_FILE" |
| 111 | + |
| 112 | +然后执行如下命令对提交进行逐一过滤,将老的目录结构转换为新的目录结构: |
| 113 | + |
| 114 | + $ cd git/bar |
| 115 | + $ git filter-branch --index-filter 'sh /path/to/transform.sh' |
| 116 | + |
| 117 | +## 用git-svn克隆目标版本库(foo) ## |
| 118 | + |
| 119 | +执行如下命令将导入的目标版本库转换为本地的 Git 版本库,如下: |
| 120 | + |
| 121 | + $ git init git/foo |
| 122 | + $ cd git/foo |
| 123 | + $ git svn init --no-metadata file:///path/to/svn/foo |
| 124 | + $ git svn fetch |
| 125 | + |
| 126 | +然后将 bar 整理好的分支变基到当前的 master 分支上: |
| 127 | + |
| 128 | + $ cd git/foo |
| 129 | + $ git fetch ../../git/bar |
| 130 | + $ git branch bar/master FETCH_HEAD |
| 131 | + $ git co bar/master |
| 132 | + $ git rebase -k --onto master --root |
| 133 | + |
| 134 | +说明: |
| 135 | + |
| 136 | +* 使用 -k 参数,执行效率更高,因为会直接调用 cherry-pick 进行变基,而不需要执行 `git format-patch` 命令将提交预先转换为补丁文件。 |
| 137 | + |
| 138 | +在执行过程中遇到冲突中断的情况,这时需要解决冲突后执行: |
| 139 | + |
| 140 | + $ git cherry-pick --continue |
| 141 | + |
| 142 | +然后执行如下命令将不在SVN版本库中的Git提交提交到SVN版本库 foo 中。 |
| 143 | + |
| 144 | + $ git svn dcommit --rmdir |
| 145 | + |
| 146 | +说明: |
| 147 | + |
| 148 | +* 使用 `--rmdir` 命令是为了避免在 SVN 版本库中残留由于目录移动产生的空目录。 |
| 149 | +* 使用 `git svn dcommit` 在SVN版本库中创建的新提交,其提交者是当前登录用户,提交时间是当前时间。 |
| 150 | + 即新的SVN提交丢失了原有SVN提交的用户名和时间信息。马上利用之前在提交说明中添加的元信息进行补救。 |
| 151 | + |
| 152 | + |
| 153 | +## 修正提交时间和提交者 ## |
| 154 | + |
| 155 | +编写如下脚本 `parse-git-log.rb`,读取Git日志对元信息进行处理。 |
| 156 | + |
| 157 | + #!/usr/bin/ruby |
| 158 | + |
| 159 | + require 'date' |
| 160 | + |
| 161 | + def to_iso8601(date) |
| 162 | + if date =~ /^[0-9]{10}/ |
| 163 | + DateTime.strptime(date, '%s').iso8601.gsub(/\+[0-9]*:[0-9]*$/, '.000000Z') |
| 164 | + else |
| 165 | + raise "Error: wrong date format: #{date}" |
| 166 | + end |
| 167 | + end |
| 168 | + |
| 169 | + def parse_git_log(io) |
| 170 | + svndict={} |
| 171 | + commit, author, date, log, rev = [] |
| 172 | + io.each_line do |line| |
| 173 | + line.strip! |
| 174 | + if line =~ /^commit ([0-9a-f]{40})/ |
| 175 | + commit = $1 |
| 176 | + author, date, log, rev = [] |
| 177 | + elsif line =~ /^From: .*, author: (.*), date: @([0-9]+)/ |
| 178 | + author = $1 |
| 179 | + date = $2 |
| 180 | + elsif line =~ /git-svn-id: .+@([0-9]+) .*/ |
| 181 | + rev = $1 |
| 182 | + if author.nil? or author.empty? |
| 183 | + STDERR.puts "Warning: no author for commit: #{commit}" |
| 184 | + next |
| 185 | + elsif date.nil? or date.empty? |
| 186 | + STDERR.puts "Warning: no author for commit: #{commit}" |
| 187 | + next |
| 188 | + end |
| 189 | + svndict[rev] = {} |
| 190 | + svndict[rev][:author] = author |
| 191 | + svndict[rev][:date] = to_iso8601 date |
| 192 | + end |
| 193 | + end |
| 194 | + svndict |
| 195 | + end |
| 196 | + |
| 197 | + url = 'file:///path/to/svn/foo' |
| 198 | + svndict = {} |
| 199 | + |
| 200 | + if ARGV.size == 1 |
| 201 | + if File.exist? ARGV[0] |
| 202 | + File.open(ARGV[0]) do |io| |
| 203 | + svndict = parse_git_log io |
| 204 | + end |
| 205 | + else |
| 206 | + STDERR.puts "Read git log from STDIN" |
| 207 | + url = ARGV[0] |
| 208 | + svndict = parse_git_log STDIN |
| 209 | + end |
| 210 | + else |
| 211 | + puts <<-EOF |
| 212 | + Usage: |
| 213 | + #{File.basename $0} git-log.txt |
| 214 | + #{File.basename $0} url-of-svn < git-log.txt |
| 215 | + EOF |
| 216 | + exit 0 |
| 217 | + end |
| 218 | + |
| 219 | + svndict.keys.map{|x| x.to_i}.sort.reverse.each do |rev| |
| 220 | + author = svndict[rev.to_s][:author] |
| 221 | + date = svndict[rev.to_s][:date] |
| 222 | + puts "svn ps --revprop -r #{rev} svn:date \"#{date}\" #{url}" |
| 223 | + puts "svn ps --revprop -r #{rev} svn:author \"#{author}\" #{url}" |
| 224 | + end |
| 225 | + |
| 226 | +然后执行如下命令,读取Git日志,将Git提交中的元信息转换为修正 SVN 提交历史的命令脚本 `fix-svn-log.sh`。 |
| 227 | + |
| 228 | + $ cd git/foo |
| 229 | + $ git log | ruby parse-git-log.rb file:///path/to/svn/foo > fix-svn-log.sh |
| 230 | + |
| 231 | +然后执行如下命令修改 SVN 的属性,还原原有SVN的提交用户和提交实现信息: |
| 232 | + |
| 233 | + $ sh fix-svn-log.sh |
| 234 | + |
| 235 | +因为此操作实际上执行 `svn ps --revprop` 命令,需要SVN版本库 foo 中创建一个可执行的 `pre-revprop-change` 钩子脚本。 |
| 236 | + |
| 237 | +至此版本库转换完毕。怎么样 `git filter-branch` 命令够强大吧。 |
0 commit comments