##// END OF EJS Templates
svn-support: Use utf-8 to encode mod_dav_svn configuration before writing to disk....
svn-support: Use utf-8 to encode mod_dav_svn configuration before writing to disk. Repository or group names may contain non ASCII characters. Therfore we have to encode the configuration before writing it to the file.

File last commit:

r548:1e26c289 default
r830:7ca2d1db default
Show More
changeset.py
465 lines | 17.0 KiB | text/x-python | PythonLexer
# -*- coding: utf-8 -*-
# Copyright (C) 2010-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/
"""
commit controller for RhodeCode showing changes between commits
"""
import logging
from collections import defaultdict
from webob.exc import HTTPForbidden, HTTPBadRequest, HTTPNotFound
from pylons import tmpl_context as c, request, response
from pylons.i18n.translation import _
from pylons.controllers.util import redirect
from rhodecode.lib import auth
from rhodecode.lib import diffs
from rhodecode.lib.auth import (
LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous)
from rhodecode.lib.base import BaseRepoController, render
from rhodecode.lib.compat import OrderedDict
from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
import rhodecode.lib.helpers as h
from rhodecode.lib.utils import action_logger, jsonify
from rhodecode.lib.utils2 import safe_unicode
from rhodecode.lib.vcs.backends.base import EmptyCommit
from rhodecode.lib.vcs.exceptions import (
RepositoryError, CommitDoesNotExistError)
from rhodecode.model.db import ChangesetComment, ChangesetStatus
from rhodecode.model.changeset_status import ChangesetStatusModel
from rhodecode.model.comment import ChangesetCommentsModel
from rhodecode.model.meta import Session
from rhodecode.model.repo import RepoModel
log = logging.getLogger(__name__)
def _update_with_GET(params, GET):
for k in ['diff1', 'diff2', 'diff']:
params[k] += GET.getall(k)
def get_ignore_ws(fid, GET):
ig_ws_global = GET.get('ignorews')
ig_ws = filter(lambda k: k.startswith('WS'), GET.getall(fid))
if ig_ws:
try:
return int(ig_ws[0].split(':')[-1])
except Exception:
pass
return ig_ws_global
def _ignorews_url(GET, fileid=None):
fileid = str(fileid) if fileid else None
params = defaultdict(list)
_update_with_GET(params, GET)
label = _('Show whitespace')
tooltiplbl = _('Show whitespace for all diffs')
ig_ws = get_ignore_ws(fileid, GET)
ln_ctx = get_line_ctx(fileid, GET)
if ig_ws is None:
params['ignorews'] += [1]
label = _('Ignore whitespace')
tooltiplbl = _('Ignore whitespace for all diffs')
ctx_key = 'context'
ctx_val = ln_ctx
# if we have passed in ln_ctx pass it along to our params
if ln_ctx:
params[ctx_key] += [ctx_val]
if fileid:
params['anchor'] = 'a_' + fileid
return h.link_to(label, h.url.current(**params), title=tooltiplbl, class_='tooltip')
def get_line_ctx(fid, GET):
ln_ctx_global = GET.get('context')
if fid:
ln_ctx = filter(lambda k: k.startswith('C'), GET.getall(fid))
else:
_ln_ctx = filter(lambda k: k.startswith('C'), GET)
ln_ctx = GET.get(_ln_ctx[0]) if _ln_ctx else ln_ctx_global
if ln_ctx:
ln_ctx = [ln_ctx]
if ln_ctx:
retval = ln_ctx[0].split(':')[-1]
else:
retval = ln_ctx_global
try:
return int(retval)
except Exception:
return 3
def _context_url(GET, fileid=None):
"""
Generates a url for context lines.
:param fileid:
"""
fileid = str(fileid) if fileid else None
ig_ws = get_ignore_ws(fileid, GET)
ln_ctx = (get_line_ctx(fileid, GET) or 3) * 2
params = defaultdict(list)
_update_with_GET(params, GET)
if ln_ctx > 0:
params['context'] += [ln_ctx]
if ig_ws:
ig_ws_key = 'ignorews'
ig_ws_val = 1
params[ig_ws_key] += [ig_ws_val]
lbl = _('Increase context')
tooltiplbl = _('Increase context for all diffs')
if fileid:
params['anchor'] = 'a_' + fileid
return h.link_to(lbl, h.url.current(**params), title=tooltiplbl, class_='tooltip')
class ChangesetController(BaseRepoController):
def __before__(self):
super(ChangesetController, self).__before__()
c.affected_files_cut_off = 60
def _index(self, commit_id_range, method):
c.ignorews_url = _ignorews_url
c.context_url = _context_url
c.fulldiff = fulldiff = request.GET.get('fulldiff')
# get ranges of commit ids if preset
commit_range = commit_id_range.split('...')[:2]
enable_comments = True
try:
pre_load = ['affected_files', 'author', 'branch', 'date',
'message', 'parents']
if len(commit_range) == 2:
enable_comments = False
commits = c.rhodecode_repo.get_commits(
start_id=commit_range[0], end_id=commit_range[1],
pre_load=pre_load)
commits = list(commits)
else:
commits = [c.rhodecode_repo.get_commit(
commit_id=commit_id_range, pre_load=pre_load)]
c.commit_ranges = commits
if not c.commit_ranges:
raise RepositoryError(
'The commit range returned an empty result')
except CommitDoesNotExistError:
msg = _('No such commit exists for this repository')
h.flash(msg, category='error')
raise HTTPNotFound()
except Exception:
log.exception("General failure")
raise HTTPNotFound()
c.changes = OrderedDict()
c.lines_added = 0
c.lines_deleted = 0
c.commit_statuses = ChangesetStatus.STATUSES
c.comments = []
c.statuses = []
c.inline_comments = []
c.inline_cnt = 0
c.files = []
# Iterate over ranges (default commit view is always one commit)
for commit in c.commit_ranges:
if method == 'show':
c.statuses.extend([ChangesetStatusModel().get_status(
c.rhodecode_db_repo.repo_id, commit.raw_id)])
c.comments.extend(ChangesetCommentsModel().get_comments(
c.rhodecode_db_repo.repo_id,
revision=commit.raw_id))
# comments from PR
st = ChangesetStatusModel().get_statuses(
c.rhodecode_db_repo.repo_id, commit.raw_id,
with_revisions=True)
# from associated statuses, check the pull requests, and
# show comments from them
prs = set(x.pull_request for x in
filter(lambda x: x.pull_request is not None, st))
for pr in prs:
c.comments.extend(pr.comments)
inlines = ChangesetCommentsModel().get_inline_comments(
c.rhodecode_db_repo.repo_id, revision=commit.raw_id)
c.inline_comments.extend(inlines.iteritems())
c.changes[commit.raw_id] = []
commit2 = commit
commit1 = commit.parents[0] if commit.parents else EmptyCommit()
# fetch global flags of ignore ws or context lines
context_lcl = get_line_ctx('', request.GET)
ign_whitespace_lcl = get_ignore_ws('', request.GET)
_diff = c.rhodecode_repo.get_diff(
commit1, commit2,
ignore_whitespace=ign_whitespace_lcl, context=context_lcl)
# 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
diff_processor = diffs.DiffProcessor(
_diff, format='gitdiff', diff_limit=diff_limit,
file_limit=file_limit, show_full_diff=fulldiff)
commit_changes = OrderedDict()
if method == 'show':
_parsed = diff_processor.prepare()
c.limited_diff = isinstance(_parsed, diffs.LimitedDiffContainer)
for f in _parsed:
c.files.append(f)
st = f['stats']
c.lines_added += st['added']
c.lines_deleted += st['deleted']
fid = h.FID(commit.raw_id, f['filename'])
diff = diff_processor.as_html(enable_comments=enable_comments,
parsed_lines=[f])
commit_changes[fid] = [
commit1.raw_id, commit2.raw_id,
f['operation'], f['filename'], diff, st, f]
else:
# downloads/raw we only need RAW diff nothing else
diff = diff_processor.as_raw()
commit_changes[''] = [None, None, None, None, diff, None, None]
c.changes[commit.raw_id] = commit_changes
# sort comments by how they were generated
c.comments = sorted(c.comments, key=lambda x: x.comment_id)
# count inline comments
for __, lines in c.inline_comments:
for comments in lines.values():
c.inline_cnt += len(comments)
if len(c.commit_ranges) == 1:
c.commit = c.commit_ranges[0]
c.parent_tmpl = ''.join(
'# Parent %s\n' % x.raw_id for x in c.commit.parents)
if method == 'download':
response.content_type = 'text/plain'
response.content_disposition = (
'attachment; filename=%s.diff' % commit_id_range[:12])
return diff
elif method == 'patch':
response.content_type = 'text/plain'
c.diff = safe_unicode(diff)
return render('changeset/patch_changeset.html')
elif method == 'raw':
response.content_type = 'text/plain'
return diff
elif method == 'show':
if len(c.commit_ranges) == 1:
return render('changeset/changeset.html')
else:
c.ancestor = None
c.target_repo = c.rhodecode_db_repo
return render('changeset/changeset_range.html')
@LoginRequired()
@HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
'repository.admin')
def index(self, revision, method='show'):
return self._index(revision, method=method)
@LoginRequired()
@HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
'repository.admin')
def changeset_raw(self, revision):
return self._index(revision, method='raw')
@LoginRequired()
@HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
'repository.admin')
def changeset_patch(self, revision):
return self._index(revision, method='patch')
@LoginRequired()
@HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
'repository.admin')
def changeset_download(self, revision):
return self._index(revision, method='download')
@LoginRequired()
@NotAnonymous()
@HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
'repository.admin')
@auth.CSRFRequired()
@jsonify
def comment(self, repo_name, revision):
commit_id = revision
status = request.POST.get('changeset_status', None)
text = request.POST.get('text')
if status:
text = text or (_('Status change %(transition_icon)s %(status)s')
% {'transition_icon': '>',
'status': ChangesetStatus.get_status_lbl(status)})
multi_commit_ids = filter(
lambda s: s not in ['', None],
request.POST.get('commit_ids', '').split(','),)
commit_ids = multi_commit_ids or [commit_id]
comment = None
for current_id in filter(None, commit_ids):
c.co = comment = ChangesetCommentsModel().create(
text=text,
repo=c.rhodecode_db_repo.repo_id,
user=c.rhodecode_user.user_id,
revision=current_id,
f_path=request.POST.get('f_path'),
line_no=request.POST.get('line'),
status_change=(ChangesetStatus.get_status_lbl(status)
if status else None),
status_change_type=status
)
# get status if set !
if status:
# if latest status was from pull request and it's closed
# disallow changing status !
# dont_allow_on_closed_pull_request = True !
try:
ChangesetStatusModel().set_status(
c.rhodecode_db_repo.repo_id,
status,
c.rhodecode_user.user_id,
comment,
revision=current_id,
dont_allow_on_closed_pull_request=True
)
except StatusChangeOnClosedPullRequestError:
msg = _('Changing the status of a commit associated with '
'a closed pull request is not allowed')
log.exception(msg)
h.flash(msg, category='warning')
return redirect(h.url(
'changeset_home', repo_name=repo_name,
revision=current_id))
# finalize, commit and redirect
Session().commit()
data = {
'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
}
if comment:
data.update(comment.get_dict())
data.update({'rendered_text':
render('changeset/changeset_comment_block.html')})
return data
@LoginRequired()
@NotAnonymous()
@HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
'repository.admin')
@auth.CSRFRequired()
def preview_comment(self):
# Technically a CSRF token is not needed as no state changes with this
# call. However, as this is a POST is better to have it, so automated
# tools don't flag it as potential CSRF.
# Post is required because the payload could be bigger than the maximum
# allowed by GET.
if not request.environ.get('HTTP_X_PARTIAL_XHR'):
raise HTTPBadRequest()
text = request.POST.get('text')
renderer = request.POST.get('renderer') or 'rst'
if text:
return h.render(text, renderer=renderer, mentions=True)
return ''
@LoginRequired()
@NotAnonymous()
@HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
'repository.admin')
@auth.CSRFRequired()
@jsonify
def delete_comment(self, repo_name, comment_id):
comment = ChangesetComment.get(comment_id)
owner = (comment.author.user_id == c.rhodecode_user.user_id)
is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
if h.HasPermissionAny('hg.admin')() or is_repo_admin or owner:
ChangesetCommentsModel().delete(comment=comment)
Session().commit()
return True
else:
raise HTTPForbidden()
@LoginRequired()
@HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
'repository.admin')
@jsonify
def changeset_info(self, repo_name, revision):
if request.is_xhr:
try:
return c.rhodecode_repo.get_commit(commit_id=revision)
except CommitDoesNotExistError as e:
return EmptyCommit(message=str(e))
else:
raise HTTPBadRequest()
@LoginRequired()
@HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
'repository.admin')
@jsonify
def changeset_children(self, repo_name, revision):
if request.is_xhr:
commit = c.rhodecode_repo.get_commit(commit_id=revision)
result = {"results": commit.children}
return result
else:
raise HTTPBadRequest()
@LoginRequired()
@HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
'repository.admin')
@jsonify
def changeset_parents(self, repo_name, revision):
if request.is_xhr:
commit = c.rhodecode_repo.get_commit(commit_id=revision)
result = {"results": commit.parents}
return result
else:
raise HTTPBadRequest()