repo_compare.py
304 lines
| 12.3 KiB
| text/x-python
|
PythonLexer
r5608 | # Copyright (C) 2012-2024 RhodeCode GmbH | ||
r1957 | # | ||
# 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 | |||
from pyramid.httpexceptions import HTTPBadRequest, HTTPNotFound, HTTPFound | |||
r4610 | |||
r1957 | from pyramid.renderers import render | ||
from pyramid.response import Response | |||
from rhodecode.apps._base import RepoAppView | |||
r3346 | |||
r1957 | from rhodecode.lib import helpers as h | ||
from rhodecode.lib import diffs, codeblocks | |||
from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator | |||
from rhodecode.lib.utils import safe_str | |||
r5065 | from rhodecode.lib.utils2 import str2bool | ||
r3346 | from rhodecode.lib.view_utils import parse_path_ref, get_commit_from_ref_name | ||
r1957 | from rhodecode.lib.vcs.exceptions import ( | ||
EmptyRepositoryError, RepositoryError, RepositoryRequirementError, | |||
NodeDoesNotExistError) | |||
from rhodecode.model.db import Repository, ChangesetStatus | |||
log = logging.getLogger(__name__) | |||
class RepoCompareView(RepoAppView): | |||
def load_default_context(self): | |||
c = self._get_local_tmpl_context(include_app_defaults=True) | |||
c.rhodecode_repo = self.rhodecode_vcs_repo | |||
return c | |||
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. | |||
""" | |||
_ = self.request.translate | |||
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') | |||
r2050 | if not partial: | ||
raise HTTPFound( | |||
h.route_path('repo_summary', repo_name=repo.repo_name)) | |||
raise HTTPBadRequest() | |||
r1957 | |||
except RepositoryError as e: | |||
log.exception(safe_str(e)) | |||
r4715 | h.flash(h.escape(safe_str(e)), category='warning') | ||
r1957 | if not partial: | ||
raise HTTPFound( | |||
h.route_path('repo_summary', repo_name=repo.repo_name)) | |||
raise HTTPBadRequest() | |||
@LoginRequired() | |||
@HasRepoPermissionAnyDecorator( | |||
'repository.read', 'repository.write', 'repository.admin') | |||
def compare_select(self): | |||
_ = self.request.translate | |||
c = self.load_default_context() | |||
source_repo = self.db_repo_name | |||
target_repo = self.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) | |||
if c.source_repo is None or c.target_repo is None: | |||
raise HTTPNotFound() | |||
c.compare_home = True | |||
c.commit_ranges = [] | |||
c.collapse_all_commits = False | |||
c.diffset = None | |||
c.limited_diff = False | |||
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 | |||
c.file_path = None | |||
return self._get_template_context(c) | |||
@LoginRequired() | |||
@HasRepoPermissionAnyDecorator( | |||
'repository.read', 'repository.write', 'repository.admin') | |||
def compare(self): | |||
_ = self.request.translate | |||
c = self.load_default_context() | |||
source_ref_type = self.request.matchdict['source_ref_type'] | |||
source_ref = self.request.matchdict['source_ref'] | |||
target_ref_type = self.request.matchdict['target_ref_type'] | |||
target_ref = self.request.matchdict['target_ref'] | |||
# source_ref will be evaluated in source_repo | |||
source_repo_name = self.db_repo_name | |||
source_path, source_id = parse_path_ref(source_ref) | |||
# target_ref will be evaluated in target_repo | |||
target_repo_name = self.request.GET.get('target_repo', source_repo_name) | |||
target_path, target_id = parse_path_ref( | |||
target_ref, default_path=self.request.GET.get('f_path', '')) | |||
# 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(self.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(self.request.GET.get('fulldiff')) | |||
r3134 | # 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) | |||
r1957 | c.file_path = target_path | ||
c.commit_statuses = ChangesetStatus.STATUSES | |||
# if partial, returns just compare_commits.html (commits log) | |||
partial = self.request.is_xhr | |||
# swap url for compare_diff page | |||
c.swap_url = h.route_path( | |||
'repo_compare', | |||
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, | |||
_query=dict(merge=merge and '1' or '', f_path=target_path)) | |||
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: | |||
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') | |||
raise HTTPFound( | |||
h.route_path('repo_compare_select', repo_name=self.db_repo_name)) | |||
if target_repo is None: | |||
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') | |||
raise HTTPFound( | |||
h.route_path('repo_compare_select', repo_name=self.db_repo_name)) | |||
source_scm = source_repo.scm_instance() | |||
target_scm = target_repo.scm_instance() | |||
source_alias = source_scm.alias | |||
target_alias = target_scm.alias | |||
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') | |||
raise HTTPFound( | |||
h.route_path('repo_compare_select', repo_name=self.db_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 | |||
r3850 | pre_load = ["author", "date", "message", "branch"] | ||
r1957 | c.ancestor = None | ||
r3773 | try: | ||
c.commit_ranges = source_scm.compare( | |||
source_commit.raw_id, target_commit.raw_id, | |||
target_scm, merge, pre_load=pre_load) or [] | |||
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 Response(msg) | |||
h.flash(msg, category='error') | |||
raise HTTPFound( | |||
h.route_path('repo_compare_select', | |||
repo_name=self.db_repo_name)) | |||
r1957 | |||
c.statuses = self.db_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 | |||
if partial: # for PR ajax commits loader | |||
if not c.ancestor: | |||
return Response('') # cannot merge if there is no ancestor | |||
html = render( | |||
'rhodecode:templates/compare/compare_commits.mako', | |||
self._get_template_context(c), self.request) | |||
return Response(html) | |||
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) | |||
r3061 | log.debug('Using ancestor %s as source_ref instead of %s', | ||
c.ancestor, source_ref) | |||
r1957 | 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 = c.visual.cut_off_limit_diff | |||
file_limit = c.visual.cut_off_limit_file | |||
log.debug('calculating diff between ' | |||
'source_ref:%s and target_ref:%s for repo `%s`', | |||
source_commit, target_commit, | |||
r5065 | safe_str(source_repo.scm_instance().path)) | ||
r1957 | |||
if source_commit.repository != target_commit.repository: | |||
r5065 | |||
r1957 | 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 HTTPFound( | |||
h.route_path('repo_compare_select', | |||
repo_name=self.db_repo_name)) | |||
txt_diff = source_repo.scm_instance().get_diff( | |||
commit1=source_commit, commit2=target_commit, | |||
r3134 | path=target_path, path1=source_path, | ||
ignore_whitespace=hide_whitespace_changes, context=diff_context) | |||
r1957 | |||
r5072 | diff_processor = diffs.DiffProcessor(txt_diff, diff_format='newdiff', | ||
diff_limit=diff_limit, | |||
file_limit=file_limit, | |||
show_full_diff=c.fulldiff) | |||
r1957 | _parsed = diff_processor.prepare() | ||
diffset = codeblocks.DiffSet( | |||
repo_name=source_repo.repo_name, | |||
Bartłomiej Wołyńczyk
|
r2685 | source_node_getter=codeblocks.diffset_node_getter(source_commit), | |
r3146 | target_repo_name=self.db_repo_name, | ||
Bartłomiej Wołyńczyk
|
r2685 | target_node_getter=codeblocks.diffset_node_getter(target_commit), | |
r1957 | ) | ||
r2618 | c.diffset = self.path_filter.render_patchset_filtered( | ||
diffset, _parsed, source_ref, target_ref) | |||
r1957 | |||
c.preview_mode = merge | |||
c.source_commit = source_commit | |||
c.target_commit = target_commit | |||
html = render( | |||
'rhodecode:templates/compare/compare_diff.mako', | |||
self._get_template_context(c), self.request) | |||
return Response(html) |