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