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