cvs.py
334 lines
| 11.2 KiB
| text/x-python
|
PythonLexer
|
r8250 | # cvs.py: CVS conversion code inspired by hg-cvs-import and git-cvsimport | ||
# | ||||
# Copyright 2005-2009 Matt Mackall <mpm@selenic.com> and others | ||||
# | ||||
# This software may be used and distributed according to the terms of the | ||||
|
r10263 | # GNU General Public License version 2 or any later version. | ||
|
r28413 | from __future__ import absolute_import | ||
|
r4536 | |||
|
r28413 | import errno | ||
import os | ||||
import re | ||||
import socket | ||||
|
r29205 | from mercurial.i18n import _ | ||
|
r43359 | from mercurial.pycompat import ( | ||
getattr, | ||||
open, | ||||
) | ||||
|
r28413 | from mercurial import ( | ||
encoding, | ||||
error, | ||||
util, | ||||
) | ||||
|
r37138 | from mercurial.utils import ( | ||
dateutil, | ||||
procutil, | ||||
) | ||||
|
r4536 | |||
|
r28413 | from . import ( | ||
common, | ||||
cvsps, | ||||
) | ||||
|
r28861 | stringio = util.stringio | ||
|
r28413 | checktool = common.checktool | ||
commit = common.commit | ||||
converter_source = common.converter_source | ||||
makedatetimestamp = common.makedatetimestamp | ||||
NoRepo = common.NoRepo | ||||
|
r4536 | |||
|
r43346 | |||
|
r4536 | class convert_cvs(converter_source): | ||
|
r35168 | def __init__(self, ui, repotype, path, revs=None): | ||
super(convert_cvs, self).__init__(ui, repotype, path, revs=revs) | ||||
|
r4807 | |||
|
r43347 | cvs = os.path.join(path, b"CVS") | ||
|
r4536 | if not os.path.exists(cvs): | ||
|
r43347 | raise NoRepo(_(b"%s does not look like a CVS checkout") % path) | ||
|
r4536 | |||
|
r43347 | checktool(b'cvs') | ||
|
r5497 | |||
|
r8048 | self.changeset = None | ||
|
r4536 | self.files = {} | ||
self.tags = {} | ||||
self.lastbranch = {} | ||||
self.socket = None | ||||
|
r43347 | self.cvsroot = open(os.path.join(cvs, b"Root"), b'rb').read()[:-1] | ||
self.cvsrepo = open(os.path.join(cvs, b"Repository"), b'rb').read()[:-1] | ||||
|
r11987 | self.encoding = encoding.encoding | ||
|
r6690 | |||
|
r4536 | self._connect() | ||
|
r8048 | def _parse(self): | ||
if self.changeset is not None: | ||||
|
r4536 | return | ||
|
r8048 | self.changeset = {} | ||
|
r4536 | |||
|
r4760 | maxrev = 0 | ||
|
r25748 | if self.revs: | ||
if len(self.revs) > 1: | ||||
|
r43346 | raise error.Abort( | ||
|
r43347 | _( | ||
b'cvs source does not support specifying ' | ||||
b'multiple revs' | ||||
) | ||||
|
r43346 | ) | ||
|
r4760 | # TODO: handle tags | ||
try: | ||||
# patchset number? | ||||
|
r25748 | maxrev = int(self.revs[0]) | ||
|
r4760 | except ValueError: | ||
|
r43346 | raise error.Abort( | ||
|
r43347 | _(b'revision %s is not a patchset number') % self.revs[0] | ||
|
r43346 | ) | ||
|
r4760 | |||
|
r39843 | d = encoding.getcwd() | ||
|
r4536 | try: | ||
os.chdir(self.path) | ||||
|
r6690 | |||
|
r43347 | cache = b'update' | ||
if not self.ui.configbool(b'convert', b'cvsps.cache'): | ||||
|
r9543 | cache = None | ||
db = cvsps.createlog(self.ui, cache=cache) | ||||
|
r43346 | db = cvsps.createchangeset( | ||
self.ui, | ||||
db, | ||||
|
r43347 | fuzz=int(self.ui.config(b'convert', b'cvsps.fuzz')), | ||
mergeto=self.ui.config(b'convert', b'cvsps.mergeto'), | ||||
mergefrom=self.ui.config(b'convert', b'cvsps.mergefrom'), | ||||
|
r43346 | ) | ||
|
r5920 | |||
|
r9543 | for cs in db: | ||
|
r10282 | if maxrev and cs.id > maxrev: | ||
|
r9543 | break | ||
|
r43346 | id = b"%d" % cs.id | ||
|
r9543 | cs.author = self.recode(cs.author) | ||
self.lastbranch[cs.branch] = id | ||||
cs.comment = self.recode(cs.comment) | ||||
|
r43347 | if self.ui.configbool(b'convert', b'localtimezone'): | ||
|
r17974 | cs.date = makedatetimestamp(cs.date[0]) | ||
|
r43347 | date = dateutil.datestr(cs.date, b'%Y-%m-%d %H:%M:%S %1%2') | ||
|
r9543 | self.tags.update(dict.fromkeys(cs.tags, id)) | ||
|
r5920 | |||
|
r9543 | files = {} | ||
for f in cs.entries: | ||||
|
r43347 | files[f.file] = b"%s%s" % ( | ||
b'.'.join([(b"%d" % x) for x in f.revision]), | ||||
[b'', b'(DEAD)'][f.dead], | ||||
|
r43346 | ) | ||
|
r6690 | |||
|
r9543 | # add current commit to set | ||
|
r43346 | c = commit( | ||
author=cs.author, | ||||
date=date, | ||||
parents=[(b"%d" % p.id) for p in cs.parents], | ||||
desc=cs.comment, | ||||
|
r43347 | branch=cs.branch or b'', | ||
|
r43346 | ) | ||
|
r9543 | self.changeset[id] = c | ||
self.files[id] = files | ||||
|
r4536 | |||
self.heads = self.lastbranch.values() | ||||
finally: | ||||
os.chdir(d) | ||||
def _connect(self): | ||||
root = self.cvsroot | ||||
conntype = None | ||||
user, host = None, None | ||||
|
r43347 | cmd = [b'cvs', b'server'] | ||
|
r4536 | |||
|
r43347 | self.ui.status(_(b"connecting to %s\n") % root) | ||
|
r4536 | |||
|
r43347 | if root.startswith(b":pserver:"): | ||
|
r4536 | root = root[9:] | ||
|
r43346 | m = re.match( | ||
r'(?:(.*?)(?::(.*?))?@)?([^:\/]*)(?::(\d*))?(.*)', root | ||||
) | ||||
|
r4536 | if m: | ||
|
r43347 | conntype = b"pserver" | ||
|
r4536 | user, passw, serv, port, root = m.groups() | ||
if not user: | ||||
|
r43347 | user = b"anonymous" | ||
|
r5082 | if not port: | ||
port = 2401 | ||||
|
r4536 | else: | ||
|
r5082 | port = int(port) | ||
|
r43347 | format0 = b":pserver:%s@%s:%s" % (user, serv, root) | ||
format1 = b":pserver:%s@%s:%d%s" % (user, serv, port, root) | ||||
|
r4536 | |||
if not passw: | ||||
|
r43347 | passw = b"A" | ||
cvspass = os.path.expanduser(b"~/.cvspass") | ||||
|
r7444 | try: | ||
|
r43347 | pf = open(cvspass, b'rb') | ||
|
r7442 | for line in pf.read().splitlines(): | ||
|
r43347 | part1, part2 = line.split(b' ', 1) | ||
|
r16683 | # /1 :pserver:user@example.com:2401/cvsroot/foo | ||
# Ah<Z | ||||
|
r43347 | if part1 == b'/1': | ||
part1, part2 = part2.split(b' ', 1) | ||||
|
r7442 | format = format1 | ||
|
r16683 | # :pserver:user@example.com:/cvsroot/foo Ah<Z | ||
|
r7442 | else: | ||
format = format0 | ||||
if part1 == format: | ||||
passw = part2 | ||||
break | ||||
pf.close() | ||||
|
r25660 | except IOError as inst: | ||
|
r7444 | if inst.errno != errno.ENOENT: | ||
if not getattr(inst, 'filename', None): | ||||
inst.filename = cvspass | ||||
raise | ||||
|
r4536 | |||
sck = socket.socket() | ||||
sck.connect((serv, port)) | ||||
|
r43346 | sck.send( | ||
|
r43347 | b"\n".join( | ||
|
r43346 | [ | ||
|
r43347 | b"BEGIN AUTH REQUEST", | ||
|
r43346 | root, | ||
user, | ||||
passw, | ||||
|
r43347 | b"END AUTH REQUEST", | ||
b"", | ||||
|
r43346 | ] | ||
) | ||||
) | ||||
|
r43347 | if sck.recv(128) != b"I LOVE YOU\n": | ||
raise error.Abort(_(b"CVS pserver authentication failed")) | ||||
|
r4536 | |||
|
r43347 | self.writep = self.readp = sck.makefile(b'r+') | ||
|
r4536 | |||
|
r43347 | if not conntype and root.startswith(b":local:"): | ||
conntype = b"local" | ||||
|
r4536 | root = root[7:] | ||
if not conntype: | ||||
# :ext:user@host/home/user/path/to/cvsroot | ||||
|
r43347 | if root.startswith(b":ext:"): | ||
|
r4536 | root = root[5:] | ||
|
r36411 | m = re.match(br'(?:([^@:/]+)@)?([^:/]+):?(.*)', root) | ||
|
r5304 | # Do not take Windows path "c:\foo\bar" for a connection strings | ||
if os.path.isdir(root) or not m: | ||||
|
r43347 | conntype = b"local" | ||
|
r4536 | else: | ||
|
r43347 | conntype = b"rsh" | ||
|
r4536 | user, host, root = m.group(1), m.group(2), m.group(3) | ||
|
r43347 | if conntype != b"pserver": | ||
if conntype == b"rsh": | ||||
rsh = encoding.environ.get(b"CVS_RSH") or b"ssh" | ||||
|
r4536 | if user: | ||
|
r43347 | cmd = [rsh, b'-l', user, host] + cmd | ||
|
r4536 | else: | ||
cmd = [rsh, host] + cmd | ||||
|
r5303 | # popen2 does not support argument lists under Windows | ||
|
r37138 | cmd = [procutil.shellquote(arg) for arg in cmd] | ||
|
r43347 | cmd = procutil.quotecommand(b' '.join(cmd)) | ||
|
r37138 | self.writep, self.readp = procutil.popen2(cmd) | ||
|
r4536 | |||
self.realroot = root | ||||
|
r43347 | self.writep.write(b"Root %s\n" % root) | ||
|
r43346 | self.writep.write( | ||
|
r43347 | b"Valid-responses ok error Valid-requests Mode" | ||
b" M Mbinary E Checked-in Created Updated" | ||||
b" Merged Removed\n" | ||||
|
r43346 | ) | ||
|
r43347 | self.writep.write(b"valid-requests\n") | ||
|
r4536 | self.writep.flush() | ||
r = self.readp.readline() | ||||
|
r43347 | if not r.startswith(b"Valid-requests"): | ||
|
r43346 | raise error.Abort( | ||
_( | ||||
|
r43347 | b'unexpected response from CVS server ' | ||
b'(expected "Valid-requests", but got %r)' | ||||
|
r43346 | ) | ||
% r | ||||
) | ||||
|
r43347 | if b"UseUnchanged" in r: | ||
self.writep.write(b"UseUnchanged\n") | ||||
|
r4536 | self.writep.flush() | ||
|
r41401 | self.readp.readline() | ||
|
r4536 | |||
def getheads(self): | ||||
|
r8048 | self._parse() | ||
|
r4536 | return self.heads | ||
|
r11134 | def getfile(self, name, rev): | ||
|
r5539 | def chunkedread(fp, count): | ||
|
r17424 | # file-objects returned by socket.makefile() do not handle | ||
|
r5539 | # large read() requests very well. | ||
chunksize = 65536 | ||||
|
r28861 | output = stringio() | ||
|
r5539 | while count > 0: | ||
data = fp.read(min(count, chunksize)) | ||||
if not data: | ||||
|
r43346 | raise error.Abort( | ||
|
r43347 | _(b"%d bytes missing from remote file") % count | ||
|
r43346 | ) | ||
|
r5539 | count -= len(data) | ||
output.write(data) | ||||
return output.getvalue() | ||||
|
r11134 | self._parse() | ||
|
r43347 | if rev.endswith(b"(DEAD)"): | ||
|
r22296 | return None, None | ||
|
r4536 | |||
|
r43347 | args = (b"-N -P -kk -r %s --" % rev).split() | ||
args.append(self.cvsrepo + b'/' + name) | ||||
|
r4536 | for x in args: | ||
|
r43347 | self.writep.write(b"Argument %s\n" % x) | ||
self.writep.write(b"Directory .\n%s\nco\n" % self.realroot) | ||||
|
r4536 | self.writep.flush() | ||
|
r43347 | data = b"" | ||
|
r10800 | mode = None | ||
|
r14494 | while True: | ||
|
r4536 | line = self.readp.readline() | ||
|
r43347 | if line.startswith(b"Created ") or line.startswith(b"Updated "): | ||
|
r43346 | self.readp.readline() # path | ||
self.readp.readline() # entries | ||||
|
r4536 | mode = self.readp.readline()[:-1] | ||
count = int(self.readp.readline()[:-1]) | ||||
|
r5539 | data = chunkedread(self.readp, count) | ||
|
r43347 | elif line.startswith(b" "): | ||
|
r4536 | data += line[1:] | ||
|
r43347 | elif line.startswith(b"M "): | ||
|
r4536 | pass | ||
|
r43347 | elif line.startswith(b"Mbinary "): | ||
|
r4536 | count = int(self.readp.readline()[:-1]) | ||
|
r5539 | data = chunkedread(self.readp, count) | ||
|
r4536 | else: | ||
|
r43347 | if line == b"ok\n": | ||
|
r10800 | if mode is None: | ||
|
r43347 | raise error.Abort(_(b'malformed response from CVS')) | ||
return (data, b"x" in mode and b"x" or b"") | ||||
elif line.startswith(b"E "): | ||||
self.ui.warn(_(b"cvs server: %s\n") % line[2:]) | ||||
elif line.startswith(b"Remove"): | ||||
|
r7874 | self.readp.readline() | ||
|
r4536 | else: | ||
|
r43347 | raise error.Abort(_(b"unknown CVS response: %s") % line) | ||
|
r4536 | |||
|
r22300 | def getchanges(self, rev, full): | ||
if full: | ||||
|
r43347 | raise error.Abort(_(b"convert from cvs does not support --full")) | ||
|
r8048 | self._parse() | ||
|
r24395 | return sorted(self.files[rev].iteritems()), {}, set() | ||
|
r4536 | |||
def getcommit(self, rev): | ||||
|
r8048 | self._parse() | ||
|
r4536 | return self.changeset[rev] | ||
def gettags(self): | ||||
|
r8048 | self._parse() | ||
|
r4536 | return self.tags | ||
|
r5381 | |||
def getchangedfiles(self, rev, i): | ||||
|
r8048 | self._parse() | ||
|
r8209 | return sorted(self.files[rev]) | ||