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