##// END OF EJS Templates
splicemap: improve error handling when source is subversion (issue2084)...
Ben Goswami -
r19122:83973dc1 default
parent child Browse files
Show More
@@ -1,448 +1,447 b''
1 1 # common.py - common code for the convert extension
2 2 #
3 3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com> and others
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 import base64, errno, subprocess, os, datetime, re
9 9 import cPickle as pickle
10 10 from mercurial import util
11 11 from mercurial.i18n import _
12 12
13 13 propertycache = util.propertycache
14 14
15 15 def encodeargs(args):
16 16 def encodearg(s):
17 17 lines = base64.encodestring(s)
18 18 lines = [l.splitlines()[0] for l in lines]
19 19 return ''.join(lines)
20 20
21 21 s = pickle.dumps(args)
22 22 return encodearg(s)
23 23
24 24 def decodeargs(s):
25 25 s = base64.decodestring(s)
26 26 return pickle.loads(s)
27 27
28 28 class MissingTool(Exception):
29 29 pass
30 30
31 31 def checktool(exe, name=None, abort=True):
32 32 name = name or exe
33 33 if not util.findexe(exe):
34 34 exc = abort and util.Abort or MissingTool
35 35 raise exc(_('cannot find required "%s" tool') % name)
36 36
37 37 class NoRepo(Exception):
38 38 pass
39 39
40 40 SKIPREV = 'SKIP'
41 41
42 42 class commit(object):
43 43 def __init__(self, author, date, desc, parents, branch=None, rev=None,
44 44 extra={}, sortkey=None):
45 45 self.author = author or 'unknown'
46 46 self.date = date or '0 0'
47 47 self.desc = desc
48 48 self.parents = parents
49 49 self.branch = branch
50 50 self.rev = rev
51 51 self.extra = extra
52 52 self.sortkey = sortkey
53 53
54 54 class converter_source(object):
55 55 """Conversion source interface"""
56 56
57 57 def __init__(self, ui, path=None, rev=None):
58 58 """Initialize conversion source (or raise NoRepo("message")
59 59 exception if path is not a valid repository)"""
60 60 self.ui = ui
61 61 self.path = path
62 62 self.rev = rev
63 63
64 64 self.encoding = 'utf-8'
65 65
66 66 def checkhexformat(self, revstr):
67 67 """ fails if revstr is not a 40 byte hex. mercurial and git both uses
68 68 such format for their revision numbering
69 69 """
70 matchobj = re.match(r'[0-9a-fA-F]{40,40}$', revstr)
71 if matchobj is None:
70 if not re.match(r'[0-9a-fA-F]{40,40}$', revstr):
72 71 raise util.Abort(_('splicemap entry %s is not a valid revision'
73 72 ' identifier') % revstr)
74 73
75 74 def before(self):
76 75 pass
77 76
78 77 def after(self):
79 78 pass
80 79
81 80 def setrevmap(self, revmap):
82 81 """set the map of already-converted revisions"""
83 82 pass
84 83
85 84 def getheads(self):
86 85 """Return a list of this repository's heads"""
87 86 raise NotImplementedError
88 87
89 88 def getfile(self, name, rev):
90 89 """Return a pair (data, mode) where data is the file content
91 90 as a string and mode one of '', 'x' or 'l'. rev is the
92 91 identifier returned by a previous call to getchanges(). Raise
93 92 IOError to indicate that name was deleted in rev.
94 93 """
95 94 raise NotImplementedError
96 95
97 96 def getchanges(self, version):
98 97 """Returns a tuple of (files, copies).
99 98
100 99 files is a sorted list of (filename, id) tuples for all files
101 100 changed between version and its first parent returned by
102 101 getcommit(). id is the source revision id of the file.
103 102
104 103 copies is a dictionary of dest: source
105 104 """
106 105 raise NotImplementedError
107 106
108 107 def getcommit(self, version):
109 108 """Return the commit object for version"""
110 109 raise NotImplementedError
111 110
112 111 def gettags(self):
113 112 """Return the tags as a dictionary of name: revision
114 113
115 114 Tag names must be UTF-8 strings.
116 115 """
117 116 raise NotImplementedError
118 117
119 118 def recode(self, s, encoding=None):
120 119 if not encoding:
121 120 encoding = self.encoding or 'utf-8'
122 121
123 122 if isinstance(s, unicode):
124 123 return s.encode("utf-8")
125 124 try:
126 125 return s.decode(encoding).encode("utf-8")
127 126 except UnicodeError:
128 127 try:
129 128 return s.decode("latin-1").encode("utf-8")
130 129 except UnicodeError:
131 130 return s.decode(encoding, "replace").encode("utf-8")
132 131
133 132 def getchangedfiles(self, rev, i):
134 133 """Return the files changed by rev compared to parent[i].
135 134
136 135 i is an index selecting one of the parents of rev. The return
137 136 value should be the list of files that are different in rev and
138 137 this parent.
139 138
140 139 If rev has no parents, i is None.
141 140
142 141 This function is only needed to support --filemap
143 142 """
144 143 raise NotImplementedError
145 144
146 145 def converted(self, rev, sinkrev):
147 146 '''Notify the source that a revision has been converted.'''
148 147 pass
149 148
150 149 def hasnativeorder(self):
151 150 """Return true if this source has a meaningful, native revision
152 151 order. For instance, Mercurial revisions are store sequentially
153 152 while there is no such global ordering with Darcs.
154 153 """
155 154 return False
156 155
157 156 def hasnativeclose(self):
158 157 """Return true if this source has ability to close branch.
159 158 """
160 159 return False
161 160
162 161 def lookuprev(self, rev):
163 162 """If rev is a meaningful revision reference in source, return
164 163 the referenced identifier in the same format used by getcommit().
165 164 return None otherwise.
166 165 """
167 166 return None
168 167
169 168 def getbookmarks(self):
170 169 """Return the bookmarks as a dictionary of name: revision
171 170
172 171 Bookmark names are to be UTF-8 strings.
173 172 """
174 173 return {}
175 174
176 175 def checkrevformat(self, revstr):
177 176 """revstr is a string that describes a revision in the given
178 177 source control system. Return true if revstr has correct
179 178 format.
180 179 """
181 180 return True
182 181
183 182 class converter_sink(object):
184 183 """Conversion sink (target) interface"""
185 184
186 185 def __init__(self, ui, path):
187 186 """Initialize conversion sink (or raise NoRepo("message")
188 187 exception if path is not a valid repository)
189 188
190 189 created is a list of paths to remove if a fatal error occurs
191 190 later"""
192 191 self.ui = ui
193 192 self.path = path
194 193 self.created = []
195 194
196 195 def getheads(self):
197 196 """Return a list of this repository's heads"""
198 197 raise NotImplementedError
199 198
200 199 def revmapfile(self):
201 200 """Path to a file that will contain lines
202 201 source_rev_id sink_rev_id
203 202 mapping equivalent revision identifiers for each system."""
204 203 raise NotImplementedError
205 204
206 205 def authorfile(self):
207 206 """Path to a file that will contain lines
208 207 srcauthor=dstauthor
209 208 mapping equivalent authors identifiers for each system."""
210 209 return None
211 210
212 211 def putcommit(self, files, copies, parents, commit, source, revmap):
213 212 """Create a revision with all changed files listed in 'files'
214 213 and having listed parents. 'commit' is a commit object
215 214 containing at a minimum the author, date, and message for this
216 215 changeset. 'files' is a list of (path, version) tuples,
217 216 'copies' is a dictionary mapping destinations to sources,
218 217 'source' is the source repository, and 'revmap' is a mapfile
219 218 of source revisions to converted revisions. Only getfile() and
220 219 lookuprev() should be called on 'source'.
221 220
222 221 Note that the sink repository is not told to update itself to
223 222 a particular revision (or even what that revision would be)
224 223 before it receives the file data.
225 224 """
226 225 raise NotImplementedError
227 226
228 227 def puttags(self, tags):
229 228 """Put tags into sink.
230 229
231 230 tags: {tagname: sink_rev_id, ...} where tagname is an UTF-8 string.
232 231 Return a pair (tag_revision, tag_parent_revision), or (None, None)
233 232 if nothing was changed.
234 233 """
235 234 raise NotImplementedError
236 235
237 236 def setbranch(self, branch, pbranches):
238 237 """Set the current branch name. Called before the first putcommit
239 238 on the branch.
240 239 branch: branch name for subsequent commits
241 240 pbranches: (converted parent revision, parent branch) tuples"""
242 241 pass
243 242
244 243 def setfilemapmode(self, active):
245 244 """Tell the destination that we're using a filemap
246 245
247 246 Some converter_sources (svn in particular) can claim that a file
248 247 was changed in a revision, even if there was no change. This method
249 248 tells the destination that we're using a filemap and that it should
250 249 filter empty revisions.
251 250 """
252 251 pass
253 252
254 253 def before(self):
255 254 pass
256 255
257 256 def after(self):
258 257 pass
259 258
260 259 def putbookmarks(self, bookmarks):
261 260 """Put bookmarks into sink.
262 261
263 262 bookmarks: {bookmarkname: sink_rev_id, ...}
264 263 where bookmarkname is an UTF-8 string.
265 264 """
266 265 pass
267 266
268 267 def hascommit(self, rev):
269 268 """Return True if the sink contains rev"""
270 269 raise NotImplementedError
271 270
272 271 class commandline(object):
273 272 def __init__(self, ui, command):
274 273 self.ui = ui
275 274 self.command = command
276 275
277 276 def prerun(self):
278 277 pass
279 278
280 279 def postrun(self):
281 280 pass
282 281
283 282 def _cmdline(self, cmd, *args, **kwargs):
284 283 cmdline = [self.command, cmd] + list(args)
285 284 for k, v in kwargs.iteritems():
286 285 if len(k) == 1:
287 286 cmdline.append('-' + k)
288 287 else:
289 288 cmdline.append('--' + k.replace('_', '-'))
290 289 try:
291 290 if len(k) == 1:
292 291 cmdline.append('' + v)
293 292 else:
294 293 cmdline[-1] += '=' + v
295 294 except TypeError:
296 295 pass
297 296 cmdline = [util.shellquote(arg) for arg in cmdline]
298 297 if not self.ui.debugflag:
299 298 cmdline += ['2>', os.devnull]
300 299 cmdline = ' '.join(cmdline)
301 300 return cmdline
302 301
303 302 def _run(self, cmd, *args, **kwargs):
304 303 def popen(cmdline):
305 304 p = subprocess.Popen(cmdline, shell=True, bufsize=-1,
306 305 close_fds=util.closefds,
307 306 stdout=subprocess.PIPE)
308 307 return p
309 308 return self._dorun(popen, cmd, *args, **kwargs)
310 309
311 310 def _run2(self, cmd, *args, **kwargs):
312 311 return self._dorun(util.popen2, cmd, *args, **kwargs)
313 312
314 313 def _dorun(self, openfunc, cmd, *args, **kwargs):
315 314 cmdline = self._cmdline(cmd, *args, **kwargs)
316 315 self.ui.debug('running: %s\n' % (cmdline,))
317 316 self.prerun()
318 317 try:
319 318 return openfunc(cmdline)
320 319 finally:
321 320 self.postrun()
322 321
323 322 def run(self, cmd, *args, **kwargs):
324 323 p = self._run(cmd, *args, **kwargs)
325 324 output = p.communicate()[0]
326 325 self.ui.debug(output)
327 326 return output, p.returncode
328 327
329 328 def runlines(self, cmd, *args, **kwargs):
330 329 p = self._run(cmd, *args, **kwargs)
331 330 output = p.stdout.readlines()
332 331 p.wait()
333 332 self.ui.debug(''.join(output))
334 333 return output, p.returncode
335 334
336 335 def checkexit(self, status, output=''):
337 336 if status:
338 337 if output:
339 338 self.ui.warn(_('%s error:\n') % self.command)
340 339 self.ui.warn(output)
341 340 msg = util.explainexit(status)[0]
342 341 raise util.Abort('%s %s' % (self.command, msg))
343 342
344 343 def run0(self, cmd, *args, **kwargs):
345 344 output, status = self.run(cmd, *args, **kwargs)
346 345 self.checkexit(status, output)
347 346 return output
348 347
349 348 def runlines0(self, cmd, *args, **kwargs):
350 349 output, status = self.runlines(cmd, *args, **kwargs)
351 350 self.checkexit(status, ''.join(output))
352 351 return output
353 352
354 353 @propertycache
355 354 def argmax(self):
356 355 # POSIX requires at least 4096 bytes for ARG_MAX
357 356 argmax = 4096
358 357 try:
359 358 argmax = os.sysconf("SC_ARG_MAX")
360 359 except (AttributeError, ValueError):
361 360 pass
362 361
363 362 # Windows shells impose their own limits on command line length,
364 363 # down to 2047 bytes for cmd.exe under Windows NT/2k and 2500 bytes
365 364 # for older 4nt.exe. See http://support.microsoft.com/kb/830473 for
366 365 # details about cmd.exe limitations.
367 366
368 367 # Since ARG_MAX is for command line _and_ environment, lower our limit
369 368 # (and make happy Windows shells while doing this).
370 369 return argmax // 2 - 1
371 370
372 371 def _limit_arglist(self, arglist, cmd, *args, **kwargs):
373 372 cmdlen = len(self._cmdline(cmd, *args, **kwargs))
374 373 limit = self.argmax - cmdlen
375 374 bytes = 0
376 375 fl = []
377 376 for fn in arglist:
378 377 b = len(fn) + 3
379 378 if bytes + b < limit or len(fl) == 0:
380 379 fl.append(fn)
381 380 bytes += b
382 381 else:
383 382 yield fl
384 383 fl = [fn]
385 384 bytes = b
386 385 if fl:
387 386 yield fl
388 387
389 388 def xargs(self, arglist, cmd, *args, **kwargs):
390 389 for l in self._limit_arglist(arglist, cmd, *args, **kwargs):
391 390 self.run0(cmd, *(list(args) + l), **kwargs)
392 391
393 392 class mapfile(dict):
394 393 def __init__(self, ui, path):
395 394 super(mapfile, self).__init__()
396 395 self.ui = ui
397 396 self.path = path
398 397 self.fp = None
399 398 self.order = []
400 399 self._read()
401 400
402 401 def _read(self):
403 402 if not self.path:
404 403 return
405 404 try:
406 405 fp = open(self.path, 'r')
407 406 except IOError, err:
408 407 if err.errno != errno.ENOENT:
409 408 raise
410 409 return
411 410 for i, line in enumerate(fp):
412 411 line = line.splitlines()[0].rstrip()
413 412 if not line:
414 413 # Ignore blank lines
415 414 continue
416 415 try:
417 416 key, value = line.rsplit(' ', 1)
418 417 except ValueError:
419 418 raise util.Abort(
420 419 _('syntax error in %s(%d): key/value pair expected')
421 420 % (self.path, i + 1))
422 421 if key not in self:
423 422 self.order.append(key)
424 423 super(mapfile, self).__setitem__(key, value)
425 424 fp.close()
426 425
427 426 def __setitem__(self, key, value):
428 427 if self.fp is None:
429 428 try:
430 429 self.fp = open(self.path, 'a')
431 430 except IOError, err:
432 431 raise util.Abort(_('could not open map file %r: %s') %
433 432 (self.path, err.strerror))
434 433 self.fp.write('%s %s\n' % (key, value))
435 434 self.fp.flush()
436 435 super(mapfile, self).__setitem__(key, value)
437 436
438 437 def close(self):
439 438 if self.fp:
440 439 self.fp.close()
441 440 self.fp = None
442 441
443 442 def makedatetimestamp(t):
444 443 """Like util.makedate() but for time t instead of current time"""
445 444 delta = (datetime.datetime.utcfromtimestamp(t) -
446 445 datetime.datetime.fromtimestamp(t))
447 446 tz = delta.days * 86400 + delta.seconds
448 447 return t, tz
@@ -1,1254 +1,1262 b''
1 1 # Subversion 1.4/1.5 Python API backend
2 2 #
3 3 # Copyright(C) 2007 Daniel Holth et al
4 4
5 5 import os, re, sys, tempfile, urllib, urllib2, xml.dom.minidom
6 6 import cPickle as pickle
7 7
8 8 from mercurial import strutil, scmutil, util, encoding
9 9 from mercurial.i18n import _
10 10
11 11 propertycache = util.propertycache
12 12
13 13 # Subversion stuff. Works best with very recent Python SVN bindings
14 14 # e.g. SVN 1.5 or backports. Thanks to the bzr folks for enhancing
15 15 # these bindings.
16 16
17 17 from cStringIO import StringIO
18 18
19 19 from common import NoRepo, MissingTool, commit, encodeargs, decodeargs
20 20 from common import commandline, converter_source, converter_sink, mapfile
21 21 from common import makedatetimestamp
22 22
23 23 try:
24 24 from svn.core import SubversionException, Pool
25 25 import svn
26 26 import svn.client
27 27 import svn.core
28 28 import svn.ra
29 29 import svn.delta
30 30 import transport
31 31 import warnings
32 32 warnings.filterwarnings('ignore',
33 33 module='svn.core',
34 34 category=DeprecationWarning)
35 35
36 36 except ImportError:
37 37 svn = None
38 38
39 39 class SvnPathNotFound(Exception):
40 40 pass
41 41
42 42 def revsplit(rev):
43 43 """Parse a revision string and return (uuid, path, revnum)."""
44 44 url, revnum = rev.rsplit('@', 1)
45 45 parts = url.split('/', 1)
46 46 mod = ''
47 47 if len(parts) > 1:
48 48 mod = '/' + parts[1]
49 49 return parts[0][4:], mod, int(revnum)
50 50
51 51 def quote(s):
52 52 # As of svn 1.7, many svn calls expect "canonical" paths. In
53 53 # theory, we should call svn.core.*canonicalize() on all paths
54 54 # before passing them to the API. Instead, we assume the base url
55 55 # is canonical and copy the behaviour of svn URL encoding function
56 56 # so we can extend it safely with new components. The "safe"
57 57 # characters were taken from the "svn_uri__char_validity" table in
58 58 # libsvn_subr/path.c.
59 59 return urllib.quote(s, "!$&'()*+,-./:=@_~")
60 60
61 61 def geturl(path):
62 62 try:
63 63 return svn.client.url_from_path(svn.core.svn_path_canonicalize(path))
64 64 except SubversionException:
65 65 # svn.client.url_from_path() fails with local repositories
66 66 pass
67 67 if os.path.isdir(path):
68 68 path = os.path.normpath(os.path.abspath(path))
69 69 if os.name == 'nt':
70 70 path = '/' + util.normpath(path)
71 71 # Module URL is later compared with the repository URL returned
72 72 # by svn API, which is UTF-8.
73 73 path = encoding.tolocal(path)
74 74 path = 'file://%s' % quote(path)
75 75 return svn.core.svn_path_canonicalize(path)
76 76
77 77 def optrev(number):
78 78 optrev = svn.core.svn_opt_revision_t()
79 79 optrev.kind = svn.core.svn_opt_revision_number
80 80 optrev.value.number = number
81 81 return optrev
82 82
83 83 class changedpath(object):
84 84 def __init__(self, p):
85 85 self.copyfrom_path = p.copyfrom_path
86 86 self.copyfrom_rev = p.copyfrom_rev
87 87 self.action = p.action
88 88
89 89 def get_log_child(fp, url, paths, start, end, limit=0,
90 90 discover_changed_paths=True, strict_node_history=False):
91 91 protocol = -1
92 92 def receiver(orig_paths, revnum, author, date, message, pool):
93 93 if orig_paths is not None:
94 94 for k, v in orig_paths.iteritems():
95 95 orig_paths[k] = changedpath(v)
96 96 pickle.dump((orig_paths, revnum, author, date, message),
97 97 fp, protocol)
98 98
99 99 try:
100 100 # Use an ra of our own so that our parent can consume
101 101 # our results without confusing the server.
102 102 t = transport.SvnRaTransport(url=url)
103 103 svn.ra.get_log(t.ra, paths, start, end, limit,
104 104 discover_changed_paths,
105 105 strict_node_history,
106 106 receiver)
107 107 except IOError:
108 108 # Caller may interrupt the iteration
109 109 pickle.dump(None, fp, protocol)
110 110 except Exception, inst:
111 111 pickle.dump(str(inst), fp, protocol)
112 112 else:
113 113 pickle.dump(None, fp, protocol)
114 114 fp.close()
115 115 # With large history, cleanup process goes crazy and suddenly
116 116 # consumes *huge* amount of memory. The output file being closed,
117 117 # there is no need for clean termination.
118 118 os._exit(0)
119 119
120 120 def debugsvnlog(ui, **opts):
121 121 """Fetch SVN log in a subprocess and channel them back to parent to
122 122 avoid memory collection issues.
123 123 """
124 124 if svn is None:
125 125 raise util.Abort(_('debugsvnlog could not load Subversion python '
126 126 'bindings'))
127 127
128 128 util.setbinary(sys.stdin)
129 129 util.setbinary(sys.stdout)
130 130 args = decodeargs(sys.stdin.read())
131 131 get_log_child(sys.stdout, *args)
132 132
133 133 class logstream(object):
134 134 """Interruptible revision log iterator."""
135 135 def __init__(self, stdout):
136 136 self._stdout = stdout
137 137
138 138 def __iter__(self):
139 139 while True:
140 140 try:
141 141 entry = pickle.load(self._stdout)
142 142 except EOFError:
143 143 raise util.Abort(_('Mercurial failed to run itself, check'
144 144 ' hg executable is in PATH'))
145 145 try:
146 146 orig_paths, revnum, author, date, message = entry
147 147 except (TypeError, ValueError):
148 148 if entry is None:
149 149 break
150 150 raise util.Abort(_("log stream exception '%s'") % entry)
151 151 yield entry
152 152
153 153 def close(self):
154 154 if self._stdout:
155 155 self._stdout.close()
156 156 self._stdout = None
157 157
158 158
159 159 # Check to see if the given path is a local Subversion repo. Verify this by
160 160 # looking for several svn-specific files and directories in the given
161 161 # directory.
162 162 def filecheck(ui, path, proto):
163 163 for x in ('locks', 'hooks', 'format', 'db'):
164 164 if not os.path.exists(os.path.join(path, x)):
165 165 return False
166 166 return True
167 167
168 168 # Check to see if a given path is the root of an svn repo over http. We verify
169 169 # this by requesting a version-controlled URL we know can't exist and looking
170 170 # for the svn-specific "not found" XML.
171 171 def httpcheck(ui, path, proto):
172 172 try:
173 173 opener = urllib2.build_opener()
174 174 rsp = opener.open('%s://%s/!svn/ver/0/.svn' % (proto, path))
175 175 data = rsp.read()
176 176 except urllib2.HTTPError, inst:
177 177 if inst.code != 404:
178 178 # Except for 404 we cannot know for sure this is not an svn repo
179 179 ui.warn(_('svn: cannot probe remote repository, assume it could '
180 180 'be a subversion repository. Use --source-type if you '
181 181 'know better.\n'))
182 182 return True
183 183 data = inst.fp.read()
184 184 except Exception:
185 185 # Could be urllib2.URLError if the URL is invalid or anything else.
186 186 return False
187 187 return '<m:human-readable errcode="160013">' in data
188 188
189 189 protomap = {'http': httpcheck,
190 190 'https': httpcheck,
191 191 'file': filecheck,
192 192 }
193 193 def issvnurl(ui, url):
194 194 try:
195 195 proto, path = url.split('://', 1)
196 196 if proto == 'file':
197 197 if (os.name == 'nt' and path[:1] == '/' and path[1:2].isalpha()
198 198 and path[2:6].lower() == '%3a/'):
199 199 path = path[:2] + ':/' + path[6:]
200 200 path = urllib.url2pathname(path)
201 201 except ValueError:
202 202 proto = 'file'
203 203 path = os.path.abspath(url)
204 204 if proto == 'file':
205 205 path = util.pconvert(path)
206 206 check = protomap.get(proto, lambda *args: False)
207 207 while '/' in path:
208 208 if check(ui, path, proto):
209 209 return True
210 210 path = path.rsplit('/', 1)[0]
211 211 return False
212 212
213 213 # SVN conversion code stolen from bzr-svn and tailor
214 214 #
215 215 # Subversion looks like a versioned filesystem, branches structures
216 216 # are defined by conventions and not enforced by the tool. First,
217 217 # we define the potential branches (modules) as "trunk" and "branches"
218 218 # children directories. Revisions are then identified by their
219 219 # module and revision number (and a repository identifier).
220 220 #
221 221 # The revision graph is really a tree (or a forest). By default, a
222 222 # revision parent is the previous revision in the same module. If the
223 223 # module directory is copied/moved from another module then the
224 224 # revision is the module root and its parent the source revision in
225 225 # the parent module. A revision has at most one parent.
226 226 #
227 227 class svn_source(converter_source):
228 228 def __init__(self, ui, url, rev=None):
229 229 super(svn_source, self).__init__(ui, url, rev=rev)
230 230
231 231 if not (url.startswith('svn://') or url.startswith('svn+ssh://') or
232 232 (os.path.exists(url) and
233 233 os.path.exists(os.path.join(url, '.svn'))) or
234 234 issvnurl(ui, url)):
235 235 raise NoRepo(_("%s does not look like a Subversion repository")
236 236 % url)
237 237 if svn is None:
238 238 raise MissingTool(_('could not load Subversion python bindings'))
239 239
240 240 try:
241 241 version = svn.core.SVN_VER_MAJOR, svn.core.SVN_VER_MINOR
242 242 if version < (1, 4):
243 243 raise MissingTool(_('Subversion python bindings %d.%d found, '
244 244 '1.4 or later required') % version)
245 245 except AttributeError:
246 246 raise MissingTool(_('Subversion python bindings are too old, 1.4 '
247 247 'or later required'))
248 248
249 249 self.lastrevs = {}
250 250
251 251 latest = None
252 252 try:
253 253 # Support file://path@rev syntax. Useful e.g. to convert
254 254 # deleted branches.
255 255 at = url.rfind('@')
256 256 if at >= 0:
257 257 latest = int(url[at + 1:])
258 258 url = url[:at]
259 259 except ValueError:
260 260 pass
261 261 self.url = geturl(url)
262 262 self.encoding = 'UTF-8' # Subversion is always nominal UTF-8
263 263 try:
264 264 self.transport = transport.SvnRaTransport(url=self.url)
265 265 self.ra = self.transport.ra
266 266 self.ctx = self.transport.client
267 267 self.baseurl = svn.ra.get_repos_root(self.ra)
268 268 # Module is either empty or a repository path starting with
269 269 # a slash and not ending with a slash.
270 270 self.module = urllib.unquote(self.url[len(self.baseurl):])
271 271 self.prevmodule = None
272 272 self.rootmodule = self.module
273 273 self.commits = {}
274 274 self.paths = {}
275 275 self.uuid = svn.ra.get_uuid(self.ra)
276 276 except SubversionException:
277 277 ui.traceback()
278 278 raise NoRepo(_("%s does not look like a Subversion repository")
279 279 % self.url)
280 280
281 281 if rev:
282 282 try:
283 283 latest = int(rev)
284 284 except ValueError:
285 285 raise util.Abort(_('svn: revision %s is not an integer') % rev)
286 286
287 287 self.trunkname = self.ui.config('convert', 'svn.trunk',
288 288 'trunk').strip('/')
289 289 self.startrev = self.ui.config('convert', 'svn.startrev', default=0)
290 290 try:
291 291 self.startrev = int(self.startrev)
292 292 if self.startrev < 0:
293 293 self.startrev = 0
294 294 except ValueError:
295 295 raise util.Abort(_('svn: start revision %s is not an integer')
296 296 % self.startrev)
297 297
298 298 try:
299 299 self.head = self.latest(self.module, latest)
300 300 except SvnPathNotFound:
301 301 self.head = None
302 302 if not self.head:
303 303 raise util.Abort(_('no revision found in module %s')
304 304 % self.module)
305 305 self.last_changed = self.revnum(self.head)
306 306
307 307 self._changescache = None
308 308
309 309 if os.path.exists(os.path.join(url, '.svn/entries')):
310 310 self.wc = url
311 311 else:
312 312 self.wc = None
313 313 self.convertfp = None
314 314
315 315 def setrevmap(self, revmap):
316 316 lastrevs = {}
317 317 for revid in revmap.iterkeys():
318 318 uuid, module, revnum = revsplit(revid)
319 319 lastrevnum = lastrevs.setdefault(module, revnum)
320 320 if revnum > lastrevnum:
321 321 lastrevs[module] = revnum
322 322 self.lastrevs = lastrevs
323 323
324 324 def exists(self, path, optrev):
325 325 try:
326 326 svn.client.ls(self.url.rstrip('/') + '/' + quote(path),
327 327 optrev, False, self.ctx)
328 328 return True
329 329 except SubversionException:
330 330 return False
331 331
332 332 def getheads(self):
333 333
334 334 def isdir(path, revnum):
335 335 kind = self._checkpath(path, revnum)
336 336 return kind == svn.core.svn_node_dir
337 337
338 338 def getcfgpath(name, rev):
339 339 cfgpath = self.ui.config('convert', 'svn.' + name)
340 340 if cfgpath is not None and cfgpath.strip() == '':
341 341 return None
342 342 path = (cfgpath or name).strip('/')
343 343 if not self.exists(path, rev):
344 344 if self.module.endswith(path) and name == 'trunk':
345 345 # we are converting from inside this directory
346 346 return None
347 347 if cfgpath:
348 348 raise util.Abort(_('expected %s to be at %r, but not found')
349 349 % (name, path))
350 350 return None
351 351 self.ui.note(_('found %s at %r\n') % (name, path))
352 352 return path
353 353
354 354 rev = optrev(self.last_changed)
355 355 oldmodule = ''
356 356 trunk = getcfgpath('trunk', rev)
357 357 self.tags = getcfgpath('tags', rev)
358 358 branches = getcfgpath('branches', rev)
359 359
360 360 # If the project has a trunk or branches, we will extract heads
361 361 # from them. We keep the project root otherwise.
362 362 if trunk:
363 363 oldmodule = self.module or ''
364 364 self.module += '/' + trunk
365 365 self.head = self.latest(self.module, self.last_changed)
366 366 if not self.head:
367 367 raise util.Abort(_('no revision found in module %s')
368 368 % self.module)
369 369
370 370 # First head in the list is the module's head
371 371 self.heads = [self.head]
372 372 if self.tags is not None:
373 373 self.tags = '%s/%s' % (oldmodule , (self.tags or 'tags'))
374 374
375 375 # Check if branches bring a few more heads to the list
376 376 if branches:
377 377 rpath = self.url.strip('/')
378 378 branchnames = svn.client.ls(rpath + '/' + quote(branches),
379 379 rev, False, self.ctx)
380 380 for branch in sorted(branchnames):
381 381 module = '%s/%s/%s' % (oldmodule, branches, branch)
382 382 if not isdir(module, self.last_changed):
383 383 continue
384 384 brevid = self.latest(module, self.last_changed)
385 385 if not brevid:
386 386 self.ui.note(_('ignoring empty branch %s\n') % branch)
387 387 continue
388 388 self.ui.note(_('found branch %s at %d\n') %
389 389 (branch, self.revnum(brevid)))
390 390 self.heads.append(brevid)
391 391
392 392 if self.startrev and self.heads:
393 393 if len(self.heads) > 1:
394 394 raise util.Abort(_('svn: start revision is not supported '
395 395 'with more than one branch'))
396 396 revnum = self.revnum(self.heads[0])
397 397 if revnum < self.startrev:
398 398 raise util.Abort(
399 399 _('svn: no revision found after start revision %d')
400 400 % self.startrev)
401 401
402 402 return self.heads
403 403
404 404 def getchanges(self, rev):
405 405 if self._changescache and self._changescache[0] == rev:
406 406 return self._changescache[1]
407 407 self._changescache = None
408 408 (paths, parents) = self.paths[rev]
409 409 if parents:
410 410 files, self.removed, copies = self.expandpaths(rev, paths, parents)
411 411 else:
412 412 # Perform a full checkout on roots
413 413 uuid, module, revnum = revsplit(rev)
414 414 entries = svn.client.ls(self.baseurl + quote(module),
415 415 optrev(revnum), True, self.ctx)
416 416 files = [n for n, e in entries.iteritems()
417 417 if e.kind == svn.core.svn_node_file]
418 418 copies = {}
419 419 self.removed = set()
420 420
421 421 files.sort()
422 422 files = zip(files, [rev] * len(files))
423 423
424 424 # caller caches the result, so free it here to release memory
425 425 del self.paths[rev]
426 426 return (files, copies)
427 427
428 428 def getchangedfiles(self, rev, i):
429 429 changes = self.getchanges(rev)
430 430 self._changescache = (rev, changes)
431 431 return [f[0] for f in changes[0]]
432 432
433 433 def getcommit(self, rev):
434 434 if rev not in self.commits:
435 435 uuid, module, revnum = revsplit(rev)
436 436 self.module = module
437 437 self.reparent(module)
438 438 # We assume that:
439 439 # - requests for revisions after "stop" come from the
440 440 # revision graph backward traversal. Cache all of them
441 441 # down to stop, they will be used eventually.
442 442 # - requests for revisions before "stop" come to get
443 443 # isolated branches parents. Just fetch what is needed.
444 444 stop = self.lastrevs.get(module, 0)
445 445 if revnum < stop:
446 446 stop = revnum + 1
447 447 self._fetch_revisions(revnum, stop)
448 448 if rev not in self.commits:
449 449 raise util.Abort(_('svn: revision %s not found') % revnum)
450 450 commit = self.commits[rev]
451 451 # caller caches the result, so free it here to release memory
452 452 del self.commits[rev]
453 453 return commit
454 454
455 def checkrevformat(self, revstr):
456 """ fails if revision format does not match the correct format"""
457 if not re.match(r'svn:[0-9a-f]{8,8}-[0-9a-f]{4,4}-'
458 '[0-9a-f]{4,4}-[0-9a-f]{4,4}-[0-9a-f]'
459 '{12,12}(.*)\@[0-9]+$',revstr):
460 raise util.Abort(_('splicemap entry %s is not a valid revision'
461 ' identifier') % revstr)
462
455 463 def gettags(self):
456 464 tags = {}
457 465 if self.tags is None:
458 466 return tags
459 467
460 468 # svn tags are just a convention, project branches left in a
461 469 # 'tags' directory. There is no other relationship than
462 470 # ancestry, which is expensive to discover and makes them hard
463 471 # to update incrementally. Worse, past revisions may be
464 472 # referenced by tags far away in the future, requiring a deep
465 473 # history traversal on every calculation. Current code
466 474 # performs a single backward traversal, tracking moves within
467 475 # the tags directory (tag renaming) and recording a new tag
468 476 # everytime a project is copied from outside the tags
469 477 # directory. It also lists deleted tags, this behaviour may
470 478 # change in the future.
471 479 pendings = []
472 480 tagspath = self.tags
473 481 start = svn.ra.get_latest_revnum(self.ra)
474 482 stream = self._getlog([self.tags], start, self.startrev)
475 483 try:
476 484 for entry in stream:
477 485 origpaths, revnum, author, date, message = entry
478 486 copies = [(e.copyfrom_path, e.copyfrom_rev, p) for p, e
479 487 in origpaths.iteritems() if e.copyfrom_path]
480 488 # Apply moves/copies from more specific to general
481 489 copies.sort(reverse=True)
482 490
483 491 srctagspath = tagspath
484 492 if copies and copies[-1][2] == tagspath:
485 493 # Track tags directory moves
486 494 srctagspath = copies.pop()[0]
487 495
488 496 for source, sourcerev, dest in copies:
489 497 if not dest.startswith(tagspath + '/'):
490 498 continue
491 499 for tag in pendings:
492 500 if tag[0].startswith(dest):
493 501 tagpath = source + tag[0][len(dest):]
494 502 tag[:2] = [tagpath, sourcerev]
495 503 break
496 504 else:
497 505 pendings.append([source, sourcerev, dest])
498 506
499 507 # Filter out tags with children coming from different
500 508 # parts of the repository like:
501 509 # /tags/tag.1 (from /trunk:10)
502 510 # /tags/tag.1/foo (from /branches/foo:12)
503 511 # Here/tags/tag.1 discarded as well as its children.
504 512 # It happens with tools like cvs2svn. Such tags cannot
505 513 # be represented in mercurial.
506 514 addeds = dict((p, e.copyfrom_path) for p, e
507 515 in origpaths.iteritems()
508 516 if e.action == 'A' and e.copyfrom_path)
509 517 badroots = set()
510 518 for destroot in addeds:
511 519 for source, sourcerev, dest in pendings:
512 520 if (not dest.startswith(destroot + '/')
513 521 or source.startswith(addeds[destroot] + '/')):
514 522 continue
515 523 badroots.add(destroot)
516 524 break
517 525
518 526 for badroot in badroots:
519 527 pendings = [p for p in pendings if p[2] != badroot
520 528 and not p[2].startswith(badroot + '/')]
521 529
522 530 # Tell tag renamings from tag creations
523 531 renamings = []
524 532 for source, sourcerev, dest in pendings:
525 533 tagname = dest.split('/')[-1]
526 534 if source.startswith(srctagspath):
527 535 renamings.append([source, sourcerev, tagname])
528 536 continue
529 537 if tagname in tags:
530 538 # Keep the latest tag value
531 539 continue
532 540 # From revision may be fake, get one with changes
533 541 try:
534 542 tagid = self.latest(source, sourcerev)
535 543 if tagid and tagname not in tags:
536 544 tags[tagname] = tagid
537 545 except SvnPathNotFound:
538 546 # It happens when we are following directories
539 547 # we assumed were copied with their parents
540 548 # but were really created in the tag
541 549 # directory.
542 550 pass
543 551 pendings = renamings
544 552 tagspath = srctagspath
545 553 finally:
546 554 stream.close()
547 555 return tags
548 556
549 557 def converted(self, rev, destrev):
550 558 if not self.wc:
551 559 return
552 560 if self.convertfp is None:
553 561 self.convertfp = open(os.path.join(self.wc, '.svn', 'hg-shamap'),
554 562 'a')
555 563 self.convertfp.write('%s %d\n' % (destrev, self.revnum(rev)))
556 564 self.convertfp.flush()
557 565
558 566 def revid(self, revnum, module=None):
559 567 return 'svn:%s%s@%s' % (self.uuid, module or self.module, revnum)
560 568
561 569 def revnum(self, rev):
562 570 return int(rev.split('@')[-1])
563 571
564 572 def latest(self, path, stop=None):
565 573 """Find the latest revid affecting path, up to stop revision
566 574 number. If stop is None, default to repository latest
567 575 revision. It may return a revision in a different module,
568 576 since a branch may be moved without a change being
569 577 reported. Return None if computed module does not belong to
570 578 rootmodule subtree.
571 579 """
572 580 def findchanges(path, start, stop=None):
573 581 stream = self._getlog([path], start, stop or 1)
574 582 try:
575 583 for entry in stream:
576 584 paths, revnum, author, date, message = entry
577 585 if stop is None and paths:
578 586 # We do not know the latest changed revision,
579 587 # keep the first one with changed paths.
580 588 break
581 589 if revnum <= stop:
582 590 break
583 591
584 592 for p in paths:
585 593 if (not path.startswith(p) or
586 594 not paths[p].copyfrom_path):
587 595 continue
588 596 newpath = paths[p].copyfrom_path + path[len(p):]
589 597 self.ui.debug("branch renamed from %s to %s at %d\n" %
590 598 (path, newpath, revnum))
591 599 path = newpath
592 600 break
593 601 if not paths:
594 602 revnum = None
595 603 return revnum, path
596 604 finally:
597 605 stream.close()
598 606
599 607 if not path.startswith(self.rootmodule):
600 608 # Requests on foreign branches may be forbidden at server level
601 609 self.ui.debug('ignoring foreign branch %r\n' % path)
602 610 return None
603 611
604 612 if stop is None:
605 613 stop = svn.ra.get_latest_revnum(self.ra)
606 614 try:
607 615 prevmodule = self.reparent('')
608 616 dirent = svn.ra.stat(self.ra, path.strip('/'), stop)
609 617 self.reparent(prevmodule)
610 618 except SubversionException:
611 619 dirent = None
612 620 if not dirent:
613 621 raise SvnPathNotFound(_('%s not found up to revision %d')
614 622 % (path, stop))
615 623
616 624 # stat() gives us the previous revision on this line of
617 625 # development, but it might be in *another module*. Fetch the
618 626 # log and detect renames down to the latest revision.
619 627 revnum, realpath = findchanges(path, stop, dirent.created_rev)
620 628 if revnum is None:
621 629 # Tools like svnsync can create empty revision, when
622 630 # synchronizing only a subtree for instance. These empty
623 631 # revisions created_rev still have their original values
624 632 # despite all changes having disappeared and can be
625 633 # returned by ra.stat(), at least when stating the root
626 634 # module. In that case, do not trust created_rev and scan
627 635 # the whole history.
628 636 revnum, realpath = findchanges(path, stop)
629 637 if revnum is None:
630 638 self.ui.debug('ignoring empty branch %r\n' % realpath)
631 639 return None
632 640
633 641 if not realpath.startswith(self.rootmodule):
634 642 self.ui.debug('ignoring foreign branch %r\n' % realpath)
635 643 return None
636 644 return self.revid(revnum, realpath)
637 645
638 646 def reparent(self, module):
639 647 """Reparent the svn transport and return the previous parent."""
640 648 if self.prevmodule == module:
641 649 return module
642 650 svnurl = self.baseurl + quote(module)
643 651 prevmodule = self.prevmodule
644 652 if prevmodule is None:
645 653 prevmodule = ''
646 654 self.ui.debug("reparent to %s\n" % svnurl)
647 655 svn.ra.reparent(self.ra, svnurl)
648 656 self.prevmodule = module
649 657 return prevmodule
650 658
651 659 def expandpaths(self, rev, paths, parents):
652 660 changed, removed = set(), set()
653 661 copies = {}
654 662
655 663 new_module, revnum = revsplit(rev)[1:]
656 664 if new_module != self.module:
657 665 self.module = new_module
658 666 self.reparent(self.module)
659 667
660 668 for i, (path, ent) in enumerate(paths):
661 669 self.ui.progress(_('scanning paths'), i, item=path,
662 670 total=len(paths))
663 671 entrypath = self.getrelpath(path)
664 672
665 673 kind = self._checkpath(entrypath, revnum)
666 674 if kind == svn.core.svn_node_file:
667 675 changed.add(self.recode(entrypath))
668 676 if not ent.copyfrom_path or not parents:
669 677 continue
670 678 # Copy sources not in parent revisions cannot be
671 679 # represented, ignore their origin for now
672 680 pmodule, prevnum = revsplit(parents[0])[1:]
673 681 if ent.copyfrom_rev < prevnum:
674 682 continue
675 683 copyfrom_path = self.getrelpath(ent.copyfrom_path, pmodule)
676 684 if not copyfrom_path:
677 685 continue
678 686 self.ui.debug("copied to %s from %s@%s\n" %
679 687 (entrypath, copyfrom_path, ent.copyfrom_rev))
680 688 copies[self.recode(entrypath)] = self.recode(copyfrom_path)
681 689 elif kind == 0: # gone, but had better be a deleted *file*
682 690 self.ui.debug("gone from %s\n" % ent.copyfrom_rev)
683 691 pmodule, prevnum = revsplit(parents[0])[1:]
684 692 parentpath = pmodule + "/" + entrypath
685 693 fromkind = self._checkpath(entrypath, prevnum, pmodule)
686 694
687 695 if fromkind == svn.core.svn_node_file:
688 696 removed.add(self.recode(entrypath))
689 697 elif fromkind == svn.core.svn_node_dir:
690 698 oroot = parentpath.strip('/')
691 699 nroot = path.strip('/')
692 700 children = self._iterfiles(oroot, prevnum)
693 701 for childpath in children:
694 702 childpath = childpath.replace(oroot, nroot)
695 703 childpath = self.getrelpath("/" + childpath, pmodule)
696 704 if childpath:
697 705 removed.add(self.recode(childpath))
698 706 else:
699 707 self.ui.debug('unknown path in revision %d: %s\n' % \
700 708 (revnum, path))
701 709 elif kind == svn.core.svn_node_dir:
702 710 if ent.action == 'M':
703 711 # If the directory just had a prop change,
704 712 # then we shouldn't need to look for its children.
705 713 continue
706 714 if ent.action == 'R' and parents:
707 715 # If a directory is replacing a file, mark the previous
708 716 # file as deleted
709 717 pmodule, prevnum = revsplit(parents[0])[1:]
710 718 pkind = self._checkpath(entrypath, prevnum, pmodule)
711 719 if pkind == svn.core.svn_node_file:
712 720 removed.add(self.recode(entrypath))
713 721 elif pkind == svn.core.svn_node_dir:
714 722 # We do not know what files were kept or removed,
715 723 # mark them all as changed.
716 724 for childpath in self._iterfiles(pmodule, prevnum):
717 725 childpath = self.getrelpath("/" + childpath)
718 726 if childpath:
719 727 changed.add(self.recode(childpath))
720 728
721 729 for childpath in self._iterfiles(path, revnum):
722 730 childpath = self.getrelpath("/" + childpath)
723 731 if childpath:
724 732 changed.add(self.recode(childpath))
725 733
726 734 # Handle directory copies
727 735 if not ent.copyfrom_path or not parents:
728 736 continue
729 737 # Copy sources not in parent revisions cannot be
730 738 # represented, ignore their origin for now
731 739 pmodule, prevnum = revsplit(parents[0])[1:]
732 740 if ent.copyfrom_rev < prevnum:
733 741 continue
734 742 copyfrompath = self.getrelpath(ent.copyfrom_path, pmodule)
735 743 if not copyfrompath:
736 744 continue
737 745 self.ui.debug("mark %s came from %s:%d\n"
738 746 % (path, copyfrompath, ent.copyfrom_rev))
739 747 children = self._iterfiles(ent.copyfrom_path, ent.copyfrom_rev)
740 748 for childpath in children:
741 749 childpath = self.getrelpath("/" + childpath, pmodule)
742 750 if not childpath:
743 751 continue
744 752 copytopath = path + childpath[len(copyfrompath):]
745 753 copytopath = self.getrelpath(copytopath)
746 754 copies[self.recode(copytopath)] = self.recode(childpath)
747 755
748 756 self.ui.progress(_('scanning paths'), None)
749 757 changed.update(removed)
750 758 return (list(changed), removed, copies)
751 759
752 760 def _fetch_revisions(self, from_revnum, to_revnum):
753 761 if from_revnum < to_revnum:
754 762 from_revnum, to_revnum = to_revnum, from_revnum
755 763
756 764 self.child_cset = None
757 765
758 766 def parselogentry(orig_paths, revnum, author, date, message):
759 767 """Return the parsed commit object or None, and True if
760 768 the revision is a branch root.
761 769 """
762 770 self.ui.debug("parsing revision %d (%d changes)\n" %
763 771 (revnum, len(orig_paths)))
764 772
765 773 branched = False
766 774 rev = self.revid(revnum)
767 775 # branch log might return entries for a parent we already have
768 776
769 777 if rev in self.commits or revnum < to_revnum:
770 778 return None, branched
771 779
772 780 parents = []
773 781 # check whether this revision is the start of a branch or part
774 782 # of a branch renaming
775 783 orig_paths = sorted(orig_paths.iteritems())
776 784 root_paths = [(p, e) for p, e in orig_paths
777 785 if self.module.startswith(p)]
778 786 if root_paths:
779 787 path, ent = root_paths[-1]
780 788 if ent.copyfrom_path:
781 789 branched = True
782 790 newpath = ent.copyfrom_path + self.module[len(path):]
783 791 # ent.copyfrom_rev may not be the actual last revision
784 792 previd = self.latest(newpath, ent.copyfrom_rev)
785 793 if previd is not None:
786 794 prevmodule, prevnum = revsplit(previd)[1:]
787 795 if prevnum >= self.startrev:
788 796 parents = [previd]
789 797 self.ui.note(
790 798 _('found parent of branch %s at %d: %s\n') %
791 799 (self.module, prevnum, prevmodule))
792 800 else:
793 801 self.ui.debug("no copyfrom path, don't know what to do.\n")
794 802
795 803 paths = []
796 804 # filter out unrelated paths
797 805 for path, ent in orig_paths:
798 806 if self.getrelpath(path) is None:
799 807 continue
800 808 paths.append((path, ent))
801 809
802 810 # Example SVN datetime. Includes microseconds.
803 811 # ISO-8601 conformant
804 812 # '2007-01-04T17:35:00.902377Z'
805 813 date = util.parsedate(date[:19] + " UTC", ["%Y-%m-%dT%H:%M:%S"])
806 814 if self.ui.configbool('convert', 'localtimezone'):
807 815 date = makedatetimestamp(date[0])
808 816
809 817 log = message and self.recode(message) or ''
810 818 author = author and self.recode(author) or ''
811 819 try:
812 820 branch = self.module.split("/")[-1]
813 821 if branch == self.trunkname:
814 822 branch = None
815 823 except IndexError:
816 824 branch = None
817 825
818 826 cset = commit(author=author,
819 827 date=util.datestr(date, '%Y-%m-%d %H:%M:%S %1%2'),
820 828 desc=log,
821 829 parents=parents,
822 830 branch=branch,
823 831 rev=rev)
824 832
825 833 self.commits[rev] = cset
826 834 # The parents list is *shared* among self.paths and the
827 835 # commit object. Both will be updated below.
828 836 self.paths[rev] = (paths, cset.parents)
829 837 if self.child_cset and not self.child_cset.parents:
830 838 self.child_cset.parents[:] = [rev]
831 839 self.child_cset = cset
832 840 return cset, branched
833 841
834 842 self.ui.note(_('fetching revision log for "%s" from %d to %d\n') %
835 843 (self.module, from_revnum, to_revnum))
836 844
837 845 try:
838 846 firstcset = None
839 847 lastonbranch = False
840 848 stream = self._getlog([self.module], from_revnum, to_revnum)
841 849 try:
842 850 for entry in stream:
843 851 paths, revnum, author, date, message = entry
844 852 if revnum < self.startrev:
845 853 lastonbranch = True
846 854 break
847 855 if not paths:
848 856 self.ui.debug('revision %d has no entries\n' % revnum)
849 857 # If we ever leave the loop on an empty
850 858 # revision, do not try to get a parent branch
851 859 lastonbranch = lastonbranch or revnum == 0
852 860 continue
853 861 cset, lastonbranch = parselogentry(paths, revnum, author,
854 862 date, message)
855 863 if cset:
856 864 firstcset = cset
857 865 if lastonbranch:
858 866 break
859 867 finally:
860 868 stream.close()
861 869
862 870 if not lastonbranch and firstcset and not firstcset.parents:
863 871 # The first revision of the sequence (the last fetched one)
864 872 # has invalid parents if not a branch root. Find the parent
865 873 # revision now, if any.
866 874 try:
867 875 firstrevnum = self.revnum(firstcset.rev)
868 876 if firstrevnum > 1:
869 877 latest = self.latest(self.module, firstrevnum - 1)
870 878 if latest:
871 879 firstcset.parents.append(latest)
872 880 except SvnPathNotFound:
873 881 pass
874 882 except SubversionException, (inst, num):
875 883 if num == svn.core.SVN_ERR_FS_NO_SUCH_REVISION:
876 884 raise util.Abort(_('svn: branch has no revision %s')
877 885 % to_revnum)
878 886 raise
879 887
880 888 def getfile(self, file, rev):
881 889 # TODO: ra.get_file transmits the whole file instead of diffs.
882 890 if file in self.removed:
883 891 raise IOError
884 892 mode = ''
885 893 try:
886 894 new_module, revnum = revsplit(rev)[1:]
887 895 if self.module != new_module:
888 896 self.module = new_module
889 897 self.reparent(self.module)
890 898 io = StringIO()
891 899 info = svn.ra.get_file(self.ra, file, revnum, io)
892 900 data = io.getvalue()
893 901 # ra.get_file() seems to keep a reference on the input buffer
894 902 # preventing collection. Release it explicitly.
895 903 io.close()
896 904 if isinstance(info, list):
897 905 info = info[-1]
898 906 mode = ("svn:executable" in info) and 'x' or ''
899 907 mode = ("svn:special" in info) and 'l' or mode
900 908 except SubversionException, e:
901 909 notfound = (svn.core.SVN_ERR_FS_NOT_FOUND,
902 910 svn.core.SVN_ERR_RA_DAV_PATH_NOT_FOUND)
903 911 if e.apr_err in notfound: # File not found
904 912 raise IOError
905 913 raise
906 914 if mode == 'l':
907 915 link_prefix = "link "
908 916 if data.startswith(link_prefix):
909 917 data = data[len(link_prefix):]
910 918 return data, mode
911 919
912 920 def _iterfiles(self, path, revnum):
913 921 """Enumerate all files in path at revnum, recursively."""
914 922 path = path.strip('/')
915 923 pool = Pool()
916 924 rpath = '/'.join([self.baseurl, quote(path)]).strip('/')
917 925 entries = svn.client.ls(rpath, optrev(revnum), True, self.ctx, pool)
918 926 if path:
919 927 path += '/'
920 928 return ((path + p) for p, e in entries.iteritems()
921 929 if e.kind == svn.core.svn_node_file)
922 930
923 931 def getrelpath(self, path, module=None):
924 932 if module is None:
925 933 module = self.module
926 934 # Given the repository url of this wc, say
927 935 # "http://server/plone/CMFPlone/branches/Plone-2_0-branch"
928 936 # extract the "entry" portion (a relative path) from what
929 937 # svn log --xml says, i.e.
930 938 # "/CMFPlone/branches/Plone-2_0-branch/tests/PloneTestCase.py"
931 939 # that is to say "tests/PloneTestCase.py"
932 940 if path.startswith(module):
933 941 relative = path.rstrip('/')[len(module):]
934 942 if relative.startswith('/'):
935 943 return relative[1:]
936 944 elif relative == '':
937 945 return relative
938 946
939 947 # The path is outside our tracked tree...
940 948 self.ui.debug('%r is not under %r, ignoring\n' % (path, module))
941 949 return None
942 950
943 951 def _checkpath(self, path, revnum, module=None):
944 952 if module is not None:
945 953 prevmodule = self.reparent('')
946 954 path = module + '/' + path
947 955 try:
948 956 # ra.check_path does not like leading slashes very much, it leads
949 957 # to PROPFIND subversion errors
950 958 return svn.ra.check_path(self.ra, path.strip('/'), revnum)
951 959 finally:
952 960 if module is not None:
953 961 self.reparent(prevmodule)
954 962
955 963 def _getlog(self, paths, start, end, limit=0, discover_changed_paths=True,
956 964 strict_node_history=False):
957 965 # Normalize path names, svn >= 1.5 only wants paths relative to
958 966 # supplied URL
959 967 relpaths = []
960 968 for p in paths:
961 969 if not p.startswith('/'):
962 970 p = self.module + '/' + p
963 971 relpaths.append(p.strip('/'))
964 972 args = [self.baseurl, relpaths, start, end, limit,
965 973 discover_changed_paths, strict_node_history]
966 974 arg = encodeargs(args)
967 975 hgexe = util.hgexecutable()
968 976 cmd = '%s debugsvnlog' % util.shellquote(hgexe)
969 977 stdin, stdout = util.popen2(util.quotecommand(cmd))
970 978 stdin.write(arg)
971 979 try:
972 980 stdin.close()
973 981 except IOError:
974 982 raise util.Abort(_('Mercurial failed to run itself, check'
975 983 ' hg executable is in PATH'))
976 984 return logstream(stdout)
977 985
978 986 pre_revprop_change = '''#!/bin/sh
979 987
980 988 REPOS="$1"
981 989 REV="$2"
982 990 USER="$3"
983 991 PROPNAME="$4"
984 992 ACTION="$5"
985 993
986 994 if [ "$ACTION" = "M" -a "$PROPNAME" = "svn:log" ]; then exit 0; fi
987 995 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-branch" ]; then exit 0; fi
988 996 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-rev" ]; then exit 0; fi
989 997
990 998 echo "Changing prohibited revision property" >&2
991 999 exit 1
992 1000 '''
993 1001
994 1002 class svn_sink(converter_sink, commandline):
995 1003 commit_re = re.compile(r'Committed revision (\d+).', re.M)
996 1004 uuid_re = re.compile(r'Repository UUID:\s*(\S+)', re.M)
997 1005
998 1006 def prerun(self):
999 1007 if self.wc:
1000 1008 os.chdir(self.wc)
1001 1009
1002 1010 def postrun(self):
1003 1011 if self.wc:
1004 1012 os.chdir(self.cwd)
1005 1013
1006 1014 def join(self, name):
1007 1015 return os.path.join(self.wc, '.svn', name)
1008 1016
1009 1017 def revmapfile(self):
1010 1018 return self.join('hg-shamap')
1011 1019
1012 1020 def authorfile(self):
1013 1021 return self.join('hg-authormap')
1014 1022
1015 1023 def __init__(self, ui, path):
1016 1024
1017 1025 converter_sink.__init__(self, ui, path)
1018 1026 commandline.__init__(self, ui, 'svn')
1019 1027 self.delete = []
1020 1028 self.setexec = []
1021 1029 self.delexec = []
1022 1030 self.copies = []
1023 1031 self.wc = None
1024 1032 self.cwd = os.getcwd()
1025 1033
1026 1034 created = False
1027 1035 if os.path.isfile(os.path.join(path, '.svn', 'entries')):
1028 1036 self.wc = os.path.realpath(path)
1029 1037 self.run0('update')
1030 1038 else:
1031 1039 if not re.search(r'^(file|http|https|svn|svn\+ssh)\://', path):
1032 1040 path = os.path.realpath(path)
1033 1041 if os.path.isdir(os.path.dirname(path)):
1034 1042 if not os.path.exists(os.path.join(path, 'db', 'fs-type')):
1035 1043 ui.status(_('initializing svn repository %r\n') %
1036 1044 os.path.basename(path))
1037 1045 commandline(ui, 'svnadmin').run0('create', path)
1038 1046 created = path
1039 1047 path = util.normpath(path)
1040 1048 if not path.startswith('/'):
1041 1049 path = '/' + path
1042 1050 path = 'file://' + path
1043 1051
1044 1052 wcpath = os.path.join(os.getcwd(), os.path.basename(path) + '-wc')
1045 1053 ui.status(_('initializing svn working copy %r\n')
1046 1054 % os.path.basename(wcpath))
1047 1055 self.run0('checkout', path, wcpath)
1048 1056
1049 1057 self.wc = wcpath
1050 1058 self.opener = scmutil.opener(self.wc)
1051 1059 self.wopener = scmutil.opener(self.wc)
1052 1060 self.childmap = mapfile(ui, self.join('hg-childmap'))
1053 1061 self.is_exec = util.checkexec(self.wc) and util.isexec or None
1054 1062
1055 1063 if created:
1056 1064 hook = os.path.join(created, 'hooks', 'pre-revprop-change')
1057 1065 fp = open(hook, 'w')
1058 1066 fp.write(pre_revprop_change)
1059 1067 fp.close()
1060 1068 util.setflags(hook, False, True)
1061 1069
1062 1070 output = self.run0('info')
1063 1071 self.uuid = self.uuid_re.search(output).group(1).strip()
1064 1072
1065 1073 def wjoin(self, *names):
1066 1074 return os.path.join(self.wc, *names)
1067 1075
1068 1076 @propertycache
1069 1077 def manifest(self):
1070 1078 # As of svn 1.7, the "add" command fails when receiving
1071 1079 # already tracked entries, so we have to track and filter them
1072 1080 # ourselves.
1073 1081 m = set()
1074 1082 output = self.run0('ls', recursive=True, xml=True)
1075 1083 doc = xml.dom.minidom.parseString(output)
1076 1084 for e in doc.getElementsByTagName('entry'):
1077 1085 for n in e.childNodes:
1078 1086 if n.nodeType != n.ELEMENT_NODE or n.tagName != 'name':
1079 1087 continue
1080 1088 name = ''.join(c.data for c in n.childNodes
1081 1089 if c.nodeType == c.TEXT_NODE)
1082 1090 # Entries are compared with names coming from
1083 1091 # mercurial, so bytes with undefined encoding. Our
1084 1092 # best bet is to assume they are in local
1085 1093 # encoding. They will be passed to command line calls
1086 1094 # later anyway, so they better be.
1087 1095 m.add(encoding.tolocal(name.encode('utf-8')))
1088 1096 break
1089 1097 return m
1090 1098
1091 1099 def putfile(self, filename, flags, data):
1092 1100 if 'l' in flags:
1093 1101 self.wopener.symlink(data, filename)
1094 1102 else:
1095 1103 try:
1096 1104 if os.path.islink(self.wjoin(filename)):
1097 1105 os.unlink(filename)
1098 1106 except OSError:
1099 1107 pass
1100 1108 self.wopener.write(filename, data)
1101 1109
1102 1110 if self.is_exec:
1103 1111 if self.is_exec(self.wjoin(filename)):
1104 1112 if 'x' not in flags:
1105 1113 self.delexec.append(filename)
1106 1114 else:
1107 1115 if 'x' in flags:
1108 1116 self.setexec.append(filename)
1109 1117 util.setflags(self.wjoin(filename), False, 'x' in flags)
1110 1118
1111 1119 def _copyfile(self, source, dest):
1112 1120 # SVN's copy command pukes if the destination file exists, but
1113 1121 # our copyfile method expects to record a copy that has
1114 1122 # already occurred. Cross the semantic gap.
1115 1123 wdest = self.wjoin(dest)
1116 1124 exists = os.path.lexists(wdest)
1117 1125 if exists:
1118 1126 fd, tempname = tempfile.mkstemp(
1119 1127 prefix='hg-copy-', dir=os.path.dirname(wdest))
1120 1128 os.close(fd)
1121 1129 os.unlink(tempname)
1122 1130 os.rename(wdest, tempname)
1123 1131 try:
1124 1132 self.run0('copy', source, dest)
1125 1133 finally:
1126 1134 self.manifest.add(dest)
1127 1135 if exists:
1128 1136 try:
1129 1137 os.unlink(wdest)
1130 1138 except OSError:
1131 1139 pass
1132 1140 os.rename(tempname, wdest)
1133 1141
1134 1142 def dirs_of(self, files):
1135 1143 dirs = set()
1136 1144 for f in files:
1137 1145 if os.path.isdir(self.wjoin(f)):
1138 1146 dirs.add(f)
1139 1147 for i in strutil.rfindall(f, '/'):
1140 1148 dirs.add(f[:i])
1141 1149 return dirs
1142 1150
1143 1151 def add_dirs(self, files):
1144 1152 add_dirs = [d for d in sorted(self.dirs_of(files))
1145 1153 if d not in self.manifest]
1146 1154 if add_dirs:
1147 1155 self.manifest.update(add_dirs)
1148 1156 self.xargs(add_dirs, 'add', non_recursive=True, quiet=True)
1149 1157 return add_dirs
1150 1158
1151 1159 def add_files(self, files):
1152 1160 files = [f for f in files if f not in self.manifest]
1153 1161 if files:
1154 1162 self.manifest.update(files)
1155 1163 self.xargs(files, 'add', quiet=True)
1156 1164 return files
1157 1165
1158 1166 def tidy_dirs(self, names):
1159 1167 deleted = []
1160 1168 for d in sorted(self.dirs_of(names), reverse=True):
1161 1169 wd = self.wjoin(d)
1162 1170 if os.listdir(wd) == '.svn':
1163 1171 self.run0('delete', d)
1164 1172 self.manifest.remove(d)
1165 1173 deleted.append(d)
1166 1174 return deleted
1167 1175
1168 1176 def addchild(self, parent, child):
1169 1177 self.childmap[parent] = child
1170 1178
1171 1179 def revid(self, rev):
1172 1180 return u"svn:%s@%s" % (self.uuid, rev)
1173 1181
1174 1182 def putcommit(self, files, copies, parents, commit, source, revmap):
1175 1183 for parent in parents:
1176 1184 try:
1177 1185 return self.revid(self.childmap[parent])
1178 1186 except KeyError:
1179 1187 pass
1180 1188
1181 1189 # Apply changes to working copy
1182 1190 for f, v in files:
1183 1191 try:
1184 1192 data, mode = source.getfile(f, v)
1185 1193 except IOError:
1186 1194 self.delete.append(f)
1187 1195 else:
1188 1196 self.putfile(f, mode, data)
1189 1197 if f in copies:
1190 1198 self.copies.append([copies[f], f])
1191 1199 files = [f[0] for f in files]
1192 1200
1193 1201 entries = set(self.delete)
1194 1202 files = frozenset(files)
1195 1203 entries.update(self.add_dirs(files.difference(entries)))
1196 1204 if self.copies:
1197 1205 for s, d in self.copies:
1198 1206 self._copyfile(s, d)
1199 1207 self.copies = []
1200 1208 if self.delete:
1201 1209 self.xargs(self.delete, 'delete')
1202 1210 for f in self.delete:
1203 1211 self.manifest.remove(f)
1204 1212 self.delete = []
1205 1213 entries.update(self.add_files(files.difference(entries)))
1206 1214 entries.update(self.tidy_dirs(entries))
1207 1215 if self.delexec:
1208 1216 self.xargs(self.delexec, 'propdel', 'svn:executable')
1209 1217 self.delexec = []
1210 1218 if self.setexec:
1211 1219 self.xargs(self.setexec, 'propset', 'svn:executable', '*')
1212 1220 self.setexec = []
1213 1221
1214 1222 fd, messagefile = tempfile.mkstemp(prefix='hg-convert-')
1215 1223 fp = os.fdopen(fd, 'w')
1216 1224 fp.write(commit.desc)
1217 1225 fp.close()
1218 1226 try:
1219 1227 output = self.run0('commit',
1220 1228 username=util.shortuser(commit.author),
1221 1229 file=messagefile,
1222 1230 encoding='utf-8')
1223 1231 try:
1224 1232 rev = self.commit_re.search(output).group(1)
1225 1233 except AttributeError:
1226 1234 if not files:
1227 1235 return parents[0]
1228 1236 self.ui.warn(_('unexpected svn output:\n'))
1229 1237 self.ui.warn(output)
1230 1238 raise util.Abort(_('unable to cope with svn output'))
1231 1239 if commit.rev:
1232 1240 self.run('propset', 'hg:convert-rev', commit.rev,
1233 1241 revprop=True, revision=rev)
1234 1242 if commit.branch and commit.branch != 'default':
1235 1243 self.run('propset', 'hg:convert-branch', commit.branch,
1236 1244 revprop=True, revision=rev)
1237 1245 for parent in parents:
1238 1246 self.addchild(parent, rev)
1239 1247 return self.revid(rev)
1240 1248 finally:
1241 1249 os.unlink(messagefile)
1242 1250
1243 1251 def puttags(self, tags):
1244 1252 self.ui.warn(_('writing Subversion tags is not yet implemented\n'))
1245 1253 return None, None
1246 1254
1247 1255 def hascommit(self, rev):
1248 1256 # This is not correct as one can convert to an existing subversion
1249 1257 # repository and childmap would not list all revisions. Too bad.
1250 1258 if rev in self.childmap:
1251 1259 return True
1252 1260 raise util.Abort(_('splice map revision %s not found in subversion '
1253 1261 'child map (revision lookups are not implemented)')
1254 1262 % rev)
@@ -1,210 +1,221 b''
1 1
2 2 $ "$TESTDIR/hghave" svn svn-bindings || exit 80
3 3
4 4 $ cat >> $HGRCPATH <<EOF
5 5 > [extensions]
6 6 > convert =
7 7 > graphlog =
8 8 > [convert]
9 9 > svn.trunk = mytrunk
10 10 > EOF
11 11
12 12 $ svnadmin create svn-repo
13 13 $ SVNREPOPATH=`pwd`/svn-repo
14 14 #if windows
15 15 $ SVNREPOURL=file:///`python -c "import urllib, sys; sys.stdout.write(urllib.quote(sys.argv[1]))" "$SVNREPOPATH"`
16 16 #else
17 17 $ SVNREPOURL=file://`python -c "import urllib, sys; sys.stdout.write(urllib.quote(sys.argv[1]))" "$SVNREPOPATH"`
18 18 #endif
19 $ INVALIDREVISIONID=svn:x2147622-4a9f-4db4-a8d3-13562ff547b2/proj%20B/mytrunk@1
20 $ VALIDREVISIONID=svn:a2147622-4a9f-4db4-a8d3-13562ff547b2/proj%20B/mytrunk/mytrunk@1
19 21
20 22 Now test that it works with trunk/tags layout, but no branches yet.
21 23
22 24 Initial svn import
23 25
24 26 $ mkdir projB
25 27 $ cd projB
26 28 $ mkdir mytrunk
27 29 $ mkdir tags
28 30 $ cd ..
29 31
30 32 $ svn import -m "init projB" projB "$SVNREPOURL/proj%20B" | sort
31 33
32 34 Adding projB/mytrunk (glob)
33 35 Adding projB/tags (glob)
34 36 Committed revision 1.
35 37
36 38 Update svn repository
37 39
38 40 $ svn co "$SVNREPOURL/proj%20B/mytrunk" B
39 41 Checked out revision 1.
40 42 $ cd B
41 43 $ echo hello > 'letter .txt'
42 44 $ svn add 'letter .txt'
43 45 A letter .txt
44 46 $ svn ci -m hello
45 47 Adding letter .txt
46 48 Transmitting file data .
47 49 Committed revision 2.
48 50
49 51 $ "$TESTDIR/svn-safe-append.py" world 'letter .txt'
50 52 $ svn ci -m world
51 53 Sending letter .txt
52 54 Transmitting file data .
53 55 Committed revision 3.
54 56
55 57 $ svn copy -m "tag v0.1" "$SVNREPOURL/proj%20B/mytrunk" "$SVNREPOURL/proj%20B/tags/v0.1"
56 58
57 59 Committed revision 4.
58 60
59 61 $ "$TESTDIR/svn-safe-append.py" 'nice day today!' 'letter .txt'
60 62 $ svn ci -m "nice day"
61 63 Sending letter .txt
62 64 Transmitting file data .
63 65 Committed revision 5.
64 66 $ cd ..
65 67
66 68 Convert to hg once and also test localtimezone option
67 69
68 70 NOTE: This doesn't check all time zones -- it merely determines that
69 71 the configuration option is taking effect.
70 72
71 73 An arbitrary (U.S.) time zone is used here. TZ=US/Hawaii is selected
72 74 since it does not use DST (unlike other U.S. time zones) and is always
73 75 a fixed difference from UTC.
74 76
75 77 $ TZ=US/Hawaii hg convert --config convert.localtimezone=True "$SVNREPOURL/proj%20B" B-hg
76 78 initializing destination B-hg repository
77 79 scanning source...
78 80 sorting...
79 81 converting...
80 82 3 init projB
81 83 2 hello
82 84 1 world
83 85 0 nice day
84 86 updating tags
85 87
86 88 Update svn repository again
87 89
88 90 $ cd B
89 91 $ "$TESTDIR/svn-safe-append.py" "see second letter" 'letter .txt'
90 92 $ echo "nice to meet you" > letter2.txt
91 93 $ svn add letter2.txt
92 94 A letter2.txt
93 95 $ svn ci -m "second letter"
94 96 Sending letter .txt
95 97 Adding letter2.txt
96 98 Transmitting file data ..
97 99 Committed revision 6.
98 100
99 101 $ svn copy -m "tag v0.2" "$SVNREPOURL/proj%20B/mytrunk" "$SVNREPOURL/proj%20B/tags/v0.2"
100 102
101 103 Committed revision 7.
102 104
103 105 $ "$TESTDIR/svn-safe-append.py" "blah-blah-blah" letter2.txt
104 106 $ svn ci -m "work in progress"
105 107 Sending letter2.txt
106 108 Transmitting file data .
107 109 Committed revision 8.
108 110 $ cd ..
109 111
110 112 $ hg convert -s svn "$SVNREPOURL/proj%20B/non-existent-path" dest
111 113 initializing destination dest repository
112 114 abort: no revision found in module /proj B/non-existent-path
113 115 [255]
114 116
115 117 ########################################
116 118
117 119 Test incremental conversion
118 120
119 121 $ TZ=US/Hawaii hg convert --config convert.localtimezone=True "$SVNREPOURL/proj%20B" B-hg
120 122 scanning source...
121 123 sorting...
122 124 converting...
123 125 1 second letter
124 126 0 work in progress
125 127 updating tags
126 128
127 129 $ cd B-hg
128 130 $ hg glog --template '{rev} {desc|firstline} date: {date|date} files: {files}\n'
129 131 o 7 update tags date: * +0000 files: .hgtags (glob)
130 132 |
131 133 o 6 work in progress date: * -1000 files: letter2.txt (glob)
132 134 |
133 135 o 5 second letter date: * -1000 files: letter .txt letter2.txt (glob)
134 136 |
135 137 o 4 update tags date: * +0000 files: .hgtags (glob)
136 138 |
137 139 o 3 nice day date: * -1000 files: letter .txt (glob)
138 140 |
139 141 o 2 world date: * -1000 files: letter .txt (glob)
140 142 |
141 143 o 1 hello date: * -1000 files: letter .txt (glob)
142 144 |
143 145 o 0 init projB date: * -1000 files: (glob)
144 146
145 147 $ hg tags -q
146 148 tip
147 149 v0.2
148 150 v0.1
149 151 $ cd ..
150 152
151 153 Test filemap
152 154 $ echo 'include letter2.txt' > filemap
153 155 $ hg convert --filemap filemap "$SVNREPOURL/proj%20B/mytrunk" fmap
154 156 initializing destination fmap repository
155 157 scanning source...
156 158 sorting...
157 159 converting...
158 160 5 init projB
159 161 4 hello
160 162 3 world
161 163 2 nice day
162 164 1 second letter
163 165 0 work in progress
164 166 $ hg -R fmap branch -q
165 167 default
166 168 $ hg glog -R fmap --template '{rev} {desc|firstline} files: {files}\n'
167 169 o 1 work in progress files: letter2.txt
168 170 |
169 171 o 0 second letter files: letter2.txt
170 172
173 test invalid splicemap1
174
175 $ cat > splicemap <<EOF
176 > $INVALIDREVISIONID $VALIDREVISIONID
177 > EOF
178 $ hg convert --splicemap splicemap "$SVNREPOURL/proj%20B/mytrunk" smap
179 initializing destination smap repository
180 abort: splicemap entry svn:x2147622-4a9f-4db4-a8d3-13562ff547b2/proj%20B/mytrunk@1 is not a valid revision identifier
181 [255]
171 182
172 183 Test stop revision
173 184 $ hg convert --rev 1 "$SVNREPOURL/proj%20B/mytrunk" stoprev
174 185 initializing destination stoprev repository
175 186 scanning source...
176 187 sorting...
177 188 converting...
178 189 0 init projB
179 190 $ hg -R stoprev branch -q
180 191 default
181 192
182 193 Check convert_revision extra-records.
183 194 This is also the only place testing more than one extra field in a revision.
184 195
185 196 $ cd stoprev
186 197 $ hg tip --debug | grep extra
187 198 extra: branch=default
188 199 extra: convert_revision=svn:........-....-....-....-............/proj B/mytrunk@1 (re)
189 200 $ cd ..
190 201
191 202 Test converting empty heads (issue3347)
192 203
193 204 $ svnadmin create svn-empty
194 205 $ svnadmin load -q svn-empty < "$TESTDIR/svn/empty.svndump"
195 206 $ hg --config convert.svn.trunk= convert svn-empty
196 207 assuming destination svn-empty-hg
197 208 initializing destination svn-empty-hg repository
198 209 scanning source...
199 210 sorting...
200 211 converting...
201 212 1 init projA
202 213 0 adddir
203 214 $ hg --config convert.svn.trunk= convert "$SVNREPOURL/../svn-empty/trunk"
204 215 assuming destination trunk-hg
205 216 initializing destination trunk-hg repository
206 217 scanning source...
207 218 sorting...
208 219 converting...
209 220 1 init projA
210 221 0 adddir
General Comments 0
You need to be logged in to leave comments. Login now