##// END OF EJS Templates
document conversion interface
Daniel Holth -
r4447:1b75e0ef default
parent child Browse files
Show More
@@ -1,650 +1,731 b''
1 #!/usr/bin/env python
1 #!/usr/bin/env python
2 #
2 #
3 # This is a generalized framework for converting between SCM
3 # This is a generalized framework for converting between SCM
4 # repository formats.
4 # repository formats.
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, cvs
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)
16 #
16 #
17 # The <mapfile> is a simple text file that maps each source commit ID to
17 # The <mapfile> is a simple text file that maps each source commit ID to
18 # the destination ID for that revision, like so:
18 # the destination ID for that revision, like so:
19 #
19 #
20 # <source ID> <destination ID>
20 # <source ID> <destination ID>
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, re, locale, socket
26 import sys, os, zlib, sha, time, re, locale, socket
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 class NoRepo(Exception): pass
32
32
33 class commit:
33 class commit:
34 def __init__(self, **parts):
34 def __init__(self, **parts):
35 for x in "author date desc parents".split():
35 for x in "author date desc parents".split():
36 if not x in parts:
36 if not x in parts:
37 abort("commit missing field %s\n" % x)
37 abort("commit missing field %s\n" % x)
38 self.__dict__.update(parts)
38 self.__dict__.update(parts)
39
39
40 quiet = 0
40 quiet = 0
41 def status(msg):
41 def status(msg):
42 if not quiet: sys.stdout.write(str(msg))
42 if not quiet: sys.stdout.write(str(msg))
43
43
44 def warn(msg):
44 def warn(msg):
45 sys.stderr.write(str(msg))
45 sys.stderr.write(str(msg))
46
46
47 def abort(msg):
47 def abort(msg):
48 raise Abort(msg)
48 raise Abort(msg)
49
49
50 def recode(s):
50 def recode(s):
51 try:
51 try:
52 return s.decode("utf-8").encode("utf-8")
52 return s.decode("utf-8").encode("utf-8")
53 except:
53 except:
54 try:
54 try:
55 return s.decode("latin-1").encode("utf-8")
55 return s.decode("latin-1").encode("utf-8")
56 except:
56 except:
57 return s.decode("utf-8", "replace").encode("utf-8")
57 return s.decode("utf-8", "replace").encode("utf-8")
58
58
59 class converter_source:
60 """Conversion source interface"""
61
62 def __init__(self, path):
63 """Initialize conversion source (or raise NoRepo("message")
64 exception if path is not a valid repository)"""
65 raise NotImplementedError()
66
67 def getheads(self):
68 """Return a list of this repository's heads"""
69 raise NotImplementedError()
70
71 def getfile(self, name, rev):
72 """Return file contents as a string"""
73 raise NotImplementedError()
74
75 def getmode(self, name, rev):
76 """Return file mode, eg. '', 'x', or 'l'"""
77 raise NotImplementedError()
78
79 def getchanges(self, version):
80 """Return sorted list of (filename, id) tuples for all files changed in rev.
81
82 id just tells us which revision to return in getfile(), e.g. in
83 git it's an object hash."""
84 raise NotImplementedError()
85
86 def getcommit(self, version):
87 """Return the commit object for version"""
88 raise NotImplementedError()
89
90 def gettags(self):
91 """Return the tags as a dictionary of name: revision"""
92 raise NotImplementedError()
93
94 class converter_sink:
95 """Conversion sink (target) interface"""
96
97 def __init__(self, path):
98 """Initialize conversion sink (or raise NoRepo("message")
99 exception if path is not a valid repository)"""
100 raise NotImplementedError()
101
102 def getheads(self):
103 """Return a list of this repository's heads"""
104 raise NotImplementedError()
105
106 def mapfile(self):
107 """Path to a file that will contain lines
108 source_rev_id sink_rev_id
109 mapping equivalent revision identifiers for each system."""
110 raise NotImplementedError()
111
112 def putfile(self, f, e, data):
113 """Put file for next putcommit().
114 f: path to file
115 e: '', 'x', or 'l' (regular file, executable, or symlink)
116 data: file contents"""
117 raise NotImplementedError()
118
119 def delfile(self, f):
120 """Delete file for next putcommit().
121 f: path to file"""
122 raise NotImplementedError()
123
124 def putcommit(self, files, parents, commit):
125 """Create a revision with all changed files listed in 'files'
126 and having listed parents. 'commit' is a commit object containing
127 at a minimum the author, date, and message for this changeset.
128 Called after putfile() and delfile() calls. Note that the sink
129 repository is not told to update itself to a particular revision
130 (or even what that revision would be) before it receives the
131 file data."""
132 raise NotImplementedError()
133
134 def puttags(self, tags):
135 """Put tags into sink.
136 tags: {tagname: sink_rev_id, ...}"""
137 raise NotImplementedError()
138
139
59 # CVS conversion code inspired by hg-cvs-import and git-cvsimport
140 # CVS conversion code inspired by hg-cvs-import and git-cvsimport
60 class convert_cvs:
141 class convert_cvs:
61 def __init__(self, path):
142 def __init__(self, path):
62 self.path = path
143 self.path = path
63 cvs = os.path.join(path, "CVS")
144 cvs = os.path.join(path, "CVS")
64 if not os.path.exists(cvs):
145 if not os.path.exists(cvs):
65 raise NoRepo("couldn't open CVS repo %s" % path)
146 raise NoRepo("couldn't open CVS repo %s" % path)
66
147
67 self.changeset = {}
148 self.changeset = {}
68 self.files = {}
149 self.files = {}
69 self.tags = {}
150 self.tags = {}
70 self.lastbranch = {}
151 self.lastbranch = {}
71 self.parent = {}
152 self.parent = {}
72 self.socket = None
153 self.socket = None
73 self.cvsroot = file(os.path.join(cvs, "Root")).read()[:-1]
154 self.cvsroot = file(os.path.join(cvs, "Root")).read()[:-1]
74 self.cvsrepo = file(os.path.join(cvs, "Repository")).read()[:-1]
155 self.cvsrepo = file(os.path.join(cvs, "Repository")).read()[:-1]
75 self.encoding = locale.getpreferredencoding()
156 self.encoding = locale.getpreferredencoding()
76 self._parse()
157 self._parse()
77 self._connect()
158 self._connect()
78
159
79 def _parse(self):
160 def _parse(self):
80 if self.changeset:
161 if self.changeset:
81 return
162 return
82
163
83 d = os.getcwd()
164 d = os.getcwd()
84 try:
165 try:
85 os.chdir(self.path)
166 os.chdir(self.path)
86 id = None
167 id = None
87 state = 0
168 state = 0
88 for l in os.popen("cvsps -A -u --cvs-direct -q"):
169 for l in os.popen("cvsps -A -u --cvs-direct -q"):
89 if state == 0: # header
170 if state == 0: # header
90 if l.startswith("PatchSet"):
171 if l.startswith("PatchSet"):
91 id = l[9:-2]
172 id = l[9:-2]
92 elif l.startswith("Date"):
173 elif l.startswith("Date"):
93 date = util.parsedate(l[6:-1], ["%Y/%m/%d %H:%M:%S"])
174 date = util.parsedate(l[6:-1], ["%Y/%m/%d %H:%M:%S"])
94 date = util.datestr(date)
175 date = util.datestr(date)
95 elif l.startswith("Branch"):
176 elif l.startswith("Branch"):
96 branch = l[8:-1]
177 branch = l[8:-1]
97 self.parent[id] = self.lastbranch.get(branch,'bad')
178 self.parent[id] = self.lastbranch.get(branch,'bad')
98 self.lastbranch[branch] = id
179 self.lastbranch[branch] = id
99 elif l.startswith("Ancestor branch"):
180 elif l.startswith("Ancestor branch"):
100 ancestor = l[17:-1]
181 ancestor = l[17:-1]
101 self.parent[id] = self.lastbranch[ancestor]
182 self.parent[id] = self.lastbranch[ancestor]
102 elif l.startswith("Author"):
183 elif l.startswith("Author"):
103 author = self.recode(l[8:-1])
184 author = self.recode(l[8:-1])
104 elif l.startswith("Tag: "):
185 elif l.startswith("Tag: "):
105 t = l[5:-1].rstrip()
186 t = l[5:-1].rstrip()
106 if t != "(none)":
187 if t != "(none)":
107 self.tags[t] = id
188 self.tags[t] = id
108 elif l.startswith("Log:"):
189 elif l.startswith("Log:"):
109 state = 1
190 state = 1
110 log = ""
191 log = ""
111 elif state == 1: # log
192 elif state == 1: # log
112 if l == "Members: \n":
193 if l == "Members: \n":
113 files = {}
194 files = {}
114 log = self.recode(log[:-1])
195 log = self.recode(log[:-1])
115 if log.isspace():
196 if log.isspace():
116 log = "*** empty log message ***\n"
197 log = "*** empty log message ***\n"
117 state = 2
198 state = 2
118 else:
199 else:
119 log += l
200 log += l
120 elif state == 2:
201 elif state == 2:
121 if l == "\n": #
202 if l == "\n": #
122 state = 0
203 state = 0
123 p = [self.parent[id]]
204 p = [self.parent[id]]
124 if id == "1":
205 if id == "1":
125 p = []
206 p = []
126 c = commit(author=author, date=date, parents=p,
207 c = commit(author=author, date=date, parents=p,
127 desc=log, branch=branch)
208 desc=log, branch=branch)
128 self.changeset[id] = c
209 self.changeset[id] = c
129 self.files[id] = files
210 self.files[id] = files
130 else:
211 else:
131 file,rev = l[1:-2].rsplit(':',1)
212 file,rev = l[1:-2].rsplit(':',1)
132 rev = rev.split("->")[1]
213 rev = rev.split("->")[1]
133 files[file] = rev
214 files[file] = rev
134
215
135 self.heads = self.lastbranch.values()
216 self.heads = self.lastbranch.values()
136 finally:
217 finally:
137 os.chdir(d)
218 os.chdir(d)
138
219
139 def _connect(self):
220 def _connect(self):
140 root = self.cvsroot
221 root = self.cvsroot
141 conntype = None
222 conntype = None
142 user, host = None, None
223 user, host = None, None
143 cmd = ['cvs', 'server']
224 cmd = ['cvs', 'server']
144
225
145 status("connecting to %s\n" % root)
226 status("connecting to %s\n" % root)
146
227
147 if root.startswith(":pserver:"):
228 if root.startswith(":pserver:"):
148 root = root[9:]
229 root = root[9:]
149 m = re.match(r'(?:(.*?)(?::(.*?))?@)?([^:\/]*)(?::(\d*))?(.*)', root)
230 m = re.match(r'(?:(.*?)(?::(.*?))?@)?([^:\/]*)(?::(\d*))?(.*)', root)
150 if m:
231 if m:
151 conntype = "pserver"
232 conntype = "pserver"
152 user, passw, serv, port, root = m.groups()
233 user, passw, serv, port, root = m.groups()
153 if not user:
234 if not user:
154 user = "anonymous"
235 user = "anonymous"
155 rr = ":pserver:" + user + "@" + serv + ":" + root
236 rr = ":pserver:" + user + "@" + serv + ":" + root
156 if port:
237 if port:
157 rr2, port = "-", int(port)
238 rr2, port = "-", int(port)
158 else:
239 else:
159 rr2, port = rr, 2401
240 rr2, port = rr, 2401
160 rr += str(port)
241 rr += str(port)
161
242
162 if not passw:
243 if not passw:
163 passw = "A"
244 passw = "A"
164 pf = open(os.path.join(os.environ["HOME"], ".cvspass"))
245 pf = open(os.path.join(os.environ["HOME"], ".cvspass"))
165 for l in pf:
246 for l in pf:
166 # :pserver:cvs@mea.tmt.tele.fi:/cvsroot/zmailer Ah<Z
247 # :pserver:cvs@mea.tmt.tele.fi:/cvsroot/zmailer Ah<Z
167 m = re.match(r'(/\d+\s+/)?(.*)', l)
248 m = re.match(r'(/\d+\s+/)?(.*)', l)
168 l = m.group(2)
249 l = m.group(2)
169 w, p = l.split(' ', 1)
250 w, p = l.split(' ', 1)
170 if w in [rr, rr2]:
251 if w in [rr, rr2]:
171 passw = p
252 passw = p
172 break
253 break
173 pf.close()
254 pf.close()
174
255
175 sck = socket.socket()
256 sck = socket.socket()
176 sck.connect((serv, port))
257 sck.connect((serv, port))
177 sck.send("\n".join(["BEGIN AUTH REQUEST", root, user, passw, "END AUTH REQUEST", ""]))
258 sck.send("\n".join(["BEGIN AUTH REQUEST", root, user, passw, "END AUTH REQUEST", ""]))
178 if sck.recv(128) != "I LOVE YOU\n":
259 if sck.recv(128) != "I LOVE YOU\n":
179 raise NoRepo("CVS pserver authentication failed")
260 raise NoRepo("CVS pserver authentication failed")
180
261
181 self.writep = self.readp = sck.makefile('r+')
262 self.writep = self.readp = sck.makefile('r+')
182
263
183 if not conntype and root.startswith(":local:"):
264 if not conntype and root.startswith(":local:"):
184 conntype = "local"
265 conntype = "local"
185 root = root[7:]
266 root = root[7:]
186
267
187 if not conntype:
268 if not conntype:
188 # :ext:user@host/home/user/path/to/cvsroot
269 # :ext:user@host/home/user/path/to/cvsroot
189 if root.startswith(":ext:"):
270 if root.startswith(":ext:"):
190 root = root[5:]
271 root = root[5:]
191 m = re.match(r'(?:([^@:/]+)@)?([^:/]+):?(.*)', root)
272 m = re.match(r'(?:([^@:/]+)@)?([^:/]+):?(.*)', root)
192 if not m:
273 if not m:
193 conntype = "local"
274 conntype = "local"
194 else:
275 else:
195 conntype = "rsh"
276 conntype = "rsh"
196 user, host, root = m.group(1), m.group(2), m.group(3)
277 user, host, root = m.group(1), m.group(2), m.group(3)
197
278
198 if conntype != "pserver":
279 if conntype != "pserver":
199 if conntype == "rsh":
280 if conntype == "rsh":
200 rsh = os.environ.get("CVS_RSH" or "rsh")
281 rsh = os.environ.get("CVS_RSH" or "rsh")
201 if user:
282 if user:
202 cmd = [rsh, '-l', user, host] + cmd
283 cmd = [rsh, '-l', user, host] + cmd
203 else:
284 else:
204 cmd = [rsh, host] + cmd
285 cmd = [rsh, host] + cmd
205
286
206 self.writep, self.readp = os.popen2(cmd)
287 self.writep, self.readp = os.popen2(cmd)
207
288
208 self.realroot = root
289 self.realroot = root
209
290
210 self.writep.write("Root %s\n" % root)
291 self.writep.write("Root %s\n" % root)
211 self.writep.write("Valid-responses ok error Valid-requests Mode"
292 self.writep.write("Valid-responses ok error Valid-requests Mode"
212 " M Mbinary E Checked-in Created Updated"
293 " M Mbinary E Checked-in Created Updated"
213 " Merged Removed\n")
294 " Merged Removed\n")
214 self.writep.write("valid-requests\n")
295 self.writep.write("valid-requests\n")
215 self.writep.flush()
296 self.writep.flush()
216 r = self.readp.readline()
297 r = self.readp.readline()
217 if not r.startswith("Valid-requests"):
298 if not r.startswith("Valid-requests"):
218 abort("server sucks\n")
299 abort("server sucks\n")
219 if "UseUnchanged" in r:
300 if "UseUnchanged" in r:
220 self.writep.write("UseUnchanged\n")
301 self.writep.write("UseUnchanged\n")
221 self.writep.flush()
302 self.writep.flush()
222 r = self.readp.readline()
303 r = self.readp.readline()
223
304
224 def getheads(self):
305 def getheads(self):
225 return self.heads
306 return self.heads
226
307
227 def _getfile(self, name, rev):
308 def _getfile(self, name, rev):
228 if rev.endswith("(DEAD)"):
309 if rev.endswith("(DEAD)"):
229 raise IOError
310 raise IOError
230
311
231 args = ("-N -P -kk -r %s --" % rev).split()
312 args = ("-N -P -kk -r %s --" % rev).split()
232 args.append(os.path.join(self.cvsrepo, name))
313 args.append(os.path.join(self.cvsrepo, name))
233 for x in args:
314 for x in args:
234 self.writep.write("Argument %s\n" % x)
315 self.writep.write("Argument %s\n" % x)
235 self.writep.write("Directory .\n%s\nco\n" % self.realroot)
316 self.writep.write("Directory .\n%s\nco\n" % self.realroot)
236 self.writep.flush()
317 self.writep.flush()
237
318
238 data = ""
319 data = ""
239 while 1:
320 while 1:
240 line = self.readp.readline()
321 line = self.readp.readline()
241 if line.startswith("Created ") or line.startswith("Updated "):
322 if line.startswith("Created ") or line.startswith("Updated "):
242 self.readp.readline() # path
323 self.readp.readline() # path
243 self.readp.readline() # entries
324 self.readp.readline() # entries
244 mode = self.readp.readline()[:-1]
325 mode = self.readp.readline()[:-1]
245 count = int(self.readp.readline()[:-1])
326 count = int(self.readp.readline()[:-1])
246 data = self.readp.read(count)
327 data = self.readp.read(count)
247 elif line.startswith(" "):
328 elif line.startswith(" "):
248 data += line[1:]
329 data += line[1:]
249 elif line.startswith("M "):
330 elif line.startswith("M "):
250 pass
331 pass
251 elif line.startswith("Mbinary "):
332 elif line.startswith("Mbinary "):
252 count = int(self.readp.readline()[:-1])
333 count = int(self.readp.readline()[:-1])
253 data = self.readp.read(count)
334 data = self.readp.read(count)
254 else:
335 else:
255 if line == "ok\n":
336 if line == "ok\n":
256 return (data, "x" in mode and "x" or "")
337 return (data, "x" in mode and "x" or "")
257 elif line.startswith("E "):
338 elif line.startswith("E "):
258 warn("cvs server: %s\n" % line[2:])
339 warn("cvs server: %s\n" % line[2:])
259 elif line.startswith("Remove"):
340 elif line.startswith("Remove"):
260 l = self.readp.readline()
341 l = self.readp.readline()
261 l = self.readp.readline()
342 l = self.readp.readline()
262 if l != "ok\n":
343 if l != "ok\n":
263 abort("unknown CVS response: %s\n" % l)
344 abort("unknown CVS response: %s\n" % l)
264 else:
345 else:
265 abort("unknown CVS response: %s\n" % line)
346 abort("unknown CVS response: %s\n" % line)
266
347
267 def getfile(self, file, rev):
348 def getfile(self, file, rev):
268 data, mode = self._getfile(file, rev)
349 data, mode = self._getfile(file, rev)
269 self.modecache[(file, rev)] = mode
350 self.modecache[(file, rev)] = mode
270 return data
351 return data
271
352
272 def getmode(self, file, rev):
353 def getmode(self, file, rev):
273 return self.modecache[(file, rev)]
354 return self.modecache[(file, rev)]
274
355
275 def getchanges(self, rev):
356 def getchanges(self, rev):
276 self.modecache = {}
357 self.modecache = {}
277 files = self.files[rev]
358 files = self.files[rev]
278 cl = files.items()
359 cl = files.items()
279 cl.sort()
360 cl.sort()
280 return cl
361 return cl
281
362
282 def recode(self, text):
363 def recode(self, text):
283 return text.decode(self.encoding, "replace").encode("utf-8")
364 return text.decode(self.encoding, "replace").encode("utf-8")
284
365
285 def getcommit(self, rev):
366 def getcommit(self, rev):
286 return self.changeset[rev]
367 return self.changeset[rev]
287
368
288 def gettags(self):
369 def gettags(self):
289 return self.tags
370 return self.tags
290
371
291 class convert_git:
372 class convert_git:
292 def __init__(self, path):
373 def __init__(self, path):
293 if os.path.isdir(path + "/.git"):
374 if os.path.isdir(path + "/.git"):
294 path += "/.git"
375 path += "/.git"
295 self.path = path
376 self.path = path
296 if not os.path.exists(path + "/objects"):
377 if not os.path.exists(path + "/objects"):
297 raise NoRepo("couldn't open GIT repo %s" % path)
378 raise NoRepo("couldn't open GIT repo %s" % path)
298
379
299 def getheads(self):
380 def getheads(self):
300 fh = os.popen("GIT_DIR=%s git-rev-parse --verify HEAD" % self.path)
381 fh = os.popen("GIT_DIR=%s git-rev-parse --verify HEAD" % self.path)
301 return [fh.read()[:-1]]
382 return [fh.read()[:-1]]
302
383
303 def catfile(self, rev, type):
384 def catfile(self, rev, type):
304 if rev == "0" * 40: raise IOError()
385 if rev == "0" * 40: raise IOError()
305 fh = os.popen("GIT_DIR=%s git-cat-file %s %s 2>/dev/null" % (self.path, type, rev))
386 fh = os.popen("GIT_DIR=%s git-cat-file %s %s 2>/dev/null" % (self.path, type, rev))
306 return fh.read()
387 return fh.read()
307
388
308 def getfile(self, name, rev):
389 def getfile(self, name, rev):
309 return self.catfile(rev, "blob")
390 return self.catfile(rev, "blob")
310
391
311 def getmode(self, name, rev):
392 def getmode(self, name, rev):
312 return self.modecache[(name, rev)]
393 return self.modecache[(name, rev)]
313
394
314 def getchanges(self, version):
395 def getchanges(self, version):
315 self.modecache = {}
396 self.modecache = {}
316 fh = os.popen("GIT_DIR=%s git-diff-tree --root -m -r %s" % (self.path, version))
397 fh = os.popen("GIT_DIR=%s git-diff-tree --root -m -r %s" % (self.path, version))
317 changes = []
398 changes = []
318 for l in fh:
399 for l in fh:
319 if "\t" not in l: continue
400 if "\t" not in l: continue
320 m, f = l[:-1].split("\t")
401 m, f = l[:-1].split("\t")
321 m = m.split()
402 m = m.split()
322 h = m[3]
403 h = m[3]
323 p = (m[1] == "100755")
404 p = (m[1] == "100755")
324 s = (m[1] == "120000")
405 s = (m[1] == "120000")
325 self.modecache[(f, h)] = (p and "x") or (s and "l") or ""
406 self.modecache[(f, h)] = (p and "x") or (s and "l") or ""
326 changes.append((f, h))
407 changes.append((f, h))
327 return changes
408 return changes
328
409
329 def getcommit(self, version):
410 def getcommit(self, version):
330 c = self.catfile(version, "commit") # read the commit hash
411 c = self.catfile(version, "commit") # read the commit hash
331 end = c.find("\n\n")
412 end = c.find("\n\n")
332 message = c[end+2:]
413 message = c[end+2:]
333 message = recode(message)
414 message = recode(message)
334 l = c[:end].splitlines()
415 l = c[:end].splitlines()
335 manifest = l[0].split()[1]
416 manifest = l[0].split()[1]
336 parents = []
417 parents = []
337 for e in l[1:]:
418 for e in l[1:]:
338 n,v = e.split(" ", 1)
419 n,v = e.split(" ", 1)
339 if n == "author":
420 if n == "author":
340 p = v.split()
421 p = v.split()
341 tm, tz = p[-2:]
422 tm, tz = p[-2:]
342 author = " ".join(p[:-2])
423 author = " ".join(p[:-2])
343 if author[0] == "<": author = author[1:-1]
424 if author[0] == "<": author = author[1:-1]
344 author = recode(author)
425 author = recode(author)
345 if n == "committer":
426 if n == "committer":
346 p = v.split()
427 p = v.split()
347 tm, tz = p[-2:]
428 tm, tz = p[-2:]
348 committer = " ".join(p[:-2])
429 committer = " ".join(p[:-2])
349 if committer[0] == "<": committer = committer[1:-1]
430 if committer[0] == "<": committer = committer[1:-1]
350 committer = recode(committer)
431 committer = recode(committer)
351 message += "\ncommitter: %s\n" % committer
432 message += "\ncommitter: %s\n" % committer
352 if n == "parent": parents.append(v)
433 if n == "parent": parents.append(v)
353
434
354 tzs, tzh, tzm = tz[-5:-4] + "1", tz[-4:-2], tz[-2:]
435 tzs, tzh, tzm = tz[-5:-4] + "1", tz[-4:-2], tz[-2:]
355 tz = -int(tzs) * (int(tzh) * 3600 + int(tzm))
436 tz = -int(tzs) * (int(tzh) * 3600 + int(tzm))
356 date = tm + " " + str(tz)
437 date = tm + " " + str(tz)
357
438
358 c = commit(parents=parents, date=date, author=author, desc=message)
439 c = commit(parents=parents, date=date, author=author, desc=message)
359 return c
440 return c
360
441
361 def gettags(self):
442 def gettags(self):
362 tags = {}
443 tags = {}
363 fh = os.popen('git-ls-remote --tags "%s" 2>/dev/null' % self.path)
444 fh = os.popen('git-ls-remote --tags "%s" 2>/dev/null' % self.path)
364 prefix = 'refs/tags/'
445 prefix = 'refs/tags/'
365 for line in fh:
446 for line in fh:
366 line = line.strip()
447 line = line.strip()
367 if not line.endswith("^{}"):
448 if not line.endswith("^{}"):
368 continue
449 continue
369 node, tag = line.split(None, 1)
450 node, tag = line.split(None, 1)
370 if not tag.startswith(prefix):
451 if not tag.startswith(prefix):
371 continue
452 continue
372 tag = tag[len(prefix):-3]
453 tag = tag[len(prefix):-3]
373 tags[tag] = node
454 tags[tag] = node
374
455
375 return tags
456 return tags
376
457
377 class convert_mercurial:
458 class convert_mercurial:
378 def __init__(self, path):
459 def __init__(self, path):
379 self.path = path
460 self.path = path
380 u = ui.ui()
461 u = ui.ui()
381 try:
462 try:
382 self.repo = hg.repository(u, path)
463 self.repo = hg.repository(u, path)
383 except:
464 except:
384 raise NoRepo("could open hg repo %s" % path)
465 raise NoRepo("could open hg repo %s" % path)
385
466
386 def mapfile(self):
467 def mapfile(self):
387 return os.path.join(self.path, ".hg", "shamap")
468 return os.path.join(self.path, ".hg", "shamap")
388
469
389 def getheads(self):
470 def getheads(self):
390 h = self.repo.changelog.heads()
471 h = self.repo.changelog.heads()
391 return [ hg.hex(x) for x in h ]
472 return [ hg.hex(x) for x in h ]
392
473
393 def putfile(self, f, e, data):
474 def putfile(self, f, e, data):
394 self.repo.wwrite(f, data, e)
475 self.repo.wwrite(f, data, e)
395 if self.repo.dirstate.state(f) == '?':
476 if self.repo.dirstate.state(f) == '?':
396 self.repo.dirstate.update([f], "a")
477 self.repo.dirstate.update([f], "a")
397
478
398 def delfile(self, f):
479 def delfile(self, f):
399 try:
480 try:
400 os.unlink(self.repo.wjoin(f))
481 os.unlink(self.repo.wjoin(f))
401 #self.repo.remove([f])
482 #self.repo.remove([f])
402 except:
483 except:
403 pass
484 pass
404
485
405 def putcommit(self, files, parents, commit):
486 def putcommit(self, files, parents, commit):
406 seen = {}
487 seen = {}
407 pl = []
488 pl = []
408 for p in parents:
489 for p in parents:
409 if p not in seen:
490 if p not in seen:
410 pl.append(p)
491 pl.append(p)
411 seen[p] = 1
492 seen[p] = 1
412 parents = pl
493 parents = pl
413
494
414 if len(parents) < 2: parents.append("0" * 40)
495 if len(parents) < 2: parents.append("0" * 40)
415 if len(parents) < 2: parents.append("0" * 40)
496 if len(parents) < 2: parents.append("0" * 40)
416 p2 = parents.pop(0)
497 p2 = parents.pop(0)
417
498
418 text = commit.desc
499 text = commit.desc
419 extra = {}
500 extra = {}
420 try:
501 try:
421 extra["branch"] = commit.branch
502 extra["branch"] = commit.branch
422 except AttributeError:
503 except AttributeError:
423 pass
504 pass
424
505
425 while parents:
506 while parents:
426 p1 = p2
507 p1 = p2
427 p2 = parents.pop(0)
508 p2 = parents.pop(0)
428 a = self.repo.rawcommit(files, text, commit.author, commit.date,
509 a = self.repo.rawcommit(files, text, commit.author, commit.date,
429 hg.bin(p1), hg.bin(p2), extra=extra)
510 hg.bin(p1), hg.bin(p2), extra=extra)
430 text = "(octopus merge fixup)\n"
511 text = "(octopus merge fixup)\n"
431 p2 = hg.hex(self.repo.changelog.tip())
512 p2 = hg.hex(self.repo.changelog.tip())
432
513
433 return p2
514 return p2
434
515
435 def puttags(self, tags):
516 def puttags(self, tags):
436 try:
517 try:
437 old = self.repo.wfile(".hgtags").read()
518 old = self.repo.wfile(".hgtags").read()
438 oldlines = old.splitlines(1)
519 oldlines = old.splitlines(1)
439 oldlines.sort()
520 oldlines.sort()
440 except:
521 except:
441 oldlines = []
522 oldlines = []
442
523
443 k = tags.keys()
524 k = tags.keys()
444 k.sort()
525 k.sort()
445 newlines = []
526 newlines = []
446 for tag in k:
527 for tag in k:
447 newlines.append("%s %s\n" % (tags[tag], tag))
528 newlines.append("%s %s\n" % (tags[tag], tag))
448
529
449 newlines.sort()
530 newlines.sort()
450
531
451 if newlines != oldlines:
532 if newlines != oldlines:
452 status("updating tags\n")
533 status("updating tags\n")
453 f = self.repo.wfile(".hgtags", "w")
534 f = self.repo.wfile(".hgtags", "w")
454 f.write("".join(newlines))
535 f.write("".join(newlines))
455 f.close()
536 f.close()
456 if not oldlines: self.repo.add([".hgtags"])
537 if not oldlines: self.repo.add([".hgtags"])
457 date = "%s 0" % int(time.mktime(time.gmtime()))
538 date = "%s 0" % int(time.mktime(time.gmtime()))
458 self.repo.rawcommit([".hgtags"], "update tags", "convert-repo",
539 self.repo.rawcommit([".hgtags"], "update tags", "convert-repo",
459 date, self.repo.changelog.tip(), hg.nullid)
540 date, self.repo.changelog.tip(), hg.nullid)
460 return hg.hex(self.repo.changelog.tip())
541 return hg.hex(self.repo.changelog.tip())
461
542
462 converters = [convert_cvs, convert_git, convert_mercurial]
543 converters = [convert_cvs, convert_git, convert_mercurial]
463
544
464 def converter(path):
545 def converter(path):
465 if not os.path.isdir(path):
546 if not os.path.isdir(path):
466 abort("%s: not a directory\n" % path)
547 abort("%s: not a directory\n" % path)
467 for c in converters:
548 for c in converters:
468 try:
549 try:
469 return c(path)
550 return c(path)
470 except NoRepo:
551 except NoRepo:
471 pass
552 pass
472 abort("%s: unknown repository type\n" % path)
553 abort("%s: unknown repository type\n" % path)
473
554
474 class convert:
555 class convert:
475 def __init__(self, source, dest, mapfile, opts):
556 def __init__(self, source, dest, mapfile, opts):
476
557
477 self.source = source
558 self.source = source
478 self.dest = dest
559 self.dest = dest
479 self.mapfile = mapfile
560 self.mapfile = mapfile
480 self.opts = opts
561 self.opts = opts
481 self.commitcache = {}
562 self.commitcache = {}
482
563
483 self.map = {}
564 self.map = {}
484 try:
565 try:
485 for l in file(self.mapfile):
566 for l in file(self.mapfile):
486 sv, dv = l[:-1].split()
567 sv, dv = l[:-1].split()
487 self.map[sv] = dv
568 self.map[sv] = dv
488 except IOError:
569 except IOError:
489 pass
570 pass
490
571
491 def walktree(self, heads):
572 def walktree(self, heads):
492 visit = heads
573 visit = heads
493 known = {}
574 known = {}
494 parents = {}
575 parents = {}
495 while visit:
576 while visit:
496 n = visit.pop(0)
577 n = visit.pop(0)
497 if n in known or n in self.map: continue
578 if n in known or n in self.map: continue
498 known[n] = 1
579 known[n] = 1
499 self.commitcache[n] = self.source.getcommit(n)
580 self.commitcache[n] = self.source.getcommit(n)
500 cp = self.commitcache[n].parents
581 cp = self.commitcache[n].parents
501 for p in cp:
582 for p in cp:
502 parents.setdefault(n, []).append(p)
583 parents.setdefault(n, []).append(p)
503 visit.append(p)
584 visit.append(p)
504
585
505 return parents
586 return parents
506
587
507 def toposort(self, parents):
588 def toposort(self, parents):
508 visit = parents.keys()
589 visit = parents.keys()
509 seen = {}
590 seen = {}
510 children = {}
591 children = {}
511
592
512 while visit:
593 while visit:
513 n = visit.pop(0)
594 n = visit.pop(0)
514 if n in seen: continue
595 if n in seen: continue
515 seen[n] = 1
596 seen[n] = 1
516 pc = 0
597 pc = 0
517 if n in parents:
598 if n in parents:
518 for p in parents[n]:
599 for p in parents[n]:
519 if p not in self.map: pc += 1
600 if p not in self.map: pc += 1
520 visit.append(p)
601 visit.append(p)
521 children.setdefault(p, []).append(n)
602 children.setdefault(p, []).append(n)
522 if not pc: root = n
603 if not pc: root = n
523
604
524 s = []
605 s = []
525 removed = {}
606 removed = {}
526 visit = children.keys()
607 visit = children.keys()
527 while visit:
608 while visit:
528 n = visit.pop(0)
609 n = visit.pop(0)
529 if n in removed: continue
610 if n in removed: continue
530 dep = 0
611 dep = 0
531 if n in parents:
612 if n in parents:
532 for p in parents[n]:
613 for p in parents[n]:
533 if p in self.map: continue
614 if p in self.map: continue
534 if p not in removed:
615 if p not in removed:
535 # we're still dependent
616 # we're still dependent
536 visit.append(n)
617 visit.append(n)
537 dep = 1
618 dep = 1
538 break
619 break
539
620
540 if not dep:
621 if not dep:
541 # all n's parents are in the list
622 # all n's parents are in the list
542 removed[n] = 1
623 removed[n] = 1
543 if n not in self.map:
624 if n not in self.map:
544 s.append(n)
625 s.append(n)
545 if n in children:
626 if n in children:
546 for c in children[n]:
627 for c in children[n]:
547 visit.insert(0, c)
628 visit.insert(0, c)
548
629
549 if opts.get('datesort'):
630 if opts.get('datesort'):
550 depth = {}
631 depth = {}
551 for n in s:
632 for n in s:
552 depth[n] = 0
633 depth[n] = 0
553 pl = [p for p in self.commitcache[n].parents if p not in self.map]
634 pl = [p for p in self.commitcache[n].parents if p not in self.map]
554 if pl:
635 if pl:
555 depth[n] = max([depth[p] for p in pl]) + 1
636 depth[n] = max([depth[p] for p in pl]) + 1
556
637
557 s = [(depth[n], self.commitcache[n].date, n) for n in s]
638 s = [(depth[n], self.commitcache[n].date, n) for n in s]
558 s.sort()
639 s.sort()
559 s = [e[2] for e in s]
640 s = [e[2] for e in s]
560
641
561 return s
642 return s
562
643
563 def copy(self, rev):
644 def copy(self, rev):
564 c = self.commitcache[rev]
645 c = self.commitcache[rev]
565 files = self.source.getchanges(rev)
646 files = self.source.getchanges(rev)
566
647
567 for f,v in files:
648 for f,v in files:
568 try:
649 try:
569 data = self.source.getfile(f, v)
650 data = self.source.getfile(f, v)
570 except IOError, inst:
651 except IOError, inst:
571 self.dest.delfile(f)
652 self.dest.delfile(f)
572 else:
653 else:
573 e = self.source.getmode(f, v)
654 e = self.source.getmode(f, v)
574 self.dest.putfile(f, e, data)
655 self.dest.putfile(f, e, data)
575
656
576 r = [self.map[v] for v in c.parents]
657 r = [self.map[v] for v in c.parents]
577 f = [f for f,v in files]
658 f = [f for f,v in files]
578 self.map[rev] = self.dest.putcommit(f, r, c)
659 self.map[rev] = self.dest.putcommit(f, r, c)
579 file(self.mapfile, "a").write("%s %s\n" % (rev, self.map[rev]))
660 file(self.mapfile, "a").write("%s %s\n" % (rev, self.map[rev]))
580
661
581 def convert(self):
662 def convert(self):
582 status("scanning source...\n")
663 status("scanning source...\n")
583 heads = self.source.getheads()
664 heads = self.source.getheads()
584 parents = self.walktree(heads)
665 parents = self.walktree(heads)
585 status("sorting...\n")
666 status("sorting...\n")
586 t = self.toposort(parents)
667 t = self.toposort(parents)
587 num = len(t)
668 num = len(t)
588 c = None
669 c = None
589
670
590 status("converting...\n")
671 status("converting...\n")
591 for c in t:
672 for c in t:
592 num -= 1
673 num -= 1
593 desc = self.commitcache[c].desc
674 desc = self.commitcache[c].desc
594 if "\n" in desc:
675 if "\n" in desc:
595 desc = desc.splitlines()[0]
676 desc = desc.splitlines()[0]
596 status("%d %s\n" % (num, desc))
677 status("%d %s\n" % (num, desc))
597 self.copy(c)
678 self.copy(c)
598
679
599 tags = self.source.gettags()
680 tags = self.source.gettags()
600 ctags = {}
681 ctags = {}
601 for k in tags:
682 for k in tags:
602 v = tags[k]
683 v = tags[k]
603 if v in self.map:
684 if v in self.map:
604 ctags[k] = self.map[v]
685 ctags[k] = self.map[v]
605
686
606 if c and ctags:
687 if c and ctags:
607 nrev = self.dest.puttags(ctags)
688 nrev = self.dest.puttags(ctags)
608 # write another hash correspondence to override the previous
689 # write another hash correspondence to override the previous
609 # one so we don't end up with extra tag heads
690 # one so we don't end up with extra tag heads
610 if nrev:
691 if nrev:
611 file(self.mapfile, "a").write("%s %s\n" % (c, nrev))
692 file(self.mapfile, "a").write("%s %s\n" % (c, nrev))
612
693
613 def command(src, dest=None, mapfile=None, **opts):
694 def command(src, dest=None, mapfile=None, **opts):
614 srcc = converter(src)
695 srcc = converter(src)
615 if not hasattr(srcc, "getcommit"):
696 if not hasattr(srcc, "getcommit"):
616 abort("%s: can't read from this repo type\n" % src)
697 abort("%s: can't read from this repo type\n" % src)
617
698
618 if not dest:
699 if not dest:
619 dest = src + "-hg"
700 dest = src + "-hg"
620 status("assuming destination %s\n" % dest)
701 status("assuming destination %s\n" % dest)
621 if not os.path.isdir(dest):
702 if not os.path.isdir(dest):
622 status("creating repository %s\n" % dest)
703 status("creating repository %s\n" % dest)
623 os.system("hg init " + dest)
704 os.system("hg init " + dest)
624 destc = converter(dest)
705 destc = converter(dest)
625 if not hasattr(destc, "putcommit"):
706 if not hasattr(destc, "putcommit"):
626 abort("%s: can't write to this repo type\n" % src)
707 abort("%s: can't write to this repo type\n" % src)
627
708
628 if not mapfile:
709 if not mapfile:
629 try:
710 try:
630 mapfile = destc.mapfile()
711 mapfile = destc.mapfile()
631 except:
712 except:
632 mapfile = os.path.join(destc, "map")
713 mapfile = os.path.join(destc, "map")
633
714
634 c = convert(srcc, destc, mapfile, opts)
715 c = convert(srcc, destc, mapfile, opts)
635 c.convert()
716 c.convert()
636
717
637 options = [('q', 'quiet', None, 'suppress output'),
718 options = [('q', 'quiet', None, 'suppress output'),
638 ('', 'datesort', None, 'try to sort changesets by date')]
719 ('', 'datesort', None, 'try to sort changesets by date')]
639 opts = {}
720 opts = {}
640 args = fancyopts.fancyopts(sys.argv[1:], options, opts)
721 args = fancyopts.fancyopts(sys.argv[1:], options, opts)
641
722
642 if opts['quiet']:
723 if opts['quiet']:
643 quiet = 1
724 quiet = 1
644
725
645 try:
726 try:
646 command(*args, **opts)
727 command(*args, **opts)
647 except Abort, inst:
728 except Abort, inst:
648 warn(inst)
729 warn(inst)
649 except KeyboardInterrupt:
730 except KeyboardInterrupt:
650 status("interrupted\n")
731 status("interrupted\n")
General Comments 0
You need to be logged in to leave comments. Login now