Changeset - f01bad8101e4
[Not reviewed]
default
0 6 0
Mads Kiilerich (mads) - 5 years ago 2020-11-09 15:58:52
mads@kiilerich.com
Grafted from: 0baf4e62d244
lib: drop is_hg and is_git

It is just as simple to be explicit.
6 files changed with 10 insertions and 29 deletions:
0 comments (0 inline, 0 general)
kallithea/controllers/pullrequests.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/>.
 
"""
 
kallithea.controllers.pullrequests
 
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 

	
 
pull requests controller for Kallithea for initializing pull requests
 

	
 
This file was forked by the Kallithea project in July 2014.
 
Original author and date, and relevant copyright and licensing information is below:
 
:created_on: May 7, 2012
 
:author: marcink
 
:copyright: (c) 2013 RhodeCode GmbH, and others.
 
:license: GPLv3, see LICENSE.md for more details.
 
"""
 

	
 
import logging
 
import traceback
 

	
 
import formencode
 
import mercurial.unionrepo
 
from tg import request
 
from tg import tmpl_context as c
 
from tg.i18n import ugettext as _
 
from webob.exc import HTTPBadRequest, HTTPForbidden, HTTPFound, HTTPNotFound
 

	
 
import kallithea.lib.helpers as h
 
from kallithea.controllers.changeset import create_cs_pr_comment, delete_cs_pr_comment
 
from kallithea.lib import auth, diffs, webutils
 
from kallithea.lib.auth import HasRepoPermissionLevelDecorator, LoginRequired
 
from kallithea.lib.base import BaseRepoController, jsonify, render
 
from kallithea.lib.graphmod import graph_data
 
from kallithea.lib.page import Page
 
from kallithea.lib.utils2 import ascii_bytes, safe_bytes, safe_int
 
from kallithea.lib.vcs.exceptions import ChangesetDoesNotExistError, EmptyRepositoryError
 
from kallithea.lib.webutils import url
 
from kallithea.model import db, meta
 
from kallithea.model.changeset_status import ChangesetStatusModel
 
from kallithea.model.comment import ChangesetCommentsModel
 
from kallithea.model.forms import PullRequestForm, PullRequestPostForm
 
from kallithea.model.pull_request import CreatePullRequestAction, CreatePullRequestIterationAction, PullRequestModel
 

	
 

	
 
log = logging.getLogger(__name__)
 

	
 

	
 
def _get_reviewer(user_id):
 
    """Look up user by ID and validate it as a potential reviewer."""
 
    try:
 
        user = db.User.get(int(user_id))
 
    except ValueError:
 
        user = None
 

	
 
    if user is None or user.is_default_user:
 
        webutils.flash(_('Invalid reviewer "%s" specified') % user_id, category='error')
 
        raise HTTPBadRequest()
 

	
 
    return user
 

	
 

	
 
class PullrequestsController(BaseRepoController):
 

	
 
    def _get_repo_refs(self, repo, rev=None, branch=None, branch_rev=None):
 
        """return a structure with repo's interesting changesets, suitable for
 
        """return a structure with scm repo's interesting changesets, suitable for
 
        the selectors in pullrequest.html
 

	
 
        rev: a revision that must be in the list somehow and selected by default
 
        branch: a branch that must be in the list and selected by default - even if closed
 
        branch_rev: a revision of which peers should be preferred and available."""
 
        # list named branches that has been merged to this named branch - it should probably merge back
 
        peers = []
 

	
 
        if branch_rev:
 
            # a revset not restricting to merge() would be better
 
            # (especially because it would get the branch point)
 
            # ... but is currently too expensive
 
            # including branches of children could be nice too
 
            peerbranches = set()
 
            for i in repo._repo.revs(
 
                b"sort(parents(branch(id(%s)) and merge()) - branch(id(%s)), -rev)",
 
                ascii_bytes(branch_rev), ascii_bytes(branch_rev),
 
            ):
 
                for abranch in repo.get_changeset(i).branches:
 
                    if abranch not in peerbranches:
 
                        n = 'branch:%s:%s' % (abranch, repo.get_changeset(abranch).raw_id)
 
                        peers.append((n, abranch))
 
                        peerbranches.add(abranch)
 

	
 
        selected = None
 
        tiprev = repo.tags.get('tip')
 
        tipbranch = None
 

	
 
        branches = []
 
        for abranch, branchrev in repo.branches.items():
 
            n = 'branch:%s:%s' % (abranch, branchrev)
 
            desc = abranch
 
            if branchrev == tiprev:
 
                tipbranch = abranch
 
                desc = '%s (current tip)' % desc
 
            branches.append((n, desc))
 
            if rev == branchrev:
 
                selected = n
 
            if branch == abranch:
 
                if not rev:
 
                    selected = n
 
                branch = None
 
        if branch:  # branch not in list - it is probably closed
 
            branchrev = repo.closed_branches.get(branch)
 
            if branchrev:
 
                n = 'branch:%s:%s' % (branch, branchrev)
 
                branches.append((n, _('%s (closed)') % branch))
 
                selected = n
 
                branch = None
 
            if branch:
 
                log.debug('branch %r not found in %s', branch, repo)
 

	
 
        bookmarks = []
 
        for bookmark, bookmarkrev in repo.bookmarks.items():
 
            n = 'book:%s:%s' % (bookmark, bookmarkrev)
 
            bookmarks.append((n, bookmark))
 
            if rev == bookmarkrev:
 
                selected = n
 

	
 
        tags = []
 
        for tag, tagrev in repo.tags.items():
 
            if tag == 'tip':
 
                continue
 
            n = 'tag:%s:%s' % (tag, tagrev)
 
            tags.append((n, tag))
 
            # note: even if rev == tagrev, don't select the static tag - it must be chosen explicitly
 

	
 
        # prio 1: rev was selected as existing entry above
 

	
 
        # prio 2: create special entry for rev; rev _must_ be used
 
        specials = []
 
        if rev and selected is None:
 
            selected = 'rev:%s:%s' % (rev, rev)
 
            specials = [(selected, '%s: %s' % (_("Changeset"), rev[:12]))]
 

	
 
        # prio 3: most recent peer branch
 
        if peers and not selected:
 
            selected = peers[0][0]
 

	
 
        # prio 4: tip revision
 
        if not selected:
 
            if h.is_hg(repo):
 
            if repo.alias == 'hg':
 
                if tipbranch:
 
                    selected = 'branch:%s:%s' % (tipbranch, tiprev)
 
                else:
 
                    selected = 'tag:null:' + repo.EMPTY_CHANGESET
 
                    tags.append((selected, 'null'))
 
            else:  # Git
 
                assert repo.alias == 'git'
 
                if not repo.branches:
 
                    selected = ''  # doesn't make sense, but better than nothing
 
                elif 'master' in repo.branches:
 
                    selected = 'branch:master:%s' % repo.branches['master']
 
                else:
 
                    k, v = list(repo.branches.items())[0]
 
                    selected = 'branch:%s:%s' % (k, v)
 

	
 
        groups = [(specials, _("Special")),
 
                  (peers, _("Peer branches")),
 
                  (bookmarks, _("Bookmarks")),
 
                  (branches, _("Branches")),
 
                  (tags, _("Tags")),
 
                  ]
 
        return [g for g in groups if g[0]], selected
 

	
 
    def _is_allowed_to_change_status(self, pull_request):
 
        if pull_request.is_closed():
 
            return False
 

	
 
        owner = request.authuser.user_id == pull_request.owner_id
 
        reviewer = db.PullRequestReviewer.query() \
 
            .filter(db.PullRequestReviewer.pull_request == pull_request) \
 
            .filter(db.PullRequestReviewer.user_id == request.authuser.user_id) \
 
            .count() != 0
 

	
 
        return request.authuser.admin or owner or reviewer
 

	
 
    @LoginRequired(allow_default_user=True)
 
    @HasRepoPermissionLevelDecorator('read')
 
    def show_all(self, repo_name):
 
        c.from_ = request.GET.get('from_') or ''
 
        c.closed = request.GET.get('closed') or ''
 
        url_params = {}
 
        if c.from_:
 
            url_params['from_'] = 1
 
        if c.closed:
 
            url_params['closed'] = 1
 
        p = safe_int(request.GET.get('page'), 1)
 

	
 
        q = db.PullRequest.query(include_closed=c.closed, sorted=True)
 
        if c.from_:
 
            q = q.filter_by(org_repo=c.db_repo)
 
        else:
 
            q = q.filter_by(other_repo=c.db_repo)
 
        c.pull_requests = q.all()
 

	
 
        c.pullrequests_pager = Page(c.pull_requests, page=p, items_per_page=100, **url_params)
 

	
 
        return render('/pullrequests/pullrequest_show_all.html')
 

	
 
    @LoginRequired()
 
    def show_my(self):
 
        c.closed = request.GET.get('closed') or ''
 

	
 
        c.my_pull_requests = db.PullRequest.query(
 
            include_closed=c.closed,
 
            sorted=True,
 
        ).filter_by(owner_id=request.authuser.user_id).all()
 

	
 
        c.participate_in_pull_requests = []
 
        c.participate_in_pull_requests_todo = []
 
        done_status = set([db.ChangesetStatus.STATUS_APPROVED, db.ChangesetStatus.STATUS_REJECTED])
 
        for pr in db.PullRequest.query(
 
            include_closed=c.closed,
 
            reviewer_id=request.authuser.user_id,
 
            sorted=True,
 
        ):
 
            status = pr.user_review_status(request.authuser.user_id) # very inefficient!!!
 
            if status in done_status:
 
                c.participate_in_pull_requests.append(pr)
 
            else:
 
                c.participate_in_pull_requests_todo.append(pr)
 

	
 
        return render('/pullrequests/pullrequest_show_my.html')
 

	
 
    @LoginRequired()
 
    @HasRepoPermissionLevelDecorator('read')
 
    def index(self):
 
        org_repo = c.db_repo
 
        org_scm_instance = org_repo.scm_instance
 
        try:
 
            org_scm_instance.get_changeset()
 
        except EmptyRepositoryError as e:
 
            webutils.flash(_('There are no changesets yet'),
 
                    category='warning')
 
            raise HTTPFound(location=url('summary_home', repo_name=org_repo.repo_name))
 

	
 
        org_rev = request.GET.get('rev_end')
 
        # rev_start is not directly useful - its parent could however be used
 
        # as default for other and thus give a simple compare view
 
        rev_start = request.GET.get('rev_start')
 
        other_rev = None
 
        if rev_start:
 
            starters = org_repo.get_changeset(rev_start).parents
 
            if starters:
 
                other_rev = starters[0].raw_id
 
            else:
 
                other_rev = org_repo.scm_instance.EMPTY_CHANGESET
 
        branch = request.GET.get('branch')
 

	
 
        c.cs_repos = [(org_repo.repo_name, org_repo.repo_name)]
 
        c.default_cs_repo = org_repo.repo_name
 
        c.cs_refs, c.default_cs_ref = self._get_repo_refs(org_scm_instance, rev=org_rev, branch=branch)
 

	
 
        default_cs_ref_type, default_cs_branch, default_cs_rev = c.default_cs_ref.split(':')
 
        if default_cs_ref_type != 'branch':
 
            default_cs_branch = org_repo.get_changeset(default_cs_rev).branch
 

	
 
        # add org repo to other so we can open pull request against peer branches on itself
 
        c.a_repos = [(org_repo.repo_name, '%s (self)' % org_repo.repo_name)]
 

	
 
        if org_repo.parent:
 
            # add parent of this fork also and select it.
 
            # use the same branch on destination as on source, if available.
 
            c.a_repos.append((org_repo.parent.repo_name, '%s (parent)' % org_repo.parent.repo_name))
 
            c.a_repo = org_repo.parent
 
            c.a_refs, c.default_a_ref = self._get_repo_refs(
 
                    org_repo.parent.scm_instance, branch=default_cs_branch, rev=other_rev)
 

	
 
        else:
 
            c.a_repo = org_repo
 
            c.a_refs, c.default_a_ref = self._get_repo_refs(org_scm_instance, rev=other_rev)
 

	
 
        # gather forks and add to this list ... even though it is rare to
 
        # request forks to pull from their parent
 
        for fork in org_repo.forks:
 
            c.a_repos.append((fork.repo_name, fork.repo_name))
 

	
 
        return render('/pullrequests/pullrequest.html')
 

	
 
    @LoginRequired()
 
    @HasRepoPermissionLevelDecorator('read')
 
    @jsonify
 
    def repo_info(self, repo_name):
 
        repo = c.db_repo
 
        refs, selected_ref = self._get_repo_refs(repo.scm_instance)
 
        return {
 
            'description': repo.description.split('\n', 1)[0],
 
            'selected_ref': selected_ref,
 
            'refs': refs,
 
            }
 

	
 
    @LoginRequired()
 
    @HasRepoPermissionLevelDecorator('read')
 
    def create(self, repo_name):
 
        repo = c.db_repo
 
        try:
 
            _form = PullRequestForm(repo.repo_id)().to_python(request.POST)
 
        except formencode.Invalid as errors:
 
            log.error(traceback.format_exc())
 
            log.error(str(errors))
 
            msg = _('Error creating pull request: %s') % errors.msg
 
            webutils.flash(msg, 'error')
 
            raise HTTPBadRequest
 

	
 
        # heads up: org and other might seem backward here ...
 
        org_ref = _form['org_ref'] # will have merge_rev as rev but symbolic name
 
        org_repo = db.Repository.guess_instance(_form['org_repo'])
 

	
 
        other_ref = _form['other_ref'] # will have symbolic name and head revision
 
        other_repo = db.Repository.guess_instance(_form['other_repo'])
 

	
 
        reviewers = []
 

	
 
        title = _form['pullrequest_title']
 
        description = _form['pullrequest_desc'].strip()
 
        owner = db.User.get(request.authuser.user_id)
 

	
 
        try:
 
            cmd = CreatePullRequestAction(org_repo, other_repo, org_ref, other_ref, title, description, owner, reviewers)
 
        except CreatePullRequestAction.ValidationError as e:
 
            webutils.flash(e, category='error', logf=log.error)
 
            raise HTTPNotFound
 

	
 
        try:
 
            pull_request = cmd.execute()
 
            meta.Session().commit()
 
        except Exception:
 
            webutils.flash(_('Error occurred while creating pull request'),
 
                    category='error')
 
            log.error(traceback.format_exc())
 
            raise HTTPFound(location=url('pullrequest_home', repo_name=repo_name))
 

	
 
        webutils.flash(_('Successfully opened new pull request'),
 
                category='success')
 
        raise HTTPFound(location=pull_request.url())
 

	
 
    def create_new_iteration(self, old_pull_request, new_rev, title, description, reviewers):
 
        owner = db.User.get(request.authuser.user_id)
 
        new_org_rev = self._get_ref_rev(old_pull_request.org_repo, 'rev', new_rev)
 
        new_other_rev = self._get_ref_rev(old_pull_request.other_repo, old_pull_request.other_ref_parts[0], old_pull_request.other_ref_parts[1])
 
        try:
 
            cmd = CreatePullRequestIterationAction(old_pull_request, new_org_rev, new_other_rev, title, description, owner, reviewers)
 
        except CreatePullRequestAction.ValidationError as e:
 
            webutils.flash(e, category='error', logf=log.error)
 
            raise HTTPNotFound
 

	
 
        try:
 
            pull_request = cmd.execute()
 
            meta.Session().commit()
 
        except Exception:
 
            webutils.flash(_('Error occurred while creating pull request'),
 
                    category='error')
 
            log.error(traceback.format_exc())
 
            raise HTTPFound(location=old_pull_request.url())
 

	
 
        webutils.flash(_('New pull request iteration created'),
 
                category='success')
 
        raise HTTPFound(location=pull_request.url())
 

	
 
    # pullrequest_post for PR editing
 
    @LoginRequired()
 
    @HasRepoPermissionLevelDecorator('read')
 
    def post(self, repo_name, pull_request_id):
 
        pull_request = db.PullRequest.get_or_404(pull_request_id)
 
        if pull_request.is_closed():
 
            raise HTTPForbidden()
 
        assert pull_request.other_repo.repo_name == repo_name
 
        # only owner or admin can update it
 
        owner = pull_request.owner_id == request.authuser.user_id
 
        repo_admin = auth.HasRepoPermissionLevel('admin')(c.repo_name)
 
        if not (auth.HasPermissionAny('hg.admin')() or repo_admin or owner):
 
            raise HTTPForbidden()
 

	
 
        _form = PullRequestPostForm()().to_python(request.POST)
 

	
 
        cur_reviewers = set(pull_request.get_reviewer_users())
 
        new_reviewers = set(_get_reviewer(s) for s in _form['review_members'])
 
        old_reviewers = set(_get_reviewer(s) for s in _form['org_review_members'])
 

	
 
        other_added = cur_reviewers - old_reviewers
 
        other_removed = old_reviewers - cur_reviewers
 

	
 
        if other_added:
 
            webutils.flash(_('Meanwhile, the following reviewers have been added: %s') %
 
                    (', '.join(u.username for u in other_added)),
 
                    category='warning')
 
        if other_removed:
 
            webutils.flash(_('Meanwhile, the following reviewers have been removed: %s') %
 
                    (', '.join(u.username for u in other_removed)),
 
                    category='warning')
 

	
 
        if _form['updaterev']:
 
            return self.create_new_iteration(pull_request,
 
                                      _form['updaterev'],
 
                                      _form['pullrequest_title'],
 
                                      _form['pullrequest_desc'],
 
                                      new_reviewers)
 

	
 
        added_reviewers = new_reviewers - old_reviewers - cur_reviewers
 
        removed_reviewers = (old_reviewers - new_reviewers) & cur_reviewers
 

	
 
        old_description = pull_request.description
 
        pull_request.title = _form['pullrequest_title']
 
        pull_request.description = _form['pullrequest_desc'].strip() or _('No description')
 
        pull_request.owner = db.User.get_by_username(_form['owner'])
 
        user = db.User.get(request.authuser.user_id)
 

	
 
        PullRequestModel().mention_from_description(user, pull_request, old_description)
 
        PullRequestModel().add_reviewers(user, pull_request, added_reviewers)
 
        PullRequestModel().remove_reviewers(user, pull_request, removed_reviewers)
 

	
 
        meta.Session().commit()
 
        webutils.flash(_('Pull request updated'), category='success')
 

	
 
        raise HTTPFound(location=pull_request.url())
 

	
 
    @LoginRequired()
 
    @HasRepoPermissionLevelDecorator('read')
 
    @jsonify
 
    def delete(self, repo_name, pull_request_id):
 
        pull_request = db.PullRequest.get_or_404(pull_request_id)
 
        # only owner can delete it !
 
        if pull_request.owner_id == request.authuser.user_id:
 
            PullRequestModel().delete(pull_request)
 
            meta.Session().commit()
 
            webutils.flash(_('Successfully deleted pull request'),
 
                    category='success')
 
            raise HTTPFound(location=url('my_pullrequests'))
 
        raise HTTPForbidden()
 

	
 
    @LoginRequired(allow_default_user=True)
 
    @HasRepoPermissionLevelDecorator('read')
 
    def show(self, repo_name, pull_request_id, extra=None):
 
        c.pull_request = db.PullRequest.get_or_404(pull_request_id)
 
        c.allowed_to_change_status = self._is_allowed_to_change_status(c.pull_request)
 
        cc_model = ChangesetCommentsModel()
 
        cs_model = ChangesetStatusModel()
 

	
 
        # pull_requests repo_name we opened it against
 
        # ie. other_repo must match
 
        if repo_name != c.pull_request.other_repo.repo_name:
 
            raise HTTPNotFound
 

	
 
        # load compare data into template context
 
        c.cs_repo = c.pull_request.org_repo
 
        (c.cs_ref_type,
 
         c.cs_ref_name,
 
         c.cs_rev) = c.pull_request.org_ref.split(':')
 

	
 
        c.a_repo = c.pull_request.other_repo
 
        (c.a_ref_type,
 
         c.a_ref_name,
 
         c.a_rev) = c.pull_request.other_ref.split(':') # a_rev is ancestor
 

	
 
        org_scm_instance = c.cs_repo.scm_instance # property with expensive cache invalidation check!!!
 
        c.cs_ranges = []
 
        for x in c.pull_request.revisions:
 
            try:
 
                c.cs_ranges.append(org_scm_instance.get_changeset(x))
 
            except ChangesetDoesNotExistError:
 
                c.cs_ranges = []
 
                webutils.flash(_('Revision %s not found in %s') % (x, c.cs_repo.repo_name),
 
                    'error')
 
                break
 
        c.cs_ranges_org = None # not stored and not important and moving target - could be calculated ...
 
        revs = [ctx.revision for ctx in reversed(c.cs_ranges)]
 
        c.jsdata = graph_data(org_scm_instance, revs)
 

	
 
        c.is_range = False
 
        try:
 
            if c.a_ref_type == 'rev': # this looks like a free range where target is ancestor
 
                cs_a = org_scm_instance.get_changeset(c.a_rev)
 
                root_parents = c.cs_ranges[0].parents
 
                c.is_range = cs_a in root_parents
 
                #c.merge_root = len(root_parents) > 1 # a range starting with a merge might deserve a warning
 
        except ChangesetDoesNotExistError: # probably because c.a_rev not found
 
            pass
 
        except IndexError: # probably because c.cs_ranges is empty, probably because revisions are missing
 
            pass
 

	
 
        avail_revs = set()
 
        avail_show = []
 
        c.cs_branch_name = c.cs_ref_name
 
        c.a_branch_name = None
 
        other_scm_instance = c.a_repo.scm_instance
 
        c.update_msg = ""
 
        c.update_msg_other = ""
 
        try:
 
            if not c.cs_ranges:
 
                c.update_msg = _('Error: changesets not found when displaying pull request from %s.') % c.cs_rev
 
            elif org_scm_instance.alias == 'hg' and c.a_ref_name != 'ancestor':
 
                if c.cs_ref_type != 'branch':
 
                    c.cs_branch_name = org_scm_instance.get_changeset(c.cs_ref_name).branch # use ref_type ?
 
                c.a_branch_name = c.a_ref_name
 
                if c.a_ref_type != 'branch':
 
                    try:
 
                        c.a_branch_name = other_scm_instance.get_changeset(c.a_ref_name).branch # use ref_type ?
 
                    except EmptyRepositoryError:
 
                        c.a_branch_name = 'null' # not a branch name ... but close enough
 
                # candidates: descendants of old head that are on the right branch
 
                #             and not are the old head itself ...
 
                #             and nothing at all if old head is a descendant of target ref name
 
                if not c.is_range and other_scm_instance._repo.revs('present(%s)::&%s', c.cs_ranges[-1].raw_id, c.a_branch_name):
 
                    c.update_msg = _('This pull request has already been merged to %s.') % c.a_branch_name
 
                elif c.pull_request.is_closed():
 
                    c.update_msg = _('This pull request has been closed and can not be updated.')
 
                else: # look for descendants of PR head on source branch in org repo
 
                    avail_revs = org_scm_instance._repo.revs('%s:: & branch(%s)',
 
                                                             revs[0], c.cs_branch_name)
 
                    if len(avail_revs) > 1: # more than just revs[0]
 
                        # also show changesets that not are descendants but would be merged in
 
                        targethead = other_scm_instance.get_changeset(c.a_branch_name).raw_id
 
                        if org_scm_instance.path != other_scm_instance.path:
 
                            # Note: org_scm_instance.path must come first so all
 
                            # valid revision numbers are 100% org_scm compatible
 
                            # - both for avail_revs and for revset results
 
                            hgrepo = mercurial.unionrepo.makeunionrepository(org_scm_instance.baseui,
 
                                                                   safe_bytes(org_scm_instance.path),
 
                                                                   safe_bytes(other_scm_instance.path))
 
                        else:
 
                            hgrepo = org_scm_instance._repo
 
                        show = set(hgrepo.revs('::%ld & !::parents(%s) & !::%s',
 
                                               avail_revs, revs[0], targethead))
 
                        if show:
 
                            c.update_msg = _('The following additional changes are available on %s:') % c.cs_branch_name
 
                        else:
 
                            c.update_msg = _('No additional changesets found for iterating on this pull request.')
 
                    else:
 
                        show = set()
 
                        avail_revs = set() # drop revs[0]
 
                        c.update_msg = _('No additional changesets found for iterating on this pull request.')
 

	
kallithea/lib/helpers.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/>.
 
"""
 
Helper functions
 

	
 
Consists of functions to typically be used within templates, but also
 
available to Controllers. This module is available to both as 'h'.
 
"""
 
import hashlib
 
import logging
 
import re
 
import textwrap
 
import urllib.parse
 

	
 
from beaker.cache import cache_region
 
from pygments import highlight as code_highlight
 
from pygments.formatters.html import HtmlFormatter
 
from tg import tmpl_context as c
 
from tg.i18n import ugettext as _
 

	
 
import kallithea
 
from kallithea.lib.annotate import annotate_highlight
 
#==============================================================================
 
# PERMS
 
#==============================================================================
 
from kallithea.lib.auth import HasPermissionAny, HasRepoGroupPermissionLevel, HasRepoPermissionLevel
 
from kallithea.lib.diffs import BIN_FILENODE, CHMOD_FILENODE, DEL_FILENODE, MOD_FILENODE, NEW_FILENODE, RENAMED_FILENODE
 
from kallithea.lib.markup_renderer import url_re
 
from kallithea.lib.pygmentsutils import get_custom_lexer
 
from kallithea.lib.utils2 import (MENTIONS_REGEX, AttributeDict, age, asbool, credentials_filter, fmt_date, link_to_ref, safe_bytes, safe_int, safe_str,
 
                                  shorter, time_to_datetime)
 
from kallithea.lib.vcs.backends.base import BaseChangeset, EmptyChangeset
 
from kallithea.lib.vcs.exceptions import ChangesetDoesNotExistError
 
#==============================================================================
 
# SCM FILTERS available via h.
 
#==============================================================================
 
from kallithea.lib.vcs.utils import author_email, author_name
 
from kallithea.lib.webutils import (HTML, Option, canonical_url, checkbox, chop_at, end_form, escape, form, format_byte_size, hidden, html_escape, js, jshtml,
 
                                    link_to, literal, password, pop_flash_messages, radio, reset, safeid, select, session_csrf_secret_name,
 
                                    session_csrf_secret_token, submit, text, textarea, truncate, url, wrap_paragraphs)
 
from kallithea.model import db
 
from kallithea.model.changeset_status import ChangesetStatusModel
 

	
 

	
 
# mute pyflakes "imported but unused"
 
# from webutils
 
assert HTML
 
assert Option
 
assert canonical_url
 
assert checkbox
 
assert chop_at
 
assert end_form
 
assert form
 
assert format_byte_size
 
assert hidden
 
assert js
 
assert jshtml
 
assert password
 
assert pop_flash_messages
 
assert radio
 
assert reset
 
assert safeid
 
assert select
 
assert session_csrf_secret_name
 
assert session_csrf_secret_token
 
assert submit
 
assert text
 
assert textarea
 
assert wrap_paragraphs
 
# from kallithea.lib.auth
 
assert HasPermissionAny
 
assert HasRepoGroupPermissionLevel
 
assert HasRepoPermissionLevel
 
# from utils2
 
assert age
 
assert fmt_date
 
assert link_to_ref
 
assert shorter
 
assert time_to_datetime
 
# from vcs
 
assert EmptyChangeset
 

	
 

	
 
log = logging.getLogger(__name__)
 

	
 

	
 
def FID(raw_id, path):
 
    """
 
    Creates a unique ID for filenode based on it's hash of path and revision
 
    it's safe to use in urls
 
    """
 
    return 'C-%s-%s' % (short_id(raw_id), hashlib.md5(safe_bytes(path)).hexdigest()[:12])
 

	
 

	
 
def get_ignore_whitespace_diff(GET):
 
    """Return true if URL requested whitespace to be ignored"""
 
    return bool(GET.get('ignorews'))
 

	
 

	
 
def ignore_whitespace_link(GET, anchor=None):
 
    """Return snippet with link to current URL with whitespace ignoring toggled"""
 
    params = dict(GET)  # ignoring duplicates
 
    if get_ignore_whitespace_diff(GET):
 
        params.pop('ignorews')
 
        title = _("Show whitespace changes")
 
    else:
 
        params['ignorews'] = '1'
 
        title = _("Ignore whitespace changes")
 
    params['anchor'] = anchor
 
    return link_to(
 
        literal('<i class="icon-strike"></i>'),
 
        url.current(**params),
 
        title=title,
 
        **{'data-toggle': 'tooltip'})
 

	
 

	
 
def get_diff_context_size(GET):
 
    """Return effective context size requested in URL"""
 
    return safe_int(GET.get('context'), default=3)
 

	
 

	
 
def increase_context_link(GET, anchor=None):
 
    """Return snippet with link to current URL with double context size"""
 
    context = get_diff_context_size(GET) * 2
 
    params = dict(GET)  # ignoring duplicates
 
    params['context'] = str(context)
 
    params['anchor'] = anchor
 
    return link_to(
 
        literal('<i class="icon-sort"></i>'),
 
        url.current(**params),
 
        title=_('Increase diff context to %(num)s lines') % {'num': context},
 
        **{'data-toggle': 'tooltip'})
 

	
 

	
 
def files_breadcrumbs(repo_name, rev, paths):
 
    url_l = [link_to(repo_name, url('files_home',
 
                                    repo_name=repo_name,
 
                                    revision=rev, f_path=''),
 
                     class_='ypjax-link')]
 
    paths_l = paths.split('/')
 
    for cnt, p in enumerate(paths_l):
 
        if p != '':
 
            url_l.append(link_to(p,
 
                                 url('files_home',
 
                                     repo_name=repo_name,
 
                                     revision=rev,
 
                                     f_path='/'.join(paths_l[:cnt + 1])
 
                                     ),
 
                                 class_='ypjax-link'
 
                                 )
 
                         )
 
    return literal('/'.join(url_l))
 

	
 

	
 
class CodeHtmlFormatter(HtmlFormatter):
 
    """
 
    My code Html Formatter for source codes
 
    """
 

	
 
    def wrap(self, source, outfile):
 
        return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
 

	
 
    def _wrap_code(self, source):
 
        for cnt, it in enumerate(source):
 
            i, t = it
 
            t = '<span id="L%s">%s</span>' % (cnt + 1, t)
 
            yield i, t
 

	
 
    def _wrap_tablelinenos(self, inner):
 
        inner_lines = []
 
        lncount = 0
 
        for t, line in inner:
 
            if t:
 
                lncount += 1
 
            inner_lines.append(line)
 

	
 
        fl = self.linenostart
 
        mw = len(str(lncount + fl - 1))
 
        sp = self.linenospecial
 
        st = self.linenostep
 
        la = self.lineanchors
 
        aln = self.anchorlinenos
 
        nocls = self.noclasses
 
        if sp:
 
            lines = []
 

	
 
            for i in range(fl, fl + lncount):
 
                if i % st == 0:
 
                    if i % sp == 0:
 
                        if aln:
 
                            lines.append('<a href="#%s%d" class="special">%*d</a>' %
 
                                         (la, i, mw, i))
 
                        else:
 
                            lines.append('<span class="special">%*d</span>' % (mw, i))
 
                    else:
 
                        if aln:
 
                            lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
 
                        else:
 
                            lines.append('%*d' % (mw, i))
 
                else:
 
                    lines.append('')
 
            ls = '\n'.join(lines)
 
        else:
 
            lines = []
 
            for i in range(fl, fl + lncount):
 
                if i % st == 0:
 
                    if aln:
 
                        lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
 
                    else:
 
                        lines.append('%*d' % (mw, i))
 
                else:
 
                    lines.append('')
 
            ls = '\n'.join(lines)
 

	
 
        # in case you wonder about the seemingly redundant <div> here: since the
 
        # content in the other cell also is wrapped in a div, some browsers in
 
        # some configurations seem to mess up the formatting...
 
        if nocls:
 
            yield 0, ('<table class="%stable">' % self.cssclass +
 
                      '<tr><td><div class="linenodiv">'
 
                      '<pre>' + ls + '</pre></div></td>'
 
                      '<td id="hlcode" class="code">')
 
        else:
 
            yield 0, ('<table class="%stable">' % self.cssclass +
 
                      '<tr><td class="linenos"><div class="linenodiv">'
 
                      '<pre>' + ls + '</pre></div></td>'
 
                      '<td id="hlcode" class="code">')
 
        yield 0, ''.join(inner_lines)
 
        yield 0, '</td></tr></table>'
 

	
 

	
 
_whitespace_re = re.compile(r'(\t)|( )(?=\n|</div>)')
 

	
 

	
 
def _markup_whitespace(m):
 
    groups = m.groups()
 
    if groups[0]:
 
        return '<u>\t</u>'
 
    if groups[1]:
 
        return ' <i></i>'
 

	
 

	
 
def markup_whitespace(s):
 
    return _whitespace_re.sub(_markup_whitespace, s)
 

	
 

	
 
def pygmentize(filenode, **kwargs):
 
    """
 
    pygmentize function using pygments
 

	
 
    :param filenode:
 
    """
 
    lexer = get_custom_lexer(filenode.extension) or filenode.lexer
 
    return literal(markup_whitespace(
 
        code_highlight(safe_str(filenode.content), lexer, CodeHtmlFormatter(**kwargs))))
 

	
 

	
 
def hsv_to_rgb(h, s, v):
 
    if s == 0.0:
 
        return v, v, v
 
    i = int(h * 6.0)  # XXX assume int() truncates!
 
    f = (h * 6.0) - i
 
    p = v * (1.0 - s)
 
    q = v * (1.0 - s * f)
 
    t = v * (1.0 - s * (1.0 - f))
 
    i = i % 6
 
    if i == 0:
 
        return v, t, p
 
    if i == 1:
 
        return q, v, p
 
    if i == 2:
 
        return p, v, t
 
    if i == 3:
 
        return p, q, v
 
    if i == 4:
 
        return t, p, v
 
    if i == 5:
 
        return v, p, q
 

	
 

	
 
def gen_color(n=10000):
 
    """generator for getting n of evenly distributed colors using
 
    hsv color and golden ratio. It always return same order of colors
 

	
 
    :returns: RGB tuple
 
    """
 

	
 
    golden_ratio = 0.618033988749895
 
    h = 0.22717784590367374
 

	
 
    for _unused in range(n):
 
        h += golden_ratio
 
        h %= 1
 
        HSV_tuple = [h, 0.95, 0.95]
 
        RGB_tuple = hsv_to_rgb(*HSV_tuple)
 
        yield [str(int(x * 256)) for x in RGB_tuple]
 

	
 

	
 
def pygmentize_annotation(repo_name, filenode, **kwargs):
 
    """
 
    pygmentize function for annotation
 

	
 
    :param filenode:
 
    """
 
    cgenerator = gen_color()
 
    color_dict = {}
 

	
 
    def get_color_string(cs):
 
        if cs in color_dict:
 
            col = color_dict[cs]
 
        else:
 
            col = color_dict[cs] = next(cgenerator)
 
        return "color: rgb(%s)! important;" % (', '.join(col))
 

	
 
    def url_func(changeset):
 
        author = escape(changeset.author)
 
        date = changeset.date
 
        message = escape(changeset.message)
 
        tooltip_html = ("<b>Author:</b> %s<br/>"
 
                        "<b>Date:</b> %s</b><br/>"
 
                        "<b>Message:</b> %s") % (author, date, message)
 

	
 
        lnk_format = show_id(changeset)
 
        uri = link_to(
 
                lnk_format,
 
                url('changeset_home', repo_name=repo_name,
 
                    revision=changeset.raw_id),
 
                style=get_color_string(changeset.raw_id),
 
                **{'data-toggle': 'popover',
 
                   'data-content': tooltip_html}
 
              )
 

	
 
        uri += '\n'
 
        return uri
 

	
 
    return literal(markup_whitespace(annotate_highlight(filenode, url_func, **kwargs)))
 

	
 

	
 
def capitalize(x):
 
    return x.capitalize()
 

	
 
def short_id(x):
 
    return x[:12]
 

	
 
def hide_credentials(x):
 
    return ''.join(credentials_filter(x))
 

	
 

	
 
def show_id(cs):
 
    """
 
    Configurable function that shows ID
 
    by default it's r123:fffeeefffeee
 

	
 
    :param cs: changeset instance
 
    """
 
    def_len = safe_int(kallithea.CONFIG.get('show_sha_length', 12))
 
    show_rev = asbool(kallithea.CONFIG.get('show_revision_number', False))
 

	
 
    raw_id = cs.raw_id[:def_len]
 
    if show_rev:
 
        return 'r%s:%s' % (cs.revision, raw_id)
 
    else:
 
        return raw_id
 

	
 

	
 
def is_git(repository):
 
    if hasattr(repository, 'alias'):
 
        _type = repository.alias
 
    elif hasattr(repository, 'repo_type'):
 
        _type = repository.repo_type
 
    else:
 
        _type = repository
 
    return _type == 'git'
 

	
 

	
 
def is_hg(repository):
 
    if hasattr(repository, 'alias'):
 
        _type = repository.alias
 
    elif hasattr(repository, 'repo_type'):
 
        _type = repository.repo_type
 
    else:
 
        _type = repository
 
    return _type == 'hg'
 

	
 

	
 
@cache_region('long_term', 'user_attr_or_none')
 
def user_attr_or_none(author, show_attr):
 
    """Try to match email part of VCS committer string with a local user and return show_attr
 
    - or return None if user not found"""
 
    email = author_email(author)
 
    if email:
 
        user = db.User.get_by_email(email)
 
        if user is not None:
 
            return getattr(user, show_attr)
 
    return None
 

	
 

	
 
def email_or_none(author):
 
    """Try to match email part of VCS committer string with a local user.
 
    Return primary email of user, email part of the specified author name, or None."""
 
    if not author:
 
        return None
 
    email = user_attr_or_none(author, 'email')
 
    if email is not None:
 
        return email # always use user's main email address - not necessarily the one used to find user
 

	
 
    # extract email from the commit string
 
    email = author_email(author)
 
    if email:
 
        return email
 

	
 
    # No valid email, not a valid user in the system, none!
 
    return None
 

	
 

	
 
def person(author, show_attr="username"):
 
    """Find the user identified by 'author', return one of the users attributes,
 
    default to the username attribute, None if there is no user"""
 
    # if author is already an instance use it for extraction
 
    if isinstance(author, db.User):
 
        return getattr(author, show_attr)
 

	
 
    value = user_attr_or_none(author, show_attr)
 
    if value is not None:
 
        return value
 

	
 
    # Still nothing?  Just pass back the author name if any, else the email
 
    return author_name(author) or author_email(author)
 

	
 

	
 
def person_by_id(id_, show_attr="username"):
 
    # maybe it's an ID ?
 
    if str(id_).isdigit() or isinstance(id_, int):
 
        id_ = int(id_)
 
        user = db.User.get(id_)
 
        if user is not None:
 
            return getattr(user, show_attr)
 
    return id_
 

	
 

	
 
def boolicon(value):
 
    """Returns boolean value of a value, represented as small html image of true/false
 
    icons
 

	
 
    :param value: value
 
    """
 

	
 
    if value:
 
        return HTML.tag('i', class_="icon-ok")
 
    else:
 
        return HTML.tag('i', class_="icon-minus-circled")
 

	
 

	
 
def action_parser(user_log, feed=False, parse_cs=False):
 
    """
 
    This helper will action_map the specified string action into translated
 
    fancy names with icons and links
 

	
 
    :param user_log: user log instance
 
    :param feed: use output for feeds (no html and fancy icons)
 
    :param parse_cs: parse Changesets into VCS instances
 
    """
 

	
 
    action = user_log.action
 
    action_params = ' '
 

	
 
    x = action.split(':')
 

	
 
    if len(x) > 1:
 
        action, action_params = x
 

	
 
    def get_cs_links():
 
        revs_limit = 3  # display this amount always
 
        revs_top_limit = 50  # show upto this amount of changesets hidden
 
        revs_ids = action_params.split(',')
 
        deleted = user_log.repository is None
 
        if deleted:
 
            return ','.join(revs_ids)
 

	
 
        repo_name = user_log.repository.repo_name
 

	
 
        def lnk(rev, repo_name):
 
            lazy_cs = False
 
            title_ = None
 
            url_ = '#'
 
            if isinstance(rev, BaseChangeset) or isinstance(rev, AttributeDict):
 
                if rev.op and rev.ref_name:
 
                    if rev.op == 'delete_branch':
 
                        lbl = _('Deleted branch: %s') % rev.ref_name
 
                    elif rev.op == 'tag':
 
                        lbl = _('Created tag: %s') % rev.ref_name
 
                    else:
 
                        lbl = 'Unknown operation %s' % rev.op
 
                else:
 
                    lazy_cs = True
 
                    lbl = rev.short_id[:8]
 
                    url_ = url('changeset_home', repo_name=repo_name,
 
                               revision=rev.raw_id)
 
            else:
 
                # changeset cannot be found - it might have been stripped or removed
 
                lbl = rev[:12]
 
                title_ = _('Changeset %s not found') % lbl
 
            if parse_cs:
 
                return link_to(lbl, url_, title=title_, **{'data-toggle': 'tooltip'})
 
            return link_to(lbl, url_, class_='lazy-cs' if lazy_cs else '',
 
                           **{'data-raw_id': rev.raw_id, 'data-repo_name': repo_name})
 

	
 
        def _get_op(rev_txt):
 
            _op = None
 
            _name = rev_txt
 
            if len(rev_txt.split('=>')) == 2:
 
                _op, _name = rev_txt.split('=>')
 
            return _op, _name
 

	
 
        revs = []
 
        if len([v for v in revs_ids if v != '']) > 0:
 
            repo = None
 
            for rev in revs_ids[:revs_top_limit]:
 
                _op, _name = _get_op(rev)
 

	
 
                # we want parsed changesets, or new log store format is bad
 
                if parse_cs:
 
                    try:
 
                        if repo is None:
 
                            repo = user_log.repository.scm_instance
 
                        _rev = repo.get_changeset(rev)
 
                        revs.append(_rev)
 
                    except ChangesetDoesNotExistError:
 
                        log.error('cannot find revision %s in this repo', rev)
 
                        revs.append(rev)
 
                else:
 
                    _rev = AttributeDict({
 
                        'short_id': rev[:12],
 
                        'raw_id': rev,
 
                        'message': '',
 
                        'op': _op,
 
                        'ref_name': _name
 
                    })
 
                    revs.append(_rev)
 
        cs_links = [" " + ', '.join(
 
            [lnk(rev, repo_name) for rev in revs[:revs_limit]]
 
        )]
 
        _op1, _name1 = _get_op(revs_ids[0])
 
        _op2, _name2 = _get_op(revs_ids[-1])
 

	
 
        _rev = '%s...%s' % (_name1, _name2)
 

	
 
        compare_view = (
 
            ' <div class="compare_view" data-toggle="tooltip" title="%s">'
 
            '<a href="%s">%s</a> </div>' % (
 
                _('Show all combined changesets %s->%s') % (
 
                    revs_ids[0][:12], revs_ids[-1][:12]
 
                ),
 
                url('changeset_home', repo_name=repo_name,
 
                    revision=_rev
 
                ),
 
                _('Compare view')
 
            )
 
        )
 

	
 
        # if we have exactly one more than normally displayed
 
        # just display it, takes less space than displaying
 
        # "and 1 more revisions"
 
        if len(revs_ids) == revs_limit + 1:
 
            cs_links.append(", " + lnk(revs[revs_limit], repo_name))
 

	
 
        # hidden-by-default ones
 
        if len(revs_ids) > revs_limit + 1:
 
            uniq_id = revs_ids[0]
 
            html_tmpl = (
 
                '<span> %s <a class="show_more" id="_%s" '
 
                'href="#more">%s</a> %s</span>'
 
            )
 
            if not feed:
 
                cs_links.append(html_tmpl % (
 
                      _('and'),
 
                      uniq_id, _('%s more') % (len(revs_ids) - revs_limit),
 
                      _('revisions')
 
                    )
 
                )
 

	
 
            if not feed:
 
                html_tmpl = '<span id="%s" style="display:none">, %s </span>'
 
            else:
 
                html_tmpl = '<span id="%s"> %s </span>'
 

	
 
            morelinks = ', '.join(
 
              [lnk(rev, repo_name) for rev in revs[revs_limit:]]
 
            )
 

	
 
            if len(revs_ids) > revs_top_limit:
 
                morelinks += ', ...'
 

	
 
            cs_links.append(html_tmpl % (uniq_id, morelinks))
 
        if len(revs) > 1:
 
            cs_links.append(compare_view)
 
        return ''.join(cs_links)
 

	
 
    def get_fork_name():
 
        repo_name = action_params
 
        url_ = url('summary_home', repo_name=repo_name)
 
        return _('Fork name %s') % link_to(action_params, url_)
 

	
 
    def get_user_name():
 
        user_name = action_params
 
        return user_name
 

	
 
    def get_users_group():
 
        group_name = action_params
 
        return group_name
 

	
 
    def get_pull_request():
 
        pull_request_id = action_params
 
        nice_id = db.PullRequest.make_nice_id(pull_request_id)
 

	
 
        deleted = user_log.repository is None
 
        if deleted:
 
            repo_name = user_log.repository_name
 
        else:
 
            repo_name = user_log.repository.repo_name
 

	
 
        return link_to(_('Pull request %s') % nice_id,
 
                    url('pullrequest_show', repo_name=repo_name,
 
                    pull_request_id=pull_request_id))
 

	
 
    def get_archive_name():
 
        archive_name = action_params
 
        return archive_name
 

	
 
    # action : translated str, callback(extractor), icon
 
    action_map = {
 
        'user_deleted_repo':           (_('[deleted] repository'),
 
                                        None, 'icon-trashcan'),
 
        'user_created_repo':           (_('[created] repository'),
 
                                        None, 'icon-plus'),
 
        'user_created_fork':           (_('[created] repository as fork'),
 
                                        None, 'icon-fork'),
 
        'user_forked_repo':            (_('[forked] repository'),
 
                                        get_fork_name, 'icon-fork'),
 
        'user_updated_repo':           (_('[updated] repository'),
 
                                        None, 'icon-pencil'),
 
        'user_downloaded_archive':      (_('[downloaded] archive from repository'),
 
                                        get_archive_name, 'icon-download-cloud'),
 
        'admin_deleted_repo':          (_('[delete] repository'),
 
                                        None, 'icon-trashcan'),
 
        'admin_created_repo':          (_('[created] repository'),
 
                                        None, 'icon-plus'),
 
        'admin_forked_repo':           (_('[forked] repository'),
 
                                        None, 'icon-fork'),
 
        'admin_updated_repo':          (_('[updated] repository'),
 
                                        None, 'icon-pencil'),
 
        'admin_created_user':          (_('[created] user'),
 
                                        get_user_name, 'icon-user'),
 
        'admin_updated_user':          (_('[updated] user'),
 
                                        get_user_name, 'icon-user'),
 
        'admin_created_users_group':   (_('[created] user group'),
 
                                        get_users_group, 'icon-pencil'),
 
        'admin_updated_users_group':   (_('[updated] user group'),
 
                                        get_users_group, 'icon-pencil'),
 
        'user_commented_revision':     (_('[commented] on revision in repository'),
 
                                        get_cs_links, 'icon-comment'),
 
        'user_commented_pull_request': (_('[commented] on pull request for'),
 
                                        get_pull_request, 'icon-comment'),
 
        'user_closed_pull_request':    (_('[closed] pull request for'),
 
                                        get_pull_request, 'icon-ok'),
 
        'push':                        (_('[pushed] into'),
 
                                        get_cs_links, 'icon-move-up'),
 
        'push_local':                  (_('[committed via Kallithea] into repository'),
 
                                        get_cs_links, 'icon-pencil'),
 
        'push_remote':                 (_('[pulled from remote] into repository'),
 
                                        get_cs_links, 'icon-move-up'),
 
        'pull':                        (_('[pulled] from'),
 
                                        None, 'icon-move-down'),
 
        'started_following_repo':      (_('[started following] repository'),
 
                                        None, 'icon-heart'),
 
        'stopped_following_repo':      (_('[stopped following] repository'),
 
                                        None, 'icon-heart-empty'),
 
    }
 

	
 
    action_str = action_map.get(action, action)
 
    if feed:
 
        action = action_str[0].replace('[', '').replace(']', '')
 
    else:
 
        action = action_str[0] \
 
            .replace('[', '<b>') \
 
            .replace(']', '</b>')
 

	
 
    action_params_func = action_str[1] if callable(action_str[1]) else (lambda: "")
 

	
 
    def action_parser_icon():
 
        action = user_log.action
 
        action_params = None
 
        x = action.split(':')
 

	
 
        if len(x) > 1:
 
            action, action_params = x
 

	
 
        ico = action_map.get(action, ['', '', ''])[2]
 
        html = """<i class="%s"></i>""" % ico
 
        return literal(html)
 

	
 
    # returned callbacks we need to call to get
 
    return [lambda: literal(action), action_params_func, action_parser_icon]
 

	
 

	
 
#==============================================================================
 
# GRAVATAR URL
 
#==============================================================================
 
def gravatar_div(email_address, cls='', size=30, **div_attributes):
 
    """Return an html literal with a span around a gravatar if they are enabled.
 
    Extra keyword parameters starting with 'div_' will get the prefix removed
 
    and '_' changed to '-' and be used as attributes on the div. The default
 
    class is 'gravatar'.
 
    """
 
    if not c.visual.use_gravatar:
 
        return ''
 
    if 'div_class' not in div_attributes:
 
        div_attributes['div_class'] = "gravatar"
 
    attributes = []
 
    for k, v in sorted(div_attributes.items()):
 
        assert k.startswith('div_'), k
 
        attributes.append(' %s="%s"' % (k[4:].replace('_', '-'), escape(v)))
 
    return literal("""<span%s>%s</span>""" %
 
                   (''.join(attributes),
 
                    gravatar(email_address, cls=cls, size=size)))
 

	
 

	
 
def gravatar(email_address, cls='', size=30):
 
    """return html element of the gravatar
 

	
 
    This method will return an <img> with the resolution double the size (for
 
    retina screens) of the image. If the url returned from gravatar_url is
 
    empty then we fallback to using an icon.
 

	
 
    """
 
    if not c.visual.use_gravatar:
 
        return ''
 

	
 
    src = gravatar_url(email_address, size * 2)
 

	
 
    if src:
 
        # here it makes sense to use style="width: ..." (instead of, say, a
 
        # stylesheet) because we using this to generate a high-res (retina) size
 
        html = ('<i class="icon-gravatar {cls}"'
 
                ' style="font-size: {size}px;background-size: {size}px;background-image: url(\'{src}\')"'
 
                '></i>').format(cls=cls, size=size, src=src)
 

	
 
    else:
 
        # if src is empty then there was no gravatar, so we use a font icon
 
        html = ("""<i class="icon-user {cls}" style="font-size: {size}px;"></i>"""
 
            .format(cls=cls, size=size))
 

	
 
    return literal(html)
 

	
 

	
 
def gravatar_url(email_address, size=30, default=''):
 
    if not c.visual.use_gravatar:
 
        return ""
 

	
 
    _def = 'anonymous@kallithea-scm.org'  # default gravatar
 
    email_address = email_address or _def
 

	
 
    if email_address == _def:
 
        return default
 

	
 
    parsed_url = urllib.parse.urlparse(url.current(qualified=True))
 
    return (c.visual.gravatar_url or db.User.DEFAULT_GRAVATAR_URL) \
 
               .replace('{email}', email_address) \
 
               .replace('{md5email}', hashlib.md5(safe_bytes(email_address).lower()).hexdigest()) \
kallithea/templates/changeset/changeset_range.html
Show inline comments
 
## -*- coding: utf-8 -*-
 
<%inherit file="/base/base.html"/>
 

	
 
<%block name="title">
 
    ${_('%s Changesets') % c.repo_name} - ${h.show_id(c.cs_ranges[0])} &gt; ${h.show_id(c.cs_ranges[-1])}
 
</%block>
 

	
 
<%def name="breadcrumbs_links()">
 
    ${_('Changesets')} -
 
    ${h.link_to(h.show_id(c.cs_ranges[0]),h.url('changeset_home',repo_name=c.repo_name,revision=c.cs_ranges[0].raw_id))}
 
    <i class="icon-right"></i>
 
    ${h.link_to(h.show_id(c.cs_ranges[-1]),h.url('changeset_home',repo_name=c.repo_name,revision=c.cs_ranges[-1].raw_id))}
 
</%def>
 

	
 
<%block name="header_menu">
 
    ${self.menu('repositories')}
 
</%block>
 

	
 
<%def name="main()">
 
${self.repo_context_bar('changelog')}
 
<div class="panel panel-primary">
 
    <div class="panel-heading clearfix">
 
        <div class="pull-left">
 
            ${self.breadcrumbs()}
 
        </div>
 
        <div class="pull-right">
 
            <a href="${h.url('compare_url',repo_name=c.repo_name,org_ref_type='rev',org_ref_name=getattr(c.cs_ranges[0].parents[0] if c.cs_ranges[0].parents else h.EmptyChangeset(),'raw_id'),other_ref_type='rev',other_ref_name=c.cs_ranges[-1].raw_id)}" class="btn btn-default btn-sm"><i class="icon-git-compare"></i>Compare Revisions</a>
 
        </div>
 
    </div>
 
    <div class="panel-body">
 
        <div>
 
                <table class="table compare_view_commits">
 
                %for cnt,cs in enumerate(c.cs_ranges):
 
                  <tr>
 
                    %if c.visual.use_gravatar:
 
                    <td>${h.gravatar_div(h.email_or_none(cs.author), size=14)}</td>
 
                    %endif
 
                    <td>${h.link_to(cs.short_id,h.url('changeset_home',repo_name=c.cs_repo.repo_name,revision=cs.raw_id))}</td>
 
                    <td class="author">${h.person(cs.author)}</td>
 
                    <td><span data-toggle="tooltip" title="${h.age(cs.date)}">${cs.date}</span></td>
 
                    <td>
 
                      %if c.statuses:
 
                        <i class="icon-circle changeset-status-${c.statuses[cnt]}" title="${_('Changeset status: %s') % h.changeset_status_lbl(c.statuses[cnt])}"></i>
 
                      %endif
 
                    </td>
 
                    <td><div class="message">${h.urlify_text(h.wrap_paragraphs(cs.message),c.repo_name)}</div></td>
 
                  </tr>
 
                %endfor
 
                </table>
 
                <h4>${_('Files affected')}</h4>
 
                <div class="cs_files">
 
                    %for cs in c.cs_ranges:
 
                        <h6>${h.link_to(h.show_id(cs),h.url('changeset_home',repo_name=c.cs_repo.repo_name,revision=cs.raw_id))}</h6>
 
                        <% a_rev, cs_rev, file_diff_data = c.changes[cs.raw_id] %>
 
                        %for fid, url_fid, op, a_path, path, diff, stats in file_diff_data:
 
                            <div class="cs_${op} clearfix">
 
                                <span class="node">
 
                                    <i class="icon-diff-${op}"></i>
 
                                    ${h.link_to(path, '#%s' % fid)}
 
                                </span>
 
                                <div class="changes">${h.fancy_file_stats(stats)}</div>
 
                            </div>
 
                        %endfor
 
                    %endfor
 
                </div>
 
        </div>
 
    </div>
 
    <%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
 
    <%namespace name="diff_block" file="/changeset/diff_block.html"/>
 
    ${diff_block.diff_block_js()}
 
    %for cs in c.cs_ranges:
 
        <div class="panel-body">
 
          ## diff block
 
          <div class="h3">
 
          ${h.gravatar_div(h.email_or_none(cs.author), size=20)}
 
          <a data-toggle="tooltip" title="${cs.message}" href="${h.url('changeset_home',repo_name=c.cs_repo.repo_name,revision=cs.raw_id)}">${h.show_id(cs)}</a>
 
            <div class="right">
 
              <span>
 
                %if len(cs.parents)>1:
 
                <span class="label label-merge">${_('Merge')}</span>
 
                %endif
 
                %if h.is_hg(c.db_repo_scm_instance):
 
                %if c.db_repo_scm_instance.alias == 'hg':
 
                  %for book in cs.bookmarks:
 
                  <span class="label label-bookmark" title="${_('Bookmark %s') % book}">
 
                     ${h.link_to(book,h.url('changeset_home',repo_name=c.cs_repo.repo_name,revision=cs.raw_id))}
 
                  </span>
 
                  %endfor
 
                %endif
 
                %for tag in cs.tags:
 
                    <span class="label label-tag" title="${_('Tag %s') % tag}">
 
                    ${h.link_to(tag,h.url('changeset_home',repo_name=c.cs_repo.repo_name,revision=cs.raw_id))}</span>
 
                %endfor
 
                %for branch in cs.branches:
 
                  <span class="label label-branch" title="${_('Branch %s') % branch}">
 
                    ${h.link_to(branch,h.url('changeset_home',repo_name=c.cs_repo.repo_name,revision=cs.raw_id))}
 
                  </span>
 
                %endfor
 
              </span>
 
            </div>
 
          </div>
 
          <% a_rev, cs_rev, file_diff_data = c.changes[cs.raw_id] %>
 
          ${diff_block.diff_block(c.repo_name, 'rev', a_rev, a_rev,
 
                                  c.repo_name, 'rev', cs_rev, cs_rev, file_diff_data)}
 
        </div>
 
    %endfor
 
</div>
 
</%def>
kallithea/templates/changeset/patch_changeset.html
Show inline comments
 
%if h.is_hg(c.db_repo_scm_instance):
 
%if c.db_repo_scm_instance.alias == 'hg':
 
# ${c.db_repo_scm_instance.alias.upper()} changeset patch
 
# User ${c.changeset.author |n}
 
# Date ${c.changeset.date}
 
# Node ID ${c.changeset.raw_id}
 
${c.parent_tmpl}
 
${c.changeset.message |n}
 

	
 
%elif h.is_git(c.db_repo_scm_instance):
 
%elif c.db_repo_scm_instance.alias == 'git':
 
From ${c.changeset.raw_id} ${c.changeset.date}
 
From: ${c.changeset.author |n}
 
Date: ${c.changeset.date}
 
Subject: [PATCH] ${c.changeset.message |n}
 
---
 

	
 
%endif
 
${c.diff|n}
kallithea/templates/pullrequests/pullrequest_show.html
Show inline comments
 
<%inherit file="/base/base.html"/>
 

	
 
<%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
 

	
 
<%block name="title">
 
    ${_('%s Pull Request %s') % (c.repo_name, c.pull_request.nice_id())}
 
</%block>
 

	
 
<%def name="breadcrumbs_links()">
 
    ${_('Pull request %s from %s#%s') % (c.pull_request.nice_id(), c.pull_request.org_repo.repo_name, c.cs_branch_name)}
 
</%def>
 

	
 
<%block name="header_menu">
 
    ${self.menu('repositories')}
 
</%block>
 

	
 
<%def name="main()">
 
<% editable = not c.pull_request.is_closed() and (h.HasPermissionAny('hg.admin')() or h.HasRepoPermissionLevel('admin')(c.repo_name) or c.pull_request.owner_id == request.authuser.user_id) %>
 
${self.repo_context_bar('showpullrequest')}
 
<div class="panel panel-primary">
 
  <div class="panel-heading clearfix">
 
    ${self.breadcrumbs()}
 
  </div>
 

	
 
  ${h.form(url('pullrequest_post', repo_name=c.repo_name, pull_request_id=c.pull_request.pull_request_id), method='post', id='pull_request_form',class_='panel-body')}
 
    <div class="form pr-box pull-left">
 
      <div class="pr-details-title ${'closed' if c.pull_request.is_closed() else ''}">
 
        <h3>
 
          ${_('Title')}: ${h.urlify_text(c.pull_request.title, c.pull_request.org_repo.repo_name)}
 
          %if c.pull_request.is_closed():
 
              (${_('Closed')})
 
          %endif
 
        </h3>
 
      </div>
 
      <div id="pr-summary">
 

	
 
        <div class="pr-not-edit form-group">
 
            <label>${_('Description')}:
 
            %if editable:
 
            <div id="pr-edit-btn">
 
              <a class="btn btn-default btn-xs" onclick="$('.pr-do-edit').show();$('.pr-not-edit').hide()">${_("Edit")}</a>
 
            </div>
 
            %endif
 
            </label>
 
            <div>
 
              <div class="formatted-fixed">${h.urlify_text(c.pull_request.description, c.pull_request.org_repo.repo_name)}</div>
 
            </div>
 
        </div>
 

	
 
        %if editable:
 
        <div class="pr-do-edit form-group" style="display:none">
 
              <label for="pullrequest_title">${_('Title')}:</label>
 
              <div>
 
                  ${h.text('pullrequest_title',class_='form-control',value=c.pull_request.title,placeholder=_('Summarize the changes'))}
 
              </div>
 
        </div>
 

	
 
        <div class="pr-do-edit form-group" style="display:none">
 
              <label for="pullrequest_desc">${_('Description')}:</label>
 
              <div>
 
                  ${h.textarea('pullrequest_desc',content=c.pull_request.description,placeholder=_('Write a short description on this pull request'),class_='form-control')}
 
              </div>
 
        </div>
 
        %endif
 

	
 
        <div class="form-group">
 
          <label>${_('Voting Result')}:</label>
 
          <div>
 
            %if c.current_voting_result:
 
              <i class="icon-circle changeset-status-${c.current_voting_result}" title="${_('Pull request status calculated from votes')}"></i>
 
              <span class="changeset-status-lbl" data-toggle="tooltip" title="${_('Pull request status calculated from votes')}">
 
                %if c.pull_request.is_closed():
 
                    ${_('Closed')},
 
                %endif
 
                ${h.changeset_status_lbl(c.current_voting_result)}
 
              </span>
 
            %endif
 
          </div>
 
        </div>
 
        <div class="form-group">
 
          <label>${_('Origin')}:</label>
 
          <div>
 
            <div>
 
              ${h.link_to_ref(c.pull_request.org_repo.repo_name, c.cs_ref_type, c.cs_ref_name, c.cs_rev)}
 
              %if c.cs_ref_type != 'branch':
 
                ${_('on')} ${h.link_to_ref(c.pull_request.org_repo.repo_name, 'branch', c.cs_branch_name)}
 
              %endif
 
            </div>
 
          </div>
 
        </div>
 
        <div class="form-group">
 
          <label>${_('Target')}:</label>
 
          <div>
 
            %if c.is_range:
 
              ${_("This is just a range of changesets and doesn't have a target or a real merge ancestor.")}
 
            %else:
 
              ${h.link_to_ref(c.pull_request.other_repo.repo_name, c.a_ref_type, c.a_ref_name)}
 
              ## we don't know other rev - c.a_rev is ancestor and not necessarily on other_name_branch branch
 
            %endif
 
          </div>
 
        </div>
 
        <div class="form-group">
 
          <label>${_('Pull changes')}:</label>
 
          <div>
 
            %if c.cs_ranges:
 
              <div>
 
               ## TODO: use cs_ranges[-1] or org_ref_parts[1] in both cases?
 
               %if h.is_hg(c.pull_request.org_repo):
 
               %if c.pull_request.org_repo.repo_type == 'hg':
 
                 <span>hg pull ${c.pull_request.org_repo.clone_url(clone_uri_tmpl=c.clone_uri_tmpl)} -r ${c.cs_ranges[-1].short_id}</span>
 
               %elif h.is_git(c.pull_request.org_repo):
 
               %elif c.pull_request.org_repo.repo_type == 'git':
 
                 <span>git pull ${c.pull_request.org_repo.clone_url(clone_uri_tmpl=c.clone_uri_tmpl)} ${c.pull_request.org_ref_parts[1]}</span>
 
               %endif
 
              </div>
 
            %endif
 
          </div>
 
        </div>
 
        <div class="form-group">
 
          <label>${_('Created on')}:</label>
 
          <div>
 
              <div>${h.fmt_date(c.pull_request.created_on)}</div>
 
          </div>
 
        </div>
 
        <div class="form-group">
 
          <label>${_('Owner')}:</label>
 
          <div class="pr-not-edit">
 
                  ${h.gravatar_div(c.pull_request.owner.email, size=20)}
 
                  <span>${c.pull_request.owner.full_name_and_username}</span><br/>
 
                  <span><a href="mailto:${c.pull_request.owner.email}">${c.pull_request.owner.email}</a></span><br/>
 
          </div>
 
          <div class="pr-do-edit" style="display:none">
 
               ${h.text('owner', class_='form-control', value=c.pull_request.owner.username, placeholder=_('Type name of user'))}
 
          </div>
 
        </div>
 

	
 
        <div class="form-group">
 
          <label>${_('Next iteration')}:</label>
 
            <div>
 
              <p>${c.update_msg}</p>
 
              %if c.avail_cs:
 
              <div id="updaterevs" class="clearfix">
 
                <div id="updaterevs-graph">
 
                  <canvas id="avail_graph_canvas"></canvas>
 
                </div>
 
                <table class="table" id="updaterevs-table">
 
                  %for cnt, cs in enumerate(c.avail_cs):
 
                    <tr id="chg_available_${cnt+1}" class="${'mergerow' if len(cs.parents) > 1 and not (editable and cs.revision in c.avail_revs) else ''}">
 
                      %if c.cs_ranges and cs.revision == c.cs_ranges[-1].revision:
 
                        %if editable:
 
                        <td>
 
                            ${h.radio(name='updaterev', value='', checked=True)}
 
                        </td>
 
                        %endif
 
                        <td colspan="4"><span>${_("Current revision - no change")}</span></td>
 
                      %else:
 
                        %if editable:
 
                        <td>
 
                          ${h.radio(name='updaterev', value=cs.raw_id, style=None if cs.revision in c.avail_revs else 'visibility: hidden')}
 
                        </td>
 
                        %endif
 
                        <td><span data-toggle="tooltip" title="${h.age(cs.date)}">${cs.date}</span></td>
 
                        <td>${h.link_to(h.show_id(cs),h.url('changeset_home',repo_name=c.cs_repo.repo_name,revision=cs.raw_id), class_='changeset_hash')}</td>
 
                        <td>
 
                          <div class="pull-right">
 
                            %for tag in cs.tags:
 
                              <span class="label label-tag" title="${_('Tag %s') % tag}">
 
                                ${h.link_to(tag,h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id))}
 
                              </span>
 
                            %endfor
 
                          </div>
 
                          <div class="message">${h.urlify_text(cs.message.splitlines()[0], c.repo_name)}</div>
 
                        </td>
 
                      %endif
 
                    </tr>
 
                  %endfor
 
                </table>
 
              </div>
 
              <div class="alert alert-info">${_("Pull request iterations do not change content once created. Select a revision to create a new iteration.")}</div>
 
              %endif
 
              %if c.update_msg_other:
 
                <div class="alert alert-info">${c.update_msg_other}</div>
 
              %endif
 
            </div>
 
        </div>
 
        %if editable:
 
        <div class="form-group">
 
          <div class="buttons">
 
            ${h.submit('pr-form-save',_('Save Changes'),class_="btn btn-default btn-sm")}
 
            ${h.submit('pr-form-clone',_('Create New Iteration with Changes'),class_="btn btn-default btn-sm",disabled='disabled')}
 
            ${h.reset('pr-form-reset',_('Cancel Changes'),class_="btn btn-default btn-sm")}
 
          </div>
 
        </div>
 
        %endif
 
      </div>
 
    </div>
 
    ## REVIEWERS
 
    <div class="pr-reviewers-box pull-left">
 
        <h4 class="pr-details-title">${_('Reviewers')}</h4>
 
        <div id="reviewers">
 
          ## members goes here !
 
          <div>
 
            %for member,status in c.pull_request_reviewers:
 
              <input type="hidden" value="${member.user_id}" name="org_review_members" />
 
            %endfor
 
            <ul id="review_members" class="list-unstyled">
 
            %for member,status in c.pull_request_reviewers:
 
              ## WARNING: the HTML below is duplicate with
 
              ## kallithea/public/js/base.js
 
              ## If you change something here it should be reflected in the template too.
 
              <li id="reviewer_${member.user_id}">
 
                <span class="reviewers_member">
 
                  <input type="hidden" value="${member.user_id}" name="review_members" />
 
                  <span class="reviewer_status" data-toggle="tooltip" title="${h.changeset_status_lbl(status)}">
 
                      <i class="icon-circle changeset-status-${status}"></i>
 
                  </span>
 
                  ${h.gravatar(member.email, size=14)}
 
                  <span>
 
                    ${member.full_name_and_username}
 
                    %if c.pull_request.owner_id == member.user_id:
 
                      (${_('Owner')})
 
                    %endif
 
                  </span>
 
                  %if editable:
 
                  <a href="#" class="reviewer_member_remove" onclick="removeReviewMember(${member.user_id})" title="${_('Remove reviewer')}">
 
                      <i class="icon-minus-circled"></i>
 
                  </a>
 
                  %endif
 
                </span>
 
              </li>
 
            %endfor
 
            </ul>
 
          </div>
 
          %if editable:
 
          <div>
 
             ${h.text('user', class_='form-control',placeholder=_('Type name of reviewer to add'))}
 
          </div>
 
          %endif
 
        </div>
 

	
 
        %if not c.pull_request_reviewers:
 
        <h4>${_('Potential Reviewers')}</h4>
 
        <div>
 
          <div>
 
            ${_('Click to add the repository owner as reviewer:')}
 
          </div>
 
          <ul class="list-unstyled">
 
            %for u in [c.pull_request.other_repo.owner]:
 
              <li>
 
                <a class="btn btn-default btn-xs missing_reviewer missing_reviewer_${u.user_id}"
 
                  href="#"
 
                  data-user_id="${u.user_id}"
 
                  data-fname="${u.name}"
 
                  data-lname="${u.lastname}"
 
                  data-nname="${u.username}"
 
                  data-gravatar_lnk="${h.gravatar_url(u.email, size=28, default='default')}"
 
                  data-gravatar_size="14"
 
                  title="Click to add reviewer to the list, then Save Changes."><i class="icon-plus"></i>${u.full_name}</a>
 
              </li>
 
            %endfor
 
          </ul>
 
        </div>
 
        %endif
 
    </div>
 
  ${h.end_form()}
 
</div>
 

	
 
<div class="panel panel-primary">
 
    <div class="panel-heading clearfix">
 
      <div class="panel-title">${_('Pull Request Content')}</div>
 
    </div>
 
    <div class="panel-body">
 
        <div>
 
            <div id="changeset_compare_view_content">
 
              <h5>
 
                  ${comment.comment_count(c.inline_cnt, len(c.comments))}
 
              </h5>
 
              ##CS
 
              <h5>
 
                ${ungettext('Showing %s commit','Showing %s commits', len(c.cs_ranges)) % len(c.cs_ranges)}
 
              </h5>
 
              <%include file="/compare/compare_cs.html" />
 

	
 
              <h5>
 
              ${_('Common ancestor')}:
 
              ${h.link_to(h.short_id(c.a_rev),h.url('changeset_home',repo_name=c.a_repo.repo_name,revision=c.a_rev), class_="changeset_hash")}
 
              </h5>
 

	
 
              ## FILES
 
              <h5>
 
              % if c.limited_diff:
 
                  ${ungettext('%s file changed', '%s files changed', len(c.file_diff_data)) % len(c.file_diff_data)}:
 
              % else:
 
                  ${ungettext('%s file changed with %s insertions and %s deletions','%s files changed with %s insertions and %s deletions', len(c.file_diff_data)) % (len(c.file_diff_data),c.lines_added,c.lines_deleted)}:
 
              %endif
 
              </h5>
 
              <div class="cs_files">
 
                %if not c.file_diff_data:
 
                   <span class="text-muted">${_('No files')}</span>
 
                %endif
 
                %for fid, url_fid, op, a_path, path, diff, stats in c.file_diff_data:
 
                    <div class="cs_${op} clearfix">
 
                      <span class="node">
 
                          <i class="icon-diff-${op}"></i>
 
                          ${h.link_to(path, '#%s' % fid)}
 
                      </span>
 
                      <div class="changes">${h.fancy_file_stats(stats)}</div>
 
                    </div>
 
                %endfor
 
                %if c.limited_diff:
 
                  <h5>${_('Changeset was too big and was cut off...')} <a href="${h.url.current(fulldiff=1, **request.GET.mixed())}">${_('Show full diff anyway')}</a></h5>
 
                %endif
 
              </div>
 
            </div>
 
        </div>
 
    </div>
 
    <script>
 
    'use strict';
 
    // TODO: switch this to pyroutes
 
    var AJAX_COMMENT_URL = ${h.js(url('pullrequest_comment',repo_name=c.repo_name,pull_request_id=c.pull_request.pull_request_id))};
 
    var AJAX_COMMENT_DELETE_URL = ${h.js(url('pullrequest_comment_delete',repo_name=c.repo_name,comment_id='__COMMENT_ID__'))};
 

	
 
    pyroutes.register('pullrequest_comment', ${h.js(url('pullrequest_comment',repo_name='%(repo_name)s',pull_request_id='%(pull_request_id)s'))}, ['repo_name', 'pull_request_id']);
 
    pyroutes.register('pullrequest_comment_delete', ${h.js(url('pullrequest_comment_delete',repo_name='%(repo_name)s',comment_id='%(comment_id)s'))}, ['repo_name', 'comment_id']);
 

	
 
    </script>
 

	
 
    ## diff block
 
    <div class="panel-body">
 
    <div class="commentable-diff">
 
    <%namespace name="diff_block" file="/changeset/diff_block.html"/>
 
    ${diff_block.diff_block_js()}
 
    ${diff_block.diff_block(c.a_repo.repo_name, c.a_ref_type, c.a_ref_name, c.a_rev,
 
                            c.cs_repo.repo_name, c.cs_ref_type, c.cs_ref_name, c.cs_rev, c.file_diff_data)}
 
    % if c.limited_diff:
 
      <h4>${_('Changeset was too big and was cut off...')} <a href="${h.url.current(fulldiff=1, **request.GET.mixed())}">${_('Show full diff anyway')}</a></h4>
 
    % endif
 
    </div>
 

	
 
    ## template for inline comment form
 
    ${comment.comment_inline_form()}
 

	
 
    ## render comments and inlines
 
    ${comment.generate_comments()}
 

	
 
    ## main comment form and it status
 
    ${comment.comments(change_status=c.allowed_to_change_status)}
 

	
 
    <script>
 
      'use strict';
 
      $(document).ready(function(){
 
          PullRequestAutoComplete($('#user'));
 
          SimpleUserAutoComplete($('#owner'));
 

	
 
          $('.code-difftable').on('click', '.add-bubble', function(){
 
              show_comment_form($(this));
 
          });
 

	
 
          var avail_jsdata = ${h.js(c.avail_jsdata)};
 
          var avail_r = new BranchRenderer('avail_graph_canvas', 'updaterevs-table', 'chg_available_');
 
          avail_r.render(avail_jsdata);
 

	
 
          $(window).resize(function(){
 
              avail_r.render(avail_jsdata);
 
          });
 

	
 
          move_comments($(".comments .comments-list-chunk"));
 

	
 
          $('#updaterevs input').change(function(e){
 
              var update = !!e.target.value;
 
              $('#pr-form-save').prop('disabled',update);
 
              $('#pr-form-clone').prop('disabled',!update);
 
          });
 
          var $org_review_members = $('#review_members').clone();
 
          $('#pr-form-reset').click(function(){
 
              $('.pr-do-edit').hide();
 
              $('.pr-not-edit').show();
 
              $('#pr-form-save').prop('disabled',false);
 
              $('#pr-form-clone').prop('disabled',true);
 
              $('#review_members').html($org_review_members);
 
          });
 

	
 
          // hack: re-navigate to target after JS is done ... if a target is set and setting href thus won't reload
 
          if (window.location.hash != "") {
 
              window.location.href = window.location.href;
 
          }
 

	
 
          $('.missing_reviewer').click(function(){
 
            var $this = $(this);
 
            addReviewMember($this.data('user_id'), $this.data('fname'), $this.data('lname'), $this.data('nname'), $this.data('gravatar_lnk'), $this.data('gravatar_size'));
 
          });
 
      });
 
    </script>
 
    </div>
 

	
 
</div>
 

	
 
</%def>
kallithea/templates/summary/summary.html
Show inline comments
 
<%inherit file="/base/base.html"/>
 
<%namespace name="changelog_table" file="/changelog/changelog_table.html"/>
 

	
 
<%block name="title">
 
    ${_('%s Summary') % c.repo_name}
 
</%block>
 

	
 
<%def name="breadcrumbs_links()">
 
    ${_('Summary')}
 

	
 
    ##FORK
 
    %if c.db_repo.fork:
 
        - <i class="icon-fork"></i>${_('Fork of')} "<a href="${h.url('summary_home',repo_name=c.db_repo.fork.repo_name)}">${c.db_repo.fork.repo_name}</a>"
 
    %endif
 

	
 
    ##REMOTE
 
    %if c.db_repo.clone_uri:
 
       - <i class="icon-fork"></i>${_('Clone from')} "<a href="${h.url(str(h.hide_credentials(c.db_repo.clone_uri)))}">${h.hide_credentials(c.db_repo.clone_uri)}</a>"
 
    %endif
 
</%def>
 

	
 
<%block name="header_menu">
 
    ${self.menu('repositories')}
 
</%block>
 

	
 
<%block name="head_extra">
 
  <link href="${h.url('atom_feed_home',repo_name=c.db_repo.repo_name,api_key=request.authuser.api_key)}" rel="alternate" title="${_('%s ATOM feed') % c.repo_name}" type="application/atom+xml" />
 
  <link href="${h.url('rss_feed_home',repo_name=c.db_repo.repo_name,api_key=request.authuser.api_key)}" rel="alternate" title="${_('%s RSS feed') % c.repo_name}" type="application/rss+xml" />
 

	
 
  <script>
 
  'use strict';
 
  function redirect_hash_branch(){
 
    var branch = window.location.hash.replace(/^#(.*)/, '$1');
 
    if (branch){
 
      window.location = ${h.js(h.url('changelog_home',repo_name=c.repo_name,branch='__BRANCH__'))}
 
        .replace('__BRANCH__',branch);
 
    }
 
  }
 
  redirect_hash_branch();
 
  window.onhashchange = function() {
 
    redirect_hash_branch();
 
  };
 
  </script>
 
</%block>
 

	
 
<%def name="main()">
 
${self.repo_context_bar('summary')}
 
<div class="panel panel-primary">
 
    <div class="panel-heading clearfix">
 
        ${self.breadcrumbs()}
 
    </div>
 
    <div id="summary-panel-body" class="form panel-body">
 
        <div id="summary" class="pull-left">
 
            <div class="form-group form-inline">
 
                <label>${_('Clone URL')}:</label>
 
                <div id="clone-url">
 
                  <div id="clone_by_name" class="input-group" style="${'display:none' if c.ssh_repo_url else ''}">
 
                    <span class="input-group-addon">${self.repolabel(c.db_repo.repo_type)}</span>
 
                    <input class="form-control" size="80" readonly="readonly" value="${c.clone_repo_url}"/>
 
                    <span class="input-group-addon btn use-id">${_('Use ID')}</span>
 
                    %if c.ssh_repo_url:
 
                    <span class="input-group-addon btn use-ssh">${_('Use SSH')}</span>
 
                    %endif
 
                  </div>
 
                  <div id="clone_by_id" class="input-group" style="display:none">
 
                    <span class="input-group-addon">${self.repolabel(c.db_repo.repo_type)}</span>
 
                    <input class="form-control" size="80" readonly="readonly" value="${c.clone_repo_url_id}"/>
 
                    <span class="input-group-addon btn use-name">${_('Use Name')}</span>
 
                    %if c.ssh_repo_url:
 
                    <span class="input-group-addon btn use-ssh">${_('Use SSH')}</span>
 
                    %endif
 
                  </div>
 
                  %if c.ssh_repo_url:
 
                  <div id="clone_ssh" class="input-group">
 
                    <span class="input-group-addon">${self.repolabel(c.db_repo.repo_type)}</span>
 
                    <input id="ssh_url" class="form-control" size="80" readonly="readonly" value="${c.ssh_repo_url}"/>
 
                    <span class="input-group-addon btn use-name">${_('Use HTTP')}</span>
 
                  </div>
 
                  %endif
 
                </div>
 
            </div>
 

	
 
            <div class="form-group">
 
              <label>${_('Description')}:</label>
 
              <div class="formatted-fixed">${h.urlify_text(c.db_repo.description, stylize=c.visual.stylify_metalabels)}</div>
 
            </div>
 

	
 
            <div class="form-group">
 
              <label>${_('Trending files')}:</label>
 
              <div>
 
                %if c.show_stats:
 
                <div id="lang_stats"></div>
 
                %else:
 
                   ${_('Statistics are disabled for this repository')}
 
                   %if h.HasPermissionAny('hg.admin')('enable stats on from summary'):
 
                        ${h.link_to(_('Enable'),h.url('edit_repo',repo_name=c.repo_name, anchor='repo_enable_statistics'),class_="btn btn-default btn-xs")}
 
                   %endif
 
                %endif
 
              </div>
 
            </div>
 

	
 
            <div class="form-group">
 
              <label>${_('Download')}:</label>
 
              <div class="form-inline">
 
                %if len(c.db_repo_scm_instance.revisions) == 0:
 
                  ${_('There are no downloads yet')}
 
                %elif not c.enable_downloads:
 
                  ${_('Downloads are disabled for this repository')}
 
                    %if h.HasPermissionAny('hg.admin')('enable downloads on from summary'):
 
                        ${h.link_to(_('Enable'),h.url('edit_repo',repo_name=c.repo_name, anchor='repo_enable_downloads'),class_="btn btn-default btn-xs")}
 
                    %endif
 
                %else:
 
                    <span id="${'zip_link'}">
 
                        <a class="btn btn-default btn-sm" href="${h.url('files_archive_home',repo_name=c.db_repo.repo_name,fname='tip.zip')}"><i class="icon-file-zip"></i>${_('Download as zip')}</a>
 
                    </span>
 
                    ${h.hidden('download_options', class_='form-control')}
 
                    <span>
 
                      <label data-toggle="tooltip" title="${_('Check this to download archive with subrepos')}">
 
                          <input id="archive_subrepos" type="checkbox" name="subrepos" />
 
                          ${_('With subrepos')}
 
                      </label>
 
                    </span>
 
                %endif
 
              </div>
 
            </div>
 
        </div>
 
        <ul id="summary-menu-stats" class="list-group pull-right">
 
            <li class="list-group-item">
 
              <a title="${_('Owner')} ${c.db_repo.owner.email}">
 
                <i class="icon-user"></i>${c.db_repo.owner.username}
 
                ${h.gravatar_div(c.db_repo.owner.email, size=18, div_class="pull-right")}
 
              </a>
 
            </li>
 
            <li class="list-group-item">
 
              <a title="${_('Followers')}" href="${h.url('repo_followers_home',repo_name=c.repo_name)}">
 
                <i class="icon-heart"></i>${_('Followers')}
 
                <span class="badge pull-right" id="current_followers_count">${c.repository_followers}</span>
 
              </a>
 
            </li>
 
            <li class="list-group-item">
 
              <a title="${_('Forks')}" href="${h.url('repo_forks_home',repo_name=c.repo_name)}">
 
                <i class="icon-fork"></i>${_('Forks')}
 
                <span class="badge pull-right">${c.repository_forks}</span>
 
              </a>
 
            </li>
 

	
 
            %if request.authuser.username != 'default':
 
            <li class="list-group-item clearfix">
 
              <a href="#" onclick="javascript:showRepoSize('repo_size_2','${c.db_repo.repo_name}')">
 
                <i class="icon-ruler"></i>${_('Size')}
 
                <span class="badge pull-right" id="repo_size_2"></span>
 
              </a>
 
            </li>
 
            %endif
 

	
 
            <li class="list-group-item">
 
            %if request.authuser.username != 'default':
 
              <a href="${h.url('atom_feed_home',repo_name=c.db_repo.repo_name,api_key=request.authuser.api_key)}"><i class="icon-rss-squared"></i>${_('Feed')}</a>
 
            %else:
 
              <a href="${h.url('atom_feed_home',repo_name=c.db_repo.repo_name)}"><i class="icon-rss-squared"></i>${_('Feed')}</a>
 
            %endif
 
            </li>
 

	
 
            %if c.show_stats:
 
            <li class="list-group-item">
 
              <a title="${_('Statistics')}" href="${h.url('repo_stats_home',repo_name=c.repo_name)}">
 
                <i class="icon-graph"></i>${_('Statistics')}
 
              </a>
 
            </li>
 
            %endif
 
        </ul>
 
    </div>
 
</div>
 

	
 

	
 
<div class="panel panel-primary">
 
    <div class="panel-heading">
 
        <div class="panel-title">
 
        %if c.cs_pagination:
 
            ${h.link_to(_('Latest Changes'),h.url('changelog_home',repo_name=c.repo_name))}
 
        %else:
 
            ${_('Quick Start')}
 
        %endif
 
        </div>
 
    </div>
 
    <div class="panel-body">
 
        <div id="shortlog_data">
 
            %if c.cs_pagination:
 
                ${changelog_table.changelog(c.repo_name, c.cs_pagination, c.cs_statuses, c.cs_comments, show_checkbox=False)}
 
                ${c.cs_pagination.pager()}
 
            %else:
 
                %if h.HasRepoPermissionLevel('write')(c.repo_name):
 
                <h4>${_('Add or upload files directly via Kallithea')}</h4>
 
                <div>
 
                  <div id="add_node_id" class="add_node">
 
                      <a class="btn btn-default btn-xs" href="${h.url('files_add_home',repo_name=c.repo_name,revision=0,f_path='', anchor='edit')}">${_('Add New File')}</a>
 
                  </div>
 
                </div>
 
                %endif
 

	
 
                <h4>${_('Push new repository')}</h4>
 
                <pre>
 
${c.db_repo_scm_instance.alias} clone ${c.clone_repo_url}
 
${c.db_repo_scm_instance.alias} add README # add first file
 
${c.db_repo_scm_instance.alias} commit -m "Initial" # commit with message
 
${c.db_repo_scm_instance.alias} push ${'origin master' if h.is_git(c.db_repo_scm_instance) else ''} # push changes back
 
${c.db_repo_scm_instance.alias} push ${'origin master' if c.db_repo_scm_instance.alias == 'git' else ''} # push changes back
 
                </pre>
 

	
 
                <h4>${_('Existing repository?')}</h4>
 
                <pre>
 
                %if h.is_git(c.db_repo_scm_instance):
 
                %if c.db_repo_scm_instance.alias == 'git':
 
git remote add origin ${c.clone_repo_url}
 
git push -u origin master
 
                %else:
 
hg push ${c.clone_repo_url}
 
                %endif
 
                </pre>
 
            %endif
 
        </div>
 
    </div>
 
</div>
 

	
 
%if c.readme_data:
 
<div id="readme" class="anchor">
 
</div>
 
<div class="panel panel-primary">
 
    <div class="panel-heading" title="${_('Readme file from revision %s:%s') % (c.db_repo.landing_rev[0], c.db_repo.landing_rev[1])}">
 
        <div class="panel-title">
 
            <a href="${h.url('files_home',repo_name=c.repo_name,revision='tip',f_path=c.readme_file)}">${c.readme_file}</a>
 
        </div>
 
    </div>
 
    <div class="readme panel-body">
 
        ${c.readme_data|n}
 
    </div>
 
</div>
 
%endif
 

	
 
<script>
 
'use strict';
 
$(document).ready(function(){
 
    $('#clone-url input').click(function(){
 
        if($(this).hasClass('selected')){
 
            $(this).removeClass('selected');
 
            return ;
 
        }else{
 
            $(this).addClass('selected');
 
            $(this).select();
 
        }
 
    });
 

	
 
    var $clone_url = $('#clone-url');
 
    var $clone_by_name = $('#clone_by_name');
 
    var $clone_by_id = $('#clone_by_id');
 
    var $clone_ssh = $('#clone_ssh');
 
    $clone_url.on('click', '.btn.use-name', function(){
 
        $clone_by_name.show();
 
        $clone_by_id.hide();
 
        $clone_ssh.hide();
 
    });
 
    $clone_url.on('click', '.btn.use-id', function(){
 
        $clone_by_id.show();
 
        $clone_by_name.hide();
 
        $clone_ssh.hide();
 
    });
 
    $clone_url.on('click', '.btn.use-ssh', function(){
 
        $clone_by_id.hide();
 
        $clone_by_name.hide();
 
        $clone_ssh.show();
 
    });
 

	
 
    var cache = {}
 
    $("#download_options").select2({
 
        placeholder: _TM['Select changeset'],
 
        dropdownAutoWidth: true,
 
        query: function(query){
 
          var key = 'cache';
 
          var cached = cache[key] ;
 
          if(cached) {
 
            var data = {results: []};
 
            //filter results
 
            $.each(cached.results, function(){
 
                var section = this.text;
 
                var children = [];
 
                $.each(this.children, function(){
 
                    if(query.term.length == 0 || this.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0 ){
 
                        children.push({'id': this.id, 'text': this.text});
 
                    }
 
                });
 
                data.results.push({'text': section, 'children': children});
 
            });
 
            query.callback(data);
 
          }else{
 
              $.ajax({
 
                url: pyroutes.url('repo_refs_data', {'repo_name': ${h.js(c.repo_name)}}),
 
                data: {},
 
                dataType: 'json',
 
                type: 'GET',
 
                success: function(data) {
 
                  cache[key] = data;
 
                  query.callback({results: data.results});
 
                }
 
              });
 
          }
 
        }
 
    });
 
    // on change of download options
 
    $('#download_options').change(function(e){
 
       var new_cs = e.added
 

	
 
       for(let k in tmpl_links){
 
           var s = $('#'+k+'_link');
 
           if(s){
 
             var title_tmpl = ${h.jshtml(_('Download %s as %s') % ('__CS_NAME__','__CS_EXT__'))};
 
             title_tmpl= title_tmpl.replace('__CS_NAME__',new_cs.text);
 
             title_tmpl = title_tmpl.replace('__CS_EXT__',k);
 
             title_tmpl = '<i class="icon-file-zip"></i>'+ title_tmpl.html_escape();
 
             var url = tmpl_links[k].replace('__CS__',new_cs.id);
 
             var subrepos = $('#archive_subrepos').is(':checked');
 
             url = url.replace('__SUB__',subrepos);
 
             url = url.replace('__NAME__',title_tmpl);
 

	
 
             s.html(url);
 
           }
 
       }
 
    });
 

	
 
    var tmpl_links = {};
 
    %for cnt,archive in enumerate(c.db_repo_scm_instance._get_archives()):
 
      tmpl_links[${h.jshtml(archive['type'])}] = ${h.js(h.link_to('__NAME__', h.url('files_archive_home',repo_name=c.db_repo.repo_name, fname='__CS__'+archive['extension'],subrepos='__SUB__'),class_='btn btn-default btn-sm'))};
 
    %endfor
 
});
 
</script>
 

	
 
%if c.show_stats:
 
<script>
 
'use strict';
 
$(document).ready(function(){
 
    var data = ${h.js(c.trending_languages)};
 
    var total = 0;
 
    var tbl = document.createElement('table');
 
    tbl.setAttribute('class','table');
 
    var cnt = 0;
 
    for (let i=0;i<data.length;i++){
 
        total+= data[i][1].count;
 
    }
 
    for (let i=0;i<data.length;i++){
 
        cnt += 1;
 

	
 
        var hide = cnt>2;
 
        var tr = document.createElement('tr');
 
        if (hide){
 
            tr.setAttribute('style','display:none');
 
            tr.setAttribute('class','stats_hidden');
 
        }
 
        var k = data[i][0];
 
        var obj = data[i][1];
 
        var percentage = Math.round((obj.count/total*100),2);
 

	
 
        var td1 = document.createElement('td');
 
        td1.width = 250;
 
        var trending_language_label = document.createElement('div');
 
        trending_language_label.innerHTML = obj.desc+" ("+k+")";
 
        td1.appendChild(trending_language_label);
 

	
 
        var td2 = document.createElement('td');
 
        td2.setAttribute('style','padding-right:14px !important');
 
        var trending_language = document.createElement('div');
 
        var nr_files = obj.count + ' ' + ${h.jshtml(_('files'))};
 

	
 
        trending_language.title = k+" "+nr_files;
 

	
 
        if (percentage>22){
 
            trending_language.innerHTML = "<b class='progress-bar' role='progressbar'"
 
                + "aria-valuemin='0' aria-valuemax='100' aria-valuenow='" + percentage
 
                + "' style='width: " + percentage + "%;'>" + percentage + "%, " + nr_files + "</b>";
 
        }
 
        else if (percentage>5){
 
            trending_language.innerHTML = "<b class='progress-bar' role='progressbar'"
 
                + "aria-valuemin='0' aria-valuemax='100' aria-valuenow='" + percentage
 
                + "' style='width: " + percentage + "%;'>" + percentage + "%</b>";
 
        }else{
 
            trending_language.innerHTML = "<b class='progress-bar' role='progressbar'"
 
                + "aria-valuemin='0' aria-valuemax='100' aria-valuenow='" + percentage
 
                + "' style='width: " + percentage + "%;'>&nbsp;</b>&nbsp;" + percentage + "%";
 
        }
 

	
 
        td2.appendChild(trending_language);
 

	
 
        tr.appendChild(td1);
 
        tr.appendChild(td2);
 
        tbl.appendChild(tr);
 
        if(cnt == 3){
 
            var show_more = document.createElement('tr');
 
            var td = document.createElement('td');
 
            let lnk = document.createElement('a');
 

	
 
            lnk.href='#';
 
            lnk.innerHTML = ${h.jshtml(_('Show more'))};
 
            lnk.id='code_stats_show_more';
 
            td.appendChild(lnk);
 

	
 
            show_more.appendChild(td);
 
            show_more.appendChild(document.createElement('td'));
 
            tbl.appendChild(show_more);
 
        }
 

	
 
    }
 
    if (data.length == 0) {
 
        tbl.innerHTML = '<tr><td>' + ${h.jshtml(_('No data ready yet'))} + '</td></tr>';
 
    }
 

	
 
    $('#lang_stats').append(tbl);
 
    $('#code_stats_show_more').click(function(){
 
        $('.stats_hidden').show();
 
        $('#code_stats_show_more').hide();
 
    });
 
});
 
</script>
 
%endif
 

	
 
</%def>
0 comments (0 inline, 0 general)