diff --git a/rhodecode/apps/repository/views/repo_pull_requests.py b/rhodecode/apps/repository/views/repo_pull_requests.py --- a/rhodecode/apps/repository/views/repo_pull_requests.py +++ b/rhodecode/apps/repository/views/repo_pull_requests.py @@ -20,18 +20,23 @@ import logging +import collections +from pyramid.httpexceptions import HTTPFound, HTTPNotFound from pyramid.view import view_config from rhodecode.apps._base import RepoAppView, DataGridAppView -from rhodecode.lib import helpers as h -from rhodecode.lib import audit_logger +from rhodecode.lib import helpers as h, diffs, codeblocks from rhodecode.lib.auth import ( LoginRequired, HasRepoPermissionAnyDecorator) from rhodecode.lib.utils import PartialRenderer -from rhodecode.lib.utils2 import str2bool +from rhodecode.lib.utils2 import str2bool, safe_int, safe_str +from rhodecode.lib.vcs.backends.base import EmptyCommit +from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError, \ + RepositoryRequirementError, NodeDoesNotExistError from rhodecode.model.comment import CommentsModel -from rhodecode.model.db import PullRequest -from rhodecode.model.pull_request import PullRequestModel +from rhodecode.model.db import PullRequest, PullRequestVersion, \ + ChangesetComment, ChangesetStatus +from rhodecode.model.pull_request import PullRequestModel, MergeCheck log = logging.getLogger(__name__) @@ -39,11 +44,11 @@ log = logging.getLogger(__name__) class RepoPullRequestsView(RepoAppView, DataGridAppView): def load_default_context(self): - c = self._get_local_tmpl_context() - + c = self._get_local_tmpl_context(include_app_defaults=True) # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead c.repo_info = self.db_repo - + c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED + c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED self._register_global_c(c) return c @@ -182,3 +187,398 @@ class RepoPullRequestsView(RepoAppView, filter_type=filter_type, opened_by=opened_by, statuses=statuses) return data + + def _get_pr_version(self, pull_request_id, version=None): + pull_request_id = safe_int(pull_request_id) + at_version = None + + if version and version == 'latest': + pull_request_ver = PullRequest.get(pull_request_id) + pull_request_obj = pull_request_ver + _org_pull_request_obj = pull_request_obj + at_version = 'latest' + elif version: + pull_request_ver = PullRequestVersion.get_or_404(version) + pull_request_obj = pull_request_ver + _org_pull_request_obj = pull_request_ver.pull_request + at_version = pull_request_ver.pull_request_version_id + else: + _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404( + pull_request_id) + + pull_request_display_obj = PullRequest.get_pr_display_object( + pull_request_obj, _org_pull_request_obj) + + return _org_pull_request_obj, pull_request_obj, \ + pull_request_display_obj, at_version + + def _get_diffset(self, source_repo_name, source_repo, + source_ref_id, target_ref_id, + target_commit, source_commit, diff_limit, fulldiff, + file_limit, display_inline_comments): + + vcs_diff = PullRequestModel().get_diff( + source_repo, source_ref_id, target_ref_id) + + diff_processor = diffs.DiffProcessor( + vcs_diff, format='newdiff', diff_limit=diff_limit, + file_limit=file_limit, show_full_diff=fulldiff) + + _parsed = diff_processor.prepare() + + def _node_getter(commit): + def get_node(fname): + try: + return commit.get_node(fname) + except NodeDoesNotExistError: + return None + + return get_node + + diffset = codeblocks.DiffSet( + repo_name=self.db_repo_name, + source_repo_name=source_repo_name, + source_node_getter=_node_getter(target_commit), + target_node_getter=_node_getter(source_commit), + comments=display_inline_comments + ) + diffset = diffset.render_patchset( + _parsed, target_commit.raw_id, source_commit.raw_id) + + return diffset + + @LoginRequired() + @HasRepoPermissionAnyDecorator( + 'repository.read', 'repository.write', 'repository.admin') + # @view_config( + # route_name='pullrequest_show', request_method='GET', + # renderer='rhodecode:templates/pullrequests/pullrequest_show.mako') + def pull_request_show(self): + pull_request_id = safe_int( + self.request.matchdict.get('pull_request_id')) + c = self.load_default_context() + + 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')) + + (pull_request_latest, + pull_request_at_ver, + pull_request_display_obj, + at_version) = self._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 to browse versions + 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.at_version = at_version + c.at_version_num = (at_version + if at_version and at_version != 'latest' + else None) + c.at_version_pos = 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) = self._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 != 'latest' + else None) + c.from_version_pos = 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 + # ie. target_repo must match + if 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.pull_request_latest = pull_request_latest + + if compare or (at_version and not at_version == 'latest'): + 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 + c.forbid_author_to_review = False + c.forbid_commit_author_to_review = 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') + c.forbid_author_to_review = rules.get( + 'forbid_author_to_review') + c.forbid_commit_author_to_review = rules.get( + 'forbid_commit_author_to_review') + except Exception: + pass + + # check merge capabilities + _merge_check = MergeCheck.validate( + pull_request_latest, user=self._rhodecode_user) + 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.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) + + comments_model = CommentsModel() + + # reviewers and statuses + c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses() + allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers] + + # GENERAL COMMENTS with versions # + q = comments_model._all_general_comments_of_pull_request(pull_request_latest) + q = q.order_by(ChangesetComment.comment_id.asc()) + 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) + c.comments = c.comment_versions[c.at_version_num]['until'] + + # INLINE COMMENTS with versions # + q = comments_model._all_inline_comments_of_pull_request(pull_request_latest) + q = q.order_by(ChangesetComment.comment_id.asc()) + inline_comments = q + + c.inline_versions = comments_model.aggregate_comments( + inline_comments, versions, c.at_version_num, inline=True) + + # inject latest version + latest_ver = PullRequest.get_pr_display_object( + pull_request_latest, pull_request_latest) + + c.versions = versions + [latest_ver] + + # 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() + + # try first shadow repo, fallback to regular repo + try: + commits_source_repo = pull_request_latest.get_shadow_repo() + except Exception: + log.debug('Failed to get shadow repo', exc_info=True) + commits_source_repo = source_scm + + c.commits_source_repo = commits_source_repo + commit_cache = {} + try: + pre_load = ["author", "branch", "date", "message"] + show_revs = pull_request_at_ver.revisions + for rev in show_revs: + comm = commits_source_repo.get_commit( + commit_id=rev, pre_load=pre_load) + c.commit_ranges.append(comm) + 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)) + + 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) + c.missing_requirements = True + + c.ancestor = None # set it to None, to hide it from PR view + + try: + ancestor_id = source_scm.get_common_ancestor( + source_commit.raw_id, target_commit.raw_id, target_scm) + c.ancestor_commit = source_scm.get_commit(ancestor_id) + except Exception: + c.ancestor_commit = None + + 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.diffset = self._get_diffset( + c.source_repo.repo_name, commits_source_repo, + source_ref_id, target_ref_id, + target_commit, source_commit, + diff_limit, c.fulldiff, file_limit, display_inline_comments) + + 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) + + # 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 = [] + mark = lambda cs, fw: list( + h.itertools.izip_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 extracting still use "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 = {} + if self._rhodecode_user.user_id in allowed_reviewers: + for co in general_comments: + if co.author.user_id == self._rhodecode_user.user_id: + # each comment has a status change + 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) diff --git a/rhodecode/controllers/pullrequests.py b/rhodecode/controllers/pullrequests.py --- a/rhodecode/controllers/pullrequests.py +++ b/rhodecode/controllers/pullrequests.py @@ -282,8 +282,9 @@ class PullrequestsController(BaseRepoCon h.flash(msg, category='error') return redirect(url('pullrequest_home', repo_name=repo_name)) - return redirect(url('pullrequest_show', repo_name=target_repo, - pull_request_id=pull_request.pull_request_id)) + raise HTTPFound( + h.route_path('pullrequest_show', repo_name=target_repo, + pull_request_id=pull_request.pull_request_id)) @LoginRequired() @NotAnonymous() @@ -420,10 +421,10 @@ class PullrequestsController(BaseRepoCon scm=pull_request.target_repo.repo_type) self._merge_pull_request(pull_request, user, extras) - return redirect(url( - 'pullrequest_show', - repo_name=pull_request.target_repo.repo_name, - pull_request_id=pull_request.pull_request_id)) + 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): merge_resp = PullRequestModel().merge( @@ -964,8 +965,10 @@ class PullrequestsController(BaseRepoCon Session().commit() if not request.is_xhr: - return redirect(h.url('pullrequest_show', repo_name=repo_name, - pull_request_id=pull_request_id)) + raise HTTPFound( + h.route_path('pullrequest_show', + repo_name=repo_name, + pull_request_id=pull_request_id)) data = { 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))), diff --git a/rhodecode/lib/action_parser.py b/rhodecode/lib/action_parser.py --- a/rhodecode/lib/action_parser.py +++ b/rhodecode/lib/action_parser.py @@ -175,6 +175,7 @@ class ActionParser(object): return group_name def get_pull_request(self): + from rhodecode.lib import helpers as h pull_request_id = self.action_params if self.is_deleted(): repo_name = self.user_log.repository_name @@ -182,8 +183,8 @@ class ActionParser(object): repo_name = self.user_log.repository.repo_name return link_to( _('Pull request #%s') % pull_request_id, - url('pullrequest_show', repo_name=repo_name, - pull_request_id=pull_request_id)) + h.route_path('pullrequest_show', repo_name=repo_name, + pull_request_id=pull_request_id)) def get_archive_name(self): archive_name = self.action_params diff --git a/rhodecode/lib/base.py b/rhodecode/lib/base.py --- a/rhodecode/lib/base.py +++ b/rhodecode/lib/base.py @@ -330,6 +330,11 @@ def attach_context_attributes(context, r context.rhodecode_instanceid = config.get('instance_id') + context.visual.cut_off_limit_diff = safe_int( + config.get('cut_off_limit_diff')) + context.visual.cut_off_limit_file = safe_int( + config.get('cut_off_limit_file')) + # AppEnlight context.appenlight_enabled = str2bool(config.get('appenlight', 'false')) context.appenlight_api_public_key = config.get( diff --git a/rhodecode/model/comment.py b/rhodecode/model/comment.py --- a/rhodecode/model/comment.py +++ b/rhodecode/model/comment.py @@ -438,8 +438,7 @@ class CommentsModel(BaseModel): pull_request_id=pull_request.pull_request_id, _anchor='comment-%s' % comment.comment_id) else: - return request.route_url( - 'pullrequest_show', + return request.route_url('pullrequest_show', repo_name=safe_str(pull_request.target_repo.repo_name), pull_request_id=pull_request.pull_request_id, _anchor='comment-%s' % comment.comment_id) diff --git a/rhodecode/model/pull_request.py b/rhodecode/model/pull_request.py --- a/rhodecode/model/pull_request.py +++ b/rhodecode/model/pull_request.py @@ -997,8 +997,7 @@ class PullRequestModel(BaseModel): 'pull_requests_global', pull_request_id=pull_request.pull_request_id,) else: - return request.route_url( - 'pullrequest_show', + return request.route_url('pullrequest_show', repo_name=safe_str(pull_request.target_repo.repo_name), pull_request_id=pull_request.pull_request_id,) @@ -1027,11 +1026,9 @@ class PullRequestModel(BaseModel): pr_source_repo = pull_request_obj.source_repo pr_target_repo = pull_request_obj.target_repo - pr_url = h.url( - 'pullrequest_show', + pr_url = h.route_url('pullrequest_show', repo_name=pr_target_repo.repo_name, - pull_request_id=pull_request_obj.pull_request_id, - qualified=True,) + pull_request_id=pull_request_obj.pull_request_id,) # set some variables for email notification pr_target_repo_url = h.route_url( diff --git a/rhodecode/templates/changelog/changelog_elements.mako b/rhodecode/templates/changelog/changelog_elements.mako --- a/rhodecode/templates/changelog/changelog_elements.mako +++ b/rhodecode/templates/changelog/changelog_elements.mako @@ -23,7 +23,7 @@ %if c.statuses.get(commit.raw_id):
%if c.statuses.get(commit.raw_id)[2]: - +
%else: diff --git a/rhodecode/templates/changeset/changeset_file_comment.mako b/rhodecode/templates/changeset/changeset_file_comment.mako --- a/rhodecode/templates/changeset/changeset_file_comment.mako +++ b/rhodecode/templates/changeset/changeset_file_comment.mako @@ -61,7 +61,7 @@ % else:
% if comment.pull_request: - + % if comment.status_change: ${_('pull request #%s') % comment.pull_request.pull_request_id}: % else: @@ -122,7 +122,7 @@ % else:
- + ${'v{}'.format(pr_index_ver)} diff --git a/rhodecode/templates/data_table/_dt_elements.mako b/rhodecode/templates/data_table/_dt_elements.mako --- a/rhodecode/templates/data_table/_dt_elements.mako +++ b/rhodecode/templates/data_table/_dt_elements.mako @@ -299,7 +299,7 @@ <%def name="pullrequest_name(pull_request_id, target_repo_name, short=False)"> - + % if short: #${pull_request_id} % else: diff --git a/rhodecode/templates/summary/summary_commits.mako b/rhodecode/templates/summary/summary_commits.mako --- a/rhodecode/templates/summary/summary_commits.mako +++ b/rhodecode/templates/summary/summary_commits.mako @@ -18,7 +18,7 @@ %if c.statuses.get(cs.raw_id):