##// END OF EJS Templates
comments: properly show version of pull request into added comments....
marcink -
r1286:e783fdd1 default
parent child Browse files
Show More

The requested changes are too big and content was truncated. Show full diff

@@ -1,468 +1,470 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 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, NodeDoesNotExistError)
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 # auto collapse if we have more than limit
202 202 collapse_limit = diffs.DiffProcessor._collapse_commits_over
203 203 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
204 204
205 205 c.commit_statuses = ChangesetStatus.STATUSES
206 206 c.inline_comments = []
207 207 c.files = []
208 208
209 209 c.statuses = []
210 210 c.comments = []
211 211 if len(c.commit_ranges) == 1:
212 212 commit = c.commit_ranges[0]
213 213 c.comments = ChangesetCommentsModel().get_comments(
214 214 c.rhodecode_db_repo.repo_id,
215 215 revision=commit.raw_id)
216 216 c.statuses.append(ChangesetStatusModel().get_status(
217 217 c.rhodecode_db_repo.repo_id, commit.raw_id))
218 218 # comments from PR
219 219 statuses = ChangesetStatusModel().get_statuses(
220 220 c.rhodecode_db_repo.repo_id, commit.raw_id,
221 221 with_revisions=True)
222 222 prs = set(st.pull_request for st in statuses
223 223 if st.pull_request is not None)
224 224 # from associated statuses, check the pull requests, and
225 225 # show comments from them
226 226 for pr in prs:
227 227 c.comments.extend(pr.comments)
228 228
229 229 # Iterate over ranges (default commit view is always one commit)
230 230 for commit in c.commit_ranges:
231 231 c.changes[commit.raw_id] = []
232 232
233 233 commit2 = commit
234 234 commit1 = commit.parents[0] if commit.parents else EmptyCommit()
235 235
236 236 _diff = c.rhodecode_repo.get_diff(
237 237 commit1, commit2,
238 238 ignore_whitespace=ign_whitespace_lcl, context=context_lcl)
239 239 diff_processor = diffs.DiffProcessor(
240 240 _diff, format='newdiff', diff_limit=diff_limit,
241 241 file_limit=file_limit, show_full_diff=fulldiff)
242 242
243 243 commit_changes = OrderedDict()
244 244 if method == 'show':
245 245 _parsed = diff_processor.prepare()
246 246 c.limited_diff = isinstance(_parsed, diffs.LimitedDiffContainer)
247 247
248 248 _parsed = diff_processor.prepare()
249 249
250 250 def _node_getter(commit):
251 251 def get_node(fname):
252 252 try:
253 253 return commit.get_node(fname)
254 254 except NodeDoesNotExistError:
255 255 return None
256 256 return get_node
257 257
258 258 inline_comments = ChangesetCommentsModel().get_inline_comments(
259 259 c.rhodecode_db_repo.repo_id, revision=commit.raw_id)
260 260 c.inline_cnt = ChangesetCommentsModel().get_inline_comments_count(
261 261 inline_comments)
262 262
263 263 diffset = codeblocks.DiffSet(
264 264 repo_name=c.repo_name,
265 265 source_node_getter=_node_getter(commit1),
266 266 target_node_getter=_node_getter(commit2),
267 267 comments=inline_comments
268 268 ).render_patchset(_parsed, commit1.raw_id, commit2.raw_id)
269 269 c.changes[commit.raw_id] = diffset
270 270 else:
271 271 # downloads/raw we only need RAW diff nothing else
272 272 diff = diff_processor.as_raw()
273 273 c.changes[commit.raw_id] = [None, None, None, None, diff, None, None]
274 274
275 275 # sort comments by how they were generated
276 276 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
277 277
278 278
279 279 if len(c.commit_ranges) == 1:
280 280 c.commit = c.commit_ranges[0]
281 281 c.parent_tmpl = ''.join(
282 282 '# Parent %s\n' % x.raw_id for x in c.commit.parents)
283 283 if method == 'download':
284 284 response.content_type = 'text/plain'
285 285 response.content_disposition = (
286 286 'attachment; filename=%s.diff' % commit_id_range[:12])
287 287 return diff
288 288 elif method == 'patch':
289 289 response.content_type = 'text/plain'
290 290 c.diff = safe_unicode(diff)
291 291 return render('changeset/patch_changeset.mako')
292 292 elif method == 'raw':
293 293 response.content_type = 'text/plain'
294 294 return diff
295 295 elif method == 'show':
296 296 if len(c.commit_ranges) == 1:
297 297 return render('changeset/changeset.mako')
298 298 else:
299 299 c.ancestor = None
300 300 c.target_repo = c.rhodecode_db_repo
301 301 return render('changeset/changeset_range.mako')
302 302
303 303 @LoginRequired()
304 304 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
305 305 'repository.admin')
306 306 def index(self, revision, method='show'):
307 307 return self._index(revision, method=method)
308 308
309 309 @LoginRequired()
310 310 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
311 311 'repository.admin')
312 312 def changeset_raw(self, revision):
313 313 return self._index(revision, method='raw')
314 314
315 315 @LoginRequired()
316 316 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
317 317 'repository.admin')
318 318 def changeset_patch(self, revision):
319 319 return self._index(revision, method='patch')
320 320
321 321 @LoginRequired()
322 322 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
323 323 'repository.admin')
324 324 def changeset_download(self, revision):
325 325 return self._index(revision, method='download')
326 326
327 327 @LoginRequired()
328 328 @NotAnonymous()
329 329 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
330 330 'repository.admin')
331 331 @auth.CSRFRequired()
332 332 @jsonify
333 333 def comment(self, repo_name, revision):
334 334 commit_id = revision
335 335 status = request.POST.get('changeset_status', None)
336 336 text = request.POST.get('text')
337 337 if status:
338 338 text = text or (_('Status change %(transition_icon)s %(status)s')
339 339 % {'transition_icon': '>',
340 340 'status': ChangesetStatus.get_status_lbl(status)})
341 341
342 342 multi_commit_ids = filter(
343 343 lambda s: s not in ['', None],
344 344 request.POST.get('commit_ids', '').split(','),)
345 345
346 346 commit_ids = multi_commit_ids or [commit_id]
347 347 comment = None
348 348 for current_id in filter(None, commit_ids):
349 349 c.co = comment = ChangesetCommentsModel().create(
350 350 text=text,
351 351 repo=c.rhodecode_db_repo.repo_id,
352 352 user=c.rhodecode_user.user_id,
353 353 revision=current_id,
354 354 f_path=request.POST.get('f_path'),
355 355 line_no=request.POST.get('line'),
356 356 status_change=(ChangesetStatus.get_status_lbl(status)
357 357 if status else None),
358 358 status_change_type=status
359 359 )
360 c.inline_comment = True if comment.line_no else False
361
360 362 # get status if set !
361 363 if status:
362 364 # if latest status was from pull request and it's closed
363 365 # disallow changing status !
364 366 # dont_allow_on_closed_pull_request = True !
365 367
366 368 try:
367 369 ChangesetStatusModel().set_status(
368 370 c.rhodecode_db_repo.repo_id,
369 371 status,
370 372 c.rhodecode_user.user_id,
371 373 comment,
372 374 revision=current_id,
373 375 dont_allow_on_closed_pull_request=True
374 376 )
375 377 except StatusChangeOnClosedPullRequestError:
376 378 msg = _('Changing the status of a commit associated with '
377 379 'a closed pull request is not allowed')
378 380 log.exception(msg)
379 381 h.flash(msg, category='warning')
380 382 return redirect(h.url(
381 383 'changeset_home', repo_name=repo_name,
382 384 revision=current_id))
383 385
384 386 # finalize, commit and redirect
385 387 Session().commit()
386 388
387 389 data = {
388 390 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
389 391 }
390 392 if comment:
391 393 data.update(comment.get_dict())
392 394 data.update({'rendered_text':
393 395 render('changeset/changeset_comment_block.mako')})
394 396
395 397 return data
396 398
397 399 @LoginRequired()
398 400 @NotAnonymous()
399 401 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
400 402 'repository.admin')
401 403 @auth.CSRFRequired()
402 404 def preview_comment(self):
403 405 # Technically a CSRF token is not needed as no state changes with this
404 406 # call. However, as this is a POST is better to have it, so automated
405 407 # tools don't flag it as potential CSRF.
406 408 # Post is required because the payload could be bigger than the maximum
407 409 # allowed by GET.
408 410 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
409 411 raise HTTPBadRequest()
410 412 text = request.POST.get('text')
411 413 renderer = request.POST.get('renderer') or 'rst'
412 414 if text:
413 415 return h.render(text, renderer=renderer, mentions=True)
414 416 return ''
415 417
416 418 @LoginRequired()
417 419 @NotAnonymous()
418 420 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
419 421 'repository.admin')
420 422 @auth.CSRFRequired()
421 423 @jsonify
422 424 def delete_comment(self, repo_name, comment_id):
423 425 comment = ChangesetComment.get(comment_id)
424 426 owner = (comment.author.user_id == c.rhodecode_user.user_id)
425 427 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
426 428 if h.HasPermissionAny('hg.admin')() or is_repo_admin or owner:
427 429 ChangesetCommentsModel().delete(comment=comment)
428 430 Session().commit()
429 431 return True
430 432 else:
431 433 raise HTTPForbidden()
432 434
433 435 @LoginRequired()
434 436 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
435 437 'repository.admin')
436 438 @jsonify
437 439 def changeset_info(self, repo_name, revision):
438 440 if request.is_xhr:
439 441 try:
440 442 return c.rhodecode_repo.get_commit(commit_id=revision)
441 443 except CommitDoesNotExistError as e:
442 444 return EmptyCommit(message=str(e))
443 445 else:
444 446 raise HTTPBadRequest()
445 447
446 448 @LoginRequired()
447 449 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
448 450 'repository.admin')
449 451 @jsonify
450 452 def changeset_children(self, repo_name, revision):
451 453 if request.is_xhr:
452 454 commit = c.rhodecode_repo.get_commit(commit_id=revision)
453 455 result = {"results": commit.children}
454 456 return result
455 457 else:
456 458 raise HTTPBadRequest()
457 459
458 460 @LoginRequired()
459 461 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
460 462 'repository.admin')
461 463 @jsonify
462 464 def changeset_parents(self, repo_name, revision):
463 465 if request.is_xhr:
464 466 commit = c.rhodecode_repo.get_commit(commit_id=revision)
465 467 result = {"results": commit.parents}
466 468 return result
467 469 else:
468 470 raise HTTPBadRequest()
@@ -1,1020 +1,1024 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2017 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 pull requests controller for rhodecode for initializing pull requests
23 23 """
24 24 import types
25 25
26 26 import peppercorn
27 27 import formencode
28 28 import logging
29 29 import collections
30 30
31 31 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPBadRequest
32 32 from pylons import request, tmpl_context as c, url
33 33 from pylons.controllers.util import redirect
34 34 from pylons.i18n.translation import _
35 35 from pyramid.threadlocal import get_current_registry
36 36 from sqlalchemy.sql import func
37 37 from sqlalchemy.sql.expression import or_
38 38
39 39 from rhodecode import events
40 40 from rhodecode.lib import auth, diffs, helpers as h, codeblocks
41 41 from rhodecode.lib.ext_json import json
42 42 from rhodecode.lib.base import (
43 43 BaseRepoController, render, vcs_operation_context)
44 44 from rhodecode.lib.auth import (
45 45 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous,
46 46 HasAcceptedRepoType, XHRRequired)
47 47 from rhodecode.lib.channelstream import channelstream_request
48 48 from rhodecode.lib.utils import jsonify
49 49 from rhodecode.lib.utils2 import (
50 50 safe_int, safe_str, str2bool, safe_unicode)
51 51 from rhodecode.lib.vcs.backends.base import (
52 52 EmptyCommit, UpdateFailureReason, EmptyRepository)
53 53 from rhodecode.lib.vcs.exceptions import (
54 54 EmptyRepositoryError, CommitDoesNotExistError, RepositoryRequirementError,
55 55 NodeDoesNotExistError)
56 56
57 57 from rhodecode.model.changeset_status import ChangesetStatusModel
58 58 from rhodecode.model.comment import ChangesetCommentsModel
59 59 from rhodecode.model.db import (PullRequest, ChangesetStatus, ChangesetComment,
60 60 Repository, PullRequestVersion)
61 61 from rhodecode.model.forms import PullRequestForm
62 62 from rhodecode.model.meta import Session
63 63 from rhodecode.model.pull_request import PullRequestModel
64 64
65 65 log = logging.getLogger(__name__)
66 66
67 67
68 68 class PullrequestsController(BaseRepoController):
69 69 def __before__(self):
70 70 super(PullrequestsController, self).__before__()
71 71
72 72 def _load_compare_data(self, pull_request, inline_comments):
73 73 """
74 74 Load context data needed for generating compare diff
75 75
76 76 :param pull_request: object related to the request
77 77 :param enable_comments: flag to determine if comments are included
78 78 """
79 79 source_repo = pull_request.source_repo
80 80 source_ref_id = pull_request.source_ref_parts.commit_id
81 81
82 82 target_repo = pull_request.target_repo
83 83 target_ref_id = pull_request.target_ref_parts.commit_id
84 84
85 85 # despite opening commits for bookmarks/branches/tags, we always
86 86 # convert this to rev to prevent changes after bookmark or branch change
87 87 c.source_ref_type = 'rev'
88 88 c.source_ref = source_ref_id
89 89
90 90 c.target_ref_type = 'rev'
91 91 c.target_ref = target_ref_id
92 92
93 93 c.source_repo = source_repo
94 94 c.target_repo = target_repo
95 95
96 96 c.fulldiff = bool(request.GET.get('fulldiff'))
97 97
98 98 # diff_limit is the old behavior, will cut off the whole diff
99 99 # if the limit is applied otherwise will just hide the
100 100 # big files from the front-end
101 101 diff_limit = self.cut_off_limit_diff
102 102 file_limit = self.cut_off_limit_file
103 103
104 104 pre_load = ["author", "branch", "date", "message"]
105 105
106 106 c.commit_ranges = []
107 107 source_commit = EmptyCommit()
108 108 target_commit = EmptyCommit()
109 109 c.missing_requirements = False
110 110 try:
111 111 c.commit_ranges = [
112 112 source_repo.get_commit(commit_id=rev, pre_load=pre_load)
113 113 for rev in pull_request.revisions]
114 114
115 115 c.statuses = source_repo.statuses(
116 116 [x.raw_id for x in c.commit_ranges])
117 117
118 118 target_commit = source_repo.get_commit(
119 119 commit_id=safe_str(target_ref_id))
120 120 source_commit = source_repo.get_commit(
121 121 commit_id=safe_str(source_ref_id))
122 122 except RepositoryRequirementError:
123 123 c.missing_requirements = True
124 124
125 125 # auto collapse if we have more than limit
126 126 collapse_limit = diffs.DiffProcessor._collapse_commits_over
127 127 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
128 128
129 129 c.changes = {}
130 130 c.missing_commits = False
131 131 if (c.missing_requirements or
132 132 isinstance(source_commit, EmptyCommit) or
133 133 source_commit == target_commit):
134 134 _parsed = []
135 135 c.missing_commits = True
136 136 else:
137 137 vcs_diff = PullRequestModel().get_diff(pull_request)
138 138 diff_processor = diffs.DiffProcessor(
139 139 vcs_diff, format='newdiff', diff_limit=diff_limit,
140 140 file_limit=file_limit, show_full_diff=c.fulldiff)
141 141
142 142 _parsed = diff_processor.prepare()
143 143 c.limited_diff = isinstance(_parsed, diffs.LimitedDiffContainer)
144 144
145 145 included_files = {}
146 146 for f in _parsed:
147 147 included_files[f['filename']] = f['stats']
148 148
149 149 c.deleted_files = [fname for fname in inline_comments if
150 150 fname not in included_files]
151 151
152 152 c.deleted_files_comments = collections.defaultdict(dict)
153 153 for fname, per_line_comments in inline_comments.items():
154 154 if fname in c.deleted_files:
155 155 c.deleted_files_comments[fname]['stats'] = 0
156 156 c.deleted_files_comments[fname]['comments'] = list()
157 157 for lno, comments in per_line_comments.items():
158 158 c.deleted_files_comments[fname]['comments'].extend(comments)
159 159
160 160 def _node_getter(commit):
161 161 def get_node(fname):
162 162 try:
163 163 return commit.get_node(fname)
164 164 except NodeDoesNotExistError:
165 165 return None
166 166 return get_node
167 167
168 168 c.diffset = codeblocks.DiffSet(
169 169 repo_name=c.repo_name,
170 170 source_repo_name=c.source_repo.repo_name,
171 171 source_node_getter=_node_getter(target_commit),
172 172 target_node_getter=_node_getter(source_commit),
173 173 comments=inline_comments
174 174 ).render_patchset(_parsed, target_commit.raw_id, source_commit.raw_id)
175 175
176 176 def _extract_ordering(self, request):
177 177 column_index = safe_int(request.GET.get('order[0][column]'))
178 178 order_dir = request.GET.get('order[0][dir]', 'desc')
179 179 order_by = request.GET.get(
180 180 'columns[%s][data][sort]' % column_index, 'name_raw')
181 181 return order_by, order_dir
182 182
183 183 @LoginRequired()
184 184 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
185 185 'repository.admin')
186 186 @HasAcceptedRepoType('git', 'hg')
187 187 def show_all(self, repo_name):
188 188 # filter types
189 189 c.active = 'open'
190 190 c.source = str2bool(request.GET.get('source'))
191 191 c.closed = str2bool(request.GET.get('closed'))
192 192 c.my = str2bool(request.GET.get('my'))
193 193 c.awaiting_review = str2bool(request.GET.get('awaiting_review'))
194 194 c.awaiting_my_review = str2bool(request.GET.get('awaiting_my_review'))
195 195 c.repo_name = repo_name
196 196
197 197 opened_by = None
198 198 if c.my:
199 199 c.active = 'my'
200 200 opened_by = [c.rhodecode_user.user_id]
201 201
202 202 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
203 203 if c.closed:
204 204 c.active = 'closed'
205 205 statuses = [PullRequest.STATUS_CLOSED]
206 206
207 207 if c.awaiting_review and not c.source:
208 208 c.active = 'awaiting'
209 209 if c.source and not c.awaiting_review:
210 210 c.active = 'source'
211 211 if c.awaiting_my_review:
212 212 c.active = 'awaiting_my'
213 213
214 214 data = self._get_pull_requests_list(
215 215 repo_name=repo_name, opened_by=opened_by, statuses=statuses)
216 216 if not request.is_xhr:
217 217 c.data = json.dumps(data['data'])
218 218 c.records_total = data['recordsTotal']
219 219 return render('/pullrequests/pullrequests.mako')
220 220 else:
221 221 return json.dumps(data)
222 222
223 223 def _get_pull_requests_list(self, repo_name, opened_by, statuses):
224 224 # pagination
225 225 start = safe_int(request.GET.get('start'), 0)
226 226 length = safe_int(request.GET.get('length'), c.visual.dashboard_items)
227 227 order_by, order_dir = self._extract_ordering(request)
228 228
229 229 if c.awaiting_review:
230 230 pull_requests = PullRequestModel().get_awaiting_review(
231 231 repo_name, source=c.source, opened_by=opened_by,
232 232 statuses=statuses, offset=start, length=length,
233 233 order_by=order_by, order_dir=order_dir)
234 234 pull_requests_total_count = PullRequestModel(
235 235 ).count_awaiting_review(
236 236 repo_name, source=c.source, statuses=statuses,
237 237 opened_by=opened_by)
238 238 elif c.awaiting_my_review:
239 239 pull_requests = PullRequestModel().get_awaiting_my_review(
240 240 repo_name, source=c.source, opened_by=opened_by,
241 241 user_id=c.rhodecode_user.user_id, statuses=statuses,
242 242 offset=start, length=length, order_by=order_by,
243 243 order_dir=order_dir)
244 244 pull_requests_total_count = PullRequestModel(
245 245 ).count_awaiting_my_review(
246 246 repo_name, source=c.source, user_id=c.rhodecode_user.user_id,
247 247 statuses=statuses, opened_by=opened_by)
248 248 else:
249 249 pull_requests = PullRequestModel().get_all(
250 250 repo_name, source=c.source, opened_by=opened_by,
251 251 statuses=statuses, offset=start, length=length,
252 252 order_by=order_by, order_dir=order_dir)
253 253 pull_requests_total_count = PullRequestModel().count_all(
254 254 repo_name, source=c.source, statuses=statuses,
255 255 opened_by=opened_by)
256 256
257 257 from rhodecode.lib.utils import PartialRenderer
258 258 _render = PartialRenderer('data_table/_dt_elements.mako')
259 259 data = []
260 260 for pr in pull_requests:
261 261 comments = ChangesetCommentsModel().get_all_comments(
262 262 c.rhodecode_db_repo.repo_id, pull_request=pr)
263 263
264 264 data.append({
265 265 'name': _render('pullrequest_name',
266 266 pr.pull_request_id, pr.target_repo.repo_name),
267 267 'name_raw': pr.pull_request_id,
268 268 'status': _render('pullrequest_status',
269 269 pr.calculated_review_status()),
270 270 'title': _render(
271 271 'pullrequest_title', pr.title, pr.description),
272 272 'description': h.escape(pr.description),
273 273 'updated_on': _render('pullrequest_updated_on',
274 274 h.datetime_to_time(pr.updated_on)),
275 275 'updated_on_raw': h.datetime_to_time(pr.updated_on),
276 276 'created_on': _render('pullrequest_updated_on',
277 277 h.datetime_to_time(pr.created_on)),
278 278 'created_on_raw': h.datetime_to_time(pr.created_on),
279 279 'author': _render('pullrequest_author',
280 280 pr.author.full_contact, ),
281 281 'author_raw': pr.author.full_name,
282 282 'comments': _render('pullrequest_comments', len(comments)),
283 283 'comments_raw': len(comments),
284 284 'closed': pr.is_closed(),
285 285 })
286 286 # json used to render the grid
287 287 data = ({
288 288 'data': data,
289 289 'recordsTotal': pull_requests_total_count,
290 290 'recordsFiltered': pull_requests_total_count,
291 291 })
292 292 return data
293 293
294 294 @LoginRequired()
295 295 @NotAnonymous()
296 296 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
297 297 'repository.admin')
298 298 @HasAcceptedRepoType('git', 'hg')
299 299 def index(self):
300 300 source_repo = c.rhodecode_db_repo
301 301
302 302 try:
303 303 source_repo.scm_instance().get_commit()
304 304 except EmptyRepositoryError:
305 305 h.flash(h.literal(_('There are no commits yet')),
306 306 category='warning')
307 307 redirect(url('summary_home', repo_name=source_repo.repo_name))
308 308
309 309 commit_id = request.GET.get('commit')
310 310 branch_ref = request.GET.get('branch')
311 311 bookmark_ref = request.GET.get('bookmark')
312 312
313 313 try:
314 314 source_repo_data = PullRequestModel().generate_repo_data(
315 315 source_repo, commit_id=commit_id,
316 316 branch=branch_ref, bookmark=bookmark_ref)
317 317 except CommitDoesNotExistError as e:
318 318 log.exception(e)
319 319 h.flash(_('Commit does not exist'), 'error')
320 320 redirect(url('pullrequest_home', repo_name=source_repo.repo_name))
321 321
322 322 default_target_repo = source_repo
323 323
324 324 if source_repo.parent:
325 325 parent_vcs_obj = source_repo.parent.scm_instance()
326 326 if parent_vcs_obj and not parent_vcs_obj.is_empty():
327 327 # change default if we have a parent repo
328 328 default_target_repo = source_repo.parent
329 329
330 330 target_repo_data = PullRequestModel().generate_repo_data(
331 331 default_target_repo)
332 332
333 333 selected_source_ref = source_repo_data['refs']['selected_ref']
334 334
335 335 title_source_ref = selected_source_ref.split(':', 2)[1]
336 336 c.default_title = PullRequestModel().generate_pullrequest_title(
337 337 source=source_repo.repo_name,
338 338 source_ref=title_source_ref,
339 339 target=default_target_repo.repo_name
340 340 )
341 341
342 342 c.default_repo_data = {
343 343 'source_repo_name': source_repo.repo_name,
344 344 'source_refs_json': json.dumps(source_repo_data),
345 345 'target_repo_name': default_target_repo.repo_name,
346 346 'target_refs_json': json.dumps(target_repo_data),
347 347 }
348 348 c.default_source_ref = selected_source_ref
349 349
350 350 return render('/pullrequests/pullrequest.mako')
351 351
352 352 @LoginRequired()
353 353 @NotAnonymous()
354 354 @XHRRequired()
355 355 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
356 356 'repository.admin')
357 357 @jsonify
358 358 def get_repo_refs(self, repo_name, target_repo_name):
359 359 repo = Repository.get_by_repo_name(target_repo_name)
360 360 if not repo:
361 361 raise HTTPNotFound
362 362 return PullRequestModel().generate_repo_data(repo)
363 363
364 364 @LoginRequired()
365 365 @NotAnonymous()
366 366 @XHRRequired()
367 367 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
368 368 'repository.admin')
369 369 @jsonify
370 370 def get_repo_destinations(self, repo_name):
371 371 repo = Repository.get_by_repo_name(repo_name)
372 372 if not repo:
373 373 raise HTTPNotFound
374 374 filter_query = request.GET.get('query')
375 375
376 376 query = Repository.query() \
377 377 .order_by(func.length(Repository.repo_name)) \
378 378 .filter(or_(
379 379 Repository.repo_name == repo.repo_name,
380 380 Repository.fork_id == repo.repo_id))
381 381
382 382 if filter_query:
383 383 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
384 384 query = query.filter(
385 385 Repository.repo_name.ilike(ilike_expression))
386 386
387 387 add_parent = False
388 388 if repo.parent:
389 389 if filter_query in repo.parent.repo_name:
390 390 parent_vcs_obj = repo.parent.scm_instance()
391 391 if parent_vcs_obj and not parent_vcs_obj.is_empty():
392 392 add_parent = True
393 393
394 394 limit = 20 - 1 if add_parent else 20
395 395 all_repos = query.limit(limit).all()
396 396 if add_parent:
397 397 all_repos += [repo.parent]
398 398
399 399 repos = []
400 400 for obj in self.scm_model.get_repos(all_repos):
401 401 repos.append({
402 402 'id': obj['name'],
403 403 'text': obj['name'],
404 404 'type': 'repo',
405 405 'obj': obj['dbrepo']
406 406 })
407 407
408 408 data = {
409 409 'more': False,
410 410 'results': [{
411 411 'text': _('Repositories'),
412 412 'children': repos
413 413 }] if repos else []
414 414 }
415 415 return data
416 416
417 417 @LoginRequired()
418 418 @NotAnonymous()
419 419 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
420 420 'repository.admin')
421 421 @HasAcceptedRepoType('git', 'hg')
422 422 @auth.CSRFRequired()
423 423 def create(self, repo_name):
424 424 repo = Repository.get_by_repo_name(repo_name)
425 425 if not repo:
426 426 raise HTTPNotFound
427 427
428 428 controls = peppercorn.parse(request.POST.items())
429 429
430 430 try:
431 431 _form = PullRequestForm(repo.repo_id)().to_python(controls)
432 432 except formencode.Invalid as errors:
433 433 if errors.error_dict.get('revisions'):
434 434 msg = 'Revisions: %s' % errors.error_dict['revisions']
435 435 elif errors.error_dict.get('pullrequest_title'):
436 436 msg = _('Pull request requires a title with min. 3 chars')
437 437 else:
438 438 msg = _('Error creating pull request: {}').format(errors)
439 439 log.exception(msg)
440 440 h.flash(msg, 'error')
441 441
442 442 # would rather just go back to form ...
443 443 return redirect(url('pullrequest_home', repo_name=repo_name))
444 444
445 445 source_repo = _form['source_repo']
446 446 source_ref = _form['source_ref']
447 447 target_repo = _form['target_repo']
448 448 target_ref = _form['target_ref']
449 449 commit_ids = _form['revisions'][::-1]
450 450 reviewers = [
451 451 (r['user_id'], r['reasons']) for r in _form['review_members']]
452 452
453 453 # find the ancestor for this pr
454 454 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
455 455 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
456 456
457 457 source_scm = source_db_repo.scm_instance()
458 458 target_scm = target_db_repo.scm_instance()
459 459
460 460 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
461 461 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
462 462
463 463 ancestor = source_scm.get_common_ancestor(
464 464 source_commit.raw_id, target_commit.raw_id, target_scm)
465 465
466 466 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
467 467 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
468 468
469 469 pullrequest_title = _form['pullrequest_title']
470 470 title_source_ref = source_ref.split(':', 2)[1]
471 471 if not pullrequest_title:
472 472 pullrequest_title = PullRequestModel().generate_pullrequest_title(
473 473 source=source_repo,
474 474 source_ref=title_source_ref,
475 475 target=target_repo
476 476 )
477 477
478 478 description = _form['pullrequest_desc']
479 479 try:
480 480 pull_request = PullRequestModel().create(
481 481 c.rhodecode_user.user_id, source_repo, source_ref, target_repo,
482 482 target_ref, commit_ids, reviewers, pullrequest_title,
483 483 description
484 484 )
485 485 Session().commit()
486 486 h.flash(_('Successfully opened new pull request'),
487 487 category='success')
488 488 except Exception as e:
489 489 msg = _('Error occurred during sending pull request')
490 490 log.exception(msg)
491 491 h.flash(msg, category='error')
492 492 return redirect(url('pullrequest_home', repo_name=repo_name))
493 493
494 494 return redirect(url('pullrequest_show', repo_name=target_repo,
495 495 pull_request_id=pull_request.pull_request_id))
496 496
497 497 @LoginRequired()
498 498 @NotAnonymous()
499 499 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
500 500 'repository.admin')
501 501 @auth.CSRFRequired()
502 502 @jsonify
503 503 def update(self, repo_name, pull_request_id):
504 504 pull_request_id = safe_int(pull_request_id)
505 505 pull_request = PullRequest.get_or_404(pull_request_id)
506 506 # only owner or admin can update it
507 507 allowed_to_update = PullRequestModel().check_user_update(
508 508 pull_request, c.rhodecode_user)
509 509 if allowed_to_update:
510 510 controls = peppercorn.parse(request.POST.items())
511 511
512 512 if 'review_members' in controls:
513 513 self._update_reviewers(
514 514 pull_request_id, controls['review_members'])
515 515 elif str2bool(request.POST.get('update_commits', 'false')):
516 516 self._update_commits(pull_request)
517 517 elif str2bool(request.POST.get('close_pull_request', 'false')):
518 518 self._reject_close(pull_request)
519 519 elif str2bool(request.POST.get('edit_pull_request', 'false')):
520 520 self._edit_pull_request(pull_request)
521 521 else:
522 522 raise HTTPBadRequest()
523 523 return True
524 524 raise HTTPForbidden()
525 525
526 526 def _edit_pull_request(self, pull_request):
527 527 try:
528 528 PullRequestModel().edit(
529 529 pull_request, request.POST.get('title'),
530 530 request.POST.get('description'))
531 531 except ValueError:
532 532 msg = _(u'Cannot update closed pull requests.')
533 533 h.flash(msg, category='error')
534 534 return
535 535 else:
536 536 Session().commit()
537 537
538 538 msg = _(u'Pull request title & description updated.')
539 539 h.flash(msg, category='success')
540 540 return
541 541
542 542 def _update_commits(self, pull_request):
543 543 resp = PullRequestModel().update_commits(pull_request)
544 544
545 545 if resp.executed:
546 546 msg = _(
547 547 u'Pull request updated to "{source_commit_id}" with '
548 548 u'{count_added} added, {count_removed} removed commits.')
549 549 msg = msg.format(
550 550 source_commit_id=pull_request.source_ref_parts.commit_id,
551 551 count_added=len(resp.changes.added),
552 552 count_removed=len(resp.changes.removed))
553 553 h.flash(msg, category='success')
554 554
555 555 registry = get_current_registry()
556 556 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
557 557 channelstream_config = rhodecode_plugins.get('channelstream', {})
558 558 if channelstream_config.get('enabled'):
559 559 message = msg + (
560 560 ' - <a onclick="window.location.reload()">'
561 561 '<strong>{}</strong></a>'.format(_('Reload page')))
562 562 channel = '/repo${}$/pr/{}'.format(
563 563 pull_request.target_repo.repo_name,
564 564 pull_request.pull_request_id
565 565 )
566 566 payload = {
567 567 'type': 'message',
568 568 'user': 'system',
569 569 'exclude_users': [request.user.username],
570 570 'channel': channel,
571 571 'message': {
572 572 'message': message,
573 573 'level': 'success',
574 574 'topic': '/notifications'
575 575 }
576 576 }
577 577 channelstream_request(
578 578 channelstream_config, [payload], '/message',
579 579 raise_exc=False)
580 580 else:
581 581 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
582 582 warning_reasons = [
583 583 UpdateFailureReason.NO_CHANGE,
584 584 UpdateFailureReason.WRONG_REF_TPYE,
585 585 ]
586 586 category = 'warning' if resp.reason in warning_reasons else 'error'
587 587 h.flash(msg, category=category)
588 588
589 589 @auth.CSRFRequired()
590 590 @LoginRequired()
591 591 @NotAnonymous()
592 592 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
593 593 'repository.admin')
594 594 def merge(self, repo_name, pull_request_id):
595 595 """
596 596 POST /{repo_name}/pull-request/{pull_request_id}
597 597
598 598 Merge will perform a server-side merge of the specified
599 599 pull request, if the pull request is approved and mergeable.
600 600 After succesfull merging, the pull request is automatically
601 601 closed, with a relevant comment.
602 602 """
603 603 pull_request_id = safe_int(pull_request_id)
604 604 pull_request = PullRequest.get_or_404(pull_request_id)
605 605 user = c.rhodecode_user
606 606
607 607 if self._meets_merge_pre_conditions(pull_request, user):
608 608 log.debug("Pre-conditions checked, trying to merge.")
609 609 extras = vcs_operation_context(
610 610 request.environ, repo_name=pull_request.target_repo.repo_name,
611 611 username=user.username, action='push',
612 612 scm=pull_request.target_repo.repo_type)
613 613 self._merge_pull_request(pull_request, user, extras)
614 614
615 615 return redirect(url(
616 616 'pullrequest_show',
617 617 repo_name=pull_request.target_repo.repo_name,
618 618 pull_request_id=pull_request.pull_request_id))
619 619
620 620 def _meets_merge_pre_conditions(self, pull_request, user):
621 621 if not PullRequestModel().check_user_merge(pull_request, user):
622 622 raise HTTPForbidden()
623 623
624 624 merge_status, msg = PullRequestModel().merge_status(pull_request)
625 625 if not merge_status:
626 626 log.debug("Cannot merge, not mergeable.")
627 627 h.flash(msg, category='error')
628 628 return False
629 629
630 630 if (pull_request.calculated_review_status()
631 631 is not ChangesetStatus.STATUS_APPROVED):
632 632 log.debug("Cannot merge, approval is pending.")
633 633 msg = _('Pull request reviewer approval is pending.')
634 634 h.flash(msg, category='error')
635 635 return False
636 636 return True
637 637
638 638 def _merge_pull_request(self, pull_request, user, extras):
639 639 merge_resp = PullRequestModel().merge(
640 640 pull_request, user, extras=extras)
641 641
642 642 if merge_resp.executed:
643 643 log.debug("The merge was successful, closing the pull request.")
644 644 PullRequestModel().close_pull_request(
645 645 pull_request.pull_request_id, user)
646 646 Session().commit()
647 647 msg = _('Pull request was successfully merged and closed.')
648 648 h.flash(msg, category='success')
649 649 else:
650 650 log.debug(
651 651 "The merge was not successful. Merge response: %s",
652 652 merge_resp)
653 653 msg = PullRequestModel().merge_status_message(
654 654 merge_resp.failure_reason)
655 655 h.flash(msg, category='error')
656 656
657 657 def _update_reviewers(self, pull_request_id, review_members):
658 658 reviewers = [
659 659 (int(r['user_id']), r['reasons']) for r in review_members]
660 660 PullRequestModel().update_reviewers(pull_request_id, reviewers)
661 661 Session().commit()
662 662
663 663 def _reject_close(self, pull_request):
664 664 if pull_request.is_closed():
665 665 raise HTTPForbidden()
666 666
667 667 PullRequestModel().close_pull_request_with_comment(
668 668 pull_request, c.rhodecode_user, c.rhodecode_db_repo)
669 669 Session().commit()
670 670
671 671 @LoginRequired()
672 672 @NotAnonymous()
673 673 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
674 674 'repository.admin')
675 675 @auth.CSRFRequired()
676 676 @jsonify
677 677 def delete(self, repo_name, pull_request_id):
678 678 pull_request_id = safe_int(pull_request_id)
679 679 pull_request = PullRequest.get_or_404(pull_request_id)
680 680 # only owner can delete it !
681 681 if pull_request.author.user_id == c.rhodecode_user.user_id:
682 682 PullRequestModel().delete(pull_request)
683 683 Session().commit()
684 684 h.flash(_('Successfully deleted pull request'),
685 685 category='success')
686 686 return redirect(url('my_account_pullrequests'))
687 687 raise HTTPForbidden()
688 688
689 689 def _get_pr_version(self, pull_request_id, version=None):
690 690 pull_request_id = safe_int(pull_request_id)
691 691 at_version = None
692 692
693 693 if version and version == 'latest':
694 694 pull_request_ver = PullRequest.get(pull_request_id)
695 695 pull_request_obj = pull_request_ver
696 696 _org_pull_request_obj = pull_request_obj
697 697 at_version = 'latest'
698 698 elif version:
699 699 pull_request_ver = PullRequestVersion.get_or_404(version)
700 700 pull_request_obj = pull_request_ver
701 701 _org_pull_request_obj = pull_request_ver.pull_request
702 702 at_version = pull_request_ver.pull_request_version_id
703 703 else:
704 704 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(pull_request_id)
705 705
706 706 pull_request_display_obj = PullRequest.get_pr_display_object(
707 707 pull_request_obj, _org_pull_request_obj)
708 708 return _org_pull_request_obj, pull_request_obj, \
709 709 pull_request_display_obj, at_version
710 710
711 711 def _get_pr_version_changes(self, version, pull_request_latest):
712 712 """
713 713 Generate changes commits, and diff data based on the current pr version
714 714 """
715 715
716 716 #TODO(marcink): save those changes as JSON metadata for chaching later.
717 717
718 718 # fake the version to add the "initial" state object
719 719 pull_request_initial = PullRequest.get_pr_display_object(
720 720 pull_request_latest, pull_request_latest,
721 721 internal_methods=['get_commit', 'versions'])
722 722 pull_request_initial.revisions = []
723 723 pull_request_initial.source_repo.get_commit = types.MethodType(
724 724 lambda *a, **k: EmptyCommit(), pull_request_initial)
725 725 pull_request_initial.source_repo.scm_instance = types.MethodType(
726 726 lambda *a, **k: EmptyRepository(), pull_request_initial)
727 727
728 728 _changes_versions = [pull_request_latest] + \
729 729 list(reversed(c.versions)) + \
730 730 [pull_request_initial]
731 731
732 732 if version == 'latest':
733 733 index = 0
734 734 else:
735 735 for pos, prver in enumerate(_changes_versions):
736 736 ver = getattr(prver, 'pull_request_version_id', -1)
737 737 if ver == safe_int(version):
738 738 index = pos
739 739 break
740 740 else:
741 741 index = 0
742 742
743 743 cur_obj = _changes_versions[index]
744 744 prev_obj = _changes_versions[index + 1]
745 745
746 746 old_commit_ids = set(prev_obj.revisions)
747 747 new_commit_ids = set(cur_obj.revisions)
748 748
749 749 changes = PullRequestModel()._calculate_commit_id_changes(
750 750 old_commit_ids, new_commit_ids)
751 751
752 752 old_diff_data, new_diff_data = PullRequestModel()._generate_update_diffs(
753 753 cur_obj, prev_obj)
754 754 file_changes = PullRequestModel()._calculate_file_changes(
755 755 old_diff_data, new_diff_data)
756 756 return changes, file_changes
757 757
758 758 @LoginRequired()
759 759 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
760 760 'repository.admin')
761 761 def show(self, repo_name, pull_request_id):
762 762 pull_request_id = safe_int(pull_request_id)
763 763 version = request.GET.get('version')
764 764
765 765 (pull_request_latest,
766 766 pull_request_at_ver,
767 767 pull_request_display_obj,
768 768 at_version) = self._get_pr_version(pull_request_id, version=version)
769 769
770 770 c.template_context['pull_request_data']['pull_request_id'] = \
771 771 pull_request_id
772 772
773 773 # pull_requests repo_name we opened it against
774 774 # ie. target_repo must match
775 775 if repo_name != pull_request_at_ver.target_repo.repo_name:
776 776 raise HTTPNotFound
777 777
778 778 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
779 779 pull_request_at_ver)
780 780
781 781 pr_closed = pull_request_latest.is_closed()
782 782 if at_version and not at_version == 'latest':
783 783 c.allowed_to_change_status = False
784 784 c.allowed_to_update = False
785 785 c.allowed_to_merge = False
786 786 c.allowed_to_delete = False
787 787 c.allowed_to_comment = False
788 788 else:
789 789 c.allowed_to_change_status = PullRequestModel(). \
790 790 check_user_change_status(pull_request_at_ver, c.rhodecode_user)
791 791 c.allowed_to_update = PullRequestModel().check_user_update(
792 792 pull_request_latest, c.rhodecode_user) and not pr_closed
793 793 c.allowed_to_merge = PullRequestModel().check_user_merge(
794 794 pull_request_latest, c.rhodecode_user) and not pr_closed
795 795 c.allowed_to_delete = PullRequestModel().check_user_delete(
796 796 pull_request_latest, c.rhodecode_user) and not pr_closed
797 797 c.allowed_to_comment = not pr_closed
798 798
799 799 cc_model = ChangesetCommentsModel()
800 800
801 801 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
802 802 c.pull_request_review_status = pull_request_at_ver.calculated_review_status()
803 803 c.pr_merge_status, c.pr_merge_msg = PullRequestModel().merge_status(
804 804 pull_request_at_ver)
805 805 c.approval_msg = None
806 806 if c.pull_request_review_status != ChangesetStatus.STATUS_APPROVED:
807 807 c.approval_msg = _('Reviewer approval is pending.')
808 808 c.pr_merge_status = False
809 809
810 810 # inline comments
811 811 inline_comments = cc_model.get_inline_comments(
812 812 c.rhodecode_db_repo.repo_id, pull_request=pull_request_id)
813 813
814 814 _inline_cnt, c.inline_versions = cc_model.get_inline_comments_count(
815 815 inline_comments, version=at_version, include_aggregates=True)
816 816
817 c.versions = pull_request_display_obj.versions()
817 818 c.at_version_num = at_version if at_version and at_version != 'latest' else None
819 c.at_version_pos = ChangesetComment.get_index_from_version(
820 c.at_version_num, c.versions)
821
818 822 is_outdated = lambda co: \
819 823 not c.at_version_num \
820 824 or co.pull_request_version_id <= c.at_version_num
821 825
822 826 # inline_comments_until_version
823 827 if c.at_version_num:
824 828 # if we use version, then do not show later comments
825 829 # than current version
826 830 paths = collections.defaultdict(lambda: collections.defaultdict(list))
827 831 for fname, per_line_comments in inline_comments.iteritems():
828 832 for lno, comments in per_line_comments.iteritems():
829 833 for co in comments:
830 834 if co.pull_request_version_id and is_outdated(co):
831 835 paths[co.f_path][co.line_no].append(co)
832 836 inline_comments = paths
833 837
834 838 # outdated comments
835 839 c.outdated_cnt = 0
836 840 if ChangesetCommentsModel.use_outdated_comments(pull_request_latest):
837 841 outdated_comments = cc_model.get_outdated_comments(
838 842 c.rhodecode_db_repo.repo_id,
839 843 pull_request=pull_request_at_ver)
840 844
841 845 # Count outdated comments and check for deleted files
842 846 is_outdated = lambda co: \
843 847 not c.at_version_num \
844 848 or co.pull_request_version_id < c.at_version_num
845 849 for file_name, lines in outdated_comments.iteritems():
846 850 for comments in lines.values():
847 851 comments = [comm for comm in comments if is_outdated(comm)]
848 852 c.outdated_cnt += len(comments)
849 853
850 854 # load compare data into template context
851 855 self._load_compare_data(pull_request_at_ver, inline_comments)
852 856
853 857 # this is a hack to properly display links, when creating PR, the
854 858 # compare view and others uses different notation, and
855 859 # compare_commits.mako renders links based on the target_repo.
856 860 # We need to swap that here to generate it properly on the html side
857 861 c.target_repo = c.source_repo
858 862
859 863 # general comments
860 864 c.comments = cc_model.get_comments(
861 865 c.rhodecode_db_repo.repo_id, pull_request=pull_request_id)
862 866
863 867 if c.allowed_to_update:
864 868 force_close = ('forced_closed', _('Close Pull Request'))
865 869 statuses = ChangesetStatus.STATUSES + [force_close]
866 870 else:
867 871 statuses = ChangesetStatus.STATUSES
868 872 c.commit_statuses = statuses
869 873
870 874 c.ancestor = None # TODO: add ancestor here
871 875 c.pull_request = pull_request_display_obj
872 876 c.pull_request_latest = pull_request_latest
873 877 c.at_version = at_version
874 878
875 c.versions = pull_request_display_obj.versions()
876 879 c.changes = None
877 880 c.file_changes = None
878 881
879 882 c.show_version_changes = 1 # control flag, not used yet
880 883
881 884 if at_version and c.show_version_changes:
882 885 c.changes, c.file_changes = self._get_pr_version_changes(
883 886 version, pull_request_latest)
884 887
885 888 return render('/pullrequests/pullrequest_show.mako')
886 889
887 890 @LoginRequired()
888 891 @NotAnonymous()
889 892 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
890 893 'repository.admin')
891 894 @auth.CSRFRequired()
892 895 @jsonify
893 896 def comment(self, repo_name, pull_request_id):
894 897 pull_request_id = safe_int(pull_request_id)
895 898 pull_request = PullRequest.get_or_404(pull_request_id)
896 899 if pull_request.is_closed():
897 900 raise HTTPForbidden()
898 901
899 902 # TODO: johbo: Re-think this bit, "approved_closed" does not exist
900 903 # as a changeset status, still we want to send it in one value.
901 904 status = request.POST.get('changeset_status', None)
902 905 text = request.POST.get('text')
903 906 if status and '_closed' in status:
904 907 close_pr = True
905 908 status = status.replace('_closed', '')
906 909 else:
907 910 close_pr = False
908 911
909 912 forced = (status == 'forced')
910 913 if forced:
911 914 status = 'rejected'
912 915
913 916 allowed_to_change_status = PullRequestModel().check_user_change_status(
914 917 pull_request, c.rhodecode_user)
915 918
916 919 if status and allowed_to_change_status:
917 920 message = (_('Status change %(transition_icon)s %(status)s')
918 921 % {'transition_icon': '>',
919 922 'status': ChangesetStatus.get_status_lbl(status)})
920 923 if close_pr:
921 924 message = _('Closing with') + ' ' + message
922 925 text = text or message
923 926 comm = ChangesetCommentsModel().create(
924 927 text=text,
925 928 repo=c.rhodecode_db_repo.repo_id,
926 929 user=c.rhodecode_user.user_id,
927 930 pull_request=pull_request_id,
928 931 f_path=request.POST.get('f_path'),
929 932 line_no=request.POST.get('line'),
930 933 status_change=(ChangesetStatus.get_status_lbl(status)
931 934 if status and allowed_to_change_status else None),
932 935 status_change_type=(status
933 936 if status and allowed_to_change_status else None),
934 937 closing_pr=close_pr
935 938 )
936 939
937 940 if allowed_to_change_status:
938 941 old_calculated_status = pull_request.calculated_review_status()
939 942 # get status if set !
940 943 if status:
941 944 ChangesetStatusModel().set_status(
942 945 c.rhodecode_db_repo.repo_id,
943 946 status,
944 947 c.rhodecode_user.user_id,
945 948 comm,
946 949 pull_request=pull_request_id
947 950 )
948 951
949 952 Session().flush()
950 953 events.trigger(events.PullRequestCommentEvent(pull_request, comm))
951 954 # we now calculate the status of pull request, and based on that
952 955 # calculation we set the commits status
953 956 calculated_status = pull_request.calculated_review_status()
954 957 if old_calculated_status != calculated_status:
955 958 PullRequestModel()._trigger_pull_request_hook(
956 959 pull_request, c.rhodecode_user, 'review_status_change')
957 960
958 961 calculated_status_lbl = ChangesetStatus.get_status_lbl(
959 962 calculated_status)
960 963
961 964 if close_pr:
962 965 status_completed = (
963 966 calculated_status in [ChangesetStatus.STATUS_APPROVED,
964 967 ChangesetStatus.STATUS_REJECTED])
965 968 if forced or status_completed:
966 969 PullRequestModel().close_pull_request(
967 970 pull_request_id, c.rhodecode_user)
968 971 else:
969 972 h.flash(_('Closing pull request on other statuses than '
970 973 'rejected or approved is forbidden. '
971 974 'Calculated status from all reviewers '
972 975 'is currently: %s') % calculated_status_lbl,
973 976 category='warning')
974 977
975 978 Session().commit()
976 979
977 980 if not request.is_xhr:
978 981 return redirect(h.url('pullrequest_show', repo_name=repo_name,
979 982 pull_request_id=pull_request_id))
980 983
981 984 data = {
982 985 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
983 986 }
984 987 if comm:
985 988 c.co = comm
989 c.inline_comment = True if comm.line_no else False
986 990 data.update(comm.get_dict())
987 991 data.update({'rendered_text':
988 992 render('changeset/changeset_comment_block.mako')})
989 993
990 994 return data
991 995
992 996 @LoginRequired()
993 997 @NotAnonymous()
994 998 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
995 999 'repository.admin')
996 1000 @auth.CSRFRequired()
997 1001 @jsonify
998 1002 def delete_comment(self, repo_name, comment_id):
999 1003 return self._delete_comment(comment_id)
1000 1004
1001 1005 def _delete_comment(self, comment_id):
1002 1006 comment_id = safe_int(comment_id)
1003 1007 co = ChangesetComment.get_or_404(comment_id)
1004 1008 if co.pull_request.is_closed():
1005 1009 # don't allow deleting comments on closed pull request
1006 1010 raise HTTPForbidden()
1007 1011
1008 1012 is_owner = co.author.user_id == c.rhodecode_user.user_id
1009 1013 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
1010 1014 if h.HasPermissionAny('hg.admin')() or is_repo_admin or is_owner:
1011 1015 old_calculated_status = co.pull_request.calculated_review_status()
1012 1016 ChangesetCommentsModel().delete(comment=co)
1013 1017 Session().commit()
1014 1018 calculated_status = co.pull_request.calculated_review_status()
1015 1019 if old_calculated_status != calculated_status:
1016 1020 PullRequestModel()._trigger_pull_request_hook(
1017 1021 co.pull_request, c.rhodecode_user, 'review_status_change')
1018 1022 return True
1019 1023 else:
1020 1024 raise HTTPForbidden()
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
@@ -1,407 +1,432 b''
1 1 // comments.less
2 2 // For use in RhodeCode applications;
3 3 // see style guide documentation for guidelines.
4 4
5 5
6 6 // Comments
7 7 .comments {
8 8 width: 100%;
9 9 }
10 10
11 11 tr.inline-comments div {
12 12 max-width: 100%;
13 13
14 14 p {
15 15 white-space: normal;
16 16 }
17 17
18 18 code, pre, .code, dd {
19 19 overflow-x: auto;
20 20 width: 1062px;
21 21 }
22 22
23 23 dd {
24 24 width: auto;
25 25 }
26 26 }
27 27
28 28 #injected_page_comments {
29 29 .comment-previous-link,
30 30 .comment-next-link,
31 31 .comment-links-divider {
32 32 display: none;
33 33 }
34 34 }
35 35
36 36 .add-comment {
37 37 margin-bottom: 10px;
38 38 }
39 39 .hide-comment-button .add-comment {
40 40 display: none;
41 41 }
42 42
43 43 .comment-bubble {
44 44 color: @grey4;
45 45 margin-top: 4px;
46 46 margin-right: 30px;
47 47 visibility: hidden;
48 48 }
49 49
50 50 .comment {
51
52 &.comment-general {
53 border: 1px solid @grey5;
54 padding: 5px 5px 5px 5px;
55 }
56
51 57 margin: @padding 0;
52 58 padding: 4px 0 0 0;
53 59 line-height: 1em;
54 60
55 61 .rc-user {
56 62 min-width: 0;
57 63 margin: -2px .5em 0 0;
58 64 }
59 65
60 66 .meta {
61 67 position: relative;
62 68 width: 100%;
63 69 margin: 0 0 .5em 0;
70 border-bottom: 1px solid @grey5;
71 padding: 8px 0px;
64 72
65 73 &:hover .permalink {
66 74 visibility: visible;
67 75 color: @rcblue;
68 76 }
69 77 }
70 78
71 79 .author,
72 80 .date {
73 81 display: inline;
74 margin: 0 .5 0 0;
75 padding: 0 .5 0 0;
76 82
77 83 &:after {
78 84 content: ' | ';
79 85 color: @grey5;
80 86 }
81 87 }
82 88
89 .author-general img {
90 top: -3px;
91 }
92 .author-inline img {
93 top: -3px;
94 }
95
83 96 .status-change,
84 97 .permalink,
85 98 .changeset-status-lbl {
86 99 display: inline;
87 100 }
88 101
89 102 .permalink {
90 103 visibility: hidden;
91 104 }
92 105
93 106 .comment-links-divider {
94 107 display: inline;
95 108 }
96 109
97 110 .comment-links-block {
98 111 float:right;
99 112 text-align: right;
100 113 min-width: 85px;
101 114
102 115 [class^="icon-"]:before,
103 116 [class*=" icon-"]:before {
104 117 margin-left: 0;
105 118 margin-right: 0;
106 119 }
107 120 }
108 121
109 122 .comment-previous-link {
110 123 display: inline-block;
111 124
112 125 .arrow_comment_link{
113 126 cursor: pointer;
114 127 i {
115 128 font-size:10px;
116 129 }
117 130 }
118 131 .arrow_comment_link.disabled {
119 132 cursor: default;
120 133 color: @grey5;
121 134 }
122 135 }
123 136
124 137 .comment-next-link {
125 138 display: inline-block;
126 139
127 140 .arrow_comment_link{
128 141 cursor: pointer;
129 142 i {
130 143 font-size:10px;
131 144 }
132 145 }
133 146 .arrow_comment_link.disabled {
134 147 cursor: default;
135 148 color: @grey5;
136 149 }
137 150 }
138 151
139 152 .flag_status {
140 153 display: inline-block;
141 154 margin: -2px .5em 0 .25em
142 155 }
143 156
144 157 .delete-comment {
145 158 display: inline-block;
146 159 color: @rcblue;
147 160
148 161 &:hover {
149 162 cursor: pointer;
150 163 }
151 164 }
152 165
153 166
154 167 .text {
155 168 clear: both;
156 border: @border-thickness solid @grey5;
157 169 .border-radius(@border-radius);
158 170 .box-sizing(border-box);
159 171
160 172 .markdown-block p,
161 173 .rst-block p {
162 174 margin: .5em 0 !important;
163 175 // TODO: lisa: This is needed because of other rst !important rules :[
164 176 }
165 177 }
178
179 .pr-version {
180 float: left;
181 margin: 0px 4px;
182 }
183 .pr-version-inline {
184 float: left;
185 margin: 1px 4px;
186 }
187 .pr-version-num {
188 font-size: 10px;
189 }
190
166 191 }
167 192
168 193 .show-outdated-comments {
169 194 display: inline;
170 195 color: @rcblue;
171 196 }
172 197
173 198 // Comment Form
174 199 div.comment-form {
175 200 margin-top: 20px;
176 201 }
177 202
178 203 .comment-form strong {
179 204 display: block;
180 205 margin-bottom: 15px;
181 206 }
182 207
183 208 .comment-form textarea {
184 209 width: 100%;
185 210 height: 100px;
186 211 font-family: 'Monaco', 'Courier', 'Courier New', monospace;
187 212 }
188 213
189 214 form.comment-form {
190 215 margin-top: 10px;
191 216 margin-left: 10px;
192 217 }
193 218
194 219 .comment-inline-form .comment-block-ta,
195 220 .comment-form .comment-block-ta,
196 221 .comment-form .preview-box {
197 222 .border-radius(@border-radius);
198 223 .box-sizing(border-box);
199 224 background-color: white;
200 225 }
201 226
202 227 .comment-form-submit {
203 228 margin-top: 5px;
204 229 margin-left: 525px;
205 230 }
206 231
207 232 .file-comments {
208 233 display: none;
209 234 }
210 235
211 236 .comment-form .preview-box.unloaded,
212 237 .comment-inline-form .preview-box.unloaded {
213 238 height: 50px;
214 239 text-align: center;
215 240 padding: 20px;
216 241 background-color: white;
217 242 }
218 243
219 244 .comment-footer {
220 245 position: relative;
221 246 width: 100%;
222 247 min-height: 42px;
223 248
224 249 .status_box,
225 250 .cancel-button {
226 251 float: left;
227 252 display: inline-block;
228 253 }
229 254
230 255 .action-buttons {
231 256 float: right;
232 257 display: inline-block;
233 258 }
234 259 }
235 260
236 261 .comment-form {
237 262
238 263 .comment {
239 264 margin-left: 10px;
240 265 }
241 266
242 267 .comment-help {
243 268 color: @grey4;
244 269 padding: 5px 0 5px 0;
245 270 }
246 271
247 272 .comment-title {
248 273 padding: 5px 0 5px 0;
249 274 }
250 275
251 276 .comment-button {
252 277 display: inline-block;
253 278 }
254 279
255 280 .comment-button .comment-button-input {
256 281 margin-right: 0;
257 282 }
258 283
259 284 .comment-footer {
260 285 margin-bottom: 110px;
261 286 margin-top: 10px;
262 287 }
263 288 }
264 289
265 290
266 291 .comment-form-login {
267 292 .comment-help {
268 293 padding: 0.9em; //same as the button
269 294 }
270 295
271 296 div.clearfix {
272 297 clear: both;
273 298 width: 100%;
274 299 display: block;
275 300 }
276 301 }
277 302
278 303 .preview-box {
279 304 min-height: 105px;
280 305 margin-bottom: 15px;
281 306 background-color: white;
282 307 .border-radius(@border-radius);
283 308 .box-sizing(border-box);
284 309 }
285 310
286 311 .add-another-button {
287 312 margin-left: 10px;
288 313 margin-top: 10px;
289 314 margin-bottom: 10px;
290 315 }
291 316
292 317 .comment .buttons {
293 318 float: right;
294 319 margin: -1px 0px 0px 0px;
295 320 }
296 321
297 322 // Inline Comment Form
298 323 .injected_diff .comment-inline-form,
299 324 .comment-inline-form {
300 325 background-color: white;
301 326 margin-top: 10px;
302 327 margin-bottom: 20px;
303 328 }
304 329
305 330 .inline-form {
306 331 padding: 10px 7px;
307 332 }
308 333
309 334 .inline-form div {
310 335 max-width: 100%;
311 336 }
312 337
313 338 .overlay {
314 339 display: none;
315 340 position: absolute;
316 341 width: 100%;
317 342 text-align: center;
318 343 vertical-align: middle;
319 344 font-size: 16px;
320 345 background: none repeat scroll 0 0 white;
321 346
322 347 &.submitting {
323 348 display: block;
324 349 opacity: 0.5;
325 350 z-index: 100;
326 351 }
327 352 }
328 353 .comment-inline-form .overlay.submitting .overlay-text {
329 354 margin-top: 5%;
330 355 }
331 356
332 357 .comment-inline-form .clearfix,
333 358 .comment-form .clearfix {
334 359 .border-radius(@border-radius);
335 360 margin: 0px;
336 361 }
337 362
338 363 .comment-inline-form .comment-footer {
339 364 margin: 10px 0px 0px 0px;
340 365 }
341 366
342 367 .hide-inline-form-button {
343 368 margin-left: 5px;
344 369 }
345 370 .comment-button .hide-inline-form {
346 371 background: white;
347 372 }
348 373
349 374 .comment-area {
350 375 padding: 8px 12px;
351 376 border: 1px solid @grey5;
352 377 .border-radius(@border-radius);
353 378 }
354 379
355 380 .comment-area-header .nav-links {
356 381 display: flex;
357 382 flex-flow: row wrap;
358 383 -webkit-flex-flow: row wrap;
359 384 width: 100%;
360 385 }
361 386
362 387 .comment-area-footer {
363 388 display: flex;
364 389 }
365 390
366 391 .comment-footer .toolbar {
367 392
368 393 }
369 394
370 395 .nav-links {
371 396 padding: 0;
372 397 margin: 0;
373 398 list-style: none;
374 399 height: auto;
375 400 border-bottom: 1px solid @grey5;
376 401 }
377 402 .nav-links li {
378 403 display: inline-block;
379 404 }
380 405 .nav-links li:before {
381 406 content: "";
382 407 }
383 408 .nav-links li a.disabled {
384 409 cursor: not-allowed;
385 410 }
386 411
387 412 .nav-links li.active a {
388 413 border-bottom: 2px solid @rcblue;
389 414 color: #000;
390 415 font-weight: 600;
391 416 }
392 417 .nav-links li a {
393 418 display: inline-block;
394 419 padding: 0px 10px 5px 10px;
395 420 margin-bottom: -1px;
396 421 font-size: 14px;
397 422 line-height: 28px;
398 423 color: #8f8f8f;
399 424 border-bottom: 2px solid transparent;
400 425 }
401 426
402 427 .toolbar-text {
403 428 float: left;
404 429 margin: -5px 0px 0px 0px;
405 430 font-size: 12px;
406 431 }
407 432
@@ -1,4 +1,4 b''
1 1 ## this is a dummy html file for partial rendering on server and sending
2 2 ## generated output via ajax after comment submit
3 3 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
4 ${comment.comment_block(c.co, inline=True)}
4 ${comment.comment_block(c.co, inline=c.inline_comment)}
@@ -1,263 +1,297 b''
1 1 ## -*- coding: utf-8 -*-
2 2 ## usage:
3 3 ## <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
4 4 ## ${comment.comment_block(comment)}
5 5 ##
6 6 <%namespace name="base" file="/base/base.mako"/>
7 7
8 8 <%def name="comment_block(comment, inline=False)">
9 9 <% outdated_at_ver = comment.outdated_at_version(getattr(c, 'at_version', None)) %>
10 <% pr_index_ver = comment.get_index_version(getattr(c, 'versions', [])) %>
10 11
11 12 <div class="comment
12 ${'comment-inline' if inline else ''}
13 ${'comment-inline' if inline else 'comment-general'}
13 14 ${'comment-outdated' if outdated_at_ver else 'comment-current'}"
14 15 id="comment-${comment.comment_id}"
15 16 line="${comment.line_no}"
16 17 data-comment-id="${comment.comment_id}"
17 18 style="${'display: none;' if outdated_at_ver else ''}">
18 19
19 20 <div class="meta">
20 <div class="author">
21 ${base.gravatar_with_user(comment.author.email, 16)}
21 <div class="author ${'author-inline' if inline else 'author-general'}">
22 ${base.gravatar_with_user(comment.author.email, 20)}
22 23 </div>
23 24 <div class="date">
24 25 ${h.age_component(comment.modified_at, time_is_local=True)}
25 26 </div>
27 % if inline:
28 <span></span>
29 % else:
26 30 <div class="status-change">
27 31 % if comment.pull_request:
28 % if comment.outdated:
29 <a href="?version=${comment.pull_request_version_id}#comment-${comment.comment_id}">
30 ${_('Outdated comment from pull request version {}').format(comment.pull_request_version_id)}
31 </a>
32 % else:
33 <a href="${h.url('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id)}">
34 %if comment.status_change:
35 ${_('Vote on pull request #%s') % comment.pull_request.pull_request_id}:
36 %else:
37 ${_('Comment on pull request #%s') % comment.pull_request.pull_request_id}
38 %endif
39 </a>
40 % endif
32 <a href="${h.url('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id)}">
33 % if comment.status_change:
34 ${_('Vote on pull request #%s') % comment.pull_request.pull_request_id}:
35 % else:
36 ${_('Comment on pull request #%s') % comment.pull_request.pull_request_id}
37 % endif
38 </a>
41 39 % else:
42 40 % if comment.status_change:
43 41 ${_('Status change on commit')}:
44 42 % else:
45 43 ${_('Comment on commit')}
46 44 % endif
47 45 % endif
48 46 </div>
49 %if comment.status_change:
47 % endif
48
49 % if comment.status_change:
50 50 <div class="${'flag_status %s' % comment.status_change[0].status}"></div>
51 51 <div title="${_('Commit status')}" class="changeset-status-lbl">
52 52 ${comment.status_change[0].status_lbl}
53 53 </div>
54 %endif
54 % endif
55
55 56 <a class="permalink" href="#comment-${comment.comment_id}"> &para;</a>
56 57
57 58 <div class="comment-links-block">
59
60 % if inline:
61 % if outdated_at_ver:
62 <div class="pr-version-inline">
63 <a href="${h.url.current(version=comment.pull_request_version_id, anchor='comment-{}'.format(comment.comment_id))}">
64 <code class="pr-version-num">
65 outdated ${'v{}'.format(pr_index_ver)}
66 </code>
67 </a>
68 </div>
69 |
70 % endif
71 % else:
72 % if comment.pull_request_version_id and pr_index_ver:
73 |
74 <div class="pr-version">
75 % if comment.outdated:
76 <a href="?version=${comment.pull_request_version_id}#comment-${comment.comment_id}">
77 ${_('Outdated comment from pull request version {}').format(pr_index_ver)}
78 </a>
79 % else:
80 <div class="tooltip" title="${_('Comment from pull request version {0}').format(pr_index_ver)}">
81 <a href="${h.url('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id, version=comment.pull_request_version_id)}">
82 <code class="pr-version-num">
83 ${'v{}'.format(pr_index_ver)}
84 </code>
85 </a>
86 </div>
87 % endif
88 </div>
89 % endif
90 % endif
91
58 92 ## show delete comment if it's not a PR (regular comments) or it's PR that is not closed
59 93 ## only super-admin, repo admin OR comment owner can delete, also hide delete if currently viewed comment is outdated
60 94 %if not outdated_at_ver and (not comment.pull_request or (comment.pull_request and not comment.pull_request.is_closed())):
61 95 ## permissions to delete
62 96 %if h.HasPermissionAny('hg.admin')() or h.HasRepoPermissionAny('repository.admin')(c.repo_name) or comment.author.user_id == c.rhodecode_user.user_id:
63 97 ## TODO: dan: add edit comment here
64 98 <a onclick="return Rhodecode.comments.deleteComment(this);" class="delete-comment"> ${_('Delete')}</a>
65 99 %else:
66 100 <button class="btn-link" disabled="disabled"> ${_('Delete')}</button>
67 101 %endif
68 102 %else:
69 103 <button class="btn-link" disabled="disabled"> ${_('Delete')}</button>
70 104 %endif
71 105
72 106 %if not outdated_at_ver:
73 107 | <a onclick="return Rhodecode.comments.prevComment(this);" class="prev-comment"> ${_('Prev')}</a>
74 108 | <a onclick="return Rhodecode.comments.nextComment(this);" class="next-comment"> ${_('Next')}</a>
75 109 %endif
76 110
77 111 </div>
78 112 </div>
79 113 <div class="text">
80 114 ${comment.render(mentions=True)|n}
81 115 </div>
82 116
83 117 </div>
84 118 </%def>
85 119 ## generate main comments
86 120 <%def name="generate_comments(include_pull_request=False, is_pull_request=False)">
87 121 <div id="comments">
88 122 %for comment in c.comments:
89 123 <div id="comment-tr-${comment.comment_id}">
90 124 ## only render comments that are not from pull request, or from
91 125 ## pull request and a status change
92 126 %if not comment.pull_request or (comment.pull_request and comment.status_change) or include_pull_request:
93 127 ${comment_block(comment)}
94 128 %endif
95 129 </div>
96 130 %endfor
97 131 ## to anchor ajax comments
98 132 <div id="injected_page_comments"></div>
99 133 </div>
100 134 </%def>
101 135
102 136 ## MAIN COMMENT FORM
103 137 <%def name="comments(post_url, cur_status, is_pull_request=False, is_compare=False, change_status=True, form_extras=None)">
104 138
105 139 %if is_compare:
106 140 <% form_id = "comments_form_compare" %>
107 141 %else:
108 142 <% form_id = "comments_form" %>
109 143 %endif
110 144
111 145
112 146 %if is_pull_request:
113 147 <div class="pull-request-merge">
114 148 %if c.allowed_to_merge:
115 149 <div class="pull-request-wrap">
116 150 <div class="pull-right">
117 151 ${h.secure_form(url('pullrequest_merge', repo_name=c.repo_name, pull_request_id=c.pull_request.pull_request_id), id='merge_pull_request_form')}
118 152 <span data-role="merge-message">${c.pr_merge_msg} ${c.approval_msg if c.approval_msg else ''}</span>
119 153 <% merge_disabled = ' disabled' if c.pr_merge_status is False else '' %>
120 154 <input type="submit" id="merge_pull_request" value="${_('Merge Pull Request')}" class="btn${merge_disabled}"${merge_disabled}>
121 155 ${h.end_form()}
122 156 </div>
123 157 </div>
124 158 %else:
125 159 <div class="pull-request-wrap">
126 160 <div class="pull-right">
127 161 <span>${c.pr_merge_msg} ${c.approval_msg if c.approval_msg else ''}</span>
128 162 </div>
129 163 </div>
130 164 %endif
131 165 </div>
132 166 %endif
133 167 <div class="comments">
134 168 <%
135 169 if is_pull_request:
136 170 placeholder = _('Leave a comment on this Pull Request.')
137 171 elif is_compare:
138 172 placeholder = _('Leave a comment on all commits in this range.')
139 173 else:
140 174 placeholder = _('Leave a comment on this Commit.')
141 175 %>
142 176 % if c.rhodecode_user.username != h.DEFAULT_USER:
143 177 <div class="comment-form ac">
144 178 ${h.secure_form(post_url, id_=form_id)}
145 179 <div class="comment-area">
146 180 <div class="comment-area-header">
147 181 <ul class="nav-links clearfix">
148 182 <li class="active">
149 183 <a href="#edit-btn" tabindex="-1" id="edit-btn">${_('Write')}</a>
150 184 </li>
151 185 <li class="">
152 186 <a href="#preview-btn" tabindex="-1" id="preview-btn">${_('Preview')}</a>
153 187 </li>
154 188 </ul>
155 189 </div>
156 190
157 191 <div class="comment-area-write" style="display: block;">
158 192 <div id="edit-container">
159 193 <textarea id="text" name="text" class="comment-block-ta ac-input"></textarea>
160 194 </div>
161 195 <div id="preview-container" class="clearfix" style="display: none;">
162 196 <div id="preview-box" class="preview-box"></div>
163 197 </div>
164 198 </div>
165 199
166 200 <div class="comment-area-footer">
167 201 <div class="toolbar">
168 202 <div class="toolbar-text">
169 203 ${(_('Comments parsed using %s syntax with %s support.') % (
170 204 ('<a href="%s">%s</a>' % (h.url('%s_help' % c.visual.default_renderer), c.visual.default_renderer.upper())),
171 205 ('<span class="tooltip" title="%s">@mention</span>' % _('Use @username inside this text to send notification to this RhodeCode user'))
172 206 )
173 207 )|n}
174 208 </div>
175 209 </div>
176 210 </div>
177 211 </div>
178 212
179 213 <div id="comment_form_extras">
180 214 %if form_extras and isinstance(form_extras, (list, tuple)):
181 215 % for form_ex_el in form_extras:
182 216 ${form_ex_el|n}
183 217 % endfor
184 218 %endif
185 219 </div>
186 220 <div class="comment-footer">
187 221 %if change_status:
188 222 <div class="status_box">
189 223 <select id="change_status" name="changeset_status">
190 224 <option></option> # Placeholder
191 225 %for status,lbl in c.commit_statuses:
192 226 <option value="${status}" data-status="${status}">${lbl}</option>
193 227 %if is_pull_request and change_status and status in ('approved', 'rejected'):
194 228 <option value="${status}_closed" data-status="${status}">${lbl} & ${_('Closed')}</option>
195 229 %endif
196 230 %endfor
197 231 </select>
198 232 </div>
199 233 %endif
200 234 <div class="action-buttons">
201 235 <div class="comment-button">${h.submit('save', _('Comment'), class_="btn btn-success comment-button-input")}</div>
202 236 </div>
203 237 </div>
204 238 ${h.end_form()}
205 239 </div>
206 240 % else:
207 241 <div class="comment-form ac">
208 242
209 243 <div class="comment-area">
210 244 <div class="comment-area-header">
211 245 <ul class="nav-links clearfix">
212 246 <li class="active">
213 247 <a class="disabled" href="#edit-btn" disabled="disabled" onclick="return false">${_('Write')}</a>
214 248 </li>
215 249 <li class="">
216 250 <a class="disabled" href="#preview-btn" disabled="disabled" onclick="return false">${_('Preview')}</a>
217 251 </li>
218 252 </ul>
219 253 </div>
220 254
221 255 <div class="comment-area-write" style="display: block;">
222 256 <div id="edit-container">
223 257 <div style="padding: 40px 0">
224 258 ${_('You need to be logged in to leave comments.')}
225 259 <a href="${h.route_path('login', _query={'came_from': h.url.current()})}">${_('Login now')}</a>
226 260 </div>
227 261 </div>
228 262 <div id="preview-container" class="clearfix" style="display: none;">
229 263 <div id="preview-box" class="preview-box"></div>
230 264 </div>
231 265 </div>
232 266
233 267 <div class="comment-area-footer">
234 268 <div class="toolbar">
235 269 <div class="toolbar-text">
236 270 </div>
237 271 </div>
238 272 </div>
239 273 </div>
240 274
241 275 <div class="comment-footer">
242 276 </div>
243 277
244 278 </div>
245 279 % endif
246 280
247 281 </div>
248 282
249 283 <script>
250 284 // init active elements of commentForm
251 285 var commitId = templateContext.commit_data.commit_id;
252 286 var pullRequestId = templateContext.pull_request_data.pull_request_id;
253 287 var lineNo;
254 288
255 289 var mainCommentForm = new CommentForm(
256 290 "#${form_id}", commitId, pullRequestId, lineNo, true);
257 291
258 292 mainCommentForm.cm.setOption('placeholder', "${placeholder}");
259 293
260 294 mainCommentForm.initStatusChangeSelector();
261 295 bindToggleButtons();
262 296 </script>
263 297 </%def>
@@ -1,711 +1,711 b''
1 1 <%def name="diff_line_anchor(filename, line, type)"><%
2 2 return '%s_%s_%i' % (h.safeid(filename), type, line)
3 3 %></%def>
4 4
5 5 <%def name="action_class(action)">
6 6 <%
7 7 return {
8 8 '-': 'cb-deletion',
9 9 '+': 'cb-addition',
10 10 ' ': 'cb-context',
11 11 }.get(action, 'cb-empty')
12 12 %>
13 13 </%def>
14 14
15 15 <%def name="op_class(op_id)">
16 16 <%
17 17 return {
18 18 DEL_FILENODE: 'deletion', # file deleted
19 19 BIN_FILENODE: 'warning' # binary diff hidden
20 20 }.get(op_id, 'addition')
21 21 %>
22 22 </%def>
23 23
24 24 <%def name="link_for(**kw)">
25 25 <%
26 26 new_args = request.GET.mixed()
27 27 new_args.update(kw)
28 28 return h.url('', **new_args)
29 29 %>
30 30 </%def>
31 31
32 32 <%def name="render_diffset(diffset, commit=None,
33 33
34 34 # collapse all file diff entries when there are more than this amount of files in the diff
35 35 collapse_when_files_over=20,
36 36
37 37 # collapse lines in the diff when more than this amount of lines changed in the file diff
38 38 lines_changed_limit=500,
39 39
40 40 # add a ruler at to the output
41 41 ruler_at_chars=0,
42 42
43 43 # show inline comments
44 44 use_comments=False,
45 45
46 46 # disable new comments
47 47 disable_new_comments=False,
48 48
49 49 # special file-comments that were deleted in previous versions
50 50 # it's used for showing outdated comments for deleted files in a PR
51 51 deleted_files_comments=None
52 52
53 53 )">
54 54
55 55 %if use_comments:
56 56 <div id="cb-comments-inline-container-template" class="js-template">
57 57 ${inline_comments_container([])}
58 58 </div>
59 59 <div class="js-template" id="cb-comment-inline-form-template">
60 60 <div class="comment-inline-form ac">
61 61
62 62 %if c.rhodecode_user.username != h.DEFAULT_USER:
63 63 ${h.form('#', method='get')}
64 64 <div class="comment-area">
65 65 <div class="comment-area-header">
66 66 <ul class="nav-links clearfix">
67 67 <li class="active">
68 68 <a href="#edit-btn" tabindex="-1" id="edit-btn_{1}">${_('Write')}</a>
69 69 </li>
70 70 <li class="">
71 71 <a href="#preview-btn" tabindex="-1" id="preview-btn_{1}">${_('Preview')}</a>
72 72 </li>
73 73 </ul>
74 74 </div>
75 75
76 76 <div class="comment-area-write" style="display: block;">
77 77 <div id="edit-container_{1}">
78 78 <textarea id="text_{1}" name="text" class="comment-block-ta ac-input"></textarea>
79 79 </div>
80 80 <div id="preview-container_{1}" class="clearfix" style="display: none;">
81 81 <div id="preview-box_{1}" class="preview-box"></div>
82 82 </div>
83 83 </div>
84 84
85 85 <div class="comment-area-footer">
86 86 <div class="toolbar">
87 87 <div class="toolbar-text">
88 88 ${(_('Comments parsed using %s syntax with %s support.') % (
89 89 ('<a href="%s">%s</a>' % (h.url('%s_help' % c.visual.default_renderer), c.visual.default_renderer.upper())),
90 90 ('<span class="tooltip" title="%s">@mention</span>' % _('Use @username inside this text to send notification to this RhodeCode user'))
91 91 )
92 92 )|n}
93 93 </div>
94 94 </div>
95 95 </div>
96 96 </div>
97 97
98 98 <div class="comment-footer">
99 99 <div class="action-buttons">
100 100 <input type="hidden" name="f_path" value="{0}">
101 101 <input type="hidden" name="line" value="{1}">
102 102 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
103 103 ${_('Cancel')}
104 104 </button>
105 105 ${h.submit('save', _('Comment'), class_='btn btn-success save-inline-form')}
106 106 </div>
107 107 ${h.end_form()}
108 108 </div>
109 109 %else:
110 110 ${h.form('', class_='inline-form comment-form-login', method='get')}
111 111 <div class="pull-left">
112 112 <div class="comment-help pull-right">
113 113 ${_('You need to be logged in to leave comments.')} <a href="${h.route_path('login', _query={'came_from': h.url.current()})}">${_('Login now')}</a>
114 114 </div>
115 115 </div>
116 116 <div class="comment-button pull-right">
117 117 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
118 118 ${_('Cancel')}
119 119 </button>
120 120 </div>
121 121 <div class="clearfix"></div>
122 122 ${h.end_form()}
123 123 %endif
124 124 </div>
125 125 </div>
126 126
127 127 %endif
128 128 <%
129 129 collapse_all = len(diffset.files) > collapse_when_files_over
130 130 %>
131 131
132 132 %if c.diffmode == 'sideside':
133 133 <style>
134 134 .wrapper {
135 135 max-width: 1600px !important;
136 136 }
137 137 </style>
138 138 %endif
139 139
140 140 %if ruler_at_chars:
141 141 <style>
142 142 .diff table.cb .cb-content:after {
143 143 content: "";
144 144 border-left: 1px solid blue;
145 145 position: absolute;
146 146 top: 0;
147 147 height: 18px;
148 148 opacity: .2;
149 149 z-index: 10;
150 150 //## +5 to account for diff action (+/-)
151 151 left: ${ruler_at_chars + 5}ch;
152 152 </style>
153 153 %endif
154 154
155 155 <div class="diffset ${disable_new_comments and 'diffset-comments-disabled'}">
156 156 <div class="diffset-heading ${diffset.limited_diff and 'diffset-heading-warning' or ''}">
157 157 %if commit:
158 158 <div class="pull-right">
159 159 <a class="btn tooltip" title="${_('Browse Files at revision {}').format(commit.raw_id)}" href="${h.url('files_home',repo_name=diffset.repo_name, revision=commit.raw_id, f_path='')}">
160 160 ${_('Browse Files')}
161 161 </a>
162 162 </div>
163 163 %endif
164 164 <h2 class="clearinner">
165 165 %if commit:
166 166 <a class="tooltip revision" title="${h.tooltip(commit.message)}" href="${h.url('changeset_home',repo_name=c.repo_name,revision=commit.raw_id)}">${'r%s:%s' % (commit.revision,h.short_id(commit.raw_id))}</a> -
167 167 ${h.age_component(commit.date)} -
168 168 %endif
169 169 %if diffset.limited_diff:
170 170 ${_('The requested commit is too big and content was truncated.')}
171 171
172 172 ${ungettext('%(num)s file changed.', '%(num)s files changed.', diffset.changed_files) % {'num': diffset.changed_files}}
173 173 <a href="${link_for(fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a>
174 174 %else:
175 175 ${ungettext('%(num)s file changed: %(linesadd)s inserted, ''%(linesdel)s deleted',
176 176 '%(num)s files changed: %(linesadd)s inserted, %(linesdel)s deleted', diffset.changed_files) % {'num': diffset.changed_files, 'linesadd': diffset.lines_added, 'linesdel': diffset.lines_deleted}}
177 177 %endif
178 178
179 <% at_ver = getattr(c, 'at_version_num', None) %>
179 <% at_ver = getattr(c, 'at_version_pos', None) %>
180 180 % if at_ver:
181 181 <div class="pull-right">
182 ${_('Changes at version %d') % at_ver}
182 ${_('Showing changes at version %d') % at_ver}
183 183 </div>
184 184 % endif
185 185
186 186 </h2>
187 187 </div>
188 188
189 189 %if not diffset.files:
190 190 <p class="empty_data">${_('No files')}</p>
191 191 %endif
192 192
193 193 <div class="filediffs">
194 194 %for i, filediff in enumerate(diffset.files):
195 195
196 196 <%
197 197 lines_changed = filediff['patch']['stats']['added'] + filediff['patch']['stats']['deleted']
198 198 over_lines_changed_limit = lines_changed > lines_changed_limit
199 199 %>
200 200 <input ${collapse_all and 'checked' or ''} class="filediff-collapse-state" id="filediff-collapse-${id(filediff)}" type="checkbox">
201 201 <div
202 202 class="filediff"
203 203 data-f-path="${filediff['patch']['filename']}"
204 204 id="a_${h.FID('', filediff['patch']['filename'])}">
205 205 <label for="filediff-collapse-${id(filediff)}" class="filediff-heading">
206 206 <div class="filediff-collapse-indicator"></div>
207 207 ${diff_ops(filediff)}
208 208 </label>
209 209 ${diff_menu(filediff, use_comments=use_comments)}
210 210 <table class="cb cb-diff-${c.diffmode} code-highlight ${over_lines_changed_limit and 'cb-collapsed' or ''}">
211 211 %if not filediff.hunks:
212 212 %for op_id, op_text in filediff['patch']['stats']['ops'].items():
213 213 <tr>
214 214 <td class="cb-text cb-${op_class(op_id)}" ${c.diffmode == 'unified' and 'colspan=4' or 'colspan=6'}>
215 215 %if op_id == DEL_FILENODE:
216 216 ${_('File was deleted')}
217 217 %elif op_id == BIN_FILENODE:
218 218 ${_('Binary file hidden')}
219 219 %else:
220 220 ${op_text}
221 221 %endif
222 222 </td>
223 223 </tr>
224 224 %endfor
225 225 %endif
226 226 %if filediff.patch['is_limited_diff']:
227 227 <tr class="cb-warning cb-collapser">
228 228 <td class="cb-text" ${c.diffmode == 'unified' and 'colspan=4' or 'colspan=6'}>
229 229 ${_('The requested commit is too big and content was truncated.')} <a href="${link_for(fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a>
230 230 </td>
231 231 </tr>
232 232 %else:
233 233 %if over_lines_changed_limit:
234 234 <tr class="cb-warning cb-collapser">
235 235 <td class="cb-text" ${c.diffmode == 'unified' and 'colspan=4' or 'colspan=6'}>
236 236 ${_('This diff has been collapsed as it changes many lines, (%i lines changed)' % lines_changed)}
237 237 <a href="#" class="cb-expand"
238 238 onclick="$(this).closest('table').removeClass('cb-collapsed'); return false;">${_('Show them')}
239 239 </a>
240 240 <a href="#" class="cb-collapse"
241 241 onclick="$(this).closest('table').addClass('cb-collapsed'); return false;">${_('Hide them')}
242 242 </a>
243 243 </td>
244 244 </tr>
245 245 %endif
246 246 %endif
247 247
248 248 %for hunk in filediff.hunks:
249 249 <tr class="cb-hunk">
250 250 <td ${c.diffmode == 'unified' and 'colspan=3' or ''}>
251 251 ## TODO: dan: add ajax loading of more context here
252 252 ## <a href="#">
253 253 <i class="icon-more"></i>
254 254 ## </a>
255 255 </td>
256 256 <td ${c.diffmode == 'sideside' and 'colspan=5' or ''}>
257 257 @@
258 258 -${hunk.source_start},${hunk.source_length}
259 259 +${hunk.target_start},${hunk.target_length}
260 260 ${hunk.section_header}
261 261 </td>
262 262 </tr>
263 263 %if c.diffmode == 'unified':
264 264 ${render_hunk_lines_unified(hunk, use_comments=use_comments)}
265 265 %elif c.diffmode == 'sideside':
266 266 ${render_hunk_lines_sideside(hunk, use_comments=use_comments)}
267 267 %else:
268 268 <tr class="cb-line">
269 269 <td>unknown diff mode</td>
270 270 </tr>
271 271 %endif
272 272 %endfor
273 273
274 274 ## outdated comments that do not fit into currently displayed lines
275 275 % for lineno, comments in filediff.left_comments.items():
276 276
277 277 %if c.diffmode == 'unified':
278 278 <tr class="cb-line">
279 279 <td class="cb-data cb-context"></td>
280 280 <td class="cb-lineno cb-context"></td>
281 281 <td class="cb-lineno cb-context"></td>
282 282 <td class="cb-content cb-context">
283 283 ${inline_comments_container(comments)}
284 284 </td>
285 285 </tr>
286 286 %elif c.diffmode == 'sideside':
287 287 <tr class="cb-line">
288 288 <td class="cb-data cb-context"></td>
289 289 <td class="cb-lineno cb-context"></td>
290 290 <td class="cb-content cb-context"></td>
291 291
292 292 <td class="cb-data cb-context"></td>
293 293 <td class="cb-lineno cb-context"></td>
294 294 <td class="cb-content cb-context">
295 295 ${inline_comments_container(comments)}
296 296 </td>
297 297 </tr>
298 298 %endif
299 299
300 300 % endfor
301 301
302 302 </table>
303 303 </div>
304 304 %endfor
305 305
306 306 ## outdated comments that are made for a file that has been deleted
307 307 % for filename, comments_dict in (deleted_files_comments or {}).items():
308 308
309 309 <div class="filediffs filediff-outdated" style="display: none">
310 310 <input ${collapse_all and 'checked' or ''} class="filediff-collapse-state" id="filediff-collapse-${id(filename)}" type="checkbox">
311 311 <div class="filediff" data-f-path="${filename}" id="a_${h.FID('', filename)}">
312 312 <label for="filediff-collapse-${id(filename)}" class="filediff-heading">
313 313 <div class="filediff-collapse-indicator"></div>
314 314 <span class="pill">
315 315 ## file was deleted
316 316 <strong>${filename}</strong>
317 317 </span>
318 318 <span class="pill-group" style="float: left">
319 319 ## file op, doesn't need translation
320 320 <span class="pill" op="removed">removed in this version</span>
321 321 </span>
322 322 <a class="pill filediff-anchor" href="#a_${h.FID('', filename)}">ΒΆ</a>
323 323 <span class="pill-group" style="float: right">
324 324 <span class="pill" op="deleted">-${comments_dict['stats']}</span>
325 325 </span>
326 326 </label>
327 327
328 328 <table class="cb cb-diff-${c.diffmode} code-highlight ${over_lines_changed_limit and 'cb-collapsed' or ''}">
329 329 <tr>
330 330 % if c.diffmode == 'unified':
331 331 <td></td>
332 332 %endif
333 333
334 334 <td></td>
335 335 <td class="cb-text cb-${op_class(BIN_FILENODE)}" ${c.diffmode == 'unified' and 'colspan=4' or 'colspan=5'}>
336 336 ${_('File was deleted in this version, and outdated comments were made on it')}
337 337 </td>
338 338 </tr>
339 339 %if c.diffmode == 'unified':
340 340 <tr class="cb-line">
341 341 <td class="cb-data cb-context"></td>
342 342 <td class="cb-lineno cb-context"></td>
343 343 <td class="cb-lineno cb-context"></td>
344 344 <td class="cb-content cb-context">
345 345 ${inline_comments_container(comments_dict['comments'])}
346 346 </td>
347 347 </tr>
348 348 %elif c.diffmode == 'sideside':
349 349 <tr class="cb-line">
350 350 <td class="cb-data cb-context"></td>
351 351 <td class="cb-lineno cb-context"></td>
352 352 <td class="cb-content cb-context"></td>
353 353
354 354 <td class="cb-data cb-context"></td>
355 355 <td class="cb-lineno cb-context"></td>
356 356 <td class="cb-content cb-context">
357 357 ${inline_comments_container(comments_dict['comments'])}
358 358 </td>
359 359 </tr>
360 360 %endif
361 361 </table>
362 362 </div>
363 363 </div>
364 364 % endfor
365 365
366 366 </div>
367 367 </div>
368 368 </%def>
369 369
370 370 <%def name="diff_ops(filediff)">
371 371 <%
372 372 stats = filediff['patch']['stats']
373 373 from rhodecode.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
374 374 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE
375 375 %>
376 376 <span class="pill">
377 377 %if filediff.source_file_path and filediff.target_file_path:
378 378 %if filediff.source_file_path != filediff.target_file_path:
379 379 ## file was renamed
380 380 <strong>${filediff.target_file_path}</strong> β¬… <del>${filediff.source_file_path}</del>
381 381 %else:
382 382 ## file was modified
383 383 <strong>${filediff.source_file_path}</strong>
384 384 %endif
385 385 %else:
386 386 %if filediff.source_file_path:
387 387 ## file was deleted
388 388 <strong>${filediff.source_file_path}</strong>
389 389 %else:
390 390 ## file was added
391 391 <strong>${filediff.target_file_path}</strong>
392 392 %endif
393 393 %endif
394 394 </span>
395 395 <span class="pill-group" style="float: left">
396 396 %if filediff.patch['is_limited_diff']:
397 397 <span class="pill tooltip" op="limited" title="The stats for this diff are not complete">limited diff</span>
398 398 %endif
399 399 %if RENAMED_FILENODE in stats['ops']:
400 400 <span class="pill" op="renamed">renamed</span>
401 401 %endif
402 402
403 403 %if NEW_FILENODE in stats['ops']:
404 404 <span class="pill" op="created">created</span>
405 405 %if filediff['target_mode'].startswith('120'):
406 406 <span class="pill" op="symlink">symlink</span>
407 407 %else:
408 408 <span class="pill" op="mode">${nice_mode(filediff['target_mode'])}</span>
409 409 %endif
410 410 %endif
411 411
412 412 %if DEL_FILENODE in stats['ops']:
413 413 <span class="pill" op="removed">removed</span>
414 414 %endif
415 415
416 416 %if CHMOD_FILENODE in stats['ops']:
417 417 <span class="pill" op="mode">
418 418 ${nice_mode(filediff['source_mode'])} ➑ ${nice_mode(filediff['target_mode'])}
419 419 </span>
420 420 %endif
421 421 </span>
422 422
423 423 <a class="pill filediff-anchor" href="#a_${h.FID('', filediff.patch['filename'])}">ΒΆ</a>
424 424
425 425 <span class="pill-group" style="float: right">
426 426 %if BIN_FILENODE in stats['ops']:
427 427 <span class="pill" op="binary">binary</span>
428 428 %if MOD_FILENODE in stats['ops']:
429 429 <span class="pill" op="modified">modified</span>
430 430 %endif
431 431 %endif
432 432 %if stats['added']:
433 433 <span class="pill" op="added">+${stats['added']}</span>
434 434 %endif
435 435 %if stats['deleted']:
436 436 <span class="pill" op="deleted">-${stats['deleted']}</span>
437 437 %endif
438 438 </span>
439 439
440 440 </%def>
441 441
442 442 <%def name="nice_mode(filemode)">
443 443 ${filemode.startswith('100') and filemode[3:] or filemode}
444 444 </%def>
445 445
446 446 <%def name="diff_menu(filediff, use_comments=False)">
447 447 <div class="filediff-menu">
448 448 %if filediff.diffset.source_ref:
449 449 %if filediff.patch['operation'] in ['D', 'M']:
450 450 <a
451 451 class="tooltip"
452 452 href="${h.url('files_home',repo_name=filediff.diffset.repo_name,f_path=filediff.source_file_path,revision=filediff.diffset.source_ref)}"
453 453 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
454 454 >
455 455 ${_('Show file before')}
456 456 </a> |
457 457 %else:
458 458 <span
459 459 class="tooltip"
460 460 title="${h.tooltip(_('File no longer present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
461 461 >
462 462 ${_('Show file before')}
463 463 </span> |
464 464 %endif
465 465 %if filediff.patch['operation'] in ['A', 'M']:
466 466 <a
467 467 class="tooltip"
468 468 href="${h.url('files_home',repo_name=filediff.diffset.source_repo_name,f_path=filediff.target_file_path,revision=filediff.diffset.target_ref)}"
469 469 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
470 470 >
471 471 ${_('Show file after')}
472 472 </a> |
473 473 %else:
474 474 <span
475 475 class="tooltip"
476 476 title="${h.tooltip(_('File no longer present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
477 477 >
478 478 ${_('Show file after')}
479 479 </span> |
480 480 %endif
481 481 <a
482 482 class="tooltip"
483 483 title="${h.tooltip(_('Raw diff'))}"
484 484 href="${h.url('files_diff_home',repo_name=filediff.diffset.repo_name,f_path=filediff.target_file_path,diff2=filediff.diffset.target_ref,diff1=filediff.diffset.source_ref,diff='raw')}"
485 485 >
486 486 ${_('Raw diff')}
487 487 </a> |
488 488 <a
489 489 class="tooltip"
490 490 title="${h.tooltip(_('Download diff'))}"
491 491 href="${h.url('files_diff_home',repo_name=filediff.diffset.repo_name,f_path=filediff.target_file_path,diff2=filediff.diffset.target_ref,diff1=filediff.diffset.source_ref,diff='download')}"
492 492 >
493 493 ${_('Download diff')}
494 494 </a>
495 495 % if use_comments:
496 496 |
497 497 % endif
498 498
499 499 ## TODO: dan: refactor ignorews_url and context_url into the diff renderer same as diffmode=unified/sideside. Also use ajax to load more context (by clicking hunks)
500 500 %if hasattr(c, 'ignorews_url'):
501 501 ${c.ignorews_url(request.GET, h.FID('', filediff['patch']['filename']))}
502 502 %endif
503 503 %if hasattr(c, 'context_url'):
504 504 ${c.context_url(request.GET, h.FID('', filediff['patch']['filename']))}
505 505 %endif
506 506
507 507 %if use_comments:
508 508 <a href="#" onclick="return Rhodecode.comments.toggleComments(this);">
509 509 <span class="show-comment-button">${_('Show comments')}</span><span class="hide-comment-button">${_('Hide comments')}</span>
510 510 </a>
511 511 %endif
512 512 %endif
513 513 </div>
514 514 </%def>
515 515
516 516
517 517 <%namespace name="commentblock" file="/changeset/changeset_file_comment.mako"/>
518 518 <%def name="inline_comments_container(comments)">
519 519 <div class="inline-comments">
520 520 %for comment in comments:
521 521 ${commentblock.comment_block(comment, inline=True)}
522 522 %endfor
523 523
524 524 % if comments and comments[-1].outdated:
525 525 <span class="btn btn-secondary cb-comment-add-button comment-outdated}"
526 526 style="display: none;}">
527 527 ${_('Add another comment')}
528 528 </span>
529 529 % else:
530 530 <span onclick="return Rhodecode.comments.createComment(this)"
531 531 class="btn btn-secondary cb-comment-add-button">
532 532 ${_('Add another comment')}
533 533 </span>
534 534 % endif
535 535
536 536 </div>
537 537 </%def>
538 538
539 539
540 540 <%def name="render_hunk_lines_sideside(hunk, use_comments=False)">
541 541 %for i, line in enumerate(hunk.sideside):
542 542 <%
543 543 old_line_anchor, new_line_anchor = None, None
544 544 if line.original.lineno:
545 545 old_line_anchor = diff_line_anchor(hunk.filediff.source_file_path, line.original.lineno, 'o')
546 546 if line.modified.lineno:
547 547 new_line_anchor = diff_line_anchor(hunk.filediff.target_file_path, line.modified.lineno, 'n')
548 548 %>
549 549
550 550 <tr class="cb-line">
551 551 <td class="cb-data ${action_class(line.original.action)}"
552 552 data-line-number="${line.original.lineno}"
553 553 >
554 554 <div>
555 555 %if line.original.comments:
556 556 <i class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
557 557 %endif
558 558 </div>
559 559 </td>
560 560 <td class="cb-lineno ${action_class(line.original.action)}"
561 561 data-line-number="${line.original.lineno}"
562 562 %if old_line_anchor:
563 563 id="${old_line_anchor}"
564 564 %endif
565 565 >
566 566 %if line.original.lineno:
567 567 <a name="${old_line_anchor}" href="#${old_line_anchor}">${line.original.lineno}</a>
568 568 %endif
569 569 </td>
570 570 <td class="cb-content ${action_class(line.original.action)}"
571 571 data-line-number="o${line.original.lineno}"
572 572 >
573 573 %if use_comments and line.original.lineno:
574 574 ${render_add_comment_button()}
575 575 %endif
576 576 <span class="cb-code">${line.original.action} ${line.original.content or '' | n}</span>
577 577 %if use_comments and line.original.lineno and line.original.comments:
578 578 ${inline_comments_container(line.original.comments)}
579 579 %endif
580 580 </td>
581 581 <td class="cb-data ${action_class(line.modified.action)}"
582 582 data-line-number="${line.modified.lineno}"
583 583 >
584 584 <div>
585 585 %if line.modified.comments:
586 586 <i class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
587 587 %endif
588 588 </div>
589 589 </td>
590 590 <td class="cb-lineno ${action_class(line.modified.action)}"
591 591 data-line-number="${line.modified.lineno}"
592 592 %if new_line_anchor:
593 593 id="${new_line_anchor}"
594 594 %endif
595 595 >
596 596 %if line.modified.lineno:
597 597 <a name="${new_line_anchor}" href="#${new_line_anchor}">${line.modified.lineno}</a>
598 598 %endif
599 599 </td>
600 600 <td class="cb-content ${action_class(line.modified.action)}"
601 601 data-line-number="n${line.modified.lineno}"
602 602 >
603 603 %if use_comments and line.modified.lineno:
604 604 ${render_add_comment_button()}
605 605 %endif
606 606 <span class="cb-code">${line.modified.action} ${line.modified.content or '' | n}</span>
607 607 %if use_comments and line.modified.lineno and line.modified.comments:
608 608 ${inline_comments_container(line.modified.comments)}
609 609 %endif
610 610 </td>
611 611 </tr>
612 612 %endfor
613 613 </%def>
614 614
615 615
616 616 <%def name="render_hunk_lines_unified(hunk, use_comments=False)">
617 617 %for old_line_no, new_line_no, action, content, comments in hunk.unified:
618 618 <%
619 619 old_line_anchor, new_line_anchor = None, None
620 620 if old_line_no:
621 621 old_line_anchor = diff_line_anchor(hunk.filediff.source_file_path, old_line_no, 'o')
622 622 if new_line_no:
623 623 new_line_anchor = diff_line_anchor(hunk.filediff.target_file_path, new_line_no, 'n')
624 624 %>
625 625 <tr class="cb-line">
626 626 <td class="cb-data ${action_class(action)}">
627 627 <div>
628 628 %if comments:
629 629 <i class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
630 630 %endif
631 631 </div>
632 632 </td>
633 633 <td class="cb-lineno ${action_class(action)}"
634 634 data-line-number="${old_line_no}"
635 635 %if old_line_anchor:
636 636 id="${old_line_anchor}"
637 637 %endif
638 638 >
639 639 %if old_line_anchor:
640 640 <a name="${old_line_anchor}" href="#${old_line_anchor}">${old_line_no}</a>
641 641 %endif
642 642 </td>
643 643 <td class="cb-lineno ${action_class(action)}"
644 644 data-line-number="${new_line_no}"
645 645 %if new_line_anchor:
646 646 id="${new_line_anchor}"
647 647 %endif
648 648 >
649 649 %if new_line_anchor:
650 650 <a name="${new_line_anchor}" href="#${new_line_anchor}">${new_line_no}</a>
651 651 %endif
652 652 </td>
653 653 <td class="cb-content ${action_class(action)}"
654 654 data-line-number="${new_line_no and 'n' or 'o'}${new_line_no or old_line_no}"
655 655 >
656 656 %if use_comments:
657 657 ${render_add_comment_button()}
658 658 %endif
659 659 <span class="cb-code">${action} ${content or '' | n}</span>
660 660 %if use_comments and comments:
661 661 ${inline_comments_container(comments)}
662 662 %endif
663 663 </td>
664 664 </tr>
665 665 %endfor
666 666 </%def>
667 667
668 668 <%def name="render_add_comment_button()">
669 669 <button class="btn btn-small btn-primary cb-comment-box-opener" onclick="return Rhodecode.comments.createComment(this)">
670 670 <span><i class="icon-comment"></i></span>
671 671 </button>
672 672 </%def>
673 673
674 674 <%def name="render_diffset_menu()">
675 675
676 676 <div class="diffset-menu clearinner">
677 677 <div class="pull-right">
678 678 <div class="btn-group">
679 679
680 680 <a
681 681 class="btn ${c.diffmode == 'sideside' and 'btn-primary'} tooltip"
682 682 title="${_('View side by side')}"
683 683 href="${h.url_replace(diffmode='sideside')}">
684 684 <span>${_('Side by Side')}</span>
685 685 </a>
686 686 <a
687 687 class="btn ${c.diffmode == 'unified' and 'btn-primary'} tooltip"
688 688 title="${_('View unified')}" href="${h.url_replace(diffmode='unified')}">
689 689 <span>${_('Unified')}</span>
690 690 </a>
691 691 </div>
692 692 </div>
693 693
694 694 <div class="pull-left">
695 695 <div class="btn-group">
696 696 <a
697 697 class="btn"
698 698 href="#"
699 699 onclick="$('input[class=filediff-collapse-state]').prop('checked', false); return false">${_('Expand All Files')}</a>
700 700 <a
701 701 class="btn"
702 702 href="#"
703 703 onclick="$('input[class=filediff-collapse-state]').prop('checked', true); return false">${_('Collapse All Files')}</a>
704 704 <a
705 705 class="btn"
706 706 href="#"
707 707 onclick="return Rhodecode.comments.toggleWideMode(this)">${_('Wide Mode Diff')}</a>
708 708 </div>
709 709 </div>
710 710 </div>
711 711 </%def>
@@ -1,638 +1,643 b''
1 1 <%inherit file="/base/base.mako"/>
2 2
3 3 <%def name="title()">
4 4 ${_('%s Pull Request #%s') % (c.repo_name, c.pull_request.pull_request_id)}
5 5 %if c.rhodecode_name:
6 6 &middot; ${h.branding(c.rhodecode_name)}
7 7 %endif
8 8 </%def>
9 9
10 10 <%def name="breadcrumbs_links()">
11 11 <span id="pr-title">
12 12 ${c.pull_request.title}
13 13 %if c.pull_request.is_closed():
14 14 (${_('Closed')})
15 15 %endif
16 16 </span>
17 17 <div id="pr-title-edit" class="input" style="display: none;">
18 18 ${h.text('pullrequest_title', id_="pr-title-input", class_="large", value=c.pull_request.title)}
19 19 </div>
20 20 </%def>
21 21
22 22 <%def name="menu_bar_nav()">
23 23 ${self.menu_items(active='repositories')}
24 24 </%def>
25 25
26 26 <%def name="menu_bar_subnav()">
27 27 ${self.repo_menu(active='showpullrequest')}
28 28 </%def>
29 29
30 30 <%def name="main()">
31 31
32 32 <script type="text/javascript">
33 33 // TODO: marcink switch this to pyroutes
34 34 AJAX_COMMENT_DELETE_URL = "${url('pullrequest_comment_delete',repo_name=c.repo_name,comment_id='__COMMENT_ID__')}";
35 35 templateContext.pull_request_data.pull_request_id = ${c.pull_request.pull_request_id};
36 36 </script>
37 37 <div class="box">
38 38 <div class="title">
39 39 ${self.repo_page_title(c.rhodecode_db_repo)}
40 40 </div>
41 41
42 42 ${self.breadcrumbs()}
43 43
44 44 <div class="box pr-summary">
45 45 <div class="summary-details block-left">
46 46 <% summary = lambda n:{False:'summary-short'}.get(n) %>
47 47 <div class="pr-details-title">
48 48 <a href="${h.url('pull_requests_global', pull_request_id=c.pull_request.pull_request_id)}">${_('Pull request #%s') % c.pull_request.pull_request_id}</a> ${_('From')} ${h.format_date(c.pull_request.created_on)}
49 49 %if c.allowed_to_update:
50 50 <div id="delete_pullrequest" class="pull-right action_button ${'' if c.allowed_to_delete else 'disabled' }" style="clear:inherit;padding: 0">
51 51 % if c.allowed_to_delete:
52 52 ${h.secure_form(url('pullrequest_delete', repo_name=c.pull_request.target_repo.repo_name, pull_request_id=c.pull_request.pull_request_id),method='delete')}
53 53 ${h.submit('remove_%s' % c.pull_request.pull_request_id, _('Delete'),
54 54 class_="btn btn-link btn-danger",onclick="return confirm('"+_('Confirm to delete this pull request')+"');")}
55 55 ${h.end_form()}
56 56 % else:
57 57 ${_('Delete')}
58 58 % endif
59 59 </div>
60 60 <div id="open_edit_pullrequest" class="pull-right action_button">${_('Edit')}</div>
61 61 <div id="close_edit_pullrequest" class="pull-right action_button" style="display: none;padding: 0">${_('Cancel')}</div>
62 62 %endif
63 63 </div>
64 64
65 65 <div id="summary" class="fields pr-details-content">
66 66 <div class="field">
67 67 <div class="label-summary">
68 68 <label>${_('Origin')}:</label>
69 69 </div>
70 70 <div class="input">
71 71 <div class="pr-origininfo">
72 72 ## branch link is only valid if it is a branch
73 73 <span class="tag">
74 74 %if c.pull_request.source_ref_parts.type == 'branch':
75 75 <a href="${h.url('changelog_home', repo_name=c.pull_request.source_repo.repo_name, branch=c.pull_request.source_ref_parts.name)}">${c.pull_request.source_ref_parts.type}: ${c.pull_request.source_ref_parts.name}</a>
76 76 %else:
77 77 ${c.pull_request.source_ref_parts.type}: ${c.pull_request.source_ref_parts.name}
78 78 %endif
79 79 </span>
80 80 <span class="clone-url">
81 81 <a href="${h.url('summary_home', repo_name=c.pull_request.source_repo.repo_name)}">${c.pull_request.source_repo.clone_url()}</a>
82 82 </span>
83 83 </div>
84 84 <div class="pr-pullinfo">
85 85 %if h.is_hg(c.pull_request.source_repo):
86 86 <input type="text" value="hg pull -r ${h.short_id(c.source_ref)} ${c.pull_request.source_repo.clone_url()}" readonly="readonly">
87 87 %elif h.is_git(c.pull_request.source_repo):
88 88 <input type="text" value="git pull ${c.pull_request.source_repo.clone_url()} ${c.pull_request.source_ref_parts.name}" readonly="readonly">
89 89 %endif
90 90 </div>
91 91 </div>
92 92 </div>
93 93 <div class="field">
94 94 <div class="label-summary">
95 95 <label>${_('Target')}:</label>
96 96 </div>
97 97 <div class="input">
98 98 <div class="pr-targetinfo">
99 99 ## branch link is only valid if it is a branch
100 100 <span class="tag">
101 101 %if c.pull_request.target_ref_parts.type == 'branch':
102 102 <a href="${h.url('changelog_home', repo_name=c.pull_request.target_repo.repo_name, branch=c.pull_request.target_ref_parts.name)}">${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}</a>
103 103 %else:
104 104 ${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}
105 105 %endif
106 106 </span>
107 107 <span class="clone-url">
108 108 <a href="${h.url('summary_home', repo_name=c.pull_request.target_repo.repo_name)}">${c.pull_request.target_repo.clone_url()}</a>
109 109 </span>
110 110 </div>
111 111 </div>
112 112 </div>
113 113
114 114 ## Link to the shadow repository.
115 115 <div class="field">
116 116 <div class="label-summary">
117 117 <label>${_('Merge')}:</label>
118 118 </div>
119 119 <div class="input">
120 120 % if not c.pull_request.is_closed() and c.pull_request.shadow_merge_ref:
121 121 <div class="pr-mergeinfo">
122 122 %if h.is_hg(c.pull_request.target_repo):
123 123 <input type="text" value="hg clone -u ${c.pull_request.shadow_merge_ref.name} ${c.shadow_clone_url} pull-request-${c.pull_request.pull_request_id}" readonly="readonly">
124 124 %elif h.is_git(c.pull_request.target_repo):
125 125 <input type="text" value="git clone --branch ${c.pull_request.shadow_merge_ref.name} ${c.shadow_clone_url} pull-request-${c.pull_request.pull_request_id}" readonly="readonly">
126 126 %endif
127 127 </div>
128 128 % else:
129 129 <div class="">
130 130 ${_('Shadow repository data not available')}.
131 131 </div>
132 132 % endif
133 133 </div>
134 134 </div>
135 135
136 136 <div class="field">
137 137 <div class="label-summary">
138 138 <label>${_('Review')}:</label>
139 139 </div>
140 140 <div class="input">
141 141 %if c.pull_request_review_status:
142 142 <div class="${'flag_status %s' % c.pull_request_review_status} tooltip pull-left"></div>
143 143 <span class="changeset-status-lbl tooltip">
144 144 %if c.pull_request.is_closed():
145 145 ${_('Closed')},
146 146 %endif
147 147 ${h.commit_status_lbl(c.pull_request_review_status)}
148 148 </span>
149 149 - ${ungettext('calculated based on %s reviewer vote', 'calculated based on %s reviewers votes', len(c.pull_request_reviewers)) % len(c.pull_request_reviewers)}
150 150 %endif
151 151 </div>
152 152 </div>
153 153 <div class="field">
154 154 <div class="pr-description-label label-summary">
155 155 <label>${_('Description')}:</label>
156 156 </div>
157 157 <div id="pr-desc" class="input">
158 158 <div class="pr-description">${h.urlify_commit_message(c.pull_request.description, c.repo_name)}</div>
159 159 </div>
160 160 <div id="pr-desc-edit" class="input textarea editor" style="display: none;">
161 161 <textarea id="pr-description-input" size="30">${c.pull_request.description}</textarea>
162 162 </div>
163 163 </div>
164 164
165 165 <div class="field">
166 166 <div class="label-summary">
167 167 <label>${_('Versions')} (${len(c.versions)+1}):</label>
168 168 </div>
169 169
170 170 <div class="pr-versions">
171 171 % if c.show_version_changes:
172 172 <table>
173 173 ## CURRENTLY SELECT PR VERSION
174 174 <tr class="version-pr" style="display: ${'' if c.at_version_num is None else 'none'}">
175 175 <td>
176 176 % if c.at_version in [None, 'latest']:
177 177 <i class="icon-ok link"></i>
178 178 % else:
179 179 <i class="icon-comment"></i> <code>${len(c.inline_versions[None])}</code>
180 180 % endif
181 181 </td>
182 182 <td>
183 183 <code>
184 184 % if c.versions:
185 185 <a href="${h.url.current(version='latest')}">${_('latest')}</a>
186 186 % else:
187 187 ${_('initial')}
188 188 % endif
189 189 </code>
190 190 </td>
191 191 <td>
192 192 <code>${c.pull_request_latest.source_ref_parts.commit_id[:6]}</code>
193 193 </td>
194 194 <td>
195 195 ${_('created')} ${h.age_component(c.pull_request_latest.updated_on)}
196 196 </td>
197 197 <td align="right">
198 198 % if c.versions and c.at_version_num in [None, 'latest']:
199 199 <span id="show-pr-versions" class="btn btn-link" onclick="$('.version-pr').show(); $(this).hide(); return false">${_('Show all versions')}</span>
200 200 % endif
201 201 </td>
202 202 </tr>
203 203
204 204 ## SHOW ALL VERSIONS OF PR
205 205 <% ver_pr = None %>
206 % for ver in reversed(c.pull_request.versions()):
206 % for data in reversed(list(enumerate(c.versions, 1))):
207 <% ver_pos = data[0] %>
208 <% ver = data[1] %>
207 209 <% ver_pr = ver.pull_request_version_id %>
210
208 211 <tr class="version-pr" style="display: ${'' if c.at_version == ver_pr else 'none'}">
209 212 <td>
210 213 % if c.at_version == ver_pr:
211 214 <i class="icon-ok link"></i>
212 215 % else:
213 216 <i class="icon-comment"></i> <code>${len(c.inline_versions[ver_pr])}</code>
214 217 % endif
215 218 </td>
216 219 <td>
217 <code><a href="${h.url.current(version=ver_pr)}">version ${ver_pr}</a></code>
220 <code class="tooltip" title="${_('Comment from pull request version {0}').format(ver_pos)}">
221 <a href="${h.url.current(version=ver_pr)}">v${ver_pos}</a>
222 </code>
218 223 </td>
219 224 <td>
220 225 <code>${ver.source_ref_parts.commit_id[:6]}</code>
221 226 </td>
222 227 <td>
223 228 ${_('created')} ${h.age_component(ver.updated_on)}
224 229 </td>
225 230 <td align="right">
226 231 % if c.at_version == ver_pr:
227 232 <span id="show-pr-versions" class="btn btn-link" onclick="$('.version-pr').show(); $(this).hide(); return false">${_('Show all versions')}</span>
228 233 % endif
229 234 </td>
230 235 </tr>
231 236 % endfor
232 237
233 238 ## show comment/inline comments summary
234 239 <tr>
235 240 <td>
236 241 </td>
237 242
238 243 <% inline_comm_count_ver = len(c.inline_versions[ver_pr])%>
239 244 <td colspan="4" style="border-top: 1px dashed #dbd9da">
240 245 ${_('Comments for this version')}:
241 246 %if c.comments:
242 247 <a href="#comments">${_("%d General ") % len(c.comments)}</a>
243 248 %else:
244 249 ${_("%d General ") % len(c.comments)}
245 250 %endif
246 251
247 252 <% inline_comm_count_ver = len(c.inline_versions[c.at_version_num])%>
248 253 %if inline_comm_count_ver:
249 254 , <a href="#" onclick="return Rhodecode.comments.nextComment();" id="inline-comments-counter">${_("%d Inline") % inline_comm_count_ver}</a>
250 255 %else:
251 256 , ${_("%d Inline") % inline_comm_count_ver}
252 257 %endif
253 258
254 259 %if c.outdated_cnt:
255 260 , <a href="#" onclick="showOutdated(); Rhodecode.comments.nextOutdatedComment(); return false;">${_("%d Outdated") % c.outdated_cnt}</a>
256 261 <a href="#" class="showOutdatedComments" onclick="showOutdated(this); return false;"> | ${_('show outdated comments')}</a>
257 262 <a href="#" class="hideOutdatedComments" style="display: none" onclick="hideOutdated(this); return false;"> | ${_('hide outdated comments')}</a>
258 263 %else:
259 264 , ${_("%d Outdated") % c.outdated_cnt}
260 265 %endif
261 266 </td>
262 267 </tr>
263 268
264 269 <tr>
265 270 <td></td>
266 271 <td colspan="4">
267 272 % if c.at_version:
268 273 <pre>
269 274 Changed commits:
270 275 * added: ${len(c.changes.added)}
271 276 * removed: ${len(c.changes.removed)}
272 277
273 278 % if not (c.file_changes.added+c.file_changes.modified+c.file_changes.removed):
274 279 No file changes found
275 280 % else:
276 281 Changed files:
277 282 %for file_name in c.file_changes.added:
278 283 * A <a href="#${'a_' + h.FID('', file_name)}">${file_name}</a>
279 284 %endfor
280 285 %for file_name in c.file_changes.modified:
281 286 * M <a href="#${'a_' + h.FID('', file_name)}">${file_name}</a>
282 287 %endfor
283 288 %for file_name in c.file_changes.removed:
284 289 * R ${file_name}
285 290 %endfor
286 291 % endif
287 292 </pre>
288 293 % endif
289 294 </td>
290 295 </tr>
291 296 </table>
292 297 % else:
293 298 ${_('Pull request versions not available')}.
294 299 % endif
295 300 </div>
296 301 </div>
297 302
298 303 <div id="pr-save" class="field" style="display: none;">
299 304 <div class="label-summary"></div>
300 305 <div class="input">
301 306 <span id="edit_pull_request" class="btn btn-small">${_('Save Changes')}</span>
302 307 </div>
303 308 </div>
304 309 </div>
305 310 </div>
306 311 <div>
307 312 ## AUTHOR
308 313 <div class="reviewers-title block-right">
309 314 <div class="pr-details-title">
310 315 ${_('Author')}
311 316 </div>
312 317 </div>
313 318 <div class="block-right pr-details-content reviewers">
314 319 <ul class="group_members">
315 320 <li>
316 321 ${self.gravatar_with_user(c.pull_request.author.email, 16)}
317 322 </li>
318 323 </ul>
319 324 </div>
320 325 ## REVIEWERS
321 326 <div class="reviewers-title block-right">
322 327 <div class="pr-details-title">
323 328 ${_('Pull request reviewers')}
324 329 %if c.allowed_to_update:
325 330 <span id="open_edit_reviewers" class="block-right action_button">${_('Edit')}</span>
326 331 <span id="close_edit_reviewers" class="block-right action_button" style="display: none;">${_('Close')}</span>
327 332 %endif
328 333 </div>
329 334 </div>
330 335 <div id="reviewers" class="block-right pr-details-content reviewers">
331 336 ## members goes here !
332 337 <input type="hidden" name="__start__" value="review_members:sequence">
333 338 <ul id="review_members" class="group_members">
334 339 %for member,reasons,status in c.pull_request_reviewers:
335 340 <li id="reviewer_${member.user_id}">
336 341 <div class="reviewers_member">
337 342 <div class="reviewer_status tooltip" title="${h.tooltip(h.commit_status_lbl(status[0][1].status if status else 'not_reviewed'))}">
338 343 <div class="${'flag_status %s' % (status[0][1].status if status else 'not_reviewed')} pull-left reviewer_member_status"></div>
339 344 </div>
340 345 <div id="reviewer_${member.user_id}_name" class="reviewer_name">
341 346 ${self.gravatar_with_user(member.email, 16)}
342 347 </div>
343 348 <input type="hidden" name="__start__" value="reviewer:mapping">
344 349 <input type="hidden" name="__start__" value="reasons:sequence">
345 350 %for reason in reasons:
346 351 <div class="reviewer_reason">- ${reason}</div>
347 352 <input type="hidden" name="reason" value="${reason}">
348 353
349 354 %endfor
350 355 <input type="hidden" name="__end__" value="reasons:sequence">
351 356 <input id="reviewer_${member.user_id}_input" type="hidden" value="${member.user_id}" name="user_id" />
352 357 <input type="hidden" name="__end__" value="reviewer:mapping">
353 358 %if c.allowed_to_update:
354 359 <div class="reviewer_member_remove action_button" onclick="removeReviewMember(${member.user_id}, true)" style="visibility: hidden;">
355 360 <i class="icon-remove-sign" ></i>
356 361 </div>
357 362 %endif
358 363 </div>
359 364 </li>
360 365 %endfor
361 366 </ul>
362 367 <input type="hidden" name="__end__" value="review_members:sequence">
363 368 %if not c.pull_request.is_closed():
364 369 <div id="add_reviewer_input" class='ac' style="display: none;">
365 370 %if c.allowed_to_update:
366 371 <div class="reviewer_ac">
367 372 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer'))}
368 373 <div id="reviewers_container"></div>
369 374 </div>
370 375 <div>
371 376 <span id="update_pull_request" class="btn btn-small">${_('Save Changes')}</span>
372 377 </div>
373 378 %endif
374 379 </div>
375 380 %endif
376 381 </div>
377 382 </div>
378 383 </div>
379 384 <div class="box">
380 385 ##DIFF
381 386 <div class="table" >
382 387 <div id="changeset_compare_view_content">
383 388 ##CS
384 389 % if c.missing_requirements:
385 390 <div class="box">
386 391 <div class="alert alert-warning">
387 392 <div>
388 393 <strong>${_('Missing requirements:')}</strong>
389 394 ${_('These commits cannot be displayed, because this repository uses the Mercurial largefiles extension, which was not enabled.')}
390 395 </div>
391 396 </div>
392 397 </div>
393 398 % elif c.missing_commits:
394 399 <div class="box">
395 400 <div class="alert alert-warning">
396 401 <div>
397 402 <strong>${_('Missing commits')}:</strong>
398 403 ${_('This pull request cannot be displayed, because one or more commits no longer exist in the source repository.')}
399 404 ${_('Please update this pull request, push the commits back into the source repository, or consider closing this pull request.')}
400 405 </div>
401 406 </div>
402 407 </div>
403 408 % endif
404 409 <div class="compare_view_commits_title">
405 410
406 411 <div class="pull-left">
407 412 <div class="btn-group">
408 413 <a
409 414 class="btn"
410 415 href="#"
411 416 onclick="$('.compare_select').show();$('.compare_select_hidden').hide(); return false">
412 417 ${ungettext('Expand %s commit','Expand %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
413 418 </a>
414 419 <a
415 420 class="btn"
416 421 href="#"
417 422 onclick="$('.compare_select').hide();$('.compare_select_hidden').show(); return false">
418 423 ${ungettext('Collapse %s commit','Collapse %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
419 424 </a>
420 425 </div>
421 426 </div>
422 427
423 428 <div class="pull-right">
424 429 % if c.allowed_to_update and not c.pull_request.is_closed():
425 430 <a id="update_commits" class="btn btn-primary pull-right">${_('Update commits')}</a>
426 431 % else:
427 432 <a class="tooltip btn disabled pull-right" disabled="disabled" title="${_('Update is disabled for current view')}">${_('Update commits')}</a>
428 433 % endif
429 434
430 435 </div>
431 436
432 437 </div>
433 438 % if not c.missing_commits:
434 439 <%include file="/compare/compare_commits.mako" />
435 440 <div class="cs_files">
436 441 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
437 442 ${cbdiffs.render_diffset_menu()}
438 443 ${cbdiffs.render_diffset(
439 444 c.diffset, use_comments=True,
440 445 collapse_when_files_over=30,
441 446 disable_new_comments=not c.allowed_to_comment,
442 447 deleted_files_comments=c.deleted_files_comments)}
443 448
444 449 </div>
445 450 % endif
446 451 </div>
447 452 </div>
448 453
449 454 ## template for inline comment form
450 455 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
451 456
452 457 ## render general comments
453 458 ${comment.generate_comments(include_pull_request=True, is_pull_request=True)}
454 459
455 460 % if not c.pull_request.is_closed():
456 461 ## main comment form and it status
457 462 ${comment.comments(h.url('pullrequest_comment', repo_name=c.repo_name,
458 463 pull_request_id=c.pull_request.pull_request_id),
459 464 c.pull_request_review_status,
460 465 is_pull_request=True, change_status=c.allowed_to_change_status)}
461 466 %endif
462 467
463 468 <script type="text/javascript">
464 469 if (location.hash) {
465 470 var result = splitDelimitedHash(location.hash);
466 471 var line = $('html').find(result.loc);
467 472 if (line.length > 0){
468 473 offsetScroll(line, 70);
469 474 }
470 475 }
471 476 $(function(){
472 477 ReviewerAutoComplete('user');
473 478 // custom code mirror
474 479 var codeMirrorInstance = initPullRequestsCodeMirror('#pr-description-input');
475 480
476 481 var PRDetails = {
477 482 editButton: $('#open_edit_pullrequest'),
478 483 closeButton: $('#close_edit_pullrequest'),
479 484 deleteButton: $('#delete_pullrequest'),
480 485 viewFields: $('#pr-desc, #pr-title'),
481 486 editFields: $('#pr-desc-edit, #pr-title-edit, #pr-save'),
482 487
483 488 init: function() {
484 489 var that = this;
485 490 this.editButton.on('click', function(e) { that.edit(); });
486 491 this.closeButton.on('click', function(e) { that.view(); });
487 492 },
488 493
489 494 edit: function(event) {
490 495 this.viewFields.hide();
491 496 this.editButton.hide();
492 497 this.deleteButton.hide();
493 498 this.closeButton.show();
494 499 this.editFields.show();
495 500 codeMirrorInstance.refresh();
496 501 },
497 502
498 503 view: function(event) {
499 504 this.editButton.show();
500 505 this.deleteButton.show();
501 506 this.editFields.hide();
502 507 this.closeButton.hide();
503 508 this.viewFields.show();
504 509 }
505 510 };
506 511
507 512 var ReviewersPanel = {
508 513 editButton: $('#open_edit_reviewers'),
509 514 closeButton: $('#close_edit_reviewers'),
510 515 addButton: $('#add_reviewer_input'),
511 516 removeButtons: $('.reviewer_member_remove'),
512 517
513 518 init: function() {
514 519 var that = this;
515 520 this.editButton.on('click', function(e) { that.edit(); });
516 521 this.closeButton.on('click', function(e) { that.close(); });
517 522 },
518 523
519 524 edit: function(event) {
520 525 this.editButton.hide();
521 526 this.closeButton.show();
522 527 this.addButton.show();
523 528 this.removeButtons.css('visibility', 'visible');
524 529 },
525 530
526 531 close: function(event) {
527 532 this.editButton.show();
528 533 this.closeButton.hide();
529 534 this.addButton.hide();
530 535 this.removeButtons.css('visibility', 'hidden');
531 536 }
532 537 };
533 538
534 539 PRDetails.init();
535 540 ReviewersPanel.init();
536 541
537 542 showOutdated = function(self){
538 543 $('.comment-outdated').show();
539 544 $('.filediff-outdated').show();
540 545 $('.showOutdatedComments').hide();
541 546 $('.hideOutdatedComments').show();
542 547
543 548 };
544 549
545 550 hideOutdated = function(self){
546 551 $('.comment-outdated').hide();
547 552 $('.filediff-outdated').hide();
548 553 $('.hideOutdatedComments').hide();
549 554 $('.showOutdatedComments').show();
550 555 };
551 556
552 557 $('#show-outdated-comments').on('click', function(e){
553 558 var button = $(this);
554 559 var outdated = $('.comment-outdated');
555 560
556 561 if (button.html() === "(Show)") {
557 562 button.html("(Hide)");
558 563 outdated.show();
559 564 } else {
560 565 button.html("(Show)");
561 566 outdated.hide();
562 567 }
563 568 });
564 569
565 570 $('.show-inline-comments').on('change', function(e){
566 571 var show = 'none';
567 572 var target = e.currentTarget;
568 573 if(target.checked){
569 574 show = ''
570 575 }
571 576 var boxid = $(target).attr('id_for');
572 577 var comments = $('#{0} .inline-comments'.format(boxid));
573 578 var fn_display = function(idx){
574 579 $(this).css('display', show);
575 580 };
576 581 $(comments).each(fn_display);
577 582 var btns = $('#{0} .inline-comments-button'.format(boxid));
578 583 $(btns).each(fn_display);
579 584 });
580 585
581 586 $('#merge_pull_request_form').submit(function() {
582 587 if (!$('#merge_pull_request').attr('disabled')) {
583 588 $('#merge_pull_request').attr('disabled', 'disabled');
584 589 }
585 590 return true;
586 591 });
587 592
588 593 $('#edit_pull_request').on('click', function(e){
589 594 var title = $('#pr-title-input').val();
590 595 var description = codeMirrorInstance.getValue();
591 596 editPullRequest(
592 597 "${c.repo_name}", "${c.pull_request.pull_request_id}",
593 598 title, description);
594 599 });
595 600
596 601 $('#update_pull_request').on('click', function(e){
597 602 updateReviewers(undefined, "${c.repo_name}", "${c.pull_request.pull_request_id}");
598 603 });
599 604
600 605 $('#update_commits').on('click', function(e){
601 606 var isDisabled = !$(e.currentTarget).attr('disabled');
602 607 $(e.currentTarget).text(_gettext('Updating...'));
603 608 $(e.currentTarget).attr('disabled', 'disabled');
604 609 if(isDisabled){
605 610 updateCommits("${c.repo_name}", "${c.pull_request.pull_request_id}");
606 611 }
607 612
608 613 });
609 614 // fixing issue with caches on firefox
610 615 $('#update_commits').removeAttr("disabled");
611 616
612 617 $('#close_pull_request').on('click', function(e){
613 618 closePullRequest("${c.repo_name}", "${c.pull_request.pull_request_id}");
614 619 });
615 620
616 621 $('.show-inline-comments').on('click', function(e){
617 622 var boxid = $(this).attr('data-comment-id');
618 623 var button = $(this);
619 624
620 625 if(button.hasClass("comments-visible")) {
621 626 $('#{0} .inline-comments'.format(boxid)).each(function(index){
622 627 $(this).hide();
623 628 });
624 629 button.removeClass("comments-visible");
625 630 } else {
626 631 $('#{0} .inline-comments'.format(boxid)).each(function(index){
627 632 $(this).show();
628 633 });
629 634 button.addClass("comments-visible");
630 635 }
631 636 });
632 637 })
633 638 </script>
634 639
635 640 </div>
636 641 </div>
637 642
638 643 </%def>
General Comments 0
You need to be logged in to leave comments. Login now