Beispiel #1
0
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
Beispiel #2
0
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
Beispiel #3
0
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)
Beispiel #4
0
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)
Beispiel #5
0
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