##// END OF EJS Templates
convert: follow svn module parent moves
Patrick Mezard -
r5958:59dce249 default
parent child Browse files
Show More
@@ -1,1038 +1,1041 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.rootmodule = self.module
185 self.rootmodule = self.module
186 self.commits = {}
186 self.commits = {}
187 self.paths = {}
187 self.paths = {}
188 self.uuid = svn.ra.get_uuid(self.ra).decode(self.encoding)
188 self.uuid = svn.ra.get_uuid(self.ra).decode(self.encoding)
189 except SubversionException, e:
189 except SubversionException, e:
190 ui.print_exc()
190 ui.print_exc()
191 raise NoRepo("%s does not look like a Subversion repo" % self.url)
191 raise NoRepo("%s does not look like a Subversion repo" % self.url)
192
192
193 if rev:
193 if rev:
194 try:
194 try:
195 latest = int(rev)
195 latest = int(rev)
196 except ValueError:
196 except ValueError:
197 raise util.Abort('svn: revision %s is not an integer' % rev)
197 raise util.Abort('svn: revision %s is not an integer' % rev)
198
198
199 try:
199 try:
200 self.get_blacklist()
200 self.get_blacklist()
201 except IOError, e:
201 except IOError, e:
202 pass
202 pass
203
203
204 self.head = self.latest(self.module, latest)
204 self.head = self.latest(self.module, latest)
205 if not self.head:
205 if not self.head:
206 raise util.Abort(_('no revision found in module %s') %
206 raise util.Abort(_('no revision found in module %s') %
207 self.module.encode(self.encoding))
207 self.module.encode(self.encoding))
208 self.last_changed = self.revnum(self.head)
208 self.last_changed = self.revnum(self.head)
209
209
210 self._changescache = None
210 self._changescache = None
211
211
212 if os.path.exists(os.path.join(url, '.svn/entries')):
212 if os.path.exists(os.path.join(url, '.svn/entries')):
213 self.wc = url
213 self.wc = url
214 else:
214 else:
215 self.wc = None
215 self.wc = None
216 self.convertfp = None
216 self.convertfp = None
217
217
218 def setrevmap(self, revmap):
218 def setrevmap(self, revmap):
219 lastrevs = {}
219 lastrevs = {}
220 for revid in revmap.iterkeys():
220 for revid in revmap.iterkeys():
221 uuid, module, revnum = self.revsplit(revid)
221 uuid, module, revnum = self.revsplit(revid)
222 lastrevnum = lastrevs.setdefault(module, revnum)
222 lastrevnum = lastrevs.setdefault(module, revnum)
223 if revnum > lastrevnum:
223 if revnum > lastrevnum:
224 lastrevs[module] = revnum
224 lastrevs[module] = revnum
225 self.lastrevs = lastrevs
225 self.lastrevs = lastrevs
226
226
227 def exists(self, path, optrev):
227 def exists(self, path, optrev):
228 try:
228 try:
229 svn.client.ls(self.url.rstrip('/') + '/' + path,
229 svn.client.ls(self.url.rstrip('/') + '/' + path,
230 optrev, False, self.ctx)
230 optrev, False, self.ctx)
231 return True
231 return True
232 except SubversionException, err:
232 except SubversionException, err:
233 return False
233 return False
234
234
235 def getheads(self):
235 def getheads(self):
236
236
237 def getcfgpath(name, rev):
237 def getcfgpath(name, rev):
238 cfgpath = self.ui.config('convert', 'svn.' + name)
238 cfgpath = self.ui.config('convert', 'svn.' + name)
239 path = (cfgpath or name).strip('/')
239 path = (cfgpath or name).strip('/')
240 if not self.exists(path, rev):
240 if not self.exists(path, rev):
241 if cfgpath:
241 if cfgpath:
242 raise util.Abort(_('expected %s to be at %r, but not found')
242 raise util.Abort(_('expected %s to be at %r, but not found')
243 % (name, path))
243 % (name, path))
244 return None
244 return None
245 self.ui.note(_('found %s at %r\n') % (name, path))
245 self.ui.note(_('found %s at %r\n') % (name, path))
246 return path
246 return path
247
247
248 rev = optrev(self.last_changed)
248 rev = optrev(self.last_changed)
249 oldmodule = ''
249 oldmodule = ''
250 trunk = getcfgpath('trunk', rev)
250 trunk = getcfgpath('trunk', rev)
251 tags = getcfgpath('tags', rev)
251 tags = getcfgpath('tags', rev)
252 branches = getcfgpath('branches', rev)
252 branches = getcfgpath('branches', rev)
253
253
254 # If the project has a trunk or branches, we will extract heads
254 # If the project has a trunk or branches, we will extract heads
255 # from them. We keep the project root otherwise.
255 # from them. We keep the project root otherwise.
256 if trunk:
256 if trunk:
257 oldmodule = self.module or ''
257 oldmodule = self.module or ''
258 self.module += '/' + trunk
258 self.module += '/' + trunk
259 self.head = self.latest(self.module, self.last_changed)
259 self.head = self.latest(self.module, self.last_changed)
260 if not self.head:
260 if not self.head:
261 raise util.Abort(_('no revision found in module %s') %
261 raise util.Abort(_('no revision found in module %s') %
262 self.module.encode(self.encoding))
262 self.module.encode(self.encoding))
263
263
264 # First head in the list is the module's head
264 # First head in the list is the module's head
265 self.heads = [self.head]
265 self.heads = [self.head]
266 self.tags = '%s/%s' % (oldmodule , (tags or 'tags'))
266 self.tags = '%s/%s' % (oldmodule , (tags or 'tags'))
267
267
268 # Check if branches bring a few more heads to the list
268 # Check if branches bring a few more heads to the list
269 if branches:
269 if branches:
270 rpath = self.url.strip('/')
270 rpath = self.url.strip('/')
271 branchnames = svn.client.ls(rpath + '/' + branches, rev, False,
271 branchnames = svn.client.ls(rpath + '/' + branches, rev, False,
272 self.ctx)
272 self.ctx)
273 for branch in branchnames.keys():
273 for branch in branchnames.keys():
274 module = '%s/%s/%s' % (oldmodule, branches, branch)
274 module = '%s/%s/%s' % (oldmodule, branches, branch)
275 brevid = self.latest(module, self.last_changed)
275 brevid = self.latest(module, self.last_changed)
276 if not brevid:
276 if not brevid:
277 self.ui.note(_('ignoring empty branch %s\n') %
277 self.ui.note(_('ignoring empty branch %s\n') %
278 branch.encode(self.encoding))
278 branch.encode(self.encoding))
279 continue
279 continue
280 self.ui.note('found branch %s at %d\n' %
280 self.ui.note('found branch %s at %d\n' %
281 (branch, self.revnum(brevid)))
281 (branch, self.revnum(brevid)))
282 self.heads.append(brevid)
282 self.heads.append(brevid)
283
283
284 return self.heads
284 return self.heads
285
285
286 def getfile(self, file, rev):
286 def getfile(self, file, rev):
287 data, mode = self._getfile(file, rev)
287 data, mode = self._getfile(file, rev)
288 self.modecache[(file, rev)] = mode
288 self.modecache[(file, rev)] = mode
289 return data
289 return data
290
290
291 def getmode(self, file, rev):
291 def getmode(self, file, rev):
292 return self.modecache[(file, rev)]
292 return self.modecache[(file, rev)]
293
293
294 def getchanges(self, rev):
294 def getchanges(self, rev):
295 if self._changescache and self._changescache[0] == rev:
295 if self._changescache and self._changescache[0] == rev:
296 return self._changescache[1]
296 return self._changescache[1]
297 self._changescache = None
297 self._changescache = None
298 self.modecache = {}
298 self.modecache = {}
299 (paths, parents) = self.paths[rev]
299 (paths, parents) = self.paths[rev]
300 if parents:
300 if parents:
301 files, copies = self.expandpaths(rev, paths, parents)
301 files, copies = self.expandpaths(rev, paths, parents)
302 else:
302 else:
303 # Perform a full checkout on roots
303 # Perform a full checkout on roots
304 uuid, module, revnum = self.revsplit(rev)
304 uuid, module, revnum = self.revsplit(rev)
305 entries = svn.client.ls(self.base + module, optrev(revnum),
305 entries = svn.client.ls(self.base + module, optrev(revnum),
306 True, self.ctx)
306 True, self.ctx)
307 files = [n for n,e in entries.iteritems()
307 files = [n for n,e in entries.iteritems()
308 if e.kind == svn.core.svn_node_file]
308 if e.kind == svn.core.svn_node_file]
309 copies = {}
309 copies = {}
310
310
311 files.sort()
311 files.sort()
312 files = zip(files, [rev] * len(files))
312 files = zip(files, [rev] * len(files))
313
313
314 # caller caches the result, so free it here to release memory
314 # caller caches the result, so free it here to release memory
315 del self.paths[rev]
315 del self.paths[rev]
316 return (files, copies)
316 return (files, copies)
317
317
318 def getchangedfiles(self, rev, i):
318 def getchangedfiles(self, rev, i):
319 changes = self.getchanges(rev)
319 changes = self.getchanges(rev)
320 self._changescache = (rev, changes)
320 self._changescache = (rev, changes)
321 return [f[0] for f in changes[0]]
321 return [f[0] for f in changes[0]]
322
322
323 def getcommit(self, rev):
323 def getcommit(self, rev):
324 if rev not in self.commits:
324 if rev not in self.commits:
325 uuid, module, revnum = self.revsplit(rev)
325 uuid, module, revnum = self.revsplit(rev)
326 self.module = module
326 self.module = module
327 self.reparent(module)
327 self.reparent(module)
328 # We assume that:
328 # We assume that:
329 # - requests for revisions after "stop" come from the
329 # - requests for revisions after "stop" come from the
330 # revision graph backward traversal. Cache all of them
330 # revision graph backward traversal. Cache all of them
331 # down to stop, they will be used eventually.
331 # down to stop, they will be used eventually.
332 # - requests for revisions before "stop" come to get
332 # - requests for revisions before "stop" come to get
333 # isolated branches parents. Just fetch what is needed.
333 # isolated branches parents. Just fetch what is needed.
334 stop = self.lastrevs.get(module, 0)
334 stop = self.lastrevs.get(module, 0)
335 if revnum < stop:
335 if revnum < stop:
336 stop = revnum + 1
336 stop = revnum + 1
337 self._fetch_revisions(revnum, stop)
337 self._fetch_revisions(revnum, stop)
338 commit = self.commits[rev]
338 commit = self.commits[rev]
339 # caller caches the result, so free it here to release memory
339 # caller caches the result, so free it here to release memory
340 del self.commits[rev]
340 del self.commits[rev]
341 return commit
341 return commit
342
342
343 def gettags(self):
343 def gettags(self):
344 tags = {}
344 tags = {}
345 start = self.revnum(self.head)
345 start = self.revnum(self.head)
346 try:
346 try:
347 for entry in get_log(self.url, [self.tags], 0, start):
347 for entry in get_log(self.url, [self.tags], 0, start):
348 orig_paths, revnum, author, date, message = entry
348 orig_paths, revnum, author, date, message = entry
349 for path in orig_paths:
349 for path in orig_paths:
350 if not path.startswith(self.tags+'/'):
350 if not path.startswith(self.tags+'/'):
351 continue
351 continue
352 ent = orig_paths[path]
352 ent = orig_paths[path]
353 source = ent.copyfrom_path
353 source = ent.copyfrom_path
354 rev = ent.copyfrom_rev
354 rev = ent.copyfrom_rev
355 tag = path.split('/')[-1]
355 tag = path.split('/')[-1]
356 tags[tag] = self.revid(rev, module=source)
356 tags[tag] = self.revid(rev, module=source)
357 except SubversionException, (inst, num):
357 except SubversionException, (inst, num):
358 self.ui.note('no tags found at revision %d\n' % start)
358 self.ui.note('no tags found at revision %d\n' % start)
359 return tags
359 return tags
360
360
361 def converted(self, rev, destrev):
361 def converted(self, rev, destrev):
362 if not self.wc:
362 if not self.wc:
363 return
363 return
364 if self.convertfp is None:
364 if self.convertfp is None:
365 self.convertfp = open(os.path.join(self.wc, '.svn', 'hg-shamap'),
365 self.convertfp = open(os.path.join(self.wc, '.svn', 'hg-shamap'),
366 'a')
366 'a')
367 self.convertfp.write('%s %d\n' % (destrev, self.revnum(rev)))
367 self.convertfp.write('%s %d\n' % (destrev, self.revnum(rev)))
368 self.convertfp.flush()
368 self.convertfp.flush()
369
369
370 # -- helper functions --
370 # -- helper functions --
371
371
372 def revid(self, revnum, module=None):
372 def revid(self, revnum, module=None):
373 if not module:
373 if not module:
374 module = self.module
374 module = self.module
375 return u"svn:%s%s@%s" % (self.uuid, module.decode(self.encoding),
375 return u"svn:%s%s@%s" % (self.uuid, module.decode(self.encoding),
376 revnum)
376 revnum)
377
377
378 def revnum(self, rev):
378 def revnum(self, rev):
379 return int(rev.split('@')[-1])
379 return int(rev.split('@')[-1])
380
380
381 def revsplit(self, rev):
381 def revsplit(self, rev):
382 url, revnum = rev.encode(self.encoding).split('@', 1)
382 url, revnum = rev.encode(self.encoding).split('@', 1)
383 revnum = int(revnum)
383 revnum = int(revnum)
384 parts = url.split('/', 1)
384 parts = url.split('/', 1)
385 uuid = parts.pop(0)[4:]
385 uuid = parts.pop(0)[4:]
386 mod = ''
386 mod = ''
387 if parts:
387 if parts:
388 mod = '/' + parts[0]
388 mod = '/' + parts[0]
389 return uuid, mod, revnum
389 return uuid, mod, revnum
390
390
391 def latest(self, path, stop=0):
391 def latest(self, path, stop=0):
392 """Find the latest revid affecting path, up to stop. It may return
392 """Find the latest revid affecting path, up to stop. It may return
393 a revision in a different module, since a branch may be moved without
393 a revision in a different module, since a branch may be moved without
394 a change being reported. Return None if computed module does not
394 a change being reported. Return None if computed module does not
395 belong to rootmodule subtree.
395 belong to rootmodule subtree.
396 """
396 """
397 if not stop:
397 if not stop:
398 stop = svn.ra.get_latest_revnum(self.ra)
398 stop = svn.ra.get_latest_revnum(self.ra)
399 try:
399 try:
400 self.reparent('')
400 self.reparent('')
401 dirent = svn.ra.stat(self.ra, path.strip('/'), stop)
401 dirent = svn.ra.stat(self.ra, path.strip('/'), stop)
402 self.reparent(self.module)
402 self.reparent(self.module)
403 except SubversionException:
403 except SubversionException:
404 dirent = None
404 dirent = None
405 if not dirent:
405 if not dirent:
406 raise util.Abort('%s not found up to revision %d' % (path, stop))
406 raise util.Abort('%s not found up to revision %d' % (path, stop))
407
407
408 # stat() gives us the previous revision on this line of development, but
408 # stat() gives us the previous revision on this line of development, but
409 # it might be in *another module*. Fetch the log and detect renames down
409 # it might be in *another module*. Fetch the log and detect renames down
410 # to the latest revision.
410 # to the latest revision.
411 stream = get_log(self.url, [path], stop, dirent.created_rev)
411 stream = get_log(self.url, [path], stop, dirent.created_rev)
412 try:
412 try:
413 for entry in stream:
413 for entry in stream:
414 paths, revnum, author, date, message = entry
414 paths, revnum, author, date, message = entry
415 if revnum <= dirent.created_rev:
415 if revnum <= dirent.created_rev:
416 break
416 break
417
417
418 for p in paths:
418 for p in paths:
419 if not path.startswith(p) or not paths[p].copyfrom_path:
419 if not path.startswith(p) or not paths[p].copyfrom_path:
420 continue
420 continue
421 newpath = paths[p].copyfrom_path + path[len(p):]
421 newpath = paths[p].copyfrom_path + path[len(p):]
422 self.ui.debug("branch renamed from %s to %s at %d\n" %
422 self.ui.debug("branch renamed from %s to %s at %d\n" %
423 (path, newpath, revnum))
423 (path, newpath, revnum))
424 path = newpath
424 path = newpath
425 break
425 break
426 finally:
426 finally:
427 stream.close()
427 stream.close()
428
428
429 if not path.startswith(self.rootmodule):
429 if not path.startswith(self.rootmodule):
430 self.ui.debug(_('ignoring foreign branch %r\n') % path)
430 self.ui.debug(_('ignoring foreign branch %r\n') % path)
431 return None
431 return None
432 return self.revid(dirent.created_rev, path)
432 return self.revid(dirent.created_rev, path)
433
433
434 def get_blacklist(self):
434 def get_blacklist(self):
435 """Avoid certain revision numbers.
435 """Avoid certain revision numbers.
436 It is not uncommon for two nearby revisions to cancel each other
436 It is not uncommon for two nearby revisions to cancel each other
437 out, e.g. 'I copied trunk into a subdirectory of itself instead
437 out, e.g. 'I copied trunk into a subdirectory of itself instead
438 of making a branch'. The converted repository is significantly
438 of making a branch'. The converted repository is significantly
439 smaller if we ignore such revisions."""
439 smaller if we ignore such revisions."""
440 self.blacklist = util.set()
440 self.blacklist = util.set()
441 blacklist = self.blacklist
441 blacklist = self.blacklist
442 for line in file("blacklist.txt", "r"):
442 for line in file("blacklist.txt", "r"):
443 if not line.startswith("#"):
443 if not line.startswith("#"):
444 try:
444 try:
445 svn_rev = int(line.strip())
445 svn_rev = int(line.strip())
446 blacklist.add(svn_rev)
446 blacklist.add(svn_rev)
447 except ValueError, e:
447 except ValueError, e:
448 pass # not an integer or a comment
448 pass # not an integer or a comment
449
449
450 def is_blacklisted(self, svn_rev):
450 def is_blacklisted(self, svn_rev):
451 return svn_rev in self.blacklist
451 return svn_rev in self.blacklist
452
452
453 def reparent(self, module):
453 def reparent(self, module):
454 svn_url = self.base + module
454 svn_url = self.base + module
455 self.ui.debug("reparent to %s\n" % svn_url.encode(self.encoding))
455 self.ui.debug("reparent to %s\n" % svn_url.encode(self.encoding))
456 svn.ra.reparent(self.ra, svn_url.encode(self.encoding))
456 svn.ra.reparent(self.ra, svn_url.encode(self.encoding))
457
457
458 def expandpaths(self, rev, paths, parents):
458 def expandpaths(self, rev, paths, parents):
459 def get_entry_from_path(path, module=self.module):
459 def get_entry_from_path(path, module=self.module):
460 # Given the repository url of this wc, say
460 # Given the repository url of this wc, say
461 # "http://server/plone/CMFPlone/branches/Plone-2_0-branch"
461 # "http://server/plone/CMFPlone/branches/Plone-2_0-branch"
462 # extract the "entry" portion (a relative path) from what
462 # extract the "entry" portion (a relative path) from what
463 # svn log --xml says, ie
463 # svn log --xml says, ie
464 # "/CMFPlone/branches/Plone-2_0-branch/tests/PloneTestCase.py"
464 # "/CMFPlone/branches/Plone-2_0-branch/tests/PloneTestCase.py"
465 # that is to say "tests/PloneTestCase.py"
465 # that is to say "tests/PloneTestCase.py"
466 if path.startswith(module):
466 if path.startswith(module):
467 relative = path[len(module):]
467 relative = path[len(module):]
468 if relative.startswith('/'):
468 if relative.startswith('/'):
469 return relative[1:]
469 return relative[1:]
470 else:
470 else:
471 return relative
471 return relative
472
472
473 # The path is outside our tracked tree...
473 # The path is outside our tracked tree...
474 self.ui.debug('%r is not under %r, ignoring\n' % (path, module))
474 self.ui.debug('%r is not under %r, ignoring\n' % (path, module))
475 return None
475 return None
476
476
477 entries = []
477 entries = []
478 copyfrom = {} # Map of entrypath, revision for finding source of deleted revisions.
478 copyfrom = {} # Map of entrypath, revision for finding source of deleted revisions.
479 copies = {}
479 copies = {}
480
480
481 new_module, revnum = self.revsplit(rev)[1:]
481 new_module, revnum = self.revsplit(rev)[1:]
482 if new_module != self.module:
482 if new_module != self.module:
483 self.module = new_module
483 self.module = new_module
484 self.reparent(self.module)
484 self.reparent(self.module)
485
485
486 for path, ent in paths:
486 for path, ent in paths:
487 entrypath = get_entry_from_path(path, module=self.module)
487 entrypath = get_entry_from_path(path, module=self.module)
488 entry = entrypath.decode(self.encoding)
488 entry = entrypath.decode(self.encoding)
489
489
490 kind = svn.ra.check_path(self.ra, entrypath, revnum)
490 kind = svn.ra.check_path(self.ra, entrypath, revnum)
491 if kind == svn.core.svn_node_file:
491 if kind == svn.core.svn_node_file:
492 if ent.copyfrom_path:
492 if ent.copyfrom_path:
493 copyfrom_path = get_entry_from_path(ent.copyfrom_path)
493 copyfrom_path = get_entry_from_path(ent.copyfrom_path)
494 if copyfrom_path:
494 if copyfrom_path:
495 self.ui.debug("Copied to %s from %s@%s\n" %
495 self.ui.debug("Copied to %s from %s@%s\n" %
496 (entrypath, copyfrom_path,
496 (entrypath, copyfrom_path,
497 ent.copyfrom_rev))
497 ent.copyfrom_rev))
498 # It's probably important for hg that the source
498 # It's probably important for hg that the source
499 # exists in the revision's parent, not just the
499 # exists in the revision's parent, not just the
500 # ent.copyfrom_rev
500 # ent.copyfrom_rev
501 fromkind = svn.ra.check_path(self.ra, copyfrom_path, ent.copyfrom_rev)
501 fromkind = svn.ra.check_path(self.ra, copyfrom_path, ent.copyfrom_rev)
502 if fromkind != 0:
502 if fromkind != 0:
503 copies[self.recode(entry)] = self.recode(copyfrom_path)
503 copies[self.recode(entry)] = self.recode(copyfrom_path)
504 entries.append(self.recode(entry))
504 entries.append(self.recode(entry))
505 elif kind == 0: # gone, but had better be a deleted *file*
505 elif kind == 0: # gone, but had better be a deleted *file*
506 self.ui.debug("gone from %s\n" % ent.copyfrom_rev)
506 self.ui.debug("gone from %s\n" % ent.copyfrom_rev)
507
507
508 # if a branch is created but entries are removed in the same
508 # if a branch is created but entries are removed in the same
509 # changeset, get the right fromrev
509 # changeset, get the right fromrev
510 # parents cannot be empty here, you cannot remove things from
510 # parents cannot be empty here, you cannot remove things from
511 # a root revision.
511 # a root revision.
512 uuid, old_module, fromrev = self.revsplit(parents[0])
512 uuid, old_module, fromrev = self.revsplit(parents[0])
513
513
514 basepath = old_module + "/" + get_entry_from_path(path, module=self.module)
514 basepath = old_module + "/" + get_entry_from_path(path, module=self.module)
515 entrypath = old_module + "/" + get_entry_from_path(path, module=self.module)
515 entrypath = old_module + "/" + get_entry_from_path(path, module=self.module)
516
516
517 def lookup_parts(p):
517 def lookup_parts(p):
518 rc = None
518 rc = None
519 parts = p.split("/")
519 parts = p.split("/")
520 for i in range(len(parts)):
520 for i in range(len(parts)):
521 part = "/".join(parts[:i])
521 part = "/".join(parts[:i])
522 info = part, copyfrom.get(part, None)
522 info = part, copyfrom.get(part, None)
523 if info[1] is not None:
523 if info[1] is not None:
524 self.ui.debug("Found parent directory %s\n" % info[1])
524 self.ui.debug("Found parent directory %s\n" % info[1])
525 rc = info
525 rc = info
526 return rc
526 return rc
527
527
528 self.ui.debug("base, entry %s %s\n" % (basepath, entrypath))
528 self.ui.debug("base, entry %s %s\n" % (basepath, entrypath))
529
529
530 frompath, froment = lookup_parts(entrypath) or (None, revnum - 1)
530 frompath, froment = lookup_parts(entrypath) or (None, revnum - 1)
531
531
532 # need to remove fragment from lookup_parts and replace with copyfrom_path
532 # need to remove fragment from lookup_parts and replace with copyfrom_path
533 if frompath is not None:
533 if frompath is not None:
534 self.ui.debug("munge-o-matic\n")
534 self.ui.debug("munge-o-matic\n")
535 self.ui.debug(entrypath + '\n')
535 self.ui.debug(entrypath + '\n')
536 self.ui.debug(entrypath[len(frompath):] + '\n')
536 self.ui.debug(entrypath[len(frompath):] + '\n')
537 entrypath = froment.copyfrom_path + entrypath[len(frompath):]
537 entrypath = froment.copyfrom_path + entrypath[len(frompath):]
538 fromrev = froment.copyfrom_rev
538 fromrev = froment.copyfrom_rev
539 self.ui.debug("Info: %s %s %s %s\n" % (frompath, froment, ent, entrypath))
539 self.ui.debug("Info: %s %s %s %s\n" % (frompath, froment, ent, entrypath))
540
540
541 # We can avoid the reparent calls if the module has not changed
541 # We can avoid the reparent calls if the module has not changed
542 # but it probably does not worth the pain.
542 # but it probably does not worth the pain.
543 self.reparent('')
543 self.reparent('')
544 fromkind = svn.ra.check_path(self.ra, entrypath.strip('/'), fromrev)
544 fromkind = svn.ra.check_path(self.ra, entrypath.strip('/'), fromrev)
545 self.reparent(self.module)
545 self.reparent(self.module)
546
546
547 if fromkind == svn.core.svn_node_file: # a deleted file
547 if fromkind == svn.core.svn_node_file: # a deleted file
548 entries.append(self.recode(entry))
548 entries.append(self.recode(entry))
549 elif fromkind == svn.core.svn_node_dir:
549 elif fromkind == svn.core.svn_node_dir:
550 # print "Deleted/moved non-file:", revnum, path, ent
550 # print "Deleted/moved non-file:", revnum, path, ent
551 # children = self._find_children(path, revnum - 1)
551 # children = self._find_children(path, revnum - 1)
552 # print "find children %s@%d from %d action %s" % (path, revnum, ent.copyfrom_rev, ent.action)
552 # print "find children %s@%d from %d action %s" % (path, revnum, ent.copyfrom_rev, ent.action)
553 # Sometimes this is tricky. For example: in
553 # Sometimes this is tricky. For example: in
554 # The Subversion Repository revision 6940 a dir
554 # The Subversion Repository revision 6940 a dir
555 # was copied and one of its files was deleted
555 # was copied and one of its files was deleted
556 # from the new location in the same commit. This
556 # from the new location in the same commit. This
557 # code can't deal with that yet.
557 # code can't deal with that yet.
558 if ent.action == 'C':
558 if ent.action == 'C':
559 children = self._find_children(path, fromrev)
559 children = self._find_children(path, fromrev)
560 else:
560 else:
561 oroot = entrypath.strip('/')
561 oroot = entrypath.strip('/')
562 nroot = path.strip('/')
562 nroot = path.strip('/')
563 children = self._find_children(oroot, fromrev)
563 children = self._find_children(oroot, fromrev)
564 children = [s.replace(oroot,nroot) for s in children]
564 children = [s.replace(oroot,nroot) for s in children]
565 # Mark all [files, not directories] as deleted.
565 # Mark all [files, not directories] as deleted.
566 for child in children:
566 for child in children:
567 # Can we move a child directory and its
567 # Can we move a child directory and its
568 # parent in the same commit? (probably can). Could
568 # parent in the same commit? (probably can). Could
569 # cause problems if instead of revnum -1,
569 # cause problems if instead of revnum -1,
570 # we have to look in (copyfrom_path, revnum - 1)
570 # we have to look in (copyfrom_path, revnum - 1)
571 entrypath = get_entry_from_path("/" + child, module=old_module)
571 entrypath = get_entry_from_path("/" + child, module=old_module)
572 if entrypath:
572 if entrypath:
573 entry = self.recode(entrypath.decode(self.encoding))
573 entry = self.recode(entrypath.decode(self.encoding))
574 if entry in copies:
574 if entry in copies:
575 # deleted file within a copy
575 # deleted file within a copy
576 del copies[entry]
576 del copies[entry]
577 else:
577 else:
578 entries.append(entry)
578 entries.append(entry)
579 else:
579 else:
580 self.ui.debug('unknown path in revision %d: %s\n' % \
580 self.ui.debug('unknown path in revision %d: %s\n' % \
581 (revnum, path))
581 (revnum, path))
582 elif kind == svn.core.svn_node_dir:
582 elif kind == svn.core.svn_node_dir:
583 # Should probably synthesize normal file entries
583 # Should probably synthesize normal file entries
584 # and handle as above to clean up copy/rename handling.
584 # and handle as above to clean up copy/rename handling.
585
585
586 # If the directory just had a prop change,
586 # If the directory just had a prop change,
587 # then we shouldn't need to look for its children.
587 # then we shouldn't need to look for its children.
588 if ent.action == 'M':
588 if ent.action == 'M':
589 continue
589 continue
590
590
591 # Also this could create duplicate entries. Not sure
591 # Also this could create duplicate entries. Not sure
592 # whether this will matter. Maybe should make entries a set.
592 # whether this will matter. Maybe should make entries a set.
593 # print "Changed directory", revnum, path, ent.action, ent.copyfrom_path, ent.copyfrom_rev
593 # print "Changed directory", revnum, path, ent.action, ent.copyfrom_path, ent.copyfrom_rev
594 # This will fail if a directory was copied
594 # This will fail if a directory was copied
595 # from another branch and then some of its files
595 # from another branch and then some of its files
596 # were deleted in the same transaction.
596 # were deleted in the same transaction.
597 children = self._find_children(path, revnum)
597 children = self._find_children(path, revnum)
598 children.sort()
598 children.sort()
599 for child in children:
599 for child in children:
600 # Can we move a child directory and its
600 # Can we move a child directory and its
601 # parent in the same commit? (probably can). Could
601 # parent in the same commit? (probably can). Could
602 # cause problems if instead of revnum -1,
602 # cause problems if instead of revnum -1,
603 # we have to look in (copyfrom_path, revnum - 1)
603 # we have to look in (copyfrom_path, revnum - 1)
604 entrypath = get_entry_from_path("/" + child, module=self.module)
604 entrypath = get_entry_from_path("/" + child, module=self.module)
605 # print child, self.module, entrypath
605 # print child, self.module, entrypath
606 if entrypath:
606 if entrypath:
607 # Need to filter out directories here...
607 # Need to filter out directories here...
608 kind = svn.ra.check_path(self.ra, entrypath, revnum)
608 kind = svn.ra.check_path(self.ra, entrypath, revnum)
609 if kind != svn.core.svn_node_dir:
609 if kind != svn.core.svn_node_dir:
610 entries.append(self.recode(entrypath))
610 entries.append(self.recode(entrypath))
611
611
612 # Copies here (must copy all from source)
612 # Copies here (must copy all from source)
613 # Probably not a real problem for us if
613 # Probably not a real problem for us if
614 # source does not exist
614 # source does not exist
615
615
616 # Can do this with the copy command "hg copy"
616 # Can do this with the copy command "hg copy"
617 # if ent.copyfrom_path:
617 # if ent.copyfrom_path:
618 # copyfrom_entry = get_entry_from_path(ent.copyfrom_path.decode(self.encoding),
618 # copyfrom_entry = get_entry_from_path(ent.copyfrom_path.decode(self.encoding),
619 # module=self.module)
619 # module=self.module)
620 # copyto_entry = entrypath
620 # copyto_entry = entrypath
621 #
621 #
622 # print "copy directory", copyfrom_entry, 'to', copyto_entry
622 # print "copy directory", copyfrom_entry, 'to', copyto_entry
623 #
623 #
624 # copies.append((copyfrom_entry, copyto_entry))
624 # copies.append((copyfrom_entry, copyto_entry))
625
625
626 if ent.copyfrom_path:
626 if ent.copyfrom_path:
627 copyfrom_path = ent.copyfrom_path.decode(self.encoding)
627 copyfrom_path = ent.copyfrom_path.decode(self.encoding)
628 copyfrom_entry = get_entry_from_path(copyfrom_path, module=self.module)
628 copyfrom_entry = get_entry_from_path(copyfrom_path, module=self.module)
629 if copyfrom_entry:
629 if copyfrom_entry:
630 copyfrom[path] = ent
630 copyfrom[path] = ent
631 self.ui.debug("mark %s came from %s\n" % (path, copyfrom[path]))
631 self.ui.debug("mark %s came from %s\n" % (path, copyfrom[path]))
632
632
633 # Good, /probably/ a regular copy. Really should check
633 # Good, /probably/ a regular copy. Really should check
634 # to see whether the parent revision actually contains
634 # to see whether the parent revision actually contains
635 # the directory in question.
635 # the directory in question.
636 children = self._find_children(self.recode(copyfrom_path), ent.copyfrom_rev)
636 children = self._find_children(self.recode(copyfrom_path), ent.copyfrom_rev)
637 children.sort()
637 children.sort()
638 for child in children:
638 for child in children:
639 entrypath = get_entry_from_path("/" + child, module=self.module)
639 entrypath = get_entry_from_path("/" + child, module=self.module)
640 if entrypath:
640 if entrypath:
641 entry = entrypath.decode(self.encoding)
641 entry = entrypath.decode(self.encoding)
642 # print "COPY COPY From", copyfrom_entry, entry
642 # print "COPY COPY From", copyfrom_entry, entry
643 copyto_path = path + entry[len(copyfrom_entry):]
643 copyto_path = path + entry[len(copyfrom_entry):]
644 copyto_entry = get_entry_from_path(copyto_path, module=self.module)
644 copyto_entry = get_entry_from_path(copyto_path, module=self.module)
645 # print "COPY", entry, "COPY To", copyto_entry
645 # print "COPY", entry, "COPY To", copyto_entry
646 copies[self.recode(copyto_entry)] = self.recode(entry)
646 copies[self.recode(copyto_entry)] = self.recode(entry)
647 # copy from quux splort/quuxfile
647 # copy from quux splort/quuxfile
648
648
649 return (util.unique(entries), copies)
649 return (util.unique(entries), copies)
650
650
651 def _fetch_revisions(self, from_revnum, to_revnum):
651 def _fetch_revisions(self, from_revnum, to_revnum):
652 if from_revnum < to_revnum:
652 if from_revnum < to_revnum:
653 from_revnum, to_revnum = to_revnum, from_revnum
653 from_revnum, to_revnum = to_revnum, from_revnum
654
654
655 self.child_cset = None
655 self.child_cset = None
656 def parselogentry(orig_paths, revnum, author, date, message):
656 def parselogentry(orig_paths, revnum, author, date, message):
657 """Return the parsed commit object or None, and True if
657 """Return the parsed commit object or None, and True if
658 the revision is a branch root.
658 the revision is a branch root.
659 """
659 """
660 self.ui.debug("parsing revision %d (%d changes)\n" %
660 self.ui.debug("parsing revision %d (%d changes)\n" %
661 (revnum, len(orig_paths)))
661 (revnum, len(orig_paths)))
662
662
663 branched = False
663 branched = False
664 rev = self.revid(revnum)
664 rev = self.revid(revnum)
665 # branch log might return entries for a parent we already have
665 # branch log might return entries for a parent we already have
666
666
667 if (rev in self.commits or revnum < to_revnum):
667 if (rev in self.commits or revnum < to_revnum):
668 return None, branched
668 return None, branched
669
669
670 parents = []
670 parents = []
671 # check whether this revision is the start of a branch
671 # check whether this revision is the start of a branch or part
672 if self.module in orig_paths:
672 # of a branch renaming
673 ent = orig_paths[self.module]
673 orig_paths = orig_paths.items()
674 orig_paths.sort()
675 root_paths = [(p,e) for p,e in orig_paths if self.module.startswith(p)]
676 if root_paths:
677 path, ent = root_paths[-1]
674 if ent.copyfrom_path:
678 if ent.copyfrom_path:
675 branched = True
679 branched = True
680 newpath = ent.copyfrom_path + self.module[len(path):]
676 # ent.copyfrom_rev may not be the actual last revision
681 # ent.copyfrom_rev may not be the actual last revision
677 previd = self.latest(ent.copyfrom_path, ent.copyfrom_rev)
682 previd = self.latest(newpath, ent.copyfrom_rev)
678 if previd is not None:
683 if previd is not None:
679 parents = [previd]
684 parents = [previd]
680 prevmodule, prevnum = self.revsplit(previd)[1:]
685 prevmodule, prevnum = self.revsplit(previd)[1:]
681 self.ui.note('found parent of branch %s at %d: %s\n' %
686 self.ui.note('found parent of branch %s at %d: %s\n' %
682 (self.module, prevnum, prevmodule))
687 (self.module, prevnum, prevmodule))
683 else:
688 else:
684 self.ui.debug("No copyfrom path, don't know what to do.\n")
689 self.ui.debug("No copyfrom path, don't know what to do.\n")
685
690
686 orig_paths = orig_paths.items()
687 orig_paths.sort()
688 paths = []
691 paths = []
689 # filter out unrelated paths
692 # filter out unrelated paths
690 for path, ent in orig_paths:
693 for path, ent in orig_paths:
691 if not path.startswith(self.module):
694 if not path.startswith(self.module):
692 self.ui.debug("boring@%s: %s\n" % (revnum, path))
695 self.ui.debug("boring@%s: %s\n" % (revnum, path))
693 continue
696 continue
694 paths.append((path, ent))
697 paths.append((path, ent))
695
698
696 # Example SVN datetime. Includes microseconds.
699 # Example SVN datetime. Includes microseconds.
697 # ISO-8601 conformant
700 # ISO-8601 conformant
698 # '2007-01-04T17:35:00.902377Z'
701 # '2007-01-04T17:35:00.902377Z'
699 date = util.parsedate(date[:19] + " UTC", ["%Y-%m-%dT%H:%M:%S"])
702 date = util.parsedate(date[:19] + " UTC", ["%Y-%m-%dT%H:%M:%S"])
700
703
701 log = message and self.recode(message) or ''
704 log = message and self.recode(message) or ''
702 author = author and self.recode(author) or ''
705 author = author and self.recode(author) or ''
703 try:
706 try:
704 branch = self.module.split("/")[-1]
707 branch = self.module.split("/")[-1]
705 if branch == 'trunk':
708 if branch == 'trunk':
706 branch = ''
709 branch = ''
707 except IndexError:
710 except IndexError:
708 branch = None
711 branch = None
709
712
710 cset = commit(author=author,
713 cset = commit(author=author,
711 date=util.datestr(date),
714 date=util.datestr(date),
712 desc=log,
715 desc=log,
713 parents=parents,
716 parents=parents,
714 branch=branch,
717 branch=branch,
715 rev=rev.encode('utf-8'))
718 rev=rev.encode('utf-8'))
716
719
717 self.commits[rev] = cset
720 self.commits[rev] = cset
718 # The parents list is *shared* among self.paths and the
721 # The parents list is *shared* among self.paths and the
719 # commit object. Both will be updated below.
722 # commit object. Both will be updated below.
720 self.paths[rev] = (paths, cset.parents)
723 self.paths[rev] = (paths, cset.parents)
721 if self.child_cset and not self.child_cset.parents:
724 if self.child_cset and not self.child_cset.parents:
722 self.child_cset.parents[:] = [rev]
725 self.child_cset.parents[:] = [rev]
723 self.child_cset = cset
726 self.child_cset = cset
724 return cset, branched
727 return cset, branched
725
728
726 self.ui.note('fetching revision log for "%s" from %d to %d\n' %
729 self.ui.note('fetching revision log for "%s" from %d to %d\n' %
727 (self.module, from_revnum, to_revnum))
730 (self.module, from_revnum, to_revnum))
728
731
729 try:
732 try:
730 firstcset = None
733 firstcset = None
731 branched = False
734 branched = False
732 stream = get_log(self.url, [self.module], from_revnum, to_revnum)
735 stream = get_log(self.url, [self.module], from_revnum, to_revnum)
733 try:
736 try:
734 for entry in stream:
737 for entry in stream:
735 paths, revnum, author, date, message = entry
738 paths, revnum, author, date, message = entry
736 if self.is_blacklisted(revnum):
739 if self.is_blacklisted(revnum):
737 self.ui.note('skipping blacklisted revision %d\n'
740 self.ui.note('skipping blacklisted revision %d\n'
738 % revnum)
741 % revnum)
739 continue
742 continue
740 if paths is None:
743 if paths is None:
741 self.ui.debug('revision %d has no entries\n' % revnum)
744 self.ui.debug('revision %d has no entries\n' % revnum)
742 continue
745 continue
743 cset, branched = parselogentry(paths, revnum, author,
746 cset, branched = parselogentry(paths, revnum, author,
744 date, message)
747 date, message)
745 if cset:
748 if cset:
746 firstcset = cset
749 firstcset = cset
747 if branched:
750 if branched:
748 break
751 break
749 finally:
752 finally:
750 stream.close()
753 stream.close()
751
754
752 if not branched and firstcset and not firstcset.parents:
755 if not branched and firstcset and not firstcset.parents:
753 # The first revision of the sequence (the last fetched one)
756 # The first revision of the sequence (the last fetched one)
754 # has invalid parents if not a branch root. Find the parent
757 # has invalid parents if not a branch root. Find the parent
755 # revision now, if any.
758 # revision now, if any.
756 try:
759 try:
757 firstrevnum = self.revnum(firstcset.rev)
760 firstrevnum = self.revnum(firstcset.rev)
758 if firstrevnum > 1:
761 if firstrevnum > 1:
759 latest = self.latest(self.module, firstrevnum - 1)
762 latest = self.latest(self.module, firstrevnum - 1)
760 if latest:
763 if latest:
761 firstcset.parents.append(latest)
764 firstcset.parents.append(latest)
762 except util.Abort:
765 except util.Abort:
763 pass
766 pass
764 except SubversionException, (inst, num):
767 except SubversionException, (inst, num):
765 if num == svn.core.SVN_ERR_FS_NO_SUCH_REVISION:
768 if num == svn.core.SVN_ERR_FS_NO_SUCH_REVISION:
766 raise NoSuchRevision(branch=self,
769 raise NoSuchRevision(branch=self,
767 revision="Revision number %d" % to_revnum)
770 revision="Revision number %d" % to_revnum)
768 raise
771 raise
769
772
770 def _getfile(self, file, rev):
773 def _getfile(self, file, rev):
771 io = StringIO()
774 io = StringIO()
772 # TODO: ra.get_file transmits the whole file instead of diffs.
775 # TODO: ra.get_file transmits the whole file instead of diffs.
773 mode = ''
776 mode = ''
774 try:
777 try:
775 new_module, revnum = self.revsplit(rev)[1:]
778 new_module, revnum = self.revsplit(rev)[1:]
776 if self.module != new_module:
779 if self.module != new_module:
777 self.module = new_module
780 self.module = new_module
778 self.reparent(self.module)
781 self.reparent(self.module)
779 info = svn.ra.get_file(self.ra, file, revnum, io)
782 info = svn.ra.get_file(self.ra, file, revnum, io)
780 if isinstance(info, list):
783 if isinstance(info, list):
781 info = info[-1]
784 info = info[-1]
782 mode = ("svn:executable" in info) and 'x' or ''
785 mode = ("svn:executable" in info) and 'x' or ''
783 mode = ("svn:special" in info) and 'l' or mode
786 mode = ("svn:special" in info) and 'l' or mode
784 except SubversionException, e:
787 except SubversionException, e:
785 notfound = (svn.core.SVN_ERR_FS_NOT_FOUND,
788 notfound = (svn.core.SVN_ERR_FS_NOT_FOUND,
786 svn.core.SVN_ERR_RA_DAV_PATH_NOT_FOUND)
789 svn.core.SVN_ERR_RA_DAV_PATH_NOT_FOUND)
787 if e.apr_err in notfound: # File not found
790 if e.apr_err in notfound: # File not found
788 raise IOError()
791 raise IOError()
789 raise
792 raise
790 data = io.getvalue()
793 data = io.getvalue()
791 if mode == 'l':
794 if mode == 'l':
792 link_prefix = "link "
795 link_prefix = "link "
793 if data.startswith(link_prefix):
796 if data.startswith(link_prefix):
794 data = data[len(link_prefix):]
797 data = data[len(link_prefix):]
795 return data, mode
798 return data, mode
796
799
797 def _find_children(self, path, revnum):
800 def _find_children(self, path, revnum):
798 path = path.strip('/')
801 path = path.strip('/')
799 pool = Pool()
802 pool = Pool()
800 rpath = '/'.join([self.base, path]).strip('/')
803 rpath = '/'.join([self.base, path]).strip('/')
801 return ['%s/%s' % (path, x) for x in svn.client.ls(rpath, optrev(revnum), True, self.ctx, pool).keys()]
804 return ['%s/%s' % (path, x) for x in svn.client.ls(rpath, optrev(revnum), True, self.ctx, pool).keys()]
802
805
803 pre_revprop_change = '''#!/bin/sh
806 pre_revprop_change = '''#!/bin/sh
804
807
805 REPOS="$1"
808 REPOS="$1"
806 REV="$2"
809 REV="$2"
807 USER="$3"
810 USER="$3"
808 PROPNAME="$4"
811 PROPNAME="$4"
809 ACTION="$5"
812 ACTION="$5"
810
813
811 if [ "$ACTION" = "M" -a "$PROPNAME" = "svn:log" ]; then exit 0; fi
814 if [ "$ACTION" = "M" -a "$PROPNAME" = "svn:log" ]; then exit 0; fi
812 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-branch" ]; then exit 0; fi
815 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-branch" ]; then exit 0; fi
813 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-rev" ]; then exit 0; fi
816 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-rev" ]; then exit 0; fi
814
817
815 echo "Changing prohibited revision property" >&2
818 echo "Changing prohibited revision property" >&2
816 exit 1
819 exit 1
817 '''
820 '''
818
821
819 class svn_sink(converter_sink, commandline):
822 class svn_sink(converter_sink, commandline):
820 commit_re = re.compile(r'Committed revision (\d+).', re.M)
823 commit_re = re.compile(r'Committed revision (\d+).', re.M)
821
824
822 def prerun(self):
825 def prerun(self):
823 if self.wc:
826 if self.wc:
824 os.chdir(self.wc)
827 os.chdir(self.wc)
825
828
826 def postrun(self):
829 def postrun(self):
827 if self.wc:
830 if self.wc:
828 os.chdir(self.cwd)
831 os.chdir(self.cwd)
829
832
830 def join(self, name):
833 def join(self, name):
831 return os.path.join(self.wc, '.svn', name)
834 return os.path.join(self.wc, '.svn', name)
832
835
833 def revmapfile(self):
836 def revmapfile(self):
834 return self.join('hg-shamap')
837 return self.join('hg-shamap')
835
838
836 def authorfile(self):
839 def authorfile(self):
837 return self.join('hg-authormap')
840 return self.join('hg-authormap')
838
841
839 def __init__(self, ui, path):
842 def __init__(self, ui, path):
840 converter_sink.__init__(self, ui, path)
843 converter_sink.__init__(self, ui, path)
841 commandline.__init__(self, ui, 'svn')
844 commandline.__init__(self, ui, 'svn')
842 self.delete = []
845 self.delete = []
843 self.setexec = []
846 self.setexec = []
844 self.delexec = []
847 self.delexec = []
845 self.copies = []
848 self.copies = []
846 self.wc = None
849 self.wc = None
847 self.cwd = os.getcwd()
850 self.cwd = os.getcwd()
848
851
849 path = os.path.realpath(path)
852 path = os.path.realpath(path)
850
853
851 created = False
854 created = False
852 if os.path.isfile(os.path.join(path, '.svn', 'entries')):
855 if os.path.isfile(os.path.join(path, '.svn', 'entries')):
853 self.wc = path
856 self.wc = path
854 self.run0('update')
857 self.run0('update')
855 else:
858 else:
856 wcpath = os.path.join(os.getcwd(), os.path.basename(path) + '-wc')
859 wcpath = os.path.join(os.getcwd(), os.path.basename(path) + '-wc')
857
860
858 if os.path.isdir(os.path.dirname(path)):
861 if os.path.isdir(os.path.dirname(path)):
859 if not os.path.exists(os.path.join(path, 'db', 'fs-type')):
862 if not os.path.exists(os.path.join(path, 'db', 'fs-type')):
860 ui.status(_('initializing svn repo %r\n') %
863 ui.status(_('initializing svn repo %r\n') %
861 os.path.basename(path))
864 os.path.basename(path))
862 commandline(ui, 'svnadmin').run0('create', path)
865 commandline(ui, 'svnadmin').run0('create', path)
863 created = path
866 created = path
864 path = util.normpath(path)
867 path = util.normpath(path)
865 if not path.startswith('/'):
868 if not path.startswith('/'):
866 path = '/' + path
869 path = '/' + path
867 path = 'file://' + path
870 path = 'file://' + path
868
871
869 ui.status(_('initializing svn wc %r\n') % os.path.basename(wcpath))
872 ui.status(_('initializing svn wc %r\n') % os.path.basename(wcpath))
870 self.run0('checkout', path, wcpath)
873 self.run0('checkout', path, wcpath)
871
874
872 self.wc = wcpath
875 self.wc = wcpath
873 self.opener = util.opener(self.wc)
876 self.opener = util.opener(self.wc)
874 self.wopener = util.opener(self.wc)
877 self.wopener = util.opener(self.wc)
875 self.childmap = mapfile(ui, self.join('hg-childmap'))
878 self.childmap = mapfile(ui, self.join('hg-childmap'))
876 self.is_exec = util.checkexec(self.wc) and util.is_exec or None
879 self.is_exec = util.checkexec(self.wc) and util.is_exec or None
877
880
878 if created:
881 if created:
879 hook = os.path.join(created, 'hooks', 'pre-revprop-change')
882 hook = os.path.join(created, 'hooks', 'pre-revprop-change')
880 fp = open(hook, 'w')
883 fp = open(hook, 'w')
881 fp.write(pre_revprop_change)
884 fp.write(pre_revprop_change)
882 fp.close()
885 fp.close()
883 util.set_flags(hook, "x")
886 util.set_flags(hook, "x")
884
887
885 xport = transport.SvnRaTransport(url=geturl(path))
888 xport = transport.SvnRaTransport(url=geturl(path))
886 self.uuid = svn.ra.get_uuid(xport.ra)
889 self.uuid = svn.ra.get_uuid(xport.ra)
887
890
888 def wjoin(self, *names):
891 def wjoin(self, *names):
889 return os.path.join(self.wc, *names)
892 return os.path.join(self.wc, *names)
890
893
891 def putfile(self, filename, flags, data):
894 def putfile(self, filename, flags, data):
892 if 'l' in flags:
895 if 'l' in flags:
893 self.wopener.symlink(data, filename)
896 self.wopener.symlink(data, filename)
894 else:
897 else:
895 try:
898 try:
896 if os.path.islink(self.wjoin(filename)):
899 if os.path.islink(self.wjoin(filename)):
897 os.unlink(filename)
900 os.unlink(filename)
898 except OSError:
901 except OSError:
899 pass
902 pass
900 self.wopener(filename, 'w').write(data)
903 self.wopener(filename, 'w').write(data)
901
904
902 if self.is_exec:
905 if self.is_exec:
903 was_exec = self.is_exec(self.wjoin(filename))
906 was_exec = self.is_exec(self.wjoin(filename))
904 else:
907 else:
905 # On filesystems not supporting execute-bit, there is no way
908 # On filesystems not supporting execute-bit, there is no way
906 # to know if it is set but asking subversion. Setting it
909 # to know if it is set but asking subversion. Setting it
907 # systematically is just as expensive and much simpler.
910 # systematically is just as expensive and much simpler.
908 was_exec = 'x' not in flags
911 was_exec = 'x' not in flags
909
912
910 util.set_flags(self.wjoin(filename), flags)
913 util.set_flags(self.wjoin(filename), flags)
911 if was_exec:
914 if was_exec:
912 if 'x' not in flags:
915 if 'x' not in flags:
913 self.delexec.append(filename)
916 self.delexec.append(filename)
914 else:
917 else:
915 if 'x' in flags:
918 if 'x' in flags:
916 self.setexec.append(filename)
919 self.setexec.append(filename)
917
920
918 def delfile(self, name):
921 def delfile(self, name):
919 self.delete.append(name)
922 self.delete.append(name)
920
923
921 def copyfile(self, source, dest):
924 def copyfile(self, source, dest):
922 self.copies.append([source, dest])
925 self.copies.append([source, dest])
923
926
924 def _copyfile(self, source, dest):
927 def _copyfile(self, source, dest):
925 # SVN's copy command pukes if the destination file exists, but
928 # SVN's copy command pukes if the destination file exists, but
926 # our copyfile method expects to record a copy that has
929 # our copyfile method expects to record a copy that has
927 # already occurred. Cross the semantic gap.
930 # already occurred. Cross the semantic gap.
928 wdest = self.wjoin(dest)
931 wdest = self.wjoin(dest)
929 exists = os.path.exists(wdest)
932 exists = os.path.exists(wdest)
930 if exists:
933 if exists:
931 fd, tempname = tempfile.mkstemp(
934 fd, tempname = tempfile.mkstemp(
932 prefix='hg-copy-', dir=os.path.dirname(wdest))
935 prefix='hg-copy-', dir=os.path.dirname(wdest))
933 os.close(fd)
936 os.close(fd)
934 os.unlink(tempname)
937 os.unlink(tempname)
935 os.rename(wdest, tempname)
938 os.rename(wdest, tempname)
936 try:
939 try:
937 self.run0('copy', source, dest)
940 self.run0('copy', source, dest)
938 finally:
941 finally:
939 if exists:
942 if exists:
940 try:
943 try:
941 os.unlink(wdest)
944 os.unlink(wdest)
942 except OSError:
945 except OSError:
943 pass
946 pass
944 os.rename(tempname, wdest)
947 os.rename(tempname, wdest)
945
948
946 def dirs_of(self, files):
949 def dirs_of(self, files):
947 dirs = set()
950 dirs = set()
948 for f in files:
951 for f in files:
949 if os.path.isdir(self.wjoin(f)):
952 if os.path.isdir(self.wjoin(f)):
950 dirs.add(f)
953 dirs.add(f)
951 for i in strutil.rfindall(f, '/'):
954 for i in strutil.rfindall(f, '/'):
952 dirs.add(f[:i])
955 dirs.add(f[:i])
953 return dirs
956 return dirs
954
957
955 def add_dirs(self, files):
958 def add_dirs(self, files):
956 add_dirs = [d for d in self.dirs_of(files)
959 add_dirs = [d for d in self.dirs_of(files)
957 if not os.path.exists(self.wjoin(d, '.svn', 'entries'))]
960 if not os.path.exists(self.wjoin(d, '.svn', 'entries'))]
958 if add_dirs:
961 if add_dirs:
959 add_dirs.sort()
962 add_dirs.sort()
960 self.xargs(add_dirs, 'add', non_recursive=True, quiet=True)
963 self.xargs(add_dirs, 'add', non_recursive=True, quiet=True)
961 return add_dirs
964 return add_dirs
962
965
963 def add_files(self, files):
966 def add_files(self, files):
964 if files:
967 if files:
965 self.xargs(files, 'add', quiet=True)
968 self.xargs(files, 'add', quiet=True)
966 return files
969 return files
967
970
968 def tidy_dirs(self, names):
971 def tidy_dirs(self, names):
969 dirs = list(self.dirs_of(names))
972 dirs = list(self.dirs_of(names))
970 dirs.sort(reverse=True)
973 dirs.sort(reverse=True)
971 deleted = []
974 deleted = []
972 for d in dirs:
975 for d in dirs:
973 wd = self.wjoin(d)
976 wd = self.wjoin(d)
974 if os.listdir(wd) == '.svn':
977 if os.listdir(wd) == '.svn':
975 self.run0('delete', d)
978 self.run0('delete', d)
976 deleted.append(d)
979 deleted.append(d)
977 return deleted
980 return deleted
978
981
979 def addchild(self, parent, child):
982 def addchild(self, parent, child):
980 self.childmap[parent] = child
983 self.childmap[parent] = child
981
984
982 def revid(self, rev):
985 def revid(self, rev):
983 return u"svn:%s@%s" % (self.uuid, rev)
986 return u"svn:%s@%s" % (self.uuid, rev)
984
987
985 def putcommit(self, files, parents, commit):
988 def putcommit(self, files, parents, commit):
986 for parent in parents:
989 for parent in parents:
987 try:
990 try:
988 return self.revid(self.childmap[parent])
991 return self.revid(self.childmap[parent])
989 except KeyError:
992 except KeyError:
990 pass
993 pass
991 entries = set(self.delete)
994 entries = set(self.delete)
992 files = util.frozenset(files)
995 files = util.frozenset(files)
993 entries.update(self.add_dirs(files.difference(entries)))
996 entries.update(self.add_dirs(files.difference(entries)))
994 if self.copies:
997 if self.copies:
995 for s, d in self.copies:
998 for s, d in self.copies:
996 self._copyfile(s, d)
999 self._copyfile(s, d)
997 self.copies = []
1000 self.copies = []
998 if self.delete:
1001 if self.delete:
999 self.xargs(self.delete, 'delete')
1002 self.xargs(self.delete, 'delete')
1000 self.delete = []
1003 self.delete = []
1001 entries.update(self.add_files(files.difference(entries)))
1004 entries.update(self.add_files(files.difference(entries)))
1002 entries.update(self.tidy_dirs(entries))
1005 entries.update(self.tidy_dirs(entries))
1003 if self.delexec:
1006 if self.delexec:
1004 self.xargs(self.delexec, 'propdel', 'svn:executable')
1007 self.xargs(self.delexec, 'propdel', 'svn:executable')
1005 self.delexec = []
1008 self.delexec = []
1006 if self.setexec:
1009 if self.setexec:
1007 self.xargs(self.setexec, 'propset', 'svn:executable', '*')
1010 self.xargs(self.setexec, 'propset', 'svn:executable', '*')
1008 self.setexec = []
1011 self.setexec = []
1009
1012
1010 fd, messagefile = tempfile.mkstemp(prefix='hg-convert-')
1013 fd, messagefile = tempfile.mkstemp(prefix='hg-convert-')
1011 fp = os.fdopen(fd, 'w')
1014 fp = os.fdopen(fd, 'w')
1012 fp.write(commit.desc)
1015 fp.write(commit.desc)
1013 fp.close()
1016 fp.close()
1014 try:
1017 try:
1015 output = self.run0('commit',
1018 output = self.run0('commit',
1016 username=util.shortuser(commit.author),
1019 username=util.shortuser(commit.author),
1017 file=messagefile,
1020 file=messagefile,
1018 encoding='utf-8')
1021 encoding='utf-8')
1019 try:
1022 try:
1020 rev = self.commit_re.search(output).group(1)
1023 rev = self.commit_re.search(output).group(1)
1021 except AttributeError:
1024 except AttributeError:
1022 self.ui.warn(_('unexpected svn output:\n'))
1025 self.ui.warn(_('unexpected svn output:\n'))
1023 self.ui.warn(output)
1026 self.ui.warn(output)
1024 raise util.Abort(_('unable to cope with svn output'))
1027 raise util.Abort(_('unable to cope with svn output'))
1025 if commit.rev:
1028 if commit.rev:
1026 self.run('propset', 'hg:convert-rev', commit.rev,
1029 self.run('propset', 'hg:convert-rev', commit.rev,
1027 revprop=True, revision=rev)
1030 revprop=True, revision=rev)
1028 if commit.branch and commit.branch != 'default':
1031 if commit.branch and commit.branch != 'default':
1029 self.run('propset', 'hg:convert-branch', commit.branch,
1032 self.run('propset', 'hg:convert-branch', commit.branch,
1030 revprop=True, revision=rev)
1033 revprop=True, revision=rev)
1031 for parent in parents:
1034 for parent in parents:
1032 self.addchild(parent, rev)
1035 self.addchild(parent, rev)
1033 return self.revid(rev)
1036 return self.revid(rev)
1034 finally:
1037 finally:
1035 os.unlink(messagefile)
1038 os.unlink(messagefile)
1036
1039
1037 def puttags(self, tags):
1040 def puttags(self, tags):
1038 self.ui.warn(_('XXX TAGS NOT IMPLEMENTED YET\n'))
1041 self.ui.warn(_('XXX TAGS NOT IMPLEMENTED YET\n'))
General Comments 0
You need to be logged in to leave comments. Login now