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