##// END OF EJS Templates
pull-requests: use merge info to show how Pull requests will be merged....
marcink -
r2053:bad6294f default
parent child Browse files
Show More
@@ -1,1191 +1,1193 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22 import collections
23 23
24 24 import formencode
25 25 import peppercorn
26 26 from pyramid.httpexceptions import (
27 27 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest)
28 28 from pyramid.view import view_config
29 29 from pyramid.renderers import render
30 30
31 31 from rhodecode import events
32 32 from rhodecode.apps._base import RepoAppView, DataGridAppView
33 33
34 34 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
35 35 from rhodecode.lib.base import vcs_operation_context
36 36 from rhodecode.lib.ext_json import json
37 37 from rhodecode.lib.auth import (
38 38 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous, CSRFRequired)
39 39 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode
40 40 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
41 41 from rhodecode.lib.vcs.exceptions import (CommitDoesNotExistError,
42 42 RepositoryRequirementError, NodeDoesNotExistError, EmptyRepositoryError)
43 43 from rhodecode.model.changeset_status import ChangesetStatusModel
44 44 from rhodecode.model.comment import CommentsModel
45 45 from rhodecode.model.db import (func, or_, PullRequest, PullRequestVersion,
46 46 ChangesetComment, ChangesetStatus, Repository)
47 47 from rhodecode.model.forms import PullRequestForm
48 48 from rhodecode.model.meta import Session
49 49 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
50 50 from rhodecode.model.scm import ScmModel
51 51
52 52 log = logging.getLogger(__name__)
53 53
54 54
55 55 class RepoPullRequestsView(RepoAppView, DataGridAppView):
56 56
57 57 def load_default_context(self):
58 58 c = self._get_local_tmpl_context(include_app_defaults=True)
59 59 # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead
60 60 c.repo_info = self.db_repo
61 61 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
62 62 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
63 63 self._register_global_c(c)
64 64 return c
65 65
66 66 def _get_pull_requests_list(
67 67 self, repo_name, source, filter_type, opened_by, statuses):
68 68
69 69 draw, start, limit = self._extract_chunk(self.request)
70 70 search_q, order_by, order_dir = self._extract_ordering(self.request)
71 71 _render = self.request.get_partial_renderer(
72 72 'data_table/_dt_elements.mako')
73 73
74 74 # pagination
75 75
76 76 if filter_type == 'awaiting_review':
77 77 pull_requests = PullRequestModel().get_awaiting_review(
78 78 repo_name, source=source, opened_by=opened_by,
79 79 statuses=statuses, offset=start, length=limit,
80 80 order_by=order_by, order_dir=order_dir)
81 81 pull_requests_total_count = PullRequestModel().count_awaiting_review(
82 82 repo_name, source=source, statuses=statuses,
83 83 opened_by=opened_by)
84 84 elif filter_type == 'awaiting_my_review':
85 85 pull_requests = PullRequestModel().get_awaiting_my_review(
86 86 repo_name, source=source, opened_by=opened_by,
87 87 user_id=self._rhodecode_user.user_id, statuses=statuses,
88 88 offset=start, length=limit, order_by=order_by,
89 89 order_dir=order_dir)
90 90 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
91 91 repo_name, source=source, user_id=self._rhodecode_user.user_id,
92 92 statuses=statuses, opened_by=opened_by)
93 93 else:
94 94 pull_requests = PullRequestModel().get_all(
95 95 repo_name, source=source, opened_by=opened_by,
96 96 statuses=statuses, offset=start, length=limit,
97 97 order_by=order_by, order_dir=order_dir)
98 98 pull_requests_total_count = PullRequestModel().count_all(
99 99 repo_name, source=source, statuses=statuses,
100 100 opened_by=opened_by)
101 101
102 102 data = []
103 103 comments_model = CommentsModel()
104 104 for pr in pull_requests:
105 105 comments = comments_model.get_all_comments(
106 106 self.db_repo.repo_id, pull_request=pr)
107 107
108 108 data.append({
109 109 'name': _render('pullrequest_name',
110 110 pr.pull_request_id, pr.target_repo.repo_name),
111 111 'name_raw': pr.pull_request_id,
112 112 'status': _render('pullrequest_status',
113 113 pr.calculated_review_status()),
114 114 'title': _render(
115 115 'pullrequest_title', pr.title, pr.description),
116 116 'description': h.escape(pr.description),
117 117 'updated_on': _render('pullrequest_updated_on',
118 118 h.datetime_to_time(pr.updated_on)),
119 119 'updated_on_raw': h.datetime_to_time(pr.updated_on),
120 120 'created_on': _render('pullrequest_updated_on',
121 121 h.datetime_to_time(pr.created_on)),
122 122 'created_on_raw': h.datetime_to_time(pr.created_on),
123 123 'author': _render('pullrequest_author',
124 124 pr.author.full_contact, ),
125 125 'author_raw': pr.author.full_name,
126 126 'comments': _render('pullrequest_comments', len(comments)),
127 127 'comments_raw': len(comments),
128 128 'closed': pr.is_closed(),
129 129 })
130 130
131 131 data = ({
132 132 'draw': draw,
133 133 'data': data,
134 134 'recordsTotal': pull_requests_total_count,
135 135 'recordsFiltered': pull_requests_total_count,
136 136 })
137 137 return data
138 138
139 139 @LoginRequired()
140 140 @HasRepoPermissionAnyDecorator(
141 141 'repository.read', 'repository.write', 'repository.admin')
142 142 @view_config(
143 143 route_name='pullrequest_show_all', request_method='GET',
144 144 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
145 145 def pull_request_list(self):
146 146 c = self.load_default_context()
147 147
148 148 req_get = self.request.GET
149 149 c.source = str2bool(req_get.get('source'))
150 150 c.closed = str2bool(req_get.get('closed'))
151 151 c.my = str2bool(req_get.get('my'))
152 152 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
153 153 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
154 154
155 155 c.active = 'open'
156 156 if c.my:
157 157 c.active = 'my'
158 158 if c.closed:
159 159 c.active = 'closed'
160 160 if c.awaiting_review and not c.source:
161 161 c.active = 'awaiting'
162 162 if c.source and not c.awaiting_review:
163 163 c.active = 'source'
164 164 if c.awaiting_my_review:
165 165 c.active = 'awaiting_my'
166 166
167 167 return self._get_template_context(c)
168 168
169 169 @LoginRequired()
170 170 @HasRepoPermissionAnyDecorator(
171 171 'repository.read', 'repository.write', 'repository.admin')
172 172 @view_config(
173 173 route_name='pullrequest_show_all_data', request_method='GET',
174 174 renderer='json_ext', xhr=True)
175 175 def pull_request_list_data(self):
176 176
177 177 # additional filters
178 178 req_get = self.request.GET
179 179 source = str2bool(req_get.get('source'))
180 180 closed = str2bool(req_get.get('closed'))
181 181 my = str2bool(req_get.get('my'))
182 182 awaiting_review = str2bool(req_get.get('awaiting_review'))
183 183 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
184 184
185 185 filter_type = 'awaiting_review' if awaiting_review \
186 186 else 'awaiting_my_review' if awaiting_my_review \
187 187 else None
188 188
189 189 opened_by = None
190 190 if my:
191 191 opened_by = [self._rhodecode_user.user_id]
192 192
193 193 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
194 194 if closed:
195 195 statuses = [PullRequest.STATUS_CLOSED]
196 196
197 197 data = self._get_pull_requests_list(
198 198 repo_name=self.db_repo_name, source=source,
199 199 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
200 200
201 201 return data
202 202
203 203 def _get_pr_version(self, pull_request_id, version=None):
204 204 at_version = None
205 205
206 206 if version and version == 'latest':
207 207 pull_request_ver = PullRequest.get(pull_request_id)
208 208 pull_request_obj = pull_request_ver
209 209 _org_pull_request_obj = pull_request_obj
210 210 at_version = 'latest'
211 211 elif version:
212 212 pull_request_ver = PullRequestVersion.get_or_404(version)
213 213 pull_request_obj = pull_request_ver
214 214 _org_pull_request_obj = pull_request_ver.pull_request
215 215 at_version = pull_request_ver.pull_request_version_id
216 216 else:
217 217 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
218 218 pull_request_id)
219 219
220 220 pull_request_display_obj = PullRequest.get_pr_display_object(
221 221 pull_request_obj, _org_pull_request_obj)
222 222
223 223 return _org_pull_request_obj, pull_request_obj, \
224 224 pull_request_display_obj, at_version
225 225
226 226 def _get_diffset(self, source_repo_name, source_repo,
227 227 source_ref_id, target_ref_id,
228 228 target_commit, source_commit, diff_limit, fulldiff,
229 229 file_limit, display_inline_comments):
230 230
231 231 vcs_diff = PullRequestModel().get_diff(
232 232 source_repo, source_ref_id, target_ref_id)
233 233
234 234 diff_processor = diffs.DiffProcessor(
235 235 vcs_diff, format='newdiff', diff_limit=diff_limit,
236 236 file_limit=file_limit, show_full_diff=fulldiff)
237 237
238 238 _parsed = diff_processor.prepare()
239 239
240 240 def _node_getter(commit):
241 241 def get_node(fname):
242 242 try:
243 243 return commit.get_node(fname)
244 244 except NodeDoesNotExistError:
245 245 return None
246 246
247 247 return get_node
248 248
249 249 diffset = codeblocks.DiffSet(
250 250 repo_name=self.db_repo_name,
251 251 source_repo_name=source_repo_name,
252 252 source_node_getter=_node_getter(target_commit),
253 253 target_node_getter=_node_getter(source_commit),
254 254 comments=display_inline_comments
255 255 )
256 256 diffset = diffset.render_patchset(
257 257 _parsed, target_commit.raw_id, source_commit.raw_id)
258 258
259 259 return diffset
260 260
261 261 @LoginRequired()
262 262 @HasRepoPermissionAnyDecorator(
263 263 'repository.read', 'repository.write', 'repository.admin')
264 264 @view_config(
265 265 route_name='pullrequest_show', request_method='GET',
266 266 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
267 267 def pull_request_show(self):
268 268 pull_request_id = self.request.matchdict['pull_request_id']
269 269
270 270 c = self.load_default_context()
271 271
272 272 version = self.request.GET.get('version')
273 273 from_version = self.request.GET.get('from_version') or version
274 274 merge_checks = self.request.GET.get('merge_checks')
275 275 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
276 276
277 277 (pull_request_latest,
278 278 pull_request_at_ver,
279 279 pull_request_display_obj,
280 280 at_version) = self._get_pr_version(
281 281 pull_request_id, version=version)
282 282 pr_closed = pull_request_latest.is_closed()
283 283
284 284 if pr_closed and (version or from_version):
285 285 # not allow to browse versions
286 286 raise HTTPFound(h.route_path(
287 287 'pullrequest_show', repo_name=self.db_repo_name,
288 288 pull_request_id=pull_request_id))
289 289
290 290 versions = pull_request_display_obj.versions()
291 291
292 292 c.at_version = at_version
293 293 c.at_version_num = (at_version
294 294 if at_version and at_version != 'latest'
295 295 else None)
296 296 c.at_version_pos = ChangesetComment.get_index_from_version(
297 297 c.at_version_num, versions)
298 298
299 299 (prev_pull_request_latest,
300 300 prev_pull_request_at_ver,
301 301 prev_pull_request_display_obj,
302 302 prev_at_version) = self._get_pr_version(
303 303 pull_request_id, version=from_version)
304 304
305 305 c.from_version = prev_at_version
306 306 c.from_version_num = (prev_at_version
307 307 if prev_at_version and prev_at_version != 'latest'
308 308 else None)
309 309 c.from_version_pos = ChangesetComment.get_index_from_version(
310 310 c.from_version_num, versions)
311 311
312 312 # define if we're in COMPARE mode or VIEW at version mode
313 313 compare = at_version != prev_at_version
314 314
315 315 # pull_requests repo_name we opened it against
316 316 # ie. target_repo must match
317 317 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
318 318 raise HTTPNotFound()
319 319
320 320 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
321 321 pull_request_at_ver)
322 322
323 323 c.pull_request = pull_request_display_obj
324 324 c.pull_request_latest = pull_request_latest
325 325
326 326 if compare or (at_version and not at_version == 'latest'):
327 327 c.allowed_to_change_status = False
328 328 c.allowed_to_update = False
329 329 c.allowed_to_merge = False
330 330 c.allowed_to_delete = False
331 331 c.allowed_to_comment = False
332 332 c.allowed_to_close = False
333 333 else:
334 334 can_change_status = PullRequestModel().check_user_change_status(
335 335 pull_request_at_ver, self._rhodecode_user)
336 336 c.allowed_to_change_status = can_change_status and not pr_closed
337 337
338 338 c.allowed_to_update = PullRequestModel().check_user_update(
339 339 pull_request_latest, self._rhodecode_user) and not pr_closed
340 340 c.allowed_to_merge = PullRequestModel().check_user_merge(
341 341 pull_request_latest, self._rhodecode_user) and not pr_closed
342 342 c.allowed_to_delete = PullRequestModel().check_user_delete(
343 343 pull_request_latest, self._rhodecode_user) and not pr_closed
344 344 c.allowed_to_comment = not pr_closed
345 345 c.allowed_to_close = c.allowed_to_merge and not pr_closed
346 346
347 347 c.forbid_adding_reviewers = False
348 348 c.forbid_author_to_review = False
349 349 c.forbid_commit_author_to_review = False
350 350
351 351 if pull_request_latest.reviewer_data and \
352 352 'rules' in pull_request_latest.reviewer_data:
353 353 rules = pull_request_latest.reviewer_data['rules'] or {}
354 354 try:
355 355 c.forbid_adding_reviewers = rules.get(
356 356 'forbid_adding_reviewers')
357 357 c.forbid_author_to_review = rules.get(
358 358 'forbid_author_to_review')
359 359 c.forbid_commit_author_to_review = rules.get(
360 360 'forbid_commit_author_to_review')
361 361 except Exception:
362 362 pass
363 363
364 364 # check merge capabilities
365 365 _merge_check = MergeCheck.validate(
366 366 pull_request_latest, user=self._rhodecode_user)
367 367 c.pr_merge_errors = _merge_check.error_details
368 368 c.pr_merge_possible = not _merge_check.failed
369 369 c.pr_merge_message = _merge_check.merge_msg
370 370
371 c.pr_merge_info = MergeCheck.get_merge_conditions(pull_request_latest)
372
371 373 c.pull_request_review_status = _merge_check.review_status
372 374 if merge_checks:
373 375 self.request.override_renderer = \
374 376 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
375 377 return self._get_template_context(c)
376 378
377 379 comments_model = CommentsModel()
378 380
379 381 # reviewers and statuses
380 382 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
381 383 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
382 384
383 385 # GENERAL COMMENTS with versions #
384 386 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
385 387 q = q.order_by(ChangesetComment.comment_id.asc())
386 388 general_comments = q
387 389
388 390 # pick comments we want to render at current version
389 391 c.comment_versions = comments_model.aggregate_comments(
390 392 general_comments, versions, c.at_version_num)
391 393 c.comments = c.comment_versions[c.at_version_num]['until']
392 394
393 395 # INLINE COMMENTS with versions #
394 396 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
395 397 q = q.order_by(ChangesetComment.comment_id.asc())
396 398 inline_comments = q
397 399
398 400 c.inline_versions = comments_model.aggregate_comments(
399 401 inline_comments, versions, c.at_version_num, inline=True)
400 402
401 403 # inject latest version
402 404 latest_ver = PullRequest.get_pr_display_object(
403 405 pull_request_latest, pull_request_latest)
404 406
405 407 c.versions = versions + [latest_ver]
406 408
407 409 # if we use version, then do not show later comments
408 410 # than current version
409 411 display_inline_comments = collections.defaultdict(
410 412 lambda: collections.defaultdict(list))
411 413 for co in inline_comments:
412 414 if c.at_version_num:
413 415 # pick comments that are at least UPTO given version, so we
414 416 # don't render comments for higher version
415 417 should_render = co.pull_request_version_id and \
416 418 co.pull_request_version_id <= c.at_version_num
417 419 else:
418 420 # showing all, for 'latest'
419 421 should_render = True
420 422
421 423 if should_render:
422 424 display_inline_comments[co.f_path][co.line_no].append(co)
423 425
424 426 # load diff data into template context, if we use compare mode then
425 427 # diff is calculated based on changes between versions of PR
426 428
427 429 source_repo = pull_request_at_ver.source_repo
428 430 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
429 431
430 432 target_repo = pull_request_at_ver.target_repo
431 433 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
432 434
433 435 if compare:
434 436 # in compare switch the diff base to latest commit from prev version
435 437 target_ref_id = prev_pull_request_display_obj.revisions[0]
436 438
437 439 # despite opening commits for bookmarks/branches/tags, we always
438 440 # convert this to rev to prevent changes after bookmark or branch change
439 441 c.source_ref_type = 'rev'
440 442 c.source_ref = source_ref_id
441 443
442 444 c.target_ref_type = 'rev'
443 445 c.target_ref = target_ref_id
444 446
445 447 c.source_repo = source_repo
446 448 c.target_repo = target_repo
447 449
448 450 c.commit_ranges = []
449 451 source_commit = EmptyCommit()
450 452 target_commit = EmptyCommit()
451 453 c.missing_requirements = False
452 454
453 455 source_scm = source_repo.scm_instance()
454 456 target_scm = target_repo.scm_instance()
455 457
456 458 # try first shadow repo, fallback to regular repo
457 459 try:
458 460 commits_source_repo = pull_request_latest.get_shadow_repo()
459 461 except Exception:
460 462 log.debug('Failed to get shadow repo', exc_info=True)
461 463 commits_source_repo = source_scm
462 464
463 465 c.commits_source_repo = commits_source_repo
464 466 commit_cache = {}
465 467 try:
466 468 pre_load = ["author", "branch", "date", "message"]
467 469 show_revs = pull_request_at_ver.revisions
468 470 for rev in show_revs:
469 471 comm = commits_source_repo.get_commit(
470 472 commit_id=rev, pre_load=pre_load)
471 473 c.commit_ranges.append(comm)
472 474 commit_cache[comm.raw_id] = comm
473 475
474 476 # Order here matters, we first need to get target, and then
475 477 # the source
476 478 target_commit = commits_source_repo.get_commit(
477 479 commit_id=safe_str(target_ref_id))
478 480
479 481 source_commit = commits_source_repo.get_commit(
480 482 commit_id=safe_str(source_ref_id))
481 483
482 484 except CommitDoesNotExistError:
483 485 log.warning(
484 486 'Failed to get commit from `{}` repo'.format(
485 487 commits_source_repo), exc_info=True)
486 488 except RepositoryRequirementError:
487 489 log.warning(
488 490 'Failed to get all required data from repo', exc_info=True)
489 491 c.missing_requirements = True
490 492
491 493 c.ancestor = None # set it to None, to hide it from PR view
492 494
493 495 try:
494 496 ancestor_id = source_scm.get_common_ancestor(
495 497 source_commit.raw_id, target_commit.raw_id, target_scm)
496 498 c.ancestor_commit = source_scm.get_commit(ancestor_id)
497 499 except Exception:
498 500 c.ancestor_commit = None
499 501
500 502 c.statuses = source_repo.statuses(
501 503 [x.raw_id for x in c.commit_ranges])
502 504
503 505 # auto collapse if we have more than limit
504 506 collapse_limit = diffs.DiffProcessor._collapse_commits_over
505 507 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
506 508 c.compare_mode = compare
507 509
508 510 # diff_limit is the old behavior, will cut off the whole diff
509 511 # if the limit is applied otherwise will just hide the
510 512 # big files from the front-end
511 513 diff_limit = c.visual.cut_off_limit_diff
512 514 file_limit = c.visual.cut_off_limit_file
513 515
514 516 c.missing_commits = False
515 517 if (c.missing_requirements
516 518 or isinstance(source_commit, EmptyCommit)
517 519 or source_commit == target_commit):
518 520
519 521 c.missing_commits = True
520 522 else:
521 523
522 524 c.diffset = self._get_diffset(
523 525 c.source_repo.repo_name, commits_source_repo,
524 526 source_ref_id, target_ref_id,
525 527 target_commit, source_commit,
526 528 diff_limit, c.fulldiff, file_limit, display_inline_comments)
527 529
528 530 c.limited_diff = c.diffset.limited_diff
529 531
530 532 # calculate removed files that are bound to comments
531 533 comment_deleted_files = [
532 534 fname for fname in display_inline_comments
533 535 if fname not in c.diffset.file_stats]
534 536
535 537 c.deleted_files_comments = collections.defaultdict(dict)
536 538 for fname, per_line_comments in display_inline_comments.items():
537 539 if fname in comment_deleted_files:
538 540 c.deleted_files_comments[fname]['stats'] = 0
539 541 c.deleted_files_comments[fname]['comments'] = list()
540 542 for lno, comments in per_line_comments.items():
541 543 c.deleted_files_comments[fname]['comments'].extend(
542 544 comments)
543 545
544 546 # this is a hack to properly display links, when creating PR, the
545 547 # compare view and others uses different notation, and
546 548 # compare_commits.mako renders links based on the target_repo.
547 549 # We need to swap that here to generate it properly on the html side
548 550 c.target_repo = c.source_repo
549 551
550 552 c.commit_statuses = ChangesetStatus.STATUSES
551 553
552 554 c.show_version_changes = not pr_closed
553 555 if c.show_version_changes:
554 556 cur_obj = pull_request_at_ver
555 557 prev_obj = prev_pull_request_at_ver
556 558
557 559 old_commit_ids = prev_obj.revisions
558 560 new_commit_ids = cur_obj.revisions
559 561 commit_changes = PullRequestModel()._calculate_commit_id_changes(
560 562 old_commit_ids, new_commit_ids)
561 563 c.commit_changes_summary = commit_changes
562 564
563 565 # calculate the diff for commits between versions
564 566 c.commit_changes = []
565 567 mark = lambda cs, fw: list(
566 568 h.itertools.izip_longest([], cs, fillvalue=fw))
567 569 for c_type, raw_id in mark(commit_changes.added, 'a') \
568 570 + mark(commit_changes.removed, 'r') \
569 571 + mark(commit_changes.common, 'c'):
570 572
571 573 if raw_id in commit_cache:
572 574 commit = commit_cache[raw_id]
573 575 else:
574 576 try:
575 577 commit = commits_source_repo.get_commit(raw_id)
576 578 except CommitDoesNotExistError:
577 579 # in case we fail extracting still use "dummy" commit
578 580 # for display in commit diff
579 581 commit = h.AttributeDict(
580 582 {'raw_id': raw_id,
581 583 'message': 'EMPTY or MISSING COMMIT'})
582 584 c.commit_changes.append([c_type, commit])
583 585
584 586 # current user review statuses for each version
585 587 c.review_versions = {}
586 588 if self._rhodecode_user.user_id in allowed_reviewers:
587 589 for co in general_comments:
588 590 if co.author.user_id == self._rhodecode_user.user_id:
589 591 # each comment has a status change
590 592 status = co.status_change
591 593 if status:
592 594 _ver_pr = status[0].comment.pull_request_version_id
593 595 c.review_versions[_ver_pr] = status[0]
594 596
595 597 return self._get_template_context(c)
596 598
597 599 def assure_not_empty_repo(self):
598 600 _ = self.request.translate
599 601
600 602 try:
601 603 self.db_repo.scm_instance().get_commit()
602 604 except EmptyRepositoryError:
603 605 h.flash(h.literal(_('There are no commits yet')),
604 606 category='warning')
605 607 raise HTTPFound(
606 608 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
607 609
608 610 @LoginRequired()
609 611 @NotAnonymous()
610 612 @HasRepoPermissionAnyDecorator(
611 613 'repository.read', 'repository.write', 'repository.admin')
612 614 @view_config(
613 615 route_name='pullrequest_new', request_method='GET',
614 616 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
615 617 def pull_request_new(self):
616 618 _ = self.request.translate
617 619 c = self.load_default_context()
618 620
619 621 self.assure_not_empty_repo()
620 622 source_repo = self.db_repo
621 623
622 624 commit_id = self.request.GET.get('commit')
623 625 branch_ref = self.request.GET.get('branch')
624 626 bookmark_ref = self.request.GET.get('bookmark')
625 627
626 628 try:
627 629 source_repo_data = PullRequestModel().generate_repo_data(
628 630 source_repo, commit_id=commit_id,
629 631 branch=branch_ref, bookmark=bookmark_ref)
630 632 except CommitDoesNotExistError as e:
631 633 log.exception(e)
632 634 h.flash(_('Commit does not exist'), 'error')
633 635 raise HTTPFound(
634 636 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
635 637
636 638 default_target_repo = source_repo
637 639
638 640 if source_repo.parent:
639 641 parent_vcs_obj = source_repo.parent.scm_instance()
640 642 if parent_vcs_obj and not parent_vcs_obj.is_empty():
641 643 # change default if we have a parent repo
642 644 default_target_repo = source_repo.parent
643 645
644 646 target_repo_data = PullRequestModel().generate_repo_data(
645 647 default_target_repo)
646 648
647 649 selected_source_ref = source_repo_data['refs']['selected_ref']
648 650
649 651 title_source_ref = selected_source_ref.split(':', 2)[1]
650 652 c.default_title = PullRequestModel().generate_pullrequest_title(
651 653 source=source_repo.repo_name,
652 654 source_ref=title_source_ref,
653 655 target=default_target_repo.repo_name
654 656 )
655 657
656 658 c.default_repo_data = {
657 659 'source_repo_name': source_repo.repo_name,
658 660 'source_refs_json': json.dumps(source_repo_data),
659 661 'target_repo_name': default_target_repo.repo_name,
660 662 'target_refs_json': json.dumps(target_repo_data),
661 663 }
662 664 c.default_source_ref = selected_source_ref
663 665
664 666 return self._get_template_context(c)
665 667
666 668 @LoginRequired()
667 669 @NotAnonymous()
668 670 @HasRepoPermissionAnyDecorator(
669 671 'repository.read', 'repository.write', 'repository.admin')
670 672 @view_config(
671 673 route_name='pullrequest_repo_refs', request_method='GET',
672 674 renderer='json_ext', xhr=True)
673 675 def pull_request_repo_refs(self):
674 676 target_repo_name = self.request.matchdict['target_repo_name']
675 677 repo = Repository.get_by_repo_name(target_repo_name)
676 678 if not repo:
677 679 raise HTTPNotFound()
678 680 return PullRequestModel().generate_repo_data(repo)
679 681
680 682 @LoginRequired()
681 683 @NotAnonymous()
682 684 @HasRepoPermissionAnyDecorator(
683 685 'repository.read', 'repository.write', 'repository.admin')
684 686 @view_config(
685 687 route_name='pullrequest_repo_destinations', request_method='GET',
686 688 renderer='json_ext', xhr=True)
687 689 def pull_request_repo_destinations(self):
688 690 _ = self.request.translate
689 691 filter_query = self.request.GET.get('query')
690 692
691 693 query = Repository.query() \
692 694 .order_by(func.length(Repository.repo_name)) \
693 695 .filter(
694 696 or_(Repository.repo_name == self.db_repo.repo_name,
695 697 Repository.fork_id == self.db_repo.repo_id))
696 698
697 699 if filter_query:
698 700 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
699 701 query = query.filter(
700 702 Repository.repo_name.ilike(ilike_expression))
701 703
702 704 add_parent = False
703 705 if self.db_repo.parent:
704 706 if filter_query in self.db_repo.parent.repo_name:
705 707 parent_vcs_obj = self.db_repo.parent.scm_instance()
706 708 if parent_vcs_obj and not parent_vcs_obj.is_empty():
707 709 add_parent = True
708 710
709 711 limit = 20 - 1 if add_parent else 20
710 712 all_repos = query.limit(limit).all()
711 713 if add_parent:
712 714 all_repos += [self.db_repo.parent]
713 715
714 716 repos = []
715 717 for obj in ScmModel().get_repos(all_repos):
716 718 repos.append({
717 719 'id': obj['name'],
718 720 'text': obj['name'],
719 721 'type': 'repo',
720 722 'obj': obj['dbrepo']
721 723 })
722 724
723 725 data = {
724 726 'more': False,
725 727 'results': [{
726 728 'text': _('Repositories'),
727 729 'children': repos
728 730 }] if repos else []
729 731 }
730 732 return data
731 733
732 734 @LoginRequired()
733 735 @NotAnonymous()
734 736 @HasRepoPermissionAnyDecorator(
735 737 'repository.read', 'repository.write', 'repository.admin')
736 738 @CSRFRequired()
737 739 @view_config(
738 740 route_name='pullrequest_create', request_method='POST',
739 741 renderer=None)
740 742 def pull_request_create(self):
741 743 _ = self.request.translate
742 744 self.assure_not_empty_repo()
743 745
744 746 controls = peppercorn.parse(self.request.POST.items())
745 747
746 748 try:
747 749 _form = PullRequestForm(self.db_repo.repo_id)().to_python(controls)
748 750 except formencode.Invalid as errors:
749 751 if errors.error_dict.get('revisions'):
750 752 msg = 'Revisions: %s' % errors.error_dict['revisions']
751 753 elif errors.error_dict.get('pullrequest_title'):
752 754 msg = _('Pull request requires a title with min. 3 chars')
753 755 else:
754 756 msg = _('Error creating pull request: {}').format(errors)
755 757 log.exception(msg)
756 758 h.flash(msg, 'error')
757 759
758 760 # would rather just go back to form ...
759 761 raise HTTPFound(
760 762 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
761 763
762 764 source_repo = _form['source_repo']
763 765 source_ref = _form['source_ref']
764 766 target_repo = _form['target_repo']
765 767 target_ref = _form['target_ref']
766 768 commit_ids = _form['revisions'][::-1]
767 769
768 770 # find the ancestor for this pr
769 771 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
770 772 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
771 773
772 774 source_scm = source_db_repo.scm_instance()
773 775 target_scm = target_db_repo.scm_instance()
774 776
775 777 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
776 778 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
777 779
778 780 ancestor = source_scm.get_common_ancestor(
779 781 source_commit.raw_id, target_commit.raw_id, target_scm)
780 782
781 783 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
782 784 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
783 785
784 786 pullrequest_title = _form['pullrequest_title']
785 787 title_source_ref = source_ref.split(':', 2)[1]
786 788 if not pullrequest_title:
787 789 pullrequest_title = PullRequestModel().generate_pullrequest_title(
788 790 source=source_repo,
789 791 source_ref=title_source_ref,
790 792 target=target_repo
791 793 )
792 794
793 795 description = _form['pullrequest_desc']
794 796
795 797 get_default_reviewers_data, validate_default_reviewers = \
796 798 PullRequestModel().get_reviewer_functions()
797 799
798 800 # recalculate reviewers logic, to make sure we can validate this
799 801 reviewer_rules = get_default_reviewers_data(
800 802 self._rhodecode_db_user, source_db_repo,
801 803 source_commit, target_db_repo, target_commit)
802 804
803 805 given_reviewers = _form['review_members']
804 806 reviewers = validate_default_reviewers(given_reviewers, reviewer_rules)
805 807
806 808 try:
807 809 pull_request = PullRequestModel().create(
808 810 self._rhodecode_user.user_id, source_repo, source_ref, target_repo,
809 811 target_ref, commit_ids, reviewers, pullrequest_title,
810 812 description, reviewer_rules
811 813 )
812 814 Session().commit()
813 815 h.flash(_('Successfully opened new pull request'),
814 816 category='success')
815 817 except Exception:
816 818 msg = _('Error occurred during creation of this pull request.')
817 819 log.exception(msg)
818 820 h.flash(msg, category='error')
819 821
820 822 # copy the args back to redirect
821 823 org_query = self.request.GET.mixed()
822 824 raise HTTPFound(
823 825 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
824 826 _query=org_query))
825 827
826 828 raise HTTPFound(
827 829 h.route_path('pullrequest_show', repo_name=target_repo,
828 830 pull_request_id=pull_request.pull_request_id))
829 831
830 832 @LoginRequired()
831 833 @NotAnonymous()
832 834 @HasRepoPermissionAnyDecorator(
833 835 'repository.read', 'repository.write', 'repository.admin')
834 836 @CSRFRequired()
835 837 @view_config(
836 838 route_name='pullrequest_update', request_method='POST',
837 839 renderer='json_ext')
838 840 def pull_request_update(self):
839 841 pull_request = PullRequest.get_or_404(
840 842 self.request.matchdict['pull_request_id'])
841 843
842 844 # only owner or admin can update it
843 845 allowed_to_update = PullRequestModel().check_user_update(
844 846 pull_request, self._rhodecode_user)
845 847 if allowed_to_update:
846 848 controls = peppercorn.parse(self.request.POST.items())
847 849
848 850 if 'review_members' in controls:
849 851 self._update_reviewers(
850 852 pull_request, controls['review_members'],
851 853 pull_request.reviewer_data)
852 854 elif str2bool(self.request.POST.get('update_commits', 'false')):
853 855 self._update_commits(pull_request)
854 856 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
855 857 self._edit_pull_request(pull_request)
856 858 else:
857 859 raise HTTPBadRequest()
858 860 return True
859 861 raise HTTPForbidden()
860 862
861 863 def _edit_pull_request(self, pull_request):
862 864 _ = self.request.translate
863 865 try:
864 866 PullRequestModel().edit(
865 867 pull_request, self.request.POST.get('title'),
866 868 self.request.POST.get('description'), self._rhodecode_user)
867 869 except ValueError:
868 870 msg = _(u'Cannot update closed pull requests.')
869 871 h.flash(msg, category='error')
870 872 return
871 873 else:
872 874 Session().commit()
873 875
874 876 msg = _(u'Pull request title & description updated.')
875 877 h.flash(msg, category='success')
876 878 return
877 879
878 880 def _update_commits(self, pull_request):
879 881 _ = self.request.translate
880 882 resp = PullRequestModel().update_commits(pull_request)
881 883
882 884 if resp.executed:
883 885
884 886 if resp.target_changed and resp.source_changed:
885 887 changed = 'target and source repositories'
886 888 elif resp.target_changed and not resp.source_changed:
887 889 changed = 'target repository'
888 890 elif not resp.target_changed and resp.source_changed:
889 891 changed = 'source repository'
890 892 else:
891 893 changed = 'nothing'
892 894
893 895 msg = _(
894 896 u'Pull request updated to "{source_commit_id}" with '
895 897 u'{count_added} added, {count_removed} removed commits. '
896 898 u'Source of changes: {change_source}')
897 899 msg = msg.format(
898 900 source_commit_id=pull_request.source_ref_parts.commit_id,
899 901 count_added=len(resp.changes.added),
900 902 count_removed=len(resp.changes.removed),
901 903 change_source=changed)
902 904 h.flash(msg, category='success')
903 905
904 906 channel = '/repo${}$/pr/{}'.format(
905 907 pull_request.target_repo.repo_name,
906 908 pull_request.pull_request_id)
907 909 message = msg + (
908 910 ' - <a onclick="window.location.reload()">'
909 911 '<strong>{}</strong></a>'.format(_('Reload page')))
910 912 channelstream.post_message(
911 913 channel, message, self._rhodecode_user.username,
912 914 registry=self.request.registry)
913 915 else:
914 916 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
915 917 warning_reasons = [
916 918 UpdateFailureReason.NO_CHANGE,
917 919 UpdateFailureReason.WRONG_REF_TYPE,
918 920 ]
919 921 category = 'warning' if resp.reason in warning_reasons else 'error'
920 922 h.flash(msg, category=category)
921 923
922 924 @LoginRequired()
923 925 @NotAnonymous()
924 926 @HasRepoPermissionAnyDecorator(
925 927 'repository.read', 'repository.write', 'repository.admin')
926 928 @CSRFRequired()
927 929 @view_config(
928 930 route_name='pullrequest_merge', request_method='POST',
929 931 renderer='json_ext')
930 932 def pull_request_merge(self):
931 933 """
932 934 Merge will perform a server-side merge of the specified
933 935 pull request, if the pull request is approved and mergeable.
934 936 After successful merging, the pull request is automatically
935 937 closed, with a relevant comment.
936 938 """
937 939 pull_request = PullRequest.get_or_404(
938 940 self.request.matchdict['pull_request_id'])
939 941
940 942 check = MergeCheck.validate(pull_request, self._rhodecode_db_user)
941 943 merge_possible = not check.failed
942 944
943 945 for err_type, error_msg in check.errors:
944 946 h.flash(error_msg, category=err_type)
945 947
946 948 if merge_possible:
947 949 log.debug("Pre-conditions checked, trying to merge.")
948 950 extras = vcs_operation_context(
949 951 self.request.environ, repo_name=pull_request.target_repo.repo_name,
950 952 username=self._rhodecode_db_user.username, action='push',
951 953 scm=pull_request.target_repo.repo_type)
952 954 self._merge_pull_request(
953 955 pull_request, self._rhodecode_db_user, extras)
954 956 else:
955 957 log.debug("Pre-conditions failed, NOT merging.")
956 958
957 959 raise HTTPFound(
958 960 h.route_path('pullrequest_show',
959 961 repo_name=pull_request.target_repo.repo_name,
960 962 pull_request_id=pull_request.pull_request_id))
961 963
962 964 def _merge_pull_request(self, pull_request, user, extras):
963 965 _ = self.request.translate
964 966 merge_resp = PullRequestModel().merge(pull_request, user, extras=extras)
965 967
966 968 if merge_resp.executed:
967 969 log.debug("The merge was successful, closing the pull request.")
968 970 PullRequestModel().close_pull_request(
969 971 pull_request.pull_request_id, user)
970 972 Session().commit()
971 973 msg = _('Pull request was successfully merged and closed.')
972 974 h.flash(msg, category='success')
973 975 else:
974 976 log.debug(
975 977 "The merge was not successful. Merge response: %s",
976 978 merge_resp)
977 979 msg = PullRequestModel().merge_status_message(
978 980 merge_resp.failure_reason)
979 981 h.flash(msg, category='error')
980 982
981 983 def _update_reviewers(self, pull_request, review_members, reviewer_rules):
982 984 _ = self.request.translate
983 985 get_default_reviewers_data, validate_default_reviewers = \
984 986 PullRequestModel().get_reviewer_functions()
985 987
986 988 try:
987 989 reviewers = validate_default_reviewers(review_members, reviewer_rules)
988 990 except ValueError as e:
989 991 log.error('Reviewers Validation: {}'.format(e))
990 992 h.flash(e, category='error')
991 993 return
992 994
993 995 PullRequestModel().update_reviewers(
994 996 pull_request, reviewers, self._rhodecode_user)
995 997 h.flash(_('Pull request reviewers updated.'), category='success')
996 998 Session().commit()
997 999
998 1000 @LoginRequired()
999 1001 @NotAnonymous()
1000 1002 @HasRepoPermissionAnyDecorator(
1001 1003 'repository.read', 'repository.write', 'repository.admin')
1002 1004 @CSRFRequired()
1003 1005 @view_config(
1004 1006 route_name='pullrequest_delete', request_method='POST',
1005 1007 renderer='json_ext')
1006 1008 def pull_request_delete(self):
1007 1009 _ = self.request.translate
1008 1010
1009 1011 pull_request = PullRequest.get_or_404(
1010 1012 self.request.matchdict['pull_request_id'])
1011 1013
1012 1014 pr_closed = pull_request.is_closed()
1013 1015 allowed_to_delete = PullRequestModel().check_user_delete(
1014 1016 pull_request, self._rhodecode_user) and not pr_closed
1015 1017
1016 1018 # only owner can delete it !
1017 1019 if allowed_to_delete:
1018 1020 PullRequestModel().delete(pull_request, self._rhodecode_user)
1019 1021 Session().commit()
1020 1022 h.flash(_('Successfully deleted pull request'),
1021 1023 category='success')
1022 1024 raise HTTPFound(h.route_path('pullrequest_show_all',
1023 1025 repo_name=self.db_repo_name))
1024 1026
1025 1027 log.warning('user %s tried to delete pull request without access',
1026 1028 self._rhodecode_user)
1027 1029 raise HTTPNotFound()
1028 1030
1029 1031 @LoginRequired()
1030 1032 @NotAnonymous()
1031 1033 @HasRepoPermissionAnyDecorator(
1032 1034 'repository.read', 'repository.write', 'repository.admin')
1033 1035 @CSRFRequired()
1034 1036 @view_config(
1035 1037 route_name='pullrequest_comment_create', request_method='POST',
1036 1038 renderer='json_ext')
1037 1039 def pull_request_comment_create(self):
1038 1040 _ = self.request.translate
1039 1041
1040 1042 pull_request = PullRequest.get_or_404(
1041 1043 self.request.matchdict['pull_request_id'])
1042 1044 pull_request_id = pull_request.pull_request_id
1043 1045
1044 1046 if pull_request.is_closed():
1045 1047 log.debug('comment: forbidden because pull request is closed')
1046 1048 raise HTTPForbidden()
1047 1049
1048 1050 c = self.load_default_context()
1049 1051
1050 1052 status = self.request.POST.get('changeset_status', None)
1051 1053 text = self.request.POST.get('text')
1052 1054 comment_type = self.request.POST.get('comment_type')
1053 1055 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
1054 1056 close_pull_request = self.request.POST.get('close_pull_request')
1055 1057
1056 1058 # the logic here should work like following, if we submit close
1057 1059 # pr comment, use `close_pull_request_with_comment` function
1058 1060 # else handle regular comment logic
1059 1061
1060 1062 if close_pull_request:
1061 1063 # only owner or admin or person with write permissions
1062 1064 allowed_to_close = PullRequestModel().check_user_update(
1063 1065 pull_request, self._rhodecode_user)
1064 1066 if not allowed_to_close:
1065 1067 log.debug('comment: forbidden because not allowed to close '
1066 1068 'pull request %s', pull_request_id)
1067 1069 raise HTTPForbidden()
1068 1070 comment, status = PullRequestModel().close_pull_request_with_comment(
1069 1071 pull_request, self._rhodecode_user, self.db_repo, message=text)
1070 1072 Session().flush()
1071 1073 events.trigger(
1072 1074 events.PullRequestCommentEvent(pull_request, comment))
1073 1075
1074 1076 else:
1075 1077 # regular comment case, could be inline, or one with status.
1076 1078 # for that one we check also permissions
1077 1079
1078 1080 allowed_to_change_status = PullRequestModel().check_user_change_status(
1079 1081 pull_request, self._rhodecode_user)
1080 1082
1081 1083 if status and allowed_to_change_status:
1082 1084 message = (_('Status change %(transition_icon)s %(status)s')
1083 1085 % {'transition_icon': '>',
1084 1086 'status': ChangesetStatus.get_status_lbl(status)})
1085 1087 text = text or message
1086 1088
1087 1089 comment = CommentsModel().create(
1088 1090 text=text,
1089 1091 repo=self.db_repo.repo_id,
1090 1092 user=self._rhodecode_user.user_id,
1091 1093 pull_request=pull_request,
1092 1094 f_path=self.request.POST.get('f_path'),
1093 1095 line_no=self.request.POST.get('line'),
1094 1096 status_change=(ChangesetStatus.get_status_lbl(status)
1095 1097 if status and allowed_to_change_status else None),
1096 1098 status_change_type=(status
1097 1099 if status and allowed_to_change_status else None),
1098 1100 comment_type=comment_type,
1099 1101 resolves_comment_id=resolves_comment_id
1100 1102 )
1101 1103
1102 1104 if allowed_to_change_status:
1103 1105 # calculate old status before we change it
1104 1106 old_calculated_status = pull_request.calculated_review_status()
1105 1107
1106 1108 # get status if set !
1107 1109 if status:
1108 1110 ChangesetStatusModel().set_status(
1109 1111 self.db_repo.repo_id,
1110 1112 status,
1111 1113 self._rhodecode_user.user_id,
1112 1114 comment,
1113 1115 pull_request=pull_request
1114 1116 )
1115 1117
1116 1118 Session().flush()
1117 1119 events.trigger(
1118 1120 events.PullRequestCommentEvent(pull_request, comment))
1119 1121
1120 1122 # we now calculate the status of pull request, and based on that
1121 1123 # calculation we set the commits status
1122 1124 calculated_status = pull_request.calculated_review_status()
1123 1125 if old_calculated_status != calculated_status:
1124 1126 PullRequestModel()._trigger_pull_request_hook(
1125 1127 pull_request, self._rhodecode_user, 'review_status_change')
1126 1128
1127 1129 Session().commit()
1128 1130
1129 1131 data = {
1130 1132 'target_id': h.safeid(h.safe_unicode(
1131 1133 self.request.POST.get('f_path'))),
1132 1134 }
1133 1135 if comment:
1134 1136 c.co = comment
1135 1137 rendered_comment = render(
1136 1138 'rhodecode:templates/changeset/changeset_comment_block.mako',
1137 1139 self._get_template_context(c), self.request)
1138 1140
1139 1141 data.update(comment.get_dict())
1140 1142 data.update({'rendered_text': rendered_comment})
1141 1143
1142 1144 return data
1143 1145
1144 1146 @LoginRequired()
1145 1147 @NotAnonymous()
1146 1148 @HasRepoPermissionAnyDecorator(
1147 1149 'repository.read', 'repository.write', 'repository.admin')
1148 1150 @CSRFRequired()
1149 1151 @view_config(
1150 1152 route_name='pullrequest_comment_delete', request_method='POST',
1151 1153 renderer='json_ext')
1152 1154 def pull_request_comment_delete(self):
1153 1155 pull_request = PullRequest.get_or_404(
1154 1156 self.request.matchdict['pull_request_id'])
1155 1157
1156 1158 comment = ChangesetComment.get_or_404(
1157 1159 self.request.matchdict['comment_id'])
1158 1160 comment_id = comment.comment_id
1159 1161
1160 1162 if pull_request.is_closed():
1161 1163 log.debug('comment: forbidden because pull request is closed')
1162 1164 raise HTTPForbidden()
1163 1165
1164 1166 if not comment:
1165 1167 log.debug('Comment with id:%s not found, skipping', comment_id)
1166 1168 # comment already deleted in another call probably
1167 1169 return True
1168 1170
1169 1171 if comment.pull_request.is_closed():
1170 1172 # don't allow deleting comments on closed pull request
1171 1173 raise HTTPForbidden()
1172 1174
1173 1175 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1174 1176 super_admin = h.HasPermissionAny('hg.admin')()
1175 1177 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1176 1178 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1177 1179 comment_repo_admin = is_repo_admin and is_repo_comment
1178 1180
1179 1181 if super_admin or comment_owner or comment_repo_admin:
1180 1182 old_calculated_status = comment.pull_request.calculated_review_status()
1181 1183 CommentsModel().delete(comment=comment, user=self._rhodecode_user)
1182 1184 Session().commit()
1183 1185 calculated_status = comment.pull_request.calculated_review_status()
1184 1186 if old_calculated_status != calculated_status:
1185 1187 PullRequestModel()._trigger_pull_request_hook(
1186 1188 comment.pull_request, self._rhodecode_user, 'review_status_change')
1187 1189 return True
1188 1190 else:
1189 1191 log.warning('No permissions for user %s to delete comment_id: %s',
1190 1192 self._rhodecode_db_user, comment_id)
1191 1193 raise HTTPNotFound()
@@ -1,1552 +1,1584 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 """
23 23 pull request model for RhodeCode
24 24 """
25 25
26 26 from collections import namedtuple
27 27 import json
28 28 import logging
29 29 import datetime
30 30 import urllib
31 31
32 32 from pylons.i18n.translation import _
33 33 from pylons.i18n.translation import lazy_ugettext
34 34 from pyramid.threadlocal import get_current_request
35 35 from sqlalchemy import or_
36 36
37 37 from rhodecode import events
38 38 from rhodecode.lib import helpers as h, hooks_utils, diffs
39 39 from rhodecode.lib import audit_logger
40 40 from rhodecode.lib.compat import OrderedDict
41 41 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
42 42 from rhodecode.lib.markup_renderer import (
43 43 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
44 44 from rhodecode.lib.utils2 import safe_unicode, safe_str, md5_safe
45 45 from rhodecode.lib.vcs.backends.base import (
46 46 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason)
47 47 from rhodecode.lib.vcs.conf import settings as vcs_settings
48 48 from rhodecode.lib.vcs.exceptions import (
49 49 CommitDoesNotExistError, EmptyRepositoryError)
50 50 from rhodecode.model import BaseModel
51 51 from rhodecode.model.changeset_status import ChangesetStatusModel
52 52 from rhodecode.model.comment import CommentsModel
53 53 from rhodecode.model.db import (
54 54 PullRequest, PullRequestReviewers, ChangesetStatus,
55 55 PullRequestVersion, ChangesetComment, Repository)
56 56 from rhodecode.model.meta import Session
57 57 from rhodecode.model.notification import NotificationModel, \
58 58 EmailNotificationModel
59 59 from rhodecode.model.scm import ScmModel
60 60 from rhodecode.model.settings import VcsSettingsModel
61 61
62 62
63 63 log = logging.getLogger(__name__)
64 64
65 65
66 66 # Data structure to hold the response data when updating commits during a pull
67 67 # request update.
68 68 UpdateResponse = namedtuple('UpdateResponse', [
69 69 'executed', 'reason', 'new', 'old', 'changes',
70 70 'source_changed', 'target_changed'])
71 71
72 72
73 73 class PullRequestModel(BaseModel):
74 74
75 75 cls = PullRequest
76 76
77 77 DIFF_CONTEXT = 3
78 78
79 79 MERGE_STATUS_MESSAGES = {
80 80 MergeFailureReason.NONE: lazy_ugettext(
81 81 'This pull request can be automatically merged.'),
82 82 MergeFailureReason.UNKNOWN: lazy_ugettext(
83 83 'This pull request cannot be merged because of an unhandled'
84 84 ' exception.'),
85 85 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
86 86 'This pull request cannot be merged because of merge conflicts.'),
87 87 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
88 88 'This pull request could not be merged because push to target'
89 89 ' failed.'),
90 90 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
91 91 'This pull request cannot be merged because the target is not a'
92 92 ' head.'),
93 93 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
94 94 'This pull request cannot be merged because the source contains'
95 95 ' more branches than the target.'),
96 96 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
97 97 'This pull request cannot be merged because the target has'
98 98 ' multiple heads.'),
99 99 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
100 100 'This pull request cannot be merged because the target repository'
101 101 ' is locked.'),
102 102 MergeFailureReason._DEPRECATED_MISSING_COMMIT: lazy_ugettext(
103 103 'This pull request cannot be merged because the target or the '
104 104 'source reference is missing.'),
105 105 MergeFailureReason.MISSING_TARGET_REF: lazy_ugettext(
106 106 'This pull request cannot be merged because the target '
107 107 'reference is missing.'),
108 108 MergeFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
109 109 'This pull request cannot be merged because the source '
110 110 'reference is missing.'),
111 111 MergeFailureReason.SUBREPO_MERGE_FAILED: lazy_ugettext(
112 112 'This pull request cannot be merged because of conflicts related '
113 113 'to sub repositories.'),
114 114 }
115 115
116 116 UPDATE_STATUS_MESSAGES = {
117 117 UpdateFailureReason.NONE: lazy_ugettext(
118 118 'Pull request update successful.'),
119 119 UpdateFailureReason.UNKNOWN: lazy_ugettext(
120 120 'Pull request update failed because of an unknown error.'),
121 121 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
122 122 'No update needed because the source and target have not changed.'),
123 123 UpdateFailureReason.WRONG_REF_TYPE: lazy_ugettext(
124 124 'Pull request cannot be updated because the reference type is '
125 125 'not supported for an update. Only Branch, Tag or Bookmark is allowed.'),
126 126 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
127 127 'This pull request cannot be updated because the target '
128 128 'reference is missing.'),
129 129 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
130 130 'This pull request cannot be updated because the source '
131 131 'reference is missing.'),
132 132 }
133 133
134 134 def __get_pull_request(self, pull_request):
135 135 return self._get_instance((
136 136 PullRequest, PullRequestVersion), pull_request)
137 137
138 138 def _check_perms(self, perms, pull_request, user, api=False):
139 139 if not api:
140 140 return h.HasRepoPermissionAny(*perms)(
141 141 user=user, repo_name=pull_request.target_repo.repo_name)
142 142 else:
143 143 return h.HasRepoPermissionAnyApi(*perms)(
144 144 user=user, repo_name=pull_request.target_repo.repo_name)
145 145
146 146 def check_user_read(self, pull_request, user, api=False):
147 147 _perms = ('repository.admin', 'repository.write', 'repository.read',)
148 148 return self._check_perms(_perms, pull_request, user, api)
149 149
150 150 def check_user_merge(self, pull_request, user, api=False):
151 151 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
152 152 return self._check_perms(_perms, pull_request, user, api)
153 153
154 154 def check_user_update(self, pull_request, user, api=False):
155 155 owner = user.user_id == pull_request.user_id
156 156 return self.check_user_merge(pull_request, user, api) or owner
157 157
158 158 def check_user_delete(self, pull_request, user):
159 159 owner = user.user_id == pull_request.user_id
160 160 _perms = ('repository.admin',)
161 161 return self._check_perms(_perms, pull_request, user) or owner
162 162
163 163 def check_user_change_status(self, pull_request, user, api=False):
164 164 reviewer = user.user_id in [x.user_id for x in
165 165 pull_request.reviewers]
166 166 return self.check_user_update(pull_request, user, api) or reviewer
167 167
168 168 def get(self, pull_request):
169 169 return self.__get_pull_request(pull_request)
170 170
171 171 def _prepare_get_all_query(self, repo_name, source=False, statuses=None,
172 172 opened_by=None, order_by=None,
173 173 order_dir='desc'):
174 174 repo = None
175 175 if repo_name:
176 176 repo = self._get_repo(repo_name)
177 177
178 178 q = PullRequest.query()
179 179
180 180 # source or target
181 181 if repo and source:
182 182 q = q.filter(PullRequest.source_repo == repo)
183 183 elif repo:
184 184 q = q.filter(PullRequest.target_repo == repo)
185 185
186 186 # closed,opened
187 187 if statuses:
188 188 q = q.filter(PullRequest.status.in_(statuses))
189 189
190 190 # opened by filter
191 191 if opened_by:
192 192 q = q.filter(PullRequest.user_id.in_(opened_by))
193 193
194 194 if order_by:
195 195 order_map = {
196 196 'name_raw': PullRequest.pull_request_id,
197 197 'title': PullRequest.title,
198 198 'updated_on_raw': PullRequest.updated_on,
199 199 'target_repo': PullRequest.target_repo_id
200 200 }
201 201 if order_dir == 'asc':
202 202 q = q.order_by(order_map[order_by].asc())
203 203 else:
204 204 q = q.order_by(order_map[order_by].desc())
205 205
206 206 return q
207 207
208 208 def count_all(self, repo_name, source=False, statuses=None,
209 209 opened_by=None):
210 210 """
211 211 Count the number of pull requests for a specific repository.
212 212
213 213 :param repo_name: target or source repo
214 214 :param source: boolean flag to specify if repo_name refers to source
215 215 :param statuses: list of pull request statuses
216 216 :param opened_by: author user of the pull request
217 217 :returns: int number of pull requests
218 218 """
219 219 q = self._prepare_get_all_query(
220 220 repo_name, source=source, statuses=statuses, opened_by=opened_by)
221 221
222 222 return q.count()
223 223
224 224 def get_all(self, repo_name, source=False, statuses=None, opened_by=None,
225 225 offset=0, length=None, order_by=None, order_dir='desc'):
226 226 """
227 227 Get all pull requests for a specific repository.
228 228
229 229 :param repo_name: target or source repo
230 230 :param source: boolean flag to specify if repo_name refers to source
231 231 :param statuses: list of pull request statuses
232 232 :param opened_by: author user of the pull request
233 233 :param offset: pagination offset
234 234 :param length: length of returned list
235 235 :param order_by: order of the returned list
236 236 :param order_dir: 'asc' or 'desc' ordering direction
237 237 :returns: list of pull requests
238 238 """
239 239 q = self._prepare_get_all_query(
240 240 repo_name, source=source, statuses=statuses, opened_by=opened_by,
241 241 order_by=order_by, order_dir=order_dir)
242 242
243 243 if length:
244 244 pull_requests = q.limit(length).offset(offset).all()
245 245 else:
246 246 pull_requests = q.all()
247 247
248 248 return pull_requests
249 249
250 250 def count_awaiting_review(self, repo_name, source=False, statuses=None,
251 251 opened_by=None):
252 252 """
253 253 Count the number of pull requests for a specific repository that are
254 254 awaiting review.
255 255
256 256 :param repo_name: target or source repo
257 257 :param source: boolean flag to specify if repo_name refers to source
258 258 :param statuses: list of pull request statuses
259 259 :param opened_by: author user of the pull request
260 260 :returns: int number of pull requests
261 261 """
262 262 pull_requests = self.get_awaiting_review(
263 263 repo_name, source=source, statuses=statuses, opened_by=opened_by)
264 264
265 265 return len(pull_requests)
266 266
267 267 def get_awaiting_review(self, repo_name, source=False, statuses=None,
268 268 opened_by=None, offset=0, length=None,
269 269 order_by=None, order_dir='desc'):
270 270 """
271 271 Get all pull requests for a specific repository that are awaiting
272 272 review.
273 273
274 274 :param repo_name: target or source repo
275 275 :param source: boolean flag to specify if repo_name refers to source
276 276 :param statuses: list of pull request statuses
277 277 :param opened_by: author user of the pull request
278 278 :param offset: pagination offset
279 279 :param length: length of returned list
280 280 :param order_by: order of the returned list
281 281 :param order_dir: 'asc' or 'desc' ordering direction
282 282 :returns: list of pull requests
283 283 """
284 284 pull_requests = self.get_all(
285 285 repo_name, source=source, statuses=statuses, opened_by=opened_by,
286 286 order_by=order_by, order_dir=order_dir)
287 287
288 288 _filtered_pull_requests = []
289 289 for pr in pull_requests:
290 290 status = pr.calculated_review_status()
291 291 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
292 292 ChangesetStatus.STATUS_UNDER_REVIEW]:
293 293 _filtered_pull_requests.append(pr)
294 294 if length:
295 295 return _filtered_pull_requests[offset:offset+length]
296 296 else:
297 297 return _filtered_pull_requests
298 298
299 299 def count_awaiting_my_review(self, repo_name, source=False, statuses=None,
300 300 opened_by=None, user_id=None):
301 301 """
302 302 Count the number of pull requests for a specific repository that are
303 303 awaiting review from a specific user.
304 304
305 305 :param repo_name: target or source repo
306 306 :param source: boolean flag to specify if repo_name refers to source
307 307 :param statuses: list of pull request statuses
308 308 :param opened_by: author user of the pull request
309 309 :param user_id: reviewer user of the pull request
310 310 :returns: int number of pull requests
311 311 """
312 312 pull_requests = self.get_awaiting_my_review(
313 313 repo_name, source=source, statuses=statuses, opened_by=opened_by,
314 314 user_id=user_id)
315 315
316 316 return len(pull_requests)
317 317
318 318 def get_awaiting_my_review(self, repo_name, source=False, statuses=None,
319 319 opened_by=None, user_id=None, offset=0,
320 320 length=None, order_by=None, order_dir='desc'):
321 321 """
322 322 Get all pull requests for a specific repository that are awaiting
323 323 review from a specific user.
324 324
325 325 :param repo_name: target or source repo
326 326 :param source: boolean flag to specify if repo_name refers to source
327 327 :param statuses: list of pull request statuses
328 328 :param opened_by: author user of the pull request
329 329 :param user_id: reviewer user of the pull request
330 330 :param offset: pagination offset
331 331 :param length: length of returned list
332 332 :param order_by: order of the returned list
333 333 :param order_dir: 'asc' or 'desc' ordering direction
334 334 :returns: list of pull requests
335 335 """
336 336 pull_requests = self.get_all(
337 337 repo_name, source=source, statuses=statuses, opened_by=opened_by,
338 338 order_by=order_by, order_dir=order_dir)
339 339
340 340 _my = PullRequestModel().get_not_reviewed(user_id)
341 341 my_participation = []
342 342 for pr in pull_requests:
343 343 if pr in _my:
344 344 my_participation.append(pr)
345 345 _filtered_pull_requests = my_participation
346 346 if length:
347 347 return _filtered_pull_requests[offset:offset+length]
348 348 else:
349 349 return _filtered_pull_requests
350 350
351 351 def get_not_reviewed(self, user_id):
352 352 return [
353 353 x.pull_request for x in PullRequestReviewers.query().filter(
354 354 PullRequestReviewers.user_id == user_id).all()
355 355 ]
356 356
357 357 def _prepare_participating_query(self, user_id=None, statuses=None,
358 358 order_by=None, order_dir='desc'):
359 359 q = PullRequest.query()
360 360 if user_id:
361 361 reviewers_subquery = Session().query(
362 362 PullRequestReviewers.pull_request_id).filter(
363 363 PullRequestReviewers.user_id == user_id).subquery()
364 364 user_filter= or_(
365 365 PullRequest.user_id == user_id,
366 366 PullRequest.pull_request_id.in_(reviewers_subquery)
367 367 )
368 368 q = PullRequest.query().filter(user_filter)
369 369
370 370 # closed,opened
371 371 if statuses:
372 372 q = q.filter(PullRequest.status.in_(statuses))
373 373
374 374 if order_by:
375 375 order_map = {
376 376 'name_raw': PullRequest.pull_request_id,
377 377 'title': PullRequest.title,
378 378 'updated_on_raw': PullRequest.updated_on,
379 379 'target_repo': PullRequest.target_repo_id
380 380 }
381 381 if order_dir == 'asc':
382 382 q = q.order_by(order_map[order_by].asc())
383 383 else:
384 384 q = q.order_by(order_map[order_by].desc())
385 385
386 386 return q
387 387
388 388 def count_im_participating_in(self, user_id=None, statuses=None):
389 389 q = self._prepare_participating_query(user_id, statuses=statuses)
390 390 return q.count()
391 391
392 392 def get_im_participating_in(
393 393 self, user_id=None, statuses=None, offset=0,
394 394 length=None, order_by=None, order_dir='desc'):
395 395 """
396 396 Get all Pull requests that i'm participating in, or i have opened
397 397 """
398 398
399 399 q = self._prepare_participating_query(
400 400 user_id, statuses=statuses, order_by=order_by,
401 401 order_dir=order_dir)
402 402
403 403 if length:
404 404 pull_requests = q.limit(length).offset(offset).all()
405 405 else:
406 406 pull_requests = q.all()
407 407
408 408 return pull_requests
409 409
410 410 def get_versions(self, pull_request):
411 411 """
412 412 returns version of pull request sorted by ID descending
413 413 """
414 414 return PullRequestVersion.query()\
415 415 .filter(PullRequestVersion.pull_request == pull_request)\
416 416 .order_by(PullRequestVersion.pull_request_version_id.asc())\
417 417 .all()
418 418
419 419 def create(self, created_by, source_repo, source_ref, target_repo,
420 420 target_ref, revisions, reviewers, title, description=None,
421 421 reviewer_data=None):
422 422
423 423 created_by_user = self._get_user(created_by)
424 424 source_repo = self._get_repo(source_repo)
425 425 target_repo = self._get_repo(target_repo)
426 426
427 427 pull_request = PullRequest()
428 428 pull_request.source_repo = source_repo
429 429 pull_request.source_ref = source_ref
430 430 pull_request.target_repo = target_repo
431 431 pull_request.target_ref = target_ref
432 432 pull_request.revisions = revisions
433 433 pull_request.title = title
434 434 pull_request.description = description
435 435 pull_request.author = created_by_user
436 436 pull_request.reviewer_data = reviewer_data
437 437
438 438 Session().add(pull_request)
439 439 Session().flush()
440 440
441 441 reviewer_ids = set()
442 442 # members / reviewers
443 443 for reviewer_object in reviewers:
444 444 user_id, reasons, mandatory = reviewer_object
445 445 user = self._get_user(user_id)
446 446
447 447 # skip duplicates
448 448 if user.user_id in reviewer_ids:
449 449 continue
450 450
451 451 reviewer_ids.add(user.user_id)
452 452
453 453 reviewer = PullRequestReviewers()
454 454 reviewer.user = user
455 455 reviewer.pull_request = pull_request
456 456 reviewer.reasons = reasons
457 457 reviewer.mandatory = mandatory
458 458 Session().add(reviewer)
459 459
460 460 # Set approval status to "Under Review" for all commits which are
461 461 # part of this pull request.
462 462 ChangesetStatusModel().set_status(
463 463 repo=target_repo,
464 464 status=ChangesetStatus.STATUS_UNDER_REVIEW,
465 465 user=created_by_user,
466 466 pull_request=pull_request
467 467 )
468 468
469 469 self.notify_reviewers(pull_request, reviewer_ids)
470 470 self._trigger_pull_request_hook(
471 471 pull_request, created_by_user, 'create')
472 472
473 473 creation_data = pull_request.get_api_data(with_merge_state=False)
474 474 self._log_audit_action(
475 475 'repo.pull_request.create', {'data': creation_data},
476 476 created_by_user, pull_request)
477 477
478 478 return pull_request
479 479
480 480 def _trigger_pull_request_hook(self, pull_request, user, action):
481 481 pull_request = self.__get_pull_request(pull_request)
482 482 target_scm = pull_request.target_repo.scm_instance()
483 483 if action == 'create':
484 484 trigger_hook = hooks_utils.trigger_log_create_pull_request_hook
485 485 elif action == 'merge':
486 486 trigger_hook = hooks_utils.trigger_log_merge_pull_request_hook
487 487 elif action == 'close':
488 488 trigger_hook = hooks_utils.trigger_log_close_pull_request_hook
489 489 elif action == 'review_status_change':
490 490 trigger_hook = hooks_utils.trigger_log_review_pull_request_hook
491 491 elif action == 'update':
492 492 trigger_hook = hooks_utils.trigger_log_update_pull_request_hook
493 493 else:
494 494 return
495 495
496 496 trigger_hook(
497 497 username=user.username,
498 498 repo_name=pull_request.target_repo.repo_name,
499 499 repo_alias=target_scm.alias,
500 500 pull_request=pull_request)
501 501
502 502 def _get_commit_ids(self, pull_request):
503 503 """
504 504 Return the commit ids of the merged pull request.
505 505
506 506 This method is not dealing correctly yet with the lack of autoupdates
507 507 nor with the implicit target updates.
508 508 For example: if a commit in the source repo is already in the target it
509 509 will be reported anyways.
510 510 """
511 511 merge_rev = pull_request.merge_rev
512 512 if merge_rev is None:
513 513 raise ValueError('This pull request was not merged yet')
514 514
515 515 commit_ids = list(pull_request.revisions)
516 516 if merge_rev not in commit_ids:
517 517 commit_ids.append(merge_rev)
518 518
519 519 return commit_ids
520 520
521 521 def merge(self, pull_request, user, extras):
522 522 log.debug("Merging pull request %s", pull_request.pull_request_id)
523 523 merge_state = self._merge_pull_request(pull_request, user, extras)
524 524 if merge_state.executed:
525 525 log.debug(
526 526 "Merge was successful, updating the pull request comments.")
527 527 self._comment_and_close_pr(pull_request, user, merge_state)
528 528
529 529 self._log_audit_action(
530 530 'repo.pull_request.merge',
531 531 {'merge_state': merge_state.__dict__},
532 532 user, pull_request)
533 533
534 534 else:
535 535 log.warn("Merge failed, not updating the pull request.")
536 536 return merge_state
537 537
538 538 def _merge_pull_request(self, pull_request, user, extras):
539 539 target_vcs = pull_request.target_repo.scm_instance()
540 540 source_vcs = pull_request.source_repo.scm_instance()
541 541 target_ref = self._refresh_reference(
542 542 pull_request.target_ref_parts, target_vcs)
543 543
544 544 message = _(
545 545 'Merge pull request #%(pr_id)s from '
546 546 '%(source_repo)s %(source_ref_name)s\n\n %(pr_title)s') % {
547 547 'pr_id': pull_request.pull_request_id,
548 548 'source_repo': source_vcs.name,
549 549 'source_ref_name': pull_request.source_ref_parts.name,
550 550 'pr_title': pull_request.title
551 551 }
552 552
553 553 workspace_id = self._workspace_id(pull_request)
554 554 use_rebase = self._use_rebase_for_merging(pull_request)
555 555
556 556 callback_daemon, extras = prepare_callback_daemon(
557 557 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
558 558 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
559 559
560 560 with callback_daemon:
561 561 # TODO: johbo: Implement a clean way to run a config_override
562 562 # for a single call.
563 563 target_vcs.config.set(
564 564 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
565 565 merge_state = target_vcs.merge(
566 566 target_ref, source_vcs, pull_request.source_ref_parts,
567 567 workspace_id, user_name=user.username,
568 568 user_email=user.email, message=message, use_rebase=use_rebase)
569 569 return merge_state
570 570
571 571 def _comment_and_close_pr(self, pull_request, user, merge_state):
572 572 pull_request.merge_rev = merge_state.merge_ref.commit_id
573 573 pull_request.updated_on = datetime.datetime.now()
574 574
575 575 CommentsModel().create(
576 576 text=unicode(_('Pull request merged and closed')),
577 577 repo=pull_request.target_repo.repo_id,
578 578 user=user.user_id,
579 579 pull_request=pull_request.pull_request_id,
580 580 f_path=None,
581 581 line_no=None,
582 582 closing_pr=True
583 583 )
584 584
585 585 Session().add(pull_request)
586 586 Session().flush()
587 587 # TODO: paris: replace invalidation with less radical solution
588 588 ScmModel().mark_for_invalidation(
589 589 pull_request.target_repo.repo_name)
590 590 self._trigger_pull_request_hook(pull_request, user, 'merge')
591 591
592 592 def has_valid_update_type(self, pull_request):
593 593 source_ref_type = pull_request.source_ref_parts.type
594 594 return source_ref_type in ['book', 'branch', 'tag']
595 595
596 596 def update_commits(self, pull_request):
597 597 """
598 598 Get the updated list of commits for the pull request
599 599 and return the new pull request version and the list
600 600 of commits processed by this update action
601 601 """
602 602 pull_request = self.__get_pull_request(pull_request)
603 603 source_ref_type = pull_request.source_ref_parts.type
604 604 source_ref_name = pull_request.source_ref_parts.name
605 605 source_ref_id = pull_request.source_ref_parts.commit_id
606 606
607 607 target_ref_type = pull_request.target_ref_parts.type
608 608 target_ref_name = pull_request.target_ref_parts.name
609 609 target_ref_id = pull_request.target_ref_parts.commit_id
610 610
611 611 if not self.has_valid_update_type(pull_request):
612 612 log.debug(
613 613 "Skipping update of pull request %s due to ref type: %s",
614 614 pull_request, source_ref_type)
615 615 return UpdateResponse(
616 616 executed=False,
617 617 reason=UpdateFailureReason.WRONG_REF_TYPE,
618 618 old=pull_request, new=None, changes=None,
619 619 source_changed=False, target_changed=False)
620 620
621 621 # source repo
622 622 source_repo = pull_request.source_repo.scm_instance()
623 623 try:
624 624 source_commit = source_repo.get_commit(commit_id=source_ref_name)
625 625 except CommitDoesNotExistError:
626 626 return UpdateResponse(
627 627 executed=False,
628 628 reason=UpdateFailureReason.MISSING_SOURCE_REF,
629 629 old=pull_request, new=None, changes=None,
630 630 source_changed=False, target_changed=False)
631 631
632 632 source_changed = source_ref_id != source_commit.raw_id
633 633
634 634 # target repo
635 635 target_repo = pull_request.target_repo.scm_instance()
636 636 try:
637 637 target_commit = target_repo.get_commit(commit_id=target_ref_name)
638 638 except CommitDoesNotExistError:
639 639 return UpdateResponse(
640 640 executed=False,
641 641 reason=UpdateFailureReason.MISSING_TARGET_REF,
642 642 old=pull_request, new=None, changes=None,
643 643 source_changed=False, target_changed=False)
644 644 target_changed = target_ref_id != target_commit.raw_id
645 645
646 646 if not (source_changed or target_changed):
647 647 log.debug("Nothing changed in pull request %s", pull_request)
648 648 return UpdateResponse(
649 649 executed=False,
650 650 reason=UpdateFailureReason.NO_CHANGE,
651 651 old=pull_request, new=None, changes=None,
652 652 source_changed=target_changed, target_changed=source_changed)
653 653
654 654 change_in_found = 'target repo' if target_changed else 'source repo'
655 655 log.debug('Updating pull request because of change in %s detected',
656 656 change_in_found)
657 657
658 658 # Finally there is a need for an update, in case of source change
659 659 # we create a new version, else just an update
660 660 if source_changed:
661 661 pull_request_version = self._create_version_from_snapshot(pull_request)
662 662 self._link_comments_to_version(pull_request_version)
663 663 else:
664 664 try:
665 665 ver = pull_request.versions[-1]
666 666 except IndexError:
667 667 ver = None
668 668
669 669 pull_request.pull_request_version_id = \
670 670 ver.pull_request_version_id if ver else None
671 671 pull_request_version = pull_request
672 672
673 673 try:
674 674 if target_ref_type in ('tag', 'branch', 'book'):
675 675 target_commit = target_repo.get_commit(target_ref_name)
676 676 else:
677 677 target_commit = target_repo.get_commit(target_ref_id)
678 678 except CommitDoesNotExistError:
679 679 return UpdateResponse(
680 680 executed=False,
681 681 reason=UpdateFailureReason.MISSING_TARGET_REF,
682 682 old=pull_request, new=None, changes=None,
683 683 source_changed=source_changed, target_changed=target_changed)
684 684
685 685 # re-compute commit ids
686 686 old_commit_ids = pull_request.revisions
687 687 pre_load = ["author", "branch", "date", "message"]
688 688 commit_ranges = target_repo.compare(
689 689 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
690 690 pre_load=pre_load)
691 691
692 692 ancestor = target_repo.get_common_ancestor(
693 693 target_commit.raw_id, source_commit.raw_id, source_repo)
694 694
695 695 pull_request.source_ref = '%s:%s:%s' % (
696 696 source_ref_type, source_ref_name, source_commit.raw_id)
697 697 pull_request.target_ref = '%s:%s:%s' % (
698 698 target_ref_type, target_ref_name, ancestor)
699 699
700 700 pull_request.revisions = [
701 701 commit.raw_id for commit in reversed(commit_ranges)]
702 702 pull_request.updated_on = datetime.datetime.now()
703 703 Session().add(pull_request)
704 704 new_commit_ids = pull_request.revisions
705 705
706 706 old_diff_data, new_diff_data = self._generate_update_diffs(
707 707 pull_request, pull_request_version)
708 708
709 709 # calculate commit and file changes
710 710 changes = self._calculate_commit_id_changes(
711 711 old_commit_ids, new_commit_ids)
712 712 file_changes = self._calculate_file_changes(
713 713 old_diff_data, new_diff_data)
714 714
715 715 # set comments as outdated if DIFFS changed
716 716 CommentsModel().outdate_comments(
717 717 pull_request, old_diff_data=old_diff_data,
718 718 new_diff_data=new_diff_data)
719 719
720 720 commit_changes = (changes.added or changes.removed)
721 721 file_node_changes = (
722 722 file_changes.added or file_changes.modified or file_changes.removed)
723 723 pr_has_changes = commit_changes or file_node_changes
724 724
725 725 # Add an automatic comment to the pull request, in case
726 726 # anything has changed
727 727 if pr_has_changes:
728 728 update_comment = CommentsModel().create(
729 729 text=self._render_update_message(changes, file_changes),
730 730 repo=pull_request.target_repo,
731 731 user=pull_request.author,
732 732 pull_request=pull_request,
733 733 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
734 734
735 735 # Update status to "Under Review" for added commits
736 736 for commit_id in changes.added:
737 737 ChangesetStatusModel().set_status(
738 738 repo=pull_request.source_repo,
739 739 status=ChangesetStatus.STATUS_UNDER_REVIEW,
740 740 comment=update_comment,
741 741 user=pull_request.author,
742 742 pull_request=pull_request,
743 743 revision=commit_id)
744 744
745 745 log.debug(
746 746 'Updated pull request %s, added_ids: %s, common_ids: %s, '
747 747 'removed_ids: %s', pull_request.pull_request_id,
748 748 changes.added, changes.common, changes.removed)
749 749 log.debug(
750 750 'Updated pull request with the following file changes: %s',
751 751 file_changes)
752 752
753 753 log.info(
754 754 "Updated pull request %s from commit %s to commit %s, "
755 755 "stored new version %s of this pull request.",
756 756 pull_request.pull_request_id, source_ref_id,
757 757 pull_request.source_ref_parts.commit_id,
758 758 pull_request_version.pull_request_version_id)
759 759 Session().commit()
760 760 self._trigger_pull_request_hook(
761 761 pull_request, pull_request.author, 'update')
762 762
763 763 return UpdateResponse(
764 764 executed=True, reason=UpdateFailureReason.NONE,
765 765 old=pull_request, new=pull_request_version, changes=changes,
766 766 source_changed=source_changed, target_changed=target_changed)
767 767
768 768 def _create_version_from_snapshot(self, pull_request):
769 769 version = PullRequestVersion()
770 770 version.title = pull_request.title
771 771 version.description = pull_request.description
772 772 version.status = pull_request.status
773 773 version.created_on = datetime.datetime.now()
774 774 version.updated_on = pull_request.updated_on
775 775 version.user_id = pull_request.user_id
776 776 version.source_repo = pull_request.source_repo
777 777 version.source_ref = pull_request.source_ref
778 778 version.target_repo = pull_request.target_repo
779 779 version.target_ref = pull_request.target_ref
780 780
781 781 version._last_merge_source_rev = pull_request._last_merge_source_rev
782 782 version._last_merge_target_rev = pull_request._last_merge_target_rev
783 783 version.last_merge_status = pull_request.last_merge_status
784 784 version.shadow_merge_ref = pull_request.shadow_merge_ref
785 785 version.merge_rev = pull_request.merge_rev
786 786 version.reviewer_data = pull_request.reviewer_data
787 787
788 788 version.revisions = pull_request.revisions
789 789 version.pull_request = pull_request
790 790 Session().add(version)
791 791 Session().flush()
792 792
793 793 return version
794 794
795 795 def _generate_update_diffs(self, pull_request, pull_request_version):
796 796
797 797 diff_context = (
798 798 self.DIFF_CONTEXT +
799 799 CommentsModel.needed_extra_diff_context())
800 800
801 801 source_repo = pull_request_version.source_repo
802 802 source_ref_id = pull_request_version.source_ref_parts.commit_id
803 803 target_ref_id = pull_request_version.target_ref_parts.commit_id
804 804 old_diff = self._get_diff_from_pr_or_version(
805 805 source_repo, source_ref_id, target_ref_id, context=diff_context)
806 806
807 807 source_repo = pull_request.source_repo
808 808 source_ref_id = pull_request.source_ref_parts.commit_id
809 809 target_ref_id = pull_request.target_ref_parts.commit_id
810 810
811 811 new_diff = self._get_diff_from_pr_or_version(
812 812 source_repo, source_ref_id, target_ref_id, context=diff_context)
813 813
814 814 old_diff_data = diffs.DiffProcessor(old_diff)
815 815 old_diff_data.prepare()
816 816 new_diff_data = diffs.DiffProcessor(new_diff)
817 817 new_diff_data.prepare()
818 818
819 819 return old_diff_data, new_diff_data
820 820
821 821 def _link_comments_to_version(self, pull_request_version):
822 822 """
823 823 Link all unlinked comments of this pull request to the given version.
824 824
825 825 :param pull_request_version: The `PullRequestVersion` to which
826 826 the comments shall be linked.
827 827
828 828 """
829 829 pull_request = pull_request_version.pull_request
830 830 comments = ChangesetComment.query()\
831 831 .filter(
832 832 # TODO: johbo: Should we query for the repo at all here?
833 833 # Pending decision on how comments of PRs are to be related
834 834 # to either the source repo, the target repo or no repo at all.
835 835 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
836 836 ChangesetComment.pull_request == pull_request,
837 837 ChangesetComment.pull_request_version == None)\
838 838 .order_by(ChangesetComment.comment_id.asc())
839 839
840 840 # TODO: johbo: Find out why this breaks if it is done in a bulk
841 841 # operation.
842 842 for comment in comments:
843 843 comment.pull_request_version_id = (
844 844 pull_request_version.pull_request_version_id)
845 845 Session().add(comment)
846 846
847 847 def _calculate_commit_id_changes(self, old_ids, new_ids):
848 848 added = [x for x in new_ids if x not in old_ids]
849 849 common = [x for x in new_ids if x in old_ids]
850 850 removed = [x for x in old_ids if x not in new_ids]
851 851 total = new_ids
852 852 return ChangeTuple(added, common, removed, total)
853 853
854 854 def _calculate_file_changes(self, old_diff_data, new_diff_data):
855 855
856 856 old_files = OrderedDict()
857 857 for diff_data in old_diff_data.parsed_diff:
858 858 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
859 859
860 860 added_files = []
861 861 modified_files = []
862 862 removed_files = []
863 863 for diff_data in new_diff_data.parsed_diff:
864 864 new_filename = diff_data['filename']
865 865 new_hash = md5_safe(diff_data['raw_diff'])
866 866
867 867 old_hash = old_files.get(new_filename)
868 868 if not old_hash:
869 869 # file is not present in old diff, means it's added
870 870 added_files.append(new_filename)
871 871 else:
872 872 if new_hash != old_hash:
873 873 modified_files.append(new_filename)
874 874 # now remove a file from old, since we have seen it already
875 875 del old_files[new_filename]
876 876
877 877 # removed files is when there are present in old, but not in NEW,
878 878 # since we remove old files that are present in new diff, left-overs
879 879 # if any should be the removed files
880 880 removed_files.extend(old_files.keys())
881 881
882 882 return FileChangeTuple(added_files, modified_files, removed_files)
883 883
884 884 def _render_update_message(self, changes, file_changes):
885 885 """
886 886 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
887 887 so it's always looking the same disregarding on which default
888 888 renderer system is using.
889 889
890 890 :param changes: changes named tuple
891 891 :param file_changes: file changes named tuple
892 892
893 893 """
894 894 new_status = ChangesetStatus.get_status_lbl(
895 895 ChangesetStatus.STATUS_UNDER_REVIEW)
896 896
897 897 changed_files = (
898 898 file_changes.added + file_changes.modified + file_changes.removed)
899 899
900 900 params = {
901 901 'under_review_label': new_status,
902 902 'added_commits': changes.added,
903 903 'removed_commits': changes.removed,
904 904 'changed_files': changed_files,
905 905 'added_files': file_changes.added,
906 906 'modified_files': file_changes.modified,
907 907 'removed_files': file_changes.removed,
908 908 }
909 909 renderer = RstTemplateRenderer()
910 910 return renderer.render('pull_request_update.mako', **params)
911 911
912 912 def edit(self, pull_request, title, description, user):
913 913 pull_request = self.__get_pull_request(pull_request)
914 914 old_data = pull_request.get_api_data(with_merge_state=False)
915 915 if pull_request.is_closed():
916 916 raise ValueError('This pull request is closed')
917 917 if title:
918 918 pull_request.title = title
919 919 pull_request.description = description
920 920 pull_request.updated_on = datetime.datetime.now()
921 921 Session().add(pull_request)
922 922 self._log_audit_action(
923 923 'repo.pull_request.edit', {'old_data': old_data},
924 924 user, pull_request)
925 925
926 926 def update_reviewers(self, pull_request, reviewer_data, user):
927 927 """
928 928 Update the reviewers in the pull request
929 929
930 930 :param pull_request: the pr to update
931 931 :param reviewer_data: list of tuples
932 932 [(user, ['reason1', 'reason2'], mandatory_flag)]
933 933 """
934 934
935 935 reviewers = {}
936 936 for user_id, reasons, mandatory in reviewer_data:
937 937 if isinstance(user_id, (int, basestring)):
938 938 user_id = self._get_user(user_id).user_id
939 939 reviewers[user_id] = {
940 940 'reasons': reasons, 'mandatory': mandatory}
941 941
942 942 reviewers_ids = set(reviewers.keys())
943 943 pull_request = self.__get_pull_request(pull_request)
944 944 current_reviewers = PullRequestReviewers.query()\
945 945 .filter(PullRequestReviewers.pull_request ==
946 946 pull_request).all()
947 947 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
948 948
949 949 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
950 950 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
951 951
952 952 log.debug("Adding %s reviewers", ids_to_add)
953 953 log.debug("Removing %s reviewers", ids_to_remove)
954 954 changed = False
955 955 for uid in ids_to_add:
956 956 changed = True
957 957 _usr = self._get_user(uid)
958 958 reviewer = PullRequestReviewers()
959 959 reviewer.user = _usr
960 960 reviewer.pull_request = pull_request
961 961 reviewer.reasons = reviewers[uid]['reasons']
962 962 # NOTE(marcink): mandatory shouldn't be changed now
963 963 # reviewer.mandatory = reviewers[uid]['reasons']
964 964 Session().add(reviewer)
965 965 self._log_audit_action(
966 966 'repo.pull_request.reviewer.add', {'data': reviewer.get_dict()},
967 967 user, pull_request)
968 968
969 969 for uid in ids_to_remove:
970 970 changed = True
971 971 reviewers = PullRequestReviewers.query()\
972 972 .filter(PullRequestReviewers.user_id == uid,
973 973 PullRequestReviewers.pull_request == pull_request)\
974 974 .all()
975 975 # use .all() in case we accidentally added the same person twice
976 976 # this CAN happen due to the lack of DB checks
977 977 for obj in reviewers:
978 978 old_data = obj.get_dict()
979 979 Session().delete(obj)
980 980 self._log_audit_action(
981 981 'repo.pull_request.reviewer.delete',
982 982 {'old_data': old_data}, user, pull_request)
983 983
984 984 if changed:
985 985 pull_request.updated_on = datetime.datetime.now()
986 986 Session().add(pull_request)
987 987
988 988 self.notify_reviewers(pull_request, ids_to_add)
989 989 return ids_to_add, ids_to_remove
990 990
991 991 def get_url(self, pull_request, request=None, permalink=False):
992 992 if not request:
993 993 request = get_current_request()
994 994
995 995 if permalink:
996 996 return request.route_url(
997 997 'pull_requests_global',
998 998 pull_request_id=pull_request.pull_request_id,)
999 999 else:
1000 1000 return request.route_url('pullrequest_show',
1001 1001 repo_name=safe_str(pull_request.target_repo.repo_name),
1002 1002 pull_request_id=pull_request.pull_request_id,)
1003 1003
1004 1004 def get_shadow_clone_url(self, pull_request):
1005 1005 """
1006 1006 Returns qualified url pointing to the shadow repository. If this pull
1007 1007 request is closed there is no shadow repository and ``None`` will be
1008 1008 returned.
1009 1009 """
1010 1010 if pull_request.is_closed():
1011 1011 return None
1012 1012 else:
1013 1013 pr_url = urllib.unquote(self.get_url(pull_request))
1014 1014 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
1015 1015
1016 1016 def notify_reviewers(self, pull_request, reviewers_ids):
1017 1017 # notification to reviewers
1018 1018 if not reviewers_ids:
1019 1019 return
1020 1020
1021 1021 pull_request_obj = pull_request
1022 1022 # get the current participants of this pull request
1023 1023 recipients = reviewers_ids
1024 1024 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
1025 1025
1026 1026 pr_source_repo = pull_request_obj.source_repo
1027 1027 pr_target_repo = pull_request_obj.target_repo
1028 1028
1029 1029 pr_url = h.route_url('pullrequest_show',
1030 1030 repo_name=pr_target_repo.repo_name,
1031 1031 pull_request_id=pull_request_obj.pull_request_id,)
1032 1032
1033 1033 # set some variables for email notification
1034 1034 pr_target_repo_url = h.route_url(
1035 1035 'repo_summary', repo_name=pr_target_repo.repo_name)
1036 1036
1037 1037 pr_source_repo_url = h.route_url(
1038 1038 'repo_summary', repo_name=pr_source_repo.repo_name)
1039 1039
1040 1040 # pull request specifics
1041 1041 pull_request_commits = [
1042 1042 (x.raw_id, x.message)
1043 1043 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
1044 1044
1045 1045 kwargs = {
1046 1046 'user': pull_request.author,
1047 1047 'pull_request': pull_request_obj,
1048 1048 'pull_request_commits': pull_request_commits,
1049 1049
1050 1050 'pull_request_target_repo': pr_target_repo,
1051 1051 'pull_request_target_repo_url': pr_target_repo_url,
1052 1052
1053 1053 'pull_request_source_repo': pr_source_repo,
1054 1054 'pull_request_source_repo_url': pr_source_repo_url,
1055 1055
1056 1056 'pull_request_url': pr_url,
1057 1057 }
1058 1058
1059 1059 # pre-generate the subject for notification itself
1060 1060 (subject,
1061 1061 _h, _e, # we don't care about those
1062 1062 body_plaintext) = EmailNotificationModel().render_email(
1063 1063 notification_type, **kwargs)
1064 1064
1065 1065 # create notification objects, and emails
1066 1066 NotificationModel().create(
1067 1067 created_by=pull_request.author,
1068 1068 notification_subject=subject,
1069 1069 notification_body=body_plaintext,
1070 1070 notification_type=notification_type,
1071 1071 recipients=recipients,
1072 1072 email_kwargs=kwargs,
1073 1073 )
1074 1074
1075 1075 def delete(self, pull_request, user):
1076 1076 pull_request = self.__get_pull_request(pull_request)
1077 1077 old_data = pull_request.get_api_data(with_merge_state=False)
1078 1078 self._cleanup_merge_workspace(pull_request)
1079 1079 self._log_audit_action(
1080 1080 'repo.pull_request.delete', {'old_data': old_data},
1081 1081 user, pull_request)
1082 1082 Session().delete(pull_request)
1083 1083
1084 1084 def close_pull_request(self, pull_request, user):
1085 1085 pull_request = self.__get_pull_request(pull_request)
1086 1086 self._cleanup_merge_workspace(pull_request)
1087 1087 pull_request.status = PullRequest.STATUS_CLOSED
1088 1088 pull_request.updated_on = datetime.datetime.now()
1089 1089 Session().add(pull_request)
1090 1090 self._trigger_pull_request_hook(
1091 1091 pull_request, pull_request.author, 'close')
1092 1092 self._log_audit_action(
1093 1093 'repo.pull_request.close', {}, user, pull_request)
1094 1094
1095 1095 def close_pull_request_with_comment(
1096 1096 self, pull_request, user, repo, message=None):
1097 1097
1098 1098 pull_request_review_status = pull_request.calculated_review_status()
1099 1099
1100 1100 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
1101 1101 # approved only if we have voting consent
1102 1102 status = ChangesetStatus.STATUS_APPROVED
1103 1103 else:
1104 1104 status = ChangesetStatus.STATUS_REJECTED
1105 1105 status_lbl = ChangesetStatus.get_status_lbl(status)
1106 1106
1107 1107 default_message = (
1108 1108 _('Closing with status change {transition_icon} {status}.')
1109 1109 ).format(transition_icon='>', status=status_lbl)
1110 1110 text = message or default_message
1111 1111
1112 1112 # create a comment, and link it to new status
1113 1113 comment = CommentsModel().create(
1114 1114 text=text,
1115 1115 repo=repo.repo_id,
1116 1116 user=user.user_id,
1117 1117 pull_request=pull_request.pull_request_id,
1118 1118 status_change=status_lbl,
1119 1119 status_change_type=status,
1120 1120 closing_pr=True
1121 1121 )
1122 1122
1123 1123 # calculate old status before we change it
1124 1124 old_calculated_status = pull_request.calculated_review_status()
1125 1125 ChangesetStatusModel().set_status(
1126 1126 repo.repo_id,
1127 1127 status,
1128 1128 user.user_id,
1129 1129 comment=comment,
1130 1130 pull_request=pull_request.pull_request_id
1131 1131 )
1132 1132
1133 1133 Session().flush()
1134 1134 events.trigger(events.PullRequestCommentEvent(pull_request, comment))
1135 1135 # we now calculate the status of pull request again, and based on that
1136 1136 # calculation trigger status change. This might happen in cases
1137 1137 # that non-reviewer admin closes a pr, which means his vote doesn't
1138 1138 # change the status, while if he's a reviewer this might change it.
1139 1139 calculated_status = pull_request.calculated_review_status()
1140 1140 if old_calculated_status != calculated_status:
1141 1141 self._trigger_pull_request_hook(
1142 1142 pull_request, user, 'review_status_change')
1143 1143
1144 1144 # finally close the PR
1145 1145 PullRequestModel().close_pull_request(
1146 1146 pull_request.pull_request_id, user)
1147 1147
1148 1148 return comment, status
1149 1149
1150 1150 def merge_status(self, pull_request):
1151 1151 if not self._is_merge_enabled(pull_request):
1152 1152 return False, _('Server-side pull request merging is disabled.')
1153 1153 if pull_request.is_closed():
1154 1154 return False, _('This pull request is closed.')
1155 1155 merge_possible, msg = self._check_repo_requirements(
1156 1156 target=pull_request.target_repo, source=pull_request.source_repo)
1157 1157 if not merge_possible:
1158 1158 return merge_possible, msg
1159 1159
1160 1160 try:
1161 1161 resp = self._try_merge(pull_request)
1162 1162 log.debug("Merge response: %s", resp)
1163 1163 status = resp.possible, self.merge_status_message(
1164 1164 resp.failure_reason)
1165 1165 except NotImplementedError:
1166 1166 status = False, _('Pull request merging is not supported.')
1167 1167
1168 1168 return status
1169 1169
1170 1170 def _check_repo_requirements(self, target, source):
1171 1171 """
1172 1172 Check if `target` and `source` have compatible requirements.
1173 1173
1174 1174 Currently this is just checking for largefiles.
1175 1175 """
1176 1176 target_has_largefiles = self._has_largefiles(target)
1177 1177 source_has_largefiles = self._has_largefiles(source)
1178 1178 merge_possible = True
1179 1179 message = u''
1180 1180
1181 1181 if target_has_largefiles != source_has_largefiles:
1182 1182 merge_possible = False
1183 1183 if source_has_largefiles:
1184 1184 message = _(
1185 1185 'Target repository large files support is disabled.')
1186 1186 else:
1187 1187 message = _(
1188 1188 'Source repository large files support is disabled.')
1189 1189
1190 1190 return merge_possible, message
1191 1191
1192 1192 def _has_largefiles(self, repo):
1193 1193 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1194 1194 'extensions', 'largefiles')
1195 1195 return largefiles_ui and largefiles_ui[0].active
1196 1196
1197 1197 def _try_merge(self, pull_request):
1198 1198 """
1199 1199 Try to merge the pull request and return the merge status.
1200 1200 """
1201 1201 log.debug(
1202 1202 "Trying out if the pull request %s can be merged.",
1203 1203 pull_request.pull_request_id)
1204 1204 target_vcs = pull_request.target_repo.scm_instance()
1205 1205
1206 1206 # Refresh the target reference.
1207 1207 try:
1208 1208 target_ref = self._refresh_reference(
1209 1209 pull_request.target_ref_parts, target_vcs)
1210 1210 except CommitDoesNotExistError:
1211 1211 merge_state = MergeResponse(
1212 1212 False, False, None, MergeFailureReason.MISSING_TARGET_REF)
1213 1213 return merge_state
1214 1214
1215 1215 target_locked = pull_request.target_repo.locked
1216 1216 if target_locked and target_locked[0]:
1217 1217 log.debug("The target repository is locked.")
1218 1218 merge_state = MergeResponse(
1219 1219 False, False, None, MergeFailureReason.TARGET_IS_LOCKED)
1220 1220 elif self._needs_merge_state_refresh(pull_request, target_ref):
1221 1221 log.debug("Refreshing the merge status of the repository.")
1222 1222 merge_state = self._refresh_merge_state(
1223 1223 pull_request, target_vcs, target_ref)
1224 1224 else:
1225 1225 possible = pull_request.\
1226 1226 last_merge_status == MergeFailureReason.NONE
1227 1227 merge_state = MergeResponse(
1228 1228 possible, False, None, pull_request.last_merge_status)
1229 1229
1230 1230 return merge_state
1231 1231
1232 1232 def _refresh_reference(self, reference, vcs_repository):
1233 1233 if reference.type in ('branch', 'book'):
1234 1234 name_or_id = reference.name
1235 1235 else:
1236 1236 name_or_id = reference.commit_id
1237 1237 refreshed_commit = vcs_repository.get_commit(name_or_id)
1238 1238 refreshed_reference = Reference(
1239 1239 reference.type, reference.name, refreshed_commit.raw_id)
1240 1240 return refreshed_reference
1241 1241
1242 1242 def _needs_merge_state_refresh(self, pull_request, target_reference):
1243 1243 return not(
1244 1244 pull_request.revisions and
1245 1245 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1246 1246 target_reference.commit_id == pull_request._last_merge_target_rev)
1247 1247
1248 1248 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1249 1249 workspace_id = self._workspace_id(pull_request)
1250 1250 source_vcs = pull_request.source_repo.scm_instance()
1251 1251 use_rebase = self._use_rebase_for_merging(pull_request)
1252 1252 merge_state = target_vcs.merge(
1253 1253 target_reference, source_vcs, pull_request.source_ref_parts,
1254 1254 workspace_id, dry_run=True, use_rebase=use_rebase)
1255 1255
1256 1256 # Do not store the response if there was an unknown error.
1257 1257 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1258 1258 pull_request._last_merge_source_rev = \
1259 1259 pull_request.source_ref_parts.commit_id
1260 1260 pull_request._last_merge_target_rev = target_reference.commit_id
1261 1261 pull_request.last_merge_status = merge_state.failure_reason
1262 1262 pull_request.shadow_merge_ref = merge_state.merge_ref
1263 1263 Session().add(pull_request)
1264 1264 Session().commit()
1265 1265
1266 1266 return merge_state
1267 1267
1268 1268 def _workspace_id(self, pull_request):
1269 1269 workspace_id = 'pr-%s' % pull_request.pull_request_id
1270 1270 return workspace_id
1271 1271
1272 1272 def merge_status_message(self, status_code):
1273 1273 """
1274 1274 Return a human friendly error message for the given merge status code.
1275 1275 """
1276 1276 return self.MERGE_STATUS_MESSAGES[status_code]
1277 1277
1278 1278 def generate_repo_data(self, repo, commit_id=None, branch=None,
1279 1279 bookmark=None):
1280 1280 all_refs, selected_ref = \
1281 1281 self._get_repo_pullrequest_sources(
1282 1282 repo.scm_instance(), commit_id=commit_id,
1283 1283 branch=branch, bookmark=bookmark)
1284 1284
1285 1285 refs_select2 = []
1286 1286 for element in all_refs:
1287 1287 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1288 1288 refs_select2.append({'text': element[1], 'children': children})
1289 1289
1290 1290 return {
1291 1291 'user': {
1292 1292 'user_id': repo.user.user_id,
1293 1293 'username': repo.user.username,
1294 1294 'firstname': repo.user.first_name,
1295 1295 'lastname': repo.user.last_name,
1296 1296 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1297 1297 },
1298 1298 'description': h.chop_at_smart(repo.description_safe, '\n'),
1299 1299 'refs': {
1300 1300 'all_refs': all_refs,
1301 1301 'selected_ref': selected_ref,
1302 1302 'select2_refs': refs_select2
1303 1303 }
1304 1304 }
1305 1305
1306 1306 def generate_pullrequest_title(self, source, source_ref, target):
1307 1307 return u'{source}#{at_ref} to {target}'.format(
1308 1308 source=source,
1309 1309 at_ref=source_ref,
1310 1310 target=target,
1311 1311 )
1312 1312
1313 1313 def _cleanup_merge_workspace(self, pull_request):
1314 1314 # Merging related cleanup
1315 1315 target_scm = pull_request.target_repo.scm_instance()
1316 1316 workspace_id = 'pr-%s' % pull_request.pull_request_id
1317 1317
1318 1318 try:
1319 1319 target_scm.cleanup_merge_workspace(workspace_id)
1320 1320 except NotImplementedError:
1321 1321 pass
1322 1322
1323 1323 def _get_repo_pullrequest_sources(
1324 1324 self, repo, commit_id=None, branch=None, bookmark=None):
1325 1325 """
1326 1326 Return a structure with repo's interesting commits, suitable for
1327 1327 the selectors in pullrequest controller
1328 1328
1329 1329 :param commit_id: a commit that must be in the list somehow
1330 1330 and selected by default
1331 1331 :param branch: a branch that must be in the list and selected
1332 1332 by default - even if closed
1333 1333 :param bookmark: a bookmark that must be in the list and selected
1334 1334 """
1335 1335
1336 1336 commit_id = safe_str(commit_id) if commit_id else None
1337 1337 branch = safe_str(branch) if branch else None
1338 1338 bookmark = safe_str(bookmark) if bookmark else None
1339 1339
1340 1340 selected = None
1341 1341
1342 1342 # order matters: first source that has commit_id in it will be selected
1343 1343 sources = []
1344 1344 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1345 1345 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1346 1346
1347 1347 if commit_id:
1348 1348 ref_commit = (h.short_id(commit_id), commit_id)
1349 1349 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1350 1350
1351 1351 sources.append(
1352 1352 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1353 1353 )
1354 1354
1355 1355 groups = []
1356 1356 for group_key, ref_list, group_name, match in sources:
1357 1357 group_refs = []
1358 1358 for ref_name, ref_id in ref_list:
1359 1359 ref_key = '%s:%s:%s' % (group_key, ref_name, ref_id)
1360 1360 group_refs.append((ref_key, ref_name))
1361 1361
1362 1362 if not selected:
1363 1363 if set([commit_id, match]) & set([ref_id, ref_name]):
1364 1364 selected = ref_key
1365 1365
1366 1366 if group_refs:
1367 1367 groups.append((group_refs, group_name))
1368 1368
1369 1369 if not selected:
1370 1370 ref = commit_id or branch or bookmark
1371 1371 if ref:
1372 1372 raise CommitDoesNotExistError(
1373 1373 'No commit refs could be found matching: %s' % ref)
1374 1374 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1375 1375 selected = 'branch:%s:%s' % (
1376 1376 repo.DEFAULT_BRANCH_NAME,
1377 1377 repo.branches[repo.DEFAULT_BRANCH_NAME]
1378 1378 )
1379 1379 elif repo.commit_ids:
1380 1380 rev = repo.commit_ids[0]
1381 1381 selected = 'rev:%s:%s' % (rev, rev)
1382 1382 else:
1383 1383 raise EmptyRepositoryError()
1384 1384 return groups, selected
1385 1385
1386 1386 def get_diff(self, source_repo, source_ref_id, target_ref_id, context=DIFF_CONTEXT):
1387 1387 return self._get_diff_from_pr_or_version(
1388 1388 source_repo, source_ref_id, target_ref_id, context=context)
1389 1389
1390 1390 def _get_diff_from_pr_or_version(
1391 1391 self, source_repo, source_ref_id, target_ref_id, context):
1392 1392 target_commit = source_repo.get_commit(
1393 1393 commit_id=safe_str(target_ref_id))
1394 1394 source_commit = source_repo.get_commit(
1395 1395 commit_id=safe_str(source_ref_id))
1396 1396 if isinstance(source_repo, Repository):
1397 1397 vcs_repo = source_repo.scm_instance()
1398 1398 else:
1399 1399 vcs_repo = source_repo
1400 1400
1401 1401 # TODO: johbo: In the context of an update, we cannot reach
1402 1402 # the old commit anymore with our normal mechanisms. It needs
1403 1403 # some sort of special support in the vcs layer to avoid this
1404 1404 # workaround.
1405 1405 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1406 1406 vcs_repo.alias == 'git'):
1407 1407 source_commit.raw_id = safe_str(source_ref_id)
1408 1408
1409 1409 log.debug('calculating diff between '
1410 1410 'source_ref:%s and target_ref:%s for repo `%s`',
1411 1411 target_ref_id, source_ref_id,
1412 1412 safe_unicode(vcs_repo.path))
1413 1413
1414 1414 vcs_diff = vcs_repo.get_diff(
1415 1415 commit1=target_commit, commit2=source_commit, context=context)
1416 1416 return vcs_diff
1417 1417
1418 1418 def _is_merge_enabled(self, pull_request):
1419 1419 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1420 1420 settings = settings_model.get_general_settings()
1421 1421 return settings.get('rhodecode_pr_merge_enabled', False)
1422 1422
1423 1423 def _use_rebase_for_merging(self, pull_request):
1424 1424 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1425 1425 settings = settings_model.get_general_settings()
1426 1426 return settings.get('rhodecode_hg_use_rebase_for_merging', False)
1427 1427
1428 1428 def _log_audit_action(self, action, action_data, user, pull_request):
1429 1429 audit_logger.store(
1430 1430 action=action,
1431 1431 action_data=action_data,
1432 1432 user=user,
1433 1433 repo=pull_request.target_repo)
1434 1434
1435 1435 def get_reviewer_functions(self):
1436 1436 """
1437 1437 Fetches functions for validation and fetching default reviewers.
1438 1438 If available we use the EE package, else we fallback to CE
1439 1439 package functions
1440 1440 """
1441 1441 try:
1442 1442 from rc_reviewers.utils import get_default_reviewers_data
1443 1443 from rc_reviewers.utils import validate_default_reviewers
1444 1444 except ImportError:
1445 1445 from rhodecode.apps.repository.utils import \
1446 1446 get_default_reviewers_data
1447 1447 from rhodecode.apps.repository.utils import \
1448 1448 validate_default_reviewers
1449 1449
1450 1450 return get_default_reviewers_data, validate_default_reviewers
1451 1451
1452 1452
1453 1453 class MergeCheck(object):
1454 1454 """
1455 1455 Perform Merge Checks and returns a check object which stores information
1456 1456 about merge errors, and merge conditions
1457 1457 """
1458 1458 TODO_CHECK = 'todo'
1459 1459 PERM_CHECK = 'perm'
1460 1460 REVIEW_CHECK = 'review'
1461 1461 MERGE_CHECK = 'merge'
1462 1462
1463 1463 def __init__(self):
1464 1464 self.review_status = None
1465 1465 self.merge_possible = None
1466 1466 self.merge_msg = ''
1467 1467 self.failed = None
1468 1468 self.errors = []
1469 1469 self.error_details = OrderedDict()
1470 1470
1471 1471 def push_error(self, error_type, message, error_key, details):
1472 1472 self.failed = True
1473 1473 self.errors.append([error_type, message])
1474 1474 self.error_details[error_key] = dict(
1475 1475 details=details,
1476 1476 error_type=error_type,
1477 1477 message=message
1478 1478 )
1479 1479
1480 1480 @classmethod
1481 1481 def validate(cls, pull_request, user, fail_early=False, translator=None):
1482 1482 # if migrated to pyramid...
1483 1483 # _ = lambda: translator or _ # use passed in translator if any
1484 1484
1485 1485 merge_check = cls()
1486 1486
1487 1487 # permissions to merge
1488 1488 user_allowed_to_merge = PullRequestModel().check_user_merge(
1489 1489 pull_request, user)
1490 1490 if not user_allowed_to_merge:
1491 1491 log.debug("MergeCheck: cannot merge, approval is pending.")
1492 1492
1493 1493 msg = _('User `{}` not allowed to perform merge.').format(user.username)
1494 1494 merge_check.push_error('error', msg, cls.PERM_CHECK, user.username)
1495 1495 if fail_early:
1496 1496 return merge_check
1497 1497
1498 1498 # review status, must be always present
1499 1499 review_status = pull_request.calculated_review_status()
1500 1500 merge_check.review_status = review_status
1501 1501
1502 1502 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
1503 1503 if not status_approved:
1504 1504 log.debug("MergeCheck: cannot merge, approval is pending.")
1505 1505
1506 1506 msg = _('Pull request reviewer approval is pending.')
1507 1507
1508 1508 merge_check.push_error(
1509 1509 'warning', msg, cls.REVIEW_CHECK, review_status)
1510 1510
1511 1511 if fail_early:
1512 1512 return merge_check
1513 1513
1514 1514 # left over TODOs
1515 1515 todos = CommentsModel().get_unresolved_todos(pull_request)
1516 1516 if todos:
1517 1517 log.debug("MergeCheck: cannot merge, {} "
1518 1518 "unresolved todos left.".format(len(todos)))
1519 1519
1520 1520 if len(todos) == 1:
1521 1521 msg = _('Cannot merge, {} TODO still not resolved.').format(
1522 1522 len(todos))
1523 1523 else:
1524 1524 msg = _('Cannot merge, {} TODOs still not resolved.').format(
1525 1525 len(todos))
1526 1526
1527 1527 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
1528 1528
1529 1529 if fail_early:
1530 1530 return merge_check
1531 1531
1532 1532 # merge possible
1533 1533 merge_status, msg = PullRequestModel().merge_status(pull_request)
1534 1534 merge_check.merge_possible = merge_status
1535 1535 merge_check.merge_msg = msg
1536 1536 if not merge_status:
1537 1537 log.debug(
1538 1538 "MergeCheck: cannot merge, pull request merge not possible.")
1539 1539 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
1540 1540
1541 1541 if fail_early:
1542 1542 return merge_check
1543 1543
1544 1544 log.debug('MergeCheck: is failed: %s', merge_check.failed)
1545 1545 return merge_check
1546 1546
1547 @classmethod
1548 def get_merge_conditions(cls, pull_request):
1549 merge_details = {}
1550
1551 model = PullRequestModel()
1552 use_rebase = model._use_rebase_for_merging(pull_request)
1553
1554 if use_rebase:
1555 merge_details['merge_strategy'] = dict(
1556 details={},
1557 message=_('Merge strategy: rebase')
1558 )
1559 else:
1560 merge_details['merge_strategy'] = dict(
1561 details={},
1562 message=_('Merge strategy: explicit merge commit')
1563 )
1564
1565 close_branch = model._close_branch_before_merging(pull_request)
1566 if close_branch:
1567 repo_type = pull_request.target_repo.repo_type
1568 if repo_type == 'hg':
1569 close_msg = _('Source branch will be closed after merge.')
1570 elif repo_type == 'git':
1571 close_msg = _('Source branch will be deleted after merge.')
1572
1573 merge_details['close_branch'] = dict(
1574 details={},
1575 message=close_msg
1576 )
1577
1578 return merge_details
1547 1579
1548 1580 ChangeTuple = namedtuple('ChangeTuple',
1549 1581 ['added', 'common', 'removed', 'total'])
1550 1582
1551 1583 FileChangeTuple = namedtuple('FileChangeTuple',
1552 1584 ['added', 'modified', 'removed'])
@@ -1,2396 +1,2400 b''
1 1 //Primary CSS
2 2
3 3 //--- IMPORTS ------------------//
4 4
5 5 @import 'helpers';
6 6 @import 'mixins';
7 7 @import 'rcicons';
8 8 @import 'fonts';
9 9 @import 'variables';
10 10 @import 'bootstrap-variables';
11 11 @import 'form-bootstrap';
12 12 @import 'codemirror';
13 13 @import 'legacy_code_styles';
14 14 @import 'readme-box';
15 15 @import 'progress-bar';
16 16
17 17 @import 'type';
18 18 @import 'alerts';
19 19 @import 'buttons';
20 20 @import 'tags';
21 21 @import 'code-block';
22 22 @import 'examples';
23 23 @import 'login';
24 24 @import 'main-content';
25 25 @import 'select2';
26 26 @import 'comments';
27 27 @import 'panels-bootstrap';
28 28 @import 'panels';
29 29 @import 'deform';
30 30
31 31 //--- BASE ------------------//
32 32 .noscript-error {
33 33 top: 0;
34 34 left: 0;
35 35 width: 100%;
36 36 z-index: 101;
37 37 text-align: center;
38 38 font-family: @text-semibold;
39 39 font-size: 120%;
40 40 color: white;
41 41 background-color: @alert2;
42 42 padding: 5px 0 5px 0;
43 43 }
44 44
45 45 html {
46 46 display: table;
47 47 height: 100%;
48 48 width: 100%;
49 49 }
50 50
51 51 body {
52 52 display: table-cell;
53 53 width: 100%;
54 54 }
55 55
56 56 //--- LAYOUT ------------------//
57 57
58 58 .hidden{
59 59 display: none !important;
60 60 }
61 61
62 62 .box{
63 63 float: left;
64 64 width: 100%;
65 65 }
66 66
67 67 .browser-header {
68 68 clear: both;
69 69 }
70 70 .main {
71 71 clear: both;
72 72 padding:0 0 @pagepadding;
73 73 height: auto;
74 74
75 75 &:after { //clearfix
76 76 content:"";
77 77 clear:both;
78 78 width:100%;
79 79 display:block;
80 80 }
81 81 }
82 82
83 83 .action-link{
84 84 margin-left: @padding;
85 85 padding-left: @padding;
86 86 border-left: @border-thickness solid @border-default-color;
87 87 }
88 88
89 89 input + .action-link, .action-link.first{
90 90 border-left: none;
91 91 }
92 92
93 93 .action-link.last{
94 94 margin-right: @padding;
95 95 padding-right: @padding;
96 96 }
97 97
98 98 .action-link.active,
99 99 .action-link.active a{
100 100 color: @grey4;
101 101 }
102 102
103 103 .clipboard-action {
104 104 cursor: pointer;
105 105 }
106 106
107 107 ul.simple-list{
108 108 list-style: none;
109 109 margin: 0;
110 110 padding: 0;
111 111 }
112 112
113 113 .main-content {
114 114 padding-bottom: @pagepadding;
115 115 }
116 116
117 117 .wide-mode-wrapper {
118 118 max-width:4000px !important;
119 119 }
120 120
121 121 .wrapper {
122 122 position: relative;
123 123 max-width: @wrapper-maxwidth;
124 124 margin: 0 auto;
125 125 }
126 126
127 127 #content {
128 128 clear: both;
129 129 padding: 0 @contentpadding;
130 130 }
131 131
132 132 .advanced-settings-fields{
133 133 input{
134 134 margin-left: @textmargin;
135 135 margin-right: @padding/2;
136 136 }
137 137 }
138 138
139 139 .cs_files_title {
140 140 margin: @pagepadding 0 0;
141 141 }
142 142
143 143 input.inline[type="file"] {
144 144 display: inline;
145 145 }
146 146
147 147 .error_page {
148 148 margin: 10% auto;
149 149
150 150 h1 {
151 151 color: @grey2;
152 152 }
153 153
154 154 .alert {
155 155 margin: @padding 0;
156 156 }
157 157
158 158 .error-branding {
159 159 font-family: @text-semibold;
160 160 color: @grey4;
161 161 }
162 162
163 163 .error_message {
164 164 font-family: @text-regular;
165 165 }
166 166
167 167 .sidebar {
168 168 min-height: 275px;
169 169 margin: 0;
170 170 padding: 0 0 @sidebarpadding @sidebarpadding;
171 171 border: none;
172 172 }
173 173
174 174 .main-content {
175 175 position: relative;
176 176 margin: 0 @sidebarpadding @sidebarpadding;
177 177 padding: 0 0 0 @sidebarpadding;
178 178 border-left: @border-thickness solid @grey5;
179 179
180 180 @media (max-width:767px) {
181 181 clear: both;
182 182 width: 100%;
183 183 margin: 0;
184 184 border: none;
185 185 }
186 186 }
187 187
188 188 .inner-column {
189 189 float: left;
190 190 width: 29.75%;
191 191 min-height: 150px;
192 192 margin: @sidebarpadding 2% 0 0;
193 193 padding: 0 2% 0 0;
194 194 border-right: @border-thickness solid @grey5;
195 195
196 196 @media (max-width:767px) {
197 197 clear: both;
198 198 width: 100%;
199 199 border: none;
200 200 }
201 201
202 202 ul {
203 203 padding-left: 1.25em;
204 204 }
205 205
206 206 &:last-child {
207 207 margin: @sidebarpadding 0 0;
208 208 border: none;
209 209 }
210 210
211 211 h4 {
212 212 margin: 0 0 @padding;
213 213 font-family: @text-semibold;
214 214 }
215 215 }
216 216 }
217 217 .error-page-logo {
218 218 width: 130px;
219 219 height: 160px;
220 220 }
221 221
222 222 // HEADER
223 223 .header {
224 224
225 225 // TODO: johbo: Fix login pages, so that they work without a min-height
226 226 // for the header and then remove the min-height. I chose a smaller value
227 227 // intentionally here to avoid rendering issues in the main navigation.
228 228 min-height: 49px;
229 229
230 230 position: relative;
231 231 vertical-align: bottom;
232 232 padding: 0 @header-padding;
233 233 background-color: @grey2;
234 234 color: @grey5;
235 235
236 236 .title {
237 237 overflow: visible;
238 238 }
239 239
240 240 &:before,
241 241 &:after {
242 242 content: "";
243 243 clear: both;
244 244 width: 100%;
245 245 }
246 246
247 247 // TODO: johbo: Avoids breaking "Repositories" chooser
248 248 .select2-container .select2-choice .select2-arrow {
249 249 display: none;
250 250 }
251 251 }
252 252
253 253 #header-inner {
254 254 &.title {
255 255 margin: 0;
256 256 }
257 257 &:before,
258 258 &:after {
259 259 content: "";
260 260 clear: both;
261 261 }
262 262 }
263 263
264 264 // Gists
265 265 #files_data {
266 266 clear: both; //for firefox
267 267 }
268 268 #gistid {
269 269 margin-right: @padding;
270 270 }
271 271
272 272 // Global Settings Editor
273 273 .textarea.editor {
274 274 float: left;
275 275 position: relative;
276 276 max-width: @texteditor-width;
277 277
278 278 select {
279 279 position: absolute;
280 280 top:10px;
281 281 right:0;
282 282 }
283 283
284 284 .CodeMirror {
285 285 margin: 0;
286 286 }
287 287
288 288 .help-block {
289 289 margin: 0 0 @padding;
290 290 padding:.5em;
291 291 background-color: @grey6;
292 292 &.pre-formatting {
293 293 white-space: pre;
294 294 }
295 295 }
296 296 }
297 297
298 298 ul.auth_plugins {
299 299 margin: @padding 0 @padding @legend-width;
300 300 padding: 0;
301 301
302 302 li {
303 303 margin-bottom: @padding;
304 304 line-height: 1em;
305 305 list-style-type: none;
306 306
307 307 .auth_buttons .btn {
308 308 margin-right: @padding;
309 309 }
310 310
311 311 &:before { content: none; }
312 312 }
313 313 }
314 314
315 315
316 316 // My Account PR list
317 317
318 318 #show_closed {
319 319 margin: 0 1em 0 0;
320 320 }
321 321
322 322 .pullrequestlist {
323 323 .closed {
324 324 background-color: @grey6;
325 325 }
326 326 .td-status {
327 327 padding-left: .5em;
328 328 }
329 329 .log-container .truncate {
330 330 height: 2.75em;
331 331 white-space: pre-line;
332 332 }
333 333 table.rctable .user {
334 334 padding-left: 0;
335 335 }
336 336 table.rctable {
337 337 td.td-description,
338 338 .rc-user {
339 339 min-width: auto;
340 340 }
341 341 }
342 342 }
343 343
344 344 // Pull Requests
345 345
346 346 .pullrequests_section_head {
347 347 display: block;
348 348 clear: both;
349 349 margin: @padding 0;
350 350 font-family: @text-bold;
351 351 }
352 352
353 353 .pr-origininfo, .pr-targetinfo {
354 354 position: relative;
355 355
356 356 .tag {
357 357 display: inline-block;
358 358 margin: 0 1em .5em 0;
359 359 }
360 360
361 361 .clone-url {
362 362 display: inline-block;
363 363 margin: 0 0 .5em 0;
364 364 padding: 0;
365 365 line-height: 1.2em;
366 366 }
367 367 }
368 368
369 369 .pr-mergeinfo {
370 370 min-width: 95% !important;
371 371 padding: 0 !important;
372 372 border: 0;
373 373 }
374 374 .pr-mergeinfo-copy {
375 375 padding: 0 0;
376 376 }
377 377
378 378 .pr-pullinfo {
379 379 min-width: 95% !important;
380 380 padding: 0 !important;
381 381 border: 0;
382 382 }
383 383 .pr-pullinfo-copy {
384 384 padding: 0 0;
385 385 }
386 386
387 387
388 388 #pr-title-input {
389 389 width: 72%;
390 390 font-size: 1em;
391 391 font-family: @text-bold;
392 392 margin: 0;
393 393 padding: 0 0 0 @padding/4;
394 394 line-height: 1.7em;
395 395 color: @text-color;
396 396 letter-spacing: .02em;
397 397 }
398 398
399 399 #pullrequest_title {
400 400 width: 100%;
401 401 box-sizing: border-box;
402 402 }
403 403
404 404 #pr_open_message {
405 405 border: @border-thickness solid #fff;
406 406 border-radius: @border-radius;
407 407 padding: @padding-large-vertical @padding-large-vertical @padding-large-vertical 0;
408 408 text-align: left;
409 409 overflow: hidden;
410 410 }
411 411
412 412 .pr-submit-button {
413 413 float: right;
414 414 margin: 0 0 0 5px;
415 415 }
416 416
417 417 .pr-spacing-container {
418 418 padding: 20px;
419 419 clear: both
420 420 }
421 421
422 422 #pr-description-input {
423 423 margin-bottom: 0;
424 424 }
425 425
426 426 .pr-description-label {
427 427 vertical-align: top;
428 428 }
429 429
430 430 .perms_section_head {
431 431 min-width: 625px;
432 432
433 433 h2 {
434 434 margin-bottom: 0;
435 435 }
436 436
437 437 .label-checkbox {
438 438 float: left;
439 439 }
440 440
441 441 &.field {
442 442 margin: @space 0 @padding;
443 443 }
444 444
445 445 &:first-child.field {
446 446 margin-top: 0;
447 447
448 448 .label {
449 449 margin-top: 0;
450 450 padding-top: 0;
451 451 }
452 452
453 453 .radios {
454 454 padding-top: 0;
455 455 }
456 456 }
457 457
458 458 .radios {
459 459 position: relative;
460 460 width: 405px;
461 461 }
462 462 }
463 463
464 464 //--- MODULES ------------------//
465 465
466 466
467 467 // Server Announcement
468 468 #server-announcement {
469 469 width: 95%;
470 470 margin: @padding auto;
471 471 padding: @padding;
472 472 border-width: 2px;
473 473 border-style: solid;
474 474 .border-radius(2px);
475 475 font-family: @text-bold;
476 476
477 477 &.info { border-color: @alert4; background-color: @alert4-inner; }
478 478 &.warning { border-color: @alert3; background-color: @alert3-inner; }
479 479 &.error { border-color: @alert2; background-color: @alert2-inner; }
480 480 &.success { border-color: @alert1; background-color: @alert1-inner; }
481 481 &.neutral { border-color: @grey3; background-color: @grey6; }
482 482 }
483 483
484 484 // Fixed Sidebar Column
485 485 .sidebar-col-wrapper {
486 486 padding-left: @sidebar-all-width;
487 487
488 488 .sidebar {
489 489 width: @sidebar-width;
490 490 margin-left: -@sidebar-all-width;
491 491 }
492 492 }
493 493
494 494 .sidebar-col-wrapper.scw-small {
495 495 padding-left: @sidebar-small-all-width;
496 496
497 497 .sidebar {
498 498 width: @sidebar-small-width;
499 499 margin-left: -@sidebar-small-all-width;
500 500 }
501 501 }
502 502
503 503
504 504 // FOOTER
505 505 #footer {
506 506 padding: 0;
507 507 text-align: center;
508 508 vertical-align: middle;
509 509 color: @grey2;
510 510 background-color: @grey6;
511 511
512 512 p {
513 513 margin: 0;
514 514 padding: 1em;
515 515 line-height: 1em;
516 516 }
517 517
518 518 .server-instance { //server instance
519 519 display: none;
520 520 }
521 521
522 522 .title {
523 523 float: none;
524 524 margin: 0 auto;
525 525 }
526 526 }
527 527
528 528 button.close {
529 529 padding: 0;
530 530 cursor: pointer;
531 531 background: transparent;
532 532 border: 0;
533 533 .box-shadow(none);
534 534 -webkit-appearance: none;
535 535 }
536 536
537 537 .close {
538 538 float: right;
539 539 font-size: 21px;
540 540 font-family: @text-bootstrap;
541 541 line-height: 1em;
542 542 font-weight: bold;
543 543 color: @grey2;
544 544
545 545 &:hover,
546 546 &:focus {
547 547 color: @grey1;
548 548 text-decoration: none;
549 549 cursor: pointer;
550 550 }
551 551 }
552 552
553 553 // GRID
554 554 .sorting,
555 555 .sorting_desc,
556 556 .sorting_asc {
557 557 cursor: pointer;
558 558 }
559 559 .sorting_desc:after {
560 560 content: "\00A0\25B2";
561 561 font-size: .75em;
562 562 }
563 563 .sorting_asc:after {
564 564 content: "\00A0\25BC";
565 565 font-size: .68em;
566 566 }
567 567
568 568
569 569 .user_auth_tokens {
570 570
571 571 &.truncate {
572 572 white-space: nowrap;
573 573 overflow: hidden;
574 574 text-overflow: ellipsis;
575 575 }
576 576
577 577 .fields .field .input {
578 578 margin: 0;
579 579 }
580 580
581 581 input#description {
582 582 width: 100px;
583 583 margin: 0;
584 584 }
585 585
586 586 .drop-menu {
587 587 // TODO: johbo: Remove this, should work out of the box when
588 588 // having multiple inputs inline
589 589 margin: 0 0 0 5px;
590 590 }
591 591 }
592 592 #user_list_table {
593 593 .closed {
594 594 background-color: @grey6;
595 595 }
596 596 }
597 597
598 598
599 599 input {
600 600 &.disabled {
601 601 opacity: .5;
602 602 }
603 603 }
604 604
605 605 // remove extra padding in firefox
606 606 input::-moz-focus-inner { border:0; padding:0 }
607 607
608 608 .adjacent input {
609 609 margin-bottom: @padding;
610 610 }
611 611
612 612 .permissions_boxes {
613 613 display: block;
614 614 }
615 615
616 616 //TODO: lisa: this should be in tables
617 617 .show_more_col {
618 618 width: 20px;
619 619 }
620 620
621 621 //FORMS
622 622
623 623 .medium-inline,
624 624 input#description.medium-inline {
625 625 display: inline;
626 626 width: @medium-inline-input-width;
627 627 min-width: 100px;
628 628 }
629 629
630 630 select {
631 631 //reset
632 632 -webkit-appearance: none;
633 633 -moz-appearance: none;
634 634
635 635 display: inline-block;
636 636 height: 28px;
637 637 width: auto;
638 638 margin: 0 @padding @padding 0;
639 639 padding: 0 18px 0 8px;
640 640 line-height:1em;
641 641 font-size: @basefontsize;
642 642 border: @border-thickness solid @rcblue;
643 643 background:white url("../images/dt-arrow-dn.png") no-repeat 100% 50%;
644 644 color: @rcblue;
645 645
646 646 &:after {
647 647 content: "\00A0\25BE";
648 648 }
649 649
650 650 &:focus {
651 651 outline: none;
652 652 }
653 653 }
654 654
655 655 option {
656 656 &:focus {
657 657 outline: none;
658 658 }
659 659 }
660 660
661 661 input,
662 662 textarea {
663 663 padding: @input-padding;
664 664 border: @input-border-thickness solid @border-highlight-color;
665 665 .border-radius (@border-radius);
666 666 font-family: @text-light;
667 667 font-size: @basefontsize;
668 668
669 669 &.input-sm {
670 670 padding: 5px;
671 671 }
672 672
673 673 &#description {
674 674 min-width: @input-description-minwidth;
675 675 min-height: 1em;
676 676 padding: 10px;
677 677 }
678 678 }
679 679
680 680 .field-sm {
681 681 input,
682 682 textarea {
683 683 padding: 5px;
684 684 }
685 685 }
686 686
687 687 textarea {
688 688 display: block;
689 689 clear: both;
690 690 width: 100%;
691 691 min-height: 100px;
692 692 margin-bottom: @padding;
693 693 .box-sizing(border-box);
694 694 overflow: auto;
695 695 }
696 696
697 697 label {
698 698 font-family: @text-light;
699 699 }
700 700
701 701 // GRAVATARS
702 702 // centers gravatar on username to the right
703 703
704 704 .gravatar {
705 705 display: inline;
706 706 min-width: 16px;
707 707 min-height: 16px;
708 708 margin: -5px 0;
709 709 padding: 0;
710 710 line-height: 1em;
711 711 border: 1px solid @grey4;
712 712 box-sizing: content-box;
713 713
714 714 &.gravatar-large {
715 715 margin: -0.5em .25em -0.5em 0;
716 716 }
717 717
718 718 & + .user {
719 719 display: inline;
720 720 margin: 0;
721 721 padding: 0 0 0 .17em;
722 722 line-height: 1em;
723 723 }
724 724 }
725 725
726 726 .user-inline-data {
727 727 display: inline-block;
728 728 float: left;
729 729 padding-left: .5em;
730 730 line-height: 1.3em;
731 731 }
732 732
733 733 .rc-user { // gravatar + user wrapper
734 734 float: left;
735 735 position: relative;
736 736 min-width: 100px;
737 737 max-width: 200px;
738 738 min-height: (@gravatar-size + @border-thickness * 2); // account for border
739 739 display: block;
740 740 padding: 0 0 0 (@gravatar-size + @basefontsize/2 + @border-thickness * 2);
741 741
742 742
743 743 .gravatar {
744 744 display: block;
745 745 position: absolute;
746 746 top: 0;
747 747 left: 0;
748 748 min-width: @gravatar-size;
749 749 min-height: @gravatar-size;
750 750 margin: 0;
751 751 }
752 752
753 753 .user {
754 754 display: block;
755 755 max-width: 175px;
756 756 padding-top: 2px;
757 757 overflow: hidden;
758 758 text-overflow: ellipsis;
759 759 }
760 760 }
761 761
762 762 .gist-gravatar,
763 763 .journal_container {
764 764 .gravatar-large {
765 765 margin: 0 .5em -10px 0;
766 766 }
767 767 }
768 768
769 769
770 770 // ADMIN SETTINGS
771 771
772 772 // Tag Patterns
773 773 .tag_patterns {
774 774 .tag_input {
775 775 margin-bottom: @padding;
776 776 }
777 777 }
778 778
779 779 .locked_input {
780 780 position: relative;
781 781
782 782 input {
783 783 display: inline;
784 784 margin: 3px 5px 0px 0px;
785 785 }
786 786
787 787 br {
788 788 display: none;
789 789 }
790 790
791 791 .error-message {
792 792 float: left;
793 793 width: 100%;
794 794 }
795 795
796 796 .lock_input_button {
797 797 display: inline;
798 798 }
799 799
800 800 .help-block {
801 801 clear: both;
802 802 }
803 803 }
804 804
805 805 // Notifications
806 806
807 807 .notifications_buttons {
808 808 margin: 0 0 @space 0;
809 809 padding: 0;
810 810
811 811 .btn {
812 812 display: inline-block;
813 813 }
814 814 }
815 815
816 816 .notification-list {
817 817
818 818 div {
819 819 display: inline-block;
820 820 vertical-align: middle;
821 821 }
822 822
823 823 .container {
824 824 display: block;
825 825 margin: 0 0 @padding 0;
826 826 }
827 827
828 828 .delete-notifications {
829 829 margin-left: @padding;
830 830 text-align: right;
831 831 cursor: pointer;
832 832 }
833 833
834 834 .read-notifications {
835 835 margin-left: @padding/2;
836 836 text-align: right;
837 837 width: 35px;
838 838 cursor: pointer;
839 839 }
840 840
841 841 .icon-minus-sign {
842 842 color: @alert2;
843 843 }
844 844
845 845 .icon-ok-sign {
846 846 color: @alert1;
847 847 }
848 848 }
849 849
850 850 .user_settings {
851 851 float: left;
852 852 clear: both;
853 853 display: block;
854 854 width: 100%;
855 855
856 856 .gravatar_box {
857 857 margin-bottom: @padding;
858 858
859 859 &:after {
860 860 content: " ";
861 861 clear: both;
862 862 width: 100%;
863 863 }
864 864 }
865 865
866 866 .fields .field {
867 867 clear: both;
868 868 }
869 869 }
870 870
871 871 .advanced_settings {
872 872 margin-bottom: @space;
873 873
874 874 .help-block {
875 875 margin-left: 0;
876 876 }
877 877
878 878 button + .help-block {
879 879 margin-top: @padding;
880 880 }
881 881 }
882 882
883 883 // admin settings radio buttons and labels
884 884 .label-2 {
885 885 float: left;
886 886 width: @label2-width;
887 887
888 888 label {
889 889 color: @grey1;
890 890 }
891 891 }
892 892 .checkboxes {
893 893 float: left;
894 894 width: @checkboxes-width;
895 895 margin-bottom: @padding;
896 896
897 897 .checkbox {
898 898 width: 100%;
899 899
900 900 label {
901 901 margin: 0;
902 902 padding: 0;
903 903 }
904 904 }
905 905
906 906 .checkbox + .checkbox {
907 907 display: inline-block;
908 908 }
909 909
910 910 label {
911 911 margin-right: 1em;
912 912 }
913 913 }
914 914
915 915 // CHANGELOG
916 916 .container_header {
917 917 float: left;
918 918 display: block;
919 919 width: 100%;
920 920 margin: @padding 0 @padding;
921 921
922 922 #filter_changelog {
923 923 float: left;
924 924 margin-right: @padding;
925 925 }
926 926
927 927 .breadcrumbs_light {
928 928 display: inline-block;
929 929 }
930 930 }
931 931
932 932 .info_box {
933 933 float: right;
934 934 }
935 935
936 936
937 937 #graph_nodes {
938 938 padding-top: 43px;
939 939 }
940 940
941 941 #graph_content{
942 942
943 943 // adjust for table headers so that graph renders properly
944 944 // #graph_nodes padding - table cell padding
945 945 padding-top: (@space - (@basefontsize * 2.4));
946 946
947 947 &.graph_full_width {
948 948 width: 100%;
949 949 max-width: 100%;
950 950 }
951 951 }
952 952
953 953 #graph {
954 954 .flag_status {
955 955 margin: 0;
956 956 }
957 957
958 958 .pagination-left {
959 959 float: left;
960 960 clear: both;
961 961 }
962 962
963 963 .log-container {
964 964 max-width: 345px;
965 965
966 966 .message{
967 967 max-width: 340px;
968 968 }
969 969 }
970 970
971 971 .graph-col-wrapper {
972 972 padding-left: 110px;
973 973
974 974 #graph_nodes {
975 975 width: 100px;
976 976 margin-left: -110px;
977 977 float: left;
978 978 clear: left;
979 979 }
980 980 }
981 981
982 982 .load-more-commits {
983 983 text-align: center;
984 984 }
985 985 .load-more-commits:hover {
986 986 background-color: @grey7;
987 987 }
988 988 .load-more-commits {
989 989 a {
990 990 display: block;
991 991 }
992 992 }
993 993 }
994 994
995 995 #filter_changelog {
996 996 float: left;
997 997 }
998 998
999 999
1000 1000 //--- THEME ------------------//
1001 1001
1002 1002 #logo {
1003 1003 float: left;
1004 1004 margin: 9px 0 0 0;
1005 1005
1006 1006 .header {
1007 1007 background-color: transparent;
1008 1008 }
1009 1009
1010 1010 a {
1011 1011 display: inline-block;
1012 1012 }
1013 1013
1014 1014 img {
1015 1015 height:30px;
1016 1016 }
1017 1017 }
1018 1018
1019 1019 .logo-wrapper {
1020 1020 float:left;
1021 1021 }
1022 1022
1023 1023 .branding{
1024 1024 float: left;
1025 1025 padding: 9px 2px;
1026 1026 line-height: 1em;
1027 1027 font-size: @navigation-fontsize;
1028 1028 }
1029 1029
1030 1030 img {
1031 1031 border: none;
1032 1032 outline: none;
1033 1033 }
1034 1034 user-profile-header
1035 1035 label {
1036 1036
1037 1037 input[type="checkbox"] {
1038 1038 margin-right: 1em;
1039 1039 }
1040 1040 input[type="radio"] {
1041 1041 margin-right: 1em;
1042 1042 }
1043 1043 }
1044 1044
1045 1045 .flag_status {
1046 1046 margin: 2px 8px 6px 2px;
1047 1047 &.under_review {
1048 1048 .circle(5px, @alert3);
1049 1049 }
1050 1050 &.approved {
1051 1051 .circle(5px, @alert1);
1052 1052 }
1053 1053 &.rejected,
1054 1054 &.forced_closed{
1055 1055 .circle(5px, @alert2);
1056 1056 }
1057 1057 &.not_reviewed {
1058 1058 .circle(5px, @grey5);
1059 1059 }
1060 1060 }
1061 1061
1062 1062 .flag_status_comment_box {
1063 1063 margin: 5px 6px 0px 2px;
1064 1064 }
1065 1065 .test_pattern_preview {
1066 1066 margin: @space 0;
1067 1067
1068 1068 p {
1069 1069 margin-bottom: 0;
1070 1070 border-bottom: @border-thickness solid @border-default-color;
1071 1071 color: @grey3;
1072 1072 }
1073 1073
1074 1074 .btn {
1075 1075 margin-bottom: @padding;
1076 1076 }
1077 1077 }
1078 1078 #test_pattern_result {
1079 1079 display: none;
1080 1080 &:extend(pre);
1081 1081 padding: .9em;
1082 1082 color: @grey3;
1083 1083 background-color: @grey7;
1084 1084 border-right: @border-thickness solid @border-default-color;
1085 1085 border-bottom: @border-thickness solid @border-default-color;
1086 1086 border-left: @border-thickness solid @border-default-color;
1087 1087 }
1088 1088
1089 1089 #repo_vcs_settings {
1090 1090 #inherit_overlay_vcs_default {
1091 1091 display: none;
1092 1092 }
1093 1093 #inherit_overlay_vcs_custom {
1094 1094 display: custom;
1095 1095 }
1096 1096 &.inherited {
1097 1097 #inherit_overlay_vcs_default {
1098 1098 display: block;
1099 1099 }
1100 1100 #inherit_overlay_vcs_custom {
1101 1101 display: none;
1102 1102 }
1103 1103 }
1104 1104 }
1105 1105
1106 1106 .issue-tracker-link {
1107 1107 color: @rcblue;
1108 1108 }
1109 1109
1110 1110 // Issue Tracker Table Show/Hide
1111 1111 #repo_issue_tracker {
1112 1112 #inherit_overlay {
1113 1113 display: none;
1114 1114 }
1115 1115 #custom_overlay {
1116 1116 display: custom;
1117 1117 }
1118 1118 &.inherited {
1119 1119 #inherit_overlay {
1120 1120 display: block;
1121 1121 }
1122 1122 #custom_overlay {
1123 1123 display: none;
1124 1124 }
1125 1125 }
1126 1126 }
1127 1127 table.issuetracker {
1128 1128 &.readonly {
1129 1129 tr, td {
1130 1130 color: @grey3;
1131 1131 }
1132 1132 }
1133 1133 .edit {
1134 1134 display: none;
1135 1135 }
1136 1136 .editopen {
1137 1137 .edit {
1138 1138 display: inline;
1139 1139 }
1140 1140 .entry {
1141 1141 display: none;
1142 1142 }
1143 1143 }
1144 1144 tr td.td-action {
1145 1145 min-width: 117px;
1146 1146 }
1147 1147 td input {
1148 1148 max-width: none;
1149 1149 min-width: 30px;
1150 1150 width: 80%;
1151 1151 }
1152 1152 .issuetracker_pref input {
1153 1153 width: 40%;
1154 1154 }
1155 1155 input.edit_issuetracker_update {
1156 1156 margin-right: 0;
1157 1157 width: auto;
1158 1158 }
1159 1159 }
1160 1160
1161 1161 table.integrations {
1162 1162 .td-icon {
1163 1163 width: 20px;
1164 1164 .integration-icon {
1165 1165 height: 20px;
1166 1166 width: 20px;
1167 1167 }
1168 1168 }
1169 1169 }
1170 1170
1171 1171 .integrations {
1172 1172 a.integration-box {
1173 1173 color: @text-color;
1174 1174 &:hover {
1175 1175 .panel {
1176 1176 background: #fbfbfb;
1177 1177 }
1178 1178 }
1179 1179 .integration-icon {
1180 1180 width: 30px;
1181 1181 height: 30px;
1182 1182 margin-right: 20px;
1183 1183 float: left;
1184 1184 }
1185 1185
1186 1186 .panel-body {
1187 1187 padding: 10px;
1188 1188 }
1189 1189 .panel {
1190 1190 margin-bottom: 10px;
1191 1191 }
1192 1192 h2 {
1193 1193 display: inline-block;
1194 1194 margin: 0;
1195 1195 min-width: 140px;
1196 1196 }
1197 1197 }
1198 1198 }
1199 1199
1200 1200 //Permissions Settings
1201 1201 #add_perm {
1202 1202 margin: 0 0 @padding;
1203 1203 cursor: pointer;
1204 1204 }
1205 1205
1206 1206 .perm_ac {
1207 1207 input {
1208 1208 width: 95%;
1209 1209 }
1210 1210 }
1211 1211
1212 1212 .autocomplete-suggestions {
1213 1213 width: auto !important; // overrides autocomplete.js
1214 1214 margin: 0;
1215 1215 border: @border-thickness solid @rcblue;
1216 1216 border-radius: @border-radius;
1217 1217 color: @rcblue;
1218 1218 background-color: white;
1219 1219 }
1220 1220 .autocomplete-selected {
1221 1221 background: #F0F0F0;
1222 1222 }
1223 1223 .ac-container-wrap {
1224 1224 margin: 0;
1225 1225 padding: 8px;
1226 1226 border-bottom: @border-thickness solid @rclightblue;
1227 1227 list-style-type: none;
1228 1228 cursor: pointer;
1229 1229
1230 1230 &:hover {
1231 1231 background-color: @rclightblue;
1232 1232 }
1233 1233
1234 1234 img {
1235 1235 height: @gravatar-size;
1236 1236 width: @gravatar-size;
1237 1237 margin-right: 1em;
1238 1238 }
1239 1239
1240 1240 strong {
1241 1241 font-weight: normal;
1242 1242 }
1243 1243 }
1244 1244
1245 1245 // Settings Dropdown
1246 1246 .user-menu .container {
1247 1247 padding: 0 4px;
1248 1248 margin: 0;
1249 1249 }
1250 1250
1251 1251 .user-menu .gravatar {
1252 1252 cursor: pointer;
1253 1253 }
1254 1254
1255 1255 .codeblock {
1256 1256 margin-bottom: @padding;
1257 1257 clear: both;
1258 1258
1259 1259 .stats{
1260 1260 overflow: hidden;
1261 1261 }
1262 1262
1263 1263 .message{
1264 1264 textarea{
1265 1265 margin: 0;
1266 1266 }
1267 1267 }
1268 1268
1269 1269 .code-header {
1270 1270 .stats {
1271 1271 line-height: 2em;
1272 1272
1273 1273 .revision_id {
1274 1274 margin-left: 0;
1275 1275 }
1276 1276 .buttons {
1277 1277 padding-right: 0;
1278 1278 }
1279 1279 }
1280 1280
1281 1281 .item{
1282 1282 margin-right: 0.5em;
1283 1283 }
1284 1284 }
1285 1285
1286 1286 #editor_container{
1287 1287 position: relative;
1288 1288 margin: @padding;
1289 1289 }
1290 1290 }
1291 1291
1292 1292 #file_history_container {
1293 1293 display: none;
1294 1294 }
1295 1295
1296 1296 .file-history-inner {
1297 1297 margin-bottom: 10px;
1298 1298 }
1299 1299
1300 1300 // Pull Requests
1301 1301 .summary-details {
1302 1302 width: 72%;
1303 1303 }
1304 1304 .pr-summary {
1305 1305 border-bottom: @border-thickness solid @grey5;
1306 1306 margin-bottom: @space;
1307 1307 }
1308 1308 .reviewers-title {
1309 1309 width: 25%;
1310 1310 min-width: 200px;
1311 1311 }
1312 1312 .reviewers {
1313 1313 width: 25%;
1314 1314 min-width: 200px;
1315 1315 }
1316 1316 .reviewers ul li {
1317 1317 position: relative;
1318 1318 width: 100%;
1319 1319 margin-bottom: 8px;
1320 1320 }
1321 1321
1322 1322 .reviewer_entry {
1323 1323 min-height: 55px;
1324 1324 }
1325 1325
1326 1326 .reviewers_member {
1327 1327 width: 100%;
1328 1328 overflow: auto;
1329 1329 }
1330 1330 .reviewer_reason {
1331 1331 padding-left: 20px;
1332 1332 }
1333 1333 .reviewer_status {
1334 1334 display: inline-block;
1335 1335 vertical-align: top;
1336 1336 width: 7%;
1337 1337 min-width: 20px;
1338 1338 height: 1.2em;
1339 1339 margin-top: 3px;
1340 1340 line-height: 1em;
1341 1341 }
1342 1342
1343 1343 .reviewer_name {
1344 1344 display: inline-block;
1345 1345 max-width: 83%;
1346 1346 padding-right: 20px;
1347 1347 vertical-align: middle;
1348 1348 line-height: 1;
1349 1349
1350 1350 .rc-user {
1351 1351 min-width: 0;
1352 1352 margin: -2px 1em 0 0;
1353 1353 }
1354 1354
1355 1355 .reviewer {
1356 1356 float: left;
1357 1357 }
1358 1358 }
1359 1359
1360 1360 .reviewer_member_mandatory,
1361 1361 .reviewer_member_mandatory_remove,
1362 1362 .reviewer_member_remove {
1363 1363 position: absolute;
1364 1364 right: 0;
1365 1365 top: 0;
1366 1366 width: 16px;
1367 1367 margin-bottom: 10px;
1368 1368 padding: 0;
1369 1369 color: black;
1370 1370 }
1371 1371
1372 1372 .reviewer_member_mandatory_remove {
1373 1373 color: @grey4;
1374 1374 }
1375 1375
1376 1376 .reviewer_member_mandatory {
1377 1377 padding-top:20px;
1378 1378 }
1379 1379
1380 1380 .reviewer_member_status {
1381 1381 margin-top: 5px;
1382 1382 }
1383 1383 .pr-summary #summary{
1384 1384 width: 100%;
1385 1385 }
1386 1386 .pr-summary .action_button:hover {
1387 1387 border: 0;
1388 1388 cursor: pointer;
1389 1389 }
1390 1390 .pr-details-title {
1391 1391 padding-bottom: 8px;
1392 1392 border-bottom: @border-thickness solid @grey5;
1393 1393
1394 1394 .action_button.disabled {
1395 1395 color: @grey4;
1396 1396 cursor: inherit;
1397 1397 }
1398 1398 .action_button {
1399 1399 color: @rcblue;
1400 1400 }
1401 1401 }
1402 1402 .pr-details-content {
1403 1403 margin-top: @textmargin;
1404 1404 margin-bottom: @textmargin;
1405 1405 }
1406 1406 .pr-description {
1407 1407 white-space:pre-wrap;
1408 1408 }
1409 1409
1410 1410 .pr-reviewer-rules {
1411 1411 padding: 10px 0px 20px 0px;
1412 1412 }
1413 1413
1414 1414 .group_members {
1415 1415 margin-top: 0;
1416 1416 padding: 0;
1417 1417 list-style: outside none none;
1418 1418
1419 1419 img {
1420 1420 height: @gravatar-size;
1421 1421 width: @gravatar-size;
1422 1422 margin-right: .5em;
1423 1423 margin-left: 3px;
1424 1424 }
1425 1425
1426 1426 .to-delete {
1427 1427 .user {
1428 1428 text-decoration: line-through;
1429 1429 }
1430 1430 }
1431 1431 }
1432 1432
1433 1433 .compare_view_commits_title {
1434 1434 .disabled {
1435 1435 cursor: inherit;
1436 1436 &:hover{
1437 1437 background-color: inherit;
1438 1438 color: inherit;
1439 1439 }
1440 1440 }
1441 1441 }
1442 1442
1443 1443 .subtitle-compare {
1444 1444 margin: -15px 0px 0px 0px;
1445 1445 }
1446 1446
1447 1447 .comments-summary-td {
1448 1448 border-top: 1px dashed @grey5;
1449 1449 }
1450 1450
1451 1451 // new entry in group_members
1452 1452 .td-author-new-entry {
1453 1453 background-color: rgba(red(@alert1), green(@alert1), blue(@alert1), 0.3);
1454 1454 }
1455 1455
1456 1456 .usergroup_member_remove {
1457 1457 width: 16px;
1458 1458 margin-bottom: 10px;
1459 1459 padding: 0;
1460 1460 color: black !important;
1461 1461 cursor: pointer;
1462 1462 }
1463 1463
1464 1464 .reviewer_ac .ac-input {
1465 1465 width: 92%;
1466 1466 margin-bottom: 1em;
1467 1467 }
1468 1468
1469 1469 .compare_view_commits tr{
1470 1470 height: 20px;
1471 1471 }
1472 1472 .compare_view_commits td {
1473 1473 vertical-align: top;
1474 1474 padding-top: 10px;
1475 1475 }
1476 1476 .compare_view_commits .author {
1477 1477 margin-left: 5px;
1478 1478 }
1479 1479
1480 1480 .compare_view_commits {
1481 1481 .color-a {
1482 1482 color: @alert1;
1483 1483 }
1484 1484
1485 1485 .color-c {
1486 1486 color: @color3;
1487 1487 }
1488 1488
1489 1489 .color-r {
1490 1490 color: @color5;
1491 1491 }
1492 1492
1493 1493 .color-a-bg {
1494 1494 background-color: @alert1;
1495 1495 }
1496 1496
1497 1497 .color-c-bg {
1498 1498 background-color: @alert3;
1499 1499 }
1500 1500
1501 1501 .color-r-bg {
1502 1502 background-color: @alert2;
1503 1503 }
1504 1504
1505 1505 .color-a-border {
1506 1506 border: 1px solid @alert1;
1507 1507 }
1508 1508
1509 1509 .color-c-border {
1510 1510 border: 1px solid @alert3;
1511 1511 }
1512 1512
1513 1513 .color-r-border {
1514 1514 border: 1px solid @alert2;
1515 1515 }
1516 1516
1517 1517 .commit-change-indicator {
1518 1518 width: 15px;
1519 1519 height: 15px;
1520 1520 position: relative;
1521 1521 left: 15px;
1522 1522 }
1523 1523
1524 1524 .commit-change-content {
1525 1525 text-align: center;
1526 1526 vertical-align: middle;
1527 1527 line-height: 15px;
1528 1528 }
1529 1529 }
1530 1530
1531 1531 .compare_view_files {
1532 1532 width: 100%;
1533 1533
1534 1534 td {
1535 1535 vertical-align: middle;
1536 1536 }
1537 1537 }
1538 1538
1539 1539 .compare_view_filepath {
1540 1540 color: @grey1;
1541 1541 }
1542 1542
1543 1543 .show_more {
1544 1544 display: inline-block;
1545 1545 position: relative;
1546 1546 vertical-align: middle;
1547 1547 width: 4px;
1548 1548 height: @basefontsize;
1549 1549
1550 1550 &:after {
1551 1551 content: "\00A0\25BE";
1552 1552 display: inline-block;
1553 1553 width:10px;
1554 1554 line-height: 5px;
1555 1555 font-size: 12px;
1556 1556 cursor: pointer;
1557 1557 }
1558 1558 }
1559 1559
1560 1560 .journal_more .show_more {
1561 1561 display: inline;
1562 1562
1563 1563 &:after {
1564 1564 content: none;
1565 1565 }
1566 1566 }
1567 1567
1568 1568 .open .show_more:after,
1569 1569 .select2-dropdown-open .show_more:after {
1570 1570 .rotate(180deg);
1571 1571 margin-left: 4px;
1572 1572 }
1573 1573
1574 1574
1575 1575 .compare_view_commits .collapse_commit:after {
1576 1576 cursor: pointer;
1577 1577 content: "\00A0\25B4";
1578 1578 margin-left: -3px;
1579 1579 font-size: 17px;
1580 1580 color: @grey4;
1581 1581 }
1582 1582
1583 1583 .diff_links {
1584 1584 margin-left: 8px;
1585 1585 }
1586 1586
1587 1587 div.ancestor {
1588 1588 margin: -30px 0px;
1589 1589 }
1590 1590
1591 1591 .cs_icon_td input[type="checkbox"] {
1592 1592 display: none;
1593 1593 }
1594 1594
1595 1595 .cs_icon_td .expand_file_icon:after {
1596 1596 cursor: pointer;
1597 1597 content: "\00A0\25B6";
1598 1598 font-size: 12px;
1599 1599 color: @grey4;
1600 1600 }
1601 1601
1602 1602 .cs_icon_td .collapse_file_icon:after {
1603 1603 cursor: pointer;
1604 1604 content: "\00A0\25BC";
1605 1605 font-size: 12px;
1606 1606 color: @grey4;
1607 1607 }
1608 1608
1609 1609 /*new binary
1610 1610 NEW_FILENODE = 1
1611 1611 DEL_FILENODE = 2
1612 1612 MOD_FILENODE = 3
1613 1613 RENAMED_FILENODE = 4
1614 1614 COPIED_FILENODE = 5
1615 1615 CHMOD_FILENODE = 6
1616 1616 BIN_FILENODE = 7
1617 1617 */
1618 1618 .cs_files_expand {
1619 1619 font-size: @basefontsize + 5px;
1620 1620 line-height: 1.8em;
1621 1621 float: right;
1622 1622 }
1623 1623
1624 1624 .cs_files_expand span{
1625 1625 color: @rcblue;
1626 1626 cursor: pointer;
1627 1627 }
1628 1628 .cs_files {
1629 1629 clear: both;
1630 1630 padding-bottom: @padding;
1631 1631
1632 1632 .cur_cs {
1633 1633 margin: 10px 2px;
1634 1634 font-weight: bold;
1635 1635 }
1636 1636
1637 1637 .node {
1638 1638 float: left;
1639 1639 }
1640 1640
1641 1641 .changes {
1642 1642 float: right;
1643 1643 color: white;
1644 1644 font-size: @basefontsize - 4px;
1645 1645 margin-top: 4px;
1646 1646 opacity: 0.6;
1647 1647 filter: Alpha(opacity=60); /* IE8 and earlier */
1648 1648
1649 1649 .added {
1650 1650 background-color: @alert1;
1651 1651 float: left;
1652 1652 text-align: center;
1653 1653 }
1654 1654
1655 1655 .deleted {
1656 1656 background-color: @alert2;
1657 1657 float: left;
1658 1658 text-align: center;
1659 1659 }
1660 1660
1661 1661 .bin {
1662 1662 background-color: @alert1;
1663 1663 text-align: center;
1664 1664 }
1665 1665
1666 1666 /*new binary*/
1667 1667 .bin.bin1 {
1668 1668 background-color: @alert1;
1669 1669 text-align: center;
1670 1670 }
1671 1671
1672 1672 /*deleted binary*/
1673 1673 .bin.bin2 {
1674 1674 background-color: @alert2;
1675 1675 text-align: center;
1676 1676 }
1677 1677
1678 1678 /*mod binary*/
1679 1679 .bin.bin3 {
1680 1680 background-color: @grey2;
1681 1681 text-align: center;
1682 1682 }
1683 1683
1684 1684 /*rename file*/
1685 1685 .bin.bin4 {
1686 1686 background-color: @alert4;
1687 1687 text-align: center;
1688 1688 }
1689 1689
1690 1690 /*copied file*/
1691 1691 .bin.bin5 {
1692 1692 background-color: @alert4;
1693 1693 text-align: center;
1694 1694 }
1695 1695
1696 1696 /*chmod file*/
1697 1697 .bin.bin6 {
1698 1698 background-color: @grey2;
1699 1699 text-align: center;
1700 1700 }
1701 1701 }
1702 1702 }
1703 1703
1704 1704 .cs_files .cs_added, .cs_files .cs_A,
1705 1705 .cs_files .cs_added, .cs_files .cs_M,
1706 1706 .cs_files .cs_added, .cs_files .cs_D {
1707 1707 height: 16px;
1708 1708 padding-right: 10px;
1709 1709 margin-top: 7px;
1710 1710 text-align: left;
1711 1711 }
1712 1712
1713 1713 .cs_icon_td {
1714 1714 min-width: 16px;
1715 1715 width: 16px;
1716 1716 }
1717 1717
1718 1718 .pull-request-merge {
1719 1719 border: 1px solid @grey5;
1720 1720 padding: 10px 0px 20px;
1721 1721 margin-top: 10px;
1722 1722 margin-bottom: 20px;
1723 1723 }
1724 1724
1725 1725 .pull-request-merge ul {
1726 1726 padding: 0px 0px;
1727 1727 }
1728 1728
1729 1729 .pull-request-merge li:before{
1730 1730 content:none;
1731 1731 }
1732 1732
1733 1733 .pull-request-merge .pull-request-wrap {
1734 1734 height: auto;
1735 1735 padding: 0px 0px;
1736 1736 text-align: right;
1737 1737 }
1738 1738
1739 1739 .pull-request-merge span {
1740 1740 margin-right: 5px;
1741 1741 }
1742 1742
1743 1743 .pull-request-merge-actions {
1744 height: 30px;
1744 min-height: 30px;
1745 1745 padding: 0px 0px;
1746 1746 }
1747 1747
1748 .pull-request-merge-info {
1749 padding: 0px 5px 5px 0px;
1750 }
1751
1748 1752 .merge-status {
1749 1753 margin-right: 5px;
1750 1754 }
1751 1755
1752 1756 .merge-message {
1753 1757 font-size: 1.2em
1754 1758 }
1755 1759
1756 1760 .merge-message.success i,
1757 1761 .merge-icon.success i {
1758 1762 color:@alert1;
1759 1763 }
1760 1764
1761 1765 .merge-message.warning i,
1762 1766 .merge-icon.warning i {
1763 1767 color: @alert3;
1764 1768 }
1765 1769
1766 1770 .merge-message.error i,
1767 1771 .merge-icon.error i {
1768 1772 color:@alert2;
1769 1773 }
1770 1774
1771 1775 .pr-versions {
1772 1776 font-size: 1.1em;
1773 1777
1774 1778 table {
1775 1779 padding: 0px 5px;
1776 1780 }
1777 1781
1778 1782 td {
1779 1783 line-height: 15px;
1780 1784 }
1781 1785
1782 1786 .flag_status {
1783 1787 margin: 0;
1784 1788 }
1785 1789
1786 1790 .compare-radio-button {
1787 1791 position: relative;
1788 1792 top: -3px;
1789 1793 }
1790 1794 }
1791 1795
1792 1796
1793 1797 #close_pull_request {
1794 1798 margin-right: 0px;
1795 1799 }
1796 1800
1797 1801 .empty_data {
1798 1802 color: @grey4;
1799 1803 }
1800 1804
1801 1805 #changeset_compare_view_content {
1802 1806 margin-bottom: @space;
1803 1807 clear: both;
1804 1808 width: 100%;
1805 1809 box-sizing: border-box;
1806 1810 .border-radius(@border-radius);
1807 1811
1808 1812 .help-block {
1809 1813 margin: @padding 0;
1810 1814 color: @text-color;
1811 1815 &.pre-formatting {
1812 1816 white-space: pre;
1813 1817 }
1814 1818 }
1815 1819
1816 1820 .empty_data {
1817 1821 margin: @padding 0;
1818 1822 }
1819 1823
1820 1824 .alert {
1821 1825 margin-bottom: @space;
1822 1826 }
1823 1827 }
1824 1828
1825 1829 .table_disp {
1826 1830 .status {
1827 1831 width: auto;
1828 1832
1829 1833 .flag_status {
1830 1834 float: left;
1831 1835 }
1832 1836 }
1833 1837 }
1834 1838
1835 1839
1836 1840 .creation_in_progress {
1837 1841 color: @grey4
1838 1842 }
1839 1843
1840 1844 .status_box_menu {
1841 1845 margin: 0;
1842 1846 }
1843 1847
1844 1848 .notification-table{
1845 1849 margin-bottom: @space;
1846 1850 display: table;
1847 1851 width: 100%;
1848 1852
1849 1853 .container{
1850 1854 display: table-row;
1851 1855
1852 1856 .notification-header{
1853 1857 border-bottom: @border-thickness solid @border-default-color;
1854 1858 }
1855 1859
1856 1860 .notification-subject{
1857 1861 display: table-cell;
1858 1862 }
1859 1863 }
1860 1864 }
1861 1865
1862 1866 // Notifications
1863 1867 .notification-header{
1864 1868 display: table;
1865 1869 width: 100%;
1866 1870 padding: floor(@basefontsize/2) 0;
1867 1871 line-height: 1em;
1868 1872
1869 1873 .desc, .delete-notifications, .read-notifications{
1870 1874 display: table-cell;
1871 1875 text-align: left;
1872 1876 }
1873 1877
1874 1878 .desc{
1875 1879 width: 1163px;
1876 1880 }
1877 1881
1878 1882 .delete-notifications, .read-notifications{
1879 1883 width: 35px;
1880 1884 min-width: 35px; //fixes when only one button is displayed
1881 1885 }
1882 1886 }
1883 1887
1884 1888 .notification-body {
1885 1889 .markdown-block,
1886 1890 .rst-block {
1887 1891 padding: @padding 0;
1888 1892 }
1889 1893
1890 1894 .notification-subject {
1891 1895 padding: @textmargin 0;
1892 1896 border-bottom: @border-thickness solid @border-default-color;
1893 1897 }
1894 1898 }
1895 1899
1896 1900
1897 1901 .notifications_buttons{
1898 1902 float: right;
1899 1903 }
1900 1904
1901 1905 #notification-status{
1902 1906 display: inline;
1903 1907 }
1904 1908
1905 1909 // Repositories
1906 1910
1907 1911 #summary.fields{
1908 1912 display: table;
1909 1913
1910 1914 .field{
1911 1915 display: table-row;
1912 1916
1913 1917 .label-summary{
1914 1918 display: table-cell;
1915 1919 min-width: @label-summary-minwidth;
1916 1920 padding-top: @padding/2;
1917 1921 padding-bottom: @padding/2;
1918 1922 padding-right: @padding/2;
1919 1923 }
1920 1924
1921 1925 .input{
1922 1926 display: table-cell;
1923 1927 padding: @padding/2;
1924 1928
1925 1929 input{
1926 1930 min-width: 29em;
1927 1931 padding: @padding/4;
1928 1932 }
1929 1933 }
1930 1934 .statistics, .downloads{
1931 1935 .disabled{
1932 1936 color: @grey4;
1933 1937 }
1934 1938 }
1935 1939 }
1936 1940 }
1937 1941
1938 1942 #summary{
1939 1943 width: 70%;
1940 1944 }
1941 1945
1942 1946
1943 1947 // Journal
1944 1948 .journal.title {
1945 1949 h5 {
1946 1950 float: left;
1947 1951 margin: 0;
1948 1952 width: 70%;
1949 1953 }
1950 1954
1951 1955 ul {
1952 1956 float: right;
1953 1957 display: inline-block;
1954 1958 margin: 0;
1955 1959 width: 30%;
1956 1960 text-align: right;
1957 1961
1958 1962 li {
1959 1963 display: inline;
1960 1964 font-size: @journal-fontsize;
1961 1965 line-height: 1em;
1962 1966
1963 1967 &:before { content: none; }
1964 1968 }
1965 1969 }
1966 1970 }
1967 1971
1968 1972 .filterexample {
1969 1973 position: absolute;
1970 1974 top: 95px;
1971 1975 left: @contentpadding;
1972 1976 color: @rcblue;
1973 1977 font-size: 11px;
1974 1978 font-family: @text-regular;
1975 1979 cursor: help;
1976 1980
1977 1981 &:hover {
1978 1982 color: @rcdarkblue;
1979 1983 }
1980 1984
1981 1985 @media (max-width:768px) {
1982 1986 position: relative;
1983 1987 top: auto;
1984 1988 left: auto;
1985 1989 display: block;
1986 1990 }
1987 1991 }
1988 1992
1989 1993
1990 1994 #journal{
1991 1995 margin-bottom: @space;
1992 1996
1993 1997 .journal_day{
1994 1998 margin-bottom: @textmargin/2;
1995 1999 padding-bottom: @textmargin/2;
1996 2000 font-size: @journal-fontsize;
1997 2001 border-bottom: @border-thickness solid @border-default-color;
1998 2002 }
1999 2003
2000 2004 .journal_container{
2001 2005 margin-bottom: @space;
2002 2006
2003 2007 .journal_user{
2004 2008 display: inline-block;
2005 2009 }
2006 2010 .journal_action_container{
2007 2011 display: block;
2008 2012 margin-top: @textmargin;
2009 2013
2010 2014 div{
2011 2015 display: inline;
2012 2016 }
2013 2017
2014 2018 div.journal_action_params{
2015 2019 display: block;
2016 2020 }
2017 2021
2018 2022 div.journal_repo:after{
2019 2023 content: "\A";
2020 2024 white-space: pre;
2021 2025 }
2022 2026
2023 2027 div.date{
2024 2028 display: block;
2025 2029 margin-bottom: @textmargin;
2026 2030 }
2027 2031 }
2028 2032 }
2029 2033 }
2030 2034
2031 2035 // Files
2032 2036 .edit-file-title {
2033 2037 border-bottom: @border-thickness solid @border-default-color;
2034 2038
2035 2039 .breadcrumbs {
2036 2040 margin-bottom: 0;
2037 2041 }
2038 2042 }
2039 2043
2040 2044 .edit-file-fieldset {
2041 2045 margin-top: @sidebarpadding;
2042 2046
2043 2047 .fieldset {
2044 2048 .left-label {
2045 2049 width: 13%;
2046 2050 }
2047 2051 .right-content {
2048 2052 width: 87%;
2049 2053 max-width: 100%;
2050 2054 }
2051 2055 .filename-label {
2052 2056 margin-top: 13px;
2053 2057 }
2054 2058 .commit-message-label {
2055 2059 margin-top: 4px;
2056 2060 }
2057 2061 .file-upload-input {
2058 2062 input {
2059 2063 display: none;
2060 2064 }
2061 2065 margin-top: 10px;
2062 2066 }
2063 2067 .file-upload-label {
2064 2068 margin-top: 10px;
2065 2069 }
2066 2070 p {
2067 2071 margin-top: 5px;
2068 2072 }
2069 2073
2070 2074 }
2071 2075 .custom-path-link {
2072 2076 margin-left: 5px;
2073 2077 }
2074 2078 #commit {
2075 2079 resize: vertical;
2076 2080 }
2077 2081 }
2078 2082
2079 2083 .delete-file-preview {
2080 2084 max-height: 250px;
2081 2085 }
2082 2086
2083 2087 .new-file,
2084 2088 #filter_activate,
2085 2089 #filter_deactivate {
2086 2090 float: left;
2087 2091 margin: 0 0 0 15px;
2088 2092 }
2089 2093
2090 2094 h3.files_location{
2091 2095 line-height: 2.4em;
2092 2096 }
2093 2097
2094 2098 .browser-nav {
2095 2099 display: table;
2096 2100 margin-bottom: @space;
2097 2101
2098 2102
2099 2103 .info_box {
2100 2104 display: inline-table;
2101 2105 height: 2.5em;
2102 2106
2103 2107 .browser-cur-rev, .info_box_elem {
2104 2108 display: table-cell;
2105 2109 vertical-align: middle;
2106 2110 }
2107 2111
2108 2112 .info_box_elem {
2109 2113 border-top: @border-thickness solid @rcblue;
2110 2114 border-bottom: @border-thickness solid @rcblue;
2111 2115
2112 2116 #at_rev, a {
2113 2117 padding: 0.6em 0.9em;
2114 2118 margin: 0;
2115 2119 .box-shadow(none);
2116 2120 border: 0;
2117 2121 height: 12px;
2118 2122 }
2119 2123
2120 2124 input#at_rev {
2121 2125 max-width: 50px;
2122 2126 text-align: right;
2123 2127 }
2124 2128
2125 2129 &.previous {
2126 2130 border: @border-thickness solid @rcblue;
2127 2131 .disabled {
2128 2132 color: @grey4;
2129 2133 cursor: not-allowed;
2130 2134 }
2131 2135 }
2132 2136
2133 2137 &.next {
2134 2138 border: @border-thickness solid @rcblue;
2135 2139 .disabled {
2136 2140 color: @grey4;
2137 2141 cursor: not-allowed;
2138 2142 }
2139 2143 }
2140 2144 }
2141 2145
2142 2146 .browser-cur-rev {
2143 2147
2144 2148 span{
2145 2149 margin: 0;
2146 2150 color: @rcblue;
2147 2151 height: 12px;
2148 2152 display: inline-block;
2149 2153 padding: 0.7em 1em ;
2150 2154 border: @border-thickness solid @rcblue;
2151 2155 margin-right: @padding;
2152 2156 }
2153 2157 }
2154 2158 }
2155 2159
2156 2160 .search_activate {
2157 2161 display: table-cell;
2158 2162 vertical-align: middle;
2159 2163
2160 2164 input, label{
2161 2165 margin: 0;
2162 2166 padding: 0;
2163 2167 }
2164 2168
2165 2169 input{
2166 2170 margin-left: @textmargin;
2167 2171 }
2168 2172
2169 2173 }
2170 2174 }
2171 2175
2172 2176 .browser-cur-rev{
2173 2177 margin-bottom: @textmargin;
2174 2178 }
2175 2179
2176 2180 #node_filter_box_loading{
2177 2181 .info_text;
2178 2182 }
2179 2183
2180 2184 .browser-search {
2181 2185 margin: -25px 0px 5px 0px;
2182 2186 }
2183 2187
2184 2188 .node-filter {
2185 2189 font-size: @repo-title-fontsize;
2186 2190 padding: 4px 0px 0px 0px;
2187 2191
2188 2192 .node-filter-path {
2189 2193 float: left;
2190 2194 color: @grey4;
2191 2195 }
2192 2196 .node-filter-input {
2193 2197 float: left;
2194 2198 margin: -2px 0px 0px 2px;
2195 2199 input {
2196 2200 padding: 2px;
2197 2201 border: none;
2198 2202 font-size: @repo-title-fontsize;
2199 2203 }
2200 2204 }
2201 2205 }
2202 2206
2203 2207
2204 2208 .browser-result{
2205 2209 td a{
2206 2210 margin-left: 0.5em;
2207 2211 display: inline-block;
2208 2212
2209 2213 em{
2210 2214 font-family: @text-bold;
2211 2215 }
2212 2216 }
2213 2217 }
2214 2218
2215 2219 .browser-highlight{
2216 2220 background-color: @grey5-alpha;
2217 2221 }
2218 2222
2219 2223
2220 2224 // Search
2221 2225
2222 2226 .search-form{
2223 2227 #q {
2224 2228 width: @search-form-width;
2225 2229 }
2226 2230 .fields{
2227 2231 margin: 0 0 @space;
2228 2232 }
2229 2233
2230 2234 label{
2231 2235 display: inline-block;
2232 2236 margin-right: @textmargin;
2233 2237 padding-top: 0.25em;
2234 2238 }
2235 2239
2236 2240
2237 2241 .results{
2238 2242 clear: both;
2239 2243 margin: 0 0 @padding;
2240 2244 }
2241 2245 }
2242 2246
2243 2247 div.search-feedback-items {
2244 2248 display: inline-block;
2245 2249 padding:0px 0px 0px 96px;
2246 2250 }
2247 2251
2248 2252 div.search-code-body {
2249 2253 background-color: #ffffff; padding: 5px 0 5px 10px;
2250 2254 pre {
2251 2255 .match { background-color: #faffa6;}
2252 2256 .break { display: block; width: 100%; background-color: #DDE7EF; color: #747474; }
2253 2257 }
2254 2258 }
2255 2259
2256 2260 .expand_commit.search {
2257 2261 .show_more.open {
2258 2262 height: auto;
2259 2263 max-height: none;
2260 2264 }
2261 2265 }
2262 2266
2263 2267 .search-results {
2264 2268
2265 2269 h2 {
2266 2270 margin-bottom: 0;
2267 2271 }
2268 2272 .codeblock {
2269 2273 border: none;
2270 2274 background: transparent;
2271 2275 }
2272 2276
2273 2277 .codeblock-header {
2274 2278 border: none;
2275 2279 background: transparent;
2276 2280 }
2277 2281
2278 2282 .code-body {
2279 2283 border: @border-thickness solid @border-default-color;
2280 2284 .border-radius(@border-radius);
2281 2285 }
2282 2286
2283 2287 .td-commit {
2284 2288 &:extend(pre);
2285 2289 border-bottom: @border-thickness solid @border-default-color;
2286 2290 }
2287 2291
2288 2292 .message {
2289 2293 height: auto;
2290 2294 max-width: 350px;
2291 2295 white-space: normal;
2292 2296 text-overflow: initial;
2293 2297 overflow: visible;
2294 2298
2295 2299 .match { background-color: #faffa6;}
2296 2300 .break { background-color: #DDE7EF; width: 100%; color: #747474; display: block; }
2297 2301 }
2298 2302
2299 2303 }
2300 2304
2301 2305 table.rctable td.td-search-results div {
2302 2306 max-width: 100%;
2303 2307 }
2304 2308
2305 2309 #tip-box, .tip-box{
2306 2310 padding: @menupadding/2;
2307 2311 display: block;
2308 2312 border: @border-thickness solid @border-highlight-color;
2309 2313 .border-radius(@border-radius);
2310 2314 background-color: white;
2311 2315 z-index: 99;
2312 2316 white-space: pre-wrap;
2313 2317 }
2314 2318
2315 2319 #linktt {
2316 2320 width: 79px;
2317 2321 }
2318 2322
2319 2323 #help_kb .modal-content{
2320 2324 max-width: 750px;
2321 2325 margin: 10% auto;
2322 2326
2323 2327 table{
2324 2328 td,th{
2325 2329 border-bottom: none;
2326 2330 line-height: 2.5em;
2327 2331 }
2328 2332 th{
2329 2333 padding-bottom: @textmargin/2;
2330 2334 }
2331 2335 td.keys{
2332 2336 text-align: center;
2333 2337 }
2334 2338 }
2335 2339
2336 2340 .block-left{
2337 2341 width: 45%;
2338 2342 margin-right: 5%;
2339 2343 }
2340 2344 .modal-footer{
2341 2345 clear: both;
2342 2346 }
2343 2347 .key.tag{
2344 2348 padding: 0.5em;
2345 2349 background-color: @rcblue;
2346 2350 color: white;
2347 2351 border-color: @rcblue;
2348 2352 .box-shadow(none);
2349 2353 }
2350 2354 }
2351 2355
2352 2356
2353 2357
2354 2358 //--- IMPORTS FOR REFACTORED STYLES ------------------//
2355 2359
2356 2360 @import 'statistics-graph';
2357 2361 @import 'tables';
2358 2362 @import 'forms';
2359 2363 @import 'diff';
2360 2364 @import 'summary';
2361 2365 @import 'navigation';
2362 2366
2363 2367 //--- SHOW/HIDE SECTIONS --//
2364 2368
2365 2369 .btn-collapse {
2366 2370 float: right;
2367 2371 text-align: right;
2368 2372 font-family: @text-light;
2369 2373 font-size: @basefontsize;
2370 2374 cursor: pointer;
2371 2375 border: none;
2372 2376 color: @rcblue;
2373 2377 }
2374 2378
2375 2379 table.rctable,
2376 2380 table.dataTable {
2377 2381 .btn-collapse {
2378 2382 float: right;
2379 2383 text-align: right;
2380 2384 }
2381 2385 }
2382 2386
2383 2387
2384 2388 // TODO: johbo: Fix for IE10, this avoids that we see a border
2385 2389 // and padding around checkboxes and radio boxes. Move to the right place,
2386 2390 // or better: Remove this once we did the form refactoring.
2387 2391 input[type=checkbox],
2388 2392 input[type=radio] {
2389 2393 padding: 0;
2390 2394 border: none;
2391 2395 }
2392 2396
2393 2397 .toggle-ajax-spinner{
2394 2398 height: 16px;
2395 2399 width: 16px;
2396 2400 }
@@ -1,156 +1,157 b''
1 1 @font-face {
2 2 font-family: 'rcicons';
3 3 src: url('../fonts/RCIcons/rcicons.eot?74666722');
4 4 src: url('../fonts/RCIcons/rcicons.eot?74666722#iefix') format('embedded-opentype'),
5 5 url('../fonts/RCIcons/rcicons.woff2?74666722') format('woff2'),
6 6 url('../fonts/RCIcons/rcicons.woff?74666722') format('woff'),
7 7 url('../fonts/RCIcons/rcicons.ttf?74666722') format('truetype'),
8 8 url('../fonts/RCIcons/rcicons.svg?74666722#rcicons') format('svg');
9 9 font-weight: normal;
10 10 font-style: normal;
11 11 }
12 12 /* Chrome hack: SVG is rendered more smooth in Windozze. 100% magic, uncomment if you need it. */
13 13 /* Note, that will break hinting! In other OS-es font will be not as sharp as it could be */
14 14 /*
15 15 @media screen and (-webkit-min-device-pixel-ratio:0) {
16 16 @font-face {
17 17 font-family: 'rcicons';
18 18 src: url('../fonts/RCIcons/rcicons.svg?74666722#rcicons') format('svg');
19 19 }
20 20 }
21 21 */
22 22
23 23 [class^="icon-"]:before, [class*=" icon-"]:before {
24 24 font-family: "rcicons";
25 25 font-style: normal;
26 26 font-weight: normal;
27 27 speak: none;
28 28
29 29 display: inline-block;
30 30 text-decoration: inherit;
31 31 width: 1em;
32 32 margin-right: .2em;
33 33 text-align: center;
34 34 /* opacity: .8; */
35 35
36 36 /* For safety - reset parent styles, that can break glyph codes*/
37 37 font-variant: normal;
38 38 text-transform: none;
39 39
40 40 /* fix buttons height, for twitter bootstrap */
41 41 line-height: 1em;
42 42
43 43 /* Animation center compensation - margins should be symmetric */
44 44 /* remove if not needed */
45 45 margin-left: .2em;
46 46
47 47 /* you can be more comfortable with increased icons size */
48 48 /* font-size: 120%; */
49 49
50 50 /* Font smoothing. That was taken from TWBS */
51 51 -webkit-font-smoothing: antialiased;
52 52 -moz-osx-font-smoothing: grayscale;
53 53
54 54 /* Uncomment for 3D effect */
55 55 /* text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3); */
56 56 }
57 57
58 58 // -- ICON CLASSES -- //
59 59
60 60 .icon-bookmark:before { content: '\e803'; } /* 'ξ ƒ' */
61 61 .icon-branch:before { content: '\e804'; } /* 'ξ „' */
62 62 .icon-lock:before { content: '\e806'; } /* 'ξ †' */
63 63 .icon-unlock:before { content: '\e807'; } /* 'ξ ‡' */
64 64 .icon-delete:before { content: '\e808'; } /* '' */
65 .icon-false:before { content: '\e808'; } /* '' */
65 66
66 67 .icon-ok:before { content: '\e809'; } /* 'ξ ‰' */
67 68 .icon-true:before { content: '\e809'; } /* 'ξ ‰' */
68 69
69 70 .icon-comment:before { content: '\e80a'; } /* '' */
70 71 .icon-comment-add:before { content: '\e816'; } /* 'ξ –' */
71 72 .icon-comment_toggle:before { content: '\e818'; } /* '' */
72 73
73 74 .icon-feed:before { content: '\e80b'; } /* 'ξ ‹' */
74 75
75 76 .icon-right:before { content: '\e80c'; } /* '' */
76 77 .icon-left:before { content: '\e80d'; } /* '' */
77 78
78 79 .icon-arrow_down:before { content: '\e80e'; } /* '' */
79 80 .icon-arrow_up:before { content: '\e80e'; } /* '' */
80 81
81 82 .icon-group:before { content: '\e812'; } /* 'ξ ’' */
82 83
83 84 .icon-fork:before { content: '\e814'; } /* 'ξ ”' */
84 85 .icon-merge:before { content: '\e814'; } /* 'ξ ”' */
85 86
86 87 .icon-more:before { content: '\e815'; } /* 'ξ •' */
87 88
88 89 .icon-git-inv:before { content: '\e80f'; } /* '' */
89 90 .icon-hg-inv:before { content: '\e810'; } /* '' */
90 91 .icon-svn-inv:before { content: '\e811'; } /* 'ξ ‘' */
91 92
92 93 .icon-git:before { content: '\e81a'; } /* '' */
93 94 .icon-hg:before { content: '\e81b'; } /* 'ξ ›' */
94 95 .icon-svn:before { content: '\e820'; } /* 'ξ  ' */
95 96
96 97 .icon-minus:before { content: '\e81c'; } /* '' */
97 98 .icon-plus:before { content: '\e81d'; } /* '' */
98 99 .icon-remove:before { content: '\e81e'; } /* '' */
99 100 .icon-remove-sign:before { content: '\e81e'; } /* '' */
100 101
101 102 .icon-rhodecode:before { content: '\e81f'; } /* '' */
102 103
103 104 .icon-tag:before { content: '\e821'; } /* 'ξ ‘' */
104 105 .icon-copy:before { content: '\f0c5'; } /* 'οƒ…' */
105 106 .icon-clipboard:before { content: '\f0c5'; } /* 'οƒ…' */
106 107
107 108
108 109 .icon-folder:before { content: '\e813'; } /* 'ξ “' */
109 110 .icon-folder-close:before { content: '\e813'; } /* 'ξ “' */
110 111
111 112 .icon-directory:before { content: '\e800'; } /* 'ξ €' */
112 113 .icon-directory-empty:before { content: '\f114'; } /* 'ο„”' */
113 114 .icon-file-text:before { content: '\f0f6'; } /* 'οƒΆ' */
114 115 .icon-file-text-inv:before { content: '\f15c'; } /* 'ο…œ' */
115 116 .icon-file-code:before { content: '\f1c9'; } /* '' */
116 117
117 118 // MERGED ICONS
118 119
119 120 .icon-repo-private:before { &:extend(.icon-lock:before); }
120 121 .icon-repo-lock:before { &:extend(.icon-lock:before); }
121 122 .icon-unlock-alt:before { &:extend(.icon-unlock:before); }
122 123 .icon-repo-unlock:before { &:extend(.icon-unlock:before); }
123 124 .icon-repo-public:before { &:extend(.icon-unlock:before); }
124 125 .icon-rss-sign:before { &:extend(.icon-feed:before); }
125 126 .icon-code-fork:before { &:extend(.icon-fork:before); }
126 127
127 128 // TRANSFORM
128 129 .icon-arrow_up:before {transform: rotate(180deg);}
129 130 .icon-merge:before {transform: rotate(180deg);}
130 131
131 132 // -- END ICON CLASSES -- //
132 133
133 134
134 135 //--- ICONS STYLING ------------------//
135 136
136 137 .icon-git { color: @color4 !important; }
137 138 .icon-hg { color: @color8 !important; }
138 139 .icon-svn { color: @color1 !important; }
139 140 .icon-git-inv { color: @color4 !important; }
140 141 .icon-hg-inv { color: @color8 !important; }
141 142 .icon-svn-inv { color: @color1 !important; }
142 143 .icon-repo-lock { color: #FF0000; }
143 144 .icon-repo-unlock { color: #FF0000; }
144 145
145 146 .repo-switcher-dropdown .select2-result-label {
146 147 .icon-git:before {
147 148 &:extend(.icon-git-transparent:before);
148 149 }
149 150 .icon-hg:before {
150 151 &:extend(.icon-hg-transparent:before);
151 152 color: @alert4;
152 153 }
153 154 .icon-svn:before {
154 155 &:extend(.icon-svn-transparent:before);
155 156 }
156 157 }
@@ -1,63 +1,78 b''
1 1
2 2 <div class="pull-request-wrap">
3 3
4 4 % if c.pr_merge_possible:
5 5 <h2 class="merge-status">
6 6 <span class="merge-icon success"><i class="icon-ok"></i></span>
7 7 ${_('This pull request can be merged automatically.')}
8 8 </h2>
9 9 % else:
10 10 <h2 class="merge-status">
11 11 <span class="merge-icon warning"><i class="icon-false"></i></span>
12 12 ${_('Merge is not currently possible because of below failed checks.')}
13 13 </h2>
14 14 % endif
15 15
16 % if c.pr_merge_errors.items():
16 17 <ul>
17 18 % for pr_check_key, pr_check_details in c.pr_merge_errors.items():
18 19 <% pr_check_type = pr_check_details['error_type'] %>
19 20 <li>
20 21 <span class="merge-message ${pr_check_type}" data-role="merge-message">
21 22 - ${pr_check_details['message']}
22 23 % if pr_check_key == 'todo':
23 24 % for co in pr_check_details['details']:
24 25 <a class="permalink" href="#comment-${co.comment_id}" onclick="Rhodecode.comments.scrollToComment($('#comment-${co.comment_id}'), 0, ${h.json.dumps(co.outdated)})"> #${co.comment_id}</a>${'' if loop.last else ','}
25 26 % endfor
26 27 % endif
27 28 </span>
28 29 </li>
29 30 % endfor
30 31 </ul>
32 % endif
31 33
32 34 <div class="pull-request-merge-actions">
33 35 % if c.allowed_to_merge:
34 <div class="pull-right">
36 ## Merge info, show only if all errors are taken care of
37 % if not c.pr_merge_errors and c.pr_merge_info:
38 <div class="pull-request-merge-info">
39 <ul>
40 % for pr_merge_key, pr_merge_details in c.pr_merge_info.items():
41 <li>
42 - ${pr_merge_details['message']}
43 </li>
44 % endfor
45 </ul>
46 </div>
47 % endif
48
49 <div>
35 50 ${h.secure_form(h.route_path('pullrequest_merge', repo_name=c.repo_name, pull_request_id=c.pull_request.pull_request_id), id='merge_pull_request_form', method='POST', request=request)}
36 51 <% merge_disabled = ' disabled' if c.pr_merge_possible is False else '' %>
37 52 <a class="btn" href="#" onclick="refreshMergeChecks(); return false;">${_('refresh checks')}</a>
38 53 <input type="submit" id="merge_pull_request" value="${_('Merge Pull Request')}" class="btn${merge_disabled}"${merge_disabled}>
39 54 ${h.end_form()}
40 55 </div>
41 56 % elif c.rhodecode_user.username != h.DEFAULT_USER:
42 57 <a class="btn" href="#" onclick="refreshMergeChecks(); return false;">${_('refresh checks')}</a>
43 58 <input type="submit" value="${_('Merge Pull Request')}" class="btn disabled" disabled="disabled" title="${_('You are not allowed to merge this pull request.')}">
44 59 % else:
45 60 <input type="submit" value="${_('Login to Merge this Pull Request')}" class="btn disabled" disabled="disabled">
46 61 % endif
47 62 </div>
48 63
49 64 % if c.allowed_to_close:
50 65 ## close PR action, injected later next to COMMENT button
51 66 <div id="close-pull-request-action" style="display: none">
52 67 % if c.pull_request_review_status == c.REVIEW_STATUS_APPROVED:
53 68 <a class="btn btn-approved-status" href="#close-as-approved" onclick="closePullRequest('${c.REVIEW_STATUS_APPROVED}'); return false;">
54 69 ${_('Close with status {}').format(h.commit_status_lbl(c.REVIEW_STATUS_APPROVED))}
55 70 </a>
56 71 % else:
57 72 <a class="btn btn-rejected-status" href="#close-as-rejected" onclick="closePullRequest('${c.REVIEW_STATUS_REJECTED}'); return false;">
58 73 ${_('Close with status {}').format(h.commit_status_lbl(c.REVIEW_STATUS_REJECTED))}
59 74 </a>
60 75 % endif
61 76 </div>
62 77 % endif
63 78 </div>
General Comments 0
You need to be logged in to leave comments. Login now