/reviewboard/scmtools/cvs.py
https://github.com/ChrisTrimble/reviewboard · Python · 281 lines · 162 code · 57 blank · 62 comment · 37 complexity · dd95c7de9382edc8d47e5d2ab3f5d610 MD5 · raw file
- import os
- import re
- import subprocess
- import tempfile
- from djblets.util.filesystem import is_exe_in_path
- from reviewboard.scmtools import sshutils
- from reviewboard.scmtools.core import SCMTool, HEAD, PRE_CREATION
- from reviewboard.scmtools.errors import SCMError, FileNotFoundError, \
- RepositoryNotFoundError
- from reviewboard.diffviewer.parser import DiffParser, DiffParserError
- # Register these URI schemes so we can handle them properly.
- sshutils.ssh_uri_schemes.append('svn+ssh')
- class CVSTool(SCMTool):
- name = "CVS"
- supports_authentication = True
- dependencies = {
- 'executables': ['cvs'],
- }
- rev_re = re.compile(r'^.*?(\d+(\.\d+)+)\r?$')
- repopath_re = re.compile(r'^(?P<hostname>.*):(?P<port>\d+)?(?P<path>.*)')
- ext_cvsroot_re = re.compile(r':ext:([^@]+@)?(?P<hostname>[^:/]+)')
- def __init__(self, repository):
- SCMTool.__init__(self, repository)
- self.cvsroot, self.repopath = \
- self.build_cvsroot(self.repository.path,
- self.repository.username,
- self.repository.password)
- self.client = CVSClient(self.cvsroot, self.repopath)
- def get_file(self, path, revision=HEAD):
- if not path:
- raise FileNotFoundError(path, revision)
- return self.client.cat_file(path, revision)
- def parse_diff_revision(self, file_str, revision_str):
- if revision_str == "PRE-CREATION":
- return file_str, PRE_CREATION
- m = self.rev_re.match(revision_str)
- if not m:
- raise SCMError("Unable to parse diff revision header '%s'" %
- revision_str)
- return file_str, m.group(1)
- def get_diffs_use_absolute_paths(self):
- return True
- def get_fields(self):
- return ['diff_path']
- def get_parser(self, data):
- return CVSDiffParser(data, self.repopath)
- @classmethod
- def build_cvsroot(cls, path, username, password):
- # NOTE: According to cvs, the following formats are valid.
- #
- # :(gserver|kserver|pserver):[[user][:password]@]host[:[port]]/path
- # [:(ext|server):][[user]@]host[:]/path
- # :local:e:\path
- # :fork:/path
- if not path.startswith(":"):
- # The user has a path or something. We'll want to parse out the
- # server name, port (if specified) and path and build a :pserver:
- # CVSROOT.
- m = cls.repopath_re.match(path)
- if m:
- path = m.group("path")
- cvsroot = ":pserver:"
- if username:
- if password:
- cvsroot += '%s:%s@' % (username,
- password)
- else:
- cvsroot += '%s@' % (username)
- cvsroot += "%s:%s%s" % (m.group("hostname"),
- m.group("port") or "",
- path)
- return cvsroot, path
- # We couldn't parse this as a hostname:port/path. Assume it's a local
- # path or a full CVSROOT and let CVS handle it.
- return path, path
- @classmethod
- def check_repository(cls, path, username=None, password=None):
- """
- Performs checks on a repository to test its validity.
- This should check if a repository exists and can be connected to.
- This will also check if the repository requires an HTTPS certificate.
- The result is returned as an exception. The exception may contain
- extra information, such as a human-readable description of the problem.
- If the repository is valid and can be connected to, no exception
- will be thrown.
- """
- # CVS paths are a bit strange, so we can't actually use the
- # SSH checking in SCMTool.check_repository. Do our own.
- m = cls.ext_cvsroot_re.match(path)
- if m:
- sshutils.check_host(m.group('hostname'), username, password)
- cvsroot, repopath = cls.build_cvsroot(path, username, password)
- client = CVSClient(cvsroot, repopath)
- try:
- client.cat_file('CVSROOT/modules', HEAD)
- except (SCMError, FileNotFoundError):
- raise RepositoryNotFoundError()
- @classmethod
- def parse_hostname(cls, path):
- """Parses a hostname from a repository path."""
- return urlparse.urlparse(path)[1] # netloc
- class CVSDiffParser(DiffParser):
- """This class is able to parse diffs created with CVS. """
- regex_small = re.compile('^RCS file: (.+)$')
- def __init__(self, data, repo):
- DiffParser.__init__(self, data)
- self.regex_full = re.compile('^RCS file: %s/(.*),v$' % re.escape(repo))
- def parse_special_header(self, linenum, info):
- linenum = super(CVSDiffParser, self).parse_special_header(linenum, info)
- if 'index' not in info:
- # We didn't find an index, so the rest is probably bogus too.
- return linenum
- m = self.regex_full.match(self.lines[linenum])
- if not m:
- m = self.regex_small.match(self.lines[linenum])
- if m:
- info['filename'] = m.group(1)
- linenum += 1
- else:
- raise DiffParserError('Unable to find RCS line', linenum)
- while self.lines[linenum].startswith('retrieving '):
- linenum += 1
- if self.lines[linenum].startswith('diff '):
- linenum += 1
- return linenum
- def parse_diff_header(self, linenum, info):
- linenum = super(CVSDiffParser, self).parse_diff_header(linenum, info)
- if info.get('origFile') == '/dev/null':
- info['origFile'] = info['newFile']
- info['origInfo'] = 'PRE-CREATION'
- elif 'filename' in info:
- info['origFile'] = info['filename']
- return linenum
- class CVSClient:
- def __init__(self, repository, path):
- self.tempdir = ""
- self.currentdir = os.getcwd()
- self.repository = repository
- self.path = path
- if not is_exe_in_path('cvs'):
- # This is technically not the right kind of error, but it's the
- # pattern we use with all the other tools.
- raise ImportError
- def cleanup(self):
- if self.currentdir != os.getcwd():
- # Restore current working directory
- os.chdir(self.currentdir)
- # Remove temporary directory
- if self.tempdir != "":
- os.rmdir(self.tempdir)
- def cat_file(self, filename, revision):
- # We strip the repo off of the fully qualified path as CVS does
- # not like to be given absolute paths.
- repos_path = self.path.split(":")[-1]
- if filename.startswith(repos_path + "/"):
- filename = filename[len(repos_path) + 1:]
- # Strip off the ",v" we sometimes get for CVS paths.
- if filename.endswith(",v"):
- filename = filename.rstrip(",v")
- # We want to try to fetch the files with different permutations of
- # "Attic" and no "Attic". This means there are 4 various permutations
- # that we have to check, based on whether we're using windows- or
- # unix-type paths
- filenameAttic = filename
- if '/Attic/' in filename:
- filename = '/'.join(filename.rsplit('/Attic/', 1))
- elif '\\Attic\\' in filename:
- filename = '\\'.join(filename.rsplit('\\Attic\\', 1))
- elif '\\' in filename:
- pos = filename.rfind('\\')
- filenameAttic = filename[0:pos] + "\\Attic" + filename[pos:]
- else:
- pos = filename.rfind('/')
- filenameAttic = filename[0:pos] + "/Attic" + filename[pos:]
- try:
- return self._cat_specific_file(filename, revision)
- except FileNotFoundError:
- return self._cat_specific_file(filenameAttic, revision)
- def _cat_specific_file(self, filename, revision):
- # Somehow CVS sometimes seems to write .cvsignore files to current
- # working directory even though we force stdout with -p.
- self.tempdir = tempfile.mkdtemp()
- os.chdir(self.tempdir)
- p = subprocess.Popen(['cvs', '-f', '-d', self.repository, 'checkout',
- '-r', str(revision), '-p', filename],
- stderr=subprocess.PIPE, stdout=subprocess.PIPE,
- close_fds=(os.name != 'nt'))
- contents = p.stdout.read()
- errmsg = p.stderr.read()
- failure = p.wait()
- # Unfortunately, CVS is not consistent about exiting non-zero on
- # errors. If the file is not found at all, then CVS will print an
- # error message on stderr, but it doesn't set an exit code with
- # pservers. If the file is found but an invalid revision is requested,
- # then cvs exits zero and nothing is printed at all. (!)
- #
- # But, when it is successful, cvs will print a header on stderr like
- # so:
- #
- # ===================================================================
- # Checking out foobar
- # RCS: /path/to/repo/foobar,v
- # VERS: 1.1
- # ***************
- # So, if nothing is in errmsg, or errmsg has a specific recognized
- # message, call it FileNotFound.
- if not errmsg or \
- errmsg.startswith('cvs checkout: cannot find module') or \
- errmsg.startswith('cvs checkout: could not read RCS file'):
- self.cleanup()
- raise FileNotFoundError(filename, revision)
- # Otherwise, if there's an exit code, or errmsg doesn't look like
- # successful header, then call it a generic SCMError.
- #
- # If the .cvspass file doesn't exist, CVS will return an error message
- # stating this. This is safe to ignore.
- if (failure and not errmsg.startswith('==========')) and \
- not ".cvspass does not exist - creating new file" in errmsg:
- self.cleanup()
- raise SCMError(errmsg)
- self.cleanup()
- return contents