|
1 # cvs.py: CVS conversion code inspired by hg-cvs-import and git-cvsimport |
|
2 # |
|
3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com> and others |
|
4 # |
|
5 # This software may be used and distributed according to the terms of the |
|
6 # GNU General Public License version 2 or any later version. |
|
7 |
|
8 import os, re, socket, errno |
|
9 from cStringIO import StringIO |
|
10 from mercurial import encoding, util |
|
11 from mercurial.i18n import _ |
|
12 |
|
13 from common import NoRepo, commit, converter_source, checktool |
|
14 import cvsps |
|
15 |
|
16 class convert_cvs(converter_source): |
|
17 def __init__(self, ui, path, rev=None): |
|
18 super(convert_cvs, self).__init__(ui, path, rev=rev) |
|
19 |
|
20 cvs = os.path.join(path, "CVS") |
|
21 if not os.path.exists(cvs): |
|
22 raise NoRepo(_("%s does not look like a CVS checkout") % path) |
|
23 |
|
24 checktool('cvs') |
|
25 |
|
26 self.changeset = None |
|
27 self.files = {} |
|
28 self.tags = {} |
|
29 self.lastbranch = {} |
|
30 self.socket = None |
|
31 self.cvsroot = open(os.path.join(cvs, "Root")).read()[:-1] |
|
32 self.cvsrepo = open(os.path.join(cvs, "Repository")).read()[:-1] |
|
33 self.encoding = encoding.encoding |
|
34 |
|
35 self._connect() |
|
36 |
|
37 def _parse(self): |
|
38 if self.changeset is not None: |
|
39 return |
|
40 self.changeset = {} |
|
41 |
|
42 maxrev = 0 |
|
43 if self.rev: |
|
44 # TODO: handle tags |
|
45 try: |
|
46 # patchset number? |
|
47 maxrev = int(self.rev) |
|
48 except ValueError: |
|
49 raise util.Abort(_('revision %s is not a patchset number') |
|
50 % self.rev) |
|
51 |
|
52 d = os.getcwd() |
|
53 try: |
|
54 os.chdir(self.path) |
|
55 id = None |
|
56 |
|
57 cache = 'update' |
|
58 if not self.ui.configbool('convert', 'cvsps.cache', True): |
|
59 cache = None |
|
60 db = cvsps.createlog(self.ui, cache=cache) |
|
61 db = cvsps.createchangeset(self.ui, db, |
|
62 fuzz=int(self.ui.config('convert', 'cvsps.fuzz', 60)), |
|
63 mergeto=self.ui.config('convert', 'cvsps.mergeto', None), |
|
64 mergefrom=self.ui.config('convert', 'cvsps.mergefrom', None)) |
|
65 |
|
66 for cs in db: |
|
67 if maxrev and cs.id > maxrev: |
|
68 break |
|
69 id = str(cs.id) |
|
70 cs.author = self.recode(cs.author) |
|
71 self.lastbranch[cs.branch] = id |
|
72 cs.comment = self.recode(cs.comment) |
|
73 date = util.datestr(cs.date) |
|
74 self.tags.update(dict.fromkeys(cs.tags, id)) |
|
75 |
|
76 files = {} |
|
77 for f in cs.entries: |
|
78 files[f.file] = "%s%s" % ('.'.join([str(x) |
|
79 for x in f.revision]), |
|
80 ['', '(DEAD)'][f.dead]) |
|
81 |
|
82 # add current commit to set |
|
83 c = commit(author=cs.author, date=date, |
|
84 parents=[str(p.id) for p in cs.parents], |
|
85 desc=cs.comment, branch=cs.branch or '') |
|
86 self.changeset[id] = c |
|
87 self.files[id] = files |
|
88 |
|
89 self.heads = self.lastbranch.values() |
|
90 finally: |
|
91 os.chdir(d) |
|
92 |
|
93 def _connect(self): |
|
94 root = self.cvsroot |
|
95 conntype = None |
|
96 user, host = None, None |
|
97 cmd = ['cvs', 'server'] |
|
98 |
|
99 self.ui.status(_("connecting to %s\n") % root) |
|
100 |
|
101 if root.startswith(":pserver:"): |
|
102 root = root[9:] |
|
103 m = re.match(r'(?:(.*?)(?::(.*?))?@)?([^:\/]*)(?::(\d*))?(.*)', |
|
104 root) |
|
105 if m: |
|
106 conntype = "pserver" |
|
107 user, passw, serv, port, root = m.groups() |
|
108 if not user: |
|
109 user = "anonymous" |
|
110 if not port: |
|
111 port = 2401 |
|
112 else: |
|
113 port = int(port) |
|
114 format0 = ":pserver:%s@%s:%s" % (user, serv, root) |
|
115 format1 = ":pserver:%s@%s:%d%s" % (user, serv, port, root) |
|
116 |
|
117 if not passw: |
|
118 passw = "A" |
|
119 cvspass = os.path.expanduser("~/.cvspass") |
|
120 try: |
|
121 pf = open(cvspass) |
|
122 for line in pf.read().splitlines(): |
|
123 part1, part2 = line.split(' ', 1) |
|
124 if part1 == '/1': |
|
125 # /1 :pserver:user@example.com:2401/cvsroot/foo Ah<Z |
|
126 part1, part2 = part2.split(' ', 1) |
|
127 format = format1 |
|
128 else: |
|
129 # :pserver:user@example.com:/cvsroot/foo Ah<Z |
|
130 format = format0 |
|
131 if part1 == format: |
|
132 passw = part2 |
|
133 break |
|
134 pf.close() |
|
135 except IOError, inst: |
|
136 if inst.errno != errno.ENOENT: |
|
137 if not getattr(inst, 'filename', None): |
|
138 inst.filename = cvspass |
|
139 raise |
|
140 |
|
141 sck = socket.socket() |
|
142 sck.connect((serv, port)) |
|
143 sck.send("\n".join(["BEGIN AUTH REQUEST", root, user, passw, |
|
144 "END AUTH REQUEST", ""])) |
|
145 if sck.recv(128) != "I LOVE YOU\n": |
|
146 raise util.Abort(_("CVS pserver authentication failed")) |
|
147 |
|
148 self.writep = self.readp = sck.makefile('r+') |
|
149 |
|
150 if not conntype and root.startswith(":local:"): |
|
151 conntype = "local" |
|
152 root = root[7:] |
|
153 |
|
154 if not conntype: |
|
155 # :ext:user@host/home/user/path/to/cvsroot |
|
156 if root.startswith(":ext:"): |
|
157 root = root[5:] |
|
158 m = re.match(r'(?:([^@:/]+)@)?([^:/]+):?(.*)', root) |
|
159 # Do not take Windows path "c:\foo\bar" for a connection strings |
|
160 if os.path.isdir(root) or not m: |
|
161 conntype = "local" |
|
162 else: |
|
163 conntype = "rsh" |
|
164 user, host, root = m.group(1), m.group(2), m.group(3) |
|
165 |
|
166 if conntype != "pserver": |
|
167 if conntype == "rsh": |
|
168 rsh = os.environ.get("CVS_RSH") or "ssh" |
|
169 if user: |
|
170 cmd = [rsh, '-l', user, host] + cmd |
|
171 else: |
|
172 cmd = [rsh, host] + cmd |
|
173 |
|
174 # popen2 does not support argument lists under Windows |
|
175 cmd = [util.shellquote(arg) for arg in cmd] |
|
176 cmd = util.quotecommand(' '.join(cmd)) |
|
177 self.writep, self.readp = util.popen2(cmd) |
|
178 |
|
179 self.realroot = root |
|
180 |
|
181 self.writep.write("Root %s\n" % root) |
|
182 self.writep.write("Valid-responses ok error Valid-requests Mode" |
|
183 " M Mbinary E Checked-in Created Updated" |
|
184 " Merged Removed\n") |
|
185 self.writep.write("valid-requests\n") |
|
186 self.writep.flush() |
|
187 r = self.readp.readline() |
|
188 if not r.startswith("Valid-requests"): |
|
189 raise util.Abort(_('unexpected response from CVS server ' |
|
190 '(expected "Valid-requests", but got %r)') |
|
191 % r) |
|
192 if "UseUnchanged" in r: |
|
193 self.writep.write("UseUnchanged\n") |
|
194 self.writep.flush() |
|
195 r = self.readp.readline() |
|
196 |
|
197 def getheads(self): |
|
198 self._parse() |
|
199 return self.heads |
|
200 |
|
201 def getfile(self, name, rev): |
|
202 |
|
203 def chunkedread(fp, count): |
|
204 # file-objects returned by socked.makefile() do not handle |
|
205 # large read() requests very well. |
|
206 chunksize = 65536 |
|
207 output = StringIO() |
|
208 while count > 0: |
|
209 data = fp.read(min(count, chunksize)) |
|
210 if not data: |
|
211 raise util.Abort(_("%d bytes missing from remote file") |
|
212 % count) |
|
213 count -= len(data) |
|
214 output.write(data) |
|
215 return output.getvalue() |
|
216 |
|
217 self._parse() |
|
218 if rev.endswith("(DEAD)"): |
|
219 raise IOError |
|
220 |
|
221 args = ("-N -P -kk -r %s --" % rev).split() |
|
222 args.append(self.cvsrepo + '/' + name) |
|
223 for x in args: |
|
224 self.writep.write("Argument %s\n" % x) |
|
225 self.writep.write("Directory .\n%s\nco\n" % self.realroot) |
|
226 self.writep.flush() |
|
227 |
|
228 data = "" |
|
229 mode = None |
|
230 while 1: |
|
231 line = self.readp.readline() |
|
232 if line.startswith("Created ") or line.startswith("Updated "): |
|
233 self.readp.readline() # path |
|
234 self.readp.readline() # entries |
|
235 mode = self.readp.readline()[:-1] |
|
236 count = int(self.readp.readline()[:-1]) |
|
237 data = chunkedread(self.readp, count) |
|
238 elif line.startswith(" "): |
|
239 data += line[1:] |
|
240 elif line.startswith("M "): |
|
241 pass |
|
242 elif line.startswith("Mbinary "): |
|
243 count = int(self.readp.readline()[:-1]) |
|
244 data = chunkedread(self.readp, count) |
|
245 else: |
|
246 if line == "ok\n": |
|
247 if mode is None: |
|
248 raise util.Abort(_('malformed response from CVS')) |
|
249 return (data, "x" in mode and "x" or "") |
|
250 elif line.startswith("E "): |
|
251 self.ui.warn(_("cvs server: %s\n") % line[2:]) |
|
252 elif line.startswith("Remove"): |
|
253 self.readp.readline() |
|
254 else: |
|
255 raise util.Abort(_("unknown CVS response: %s") % line) |
|
256 |
|
257 def getchanges(self, rev): |
|
258 self._parse() |
|
259 return sorted(self.files[rev].iteritems()), {} |
|
260 |
|
261 def getcommit(self, rev): |
|
262 self._parse() |
|
263 return self.changeset[rev] |
|
264 |
|
265 def gettags(self): |
|
266 self._parse() |
|
267 return self.tags |
|
268 |
|
269 def getchangedfiles(self, rev, i): |
|
270 self._parse() |
|
271 return sorted(self.files[rev]) |