##// END OF EJS Templates
diffs: fix bug where relevant pull request comments were not being...
dan -
r1158:d20e7e21 default
parent child Browse files
Show More
@@ -1,465 +1,464 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 commit controller for RhodeCode showing changes between commits
23 23 """
24 24
25 25 import logging
26 26
27 27 from collections import defaultdict
28 28 from webob.exc import HTTPForbidden, HTTPBadRequest, HTTPNotFound
29 29
30 30 from pylons import tmpl_context as c, request, response
31 31 from pylons.i18n.translation import _
32 32 from pylons.controllers.util import redirect
33 33
34 34 from rhodecode.lib import auth
35 35 from rhodecode.lib import diffs, codeblocks
36 36 from rhodecode.lib.auth import (
37 37 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous)
38 38 from rhodecode.lib.base import BaseRepoController, render
39 39 from rhodecode.lib.compat import OrderedDict
40 40 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
41 41 import rhodecode.lib.helpers as h
42 42 from rhodecode.lib.utils import action_logger, jsonify
43 43 from rhodecode.lib.utils2 import safe_unicode
44 44 from rhodecode.lib.vcs.backends.base import EmptyCommit
45 45 from rhodecode.lib.vcs.exceptions import (
46 46 RepositoryError, CommitDoesNotExistError)
47 47 from rhodecode.model.db import ChangesetComment, ChangesetStatus
48 48 from rhodecode.model.changeset_status import ChangesetStatusModel
49 49 from rhodecode.model.comment import ChangesetCommentsModel
50 50 from rhodecode.model.meta import Session
51 51 from rhodecode.model.repo import RepoModel
52 52
53 53
54 54 log = logging.getLogger(__name__)
55 55
56 56
57 57 def _update_with_GET(params, GET):
58 58 for k in ['diff1', 'diff2', 'diff']:
59 59 params[k] += GET.getall(k)
60 60
61 61
62 62 def get_ignore_ws(fid, GET):
63 63 ig_ws_global = GET.get('ignorews')
64 64 ig_ws = filter(lambda k: k.startswith('WS'), GET.getall(fid))
65 65 if ig_ws:
66 66 try:
67 67 return int(ig_ws[0].split(':')[-1])
68 68 except Exception:
69 69 pass
70 70 return ig_ws_global
71 71
72 72
73 73 def _ignorews_url(GET, fileid=None):
74 74 fileid = str(fileid) if fileid else None
75 75 params = defaultdict(list)
76 76 _update_with_GET(params, GET)
77 77 label = _('Show whitespace')
78 78 tooltiplbl = _('Show whitespace for all diffs')
79 79 ig_ws = get_ignore_ws(fileid, GET)
80 80 ln_ctx = get_line_ctx(fileid, GET)
81 81
82 82 if ig_ws is None:
83 83 params['ignorews'] += [1]
84 84 label = _('Ignore whitespace')
85 85 tooltiplbl = _('Ignore whitespace for all diffs')
86 86 ctx_key = 'context'
87 87 ctx_val = ln_ctx
88 88
89 89 # if we have passed in ln_ctx pass it along to our params
90 90 if ln_ctx:
91 91 params[ctx_key] += [ctx_val]
92 92
93 93 if fileid:
94 94 params['anchor'] = 'a_' + fileid
95 95 return h.link_to(label, h.url.current(**params), title=tooltiplbl, class_='tooltip')
96 96
97 97
98 98 def get_line_ctx(fid, GET):
99 99 ln_ctx_global = GET.get('context')
100 100 if fid:
101 101 ln_ctx = filter(lambda k: k.startswith('C'), GET.getall(fid))
102 102 else:
103 103 _ln_ctx = filter(lambda k: k.startswith('C'), GET)
104 104 ln_ctx = GET.get(_ln_ctx[0]) if _ln_ctx else ln_ctx_global
105 105 if ln_ctx:
106 106 ln_ctx = [ln_ctx]
107 107
108 108 if ln_ctx:
109 109 retval = ln_ctx[0].split(':')[-1]
110 110 else:
111 111 retval = ln_ctx_global
112 112
113 113 try:
114 114 return int(retval)
115 115 except Exception:
116 116 return 3
117 117
118 118
119 119 def _context_url(GET, fileid=None):
120 120 """
121 121 Generates a url for context lines.
122 122
123 123 :param fileid:
124 124 """
125 125
126 126 fileid = str(fileid) if fileid else None
127 127 ig_ws = get_ignore_ws(fileid, GET)
128 128 ln_ctx = (get_line_ctx(fileid, GET) or 3) * 2
129 129
130 130 params = defaultdict(list)
131 131 _update_with_GET(params, GET)
132 132
133 133 if ln_ctx > 0:
134 134 params['context'] += [ln_ctx]
135 135
136 136 if ig_ws:
137 137 ig_ws_key = 'ignorews'
138 138 ig_ws_val = 1
139 139 params[ig_ws_key] += [ig_ws_val]
140 140
141 141 lbl = _('Increase context')
142 142 tooltiplbl = _('Increase context for all diffs')
143 143
144 144 if fileid:
145 145 params['anchor'] = 'a_' + fileid
146 146 return h.link_to(lbl, h.url.current(**params), title=tooltiplbl, class_='tooltip')
147 147
148 148
149 149 class ChangesetController(BaseRepoController):
150 150
151 151 def __before__(self):
152 152 super(ChangesetController, self).__before__()
153 153 c.affected_files_cut_off = 60
154 154
155 155 def _index(self, commit_id_range, method):
156 156 c.ignorews_url = _ignorews_url
157 157 c.context_url = _context_url
158 158 c.fulldiff = fulldiff = request.GET.get('fulldiff')
159 159
160 160 # fetch global flags of ignore ws or context lines
161 161 context_lcl = get_line_ctx('', request.GET)
162 162 ign_whitespace_lcl = get_ignore_ws('', request.GET)
163 163
164 164 # diff_limit will cut off the whole diff if the limit is applied
165 165 # otherwise it will just hide the big files from the front-end
166 166 diff_limit = self.cut_off_limit_diff
167 167 file_limit = self.cut_off_limit_file
168 168
169 169 # get ranges of commit ids if preset
170 170 commit_range = commit_id_range.split('...')[:2]
171 171
172 172 try:
173 173 pre_load = ['affected_files', 'author', 'branch', 'date',
174 174 'message', 'parents']
175 175
176 176 if len(commit_range) == 2:
177 177 commits = c.rhodecode_repo.get_commits(
178 178 start_id=commit_range[0], end_id=commit_range[1],
179 179 pre_load=pre_load)
180 180 commits = list(commits)
181 181 else:
182 182 commits = [c.rhodecode_repo.get_commit(
183 183 commit_id=commit_id_range, pre_load=pre_load)]
184 184
185 185 c.commit_ranges = commits
186 186 if not c.commit_ranges:
187 187 raise RepositoryError(
188 188 'The commit range returned an empty result')
189 189 except CommitDoesNotExistError:
190 190 msg = _('No such commit exists for this repository')
191 191 h.flash(msg, category='error')
192 192 raise HTTPNotFound()
193 193 except Exception:
194 194 log.exception("General failure")
195 195 raise HTTPNotFound()
196 196
197 197 c.changes = OrderedDict()
198 198 c.lines_added = 0
199 199 c.lines_deleted = 0
200 200
201 201 c.commit_statuses = ChangesetStatus.STATUSES
202 202 c.inline_comments = []
203 203 c.inline_cnt = 0
204 204 c.files = []
205 205
206 206 c.statuses = []
207 207 c.comments = []
208 208 if len(c.commit_ranges) == 1:
209 209 commit = c.commit_ranges[0]
210 210 c.comments = ChangesetCommentsModel().get_comments(
211 211 c.rhodecode_db_repo.repo_id,
212 212 revision=commit.raw_id)
213 213 c.statuses.append(ChangesetStatusModel().get_status(
214 214 c.rhodecode_db_repo.repo_id, commit.raw_id))
215 215 # comments from PR
216 216 statuses = ChangesetStatusModel().get_statuses(
217 217 c.rhodecode_db_repo.repo_id, commit.raw_id,
218 218 with_revisions=True)
219 219 prs = set(st.pull_request for st in statuses
220 if st is st.pull_request is not None)
221
220 if st.pull_request is not None)
222 221 # from associated statuses, check the pull requests, and
223 222 # show comments from them
224 223 for pr in prs:
225 224 c.comments.extend(pr.comments)
226 225
227 226 # Iterate over ranges (default commit view is always one commit)
228 227 for commit in c.commit_ranges:
229 228 c.changes[commit.raw_id] = []
230 229
231 230 commit2 = commit
232 231 commit1 = commit.parents[0] if commit.parents else EmptyCommit()
233 232
234 233 _diff = c.rhodecode_repo.get_diff(
235 234 commit1, commit2,
236 235 ignore_whitespace=ign_whitespace_lcl, context=context_lcl)
237 236 diff_processor = diffs.DiffProcessor(
238 237 _diff, format='newdiff', diff_limit=diff_limit,
239 238 file_limit=file_limit, show_full_diff=fulldiff)
240 239
241 240 commit_changes = OrderedDict()
242 241 if method == 'show':
243 242 _parsed = diff_processor.prepare()
244 243 c.limited_diff = isinstance(_parsed, diffs.LimitedDiffContainer)
245 244
246 245 _parsed = diff_processor.prepare()
247 246
248 247 def _node_getter(commit):
249 248 def get_node(fname):
250 249 try:
251 250 return commit.get_node(fname)
252 251 except NodeDoesNotExistError:
253 252 return None
254 253 return get_node
255 254
256 255 inline_comments = ChangesetCommentsModel().get_inline_comments(
257 256 c.rhodecode_db_repo.repo_id, revision=commit.raw_id)
258 257 c.inline_cnt += len(inline_comments)
259 258
260 259 diffset = codeblocks.DiffSet(
261 260 repo_name=c.repo_name,
262 261 source_node_getter=_node_getter(commit1),
263 262 target_node_getter=_node_getter(commit2),
264 263 comments=inline_comments
265 264 ).render_patchset(_parsed, commit1.raw_id, commit2.raw_id)
266 265 c.changes[commit.raw_id] = diffset
267 266 else:
268 267 # downloads/raw we only need RAW diff nothing else
269 268 diff = diff_processor.as_raw()
270 269 c.changes[commit.raw_id] = [None, None, None, None, diff, None, None]
271 270
272 271 # sort comments by how they were generated
273 272 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
274 273
275 274
276 275 if len(c.commit_ranges) == 1:
277 276 c.commit = c.commit_ranges[0]
278 277 c.parent_tmpl = ''.join(
279 278 '# Parent %s\n' % x.raw_id for x in c.commit.parents)
280 279 if method == 'download':
281 280 response.content_type = 'text/plain'
282 281 response.content_disposition = (
283 282 'attachment; filename=%s.diff' % commit_id_range[:12])
284 283 return diff
285 284 elif method == 'patch':
286 285 response.content_type = 'text/plain'
287 286 c.diff = safe_unicode(diff)
288 287 return render('changeset/patch_changeset.html')
289 288 elif method == 'raw':
290 289 response.content_type = 'text/plain'
291 290 return diff
292 291 elif method == 'show':
293 292 if len(c.commit_ranges) == 1:
294 293 return render('changeset/changeset.html')
295 294 else:
296 295 c.ancestor = None
297 296 c.target_repo = c.rhodecode_db_repo
298 297 return render('changeset/changeset_range.html')
299 298
300 299 @LoginRequired()
301 300 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
302 301 'repository.admin')
303 302 def index(self, revision, method='show'):
304 303 return self._index(revision, method=method)
305 304
306 305 @LoginRequired()
307 306 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
308 307 'repository.admin')
309 308 def changeset_raw(self, revision):
310 309 return self._index(revision, method='raw')
311 310
312 311 @LoginRequired()
313 312 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
314 313 'repository.admin')
315 314 def changeset_patch(self, revision):
316 315 return self._index(revision, method='patch')
317 316
318 317 @LoginRequired()
319 318 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
320 319 'repository.admin')
321 320 def changeset_download(self, revision):
322 321 return self._index(revision, method='download')
323 322
324 323 @LoginRequired()
325 324 @NotAnonymous()
326 325 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
327 326 'repository.admin')
328 327 @auth.CSRFRequired()
329 328 @jsonify
330 329 def comment(self, repo_name, revision):
331 330 commit_id = revision
332 331 status = request.POST.get('changeset_status', None)
333 332 text = request.POST.get('text')
334 333 if status:
335 334 text = text or (_('Status change %(transition_icon)s %(status)s')
336 335 % {'transition_icon': '>',
337 336 'status': ChangesetStatus.get_status_lbl(status)})
338 337
339 338 multi_commit_ids = filter(
340 339 lambda s: s not in ['', None],
341 340 request.POST.get('commit_ids', '').split(','),)
342 341
343 342 commit_ids = multi_commit_ids or [commit_id]
344 343 comment = None
345 344 for current_id in filter(None, commit_ids):
346 345 c.co = comment = ChangesetCommentsModel().create(
347 346 text=text,
348 347 repo=c.rhodecode_db_repo.repo_id,
349 348 user=c.rhodecode_user.user_id,
350 349 revision=current_id,
351 350 f_path=request.POST.get('f_path'),
352 351 line_no=request.POST.get('line'),
353 352 status_change=(ChangesetStatus.get_status_lbl(status)
354 353 if status else None),
355 354 status_change_type=status
356 355 )
357 356 # get status if set !
358 357 if status:
359 358 # if latest status was from pull request and it's closed
360 359 # disallow changing status !
361 360 # dont_allow_on_closed_pull_request = True !
362 361
363 362 try:
364 363 ChangesetStatusModel().set_status(
365 364 c.rhodecode_db_repo.repo_id,
366 365 status,
367 366 c.rhodecode_user.user_id,
368 367 comment,
369 368 revision=current_id,
370 369 dont_allow_on_closed_pull_request=True
371 370 )
372 371 except StatusChangeOnClosedPullRequestError:
373 372 msg = _('Changing the status of a commit associated with '
374 373 'a closed pull request is not allowed')
375 374 log.exception(msg)
376 375 h.flash(msg, category='warning')
377 376 return redirect(h.url(
378 377 'changeset_home', repo_name=repo_name,
379 378 revision=current_id))
380 379
381 380 # finalize, commit and redirect
382 381 Session().commit()
383 382
384 383 data = {
385 384 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
386 385 }
387 386 if comment:
388 387 data.update(comment.get_dict())
389 388 data.update({'rendered_text':
390 389 render('changeset/changeset_comment_block.html')})
391 390
392 391 return data
393 392
394 393 @LoginRequired()
395 394 @NotAnonymous()
396 395 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
397 396 'repository.admin')
398 397 @auth.CSRFRequired()
399 398 def preview_comment(self):
400 399 # Technically a CSRF token is not needed as no state changes with this
401 400 # call. However, as this is a POST is better to have it, so automated
402 401 # tools don't flag it as potential CSRF.
403 402 # Post is required because the payload could be bigger than the maximum
404 403 # allowed by GET.
405 404 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
406 405 raise HTTPBadRequest()
407 406 text = request.POST.get('text')
408 407 renderer = request.POST.get('renderer') or 'rst'
409 408 if text:
410 409 return h.render(text, renderer=renderer, mentions=True)
411 410 return ''
412 411
413 412 @LoginRequired()
414 413 @NotAnonymous()
415 414 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
416 415 'repository.admin')
417 416 @auth.CSRFRequired()
418 417 @jsonify
419 418 def delete_comment(self, repo_name, comment_id):
420 419 comment = ChangesetComment.get(comment_id)
421 420 owner = (comment.author.user_id == c.rhodecode_user.user_id)
422 421 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
423 422 if h.HasPermissionAny('hg.admin')() or is_repo_admin or owner:
424 423 ChangesetCommentsModel().delete(comment=comment)
425 424 Session().commit()
426 425 return True
427 426 else:
428 427 raise HTTPForbidden()
429 428
430 429 @LoginRequired()
431 430 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
432 431 'repository.admin')
433 432 @jsonify
434 433 def changeset_info(self, repo_name, revision):
435 434 if request.is_xhr:
436 435 try:
437 436 return c.rhodecode_repo.get_commit(commit_id=revision)
438 437 except CommitDoesNotExistError as e:
439 438 return EmptyCommit(message=str(e))
440 439 else:
441 440 raise HTTPBadRequest()
442 441
443 442 @LoginRequired()
444 443 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
445 444 'repository.admin')
446 445 @jsonify
447 446 def changeset_children(self, repo_name, revision):
448 447 if request.is_xhr:
449 448 commit = c.rhodecode_repo.get_commit(commit_id=revision)
450 449 result = {"results": commit.children}
451 450 return result
452 451 else:
453 452 raise HTTPBadRequest()
454 453
455 454 @LoginRequired()
456 455 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
457 456 'repository.admin')
458 457 @jsonify
459 458 def changeset_parents(self, repo_name, revision):
460 459 if request.is_xhr:
461 460 commit = c.rhodecode_repo.get_commit(commit_id=revision)
462 461 result = {"results": commit.parents}
463 462 return result
464 463 else:
465 464 raise HTTPBadRequest()
General Comments 0
You need to be logged in to leave comments. Login now