##// END OF EJS Templates
fix(caching): fixed problems with Cache query for users....
fix(caching): fixed problems with Cache query for users. The old way of querying caused the user get query to be always cached, and returning old results even in 2fa forms. The new limited query doesn't cache the user object resolving issues

File last commit:

r5211:5e903185 default
r5365:ae8a165b default
Show More
repo_pull_requests.py
1878 lines | 78.1 KiB | text/x-python | PythonLexer
# Copyright (C) 2011-2023 RhodeCode GmbH
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License, version 3
# (only), as published by the Free Software Foundation.
#
# 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 Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# This program is dual-licensed. If you wish to learn more about the
# RhodeCode Enterprise Edition, including its added features, Support services,
# and proprietary license terms, please see https://rhodecode.com/licenses/
import logging
import collections
import formencode
import formencode.htmlfill
import peppercorn
from pyramid.httpexceptions import (
HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest, HTTPConflict)
from pyramid.renderers import render
from rhodecode.apps._base import RepoAppView, DataGridAppView
from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
from rhodecode.lib.base import vcs_operation_context
from rhodecode.lib.diffs import load_cached_diff, cache_diff, diff_cache_exist
from rhodecode.lib.exceptions import CommentVersionMismatch
from rhodecode.lib import ext_json
from rhodecode.lib.auth import (
LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
NotAnonymous, CSRFRequired)
from rhodecode.lib.utils2 import str2bool, safe_str, safe_int, aslist, retry
from rhodecode.lib.vcs.backends.base import (
EmptyCommit, UpdateFailureReason, unicode_to_reference)
from rhodecode.lib.vcs.exceptions import (
CommitDoesNotExistError, RepositoryRequirementError, EmptyRepositoryError)
from rhodecode.model.changeset_status import ChangesetStatusModel
from rhodecode.model.comment import CommentsModel
from rhodecode.model.db import (
func, false, or_, PullRequest, ChangesetComment, ChangesetStatus, Repository,
PullRequestReviewers)
from rhodecode.model.forms import PullRequestForm
from rhodecode.model.meta import Session
from rhodecode.model.pull_request import PullRequestModel, MergeCheck
from rhodecode.model.scm import ScmModel
log = logging.getLogger(__name__)
class RepoPullRequestsView(RepoAppView, DataGridAppView):
def load_default_context(self):
c = self._get_local_tmpl_context(include_app_defaults=True)
c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
# backward compat., we use for OLD PRs a plain renderer
c.renderer = 'plain'
return c
def _get_pull_requests_list(
self, repo_name, source, filter_type, opened_by, statuses):
draw, start, limit = self._extract_chunk(self.request)
search_q, order_by, order_dir = self._extract_ordering(self.request)
_render = self.request.get_partial_renderer(
'rhodecode:templates/data_table/_dt_elements.mako')
# pagination
if filter_type == 'awaiting_review':
pull_requests = PullRequestModel().get_awaiting_review(
repo_name,
search_q=search_q, statuses=statuses,
offset=start, length=limit, order_by=order_by, order_dir=order_dir)
pull_requests_total_count = PullRequestModel().count_awaiting_review(
repo_name,
search_q=search_q, statuses=statuses)
elif filter_type == 'awaiting_my_review':
pull_requests = PullRequestModel().get_awaiting_my_review(
repo_name, self._rhodecode_user.user_id,
search_q=search_q, statuses=statuses,
offset=start, length=limit, order_by=order_by, order_dir=order_dir)
pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
repo_name, self._rhodecode_user.user_id,
search_q=search_q, statuses=statuses)
else:
pull_requests = PullRequestModel().get_all(
repo_name, search_q=search_q, source=source, opened_by=opened_by,
statuses=statuses, offset=start, length=limit,
order_by=order_by, order_dir=order_dir)
pull_requests_total_count = PullRequestModel().count_all(
repo_name, search_q=search_q, source=source, statuses=statuses,
opened_by=opened_by)
data = []
comments_model = CommentsModel()
for pr in pull_requests:
comments_count = comments_model.get_all_comments(
self.db_repo.repo_id, pull_request=pr,
include_drafts=False, count_only=True)
review_statuses = pr.reviewers_statuses(user=self._rhodecode_db_user)
my_review_status = ChangesetStatus.STATUS_NOT_REVIEWED
if review_statuses and review_statuses[4]:
_review_obj, _user, _reasons, _mandatory, statuses = review_statuses
my_review_status = statuses[0][1].status
data.append({
'name': _render('pullrequest_name',
pr.pull_request_id, pr.pull_request_state,
pr.work_in_progress, pr.target_repo.repo_name,
short=True),
'name_raw': pr.pull_request_id,
'status': _render('pullrequest_status',
pr.calculated_review_status()),
'my_status': _render('pullrequest_status',
my_review_status),
'title': _render('pullrequest_title', pr.title, pr.description),
'pr_flow': _render('pullrequest_commit_flow', pr),
'description': h.escape(pr.description),
'updated_on': _render('pullrequest_updated_on',
h.datetime_to_time(pr.updated_on),
pr.versions_count),
'updated_on_raw': h.datetime_to_time(pr.updated_on),
'created_on': _render('pullrequest_updated_on',
h.datetime_to_time(pr.created_on)),
'created_on_raw': h.datetime_to_time(pr.created_on),
'state': pr.pull_request_state,
'author': _render('pullrequest_author',
pr.author.full_contact, ),
'author_raw': pr.author.full_name,
'comments': _render('pullrequest_comments', comments_count),
'comments_raw': comments_count,
'closed': pr.is_closed(),
})
data = ({
'draw': draw,
'data': data,
'recordsTotal': pull_requests_total_count,
'recordsFiltered': pull_requests_total_count,
})
return data
@LoginRequired()
@HasRepoPermissionAnyDecorator(
'repository.read', 'repository.write', 'repository.admin')
def pull_request_list(self):
c = self.load_default_context()
req_get = self.request.GET
c.source = str2bool(req_get.get('source'))
c.closed = str2bool(req_get.get('closed'))
c.my = str2bool(req_get.get('my'))
c.awaiting_review = str2bool(req_get.get('awaiting_review'))
c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
c.active = 'open'
if c.my:
c.active = 'my'
if c.closed:
c.active = 'closed'
if c.awaiting_review and not c.source:
c.active = 'awaiting'
if c.source and not c.awaiting_review:
c.active = 'source'
if c.awaiting_my_review:
c.active = 'awaiting_my'
return self._get_template_context(c)
@LoginRequired()
@HasRepoPermissionAnyDecorator(
'repository.read', 'repository.write', 'repository.admin')
def pull_request_list_data(self):
self.load_default_context()
# additional filters
req_get = self.request.GET
source = str2bool(req_get.get('source'))
closed = str2bool(req_get.get('closed'))
my = str2bool(req_get.get('my'))
awaiting_review = str2bool(req_get.get('awaiting_review'))
awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
filter_type = 'awaiting_review' if awaiting_review \
else 'awaiting_my_review' if awaiting_my_review \
else None
opened_by = None
if my:
opened_by = [self._rhodecode_user.user_id]
statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
if closed:
statuses = [PullRequest.STATUS_CLOSED]
data = self._get_pull_requests_list(
repo_name=self.db_repo_name, source=source,
filter_type=filter_type, opened_by=opened_by, statuses=statuses)
return data
def _is_diff_cache_enabled(self, target_repo):
caching_enabled = self._get_general_setting(
target_repo, 'rhodecode_diff_cache')
log.debug('Diff caching enabled: %s', caching_enabled)
return caching_enabled
def _get_diffset(self, source_repo_name, source_repo,
ancestor_commit,
source_ref_id, target_ref_id,
target_commit, source_commit, diff_limit, file_limit,
fulldiff, hide_whitespace_changes, diff_context, use_ancestor=True):
target_commit_final = target_commit
source_commit_final = source_commit
if use_ancestor:
# we might want to not use it for versions
target_ref_id = ancestor_commit.raw_id
target_commit_final = ancestor_commit
vcs_diff = PullRequestModel().get_diff(
source_repo, source_ref_id, target_ref_id,
hide_whitespace_changes, diff_context)
diff_processor = diffs.DiffProcessor(vcs_diff, diff_format='newdiff', diff_limit=diff_limit,
file_limit=file_limit, show_full_diff=fulldiff)
_parsed = diff_processor.prepare()
diffset = codeblocks.DiffSet(
repo_name=self.db_repo_name,
source_repo_name=source_repo_name,
source_node_getter=codeblocks.diffset_node_getter(target_commit_final),
target_node_getter=codeblocks.diffset_node_getter(source_commit_final),
)
diffset = self.path_filter.render_patchset_filtered(
diffset, _parsed, target_ref_id, source_ref_id)
return diffset
def _get_range_diffset(self, source_scm, source_repo,
commit1, commit2, diff_limit, file_limit,
fulldiff, hide_whitespace_changes, diff_context):
vcs_diff = source_scm.get_diff(
commit1, commit2,
ignore_whitespace=hide_whitespace_changes,
context=diff_context)
diff_processor = diffs.DiffProcessor(vcs_diff, diff_format='newdiff',
diff_limit=diff_limit,
file_limit=file_limit, show_full_diff=fulldiff)
_parsed = diff_processor.prepare()
diffset = codeblocks.DiffSet(
repo_name=source_repo.repo_name,
source_node_getter=codeblocks.diffset_node_getter(commit1),
target_node_getter=codeblocks.diffset_node_getter(commit2))
diffset = self.path_filter.render_patchset_filtered(
diffset, _parsed, commit1.raw_id, commit2.raw_id)
return diffset
def register_comments_vars(self, c, pull_request, versions, include_drafts=True):
comments_model = CommentsModel()
# GENERAL COMMENTS with versions #
q = comments_model._all_general_comments_of_pull_request(pull_request)
q = q.order_by(ChangesetComment.comment_id.asc())
if not include_drafts:
q = q.filter(ChangesetComment.draft == false())
general_comments = q
# pick comments we want to render at current version
c.comment_versions = comments_model.aggregate_comments(
general_comments, versions, c.at_version_num)
# INLINE COMMENTS with versions #
q = comments_model._all_inline_comments_of_pull_request(pull_request)
q = q.order_by(ChangesetComment.comment_id.asc())
if not include_drafts:
q = q.filter(ChangesetComment.draft == false())
inline_comments = q
c.inline_versions = comments_model.aggregate_comments(
inline_comments, versions, c.at_version_num, inline=True)
# Comments inline+general
if c.at_version:
c.inline_comments_flat = c.inline_versions[c.at_version_num]['display']
c.comments = c.comment_versions[c.at_version_num]['display']
else:
c.inline_comments_flat = c.inline_versions[c.at_version_num]['until']
c.comments = c.comment_versions[c.at_version_num]['until']
return general_comments, inline_comments
@LoginRequired()
@HasRepoPermissionAnyDecorator(
'repository.read', 'repository.write', 'repository.admin')
def pull_request_show(self):
_ = self.request.translate
c = self.load_default_context()
pull_request = PullRequest.get_or_404(
self.request.matchdict['pull_request_id'])
pull_request_id = pull_request.pull_request_id
c.state_progressing = pull_request.is_state_changing()
c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
_new_state = {
'created': PullRequest.STATE_CREATED,
}.get(self.request.GET.get('force_state'))
can_force_state = c.is_super_admin or HasRepoPermissionAny('repository.admin')(c.repo_name)
if can_force_state and _new_state:
with pull_request.set_state(PullRequest.STATE_UPDATING, final_state=_new_state):
h.flash(
_('Pull Request state was force changed to `{}`').format(_new_state),
category='success')
Session().commit()
raise HTTPFound(h.route_path(
'pullrequest_show', repo_name=self.db_repo_name,
pull_request_id=pull_request_id))
version = self.request.GET.get('version')
from_version = self.request.GET.get('from_version') or version
merge_checks = self.request.GET.get('merge_checks')
c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
force_refresh = str2bool(self.request.GET.get('force_refresh'))
c.range_diff_on = self.request.GET.get('range-diff') == "1"
# fetch global flags of ignore ws or context lines
diff_context = diffs.get_diff_context(self.request)
hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
(pull_request_latest,
pull_request_at_ver,
pull_request_display_obj,
at_version) = PullRequestModel().get_pr_version(
pull_request_id, version=version)
pr_closed = pull_request_latest.is_closed()
if pr_closed and (version or from_version):
# not allow browsing versions for closed PR
raise HTTPFound(h.route_path(
'pullrequest_show', repo_name=self.db_repo_name,
pull_request_id=pull_request_id))
versions = pull_request_display_obj.versions()
c.commit_versions = PullRequestModel().pr_commits_versions(versions)
# used to store per-commit range diffs
c.changes = collections.OrderedDict()
c.at_version = at_version
c.at_version_num = (at_version
if at_version and at_version != PullRequest.LATEST_VER
else None)
c.at_version_index = ChangesetComment.get_index_from_version(
c.at_version_num, versions)
(prev_pull_request_latest,
prev_pull_request_at_ver,
prev_pull_request_display_obj,
prev_at_version) = PullRequestModel().get_pr_version(
pull_request_id, version=from_version)
c.from_version = prev_at_version
c.from_version_num = (prev_at_version
if prev_at_version and prev_at_version != PullRequest.LATEST_VER
else None)
c.from_version_index = ChangesetComment.get_index_from_version(
c.from_version_num, versions)
# define if we're in COMPARE mode or VIEW at version mode
compare = at_version != prev_at_version
# pull_requests repo_name we opened it against
# i.e., target_repo must match
if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
log.warning('Mismatch between the current repo: %s, and target %s',
self.db_repo_name, pull_request_at_ver.target_repo.repo_name)
raise HTTPNotFound()
c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(pull_request_at_ver)
c.pull_request = pull_request_display_obj
c.renderer = pull_request_at_ver.description_renderer or c.renderer
c.pull_request_latest = pull_request_latest
# inject latest version
latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
c.versions = versions + [latest_ver]
if compare or (at_version and not at_version == PullRequest.LATEST_VER):
c.allowed_to_change_status = False
c.allowed_to_update = False
c.allowed_to_merge = False
c.allowed_to_delete = False
c.allowed_to_comment = False
c.allowed_to_close = False
else:
can_change_status = PullRequestModel().check_user_change_status(
pull_request_at_ver, self._rhodecode_user)
c.allowed_to_change_status = can_change_status and not pr_closed
c.allowed_to_update = PullRequestModel().check_user_update(
pull_request_latest, self._rhodecode_user) and not pr_closed
c.allowed_to_merge = PullRequestModel().check_user_merge(
pull_request_latest, self._rhodecode_user) and not pr_closed
c.allowed_to_delete = PullRequestModel().check_user_delete(
pull_request_latest, self._rhodecode_user) and not pr_closed
c.allowed_to_comment = not pr_closed
c.allowed_to_close = c.allowed_to_merge and not pr_closed
c.forbid_adding_reviewers = False
if pull_request_latest.reviewer_data and \
'rules' in pull_request_latest.reviewer_data:
rules = pull_request_latest.reviewer_data['rules'] or {}
try:
c.forbid_adding_reviewers = rules.get('forbid_adding_reviewers')
except Exception:
pass
# check merge capabilities
_merge_check = MergeCheck.validate(
pull_request_latest, auth_user=self._rhodecode_user,
translator=self.request.translate,
force_shadow_repo_refresh=force_refresh)
c.pr_merge_errors = _merge_check.error_details
c.pr_merge_possible = not _merge_check.failed
c.pr_merge_message = _merge_check.merge_msg
c.pr_merge_source_commit = _merge_check.source_commit
c.pr_merge_target_commit = _merge_check.target_commit
c.pr_merge_info = MergeCheck.get_merge_conditions(
pull_request_latest, translator=self.request.translate)
c.pull_request_review_status = _merge_check.review_status
if merge_checks:
self.request.override_renderer = \
'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
return self._get_template_context(c)
c.reviewers_count = pull_request.reviewers_count
c.observers_count = pull_request.observers_count
# reviewers and statuses
c.pull_request_default_reviewers_data_json = ext_json.str_json(pull_request.reviewer_data)
c.pull_request_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
c.pull_request_set_observers_data_json = collections.OrderedDict({'observers': []})
# reviewers
for review_obj, member, reasons, mandatory, status in pull_request_at_ver.reviewers_statuses():
member_reviewer = h.reviewer_as_json(
member, reasons=reasons, mandatory=mandatory,
role=review_obj.role,
user_group=review_obj.rule_user_group_data()
)
current_review_status = status[0][1].status if status else ChangesetStatus.STATUS_NOT_REVIEWED
member_reviewer['review_status'] = current_review_status
member_reviewer['review_status_label'] = h.commit_status_lbl(current_review_status)
member_reviewer['allowed_to_update'] = c.allowed_to_update
c.pull_request_set_reviewers_data_json['reviewers'].append(member_reviewer)
c.pull_request_set_reviewers_data_json = ext_json.str_json(c.pull_request_set_reviewers_data_json)
# observers
for observer_obj, member in pull_request_at_ver.observers():
member_observer = h.reviewer_as_json(
member, reasons=[], mandatory=False,
role=observer_obj.role,
user_group=observer_obj.rule_user_group_data()
)
member_observer['allowed_to_update'] = c.allowed_to_update
c.pull_request_set_observers_data_json['observers'].append(member_observer)
c.pull_request_set_observers_data_json = ext_json.str_json(c.pull_request_set_observers_data_json)
general_comments, inline_comments = \
self.register_comments_vars(c, pull_request_latest, versions)
# TODOs
c.unresolved_comments = CommentsModel() \
.get_pull_request_unresolved_todos(pull_request_latest)
c.resolved_comments = CommentsModel() \
.get_pull_request_resolved_todos(pull_request_latest)
# Drafts
c.draft_comments = CommentsModel().get_pull_request_drafts(
self._rhodecode_db_user.user_id,
pull_request_latest)
# if we use version, then do not show later comments
# than current version
display_inline_comments = collections.defaultdict(
lambda: collections.defaultdict(list))
for co in inline_comments:
if c.at_version_num:
# pick comments that are at least UPTO given version, so we
# don't render comments for higher version
should_render = co.pull_request_version_id and \
co.pull_request_version_id <= c.at_version_num
else:
# showing all, for 'latest'
should_render = True
if should_render:
display_inline_comments[co.f_path][co.line_no].append(co)
# load diff data into template context, if we use compare mode then
# diff is calculated based on changes between versions of PR
source_repo = pull_request_at_ver.source_repo
source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
target_repo = pull_request_at_ver.target_repo
target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
if compare:
# in compare switch the diff base to latest commit from prev version
target_ref_id = prev_pull_request_display_obj.revisions[0]
# despite opening commits for bookmarks/branches/tags, we always
# convert this to rev to prevent changes after bookmark or branch change
c.source_ref_type = 'rev'
c.source_ref = source_ref_id
c.target_ref_type = 'rev'
c.target_ref = target_ref_id
c.source_repo = source_repo
c.target_repo = target_repo
c.commit_ranges = []
source_commit = EmptyCommit()
target_commit = EmptyCommit()
c.missing_requirements = False
source_scm = source_repo.scm_instance()
target_scm = target_repo.scm_instance()
shadow_scm = None
try:
shadow_scm = pull_request_latest.get_shadow_repo()
except Exception:
log.debug('Failed to get shadow repo', exc_info=True)
# try first the existing source_repo, and then shadow
# repo if we can obtain one
commits_source_repo = source_scm
if shadow_scm:
commits_source_repo = shadow_scm
c.commits_source_repo = commits_source_repo
c.ancestor = None # set it to None, to hide it from PR view
# empty version means latest, so we keep this to prevent
# double caching
version_normalized = version or PullRequest.LATEST_VER
from_version_normalized = from_version or PullRequest.LATEST_VER
cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(target_repo)
cache_file_path = diff_cache_exist(
cache_path, 'pull_request', pull_request_id, version_normalized,
from_version_normalized, source_ref_id, target_ref_id,
hide_whitespace_changes, diff_context, c.fulldiff)
caching_enabled = self._is_diff_cache_enabled(c.target_repo)
force_recache = self.get_recache_flag()
cached_diff = None
if caching_enabled:
cached_diff = load_cached_diff(cache_file_path)
has_proper_commit_cache = (
cached_diff and cached_diff.get('commits')
and len(cached_diff.get('commits', [])) == 5
and cached_diff.get('commits')[0]
and cached_diff.get('commits')[3])
if not force_recache and not c.range_diff_on and has_proper_commit_cache:
diff_commit_cache = \
(ancestor_commit, commit_cache, missing_requirements,
source_commit, target_commit) = cached_diff['commits']
else:
# NOTE(marcink): we reach potentially unreachable errors when a PR has
# merge errors resulting in potentially hidden commits in the shadow repo.
maybe_unreachable = _merge_check.MERGE_CHECK in _merge_check.error_details \
and _merge_check.merge_response
maybe_unreachable = maybe_unreachable \
and _merge_check.merge_response.metadata.get('unresolved_files')
log.debug("Using unreachable commits due to MERGE_CHECK in merge simulation")
diff_commit_cache = \
(ancestor_commit, commit_cache, missing_requirements,
source_commit, target_commit) = self.get_commits(
commits_source_repo,
pull_request_at_ver,
source_commit,
source_ref_id,
source_scm,
target_commit,
target_ref_id,
target_scm,
maybe_unreachable=maybe_unreachable)
# register our commit range
for comm in commit_cache.values():
c.commit_ranges.append(comm)
c.missing_requirements = missing_requirements
c.ancestor_commit = ancestor_commit
c.statuses = source_repo.statuses(
[x.raw_id for x in c.commit_ranges])
# auto collapse if we have more than limit
collapse_limit = diffs.DiffProcessor._collapse_commits_over
c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
c.compare_mode = compare
# diff_limit is the old behavior, will cut off the whole diff
# if the limit is applied otherwise will just hide the
# big files from the front-end
diff_limit = c.visual.cut_off_limit_diff
file_limit = c.visual.cut_off_limit_file
c.missing_commits = False
if (c.missing_requirements
or isinstance(source_commit, EmptyCommit)
or source_commit == target_commit):
c.missing_commits = True
else:
c.inline_comments = display_inline_comments
use_ancestor = True
if from_version_normalized != version_normalized:
use_ancestor = False
has_proper_diff_cache = cached_diff and cached_diff.get('commits')
if not force_recache and has_proper_diff_cache:
c.diffset = cached_diff['diff']
else:
try:
c.diffset = self._get_diffset(
c.source_repo.repo_name, commits_source_repo,
c.ancestor_commit,
source_ref_id, target_ref_id,
target_commit, source_commit,
diff_limit, file_limit, c.fulldiff,
hide_whitespace_changes, diff_context,
use_ancestor=use_ancestor
)
# save cached diff
if caching_enabled:
cache_diff(cache_file_path, c.diffset, diff_commit_cache)
except CommitDoesNotExistError:
log.exception('Failed to generate diffset')
c.missing_commits = True
if not c.missing_commits:
c.limited_diff = c.diffset.limited_diff
# calculate removed files that are bound to comments
comment_deleted_files = [
fname for fname in display_inline_comments
if fname not in c.diffset.file_stats]
c.deleted_files_comments = collections.defaultdict(dict)
for fname, per_line_comments in display_inline_comments.items():
if fname in comment_deleted_files:
c.deleted_files_comments[fname]['stats'] = 0
c.deleted_files_comments[fname]['comments'] = list()
for lno, comments in per_line_comments.items():
c.deleted_files_comments[fname]['comments'].extend(comments)
# maybe calculate the range diff
if c.range_diff_on:
# TODO(marcink): set whitespace/context
context_lcl = 3
ign_whitespace_lcl = False
for commit in c.commit_ranges:
commit2 = commit
commit1 = commit.first_parent
range_diff_cache_file_path = diff_cache_exist(
cache_path, 'diff', commit.raw_id,
ign_whitespace_lcl, context_lcl, c.fulldiff)
cached_diff = None
if caching_enabled:
cached_diff = load_cached_diff(range_diff_cache_file_path)
has_proper_diff_cache = cached_diff and cached_diff.get('diff')
if not force_recache and has_proper_diff_cache:
diffset = cached_diff['diff']
else:
diffset = self._get_range_diffset(
commits_source_repo, source_repo,
commit1, commit2, diff_limit, file_limit,
c.fulldiff, ign_whitespace_lcl, context_lcl
)
# save cached diff
if caching_enabled:
cache_diff(range_diff_cache_file_path, diffset, None)
c.changes[commit.raw_id] = diffset
# this is a hack to properly display links, when creating PR, the
# compare view and others uses different notation, and
# compare_commits.mako renders links based on the target_repo.
# We need to swap that here to generate it properly on the html side
c.target_repo = c.source_repo
c.commit_statuses = ChangesetStatus.STATUSES
c.show_version_changes = not pr_closed
if c.show_version_changes:
cur_obj = pull_request_at_ver
prev_obj = prev_pull_request_at_ver
old_commit_ids = prev_obj.revisions
new_commit_ids = cur_obj.revisions
commit_changes = PullRequestModel()._calculate_commit_id_changes(
old_commit_ids, new_commit_ids)
c.commit_changes_summary = commit_changes
# calculate the diff for commits between versions
c.commit_changes = []
def mark(cs, fw):
return list(h.itertools.zip_longest([], cs, fillvalue=fw))
for c_type, raw_id in mark(commit_changes.added, 'a') \
+ mark(commit_changes.removed, 'r') \
+ mark(commit_changes.common, 'c'):
if raw_id in commit_cache:
commit = commit_cache[raw_id]
else:
try:
commit = commits_source_repo.get_commit(raw_id)
except CommitDoesNotExistError:
# in case we fail getting the commit, still use a dummy commit
# for display in commit diff
commit = h.AttributeDict(
{'raw_id': raw_id,
'message': 'EMPTY or MISSING COMMIT'})
c.commit_changes.append([c_type, commit])
# current user review statuses for each version
c.review_versions = {}
is_reviewer = PullRequestModel().is_user_reviewer(
pull_request, self._rhodecode_user)
if is_reviewer:
for co in general_comments:
if co.author.user_id == self._rhodecode_user.user_id:
status = co.status_change
if status:
_ver_pr = status[0].comment.pull_request_version_id
c.review_versions[_ver_pr] = status[0]
return self._get_template_context(c)
def get_commits(
self, commits_source_repo, pull_request_at_ver, source_commit,
source_ref_id, source_scm, target_commit, target_ref_id, target_scm,
maybe_unreachable=False):
commit_cache = collections.OrderedDict()
missing_requirements = False
try:
pre_load = ["author", "date", "message", "branch", "parents"]
pull_request_commits = pull_request_at_ver.revisions
log.debug('Loading %s commits from %s',
len(pull_request_commits), commits_source_repo)
for rev in pull_request_commits:
comm = commits_source_repo.get_commit(commit_id=rev, pre_load=pre_load,
maybe_unreachable=maybe_unreachable)
commit_cache[comm.raw_id] = comm
# Order here matters, we first need to get target, and then
# the source
target_commit = commits_source_repo.get_commit(
commit_id=safe_str(target_ref_id))
source_commit = commits_source_repo.get_commit(
commit_id=safe_str(source_ref_id), maybe_unreachable=True)
except CommitDoesNotExistError:
log.warning('Failed to get commit from `{}` repo'.format(
commits_source_repo), exc_info=True)
except RepositoryRequirementError:
log.warning('Failed to get all required data from repo', exc_info=True)
missing_requirements = True
pr_ancestor_id = pull_request_at_ver.common_ancestor_id
try:
ancestor_commit = source_scm.get_commit(pr_ancestor_id)
except Exception:
ancestor_commit = None
return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit
def assure_not_empty_repo(self):
_ = self.request.translate
try:
self.db_repo.scm_instance().get_commit()
except EmptyRepositoryError:
h.flash(h.literal(_('There are no commits yet')),
category='warning')
raise HTTPFound(
h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
@LoginRequired()
@NotAnonymous()
@HasRepoPermissionAnyDecorator(
'repository.read', 'repository.write', 'repository.admin')
def pull_request_new(self):
_ = self.request.translate
c = self.load_default_context()
self.assure_not_empty_repo()
source_repo = self.db_repo
commit_id = self.request.GET.get('commit')
branch_ref = self.request.GET.get('branch')
bookmark_ref = self.request.GET.get('bookmark')
try:
source_repo_data = PullRequestModel().generate_repo_data(
source_repo, commit_id=commit_id,
branch=branch_ref, bookmark=bookmark_ref,
translator=self.request.translate)
except CommitDoesNotExistError as e:
log.exception(e)
h.flash(_('Commit does not exist'), 'error')
raise HTTPFound(
h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
default_target_repo = source_repo
if source_repo.parent and c.has_origin_repo_read_perm:
parent_vcs_obj = source_repo.parent.scm_instance()
if parent_vcs_obj and not parent_vcs_obj.is_empty():
# change default if we have a parent repo
default_target_repo = source_repo.parent
target_repo_data = PullRequestModel().generate_repo_data(
default_target_repo, translator=self.request.translate)
selected_source_ref = source_repo_data['refs']['selected_ref']
title_source_ref = ''
if selected_source_ref:
title_source_ref = selected_source_ref.split(':', 2)[1]
c.default_title = PullRequestModel().generate_pullrequest_title(
source=source_repo.repo_name,
source_ref=title_source_ref,
target=default_target_repo.repo_name
)
c.default_repo_data = {
'source_repo_name': source_repo.repo_name,
'source_refs_json': ext_json.str_json(source_repo_data),
'target_repo_name': default_target_repo.repo_name,
'target_refs_json': ext_json.str_json(target_repo_data),
}
c.default_source_ref = selected_source_ref
return self._get_template_context(c)
@LoginRequired()
@NotAnonymous()
@HasRepoPermissionAnyDecorator(
'repository.read', 'repository.write', 'repository.admin')
def pull_request_repo_refs(self):
self.load_default_context()
target_repo_name = self.request.matchdict['target_repo_name']
repo = Repository.get_by_repo_name(target_repo_name)
if not repo:
raise HTTPNotFound()
target_perm = HasRepoPermissionAny(
'repository.read', 'repository.write', 'repository.admin')(
target_repo_name)
if not target_perm:
raise HTTPNotFound()
return PullRequestModel().generate_repo_data(
repo, translator=self.request.translate)
@LoginRequired()
@NotAnonymous()
@HasRepoPermissionAnyDecorator(
'repository.read', 'repository.write', 'repository.admin')
def pullrequest_repo_targets(self):
_ = self.request.translate
filter_query = self.request.GET.get('query')
# get the parents
parent_target_repos = []
if self.db_repo.parent:
parents_query = Repository.query() \
.order_by(func.length(Repository.repo_name)) \
.filter(Repository.fork_id == self.db_repo.parent.repo_id)
if filter_query:
ilike_expression = f'%{safe_str(filter_query)}%'
parents_query = parents_query.filter(
Repository.repo_name.ilike(ilike_expression))
parents = parents_query.limit(20).all()
for parent in parents:
parent_vcs_obj = parent.scm_instance()
if parent_vcs_obj and not parent_vcs_obj.is_empty():
parent_target_repos.append(parent)
# get other forks, and repo itself
query = Repository.query() \
.order_by(func.length(Repository.repo_name)) \
.filter(
or_(Repository.repo_id == self.db_repo.repo_id, # repo itself
Repository.fork_id == self.db_repo.repo_id) # forks of this repo
) \
.filter(~Repository.repo_id.in_([x.repo_id for x in parent_target_repos]))
if filter_query:
ilike_expression = f'%{safe_str(filter_query)}%'
query = query.filter(Repository.repo_name.ilike(ilike_expression))
limit = max(20 - len(parent_target_repos), 5) # not less then 5
target_repos = query.limit(limit).all()
all_target_repos = target_repos + parent_target_repos
repos = []
# This checks permissions to the repositories
for obj in ScmModel().get_repos(all_target_repos):
repos.append({
'id': obj['name'],
'text': obj['name'],
'type': 'repo',
'repo_id': obj['dbrepo']['repo_id'],
'repo_type': obj['dbrepo']['repo_type'],
'private': obj['dbrepo']['private'],
})
data = {
'more': False,
'results': [{
'text': _('Repositories'),
'children': repos
}] if repos else []
}
return data
@classmethod
def get_comment_ids(cls, post_data):
return filter(lambda e: e > 0, map(safe_int, aslist(post_data.get('comments'), ',')))
@LoginRequired()
@NotAnonymous()
@HasRepoPermissionAnyDecorator(
'repository.read', 'repository.write', 'repository.admin')
def pullrequest_comments(self):
self.load_default_context()
pull_request = PullRequest.get_or_404(
self.request.matchdict['pull_request_id'])
pull_request_id = pull_request.pull_request_id
version = self.request.GET.get('version')
_render = self.request.get_partial_renderer(
'rhodecode:templates/base/sidebar.mako')
c = _render.get_call_context()
(pull_request_latest,
pull_request_at_ver,
pull_request_display_obj,
at_version) = PullRequestModel().get_pr_version(
pull_request_id, version=version)
versions = pull_request_display_obj.versions()
latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
c.versions = versions + [latest_ver]
c.at_version = at_version
c.at_version_num = (at_version
if at_version and at_version != PullRequest.LATEST_VER
else None)
self.register_comments_vars(c, pull_request_latest, versions, include_drafts=False)
all_comments = c.inline_comments_flat + c.comments
existing_ids = self.get_comment_ids(self.request.POST)
return _render('comments_table', all_comments, len(all_comments),
existing_ids=existing_ids)
@LoginRequired()
@NotAnonymous()
@HasRepoPermissionAnyDecorator(
'repository.read', 'repository.write', 'repository.admin')
def pullrequest_todos(self):
self.load_default_context()
pull_request = PullRequest.get_or_404(
self.request.matchdict['pull_request_id'])
pull_request_id = pull_request.pull_request_id
version = self.request.GET.get('version')
_render = self.request.get_partial_renderer(
'rhodecode:templates/base/sidebar.mako')
c = _render.get_call_context()
(pull_request_latest,
pull_request_at_ver,
pull_request_display_obj,
at_version) = PullRequestModel().get_pr_version(
pull_request_id, version=version)
versions = pull_request_display_obj.versions()
latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
c.versions = versions + [latest_ver]
c.at_version = at_version
c.at_version_num = (at_version
if at_version and at_version != PullRequest.LATEST_VER
else None)
c.unresolved_comments = CommentsModel() \
.get_pull_request_unresolved_todos(pull_request, include_drafts=False)
c.resolved_comments = CommentsModel() \
.get_pull_request_resolved_todos(pull_request, include_drafts=False)
all_comments = c.unresolved_comments + c.resolved_comments
existing_ids = self.get_comment_ids(self.request.POST)
return _render('comments_table', all_comments, len(c.unresolved_comments),
todo_comments=True, existing_ids=existing_ids)
@LoginRequired()
@NotAnonymous()
@HasRepoPermissionAnyDecorator(
'repository.read', 'repository.write', 'repository.admin')
def pullrequest_drafts(self):
self.load_default_context()
pull_request = PullRequest.get_or_404(
self.request.matchdict['pull_request_id'])
pull_request_id = pull_request.pull_request_id
version = self.request.GET.get('version')
_render = self.request.get_partial_renderer(
'rhodecode:templates/base/sidebar.mako')
c = _render.get_call_context()
(pull_request_latest,
pull_request_at_ver,
pull_request_display_obj,
at_version) = PullRequestModel().get_pr_version(
pull_request_id, version=version)
versions = pull_request_display_obj.versions()
latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
c.versions = versions + [latest_ver]
c.at_version = at_version
c.at_version_num = (at_version
if at_version and at_version != PullRequest.LATEST_VER
else None)
c.draft_comments = CommentsModel() \
.get_pull_request_drafts(self._rhodecode_db_user.user_id, pull_request)
all_comments = c.draft_comments
existing_ids = self.get_comment_ids(self.request.POST)
return _render('comments_table', all_comments, len(all_comments),
existing_ids=existing_ids, draft_comments=True)
@LoginRequired()
@NotAnonymous()
@HasRepoPermissionAnyDecorator(
'repository.read', 'repository.write', 'repository.admin')
@CSRFRequired()
def pull_request_create(self):
_ = self.request.translate
self.assure_not_empty_repo()
self.load_default_context()
controls = peppercorn.parse(self.request.POST.items())
try:
form = PullRequestForm(
self.request.translate, self.db_repo.repo_id)()
_form = form.to_python(controls)
except formencode.Invalid as errors:
if errors.error_dict.get('revisions'):
msg = 'Revisions: {}'.format(errors.error_dict['revisions'])
elif errors.error_dict.get('pullrequest_title'):
msg = errors.error_dict.get('pullrequest_title')
else:
msg = _('Error creating pull request: {}').format(errors)
log.exception(msg)
h.flash(msg, 'error')
# would rather just go back to form ...
raise HTTPFound(
h.route_path('pullrequest_new', repo_name=self.db_repo_name))
source_repo = _form['source_repo']
source_ref = _form['source_ref']
target_repo = _form['target_repo']
target_ref = _form['target_ref']
commit_ids = _form['revisions'][::-1]
common_ancestor_id = _form['common_ancestor']
# find the ancestor for this pr
source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
if not (source_db_repo or target_db_repo):
h.flash(_('source_repo or target repo not found'), category='error')
raise HTTPFound(
h.route_path('pullrequest_new', repo_name=self.db_repo_name))
# re-check permissions again here
# source_repo we must have read permissions
source_perm = HasRepoPermissionAny(
'repository.read', 'repository.write', 'repository.admin')(
source_db_repo.repo_name)
if not source_perm:
msg = _('Not Enough permissions to source repo `{}`.'.format(
source_db_repo.repo_name))
h.flash(msg, category='error')
# copy the args back to redirect
org_query = self.request.GET.mixed()
raise HTTPFound(
h.route_path('pullrequest_new', repo_name=self.db_repo_name,
_query=org_query))
# target repo we must have read permissions, and also later on
# we want to check branch permissions here
target_perm = HasRepoPermissionAny(
'repository.read', 'repository.write', 'repository.admin')(
target_db_repo.repo_name)
if not target_perm:
msg = _('Not Enough permissions to target repo `{}`.'.format(
target_db_repo.repo_name))
h.flash(msg, category='error')
# copy the args back to redirect
org_query = self.request.GET.mixed()
raise HTTPFound(
h.route_path('pullrequest_new', repo_name=self.db_repo_name,
_query=org_query))
source_scm = source_db_repo.scm_instance()
target_scm = target_db_repo.scm_instance()
source_ref_obj = unicode_to_reference(source_ref)
target_ref_obj = unicode_to_reference(target_ref)
source_commit = source_scm.get_commit(source_ref_obj.commit_id)
target_commit = target_scm.get_commit(target_ref_obj.commit_id)
ancestor = source_scm.get_common_ancestor(
source_commit.raw_id, target_commit.raw_id, target_scm)
# recalculate target ref based on ancestor
target_ref = ':'.join((target_ref_obj.type, target_ref_obj.name, ancestor))
get_default_reviewers_data, validate_default_reviewers, validate_observers = \
PullRequestModel().get_reviewer_functions()
# recalculate reviewers logic, to make sure we can validate this
reviewer_rules = get_default_reviewers_data(
self._rhodecode_db_user,
source_db_repo,
source_ref_obj,
target_db_repo,
target_ref_obj,
include_diff_info=False)
reviewers = validate_default_reviewers(_form['review_members'], reviewer_rules)
observers = validate_observers(_form['observer_members'], reviewer_rules)
pullrequest_title = _form['pullrequest_title']
title_source_ref = source_ref_obj.name
if not pullrequest_title:
pullrequest_title = PullRequestModel().generate_pullrequest_title(
source=source_repo,
source_ref=title_source_ref,
target=target_repo
)
description = _form['pullrequest_desc']
description_renderer = _form['description_renderer']
try:
pull_request = PullRequestModel().create(
created_by=self._rhodecode_user.user_id,
source_repo=source_repo,
source_ref=source_ref,
target_repo=target_repo,
target_ref=target_ref,
revisions=commit_ids,
common_ancestor_id=common_ancestor_id,
reviewers=reviewers,
observers=observers,
title=pullrequest_title,
description=description,
description_renderer=description_renderer,
reviewer_data=reviewer_rules,
auth_user=self._rhodecode_user
)
Session().commit()
h.flash(_('Successfully opened new pull request'),
category='success')
except Exception:
msg = _('Error occurred during creation of this pull request.')
log.exception(msg)
h.flash(msg, category='error')
# copy the args back to redirect
org_query = self.request.GET.mixed()
raise HTTPFound(
h.route_path('pullrequest_new', repo_name=self.db_repo_name,
_query=org_query))
raise HTTPFound(
h.route_path('pullrequest_show', repo_name=target_repo,
pull_request_id=pull_request.pull_request_id))
@LoginRequired()
@NotAnonymous()
@HasRepoPermissionAnyDecorator(
'repository.read', 'repository.write', 'repository.admin')
@CSRFRequired()
def pull_request_update(self):
pull_request = PullRequest.get_or_404(
self.request.matchdict['pull_request_id'])
_ = self.request.translate
c = self.load_default_context()
redirect_url = None
# we do this check as first, because we want to know ASAP in the flow that
# pr is updating currently
is_state_changing = pull_request.is_state_changing()
if pull_request.is_closed():
log.debug('update: forbidden because pull request is closed')
msg = _('Cannot update closed pull requests.')
h.flash(msg, category='error')
return {'response': True,
'redirect_url': redirect_url}
c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
# only owner or admin can update it
allowed_to_update = PullRequestModel().check_user_update(
pull_request, self._rhodecode_user)
if allowed_to_update:
controls = peppercorn.parse(self.request.POST.items())
force_refresh = str2bool(self.request.POST.get('force_refresh', 'false'))
do_update_commits = str2bool(self.request.POST.get('update_commits', 'false'))
if 'review_members' in controls:
self._update_reviewers(
c,
pull_request, controls['review_members'],
pull_request.reviewer_data,
PullRequestReviewers.ROLE_REVIEWER)
elif 'observer_members' in controls:
self._update_reviewers(
c,
pull_request, controls['observer_members'],
pull_request.reviewer_data,
PullRequestReviewers.ROLE_OBSERVER)
elif do_update_commits:
if is_state_changing:
log.debug('commits update: forbidden because pull request is in state %s',
pull_request.pull_request_state)
msg = _('Cannot update pull requests commits in state other than `{}`. '
'Current state is: `{}`').format(
PullRequest.STATE_CREATED, pull_request.pull_request_state)
h.flash(msg, category='error')
return {'response': True,
'redirect_url': redirect_url}
self._update_commits(c, pull_request)
if force_refresh:
redirect_url = h.route_path(
'pullrequest_show', repo_name=self.db_repo_name,
pull_request_id=pull_request.pull_request_id,
_query={"force_refresh": 1})
elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
self._edit_pull_request(pull_request)
else:
log.error('Unhandled update data.')
raise HTTPBadRequest()
return {'response': True,
'redirect_url': redirect_url}
raise HTTPForbidden()
def _edit_pull_request(self, pull_request):
"""
Edit title and description
"""
_ = self.request.translate
try:
PullRequestModel().edit(
pull_request,
self.request.POST.get('title'),
self.request.POST.get('description'),
self.request.POST.get('description_renderer'),
self._rhodecode_user)
except ValueError:
msg = _('Cannot update closed pull requests.')
h.flash(msg, category='error')
return
else:
Session().commit()
msg = _('Pull request title & description updated.')
h.flash(msg, category='success')
return
def _update_commits(self, c, pull_request):
_ = self.request.translate
log.debug('pull-request: running update commits actions')
@retry(exception=Exception, n_tries=3, delay=2)
def commits_update():
return PullRequestModel().update_commits(
pull_request, self._rhodecode_db_user)
with pull_request.set_state(PullRequest.STATE_UPDATING):
resp = commits_update() # retry x3
if resp.executed:
if resp.target_changed and resp.source_changed:
changed = 'target and source repositories'
elif resp.target_changed and not resp.source_changed:
changed = 'target repository'
elif not resp.target_changed and resp.source_changed:
changed = 'source repository'
else:
changed = 'nothing'
msg = _('Pull request updated to "{source_commit_id}" with '
'{count_added} added, {count_removed} removed commits. '
'Source of changes: {change_source}.')
msg = msg.format(
source_commit_id=pull_request.source_ref_parts.commit_id,
count_added=len(resp.changes.added),
count_removed=len(resp.changes.removed),
change_source=changed)
h.flash(msg, category='success')
channelstream.pr_update_channelstream_push(
self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
else:
msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
warning_reasons = [
UpdateFailureReason.NO_CHANGE,
UpdateFailureReason.WRONG_REF_TYPE,
]
category = 'warning' if resp.reason in warning_reasons else 'error'
h.flash(msg, category=category)
def _update_reviewers(self, c, pull_request, review_members, reviewer_rules, role):
_ = self.request.translate
get_default_reviewers_data, validate_default_reviewers, validate_observers = \
PullRequestModel().get_reviewer_functions()
if role == PullRequestReviewers.ROLE_REVIEWER:
try:
reviewers = validate_default_reviewers(review_members, reviewer_rules)
except ValueError as e:
log.error(f'Reviewers Validation: {e}')
h.flash(e, category='error')
return
old_calculated_status = pull_request.calculated_review_status()
PullRequestModel().update_reviewers(
pull_request, reviewers, self._rhodecode_db_user)
Session().commit()
msg = _('Pull request reviewers updated.')
h.flash(msg, category='success')
channelstream.pr_update_channelstream_push(
self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
# trigger status changed if change in reviewers changes the status
calculated_status = pull_request.calculated_review_status()
if old_calculated_status != calculated_status:
PullRequestModel().trigger_pull_request_hook(
pull_request, self._rhodecode_user, 'review_status_change',
data={'status': calculated_status})
elif role == PullRequestReviewers.ROLE_OBSERVER:
try:
observers = validate_observers(review_members, reviewer_rules)
except ValueError as e:
log.error(f'Observers Validation: {e}')
h.flash(e, category='error')
return
PullRequestModel().update_observers(
pull_request, observers, self._rhodecode_db_user)
Session().commit()
msg = _('Pull request observers updated.')
h.flash(msg, category='success')
channelstream.pr_update_channelstream_push(
self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
@LoginRequired()
@NotAnonymous()
@HasRepoPermissionAnyDecorator(
'repository.read', 'repository.write', 'repository.admin')
@CSRFRequired()
def pull_request_merge(self):
"""
Merge will perform a server-side merge of the specified
pull request, if the pull request is approved and mergeable.
After successful merging, the pull request is automatically
closed, with a relevant comment.
"""
pull_request = PullRequest.get_or_404(
self.request.matchdict['pull_request_id'])
_ = self.request.translate
if pull_request.is_state_changing():
log.debug('show: forbidden because pull request is in state %s',
pull_request.pull_request_state)
msg = _('Cannot merge pull requests in state other than `{}`. '
'Current state is: `{}`').format(PullRequest.STATE_CREATED,
pull_request.pull_request_state)
h.flash(msg, category='error')
raise HTTPFound(
h.route_path('pullrequest_show',
repo_name=pull_request.target_repo.repo_name,
pull_request_id=pull_request.pull_request_id))
self.load_default_context()
with pull_request.set_state(PullRequest.STATE_UPDATING):
check = MergeCheck.validate(
pull_request, auth_user=self._rhodecode_user,
translator=self.request.translate)
merge_possible = not check.failed
for err_type, error_msg in check.errors:
h.flash(error_msg, category=err_type)
if merge_possible:
log.debug("Pre-conditions checked, trying to merge.")
extras = vcs_operation_context(
self.request.environ, repo_name=pull_request.target_repo.repo_name,
username=self._rhodecode_db_user.username, action='push',
scm=pull_request.target_repo.repo_type)
with pull_request.set_state(PullRequest.STATE_UPDATING):
self._merge_pull_request(
pull_request, self._rhodecode_db_user, extras)
else:
log.debug("Pre-conditions failed, NOT merging.")
raise HTTPFound(
h.route_path('pullrequest_show',
repo_name=pull_request.target_repo.repo_name,
pull_request_id=pull_request.pull_request_id))
def _merge_pull_request(self, pull_request, user, extras):
_ = self.request.translate
merge_resp = PullRequestModel().merge_repo(pull_request, user, extras=extras)
if merge_resp.executed:
log.debug("The merge was successful, closing the pull request.")
PullRequestModel().close_pull_request(
pull_request.pull_request_id, user)
Session().commit()
msg = _('Pull request was successfully merged and closed.')
h.flash(msg, category='success')
else:
log.debug(
"The merge was not successful. Merge response: %s", merge_resp)
msg = merge_resp.merge_status_message
h.flash(msg, category='error')
@LoginRequired()
@NotAnonymous()
@HasRepoPermissionAnyDecorator(
'repository.read', 'repository.write', 'repository.admin')
@CSRFRequired()
def pull_request_delete(self):
_ = self.request.translate
pull_request = PullRequest.get_or_404(
self.request.matchdict['pull_request_id'])
self.load_default_context()
pr_closed = pull_request.is_closed()
allowed_to_delete = PullRequestModel().check_user_delete(
pull_request, self._rhodecode_user) and not pr_closed
# only owner can delete it !
if allowed_to_delete:
PullRequestModel().delete(pull_request, self._rhodecode_user)
Session().commit()
h.flash(_('Successfully deleted pull request'),
category='success')
raise HTTPFound(h.route_path('pullrequest_show_all',
repo_name=self.db_repo_name))
log.warning('user %s tried to delete pull request without access',
self._rhodecode_user)
raise HTTPNotFound()
def _pull_request_comments_create(self, pull_request, comments):
_ = self.request.translate
data = {}
if not comments:
return
pull_request_id = pull_request.pull_request_id
all_drafts = len([x for x in comments if str2bool(x['is_draft'])]) == len(comments)
for entry in comments:
c = self.load_default_context()
comment_type = entry['comment_type']
text = entry['text']
status = entry['status']
is_draft = str2bool(entry['is_draft'])
resolves_comment_id = entry['resolves_comment_id']
close_pull_request = entry['close_pull_request']
f_path = entry['f_path']
line_no = entry['line']
target_elem_id = f'file-{h.safeid(h.safe_str(f_path))}'
# the logic here should work like following, if we submit close
# pr comment, use `close_pull_request_with_comment` function
# else handle regular comment logic
if close_pull_request:
# only owner or admin or person with write permissions
allowed_to_close = PullRequestModel().check_user_update(
pull_request, self._rhodecode_user)
if not allowed_to_close:
log.debug('comment: forbidden because not allowed to close '
'pull request %s', pull_request_id)
raise HTTPForbidden()
# This also triggers `review_status_change`
comment, status = PullRequestModel().close_pull_request_with_comment(
pull_request, self._rhodecode_user, self.db_repo, message=text,
auth_user=self._rhodecode_user)
Session().flush()
is_inline = comment.is_inline
PullRequestModel().trigger_pull_request_hook(
pull_request, self._rhodecode_user, 'comment',
data={'comment': comment})
else:
# regular comment case, could be inline, or one with status.
# for that one we check also permissions
# Additionally ENSURE if somehow draft is sent we're then unable to change status
allowed_to_change_status = PullRequestModel().check_user_change_status(
pull_request, self._rhodecode_user) and not is_draft
if status and allowed_to_change_status:
message = (_('Status change %(transition_icon)s %(status)s')
% {'transition_icon': '>',
'status': ChangesetStatus.get_status_lbl(status)})
text = text or message
comment = CommentsModel().create(
text=text,
repo=self.db_repo.repo_id,
user=self._rhodecode_user.user_id,
pull_request=pull_request,
f_path=f_path,
line_no=line_no,
status_change=(ChangesetStatus.get_status_lbl(status)
if status and allowed_to_change_status else None),
status_change_type=(status
if status and allowed_to_change_status else None),
comment_type=comment_type,
is_draft=is_draft,
resolves_comment_id=resolves_comment_id,
auth_user=self._rhodecode_user,
send_email=not is_draft, # skip notification for draft comments
)
is_inline = comment.is_inline
if allowed_to_change_status:
# calculate old status before we change it
old_calculated_status = pull_request.calculated_review_status()
# get status if set !
if status:
ChangesetStatusModel().set_status(
self.db_repo.repo_id,
status,
self._rhodecode_user.user_id,
comment,
pull_request=pull_request
)
Session().flush()
# this is somehow required to get access to some relationship
# loaded on comment
Session().refresh(comment)
# skip notifications for drafts
if not is_draft:
PullRequestModel().trigger_pull_request_hook(
pull_request, self._rhodecode_user, 'comment',
data={'comment': comment})
# we now calculate the status of pull request, and based on that
# calculation we set the commits status
calculated_status = pull_request.calculated_review_status()
if old_calculated_status != calculated_status:
PullRequestModel().trigger_pull_request_hook(
pull_request, self._rhodecode_user, 'review_status_change',
data={'status': calculated_status})
comment_id = comment.comment_id
data[comment_id] = {
'target_id': target_elem_id
}
Session().flush()
c.co = comment
c.at_version_num = None
c.is_new = True
rendered_comment = render(
'rhodecode:templates/changeset/changeset_comment_block.mako',
self._get_template_context(c), self.request)
data[comment_id].update(comment.get_dict())
data[comment_id].update({'rendered_text': rendered_comment})
Session().commit()
# skip channelstream for draft comments
if not all_drafts:
comment_broadcast_channel = channelstream.comment_channel(
self.db_repo_name, pull_request_obj=pull_request)
comment_data = data
posted_comment_type = 'inline' if is_inline else 'general'
if len(data) == 1:
msg = _('posted {} new {} comment').format(len(data), posted_comment_type)
else:
msg = _('posted {} new {} comments').format(len(data), posted_comment_type)
channelstream.comment_channelstream_push(
self.request, comment_broadcast_channel, self._rhodecode_user, msg,
comment_data=comment_data)
return data
@LoginRequired()
@NotAnonymous()
@HasRepoPermissionAnyDecorator(
'repository.read', 'repository.write', 'repository.admin')
@CSRFRequired()
def pull_request_comment_create(self):
_ = self.request.translate
pull_request = PullRequest.get_or_404(self.request.matchdict['pull_request_id'])
if pull_request.is_closed():
log.debug('comment: forbidden because pull request is closed')
raise HTTPForbidden()
allowed_to_comment = PullRequestModel().check_user_comment(
pull_request, self._rhodecode_user)
if not allowed_to_comment:
log.debug('comment: forbidden because pull request is from forbidden repo')
raise HTTPForbidden()
comment_data = {
'comment_type': self.request.POST.get('comment_type'),
'text': self.request.POST.get('text'),
'status': self.request.POST.get('changeset_status', None),
'is_draft': self.request.POST.get('draft'),
'resolves_comment_id': self.request.POST.get('resolves_comment_id', None),
'close_pull_request': self.request.POST.get('close_pull_request'),
'f_path': self.request.POST.get('f_path'),
'line': self.request.POST.get('line'),
}
data = self._pull_request_comments_create(pull_request, [comment_data])
return data
@LoginRequired()
@NotAnonymous()
@HasRepoPermissionAnyDecorator(
'repository.read', 'repository.write', 'repository.admin')
@CSRFRequired()
def pull_request_comment_delete(self):
pull_request = PullRequest.get_or_404(
self.request.matchdict['pull_request_id'])
comment = ChangesetComment.get_or_404(
self.request.matchdict['comment_id'])
comment_id = comment.comment_id
if comment.immutable:
# don't allow deleting comments that are immutable
raise HTTPForbidden()
if pull_request.is_closed():
log.debug('comment: forbidden because pull request is closed')
raise HTTPForbidden()
if not comment:
log.debug('Comment with id:%s not found, skipping', comment_id)
# comment already deleted in another call probably
return True
if comment.pull_request.is_closed():
# don't allow deleting comments on closed pull request
raise HTTPForbidden()
is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
super_admin = h.HasPermissionAny('hg.admin')()
comment_owner = comment.author.user_id == self._rhodecode_user.user_id
is_repo_comment = comment.repo.repo_name == self.db_repo_name
comment_repo_admin = is_repo_admin and is_repo_comment
if comment.draft and not comment_owner:
# We never allow to delete draft comments for other than owners
raise HTTPNotFound()
if super_admin or comment_owner or comment_repo_admin:
old_calculated_status = comment.pull_request.calculated_review_status()
CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
Session().commit()
calculated_status = comment.pull_request.calculated_review_status()
if old_calculated_status != calculated_status:
PullRequestModel().trigger_pull_request_hook(
comment.pull_request, self._rhodecode_user, 'review_status_change',
data={'status': calculated_status})
return True
else:
log.warning('No permissions for user %s to delete comment_id: %s',
self._rhodecode_db_user, comment_id)
raise HTTPNotFound()
@LoginRequired()
@NotAnonymous()
@HasRepoPermissionAnyDecorator(
'repository.read', 'repository.write', 'repository.admin')
@CSRFRequired()
def pull_request_comment_edit(self):
self.load_default_context()
pull_request = PullRequest.get_or_404(
self.request.matchdict['pull_request_id']
)
comment = ChangesetComment.get_or_404(
self.request.matchdict['comment_id']
)
comment_id = comment.comment_id
if comment.immutable:
# don't allow deleting comments that are immutable
raise HTTPForbidden()
if pull_request.is_closed():
log.debug('comment: forbidden because pull request is closed')
raise HTTPForbidden()
if comment.pull_request.is_closed():
# don't allow deleting comments on closed pull request
raise HTTPForbidden()
is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
super_admin = h.HasPermissionAny('hg.admin')()
comment_owner = comment.author.user_id == self._rhodecode_user.user_id
is_repo_comment = comment.repo.repo_name == self.db_repo_name
comment_repo_admin = is_repo_admin and is_repo_comment
if super_admin or comment_owner or comment_repo_admin:
text = self.request.POST.get('text')
version = self.request.POST.get('version')
if text == comment.text:
log.warning(
'Comment(PR): '
'Trying to create new version '
'with the same comment body {}'.format(
comment_id,
)
)
raise HTTPNotFound()
if version.isdigit():
version = int(version)
else:
log.warning(
'Comment(PR): Wrong version type {} {} '
'for comment {}'.format(
version,
type(version),
comment_id,
)
)
raise HTTPNotFound()
try:
comment_history = CommentsModel().edit(
comment_id=comment_id,
text=text,
auth_user=self._rhodecode_user,
version=version,
)
except CommentVersionMismatch:
raise HTTPConflict()
if not comment_history:
raise HTTPNotFound()
Session().commit()
if not comment.draft:
PullRequestModel().trigger_pull_request_hook(
pull_request, self._rhodecode_user, 'comment_edit',
data={'comment': comment})
return {
'comment_history_id': comment_history.comment_history_id,
'comment_id': comment.comment_id,
'comment_version': comment_history.version,
'comment_author_username': comment_history.author.username,
'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16, request=self.request),
'comment_created_on': h.age_component(comment_history.created_on,
time_is_local=True),
}
else:
log.warning('No permissions for user %s to edit comment_id: %s',
self._rhodecode_db_user, comment_id)
raise HTTPNotFound()