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