##// END OF EJS Templates
diffs: add new diffs to pull request page
dan -
r1159:ac92afc0 default
parent child Browse files
Show More
@@ -1,464 +1,464 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 commit controller for RhodeCode showing changes between commits
23 23 """
24 24
25 25 import logging
26 26
27 27 from collections import defaultdict
28 28 from webob.exc import HTTPForbidden, HTTPBadRequest, HTTPNotFound
29 29
30 30 from pylons import tmpl_context as c, request, response
31 31 from pylons.i18n.translation import _
32 32 from pylons.controllers.util import redirect
33 33
34 34 from rhodecode.lib import auth
35 35 from rhodecode.lib import diffs, codeblocks
36 36 from rhodecode.lib.auth import (
37 37 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous)
38 38 from rhodecode.lib.base import BaseRepoController, render
39 39 from rhodecode.lib.compat import OrderedDict
40 40 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
41 41 import rhodecode.lib.helpers as h
42 42 from rhodecode.lib.utils import action_logger, jsonify
43 43 from rhodecode.lib.utils2 import safe_unicode
44 44 from rhodecode.lib.vcs.backends.base import EmptyCommit
45 45 from rhodecode.lib.vcs.exceptions import (
46 RepositoryError, CommitDoesNotExistError)
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 c.commit_statuses = ChangesetStatus.STATUSES
202 202 c.inline_comments = []
203 203 c.inline_cnt = 0
204 204 c.files = []
205 205
206 206 c.statuses = []
207 207 c.comments = []
208 208 if len(c.commit_ranges) == 1:
209 209 commit = c.commit_ranges[0]
210 210 c.comments = ChangesetCommentsModel().get_comments(
211 211 c.rhodecode_db_repo.repo_id,
212 212 revision=commit.raw_id)
213 213 c.statuses.append(ChangesetStatusModel().get_status(
214 214 c.rhodecode_db_repo.repo_id, commit.raw_id))
215 215 # comments from PR
216 216 statuses = ChangesetStatusModel().get_statuses(
217 217 c.rhodecode_db_repo.repo_id, commit.raw_id,
218 218 with_revisions=True)
219 219 prs = set(st.pull_request for st in statuses
220 220 if st.pull_request is not None)
221 221 # from associated statuses, check the pull requests, and
222 222 # show comments from them
223 223 for pr in prs:
224 224 c.comments.extend(pr.comments)
225 225
226 226 # Iterate over ranges (default commit view is always one commit)
227 227 for commit in c.commit_ranges:
228 228 c.changes[commit.raw_id] = []
229 229
230 230 commit2 = commit
231 231 commit1 = commit.parents[0] if commit.parents else EmptyCommit()
232 232
233 233 _diff = c.rhodecode_repo.get_diff(
234 234 commit1, commit2,
235 235 ignore_whitespace=ign_whitespace_lcl, context=context_lcl)
236 236 diff_processor = diffs.DiffProcessor(
237 237 _diff, format='newdiff', diff_limit=diff_limit,
238 238 file_limit=file_limit, show_full_diff=fulldiff)
239 239
240 240 commit_changes = OrderedDict()
241 241 if method == 'show':
242 242 _parsed = diff_processor.prepare()
243 243 c.limited_diff = isinstance(_parsed, diffs.LimitedDiffContainer)
244 244
245 245 _parsed = diff_processor.prepare()
246 246
247 247 def _node_getter(commit):
248 248 def get_node(fname):
249 249 try:
250 250 return commit.get_node(fname)
251 251 except NodeDoesNotExistError:
252 252 return None
253 253 return get_node
254 254
255 255 inline_comments = ChangesetCommentsModel().get_inline_comments(
256 256 c.rhodecode_db_repo.repo_id, revision=commit.raw_id)
257 257 c.inline_cnt += len(inline_comments)
258 258
259 259 diffset = codeblocks.DiffSet(
260 260 repo_name=c.repo_name,
261 261 source_node_getter=_node_getter(commit1),
262 262 target_node_getter=_node_getter(commit2),
263 263 comments=inline_comments
264 264 ).render_patchset(_parsed, commit1.raw_id, commit2.raw_id)
265 265 c.changes[commit.raw_id] = diffset
266 266 else:
267 267 # downloads/raw we only need RAW diff nothing else
268 268 diff = diff_processor.as_raw()
269 269 c.changes[commit.raw_id] = [None, None, None, None, diff, None, None]
270 270
271 271 # sort comments by how they were generated
272 272 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
273 273
274 274
275 275 if len(c.commit_ranges) == 1:
276 276 c.commit = c.commit_ranges[0]
277 277 c.parent_tmpl = ''.join(
278 278 '# Parent %s\n' % x.raw_id for x in c.commit.parents)
279 279 if method == 'download':
280 280 response.content_type = 'text/plain'
281 281 response.content_disposition = (
282 282 'attachment; filename=%s.diff' % commit_id_range[:12])
283 283 return diff
284 284 elif method == 'patch':
285 285 response.content_type = 'text/plain'
286 286 c.diff = safe_unicode(diff)
287 287 return render('changeset/patch_changeset.html')
288 288 elif method == 'raw':
289 289 response.content_type = 'text/plain'
290 290 return diff
291 291 elif method == 'show':
292 292 if len(c.commit_ranges) == 1:
293 293 return render('changeset/changeset.html')
294 294 else:
295 295 c.ancestor = None
296 296 c.target_repo = c.rhodecode_db_repo
297 297 return render('changeset/changeset_range.html')
298 298
299 299 @LoginRequired()
300 300 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
301 301 'repository.admin')
302 302 def index(self, revision, method='show'):
303 303 return self._index(revision, method=method)
304 304
305 305 @LoginRequired()
306 306 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
307 307 'repository.admin')
308 308 def changeset_raw(self, revision):
309 309 return self._index(revision, method='raw')
310 310
311 311 @LoginRequired()
312 312 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
313 313 'repository.admin')
314 314 def changeset_patch(self, revision):
315 315 return self._index(revision, method='patch')
316 316
317 317 @LoginRequired()
318 318 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
319 319 'repository.admin')
320 320 def changeset_download(self, revision):
321 321 return self._index(revision, method='download')
322 322
323 323 @LoginRequired()
324 324 @NotAnonymous()
325 325 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
326 326 'repository.admin')
327 327 @auth.CSRFRequired()
328 328 @jsonify
329 329 def comment(self, repo_name, revision):
330 330 commit_id = revision
331 331 status = request.POST.get('changeset_status', None)
332 332 text = request.POST.get('text')
333 333 if status:
334 334 text = text or (_('Status change %(transition_icon)s %(status)s')
335 335 % {'transition_icon': '>',
336 336 'status': ChangesetStatus.get_status_lbl(status)})
337 337
338 338 multi_commit_ids = filter(
339 339 lambda s: s not in ['', None],
340 340 request.POST.get('commit_ids', '').split(','),)
341 341
342 342 commit_ids = multi_commit_ids or [commit_id]
343 343 comment = None
344 344 for current_id in filter(None, commit_ids):
345 345 c.co = comment = ChangesetCommentsModel().create(
346 346 text=text,
347 347 repo=c.rhodecode_db_repo.repo_id,
348 348 user=c.rhodecode_user.user_id,
349 349 revision=current_id,
350 350 f_path=request.POST.get('f_path'),
351 351 line_no=request.POST.get('line'),
352 352 status_change=(ChangesetStatus.get_status_lbl(status)
353 353 if status else None),
354 354 status_change_type=status
355 355 )
356 356 # get status if set !
357 357 if status:
358 358 # if latest status was from pull request and it's closed
359 359 # disallow changing status !
360 360 # dont_allow_on_closed_pull_request = True !
361 361
362 362 try:
363 363 ChangesetStatusModel().set_status(
364 364 c.rhodecode_db_repo.repo_id,
365 365 status,
366 366 c.rhodecode_user.user_id,
367 367 comment,
368 368 revision=current_id,
369 369 dont_allow_on_closed_pull_request=True
370 370 )
371 371 except StatusChangeOnClosedPullRequestError:
372 372 msg = _('Changing the status of a commit associated with '
373 373 'a closed pull request is not allowed')
374 374 log.exception(msg)
375 375 h.flash(msg, category='warning')
376 376 return redirect(h.url(
377 377 'changeset_home', repo_name=repo_name,
378 378 revision=current_id))
379 379
380 380 # finalize, commit and redirect
381 381 Session().commit()
382 382
383 383 data = {
384 384 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
385 385 }
386 386 if comment:
387 387 data.update(comment.get_dict())
388 388 data.update({'rendered_text':
389 389 render('changeset/changeset_comment_block.html')})
390 390
391 391 return data
392 392
393 393 @LoginRequired()
394 394 @NotAnonymous()
395 395 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
396 396 'repository.admin')
397 397 @auth.CSRFRequired()
398 398 def preview_comment(self):
399 399 # Technically a CSRF token is not needed as no state changes with this
400 400 # call. However, as this is a POST is better to have it, so automated
401 401 # tools don't flag it as potential CSRF.
402 402 # Post is required because the payload could be bigger than the maximum
403 403 # allowed by GET.
404 404 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
405 405 raise HTTPBadRequest()
406 406 text = request.POST.get('text')
407 407 renderer = request.POST.get('renderer') or 'rst'
408 408 if text:
409 409 return h.render(text, renderer=renderer, mentions=True)
410 410 return ''
411 411
412 412 @LoginRequired()
413 413 @NotAnonymous()
414 414 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
415 415 'repository.admin')
416 416 @auth.CSRFRequired()
417 417 @jsonify
418 418 def delete_comment(self, repo_name, comment_id):
419 419 comment = ChangesetComment.get(comment_id)
420 420 owner = (comment.author.user_id == c.rhodecode_user.user_id)
421 421 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
422 422 if h.HasPermissionAny('hg.admin')() or is_repo_admin or owner:
423 423 ChangesetCommentsModel().delete(comment=comment)
424 424 Session().commit()
425 425 return True
426 426 else:
427 427 raise HTTPForbidden()
428 428
429 429 @LoginRequired()
430 430 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
431 431 'repository.admin')
432 432 @jsonify
433 433 def changeset_info(self, repo_name, revision):
434 434 if request.is_xhr:
435 435 try:
436 436 return c.rhodecode_repo.get_commit(commit_id=revision)
437 437 except CommitDoesNotExistError as e:
438 438 return EmptyCommit(message=str(e))
439 439 else:
440 440 raise HTTPBadRequest()
441 441
442 442 @LoginRequired()
443 443 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
444 444 'repository.admin')
445 445 @jsonify
446 446 def changeset_children(self, repo_name, revision):
447 447 if request.is_xhr:
448 448 commit = c.rhodecode_repo.get_commit(commit_id=revision)
449 449 result = {"results": commit.children}
450 450 return result
451 451 else:
452 452 raise HTTPBadRequest()
453 453
454 454 @LoginRequired()
455 455 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
456 456 'repository.admin')
457 457 @jsonify
458 458 def changeset_parents(self, repo_name, revision):
459 459 if request.is_xhr:
460 460 commit = c.rhodecode_repo.get_commit(commit_id=revision)
461 461 result = {"results": commit.parents}
462 462 return result
463 463 else:
464 464 raise HTTPBadRequest()
@@ -1,889 +1,911 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 pull requests controller for rhodecode for initializing pull requests
23 23 """
24 24
25 25 import peppercorn
26 26 import formencode
27 27 import logging
28 28
29 29 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPBadRequest
30 30 from pylons import request, tmpl_context as c, url
31 31 from pylons.controllers.util import redirect
32 32 from pylons.i18n.translation import _
33 33 from pyramid.threadlocal import get_current_registry
34 34 from sqlalchemy.sql import func
35 35 from sqlalchemy.sql.expression import or_
36 36
37 37 from rhodecode import events
38 from rhodecode.lib import auth, diffs, helpers as h
38 from rhodecode.lib import auth, diffs, helpers as h, codeblocks
39 39 from rhodecode.lib.ext_json import json
40 40 from rhodecode.lib.base import (
41 41 BaseRepoController, render, vcs_operation_context)
42 42 from rhodecode.lib.auth import (
43 43 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous,
44 44 HasAcceptedRepoType, XHRRequired)
45 45 from rhodecode.lib.channelstream import channelstream_request
46 from rhodecode.lib.compat import OrderedDict
46 47 from rhodecode.lib.utils import jsonify
47 48 from rhodecode.lib.utils2 import safe_int, safe_str, str2bool, safe_unicode
48 49 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
49 50 from rhodecode.lib.vcs.exceptions import (
50 EmptyRepositoryError, CommitDoesNotExistError, RepositoryRequirementError)
51 EmptyRepositoryError, CommitDoesNotExistError, RepositoryRequirementError,
52 NodeDoesNotExistError)
51 53 from rhodecode.lib.diffs import LimitedDiffContainer
52 54 from rhodecode.model.changeset_status import ChangesetStatusModel
53 55 from rhodecode.model.comment import ChangesetCommentsModel
54 56 from rhodecode.model.db import PullRequest, ChangesetStatus, ChangesetComment, \
55 57 Repository
56 58 from rhodecode.model.forms import PullRequestForm
57 59 from rhodecode.model.meta import Session
58 60 from rhodecode.model.pull_request import PullRequestModel
59 61
60 62 log = logging.getLogger(__name__)
61 63
62 64
63 65 class PullrequestsController(BaseRepoController):
64 66 def __before__(self):
65 67 super(PullrequestsController, self).__before__()
66 68
67 def _load_compare_data(self, pull_request, enable_comments=True):
69 def _load_compare_data(self, pull_request, inline_comments, enable_comments=True):
68 70 """
69 71 Load context data needed for generating compare diff
70 72
71 73 :param pull_request: object related to the request
72 74 :param enable_comments: flag to determine if comments are included
73 75 """
74 76 source_repo = pull_request.source_repo
75 77 source_ref_id = pull_request.source_ref_parts.commit_id
76 78
77 79 target_repo = pull_request.target_repo
78 80 target_ref_id = pull_request.target_ref_parts.commit_id
79 81
80 82 # despite opening commits for bookmarks/branches/tags, we always
81 83 # convert this to rev to prevent changes after bookmark or branch change
82 84 c.source_ref_type = 'rev'
83 85 c.source_ref = source_ref_id
84 86
85 87 c.target_ref_type = 'rev'
86 88 c.target_ref = target_ref_id
87 89
88 90 c.source_repo = source_repo
89 91 c.target_repo = target_repo
90 92
91 93 c.fulldiff = bool(request.GET.get('fulldiff'))
92 94
93 95 # diff_limit is the old behavior, will cut off the whole diff
94 96 # if the limit is applied otherwise will just hide the
95 97 # big files from the front-end
96 98 diff_limit = self.cut_off_limit_diff
97 99 file_limit = self.cut_off_limit_file
98 100
99 101 pre_load = ["author", "branch", "date", "message"]
100 102
101 103 c.commit_ranges = []
102 104 source_commit = EmptyCommit()
103 105 target_commit = EmptyCommit()
104 106 c.missing_requirements = False
105 107 try:
106 108 c.commit_ranges = [
107 109 source_repo.get_commit(commit_id=rev, pre_load=pre_load)
108 110 for rev in pull_request.revisions]
109 111
110 112 c.statuses = source_repo.statuses(
111 113 [x.raw_id for x in c.commit_ranges])
112 114
113 115 target_commit = source_repo.get_commit(
114 116 commit_id=safe_str(target_ref_id))
115 117 source_commit = source_repo.get_commit(
116 118 commit_id=safe_str(source_ref_id))
117 119 except RepositoryRequirementError:
118 120 c.missing_requirements = True
119 121
122 c.changes = {}
120 123 c.missing_commits = False
121 124 if (c.missing_requirements or
122 125 isinstance(source_commit, EmptyCommit) or
123 126 source_commit == target_commit):
124 127 _parsed = []
125 128 c.missing_commits = True
126 129 else:
127 130 vcs_diff = PullRequestModel().get_diff(pull_request)
128 131 diff_processor = diffs.DiffProcessor(
129 vcs_diff, format='gitdiff', diff_limit=diff_limit,
132 vcs_diff, format='newdiff', diff_limit=diff_limit,
130 133 file_limit=file_limit, show_full_diff=c.fulldiff)
131 134 _parsed = diff_processor.prepare()
132 135
133 c.limited_diff = isinstance(_parsed, LimitedDiffContainer)
136 commit_changes = OrderedDict()
137 _parsed = diff_processor.prepare()
138 c.limited_diff = isinstance(_parsed, diffs.LimitedDiffContainer)
139
140 _parsed = diff_processor.prepare()
141
142 def _node_getter(commit):
143 def get_node(fname):
144 try:
145 return commit.get_node(fname)
146 except NodeDoesNotExistError:
147 return None
148 return get_node
149
150 c.diffset = codeblocks.DiffSet(
151 repo_name=c.repo_name,
152 source_node_getter=_node_getter(target_commit),
153 target_node_getter=_node_getter(source_commit),
154 comments=inline_comments
155 ).render_patchset(_parsed, target_commit.raw_id, source_commit.raw_id)
156
134 157
135 158 c.files = []
136 159 c.changes = {}
137 160 c.lines_added = 0
138 161 c.lines_deleted = 0
139 162 c.included_files = []
140 163 c.deleted_files = []
141 164
142 for f in _parsed:
143 st = f['stats']
144 c.lines_added += st['added']
145 c.lines_deleted += st['deleted']
165 # for f in _parsed:
166 # st = f['stats']
167 # c.lines_added += st['added']
168 # c.lines_deleted += st['deleted']
146 169
147 fid = h.FID('', f['filename'])
148 c.files.append([fid, f['operation'], f['filename'], f['stats']])
149 c.included_files.append(f['filename'])
150 html_diff = diff_processor.as_html(enable_comments=enable_comments,
151 parsed_lines=[f])
152 c.changes[fid] = [f['operation'], f['filename'], html_diff, f]
170 # fid = h.FID('', f['filename'])
171 # c.files.append([fid, f['operation'], f['filename'], f['stats']])
172 # c.included_files.append(f['filename'])
173 # html_diff = diff_processor.as_html(enable_comments=enable_comments,
174 # parsed_lines=[f])
175 # c.changes[fid] = [f['operation'], f['filename'], html_diff, f]
153 176
154 177 def _extract_ordering(self, request):
155 178 column_index = safe_int(request.GET.get('order[0][column]'))
156 179 order_dir = request.GET.get('order[0][dir]', 'desc')
157 180 order_by = request.GET.get(
158 181 'columns[%s][data][sort]' % column_index, 'name_raw')
159 182 return order_by, order_dir
160 183
161 184 @LoginRequired()
162 185 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
163 186 'repository.admin')
164 187 @HasAcceptedRepoType('git', 'hg')
165 188 def show_all(self, repo_name):
166 189 # filter types
167 190 c.active = 'open'
168 191 c.source = str2bool(request.GET.get('source'))
169 192 c.closed = str2bool(request.GET.get('closed'))
170 193 c.my = str2bool(request.GET.get('my'))
171 194 c.awaiting_review = str2bool(request.GET.get('awaiting_review'))
172 195 c.awaiting_my_review = str2bool(request.GET.get('awaiting_my_review'))
173 196 c.repo_name = repo_name
174 197
175 198 opened_by = None
176 199 if c.my:
177 200 c.active = 'my'
178 201 opened_by = [c.rhodecode_user.user_id]
179 202
180 203 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
181 204 if c.closed:
182 205 c.active = 'closed'
183 206 statuses = [PullRequest.STATUS_CLOSED]
184 207
185 208 if c.awaiting_review and not c.source:
186 209 c.active = 'awaiting'
187 210 if c.source and not c.awaiting_review:
188 211 c.active = 'source'
189 212 if c.awaiting_my_review:
190 213 c.active = 'awaiting_my'
191 214
192 215 data = self._get_pull_requests_list(
193 216 repo_name=repo_name, opened_by=opened_by, statuses=statuses)
194 217 if not request.is_xhr:
195 218 c.data = json.dumps(data['data'])
196 219 c.records_total = data['recordsTotal']
197 220 return render('/pullrequests/pullrequests.html')
198 221 else:
199 222 return json.dumps(data)
200 223
201 224 def _get_pull_requests_list(self, repo_name, opened_by, statuses):
202 225 # pagination
203 226 start = safe_int(request.GET.get('start'), 0)
204 227 length = safe_int(request.GET.get('length'), c.visual.dashboard_items)
205 228 order_by, order_dir = self._extract_ordering(request)
206 229
207 230 if c.awaiting_review:
208 231 pull_requests = PullRequestModel().get_awaiting_review(
209 232 repo_name, source=c.source, opened_by=opened_by,
210 233 statuses=statuses, offset=start, length=length,
211 234 order_by=order_by, order_dir=order_dir)
212 235 pull_requests_total_count = PullRequestModel(
213 236 ).count_awaiting_review(
214 237 repo_name, source=c.source, statuses=statuses,
215 238 opened_by=opened_by)
216 239 elif c.awaiting_my_review:
217 240 pull_requests = PullRequestModel().get_awaiting_my_review(
218 241 repo_name, source=c.source, opened_by=opened_by,
219 242 user_id=c.rhodecode_user.user_id, statuses=statuses,
220 243 offset=start, length=length, order_by=order_by,
221 244 order_dir=order_dir)
222 245 pull_requests_total_count = PullRequestModel(
223 246 ).count_awaiting_my_review(
224 247 repo_name, source=c.source, user_id=c.rhodecode_user.user_id,
225 248 statuses=statuses, opened_by=opened_by)
226 249 else:
227 250 pull_requests = PullRequestModel().get_all(
228 251 repo_name, source=c.source, opened_by=opened_by,
229 252 statuses=statuses, offset=start, length=length,
230 253 order_by=order_by, order_dir=order_dir)
231 254 pull_requests_total_count = PullRequestModel().count_all(
232 255 repo_name, source=c.source, statuses=statuses,
233 256 opened_by=opened_by)
234 257
235 258 from rhodecode.lib.utils import PartialRenderer
236 259 _render = PartialRenderer('data_table/_dt_elements.html')
237 260 data = []
238 261 for pr in pull_requests:
239 262 comments = ChangesetCommentsModel().get_all_comments(
240 263 c.rhodecode_db_repo.repo_id, pull_request=pr)
241 264
242 265 data.append({
243 266 'name': _render('pullrequest_name',
244 267 pr.pull_request_id, pr.target_repo.repo_name),
245 268 'name_raw': pr.pull_request_id,
246 269 'status': _render('pullrequest_status',
247 270 pr.calculated_review_status()),
248 271 'title': _render(
249 272 'pullrequest_title', pr.title, pr.description),
250 273 'description': h.escape(pr.description),
251 274 'updated_on': _render('pullrequest_updated_on',
252 275 h.datetime_to_time(pr.updated_on)),
253 276 'updated_on_raw': h.datetime_to_time(pr.updated_on),
254 277 'created_on': _render('pullrequest_updated_on',
255 278 h.datetime_to_time(pr.created_on)),
256 279 'created_on_raw': h.datetime_to_time(pr.created_on),
257 280 'author': _render('pullrequest_author',
258 281 pr.author.full_contact, ),
259 282 'author_raw': pr.author.full_name,
260 283 'comments': _render('pullrequest_comments', len(comments)),
261 284 'comments_raw': len(comments),
262 285 'closed': pr.is_closed(),
263 286 })
264 287 # json used to render the grid
265 288 data = ({
266 289 'data': data,
267 290 'recordsTotal': pull_requests_total_count,
268 291 'recordsFiltered': pull_requests_total_count,
269 292 })
270 293 return data
271 294
272 295 @LoginRequired()
273 296 @NotAnonymous()
274 297 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
275 298 'repository.admin')
276 299 @HasAcceptedRepoType('git', 'hg')
277 300 def index(self):
278 301 source_repo = c.rhodecode_db_repo
279 302
280 303 try:
281 304 source_repo.scm_instance().get_commit()
282 305 except EmptyRepositoryError:
283 306 h.flash(h.literal(_('There are no commits yet')),
284 307 category='warning')
285 308 redirect(url('summary_home', repo_name=source_repo.repo_name))
286 309
287 310 commit_id = request.GET.get('commit')
288 311 branch_ref = request.GET.get('branch')
289 312 bookmark_ref = request.GET.get('bookmark')
290 313
291 314 try:
292 315 source_repo_data = PullRequestModel().generate_repo_data(
293 316 source_repo, commit_id=commit_id,
294 317 branch=branch_ref, bookmark=bookmark_ref)
295 318 except CommitDoesNotExistError as e:
296 319 log.exception(e)
297 320 h.flash(_('Commit does not exist'), 'error')
298 321 redirect(url('pullrequest_home', repo_name=source_repo.repo_name))
299 322
300 323 default_target_repo = source_repo
301 324
302 325 if source_repo.parent:
303 326 parent_vcs_obj = source_repo.parent.scm_instance()
304 327 if parent_vcs_obj and not parent_vcs_obj.is_empty():
305 328 # change default if we have a parent repo
306 329 default_target_repo = source_repo.parent
307 330
308 331 target_repo_data = PullRequestModel().generate_repo_data(
309 332 default_target_repo)
310 333
311 334 selected_source_ref = source_repo_data['refs']['selected_ref']
312 335
313 336 title_source_ref = selected_source_ref.split(':', 2)[1]
314 337 c.default_title = PullRequestModel().generate_pullrequest_title(
315 338 source=source_repo.repo_name,
316 339 source_ref=title_source_ref,
317 340 target=default_target_repo.repo_name
318 341 )
319 342
320 343 c.default_repo_data = {
321 344 'source_repo_name': source_repo.repo_name,
322 345 'source_refs_json': json.dumps(source_repo_data),
323 346 'target_repo_name': default_target_repo.repo_name,
324 347 'target_refs_json': json.dumps(target_repo_data),
325 348 }
326 349 c.default_source_ref = selected_source_ref
327 350
328 351 return render('/pullrequests/pullrequest.html')
329 352
330 353 @LoginRequired()
331 354 @NotAnonymous()
332 355 @XHRRequired()
333 356 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
334 357 'repository.admin')
335 358 @jsonify
336 359 def get_repo_refs(self, repo_name, target_repo_name):
337 360 repo = Repository.get_by_repo_name(target_repo_name)
338 361 if not repo:
339 362 raise HTTPNotFound
340 363 return PullRequestModel().generate_repo_data(repo)
341 364
342 365 @LoginRequired()
343 366 @NotAnonymous()
344 367 @XHRRequired()
345 368 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
346 369 'repository.admin')
347 370 @jsonify
348 371 def get_repo_destinations(self, repo_name):
349 372 repo = Repository.get_by_repo_name(repo_name)
350 373 if not repo:
351 374 raise HTTPNotFound
352 375 filter_query = request.GET.get('query')
353 376
354 377 query = Repository.query() \
355 378 .order_by(func.length(Repository.repo_name)) \
356 379 .filter(or_(
357 380 Repository.repo_name == repo.repo_name,
358 381 Repository.fork_id == repo.repo_id))
359 382
360 383 if filter_query:
361 384 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
362 385 query = query.filter(
363 386 Repository.repo_name.ilike(ilike_expression))
364 387
365 388 add_parent = False
366 389 if repo.parent:
367 390 if filter_query in repo.parent.repo_name:
368 391 parent_vcs_obj = repo.parent.scm_instance()
369 392 if parent_vcs_obj and not parent_vcs_obj.is_empty():
370 393 add_parent = True
371 394
372 395 limit = 20 - 1 if add_parent else 20
373 396 all_repos = query.limit(limit).all()
374 397 if add_parent:
375 398 all_repos += [repo.parent]
376 399
377 400 repos = []
378 401 for obj in self.scm_model.get_repos(all_repos):
379 402 repos.append({
380 403 'id': obj['name'],
381 404 'text': obj['name'],
382 405 'type': 'repo',
383 406 'obj': obj['dbrepo']
384 407 })
385 408
386 409 data = {
387 410 'more': False,
388 411 'results': [{
389 412 'text': _('Repositories'),
390 413 'children': repos
391 414 }] if repos else []
392 415 }
393 416 return data
394 417
395 418 @LoginRequired()
396 419 @NotAnonymous()
397 420 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
398 421 'repository.admin')
399 422 @HasAcceptedRepoType('git', 'hg')
400 423 @auth.CSRFRequired()
401 424 def create(self, repo_name):
402 425 repo = Repository.get_by_repo_name(repo_name)
403 426 if not repo:
404 427 raise HTTPNotFound
405 428
406 429 controls = peppercorn.parse(request.POST.items())
407 430
408 431 try:
409 432 _form = PullRequestForm(repo.repo_id)().to_python(controls)
410 433 except formencode.Invalid as errors:
411 434 if errors.error_dict.get('revisions'):
412 435 msg = 'Revisions: %s' % errors.error_dict['revisions']
413 436 elif errors.error_dict.get('pullrequest_title'):
414 437 msg = _('Pull request requires a title with min. 3 chars')
415 438 else:
416 439 msg = _('Error creating pull request: {}').format(errors)
417 440 log.exception(msg)
418 441 h.flash(msg, 'error')
419 442
420 443 # would rather just go back to form ...
421 444 return redirect(url('pullrequest_home', repo_name=repo_name))
422 445
423 446 source_repo = _form['source_repo']
424 447 source_ref = _form['source_ref']
425 448 target_repo = _form['target_repo']
426 449 target_ref = _form['target_ref']
427 450 commit_ids = _form['revisions'][::-1]
428 451 reviewers = [
429 452 (r['user_id'], r['reasons']) for r in _form['review_members']]
430 453
431 454 # find the ancestor for this pr
432 455 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
433 456 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
434 457
435 458 source_scm = source_db_repo.scm_instance()
436 459 target_scm = target_db_repo.scm_instance()
437 460
438 461 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
439 462 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
440 463
441 464 ancestor = source_scm.get_common_ancestor(
442 465 source_commit.raw_id, target_commit.raw_id, target_scm)
443 466
444 467 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
445 468 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
446 469
447 470 pullrequest_title = _form['pullrequest_title']
448 471 title_source_ref = source_ref.split(':', 2)[1]
449 472 if not pullrequest_title:
450 473 pullrequest_title = PullRequestModel().generate_pullrequest_title(
451 474 source=source_repo,
452 475 source_ref=title_source_ref,
453 476 target=target_repo
454 477 )
455 478
456 479 description = _form['pullrequest_desc']
457 480 try:
458 481 pull_request = PullRequestModel().create(
459 482 c.rhodecode_user.user_id, source_repo, source_ref, target_repo,
460 483 target_ref, commit_ids, reviewers, pullrequest_title,
461 484 description
462 485 )
463 486 Session().commit()
464 487 h.flash(_('Successfully opened new pull request'),
465 488 category='success')
466 489 except Exception as e:
467 490 msg = _('Error occurred during sending pull request')
468 491 log.exception(msg)
469 492 h.flash(msg, category='error')
470 493 return redirect(url('pullrequest_home', repo_name=repo_name))
471 494
472 495 return redirect(url('pullrequest_show', repo_name=target_repo,
473 496 pull_request_id=pull_request.pull_request_id))
474 497
475 498 @LoginRequired()
476 499 @NotAnonymous()
477 500 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
478 501 'repository.admin')
479 502 @auth.CSRFRequired()
480 503 @jsonify
481 504 def update(self, repo_name, pull_request_id):
482 505 pull_request_id = safe_int(pull_request_id)
483 506 pull_request = PullRequest.get_or_404(pull_request_id)
484 507 # only owner or admin can update it
485 508 allowed_to_update = PullRequestModel().check_user_update(
486 509 pull_request, c.rhodecode_user)
487 510 if allowed_to_update:
488 511 controls = peppercorn.parse(request.POST.items())
489 512
490 513 if 'review_members' in controls:
491 514 self._update_reviewers(
492 515 pull_request_id, controls['review_members'])
493 516 elif str2bool(request.POST.get('update_commits', 'false')):
494 517 self._update_commits(pull_request)
495 518 elif str2bool(request.POST.get('close_pull_request', 'false')):
496 519 self._reject_close(pull_request)
497 520 elif str2bool(request.POST.get('edit_pull_request', 'false')):
498 521 self._edit_pull_request(pull_request)
499 522 else:
500 523 raise HTTPBadRequest()
501 524 return True
502 525 raise HTTPForbidden()
503 526
504 527 def _edit_pull_request(self, pull_request):
505 528 try:
506 529 PullRequestModel().edit(
507 530 pull_request, request.POST.get('title'),
508 531 request.POST.get('description'))
509 532 except ValueError:
510 533 msg = _(u'Cannot update closed pull requests.')
511 534 h.flash(msg, category='error')
512 535 return
513 536 else:
514 537 Session().commit()
515 538
516 539 msg = _(u'Pull request title & description updated.')
517 540 h.flash(msg, category='success')
518 541 return
519 542
520 543 def _update_commits(self, pull_request):
521 544 resp = PullRequestModel().update_commits(pull_request)
522 545
523 546 if resp.executed:
524 547 msg = _(
525 548 u'Pull request updated to "{source_commit_id}" with '
526 549 u'{count_added} added, {count_removed} removed commits.')
527 550 msg = msg.format(
528 551 source_commit_id=pull_request.source_ref_parts.commit_id,
529 552 count_added=len(resp.changes.added),
530 553 count_removed=len(resp.changes.removed))
531 554 h.flash(msg, category='success')
532 555
533 556 registry = get_current_registry()
534 557 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
535 558 channelstream_config = rhodecode_plugins.get('channelstream', {})
536 559 if channelstream_config.get('enabled'):
537 560 message = msg + (
538 561 ' - <a onclick="window.location.reload()">'
539 562 '<strong>{}</strong></a>'.format(_('Reload page')))
540 563 channel = '/repo${}$/pr/{}'.format(
541 564 pull_request.target_repo.repo_name,
542 565 pull_request.pull_request_id
543 566 )
544 567 payload = {
545 568 'type': 'message',
546 569 'user': 'system',
547 570 'exclude_users': [request.user.username],
548 571 'channel': channel,
549 572 'message': {
550 573 'message': message,
551 574 'level': 'success',
552 575 'topic': '/notifications'
553 576 }
554 577 }
555 578 channelstream_request(
556 579 channelstream_config, [payload], '/message',
557 580 raise_exc=False)
558 581 else:
559 582 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
560 583 warning_reasons = [
561 584 UpdateFailureReason.NO_CHANGE,
562 585 UpdateFailureReason.WRONG_REF_TPYE,
563 586 ]
564 587 category = 'warning' if resp.reason in warning_reasons else 'error'
565 588 h.flash(msg, category=category)
566 589
567 590 @auth.CSRFRequired()
568 591 @LoginRequired()
569 592 @NotAnonymous()
570 593 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
571 594 'repository.admin')
572 595 def merge(self, repo_name, pull_request_id):
573 596 """
574 597 POST /{repo_name}/pull-request/{pull_request_id}
575 598
576 599 Merge will perform a server-side merge of the specified
577 600 pull request, if the pull request is approved and mergeable.
578 601 After succesfull merging, the pull request is automatically
579 602 closed, with a relevant comment.
580 603 """
581 604 pull_request_id = safe_int(pull_request_id)
582 605 pull_request = PullRequest.get_or_404(pull_request_id)
583 606 user = c.rhodecode_user
584 607
585 608 if self._meets_merge_pre_conditions(pull_request, user):
586 609 log.debug("Pre-conditions checked, trying to merge.")
587 610 extras = vcs_operation_context(
588 611 request.environ, repo_name=pull_request.target_repo.repo_name,
589 612 username=user.username, action='push',
590 613 scm=pull_request.target_repo.repo_type)
591 614 self._merge_pull_request(pull_request, user, extras)
592 615
593 616 return redirect(url(
594 617 'pullrequest_show',
595 618 repo_name=pull_request.target_repo.repo_name,
596 619 pull_request_id=pull_request.pull_request_id))
597 620
598 621 def _meets_merge_pre_conditions(self, pull_request, user):
599 622 if not PullRequestModel().check_user_merge(pull_request, user):
600 623 raise HTTPForbidden()
601 624
602 625 merge_status, msg = PullRequestModel().merge_status(pull_request)
603 626 if not merge_status:
604 627 log.debug("Cannot merge, not mergeable.")
605 628 h.flash(msg, category='error')
606 629 return False
607 630
608 631 if (pull_request.calculated_review_status()
609 632 is not ChangesetStatus.STATUS_APPROVED):
610 633 log.debug("Cannot merge, approval is pending.")
611 634 msg = _('Pull request reviewer approval is pending.')
612 635 h.flash(msg, category='error')
613 636 return False
614 637 return True
615 638
616 639 def _merge_pull_request(self, pull_request, user, extras):
617 640 merge_resp = PullRequestModel().merge(
618 641 pull_request, user, extras=extras)
619 642
620 643 if merge_resp.executed:
621 644 log.debug("The merge was successful, closing the pull request.")
622 645 PullRequestModel().close_pull_request(
623 646 pull_request.pull_request_id, user)
624 647 Session().commit()
625 648 msg = _('Pull request was successfully merged and closed.')
626 649 h.flash(msg, category='success')
627 650 else:
628 651 log.debug(
629 652 "The merge was not successful. Merge response: %s",
630 653 merge_resp)
631 654 msg = PullRequestModel().merge_status_message(
632 655 merge_resp.failure_reason)
633 656 h.flash(msg, category='error')
634 657
635 658 def _update_reviewers(self, pull_request_id, review_members):
636 659 reviewers = [
637 660 (int(r['user_id']), r['reasons']) for r in review_members]
638 661 PullRequestModel().update_reviewers(pull_request_id, reviewers)
639 662 Session().commit()
640 663
641 664 def _reject_close(self, pull_request):
642 665 if pull_request.is_closed():
643 666 raise HTTPForbidden()
644 667
645 668 PullRequestModel().close_pull_request_with_comment(
646 669 pull_request, c.rhodecode_user, c.rhodecode_db_repo)
647 670 Session().commit()
648 671
649 672 @LoginRequired()
650 673 @NotAnonymous()
651 674 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
652 675 'repository.admin')
653 676 @auth.CSRFRequired()
654 677 @jsonify
655 678 def delete(self, repo_name, pull_request_id):
656 679 pull_request_id = safe_int(pull_request_id)
657 680 pull_request = PullRequest.get_or_404(pull_request_id)
658 681 # only owner can delete it !
659 682 if pull_request.author.user_id == c.rhodecode_user.user_id:
660 683 PullRequestModel().delete(pull_request)
661 684 Session().commit()
662 685 h.flash(_('Successfully deleted pull request'),
663 686 category='success')
664 687 return redirect(url('my_account_pullrequests'))
665 688 raise HTTPForbidden()
666 689
667 690 @LoginRequired()
668 691 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
669 692 'repository.admin')
670 693 def show(self, repo_name, pull_request_id):
671 694 pull_request_id = safe_int(pull_request_id)
672 695 c.pull_request = PullRequest.get_or_404(pull_request_id)
673 696
674 697 c.template_context['pull_request_data']['pull_request_id'] = \
675 698 pull_request_id
676 699
677 700 # pull_requests repo_name we opened it against
678 701 # ie. target_repo must match
679 702 if repo_name != c.pull_request.target_repo.repo_name:
680 703 raise HTTPNotFound
681 704
682 705 c.allowed_to_change_status = PullRequestModel(). \
683 706 check_user_change_status(c.pull_request, c.rhodecode_user)
684 707 c.allowed_to_update = PullRequestModel().check_user_update(
685 708 c.pull_request, c.rhodecode_user) and not c.pull_request.is_closed()
686 709 c.allowed_to_merge = PullRequestModel().check_user_merge(
687 710 c.pull_request, c.rhodecode_user) and not c.pull_request.is_closed()
688 711 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
689 712 c.pull_request)
690 713 c.allowed_to_delete = PullRequestModel().check_user_delete(
691 714 c.pull_request, c.rhodecode_user) and not c.pull_request.is_closed()
692 715
693 716 cc_model = ChangesetCommentsModel()
694 717
695 718 c.pull_request_reviewers = c.pull_request.reviewers_statuses()
696 719
697 720 c.pull_request_review_status = c.pull_request.calculated_review_status()
698 721 c.pr_merge_status, c.pr_merge_msg = PullRequestModel().merge_status(
699 722 c.pull_request)
700 723 c.approval_msg = None
701 724 if c.pull_request_review_status != ChangesetStatus.STATUS_APPROVED:
702 725 c.approval_msg = _('Reviewer approval is pending.')
703 726 c.pr_merge_status = False
704 727 # load compare data into template context
705 728 enable_comments = not c.pull_request.is_closed()
706 self._load_compare_data(c.pull_request, enable_comments=enable_comments)
707 729
708 # this is a hack to properly display links, when creating PR, the
709 # compare view and others uses different notation, and
710 # compare_commits.html renders links based on the target_repo.
711 # We need to swap that here to generate it properly on the html side
712 c.target_repo = c.source_repo
713 730
714 731 # inline comments
715 c.inline_cnt = 0
716 732 c.inline_comments = cc_model.get_inline_comments(
717 733 c.rhodecode_db_repo.repo_id,
718 pull_request=pull_request_id).items()
719 # count inline comments
720 for __, lines in c.inline_comments:
721 for comments in lines.values():
722 c.inline_cnt += len(comments)
734 pull_request=pull_request_id)
735 c.inline_cnt = len(c.inline_comments)
723 736
724 737 # outdated comments
725 738 c.outdated_cnt = 0
726 739 if ChangesetCommentsModel.use_outdated_comments(c.pull_request):
727 740 c.outdated_comments = cc_model.get_outdated_comments(
728 741 c.rhodecode_db_repo.repo_id,
729 742 pull_request=c.pull_request)
730 743 # Count outdated comments and check for deleted files
731 744 for file_name, lines in c.outdated_comments.iteritems():
732 745 for comments in lines.values():
733 746 c.outdated_cnt += len(comments)
734 747 if file_name not in c.included_files:
735 748 c.deleted_files.append(file_name)
736 749 else:
737 750 c.outdated_comments = {}
738 751
752 self._load_compare_data(
753 c.pull_request, c.inline_comments, enable_comments=enable_comments)
754
755 # this is a hack to properly display links, when creating PR, the
756 # compare view and others uses different notation, and
757 # compare_commits.html renders links based on the target_repo.
758 # We need to swap that here to generate it properly on the html side
759 c.target_repo = c.source_repo
760
739 761 # comments
740 762 c.comments = cc_model.get_comments(c.rhodecode_db_repo.repo_id,
741 763 pull_request=pull_request_id)
742 764
743 765 if c.allowed_to_update:
744 766 force_close = ('forced_closed', _('Close Pull Request'))
745 767 statuses = ChangesetStatus.STATUSES + [force_close]
746 768 else:
747 769 statuses = ChangesetStatus.STATUSES
748 770 c.commit_statuses = statuses
749 771
750 772 c.ancestor = None # TODO: add ancestor here
751 773
752 774 return render('/pullrequests/pullrequest_show.html')
753 775
754 776 @LoginRequired()
755 777 @NotAnonymous()
756 778 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
757 779 'repository.admin')
758 780 @auth.CSRFRequired()
759 781 @jsonify
760 782 def comment(self, repo_name, pull_request_id):
761 783 pull_request_id = safe_int(pull_request_id)
762 784 pull_request = PullRequest.get_or_404(pull_request_id)
763 785 if pull_request.is_closed():
764 786 raise HTTPForbidden()
765 787
766 788 # TODO: johbo: Re-think this bit, "approved_closed" does not exist
767 789 # as a changeset status, still we want to send it in one value.
768 790 status = request.POST.get('changeset_status', None)
769 791 text = request.POST.get('text')
770 792 if status and '_closed' in status:
771 793 close_pr = True
772 794 status = status.replace('_closed', '')
773 795 else:
774 796 close_pr = False
775 797
776 798 forced = (status == 'forced')
777 799 if forced:
778 800 status = 'rejected'
779 801
780 802 allowed_to_change_status = PullRequestModel().check_user_change_status(
781 803 pull_request, c.rhodecode_user)
782 804
783 805 if status and allowed_to_change_status:
784 806 message = (_('Status change %(transition_icon)s %(status)s')
785 807 % {'transition_icon': '>',
786 808 'status': ChangesetStatus.get_status_lbl(status)})
787 809 if close_pr:
788 810 message = _('Closing with') + ' ' + message
789 811 text = text or message
790 812 comm = ChangesetCommentsModel().create(
791 813 text=text,
792 814 repo=c.rhodecode_db_repo.repo_id,
793 815 user=c.rhodecode_user.user_id,
794 816 pull_request=pull_request_id,
795 817 f_path=request.POST.get('f_path'),
796 818 line_no=request.POST.get('line'),
797 819 status_change=(ChangesetStatus.get_status_lbl(status)
798 820 if status and allowed_to_change_status else None),
799 821 status_change_type=(status
800 822 if status and allowed_to_change_status else None),
801 823 closing_pr=close_pr
802 824 )
803 825
804 826
805 827
806 828 if allowed_to_change_status:
807 829 old_calculated_status = pull_request.calculated_review_status()
808 830 # get status if set !
809 831 if status:
810 832 ChangesetStatusModel().set_status(
811 833 c.rhodecode_db_repo.repo_id,
812 834 status,
813 835 c.rhodecode_user.user_id,
814 836 comm,
815 837 pull_request=pull_request_id
816 838 )
817 839
818 840 Session().flush()
819 841 events.trigger(events.PullRequestCommentEvent(pull_request, comm))
820 842 # we now calculate the status of pull request, and based on that
821 843 # calculation we set the commits status
822 844 calculated_status = pull_request.calculated_review_status()
823 845 if old_calculated_status != calculated_status:
824 846 PullRequestModel()._trigger_pull_request_hook(
825 847 pull_request, c.rhodecode_user, 'review_status_change')
826 848
827 849 calculated_status_lbl = ChangesetStatus.get_status_lbl(
828 850 calculated_status)
829 851
830 852 if close_pr:
831 853 status_completed = (
832 854 calculated_status in [ChangesetStatus.STATUS_APPROVED,
833 855 ChangesetStatus.STATUS_REJECTED])
834 856 if forced or status_completed:
835 857 PullRequestModel().close_pull_request(
836 858 pull_request_id, c.rhodecode_user)
837 859 else:
838 860 h.flash(_('Closing pull request on other statuses than '
839 861 'rejected or approved is forbidden. '
840 862 'Calculated status from all reviewers '
841 863 'is currently: %s') % calculated_status_lbl,
842 864 category='warning')
843 865
844 866 Session().commit()
845 867
846 868 if not request.is_xhr:
847 869 return redirect(h.url('pullrequest_show', repo_name=repo_name,
848 870 pull_request_id=pull_request_id))
849 871
850 872 data = {
851 873 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
852 874 }
853 875 if comm:
854 876 c.co = comm
855 877 data.update(comm.get_dict())
856 878 data.update({'rendered_text':
857 879 render('changeset/changeset_comment_block.html')})
858 880
859 881 return data
860 882
861 883 @LoginRequired()
862 884 @NotAnonymous()
863 885 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
864 886 'repository.admin')
865 887 @auth.CSRFRequired()
866 888 @jsonify
867 889 def delete_comment(self, repo_name, comment_id):
868 890 return self._delete_comment(comment_id)
869 891
870 892 def _delete_comment(self, comment_id):
871 893 comment_id = safe_int(comment_id)
872 894 co = ChangesetComment.get_or_404(comment_id)
873 895 if co.pull_request.is_closed():
874 896 # don't allow deleting comments on closed pull request
875 897 raise HTTPForbidden()
876 898
877 899 is_owner = co.author.user_id == c.rhodecode_user.user_id
878 900 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
879 901 if h.HasPermissionAny('hg.admin')() or is_repo_admin or is_owner:
880 902 old_calculated_status = co.pull_request.calculated_review_status()
881 903 ChangesetCommentsModel().delete(comment=co)
882 904 Session().commit()
883 905 calculated_status = co.pull_request.calculated_review_status()
884 906 if old_calculated_status != calculated_status:
885 907 PullRequestModel()._trigger_pull_request_hook(
886 908 co.pull_request, c.rhodecode_user, 'review_status_change')
887 909 return True
888 910 else:
889 911 raise HTTPForbidden()
@@ -1,1167 +1,1174 b''
1 1 // Default styles
2 2
3 3 .diff-collapse {
4 4 margin: @padding 0;
5 5 text-align: right;
6 6 }
7 7
8 8 .diff-container {
9 9 margin-bottom: @space;
10 10
11 11 .diffblock {
12 12 margin-bottom: @space;
13 13 }
14 14
15 15 &.hidden {
16 16 display: none;
17 17 overflow: hidden;
18 18 }
19 19 }
20 20
21 21 .compare_view_files {
22 22
23 23 .diff-container {
24 24
25 25 .diffblock {
26 26 margin-bottom: 0;
27 27 }
28 28 }
29 29 }
30 30
31 31 div.diffblock .sidebyside {
32 32 background: #ffffff;
33 33 }
34 34
35 35 div.diffblock {
36 36 overflow-x: auto;
37 37 overflow-y: hidden;
38 38 clear: both;
39 39 padding: 0px;
40 40 background: @grey6;
41 41 border: @border-thickness solid @grey5;
42 42 -webkit-border-radius: @border-radius @border-radius 0px 0px;
43 43 border-radius: @border-radius @border-radius 0px 0px;
44 44
45 45
46 46 .comments-number {
47 47 float: right;
48 48 }
49 49
50 50 // BEGIN CODE-HEADER STYLES
51 51
52 52 .code-header {
53 53 background: @grey6;
54 54 padding: 10px 0 10px 0;
55 55 height: auto;
56 56 width: 100%;
57 57
58 58 .hash {
59 59 float: left;
60 60 padding: 2px 0 0 2px;
61 61 }
62 62
63 63 .date {
64 64 float: left;
65 65 text-transform: uppercase;
66 66 padding: 4px 0px 0px 2px;
67 67 }
68 68
69 69 div {
70 70 margin-left: 4px;
71 71 }
72 72
73 73 div.compare_header {
74 74 min-height: 40px;
75 75 margin: 0;
76 76 padding: 0 @padding;
77 77
78 78 .drop-menu {
79 79 float:left;
80 80 display: block;
81 81 margin:0 0 @padding 0;
82 82 }
83 83
84 84 .compare-label {
85 85 float: left;
86 86 clear: both;
87 87 display: inline-block;
88 88 min-width: 5em;
89 89 margin: 0;
90 90 padding: @button-padding @button-padding @button-padding 0;
91 91 font-family: @text-semibold;
92 92 }
93 93
94 94 .compare-buttons {
95 95 float: left;
96 96 margin: 0;
97 97 padding: 0 0 @padding;
98 98
99 99 .btn {
100 100 margin: 0 @padding 0 0;
101 101 }
102 102 }
103 103 }
104 104
105 105 }
106 106
107 107 .parents {
108 108 float: left;
109 109 width: 100px;
110 110 font-weight: 400;
111 111 vertical-align: middle;
112 112 padding: 0px 2px 0px 2px;
113 113 background-color: @grey6;
114 114
115 115 #parent_link {
116 116 margin: 00px 2px;
117 117
118 118 &.double {
119 119 margin: 0px 2px;
120 120 }
121 121
122 122 &.disabled{
123 123 margin-right: @padding;
124 124 }
125 125 }
126 126 }
127 127
128 128 .children {
129 129 float: right;
130 130 width: 100px;
131 131 font-weight: 400;
132 132 vertical-align: middle;
133 133 text-align: right;
134 134 padding: 0px 2px 0px 2px;
135 135 background-color: @grey6;
136 136
137 137 #child_link {
138 138 margin: 0px 2px;
139 139
140 140 &.double {
141 141 margin: 0px 2px;
142 142 }
143 143
144 144 &.disabled{
145 145 margin-right: @padding;
146 146 }
147 147 }
148 148 }
149 149
150 150 .changeset_header {
151 151 height: 16px;
152 152
153 153 & > div{
154 154 margin-right: @padding;
155 155 }
156 156 }
157 157
158 158 .changeset_file {
159 159 text-align: left;
160 160 float: left;
161 161 padding: 0;
162 162
163 163 a{
164 164 display: inline-block;
165 165 margin-right: 0.5em;
166 166 }
167 167
168 168 #selected_mode{
169 169 margin-left: 0;
170 170 }
171 171 }
172 172
173 173 .diff-menu-wrapper {
174 174 float: left;
175 175 }
176 176
177 177 .diff-menu {
178 178 position: absolute;
179 179 background: none repeat scroll 0 0 #FFFFFF;
180 180 border-color: #003367 @grey3 @grey3;
181 181 border-right: 1px solid @grey3;
182 182 border-style: solid solid solid;
183 183 border-width: @border-thickness;
184 184 box-shadow: 2px 8px 4px rgba(0, 0, 0, 0.2);
185 185 margin-top: 5px;
186 186 margin-left: 1px;
187 187 }
188 188
189 189 .diff-actions, .editor-actions {
190 190 float: left;
191 191
192 192 input{
193 193 margin: 0 0.5em 0 0;
194 194 }
195 195 }
196 196
197 197 // END CODE-HEADER STYLES
198 198
199 199 // BEGIN CODE-BODY STYLES
200 200
201 201 .code-body {
202 202 background: white;
203 203 padding: 0;
204 204 background-color: #ffffff;
205 205 position: relative;
206 206 max-width: none;
207 207 box-sizing: border-box;
208 208 // TODO: johbo: Parent has overflow: auto, this forces the child here
209 209 // to have the intended size and to scroll. Should be simplified.
210 210 width: 100%;
211 211 overflow-x: auto;
212 212 }
213 213
214 214 pre.raw {
215 215 background: white;
216 216 color: @grey1;
217 217 }
218 218 // END CODE-BODY STYLES
219 219
220 220 }
221 221
222 222
223 223 table.code-difftable {
224 224 border-collapse: collapse;
225 225 width: 99%;
226 226 border-radius: 0px !important;
227 227
228 228 td {
229 229 padding: 0 !important;
230 230 background: none !important;
231 231 border: 0 !important;
232 232 }
233 233
234 234 .context {
235 235 background: none repeat scroll 0 0 #DDE7EF;
236 236 }
237 237
238 238 .add {
239 239 background: none repeat scroll 0 0 #DDFFDD;
240 240
241 241 ins {
242 242 background: none repeat scroll 0 0 #AAFFAA;
243 243 text-decoration: none;
244 244 }
245 245 }
246 246
247 247 .del {
248 248 background: none repeat scroll 0 0 #FFDDDD;
249 249
250 250 del {
251 251 background: none repeat scroll 0 0 #FFAAAA;
252 252 text-decoration: none;
253 253 }
254 254 }
255 255
256 256 /** LINE NUMBERS **/
257 257 .lineno {
258 258 padding-left: 2px !important;
259 259 padding-right: 2px;
260 260 text-align: right;
261 261 width: 32px;
262 262 -moz-user-select: none;
263 263 -webkit-user-select: none;
264 264 border-right: @border-thickness solid @grey5 !important;
265 265 border-left: 0px solid #CCC !important;
266 266 border-top: 0px solid #CCC !important;
267 267 border-bottom: none !important;
268 268
269 269 a {
270 270 &:extend(pre);
271 271 text-align: right;
272 272 padding-right: 2px;
273 273 cursor: pointer;
274 274 display: block;
275 275 width: 32px;
276 276 }
277 277 }
278 278
279 279 .context {
280 280 cursor: auto;
281 281 &:extend(pre);
282 282 }
283 283
284 284 .lineno-inline {
285 285 background: none repeat scroll 0 0 #FFF !important;
286 286 padding-left: 2px;
287 287 padding-right: 2px;
288 288 text-align: right;
289 289 width: 30px;
290 290 -moz-user-select: none;
291 291 -webkit-user-select: none;
292 292 }
293 293
294 294 /** CODE **/
295 295 .code {
296 296 display: block;
297 297 width: 100%;
298 298
299 299 td {
300 300 margin: 0;
301 301 padding: 0;
302 302 }
303 303
304 304 pre {
305 305 margin: 0;
306 306 padding: 0;
307 307 margin-left: .5em;
308 308 }
309 309 }
310 310 }
311 311
312 312
313 313 // Comments
314 314
315 315 div.comment:target {
316 316 border-left: 6px solid @comment-highlight-color;
317 317 padding-left: 3px;
318 318 margin-left: -9px;
319 319 }
320 320
321 321 //TODO: anderson: can't get an absolute number out of anything, so had to put the
322 322 //current values that might change. But to make it clear I put as a calculation
323 323 @comment-max-width: 1065px;
324 324 @pr-extra-margin: 34px;
325 325 @pr-border-spacing: 4px;
326 326 @pr-comment-width: @comment-max-width - @pr-extra-margin - @pr-border-spacing;
327 327
328 328 // Pull Request
329 329 .cs_files .code-difftable {
330 330 border: @border-thickness solid @grey5; //borders only on PRs
331 331
332 332 .comment-inline-form,
333 333 div.comment {
334 334 width: @pr-comment-width;
335 335 }
336 336 }
337 337
338 338 // Changeset
339 339 .code-difftable {
340 340 .comment-inline-form,
341 341 div.comment {
342 342 width: @comment-max-width;
343 343 }
344 344 }
345 345
346 346 //Style page
347 347 @style-extra-margin: @sidebar-width + (@sidebarpadding * 3) + @padding;
348 348 #style-page .code-difftable{
349 349 .comment-inline-form,
350 350 div.comment {
351 351 width: @comment-max-width - @style-extra-margin;
352 352 }
353 353 }
354 354
355 355 #context-bar > h2 {
356 356 font-size: 20px;
357 357 }
358 358
359 359 #context-bar > h2> a {
360 360 font-size: 20px;
361 361 }
362 362 // end of defaults
363 363
364 364 .file_diff_buttons {
365 365 padding: 0 0 @padding;
366 366
367 367 .drop-menu {
368 368 float: left;
369 369 margin: 0 @padding 0 0;
370 370 }
371 371 .btn {
372 372 margin: 0 @padding 0 0;
373 373 }
374 374 }
375 375
376 376 .code-body.textarea.editor {
377 377 max-width: none;
378 378 padding: 15px;
379 379 }
380 380
381 381 td.injected_diff{
382 382 max-width: 1178px;
383 383 overflow-x: auto;
384 384 overflow-y: hidden;
385 385
386 386 div.diff-container,
387 387 div.diffblock{
388 388 max-width: 100%;
389 389 }
390 390
391 391 div.code-body {
392 392 max-width: 1124px;
393 393 overflow-x: auto;
394 394 overflow-y: hidden;
395 395 padding: 0;
396 396 }
397 397 div.diffblock {
398 398 border: none;
399 399 }
400 400
401 401 &.inline-form {
402 402 width: 99%
403 403 }
404 404 }
405 405
406 406
407 407 table.code-difftable {
408 408 width: 100%;
409 409 }
410 410
411 411 /** PYGMENTS COLORING **/
412 412 div.codeblock {
413 413
414 414 // TODO: johbo: Added interim to get rid of the margin around
415 415 // Select2 widgets. This needs further cleanup.
416 416 margin-top: @padding;
417 417
418 418 overflow: auto;
419 419 padding: 0px;
420 420 border: @border-thickness solid @grey5;
421 421 background: @grey6;
422 422 .border-radius(@border-radius);
423 423
424 424 #remove_gist {
425 425 float: right;
426 426 }
427 427
428 428 .author {
429 429 clear: both;
430 430 vertical-align: middle;
431 431 font-family: @text-bold;
432 432 }
433 433
434 434 .btn-mini {
435 435 float: left;
436 436 margin: 0 5px 0 0;
437 437 }
438 438
439 439 .code-header {
440 440 padding: @padding;
441 441 border-bottom: @border-thickness solid @grey5;
442 442
443 443 .rc-user {
444 444 min-width: 0;
445 445 margin-right: .5em;
446 446 }
447 447
448 448 .stats {
449 449 clear: both;
450 450 margin: 0 0 @padding 0;
451 451 padding: 0;
452 452 .left {
453 453 float: left;
454 454 clear: left;
455 455 max-width: 75%;
456 456 margin: 0 0 @padding 0;
457 457
458 458 &.item {
459 459 margin-right: @padding;
460 460 &.last { border-right: none; }
461 461 }
462 462 }
463 463 .buttons { float: right; }
464 464 .author {
465 465 height: 25px; margin-left: 15px; font-weight: bold;
466 466 }
467 467 }
468 468
469 469 .commit {
470 470 margin: 5px 0 0 26px;
471 471 font-weight: normal;
472 472 white-space: pre-wrap;
473 473 }
474 474 }
475 475
476 476 .message {
477 477 position: relative;
478 478 margin: @padding;
479 479
480 480 .codeblock-label {
481 481 margin: 0 0 1em 0;
482 482 }
483 483 }
484 484
485 485 .code-body {
486 486 padding: @padding;
487 487 background-color: #ffffff;
488 488 min-width: 100%;
489 489 box-sizing: border-box;
490 490 // TODO: johbo: Parent has overflow: auto, this forces the child here
491 491 // to have the intended size and to scroll. Should be simplified.
492 492 width: 100%;
493 493 overflow-x: auto;
494 494 }
495 495 }
496 496
497 497 .code-highlighttable,
498 498 div.codeblock {
499 499
500 500 &.readme {
501 501 background-color: white;
502 502 }
503 503
504 504 .markdown-block table {
505 505 border-collapse: collapse;
506 506
507 507 th,
508 508 td {
509 509 padding: .5em;
510 510 border: @border-thickness solid @border-default-color;
511 511 }
512 512 }
513 513
514 514 table {
515 515 border: 0px;
516 516 margin: 0;
517 517 letter-spacing: normal;
518 518
519 519
520 520 td {
521 521 border: 0px;
522 522 vertical-align: top;
523 523 }
524 524 }
525 525 }
526 526
527 527 div.codeblock .code-header .search-path { padding: 0 0 0 10px; }
528 528 div.search-code-body {
529 529 background-color: #ffffff; padding: 5px 0 5px 10px;
530 530 pre {
531 531 .match { background-color: #faffa6;}
532 532 .break { display: block; width: 100%; background-color: #DDE7EF; color: #747474; }
533 533 }
534 534 .code-highlighttable {
535 535 border-collapse: collapse;
536 536
537 537 tr:hover {
538 538 background: #fafafa;
539 539 }
540 540 td.code {
541 541 padding-left: 10px;
542 542 }
543 543 td.line {
544 544 border-right: 1px solid #ccc !important;
545 545 padding-right: 10px;
546 546 text-align: right;
547 547 font-family: "Lucida Console",Monaco,monospace;
548 548 span {
549 549 white-space: pre-wrap;
550 550 color: #666666;
551 551 }
552 552 }
553 553 }
554 554 }
555 555
556 556 div.annotatediv { margin-left: 2px; margin-right: 4px; }
557 557 .code-highlight {
558 558 margin: 0; padding: 0; border-left: @border-thickness solid @grey5;
559 559 pre, .linenodiv pre { padding: 0 5px; margin: 0; }
560 560 pre div:target {background-color: @comment-highlight-color !important;}
561 561 }
562 562
563 563 .linenos a { text-decoration: none; }
564 564
565 565 .CodeMirror-selected { background: @rchighlightblue; }
566 566 .CodeMirror-focused .CodeMirror-selected { background: @rchighlightblue; }
567 567 .CodeMirror ::selection { background: @rchighlightblue; }
568 568 .CodeMirror ::-moz-selection { background: @rchighlightblue; }
569 569
570 570 .code { display: block; border:0px !important; }
571 571 .code-highlight, /* TODO: dan: merge codehilite into code-highlight */
572 572 .codehilite {
573 573 .hll { background-color: #ffffcc }
574 574 .c { color: #408080; font-style: italic } /* Comment */
575 575 .err, .codehilite .err { border: @border-thickness solid #FF0000 } /* Error */
576 576 .k { color: #008000; font-weight: bold } /* Keyword */
577 577 .o { color: #666666 } /* Operator */
578 578 .cm { color: #408080; font-style: italic } /* Comment.Multiline */
579 579 .cp { color: #BC7A00 } /* Comment.Preproc */
580 580 .c1 { color: #408080; font-style: italic } /* Comment.Single */
581 581 .cs { color: #408080; font-style: italic } /* Comment.Special */
582 582 .gd { color: #A00000 } /* Generic.Deleted */
583 583 .ge { font-style: italic } /* Generic.Emph */
584 584 .gr { color: #FF0000 } /* Generic.Error */
585 585 .gh { color: #000080; font-weight: bold } /* Generic.Heading */
586 586 .gi { color: #00A000 } /* Generic.Inserted */
587 587 .go { color: #808080 } /* Generic.Output */
588 588 .gp { color: #000080; font-weight: bold } /* Generic.Prompt */
589 589 .gs { font-weight: bold } /* Generic.Strong */
590 590 .gu { color: #800080; font-weight: bold } /* Generic.Subheading */
591 591 .gt { color: #0040D0 } /* Generic.Traceback */
592 592 .kc { color: #008000; font-weight: bold } /* Keyword.Constant */
593 593 .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */
594 594 .kn { color: #008000; font-weight: bold } /* Keyword.Namespace */
595 595 .kp { color: #008000 } /* Keyword.Pseudo */
596 596 .kr { color: #008000; font-weight: bold } /* Keyword.Reserved */
597 597 .kt { color: #B00040 } /* Keyword.Type */
598 598 .m { color: #666666 } /* Literal.Number */
599 599 .s { color: #BA2121 } /* Literal.String */
600 600 .na { color: #7D9029 } /* Name.Attribute */
601 601 .nb { color: #008000 } /* Name.Builtin */
602 602 .nc { color: #0000FF; font-weight: bold } /* Name.Class */
603 603 .no { color: #880000 } /* Name.Constant */
604 604 .nd { color: #AA22FF } /* Name.Decorator */
605 605 .ni { color: #999999; font-weight: bold } /* Name.Entity */
606 606 .ne { color: #D2413A; font-weight: bold } /* Name.Exception */
607 607 .nf { color: #0000FF } /* Name.Function */
608 608 .nl { color: #A0A000 } /* Name.Label */
609 609 .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */
610 610 .nt { color: #008000; font-weight: bold } /* Name.Tag */
611 611 .nv { color: #19177C } /* Name.Variable */
612 612 .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */
613 613 .w { color: #bbbbbb } /* Text.Whitespace */
614 614 .mf { color: #666666 } /* Literal.Number.Float */
615 615 .mh { color: #666666 } /* Literal.Number.Hex */
616 616 .mi { color: #666666 } /* Literal.Number.Integer */
617 617 .mo { color: #666666 } /* Literal.Number.Oct */
618 618 .sb { color: #BA2121 } /* Literal.String.Backtick */
619 619 .sc { color: #BA2121 } /* Literal.String.Char */
620 620 .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */
621 621 .s2 { color: #BA2121 } /* Literal.String.Double */
622 622 .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */
623 623 .sh { color: #BA2121 } /* Literal.String.Heredoc */
624 624 .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */
625 625 .sx { color: #008000 } /* Literal.String.Other */
626 626 .sr { color: #BB6688 } /* Literal.String.Regex */
627 627 .s1 { color: #BA2121 } /* Literal.String.Single */
628 628 .ss { color: #19177C } /* Literal.String.Symbol */
629 629 .bp { color: #008000 } /* Name.Builtin.Pseudo */
630 630 .vc { color: #19177C } /* Name.Variable.Class */
631 631 .vg { color: #19177C } /* Name.Variable.Global */
632 632 .vi { color: #19177C } /* Name.Variable.Instance */
633 633 .il { color: #666666 } /* Literal.Number.Integer.Long */
634 634 }
635 635
636 636 /* customized pre blocks for markdown/rst */
637 637 pre.literal-block, .codehilite pre{
638 638 padding: @padding;
639 639 border: 1px solid @grey6;
640 640 .border-radius(@border-radius);
641 641 background-color: @grey7;
642 642 }
643 643
644 644
645 645 /* START NEW CODE BLOCK CSS */
646 646
647 647 @cb-line-height: 18px;
648 648 @cb-line-code-padding: 10px;
649 649 @cb-text-padding: 5px;
650 650
651 651 @pill-padding: 2px 7px;
652 652
653 653 input.filediff-collapse-state {
654 654 display: none;
655 655
656 656 &:checked + .filediff { /* file diff is collapsed */
657 657 .cb {
658 658 display: none
659 659 }
660 660 .filediff-collapse-indicator {
661 661 border-width: 9px 0 9px 15.6px;
662 662 border-color: transparent transparent transparent #ccc;
663 663 }
664 664 .filediff-menu {
665 665 display: none;
666 666 }
667 667 margin: -1px 0 0 0;
668 668 }
669 669
670 670 &+ .filediff { /* file diff is expanded */
671 671 .filediff-collapse-indicator {
672 672 border-width: 15.6px 9px 0 9px;
673 673 border-color: #ccc transparent transparent transparent;
674 674 }
675 675 .filediff-menu {
676 676 display: block;
677 677 }
678 678 margin: 20px 0;
679 679 &:nth-child(2) {
680 680 margin: 0;
681 681 }
682 682 }
683 683 }
684 684 .cs_files {
685 685 clear: both;
686 686 }
687 687
688 688 .diffset-menu {
689 689 margin-bottom: 20px;
690 690 }
691 691 .diffset {
692 692 margin: 20px auto;
693 693 .diffset-heading {
694 694 border: 1px solid @grey5;
695 695 margin-bottom: -1px;
696 696 // margin-top: 20px;
697 697 h2 {
698 698 margin: 0;
699 699 line-height: 38px;
700 700 padding-left: 10px;
701 701 }
702 702 .btn {
703 703 margin: 0;
704 704 }
705 705 background: @grey6;
706 706 display: block;
707 707 padding: 5px;
708 708 }
709 709 .diffset-heading-warning {
710 710 background: @alert3-inner;
711 711 border: 1px solid @alert3;
712 712 }
713 &.diffset-comments-disabled {
714 .cb-comment-box-opener, .comment-inline-form, .cb-comment-add-button {
715 display: none !important;
713 716 }
717 }
718 }
719
714 720 .pill {
715 721 display: block;
716 722 float: left;
717 723 padding: @pill-padding;
718 724 }
719 725 .pill-group {
720 726 .pill {
721 727 opacity: .8;
722 728 &:first-child {
723 729 border-radius: @border-radius 0 0 @border-radius;
724 730 }
725 731 &:last-child {
726 732 border-radius: 0 @border-radius @border-radius 0;
727 733 }
728 734 &:only-child {
729 735 border-radius: @border-radius;
730 736 }
731 737 }
732 738 }
733 739
734 740 .filediff {
735 741 border: 1px solid @grey5;
736 742
737 743 /* START OVERRIDES */
738 744 .code-highlight {
739 745 border: none; // TODO: remove this border from the global
740 746 // .code-highlight, it doesn't belong there
741 747 }
742 748 label {
743 749 margin: 0; // TODO: remove this margin definition from global label
744 750 // it doesn't belong there - if margin on labels
745 751 // are needed for a form they should be defined
746 752 // in the form's class
747 753 }
748 754 /* END OVERRIDES */
749 755
750 756 * {
751 757 box-sizing: border-box;
752 758 }
753 759 .filediff-anchor {
754 760 visibility: hidden;
755 761 }
756 762 &:hover {
757 763 .filediff-anchor {
758 764 visibility: visible;
759 765 }
760 766 }
761 767
762 768 .filediff-collapse-indicator {
763 769 width: 0;
764 770 height: 0;
765 771 border-style: solid;
766 772 float: left;
767 773 margin: 2px 2px 0 0;
768 774 cursor: pointer;
769 775 }
770 776
771 777 .filediff-heading {
772 778 background: @grey7;
773 779 cursor: pointer;
774 780 display: block;
775 781 padding: 5px 10px;
776 782 }
777 783 .filediff-heading:after {
778 784 content: "";
779 785 display: table;
780 786 clear: both;
781 787 }
782 788 .filediff-heading:hover {
783 789 background: #e1e9f4 !important;
784 790 }
785 791
786 792 .filediff-menu {
787 793 float: right;
788 794
789 795 &> a, &> span {
790 796 padding: 5px;
791 797 display: block;
792 798 float: left
793 799 }
794 800 }
795 801
796 802 .pill {
797 803 &[op="name"] {
798 804 background: none;
799 805 color: @grey2;
800 806 opacity: 1;
801 807 color: white;
802 808 }
803 809 &[op="limited"] {
804 810 background: @grey2;
805 811 color: white;
806 812 }
807 813 &[op="binary"] {
808 814 background: @color7;
809 815 color: white;
810 816 }
811 817 &[op="modified"] {
812 818 background: @alert1;
813 819 color: white;
814 820 }
815 821 &[op="renamed"] {
816 822 background: @color4;
817 823 color: white;
818 824 }
819 825 &[op="mode"] {
820 826 background: @grey3;
821 827 color: white;
822 828 }
823 829 &[op="symlink"] {
824 830 background: @color8;
825 831 color: white;
826 832 }
827 833
828 834 &[op="added"] { /* added lines */
829 835 background: @alert1;
830 836 color: white;
831 837 }
832 838 &[op="deleted"] { /* deleted lines */
833 839 background: @alert2;
834 840 color: white;
835 841 }
836 842
837 843 &[op="created"] { /* created file */
838 844 background: @alert1;
839 845 color: white;
840 846 }
841 847 &[op="removed"] { /* deleted file */
842 848 background: @color5;
843 849 color: white;
844 850 }
845 851 }
846 852
847 853 .filediff-collapse-button, .filediff-expand-button {
848 854 cursor: pointer;
849 855 }
850 856 .filediff-collapse-button {
851 857 display: inline;
852 858 }
853 859 .filediff-expand-button {
854 860 display: none;
855 861 }
856 862 .filediff-collapsed .filediff-collapse-button {
857 863 display: none;
858 864 }
859 865 .filediff-collapsed .filediff-expand-button {
860 866 display: inline;
861 867 }
862 868
863 869 @comment-padding: 5px;
864 870
865 871 /**** COMMENTS ****/
866 872
867 873 .filediff-menu {
868 874 .show-comment-button {
869 875 display: none;
870 876 }
871 877 }
872 878 &.hide-comments {
873 879 .inline-comments {
874 880 display: none;
875 881 }
876 882 .filediff-menu {
877 883 .show-comment-button {
878 884 display: inline;
879 885 }
880 886 .hide-comment-button {
881 887 display: none;
882 888 }
883 889 }
884 890 }
885 891
886 892 .hide-line-comments {
887 893 .inline-comments {
888 894 display: none;
889 895 }
890 896 }
891 897 .inline-comments {
892 898 border-radius: @border-radius;
893 899 background: @grey6;
894 900 .comment {
895 901 margin: 0;
896 902 border-radius: @border-radius;
897 903 }
898 904 .comment-outdated {
899 905 opacity: 0.5;
900 906 }
901 907 .comment-inline {
902 908 background: white;
903 909 padding: (@comment-padding + 3px) @comment-padding;
904 910 border: @comment-padding solid @grey6;
905 911
906 912 .text {
907 913 border: none;
908 914 }
909 915 .meta {
910 916 border-bottom: 1px solid @grey6;
911 917 padding-bottom: 10px;
912 918 }
913 919 }
914 920 .comment-selected {
915 921 border-left: 6px solid @comment-highlight-color;
916 922 }
917 923 .comment-inline-form {
918 924 padding: @comment-padding;
919 925 display: none;
920 926 }
921 927 .cb-comment-add-button {
922 928 margin: @comment-padding;
923 929 }
924 930 /* hide add comment button when form is open */
925 931 .comment-inline-form-open + .cb-comment-add-button {
926 932 display: none;
927 933 }
928 934 .comment-inline-form-open {
929 935 display: block;
930 936 }
931 937 /* hide add comment button when form but no comments */
932 938 .comment-inline-form:first-child + .cb-comment-add-button {
933 939 display: none;
934 940 }
935 941 /* hide add comment button when no comments or form */
936 942 .cb-comment-add-button:first-child {
937 943 display: none;
938 944 }
939 945 /* hide add comment button when only comment is being deleted */
940 946 .comment-deleting:first-child + .cb-comment-add-button {
941 947 display: none;
942 948 }
943 949 }
944 950 /**** END COMMENTS ****/
945 951
946 952 }
947 953
948 954
949 955 table.cb {
950 956 width: 100%;
951 957 border-collapse: collapse;
952 958
953 959 .cb-text {
954 960 padding: @cb-text-padding;
955 961 }
956 962 .cb-hunk {
957 963 padding: @cb-text-padding;
958 964 }
959 965 .cb-expand {
960 966 display: none;
961 967 }
962 968 .cb-collapse {
963 969 display: inline;
964 970 }
965 971 &.cb-collapsed {
966 972 .cb-line {
967 973 display: none;
968 974 }
969 975 .cb-expand {
970 976 display: inline;
971 977 }
972 978 .cb-collapse {
973 979 display: none;
974 980 }
975 981 }
976 982
977 983 /* intentionally general selector since .cb-line-selected must override it
978 984 and they both use !important since the td itself may have a random color
979 985 generated by annotation blocks. TLDR: if you change it, make sure
980 986 annotated block selection and line selection in file view still work */
981 987 .cb-line-fresh .cb-content {
982 988 background: white !important;
983 989 }
984 990 .cb-warning {
985 991 background: #fff4dd;
986 992 }
987 993
988 994 &.cb-diff-sideside {
989 995 td {
990 996 &.cb-content {
991 997 width: 50%;
992 998 }
993 999 }
994 1000 }
995 1001
996 1002 tr {
997 1003 &.cb-annotate {
998 1004 border-top: 1px solid #eee;
999 1005
1000 1006 &+ .cb-line {
1001 1007 border-top: 1px solid #eee;
1002 1008 }
1003 1009
1004 1010 &:first-child {
1005 1011 border-top: none;
1006 1012 &+ .cb-line {
1007 1013 border-top: none;
1008 1014 }
1009 1015 }
1010 1016 }
1011 1017
1012 1018 &.cb-hunk {
1013 1019 font-family: @font-family-monospace;
1014 1020 color: rgba(0, 0, 0, 0.3);
1015 1021
1016 1022 td {
1017 1023 &:first-child {
1018 1024 background: #edf2f9;
1019 1025 }
1020 1026 &:last-child {
1021 1027 background: #f4f7fb;
1022 1028 }
1023 1029 }
1024 1030 }
1025 1031 }
1026 1032
1033
1027 1034 td {
1028 1035 vertical-align: top;
1029 1036 padding: 0;
1030 1037
1031 1038 &.cb-content {
1032 1039 font-size: 12.35px;
1033 1040
1034 1041 &.cb-line-selected .cb-code {
1035 1042 background: @comment-highlight-color !important;
1036 1043 }
1037 1044
1038 1045 span.cb-code {
1039 1046 line-height: @cb-line-height;
1040 1047 padding-left: @cb-line-code-padding;
1041 1048 padding-right: @cb-line-code-padding;
1042 1049 display: block;
1043 1050 white-space: pre-wrap;
1044 1051 font-family: @font-family-monospace;
1045 1052 word-break: break-word;
1046 1053 .nonl {
1047 1054 color: @color5;
1048 1055 }
1049 1056 }
1050 1057
1051 1058 &> button.cb-comment-box-opener {
1052 1059 padding: 2px 6px 2px 6px;
1053 1060 margin-left: -20px;
1054 1061 margin-top: -2px;
1055 1062 border-radius: @border-radius;
1056 1063 position: absolute;
1057 1064 display: none;
1058 1065 }
1059 1066 .cb-comment {
1060 1067 margin-top: 10px;
1061 1068 white-space: normal;
1062 1069 }
1063 1070 }
1064 1071 &:hover {
1065 1072 button.cb-comment-box-opener {
1066 1073 display: block;
1067 1074 }
1068 1075 &+ td button.cb-comment-box-opener {
1069 1076 display: block
1070 1077 }
1071 1078 }
1072 1079
1073 1080 &.cb-data {
1074 1081 text-align: right;
1075 1082 width: 30px;
1076 1083 font-family: @font-family-monospace;
1077 1084
1078 1085 .icon-comment {
1079 1086 cursor: pointer;
1080 1087 }
1081 1088 &.cb-line-selected > div {
1082 1089 display: block;
1083 1090 background: @comment-highlight-color !important;
1084 1091 line-height: @cb-line-height;
1085 1092 color: rgba(0, 0, 0, 0.3);
1086 1093 }
1087 1094 }
1088 1095
1089 1096 &.cb-lineno {
1090 1097 padding: 0;
1091 1098 width: 50px;
1092 1099 color: rgba(0, 0, 0, 0.3);
1093 1100 text-align: right;
1094 1101 border-right: 1px solid #eee;
1095 1102 font-family: @font-family-monospace;
1096 1103
1097 1104 a::before {
1098 1105 content: attr(data-line-no);
1099 1106 }
1100 1107 &.cb-line-selected a {
1101 1108 background: @comment-highlight-color !important;
1102 1109 }
1103 1110
1104 1111 a {
1105 1112 display: block;
1106 1113 padding-right: @cb-line-code-padding;
1107 1114 padding-left: @cb-line-code-padding;
1108 1115 line-height: @cb-line-height;
1109 1116 color: rgba(0, 0, 0, 0.3);
1110 1117 }
1111 1118 }
1112 1119
1113 1120 &.cb-empty {
1114 1121 background: @grey7;
1115 1122 }
1116 1123
1117 1124 ins {
1118 1125 color: black;
1119 1126 background: #a6f3a6;
1120 1127 text-decoration: none;
1121 1128 }
1122 1129 del {
1123 1130 color: black;
1124 1131 background: #f8cbcb;
1125 1132 text-decoration: none;
1126 1133 }
1127 1134 &.cb-addition {
1128 1135 background: #ecffec;
1129 1136
1130 1137 &.blob-lineno {
1131 1138 background: #ddffdd;
1132 1139 }
1133 1140 }
1134 1141 &.cb-deletion {
1135 1142 background: #ffecec;
1136 1143
1137 1144 &.blob-lineno {
1138 1145 background: #ffdddd;
1139 1146 }
1140 1147 }
1141 1148
1142 1149 &.cb-annotate-info {
1143 1150 width: 320px;
1144 1151 min-width: 320px;
1145 1152 max-width: 320px;
1146 1153 padding: 5px 2px;
1147 1154 font-size: 13px;
1148 1155
1149 1156 strong.cb-annotate-message {
1150 1157 padding: 5px 0;
1151 1158 white-space: pre-line;
1152 1159 display: inline-block;
1153 1160 }
1154 1161 .rc-user {
1155 1162 float: none;
1156 1163 padding: 0 6px 0 17px;
1157 1164 min-width: auto;
1158 1165 min-height: auto;
1159 1166 }
1160 1167 }
1161 1168
1162 1169 &.cb-annotate-revision {
1163 1170 cursor: pointer;
1164 1171 text-align: right;
1165 1172 }
1166 1173 }
1167 1174 }
@@ -1,567 +1,569 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 return {
7 7 '-': 'cb-deletion',
8 8 '+': 'cb-addition',
9 9 ' ': 'cb-context',
10 10 }.get(action, 'cb-empty')
11 11 %></%def>
12 12
13 13 <%def name="op_class(op_id)"><%
14 14 return {
15 15 DEL_FILENODE: 'deletion', # file deleted
16 16 BIN_FILENODE: 'warning' # binary diff hidden
17 17 }.get(op_id, 'addition')
18 18 %></%def>
19 19
20 20 <%def name="link_for(**kw)"><%
21 21 new_args = request.GET.mixed()
22 22 new_args.update(kw)
23 23 return h.url('', **new_args)
24 24 %></%def>
25 25
26 26 <%def name="render_diffset(diffset, commit=None,
27 27
28 28 # collapse all file diff entries when there are more than this amount of files in the diff
29 29 collapse_when_files_over=20,
30 30
31 31 # collapse lines in the diff when more than this amount of lines changed in the file diff
32 32 lines_changed_limit=500,
33 33
34 34 # add a ruler at to the output
35 35 ruler_at_chars=0,
36 36
37 # turn on inline comments
37 # show inline comments
38 38 use_comments=False,
39 39
40 # disable new comments
41 disable_new_comments=False,
42
40 43 )">
41 44
42 45 %if use_comments:
43 46 <div id="cb-comments-inline-container-template" class="js-template">
44 47 ${inline_comments_container([])}
45 48 </div>
46 49 <div class="js-template" id="cb-comment-inline-form-template">
47 50 <div class="comment-inline-form ac">
48 51 %if c.rhodecode_user.username != h.DEFAULT_USER:
49 52 ${h.form('#', method='get')}
50 53 <div id="edit-container_{1}" class="clearfix">
51 54 <div class="comment-title pull-left">
52 55 ${_('Create a comment on line {1}.')}
53 56 </div>
54 57 <div class="comment-help pull-right">
55 58 ${(_('Comments parsed using %s syntax with %s support.') % (
56 59 ('<a href="%s">%s</a>' % (h.url('%s_help' % c.visual.default_renderer), c.visual.default_renderer.upper())),
57 60 ('<span class="tooltip" title="%s">@mention</span>' % _('Use @username inside this text to send notification to this RhodeCode user'))
58 61 )
59 62 )|n
60 63 }
61 64 </div>
62 65 <div style="clear: both"></div>
63 66 <textarea id="text_{1}" name="text" class="comment-block-ta ac-input"></textarea>
64 67 </div>
65 68 <div id="preview-container_{1}" class="clearfix" style="display: none;">
66 69 <div class="comment-help">
67 70 ${_('Comment preview')}
68 71 </div>
69 72 <div id="preview-box_{1}" class="preview-box"></div>
70 73 </div>
71 74 <div class="comment-footer">
72 75 <div class="action-buttons">
73 76 <input type="hidden" name="f_path" value="{0}">
74 77 <input type="hidden" name="line" value="{1}">
75 78 <button id="preview-btn_{1}" class="btn btn-secondary">${_('Preview')}</button>
76 79 <button id="edit-btn_{1}" class="btn btn-secondary" style="display: none;">${_('Edit')}</button>
77 80 ${h.submit('save', _('Comment'), class_='btn btn-success save-inline-form')}
78 81 </div>
79 82 <div class="comment-button">
80 83 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
81 84 ${_('Cancel')}
82 85 </button>
83 86 </div>
84 87 ${h.end_form()}
85 88 </div>
86 89 %else:
87 90 ${h.form('', class_='inline-form comment-form-login', method='get')}
88 91 <div class="pull-left">
89 92 <div class="comment-help pull-right">
90 93 ${_('You need to be logged in to comment.')} <a href="${h.route_path('login', _query={'came_from': h.url.current()})}">${_('Login now')}</a>
91 94 </div>
92 95 </div>
93 96 <div class="comment-button pull-right">
94 97 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
95 98 ${_('Cancel')}
96 99 </button>
97 100 </div>
98 101 <div class="clearfix"></div>
99 102 ${h.end_form()}
100 103 %endif
101 104 </div>
102 105 </div>
103 106
104 107 %endif
105 108 <%
106 109 collapse_all = len(diffset.files) > collapse_when_files_over
107 110 %>
108 111
109 112 %if c.diffmode == 'sideside':
110 113 <style>
111 114 .wrapper {
112 115 max-width: 1600px !important;
113 116 }
114 117 </style>
115 118 %endif
116 119 %if ruler_at_chars:
117 120 <style>
118 121 .diff table.cb .cb-content:after {
119 122 content: "";
120 123 border-left: 1px solid blue;
121 124 position: absolute;
122 125 top: 0;
123 126 height: 18px;
124 127 opacity: .2;
125 128 z-index: 10;
126 129 ## +5 to account for diff action (+/-)
127 130 left: ${ruler_at_chars + 5}ch;
128 131 </style>
129 132 %endif
130
131 <div class="diffset">
133 <div class="diffset ${disable_new_comments and 'diffset-comments-disabled'}">
132 134 <div class="diffset-heading ${diffset.limited_diff and 'diffset-heading-warning' or ''}">
133 135 %if commit:
134 136 <div class="pull-right">
135 137 <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='')}">
136 138 ${_('Browse Files')}
137 139 </a>
138 140 </div>
139 141 %endif
140 142 <h2 class="clearinner">
141 143 %if commit:
142 144 <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> -
143 145 ${h.age_component(commit.date)} -
144 146 %endif
145 147 %if diffset.limited_diff:
146 148 ${_('The requested commit is too big and content was truncated.')}
147 149
148 150 ${ungettext('%(num)s file changed.', '%(num)s files changed.', diffset.changed_files) % {'num': diffset.changed_files}}
149 151 <a href="${link_for(fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a>
150 152 %else:
151 153 ${ungettext('%(num)s file changed: %(linesadd)s inserted, ''%(linesdel)s deleted',
152 154 '%(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}}
153 155 %endif
154 156 </h2>
155 157 </div>
156 158
157 159 %if not diffset.files:
158 160 <p class="empty_data">${_('No files')}</p>
159 161 %endif
160 162
161 163 <div class="filediffs">
162 164 %for i, filediff in enumerate(diffset.files):
163 165 <%
164 166 lines_changed = filediff['patch']['stats']['added'] + filediff['patch']['stats']['deleted']
165 167 over_lines_changed_limit = lines_changed > lines_changed_limit
166 168 %>
167 169 <input ${collapse_all and 'checked' or ''} class="filediff-collapse-state" id="filediff-collapse-${id(filediff)}" type="checkbox">
168 170 <div
169 171 class="filediff"
170 172 data-f-path="${filediff['patch']['filename']}"
171 173 id="a_${h.FID('', filediff['patch']['filename'])}">
172 174 <label for="filediff-collapse-${id(filediff)}" class="filediff-heading">
173 175 <div class="filediff-collapse-indicator"></div>
174 176 ${diff_ops(filediff)}
175 177 </label>
176 178 ${diff_menu(filediff, use_comments=use_comments)}
177 179 <table class="cb cb-diff-${c.diffmode} code-highlight ${over_lines_changed_limit and 'cb-collapsed' or ''}">
178 180 %if not filediff.hunks:
179 181 %for op_id, op_text in filediff['patch']['stats']['ops'].items():
180 182 <tr>
181 183 <td class="cb-text cb-${op_class(op_id)}" ${c.diffmode == 'unified' and 'colspan=3' or 'colspan=4'}>
182 184 %if op_id == DEL_FILENODE:
183 185 ${_('File was deleted')}
184 186 %elif op_id == BIN_FILENODE:
185 187 ${_('Binary file hidden')}
186 188 %else:
187 189 ${op_text}
188 190 %endif
189 191 </td>
190 192 </tr>
191 193 %endfor
192 194 %endif
193 195 %if over_lines_changed_limit:
194 196 <tr class="cb-warning cb-collapser">
195 197 <td class="cb-text" ${c.diffmode == 'unified' and 'colspan=4' or 'colspan=4'}>
196 198 ${_('This diff has been collapsed as it changes many lines, (%i lines changed)' % lines_changed)}
197 199 <a href="#" class="cb-expand"
198 200 onclick="$(this).closest('table').removeClass('cb-collapsed'); return false;">${_('Show them')}
199 201 </a>
200 202 <a href="#" class="cb-collapse"
201 203 onclick="$(this).closest('table').addClass('cb-collapsed'); return false;">${_('Hide them')}
202 204 </a>
203 205 </td>
204 206 </tr>
205 207 %endif
206 208 %if filediff.patch['is_limited_diff']:
207 209 <tr class="cb-warning cb-collapser">
208 210 <td class="cb-text" ${c.diffmode == 'unified' and 'colspan=4' or 'colspan=4'}>
209 211 ${_('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>
210 212 </td>
211 213 </tr>
212 214 %endif
213 215 %for hunk in filediff.hunks:
214 216 <tr class="cb-hunk">
215 217 <td ${c.diffmode == 'unified' and 'colspan=3' or ''}>
216 218 ## TODO: dan: add ajax loading of more context here
217 219 ## <a href="#">
218 220 <i class="icon-more"></i>
219 221 ## </a>
220 222 </td>
221 223 <td ${c.diffmode == 'sideside' and 'colspan=5' or ''}>
222 224 @@
223 225 -${hunk.source_start},${hunk.source_length}
224 226 +${hunk.target_start},${hunk.target_length}
225 227 ${hunk.section_header}
226 228 </td>
227 229 </tr>
228 230 %if c.diffmode == 'unified':
229 231 ${render_hunk_lines_unified(hunk, use_comments=use_comments)}
230 232 %elif c.diffmode == 'sideside':
231 233 ${render_hunk_lines_sideside(hunk, use_comments=use_comments)}
232 234 %else:
233 235 <tr class="cb-line">
234 236 <td>unknown diff mode</td>
235 237 </tr>
236 238 %endif
237 239 %endfor
238 240 </table>
239 241 </div>
240 242 %endfor
241 243 </div>
242 244 </div>
243 245 </%def>
244 246
245 247 <%def name="diff_ops(filediff)">
246 248 <%
247 249 stats = filediff['patch']['stats']
248 250 from rhodecode.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
249 251 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE
250 252 %>
251 253 <span class="pill">
252 254 %if filediff.source_file_path and filediff.target_file_path:
253 255 %if filediff.source_file_path != filediff.target_file_path: # file was renamed
254 256 <strong>${filediff.target_file_path}</strong> β¬… <del>${filediff.source_file_path}</del>
255 257 %else:
256 258 ## file was modified
257 259 <strong>${filediff.source_file_path}</strong>
258 260 %endif
259 261 %else:
260 262 %if filediff.source_file_path:
261 263 ## file was deleted
262 264 <strong>${filediff.source_file_path}</strong>
263 265 %else:
264 266 ## file was added
265 267 <strong>${filediff.target_file_path}</strong>
266 268 %endif
267 269 %endif
268 270 </span>
269 271 <span class="pill-group" style="float: left">
270 272 %if filediff.patch['is_limited_diff']:
271 273 <span class="pill tooltip" op="limited" title="The stats for this diff are not complete">limited diff</span>
272 274 %endif
273 275 %if RENAMED_FILENODE in stats['ops']:
274 276 <span class="pill" op="renamed">renamed</span>
275 277 %endif
276 278
277 279 %if NEW_FILENODE in stats['ops']:
278 280 <span class="pill" op="created">created</span>
279 281 %if filediff['target_mode'].startswith('120'):
280 282 <span class="pill" op="symlink">symlink</span>
281 283 %else:
282 284 <span class="pill" op="mode">${nice_mode(filediff['target_mode'])}</span>
283 285 %endif
284 286 %endif
285 287
286 288 %if DEL_FILENODE in stats['ops']:
287 289 <span class="pill" op="removed">removed</span>
288 290 %endif
289 291
290 292 %if CHMOD_FILENODE in stats['ops']:
291 293 <span class="pill" op="mode">
292 294 ${nice_mode(filediff['source_mode'])} ➑ ${nice_mode(filediff['target_mode'])}
293 295 </span>
294 296 %endif
295 297 </span>
296 298
297 299 <a class="pill filediff-anchor" href="#a_${h.FID('', filediff.patch['filename'])}">ΒΆ</a>
298 300
299 301 <span class="pill-group" style="float: right">
300 302 %if BIN_FILENODE in stats['ops']:
301 303 <span class="pill" op="binary">binary</span>
302 304 %if MOD_FILENODE in stats['ops']:
303 305 <span class="pill" op="modified">modified</span>
304 306 %endif
305 307 %endif
306 308 %if stats['added']:
307 309 <span class="pill" op="added">+${stats['added']}</span>
308 310 %endif
309 311 %if stats['deleted']:
310 312 <span class="pill" op="deleted">-${stats['deleted']}</span>
311 313 %endif
312 314 </span>
313 315
314 316 </%def>
315 317
316 318 <%def name="nice_mode(filemode)">
317 319 ${filemode.startswith('100') and filemode[3:] or filemode}
318 320 </%def>
319 321
320 322 <%def name="diff_menu(filediff, use_comments=False)">
321 323 <div class="filediff-menu">
322 324 %if filediff.diffset.source_ref:
323 325 %if filediff.patch['operation'] in ['D', 'M']:
324 326 <a
325 327 class="tooltip"
326 328 href="${h.url('files_home',repo_name=filediff.diffset.repo_name,f_path=filediff.source_file_path,revision=filediff.diffset.source_ref)}"
327 329 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
328 330 >
329 331 ${_('Show file before')}
330 332 </a>
331 333 %else:
332 334 <span
333 335 class="tooltip"
334 336 title="${h.tooltip(_('File no longer present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
335 337 >
336 338 ${_('Show file before')}
337 339 </span>
338 340 %endif
339 341 %if filediff.patch['operation'] in ['A', 'M']:
340 342 <a
341 343 class="tooltip"
342 344 href="${h.url('files_home',repo_name=filediff.diffset.repo_name,f_path=filediff.target_file_path,revision=filediff.diffset.target_ref)}"
343 345 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
344 346 >
345 347 ${_('Show file after')}
346 348 </a>
347 349 %else:
348 350 <span
349 351 class="tooltip"
350 352 title="${h.tooltip(_('File no longer present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
351 353 >
352 354 ${_('Show file after')}
353 355 </span>
354 356 %endif
355 357 <a
356 358 class="tooltip"
357 359 title="${h.tooltip(_('Raw diff'))}"
358 360 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')}"
359 361 >
360 362 ${_('Raw diff')}
361 363 </a>
362 364 <a
363 365 class="tooltip"
364 366 title="${h.tooltip(_('Download diff'))}"
365 367 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')}"
366 368 >
367 369 ${_('Download diff')}
368 370 </a>
369 371
370 372 ## 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)
371 373 %if hasattr(c, 'ignorews_url'):
372 374 ${c.ignorews_url(request.GET, h.FID('', filediff['patch']['filename']))}
373 375 %endif
374 376 %if hasattr(c, 'context_url'):
375 377 ${c.context_url(request.GET, h.FID('', filediff['patch']['filename']))}
376 378 %endif
377 379
378 380
379 381 %if use_comments:
380 382 <a href="#" onclick="return Rhodecode.comments.toggleComments(this);">
381 383 <span class="show-comment-button">${_('Show comments')}</span><span class="hide-comment-button">${_('Hide comments')}</span>
382 384 </a>
383 385 %endif
384 386 %endif
385 387 </div>
386 388 </%def>
387 389
388 390
389 391 <%namespace name="commentblock" file="/changeset/changeset_file_comment.html"/>
390 392 <%def name="inline_comments_container(comments)">
391 393 <div class="inline-comments">
392 394 %for comment in comments:
393 395 ${commentblock.comment_block(comment, inline=True)}
394 396 %endfor
395 397 <span onclick="return Rhodecode.comments.createComment(this)"
396 398 class="btn btn-secondary cb-comment-add-button">
397 399 ${_('Add another comment')}
398 400 </span>
399 401 </div>
400 402 </%def>
401 403
402 404
403 405 <%def name="render_hunk_lines_sideside(hunk, use_comments=False)">
404 406 %for i, line in enumerate(hunk.sideside):
405 407 <%
406 408 old_line_anchor, new_line_anchor = None, None
407 409 if line.original.lineno:
408 410 old_line_anchor = diff_line_anchor(hunk.filediff.source_file_path, line.original.lineno, 'o')
409 411 if line.modified.lineno:
410 412 new_line_anchor = diff_line_anchor(hunk.filediff.target_file_path, line.modified.lineno, 'n')
411 413 %>
412 414 <tr class="cb-line">
413 415 <td class="cb-data ${action_class(line.original.action)}"
414 416 data-line-number="${line.original.lineno}"
415 417 >
416 418 <div>
417 419 %if line.original.comments:
418 420 <i class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
419 421 %endif
420 422 </div>
421 423 </td>
422 424 <td class="cb-lineno ${action_class(line.original.action)}"
423 425 data-line-number="${line.original.lineno}"
424 426 %if old_line_anchor:
425 427 id="${old_line_anchor}"
426 428 %endif
427 429 >
428 430 %if line.original.lineno:
429 431 <a name="${old_line_anchor}" href="#${old_line_anchor}">${line.original.lineno}</a>
430 432 %endif
431 433 </td>
432 434 <td class="cb-content ${action_class(line.original.action)}"
433 435 data-line-number="o${line.original.lineno}"
434 436 >
435 437 %if use_comments and line.original.lineno:
436 438 ${render_add_comment_button()}
437 439 %endif
438 440 <span class="cb-code">${line.original.action} ${line.original.content or '' | n}</span>
439 441 %if use_comments and line.original.lineno and line.original.comments:
440 442 ${inline_comments_container(line.original.comments)}
441 443 %endif
442 444 </td>
443 445 <td class="cb-data ${action_class(line.modified.action)}"
444 446 data-line-number="${line.modified.lineno}"
445 447 >
446 448 <div>
447 449 %if line.modified.comments:
448 450 <i class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
449 451 %endif
450 452 </div>
451 453 </td>
452 454 <td class="cb-lineno ${action_class(line.modified.action)}"
453 455 data-line-number="${line.modified.lineno}"
454 456 %if new_line_anchor:
455 457 id="${new_line_anchor}"
456 458 %endif
457 459 >
458 460 %if line.modified.lineno:
459 461 <a name="${new_line_anchor}" href="#${new_line_anchor}">${line.modified.lineno}</a>
460 462 %endif
461 463 </td>
462 464 <td class="cb-content ${action_class(line.modified.action)}"
463 465 data-line-number="n${line.modified.lineno}"
464 466 >
465 467 %if use_comments and line.modified.lineno:
466 468 ${render_add_comment_button()}
467 469 %endif
468 470 <span class="cb-code">${line.modified.action} ${line.modified.content or '' | n}</span>
469 471 %if use_comments and line.modified.lineno and line.modified.comments:
470 472 ${inline_comments_container(line.modified.comments)}
471 473 %endif
472 474 </td>
473 475 </tr>
474 476 %endfor
475 477 </%def>
476 478
477 479
478 480 <%def name="render_hunk_lines_unified(hunk, use_comments=False)">
479 481 %for old_line_no, new_line_no, action, content, comments in hunk.unified:
480 482 <%
481 483 old_line_anchor, new_line_anchor = None, None
482 484 if old_line_no:
483 485 old_line_anchor = diff_line_anchor(hunk.filediff.source_file_path, old_line_no, 'o')
484 486 if new_line_no:
485 487 new_line_anchor = diff_line_anchor(hunk.filediff.target_file_path, new_line_no, 'n')
486 488 %>
487 489 <tr class="cb-line">
488 490 <td class="cb-data ${action_class(action)}">
489 491 <div>
490 492 %if comments:
491 493 <i class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
492 494 %endif
493 495 </div>
494 496 </td>
495 497 <td class="cb-lineno ${action_class(action)}"
496 498 data-line-number="${old_line_no}"
497 499 %if old_line_anchor:
498 500 id="${old_line_anchor}"
499 501 %endif
500 502 >
501 503 %if old_line_anchor:
502 504 <a name="${old_line_anchor}" href="#${old_line_anchor}">${old_line_no}</a>
503 505 %endif
504 506 </td>
505 507 <td class="cb-lineno ${action_class(action)}"
506 508 data-line-number="${new_line_no}"
507 509 %if new_line_anchor:
508 510 id="${new_line_anchor}"
509 511 %endif
510 512 >
511 513 %if new_line_anchor:
512 514 <a name="${new_line_anchor}" href="#${new_line_anchor}">${new_line_no}</a>
513 515 %endif
514 516 </td>
515 517 <td class="cb-content ${action_class(action)}"
516 518 data-line-number="${new_line_no and 'n' or 'o'}${new_line_no or old_line_no}"
517 519 >
518 520 %if use_comments:
519 521 ${render_add_comment_button()}
520 522 %endif
521 523 <span class="cb-code">${action} ${content or '' | n}</span>
522 524 %if use_comments and comments:
523 525 ${inline_comments_container(comments)}
524 526 %endif
525 527 </td>
526 528 </tr>
527 529 %endfor
528 530 </%def>
529 531
530 532 <%def name="render_add_comment_button()">
531 533 <button
532 534 class="btn btn-small btn-primary cb-comment-box-opener"
533 535 onclick="return Rhodecode.comments.createComment(this)"
534 536 ><span>+</span></button>
535 537 </%def>
536 538
537 539 <%def name="render_diffset_menu()">
538 540 <div class="diffset-menu clearinner">
539 541 <div class="pull-right">
540 542 <div class="btn-group">
541 543 <a
542 544 class="btn ${c.diffmode == 'sideside' and 'btn-primary'} tooltip"
543 545 title="${_('View side by side')}"
544 546 href="${h.url_replace(diffmode='sideside')}">
545 547 <span>${_('Side by Side')}</span>
546 548 </a>
547 549 <a
548 550 class="btn ${c.diffmode == 'unified' and 'btn-primary'} tooltip"
549 551 title="${_('View unified')}" href="${h.url_replace(diffmode='unified')}">
550 552 <span>${_('Unified')}</span>
551 553 </a>
552 554 </div>
553 555 </div>
554 556 <div class="pull-left">
555 557 <div class="btn-group">
556 558 <a
557 559 class="btn"
558 560 href="#"
559 561 onclick="$('input[class=filediff-collapse-state]').prop('checked', false); return false">${_('Expand All')}</a>
560 562 <a
561 563 class="btn"
562 564 href="#"
563 565 onclick="$('input[class=filediff-collapse-state]').prop('checked', true); return false">${_('Collapse All')}</a>
564 566 </div>
565 567 </div>
566 568 </div>
567 569 </%def>
@@ -1,636 +1,511 b''
1 1 <%inherit file="/base/base.html"/>
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 <script type="text/javascript">
32 32 // TODO: marcink switch this to pyroutes
33 33 AJAX_COMMENT_DELETE_URL = "${url('pullrequest_comment_delete',repo_name=c.repo_name,comment_id='__COMMENT_ID__')}";
34 34 templateContext.pull_request_data.pull_request_id = ${c.pull_request.pull_request_id};
35 35 </script>
36 36 <div class="box">
37 37 <div class="title">
38 38 ${self.repo_page_title(c.rhodecode_db_repo)}
39 39 </div>
40 40
41 41 ${self.breadcrumbs()}
42 42
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 ${_('Pull request #%s') % c.pull_request.pull_request_id} ${_('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 edit')}</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 %if not c.pull_request.is_closed() and c.pull_request.shadow_merge_ref:
116 116 <div class="field">
117 117 <div class="label-summary">
118 118 <label>Merge:</label>
119 119 </div>
120 120 <div class="input">
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 </div>
129 129 </div>
130 130 %endif
131 131
132 132 <div class="field">
133 133 <div class="label-summary">
134 134 <label>${_('Review')}:</label>
135 135 </div>
136 136 <div class="input">
137 137 %if c.pull_request_review_status:
138 138 <div class="${'flag_status %s' % c.pull_request_review_status} tooltip pull-left"></div>
139 139 <span class="changeset-status-lbl tooltip">
140 140 %if c.pull_request.is_closed():
141 141 ${_('Closed')},
142 142 %endif
143 143 ${h.commit_status_lbl(c.pull_request_review_status)}
144 144 </span>
145 145 - ${ungettext('calculated based on %s reviewer vote', 'calculated based on %s reviewers votes', len(c.pull_request_reviewers)) % len(c.pull_request_reviewers)}
146 146 %endif
147 147 </div>
148 148 </div>
149 149 <div class="field">
150 150 <div class="pr-description-label label-summary">
151 151 <label>${_('Description')}:</label>
152 152 </div>
153 153 <div id="pr-desc" class="input">
154 154 <div class="pr-description">${h.urlify_commit_message(c.pull_request.description, c.repo_name)}</div>
155 155 </div>
156 156 <div id="pr-desc-edit" class="input textarea editor" style="display: none;">
157 157 <textarea id="pr-description-input" size="30">${c.pull_request.description}</textarea>
158 158 </div>
159 159 </div>
160 160 <div class="field">
161 161 <div class="label-summary">
162 162 <label>${_('Comments')}:</label>
163 163 </div>
164 164 <div class="input">
165 165 <div>
166 166 <div class="comments-number">
167 167 %if c.comments:
168 168 <a href="#comments">${ungettext("%d Pull request comment", "%d Pull request comments", len(c.comments)) % len(c.comments)}</a>,
169 169 %else:
170 170 ${ungettext("%d Pull request comment", "%d Pull request comments", len(c.comments)) % len(c.comments)}
171 171 %endif
172 172 %if c.inline_cnt:
173 173 ## this is replaced with a proper link to first comment via JS linkifyComments() func
174 174 <a href="#inline-comments" id="inline-comments-counter">${ungettext("%d Inline Comment", "%d Inline Comments", c.inline_cnt) % c.inline_cnt}</a>
175 175 %else:
176 176 ${ungettext("%d Inline Comment", "%d Inline Comments", c.inline_cnt) % c.inline_cnt}
177 177 %endif
178 178
179 179 % if c.outdated_cnt:
180 180 ,${ungettext("%d Outdated Comment", "%d Outdated Comments", c.outdated_cnt) % c.outdated_cnt} <span id="show-outdated-comments" class="btn btn-link">${_('(Show)')}</span>
181 181 % endif
182 182 </div>
183 183 </div>
184 184 </div>
185 185 </div>
186 186 <div id="pr-save" class="field" style="display: none;">
187 187 <div class="label-summary"></div>
188 188 <div class="input">
189 189 <span id="edit_pull_request" class="btn btn-small">${_('Save Changes')}</span>
190 190 </div>
191 191 </div>
192 192 </div>
193 193 </div>
194 194 <div>
195 195 ## AUTHOR
196 196 <div class="reviewers-title block-right">
197 197 <div class="pr-details-title">
198 198 ${_('Author')}
199 199 </div>
200 200 </div>
201 201 <div class="block-right pr-details-content reviewers">
202 202 <ul class="group_members">
203 203 <li>
204 204 ${self.gravatar_with_user(c.pull_request.author.email, 16)}
205 205 </li>
206 206 </ul>
207 207 </div>
208 208 ## REVIEWERS
209 209 <div class="reviewers-title block-right">
210 210 <div class="pr-details-title">
211 211 ${_('Pull request reviewers')}
212 212 %if c.allowed_to_update:
213 213 <span id="open_edit_reviewers" class="block-right action_button">${_('Edit')}</span>
214 214 <span id="close_edit_reviewers" class="block-right action_button" style="display: none;">${_('Close')}</span>
215 215 %endif
216 216 </div>
217 217 </div>
218 218 <div id="reviewers" class="block-right pr-details-content reviewers">
219 219 ## members goes here !
220 220 <input type="hidden" name="__start__" value="review_members:sequence">
221 221 <ul id="review_members" class="group_members">
222 222 %for member,reasons,status in c.pull_request_reviewers:
223 223 <li id="reviewer_${member.user_id}">
224 224 <div class="reviewers_member">
225 225 <div class="reviewer_status tooltip" title="${h.tooltip(h.commit_status_lbl(status[0][1].status if status else 'not_reviewed'))}">
226 226 <div class="${'flag_status %s' % (status[0][1].status if status else 'not_reviewed')} pull-left reviewer_member_status"></div>
227 227 </div>
228 228 <div id="reviewer_${member.user_id}_name" class="reviewer_name">
229 229 ${self.gravatar_with_user(member.email, 16)}
230 230 </div>
231 231 <input type="hidden" name="__start__" value="reviewer:mapping">
232 232 <input type="hidden" name="__start__" value="reasons:sequence">
233 233 %for reason in reasons:
234 234 <div class="reviewer_reason">- ${reason}</div>
235 235 <input type="hidden" name="reason" value="${reason}">
236 236
237 237 %endfor
238 238 <input type="hidden" name="__end__" value="reasons:sequence">
239 239 <input id="reviewer_${member.user_id}_input" type="hidden" value="${member.user_id}" name="user_id" />
240 240 <input type="hidden" name="__end__" value="reviewer:mapping">
241 241 %if c.allowed_to_update:
242 242 <div class="reviewer_member_remove action_button" onclick="removeReviewMember(${member.user_id}, true)" style="visibility: hidden;">
243 243 <i class="icon-remove-sign" ></i>
244 244 </div>
245 245 %endif
246 246 </div>
247 247 </li>
248 248 %endfor
249 249 </ul>
250 250 <input type="hidden" name="__end__" value="review_members:sequence">
251 251 %if not c.pull_request.is_closed():
252 252 <div id="add_reviewer_input" class='ac' style="display: none;">
253 253 %if c.allowed_to_update:
254 254 <div class="reviewer_ac">
255 255 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer'))}
256 256 <div id="reviewers_container"></div>
257 257 </div>
258 258 <div>
259 259 <span id="update_pull_request" class="btn btn-small">${_('Save Changes')}</span>
260 260 </div>
261 261 %endif
262 262 </div>
263 263 %endif
264 264 </div>
265 265 </div>
266 266 </div>
267 267 <div class="box">
268 268 ##DIFF
269 269 <div class="table" >
270 270 <div id="changeset_compare_view_content">
271 271 ##CS
272 272 % if c.missing_requirements:
273 273 <div class="box">
274 274 <div class="alert alert-warning">
275 275 <div>
276 276 <strong>${_('Missing requirements:')}</strong>
277 277 ${_('These commits cannot be displayed, because this repository uses the Mercurial largefiles extension, which was not enabled.')}
278 278 </div>
279 279 </div>
280 280 </div>
281 281 % elif c.missing_commits:
282 282 <div class="box">
283 283 <div class="alert alert-warning">
284 284 <div>
285 285 <strong>${_('Missing commits')}:</strong>
286 286 ${_('This pull request cannot be displayed, because one or more commits no longer exist in the source repository.')}
287 287 ${_('Please update this pull request, push the commits back into the source repository, or consider closing this pull request.')}
288 288 </div>
289 289 </div>
290 290 </div>
291 291 % endif
292 292 <div class="compare_view_commits_title">
293 293 % if c.allowed_to_update and not c.pull_request.is_closed():
294 294 <button id="update_commits" class="btn btn-small">${_('Update commits')}</button>
295 295 % endif
296 296 % if len(c.commit_ranges):
297 297 <h2>${ungettext('Compare View: %s commit','Compare View: %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}</h2>
298 298 % endif
299 299 </div>
300 300 % if not c.missing_commits:
301 301 <%include file="/compare/compare_commits.html" />
302 ## FILES
303 <div class="cs_files_title">
304 <span class="cs_files_expand">
305 <span id="expand_all_files">${_('Expand All')}</span> | <span id="collapse_all_files">${_('Collapse All')}</span>
306 </span>
307 <h2>
308 ${diff_block.diff_summary_text(len(c.files), c.lines_added, c.lines_deleted, c.limited_diff)}
309 </h2>
302 <div class="cs_files">
303 <%namespace name="cbdiffs" file="/codeblocks/diffs.html"/>
304 ${cbdiffs.render_diffset_menu()}
305 ${cbdiffs.render_diffset(
306 c.diffset, use_comments=True,
307 collapse_when_files_over=30,
308 disable_new_comments=c.pull_request.is_closed())}
309
310 310 </div>
311 311 % endif
312 <div class="cs_files">
313 %if not c.files and not c.missing_commits:
314 <span class="empty_data">${_('No files')}</span>
315 %endif
316 <table class="compare_view_files">
317 <%namespace name="diff_block" file="/changeset/diff_block.html"/>
318 %for FID, change, path, stats in c.files:
319 <tr class="cs_${change} collapse_file" fid="${FID}">
320 <td class="cs_icon_td">
321 <span class="collapse_file_icon" fid="${FID}"></span>
322 </td>
323 <td class="cs_icon_td">
324 <div class="flag_status not_reviewed hidden"></div>
325 </td>
326 <td class="cs_${change}" id="a_${FID}">
327 <div class="node">
328 <a href="#a_${FID}">
329 <i class="icon-file-${change.lower()}"></i>
330 ${h.safe_unicode(path)}
331 </a>
332 312 </div>
333 </td>
334 <td>
335 <div class="changes pull-right">${h.fancy_file_stats(stats)}</div>
336 <div class="comment-bubble pull-right" data-path="${path}">
337 <i class="icon-comment"></i>
338 </div>
339 </td>
340 </tr>
341 <tr fid="${FID}" id="diff_${FID}" class="diff_links">
342 <td></td>
343 <td></td>
344 <td class="cs_${change}">
345 %if c.target_repo.repo_name == c.repo_name:
346 ${diff_block.diff_menu(c.repo_name, h.safe_unicode(path), c.target_ref, c.source_ref, change)}
347 %else:
348 ## this is slightly different case later, since the other repo can have this
349 ## file in other state than the origin repo
350 ${diff_block.diff_menu(c.target_repo.repo_name, h.safe_unicode(path), c.target_ref, c.source_ref, change)}
351 %endif
352 </td>
353 <td class="td-actions rc-form">
354 <div data-comment-id="${FID}" class="btn-link show-inline-comments comments-visible">
355 <span class="comments-show">${_('Show comments')}</span>
356 <span class="comments-hide">${_('Hide comments')}</span>
357 </div>
358 </td>
359 </tr>
360 <tr id="tr_${FID}">
361 <td></td>
362 <td></td>
363 <td class="injected_diff" colspan="2">
364 ${diff_block.diff_block_simple([c.changes[FID]])}
365 </td>
366 </tr>
367
368 ## Loop through inline comments
369 % if c.outdated_comments.get(path,False):
370 <tr class="outdated">
371 <td></td>
372 <td></td>
373 <td colspan="2">
374 <p>${_('Outdated Inline Comments')}:</p>
375 </td>
376 </tr>
377 <tr class="outdated">
378 <td></td>
379 <td></td>
380 <td colspan="2" class="outdated_comment_block">
381 % for line, comments in c.outdated_comments[path].iteritems():
382 <div class="inline-comment-placeholder" path="${path}" target_id="${h.safeid(h.safe_unicode(path))}">
383 % for co in comments:
384 ${comment.comment_block_outdated(co)}
385 % endfor
386 </div>
387 % endfor
388 </td>
389 </tr>
390 % endif
391 %endfor
392 ## Loop through inline comments for deleted files
393 %for path in c.deleted_files:
394 <tr class="outdated deleted">
395 <td></td>
396 <td></td>
397 <td>${path}</td>
398 </tr>
399 <tr class="outdated deleted">
400 <td></td>
401 <td></td>
402 <td>(${_('Removed')})</td>
403 </tr>
404 % if path in c.outdated_comments:
405 <tr class="outdated deleted">
406 <td></td>
407 <td></td>
408 <td colspan="2">
409 <p>${_('Outdated Inline Comments')}:</p>
410 </td>
411 </tr>
412 <tr class="outdated">
413 <td></td>
414 <td></td>
415 <td colspan="2" class="outdated_comment_block">
416 % for line, comments in c.outdated_comments[path].iteritems():
417 <div class="inline-comment-placeholder" path="${path}" target_id="${h.safeid(h.safe_unicode(path))}">
418 % for co in comments:
419 ${comment.comment_block_outdated(co)}
420 % endfor
421 </div>
422 % endfor
423 </td>
424 </tr>
425 % endif
426 %endfor
427 </table>
428 </div>
429 % if c.limited_diff:
430 <h5>${_('Commit was too big and was cut off...')} <a href="${h.url.current(fulldiff=1, **request.GET.mixed())}" onclick="return confirm('${_("Showing a huge diff might take some time and resources")}')">${_('Show full diff')}</a></h5>
431 % endif
432 </div>
433 </div>
434
435 % if c.limited_diff:
436 <p>${_('Commit was too big and was cut off...')} <a href="${h.url.current(fulldiff=1, **request.GET.mixed())}" onclick="return confirm('${_("Showing a huge diff might take some time and resources")}')">${_('Show full diff')}</a></p>
437 % endif
438 313
439 314 ## template for inline comment form
440 315 <%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
441 316 ${comment.comment_inline_form()}
442 317
443 318 ## render comments and inlines
444 319 ${comment.generate_comments(include_pull_request=True, is_pull_request=True)}
445 320
446 321 % if not c.pull_request.is_closed():
447 322 ## main comment form and it status
448 323 ${comment.comments(h.url('pullrequest_comment', repo_name=c.repo_name,
449 324 pull_request_id=c.pull_request.pull_request_id),
450 325 c.pull_request_review_status,
451 326 is_pull_request=True, change_status=c.allowed_to_change_status)}
452 327 %endif
453 328
454 329 <script type="text/javascript">
455 330 if (location.hash) {
456 331 var result = splitDelimitedHash(location.hash);
457 332 var line = $('html').find(result.loc);
458 333 if (line.length > 0){
459 334 offsetScroll(line, 70);
460 335 }
461 336 }
462 337 $(function(){
463 338 ReviewerAutoComplete('user');
464 339 // custom code mirror
465 340 var codeMirrorInstance = initPullRequestsCodeMirror('#pr-description-input');
466 341
467 342 var PRDetails = {
468 343 editButton: $('#open_edit_pullrequest'),
469 344 closeButton: $('#close_edit_pullrequest'),
470 345 deleteButton: $('#delete_pullrequest'),
471 346 viewFields: $('#pr-desc, #pr-title'),
472 347 editFields: $('#pr-desc-edit, #pr-title-edit, #pr-save'),
473 348
474 349 init: function() {
475 350 var that = this;
476 351 this.editButton.on('click', function(e) { that.edit(); });
477 352 this.closeButton.on('click', function(e) { that.view(); });
478 353 },
479 354
480 355 edit: function(event) {
481 356 this.viewFields.hide();
482 357 this.editButton.hide();
483 358 this.deleteButton.hide();
484 359 this.closeButton.show();
485 360 this.editFields.show();
486 361 codeMirrorInstance.refresh();
487 362 },
488 363
489 364 view: function(event) {
490 365 this.editButton.show();
491 366 this.deleteButton.show();
492 367 this.editFields.hide();
493 368 this.closeButton.hide();
494 369 this.viewFields.show();
495 370 }
496 371 };
497 372
498 373 var ReviewersPanel = {
499 374 editButton: $('#open_edit_reviewers'),
500 375 closeButton: $('#close_edit_reviewers'),
501 376 addButton: $('#add_reviewer_input'),
502 377 removeButtons: $('.reviewer_member_remove'),
503 378
504 379 init: function() {
505 380 var that = this;
506 381 this.editButton.on('click', function(e) { that.edit(); });
507 382 this.closeButton.on('click', function(e) { that.close(); });
508 383 },
509 384
510 385 edit: function(event) {
511 386 this.editButton.hide();
512 387 this.closeButton.show();
513 388 this.addButton.show();
514 389 this.removeButtons.css('visibility', 'visible');
515 390 },
516 391
517 392 close: function(event) {
518 393 this.editButton.show();
519 394 this.closeButton.hide();
520 395 this.addButton.hide();
521 396 this.removeButtons.css('visibility', 'hidden');
522 397 },
523 398 };
524 399
525 400 PRDetails.init();
526 401 ReviewersPanel.init();
527 402
528 403 $('#show-outdated-comments').on('click', function(e){
529 404 var button = $(this);
530 405 var outdated = $('.outdated');
531 406 if (button.html() === "(Show)") {
532 407 button.html("(Hide)");
533 408 outdated.show();
534 409 } else {
535 410 button.html("(Show)");
536 411 outdated.hide();
537 412 }
538 413 });
539 414
540 415 $('.show-inline-comments').on('change', function(e){
541 416 var show = 'none';
542 417 var target = e.currentTarget;
543 418 if(target.checked){
544 419 show = ''
545 420 }
546 421 var boxid = $(target).attr('id_for');
547 422 var comments = $('#{0} .inline-comments'.format(boxid));
548 423 var fn_display = function(idx){
549 424 $(this).css('display', show);
550 425 };
551 426 $(comments).each(fn_display);
552 427 var btns = $('#{0} .inline-comments-button'.format(boxid));
553 428 $(btns).each(fn_display);
554 429 });
555 430
556 431 // inject comments into their proper positions
557 432 var file_comments = $('.inline-comment-placeholder');
558 433 %if c.pull_request.is_closed():
559 434 renderInlineComments(file_comments, false);
560 435 %else:
561 436 renderInlineComments(file_comments, true);
562 437 %endif
563 438 var commentTotals = {};
564 439 $.each(file_comments, function(i, comment) {
565 440 var path = $(comment).attr('path');
566 441 var comms = $(comment).children().length;
567 442 if (path in commentTotals) {
568 443 commentTotals[path] += comms;
569 444 } else {
570 445 commentTotals[path] = comms;
571 446 }
572 447 });
573 448 $.each(commentTotals, function(path, total) {
574 449 var elem = $('.comment-bubble[data-path="'+ path +'"]');
575 450 elem.css('visibility', 'visible');
576 451 elem.html(elem.html() + ' ' + total );
577 452 });
578 453
579 454 $('#merge_pull_request_form').submit(function() {
580 455 if (!$('#merge_pull_request').attr('disabled')) {
581 456 $('#merge_pull_request').attr('disabled', 'disabled');
582 457 }
583 458 return true;
584 459 });
585 460
586 461 $('#edit_pull_request').on('click', function(e){
587 462 var title = $('#pr-title-input').val();
588 463 var description = codeMirrorInstance.getValue();
589 464 editPullRequest(
590 465 "${c.repo_name}", "${c.pull_request.pull_request_id}",
591 466 title, description);
592 467 });
593 468
594 469 $('#update_pull_request').on('click', function(e){
595 470 updateReviewers(undefined, "${c.repo_name}", "${c.pull_request.pull_request_id}");
596 471 });
597 472
598 473 $('#update_commits').on('click', function(e){
599 474 var isDisabled = !$(e.currentTarget).attr('disabled');
600 475 $(e.currentTarget).text(_gettext('Updating...'));
601 476 $(e.currentTarget).attr('disabled', 'disabled');
602 477 if(isDisabled){
603 478 updateCommits("${c.repo_name}", "${c.pull_request.pull_request_id}");
604 479 }
605 480
606 481 });
607 482 // fixing issue with caches on firefox
608 483 $('#update_commits').removeAttr("disabled");
609 484
610 485 $('#close_pull_request').on('click', function(e){
611 486 closePullRequest("${c.repo_name}", "${c.pull_request.pull_request_id}");
612 487 });
613 488
614 489 $('.show-inline-comments').on('click', function(e){
615 490 var boxid = $(this).attr('data-comment-id');
616 491 var button = $(this);
617 492
618 493 if(button.hasClass("comments-visible")) {
619 494 $('#{0} .inline-comments'.format(boxid)).each(function(index){
620 495 $(this).hide();
621 496 });
622 497 button.removeClass("comments-visible");
623 498 } else {
624 499 $('#{0} .inline-comments'.format(boxid)).each(function(index){
625 500 $(this).show();
626 501 });
627 502 button.addClass("comments-visible");
628 503 }
629 504 });
630 505 })
631 506 </script>
632 507
633 508 </div>
634 509 </div>
635 510
636 511 </%def>
General Comments 0
You need to be logged in to leave comments. Login now