##// END OF EJS Templates
diffs: compare overhaul....
diffs: compare overhaul. - made compare and commit range pages more consistent with other commit diff pages - removed mergerly, closes #4665. Old diff2way is replaced by new diffs with side-by-side mode. - cleanup button behaviour on compare page. Added help text and generally improved UX - switched file-diffs to compare page with file filter. Part of #4000 - added collapse/expand commits buttons in compare views.

File last commit:

r1259:8e9f93ec default
r1259:8e9f93ec default
Show More
compare.py
270 lines | 10.8 KiB | text/x-python | PythonLexer
# -*- coding: utf-8 -*-
# Copyright (C) 2012-2016 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/
"""
Compare controller for showing differences between two commits/refs/tags etc.
"""
import logging
from webob.exc import HTTPBadRequest
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
from rhodecode.lib import diffs, codeblocks
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 (
EmptyRepositoryError, RepositoryError, RepositoryRequirementError,
NodeDoesNotExistError)
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')
redirect(url('summary_home', repo_name=repo.repo_name))
except RepositoryError as e:
msg = safe_str(e)
log.exception(msg)
h.flash(msg, category='warning')
if not partial:
redirect(h.url('summary_home', repo_name=repo.repo_name))
raise HTTPBadRequest()
@LoginRequired()
@HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
'repository.admin')
def index(self, repo_name):
c.compare_home = True
c.commit_ranges = []
c.diffset = None
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)
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 render('compare/compare_diff.html')
@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)
target_path, target_id = parse_path_ref(
target_ref, default_path=request.GET.get('f_path', ''))
c.file_path = target_path
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,
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:
msg = _('Could not find the original repo: %(repo)s') % {
'repo': source_repo}
log.error(msg)
h.flash(msg, category='error')
return redirect(url('compare_home', repo_name=c.repo_name))
if target_repo is None:
msg = _('Could not find the other repo: %(repo)s') % {
'repo': target_repo_name}
log.error(msg)
h.flash(msg, category='error')
return redirect(url('compare_home', repo_name=c.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')
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
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))
c.statuses = c.rhodecode_db_repo.statuses(
[x.raw_id for x in c.commit_ranges])
if partial: # for PR ajax commits loader
if not c.ancestor:
return '' # cannot merge if there is no ancestor
return render('compare/compare_commits.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 = 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,
path=target_path, path1=source_path)
diff_processor = diffs.DiffProcessor(
txtdiff, format='newdiff', diff_limit=diff_limit,
file_limit=file_limit, show_full_diff=c.fulldiff)
_parsed = diff_processor.prepare()
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
c.diffset = codeblocks.DiffSet(
repo_name=source_repo.repo_name,
source_node_getter=_node_getter(source_commit),
target_node_getter=_node_getter(target_commit),
).render_patchset(_parsed, source_ref, target_ref)
c.preview_mode = merge
c.source_commit = source_commit
c.target_commit = target_commit
return render('compare/compare_diff.html')