Mercurial > hg
comparison contrib/convert-repo @ 3953:fad134931327
convert-repo: add basic CVS import support
author | Matt Mackall <mpm@selenic.com> |
---|---|
date | Fri, 22 Dec 2006 17:59:34 -0600 |
parents | b58c1681d23b |
children | 9af4b853ed4d |
comparison
equal
deleted
inserted
replaced
3952:32c1653b7dad | 3953:fad134931327 |
---|---|
5 # | 5 # |
6 # To use, run: | 6 # To use, run: |
7 # | 7 # |
8 # convert-repo <source> [<dest> [<mapfile>]] | 8 # convert-repo <source> [<dest> [<mapfile>]] |
9 # | 9 # |
10 # Currently accepted source formats: git | 10 # Currently accepted source formats: git, cvs |
11 # Currently accepted destination formats: hg | 11 # Currently accepted destination formats: hg |
12 # | 12 # |
13 # If destination isn't given, a new Mercurial repo named <src>-hg will | 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 | 14 # be created. If <mapfile> isn't given, it will be put in a default |
15 # location (<dest>/.hg/shamap by default) | 15 # location (<dest>/.hg/shamap by default) |
21 # | 21 # |
22 # If the file doesn't exist, it's automatically created. It's updated | 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 | 23 # on each commit copied, so convert-repo can be interrupted and can |
24 # be run repeatedly to copy new commits. | 24 # be run repeatedly to copy new commits. |
25 | 25 |
26 import sys, os, zlib, sha, time | 26 import sys, os, zlib, sha, time, re, locale |
27 os.environ["HGENCODING"] = "utf-8" | 27 os.environ["HGENCODING"] = "utf-8" |
28 from mercurial import hg, ui, util, fancyopts | 28 from mercurial import hg, ui, util, fancyopts |
29 | 29 |
30 class Abort(Exception): pass | 30 class Abort(Exception): pass |
31 class NoRepo(Exception): pass | |
31 | 32 |
32 quiet = 0 | 33 quiet = 0 |
33 def status(msg): | 34 def status(msg): |
34 if not quiet: sys.stdout.write(str(msg)) | 35 if not quiet: sys.stdout.write(str(msg)) |
35 | 36 |
45 except: | 46 except: |
46 try: | 47 try: |
47 return s.decode("latin-1").encode("utf-8") | 48 return s.decode("latin-1").encode("utf-8") |
48 except: | 49 except: |
49 return s.decode("utf-8", "replace").encode("utf-8") | 50 return s.decode("utf-8", "replace").encode("utf-8") |
51 | |
52 # CVS conversion code inspired by hg-cvs-import and git-cvsimport | |
53 class convert_cvs: | |
54 def __init__(self, path): | |
55 self.path = path | |
56 cvs = os.path.join(path, "CVS") | |
57 if not os.path.exists(cvs): | |
58 raise NoRepo("couldn't open CVS repo %s" % path) | |
59 | |
60 self.changeset = {} | |
61 self.tags = {} | |
62 self.lastbranch = {} | |
63 self.parent = {} | |
64 self.socket = None | |
65 self.cvsroot = file(os.path.join(cvs, "Root")).read()[:-1] | |
66 self.cvsrepo = file(os.path.join(cvs, "Repository")).read()[:-1] | |
67 self.encoding = locale.getpreferredencoding() | |
68 self._parse() | |
69 self._connect() | |
70 | |
71 def _parse(self): | |
72 if self.changeset: | |
73 return | |
74 | |
75 d = os.getcwd() | |
76 try: | |
77 os.chdir(self.path) | |
78 id = None | |
79 state = 0 | |
80 for l in os.popen("cvsps -A"): | |
81 if state == 0: # header | |
82 if l.startswith("PatchSet"): | |
83 id = l[9:-2] | |
84 elif l.startswith("Date"): | |
85 date = util.parsedate(l[6:-1], ["%Y/%m/%d %H:%M:%S"]) | |
86 date = util.datestr(date) | |
87 elif l.startswith("Branch"): | |
88 branch = l[8:-1] | |
89 self.parent[id] = self.lastbranch.get(branch,'bad') | |
90 self.lastbranch[branch] = id | |
91 elif l.startswith("Ancestor branch"): | |
92 ancestor = l[17:-1] | |
93 self.parent[id] = self.lastbranch[ancestor] | |
94 elif l.startswith("Author"): | |
95 author = self.recode(l[8:-1]) | |
96 elif l.startswith("Tag: "): | |
97 t = l[5:-1] | |
98 if t != "(none) ": | |
99 self.tags[t] = id | |
100 elif l.startswith("Log:"): | |
101 state = 1 | |
102 log = "" | |
103 elif state == 1: # log | |
104 if l == "Members: \n": | |
105 files = {} | |
106 log = self.recode(log[:-1]) | |
107 if log.isspace(): | |
108 log = "*** empty log message ***\n" | |
109 state = 2 | |
110 else: | |
111 log += l | |
112 elif state == 2: | |
113 if l == "\n": # | |
114 state = 0 | |
115 self.changeset[id] = (date, author, log, files) | |
116 else: | |
117 file,rev = l[1:-2].rsplit(':',1) | |
118 rev = rev.split("->")[1] | |
119 files[file] = rev | |
120 | |
121 self.heads = self.lastbranch.values() | |
122 finally: | |
123 os.chdir(d) | |
124 | |
125 def _connect(self): | |
126 root = self.cvsroot | |
127 local = False | |
128 user, host = None, None | |
129 cmd = ['cvs', 'server'] | |
130 | |
131 status("connecting to %s\n" % root) | |
132 | |
133 # only non-pserver for now | |
134 if root.startswith(":pserver"): | |
135 abort("can't handle pserver mode yet: %s\n" % root) | |
136 | |
137 if root.startswith(":local:"): | |
138 local = True | |
139 root = root[7:] | |
140 else: | |
141 # :ext:user@host/home/user/path/to/cvsroot | |
142 if root.startswith(":ext:"): | |
143 root = root[5:] | |
144 m = re.match(r'(?:([^@:/]+)@)?([^:/]+):?(.*)', root) | |
145 if not m: | |
146 local = True | |
147 else: | |
148 local = False | |
149 user, host, root = m.group(1), m.group(2), m.group(3) | |
150 | |
151 if not local: | |
152 rsh = os.environ.get("CVS_RSH" or "rsh") | |
153 if user: | |
154 cmd = [rsh, '-l', user, host] + cmd | |
155 else: | |
156 cmd = [rsh, host] + cmd | |
157 | |
158 self.writep, self.readp = os.popen2(cmd) | |
159 self.realroot = root | |
160 | |
161 self.writep.write("Root %s\n" % root) | |
162 self.writep.write("Valid-responses ok error Valid-requests Mode" | |
163 " M Mbinary E Checked-in Created Updated" | |
164 " Merged Removed\n") | |
165 self.writep.write("valid-requests\n") | |
166 self.writep.flush() | |
167 r = self.readp.readline() | |
168 if not r.startswith("Valid-requests"): | |
169 abort("server sucks\n") | |
170 if "UseUnchanged" in r: | |
171 self.writep.write("UseUnchanged\n") | |
172 self.writep.flush() | |
173 r = self.readp.readline() | |
174 | |
175 def getheads(self): | |
176 return self.heads | |
177 | |
178 def getfile(self, name, rev): | |
179 if rev.endswith("(DEAD)"): | |
180 raise IOError | |
181 | |
182 args = ("-N -P -kk -r %s --" % rev).split() | |
183 args.append(os.path.join(self.cvsrepo, name)) | |
184 for x in args: | |
185 self.writep.write("Argument %s\n" % x) | |
186 self.writep.write("Directory .\n%s\nco\n" % self.realroot) | |
187 self.writep.flush() | |
188 | |
189 data = "" | |
190 while 1: | |
191 line = self.readp.readline() | |
192 if line.startswith("Created ") or line.startswith("Updated "): | |
193 self.readp.readline() # path | |
194 self.readp.readline() # entries | |
195 mode = self.readp.readline()[:-1] | |
196 count = int(self.readp.readline()[:-1]) | |
197 data = self.readp.read(count) | |
198 elif line.startswith(" "): | |
199 data += line[1:] | |
200 elif line.startswith("M "): | |
201 pass | |
202 elif line.startswith("Mbinary "): | |
203 count = int(self.readp.readline()[:-1]) | |
204 data = self.readp.read(count) | |
205 else: | |
206 if line == "ok\n": | |
207 return data | |
208 elif line.startswith("E "): | |
209 warn("cvs server: %s\n" % line[2:]) | |
210 elif line.startswith("Remove"): | |
211 l = self.readp.readline() | |
212 l = self.readp.readline() | |
213 if l != "ok\n": | |
214 abort("unknown CVS response: %s\n" % l) | |
215 else: | |
216 abort("unknown CVS response: %s\n" % line) | |
217 | |
218 def getchanges(self, rev): | |
219 files = self.changeset[rev][3] | |
220 cl = [ (f, r, 0) for f,r in files.items() ] | |
221 cl.sort() | |
222 return cl | |
223 | |
224 def recode(self, text): | |
225 return text.decode(self.encoding, "replace").encode("utf-8") | |
226 | |
227 def getcommit(self, rev): | |
228 cs = self.changeset[rev] | |
229 parents = [self.parent[rev]] | |
230 if rev == "1": | |
231 parents = [] | |
232 return (parents, cs[1], cs[0], cs[2]) | |
233 | |
234 def gettags(self): | |
235 return self.tags | |
50 | 236 |
51 class convert_git: | 237 class convert_git: |
52 def __init__(self, path): | 238 def __init__(self, path): |
53 if os.path.isdir(path + "/.git"): | 239 if os.path.isdir(path + "/.git"): |
54 path += "/.git" | 240 path += "/.git" |
55 self.path = path | 241 self.path = path |
56 if not os.path.exists(path + "/HEAD"): | 242 if not os.path.exists(path + "/HEAD"): |
57 raise TypeError("couldn't open GIT repo %s" % path) | 243 raise NoRepo("couldn't open GIT repo %s" % path) |
58 | 244 |
59 def getheads(self): | 245 def getheads(self): |
60 fh = os.popen("GIT_DIR=%s git-rev-parse --verify HEAD" % self.path) | 246 fh = os.popen("GIT_DIR=%s git-rev-parse --verify HEAD" % self.path) |
61 return [fh.read()[:-1]] | 247 return [fh.read()[:-1]] |
62 | 248 |
127 self.path = path | 313 self.path = path |
128 u = ui.ui() | 314 u = ui.ui() |
129 try: | 315 try: |
130 self.repo = hg.repository(u, path) | 316 self.repo = hg.repository(u, path) |
131 except: | 317 except: |
132 raise TypeError("could open hg repo %s" % path) | 318 raise NoRepo("could open hg repo %s" % path) |
133 | 319 |
134 def mapfile(self): | 320 def mapfile(self): |
135 return os.path.join(self.path, ".hg", "shamap") | 321 return os.path.join(self.path, ".hg", "shamap") |
136 | 322 |
137 def getheads(self): | 323 def getheads(self): |
166 p2 = parents.pop(0) | 352 p2 = parents.pop(0) |
167 | 353 |
168 while parents: | 354 while parents: |
169 p1 = p2 | 355 p1 = p2 |
170 p2 = parents.pop(0) | 356 p2 = parents.pop(0) |
171 self.repo.rawcommit(files, text, author, dest, | 357 a = self.repo.rawcommit(files, text, author, dest, |
172 hg.bin(p1), hg.bin(p2)) | 358 hg.bin(p1), hg.bin(p2)) |
173 text = "(octopus merge fixup)\n" | 359 text = "(octopus merge fixup)\n" |
174 p2 = hg.hex(self.repo.changelog.tip()) | 360 p2 = hg.hex(self.repo.changelog.tip()) |
175 | 361 |
176 return p2 | 362 return p2 |
177 | 363 |
200 date = "%s 0" % int(time.mktime(time.gmtime())) | 386 date = "%s 0" % int(time.mktime(time.gmtime())) |
201 self.repo.rawcommit([".hgtags"], "update tags", "convert-repo", | 387 self.repo.rawcommit([".hgtags"], "update tags", "convert-repo", |
202 date, self.repo.changelog.tip(), hg.nullid) | 388 date, self.repo.changelog.tip(), hg.nullid) |
203 return hg.hex(self.repo.changelog.tip()) | 389 return hg.hex(self.repo.changelog.tip()) |
204 | 390 |
205 converters = [convert_git, convert_mercurial] | 391 converters = [convert_cvs, convert_git, convert_mercurial] |
206 | 392 |
207 def converter(path): | 393 def converter(path): |
208 if not os.path.isdir(path): | 394 if not os.path.isdir(path): |
209 abort("%s: not a directory\n" % path) | 395 abort("%s: not a directory\n" % path) |
210 for c in converters: | 396 for c in converters: |
211 try: | 397 try: |
212 return c(path) | 398 return c(path) |
213 except TypeError: | 399 except NoRepo: |
214 pass | 400 pass |
215 abort("%s: unknown repository type\n" % path) | 401 abort("%s: unknown repository type\n" % path) |
216 | 402 |
217 class convert: | 403 class convert: |
218 def __init__(self, source, dest, mapfile): | 404 def __init__(self, source, dest, mapfile): |
317 c = None | 503 c = None |
318 | 504 |
319 status("converting...\n") | 505 status("converting...\n") |
320 for c in t: | 506 for c in t: |
321 num -= 1 | 507 num -= 1 |
322 desc = self.commitcache[c][3].splitlines()[0] | 508 desc = self.commitcache[c][3] |
509 if "\n" in desc: | |
510 desc = desc.splitlines()[0] | |
323 status("%d %s\n" % (num, desc)) | 511 status("%d %s\n" % (num, desc)) |
324 self.copy(c) | 512 self.copy(c) |
325 | 513 |
326 tags = self.source.gettags() | 514 tags = self.source.gettags() |
327 ctags = {} | 515 ctags = {} |