changeset.py
404 lines
| 14.6 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 | |||
r3619 | from webob.exc import HTTPForbidden, HTTPBadRequest, HTTPNotFound | |||
r812 | ||||
r547 | from pylons import tmpl_context as c, url, request, response | |||
from pylons.i18n.translation import _ | ||||
from pylons.controllers.util import redirect | ||||
r3061 | from rhodecode.lib.utils import jsonify | |||
r812 | ||||
r3061 | from rhodecode.lib.vcs.exceptions import RepositoryError, \ | |||
r1776 | ChangesetDoesNotExistError | |||
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 | |||
r3061 | from rhodecode.model.repo import RepoModel | |||
r2995 | from rhodecode.lib.diffs import LimitedDiffContainer | |||
r2677 | from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError | |||
r2684 | from rhodecode.lib.vcs.backends.base import EmptyChangeset | |||
r2996 | from rhodecode.lib.utils2 import safe_unicode | |||
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') | |||
r2995 | 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] | ||||
r1787 | ||||
r1776 | if ln_ctx: | |||
retval = ln_ctx[0].split(':')[-1] | ||||
else: | ||||
retval = ln_ctx_global | ||||
try: | ||||
return int(retval) | ||||
except: | ||||
r2995 | return 3 | |||
r1776 | ||||
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 | ||||
r2996 | def index(self, revision, method='show'): | |||
r1776 | c.anchor_url = anchor_url | |||
c.ignorews_url = _ignorews_url | ||||
c.context_url = _context_url | ||||
r2996 | c.fulldiff = fulldiff = 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, | |||
r3023 | 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()) | |||
|
r3573 | h.flash(str(e), category='error') | ||
r3619 | raise HTTPNotFound() | |||
r977 | ||||
c.changes = OrderedDict() | ||||
r1789 | ||||
c.lines_added = 0 # count of lines added | ||||
c.lines_deleted = 0 # count of lines removes | ||||
r2217 | c.changeset_statuses = ChangesetStatus.STATUSES | |||
r1670 | c.comments = [] | |||
r2215 | c.statuses = [] | |||
r1677 | c.inline_comments = [] | |||
c.inline_cnt = 0 | ||||
r2995 | ||||
r1274 | # Iterate over ranges (default changeset view is always one changeset) | |||
r977 | for changeset in c.cs_ranges: | |||
r2995 | inlines = [] | |||
if method == 'show': | ||||
r3176 | c.statuses.extend([ChangesetStatusModel().get_status( | |||
c.rhodecode_db_repo.repo_id, changeset.raw_id)]) | ||||
r2215 | ||||
r2995 | c.comments.extend(ChangesetCommentsModel()\ | |||
.get_comments(c.rhodecode_db_repo.repo_id, | ||||
revision=changeset.raw_id)) | ||||
r3176 | ||||
#comments from PR | ||||
st = ChangesetStatusModel().get_statuses( | ||||
c.rhodecode_db_repo.repo_id, changeset.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 != None, st)]) | ||||
for pr in prs: | ||||
c.comments.extend(pr.comments) | ||||
r2995 | inlines = ChangesetCommentsModel()\ | |||
.get_inline_comments(c.rhodecode_db_repo.repo_id, | ||||
revision=changeset.raw_id) | ||||
c.inline_comments.extend(inlines) | ||||
r977 | c.changes[changeset.raw_id] = [] | |||
r2995 | ||||
cs2 = changeset.raw_id | ||||
cs1 = changeset.parents[0].raw_id if changeset.parents else EmptyChangeset() | ||||
context_lcl = get_line_ctx('', request.GET) | ||||
ign_whitespace_lcl = ign_whitespace_lcl = get_ignore_ws('', request.GET) | ||||
r977 | ||||
r2995 | _diff = c.rhodecode_repo.get_diff(cs1, cs2, | |||
ignore_whitespace=ign_whitespace_lcl, context=context_lcl) | ||||
r2996 | diff_limit = self.cut_off_limit if not fulldiff else None | |||
r2995 | diff_processor = diffs.DiffProcessor(_diff, | |||
vcs=c.rhodecode_repo.alias, | ||||
format='gitdiff', | ||||
diff_limit=diff_limit) | ||||
cs_changes = OrderedDict() | ||||
if method == 'show': | ||||
_parsed = diff_processor.prepare() | ||||
c.limited_diff = False | ||||
if isinstance(_parsed, LimitedDiffContainer): | ||||
c.limited_diff = True | ||||
for f in _parsed: | ||||
st = f['stats'] | ||||
if st[0] != 'b': | ||||
c.lines_added += st[0] | ||||
c.lines_deleted += st[1] | ||||
fid = h.FID(changeset.raw_id, f['filename']) | ||||
diff = diff_processor.as_html(enable_comments=enable_comments, | ||||
parsed_lines=[f]) | ||||
cs_changes[fid] = [cs1, cs2, f['operation'], f['filename'], | ||||
diff, st] | ||||
else: | ||||
# downloads/raw we only need RAW diff nothing else | ||||
diff = diff_processor.as_raw() | ||||
cs_changes[''] = [None, None, None, None, diff, None] | ||||
c.changes[changeset.raw_id] = cs_changes | ||||
r636 | ||||
r3176 | #sort comments by how they were generated | |||
c.comments = sorted(c.comments, key=lambda x: x.comment_id) | ||||
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] | ||||
r2995 | c.parent_tmpl = ''.join(['# Parent %s\n' % x.raw_id | |||
for x in c.changeset.parents]) | ||||
if method == 'download': | ||||
response.content_type = 'text/plain' | ||||
r2996 | response.content_disposition = 'attachment; filename=%s.diff' \ | |||
% revision[:12] | ||||
return diff | ||||
elif method == 'patch': | ||||
response.content_type = 'text/plain' | ||||
c.diff = safe_unicode(diff) | ||||
return render('changeset/patch_changeset.html') | ||||
r2995 | elif method == 'raw': | |||
response.content_type = 'text/plain' | ||||
r2996 | return diff | |||
r2995 | elif method == 'show': | |||
if len(c.cs_ranges) == 1: | ||||
return render('changeset/changeset.html') | ||||
else: | ||||
return render('changeset/changeset_range.html') | ||||
r547 | ||||
r2996 | def changeset_raw(self, revision): | |||
return self.index(revision, method='raw') | ||||
def changeset_patch(self, revision): | ||||
return self.index(revision, method='patch') | ||||
def changeset_download(self, revision): | ||||
return self.index(revision, method='download') | ||||
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 | ||||
r3430 | c.co = 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 | |||
r2815 | # disallow changing status ! | |||
r2677 | # 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()) | ||||
r3176 | msg = _('Changing status on a changeset associated with ' | |||
r2677 | '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)) | ||||
r3430 | #only ajax below | |||
r2187 | data = { | |||
'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))), | ||||
} | ||||
if 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) | |||
|
r3141 | owner = 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() | ||||
r2971 | ||||
@jsonify | ||||
def changeset_info(self, repo_name, revision): | ||||
r2978 | if request.is_xhr: | |||
try: | ||||
return c.rhodecode_repo.get_changeset(revision) | ||||
except ChangesetDoesNotExistError, e: | ||||
return EmptyChangeset(message=str(e)) | ||||
r2971 | else: | |||
raise HTTPBadRequest() | ||||