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 = {} |
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 |
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 = {} |
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") |
|