comparison hgext/convert/__init__.py @ 4513:ac2fe196ac9b

Turns convert.py into a real extension
author Edouard Gomez <ed.gomez@free.fr>
date Fri, 25 May 2007 00:56:48 +0200
parents 91709ba3cc88
children 86a66cce9566
comparison
equal deleted inserted replaced
4512:91709ba3cc88 4513:ac2fe196ac9b
1 #!/usr/bin/env python 1 # convert.py Foreign SCM converter
2 # 2 #
3 # This is a generalized framework for converting between SCM 3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
4 # repository formats.
5 # 4 #
6 # To use, run: 5 # This software may be used and distributed according to the terms
7 # 6 # of the GNU General Public License, incorporated herein by reference.
8 # convert-repo <source> [<dest> [<mapfile>]]
9 #
10 # Currently accepted source formats: git, cvs
11 # Currently accepted destination formats: hg
12 #
13 # If destination isn't given, a new Mercurial repo named <src>-hg will
14 # be created. If <mapfile> isn't given, it will be put in a default
15 # location (<dest>/.hg/shamap by default)
16 #
17 # The <mapfile> is a simple text file that maps each source commit ID to
18 # the destination ID for that revision, like so:
19 #
20 # <source ID> <destination ID>
21 #
22 # If the file doesn't exist, it's automatically created. It's updated
23 # on each commit copied, so convert-repo can be interrupted and can
24 # be run repeatedly to copy new commits.
25 7
26 import sys, os, zlib, sha, time, re, locale, socket 8 import sys, os, zlib, sha, time, re, locale, socket
27 os.environ["HGENCODING"] = "utf-8" 9 from mercurial import hg, ui, util, commands
28 from mercurial import hg, ui, util, fancyopts 10
29 11 commands.norepo += " convert"
30 class Abort(Exception): pass 12
31 class NoRepo(Exception): pass 13 class NoRepo(Exception): pass
32 14
33 class commit(object): 15 class commit(object):
34 def __init__(self, **parts): 16 def __init__(self, **parts):
35 for x in "author date desc parents".split(): 17 for x in "author date desc parents".split():
36 if not x in parts: 18 if not x in parts:
37 abort("commit missing field %s\n" % x) 19 raise util.Abort("commit missing field %s\n" % x)
38 self.__dict__.update(parts) 20 self.__dict__.update(parts)
39
40 quiet = 0
41 def status(msg):
42 if not quiet: sys.stdout.write(str(msg))
43
44 def warn(msg):
45 sys.stderr.write(str(msg))
46
47 def abort(msg):
48 raise Abort(msg)
49 21
50 def recode(s): 22 def recode(s):
51 try: 23 try:
52 return s.decode("utf-8").encode("utf-8") 24 return s.decode("utf-8").encode("utf-8")
53 except: 25 except:
57 return s.decode("utf-8", "replace").encode("utf-8") 29 return s.decode("utf-8", "replace").encode("utf-8")
58 30
59 class converter_source(object): 31 class converter_source(object):
60 """Conversion source interface""" 32 """Conversion source interface"""
61 33
62 def __init__(self, path): 34 def __init__(self, ui, path):
63 """Initialize conversion source (or raise NoRepo("message") 35 """Initialize conversion source (or raise NoRepo("message")
64 exception if path is not a valid repository)""" 36 exception if path is not a valid repository)"""
65 raise NotImplementedError() 37 raise NotImplementedError()
66 38
67 def getheads(self): 39 def getheads(self):
92 raise NotImplementedError() 64 raise NotImplementedError()
93 65
94 class converter_sink(object): 66 class converter_sink(object):
95 """Conversion sink (target) interface""" 67 """Conversion sink (target) interface"""
96 68
97 def __init__(self, path): 69 def __init__(self, ui, path):
98 """Initialize conversion sink (or raise NoRepo("message") 70 """Initialize conversion sink (or raise NoRepo("message")
99 exception if path is not a valid repository)""" 71 exception if path is not a valid repository)"""
100 raise NotImplementedError() 72 raise NotImplementedError()
101 73
102 def getheads(self): 74 def getheads(self):
137 raise NotImplementedError() 109 raise NotImplementedError()
138 110
139 111
140 # CVS conversion code inspired by hg-cvs-import and git-cvsimport 112 # CVS conversion code inspired by hg-cvs-import and git-cvsimport
141 class convert_cvs(converter_source): 113 class convert_cvs(converter_source):
142 def __init__(self, path): 114 def __init__(self, ui, path):
143 self.path = path 115 self.path = path
116 self.ui = ui
144 cvs = os.path.join(path, "CVS") 117 cvs = os.path.join(path, "CVS")
145 if not os.path.exists(cvs): 118 if not os.path.exists(cvs):
146 raise NoRepo("couldn't open CVS repo %s" % path) 119 raise NoRepo("couldn't open CVS repo %s" % path)
147 120
148 self.changeset = {} 121 self.changeset = {}
221 root = self.cvsroot 194 root = self.cvsroot
222 conntype = None 195 conntype = None
223 user, host = None, None 196 user, host = None, None
224 cmd = ['cvs', 'server'] 197 cmd = ['cvs', 'server']
225 198
226 status("connecting to %s\n" % root) 199 self.ui.status("connecting to %s\n" % root)
227 200
228 if root.startswith(":pserver:"): 201 if root.startswith(":pserver:"):
229 root = root[9:] 202 root = root[9:]
230 m = re.match(r'(?:(.*?)(?::(.*?))?@)?([^:\/]*)(?::(\d*))?(.*)', root) 203 m = re.match(r'(?:(.*?)(?::(.*?))?@)?([^:\/]*)(?::(\d*))?(.*)', root)
231 if m: 204 if m:
294 " Merged Removed\n") 267 " Merged Removed\n")
295 self.writep.write("valid-requests\n") 268 self.writep.write("valid-requests\n")
296 self.writep.flush() 269 self.writep.flush()
297 r = self.readp.readline() 270 r = self.readp.readline()
298 if not r.startswith("Valid-requests"): 271 if not r.startswith("Valid-requests"):
299 abort("server sucks\n") 272 raise util.Abort("server sucks\n")
300 if "UseUnchanged" in r: 273 if "UseUnchanged" in r:
301 self.writep.write("UseUnchanged\n") 274 self.writep.write("UseUnchanged\n")
302 self.writep.flush() 275 self.writep.flush()
303 r = self.readp.readline() 276 r = self.readp.readline()
304 277
334 data = self.readp.read(count) 307 data = self.readp.read(count)
335 else: 308 else:
336 if line == "ok\n": 309 if line == "ok\n":
337 return (data, "x" in mode and "x" or "") 310 return (data, "x" in mode and "x" or "")
338 elif line.startswith("E "): 311 elif line.startswith("E "):
339 warn("cvs server: %s\n" % line[2:]) 312 self.ui.warn("cvs server: %s\n" % line[2:])
340 elif line.startswith("Remove"): 313 elif line.startswith("Remove"):
341 l = self.readp.readline() 314 l = self.readp.readline()
342 l = self.readp.readline() 315 l = self.readp.readline()
343 if l != "ok\n": 316 if l != "ok\n":
344 abort("unknown CVS response: %s\n" % l) 317 raise util.Abort("unknown CVS response: %s\n" % l)
345 else: 318 else:
346 abort("unknown CVS response: %s\n" % line) 319 raise util.Abort("unknown CVS response: %s\n" % line)
347 320
348 def getfile(self, file, rev): 321 def getfile(self, file, rev):
349 data, mode = self._getfile(file, rev) 322 data, mode = self._getfile(file, rev)
350 self.modecache[(file, rev)] = mode 323 self.modecache[(file, rev)] = mode
351 return data 324 return data
368 341
369 def gettags(self): 342 def gettags(self):
370 return self.tags 343 return self.tags
371 344
372 class convert_git(converter_source): 345 class convert_git(converter_source):
373 def __init__(self, path): 346 def __init__(self, ui, path):
374 if os.path.isdir(path + "/.git"): 347 if os.path.isdir(path + "/.git"):
375 path += "/.git" 348 path += "/.git"
376 self.path = path 349 self.path = path
350 self.ui = ui
377 if not os.path.exists(path + "/objects"): 351 if not os.path.exists(path + "/objects"):
378 raise NoRepo("couldn't open GIT repo %s" % path) 352 raise NoRepo("couldn't open GIT repo %s" % path)
379 353
380 def getheads(self): 354 def getheads(self):
381 fh = os.popen("GIT_DIR=%s git-rev-parse --verify HEAD" % self.path) 355 fh = os.popen("GIT_DIR=%s git-rev-parse --verify HEAD" % self.path)
454 tags[tag] = node 428 tags[tag] = node
455 429
456 return tags 430 return tags
457 431
458 class convert_mercurial(converter_sink): 432 class convert_mercurial(converter_sink):
459 def __init__(self, path): 433 def __init__(self, ui, path):
460 self.path = path 434 self.path = path
461 u = ui.ui() 435 self.ui = ui
462 try: 436 try:
463 self.repo = hg.repository(u, path) 437 self.repo = hg.repository(self.ui, path)
464 except: 438 except:
465 raise NoRepo("could open hg repo %s" % path) 439 raise NoRepo("could open hg repo %s" % path)
466 440
467 def mapfile(self): 441 def mapfile(self):
468 return os.path.join(self.path, ".hg", "shamap") 442 return os.path.join(self.path, ".hg", "shamap")
528 newlines.append("%s %s\n" % (tags[tag], tag)) 502 newlines.append("%s %s\n" % (tags[tag], tag))
529 503
530 newlines.sort() 504 newlines.sort()
531 505
532 if newlines != oldlines: 506 if newlines != oldlines:
533 status("updating tags\n") 507 self.ui.status("updating tags\n")
534 f = self.repo.wfile(".hgtags", "w") 508 f = self.repo.wfile(".hgtags", "w")
535 f.write("".join(newlines)) 509 f.write("".join(newlines))
536 f.close() 510 f.close()
537 if not oldlines: self.repo.add([".hgtags"]) 511 if not oldlines: self.repo.add([".hgtags"])
538 date = "%s 0" % int(time.mktime(time.gmtime())) 512 date = "%s 0" % int(time.mktime(time.gmtime()))
540 date, self.repo.changelog.tip(), hg.nullid) 514 date, self.repo.changelog.tip(), hg.nullid)
541 return hg.hex(self.repo.changelog.tip()) 515 return hg.hex(self.repo.changelog.tip())
542 516
543 converters = [convert_cvs, convert_git, convert_mercurial] 517 converters = [convert_cvs, convert_git, convert_mercurial]
544 518
545 def converter(path): 519 def converter(ui, path):
546 if not os.path.isdir(path): 520 if not os.path.isdir(path):
547 abort("%s: not a directory\n" % path) 521 raise util.Abort("%s: not a directory\n" % path)
548 for c in converters: 522 for c in converters:
549 try: 523 try:
550 return c(path) 524 return c(ui, path)
551 except NoRepo: 525 except NoRepo:
552 pass 526 pass
553 abort("%s: unknown repository type\n" % path) 527 raise util.Abort("%s: unknown repository type\n" % path)
554 528
555 class convert(object): 529 class convert(object):
556 def __init__(self, source, dest, mapfile, opts): 530 def __init__(self, ui, source, dest, mapfile, opts):
557 531
558 self.source = source 532 self.source = source
559 self.dest = dest 533 self.dest = dest
534 self.ui = ui
560 self.mapfile = mapfile 535 self.mapfile = mapfile
561 self.opts = opts 536 self.opts = opts
562 self.commitcache = {} 537 self.commitcache = {}
563 538
564 self.map = {} 539 self.map = {}
625 s.append(n) 600 s.append(n)
626 if n in children: 601 if n in children:
627 for c in children[n]: 602 for c in children[n]:
628 visit.insert(0, c) 603 visit.insert(0, c)
629 604
630 if opts.get('datesort'): 605 if self.opts.get('datesort'):
631 depth = {} 606 depth = {}
632 for n in s: 607 for n in s:
633 depth[n] = 0 608 depth[n] = 0
634 pl = [p for p in self.commitcache[n].parents if p not in self.map] 609 pl = [p for p in self.commitcache[n].parents if p not in self.map]
635 if pl: 610 if pl:
658 f = [f for f,v in files] 633 f = [f for f,v in files]
659 self.map[rev] = self.dest.putcommit(f, r, c) 634 self.map[rev] = self.dest.putcommit(f, r, c)
660 file(self.mapfile, "a").write("%s %s\n" % (rev, self.map[rev])) 635 file(self.mapfile, "a").write("%s %s\n" % (rev, self.map[rev]))
661 636
662 def convert(self): 637 def convert(self):
663 status("scanning source...\n") 638 self.ui.status("scanning source...\n")
664 heads = self.source.getheads() 639 heads = self.source.getheads()
665 parents = self.walktree(heads) 640 parents = self.walktree(heads)
666 status("sorting...\n") 641 self.ui.status("sorting...\n")
667 t = self.toposort(parents) 642 t = self.toposort(parents)
668 num = len(t) 643 num = len(t)
669 c = None 644 c = None
670 645
671 status("converting...\n") 646 self.ui.status("converting...\n")
672 for c in t: 647 for c in t:
673 num -= 1 648 num -= 1
674 desc = self.commitcache[c].desc 649 desc = self.commitcache[c].desc
675 if "\n" in desc: 650 if "\n" in desc:
676 desc = desc.splitlines()[0] 651 desc = desc.splitlines()[0]
677 status("%d %s\n" % (num, desc)) 652 self.ui.status("%d %s\n" % (num, desc))
678 self.copy(c) 653 self.copy(c)
679 654
680 tags = self.source.gettags() 655 tags = self.source.gettags()
681 ctags = {} 656 ctags = {}
682 for k in tags: 657 for k in tags:
689 # write another hash correspondence to override the previous 664 # write another hash correspondence to override the previous
690 # one so we don't end up with extra tag heads 665 # one so we don't end up with extra tag heads
691 if nrev: 666 if nrev:
692 file(self.mapfile, "a").write("%s %s\n" % (c, nrev)) 667 file(self.mapfile, "a").write("%s %s\n" % (c, nrev))
693 668
694 def command(src, dest=None, mapfile=None, **opts): 669 def _convert(ui, src, dest=None, mapfile=None, **opts):
695 srcc = converter(src) 670 '''Convert a foreign SCM repository to a Mercurial one.
671
672 Accepted source formats:
673 - GIT
674 - CVS
675
676 Accepted destination formats:
677 - Mercurial
678
679 If destination isn't given, a new Mercurial repo named <src>-hg will
680 be created. If <mapfile> isn't given, it will be put in a default
681 location (<dest>/.hg/shamap by default)
682
683 The <mapfile> is a simple text file that maps each source commit ID to
684 the destination ID for that revision, like so:
685
686 <source ID> <destination ID>
687
688 If the file doesn't exist, it's automatically created. It's updated
689 on each commit copied, so convert-repo can be interrupted and can
690 be run repeatedly to copy new commits.
691 '''
692
693 srcc = converter(ui, src)
696 if not hasattr(srcc, "getcommit"): 694 if not hasattr(srcc, "getcommit"):
697 abort("%s: can't read from this repo type\n" % src) 695 raise util.Abort("%s: can't read from this repo type\n" % src)
698 696
699 if not dest: 697 if not dest:
700 dest = src + "-hg" 698 dest = src + "-hg"
701 status("assuming destination %s\n" % dest) 699 ui.status("assuming destination %s\n" % dest)
702 if not os.path.isdir(dest): 700 if not os.path.isdir(dest):
703 status("creating repository %s\n" % dest) 701 ui.status("creating repository %s\n" % dest)
704 os.system("hg init " + dest) 702 os.system("hg init " + dest)
705 destc = converter(dest) 703 destc = converter(ui, dest)
706 if not hasattr(destc, "putcommit"): 704 if not hasattr(destc, "putcommit"):
707 abort("%s: can't write to this repo type\n" % src) 705 raise util.Abort("%s: can't write to this repo type\n" % src)
708 706
709 if not mapfile: 707 if not mapfile:
710 try: 708 try:
711 mapfile = destc.mapfile() 709 mapfile = destc.mapfile()
712 except: 710 except:
713 mapfile = os.path.join(destc, "map") 711 mapfile = os.path.join(destc, "map")
714 712
715 c = convert(srcc, destc, mapfile, opts) 713 c = convert(ui, srcc, destc, mapfile, opts)
716 c.convert() 714 c.convert()
717 715
718 options = [('q', 'quiet', None, 'suppress output'), 716 cmdtable = {
719 ('', 'datesort', None, 'try to sort changesets by date')] 717 "convert": (_convert,
720 opts = {} 718 [('', 'datesort', None, 'try to sort changesets by date')],
721 args = fancyopts.fancyopts(sys.argv[1:], options, opts) 719 'hg convert [OPTIONS] <src> [dst [map]]'),
722 720 }
723 if opts['quiet']:
724 quiet = 1
725
726 try:
727 command(*args, **opts)
728 except Abort, inst:
729 warn(inst)
730 except KeyboardInterrupt:
731 status("interrupted\n")