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