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