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