def accept_ssl_certificate(self, path, on_failure=None): """If the repository uses SSL, this method is used to determine whether the SSL certificate can be automatically accepted. If the cert cannot be accepted, the ``on_failure`` callback is executed. ``on_failure`` signature:: void on_failure(e:Exception, path:str, cert:dict) """ self._accept_cert = {} self._accept_on_failure = on_failure auth = ra.Auth([ ra.get_simple_provider(), ra.get_username_provider(), ra.get_ssl_server_trust_prompt_provider(self._accept_trust_prompt), ]) cfg = get_config(self.config_dir) client = SVNClient(cfg, auth) try: info = client.info(path) logging.debug('SVN: Got repository information for %s: %s' % (path, info)) except SubversionException as e: if on_failure: on_failure(e, path, self._accept_cert)
def accept_ssl_certificate(self, path, on_failure=None): """If the repository uses SSL, this method is used to determine whether the SSL certificate can be automatically accepted. If the cert cannot be accepted, the ``on_failure`` callback is executed. ``on_failure`` signature:: void on_failure(e:Exception, path:str, cert:dict) """ cert = {} def _accept_trust_prompt(realm, failures, certinfo, may_save): cert.update({ 'realm': realm, 'failures': failures, 'hostname': certinfo[0], 'finger_print': certinfo[1], 'valid_from': certinfo[2], 'valid_until': certinfo[3], 'issuer_dname': certinfo[4], }) if on_failure: return 0, False else: del cert['failures'] return failures, True auth = ra.Auth([ ra.get_simple_provider(), ra.get_username_provider(), ra.get_ssl_client_cert_file_provider(), ra.get_ssl_client_cert_pw_file_provider(), ra.get_ssl_server_trust_file_provider(), ra.get_ssl_server_trust_prompt_provider(_accept_trust_prompt), ]) if self.username: auth.set_parameter(AUTH_PARAM_DEFAULT_USERNAME, self.username) if self.password: auth.set_parameter(AUTH_PARAM_DEFAULT_PASSWORD, self.password) cfg = get_config(self.config_dir) client = SVNClient(cfg, auth) try: info = client.info(path) logging.debug('SVN: Got repository information for %s: %s' % (path, info)) except SubversionException as e: if on_failure: on_failure(e, path, cert) return cert
def accept_ssl_certificate(self, path, on_failure=None): """If the repository uses SSL, this method is used to determine whether the SSL certificate can be automatically accepted. If the cert cannot be accepted, the ``on_failure`` callback is executed. ``on_failure`` signature:: void on_failure(e:Exception, path:str, cert:dict) """ cert = {} def _accept_trust_prompt(realm, failures, certinfo, may_save): cert.update({ 'realm': realm, 'failures': failures, 'hostname': certinfo[0], 'finger_print': certinfo[1], 'valid_from': certinfo[2], 'valid_until': certinfo[3], 'issuer_dname': certinfo[4], }) if on_failure: return 0, False else: del cert['failures'] return failures, True auth = ra.Auth([ ra.get_simple_provider(), ra.get_username_provider(), ra.get_ssl_client_cert_file_provider(), ra.get_ssl_client_cert_pw_file_provider(), ra.get_ssl_server_trust_file_provider(), ra.get_ssl_server_trust_prompt_provider(_accept_trust_prompt), ]) if self.username: auth.set_parameter(AUTH_PARAM_DEFAULT_USERNAME, B(self.username)) if self.password: auth.set_parameter(AUTH_PARAM_DEFAULT_PASSWORD, B(self.password)) cfg = get_config(self.config_dir) client = SVNClient(cfg, auth) try: info = client.info(path) logging.debug('SVN: Got repository information for %s: %s' % (path, info)) except SubversionException as e: if on_failure: on_failure(e, path, cert) return cert
def accept_ssl_certificate(self, path, on_failure=None): """If the repository uses SSL, this method is used to determine whether the SSL certificate can be automatically accepted. If the cert cannot be accepted, the ``on_failure`` callback is executed. ``on_failure`` signature:: void on_failure(e:Exception, path:str, cert:dict) """ cert = {} def _accept_trust_prompt(realm, failures, certinfo, may_save): cert.update( { "realm": realm, "failures": failures, "hostname": certinfo[0], "finger_print": certinfo[1], "valid_from": certinfo[2], "valid_until": certinfo[3], "issuer_dname": certinfo[4], } ) if on_failure: return 0, False else: del cert["failures"] return failures, True auth = ra.Auth( [ ra.get_simple_provider(), ra.get_username_provider(), ra.get_ssl_client_cert_file_provider(), ra.get_ssl_client_cert_pw_file_provider(), ra.get_ssl_server_trust_file_provider(), ra.get_ssl_server_trust_prompt_provider(_accept_trust_prompt), ] ) cfg = get_config(self.config_dir) client = SVNClient(cfg, auth) try: info = client.info(path) logging.debug("SVN: Got repository information for %s: %s" % (path, info)) except SubversionException as e: if on_failure: on_failure(e, path, cert) return cert
class Client(base.Client): required_module = 'subvertpy' def __init__(self, config_dir, repopath, username=None, password=None): super(Client, self).__init__(config_dir, repopath, username, password) self.repopath = B(self.repopath) self.config_dir = B(config_dir) self._ssl_trust_prompt_cb = None auth_providers = [ ra.get_simple_provider(), ra.get_username_provider(), ] if repopath.startswith('https:'): auth_providers += [ ra.get_ssl_client_cert_file_provider(), ra.get_ssl_client_cert_pw_file_provider(), ra.get_ssl_server_trust_file_provider(), ra.get_ssl_server_trust_prompt_provider(self.ssl_trust_prompt), ] self.auth = ra.Auth(auth_providers) if username: self.auth.set_parameter(B('svn:auth:username'), B(username)) if password: self.auth.set_parameter(B('svn:auth:password'), B(password)) cfg = get_config(self.config_dir) self.client = SVNClient(cfg, auth=self.auth) def set_ssl_server_trust_prompt(self, cb): self._ssl_trust_prompt_cb = cb def get_file(self, path, revision=HEAD): """Returns the contents of a given file at the given revision.""" if not path: raise FileNotFoundError(path, revision) revnum = self._normalize_revision(revision) path = B(self.normalize_path(path)) data = six.StringIO() try: self.client.cat(path, data, revnum) except SubversionException as e: raise FileNotFoundError(e) contents = data.getvalue() keywords = self.get_keywords(path, revision) if keywords: contents = self.collapse_keywords(contents, keywords) return contents def get_keywords(self, path, revision=HEAD): """Returns a list of SVN keywords for a given path.""" revnum = self._normalize_revision(revision, negatives_allowed=False) path = self.normalize_path(path) return self.client.propget(SVN_KEYWORDS, path, None, revnum).get(path) def _normalize_revision(self, revision, negatives_allowed=True): if revision is None: return None elif revision == HEAD: return B('HEAD') elif revision == PRE_CREATION: raise FileNotFoundError('', revision) elif isinstance(revision, Revision): revision = int(revision.name) elif isinstance(revision, (B,) + six.string_types): revision = int(revision) return revision def get_filenames_in_revision(self, revision): """Returns a list of filenames associated with the revision.""" paths = {} def log_cb(changed_paths, rev, props, has_children=False): paths.update(changed_paths) revnum = self._normalize_revision(revision) self.client.log(log_cb, self.repopath, revnum, revnum, limit=1, discover_changed_paths=True) if paths: return paths.keys() else: return [] @property def repository_info(self): """Returns metadata about the repository: * UUID * Root URL * URL """ try: base = os.path.basename(self.repopath) info = self.client.info(self.repopath, 'HEAD')[base] except SubversionException as e: raise SCMError(e) return { 'uuid': info.repos_uuid, 'root_url': info.repos_root_url, 'url': info.url } def ssl_trust_prompt(self, realm, failures, certinfo, may_save): """ Callback for ``subvertpy.ra.get_ssl_server_trust_prompt_provider``. ``may_save`` indicates whether to save the cert info for subsequent requests. Calls ``callback_ssl_server_trust_prompt`` if it exists. :param certinfo: (hostname, fingerprint, valid_from, valid_until, issuer_dname, ascii_cert) :return: (accepted_failures, may_save) """ if self._ssl_trust_prompt_cb: trust_dict = { 'realm': realm, 'failures': failures, 'hostname': certinfo[0], 'finger_print': certinfo[1], 'valid_from': certinfo[2], 'valid_until': certinfo[3], 'issuer_dname': certinfo[4], } return self._trust_prompt_cb(trust_dict)[1:] else: return None def accept_ssl_certificate(self, path, on_failure=None): """If the repository uses SSL, this method is used to determine whether the SSL certificate can be automatically accepted. If the cert cannot be accepted, the ``on_failure`` callback is executed. ``on_failure`` signature:: void on_failure(e:Exception, path:str, cert:dict) """ cert = {} def _accept_trust_prompt(realm, failures, certinfo, may_save): cert.update({ 'realm': realm, 'failures': failures, 'hostname': certinfo[0], 'finger_print': certinfo[1], 'valid_from': certinfo[2], 'valid_until': certinfo[3], 'issuer_dname': certinfo[4], }) if on_failure: return 0, False else: del cert['failures'] return failures, True auth = ra.Auth([ ra.get_simple_provider(), ra.get_username_provider(), ra.get_ssl_client_cert_file_provider(), ra.get_ssl_client_cert_pw_file_provider(), ra.get_ssl_server_trust_file_provider(), ra.get_ssl_server_trust_prompt_provider(_accept_trust_prompt), ]) cfg = get_config(self.config_dir) client = SVNClient(cfg, auth) try: info = client.info(path) logging.debug('SVN: Got repository information for %s: %s' % (path, info)) except SubversionException as e: if on_failure: on_failure(e, path, cert) return cert def get_log(self, path, start=None, end=None, limit=None, discover_changed_paths=False, limit_to_path=False): """Returns log entries at the specified path. The log entries will appear ordered from most recent to least, with 'start' being the most recent commit in the range. If 'start' is not specified, then it will default to 'HEAD'. If 'end' is not specified, it will default to '1'. To limit the commits to the given path, not factoring in history from any branch operations, set 'limit_to_path' to True. """ def log_cb(changed_paths, revision, props, has_children): commit = { 'revision': six.text_type(revision), } if 'svn:date' in props: commit['date'] = datetime.strptime(props['svn:date'], '%Y-%m-%dT%H:%M:%S.%fZ') if 'svn:author' in props: commit['author'] = props['svn:author'] if 'svn:log' in props: commit['message'] = props['svn:log'] commits.append(commit) if start is None: start = self.LOG_DEFAULT_START if end is None: end = self.LOG_DEFAULT_END commits = [] self.client.log(log_cb, paths=B(self.normalize_path(path)), start_rev=self._normalize_revision(start), end_rev=self._normalize_revision(end), limit=limit, discover_changed_paths=discover_changed_paths, strict_node_history=limit_to_path) return commits def list_dir(self, path): """Lists the contents of the specified path. The result will be an ordered dictionary of contents, mapping filenames or directory names with a dictionary containing: * ``path`` - The full path of the file or directory. * ``created_rev`` - The revision where the file or directory was created. """ result = SortedDict() if api_version()[:2] >= (1, 5): depth = 2 # Immediate files in this path. Only in 1.5+. else: depth = 0 # This will trigger recurse=False for SVN < 1.5. dirents = self.client.list(B(self.normalize_path(path)), None, depth) for name, dirent in six.iteritems(dirents): if name: result[six.text_type(name)] = { 'path': '%s/%s' % (path.strip('/'), name), 'created_rev': six.text_type(dirent['created_rev']), } return result def diff(self, revision1, revision2, path=None): """Returns a diff between two revisions. The diff will contain the differences between the two revisions, and may optionally be limited to a specific path. The returned diff will be returned as a Unicode object. """ if path: path = self.normalize_path(path) else: path = self.repopath out = None err = None try: out, err = self.client.diff(self._normalize_revision(revision1), self._normalize_revision(revision2), B(path), B(path), diffopts=DIFF_UNIFIED) diff = out.read().decode('utf-8') except Exception as e: logging.error('Failed to generate diff using subvertpy for ' 'revisions %s:%s for path %s: %s', revision1, revision2, path, e, exc_info=1) raise SCMError( _('Unable to get diff revisions %s through %s: %s') % (revision1, revision2, e)) finally: if out: out.close() if err: err.close() return diff
class Client(base.Client): """Subvertpy-backed Subversion client.""" required_module = 'subvertpy' def __init__(self, config_dir, repopath, username=None, password=None): """Initialize the client. Args: config_dir (unicode): The Subversion configuration directory. repopath (unicode): The path to the Subversion repository. username (unicode, optional): The username used to authenticate with the repository. password (unicode, optional): The password used to authenticate with the repository. """ super(Client, self).__init__(config_dir, repopath, username, password) self.repopath = self.repopath self.config_dir = config_dir self._ssl_trust_prompt_cb = None auth_providers = [ ra.get_simple_provider(), ra.get_username_provider(), ] if repopath.startswith('https:'): auth_providers += [ ra.get_ssl_client_cert_file_provider(), ra.get_ssl_client_cert_pw_file_provider(), ra.get_ssl_server_trust_file_provider(), ra.get_ssl_server_trust_prompt_provider(self.ssl_trust_prompt), ] self.auth = ra.Auth(auth_providers) self.username = None self.password = None if username: self.username = username self.auth.set_parameter(AUTH_PARAM_DEFAULT_USERNAME, self.username) if password: self.password = password self.auth.set_parameter(AUTH_PARAM_DEFAULT_PASSWORD, self.password) cfg = get_config(self.config_dir) self.client = SVNClient(cfg, auth=self.auth) def set_ssl_server_trust_prompt(self, cb): """Set the callback for verifying SSL certificates. Args: cb (callable): The callback used to verify certificates. """ self._ssl_trust_prompt_cb = cb def get_file(self, path, revision=HEAD): """Return the contents of a given file at the given revision. Args: path (unicode): The path to the file. revision (unicode or reviewboard.scmtools.core.Revision, optional): The revision of the file to fetch. Returns: bytes: The file contents. Raises: reviewboard.scmtools.errors.FileNotFoundError: The file could not be found in the repository. """ if not path: raise FileNotFoundError(path, revision) revnum = self._normalize_revision(revision) path = self.normalize_path(path) data = io.BytesIO() try: self.client.cat(path, data, revnum) except SubversionException as e: raise FileNotFoundError(e) contents = data.getvalue() keywords = self.get_keywords(path, revision) if keywords: contents = self.collapse_keywords(contents, keywords) return contents def get_keywords(self, path, revision=HEAD): """Return a list of SVN keywords for a given path. Args: path (unicode): The path to the file in the repository. revision (unicode or reviewboard.scmtools.core.Revision, optional): The revision of the file. Returns: dict: A dictionary of properties. All keys are Unicode strings and all values are bytes. """ revnum = self._normalize_revision(revision) path = self.normalize_path(path) return self.client.propget('svn:keywords', path, None, revnum).get(path) def _normalize_revision(self, revision): """Normalize a revision to an integer or byte string. Args: revision (object): The revision to normalize. This can be an integer, byte string, Unicode string, :py:class:`~reviewboard.scmtools.core.Revision` object, or ``None``. Returns: object: The resulting revision. This may be an integer (if providing a revision number) or a Unicode string (if using an identifier like "HEAD"). Raises: reviewboard.scmtools.errors.FileNotFoundError: The revision indicates that the file does not yet exist. """ if revision is None: return None elif revision == HEAD: return 'HEAD' elif revision == PRE_CREATION: raise FileNotFoundError('', revision) elif isinstance(revision, Revision): revision = int(revision.name) elif isinstance(revision, (six.text_type, six.binary_type)): revision = int(revision) return revision @property def repository_info(self): """Metadata about the repository. This is a dictionary containing the following keys: ``uuid`` (:py:class:`unicode`): The UUID of the repository. ``root_url`` (:py:class:`unicode`): The root URL of the configured repository. ``url`` (:py:class:`unicoe`): The full URL of the configured repository. """ try: base = os.path.basename(self.repopath) info = self.client.info(self.repopath, 'HEAD')[base] except SubversionException as e: raise SVNTool.normalize_error(e) return { 'uuid': force_text(info.repos_uuid), 'root_url': force_text(info.repos_root_url), 'url': force_text(info.url), } def ssl_trust_prompt(self, realm, failures, certinfo, may_save): """ Callback for ``subvertpy.ra.get_ssl_server_trust_prompt_provider``. ``may_save`` indicates whether to save the cert info for subsequent requests. Calls ``callback_ssl_server_trust_prompt`` if it exists. :param certinfo: (hostname, fingerprint, valid_from, valid_until, issuer_dname, ascii_cert) :return: (accepted_failures, may_save) """ if self._ssl_trust_prompt_cb: trust_dict = { 'realm': realm, 'failures': failures, 'hostname': certinfo[0], 'finger_print': certinfo[1], 'valid_from': certinfo[2], 'valid_until': certinfo[3], 'issuer_dname': certinfo[4], } return self._ssl_trust_prompt_cb(trust_dict)[1:] else: return None def accept_ssl_certificate(self, path, on_failure=None): """If the repository uses SSL, this method is used to determine whether the SSL certificate can be automatically accepted. If the cert cannot be accepted, the ``on_failure`` callback is executed. ``on_failure`` signature:: void on_failure(e:Exception, path:str, cert:dict) """ cert = {} def _accept_trust_prompt(realm, failures, certinfo, may_save): cert.update({ 'realm': realm, 'failures': failures, 'hostname': certinfo[0], 'finger_print': certinfo[1], 'valid_from': certinfo[2], 'valid_until': certinfo[3], 'issuer_dname': certinfo[4], }) if on_failure: return 0, False else: del cert['failures'] return failures, True auth = ra.Auth([ ra.get_simple_provider(), ra.get_username_provider(), ra.get_ssl_client_cert_file_provider(), ra.get_ssl_client_cert_pw_file_provider(), ra.get_ssl_server_trust_file_provider(), ra.get_ssl_server_trust_prompt_provider(_accept_trust_prompt), ]) if self.username: auth.set_parameter(AUTH_PARAM_DEFAULT_USERNAME, self.username) if self.password: auth.set_parameter(AUTH_PARAM_DEFAULT_PASSWORD, self.password) cfg = get_config(self.config_dir) client = SVNClient(cfg, auth) try: info = client.info(path) logging.debug('SVN: Got repository information for %s: %s' % (path, info)) except SubversionException as e: if on_failure: on_failure(e, path, cert) return cert def get_log(self, path, start=None, end=None, limit=None, discover_changed_paths=False, limit_to_path=False): """Returns log entries at the specified path. The log entries will appear ordered from most recent to least, with 'start' being the most recent commit in the range. If 'start' is not specified, then it will default to 'HEAD'. If 'end' is not specified, it will default to '1'. To limit the commits to the given path, not factoring in history from any branch operations, set 'limit_to_path' to True. """ def log_cb(changed_paths, revision, props, has_children): commit = { 'revision': force_text(revision), } if 'svn:date' in props: commit['date'] = \ datetime.strptime(props['svn:date'].decode('utf-8'), '%Y-%m-%dT%H:%M:%S.%fZ') if 'svn:author' in props: commit['author'] = props['svn:author'] if 'svn:log' in props: commit['message'] = props['svn:log'] commits.append(commit) if start is None: start = self.LOG_DEFAULT_START if end is None: end = self.LOG_DEFAULT_END commits = [] self.client.log(log_cb, paths=self.normalize_path(path), start_rev=self._normalize_revision(start), end_rev=self._normalize_revision(end), limit=limit, discover_changed_paths=discover_changed_paths, strict_node_history=limit_to_path) return commits def list_dir(self, path): """Lists the contents of the specified path. The result will be an ordered dictionary of contents, mapping filenames or directory names with a dictionary containing: * ``path`` - The full path of the file or directory. * ``created_rev`` - The revision where the file or directory was created. """ result = OrderedDict() if api_version()[:2] >= (1, 5): depth = 2 # Immediate files in this path. Only in 1.5+. else: depth = 0 # This will trigger recurse=False for SVN < 1.5. # subvertpy asserts that svn_uri not ends with slash norm_path = self.normalize_path(path).rstrip('/') dirents = self.client.list(norm_path, None, depth) for name, dirent in six.iteritems(dirents): if name: result[six.text_type(name)] = { 'path': '%s/%s' % (path.strip('/'), name), 'created_rev': six.text_type(dirent['created_rev']), } return result def diff(self, revision1, revision2): """Return a diff between two revisions. The diff will contain the differences between the two revisions, and may optionally be limited to a specific path. Args: revision1 (unicode): The older revision for the diff. revision2 (unicode): The newer revision for the diff. Returns: bytes: The resulting diff. """ out = None err = None try: out, err = self.client.diff(self._normalize_revision(revision1), self._normalize_revision(revision2), self.repopath, self.repopath, diffopts=['-u']) diff = out.read() except Exception as e: logging.error('Failed to generate diff using subvertpy for ' 'revisions %s:%s for path %s: %s', revision1, revision2, path, e, exc_info=1) raise SCMError( _('Unable to get diff revisions %s through %s: %s') % (revision1, revision2, e)) finally: if out: out.close() if err: err.close() return diff
class Client(base.Client): required_module = 'subvertpy' def __init__(self, config_dir, repopath, username=None, password=None): super(Client, self).__init__(config_dir, repopath, username, password) self.repopath = B(self.repopath) self.config_dir = B(config_dir) auth_providers = [ ra.get_simple_provider(), ra.get_username_provider(), ] if repopath.startswith('https:'): auth_providers.append( ra.get_ssl_server_trust_prompt_provider(self.ssl_trust_prompt)) self.auth = ra.Auth(auth_providers) if username: self.auth.set_parameter(B('svn:auth:username'), B(username)) if password: self.auth.set_parameter(B('svn:auth:password'), B(password)) cfg = get_config(self.config_dir) self.client = SVNClient(cfg, auth=self.auth) @property def ra(self): """Lazily creates the ``RemoteAccess`` object so ``accept_ssl_certificate`` works properly. """ if not hasattr(self, '_ra'): self._ra = ra.RemoteAccess(self.repopath, auth=self.auth) return self._ra @property def branches(self): """Returns a list of branches. This assumes the standard layout in the repository.""" results = [] try: root_dirents = \ self.ra.get_dir(B('.'), -1, ra.DIRENT_CREATED_REV)[0] except SubversionException as e: raise SCMError(e) trunk = B('trunk') if trunk in root_dirents: # Looks like the standard layout. Adds trunk and any branches. created_rev = root_dirents[trunk]['created_rev'] results.append(Branch('trunk', six.text_type(created_rev), True)) try: dirents = self.ra.get_dir(B('branches'), -1, ra.DIRENT_CREATED_REV)[0] branches = {} for name, dirent in six.iteritems(dirents): branches[six.text_type(name)] = six.text_type( dirent['created_rev']) for name in sorted(six.iterkeys(branches)): results.append(Branch(name, branches[name])) except SubversionException as e: pass else: # If the repository doesn't use the standard layout, just use a # listing of the root directory as the "branches". This probably # corresponds to a list of projects instead of branches, but it # will at least give people a useful result. branches = {} for name, dirent in six.iteritems(root_dirents): branches[six.text_type(name)] = six.text_type( dirent['created_rev']) default = True for name in sorted(six.iterkeys(branches)): results.append(Branch(name, branches[name], default)) default = False return results def get_commits(self, start): """Returns a list of commits.""" results = [] if start.isdigit(): start = int(start) commits = list(self.ra.iter_log(None, start, end=0, limit=31)) # We fetch one more commit than we care about, because the entries in # the svn log doesn't include the parent revision. for i, (_, rev, props, _) in enumerate(commits[:-1]): parent = commits[i + 1] commit = Commit(props[SVN_AUTHOR], six.text_type(rev), # [:-1] to remove the Z props[SVN_DATE][:-1], props[SVN_LOG], six.text_type(parent[1])) results.append(commit) return results def get_change(self, revision, cache_key): """Get an individual change. This returns a tuple with the commit message and the diff contents. """ revision = int(revision) commit = cache.get(cache_key) if commit: message = commit.message author_name = commit.author_name date = commit.date base_revision = commit.parent else: commits = list(self.ra.iter_log(None, revision, 0, limit=2)) rev, props = commits[0][1:3] message = props[SVN_LOG] author_name = props[SVN_AUTHOR] date = props[SVN_DATE] if len(commits) > 1: base_revision = commits[1][1] else: base_revision = 0 try: out, err = self.client.diff(int(base_revision), int(revision), self.repopath, self.repopath, diffopts=DIFF_UNIFIED) except Exception as e: raise SCMError(e) commit = Commit(author_name, six.text_type(revision), date, message, six.text_type(base_revision)) commit.diff = out.read() return commit def get_file(self, path, revision=HEAD): """Returns the contents of a given file at the given revision.""" if not path: raise FileNotFoundError(path, revision) revnum = self._normalize_revision(revision) path = B(self.normalize_path(path)) data = six.StringIO() try: self.client.cat(path, data, revnum) except SubversionException as e: raise FileNotFoundError(e) contents = data.getvalue() keywords = self.get_keywords(path, revision) if keywords: contents = self.collapse_keywords(contents, keywords) return contents def get_keywords(self, path, revision=HEAD): """Returns a list of SVN keywords for a given path.""" revnum = self._normalize_revision(revision, negatives_allowed=False) path = self.normalize_path(path) return self.client.propget(SVN_KEYWORDS, path, None, revnum).get(path) def _normalize_revision(self, revision, negatives_allowed=True): if revision == HEAD: return B('HEAD') elif revision == PRE_CREATION: raise FileNotFoundError('', revision) elif isinstance(revision, Revision): revnum = int(revision.name) elif isinstance(revision, (B,) + six.string_types): revnum = int(revision) return revnum def get_filenames_in_revision(self, revision): """Returns a list of filenames associated with the revision.""" paths = {} def log_cb(changed_paths, rev, props, has_children=False): paths.update(changed_paths) revnum = self._normalize_revision(revision) self.client.log(log_cb, self.repopath, revnum, revnum, limit=1, discover_changed_paths=True) if paths: return paths.keys() else: return [] @property def repository_info(self): """Returns metadata about the repository: * UUID * Root URL * URL """ try: base = os.path.basename(self.repopath) info = self.client.info(self.repopath, 'HEAD')[base] except SubversionException as e: raise SCMError(e) return { 'uuid': info.repos_uuid, 'root_url': info.repos_root_url, 'url': info.url } def ssl_trust_prompt(self, realm, failures, certinfo, may_save): """ Callback for ``subvertpy.ra.get_ssl_server_trust_prompt_provider``. ``may_save`` indicates whether to save the cert info for subsequent requests. Calls ``callback_ssl_server_trust_prompt`` if it exists. :param certinfo: (hostname, fingerprint, valid_from, valid_until, issuer_dname, ascii_cert) :return: (accepted_failures, may_save) """ if hasattr(self, 'callback_ssl_server_trust_prompt'): trust_dict = { 'realm': realm, 'failures': failures, 'hostname': certinfo[0], 'finger_print': certinfo[1], 'valid_from': certinfo[2], 'valid_until': certinfo[3], 'issuer_dname': certinfo[4], } return self.callback_ssl_server_trust_prompt(trust_dict)[1:] else: return None def _accept_trust_prompt(self, realm, failures, certinfo, may_save): """ Callback for ``subvertpy.ra.get_ssl_server_trust_prompt_provider``. ``may_save`` indicates whether to save the cert info for subsequent requests. USED ONLY FOR ``accept_ssl_certificate``. :param certinfo: (hostname, fingerprint, valid_from, valid_until, issuer_dname, ascii_cert) :return: (accepted_failures, may_save) """ self._accept_cert.update({ 'realm': realm, 'failures': failures, 'hostname': certinfo[0], 'finger_print': certinfo[1], 'valid_from': certinfo[2], 'valid_until': certinfo[3], 'issuer_dname': certinfo[4], }) if self._accept_on_failure: return None else: return failures, True def accept_ssl_certificate(self, path, on_failure=None): """If the repository uses SSL, this method is used to determine whether the SSL certificate can be automatically accepted. If the cert cannot be accepted, the ``on_failure`` callback is executed. ``on_failure`` signature:: void on_failure(e:Exception, path:str, cert:dict) """ self._accept_cert = {} self._accept_on_failure = on_failure auth = ra.Auth([ ra.get_simple_provider(), ra.get_username_provider(), ra.get_ssl_server_trust_prompt_provider(self._accept_trust_prompt), ]) cfg = get_config(self.config_dir) client = SVNClient(cfg, auth) try: info = client.info(path) logging.debug('SVN: Got repository information for %s: %s' % (path, info)) except SubversionException as e: if on_failure: on_failure(e, path, self._accept_cert)
class Client(base.Client): required_module = 'subvertpy' def __init__(self, config_dir, repopath, username=None, password=None): super(Client, self).__init__(config_dir, repopath, username, password) self.repopath = B(self.repopath) self.config_dir = B(config_dir) auth_providers = [ ra.get_simple_provider(), ra.get_username_provider(), ] if repopath.startswith('https:'): auth_providers.append( ra.get_ssl_server_trust_prompt_provider(self.ssl_trust_prompt)) self.auth = ra.Auth(auth_providers) if username: self.auth.set_parameter(B('svn:auth:username'), B(username)) if password: self.auth.set_parameter(B('svn:auth:password'), B(password)) cfg = get_config(self.config_dir) self.client = SVNClient(cfg, auth=self.auth) @property def ra(self): """Lazily creates the ``RemoteAccess`` object so ``accept_ssl_certificate`` works properly. """ if not hasattr(self, '_ra'): self._ra = ra.RemoteAccess(self.repopath, auth=self.auth) return self._ra @property def branches(self): """Returns a list of branches. This assumes the standard layout in the repository.""" results = [] try: root_dirents = \ self.ra.get_dir(B('.'), -1, ra.DIRENT_CREATED_REV)[0] except SubversionException as e: raise SCMError(e) trunk = B('trunk') if trunk in root_dirents: # Looks like the standard layout. Adds trunk and any branches. created_rev = root_dirents[trunk]['created_rev'] results.append(Branch('trunk', six.text_type(created_rev), True)) try: dirents = self.ra.get_dir(B('branches'), -1, ra.DIRENT_CREATED_REV)[0] branches = {} for name, dirent in six.iteritems(dirents): branches[six.text_type(name)] = six.text_type( dirent['created_rev']) for name in sorted(six.iterkeys(branches)): results.append(Branch(name, branches[name])) except SubversionException as e: pass else: # If the repository doesn't use the standard layout, just use a # listing of the root directory as the "branches". This probably # corresponds to a list of projects instead of branches, but it # will at least give people a useful result. branches = {} for name, dirent in six.iteritems(root_dirents): branches[six.text_type(name)] = six.text_type( dirent['created_rev']) default = True for name in sorted(six.iterkeys(branches)): results.append(Branch(name, branches[name], default)) default = False return results def get_commits(self, start): """Returns a list of commits.""" results = [] if start.isdigit(): start = int(start) commits = list(self.ra.iter_log(None, start, end=0, limit=31)) # We fetch one more commit than we care about, because the entries in # the svn log doesn't include the parent revision. for i, (_, rev, props, _) in enumerate(commits[:-1]): parent = commits[i + 1] commit = Commit( props[SVN_AUTHOR], six.text_type(rev), # [:-1] to remove the Z props[SVN_DATE][:-1], props[SVN_LOG], six.text_type(parent[1])) results.append(commit) return results def get_change(self, revision, cache_key): """Get an individual change. This returns a tuple with the commit message and the diff contents. """ revision = int(revision) commit = cache.get(cache_key) if commit: message = commit.message author_name = commit.author_name date = commit.date base_revision = commit.parent else: commits = list(self.ra.iter_log(None, revision, 0, limit=2)) rev, props = commits[0][1:3] message = props[SVN_LOG].decode('utf-8', 'replace') author_name = props[SVN_AUTHOR].decode('utf-8', 'replace') date = props[SVN_DATE] if len(commits) > 1: base_revision = commits[1][1] else: base_revision = 0 try: out, err = self.client.diff(int(base_revision), int(revision), self.repopath, self.repopath, diffopts=DIFF_UNIFIED) except Exception as e: raise SCMError(e) commit = Commit(author_name, six.text_type(revision), date, message, six.text_type(base_revision)) commit.diff = out.read().decode('utf-8') return commit def get_file(self, path, revision=HEAD): """Returns the contents of a given file at the given revision.""" if not path: raise FileNotFoundError(path, revision) revnum = self._normalize_revision(revision) path = B(self.normalize_path(path)) data = six.StringIO() try: self.client.cat(path, data, revnum) except SubversionException as e: raise FileNotFoundError(e) contents = data.getvalue() keywords = self.get_keywords(path, revision) if keywords: contents = self.collapse_keywords(contents, keywords) return contents def get_keywords(self, path, revision=HEAD): """Returns a list of SVN keywords for a given path.""" revnum = self._normalize_revision(revision, negatives_allowed=False) path = self.normalize_path(path) return self.client.propget(SVN_KEYWORDS, path, None, revnum).get(path) def _normalize_revision(self, revision, negatives_allowed=True): if revision == HEAD: return B('HEAD') elif revision == PRE_CREATION: raise FileNotFoundError('', revision) elif isinstance(revision, Revision): revnum = int(revision.name) elif isinstance(revision, (B, ) + six.string_types): revnum = int(revision) return revnum def get_filenames_in_revision(self, revision): """Returns a list of filenames associated with the revision.""" paths = {} def log_cb(changed_paths, rev, props, has_children=False): paths.update(changed_paths) revnum = self._normalize_revision(revision) self.client.log(log_cb, self.repopath, revnum, revnum, limit=1, discover_changed_paths=True) if paths: return paths.keys() else: return [] @property def repository_info(self): """Returns metadata about the repository: * UUID * Root URL * URL """ try: base = os.path.basename(self.repopath) info = self.client.info(self.repopath, 'HEAD')[base] except SubversionException as e: raise SCMError(e) return { 'uuid': info.repos_uuid, 'root_url': info.repos_root_url, 'url': info.url } def ssl_trust_prompt(self, realm, failures, certinfo, may_save): """ Callback for ``subvertpy.ra.get_ssl_server_trust_prompt_provider``. ``may_save`` indicates whether to save the cert info for subsequent requests. Calls ``callback_ssl_server_trust_prompt`` if it exists. :param certinfo: (hostname, fingerprint, valid_from, valid_until, issuer_dname, ascii_cert) :return: (accepted_failures, may_save) """ if hasattr(self, 'callback_ssl_server_trust_prompt'): trust_dict = { 'realm': realm, 'failures': failures, 'hostname': certinfo[0], 'finger_print': certinfo[1], 'valid_from': certinfo[2], 'valid_until': certinfo[3], 'issuer_dname': certinfo[4], } return self.callback_ssl_server_trust_prompt(trust_dict)[1:] else: return None def _accept_trust_prompt(self, realm, failures, certinfo, may_save): """ Callback for ``subvertpy.ra.get_ssl_server_trust_prompt_provider``. ``may_save`` indicates whether to save the cert info for subsequent requests. USED ONLY FOR ``accept_ssl_certificate``. :param certinfo: (hostname, fingerprint, valid_from, valid_until, issuer_dname, ascii_cert) :return: (accepted_failures, may_save) """ self._accept_cert.update({ 'realm': realm, 'failures': failures, 'hostname': certinfo[0], 'finger_print': certinfo[1], 'valid_from': certinfo[2], 'valid_until': certinfo[3], 'issuer_dname': certinfo[4], }) if self._accept_on_failure: return None else: return failures, True def accept_ssl_certificate(self, path, on_failure=None): """If the repository uses SSL, this method is used to determine whether the SSL certificate can be automatically accepted. If the cert cannot be accepted, the ``on_failure`` callback is executed. ``on_failure`` signature:: void on_failure(e:Exception, path:str, cert:dict) """ self._accept_cert = {} self._accept_on_failure = on_failure auth = ra.Auth([ ra.get_simple_provider(), ra.get_username_provider(), ra.get_ssl_server_trust_prompt_provider(self._accept_trust_prompt), ]) cfg = get_config(self.config_dir) client = SVNClient(cfg, auth) try: info = client.info(path) logging.debug('SVN: Got repository information for %s: %s' % (path, info)) except SubversionException as e: if on_failure: on_failure(e, path, self._accept_cert)
class Client(base.Client): required_module = 'subvertpy' def __init__(self, config_dir, repopath, username=None, password=None): super(Client, self).__init__(config_dir, repopath, username, password) self.repopath = B(self.repopath) self.config_dir = B(config_dir) self._ssl_trust_prompt_cb = None auth_providers = [ ra.get_simple_provider(), ra.get_username_provider(), ] if repopath.startswith('https:'): auth_providers += [ ra.get_ssl_client_cert_file_provider(), ra.get_ssl_client_cert_pw_file_provider(), ra.get_ssl_server_trust_file_provider(), ra.get_ssl_server_trust_prompt_provider(self.ssl_trust_prompt), ] self.auth = ra.Auth(auth_providers) if username: self.auth.set_parameter(B('svn:auth:username'), B(username)) if password: self.auth.set_parameter(B('svn:auth:password'), B(password)) cfg = get_config(self.config_dir) self.client = SVNClient(cfg, auth=self.auth) def set_ssl_server_trust_prompt(self, cb): self._ssl_trust_prompt_cb = cb def get_file(self, path, revision=HEAD): """Returns the contents of a given file at the given revision.""" if not path: raise FileNotFoundError(path, revision) revnum = self._normalize_revision(revision) path = B(self.normalize_path(path)) data = six.StringIO() try: self.client.cat(path, data, revnum) except SubversionException as e: raise FileNotFoundError(e) contents = data.getvalue() keywords = self.get_keywords(path, revision) if keywords: contents = self.collapse_keywords(contents, keywords) return contents def get_keywords(self, path, revision=HEAD): """Returns a list of SVN keywords for a given path.""" revnum = self._normalize_revision(revision, negatives_allowed=False) path = self.normalize_path(path) return self.client.propget(SVN_KEYWORDS, path, None, revnum).get(path) def _normalize_revision(self, revision, negatives_allowed=True): if revision is None: return None elif revision == HEAD: return B('HEAD') elif revision == PRE_CREATION: raise FileNotFoundError('', revision) elif isinstance(revision, Revision): revision = int(revision.name) elif isinstance(revision, (B, ) + six.string_types): revision = int(revision) return revision def get_filenames_in_revision(self, revision): """Returns a list of filenames associated with the revision.""" paths = {} def log_cb(changed_paths, rev, props, has_children=False): paths.update(changed_paths) revnum = self._normalize_revision(revision) self.client.log(log_cb, self.repopath, revnum, revnum, limit=1, discover_changed_paths=True) if paths: return paths.keys() else: return [] @property def repository_info(self): """Returns metadata about the repository: * UUID * Root URL * URL """ try: base = os.path.basename(self.repopath) info = self.client.info(self.repopath, 'HEAD')[base] except SubversionException as e: raise SCMError(e) return { 'uuid': info.repos_uuid, 'root_url': info.repos_root_url, 'url': info.url } def ssl_trust_prompt(self, realm, failures, certinfo, may_save): """ Callback for ``subvertpy.ra.get_ssl_server_trust_prompt_provider``. ``may_save`` indicates whether to save the cert info for subsequent requests. Calls ``callback_ssl_server_trust_prompt`` if it exists. :param certinfo: (hostname, fingerprint, valid_from, valid_until, issuer_dname, ascii_cert) :return: (accepted_failures, may_save) """ if self._ssl_trust_prompt_cb: trust_dict = { 'realm': realm, 'failures': failures, 'hostname': certinfo[0], 'finger_print': certinfo[1], 'valid_from': certinfo[2], 'valid_until': certinfo[3], 'issuer_dname': certinfo[4], } return self._trust_prompt_cb(trust_dict)[1:] else: return None def accept_ssl_certificate(self, path, on_failure=None): """If the repository uses SSL, this method is used to determine whether the SSL certificate can be automatically accepted. If the cert cannot be accepted, the ``on_failure`` callback is executed. ``on_failure`` signature:: void on_failure(e:Exception, path:str, cert:dict) """ cert = {} def _accept_trust_prompt(realm, failures, certinfo, may_save): cert.update({ 'realm': realm, 'failures': failures, 'hostname': certinfo[0], 'finger_print': certinfo[1], 'valid_from': certinfo[2], 'valid_until': certinfo[3], 'issuer_dname': certinfo[4], }) if on_failure: return 0, False else: del cert['failures'] return failures, True auth = ra.Auth([ ra.get_simple_provider(), ra.get_username_provider(), ra.get_ssl_client_cert_file_provider(), ra.get_ssl_client_cert_pw_file_provider(), ra.get_ssl_server_trust_file_provider(), ra.get_ssl_server_trust_prompt_provider(_accept_trust_prompt), ]) cfg = get_config(self.config_dir) client = SVNClient(cfg, auth) try: info = client.info(path) logging.debug('SVN: Got repository information for %s: %s' % (path, info)) except SubversionException as e: if on_failure: on_failure(e, path, cert) return cert def get_log(self, path, start=None, end=None, limit=None, discover_changed_paths=False, limit_to_path=False): """Returns log entries at the specified path. The log entries will appear ordered from most recent to least, with 'start' being the most recent commit in the range. If 'start' is not specified, then it will default to 'HEAD'. If 'end' is not specified, it will default to '1'. To limit the commits to the given path, not factoring in history from any branch operations, set 'limit_to_path' to True. """ def log_cb(changed_paths, revision, props, has_children): commit = { 'revision': six.text_type(revision), } if 'svn:date' in props: commit['date'] = datetime.strptime(props['svn:date'], '%Y-%m-%dT%H:%M:%S.%fZ') if 'svn:author' in props: commit['author'] = props['svn:author'] if 'svn:log' in props: commit['message'] = props['svn:log'] commits.append(commit) if start is None: start = self.LOG_DEFAULT_START if end is None: end = self.LOG_DEFAULT_END commits = [] self.client.log(log_cb, paths=B(self.normalize_path(path)), start_rev=self._normalize_revision(start), end_rev=self._normalize_revision(end), limit=limit, discover_changed_paths=discover_changed_paths, strict_node_history=limit_to_path) return commits def list_dir(self, path): """Lists the contents of the specified path. The result will be an ordered dictionary of contents, mapping filenames or directory names with a dictionary containing: * ``path`` - The full path of the file or directory. * ``created_rev`` - The revision where the file or directory was created. """ result = SortedDict() if api_version()[:2] >= (1, 5): depth = 2 # Immediate files in this path. Only in 1.5+. else: depth = 0 # This will trigger recurse=False for SVN < 1.5. # subvertpy asserts that svn_uri not ends with slash norm_path = B(self.normalize_path(path)).rstrip('/') dirents = self.client.list(norm_path, None, depth) for name, dirent in six.iteritems(dirents): if name: result[six.text_type(name)] = { 'path': '%s/%s' % (path.strip('/'), name), 'created_rev': six.text_type(dirent['created_rev']), } return result def diff(self, revision1, revision2, path=None): """Returns a diff between two revisions. The diff will contain the differences between the two revisions, and may optionally be limited to a specific path. The returned diff will be returned as a Unicode object. """ if path: path = self.normalize_path(path) else: path = self.repopath out = None err = None try: out, err = self.client.diff(self._normalize_revision(revision1), self._normalize_revision(revision2), B(path), B(path), diffopts=DIFF_UNIFIED) diff = out.read().decode('utf-8') except Exception as e: logging.error( 'Failed to generate diff using subvertpy for ' 'revisions %s:%s for path %s: %s', revision1, revision2, path, e, exc_info=1) raise SCMError( _('Unable to get diff revisions %s through %s: %s') % (revision1, revision2, e)) finally: if out: out.close() if err: err.close() return diff