Changeset View
Changeset View
Standalone View
Standalone View
swh/loader/cvs/cvsclient.py
# Copyright (C) 2015-2021 The Software Heritage developers | # Copyright (C) 2015-2021 The Software Heritage developers | ||||
# See the AUTHORS file at the top-level directory of this distribution | # See the AUTHORS file at the top-level directory of this distribution | ||||
# License: GNU Affero General Public License version 3, or any later version | # License: GNU Affero General Public License version 3, or any later version | ||||
# See top-level LICENSE file for more information | # See top-level LICENSE file for more information | ||||
"""Minimal CVS client implementation | """Minimal CVS client implementation | ||||
""" | """ | ||||
import os.path | |||||
import re | |||||
import socket | import socket | ||||
import subprocess | import subprocess | ||||
import os.path | |||||
import tempfile | import tempfile | ||||
import re | |||||
from swh.loader.exception import NotFound | from swh.loader.exception import NotFound | ||||
CVS_PSERVER_PORT = 2401 | CVS_PSERVER_PORT = 2401 | ||||
CVS_PROTOCOL_BUFFER_SIZE = 8192 | CVS_PROTOCOL_BUFFER_SIZE = 8192 | ||||
EXAMPLE_PSERVER_URL = "pserver://user:password@cvs.example.com/cvsroot/repository" | EXAMPLE_PSERVER_URL = "pserver://user:password@cvs.example.com/cvsroot/repository" | ||||
EXAMPLE_SSH_URL = "ssh://user@cvs.example.com/cvsroot/repository" | EXAMPLE_SSH_URL = "ssh://user@cvs.example.com/cvsroot/repository" | ||||
VALID_RESPONSES = ["ok", "error", "Valid-requests", "Checked-in", | VALID_RESPONSES = [ | ||||
"New-entry", "Checksum", "Copy-file", "Updated", "Created", | "ok", | ||||
"Update-existing", "Merged", "Patched", "Rcs-diff", "Mode", | "error", | ||||
"Removed", "Remove-entry", "Template", "Notified", | "Valid-requests", | ||||
"Module-expansion", "Wrapper-rcsOption", "M", "Mbinary", | "Checked-in", | ||||
"E", "F", "MT"] | "New-entry", | ||||
"Checksum", | |||||
"Copy-file", | |||||
"Updated", | |||||
"Created", | |||||
"Update-existing", | |||||
"Merged", | |||||
"Patched", | |||||
"Rcs-diff", | |||||
"Mode", | |||||
"Removed", | |||||
"Remove-entry", | |||||
"Template", | |||||
"Notified", | |||||
"Module-expansion", | |||||
"Wrapper-rcsOption", | |||||
"M", | |||||
"Mbinary", | |||||
"E", | |||||
"F", | |||||
"MT", | |||||
] | |||||
# Trivially encode strings to protect them from innocent eyes (i.e., | # Trivially encode strings to protect them from innocent eyes (i.e., | ||||
# inadvertent password compromises, like a network administrator | # inadvertent password compromises, like a network administrator | ||||
# who's watching packets for legitimate reasons and accidentally sees | # who's watching packets for legitimate reasons and accidentally sees | ||||
# the password protocol go by). | # the password protocol go by). | ||||
# | # | ||||
# This is NOT secure encryption. | # This is NOT secure encryption. | ||||
def scramble_password(password): | def scramble_password(password): | ||||
s = ['A'] # scramble scheme version number | s = ["A"] # scramble scheme version number | ||||
# fmt: off | # fmt: off | ||||
scramble_shifts = [ | scramble_shifts = [ | ||||
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, # noqa: E241 | 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, # noqa: E241 | ||||
16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, # noqa: E241,E131,E501 | 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, # noqa: E241,E131,E501 | ||||
114,120, 53, 79, 96,109, 72,108, 70, 64, 76, 67,116, 74, 68, 87, # noqa: E241,E131,E501 | 114,120, 53, 79, 96,109, 72,108, 70, 64, 76, 67,116, 74, 68, 87, # noqa: E241,E131,E501 | ||||
111, 52, 75,119, 49, 34, 82, 81, 95, 65,112, 86,118,110,122,105, # noqa: E241,E131,E501 | 111, 52, 75,119, 49, 34, 82, 81, 95, 65,112, 86,118,110,122,105, # noqa: E241,E131,E501 | ||||
41, 57, 83, 43, 46,102, 40, 89, 38,103, 45, 50, 42,123, 91, 35, # noqa: E241,E131,E501 | 41, 57, 83, 43, 46,102, 40, 89, 38,103, 45, 50, 42,123, 91, 35, # noqa: E241,E131,E501 | ||||
125, 55, 54, 66,124,126, 59, 47, 92, 71,115, 78, 88,107,106, 56, # noqa: E241,E131,E501 | 125, 55, 54, 66,124,126, 59, 47, 92, 71,115, 78, 88,107,106, 56, # noqa: E241,E131,E501 | ||||
36,121,117,104,101,100, 69, 73, 99, 63, 94, 93, 39, 37, 61, 48, # noqa: E241,E131,E501 | 36,121,117,104,101,100, 69, 73, 99, 63, 94, 93, 39, 37, 61, 48, # noqa: E241,E131,E501 | ||||
58,113, 32, 90, 44, 98, 60, 51, 33, 97, 62, 77, 84, 80, 85,223, # noqa: E241,E131,E501 | 58,113, 32, 90, 44, 98, 60, 51, 33, 97, 62, 77, 84, 80, 85,223, # noqa: E241,E131,E501 | ||||
225,216,187,166,229,189,222,188,141,249,148,200,184,136,248,190, # noqa: E241,E131,E501 | 225,216,187,166,229,189,222,188,141,249,148,200,184,136,248,190, # noqa: E241,E131,E501 | ||||
199,170,181,204,138,232,218,183,255,234,220,247,213,203,226,193, # noqa: E241,E131,E501 | 199,170,181,204,138,232,218,183,255,234,220,247,213,203,226,193, # noqa: E241,E131,E501 | ||||
174,172,228,252,217,201,131,230,197,211,145,238,161,179,160,212, # noqa: E241,E131,E501 | 174,172,228,252,217,201,131,230,197,211,145,238,161,179,160,212, # noqa: E241,E131,E501 | ||||
207,221,254,173,202,146,224,151,140,196,205,130,135,133,143,246, # noqa: E241,E131,E501 | 207,221,254,173,202,146,224,151,140,196,205,130,135,133,143,246, # noqa: E241,E131,E501 | ||||
192,159,244,239,185,168,215,144,139,165,180,157,147,186,214,176, # noqa: E241,E131,E501 | 192,159,244,239,185,168,215,144,139,165,180,157,147,186,214,176, # noqa: E241,E131,E501 | ||||
227,231,219,169,175,156,206,198,129,164,150,210,154,177,134,127, # noqa: E241,E131,E501 | 227,231,219,169,175,156,206,198,129,164,150,210,154,177,134,127, # noqa: E241,E131,E501 | ||||
182,128,158,208,162,132,167,209,149,241,153,251,237,236,171,195, # noqa: E241,E131,E501 | 182,128,158,208,162,132,167,209,149,241,153,251,237,236,171,195, # noqa: E241,E131,E501 | ||||
243,233,253,240,194,250,191,155,142,137,245,235,163,242,178,152] # noqa: E241,E131,E501 | 243,233,253,240,194,250,191,155,142,137,245,235,163,242,178,152] # noqa: E241,E131,E501 | ||||
# fmt: on | # fmt: on | ||||
for c in password: | for c in password: | ||||
s.append('%c' % scramble_shifts[ord(c)]) | s.append("%c" % scramble_shifts[ord(c)]) | ||||
return "".join(s) | return "".join(s) | ||||
class CVSProtocolError(Exception): | class CVSProtocolError(Exception): | ||||
pass | pass | ||||
_re_kb_opt = re.compile(b'\/-kb\/') # noqa: W605 | _re_kb_opt = re.compile(b"\/-kb\/") # noqa: W605 | ||||
class CVSClient: | class CVSClient: | ||||
def connect_pserver(self, hostname, port, auth): | def connect_pserver(self, hostname, port, auth): | ||||
if port is None: | if port is None: | ||||
port = CVS_PSERVER_PORT | port = CVS_PSERVER_PORT | ||||
if auth is None: | if auth is None: | ||||
raise NotFound("Username and password are required for " | raise NotFound( | ||||
"a pserver connection: %s" % EXAMPLE_PSERVER_URL) | "Username and password are required for " | ||||
"a pserver connection: %s" % EXAMPLE_PSERVER_URL | |||||
) | |||||
try: | try: | ||||
user = auth.split(':')[0] | user = auth.split(":")[0] | ||||
password = auth.split(':')[1] | password = auth.split(":")[1] | ||||
except IndexError: | except IndexError: | ||||
raise NotFound("Username and password are required for " | raise NotFound( | ||||
"a pserver connection: %s" % EXAMPLE_PSERVER_URL) | "Username and password are required for " | ||||
"a pserver connection: %s" % EXAMPLE_PSERVER_URL | |||||
) | |||||
try: | try: | ||||
self.socket = socket.create_connection((hostname, port)) | self.socket = socket.create_connection((hostname, port)) | ||||
except ConnectionRefusedError: | except ConnectionRefusedError: | ||||
raise NotFound("Could not connect to %s:%s", hostname, port) | raise NotFound("Could not connect to %s:%s", hostname, port) | ||||
scrambled_password = scramble_password(password) | scrambled_password = scramble_password(password) | ||||
request = "BEGIN AUTH REQUEST\n%s\n%s\n%s\nEND AUTH REQUEST\n" \ | request = "BEGIN AUTH REQUEST\n%s\n%s\n%s\nEND AUTH REQUEST\n" % ( | ||||
% (self.cvsroot_path, user, scrambled_password) | self.cvsroot_path, | ||||
user, | |||||
scrambled_password, | |||||
) | |||||
print("Request: %s\n" % request) | print("Request: %s\n" % request) | ||||
self.socket.sendall(request.encode('UTF-8')) | self.socket.sendall(request.encode("UTF-8")) | ||||
response = self.conn_read_line() | response = self.conn_read_line() | ||||
if response != b"I LOVE YOU\n": | if response != b"I LOVE YOU\n": | ||||
raise NotFound("pserver authentication failed for %s:%s: %s" % | raise NotFound( | ||||
(hostname, port, response)) | "pserver authentication failed for %s:%s: %s" | ||||
% (hostname, port, response) | |||||
) | |||||
def connect_ssh(self, hostname, port, auth): | def connect_ssh(self, hostname, port, auth): | ||||
command = ['ssh'] | command = ["ssh"] | ||||
if auth is not None: | if auth is not None: | ||||
# Assume 'auth' contains only a user name. | # Assume 'auth' contains only a user name. | ||||
# We do not support password authentication with SSH since the | # We do not support password authentication with SSH since the | ||||
# anoncvs user is usually granted access without a password. | # anoncvs user is usually granted access without a password. | ||||
command += ['-l' , '%s' % auth] | command += ["-l", "%s" % auth] | ||||
if port is not None: | if port is not None: | ||||
command += ['-p' , '%d' % port] | command += ["-p", "%d" % port] | ||||
# accept new SSH hosts keys upon first use; changed host keys | # accept new SSH hosts keys upon first use; changed host keys | ||||
# will require intervention | # will require intervention | ||||
command += ['-o', "StrictHostKeyChecking=accept-new"] | command += ["-o", "StrictHostKeyChecking=accept-new"] | ||||
# disable interactive prompting | # disable interactive prompting | ||||
command += ['-o', "BatchMode=yes"] | command += ["-o", "BatchMode=yes"] | ||||
# disable further option processing by adding '--' | # disable further option processing by adding '--' | ||||
command += ['--'] | command += ["--"] | ||||
command += ['%s' % hostname, 'cvs', 'server'] | command += ["%s" % hostname, "cvs", "server"] | ||||
# use non-buffered I/O to match behaviour of self.socket | # use non-buffered I/O to match behaviour of self.socket | ||||
self.ssh = subprocess.Popen(command, | self.ssh = subprocess.Popen( | ||||
bufsize=0, | command, bufsize=0, stdin=subprocess.PIPE, stdout=subprocess.PIPE | ||||
stdin=subprocess.PIPE, stdout=subprocess.PIPE) | ) | ||||
def connect_fake(self, hostname, port, auth): | def connect_fake(self, hostname, port, auth): | ||||
command = ['cvs', 'server'] | command = ["cvs", "server"] | ||||
# use non-buffered I/O to match behaviour of self.socket | # use non-buffered I/O to match behaviour of self.socket | ||||
self.ssh = subprocess.Popen(command, | self.ssh = subprocess.Popen( | ||||
bufsize=0, | command, bufsize=0, stdin=subprocess.PIPE, stdout=subprocess.PIPE | ||||
stdin=subprocess.PIPE, stdout=subprocess.PIPE) | ) | ||||
def conn_read_line(self, require_newline=True): | def conn_read_line(self, require_newline=True): | ||||
if len(self.linebuffer) != 0: | if len(self.linebuffer) != 0: | ||||
return self.linebuffer.pop(0) | return self.linebuffer.pop(0) | ||||
buf = b'' | buf = b"" | ||||
idx = -1 | idx = -1 | ||||
while idx == -1: | while idx == -1: | ||||
if len(buf) >= CVS_PROTOCOL_BUFFER_SIZE: | if len(buf) >= CVS_PROTOCOL_BUFFER_SIZE: | ||||
if require_newline: | if require_newline: | ||||
raise CVSProtocolError("Overlong response from " | raise CVSProtocolError( | ||||
"CVS server: %s" % buf) | "Overlong response from " "CVS server: %s" % buf | ||||
) | |||||
else: | else: | ||||
break | break | ||||
if self.socket: | if self.socket: | ||||
buf += self.socket.recv(CVS_PROTOCOL_BUFFER_SIZE) | buf += self.socket.recv(CVS_PROTOCOL_BUFFER_SIZE) | ||||
elif self.ssh: | elif self.ssh: | ||||
buf += self.ssh.stdout.read(CVS_PROTOCOL_BUFFER_SIZE) | buf += self.ssh.stdout.read(CVS_PROTOCOL_BUFFER_SIZE) | ||||
else: | else: | ||||
raise Exception("No valid connection") | raise Exception("No valid connection") | ||||
if not buf: | if not buf: | ||||
return None | return None | ||||
idx = buf.rfind(b'\n') | idx = buf.rfind(b"\n") | ||||
if idx != -1: | if idx != -1: | ||||
self.linebuffer = buf[:idx + 1].splitlines(keepends=True) | self.linebuffer = buf[: idx + 1].splitlines(keepends=True) | ||||
else: | else: | ||||
if require_newline: | if require_newline: | ||||
raise CVSProtocolError("Invalid response from CVS server: %s" % buf) | raise CVSProtocolError("Invalid response from CVS server: %s" % buf) | ||||
else: | else: | ||||
self.linebuffer.append(buf) | self.linebuffer.append(buf) | ||||
if len(self.incomplete_line) > 0: | if len(self.incomplete_line) > 0: | ||||
self.linebuffer[0] = self.incomplete_line + self.linebuffer[0] | self.linebuffer[0] = self.incomplete_line + self.linebuffer[0] | ||||
if idx != -1: | if idx != -1: | ||||
self.incomplete_line = buf[idx + 1:] | self.incomplete_line = buf[idx + 1 :] | ||||
else: | else: | ||||
self.incomplete_line = b'' | self.incomplete_line = b"" | ||||
return self.linebuffer.pop(0) | return self.linebuffer.pop(0) | ||||
def conn_write(self, data): | def conn_write(self, data): | ||||
if self.socket: | if self.socket: | ||||
return self.socket.sendall(data) | return self.socket.sendall(data) | ||||
if self.ssh: | if self.ssh: | ||||
self.ssh.stdin.write(data) | self.ssh.stdin.write(data) | ||||
return self.ssh.stdin.flush() | return self.ssh.stdin.flush() | ||||
raise Exception("No valid connection") | raise Exception("No valid connection") | ||||
def conn_write_str(self, s): | def conn_write_str(self, s): | ||||
return self.conn_write(s.encode('UTF-8')) | return self.conn_write(s.encode("UTF-8")) | ||||
def conn_close(self): | def conn_close(self): | ||||
if self.socket: | if self.socket: | ||||
self.socket.close() | self.socket.close() | ||||
if self.ssh: | if self.ssh: | ||||
self.ssh.kill() | self.ssh.kill() | ||||
try: | try: | ||||
self.ssh.wait(timeout=10) | self.ssh.wait(timeout=10) | ||||
except subprocess.TimeoutExpired as e: | except subprocess.TimeoutExpired as e: | ||||
raise subprocess.TimeoutExpired("Could not terminate " | raise subprocess.TimeoutExpired( | ||||
"ssh program: %s" % e) | "Could not terminate " "ssh program: %s" % e | ||||
) | |||||
def __init__(self, url): | def __init__(self, url): | ||||
""" | """ | ||||
Connect to a CVS server at the specified URL and perform the initial | Connect to a CVS server at the specified URL and perform the initial | ||||
CVS protocol handshake. | CVS protocol handshake. | ||||
""" | """ | ||||
self.hostname = url.host | self.hostname = url.host | ||||
self.cvsroot_path = os.path.dirname(url.path) | self.cvsroot_path = os.path.dirname(url.path) | ||||
self.cvs_module_name = os.path.basename(url.path) | self.cvs_module_name = os.path.basename(url.path) | ||||
self.socket = None | self.socket = None | ||||
self.ssh = None | self.ssh = None | ||||
self.linebuffer = list() | self.linebuffer = list() | ||||
self.incomplete_line = b'' | self.incomplete_line = b"" | ||||
if url.scheme == 'pserver': | if url.scheme == "pserver": | ||||
self.connect_pserver(url.host, url.port, url.auth) | self.connect_pserver(url.host, url.port, url.auth) | ||||
elif url.scheme == 'ssh': | elif url.scheme == "ssh": | ||||
self.connect_ssh(url.host, url.port, url.auth) | self.connect_ssh(url.host, url.port, url.auth) | ||||
elif url.scheme == 'fake': | elif url.scheme == "fake": | ||||
self.connect_fake(url.host, url.port, url.auth) | self.connect_fake(url.host, url.port, url.auth) | ||||
else: | else: | ||||
raise NotFound("Invalid CVS origin URL '%s'" % url) | raise NotFound("Invalid CVS origin URL '%s'" % url) | ||||
# we should have a connection now | # we should have a connection now | ||||
assert self.socket or self.ssh | assert self.socket or self.ssh | ||||
self.conn_write_str("Root %s\nValid-responses %s\nvalid-requests\n" | self.conn_write_str( | ||||
"UseUnchanged\n" % | "Root %s\nValid-responses %s\nvalid-requests\n" | ||||
(self.cvsroot_path, ' '.join(VALID_RESPONSES))) | "UseUnchanged\n" % (self.cvsroot_path, " ".join(VALID_RESPONSES)) | ||||
) | |||||
response = self.conn_read_line() | response = self.conn_read_line() | ||||
if not response: | if not response: | ||||
raise CVSProtocolError("No response from CVS server") | raise CVSProtocolError("No response from CVS server") | ||||
try: | try: | ||||
if response[0:15] != b"Valid-requests ": | if response[0:15] != b"Valid-requests ": | ||||
raise CVSProtocolError("Invalid response from " | raise CVSProtocolError( | ||||
"CVS server: %s" % response) | "Invalid response from " "CVS server: %s" % response | ||||
) | |||||
except IndexError: | except IndexError: | ||||
raise CVSProtocolError("Invalid response from CVS server: %s" % response) | raise CVSProtocolError("Invalid response from CVS server: %s" % response) | ||||
response = self.conn_read_line() | response = self.conn_read_line() | ||||
if response != b"ok\n": | if response != b"ok\n": | ||||
raise CVSProtocolError("Invalid response from CVS server: %s" % response) | raise CVSProtocolError("Invalid response from CVS server: %s" % response) | ||||
def __del__(self): | def __del__(self): | ||||
self.conn_close() | self.conn_close() | ||||
def _parse_rlog_response(self, fp): | def _parse_rlog_response(self, fp): | ||||
rlog_output = tempfile.TemporaryFile() | rlog_output = tempfile.TemporaryFile() | ||||
expect_error = False | expect_error = False | ||||
for line in fp.readlines(): | for line in fp.readlines(): | ||||
if expect_error: | if expect_error: | ||||
raise CVSProtocolError('CVS server error: %s' % line) | raise CVSProtocolError("CVS server error: %s" % line) | ||||
if line == b'ok\n': | if line == b"ok\n": | ||||
break | break | ||||
elif line == b'M \n': | elif line == b"M \n": | ||||
continue | continue | ||||
elif line[0:2] == b'M ': | elif line[0:2] == b"M ": | ||||
rlog_output.write(line[2:]) | rlog_output.write(line[2:]) | ||||
elif line[0:8] == b'MT text ': | elif line[0:8] == b"MT text ": | ||||
rlog_output.write(line[8:-1]) | rlog_output.write(line[8:-1]) | ||||
elif line[0:8] == b'MT date ': | elif line[0:8] == b"MT date ": | ||||
rlog_output.write(line[8:-1]) | rlog_output.write(line[8:-1]) | ||||
elif line[0:10] == b'MT newline': | elif line[0:10] == b"MT newline": | ||||
rlog_output.write(line[10:]) | rlog_output.write(line[10:]) | ||||
elif line[0:7] == b'error ': | elif line[0:7] == b"error ": | ||||
expect_error = True | expect_error = True | ||||
continue | continue | ||||
else: | else: | ||||
raise CVSProtocolError('Bad CVS protocol response: %s' % line) | raise CVSProtocolError("Bad CVS protocol response: %s" % line) | ||||
rlog_output.seek(0) | rlog_output.seek(0) | ||||
return rlog_output | return rlog_output | ||||
def fetch_rlog(self): | def fetch_rlog(self): | ||||
fp = tempfile.TemporaryFile() | fp = tempfile.TemporaryFile() | ||||
self.conn_write_str("Global_option -q\nArgument --\nArgument %s\nrlog\n" % | self.conn_write_str( | ||||
self.cvs_module_name) | "Global_option -q\nArgument --\nArgument %s\nrlog\n" % self.cvs_module_name | ||||
) | |||||
while True: | while True: | ||||
response = self.conn_read_line() | response = self.conn_read_line() | ||||
if response is None: | if response is None: | ||||
raise CVSProtocolError("No response from CVS server") | raise CVSProtocolError("No response from CVS server") | ||||
if response[0:2] == b"E ": | if response[0:2] == b"E ": | ||||
raise CVSProtocolError("Error response from CVS server: %s" % response) | raise CVSProtocolError("Error response from CVS server: %s" % response) | ||||
fp.write(response) | fp.write(response) | ||||
if response == b"ok\n": | if response == b"ok\n": | ||||
break | break | ||||
fp.seek(0) | fp.seek(0) | ||||
return self._parse_rlog_response(fp) | return self._parse_rlog_response(fp) | ||||
def checkout(self, path, rev, dest_dir): | def checkout(self, path, rev, dest_dir): | ||||
skip_line = False | skip_line = False | ||||
expect_modeline = False | expect_modeline = False | ||||
expect_bytecount = False | expect_bytecount = False | ||||
have_bytecount = False | have_bytecount = False | ||||
bytecount = 0 | bytecount = 0 | ||||
dirname = os.path.dirname(path) | dirname = os.path.dirname(path) | ||||
if dirname: | if dirname: | ||||
self.conn_write_str("Directory %s\n%s\n" % (dirname, dirname)) | self.conn_write_str("Directory %s\n%s\n" % (dirname, dirname)) | ||||
filename = os.path.basename(path) | filename = os.path.basename(path) | ||||
co_output = tempfile.NamedTemporaryFile(dir=dest_dir, delete=True, | co_output = tempfile.NamedTemporaryFile( | ||||
prefix='cvsclient-checkout-%s-r%s-' % | dir=dest_dir, | ||||
(filename, rev)) | delete=True, | ||||
prefix="cvsclient-checkout-%s-r%s-" % (filename, rev), | |||||
) | |||||
# TODO: cvs <= 1.10 servers expect to be given every Directory along the path. | # TODO: cvs <= 1.10 servers expect to be given every Directory along the path. | ||||
self.conn_write_str("Directory %s\n%s\n" | self.conn_write_str( | ||||
"Directory %s\n%s\n" | |||||
"Global_option -q\n" | "Global_option -q\n" | ||||
"Argument -r%s\n" | "Argument -r%s\n" | ||||
"Argument -kb\n" | "Argument -kb\n" | ||||
"Argument --\nArgument %s\nco \n" % | "Argument --\nArgument %s\nco \n" | ||||
(self.cvs_module_name, self.cvs_module_name, rev, path)) | % (self.cvs_module_name, self.cvs_module_name, rev, path) | ||||
) | |||||
while True: | while True: | ||||
if have_bytecount and bytecount > 0: | if have_bytecount and bytecount > 0: | ||||
response = self.conn_read_line(require_newline=False) | response = self.conn_read_line(require_newline=False) | ||||
if response is None: | if response is None: | ||||
raise CVSProtocolError("Incomplete response from CVS server") | raise CVSProtocolError("Incomplete response from CVS server") | ||||
co_output.write(response) | co_output.write(response) | ||||
bytecount -= len(response) | bytecount -= len(response) | ||||
if bytecount < 0: | if bytecount < 0: | ||||
raise CVSProtocolError("Overlong response from " | raise CVSProtocolError( | ||||
"CVS server: %s" % response) | "Overlong response from " "CVS server: %s" % response | ||||
) | |||||
continue | continue | ||||
else: | else: | ||||
response = self.conn_read_line() | response = self.conn_read_line() | ||||
if response[0:2] == b'E ': | if response[0:2] == b"E ": | ||||
raise CVSProtocolError('Error from CVS server: %s' % response) | raise CVSProtocolError("Error from CVS server: %s" % response) | ||||
if have_bytecount and bytecount == 0 and response == b'ok\n': | if have_bytecount and bytecount == 0 and response == b"ok\n": | ||||
break | break | ||||
if skip_line: | if skip_line: | ||||
skip_line = False | skip_line = False | ||||
continue | continue | ||||
elif expect_bytecount: | elif expect_bytecount: | ||||
try: | try: | ||||
bytecount = int(response[0:-1]) # strip trailing \n | bytecount = int(response[0:-1]) # strip trailing \n | ||||
except ValueError: | except ValueError: | ||||
raise CVSProtocolError('Bad CVS protocol response: %s' % response) | raise CVSProtocolError("Bad CVS protocol response: %s" % response) | ||||
have_bytecount = True | have_bytecount = True | ||||
continue | continue | ||||
elif response == b'M \n': | elif response == b"M \n": | ||||
continue | continue | ||||
elif response == b'MT +updated\n': | elif response == b"MT +updated\n": | ||||
continue | continue | ||||
elif response == b'MT -updated\n': | elif response == b"MT -updated\n": | ||||
continue | continue | ||||
elif response[0:9] == b'MT fname ': | elif response[0:9] == b"MT fname ": | ||||
continue | continue | ||||
elif response[0:8] == b'Created ': | elif response[0:8] == b"Created ": | ||||
skip_line = True | skip_line = True | ||||
continue | continue | ||||
elif response[0:1] == b'/' and _re_kb_opt.search(response): | elif response[0:1] == b"/" and _re_kb_opt.search(response): | ||||
expect_modeline = True | expect_modeline = True | ||||
continue | continue | ||||
elif expect_modeline and response[0:2] == b'u=': | elif expect_modeline and response[0:2] == b"u=": | ||||
expect_modeline = False | expect_modeline = False | ||||
expect_bytecount = True | expect_bytecount = True | ||||
continue | continue | ||||
elif response[0:2] == b'M ': | elif response[0:2] == b"M ": | ||||
continue | continue | ||||
elif response[0:8] == b'MT text ': | elif response[0:8] == b"MT text ": | ||||
continue | continue | ||||
elif response[0:10] == b'MT newline': | elif response[0:10] == b"MT newline": | ||||
continue | continue | ||||
else: | else: | ||||
raise CVSProtocolError('Bad CVS protocol response: %s' % response) | raise CVSProtocolError("Bad CVS protocol response: %s" % response) | ||||
co_output.seek(0) | co_output.seek(0) | ||||
return co_output | return co_output |