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