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