##// END OF EJS Templates
convert: fix svn_source.latest()
Patrick Mezard -
r5955:c4496b7c default
parent child Browse files
Show More
@@ -0,0 +1,60 b''
1 #!/bin/sh
2
3 "$TESTDIR/hghave" svn svn-bindings || exit 80
4
5 fix_path()
6 {
7 tr '\\' /
8 }
9
10 echo "[extensions]" >> $HGRCPATH
11 echo "convert = " >> $HGRCPATH
12 echo "hgext.graphlog =" >> $HGRCPATH
13
14 svnadmin create svn-repo
15
16 svnpath=`pwd | fix_path`
17 # SVN wants all paths to start with a slash. Unfortunately,
18 # Windows ones don't. Handle that.
19 expr $svnpath : "\/" > /dev/null
20 if [ $? -ne 0 ]; then
21 svnpath='/'$svnpath
22 fi
23
24 echo % initial svn import
25 mkdir projA
26 cd projA
27 mkdir trunk
28 mkdir trunk/d1
29 echo b > trunk/d1/b
30 cd ..
31
32 svnurl=file://$svnpath/svn-repo/projA
33 svn import -m "init projA" projA $svnurl | fix_path
34
35 # Build a module renaming chain which used to confuse the converter.
36 echo % update svn repository
37 svn co $svnurl A | fix_path
38 cd A
39 svn mv $svnurl/trunk $svnurl/subproject -m movedtrunk
40 svn up
41 mkdir subproject/trunk
42 svn add subproject/trunk
43 svn ci -m createtrunk
44 mkdir subproject/branches
45 svn add subproject/branches
46 svn ci -m createbranches
47 svn mv $svnurl/subproject/d1 $svnurl/subproject/trunk/d1 -m moved1
48 svn up
49 echo b >> subproject/trunk/d1/b
50 svn ci -m changeb
51 svn mv $svnurl/subproject/trunk/d1 $svnurl/subproject/branches/d1 -m moved1again
52 cd ..
53
54 echo % convert trunk and branches
55 hg convert --datesort $svnurl/subproject A-hg
56
57 cd A-hg
58 hg glog --template '#rev# #desc|firstline# files: #files#\n'
59 hg branches | sed 's/:.*/:/'
60 cd ..
@@ -0,0 +1,68 b''
1 % initial svn import
2 Adding projA/trunk
3 Adding projA/trunk/d1
4 Adding projA/trunk/d1/b
5
6 Committed revision 1.
7 % update svn repository
8 A A/trunk
9 A A/trunk/d1
10 A A/trunk/d1/b
11 Checked out revision 1.
12
13 Committed revision 2.
14 D trunk
15 A subproject
16 A subproject/d1
17 A subproject/d1/b
18 Updated to revision 2.
19 A subproject/trunk
20 Adding subproject/trunk
21
22 Committed revision 3.
23 A subproject/branches
24 Adding subproject/branches
25
26 Committed revision 4.
27
28 Committed revision 5.
29 A subproject/trunk/d1
30 A subproject/trunk/d1/b
31 D subproject/d1
32 Updated to revision 5.
33 Sending subproject/trunk/d1/b
34 Transmitting file data .
35 Committed revision 6.
36
37 Committed revision 7.
38 % convert trunk and branches
39 initializing destination A-hg repository
40 scanning source...
41 sorting...
42 converting...
43 7 init projA
44 6 createtrunk
45 5 moved1
46 4 moved1
47 3 changeb
48 2 changeb
49 1 moved1again
50 0 moved1again
51 o 7 moved1again files: d1/b
52 |
53 | o 6 moved1again files:
54 | |
55 o | 5 changeb files: d1/b
56 | |
57 | o 4 changeb files: b
58 | |
59 o | 3 moved1 files: d1/b
60 | |
61 | o 2 moved1 files:
62 | |
63 o | 1 createtrunk files:
64 /
65 o 0 init projA files: b
66
67 default 7:
68 d1 6:
@@ -1,984 +1,1008 b''
1 # Subversion 1.4/1.5 Python API backend
1 # Subversion 1.4/1.5 Python API backend
2 #
2 #
3 # Copyright(C) 2007 Daniel Holth et al
3 # Copyright(C) 2007 Daniel Holth et al
4 #
4 #
5 # Configuration options:
5 # Configuration options:
6 #
6 #
7 # convert.svn.trunk
7 # convert.svn.trunk
8 # Relative path to the trunk (default: "trunk")
8 # Relative path to the trunk (default: "trunk")
9 # convert.svn.branches
9 # convert.svn.branches
10 # Relative path to tree of branches (default: "branches")
10 # Relative path to tree of branches (default: "branches")
11 # convert.svn.tags
11 # convert.svn.tags
12 # Relative path to tree of tags (default: "tags")
12 # Relative path to tree of tags (default: "tags")
13 #
13 #
14 # Set these in a hgrc, or on the command line as follows:
14 # Set these in a hgrc, or on the command line as follows:
15 #
15 #
16 # hg convert --config convert.svn.trunk=wackoname [...]
16 # hg convert --config convert.svn.trunk=wackoname [...]
17
17
18 import locale
18 import locale
19 import os
19 import os
20 import re
20 import re
21 import sys
21 import sys
22 import cPickle as pickle
22 import cPickle as pickle
23 import tempfile
23 import tempfile
24
24
25 from mercurial import strutil, util
25 from mercurial import strutil, util
26 from mercurial.i18n import _
26 from mercurial.i18n import _
27
27
28 # Subversion stuff. Works best with very recent Python SVN bindings
28 # Subversion stuff. Works best with very recent Python SVN bindings
29 # e.g. SVN 1.5 or backports. Thanks to the bzr folks for enhancing
29 # e.g. SVN 1.5 or backports. Thanks to the bzr folks for enhancing
30 # these bindings.
30 # these bindings.
31
31
32 from cStringIO import StringIO
32 from cStringIO import StringIO
33
33
34 from common import NoRepo, commit, converter_source, encodeargs, decodeargs
34 from common import NoRepo, commit, converter_source, encodeargs, decodeargs
35 from common import commandline, converter_sink, mapfile
35 from common import commandline, converter_sink, mapfile
36
36
37 try:
37 try:
38 from svn.core import SubversionException, Pool
38 from svn.core import SubversionException, Pool
39 import svn
39 import svn
40 import svn.client
40 import svn.client
41 import svn.core
41 import svn.core
42 import svn.ra
42 import svn.ra
43 import svn.delta
43 import svn.delta
44 import transport
44 import transport
45 except ImportError:
45 except ImportError:
46 pass
46 pass
47
47
48 def geturl(path):
48 def geturl(path):
49 try:
49 try:
50 return svn.client.url_from_path(svn.core.svn_path_canonicalize(path))
50 return svn.client.url_from_path(svn.core.svn_path_canonicalize(path))
51 except SubversionException:
51 except SubversionException:
52 pass
52 pass
53 if os.path.isdir(path):
53 if os.path.isdir(path):
54 path = os.path.normpath(os.path.abspath(path))
54 path = os.path.normpath(os.path.abspath(path))
55 if os.name == 'nt':
55 if os.name == 'nt':
56 path = '/' + util.normpath(path)
56 path = '/' + util.normpath(path)
57 return 'file://%s' % path
57 return 'file://%s' % path
58 return path
58 return path
59
59
60 def optrev(number):
60 def optrev(number):
61 optrev = svn.core.svn_opt_revision_t()
61 optrev = svn.core.svn_opt_revision_t()
62 optrev.kind = svn.core.svn_opt_revision_number
62 optrev.kind = svn.core.svn_opt_revision_number
63 optrev.value.number = number
63 optrev.value.number = number
64 return optrev
64 return optrev
65
65
66 class changedpath(object):
66 class changedpath(object):
67 def __init__(self, p):
67 def __init__(self, p):
68 self.copyfrom_path = p.copyfrom_path
68 self.copyfrom_path = p.copyfrom_path
69 self.copyfrom_rev = p.copyfrom_rev
69 self.copyfrom_rev = p.copyfrom_rev
70 self.action = p.action
70 self.action = p.action
71
71
72 def get_log_child(fp, url, paths, start, end, limit=0, discover_changed_paths=True,
72 def get_log_child(fp, url, paths, start, end, limit=0, discover_changed_paths=True,
73 strict_node_history=False):
73 strict_node_history=False):
74 protocol = -1
74 protocol = -1
75 def receiver(orig_paths, revnum, author, date, message, pool):
75 def receiver(orig_paths, revnum, author, date, message, pool):
76 if orig_paths is not None:
76 if orig_paths is not None:
77 for k, v in orig_paths.iteritems():
77 for k, v in orig_paths.iteritems():
78 orig_paths[k] = changedpath(v)
78 orig_paths[k] = changedpath(v)
79 pickle.dump((orig_paths, revnum, author, date, message),
79 pickle.dump((orig_paths, revnum, author, date, message),
80 fp, protocol)
80 fp, protocol)
81
81
82 try:
82 try:
83 # Use an ra of our own so that our parent can consume
83 # Use an ra of our own so that our parent can consume
84 # our results without confusing the server.
84 # our results without confusing the server.
85 t = transport.SvnRaTransport(url=url)
85 t = transport.SvnRaTransport(url=url)
86 svn.ra.get_log(t.ra, paths, start, end, limit,
86 svn.ra.get_log(t.ra, paths, start, end, limit,
87 discover_changed_paths,
87 discover_changed_paths,
88 strict_node_history,
88 strict_node_history,
89 receiver)
89 receiver)
90 except SubversionException, (inst, num):
90 except SubversionException, (inst, num):
91 pickle.dump(num, fp, protocol)
91 pickle.dump(num, fp, protocol)
92 except IOError:
92 except IOError:
93 # Caller may interrupt the iteration
93 # Caller may interrupt the iteration
94 pickle.dump(None, fp, protocol)
94 pickle.dump(None, fp, protocol)
95 else:
95 else:
96 pickle.dump(None, fp, protocol)
96 pickle.dump(None, fp, protocol)
97 fp.close()
97 fp.close()
98
98
99 def debugsvnlog(ui, **opts):
99 def debugsvnlog(ui, **opts):
100 """Fetch SVN log in a subprocess and channel them back to parent to
100 """Fetch SVN log in a subprocess and channel them back to parent to
101 avoid memory collection issues.
101 avoid memory collection issues.
102 """
102 """
103 util.set_binary(sys.stdin)
103 util.set_binary(sys.stdin)
104 util.set_binary(sys.stdout)
104 util.set_binary(sys.stdout)
105 args = decodeargs(sys.stdin.read())
105 args = decodeargs(sys.stdin.read())
106 get_log_child(sys.stdout, *args)
106 get_log_child(sys.stdout, *args)
107
107
108 class logstream:
108 class logstream:
109 """Interruptible revision log iterator."""
109 """Interruptible revision log iterator."""
110 def __init__(self, stdout):
110 def __init__(self, stdout):
111 self._stdout = stdout
111 self._stdout = stdout
112
112
113 def __iter__(self):
113 def __iter__(self):
114 while True:
114 while True:
115 entry = pickle.load(self._stdout)
115 entry = pickle.load(self._stdout)
116 try:
116 try:
117 orig_paths, revnum, author, date, message = entry
117 orig_paths, revnum, author, date, message = entry
118 except:
118 except:
119 if entry is None:
119 if entry is None:
120 break
120 break
121 raise SubversionException("child raised exception", entry)
121 raise SubversionException("child raised exception", entry)
122 yield entry
122 yield entry
123
123
124 def close(self):
124 def close(self):
125 if self._stdout:
125 if self._stdout:
126 self._stdout.close()
126 self._stdout.close()
127 self._stdout = None
127 self._stdout = None
128
128
129 def get_log(url, paths, start, end, limit=0, discover_changed_paths=True,
129 def get_log(url, paths, start, end, limit=0, discover_changed_paths=True,
130 strict_node_history=False):
130 strict_node_history=False):
131 args = [url, paths, start, end, limit, discover_changed_paths,
131 args = [url, paths, start, end, limit, discover_changed_paths,
132 strict_node_history]
132 strict_node_history]
133 arg = encodeargs(args)
133 arg = encodeargs(args)
134 hgexe = util.hgexecutable()
134 hgexe = util.hgexecutable()
135 cmd = '%s debugsvnlog' % util.shellquote(hgexe)
135 cmd = '%s debugsvnlog' % util.shellquote(hgexe)
136 stdin, stdout = os.popen2(cmd, 'b')
136 stdin, stdout = os.popen2(cmd, 'b')
137 stdin.write(arg)
137 stdin.write(arg)
138 stdin.close()
138 stdin.close()
139 return logstream(stdout)
139 return logstream(stdout)
140
140
141 # SVN conversion code stolen from bzr-svn and tailor
141 # SVN conversion code stolen from bzr-svn and tailor
142 #
142 #
143 # Subversion looks like a versioned filesystem, branches structures
143 # Subversion looks like a versioned filesystem, branches structures
144 # are defined by conventions and not enforced by the tool. First,
144 # are defined by conventions and not enforced by the tool. First,
145 # we define the potential branches (modules) as "trunk" and "branches"
145 # we define the potential branches (modules) as "trunk" and "branches"
146 # children directories. Revisions are then identified by their
146 # children directories. Revisions are then identified by their
147 # module and revision number (and a repository identifier).
147 # module and revision number (and a repository identifier).
148 #
148 #
149 # The revision graph is really a tree (or a forest). By default, a
149 # The revision graph is really a tree (or a forest). By default, a
150 # revision parent is the previous revision in the same module. If the
150 # revision parent is the previous revision in the same module. If the
151 # module directory is copied/moved from another module then the
151 # module directory is copied/moved from another module then the
152 # revision is the module root and its parent the source revision in
152 # revision is the module root and its parent the source revision in
153 # the parent module. A revision has at most one parent.
153 # the parent module. A revision has at most one parent.
154 #
154 #
155 class svn_source(converter_source):
155 class svn_source(converter_source):
156 def __init__(self, ui, url, rev=None):
156 def __init__(self, ui, url, rev=None):
157 super(svn_source, self).__init__(ui, url, rev=rev)
157 super(svn_source, self).__init__(ui, url, rev=rev)
158
158
159 try:
159 try:
160 SubversionException
160 SubversionException
161 except NameError:
161 except NameError:
162 raise NoRepo('Subversion python bindings could not be loaded')
162 raise NoRepo('Subversion python bindings could not be loaded')
163
163
164 self.encoding = locale.getpreferredencoding()
164 self.encoding = locale.getpreferredencoding()
165 self.lastrevs = {}
165 self.lastrevs = {}
166
166
167 latest = None
167 latest = None
168 try:
168 try:
169 # Support file://path@rev syntax. Useful e.g. to convert
169 # Support file://path@rev syntax. Useful e.g. to convert
170 # deleted branches.
170 # deleted branches.
171 at = url.rfind('@')
171 at = url.rfind('@')
172 if at >= 0:
172 if at >= 0:
173 latest = int(url[at+1:])
173 latest = int(url[at+1:])
174 url = url[:at]
174 url = url[:at]
175 except ValueError, e:
175 except ValueError, e:
176 pass
176 pass
177 self.url = geturl(url)
177 self.url = geturl(url)
178 self.encoding = 'UTF-8' # Subversion is always nominal UTF-8
178 self.encoding = 'UTF-8' # Subversion is always nominal UTF-8
179 try:
179 try:
180 self.transport = transport.SvnRaTransport(url=self.url)
180 self.transport = transport.SvnRaTransport(url=self.url)
181 self.ra = self.transport.ra
181 self.ra = self.transport.ra
182 self.ctx = self.transport.client
182 self.ctx = self.transport.client
183 self.base = svn.ra.get_repos_root(self.ra)
183 self.base = svn.ra.get_repos_root(self.ra)
184 self.module = self.url[len(self.base):]
184 self.module = self.url[len(self.base):]
185 self.commits = {}
185 self.commits = {}
186 self.paths = {}
186 self.paths = {}
187 self.uuid = svn.ra.get_uuid(self.ra).decode(self.encoding)
187 self.uuid = svn.ra.get_uuid(self.ra).decode(self.encoding)
188 except SubversionException, e:
188 except SubversionException, e:
189 ui.print_exc()
189 ui.print_exc()
190 raise NoRepo("%s does not look like a Subversion repo" % self.url)
190 raise NoRepo("%s does not look like a Subversion repo" % self.url)
191
191
192 if rev:
192 if rev:
193 try:
193 try:
194 latest = int(rev)
194 latest = int(rev)
195 except ValueError:
195 except ValueError:
196 raise util.Abort('svn: revision %s is not an integer' % rev)
196 raise util.Abort('svn: revision %s is not an integer' % rev)
197
197
198 try:
198 try:
199 self.get_blacklist()
199 self.get_blacklist()
200 except IOError, e:
200 except IOError, e:
201 pass
201 pass
202
202
203 self.last_changed = self.latest(self.module, latest)
203 self.head = self.latest(self.module, latest)
204
204 self.last_changed = self.revnum(self.head)
205 self.head = self.revid(self.last_changed)
205
206 self._changescache = None
206 self._changescache = None
207
207
208 if os.path.exists(os.path.join(url, '.svn/entries')):
208 if os.path.exists(os.path.join(url, '.svn/entries')):
209 self.wc = url
209 self.wc = url
210 else:
210 else:
211 self.wc = None
211 self.wc = None
212 self.convertfp = None
212 self.convertfp = None
213
213
214 def setrevmap(self, revmap):
214 def setrevmap(self, revmap):
215 lastrevs = {}
215 lastrevs = {}
216 for revid in revmap.iterkeys():
216 for revid in revmap.iterkeys():
217 uuid, module, revnum = self.revsplit(revid)
217 uuid, module, revnum = self.revsplit(revid)
218 lastrevnum = lastrevs.setdefault(module, revnum)
218 lastrevnum = lastrevs.setdefault(module, revnum)
219 if revnum > lastrevnum:
219 if revnum > lastrevnum:
220 lastrevs[module] = revnum
220 lastrevs[module] = revnum
221 self.lastrevs = lastrevs
221 self.lastrevs = lastrevs
222
222
223 def exists(self, path, optrev):
223 def exists(self, path, optrev):
224 try:
224 try:
225 svn.client.ls(self.url.rstrip('/') + '/' + path,
225 svn.client.ls(self.url.rstrip('/') + '/' + path,
226 optrev, False, self.ctx)
226 optrev, False, self.ctx)
227 return True
227 return True
228 except SubversionException, err:
228 except SubversionException, err:
229 return False
229 return False
230
230
231 def getheads(self):
231 def getheads(self):
232
232
233 def getcfgpath(name, rev):
233 def getcfgpath(name, rev):
234 cfgpath = self.ui.config('convert', 'svn.' + name)
234 cfgpath = self.ui.config('convert', 'svn.' + name)
235 path = (cfgpath or name).strip('/')
235 path = (cfgpath or name).strip('/')
236 if not self.exists(path, rev):
236 if not self.exists(path, rev):
237 if cfgpath:
237 if cfgpath:
238 raise util.Abort(_('expected %s to be at %r, but not found')
238 raise util.Abort(_('expected %s to be at %r, but not found')
239 % (name, path))
239 % (name, path))
240 return None
240 return None
241 self.ui.note(_('found %s at %r\n') % (name, path))
241 self.ui.note(_('found %s at %r\n') % (name, path))
242 return path
242 return path
243
243
244 rev = optrev(self.last_changed)
244 rev = optrev(self.last_changed)
245 oldmodule = ''
245 oldmodule = ''
246 trunk = getcfgpath('trunk', rev)
246 trunk = getcfgpath('trunk', rev)
247 tags = getcfgpath('tags', rev)
247 tags = getcfgpath('tags', rev)
248 branches = getcfgpath('branches', rev)
248 branches = getcfgpath('branches', rev)
249
249
250 # If the project has a trunk or branches, we will extract heads
250 # If the project has a trunk or branches, we will extract heads
251 # from them. We keep the project root otherwise.
251 # from them. We keep the project root otherwise.
252 if trunk:
252 if trunk:
253 oldmodule = self.module or ''
253 oldmodule = self.module or ''
254 self.module += '/' + trunk
254 self.module += '/' + trunk
255 lt = self.latest(self.module, self.last_changed)
255 self.head = self.latest(self.module, self.last_changed)
256 self.head = self.revid(lt)
257
256
258 # First head in the list is the module's head
257 # First head in the list is the module's head
259 self.heads = [self.head]
258 self.heads = [self.head]
260 self.tags = '%s/%s' % (oldmodule , (tags or 'tags'))
259 self.tags = '%s/%s' % (oldmodule , (tags or 'tags'))
261
260
262 # Check if branches bring a few more heads to the list
261 # Check if branches bring a few more heads to the list
263 if branches:
262 if branches:
264 rpath = self.url.strip('/')
263 rpath = self.url.strip('/')
265 branchnames = svn.client.ls(rpath + '/' + branches, rev, False,
264 branchnames = svn.client.ls(rpath + '/' + branches, rev, False,
266 self.ctx)
265 self.ctx)
267 for branch in branchnames.keys():
266 for branch in branchnames.keys():
268 module = '%s/%s/%s' % (oldmodule, branches, branch)
267 module = '%s/%s/%s' % (oldmodule, branches, branch)
269 brevnum = self.latest(module, self.last_changed)
268 brevid = self.latest(module, self.last_changed)
270 brev = self.revid(brevnum, module)
269 self.ui.note('found branch %s at %d\n' %
271 self.ui.note('found branch %s at %d\n' % (branch, brevnum))
270 (branch, self.revnum(brevid)))
272 self.heads.append(brev)
271 self.heads.append(brevid)
273
272
274 return self.heads
273 return self.heads
275
274
276 def getfile(self, file, rev):
275 def getfile(self, file, rev):
277 data, mode = self._getfile(file, rev)
276 data, mode = self._getfile(file, rev)
278 self.modecache[(file, rev)] = mode
277 self.modecache[(file, rev)] = mode
279 return data
278 return data
280
279
281 def getmode(self, file, rev):
280 def getmode(self, file, rev):
282 return self.modecache[(file, rev)]
281 return self.modecache[(file, rev)]
283
282
284 def getchanges(self, rev):
283 def getchanges(self, rev):
285 if self._changescache and self._changescache[0] == rev:
284 if self._changescache and self._changescache[0] == rev:
286 return self._changescache[1]
285 return self._changescache[1]
287 self._changescache = None
286 self._changescache = None
288 self.modecache = {}
287 self.modecache = {}
289 (paths, parents) = self.paths[rev]
288 (paths, parents) = self.paths[rev]
290 files, copies = self.expandpaths(rev, paths, parents)
289 files, copies = self.expandpaths(rev, paths, parents)
291 files.sort()
290 files.sort()
292 files = zip(files, [rev] * len(files))
291 files = zip(files, [rev] * len(files))
293
292
294 # caller caches the result, so free it here to release memory
293 # caller caches the result, so free it here to release memory
295 del self.paths[rev]
294 del self.paths[rev]
296 return (files, copies)
295 return (files, copies)
297
296
298 def getchangedfiles(self, rev, i):
297 def getchangedfiles(self, rev, i):
299 changes = self.getchanges(rev)
298 changes = self.getchanges(rev)
300 self._changescache = (rev, changes)
299 self._changescache = (rev, changes)
301 return [f[0] for f in changes[0]]
300 return [f[0] for f in changes[0]]
302
301
303 def getcommit(self, rev):
302 def getcommit(self, rev):
304 if rev not in self.commits:
303 if rev not in self.commits:
305 uuid, module, revnum = self.revsplit(rev)
304 uuid, module, revnum = self.revsplit(rev)
306 self.module = module
305 self.module = module
307 self.reparent(module)
306 self.reparent(module)
308 # We assume that:
307 # We assume that:
309 # - requests for revisions after "stop" come from the
308 # - requests for revisions after "stop" come from the
310 # revision graph backward traversal. Cache all of them
309 # revision graph backward traversal. Cache all of them
311 # down to stop, they will be used eventually.
310 # down to stop, they will be used eventually.
312 # - requests for revisions before "stop" come to get
311 # - requests for revisions before "stop" come to get
313 # isolated branches parents. Just fetch what is needed.
312 # isolated branches parents. Just fetch what is needed.
314 stop = self.lastrevs.get(module, 0)
313 stop = self.lastrevs.get(module, 0)
315 if revnum < stop:
314 if revnum < stop:
316 stop = revnum + 1
315 stop = revnum + 1
317 self._fetch_revisions(revnum, stop)
316 self._fetch_revisions(revnum, stop)
318 commit = self.commits[rev]
317 commit = self.commits[rev]
319 # caller caches the result, so free it here to release memory
318 # caller caches the result, so free it here to release memory
320 del self.commits[rev]
319 del self.commits[rev]
321 return commit
320 return commit
322
321
323 def gettags(self):
322 def gettags(self):
324 tags = {}
323 tags = {}
325 start = self.revnum(self.head)
324 start = self.revnum(self.head)
326 try:
325 try:
327 for entry in get_log(self.url, [self.tags], 0, start):
326 for entry in get_log(self.url, [self.tags], 0, start):
328 orig_paths, revnum, author, date, message = entry
327 orig_paths, revnum, author, date, message = entry
329 for path in orig_paths:
328 for path in orig_paths:
330 if not path.startswith(self.tags+'/'):
329 if not path.startswith(self.tags+'/'):
331 continue
330 continue
332 ent = orig_paths[path]
331 ent = orig_paths[path]
333 source = ent.copyfrom_path
332 source = ent.copyfrom_path
334 rev = ent.copyfrom_rev
333 rev = ent.copyfrom_rev
335 tag = path.split('/')[-1]
334 tag = path.split('/')[-1]
336 tags[tag] = self.revid(rev, module=source)
335 tags[tag] = self.revid(rev, module=source)
337 except SubversionException, (inst, num):
336 except SubversionException, (inst, num):
338 self.ui.note('no tags found at revision %d\n' % start)
337 self.ui.note('no tags found at revision %d\n' % start)
339 return tags
338 return tags
340
339
341 def converted(self, rev, destrev):
340 def converted(self, rev, destrev):
342 if not self.wc:
341 if not self.wc:
343 return
342 return
344 if self.convertfp is None:
343 if self.convertfp is None:
345 self.convertfp = open(os.path.join(self.wc, '.svn', 'hg-shamap'),
344 self.convertfp = open(os.path.join(self.wc, '.svn', 'hg-shamap'),
346 'a')
345 'a')
347 self.convertfp.write('%s %d\n' % (destrev, self.revnum(rev)))
346 self.convertfp.write('%s %d\n' % (destrev, self.revnum(rev)))
348 self.convertfp.flush()
347 self.convertfp.flush()
349
348
350 # -- helper functions --
349 # -- helper functions --
351
350
352 def revid(self, revnum, module=None):
351 def revid(self, revnum, module=None):
353 if not module:
352 if not module:
354 module = self.module
353 module = self.module
355 return u"svn:%s%s@%s" % (self.uuid, module.decode(self.encoding),
354 return u"svn:%s%s@%s" % (self.uuid, module.decode(self.encoding),
356 revnum)
355 revnum)
357
356
358 def revnum(self, rev):
357 def revnum(self, rev):
359 return int(rev.split('@')[-1])
358 return int(rev.split('@')[-1])
360
359
361 def revsplit(self, rev):
360 def revsplit(self, rev):
362 url, revnum = rev.encode(self.encoding).split('@', 1)
361 url, revnum = rev.encode(self.encoding).split('@', 1)
363 revnum = int(revnum)
362 revnum = int(revnum)
364 parts = url.split('/', 1)
363 parts = url.split('/', 1)
365 uuid = parts.pop(0)[4:]
364 uuid = parts.pop(0)[4:]
366 mod = ''
365 mod = ''
367 if parts:
366 if parts:
368 mod = '/' + parts[0]
367 mod = '/' + parts[0]
369 return uuid, mod, revnum
368 return uuid, mod, revnum
370
369
371 def latest(self, path, stop=0):
370 def latest(self, path, stop=0):
372 'find the latest revision affecting path, up to stop'
371 """Find the latest revid affecting path, up to stop. It may return
372 a revision in a different module, since a branch may be moved without
373 a change being reported.
374 """
373 if not stop:
375 if not stop:
374 stop = svn.ra.get_latest_revnum(self.ra)
376 stop = svn.ra.get_latest_revnum(self.ra)
375 try:
377 try:
376 self.reparent('')
378 self.reparent('')
377 dirent = svn.ra.stat(self.ra, path.strip('/'), stop)
379 dirent = svn.ra.stat(self.ra, path.strip('/'), stop)
378 self.reparent(self.module)
380 self.reparent(self.module)
379 except SubversionException:
381 except SubversionException:
380 dirent = None
382 dirent = None
381 if not dirent:
383 if not dirent:
382 raise util.Abort('%s not found up to revision %d' % (path, stop))
384 raise util.Abort('%s not found up to revision %d' % (path, stop))
383
385
384 return dirent.created_rev
386 # stat() gives us the previous revision on this line of development, but
387 # it might be in *another module*. Fetch the log and detect renames down
388 # to the latest revision.
389 stream = get_log(self.url, [path], stop, dirent.created_rev)
390 try:
391 for entry in stream:
392 paths, revnum, author, date, message = entry
393 if revnum <= dirent.created_rev:
394 break
395
396 for p in paths:
397 if not path.startswith(p) or not paths[p].copyfrom_path:
398 continue
399 newpath = paths[p].copyfrom_path + path[len(p):]
400 self.ui.debug("branch renamed from %s to %s at %d\n" %
401 (path, newpath, revnum))
402 path = newpath
403 break
404 finally:
405 stream.close()
406
407 return self.revid(dirent.created_rev, path)
385
408
386 def get_blacklist(self):
409 def get_blacklist(self):
387 """Avoid certain revision numbers.
410 """Avoid certain revision numbers.
388 It is not uncommon for two nearby revisions to cancel each other
411 It is not uncommon for two nearby revisions to cancel each other
389 out, e.g. 'I copied trunk into a subdirectory of itself instead
412 out, e.g. 'I copied trunk into a subdirectory of itself instead
390 of making a branch'. The converted repository is significantly
413 of making a branch'. The converted repository is significantly
391 smaller if we ignore such revisions."""
414 smaller if we ignore such revisions."""
392 self.blacklist = util.set()
415 self.blacklist = util.set()
393 blacklist = self.blacklist
416 blacklist = self.blacklist
394 for line in file("blacklist.txt", "r"):
417 for line in file("blacklist.txt", "r"):
395 if not line.startswith("#"):
418 if not line.startswith("#"):
396 try:
419 try:
397 svn_rev = int(line.strip())
420 svn_rev = int(line.strip())
398 blacklist.add(svn_rev)
421 blacklist.add(svn_rev)
399 except ValueError, e:
422 except ValueError, e:
400 pass # not an integer or a comment
423 pass # not an integer or a comment
401
424
402 def is_blacklisted(self, svn_rev):
425 def is_blacklisted(self, svn_rev):
403 return svn_rev in self.blacklist
426 return svn_rev in self.blacklist
404
427
405 def reparent(self, module):
428 def reparent(self, module):
406 svn_url = self.base + module
429 svn_url = self.base + module
407 self.ui.debug("reparent to %s\n" % svn_url.encode(self.encoding))
430 self.ui.debug("reparent to %s\n" % svn_url.encode(self.encoding))
408 svn.ra.reparent(self.ra, svn_url.encode(self.encoding))
431 svn.ra.reparent(self.ra, svn_url.encode(self.encoding))
409
432
410 def expandpaths(self, rev, paths, parents):
433 def expandpaths(self, rev, paths, parents):
411 def get_entry_from_path(path, module=self.module):
434 def get_entry_from_path(path, module=self.module):
412 # Given the repository url of this wc, say
435 # Given the repository url of this wc, say
413 # "http://server/plone/CMFPlone/branches/Plone-2_0-branch"
436 # "http://server/plone/CMFPlone/branches/Plone-2_0-branch"
414 # extract the "entry" portion (a relative path) from what
437 # extract the "entry" portion (a relative path) from what
415 # svn log --xml says, ie
438 # svn log --xml says, ie
416 # "/CMFPlone/branches/Plone-2_0-branch/tests/PloneTestCase.py"
439 # "/CMFPlone/branches/Plone-2_0-branch/tests/PloneTestCase.py"
417 # that is to say "tests/PloneTestCase.py"
440 # that is to say "tests/PloneTestCase.py"
418 if path.startswith(module):
441 if path.startswith(module):
419 relative = path[len(module):]
442 relative = path[len(module):]
420 if relative.startswith('/'):
443 if relative.startswith('/'):
421 return relative[1:]
444 return relative[1:]
422 else:
445 else:
423 return relative
446 return relative
424
447
425 # The path is outside our tracked tree...
448 # The path is outside our tracked tree...
426 self.ui.debug('%r is not under %r, ignoring\n' % (path, module))
449 self.ui.debug('%r is not under %r, ignoring\n' % (path, module))
427 return None
450 return None
428
451
429 entries = []
452 entries = []
430 copyfrom = {} # Map of entrypath, revision for finding source of deleted revisions.
453 copyfrom = {} # Map of entrypath, revision for finding source of deleted revisions.
431 copies = {}
454 copies = {}
432
455
433 new_module, revnum = self.revsplit(rev)[1:]
456 new_module, revnum = self.revsplit(rev)[1:]
434 if new_module != self.module:
457 if new_module != self.module:
435 self.module = new_module
458 self.module = new_module
436 self.reparent(self.module)
459 self.reparent(self.module)
437
460
438 for path, ent in paths:
461 for path, ent in paths:
439 entrypath = get_entry_from_path(path, module=self.module)
462 entrypath = get_entry_from_path(path, module=self.module)
440 entry = entrypath.decode(self.encoding)
463 entry = entrypath.decode(self.encoding)
441
464
442 kind = svn.ra.check_path(self.ra, entrypath, revnum)
465 kind = svn.ra.check_path(self.ra, entrypath, revnum)
443 if kind == svn.core.svn_node_file:
466 if kind == svn.core.svn_node_file:
444 if ent.copyfrom_path:
467 if ent.copyfrom_path:
445 copyfrom_path = get_entry_from_path(ent.copyfrom_path)
468 copyfrom_path = get_entry_from_path(ent.copyfrom_path)
446 if copyfrom_path:
469 if copyfrom_path:
447 self.ui.debug("Copied to %s from %s@%s\n" %
470 self.ui.debug("Copied to %s from %s@%s\n" %
448 (entrypath, copyfrom_path,
471 (entrypath, copyfrom_path,
449 ent.copyfrom_rev))
472 ent.copyfrom_rev))
450 # It's probably important for hg that the source
473 # It's probably important for hg that the source
451 # exists in the revision's parent, not just the
474 # exists in the revision's parent, not just the
452 # ent.copyfrom_rev
475 # ent.copyfrom_rev
453 fromkind = svn.ra.check_path(self.ra, copyfrom_path, ent.copyfrom_rev)
476 fromkind = svn.ra.check_path(self.ra, copyfrom_path, ent.copyfrom_rev)
454 if fromkind != 0:
477 if fromkind != 0:
455 copies[self.recode(entry)] = self.recode(copyfrom_path)
478 copies[self.recode(entry)] = self.recode(copyfrom_path)
456 entries.append(self.recode(entry))
479 entries.append(self.recode(entry))
457 elif kind == 0: # gone, but had better be a deleted *file*
480 elif kind == 0: # gone, but had better be a deleted *file*
458 self.ui.debug("gone from %s\n" % ent.copyfrom_rev)
481 self.ui.debug("gone from %s\n" % ent.copyfrom_rev)
459
482
460 # if a branch is created but entries are removed in the same
483 # if a branch is created but entries are removed in the same
461 # changeset, get the right fromrev
484 # changeset, get the right fromrev
462 # parents cannot be empty here, you cannot remove things from
485 # parents cannot be empty here, you cannot remove things from
463 # a root revision.
486 # a root revision.
464 uuid, old_module, fromrev = self.revsplit(parents[0])
487 uuid, old_module, fromrev = self.revsplit(parents[0])
465
488
466 basepath = old_module + "/" + get_entry_from_path(path, module=self.module)
489 basepath = old_module + "/" + get_entry_from_path(path, module=self.module)
467 entrypath = old_module + "/" + get_entry_from_path(path, module=self.module)
490 entrypath = old_module + "/" + get_entry_from_path(path, module=self.module)
468
491
469 def lookup_parts(p):
492 def lookup_parts(p):
470 rc = None
493 rc = None
471 parts = p.split("/")
494 parts = p.split("/")
472 for i in range(len(parts)):
495 for i in range(len(parts)):
473 part = "/".join(parts[:i])
496 part = "/".join(parts[:i])
474 info = part, copyfrom.get(part, None)
497 info = part, copyfrom.get(part, None)
475 if info[1] is not None:
498 if info[1] is not None:
476 self.ui.debug("Found parent directory %s\n" % info[1])
499 self.ui.debug("Found parent directory %s\n" % info[1])
477 rc = info
500 rc = info
478 return rc
501 return rc
479
502
480 self.ui.debug("base, entry %s %s\n" % (basepath, entrypath))
503 self.ui.debug("base, entry %s %s\n" % (basepath, entrypath))
481
504
482 frompath, froment = lookup_parts(entrypath) or (None, revnum - 1)
505 frompath, froment = lookup_parts(entrypath) or (None, revnum - 1)
483
506
484 # need to remove fragment from lookup_parts and replace with copyfrom_path
507 # need to remove fragment from lookup_parts and replace with copyfrom_path
485 if frompath is not None:
508 if frompath is not None:
486 self.ui.debug("munge-o-matic\n")
509 self.ui.debug("munge-o-matic\n")
487 self.ui.debug(entrypath + '\n')
510 self.ui.debug(entrypath + '\n')
488 self.ui.debug(entrypath[len(frompath):] + '\n')
511 self.ui.debug(entrypath[len(frompath):] + '\n')
489 entrypath = froment.copyfrom_path + entrypath[len(frompath):]
512 entrypath = froment.copyfrom_path + entrypath[len(frompath):]
490 fromrev = froment.copyfrom_rev
513 fromrev = froment.copyfrom_rev
491 self.ui.debug("Info: %s %s %s %s\n" % (frompath, froment, ent, entrypath))
514 self.ui.debug("Info: %s %s %s %s\n" % (frompath, froment, ent, entrypath))
492
515
493 # We can avoid the reparent calls if the module has not changed
516 # We can avoid the reparent calls if the module has not changed
494 # but it probably does not worth the pain.
517 # but it probably does not worth the pain.
495 self.reparent('')
518 self.reparent('')
496 fromkind = svn.ra.check_path(self.ra, entrypath.strip('/'), fromrev)
519 fromkind = svn.ra.check_path(self.ra, entrypath.strip('/'), fromrev)
497 self.reparent(self.module)
520 self.reparent(self.module)
498
521
499 if fromkind == svn.core.svn_node_file: # a deleted file
522 if fromkind == svn.core.svn_node_file: # a deleted file
500 entries.append(self.recode(entry))
523 entries.append(self.recode(entry))
501 elif fromkind == svn.core.svn_node_dir:
524 elif fromkind == svn.core.svn_node_dir:
502 # print "Deleted/moved non-file:", revnum, path, ent
525 # print "Deleted/moved non-file:", revnum, path, ent
503 # children = self._find_children(path, revnum - 1)
526 # children = self._find_children(path, revnum - 1)
504 # print "find children %s@%d from %d action %s" % (path, revnum, ent.copyfrom_rev, ent.action)
527 # print "find children %s@%d from %d action %s" % (path, revnum, ent.copyfrom_rev, ent.action)
505 # Sometimes this is tricky. For example: in
528 # Sometimes this is tricky. For example: in
506 # The Subversion Repository revision 6940 a dir
529 # The Subversion Repository revision 6940 a dir
507 # was copied and one of its files was deleted
530 # was copied and one of its files was deleted
508 # from the new location in the same commit. This
531 # from the new location in the same commit. This
509 # code can't deal with that yet.
532 # code can't deal with that yet.
510 if ent.action == 'C':
533 if ent.action == 'C':
511 children = self._find_children(path, fromrev)
534 children = self._find_children(path, fromrev)
512 else:
535 else:
513 oroot = entrypath.strip('/')
536 oroot = entrypath.strip('/')
514 nroot = path.strip('/')
537 nroot = path.strip('/')
515 children = self._find_children(oroot, fromrev)
538 children = self._find_children(oroot, fromrev)
516 children = [s.replace(oroot,nroot) for s in children]
539 children = [s.replace(oroot,nroot) for s in children]
517 # Mark all [files, not directories] as deleted.
540 # Mark all [files, not directories] as deleted.
518 for child in children:
541 for child in children:
519 # Can we move a child directory and its
542 # Can we move a child directory and its
520 # parent in the same commit? (probably can). Could
543 # parent in the same commit? (probably can). Could
521 # cause problems if instead of revnum -1,
544 # cause problems if instead of revnum -1,
522 # we have to look in (copyfrom_path, revnum - 1)
545 # we have to look in (copyfrom_path, revnum - 1)
523 entrypath = get_entry_from_path("/" + child, module=old_module)
546 entrypath = get_entry_from_path("/" + child, module=old_module)
524 if entrypath:
547 if entrypath:
525 entry = self.recode(entrypath.decode(self.encoding))
548 entry = self.recode(entrypath.decode(self.encoding))
526 if entry in copies:
549 if entry in copies:
527 # deleted file within a copy
550 # deleted file within a copy
528 del copies[entry]
551 del copies[entry]
529 else:
552 else:
530 entries.append(entry)
553 entries.append(entry)
531 else:
554 else:
532 self.ui.debug('unknown path in revision %d: %s\n' % \
555 self.ui.debug('unknown path in revision %d: %s\n' % \
533 (revnum, path))
556 (revnum, path))
534 elif kind == svn.core.svn_node_dir:
557 elif kind == svn.core.svn_node_dir:
535 # Should probably synthesize normal file entries
558 # Should probably synthesize normal file entries
536 # and handle as above to clean up copy/rename handling.
559 # and handle as above to clean up copy/rename handling.
537
560
538 # If the directory just had a prop change,
561 # If the directory just had a prop change,
539 # then we shouldn't need to look for its children.
562 # then we shouldn't need to look for its children.
540 if ent.action == 'M':
563 if ent.action == 'M':
541 continue
564 continue
542
565
543 # Also this could create duplicate entries. Not sure
566 # Also this could create duplicate entries. Not sure
544 # whether this will matter. Maybe should make entries a set.
567 # whether this will matter. Maybe should make entries a set.
545 # print "Changed directory", revnum, path, ent.action, ent.copyfrom_path, ent.copyfrom_rev
568 # print "Changed directory", revnum, path, ent.action, ent.copyfrom_path, ent.copyfrom_rev
546 # This will fail if a directory was copied
569 # This will fail if a directory was copied
547 # from another branch and then some of its files
570 # from another branch and then some of its files
548 # were deleted in the same transaction.
571 # were deleted in the same transaction.
549 children = self._find_children(path, revnum)
572 children = self._find_children(path, revnum)
550 children.sort()
573 children.sort()
551 for child in children:
574 for child in children:
552 # Can we move a child directory and its
575 # Can we move a child directory and its
553 # parent in the same commit? (probably can). Could
576 # parent in the same commit? (probably can). Could
554 # cause problems if instead of revnum -1,
577 # cause problems if instead of revnum -1,
555 # we have to look in (copyfrom_path, revnum - 1)
578 # we have to look in (copyfrom_path, revnum - 1)
556 entrypath = get_entry_from_path("/" + child, module=self.module)
579 entrypath = get_entry_from_path("/" + child, module=self.module)
557 # print child, self.module, entrypath
580 # print child, self.module, entrypath
558 if entrypath:
581 if entrypath:
559 # Need to filter out directories here...
582 # Need to filter out directories here...
560 kind = svn.ra.check_path(self.ra, entrypath, revnum)
583 kind = svn.ra.check_path(self.ra, entrypath, revnum)
561 if kind != svn.core.svn_node_dir:
584 if kind != svn.core.svn_node_dir:
562 entries.append(self.recode(entrypath))
585 entries.append(self.recode(entrypath))
563
586
564 # Copies here (must copy all from source)
587 # Copies here (must copy all from source)
565 # Probably not a real problem for us if
588 # Probably not a real problem for us if
566 # source does not exist
589 # source does not exist
567
590
568 # Can do this with the copy command "hg copy"
591 # Can do this with the copy command "hg copy"
569 # if ent.copyfrom_path:
592 # if ent.copyfrom_path:
570 # copyfrom_entry = get_entry_from_path(ent.copyfrom_path.decode(self.encoding),
593 # copyfrom_entry = get_entry_from_path(ent.copyfrom_path.decode(self.encoding),
571 # module=self.module)
594 # module=self.module)
572 # copyto_entry = entrypath
595 # copyto_entry = entrypath
573 #
596 #
574 # print "copy directory", copyfrom_entry, 'to', copyto_entry
597 # print "copy directory", copyfrom_entry, 'to', copyto_entry
575 #
598 #
576 # copies.append((copyfrom_entry, copyto_entry))
599 # copies.append((copyfrom_entry, copyto_entry))
577
600
578 if ent.copyfrom_path:
601 if ent.copyfrom_path:
579 copyfrom_path = ent.copyfrom_path.decode(self.encoding)
602 copyfrom_path = ent.copyfrom_path.decode(self.encoding)
580 copyfrom_entry = get_entry_from_path(copyfrom_path, module=self.module)
603 copyfrom_entry = get_entry_from_path(copyfrom_path, module=self.module)
581 if copyfrom_entry:
604 if copyfrom_entry:
582 copyfrom[path] = ent
605 copyfrom[path] = ent
583 self.ui.debug("mark %s came from %s\n" % (path, copyfrom[path]))
606 self.ui.debug("mark %s came from %s\n" % (path, copyfrom[path]))
584
607
585 # Good, /probably/ a regular copy. Really should check
608 # Good, /probably/ a regular copy. Really should check
586 # to see whether the parent revision actually contains
609 # to see whether the parent revision actually contains
587 # the directory in question.
610 # the directory in question.
588 children = self._find_children(self.recode(copyfrom_path), ent.copyfrom_rev)
611 children = self._find_children(self.recode(copyfrom_path), ent.copyfrom_rev)
589 children.sort()
612 children.sort()
590 for child in children:
613 for child in children:
591 entrypath = get_entry_from_path("/" + child, module=self.module)
614 entrypath = get_entry_from_path("/" + child, module=self.module)
592 if entrypath:
615 if entrypath:
593 entry = entrypath.decode(self.encoding)
616 entry = entrypath.decode(self.encoding)
594 # print "COPY COPY From", copyfrom_entry, entry
617 # print "COPY COPY From", copyfrom_entry, entry
595 copyto_path = path + entry[len(copyfrom_entry):]
618 copyto_path = path + entry[len(copyfrom_entry):]
596 copyto_entry = get_entry_from_path(copyto_path, module=self.module)
619 copyto_entry = get_entry_from_path(copyto_path, module=self.module)
597 # print "COPY", entry, "COPY To", copyto_entry
620 # print "COPY", entry, "COPY To", copyto_entry
598 copies[self.recode(copyto_entry)] = self.recode(entry)
621 copies[self.recode(copyto_entry)] = self.recode(entry)
599 # copy from quux splort/quuxfile
622 # copy from quux splort/quuxfile
600
623
601 return (util.unique(entries), copies)
624 return (util.unique(entries), copies)
602
625
603 def _fetch_revisions(self, from_revnum, to_revnum):
626 def _fetch_revisions(self, from_revnum, to_revnum):
604 if from_revnum < to_revnum:
627 if from_revnum < to_revnum:
605 from_revnum, to_revnum = to_revnum, from_revnum
628 from_revnum, to_revnum = to_revnum, from_revnum
606
629
607 self.child_cset = None
630 self.child_cset = None
608 def parselogentry(orig_paths, revnum, author, date, message):
631 def parselogentry(orig_paths, revnum, author, date, message):
609 """Return the parsed commit object or None, and True if
632 """Return the parsed commit object or None, and True if
610 the revision is a branch root.
633 the revision is a branch root.
611 """
634 """
612 self.ui.debug("parsing revision %d (%d changes)\n" %
635 self.ui.debug("parsing revision %d (%d changes)\n" %
613 (revnum, len(orig_paths)))
636 (revnum, len(orig_paths)))
614
637
615 rev = self.revid(revnum)
638 rev = self.revid(revnum)
616 # branch log might return entries for a parent we already have
639 # branch log might return entries for a parent we already have
617
640
618 if (rev in self.commits or revnum < to_revnum):
641 if (rev in self.commits or revnum < to_revnum):
619 return None, False
642 return None, False
620
643
621 parents = []
644 parents = []
622 # check whether this revision is the start of a branch
645 # check whether this revision is the start of a branch
623 if self.module in orig_paths:
646 if self.module in orig_paths:
624 ent = orig_paths[self.module]
647 ent = orig_paths[self.module]
625 if ent.copyfrom_path:
648 if ent.copyfrom_path:
626 # ent.copyfrom_rev may not be the actual last revision
649 # ent.copyfrom_rev may not be the actual last revision
627 prev = self.latest(ent.copyfrom_path, ent.copyfrom_rev)
650 previd = self.latest(ent.copyfrom_path, ent.copyfrom_rev)
628 parents = [self.revid(prev, ent.copyfrom_path)]
651 parents = [previd]
629 self.ui.note('found parent of branch %s at %d: %s\n' % \
652 prevmodule, prevnum = self.revsplit(previd)[1:]
630 (self.module, prev, ent.copyfrom_path))
653 self.ui.note('found parent of branch %s at %d: %s\n' %
654 (self.module, prevnum, prevmodule))
631 else:
655 else:
632 self.ui.debug("No copyfrom path, don't know what to do.\n")
656 self.ui.debug("No copyfrom path, don't know what to do.\n")
633
657
634 orig_paths = orig_paths.items()
658 orig_paths = orig_paths.items()
635 orig_paths.sort()
659 orig_paths.sort()
636 paths = []
660 paths = []
637 # filter out unrelated paths
661 # filter out unrelated paths
638 for path, ent in orig_paths:
662 for path, ent in orig_paths:
639 if not path.startswith(self.module):
663 if not path.startswith(self.module):
640 self.ui.debug("boring@%s: %s\n" % (revnum, path))
664 self.ui.debug("boring@%s: %s\n" % (revnum, path))
641 continue
665 continue
642 paths.append((path, ent))
666 paths.append((path, ent))
643
667
644 # Example SVN datetime. Includes microseconds.
668 # Example SVN datetime. Includes microseconds.
645 # ISO-8601 conformant
669 # ISO-8601 conformant
646 # '2007-01-04T17:35:00.902377Z'
670 # '2007-01-04T17:35:00.902377Z'
647 date = util.parsedate(date[:19] + " UTC", ["%Y-%m-%dT%H:%M:%S"])
671 date = util.parsedate(date[:19] + " UTC", ["%Y-%m-%dT%H:%M:%S"])
648
672
649 log = message and self.recode(message) or ''
673 log = message and self.recode(message) or ''
650 author = author and self.recode(author) or ''
674 author = author and self.recode(author) or ''
651 try:
675 try:
652 branch = self.module.split("/")[-1]
676 branch = self.module.split("/")[-1]
653 if branch == 'trunk':
677 if branch == 'trunk':
654 branch = ''
678 branch = ''
655 except IndexError:
679 except IndexError:
656 branch = None
680 branch = None
657
681
658 cset = commit(author=author,
682 cset = commit(author=author,
659 date=util.datestr(date),
683 date=util.datestr(date),
660 desc=log,
684 desc=log,
661 parents=parents,
685 parents=parents,
662 branch=branch,
686 branch=branch,
663 rev=rev.encode('utf-8'))
687 rev=rev.encode('utf-8'))
664
688
665 self.commits[rev] = cset
689 self.commits[rev] = cset
666 # The parents list is *shared* among self.paths and the
690 # The parents list is *shared* among self.paths and the
667 # commit object. Both will be updated below.
691 # commit object. Both will be updated below.
668 self.paths[rev] = (paths, cset.parents)
692 self.paths[rev] = (paths, cset.parents)
669 if self.child_cset and not self.child_cset.parents:
693 if self.child_cset and not self.child_cset.parents:
670 self.child_cset.parents[:] = [rev]
694 self.child_cset.parents[:] = [rev]
671 self.child_cset = cset
695 self.child_cset = cset
672 return cset, len(parents) > 0
696 return cset, len(parents) > 0
673
697
674 self.ui.note('fetching revision log for "%s" from %d to %d\n' %
698 self.ui.note('fetching revision log for "%s" from %d to %d\n' %
675 (self.module, from_revnum, to_revnum))
699 (self.module, from_revnum, to_revnum))
676
700
677 try:
701 try:
678 firstcset = None
702 firstcset = None
679 stream = get_log(self.url, [self.module], from_revnum, to_revnum)
703 stream = get_log(self.url, [self.module], from_revnum, to_revnum)
680 try:
704 try:
681 for entry in stream:
705 for entry in stream:
682 paths, revnum, author, date, message = entry
706 paths, revnum, author, date, message = entry
683 if self.is_blacklisted(revnum):
707 if self.is_blacklisted(revnum):
684 self.ui.note('skipping blacklisted revision %d\n'
708 self.ui.note('skipping blacklisted revision %d\n'
685 % revnum)
709 % revnum)
686 continue
710 continue
687 if paths is None:
711 if paths is None:
688 self.ui.debug('revision %d has no entries\n' % revnum)
712 self.ui.debug('revision %d has no entries\n' % revnum)
689 continue
713 continue
690 cset, branched = parselogentry(paths, revnum, author,
714 cset, branched = parselogentry(paths, revnum, author,
691 date, message)
715 date, message)
692 if cset:
716 if cset:
693 firstcset = cset
717 firstcset = cset
694 if branched:
718 if branched:
695 break
719 break
696 finally:
720 finally:
697 stream.close()
721 stream.close()
698
722
699 if firstcset and not firstcset.parents:
723 if firstcset and not firstcset.parents:
700 # The first revision of the sequence (the last fetched one)
724 # The first revision of the sequence (the last fetched one)
701 # has invalid parents if not a branch root. Find the parent
725 # has invalid parents if not a branch root. Find the parent
702 # revision now, if any.
726 # revision now, if any.
703 try:
727 try:
704 firstrevnum = self.revnum(firstcset.rev)
728 firstrevnum = self.revnum(firstcset.rev)
705 if firstrevnum > 1:
729 if firstrevnum > 1:
706 latest = self.latest(self.module, firstrevnum - 1)
730 latest = self.latest(self.module, firstrevnum - 1)
707 firstcset.parents.append(self.revid(latest))
731 firstcset.parents.append(latest)
708 except util.Abort:
732 except util.Abort:
709 pass
733 pass
710 except SubversionException, (inst, num):
734 except SubversionException, (inst, num):
711 if num == svn.core.SVN_ERR_FS_NO_SUCH_REVISION:
735 if num == svn.core.SVN_ERR_FS_NO_SUCH_REVISION:
712 raise NoSuchRevision(branch=self,
736 raise NoSuchRevision(branch=self,
713 revision="Revision number %d" % to_revnum)
737 revision="Revision number %d" % to_revnum)
714 raise
738 raise
715
739
716 def _getfile(self, file, rev):
740 def _getfile(self, file, rev):
717 io = StringIO()
741 io = StringIO()
718 # TODO: ra.get_file transmits the whole file instead of diffs.
742 # TODO: ra.get_file transmits the whole file instead of diffs.
719 mode = ''
743 mode = ''
720 try:
744 try:
721 new_module, revnum = self.revsplit(rev)[1:]
745 new_module, revnum = self.revsplit(rev)[1:]
722 if self.module != new_module:
746 if self.module != new_module:
723 self.module = new_module
747 self.module = new_module
724 self.reparent(self.module)
748 self.reparent(self.module)
725 info = svn.ra.get_file(self.ra, file, revnum, io)
749 info = svn.ra.get_file(self.ra, file, revnum, io)
726 if isinstance(info, list):
750 if isinstance(info, list):
727 info = info[-1]
751 info = info[-1]
728 mode = ("svn:executable" in info) and 'x' or ''
752 mode = ("svn:executable" in info) and 'x' or ''
729 mode = ("svn:special" in info) and 'l' or mode
753 mode = ("svn:special" in info) and 'l' or mode
730 except SubversionException, e:
754 except SubversionException, e:
731 notfound = (svn.core.SVN_ERR_FS_NOT_FOUND,
755 notfound = (svn.core.SVN_ERR_FS_NOT_FOUND,
732 svn.core.SVN_ERR_RA_DAV_PATH_NOT_FOUND)
756 svn.core.SVN_ERR_RA_DAV_PATH_NOT_FOUND)
733 if e.apr_err in notfound: # File not found
757 if e.apr_err in notfound: # File not found
734 raise IOError()
758 raise IOError()
735 raise
759 raise
736 data = io.getvalue()
760 data = io.getvalue()
737 if mode == 'l':
761 if mode == 'l':
738 link_prefix = "link "
762 link_prefix = "link "
739 if data.startswith(link_prefix):
763 if data.startswith(link_prefix):
740 data = data[len(link_prefix):]
764 data = data[len(link_prefix):]
741 return data, mode
765 return data, mode
742
766
743 def _find_children(self, path, revnum):
767 def _find_children(self, path, revnum):
744 path = path.strip('/')
768 path = path.strip('/')
745 pool = Pool()
769 pool = Pool()
746 rpath = '/'.join([self.base, path]).strip('/')
770 rpath = '/'.join([self.base, path]).strip('/')
747 return ['%s/%s' % (path, x) for x in svn.client.ls(rpath, optrev(revnum), True, self.ctx, pool).keys()]
771 return ['%s/%s' % (path, x) for x in svn.client.ls(rpath, optrev(revnum), True, self.ctx, pool).keys()]
748
772
749 pre_revprop_change = '''#!/bin/sh
773 pre_revprop_change = '''#!/bin/sh
750
774
751 REPOS="$1"
775 REPOS="$1"
752 REV="$2"
776 REV="$2"
753 USER="$3"
777 USER="$3"
754 PROPNAME="$4"
778 PROPNAME="$4"
755 ACTION="$5"
779 ACTION="$5"
756
780
757 if [ "$ACTION" = "M" -a "$PROPNAME" = "svn:log" ]; then exit 0; fi
781 if [ "$ACTION" = "M" -a "$PROPNAME" = "svn:log" ]; then exit 0; fi
758 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-branch" ]; then exit 0; fi
782 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-branch" ]; then exit 0; fi
759 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-rev" ]; then exit 0; fi
783 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-rev" ]; then exit 0; fi
760
784
761 echo "Changing prohibited revision property" >&2
785 echo "Changing prohibited revision property" >&2
762 exit 1
786 exit 1
763 '''
787 '''
764
788
765 class svn_sink(converter_sink, commandline):
789 class svn_sink(converter_sink, commandline):
766 commit_re = re.compile(r'Committed revision (\d+).', re.M)
790 commit_re = re.compile(r'Committed revision (\d+).', re.M)
767
791
768 def prerun(self):
792 def prerun(self):
769 if self.wc:
793 if self.wc:
770 os.chdir(self.wc)
794 os.chdir(self.wc)
771
795
772 def postrun(self):
796 def postrun(self):
773 if self.wc:
797 if self.wc:
774 os.chdir(self.cwd)
798 os.chdir(self.cwd)
775
799
776 def join(self, name):
800 def join(self, name):
777 return os.path.join(self.wc, '.svn', name)
801 return os.path.join(self.wc, '.svn', name)
778
802
779 def revmapfile(self):
803 def revmapfile(self):
780 return self.join('hg-shamap')
804 return self.join('hg-shamap')
781
805
782 def authorfile(self):
806 def authorfile(self):
783 return self.join('hg-authormap')
807 return self.join('hg-authormap')
784
808
785 def __init__(self, ui, path):
809 def __init__(self, ui, path):
786 converter_sink.__init__(self, ui, path)
810 converter_sink.__init__(self, ui, path)
787 commandline.__init__(self, ui, 'svn')
811 commandline.__init__(self, ui, 'svn')
788 self.delete = []
812 self.delete = []
789 self.setexec = []
813 self.setexec = []
790 self.delexec = []
814 self.delexec = []
791 self.copies = []
815 self.copies = []
792 self.wc = None
816 self.wc = None
793 self.cwd = os.getcwd()
817 self.cwd = os.getcwd()
794
818
795 path = os.path.realpath(path)
819 path = os.path.realpath(path)
796
820
797 created = False
821 created = False
798 if os.path.isfile(os.path.join(path, '.svn', 'entries')):
822 if os.path.isfile(os.path.join(path, '.svn', 'entries')):
799 self.wc = path
823 self.wc = path
800 self.run0('update')
824 self.run0('update')
801 else:
825 else:
802 wcpath = os.path.join(os.getcwd(), os.path.basename(path) + '-wc')
826 wcpath = os.path.join(os.getcwd(), os.path.basename(path) + '-wc')
803
827
804 if os.path.isdir(os.path.dirname(path)):
828 if os.path.isdir(os.path.dirname(path)):
805 if not os.path.exists(os.path.join(path, 'db', 'fs-type')):
829 if not os.path.exists(os.path.join(path, 'db', 'fs-type')):
806 ui.status(_('initializing svn repo %r\n') %
830 ui.status(_('initializing svn repo %r\n') %
807 os.path.basename(path))
831 os.path.basename(path))
808 commandline(ui, 'svnadmin').run0('create', path)
832 commandline(ui, 'svnadmin').run0('create', path)
809 created = path
833 created = path
810 path = util.normpath(path)
834 path = util.normpath(path)
811 if not path.startswith('/'):
835 if not path.startswith('/'):
812 path = '/' + path
836 path = '/' + path
813 path = 'file://' + path
837 path = 'file://' + path
814
838
815 ui.status(_('initializing svn wc %r\n') % os.path.basename(wcpath))
839 ui.status(_('initializing svn wc %r\n') % os.path.basename(wcpath))
816 self.run0('checkout', path, wcpath)
840 self.run0('checkout', path, wcpath)
817
841
818 self.wc = wcpath
842 self.wc = wcpath
819 self.opener = util.opener(self.wc)
843 self.opener = util.opener(self.wc)
820 self.wopener = util.opener(self.wc)
844 self.wopener = util.opener(self.wc)
821 self.childmap = mapfile(ui, self.join('hg-childmap'))
845 self.childmap = mapfile(ui, self.join('hg-childmap'))
822 self.is_exec = util.checkexec(self.wc) and util.is_exec or None
846 self.is_exec = util.checkexec(self.wc) and util.is_exec or None
823
847
824 if created:
848 if created:
825 hook = os.path.join(created, 'hooks', 'pre-revprop-change')
849 hook = os.path.join(created, 'hooks', 'pre-revprop-change')
826 fp = open(hook, 'w')
850 fp = open(hook, 'w')
827 fp.write(pre_revprop_change)
851 fp.write(pre_revprop_change)
828 fp.close()
852 fp.close()
829 util.set_flags(hook, "x")
853 util.set_flags(hook, "x")
830
854
831 xport = transport.SvnRaTransport(url=geturl(path))
855 xport = transport.SvnRaTransport(url=geturl(path))
832 self.uuid = svn.ra.get_uuid(xport.ra)
856 self.uuid = svn.ra.get_uuid(xport.ra)
833
857
834 def wjoin(self, *names):
858 def wjoin(self, *names):
835 return os.path.join(self.wc, *names)
859 return os.path.join(self.wc, *names)
836
860
837 def putfile(self, filename, flags, data):
861 def putfile(self, filename, flags, data):
838 if 'l' in flags:
862 if 'l' in flags:
839 self.wopener.symlink(data, filename)
863 self.wopener.symlink(data, filename)
840 else:
864 else:
841 try:
865 try:
842 if os.path.islink(self.wjoin(filename)):
866 if os.path.islink(self.wjoin(filename)):
843 os.unlink(filename)
867 os.unlink(filename)
844 except OSError:
868 except OSError:
845 pass
869 pass
846 self.wopener(filename, 'w').write(data)
870 self.wopener(filename, 'w').write(data)
847
871
848 if self.is_exec:
872 if self.is_exec:
849 was_exec = self.is_exec(self.wjoin(filename))
873 was_exec = self.is_exec(self.wjoin(filename))
850 else:
874 else:
851 # On filesystems not supporting execute-bit, there is no way
875 # On filesystems not supporting execute-bit, there is no way
852 # to know if it is set but asking subversion. Setting it
876 # to know if it is set but asking subversion. Setting it
853 # systematically is just as expensive and much simpler.
877 # systematically is just as expensive and much simpler.
854 was_exec = 'x' not in flags
878 was_exec = 'x' not in flags
855
879
856 util.set_flags(self.wjoin(filename), flags)
880 util.set_flags(self.wjoin(filename), flags)
857 if was_exec:
881 if was_exec:
858 if 'x' not in flags:
882 if 'x' not in flags:
859 self.delexec.append(filename)
883 self.delexec.append(filename)
860 else:
884 else:
861 if 'x' in flags:
885 if 'x' in flags:
862 self.setexec.append(filename)
886 self.setexec.append(filename)
863
887
864 def delfile(self, name):
888 def delfile(self, name):
865 self.delete.append(name)
889 self.delete.append(name)
866
890
867 def copyfile(self, source, dest):
891 def copyfile(self, source, dest):
868 self.copies.append([source, dest])
892 self.copies.append([source, dest])
869
893
870 def _copyfile(self, source, dest):
894 def _copyfile(self, source, dest):
871 # SVN's copy command pukes if the destination file exists, but
895 # SVN's copy command pukes if the destination file exists, but
872 # our copyfile method expects to record a copy that has
896 # our copyfile method expects to record a copy that has
873 # already occurred. Cross the semantic gap.
897 # already occurred. Cross the semantic gap.
874 wdest = self.wjoin(dest)
898 wdest = self.wjoin(dest)
875 exists = os.path.exists(wdest)
899 exists = os.path.exists(wdest)
876 if exists:
900 if exists:
877 fd, tempname = tempfile.mkstemp(
901 fd, tempname = tempfile.mkstemp(
878 prefix='hg-copy-', dir=os.path.dirname(wdest))
902 prefix='hg-copy-', dir=os.path.dirname(wdest))
879 os.close(fd)
903 os.close(fd)
880 os.unlink(tempname)
904 os.unlink(tempname)
881 os.rename(wdest, tempname)
905 os.rename(wdest, tempname)
882 try:
906 try:
883 self.run0('copy', source, dest)
907 self.run0('copy', source, dest)
884 finally:
908 finally:
885 if exists:
909 if exists:
886 try:
910 try:
887 os.unlink(wdest)
911 os.unlink(wdest)
888 except OSError:
912 except OSError:
889 pass
913 pass
890 os.rename(tempname, wdest)
914 os.rename(tempname, wdest)
891
915
892 def dirs_of(self, files):
916 def dirs_of(self, files):
893 dirs = set()
917 dirs = set()
894 for f in files:
918 for f in files:
895 if os.path.isdir(self.wjoin(f)):
919 if os.path.isdir(self.wjoin(f)):
896 dirs.add(f)
920 dirs.add(f)
897 for i in strutil.rfindall(f, '/'):
921 for i in strutil.rfindall(f, '/'):
898 dirs.add(f[:i])
922 dirs.add(f[:i])
899 return dirs
923 return dirs
900
924
901 def add_dirs(self, files):
925 def add_dirs(self, files):
902 add_dirs = [d for d in self.dirs_of(files)
926 add_dirs = [d for d in self.dirs_of(files)
903 if not os.path.exists(self.wjoin(d, '.svn', 'entries'))]
927 if not os.path.exists(self.wjoin(d, '.svn', 'entries'))]
904 if add_dirs:
928 if add_dirs:
905 add_dirs.sort()
929 add_dirs.sort()
906 self.xargs(add_dirs, 'add', non_recursive=True, quiet=True)
930 self.xargs(add_dirs, 'add', non_recursive=True, quiet=True)
907 return add_dirs
931 return add_dirs
908
932
909 def add_files(self, files):
933 def add_files(self, files):
910 if files:
934 if files:
911 self.xargs(files, 'add', quiet=True)
935 self.xargs(files, 'add', quiet=True)
912 return files
936 return files
913
937
914 def tidy_dirs(self, names):
938 def tidy_dirs(self, names):
915 dirs = list(self.dirs_of(names))
939 dirs = list(self.dirs_of(names))
916 dirs.sort(reverse=True)
940 dirs.sort(reverse=True)
917 deleted = []
941 deleted = []
918 for d in dirs:
942 for d in dirs:
919 wd = self.wjoin(d)
943 wd = self.wjoin(d)
920 if os.listdir(wd) == '.svn':
944 if os.listdir(wd) == '.svn':
921 self.run0('delete', d)
945 self.run0('delete', d)
922 deleted.append(d)
946 deleted.append(d)
923 return deleted
947 return deleted
924
948
925 def addchild(self, parent, child):
949 def addchild(self, parent, child):
926 self.childmap[parent] = child
950 self.childmap[parent] = child
927
951
928 def revid(self, rev):
952 def revid(self, rev):
929 return u"svn:%s@%s" % (self.uuid, rev)
953 return u"svn:%s@%s" % (self.uuid, rev)
930
954
931 def putcommit(self, files, parents, commit):
955 def putcommit(self, files, parents, commit):
932 for parent in parents:
956 for parent in parents:
933 try:
957 try:
934 return self.revid(self.childmap[parent])
958 return self.revid(self.childmap[parent])
935 except KeyError:
959 except KeyError:
936 pass
960 pass
937 entries = set(self.delete)
961 entries = set(self.delete)
938 files = util.frozenset(files)
962 files = util.frozenset(files)
939 entries.update(self.add_dirs(files.difference(entries)))
963 entries.update(self.add_dirs(files.difference(entries)))
940 if self.copies:
964 if self.copies:
941 for s, d in self.copies:
965 for s, d in self.copies:
942 self._copyfile(s, d)
966 self._copyfile(s, d)
943 self.copies = []
967 self.copies = []
944 if self.delete:
968 if self.delete:
945 self.xargs(self.delete, 'delete')
969 self.xargs(self.delete, 'delete')
946 self.delete = []
970 self.delete = []
947 entries.update(self.add_files(files.difference(entries)))
971 entries.update(self.add_files(files.difference(entries)))
948 entries.update(self.tidy_dirs(entries))
972 entries.update(self.tidy_dirs(entries))
949 if self.delexec:
973 if self.delexec:
950 self.xargs(self.delexec, 'propdel', 'svn:executable')
974 self.xargs(self.delexec, 'propdel', 'svn:executable')
951 self.delexec = []
975 self.delexec = []
952 if self.setexec:
976 if self.setexec:
953 self.xargs(self.setexec, 'propset', 'svn:executable', '*')
977 self.xargs(self.setexec, 'propset', 'svn:executable', '*')
954 self.setexec = []
978 self.setexec = []
955
979
956 fd, messagefile = tempfile.mkstemp(prefix='hg-convert-')
980 fd, messagefile = tempfile.mkstemp(prefix='hg-convert-')
957 fp = os.fdopen(fd, 'w')
981 fp = os.fdopen(fd, 'w')
958 fp.write(commit.desc)
982 fp.write(commit.desc)
959 fp.close()
983 fp.close()
960 try:
984 try:
961 output = self.run0('commit',
985 output = self.run0('commit',
962 username=util.shortuser(commit.author),
986 username=util.shortuser(commit.author),
963 file=messagefile,
987 file=messagefile,
964 encoding='utf-8')
988 encoding='utf-8')
965 try:
989 try:
966 rev = self.commit_re.search(output).group(1)
990 rev = self.commit_re.search(output).group(1)
967 except AttributeError:
991 except AttributeError:
968 self.ui.warn(_('unexpected svn output:\n'))
992 self.ui.warn(_('unexpected svn output:\n'))
969 self.ui.warn(output)
993 self.ui.warn(output)
970 raise util.Abort(_('unable to cope with svn output'))
994 raise util.Abort(_('unable to cope with svn output'))
971 if commit.rev:
995 if commit.rev:
972 self.run('propset', 'hg:convert-rev', commit.rev,
996 self.run('propset', 'hg:convert-rev', commit.rev,
973 revprop=True, revision=rev)
997 revprop=True, revision=rev)
974 if commit.branch and commit.branch != 'default':
998 if commit.branch and commit.branch != 'default':
975 self.run('propset', 'hg:convert-branch', commit.branch,
999 self.run('propset', 'hg:convert-branch', commit.branch,
976 revprop=True, revision=rev)
1000 revprop=True, revision=rev)
977 for parent in parents:
1001 for parent in parents:
978 self.addchild(parent, rev)
1002 self.addchild(parent, rev)
979 return self.revid(rev)
1003 return self.revid(rev)
980 finally:
1004 finally:
981 os.unlink(messagefile)
1005 os.unlink(messagefile)
982
1006
983 def puttags(self, tags):
1007 def puttags(self, tags):
984 self.ui.warn(_('XXX TAGS NOT IMPLEMENTED YET\n'))
1008 self.ui.warn(_('XXX TAGS NOT IMPLEMENTED YET\n'))
General Comments 0
You need to be logged in to leave comments. Login now