Changeset - d332fca29474
[Not reviewed]
default
3 3 3
Mads Kiilerich (mads) - 5 years ago 2020-10-10 21:05:12
mads@kiilerich.com
Grafted from: 8615cedf87bd
config: move various py templates to kallithea/templates/py/

For some reason, we had some python templates in kallithea/config .
kallithea.config is mainly the TG entry point, and thus a high level controller
thing - not a place to store templates.

Instead, use the templates directory and introduce a new py subdirectory.

With git hook templates in a templates directory, there is no need for tmpl in
the name.
6 files changed with 3 insertions and 9 deletions:
0 comments (0 inline, 0 general)
.coveragerc
Show inline comments
 
[run]
 
omit =
 
    # the bin scripts are not part of the Kallithea web app
 
    kallithea/bin/*
 
    # we ship with no active extensions
 
    kallithea/config/rcextensions/*
 
    # dbmigrate is not a part of the Kallithea web app
 
    kallithea/lib/dbmigrate/*
 
    # the tests themselves should not be part of the coverage report
 
    kallithea/tests/*
 
    # the scm hooks are not run in the kallithea process
 
    kallithea/config/post_receive_tmpl.py
 
    kallithea/config/pre_receive_tmpl.py
 

	
 
# same omit lines should be present in sections 'run' and 'report'
 
[report]
 
omit =
 
    # the bin scripts are not part of the Kallithea web app
 
    kallithea/bin/*
 
    # we ship with no active extensions
 
    kallithea/config/rcextensions/*
 
    # dbmigrate is not a part of the Kallithea web app
 
    kallithea/lib/dbmigrate/*
 
    # the tests themselves should not be part of the coverage report
 
    kallithea/tests/*
 
    # the scm hooks are not run in the kallithea process
 
    kallithea/config/post_receive_tmpl.py
 
    kallithea/config/pre_receive_tmpl.py
 

	
 
[paths]
 
source =
 
    kallithea/
 
    **/workspace/*/kallithea
kallithea/bin/kallithea_cli_extensions.py
Show inline comments
 
# -*- coding: utf-8 -*-
 
# This program is free software: you can redistribute it and/or modify
 
# it under the terms of the GNU General Public License as published by
 
# the Free Software Foundation, either version 3 of the License, or
 
# (at your option) any later version.
 
#
 
# This program is distributed in the hope that it will be useful,
 
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
# GNU General Public License for more details.
 
#
 
# You should have received a copy of the GNU General Public License
 
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
"""
 
This file was forked by the Kallithea project in July 2014 and later moved.
 
Original author and date, and relevant copyright and licensing information is below:
 
:created_on: Mar 6, 2012
 
:author: marcink
 
:copyright: (c) 2013 RhodeCode GmbH, and others.
 
:license: GPLv3, see LICENSE.md for more details.
 
"""
 
import os
 

	
 
import click
 
import pkg_resources
 

	
 
import kallithea
 
import kallithea.bin.kallithea_cli_base as cli_base
 
from kallithea.lib.utils2 import ask_ok
 

	
 

	
 
@cli_base.register_command(needs_config_file=True)
 
def extensions_create():
 
    """Write template file for extending Kallithea in Python.
 

	
 
    Create a template `extensions.py` file next to the ini file. Local
 
    customizations in that file will survive upgrades. The file contains
 
    instructions on how it can be customized.
 
    """
 
    here = kallithea.CONFIG['here']
 
    content = pkg_resources.resource_string(
 
        'kallithea', os.path.join('config', 'extensions', 'extensions.py')
 
        'kallithea', os.path.join('templates', 'py', 'extensions.py')
 
    )
 
    ext_file = os.path.join(here, 'extensions.py')
 
    if os.path.exists(ext_file):
 
        msg = ('Extension file %s already exists, do you want '
 
               'to overwrite it ? [y/n] ') % ext_file
 
        if not ask_ok(msg):
 
            click.echo('Nothing done, exiting...')
 
            return
 

	
 
    dirname = os.path.dirname(ext_file)
 
    if not os.path.isdir(dirname):
 
        os.makedirs(dirname)
 
    with open(ext_file, 'wb') as f:
 
        f.write(content)
 
        click.echo('Wrote new extensions file to %s' % ext_file)
kallithea/model/scm.py
Show inline comments
 
@@ -324,438 +324,438 @@ class ScmModel(object):
 

	
 
    def _handle_push(self, repo, username, ip_addr, action, repo_name, revisions):
 
        """
 
        Handle that the repository has changed.
 
        Adds an action log entry with the new revisions, and the head revision
 
        cache and in-memory caches are invalidated/updated.
 

	
 
        :param username: username who pushes
 
        :param action: push/push_local/push_remote
 
        :param repo_name: name of repo
 
        :param revisions: list of revisions that we pushed
 
        """
 
        set_hook_environment(username, ip_addr, repo_name, repo_alias=repo.alias, action=action)
 
        process_pushed_raw_ids(revisions) # also calls mark_for_invalidation
 

	
 
    def _get_IMC_module(self, scm_type):
 
        """
 
        Returns InMemoryCommit class based on scm_type
 

	
 
        :param scm_type:
 
        """
 
        if scm_type == 'hg':
 
            from kallithea.lib.vcs.backends.hg import MercurialInMemoryChangeset
 
            return MercurialInMemoryChangeset
 

	
 
        if scm_type == 'git':
 
            from kallithea.lib.vcs.backends.git import GitInMemoryChangeset
 
            return GitInMemoryChangeset
 

	
 
        raise Exception('Invalid scm_type, must be one of hg,git got %s'
 
                        % (scm_type,))
 

	
 
    def pull_changes(self, repo, username, ip_addr, clone_uri=None):
 
        """
 
        Pull from "clone URL" or fork origin.
 
        """
 
        dbrepo = self.__get_repo(repo)
 
        if clone_uri is None:
 
            clone_uri = dbrepo.clone_uri or dbrepo.fork and dbrepo.fork.repo_full_path
 
        if not clone_uri:
 
            raise Exception("This repository doesn't have a clone uri")
 

	
 
        repo = dbrepo.scm_instance
 
        repo_name = dbrepo.repo_name
 
        try:
 
            if repo.alias == 'git':
 
                repo.fetch(clone_uri)
 
                # git doesn't really have something like post-fetch action
 
                # we fake that now.
 
                # TODO: extract fetched revisions ... somehow ...
 
                self._handle_push(repo,
 
                                  username=username,
 
                                  ip_addr=ip_addr,
 
                                  action='push_remote',
 
                                  repo_name=repo_name,
 
                                  revisions=[])
 
            else:
 
                set_hook_environment(username, ip_addr, dbrepo.repo_name,
 
                                           repo.alias, action='push_remote')
 
                repo.pull(clone_uri)
 
        except Exception:
 
            log.error(traceback.format_exc())
 
            raise
 

	
 
    def commit_change(self, repo, repo_name, cs, user, ip_addr, author, message,
 
                      content, f_path):
 
        """
 
        Commit a change to a single file
 

	
 
        :param repo: a db_repo.scm_instance
 
        """
 
        user = User.guess_instance(user)
 
        IMC = self._get_IMC_module(repo.alias)
 
        imc = IMC(repo)
 
        imc.change(FileNode(f_path, content, mode=cs.get_file_mode(f_path)))
 
        try:
 
            tip = imc.commit(message=message, author=author,
 
                             parents=[cs], branch=cs.branch)
 
        except Exception as e:
 
            log.error(traceback.format_exc())
 
            # clear caches - we also want a fresh object if commit fails
 
            self.mark_for_invalidation(repo_name)
 
            raise IMCCommitError(str(e))
 
        self._handle_push(repo,
 
                          username=user.username,
 
                          ip_addr=ip_addr,
 
                          action='push_local',
 
                          repo_name=repo_name,
 
                          revisions=[tip.raw_id])
 
        return tip
 

	
 
    def _sanitize_path(self, f_path):
 
        if f_path.startswith('/') or f_path.startswith('.') or '../' in f_path:
 
            raise NonRelativePathError('%s is not an relative path' % f_path)
 
        if f_path:
 
            f_path = posixpath.normpath(f_path)
 
        return f_path
 

	
 
    def get_nodes(self, repo_name, revision, root_path='/', flat=True):
 
        """
 
        Recursively walk root dir and return a set of all paths found.
 

	
 
        :param repo_name: name of repository
 
        :param revision: revision for which to list nodes
 
        :param root_path: root path to list
 
        :param flat: return as a list, if False returns a dict with description
 

	
 
        """
 
        _files = list()
 
        _dirs = list()
 
        try:
 
            _repo = self.__get_repo(repo_name)
 
            changeset = _repo.scm_instance.get_changeset(revision)
 
            root_path = root_path.lstrip('/')
 
            for topnode, dirs, files in changeset.walk(root_path):
 
                for f in files:
 
                    _files.append(f.path if flat else {"name": f.path,
 
                                                       "type": "file"})
 
                for d in dirs:
 
                    _dirs.append(d.path if flat else {"name": d.path,
 
                                                      "type": "dir"})
 
        except RepositoryError:
 
            log.debug(traceback.format_exc())
 
            raise
 

	
 
        return _dirs, _files
 

	
 
    def create_nodes(self, user, ip_addr, repo, message, nodes, parent_cs=None,
 
                     author=None, trigger_push_hook=True):
 
        """
 
        Commits specified nodes to repo.
 

	
 
        :param user: Kallithea User object or user_id, the committer
 
        :param repo: Kallithea Repository object
 
        :param message: commit message
 
        :param nodes: mapping {filename:{'content':content},...}
 
        :param parent_cs: parent changeset, can be empty than it's initial commit
 
        :param author: author of commit, cna be different that committer only for git
 
        :param trigger_push_hook: trigger push hooks
 

	
 
        :returns: new committed changeset
 
        """
 

	
 
        user = User.guess_instance(user)
 
        scm_instance = repo.scm_instance_no_cache()
 

	
 
        processed_nodes = []
 
        for f_path in nodes:
 
            content = nodes[f_path]['content']
 
            f_path = self._sanitize_path(f_path)
 
            if not isinstance(content, str) and not isinstance(content, bytes):
 
                content = content.read()
 
            processed_nodes.append((f_path, content))
 

	
 
        message = message
 
        committer = user.full_contact
 
        if not author:
 
            author = committer
 

	
 
        IMC = self._get_IMC_module(scm_instance.alias)
 
        imc = IMC(scm_instance)
 

	
 
        if not parent_cs:
 
            parent_cs = EmptyChangeset(alias=scm_instance.alias)
 

	
 
        if isinstance(parent_cs, EmptyChangeset):
 
            # EmptyChangeset means we we're editing empty repository
 
            parents = None
 
        else:
 
            parents = [parent_cs]
 
        # add multiple nodes
 
        for path, content in processed_nodes:
 
            imc.add(FileNode(path, content=content))
 

	
 
        tip = imc.commit(message=message,
 
                         author=author,
 
                         parents=parents,
 
                         branch=parent_cs.branch)
 

	
 
        if trigger_push_hook:
 
            self._handle_push(scm_instance,
 
                              username=user.username,
 
                              ip_addr=ip_addr,
 
                              action='push_local',
 
                              repo_name=repo.repo_name,
 
                              revisions=[tip.raw_id])
 
        else:
 
            self.mark_for_invalidation(repo.repo_name)
 
        return tip
 

	
 
    def update_nodes(self, user, ip_addr, repo, message, nodes, parent_cs=None,
 
                     author=None, trigger_push_hook=True):
 
        """
 
        Commits specified nodes to repo. Again.
 
        """
 
        user = User.guess_instance(user)
 
        scm_instance = repo.scm_instance_no_cache()
 

	
 
        message = message
 
        committer = user.full_contact
 
        if not author:
 
            author = committer
 

	
 
        imc_class = self._get_IMC_module(scm_instance.alias)
 
        imc = imc_class(scm_instance)
 

	
 
        if not parent_cs:
 
            parent_cs = EmptyChangeset(alias=scm_instance.alias)
 

	
 
        if isinstance(parent_cs, EmptyChangeset):
 
            # EmptyChangeset means we we're editing empty repository
 
            parents = None
 
        else:
 
            parents = [parent_cs]
 

	
 
        # add multiple nodes
 
        for _filename, data in nodes.items():
 
            # new filename, can be renamed from the old one
 
            filename = self._sanitize_path(data['filename'])
 
            old_filename = self._sanitize_path(_filename)
 
            content = data['content']
 

	
 
            filenode = FileNode(old_filename, content=content)
 
            op = data['op']
 
            if op == 'add':
 
                imc.add(filenode)
 
            elif op == 'del':
 
                imc.remove(filenode)
 
            elif op == 'mod':
 
                if filename != old_filename:
 
                    # TODO: handle renames, needs vcs lib changes
 
                    imc.remove(filenode)
 
                    imc.add(FileNode(filename, content=content))
 
                else:
 
                    imc.change(filenode)
 

	
 
        # commit changes
 
        tip = imc.commit(message=message,
 
                         author=author,
 
                         parents=parents,
 
                         branch=parent_cs.branch)
 

	
 
        if trigger_push_hook:
 
            self._handle_push(scm_instance,
 
                              username=user.username,
 
                              ip_addr=ip_addr,
 
                              action='push_local',
 
                              repo_name=repo.repo_name,
 
                              revisions=[tip.raw_id])
 
        else:
 
            self.mark_for_invalidation(repo.repo_name)
 

	
 
    def delete_nodes(self, user, ip_addr, repo, message, nodes, parent_cs=None,
 
                     author=None, trigger_push_hook=True):
 
        """
 
        Deletes specified nodes from repo.
 

	
 
        :param user: Kallithea User object or user_id, the committer
 
        :param repo: Kallithea Repository object
 
        :param message: commit message
 
        :param nodes: mapping {filename:{'content':content},...}
 
        :param parent_cs: parent changeset, can be empty than it's initial commit
 
        :param author: author of commit, cna be different that committer only for git
 
        :param trigger_push_hook: trigger push hooks
 

	
 
        :returns: new committed changeset after deletion
 
        """
 

	
 
        user = User.guess_instance(user)
 
        scm_instance = repo.scm_instance_no_cache()
 

	
 
        processed_nodes = []
 
        for f_path in nodes:
 
            f_path = self._sanitize_path(f_path)
 
            # content can be empty but for compatibility it allows same dicts
 
            # structure as add_nodes
 
            content = nodes[f_path].get('content')
 
            processed_nodes.append((f_path, content))
 

	
 
        message = message
 
        committer = user.full_contact
 
        if not author:
 
            author = committer
 

	
 
        IMC = self._get_IMC_module(scm_instance.alias)
 
        imc = IMC(scm_instance)
 

	
 
        if not parent_cs:
 
            parent_cs = EmptyChangeset(alias=scm_instance.alias)
 

	
 
        if isinstance(parent_cs, EmptyChangeset):
 
            # EmptyChangeset means we we're editing empty repository
 
            parents = None
 
        else:
 
            parents = [parent_cs]
 
        # add multiple nodes
 
        for path, content in processed_nodes:
 
            imc.remove(FileNode(path, content=content))
 

	
 
        tip = imc.commit(message=message,
 
                         author=author,
 
                         parents=parents,
 
                         branch=parent_cs.branch)
 

	
 
        if trigger_push_hook:
 
            self._handle_push(scm_instance,
 
                              username=user.username,
 
                              ip_addr=ip_addr,
 
                              action='push_local',
 
                              repo_name=repo.repo_name,
 
                              revisions=[tip.raw_id])
 
        else:
 
            self.mark_for_invalidation(repo.repo_name)
 
        return tip
 

	
 
    def get_unread_journal(self):
 
        return UserLog.query().count()
 

	
 
    def get_repo_landing_revs(self, repo=None):
 
        """
 
        Generates select option with tags branches and bookmarks (for hg only)
 
        grouped by type
 

	
 
        :param repo:
 
        """
 

	
 
        hist_l = []
 
        choices = []
 
        hist_l.append(('rev:tip', _('latest tip')))
 
        choices.append('rev:tip')
 
        if repo is None:
 
            return choices, hist_l
 

	
 
        repo = self.__get_repo(repo)
 
        repo = repo.scm_instance
 

	
 
        branches_group = ([('branch:%s' % k, k) for k, v in
 
                           repo.branches.items()], _("Branches"))
 
        hist_l.append(branches_group)
 
        choices.extend([x[0] for x in branches_group[0]])
 

	
 
        if repo.alias == 'hg':
 
            bookmarks_group = ([('book:%s' % k, k) for k, v in
 
                                repo.bookmarks.items()], _("Bookmarks"))
 
            hist_l.append(bookmarks_group)
 
            choices.extend([x[0] for x in bookmarks_group[0]])
 

	
 
        tags_group = ([('tag:%s' % k, k) for k, v in
 
                       repo.tags.items()], _("Tags"))
 
        hist_l.append(tags_group)
 
        choices.extend([x[0] for x in tags_group[0]])
 

	
 
        return choices, hist_l
 

	
 
    def _get_git_hook_interpreter(self):
 
        """Return a suitable interpreter for Git hooks.
 

	
 
        Return a suitable string to be written in the POSIX #! shebang line for
 
        Git hook scripts so they invoke Kallithea code with the right Python
 
        interpreter and in the right environment.
 
        """
 
        # Note: sys.executable might not point at a usable Python interpreter. For
 
        # example, when using uwsgi, it will point at the uwsgi program itself.
 
        # FIXME This may not work on Windows and may need a shell wrapper script.
 
        return (kallithea.CONFIG.get('git_hook_interpreter')
 
                or sys.executable
 
                or '/usr/bin/env python3')
 

	
 
    def install_git_hooks(self, repo, force=False):
 
        """
 
        Creates a kallithea hook inside a git repository
 

	
 
        :param repo: Instance of VCS repo
 
        :param force: Overwrite existing non-Kallithea hooks
 
        """
 

	
 
        hooks_path = os.path.join(repo.path, 'hooks')
 
        if not repo.bare:
 
            hooks_path = os.path.join(repo.path, '.git', 'hooks')
 
        if not os.path.isdir(hooks_path):
 
            os.makedirs(hooks_path)
 

	
 
        tmpl_post = b"#!%s\n" % safe_bytes(self._get_git_hook_interpreter())
 
        tmpl_post += pkg_resources.resource_string(
 
            'kallithea', os.path.join('config', 'post_receive_tmpl.py')
 
            'kallithea', os.path.join('templates', 'py', 'git_post_receive_hook.py')
 
        )
 
        tmpl_pre = b"#!%s\n" % safe_bytes(self._get_git_hook_interpreter())
 
        tmpl_pre += pkg_resources.resource_string(
 
            'kallithea', os.path.join('config', 'pre_receive_tmpl.py')
 
            'kallithea', os.path.join('templates', 'py', 'git_pre_receive_hook.py')
 
        )
 

	
 
        for h_type, tmpl in [('pre', tmpl_pre), ('post', tmpl_post)]:
 
            hook_file = os.path.join(hooks_path, '%s-receive' % h_type)
 
            other_hook = False
 
            log.debug('Installing git hook in repo %s', repo)
 
            if os.path.exists(hook_file):
 
                # let's take a look at this hook, maybe it's kallithea ?
 
                log.debug('hook exists, checking if it is from kallithea')
 
                with open(hook_file, 'rb') as f:
 
                    data = f.read()
 
                    matches = re.search(br'^KALLITHEA_HOOK_VER\s*=\s*(.*)$', data, flags=re.MULTILINE)
 
                    if matches:
 
                        ver = matches.groups()[0]
 
                        log.debug('Found Kallithea hook - it has KALLITHEA_HOOK_VER %r', ver)
 
                    else:
 
                        log.debug('Found non-Kallithea hook at %s', hook_file)
 
                        other_hook = True
 

	
 
            if other_hook and not force:
 
                log.warning('skipping overwriting hook file %s', hook_file)
 
            else:
 
                log.debug('writing %s hook file !', h_type)
 
                try:
 
                    with open(hook_file, 'wb') as f:
 
                        tmpl = tmpl.replace(b'_TMPL_', safe_bytes(kallithea.__version__))
 
                        f.write(tmpl)
 
                    os.chmod(hook_file, 0o755)
 
                except IOError as e:
 
                    log.error('error writing hook %s: %s', hook_file, e)
 

	
 

	
 
def AvailableRepoGroupChoices(repo_group_perm_level, extras=()):
 
    """Return group_id,string tuples with choices for all the repo groups where
 
    the user has the necessary permissions.
 

	
 
    Top level is -1.
 
    """
 
    groups = RepoGroup.query().all()
 
    if HasPermissionAny('hg.admin')('available repo groups'):
 
        groups.append(None)
 
    else:
 
        groups = list(RepoGroupList(groups, perm_level=repo_group_perm_level))
 
        if HasPermissionAny('hg.create.repository')('available repo groups'):
 
            groups.append(None)
 
        for extra in extras:
 
            if not any(rg == extra for rg in groups):
 
                groups.append(extra)
 
    return RepoGroup.groups_choices(groups=groups)
kallithea/templates/py/extensions.py
Show inline comments
 
file renamed from kallithea/config/extensions/extensions.py to kallithea/templates/py/extensions.py
kallithea/templates/py/git_post_receive_hook.py
Show inline comments
 
file renamed from kallithea/config/post_receive_tmpl.py to kallithea/templates/py/git_post_receive_hook.py
kallithea/templates/py/git_pre_receive_hook.py
Show inline comments
 
file renamed from kallithea/config/pre_receive_tmpl.py to kallithea/templates/py/git_pre_receive_hook.py
0 comments (0 inline, 0 general)