##// END OF EJS Templates
markup-renderer: use safe fetching of attributes to prevent from errors on malformed html.
markup-renderer: use safe fetching of attributes to prevent from errors on malformed html.

File last commit:

r1826:76aa3640 default
r1840:05beb7b6 default
Show More
changeset.py
490 lines | 18.1 KiB | text/x-python | PythonLexer
project: added all source files and assets
r1 # -*- coding: utf-8 -*-
license: updated copyright year to 2017
r1271 # Copyright (C) 2010-2017 RhodeCode GmbH
project: added all source files and assets
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/
"""
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
dan
changeset: add new diffs to changeset controller
r1139 from rhodecode.lib import diffs, codeblocks
project: added all source files and assets
r1 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
cleanup: imports cleanup.
r1796 from rhodecode.lib.utils import jsonify
security: make sure the admin of repo can only delete comments which are from the same repo....
r1818 from rhodecode.lib.utils2 import safe_unicode, safe_int
project: added all source files and assets
r1 from rhodecode.lib.vcs.backends.base import EmptyCommit
from rhodecode.lib.vcs.exceptions import (
dan
diffs: add new diffs to pull request page
r1159 RepositoryError, CommitDoesNotExistError, NodeDoesNotExistError)
project: added all source files and assets
r1 from rhodecode.model.db import ChangesetComment, ChangesetStatus
from rhodecode.model.changeset_status import ChangesetStatusModel
comments: renamed ChangesetCommentsModel to CommentsModel to reflect what it actually does....
r1323 from rhodecode.model.comment import CommentsModel
project: added all source files and assets
r1 from rhodecode.model.meta import Session
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')
dan
diffs: add comments to changeset diffs
r1143
# 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_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
project: added all source files and assets
r1 # get ranges of commit ids if preset
commit_range = commit_id_range.split('...')[:2]
dan
diffs: add comments to changeset diffs
r1143
project: added all source files and assets
r1 try:
pre_load = ['affected_files', 'author', 'branch', 'date',
'message', 'parents']
if len(commit_range) == 2:
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
pull-requests: updated versioning support....
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
project: added all source files and assets
r1 c.commit_statuses = ChangesetStatus.STATUSES
c.inline_comments = []
c.files = []
dan
diffs: add comments to changeset diffs
r1143 c.statuses = []
c.comments = []
commit-page: show unresolved TODOs on commit page below comments.
r1385 c.unresolved_comments = []
dan
diffs: add comments to changeset diffs
r1143 if len(c.commit_ranges) == 1:
commit = c.commit_ranges[0]
comments: renamed ChangesetCommentsModel to CommentsModel to reflect what it actually does....
r1323 c.comments = CommentsModel().get_comments(
dan
diffs: add comments to changeset diffs
r1143 c.rhodecode_db_repo.repo_id,
revision=commit.raw_id)
c.statuses.append(ChangesetStatusModel().get_status(
c.rhodecode_db_repo.repo_id, commit.raw_id))
# comments from PR
statuses = ChangesetStatusModel().get_statuses(
c.rhodecode_db_repo.repo_id, commit.raw_id,
with_revisions=True)
prs = set(st.pull_request for st in statuses
dan
diffs: fix bug where relevant pull request comments were not being...
r1158 if st.pull_request is not None)
dan
diffs: add comments to changeset diffs
r1143 # from associated statuses, check the pull requests, and
# show comments from them
for pr in prs:
c.comments.extend(pr.comments)
commit-page: show unresolved TODOs on commit page below comments.
r1385 c.unresolved_comments = CommentsModel()\
.get_commit_unresolved_todos(commit.raw_id)
project: added all source files and assets
r1 # Iterate over ranges (default commit view is always one commit)
for commit in c.commit_ranges:
c.changes[commit.raw_id] = []
commit2 = commit
commit1 = commit.parents[0] if commit.parents else EmptyCommit()
_diff = c.rhodecode_repo.get_diff(
commit1, commit2,
ignore_whitespace=ign_whitespace_lcl, context=context_lcl)
diff_processor = diffs.DiffProcessor(
dan
changeset: add new diffs to changeset controller
r1139 _diff, format='newdiff', diff_limit=diff_limit,
project: added all source files and assets
r1 file_limit=file_limit, show_full_diff=fulldiff)
dan
diffs: add comments to changeset diffs
r1143
project: added all source files and assets
r1 commit_changes = OrderedDict()
if method == 'show':
_parsed = diff_processor.prepare()
c.limited_diff = isinstance(_parsed, diffs.LimitedDiffContainer)
dan
changeset: add new diffs to changeset controller
r1139
_parsed = diff_processor.prepare()
def _node_getter(commit):
def get_node(fname):
try:
return commit.get_node(fname)
except NodeDoesNotExistError:
return None
return get_node
comments: renamed ChangesetCommentsModel to CommentsModel to reflect what it actually does....
r1323 inline_comments = CommentsModel().get_inline_comments(
dan
diffs: add comments to changeset diffs
r1143 c.rhodecode_db_repo.repo_id, revision=commit.raw_id)
comments: renamed ChangesetCommentsModel to CommentsModel to reflect what it actually does....
r1323 c.inline_cnt = CommentsModel().get_inline_comments_count(
inline-comments: added helper to properly count inline comments.
r1190 inline_comments)
dan
diffs: add comments to changeset diffs
r1143
dan
changeset: add new diffs to changeset controller
r1139 diffset = codeblocks.DiffSet(
dan
diffs: add repo_name as parameter of diffset - fixes bug...
r1142 repo_name=c.repo_name,
dan
changeset: add new diffs to changeset controller
r1139 source_node_getter=_node_getter(commit1),
target_node_getter=_node_getter(commit2),
dan
diffs: add comments to changeset diffs
r1143 comments=inline_comments
dan
changeset: add new diffs to changeset controller
r1139 ).render_patchset(_parsed, commit1.raw_id, commit2.raw_id)
c.changes[commit.raw_id] = diffset
project: added all source files and assets
r1 else:
# downloads/raw we only need RAW diff nothing else
diff = diff_processor.as_raw()
dan
changeset: add new diffs to changeset controller
r1139 c.changes[commit.raw_id] = [None, None, None, None, diff, None, None]
project: added all source files and assets
r1
# sort comments by how they were generated
c.comments = sorted(c.comments, key=lambda x: x.comment_id)
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)
templating: use .mako as extensions for template files.
r1282 return render('changeset/patch_changeset.mako')
project: added all source files and assets
r1 elif method == 'raw':
response.content_type = 'text/plain'
return diff
elif method == 'show':
if len(c.commit_ranges) == 1:
templating: use .mako as extensions for template files.
r1282 return render('changeset/changeset.mako')
project: added all source files and assets
r1 else:
c.ancestor = None
c.target_repo = c.rhodecode_db_repo
templating: use .mako as extensions for template files.
r1282 return render('changeset/changeset_range.mako')
project: added all source files and assets
r1
@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')
comments: add comments type into comments.
r1324 comment_type = request.POST.get('comment_type')
comments: enabled resolution for general comments, and finalized how general comment is build
r1326 resolves_comment_id = request.POST.get('resolves_comment_id', None)
comments: add comments type into comments.
r1324
project: added all source files and assets
r1 if status:
text = text or (_('Status change %(transition_icon)s %(status)s')
% {'transition_icon': '>',
'status': ChangesetStatus.get_status_lbl(status)})
comments: fixed compare view comments.
r1331 multi_commit_ids = []
for _commit_id in request.POST.get('commit_ids', '').split(','):
if _commit_id not in ['', None, EmptyCommit.raw_id]:
if _commit_id not in multi_commit_ids:
multi_commit_ids.append(_commit_id)
project: added all source files and assets
r1
commit_ids = multi_commit_ids or [commit_id]
comments: fixed compare view comments.
r1331
project: added all source files and assets
r1 comment = None
for current_id in filter(None, commit_ids):
comments: renamed ChangesetCommentsModel to CommentsModel to reflect what it actually does....
r1323 c.co = comment = CommentsModel().create(
project: added all source files and assets
r1 text=text,
repo=c.rhodecode_db_repo.repo_id,
user=c.rhodecode_user.user_id,
comments: changed signature of create method of ChangesetComment....
r1322 commit_id=current_id,
project: added all source files and assets
r1 f_path=request.POST.get('f_path'),
line_no=request.POST.get('line'),
status_change=(ChangesetStatus.get_status_lbl(status)
emails: added new tags to status sent...
r548 if status else None),
comments: add comments type into comments.
r1324 status_change_type=status,
comments: enabled resolution for general comments, and finalized how general comment is build
r1326 comment_type=comment_type,
resolves_comment_id=resolves_comment_id
project: added all source files and assets
r1 )
comments: properly show version of pull request into added comments....
r1286
project: added all source files and assets
r1 # 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':
templating: use .mako as extensions for template files.
r1282 render('changeset/changeset_comment_block.mako')})
project: added all source files and assets
r1
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):
security: make sure the admin of repo can only delete comments which are from the same repo....
r1818 comment = ChangesetComment.get_or_404(safe_int(comment_id))
comments: skip already deleted comments and make sure we don't raise unnecessary errors.
r1330 if not comment:
log.debug('Comment with id:%s not found, skipping', comment_id)
# comment already deleted in another call probably
return True
project: added all source files and assets
r1 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
security: make sure the admin of repo can only delete comments which are from the same repo....
r1818 super_admin = h.HasPermissionAny('hg.admin')()
comment_owner = (comment.author.user_id == c.rhodecode_user.user_id)
is_repo_comment = comment.repo.repo_name == c.repo_name
comment_repo_admin = is_repo_admin and is_repo_comment
if super_admin or comment_owner or comment_repo_admin:
audit-logs: implemented pull request and comment events.
r1807 CommentsModel().delete(comment=comment, user=c.rhodecode_user)
project: added all source files and assets
r1 Session().commit()
return True
else:
security: use 404 instead of 403 in case missing permissions for comment deletion....
r1826 log.warning('No permissions for user %s to delete comment_id: %s',
c.rhodecode_user, comment_id)
raise HTTPNotFound()
project: added all source files and assets
r1
@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()