compare.py
286 lines
| 11.5 KiB
| text/x-python
|
PythonLexer
r1 | # -*- coding: utf-8 -*- | |||
r1271 | # Copyright (C) 2012-2017 RhodeCode GmbH | |||
r1 | # | |||
# 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/ | ||||
""" | ||||
Compare controller for showing differences between two commits/refs/tags etc. | ||||
""" | ||||
import logging | ||||
r1838 | from webob.exc import HTTPBadRequest, HTTPNotFound | |||
r1 | from pylons import request, tmpl_context as c, url | |||
from pylons.controllers.util import redirect | ||||
from pylons.i18n.translation import _ | ||||
from rhodecode.controllers.utils import parse_path_ref, get_commit_from_ref_name | ||||
from rhodecode.lib import helpers as h | ||||
r1030 | from rhodecode.lib import diffs, codeblocks | |||
r1 | from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator | |||
from rhodecode.lib.base import BaseRepoController, render | ||||
from rhodecode.lib.utils import safe_str | ||||
from rhodecode.lib.utils2 import safe_unicode, str2bool | ||||
from rhodecode.lib.vcs.exceptions import ( | ||||
r1030 | EmptyRepositoryError, RepositoryError, RepositoryRequirementError, | |||
NodeDoesNotExistError) | ||||
r1 | from rhodecode.model.db import Repository, ChangesetStatus | |||
log = logging.getLogger(__name__) | ||||
class CompareController(BaseRepoController): | ||||
def __before__(self): | ||||
super(CompareController, self).__before__() | ||||
def _get_commit_or_redirect( | ||||
self, ref, ref_type, repo, redirect_after=True, partial=False): | ||||
""" | ||||
This is a safe way to get a commit. If an error occurs it | ||||
redirects to a commit with a proper message. If partial is set | ||||
then it does not do redirect raise and throws an exception instead. | ||||
""" | ||||
try: | ||||
return get_commit_from_ref_name(repo, safe_str(ref), ref_type) | ||||
except EmptyRepositoryError: | ||||
if not redirect_after: | ||||
return repo.scm_instance().EMPTY_COMMIT | ||||
h.flash(h.literal(_('There are no commits yet')), | ||||
category='warning') | ||||
r1785 | redirect(h.route_path('repo_summary', repo_name=repo.repo_name)) | |||
r1 | ||||
except RepositoryError as e: | ||||
r1838 | log.exception(safe_str(e)) | |||
h.flash(safe_str(h.escape(e)), category='warning') | ||||
r1 | if not partial: | |||
r1785 | redirect(h.route_path('repo_summary', repo_name=repo.repo_name)) | |||
r1 | raise HTTPBadRequest() | |||
@LoginRequired() | ||||
@HasRepoPermissionAnyDecorator('repository.read', 'repository.write', | ||||
'repository.admin') | ||||
def index(self, repo_name): | ||||
c.compare_home = True | ||||
c.commit_ranges = [] | ||||
r1268 | c.collapse_all_commits = False | |||
r1030 | c.diffset = None | |||
r1 | c.limited_diff = False | |||
source_repo = c.rhodecode_db_repo.repo_name | ||||
target_repo = request.GET.get('target_repo', source_repo) | ||||
c.source_repo = Repository.get_by_repo_name(source_repo) | ||||
c.target_repo = Repository.get_by_repo_name(target_repo) | ||||
r1838 | ||||
if c.source_repo is None or c.target_repo is None: | ||||
raise HTTPNotFound() | ||||
r1 | c.source_ref = c.target_ref = _('Select commit') | |||
c.source_ref_type = "" | ||||
c.target_ref_type = "" | ||||
c.commit_statuses = ChangesetStatus.STATUSES | ||||
c.preview_mode = False | ||||
r1259 | c.file_path = None | |||
r1282 | return render('compare/compare_diff.mako') | |||
r1 | ||||
@LoginRequired() | ||||
@HasRepoPermissionAnyDecorator('repository.read', 'repository.write', | ||||
'repository.admin') | ||||
def compare(self, repo_name, source_ref_type, source_ref, | ||||
target_ref_type, target_ref): | ||||
# source_ref will be evaluated in source_repo | ||||
source_repo_name = c.rhodecode_db_repo.repo_name | ||||
source_path, source_id = parse_path_ref(source_ref) | ||||
# target_ref will be evaluated in target_repo | ||||
target_repo_name = request.GET.get('target_repo', source_repo_name) | ||||
r1259 | target_path, target_id = parse_path_ref( | |||
target_ref, default_path=request.GET.get('f_path', '')) | ||||
r1 | ||||
r1259 | c.file_path = target_path | |||
r1 | c.commit_statuses = ChangesetStatus.STATUSES | |||
# if merge is True | ||||
# Show what changes since the shared ancestor commit of target/source | ||||
# the source would get if it was merged with target. Only commits | ||||
# which are in target but not in source will be shown. | ||||
merge = str2bool(request.GET.get('merge')) | ||||
# if merge is False | ||||
# Show a raw diff of source/target refs even if no ancestor exists | ||||
# c.fulldiff disables cut_off_limit | ||||
c.fulldiff = str2bool(request.GET.get('fulldiff')) | ||||
# if partial, returns just compare_commits.html (commits log) | ||||
partial = request.is_xhr | ||||
# swap url for compare_diff page | ||||
c.swap_url = h.url( | ||||
'compare_url', | ||||
repo_name=target_repo_name, | ||||
source_ref_type=target_ref_type, | ||||
source_ref=target_ref, | ||||
target_repo=source_repo_name, | ||||
target_ref_type=source_ref_type, | ||||
target_ref=source_ref, | ||||
r1259 | merge=merge and '1' or '', | |||
f_path=target_path) | ||||
r1 | ||||
source_repo = Repository.get_by_repo_name(source_repo_name) | ||||
target_repo = Repository.get_by_repo_name(target_repo_name) | ||||
if source_repo is None: | ||||
r1838 | log.error('Could not find the source repo: {}' | |||
.format(source_repo_name)) | ||||
h.flash(_('Could not find the source repo: `{}`') | ||||
.format(h.escape(source_repo_name)), category='error') | ||||
r1 | return redirect(url('compare_home', repo_name=c.repo_name)) | |||
if target_repo is None: | ||||
r1838 | log.error('Could not find the target repo: {}' | |||
.format(source_repo_name)) | ||||
h.flash(_('Could not find the target repo: `{}`') | ||||
.format(h.escape(target_repo_name)), category='error') | ||||
r1 | return redirect(url('compare_home', repo_name=c.repo_name)) | |||
r1259 | source_scm = source_repo.scm_instance() | |||
target_scm = target_repo.scm_instance() | ||||
source_alias = source_scm.alias | ||||
target_alias = target_scm.alias | ||||
r1 | if source_alias != target_alias: | |||
msg = _('The comparison of two different kinds of remote repos ' | ||||
'is not available') | ||||
log.error(msg) | ||||
h.flash(msg, category='error') | ||||
return redirect(url('compare_home', repo_name=c.repo_name)) | ||||
source_commit = self._get_commit_or_redirect( | ||||
ref=source_id, ref_type=source_ref_type, repo=source_repo, | ||||
partial=partial) | ||||
target_commit = self._get_commit_or_redirect( | ||||
ref=target_id, ref_type=target_ref_type, repo=target_repo, | ||||
partial=partial) | ||||
c.compare_home = False | ||||
c.source_repo = source_repo | ||||
c.target_repo = target_repo | ||||
c.source_ref = source_ref | ||||
c.target_ref = target_ref | ||||
c.source_ref_type = source_ref_type | ||||
c.target_ref_type = target_ref_type | ||||
pre_load = ["author", "branch", "date", "message"] | ||||
c.ancestor = None | ||||
r1262 | ||||
if c.file_path: | ||||
if source_commit == target_commit: | ||||
c.commit_ranges = [] | ||||
else: | ||||
c.commit_ranges = [target_commit] | ||||
else: | ||||
try: | ||||
c.commit_ranges = source_scm.compare( | ||||
source_commit.raw_id, target_commit.raw_id, | ||||
target_scm, merge, pre_load=pre_load) | ||||
if merge: | ||||
c.ancestor = source_scm.get_common_ancestor( | ||||
source_commit.raw_id, target_commit.raw_id, target_scm) | ||||
except RepositoryRequirementError: | ||||
msg = _('Could not compare repos with different ' | ||||
'large file settings') | ||||
log.error(msg) | ||||
if partial: | ||||
return msg | ||||
h.flash(msg, category='error') | ||||
return redirect(url('compare_home', repo_name=c.repo_name)) | ||||
r1 | ||||
c.statuses = c.rhodecode_db_repo.statuses( | ||||
[x.raw_id for x in c.commit_ranges]) | ||||
r1268 | # 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 | ||||
r1259 | if partial: # for PR ajax commits loader | |||
r64 | if not c.ancestor: | |||
r1259 | return '' # cannot merge if there is no ancestor | |||
r1282 | return render('compare/compare_commits.mako') | |||
r1 | ||||
if c.ancestor: | ||||
# case we want a simple diff without incoming commits, | ||||
# previewing what will be merged. | ||||
# Make the diff on target repo (which is known to have target_ref) | ||||
log.debug('Using ancestor %s as source_ref instead of %s' | ||||
% (c.ancestor, source_ref)) | ||||
source_repo = target_repo | ||||
source_commit = target_repo.get_commit(commit_id=c.ancestor) | ||||
# diff_limit will cut off the whole diff if the limit is applied | ||||
# otherwise it will just hide the big files from the front-end | ||||
diff_limit = self.cut_off_limit_diff | ||||
file_limit = self.cut_off_limit_file | ||||
log.debug('calculating diff between ' | ||||
'source_ref:%s and target_ref:%s for repo `%s`', | ||||
source_commit, target_commit, | ||||
safe_unicode(source_repo.scm_instance().path)) | ||||
if source_commit.repository != target_commit.repository: | ||||
msg = _( | ||||
"Repositories unrelated. " | ||||
"Cannot compare commit %(commit1)s from repository %(repo1)s " | ||||
"with commit %(commit2)s from repository %(repo2)s.") % { | ||||
'commit1': h.show_id(source_commit), | ||||
'repo1': source_repo.repo_name, | ||||
'commit2': h.show_id(target_commit), | ||||
'repo2': target_repo.repo_name, | ||||
} | ||||
h.flash(msg, category='error') | ||||
raise HTTPBadRequest() | ||||
txtdiff = source_repo.scm_instance().get_diff( | ||||
commit1=source_commit, commit2=target_commit, | ||||
r1259 | path=target_path, path1=source_path) | |||
r1 | diff_processor = diffs.DiffProcessor( | |||
r1030 | txtdiff, format='newdiff', diff_limit=diff_limit, | |||
r1 | file_limit=file_limit, show_full_diff=c.fulldiff) | |||
_parsed = diff_processor.prepare() | ||||
r1030 | def _node_getter(commit): | |||
""" Returns a function that returns a node for a commit or None """ | ||||
def get_node(fname): | ||||
try: | ||||
return commit.get_node(fname) | ||||
except NodeDoesNotExistError: | ||||
return None | ||||
return get_node | ||||
r1 | ||||
r1844 | diffset = codeblocks.DiffSet( | |||
r1142 | repo_name=source_repo.repo_name, | |||
r1030 | source_node_getter=_node_getter(source_commit), | |||
target_node_getter=_node_getter(target_commit), | ||||
r1844 | ) | |||
c.diffset = diffset.render_patchset( | ||||
_parsed, source_ref, target_ref) | ||||
r1 | ||||
c.preview_mode = merge | ||||
r1259 | c.source_commit = source_commit | |||
c.target_commit = target_commit | ||||
r1 | ||||
r1282 | return render('compare/compare_diff.mako') | |||