##// END OF EJS Templates
moved around some code in changeset controllers to properly log which function was decorated....
marcink -
r3750:244f184f beta
parent child Browse files
Show More
@@ -1,437 +1,440 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.changeset
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 changeset controller for pylons showoing changes beetween
7 7 revisions
8 8
9 9 :created_on: Apr 25, 2010
10 10 :author: marcink
11 11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
12 12 :license: GPLv3, see COPYING for more details.
13 13 """
14 14 # This program is free software: you can redistribute it and/or modify
15 15 # it under the terms of the GNU General Public License as published by
16 16 # the Free Software Foundation, either version 3 of the License, or
17 17 # (at your option) any later version.
18 18 #
19 19 # This program is distributed in the hope that it will be useful,
20 20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 22 # GNU General Public License for more details.
23 23 #
24 24 # You should have received a copy of the GNU General Public License
25 25 # along with this program. If not, see <http://www.gnu.org/licenses/>.
26 26 import logging
27 27 import traceback
28 28 from collections import defaultdict
29 29 from webob.exc import HTTPForbidden, HTTPBadRequest, HTTPNotFound
30 30
31 31 from pylons import tmpl_context as c, url, request, response
32 32 from pylons.i18n.translation import _
33 33 from pylons.controllers.util import redirect
34 34 from rhodecode.lib.utils import jsonify
35 35
36 36 from rhodecode.lib.vcs.exceptions import RepositoryError, \
37 37 ChangesetDoesNotExistError
38 38
39 39 import rhodecode.lib.helpers as h
40 40 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator,\
41 41 NotAnonymous
42 42 from rhodecode.lib.base import BaseRepoController, render
43 43 from rhodecode.lib.utils import action_logger
44 44 from rhodecode.lib.compat import OrderedDict
45 45 from rhodecode.lib import diffs
46 46 from rhodecode.model.db import ChangesetComment, ChangesetStatus
47 47 from rhodecode.model.comment import ChangesetCommentsModel
48 48 from rhodecode.model.changeset_status import ChangesetStatusModel
49 49 from rhodecode.model.meta import Session
50 50 from rhodecode.model.repo import RepoModel
51 51 from rhodecode.lib.diffs import LimitedDiffContainer
52 52 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
53 53 from rhodecode.lib.vcs.backends.base import EmptyChangeset
54 54 from rhodecode.lib.utils2 import safe_unicode
55 55
56 56 log = logging.getLogger(__name__)
57 57
58 58
59 59 def _update_with_GET(params, GET):
60 60 for k in ['diff1', 'diff2', 'diff']:
61 61 params[k] += GET.getall(k)
62 62
63 63
64 64 def anchor_url(revision, path, GET):
65 65 fid = h.FID(revision, path)
66 66 return h.url.current(anchor=fid, **dict(GET))
67 67
68 68
69 69 def get_ignore_ws(fid, GET):
70 70 ig_ws_global = GET.get('ignorews')
71 71 ig_ws = filter(lambda k: k.startswith('WS'), GET.getall(fid))
72 72 if ig_ws:
73 73 try:
74 74 return int(ig_ws[0].split(':')[-1])
75 75 except Exception:
76 76 pass
77 77 return ig_ws_global
78 78
79 79
80 80 def _ignorews_url(GET, fileid=None):
81 81 fileid = str(fileid) if fileid else None
82 82 params = defaultdict(list)
83 83 _update_with_GET(params, GET)
84 84 lbl = _('Show white space')
85 85 ig_ws = get_ignore_ws(fileid, GET)
86 86 ln_ctx = get_line_ctx(fileid, GET)
87 87 # global option
88 88 if fileid is None:
89 89 if ig_ws is None:
90 90 params['ignorews'] += [1]
91 91 lbl = _('Ignore white space')
92 92 ctx_key = 'context'
93 93 ctx_val = ln_ctx
94 94 # per file options
95 95 else:
96 96 if ig_ws is None:
97 97 params[fileid] += ['WS:1']
98 98 lbl = _('Ignore white space')
99 99
100 100 ctx_key = fileid
101 101 ctx_val = 'C:%s' % ln_ctx
102 102 # if we have passed in ln_ctx pass it along to our params
103 103 if ln_ctx:
104 104 params[ctx_key] += [ctx_val]
105 105
106 106 params['anchor'] = fileid
107 107 img = h.image(h.url('/images/icons/text_strikethrough.png'), lbl, class_='icon')
108 108 return h.link_to(img, h.url.current(**params), title=lbl, class_='tooltip')
109 109
110 110
111 111 def get_line_ctx(fid, GET):
112 112 ln_ctx_global = GET.get('context')
113 113 if fid:
114 114 ln_ctx = filter(lambda k: k.startswith('C'), GET.getall(fid))
115 115 else:
116 116 _ln_ctx = filter(lambda k: k.startswith('C'), GET)
117 117 ln_ctx = GET.get(_ln_ctx[0]) if _ln_ctx else ln_ctx_global
118 118 if ln_ctx:
119 119 ln_ctx = [ln_ctx]
120 120
121 121 if ln_ctx:
122 122 retval = ln_ctx[0].split(':')[-1]
123 123 else:
124 124 retval = ln_ctx_global
125 125
126 126 try:
127 127 return int(retval)
128 128 except Exception:
129 129 return 3
130 130
131 131
132 132 def _context_url(GET, fileid=None):
133 133 """
134 134 Generates url for context lines
135 135
136 136 :param fileid:
137 137 """
138 138
139 139 fileid = str(fileid) if fileid else None
140 140 ig_ws = get_ignore_ws(fileid, GET)
141 141 ln_ctx = (get_line_ctx(fileid, GET) or 3) * 2
142 142
143 143 params = defaultdict(list)
144 144 _update_with_GET(params, GET)
145 145
146 146 # global option
147 147 if fileid is None:
148 148 if ln_ctx > 0:
149 149 params['context'] += [ln_ctx]
150 150
151 151 if ig_ws:
152 152 ig_ws_key = 'ignorews'
153 153 ig_ws_val = 1
154 154
155 155 # per file option
156 156 else:
157 157 params[fileid] += ['C:%s' % ln_ctx]
158 158 ig_ws_key = fileid
159 159 ig_ws_val = 'WS:%s' % 1
160 160
161 161 if ig_ws:
162 162 params[ig_ws_key] += [ig_ws_val]
163 163
164 164 lbl = _('%s line context') % ln_ctx
165 165
166 166 params['anchor'] = fileid
167 167 img = h.image(h.url('/images/icons/table_add.png'), lbl, class_='icon')
168 168 return h.link_to(img, h.url.current(**params), title=lbl, class_='tooltip')
169 169
170 170
171 171 class ChangesetController(BaseRepoController):
172 172
173 173 def __before__(self):
174 174 super(ChangesetController, self).__before__()
175 175 c.affected_files_cut_off = 60
176 176 repo_model = RepoModel()
177 177 c.users_array = repo_model.get_users_js()
178 178 c.users_groups_array = repo_model.get_users_groups_js()
179 179
180 @LoginRequired()
181 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
182 'repository.admin')
183 def index(self, revision, method='show'):
180 def _index(self, revision, method):
184 181 c.anchor_url = anchor_url
185 182 c.ignorews_url = _ignorews_url
186 183 c.context_url = _context_url
187 184 c.fulldiff = fulldiff = request.GET.get('fulldiff')
188 185 #get ranges of revisions if preset
189 186 rev_range = revision.split('...')[:2]
190 187 enable_comments = True
191 188 try:
192 189 if len(rev_range) == 2:
193 190 enable_comments = False
194 191 rev_start = rev_range[0]
195 192 rev_end = rev_range[1]
196 193 rev_ranges = c.rhodecode_repo.get_changesets(start=rev_start,
197 194 end=rev_end)
198 195 else:
199 196 rev_ranges = [c.rhodecode_repo.get_changeset(revision)]
200 197
201 198 c.cs_ranges = list(rev_ranges)
202 199 if not c.cs_ranges:
203 200 raise RepositoryError('Changeset range returned empty result')
204 201
205 202 except (RepositoryError, ChangesetDoesNotExistError, Exception), e:
206 203 log.error(traceback.format_exc())
207 204 h.flash(str(e), category='error')
208 205 raise HTTPNotFound()
209 206
210 207 c.changes = OrderedDict()
211 208
212 209 c.lines_added = 0 # count of lines added
213 210 c.lines_deleted = 0 # count of lines removes
214 211
215 212 c.changeset_statuses = ChangesetStatus.STATUSES
216 213 c.comments = []
217 214 c.statuses = []
218 215 c.inline_comments = []
219 216 c.inline_cnt = 0
220 217
221 218 # Iterate over ranges (default changeset view is always one changeset)
222 219 for changeset in c.cs_ranges:
223 220 inlines = []
224 221 if method == 'show':
225 222 c.statuses.extend([ChangesetStatusModel().get_status(
226 223 c.rhodecode_db_repo.repo_id, changeset.raw_id)])
227 224
228 225 c.comments.extend(ChangesetCommentsModel()\
229 226 .get_comments(c.rhodecode_db_repo.repo_id,
230 227 revision=changeset.raw_id))
231 228
232 229 #comments from PR
233 230 st = ChangesetStatusModel().get_statuses(
234 231 c.rhodecode_db_repo.repo_id, changeset.raw_id,
235 232 with_revisions=True)
236 233 # from associated statuses, check the pull requests, and
237 234 # show comments from them
238 235
239 236 prs = set([x.pull_request for x in
240 237 filter(lambda x: x.pull_request != None, st)])
241 238
242 239 for pr in prs:
243 240 c.comments.extend(pr.comments)
244 241 inlines = ChangesetCommentsModel()\
245 242 .get_inline_comments(c.rhodecode_db_repo.repo_id,
246 243 revision=changeset.raw_id)
247 244 c.inline_comments.extend(inlines)
248 245
249 246 c.changes[changeset.raw_id] = []
250 247
251 248 cs2 = changeset.raw_id
252 249 cs1 = changeset.parents[0].raw_id if changeset.parents else EmptyChangeset()
253 250 context_lcl = get_line_ctx('', request.GET)
254 251 ign_whitespace_lcl = ign_whitespace_lcl = get_ignore_ws('', request.GET)
255 252
256 253 _diff = c.rhodecode_repo.get_diff(cs1, cs2,
257 254 ignore_whitespace=ign_whitespace_lcl, context=context_lcl)
258 255 diff_limit = self.cut_off_limit if not fulldiff else None
259 256 diff_processor = diffs.DiffProcessor(_diff,
260 257 vcs=c.rhodecode_repo.alias,
261 258 format='gitdiff',
262 259 diff_limit=diff_limit)
263 260 cs_changes = OrderedDict()
264 261 if method == 'show':
265 262 _parsed = diff_processor.prepare()
266 263 c.limited_diff = False
267 264 if isinstance(_parsed, LimitedDiffContainer):
268 265 c.limited_diff = True
269 266 for f in _parsed:
270 267 st = f['stats']
271 268 if st[0] != 'b':
272 269 c.lines_added += st[0]
273 270 c.lines_deleted += st[1]
274 271 fid = h.FID(changeset.raw_id, f['filename'])
275 272 diff = diff_processor.as_html(enable_comments=enable_comments,
276 273 parsed_lines=[f])
277 274 cs_changes[fid] = [cs1, cs2, f['operation'], f['filename'],
278 275 diff, st]
279 276 else:
280 277 # downloads/raw we only need RAW diff nothing else
281 278 diff = diff_processor.as_raw()
282 279 cs_changes[''] = [None, None, None, None, diff, None]
283 280 c.changes[changeset.raw_id] = cs_changes
284 281
285 282 #sort comments by how they were generated
286 283 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
287 284
288 285 # count inline comments
289 286 for __, lines in c.inline_comments:
290 287 for comments in lines.values():
291 288 c.inline_cnt += len(comments)
292 289
293 290 if len(c.cs_ranges) == 1:
294 291 c.changeset = c.cs_ranges[0]
295 292 c.parent_tmpl = ''.join(['# Parent %s\n' % x.raw_id
296 293 for x in c.changeset.parents])
297 294 if method == 'download':
298 295 response.content_type = 'text/plain'
299 296 response.content_disposition = 'attachment; filename=%s.diff' \
300 297 % revision[:12]
301 298 return diff
302 299 elif method == 'patch':
303 300 response.content_type = 'text/plain'
304 301 c.diff = safe_unicode(diff)
305 302 return render('changeset/patch_changeset.html')
306 303 elif method == 'raw':
307 304 response.content_type = 'text/plain'
308 305 return diff
309 306 elif method == 'show':
310 307 if len(c.cs_ranges) == 1:
311 308 return render('changeset/changeset.html')
312 309 else:
313 310 return render('changeset/changeset_range.html')
314 311
315 312 @LoginRequired()
316 313 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
317 314 'repository.admin')
315 def index(self, revision, method='show'):
316 return self._index(revision, method=method)
317
318 @LoginRequired()
319 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
320 'repository.admin')
318 321 def changeset_raw(self, revision):
319 return self.index(revision, method='raw')
322 return self._index(revision, method='raw')
320 323
321 324 @LoginRequired()
322 325 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
323 326 'repository.admin')
324 327 def changeset_patch(self, revision):
325 return self.index(revision, method='patch')
328 return self._index(revision, method='patch')
326 329
327 330 @LoginRequired()
328 331 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
329 332 'repository.admin')
330 333 def changeset_download(self, revision):
331 return self.index(revision, method='download')
334 return self._index(revision, method='download')
332 335
333 336 @LoginRequired()
334 337 @NotAnonymous()
335 338 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
336 339 'repository.admin')
337 340 @jsonify
338 341 def comment(self, repo_name, revision):
339 342 status = request.POST.get('changeset_status')
340 343 change_status = request.POST.get('change_changeset_status')
341 344 text = request.POST.get('text')
342 345 if status and change_status:
343 346 text = text or (_('Status change -> %s')
344 347 % ChangesetStatus.get_status_lbl(status))
345 348
346 349 c.co = comm = ChangesetCommentsModel().create(
347 350 text=text,
348 351 repo=c.rhodecode_db_repo.repo_id,
349 352 user=c.rhodecode_user.user_id,
350 353 revision=revision,
351 354 f_path=request.POST.get('f_path'),
352 355 line_no=request.POST.get('line'),
353 356 status_change=(ChangesetStatus.get_status_lbl(status)
354 357 if status and change_status else None)
355 358 )
356 359
357 360 # get status if set !
358 361 if status and change_status:
359 362 # if latest status was from pull request and it's closed
360 363 # disallow changing status !
361 364 # dont_allow_on_closed_pull_request = True !
362 365
363 366 try:
364 367 ChangesetStatusModel().set_status(
365 368 c.rhodecode_db_repo.repo_id,
366 369 status,
367 370 c.rhodecode_user.user_id,
368 371 comm,
369 372 revision=revision,
370 373 dont_allow_on_closed_pull_request=True
371 374 )
372 375 except StatusChangeOnClosedPullRequestError:
373 376 log.error(traceback.format_exc())
374 377 msg = _('Changing status on a changeset associated with '
375 378 'a closed pull request is not allowed')
376 379 h.flash(msg, category='warning')
377 380 return redirect(h.url('changeset_home', repo_name=repo_name,
378 381 revision=revision))
379 382 action_logger(self.rhodecode_user,
380 383 'user_commented_revision:%s' % revision,
381 384 c.rhodecode_db_repo, self.ip_addr, self.sa)
382 385
383 386 Session().commit()
384 387
385 388 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
386 389 return redirect(h.url('changeset_home', repo_name=repo_name,
387 390 revision=revision))
388 391 #only ajax below
389 392 data = {
390 393 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
391 394 }
392 395 if comm:
393 396 data.update(comm.get_dict())
394 397 data.update({'rendered_text':
395 398 render('changeset/changeset_comment_block.html')})
396 399
397 400 return data
398 401
399 402 @LoginRequired()
400 403 @NotAnonymous()
401 404 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
402 405 'repository.admin')
403 406 def preview_comment(self):
404 407 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
405 408 raise HTTPBadRequest()
406 409 text = request.POST.get('text')
407 410 if text:
408 411 return h.rst_w_mentions(text)
409 412 return ''
410 413
411 414 @LoginRequired()
412 415 @NotAnonymous()
413 416 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
414 417 'repository.admin')
415 418 @jsonify
416 419 def delete_comment(self, repo_name, comment_id):
417 420 co = ChangesetComment.get(comment_id)
418 421 owner = co.author.user_id == c.rhodecode_user.user_id
419 422 if h.HasPermissionAny('hg.admin', 'repository.admin')() or owner:
420 423 ChangesetCommentsModel().delete(comment=co)
421 424 Session().commit()
422 425 return True
423 426 else:
424 427 raise HTTPForbidden()
425 428
426 429 @LoginRequired()
427 430 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
428 431 'repository.admin')
429 432 @jsonify
430 433 def changeset_info(self, repo_name, revision):
431 434 if request.is_xhr:
432 435 try:
433 436 return c.rhodecode_repo.get_changeset(revision)
434 437 except ChangesetDoesNotExistError, e:
435 438 return EmptyChangeset(message=str(e))
436 439 else:
437 440 raise HTTPBadRequest()
General Comments 0
You need to be logged in to leave comments. Login now