# -*- 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.compare
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
compare controller for pylons showing differences between two
repos, branches, bookmarks or tips
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 6, 2012
:author: marcink
:copyright: (c) 2013 RhodeCode GmbH, and others.
:license: GPLv3, see LICENSE.md for more details.
import logging
import re
from webob.exc import HTTPBadRequest
from pylons import request, tmpl_context as c, url
from pylons.controllers.util import redirect
from pylons.i18n.translation import _
from kallithea.lib.vcs.utils.hgcompat import unionrepo
from kallithea.lib import helpers as h
from kallithea.lib.base import BaseRepoController, render
from kallithea.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
from kallithea.lib import diffs
from kallithea.model.db import Repository
from kallithea.lib.diffs import LimitedDiffContainer
from kallithea.controllers.changeset import _ignorews_url,\
_context_url, get_line_ctx, get_ignore_ws
from kallithea.lib.graphmod import graph_data
from kallithea.lib.compat import json
log = logging.getLogger(__name__)
class CompareController(BaseRepoController):
def __before__(self):
super(CompareController, self).__before__()
@staticmethod
def _get_changesets(alias, org_repo, org_rev, other_repo, other_rev):
Returns lists of changesets that can be merged from org_repo@org_rev
to other_repo@other_rev
... and the other way
... and the ancestor that would be used for merge
:param org_repo: repo object, that is most likely the orginal repo we forked from
:param org_rev: the revision we want our compare to be made
:param other_repo: repo object, mostl likely the fork of org_repo. It hass
all changesets that we need to obtain
:param other_rev: revision we want out compare to be made on other_repo
ancestor = None
if org_rev == other_rev:
org_changesets = []
other_changesets = []
ancestor = org_rev
elif alias == 'hg':
#case two independent repos
if org_repo != other_repo:
hgrepo = unionrepo.unionrepository(other_repo.baseui,
other_repo.path,
org_repo.path)
# all ancestors of other_rev will be in other_repo and
# rev numbers from hgrepo can be used in other_repo - org_rev ancestors cannot
#no remote compare do it on the same repository
else:
hgrepo = other_repo._repo
ancestors = hgrepo.revs("ancestor(id(%s), id(%s))", org_rev, other_rev)
if ancestors:
# pick arbitrary ancestor - but there is usually only one
ancestor = hgrepo[ancestors[0]].hex()
other_revs = hgrepo.revs("ancestors(id(%s)) and not ancestors(id(%s)) and not id(%s)",
other_rev, org_rev, org_rev)
other_changesets = [other_repo.get_changeset(rev) for rev in other_revs]
org_revs = hgrepo.revs("ancestors(id(%s)) and not ancestors(id(%s)) and not id(%s)",
org_rev, other_rev, other_rev)
org_changesets = [org_repo.get_changeset(hgrepo[rev].hex()) for rev in org_revs]
elif alias == 'git':
from dulwich.repo import Repo
from dulwich.client import SubprocessGitClient
gitrepo = Repo(org_repo.path)
SubprocessGitClient(thin_packs=False).fetch(other_repo.path, gitrepo)
gitrepo_remote = Repo(other_repo.path)
SubprocessGitClient(thin_packs=False).fetch(org_repo.path, gitrepo_remote)
revs = []
for x in gitrepo_remote.get_walker(include=[other_rev],
exclude=[org_rev]):
revs.append(x.commit.id)
other_changesets = [other_repo.get_changeset(rev) for rev in reversed(revs)]
if other_changesets:
ancestor = other_changesets[0].parents[0].raw_id
# no changesets from other repo, ancestor is the other_rev
ancestor = other_rev
so, se = org_repo.run_git_command(
'log --reverse --pretty="format: %%H" -s %s..%s'
% (org_rev, other_rev)
)
other_changesets = [org_repo.get_changeset(cs)
for cs in re.findall(r'[0-9a-fA-F]{40}', so)]
'merge-base %s %s' % (org_rev, other_rev)
ancestor = re.findall(r'[0-9a-fA-F]{40}', so)[0]
raise Exception('Bad alias only git and hg is allowed')
return other_changesets, org_changesets, ancestor
@LoginRequired()
@HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
'repository.admin')
def index(self, repo_name):
c.compare_home = True
org_repo = c.db_repo.repo_name
other_repo = request.GET.get('other_repo', org_repo)
c.a_repo = Repository.get_by_repo_name(org_repo)
c.cs_repo = Repository.get_by_repo_name(other_repo)
c.a_ref_name = c.cs_ref_name = _('Select changeset')
return render('compare/compare_diff.html')
def compare(self, repo_name, org_ref_type, org_ref_name, other_ref_type, other_ref_name):
# If merge is True:
# Show what org would get if merged with other:
# List changesets that are ancestors of other but not of org.
# New changesets in org is thus ignored.
# Diff will be from common ancestor, and merges of org to other will thus be ignored.
# If merge is False:
# Make a raw diff from org to other, no matter if related or not.
# Changesets in one and not in the other will be ignored
merge = bool(request.GET.get('merge'))
# fulldiff disables cut_off_limit
c.fulldiff = request.GET.get('fulldiff')
# partial uses compare_cs.html template directly
partial = request.environ.get('HTTP_X_PARTIAL_XHR')
# as_form puts hidden input field with changeset revisions
c.as_form = partial and request.GET.get('as_form')
# swap url for compare_diff page - never partial and never as_form
c.swap_url = h.url('compare_url',
repo_name=other_repo,
org_ref_type=other_ref_type, org_ref_name=other_ref_name,
other_repo=org_repo,
other_ref_type=org_ref_type, other_ref_name=org_ref_name,
merge=merge or '')
# set callbacks for generating markup for icons
c.ignorews_url = _ignorews_url
c.context_url = _context_url
ignore_whitespace = request.GET.get('ignorews') == '1'
line_context = request.GET.get('context', 3)
org_repo = Repository.get_by_repo_name(org_repo)
other_repo = Repository.get_by_repo_name(other_repo)
if org_repo is None:
msg = 'Could not find org repo %s' % org_repo
log.error(msg)
h.flash(msg, category='error')
return redirect(url('compare_home', repo_name=c.repo_name))
if other_repo is None:
msg = 'Could not find other repo %s' % other_repo
if org_repo.scm_instance.alias != other_repo.scm_instance.alias:
msg = 'compare of two different kind of remote repos not available'
c.a_rev = self._get_ref_rev(org_repo, org_ref_type, org_ref_name)
c.a_rev = self._get_ref_rev(org_repo, org_ref_type, org_ref_name,
returnempty=True)
c.cs_rev = self._get_ref_rev(other_repo, other_ref_type, other_ref_name)
c.compare_home = False
c.a_repo = org_repo
c.a_ref_name = org_ref_name
c.a_ref_type = org_ref_type
c.cs_repo = other_repo
c.cs_ref_name = other_ref_name
c.cs_ref_type = other_ref_type
c.cs_ranges, c.cs_ranges_org, c.ancestor = self._get_changesets(
org_repo.scm_instance.alias, org_repo.scm_instance, c.a_rev,
other_repo.scm_instance, c.cs_rev)
raw_ids = [x.raw_id for x in c.cs_ranges]
c.cs_comments = other_repo.get_comments(raw_ids)
c.statuses = other_repo.statuses(raw_ids)
revs = [ctx.revision for ctx in reversed(c.cs_ranges)]
c.jsdata = json.dumps(graph_data(c.cs_repo.scm_instance, revs))
if partial:
return render('compare/compare_cs.html')
if merge and c.ancestor:
# case we want a simple diff without incoming changesets,
# previewing what will be merged.
# Make the diff on the other repo (which is known to have other_rev)
log.debug('Using ancestor %s as rev1 instead of %s'
% (c.ancestor, c.a_rev))
rev1 = c.ancestor
org_repo = other_repo
else: # comparing tips, not necessarily linearly related
if merge:
log.error('Unable to find ancestor revision')
# TODO: we could do this by using hg unionrepo
log.error('cannot compare across repos %s and %s', org_repo, other_repo)
h.flash(_('Cannot compare repositories without using common ancestor'), category='error')
raise HTTPBadRequest
rev1 = c.a_rev
diff_limit = self.cut_off_limit if not c.fulldiff else None
log.debug('running diff between %s and %s in %s'
% (rev1, c.cs_rev, org_repo.scm_instance.path))
txtdiff = org_repo.scm_instance.get_diff(rev1=rev1, rev2=c.cs_rev,
ignore_whitespace=ignore_whitespace,
context=line_context)
diff_processor = diffs.DiffProcessor(txtdiff or '', format='gitdiff',
diff_limit=diff_limit)
_parsed = diff_processor.prepare()
c.limited_diff = False
if isinstance(_parsed, LimitedDiffContainer):
c.limited_diff = True
c.files = []
c.changes = {}
c.lines_added = 0
c.lines_deleted = 0
for f in _parsed:
st = f['stats']
if not st['binary']:
c.lines_added += st['added']
c.lines_deleted += st['deleted']
fid = h.FID('', f['filename'])
c.files.append([fid, f['operation'], f['filename'], f['stats']])
htmldiff = diff_processor.as_html(enable_comments=False, parsed_lines=[f])
c.changes[fid] = [f['operation'], f['filename'], htmldiff]
@@ -199,562 +199,565 @@ class PullrequestsController(BaseRepoCon
c.from_ = request.GET.get('from_') or ''
c.closed = request.GET.get('closed') or ''
c.pull_requests = PullRequestModel().get_all(repo_name, from_=c.from_, closed=c.closed)
c.repo_name = repo_name
p = safe_int(request.GET.get('page', 1), 1)
c.pullrequests_pager = Page(c.pull_requests, page=p, items_per_page=10)
c.pullrequest_data = render('/pullrequests/pullrequest_data.html')
if request.environ.get('HTTP_X_PARTIAL_XHR'):
return c.pullrequest_data
return render('/pullrequests/pullrequest_show_all.html')
def show_my(self): # my_account_my_pullrequests
c.show_closed = request.GET.get('pr_show_closed')
return render('/pullrequests/pullrequest_show_my.html')
@NotAnonymous()
def show_my_data(self):
def _filter(pr):
s = sorted(pr, key=lambda o: o.created_on, reverse=True)
if not c.show_closed:
s = filter(lambda p: p.status != PullRequest.STATUS_CLOSED, s)
return s
c.my_pull_requests = _filter(PullRequest.query()\
.filter(PullRequest.user_id ==
self.authuser.user_id)\
.all())
c.participate_in_pull_requests = _filter(PullRequest.query()\
.join(PullRequestReviewers)\
.filter(PullRequestReviewers.user_id ==
return render('/pullrequests/pullrequest_show_my_data.html')
def index(self):
org_repo = c.db_repo
org_scm_instance = org_repo.scm_instance
try:
org_scm_instance.get_changeset()
except EmptyRepositoryError, e:
h.flash(h.literal(_('There are no changesets yet')),
category='warning')
redirect(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
#other_rev = request.POST.get('rev_start')
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)
# 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)]
# add parent of this fork also and select it
if org_repo.parent:
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)
c.a_refs, c.default_a_ref = self._get_repo_refs(org_scm_instance) # without rev and branch
# 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')
@jsonify
def repo_info(self, repo_name):
repo = RepoModel()._get_repo(repo_name)
refs, selected_ref = self._get_repo_refs(repo.scm_instance)
return {
'description': repo.description.split('\n', 1)[0],
'selected_ref': selected_ref,
'refs': refs,
'user': dict(user_id=repo.user.user_id,
username=repo.user.username,
firstname=repo.user.firstname,
lastname=repo.user.lastname,
gravatar_link=h.gravatar_url(repo.user.email, 14)),
}
def create(self, repo_name):
_form = PullRequestForm(repo.repo_id)().to_python(request.POST)
except formencode.Invalid, errors:
log.error(traceback.format_exc())
log.error(str(errors))
msg = _('Error creating pull request: %s') % errors.msg
h.flash(msg, 'error')
# heads up: org and other might seem backward here ...
org_repo_name = _form['org_repo']
org_ref = _form['org_ref'] # will have merge_rev as rev but symbolic name
org_repo = RepoModel()._get_repo(org_repo_name)
(org_ref_type,
org_ref_name,
org_rev) = org_ref.split(':')
other_repo_name = _form['other_repo']
other_ref = _form['other_ref'] # will have symbolic name and head revision
other_repo = RepoModel()._get_repo(other_repo_name)
(other_ref_type,
other_ref_name,
other_rev) = other_ref.split(':')
cs_ranges, _cs_ranges_not, ancestor_rev = \
CompareController._get_changesets(org_repo.scm_instance.alias,
other_repo.scm_instance, other_rev, # org and other "swapped"
org_repo.scm_instance, org_rev,
revisions = [cs.raw_id for cs in cs_ranges]
# hack: ancestor_rev is not an other_rev but we want to show the
# requested destination and have the exact ancestor
other_ref = '%s:%s:%s' % (other_ref_type, other_ref_name, ancestor_rev)
reviewers = _form['review_members']
title = _form['pullrequest_title']
if not title:
title = '%s#%s to %s#%s' % (org_repo_name, h.short_ref(org_ref_type, org_ref_name),
other_repo_name, h.short_ref(other_ref_type, other_ref_name))
description = _form['pullrequest_desc'].strip() or _('No description')
pull_request = PullRequestModel().create(
self.authuser.user_id, org_repo_name, org_ref, other_repo_name,
other_ref, revisions, reviewers, title, description
Session().commit()
h.flash(_('Successfully opened new pull request'),
category='success')
except Exception:
h.flash(_('Error occurred while creating pull request'),
category='error')
return redirect(url('pullrequest_home', repo_name=repo_name))
return redirect(pull_request.url())
def copy_update(self, repo_name, pull_request_id):
old_pull_request = PullRequest.get_or_404(pull_request_id)
assert old_pull_request.other_repo.repo_name == repo_name
if old_pull_request.is_closed():
raise HTTPForbidden()
org_repo = RepoModel()._get_repo(old_pull_request.org_repo.repo_name)
org_ref_type, org_ref_name, org_rev = old_pull_request.org_ref.split(':')
updaterev = request.POST.get('updaterev')
if updaterev:
new_org_rev = self._get_ref_rev(org_repo, 'rev', updaterev)
# assert org_ref_type == 'branch', org_ref_type # TODO: what if not?
new_org_rev = self._get_ref_rev(org_repo, org_ref_type, org_ref_name)
other_repo = RepoModel()._get_repo(old_pull_request.other_repo.repo_name)
other_ref_type, other_ref_name, other_rev = old_pull_request.other_ref.split(':') # other_rev is ancestor
#assert other_ref_type == 'branch', other_ref_type # TODO: what if not?
new_other_rev = self._get_ref_rev(other_repo, other_ref_type, other_ref_name)
cs_ranges, _cs_ranges_not, ancestor_rev = CompareController._get_changesets(org_repo.scm_instance.alias,
other_repo.scm_instance, new_other_rev, # org and other "swapped"
org_repo.scm_instance, new_org_rev)
old_revisions = set(old_pull_request.revisions)
new_revisions = [r for r in revisions if r not in old_revisions]
lost = old_revisions.difference(revisions)
infos = ['','', 'This is an update of %s "%s".' %
(h.canonical_url('pullrequest_show', repo_name=old_pull_request.other_repo.repo_name,
pull_request_id=pull_request_id),
old_pull_request.title)]
if lost:
infos.append(_('Missing changesets since the previous version:'))
for r in old_pull_request.revisions:
if r in lost:
desc = org_repo.get_changeset(r).message.split('\n')[0]
infos.append(' %s "%s"' % (h.short_id(r), desc))
if new_revisions:
infos.append(_('New changesets on %s %s since the previous version:') % (org_ref_type, org_ref_name))
for r in reversed(revisions):
if r in new_revisions:
infos.append(' %s %s' % (h.short_id(r), h.shorter(desc, 80)))
if ancestor_rev == other_rev:
infos.append(_("Ancestor didn't change - show diff since previous version:"))
infos.append(h.canonical_url('compare_url',
repo_name=org_repo.repo_name, # other_repo is always same as repo_name
org_ref_type='rev', org_ref_name=h.short_id(org_rev), # use old org_rev as base
other_ref_type='rev', other_ref_name=h.short_id(new_org_rev),
)) # note: linear diff, merge or not doesn't matter
infos.append(_('This pull request is based on another %s revision and there is no simple diff.') % other_ref_name)
infos.append(_('No changes found on %s %s since previous version.') % (org_ref_type, org_ref_name))
# TODO: fail?
# hack: ancestor_rev is not an other_ref but we want to show the
new_other_ref = '%s:%s:%s' % (other_ref_type, other_ref_name, ancestor_rev)
new_org_ref = '%s:%s:%s' % (org_ref_type, org_ref_name, new_org_rev)
reviewers = [r.user_id for r in old_pull_request.reviewers]
old_title, old_v = re.match(r'(.*)\(v(\d+)\)\s*$', old_pull_request.title).groups()
v = int(old_v) + 1
except (AttributeError, ValueError):
old_title = old_pull_request.title
v = 2
title = '%s (v%s)' % (old_title.strip(), v)
description = (old_pull_request.description.rstrip() +
'\n'.join(infos))
self.authuser.user_id,
old_pull_request.org_repo.repo_name, new_org_ref,
old_pull_request.other_repo.repo_name, new_other_ref,
revisions, reviewers, title, description
return redirect(old_pull_request.url())
ChangesetCommentsModel().create(
text=_('Closed, replaced by %s .') % h.canonical_url('pullrequest_show',
repo_name=old_pull_request.other_repo.repo_name,
pull_request_id=pull_request.pull_request_id),
repo=old_pull_request.other_repo.repo_id,
user=c.authuser.user_id,
pull_request=pull_request_id,
closing_pr=True)
PullRequestModel().close_pull_request(pull_request_id)
h.flash(_('Pull request update created'),
# pullrequest_post for PR editing
def post(self, repo_name, pull_request_id):
pull_request = PullRequest.get_or_404(pull_request_id)
old_description = pull_request.description
_form = PullRequestPostForm()().to_python(request.POST)
pull_request.title = _form['pullrequest_title']
pull_request.description = _form['pullrequest_desc'].strip() or _('No description')
PullRequestModel().mention_from_description(pull_request, old_description)
h.flash(_('Pull request updated'), category='success')
# pullrequest_update for updating reviewer list
def update(self, repo_name, pull_request_id):
if pull_request.is_closed():
#only owner or admin can update it
owner = pull_request.author.user_id == c.authuser.user_id
repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
if h.HasPermissionAny('hg.admin') or repo_admin or owner:
reviewers_ids = map(int, filter(lambda v: v not in [None, ''],
request.POST.get('reviewers_ids', '').split(',')))
PullRequestModel().update_reviewers(pull_request_id, reviewers_ids)
return True
def delete(self, repo_name, pull_request_id):
#only owner can delete it !
if pull_request.author.user_id == c.authuser.user_id:
PullRequestModel().delete(pull_request)
h.flash(_('Successfully deleted pull request'),
return redirect(url('my_pullrequests'))
def show(self, repo_name, pull_request_id, extra=None):
repo_model = RepoModel()
c.users_array = repo_model.get_users_js()
c.user_groups_array = repo_model.get_user_groups_js()
c.pull_request = PullRequest.get_or_404(pull_request_id)
c.allowed_to_change_status = self._get_is_allowed_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(':') # other_rev is ancestor
org_scm_instance = c.cs_repo.scm_instance # property with expensive cache invalidation check!!!
c.cs_repo = c.cs_repo
c.cs_ranges = [org_scm_instance.get_changeset(x) for x in c.pull_request.revisions]
c.cs_ranges_org = None # not stored and not important and moving target - could be calculated ...
c.jsdata = json.dumps(graph_data(org_scm_instance, revs))
c.available = []
c.cs_branch_name = c.cs_ref_name
other_scm_instance = c.a_repo.scm_instance
c.update_msg = ""
c.update_msg_other = ""
if 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 ?
other_branch_name = c.a_ref_name
if c.a_ref_type != 'branch':
other_branch_name = other_scm_instance.get_changeset(c.a_ref_name).branch # use ref_type ?
except EmptyRepositoryError:
other_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 descendent of target ref name
if other_scm_instance._repo.revs('present(%s)::&%s', c.cs_ranges[-1].raw_id, other_branch_name):
c.update_msg = _('This pull request has already been merged to %s.') % other_branch_name
else: # look for children of PR head on source branch in org repo
arevs = org_scm_instance._repo.revs('%s:: & branch(%s) - %s',
revs[0], c.cs_branch_name, revs[0])
if arevs:
if c.pull_request.is_closed():
c.update_msg = _('This pull request has been closed and can not be updated with descendent changes on %s:') % c.cs_branch_name
c.update_msg = _('This pull request can be updated with descendent changes on %s:') % c.cs_branch_name
c.available = [org_scm_instance.get_changeset(x) for x in arevs]
c.update_msg = _('No changesets found for updating this pull request.')
revs = org_scm_instance._repo.revs('head() & not (%s::) & branch(%s) & !closed()', revs[0], c.cs_branch_name)
if revs:
c.update_msg_other = _('Note: Branch %s also contains unrelated changes, such as %s.') % (c.cs_branch_name,
h.short_id(org_scm_instance.get_changeset((max(revs))).raw_id))
c.update_msg_other = _('Branch %s does not contain other changes.') % c.cs_branch_name
elif org_scm_instance.alias == 'git':
c.update_msg = _("Git pull requests don't support updates yet.")
c.cs_comments = c.cs_repo.get_comments(raw_ids)
c.statuses = c.cs_repo.statuses(raw_ids)
# we swap org/other ref since we run a simple diff on one repo
% (c.a_rev, c.cs_rev, org_scm_instance.path))
txtdiff = org_scm_instance.get_diff(rev1=safe_str(c.a_rev), rev2=safe_str(c.cs_rev),
htmldiff = diff_processor.as_html(enable_comments=True,
parsed_lines=[f])
# inline comments
c.inline_cnt = 0
c.inline_comments = cc_model.get_inline_comments(
c.db_repo.repo_id,
pull_request=pull_request_id)
# count inline comments
for __, lines in c.inline_comments:
for comments in lines.values():
c.inline_cnt += len(comments)
# comments
c.comments = cc_model.get_comments(c.db_repo.repo_id,
# (badly named) pull-request status calculation based on reviewer votes
(c.pull_request_reviewers,
c.pull_request_pending_reviewers,
c.current_voting_result,
) = cs_model.calculate_pull_request_result(c.pull_request)
c.changeset_statuses = ChangesetStatus.STATUSES
c.as_form = False
c.ancestor = None # there is one - but right here we don't know which
return render('/pullrequests/pullrequest_show.html')
def comment(self, repo_name, pull_request_id):
status = 0
close_pr = False
allowed_to_change_status = self._get_is_allowed_change_status(pull_request)
if allowed_to_change_status:
status = request.POST.get('changeset_status')
close_pr = request.POST.get('save_close')
text = request.POST.get('text', '').strip() or _('No comments.')
if close_pr:
text = _('Closing.') + '\n' + text
comm = ChangesetCommentsModel().create(
text=text,
repo=c.db_repo.repo_id,
f_path=request.POST.get('f_path'),
line_no=request.POST.get('line'),
status_change=(ChangesetStatus.get_status_lbl(status)
if status and allowed_to_change_status else None),
closing_pr=close_pr
action_logger(self.authuser,
'user_commented_pull_request:%s' % pull_request_id,
c.db_repo, self.ip_addr, self.sa)
# get status if set !
if status:
ChangesetStatusModel().set_status(
status,
c.authuser.user_id,
comm,
pull_request=pull_request_id
'user_closed_pull_request:%s' % pull_request_id,
if not request.environ.get('HTTP_X_PARTIAL_XHR'):
data = {
'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
if comm:
c.co = comm
data.update(comm.get_dict())
data.update({'rendered_text':
render('changeset/changeset_comment_block.html')})
return data
def delete_comment(self, repo_name, comment_id):
co = ChangesetComment.get(comment_id)
if co.pull_request.is_closed():
#don't allow deleting comments on closed pull request
owner = co.author.user_id == c.authuser.user_id
ChangesetCommentsModel().delete(comment=co)
@@ -55,403 +55,405 @@ from kallithea.lib.vcs.exceptions import
from kallithea.model import meta
from kallithea.model.db import Repository, Ui, User, Setting
from kallithea.model.notification import NotificationModel
from kallithea.model.scm import ScmModel
from kallithea.model.pull_request import PullRequestModel
def _filter_proxy(ip):
HEADERS can have multiple ips inside the left-most being the original
client, and each successive proxy that passed the request adding the IP
address where it received the request from.
:param ip:
if ',' in ip:
_ips = ip.split(',')
_first_ip = _ips[0].strip()
log.debug('Got multiple IPs %s, using %s' % (','.join(_ips), _first_ip))
return _first_ip
return ip
def _get_ip_addr(environ):
proxy_key = 'HTTP_X_REAL_IP'
proxy_key2 = 'HTTP_X_FORWARDED_FOR'
def_key = 'REMOTE_ADDR'
ip = environ.get(proxy_key)
if ip:
return _filter_proxy(ip)
ip = environ.get(proxy_key2)
ip = environ.get(def_key, '0.0.0.0')
def _get_access_path(environ):
path = environ.get('PATH_INFO')
org_req = environ.get('pylons.original_request')
if org_req:
path = org_req.environ.get('PATH_INFO')
return path
class BasicAuth(paste.auth.basic.AuthBasicAuthenticator):
def __init__(self, realm, authfunc, auth_http_code=None):
self.realm = realm
self.authfunc = authfunc
self._rc_auth_http_code = auth_http_code
def build_authentication(self):
head = paste.httpheaders.WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
if self._rc_auth_http_code and self._rc_auth_http_code == '403':
# return 403 if alternative http return code is specified in
# Kallithea config
return paste.httpexceptions.HTTPForbidden(headers=head)
return paste.httpexceptions.HTTPUnauthorized(headers=head)
def authenticate(self, environ):
authorization = paste.httpheaders.AUTHORIZATION(environ)
if not authorization:
return self.build_authentication()
(authmeth, auth) = authorization.split(' ', 1)
if 'basic' != authmeth.lower():
auth = auth.strip().decode('base64')
_parts = auth.split(':', 1)
if len(_parts) == 2:
username, password = _parts
if self.authfunc(username, password, environ):
return username
__call__ = authenticate
class BaseVCSController(object):
def __init__(self, application, config):
self.application = application
self.config = config
# base path of repo locations
self.basepath = self.config['base_path']
#authenticate this VCS request using authfunc
self.authenticate = BasicAuth('', auth_modules.authenticate,
config.get('auth_ret_code'))
self.ip_addr = '0.0.0.0'
def _handle_request(self, environ, start_response):
raise NotImplementedError()
def _get_by_id(self, repo_name):
Gets a special pattern _<ID> from clone url and tries to replace it
with a repository_name for support of _<ID> non changable urls
:param repo_name:
data = repo_name.split('/')
if len(data) >= 2:
from kallithea.lib.utils import get_repo_by_id
by_id_match = get_repo_by_id(repo_name)
if by_id_match:
data[1] = by_id_match
return '/'.join(data)
def _invalidate_cache(self, repo_name):
Set's cache for this repository for invalidation on next access
:param repo_name: full repo name, also a cache key
ScmModel().mark_for_invalidation(repo_name)
def _check_permission(self, action, user, repo_name, ip_addr=None):
Checks permissions using action (push/pull) user and repository
name
:param action: push or pull action
:param user: user instance
:param repo_name: repository name
# check IP
inherit = user.inherit_default_permissions
ip_allowed = AuthUser.check_ip_allowed(user.user_id, ip_addr,
inherit_from_default=inherit)
if ip_allowed:
log.info('Access for IP:%s allowed' % (ip_addr,))
return False
if action == 'push':
if not HasPermissionAnyMiddleware('repository.write',
'repository.admin')(user,
repo_name):
#any other action need at least read permission
if not HasPermissionAnyMiddleware('repository.read',
'repository.write',
def _get_ip_addr(self, environ):
return _get_ip_addr(environ)
def _check_ssl(self, environ):
Checks the SSL check flag and returns False if SSL is not present
and required True otherwise
#check if we have SSL required ! if not it's a bad request !
if str2bool(Ui.get_by_key('push_ssl').ui_value):
org_proto = environ.get('wsgi._org_proto', environ['wsgi.url_scheme'])
if org_proto != 'https':
log.debug('proto is %s and SSL is required BAD REQUEST !'
% org_proto)
def _check_locking_state(self, environ, action, repo, user_id):
Checks locking on this repository, if locking is enabled and lock is
present returns a tuple of make_lock, locked, locked_by.
make_lock can have 3 states None (do nothing) True, make lock
False release lock, This value is later propagated to hooks, which
do the locking. Think about this as signals passed to hooks what to do.
locked = False # defines that locked error should be thrown to user
make_lock = None
repo = Repository.get_by_repo_name(repo)
user = User.get(user_id)
# this is kind of hacky, but due to how mercurial handles client-server
# server see all operation on changeset; bookmarks, phases and
# obsolescence marker in different transaction, we don't want to check
# locking on those
obsolete_call = environ['QUERY_STRING'] in ['cmd=listkeys',]
locked_by = repo.locked
if repo and repo.enable_locking and not obsolete_call:
#check if it's already locked !, if it is compare users
user_id, _date = repo.locked
if user.user_id == user_id:
log.debug('Got push from user %s, now unlocking' % (user))
# unlock if we have push from user who locked
make_lock = False
# we're not the same user who locked, ban with 423 !
locked = True
if action == 'pull':
if repo.locked[0] and repo.locked[1]:
log.debug('Setting lock on repo %s by %s' % (repo, user))
make_lock = True
log.debug('Repository %s do not have locking enabled' % (repo))
log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s'
% (make_lock, locked, locked_by))
return make_lock, locked, locked_by
def __call__(self, environ, start_response):
start = time.time()
return self._handle_request(environ, start_response)
finally:
log = logging.getLogger('kallithea.' + self.__class__.__name__)
log.debug('Request time: %.3fs' % (time.time() - start))
meta.Session.remove()
class BaseController(WSGIController):
__before__ is called before controller methods and after __call__
c.kallithea_version = __version__
rc_config = Setting.get_app_settings()
# Visual options
c.visual = AttributeDict({})
## DB stored
c.visual.show_public_icon = str2bool(rc_config.get('show_public_icon'))
c.visual.show_private_icon = str2bool(rc_config.get('show_private_icon'))
c.visual.stylify_metatags = str2bool(rc_config.get('stylify_metatags'))
c.visual.dashboard_items = safe_int(rc_config.get('dashboard_items', 100))
c.visual.admin_grid_items = safe_int(rc_config.get('admin_grid_items', 100))
c.visual.repository_fields = str2bool(rc_config.get('repository_fields'))
c.visual.show_version = str2bool(rc_config.get('show_version'))
c.visual.use_gravatar = str2bool(rc_config.get('use_gravatar'))
c.visual.gravatar_url = rc_config.get('gravatar_url')
c.ga_code = rc_config.get('ga_code')
# TODO: replace undocumented backwards compatibility hack with db upgrade and rename ga_code
if c.ga_code and '<' not in c.ga_code:
c.ga_code = '''<script type="text/javascript">
var _gaq = _gaq || [];
_gaq.push(['_setAccount', '%s']);
_gaq.push(['_trackPageview']);
(function() {
var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
})();
</script>''' % c.ga_code
c.site_name = rc_config.get('title')
c.clone_uri_tmpl = rc_config.get('clone_uri_tmpl')
## INI stored
c.visual.allow_repo_location_change = str2bool(config.get('allow_repo_location_change', True))
c.visual.allow_custom_hooks_settings = str2bool(config.get('allow_custom_hooks_settings', True))
c.instance_id = config.get('instance_id')
c.issues_url = config.get('bugtracker', url('issues_url'))
# END CONFIG VARS
c.repo_name = get_repo_slug(request) # can be empty
c.backends = BACKENDS.keys()
c.unread_notifications = NotificationModel()\
.get_unread_cnt_for_user(c.authuser.user_id)
self.cut_off_limit = safe_int(config.get('cut_off_limit'))
c.my_pr_count = PullRequestModel().get_pullrequest_cnt_for_user(c.authuser.user_id)
self.sa = meta.Session
self.scm_model = ScmModel(self.sa)
"""Invoke the Controller"""
# WSGIController.__call__ dispatches to the Controller method
# the request is routed to. This routing information is
# available in environ['pylons.routes_dict']
self.ip_addr = _get_ip_addr(environ)
# make sure that we update permissions each time we call controller
api_key = request.GET.get('api_key')
if api_key:
# when using API_KEY we are sure user exists.
auth_user = AuthUser(api_key=api_key, ip_addr=self.ip_addr)
authenticated = False
cookie_store = CookieStoreWrapper(session.get('authuser'))
auth_user = AuthUser(user_id=cookie_store.get('user_id', None),
ip_addr=self.ip_addr)
except UserCreationError, e:
h.flash(e, 'error')
# container auth or other auth functions that create users on
# the fly can throw this exception signaling that there's issue
# with user creation, explanation should be provided in
# Exception itself
auth_user = AuthUser(ip_addr=self.ip_addr)
authenticated = cookie_store.get('is_authenticated')
if not auth_user.is_authenticated and auth_user.user_id is not None:
# user is not authenticated and not empty
auth_user.set_authenticated(authenticated)
request.user = auth_user
#set globals for auth user
self.authuser = c.authuser = auth_user
log.info('IP: %s User: %s accessed %s' % (
self.ip_addr, auth_user, safe_unicode(_get_access_path(environ)))
return WSGIController.__call__(self, environ, start_response)
class BaseRepoController(BaseController):
Base class for controllers responsible for loading all needed data for
repository loaded items are
c.db_repo_scm_instance: instance of scm repository
c.db_repo: instance of db
c.repository_followers: number of followers
c.repository_forks: number of forks
c.repository_following: weather the current user is following the current repo
super(BaseRepoController, self).__before__()
if c.repo_name: # extracted from routes
_dbr = Repository.get_by_repo_name(c.repo_name)
if not _dbr:
return
log.debug('Found repository in database %s with state `%s`'
% (safe_unicode(_dbr), safe_unicode(_dbr.repo_state)))
route = getattr(request.environ.get('routes.route'), 'name', '')
# allow to delete repos that are somehow damages in filesystem
if route in ['delete_repo']:
if _dbr.repo_state in [Repository.STATE_PENDING]:
if route in ['repo_creating_home']:
check_url = url('repo_creating_home', repo_name=c.repo_name)
return redirect(check_url)
dbr = c.db_repo = _dbr
c.db_repo_scm_instance = c.db_repo.scm_instance
if c.db_repo_scm_instance is None:
log.error('%s this repository is present in database but it '
'cannot be created as an scm instance', c.repo_name)
h.flash(h.literal(_('Repository not found in the filesystem')),
raise paste.httpexceptions.HTTPNotFound()
# some globals counter for menu
c.repository_followers = self.scm_model.get_followers(dbr)
c.repository_forks = self.scm_model.get_forks(dbr)
c.repository_pull_requests = self.scm_model.get_pull_requests(dbr)
c.repository_following = self.scm_model.is_following_repo(
c.repo_name, self.authuser.user_id)
def _get_ref_rev(repo, ref_type, ref_name):
def _get_ref_rev(repo, ref_type, ref_name, returnempty=False):
Safe way to get changeset. If error occurs show error.
return repo.scm_instance.get_ref_revision(ref_type, ref_name)
except EmptyRepositoryError as e:
if returnempty:
return repo.scm_instance.EMPTY_CHANGESET
raise webob.exc.HTTPNotFound()
except ChangesetDoesNotExistError as e:
h.flash(h.literal(_('Changeset not found')),
except RepositoryError as e:
h.flash(safe_str(e), category='error')
raise webob.exc.HTTPBadRequest()
Status change: