Skip to content

Commit 6e12747

Browse files
committed
New post: svn migrate with git svn and git filter-branch
Signed-off-by: Jiang Xin <[email protected]>
1 parent 3becd7e commit 6e12747

File tree

1 file changed

+237
-0
lines changed

1 file changed

+237
-0
lines changed
Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
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

Comments
 (0)