PageRenderTime 58ms CodeModel.GetById 30ms RepoModel.GetById 0ms app.codeStats 1ms

/reviewboard/scmtools/cvs.py

https://github.com/ChrisTrimble/reviewboard
Python | 281 lines | 250 code | 21 blank | 10 comment | 9 complexity | dd95c7de9382edc8d47e5d2ab3f5d610 MD5 | raw file
  1. import os
  2. import re
  3. import subprocess
  4. import tempfile
  5. from djblets.util.filesystem import is_exe_in_path
  6. from reviewboard.scmtools import sshutils
  7. from reviewboard.scmtools.core import SCMTool, HEAD, PRE_CREATION
  8. from reviewboard.scmtools.errors import SCMError, FileNotFoundError, \
  9. RepositoryNotFoundError
  10. from reviewboard.diffviewer.parser import DiffParser, DiffParserError
  11. # Register these URI schemes so we can handle them properly.
  12. sshutils.ssh_uri_schemes.append('svn+ssh')
  13. class CVSTool(SCMTool):
  14. name = "CVS"
  15. supports_authentication = True
  16. dependencies = {
  17. 'executables': ['cvs'],
  18. }
  19. rev_re = re.compile(r'^.*?(\d+(\.\d+)+)\r?$')
  20. repopath_re = re.compile(r'^(?P<hostname>.*):(?P<port>\d+)?(?P<path>.*)')
  21. ext_cvsroot_re = re.compile(r':ext:([^@]+@)?(?P<hostname>[^:/]+)')
  22. def __init__(self, repository):
  23. SCMTool.__init__(self, repository)
  24. self.cvsroot, self.repopath = \
  25. self.build_cvsroot(self.repository.path,
  26. self.repository.username,
  27. self.repository.password)
  28. self.client = CVSClient(self.cvsroot, self.repopath)
  29. def get_file(self, path, revision=HEAD):
  30. if not path:
  31. raise FileNotFoundError(path, revision)
  32. return self.client.cat_file(path, revision)
  33. def parse_diff_revision(self, file_str, revision_str):
  34. if revision_str == "PRE-CREATION":
  35. return file_str, PRE_CREATION
  36. m = self.rev_re.match(revision_str)
  37. if not m:
  38. raise SCMError("Unable to parse diff revision header '%s'" %
  39. revision_str)
  40. return file_str, m.group(1)
  41. def get_diffs_use_absolute_paths(self):
  42. return True
  43. def get_fields(self):
  44. return ['diff_path']
  45. def get_parser(self, data):
  46. return CVSDiffParser(data, self.repopath)
  47. @classmethod
  48. def build_cvsroot(cls, path, username, password):
  49. # NOTE: According to cvs, the following formats are valid.
  50. #
  51. # :(gserver|kserver|pserver):[[user][:password]@]host[:[port]]/path
  52. # [:(ext|server):][[user]@]host[:]/path
  53. # :local:e:\path
  54. # :fork:/path
  55. if not path.startswith(":"):
  56. # The user has a path or something. We'll want to parse out the
  57. # server name, port (if specified) and path and build a :pserver:
  58. # CVSROOT.
  59. m = cls.repopath_re.match(path)
  60. if m:
  61. path = m.group("path")
  62. cvsroot = ":pserver:"
  63. if username:
  64. if password:
  65. cvsroot += '%s:%s@' % (username,
  66. password)
  67. else:
  68. cvsroot += '%s@' % (username)
  69. cvsroot += "%s:%s%s" % (m.group("hostname"),
  70. m.group("port") or "",
  71. path)
  72. return cvsroot, path
  73. # We couldn't parse this as a hostname:port/path. Assume it's a local
  74. # path or a full CVSROOT and let CVS handle it.
  75. return path, path
  76. @classmethod
  77. def check_repository(cls, path, username=None, password=None):
  78. """
  79. Performs checks on a repository to test its validity.
  80. This should check if a repository exists and can be connected to.
  81. This will also check if the repository requires an HTTPS certificate.
  82. The result is returned as an exception. The exception may contain
  83. extra information, such as a human-readable description of the problem.
  84. If the repository is valid and can be connected to, no exception
  85. will be thrown.
  86. """
  87. # CVS paths are a bit strange, so we can't actually use the
  88. # SSH checking in SCMTool.check_repository. Do our own.
  89. m = cls.ext_cvsroot_re.match(path)
  90. if m:
  91. sshutils.check_host(m.group('hostname'), username, password)
  92. cvsroot, repopath = cls.build_cvsroot(path, username, password)
  93. client = CVSClient(cvsroot, repopath)
  94. try:
  95. client.cat_file('CVSROOT/modules', HEAD)
  96. except (SCMError, FileNotFoundError):
  97. raise RepositoryNotFoundError()
  98. @classmethod
  99. def parse_hostname(cls, path):
  100. """Parses a hostname from a repository path."""
  101. return urlparse.urlparse(path)[1] # netloc
  102. class CVSDiffParser(DiffParser):
  103. """This class is able to parse diffs created with CVS. """
  104. regex_small = re.compile('^RCS file: (.+)$')
  105. def __init__(self, data, repo):
  106. DiffParser.__init__(self, data)
  107. self.regex_full = re.compile('^RCS file: %s/(.*),v$' % re.escape(repo))
  108. def parse_special_header(self, linenum, info):
  109. linenum = super(CVSDiffParser, self).parse_special_header(linenum, info)
  110. if 'index' not in info:
  111. # We didn't find an index, so the rest is probably bogus too.
  112. return linenum
  113. m = self.regex_full.match(self.lines[linenum])
  114. if not m:
  115. m = self.regex_small.match(self.lines[linenum])
  116. if m:
  117. info['filename'] = m.group(1)
  118. linenum += 1
  119. else:
  120. raise DiffParserError('Unable to find RCS line', linenum)
  121. while self.lines[linenum].startswith('retrieving '):
  122. linenum += 1
  123. if self.lines[linenum].startswith('diff '):
  124. linenum += 1
  125. return linenum
  126. def parse_diff_header(self, linenum, info):
  127. linenum = super(CVSDiffParser, self).parse_diff_header(linenum, info)
  128. if info.get('origFile') == '/dev/null':
  129. info['origFile'] = info['newFile']
  130. info['origInfo'] = 'PRE-CREATION'
  131. elif 'filename' in info:
  132. info['origFile'] = info['filename']
  133. return linenum
  134. class CVSClient:
  135. def __init__(self, repository, path):
  136. self.tempdir = ""
  137. self.currentdir = os.getcwd()
  138. self.repository = repository
  139. self.path = path
  140. if not is_exe_in_path('cvs'):
  141. # This is technically not the right kind of error, but it's the
  142. # pattern we use with all the other tools.
  143. raise ImportError
  144. def cleanup(self):
  145. if self.currentdir != os.getcwd():
  146. # Restore current working directory
  147. os.chdir(self.currentdir)
  148. # Remove temporary directory
  149. if self.tempdir != "":
  150. os.rmdir(self.tempdir)
  151. def cat_file(self, filename, revision):
  152. # We strip the repo off of the fully qualified path as CVS does
  153. # not like to be given absolute paths.
  154. repos_path = self.path.split(":")[-1]
  155. if filename.startswith(repos_path + "/"):
  156. filename = filename[len(repos_path) + 1:]
  157. # Strip off the ",v" we sometimes get for CVS paths.
  158. if filename.endswith(",v"):
  159. filename = filename.rstrip(",v")
  160. # We want to try to fetch the files with different permutations of
  161. # "Attic" and no "Attic". This means there are 4 various permutations
  162. # that we have to check, based on whether we're using windows- or
  163. # unix-type paths
  164. filenameAttic = filename
  165. if '/Attic/' in filename:
  166. filename = '/'.join(filename.rsplit('/Attic/', 1))
  167. elif '\\Attic\\' in filename:
  168. filename = '\\'.join(filename.rsplit('\\Attic\\', 1))
  169. elif '\\' in filename:
  170. pos = filename.rfind('\\')
  171. filenameAttic = filename[0:pos] + "\\Attic" + filename[pos:]
  172. else:
  173. pos = filename.rfind('/')
  174. filenameAttic = filename[0:pos] + "/Attic" + filename[pos:]
  175. try:
  176. return self._cat_specific_file(filename, revision)
  177. except FileNotFoundError:
  178. return self._cat_specific_file(filenameAttic, revision)
  179. def _cat_specific_file(self, filename, revision):
  180. # Somehow CVS sometimes seems to write .cvsignore files to current
  181. # working directory even though we force stdout with -p.
  182. self.tempdir = tempfile.mkdtemp()
  183. os.chdir(self.tempdir)
  184. p = subprocess.Popen(['cvs', '-f', '-d', self.repository, 'checkout',
  185. '-r', str(revision), '-p', filename],
  186. stderr=subprocess.PIPE, stdout=subprocess.PIPE,
  187. close_fds=(os.name != 'nt'))
  188. contents = p.stdout.read()
  189. errmsg = p.stderr.read()
  190. failure = p.wait()
  191. # Unfortunately, CVS is not consistent about exiting non-zero on
  192. # errors. If the file is not found at all, then CVS will print an
  193. # error message on stderr, but it doesn't set an exit code with
  194. # pservers. If the file is found but an invalid revision is requested,
  195. # then cvs exits zero and nothing is printed at all. (!)
  196. #
  197. # But, when it is successful, cvs will print a header on stderr like
  198. # so:
  199. #
  200. # ===================================================================
  201. # Checking out foobar
  202. # RCS: /path/to/repo/foobar,v
  203. # VERS: 1.1
  204. # ***************
  205. # So, if nothing is in errmsg, or errmsg has a specific recognized
  206. # message, call it FileNotFound.
  207. if not errmsg or \
  208. errmsg.startswith('cvs checkout: cannot find module') or \
  209. errmsg.startswith('cvs checkout: could not read RCS file'):
  210. self.cleanup()
  211. raise FileNotFoundError(filename, revision)
  212. # Otherwise, if there's an exit code, or errmsg doesn't look like
  213. # successful header, then call it a generic SCMError.
  214. #
  215. # If the .cvspass file doesn't exist, CVS will return an error message
  216. # stating this. This is safe to ignore.
  217. if (failure and not errmsg.startswith('==========')) and \
  218. not ".cvspass does not exist - creating new file" in errmsg:
  219. self.cleanup()
  220. raise SCMError(errmsg)
  221. self.cleanup()
  222. return contents