Example #1
0
    def test_pre_process_request_sync_skipped_for_invalid_connector(self):
        """Repository synchronization is skipped for an invalid connector."""
        self.env.config.set('repositories', 'repos.dir', '/some/path')
        self.env.config.set('repositories', 'repos.type', 'invalid')
        self.env.config.set('repositories', 'repos.sync_per_request', True)
        req = MockRequest(self.env)
        handler = Mock()
        repos_manager = RepositoryManager(self.env)

        repos_manager.pre_process_request(req, handler)

        self.assertNotIn('invalid', repos_manager.get_supported_types())
        self.assertEqual([], req.chrome['warnings'])
Example #2
0
class RepositoryManager(Component):
    """Adds creation, modification and deletion of repositories.

    This class extends Trac's `RepositoryManager` and adds some
    capabilities that allow users to create and manage repositories.
    The original `RepositoryManager` *just* allows adding and removing
    existing repositories from Trac's database, which means that still
    someone must do some shell work on the server.

    To work nicely together with manually created and added repositories
    a new `ManagedRepository` class is used to mark the ones that can be
    handled by this module. It also implements forking, if the connector
    supports that, which creates instances of `ForkedRepository`.
    """

    base_dir = Option('repository-manager',
                      'base_dir',
                      'repositories',
                      doc="""The base folder in which repositories will be
                             created.
                             """)
    owner_as_maintainer = BoolOption('repository-manager',
                                     'owner_as_maintainer',
                                     True,
                                     doc="""If true, the owner will have the
                                            role of a maintainer, too.
                                            Otherwise, he will only act as an
                                            administrator for his repositories.
                                            """)

    connectors = ExtensionPoint(IAdministrativeRepositoryConnector)

    manager = None

    roles = ('maintainer', 'writer', 'reader')

    def __init__(self):
        self.manager = TracRepositoryManager(self.env)

    def get_supported_types(self):
        """Return the list of supported repository types."""
        types = set(type for connector in self.connectors
                    for (type, prio) in connector.get_supported_types() or []
                    if prio >= 0)
        return list(types & set(self.manager.get_supported_types()))

    def get_forkable_types(self):
        """Return the list of forkable repository types."""
        return list(type for type in self.get_supported_types()
                    if self.can_fork(type))

    def can_fork(self, type):
        """Return whether the given repository type can be forked."""
        return self._get_repository_connector(type).can_fork(type)

    def can_delete_changesets(self, type):
        """Return whether the given repository type can delete changesets."""
        return self._get_repository_connector(type).can_delete_changesets(type)

    def can_ban_changesets(self, type):
        """Return whether the given repository type can ban changesets."""
        return self._get_repository_connector(type).can_ban_changesets(type)

    def get_forkable_repositories(self):
        """Return a dictionary of repository information, indexed by
        name and including only repositories that can be forked."""
        repositories = self.manager.get_all_repositories()
        result = {}
        for key in repositories:
            if repositories[key]['type'] in self.get_forkable_types():
                result[key] = repositories[key]['name']
        return result

    def get_managed_repositories(self):
        """Return the list of existing managed repositories."""
        repositories = self.manager.get_all_repositories()
        result = {}
        for key in repositories:
            try:
                self.get_repository(repositories[key]['name'], True)
                result[key] = repositories[key]['name']
            except:
                pass
        return result

    def get_repository(self, name, convert_to_managed=False):
        """Retrieve the appropriate repository for the given name.

        Converts the found repository into a `ManagedRepository`, if
        requested. In that case, expect an exception if the found
        repository was not created using this `RepositoryManager`.
        """
        repo = self.manager.get_repository(name)
        if repo and convert_to_managed:
            convert_managed_repository(self.env, repo)
        return repo

    def get_repository_by_id(self, id, convert_to_managed=False):
        """Retrieve a matching `Repository` for the given id."""
        repositories = self.manager.get_all_repositories()
        for name, info in repositories.iteritems():
            if info['id'] == int(id):
                return self.get_repository(name, convert_to_managed)
        return None

    def get_repository_by_path(self, path):
        """Retrieve a matching `Repository` for the given path."""
        return self.manager.get_repository_by_path(path)

    def get_base_directory(self, type):
        """Get the base directory for the given repository type."""
        return os.path.join(self.env.path, self.base_dir, type)

    def create(self, repo):
        """Create a new empty repository.

         * Checks if the new repository can be created and added
         * Prepares the filesystem
         * Uses an appropriate connector to create and initialize the
           repository
         * Postprocesses the filesystem (modes)
         * Inserts everything into the database and synchronizes Trac
        """
        if self.get_repository(repo['name']) or os.path.lexists(repo['dir']):
            raise TracError(_("Repository or directory already exists."))

        self._prepare_base_directory(repo['dir'])

        self._get_repository_connector(repo['type']).create(repo)

        self._adjust_modes(repo['dir'])

        with self.env.db_transaction as db:
            id = self.manager.get_repository_id(repo['name'])
            roles = list((id, role + 's', '') for role in self.roles)
            db.executemany(
                "INSERT INTO repository (id, name, value) VALUES (%s, %s, %s)",
                [(id, 'dir', repo['dir']), (id, 'type', repo['type']),
                 (id, 'owner', repo['owner'])] + roles)
            self.manager.reload_repositories()
        self.manager.get_repository(repo['name']).sync(None, True)
        self.update_auth_files()

    def fork_local(self, repo):
        """Fork a local repository.

         * Checks if the new repository can be created and added
         * Checks if the origin exists and can be forked
         * The filesystem is obviously already prepared
         * Uses an appropriate connector to fork the repository
         * Postprocesses the filesystem (modes)
         * Inserts everything into the database and synchronizes Trac
        """
        if self.get_repository(repo['name']) or os.path.lexists(repo['dir']):
            raise TracError(_("Repository or directory already exists."))

        origin = self.get_repository(repo['origin'], True)
        if not origin:
            raise TracError(_("Origin for local fork does not exist."))
        if origin.type != repo['type']:
            raise TracError(
                _("Fork of local repository must have same type "
                  "as origin."))
        repo.update({'origin_url': 'file://' + origin.directory})

        self._prepare_base_directory(repo['dir'])

        self._get_repository_connector(repo['type']).fork(repo)

        self._adjust_modes(repo['dir'])

        with self.env.db_transaction as db:
            id = self.manager.get_repository_id(repo['name'])
            roles = list((id, role + 's', '') for role in self.roles)
            db.executemany(
                "INSERT INTO repository (id, name, value) VALUES (%s, %s, %s)",
                [(id, 'dir', repo['dir']), (id, 'type', repo['type']),
                 (id, 'owner', repo['owner']),
                 (id, 'description', origin.description),
                 (id, 'origin', origin.id),
                 (id, 'inherit_readers', True)] + roles)
            self.manager.reload_repositories()
        self.manager.get_repository(repo['name']).sync(None, True)
        self.update_auth_files()

    def modify(self, repo, data):
        """Modify an existing repository."""
        convert_managed_repository(self.env, repo)
        if repo.directory != data['dir']:
            shutil.move(repo.directory, data['dir'])
        with self.env.db_transaction as db:
            db.executemany(
                "UPDATE repository SET value = %s WHERE id = %s AND name = %s",
                [(data[key], repo.id, key) for key in data])
            self.manager.reload_repositories()
        if repo.directory != data['dir']:
            repo = self.get_repository(data['name'])
            repo.sync(clean=True)
        self.update_auth_files()

    def remove(self, repo, delete):
        """Remove an existing repository.

        Depending on the parameter delete this method also removes the
        repository from the filesystem. This can not be undone.
        """
        convert_managed_repository(self.env, repo)
        if delete:
            shutil.rmtree(repo.directory)
        with self.env.db_transaction as db:
            db("DELETE FROM repository WHERE id = %d" % repo.id)
            db("DELETE FROM revision WHERE repos = %d" % repo.id)
            db("DELETE FROM node_change WHERE repos = %d" % repo.id)
        self.manager.reload_repositories()
        self.update_auth_files()

    def delete_changeset(self, repo, rev, ban):
        """Delete a changeset from a managed repository, if supported.

        Depending on the parameter ban this method also marks the
        changeset to be kept out of the repository. That features needs
        special support by the used scm.
        """
        convert_managed_repository(self.env, repo)
        self._get_repository_connector(repo.type).delete_changeset(
            repo, rev, ban)

    def add_role(self, repo, role, subject):
        """Add a role for the given repository."""
        assert role in self.roles
        convert_managed_repository(self.env, repo)
        role_attr = '_' + role + 's'
        setattr(repo, role_attr, getattr(repo, role_attr) | set([subject]))
        self._update_roles_in_db(repo)

    def revoke_roles(self, repo, roles):
        """Revoke a list of `role, subject` pairs."""
        convert_managed_repository(self.env, repo)
        for role, subject in roles:
            role_attr = '_' + role + 's'
            config = getattr(repo, role_attr)
            config = config - set([subject])
            setattr(repo, role_attr, getattr(repo, role_attr) - set([subject]))
        self._update_roles_in_db(repo)

    def update_auth_files(self):
        """Rewrites all configured auth files for all managed
        repositories.
        """
        types = self.get_supported_types()
        all_repositories = []
        for repo in self.manager.get_real_repositories():
            try:
                convert_managed_repository(self.env, repo)
                all_repositories.append(repo)
            except:
                pass
        for type in types:
            repos = [repo for repo in all_repositories if repo.type == type]
            self._get_repository_connector(type).update_auth_files(repos)

        authz_source_file = AuthzSourcePolicy(self.env).authz_file
        if authz_source_file:
            authz_source_path = os.path.join(self.env.path, authz_source_file)

            authz = ConfigParser()

            groups = set()
            for repo in all_repositories:
                groups |= {
                    name
                    for name in repo.maintainers() if name[0] == '@'
                }
                groups |= {name for name in repo.writers() if name[0] == '@'}
                groups |= {name for name in repo.readers() if name[0] == '@'}

            authz.add_section('groups')
            for group in groups:
                members = expand_user_set(self.env, [group])
                authz.set('groups', group[1:], ', '.join(sorted(members)))
            authenticated = sorted({u[0] for u in self.env.get_known_users()})
            authz.set('groups', 'authenticated', ', '.join(authenticated))

            for repo in all_repositories:
                section = repo.reponame + ':/'
                authz.add_section(section)
                r = repo.maintainers() | repo.writers() | repo.readers()

                def apply_user_list(users, action):
                    if not users:
                        return
                    if 'anonymous' in users:
                        authz.set(section, '*', action)
                        return
                    if 'authenticated' in users:
                        authz.set(section, '@authenticated', action)
                        return
                    for user in sorted(users):
                        authz.set(section, user, action)

                apply_user_list(r, 'r')

            self._prepare_base_directory(authz_source_path)
            with open(authz_source_path, 'wb') as authz_file:
                authz.write(authz_file)
            try:
                modes = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IWGRP
                os.chmod(authz_source_path, modes)
            except:
                pass

    ### Private methods
    def _get_repository_connector(self, repo_type):
        """Get the matching connector with maximum priority."""
        return max(((connector, type, prio) for connector in self.connectors
                    for (type, prio) in connector.get_supported_types()
                    if prio >= 0 and type == repo_type),
                   key=lambda x: x[2])[0]

    def _prepare_base_directory(self, directory):
        """Create the base directories and set the correct modes."""
        base = os.path.dirname(directory)
        original_umask = os.umask(0)
        try:
            os.makedirs(base, stat.S_IRWXU | stat.S_IRWXG)
        except OSError, e:
            if e.errno == errno.EEXIST and os.path.isdir(base):
                pass
            else:
                raise
        finally: