# Copyright (C) 2012-2020 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 . # # 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 from pyramid.renderers import render from pyramid.response import Response from rhodecode.apps._base import RepoAppView 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 from rhodecode.lib.utils2 import safe_unicode, str2bool from rhodecode.lib.view_utils import parse_path_ref, get_commit_from_ref_name 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') if not partial: raise HTTPFound( h.route_path('repo_summary', repo_name=repo.repo_name)) raise HTTPBadRequest() except RepositoryError as e: log.exception(safe_str(e)) h.flash(h.escape(safe_str(e)), category='warning') 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')) # 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) 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 pre_load = ["author", "date", "message", "branch"] c.ancestor = None 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)) 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) 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 = 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, 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 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, path=target_path, path1=source_path, ignore_whitespace=hide_whitespace_changes, context=diff_context) diff_processor = diffs.DiffProcessor( txt_diff, format='newdiff', diff_limit=diff_limit, file_limit=file_limit, show_full_diff=c.fulldiff) _parsed = diff_processor.prepare() diffset = codeblocks.DiffSet( repo_name=source_repo.repo_name, source_node_getter=codeblocks.diffset_node_getter(source_commit), target_repo_name=self.db_repo_name, target_node_getter=codeblocks.diffset_node_getter(target_commit), ) c.diffset = self.path_filter.render_patchset_filtered( diffset, _parsed, source_ref, target_ref) 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)