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