changeset.py
447 lines
| 16.3 KiB
| text/x-python
|
PythonLexer
r812 | # -*- coding: utf-8 -*- | |||
""" | ||||
rhodecode.controllers.changeset | ||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||||
r1203 | changeset controller for pylons showoing changes beetween | |||
r977 | revisions | |||
r1203 | ||||
r812 | :created_on: Apr 25, 2010 | |||
:author: marcink | ||||
r1824 | :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com> | |||
r812 | :license: GPLv3, see COPYING for more details. | |||
""" | ||||
r1206 | # This program is free software: you can redistribute it and/or modify | |||
# it under the terms of the GNU General Public License as published by | ||||
# the Free Software Foundation, either version 3 of the License, or | ||||
# (at your option) any later version. | ||||
r1203 | # | |||
r547 | # 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. | ||||
r1203 | # | |||
r547 | # You should have received a copy of the GNU General Public License | |||
r1206 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | |||
r812 | import logging | |||
import traceback | ||||
r1776 | from collections import defaultdict | |||
from webob.exc import HTTPForbidden | ||||
r812 | ||||
r547 | from pylons import tmpl_context as c, url, request, response | |||
from pylons.i18n.translation import _ | ||||
from pylons.controllers.util import redirect | ||||
r1670 | from pylons.decorators import jsonify | |||
r812 | ||||
r2007 | from rhodecode.lib.vcs.exceptions import RepositoryError, ChangesetError, \ | |||
r1776 | ChangesetDoesNotExistError | |||
r2007 | from rhodecode.lib.vcs.nodes import FileNode | |||
r1776 | ||||
r812 | import rhodecode.lib.helpers as h | |||
r1712 | from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator | |||
r1045 | from rhodecode.lib.base import BaseRepoController, render | |||
r2684 | from rhodecode.lib.utils import action_logger | |||
r1514 | from rhodecode.lib.compat import OrderedDict | |||
r1753 | from rhodecode.lib import diffs | |||
r2217 | from rhodecode.model.db import ChangesetComment, ChangesetStatus | |||
r1670 | from rhodecode.model.comment import ChangesetCommentsModel | |||
r2216 | from rhodecode.model.changeset_status import ChangesetStatusModel | |||
r1713 | from rhodecode.model.meta import Session | |||
r1789 | from rhodecode.lib.diffs import wrapped_diff | |||
r2368 | from rhodecode.model.repo import RepoModel | |||
r2677 | from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError | |||
r2684 | from rhodecode.lib.vcs.backends.base import EmptyChangeset | |||
r547 | ||||
log = logging.getLogger(__name__) | ||||
r1212 | ||||
r2161 | def _update_with_GET(params, GET): | |||
for k in ['diff1', 'diff2', 'diff']: | ||||
params[k] += GET.getall(k) | ||||
def anchor_url(revision, path, GET): | ||||
r1776 | fid = h.FID(revision, path) | |||
r2161 | return h.url.current(anchor=fid, **dict(GET)) | |||
r1787 | ||||
r1776 | ||||
def get_ignore_ws(fid, GET): | ||||
r2161 | ig_ws_global = GET.get('ignorews') | |||
r1787 | ig_ws = filter(lambda k: k.startswith('WS'), GET.getall(fid)) | |||
r1776 | if ig_ws: | |||
try: | ||||
return int(ig_ws[0].split(':')[-1]) | ||||
except: | ||||
pass | ||||
return ig_ws_global | ||||
r1787 | ||||
r2161 | def _ignorews_url(GET, fileid=None): | |||
fileid = str(fileid) if fileid else None | ||||
r1776 | params = defaultdict(list) | |||
r2161 | _update_with_GET(params, GET) | |||
r1776 | lbl = _('show white space') | |||
r2161 | ig_ws = get_ignore_ws(fileid, GET) | |||
ln_ctx = get_line_ctx(fileid, GET) | ||||
r1776 | # global option | |||
if fileid is None: | ||||
if ig_ws is None: | ||||
params['ignorews'] += [1] | ||||
lbl = _('ignore white space') | ||||
ctx_key = 'context' | ||||
ctx_val = ln_ctx | ||||
# per file options | ||||
else: | ||||
if ig_ws is None: | ||||
params[fileid] += ['WS:1'] | ||||
lbl = _('ignore white space') | ||||
ctx_key = fileid | ||||
ctx_val = 'C:%s' % ln_ctx | ||||
# if we have passed in ln_ctx pass it along to our params | ||||
if ln_ctx: | ||||
params[ctx_key] += [ctx_val] | ||||
r1787 | ||||
r1776 | params['anchor'] = fileid | |||
|
r2073 | img = h.image(h.url('/images/icons/text_strikethrough.png'), lbl, class_='icon') | ||
r1902 | return h.link_to(img, h.url.current(**params), title=lbl, class_='tooltip') | |||
r1776 | ||||
r1787 | ||||
r1776 | def get_line_ctx(fid, GET): | |||
r2161 | ln_ctx_global = GET.get('context') | |||
r1787 | ln_ctx = filter(lambda k: k.startswith('C'), GET.getall(fid)) | |||
r1776 | if ln_ctx: | |||
retval = ln_ctx[0].split(':')[-1] | ||||
else: | ||||
retval = ln_ctx_global | ||||
try: | ||||
return int(retval) | ||||
except: | ||||
return | ||||
r1787 | ||||
r2161 | def _context_url(GET, fileid=None): | |||
r1776 | """ | |||
Generates url for context lines | ||||
r1787 | ||||
r1776 | :param fileid: | |||
""" | ||||
r2161 | ||||
fileid = str(fileid) if fileid else None | ||||
ig_ws = get_ignore_ws(fileid, GET) | ||||
ln_ctx = (get_line_ctx(fileid, GET) or 3) * 2 | ||||
r1776 | ||||
params = defaultdict(list) | ||||
r2161 | _update_with_GET(params, GET) | |||
r1776 | ||||
# global option | ||||
if fileid is None: | ||||
if ln_ctx > 0: | ||||
params['context'] += [ln_ctx] | ||||
if ig_ws: | ||||
ig_ws_key = 'ignorews' | ||||
ig_ws_val = 1 | ||||
# per file option | ||||
else: | ||||
params[fileid] += ['C:%s' % ln_ctx] | ||||
ig_ws_key = fileid | ||||
ig_ws_val = 'WS:%s' % 1 | ||||
r1787 | ||||
r1776 | if ig_ws: | |||
params[ig_ws_key] += [ig_ws_val] | ||||
lbl = _('%s line context') % ln_ctx | ||||
params['anchor'] = fileid | ||||
|
r2073 | img = h.image(h.url('/images/icons/table_add.png'), lbl, class_='icon') | ||
r1902 | return h.link_to(img, h.url.current(**params), title=lbl, class_='tooltip') | |||
r1776 | ||||
r1787 | ||||
r1045 | class ChangesetController(BaseRepoController): | |||
r636 | ||||
r547 | @LoginRequired() | |||
@HasRepoPermissionAnyDecorator('repository.read', 'repository.write', | ||||
r636 | 'repository.admin') | |||
r547 | def __before__(self): | |||
super(ChangesetController, self).__before__() | ||||
r1130 | c.affected_files_cut_off = 60 | |||
r2368 | repo_model = RepoModel() | |||
c.users_array = repo_model.get_users_js() | ||||
c.users_groups_array = repo_model.get_users_groups_js() | ||||
r636 | ||||
r547 | def index(self, revision): | |||
r636 | ||||
r1776 | c.anchor_url = anchor_url | |||
c.ignorews_url = _ignorews_url | ||||
c.context_url = _context_url | ||||
r2161 | limit_off = request.GET.get('fulldiff') | |||
r977 | #get ranges of revisions if preset | |||
rev_range = revision.split('...')[:2] | ||||
r1787 | enable_comments = True | |||
r547 | try: | |||
r977 | if len(rev_range) == 2: | |||
r1787 | enable_comments = False | |||
r977 | rev_start = rev_range[0] | |||
rev_end = rev_range[1] | ||||
r1107 | rev_ranges = c.rhodecode_repo.get_changesets(start=rev_start, | |||
end=rev_end) | ||||
r977 | else: | |||
r1108 | rev_ranges = [c.rhodecode_repo.get_changeset(revision)] | |||
r983 | ||||
c.cs_ranges = list(rev_ranges) | ||||
r1656 | if not c.cs_ranges: | |||
raise RepositoryError('Changeset range returned empty result') | ||||
r983 | ||||
r978 | except (RepositoryError, ChangesetDoesNotExistError, Exception), e: | |||
r547 | log.error(traceback.format_exc()) | |||
r644 | h.flash(str(e), category='warning') | |||
r636 | return redirect(url('home')) | |||
r977 | ||||
c.changes = OrderedDict() | ||||
r1789 | ||||
c.lines_added = 0 # count of lines added | ||||
c.lines_deleted = 0 # count of lines removes | ||||
cumulative_diff = 0 | ||||
r1280 | c.cut_off = False # defines if cut off limit is reached | |||
r2217 | c.changeset_statuses = ChangesetStatus.STATUSES | |||
r1670 | c.comments = [] | |||
r2215 | c.statuses = [] | |||
r1677 | c.inline_comments = [] | |||
c.inline_cnt = 0 | ||||
r1274 | # Iterate over ranges (default changeset view is always one changeset) | |||
r977 | for changeset in c.cs_ranges: | |||
r2215 | ||||
r2217 | c.statuses.extend([ChangesetStatusModel()\ | |||
r2215 | .get_status(c.rhodecode_db_repo.repo_id, | |||
r2217 | changeset.raw_id)]) | |||
r2215 | ||||
r1675 | c.comments.extend(ChangesetCommentsModel()\ | |||
.get_comments(c.rhodecode_db_repo.repo_id, | ||||
r2439 | revision=changeset.raw_id)) | |||
r1677 | inlines = ChangesetCommentsModel()\ | |||
.get_inline_comments(c.rhodecode_db_repo.repo_id, | ||||
r2439 | revision=changeset.raw_id) | |||
r1677 | c.inline_comments.extend(inlines) | |||
r977 | c.changes[changeset.raw_id] = [] | |||
try: | ||||
changeset_parent = changeset.parents[0] | ||||
except IndexError: | ||||
changeset_parent = None | ||||
#================================================================== | ||||
r547 | # ADDED FILES | |||
r977 | #================================================================== | |||
for node in changeset.added: | ||||
r1789 | fid = h.FID(revision, node.path) | |||
line_context_lcl = get_line_ctx(fid, request.GET) | ||||
ign_whitespace_lcl = get_ignore_ws(fid, request.GET) | ||||
lim = self.cut_off_limit | ||||
if cumulative_diff > self.cut_off_limit: | ||||
r2161 | lim = -1 if limit_off is None else None | |||
r2083 | size, cs1, cs2, diff, st = wrapped_diff( | |||
filenode_old=None, | ||||
filenode_new=node, | ||||
cut_off_limit=lim, | ||||
ignore_whitespace=ign_whitespace_lcl, | ||||
line_context=line_context_lcl, | ||||
enable_comments=enable_comments | ||||
) | ||||
r1789 | cumulative_diff += size | |||
r1257 | c.lines_added += st[0] | |||
c.lines_deleted += st[1] | ||||
r2083 | c.changes[changeset.raw_id].append( | |||
('added', node, diff, cs1, cs2, st) | ||||
) | ||||
r636 | ||||
r977 | #================================================================== | |||
r547 | # CHANGED FILES | |||
r977 | #================================================================== | |||
r1789 | for node in changeset.changed: | |||
try: | ||||
filenode_old = changeset_parent.get_node(node.path) | ||||
except ChangesetError: | ||||
log.warning('Unable to fetch parent node for diff') | ||||
filenode_old = FileNode(node.path, '', EmptyChangeset()) | ||||
r636 | ||||
r1789 | fid = h.FID(revision, node.path) | |||
line_context_lcl = get_line_ctx(fid, request.GET) | ||||
ign_whitespace_lcl = get_ignore_ws(fid, request.GET) | ||||
lim = self.cut_off_limit | ||||
if cumulative_diff > self.cut_off_limit: | ||||
r2161 | lim = -1 if limit_off is None else None | |||
r2083 | size, cs1, cs2, diff, st = wrapped_diff( | |||
filenode_old=filenode_old, | ||||
filenode_new=node, | ||||
cut_off_limit=lim, | ||||
ignore_whitespace=ign_whitespace_lcl, | ||||
line_context=line_context_lcl, | ||||
enable_comments=enable_comments | ||||
) | ||||
r1789 | cumulative_diff += size | |||
c.lines_added += st[0] | ||||
c.lines_deleted += st[1] | ||||
r2083 | c.changes[changeset.raw_id].append( | |||
('changed', node, diff, cs1, cs2, st) | ||||
) | ||||
r977 | #================================================================== | |||
r1203 | # REMOVED FILES | |||
r977 | #================================================================== | |||
r1789 | for node in changeset.removed: | |||
r2083 | c.changes[changeset.raw_id].append( | |||
('removed', node, None, None, None, (0, 0)) | ||||
) | ||||
r636 | ||||
r1677 | # count inline comments | |||
r2440 | for __, lines in c.inline_comments: | |||
r1677 | for comments in lines.values(): | |||
c.inline_cnt += len(comments) | ||||
r977 | if len(c.cs_ranges) == 1: | |||
c.changeset = c.cs_ranges[0] | ||||
c.changes = c.changes[c.changeset.raw_id] | ||||
return render('changeset/changeset.html') | ||||
else: | ||||
return render('changeset/changeset_range.html') | ||||
r547 | ||||
def raw_changeset(self, revision): | ||||
r636 | ||||
r547 | method = request.GET.get('diff', 'show') | |||
r1752 | ignore_whitespace = request.GET.get('ignorews') == '1' | |||
r1768 | line_context = request.GET.get('context', 3) | |||
r547 | try: | |||
r1045 | c.scm_type = c.rhodecode_repo.alias | |||
c.changeset = c.rhodecode_repo.get_changeset(revision) | ||||
r547 | except RepositoryError: | |||
log.error(traceback.format_exc()) | ||||
r636 | return redirect(url('home')) | |||
r547 | else: | |||
try: | ||||
r977 | c.changeset_parent = c.changeset.parents[0] | |||
r547 | except IndexError: | |||
r977 | c.changeset_parent = None | |||
r547 | c.changes = [] | |||
r636 | ||||
r547 | for node in c.changeset.added: | |||
filenode_old = FileNode(node.path, '') | ||||
if filenode_old.is_binary or node.is_binary: | ||||
r812 | diff = _('binary file') + '\n' | |||
r636 | else: | |||
r1753 | f_gitdiff = diffs.get_gitdiff(filenode_old, node, | |||
r1768 | ignore_whitespace=ignore_whitespace, | |||
context=line_context) | ||||
r1753 | diff = diffs.DiffProcessor(f_gitdiff, | |||
r1212 | format='gitdiff').raw_diff() | |||
r547 | ||||
cs1 = None | ||||
r2084 | cs2 = node.changeset.raw_id | |||
r547 | c.changes.append(('added', node, diff, cs1, cs2)) | |||
r636 | ||||
r547 | for node in c.changeset.changed: | |||
r977 | filenode_old = c.changeset_parent.get_node(node.path) | |||
r547 | if filenode_old.is_binary or node.is_binary: | |||
diff = _('binary file') | ||||
r636 | else: | |||
r1753 | f_gitdiff = diffs.get_gitdiff(filenode_old, node, | |||
r1768 | ignore_whitespace=ignore_whitespace, | |||
context=line_context) | ||||
r1753 | diff = diffs.DiffProcessor(f_gitdiff, | |||
r1212 | format='gitdiff').raw_diff() | |||
r547 | ||||
r2084 | cs1 = filenode_old.changeset.raw_id | |||
cs2 = node.changeset.raw_id | ||||
r636 | c.changes.append(('changed', node, diff, cs1, cs2)) | |||
r547 | response.content_type = 'text/plain' | |||
r812 | ||||
r547 | if method == 'download': | |||
r1212 | response.content_disposition = 'attachment; filename=%s.patch' \ | |||
% revision | ||||
r812 | ||||
r2083 | c.parent_tmpl = ''.join(['# Parent %s\n' % x.raw_id | |||
for x in c.changeset.parents]) | ||||
r636 | ||||
r547 | c.diffs = '' | |||
for x in c.changes: | ||||
c.diffs += x[2] | ||||
r812 | ||||
r547 | return render('changeset/raw_changeset.html') | |||
r1670 | ||||
r2187 | @jsonify | |||
r1670 | def comment(self, repo_name, revision): | |||
r2296 | status = request.POST.get('changeset_status') | |||
change_status = request.POST.get('change_changeset_status') | ||||
r2796 | text = request.POST.get('text') | |||
if status and change_status: | ||||
text = text or (_('Status change -> %s') | ||||
% ChangesetStatus.get_status_lbl(status)) | ||||
r2296 | ||||
r2187 | comm = ChangesetCommentsModel().create( | |||
r2796 | text=text, | |||
r2541 | repo=c.rhodecode_db_repo.repo_id, | |||
user=c.rhodecode_user.user_id, | ||||
r2187 | revision=revision, | |||
f_path=request.POST.get('f_path'), | ||||
r2296 | line_no=request.POST.get('line'), | |||
r2478 | status_change=(ChangesetStatus.get_status_lbl(status) | |||
r2296 | if status and change_status else None) | |||
r2187 | ) | |||
r2217 | ||||
# get status if set ! | ||||
r2296 | if status and change_status: | |||
r2677 | # 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, | ||||
comm, | ||||
revision=revision, | ||||
dont_allow_on_closed_pull_request=True | ||||
) | ||||
except StatusChangeOnClosedPullRequestError: | ||||
log.error(traceback.format_exc()) | ||||
msg = _('Changing status on a changeset associated with' | ||||
'a closed pull request is not allowed') | ||||
h.flash(msg, category='warning') | ||||
return redirect(h.url('changeset_home', repo_name=repo_name, | ||||
revision=revision)) | ||||
r2375 | action_logger(self.rhodecode_user, | |||
'user_commented_revision:%s' % revision, | ||||
c.rhodecode_db_repo, self.ip_addr, self.sa) | ||||
r2677 | Session().commit() | |||
r2375 | ||||
r2189 | if not request.environ.get('HTTP_X_PARTIAL_XHR'): | |||
return redirect(h.url('changeset_home', repo_name=repo_name, | ||||
revision=revision)) | ||||
r2187 | data = { | |||
'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))), | ||||
} | ||||
if comm: | ||||
c.co = comm | ||||
data.update(comm.get_dict()) | ||||
r2188 | data.update({'rendered_text': | |||
render('changeset/changeset_comment_block.html')}) | ||||
r2189 | ||||
r2187 | return data | |||
r1670 | ||||
@jsonify | ||||
r1716 | def delete_comment(self, repo_name, comment_id): | |||
r1674 | co = ChangesetComment.get(comment_id) | |||
r1787 | owner = lambda: co.author.user_id == c.rhodecode_user.user_id | |||
r1712 | if h.HasPermissionAny('hg.admin', 'repository.admin')() or owner: | |||
r1713 | ChangesetCommentsModel().delete(comment=co) | |||
r2677 | Session().commit() | |||
r1674 | return True | |||
else: | ||||
raise HTTPForbidden() | ||||