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