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