##// END OF EJS Templates
pull-requests: added retry mechanism for updating pull requests.
super-admin -
r4696:7a5e2fc4 stable
parent child Browse files
Show More

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

@@ -1,1868 +1,1872 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2020 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 formencode.htmlfill
26 26 import peppercorn
27 27 from pyramid.httpexceptions import (
28 28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest, HTTPConflict)
29 29
30 30 from pyramid.renderers import render
31 31
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.diffs import load_cached_diff, cache_diff, diff_cache_exist
37 37 from rhodecode.lib.exceptions import CommentVersionMismatch
38 38 from rhodecode.lib.ext_json import json
39 39 from rhodecode.lib.auth import (
40 40 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
41 41 NotAnonymous, CSRFRequired)
42 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode, safe_int, aslist
42 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode, safe_int, aslist, retry
43 43 from rhodecode.lib.vcs.backends.base import (
44 44 EmptyCommit, UpdateFailureReason, unicode_to_reference)
45 45 from rhodecode.lib.vcs.exceptions import (
46 46 CommitDoesNotExistError, RepositoryRequirementError, EmptyRepositoryError)
47 47 from rhodecode.model.changeset_status import ChangesetStatusModel
48 48 from rhodecode.model.comment import CommentsModel
49 49 from rhodecode.model.db import (
50 50 func, false, or_, PullRequest, ChangesetComment, ChangesetStatus, Repository,
51 51 PullRequestReviewers)
52 52 from rhodecode.model.forms import PullRequestForm
53 53 from rhodecode.model.meta import Session
54 54 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
55 55 from rhodecode.model.scm import ScmModel
56 56
57 57 log = logging.getLogger(__name__)
58 58
59 59
60 60 class RepoPullRequestsView(RepoAppView, DataGridAppView):
61 61
62 62 def load_default_context(self):
63 63 c = self._get_local_tmpl_context(include_app_defaults=True)
64 64 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
65 65 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
66 66 # backward compat., we use for OLD PRs a plain renderer
67 67 c.renderer = 'plain'
68 68 return c
69 69
70 70 def _get_pull_requests_list(
71 71 self, repo_name, source, filter_type, opened_by, statuses):
72 72
73 73 draw, start, limit = self._extract_chunk(self.request)
74 74 search_q, order_by, order_dir = self._extract_ordering(self.request)
75 75 _render = self.request.get_partial_renderer(
76 76 'rhodecode:templates/data_table/_dt_elements.mako')
77 77
78 78 # pagination
79 79
80 80 if filter_type == 'awaiting_review':
81 81 pull_requests = PullRequestModel().get_awaiting_review(
82 82 repo_name,
83 83 search_q=search_q, statuses=statuses,
84 84 offset=start, length=limit, order_by=order_by, order_dir=order_dir)
85 85 pull_requests_total_count = PullRequestModel().count_awaiting_review(
86 86 repo_name,
87 87 search_q=search_q, statuses=statuses)
88 88 elif filter_type == 'awaiting_my_review':
89 89 pull_requests = PullRequestModel().get_awaiting_my_review(
90 90 repo_name, self._rhodecode_user.user_id,
91 91 search_q=search_q, statuses=statuses,
92 92 offset=start, length=limit, order_by=order_by, order_dir=order_dir)
93 93 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
94 94 repo_name, self._rhodecode_user.user_id,
95 95 search_q=search_q, statuses=statuses)
96 96 else:
97 97 pull_requests = PullRequestModel().get_all(
98 98 repo_name, search_q=search_q, source=source, opened_by=opened_by,
99 99 statuses=statuses, offset=start, length=limit,
100 100 order_by=order_by, order_dir=order_dir)
101 101 pull_requests_total_count = PullRequestModel().count_all(
102 102 repo_name, search_q=search_q, source=source, statuses=statuses,
103 103 opened_by=opened_by)
104 104
105 105 data = []
106 106 comments_model = CommentsModel()
107 107 for pr in pull_requests:
108 108 comments_count = comments_model.get_all_comments(
109 109 self.db_repo.repo_id, pull_request=pr,
110 110 include_drafts=False, count_only=True)
111 111
112 112 review_statuses = pr.reviewers_statuses(user=self._rhodecode_db_user)
113 113 my_review_status = ChangesetStatus.STATUS_NOT_REVIEWED
114 114 if review_statuses and review_statuses[4]:
115 115 _review_obj, _user, _reasons, _mandatory, statuses = review_statuses
116 116 my_review_status = statuses[0][1].status
117 117
118 118 data.append({
119 119 'name': _render('pullrequest_name',
120 120 pr.pull_request_id, pr.pull_request_state,
121 121 pr.work_in_progress, pr.target_repo.repo_name,
122 122 short=True),
123 123 'name_raw': pr.pull_request_id,
124 124 'status': _render('pullrequest_status',
125 125 pr.calculated_review_status()),
126 126 'my_status': _render('pullrequest_status',
127 127 my_review_status),
128 128 'title': _render('pullrequest_title', pr.title, pr.description),
129 129 'description': h.escape(pr.description),
130 130 'updated_on': _render('pullrequest_updated_on',
131 131 h.datetime_to_time(pr.updated_on),
132 132 pr.versions_count),
133 133 'updated_on_raw': h.datetime_to_time(pr.updated_on),
134 134 'created_on': _render('pullrequest_updated_on',
135 135 h.datetime_to_time(pr.created_on)),
136 136 'created_on_raw': h.datetime_to_time(pr.created_on),
137 137 'state': pr.pull_request_state,
138 138 'author': _render('pullrequest_author',
139 139 pr.author.full_contact, ),
140 140 'author_raw': pr.author.full_name,
141 141 'comments': _render('pullrequest_comments', comments_count),
142 142 'comments_raw': comments_count,
143 143 'closed': pr.is_closed(),
144 144 })
145 145
146 146 data = ({
147 147 'draw': draw,
148 148 'data': data,
149 149 'recordsTotal': pull_requests_total_count,
150 150 'recordsFiltered': pull_requests_total_count,
151 151 })
152 152 return data
153 153
154 154 @LoginRequired()
155 155 @HasRepoPermissionAnyDecorator(
156 156 'repository.read', 'repository.write', 'repository.admin')
157 157 def pull_request_list(self):
158 158 c = self.load_default_context()
159 159
160 160 req_get = self.request.GET
161 161 c.source = str2bool(req_get.get('source'))
162 162 c.closed = str2bool(req_get.get('closed'))
163 163 c.my = str2bool(req_get.get('my'))
164 164 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
165 165 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
166 166
167 167 c.active = 'open'
168 168 if c.my:
169 169 c.active = 'my'
170 170 if c.closed:
171 171 c.active = 'closed'
172 172 if c.awaiting_review and not c.source:
173 173 c.active = 'awaiting'
174 174 if c.source and not c.awaiting_review:
175 175 c.active = 'source'
176 176 if c.awaiting_my_review:
177 177 c.active = 'awaiting_my'
178 178
179 179 return self._get_template_context(c)
180 180
181 181 @LoginRequired()
182 182 @HasRepoPermissionAnyDecorator(
183 183 'repository.read', 'repository.write', 'repository.admin')
184 184 def pull_request_list_data(self):
185 185 self.load_default_context()
186 186
187 187 # additional filters
188 188 req_get = self.request.GET
189 189 source = str2bool(req_get.get('source'))
190 190 closed = str2bool(req_get.get('closed'))
191 191 my = str2bool(req_get.get('my'))
192 192 awaiting_review = str2bool(req_get.get('awaiting_review'))
193 193 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
194 194
195 195 filter_type = 'awaiting_review' if awaiting_review \
196 196 else 'awaiting_my_review' if awaiting_my_review \
197 197 else None
198 198
199 199 opened_by = None
200 200 if my:
201 201 opened_by = [self._rhodecode_user.user_id]
202 202
203 203 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
204 204 if closed:
205 205 statuses = [PullRequest.STATUS_CLOSED]
206 206
207 207 data = self._get_pull_requests_list(
208 208 repo_name=self.db_repo_name, source=source,
209 209 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
210 210
211 211 return data
212 212
213 213 def _is_diff_cache_enabled(self, target_repo):
214 214 caching_enabled = self._get_general_setting(
215 215 target_repo, 'rhodecode_diff_cache')
216 216 log.debug('Diff caching enabled: %s', caching_enabled)
217 217 return caching_enabled
218 218
219 219 def _get_diffset(self, source_repo_name, source_repo,
220 220 ancestor_commit,
221 221 source_ref_id, target_ref_id,
222 222 target_commit, source_commit, diff_limit, file_limit,
223 223 fulldiff, hide_whitespace_changes, diff_context, use_ancestor=True):
224 224
225 225 target_commit_final = target_commit
226 226 source_commit_final = source_commit
227 227
228 228 if use_ancestor:
229 229 # we might want to not use it for versions
230 230 target_ref_id = ancestor_commit.raw_id
231 231 target_commit_final = ancestor_commit
232 232
233 233 vcs_diff = PullRequestModel().get_diff(
234 234 source_repo, source_ref_id, target_ref_id,
235 235 hide_whitespace_changes, diff_context)
236 236
237 237 diff_processor = diffs.DiffProcessor(
238 238 vcs_diff, format='newdiff', diff_limit=diff_limit,
239 239 file_limit=file_limit, show_full_diff=fulldiff)
240 240
241 241 _parsed = diff_processor.prepare()
242 242
243 243 diffset = codeblocks.DiffSet(
244 244 repo_name=self.db_repo_name,
245 245 source_repo_name=source_repo_name,
246 246 source_node_getter=codeblocks.diffset_node_getter(target_commit_final),
247 247 target_node_getter=codeblocks.diffset_node_getter(source_commit_final),
248 248 )
249 249 diffset = self.path_filter.render_patchset_filtered(
250 250 diffset, _parsed, target_ref_id, source_ref_id)
251 251
252 252 return diffset
253 253
254 254 def _get_range_diffset(self, source_scm, source_repo,
255 255 commit1, commit2, diff_limit, file_limit,
256 256 fulldiff, hide_whitespace_changes, diff_context):
257 257 vcs_diff = source_scm.get_diff(
258 258 commit1, commit2,
259 259 ignore_whitespace=hide_whitespace_changes,
260 260 context=diff_context)
261 261
262 262 diff_processor = diffs.DiffProcessor(
263 263 vcs_diff, format='newdiff', diff_limit=diff_limit,
264 264 file_limit=file_limit, show_full_diff=fulldiff)
265 265
266 266 _parsed = diff_processor.prepare()
267 267
268 268 diffset = codeblocks.DiffSet(
269 269 repo_name=source_repo.repo_name,
270 270 source_node_getter=codeblocks.diffset_node_getter(commit1),
271 271 target_node_getter=codeblocks.diffset_node_getter(commit2))
272 272
273 273 diffset = self.path_filter.render_patchset_filtered(
274 274 diffset, _parsed, commit1.raw_id, commit2.raw_id)
275 275
276 276 return diffset
277 277
278 278 def register_comments_vars(self, c, pull_request, versions, include_drafts=True):
279 279 comments_model = CommentsModel()
280 280
281 281 # GENERAL COMMENTS with versions #
282 282 q = comments_model._all_general_comments_of_pull_request(pull_request)
283 283 q = q.order_by(ChangesetComment.comment_id.asc())
284 284 if not include_drafts:
285 285 q = q.filter(ChangesetComment.draft == false())
286 286 general_comments = q
287 287
288 288 # pick comments we want to render at current version
289 289 c.comment_versions = comments_model.aggregate_comments(
290 290 general_comments, versions, c.at_version_num)
291 291
292 292 # INLINE COMMENTS with versions #
293 293 q = comments_model._all_inline_comments_of_pull_request(pull_request)
294 294 q = q.order_by(ChangesetComment.comment_id.asc())
295 295 if not include_drafts:
296 296 q = q.filter(ChangesetComment.draft == false())
297 297 inline_comments = q
298 298
299 299 c.inline_versions = comments_model.aggregate_comments(
300 300 inline_comments, versions, c.at_version_num, inline=True)
301 301
302 302 # Comments inline+general
303 303 if c.at_version:
304 304 c.inline_comments_flat = c.inline_versions[c.at_version_num]['display']
305 305 c.comments = c.comment_versions[c.at_version_num]['display']
306 306 else:
307 307 c.inline_comments_flat = c.inline_versions[c.at_version_num]['until']
308 308 c.comments = c.comment_versions[c.at_version_num]['until']
309 309
310 310 return general_comments, inline_comments
311 311
312 312 @LoginRequired()
313 313 @HasRepoPermissionAnyDecorator(
314 314 'repository.read', 'repository.write', 'repository.admin')
315 315 def pull_request_show(self):
316 316 _ = self.request.translate
317 317 c = self.load_default_context()
318 318
319 319 pull_request = PullRequest.get_or_404(
320 320 self.request.matchdict['pull_request_id'])
321 321 pull_request_id = pull_request.pull_request_id
322 322
323 323 c.state_progressing = pull_request.is_state_changing()
324 324 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
325 325
326 326 _new_state = {
327 327 'created': PullRequest.STATE_CREATED,
328 328 }.get(self.request.GET.get('force_state'))
329 329
330 330 if c.is_super_admin and _new_state:
331 331 with pull_request.set_state(PullRequest.STATE_UPDATING, final_state=_new_state):
332 332 h.flash(
333 333 _('Pull Request state was force changed to `{}`').format(_new_state),
334 334 category='success')
335 335 Session().commit()
336 336
337 337 raise HTTPFound(h.route_path(
338 338 'pullrequest_show', repo_name=self.db_repo_name,
339 339 pull_request_id=pull_request_id))
340 340
341 341 version = self.request.GET.get('version')
342 342 from_version = self.request.GET.get('from_version') or version
343 343 merge_checks = self.request.GET.get('merge_checks')
344 344 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
345 345 force_refresh = str2bool(self.request.GET.get('force_refresh'))
346 346 c.range_diff_on = self.request.GET.get('range-diff') == "1"
347 347
348 348 # fetch global flags of ignore ws or context lines
349 349 diff_context = diffs.get_diff_context(self.request)
350 350 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
351 351
352 352 (pull_request_latest,
353 353 pull_request_at_ver,
354 354 pull_request_display_obj,
355 355 at_version) = PullRequestModel().get_pr_version(
356 356 pull_request_id, version=version)
357 357
358 358 pr_closed = pull_request_latest.is_closed()
359 359
360 360 if pr_closed and (version or from_version):
361 361 # not allow to browse versions for closed PR
362 362 raise HTTPFound(h.route_path(
363 363 'pullrequest_show', repo_name=self.db_repo_name,
364 364 pull_request_id=pull_request_id))
365 365
366 366 versions = pull_request_display_obj.versions()
367 367
368 368 c.commit_versions = PullRequestModel().pr_commits_versions(versions)
369 369
370 370 # used to store per-commit range diffs
371 371 c.changes = collections.OrderedDict()
372 372
373 373 c.at_version = at_version
374 374 c.at_version_num = (at_version
375 375 if at_version and at_version != PullRequest.LATEST_VER
376 376 else None)
377 377
378 378 c.at_version_index = ChangesetComment.get_index_from_version(
379 379 c.at_version_num, versions)
380 380
381 381 (prev_pull_request_latest,
382 382 prev_pull_request_at_ver,
383 383 prev_pull_request_display_obj,
384 384 prev_at_version) = PullRequestModel().get_pr_version(
385 385 pull_request_id, version=from_version)
386 386
387 387 c.from_version = prev_at_version
388 388 c.from_version_num = (prev_at_version
389 389 if prev_at_version and prev_at_version != PullRequest.LATEST_VER
390 390 else None)
391 391 c.from_version_index = ChangesetComment.get_index_from_version(
392 392 c.from_version_num, versions)
393 393
394 394 # define if we're in COMPARE mode or VIEW at version mode
395 395 compare = at_version != prev_at_version
396 396
397 397 # pull_requests repo_name we opened it against
398 398 # ie. target_repo must match
399 399 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
400 400 log.warning('Mismatch between the current repo: %s, and target %s',
401 401 self.db_repo_name, pull_request_at_ver.target_repo.repo_name)
402 402 raise HTTPNotFound()
403 403
404 404 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(pull_request_at_ver)
405 405
406 406 c.pull_request = pull_request_display_obj
407 407 c.renderer = pull_request_at_ver.description_renderer or c.renderer
408 408 c.pull_request_latest = pull_request_latest
409 409
410 410 # inject latest version
411 411 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
412 412 c.versions = versions + [latest_ver]
413 413
414 414 if compare or (at_version and not at_version == PullRequest.LATEST_VER):
415 415 c.allowed_to_change_status = False
416 416 c.allowed_to_update = False
417 417 c.allowed_to_merge = False
418 418 c.allowed_to_delete = False
419 419 c.allowed_to_comment = False
420 420 c.allowed_to_close = False
421 421 else:
422 422 can_change_status = PullRequestModel().check_user_change_status(
423 423 pull_request_at_ver, self._rhodecode_user)
424 424 c.allowed_to_change_status = can_change_status and not pr_closed
425 425
426 426 c.allowed_to_update = PullRequestModel().check_user_update(
427 427 pull_request_latest, self._rhodecode_user) and not pr_closed
428 428 c.allowed_to_merge = PullRequestModel().check_user_merge(
429 429 pull_request_latest, self._rhodecode_user) and not pr_closed
430 430 c.allowed_to_delete = PullRequestModel().check_user_delete(
431 431 pull_request_latest, self._rhodecode_user) and not pr_closed
432 432 c.allowed_to_comment = not pr_closed
433 433 c.allowed_to_close = c.allowed_to_merge and not pr_closed
434 434
435 435 c.forbid_adding_reviewers = False
436 436
437 437 if pull_request_latest.reviewer_data and \
438 438 'rules' in pull_request_latest.reviewer_data:
439 439 rules = pull_request_latest.reviewer_data['rules'] or {}
440 440 try:
441 441 c.forbid_adding_reviewers = rules.get('forbid_adding_reviewers')
442 442 except Exception:
443 443 pass
444 444
445 445 # check merge capabilities
446 446 _merge_check = MergeCheck.validate(
447 447 pull_request_latest, auth_user=self._rhodecode_user,
448 448 translator=self.request.translate,
449 449 force_shadow_repo_refresh=force_refresh)
450 450
451 451 c.pr_merge_errors = _merge_check.error_details
452 452 c.pr_merge_possible = not _merge_check.failed
453 453 c.pr_merge_message = _merge_check.merge_msg
454 454 c.pr_merge_source_commit = _merge_check.source_commit
455 455 c.pr_merge_target_commit = _merge_check.target_commit
456 456
457 457 c.pr_merge_info = MergeCheck.get_merge_conditions(
458 458 pull_request_latest, translator=self.request.translate)
459 459
460 460 c.pull_request_review_status = _merge_check.review_status
461 461 if merge_checks:
462 462 self.request.override_renderer = \
463 463 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
464 464 return self._get_template_context(c)
465 465
466 466 c.reviewers_count = pull_request.reviewers_count
467 467 c.observers_count = pull_request.observers_count
468 468
469 469 # reviewers and statuses
470 470 c.pull_request_default_reviewers_data_json = json.dumps(pull_request.reviewer_data)
471 471 c.pull_request_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
472 472 c.pull_request_set_observers_data_json = collections.OrderedDict({'observers': []})
473 473
474 474 for review_obj, member, reasons, mandatory, status in pull_request_at_ver.reviewers_statuses():
475 475 member_reviewer = h.reviewer_as_json(
476 476 member, reasons=reasons, mandatory=mandatory,
477 477 role=review_obj.role,
478 478 user_group=review_obj.rule_user_group_data()
479 479 )
480 480
481 481 current_review_status = status[0][1].status if status else ChangesetStatus.STATUS_NOT_REVIEWED
482 482 member_reviewer['review_status'] = current_review_status
483 483 member_reviewer['review_status_label'] = h.commit_status_lbl(current_review_status)
484 484 member_reviewer['allowed_to_update'] = c.allowed_to_update
485 485 c.pull_request_set_reviewers_data_json['reviewers'].append(member_reviewer)
486 486
487 487 c.pull_request_set_reviewers_data_json = json.dumps(c.pull_request_set_reviewers_data_json)
488 488
489 489 for observer_obj, member in pull_request_at_ver.observers():
490 490 member_observer = h.reviewer_as_json(
491 491 member, reasons=[], mandatory=False,
492 492 role=observer_obj.role,
493 493 user_group=observer_obj.rule_user_group_data()
494 494 )
495 495 member_observer['allowed_to_update'] = c.allowed_to_update
496 496 c.pull_request_set_observers_data_json['observers'].append(member_observer)
497 497
498 498 c.pull_request_set_observers_data_json = json.dumps(c.pull_request_set_observers_data_json)
499 499
500 500 general_comments, inline_comments = \
501 501 self.register_comments_vars(c, pull_request_latest, versions)
502 502
503 503 # TODOs
504 504 c.unresolved_comments = CommentsModel() \
505 505 .get_pull_request_unresolved_todos(pull_request_latest)
506 506 c.resolved_comments = CommentsModel() \
507 507 .get_pull_request_resolved_todos(pull_request_latest)
508 508
509 509 # Drafts
510 510 c.draft_comments = CommentsModel().get_pull_request_drafts(
511 511 self._rhodecode_db_user.user_id,
512 512 pull_request_latest)
513 513
514 514 # if we use version, then do not show later comments
515 515 # than current version
516 516 display_inline_comments = collections.defaultdict(
517 517 lambda: collections.defaultdict(list))
518 518 for co in inline_comments:
519 519 if c.at_version_num:
520 520 # pick comments that are at least UPTO given version, so we
521 521 # don't render comments for higher version
522 522 should_render = co.pull_request_version_id and \
523 523 co.pull_request_version_id <= c.at_version_num
524 524 else:
525 525 # showing all, for 'latest'
526 526 should_render = True
527 527
528 528 if should_render:
529 529 display_inline_comments[co.f_path][co.line_no].append(co)
530 530
531 531 # load diff data into template context, if we use compare mode then
532 532 # diff is calculated based on changes between versions of PR
533 533
534 534 source_repo = pull_request_at_ver.source_repo
535 535 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
536 536
537 537 target_repo = pull_request_at_ver.target_repo
538 538 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
539 539
540 540 if compare:
541 541 # in compare switch the diff base to latest commit from prev version
542 542 target_ref_id = prev_pull_request_display_obj.revisions[0]
543 543
544 544 # despite opening commits for bookmarks/branches/tags, we always
545 545 # convert this to rev to prevent changes after bookmark or branch change
546 546 c.source_ref_type = 'rev'
547 547 c.source_ref = source_ref_id
548 548
549 549 c.target_ref_type = 'rev'
550 550 c.target_ref = target_ref_id
551 551
552 552 c.source_repo = source_repo
553 553 c.target_repo = target_repo
554 554
555 555 c.commit_ranges = []
556 556 source_commit = EmptyCommit()
557 557 target_commit = EmptyCommit()
558 558 c.missing_requirements = False
559 559
560 560 source_scm = source_repo.scm_instance()
561 561 target_scm = target_repo.scm_instance()
562 562
563 563 shadow_scm = None
564 564 try:
565 565 shadow_scm = pull_request_latest.get_shadow_repo()
566 566 except Exception:
567 567 log.debug('Failed to get shadow repo', exc_info=True)
568 568 # try first the existing source_repo, and then shadow
569 569 # repo if we can obtain one
570 570 commits_source_repo = source_scm
571 571 if shadow_scm:
572 572 commits_source_repo = shadow_scm
573 573
574 574 c.commits_source_repo = commits_source_repo
575 575 c.ancestor = None # set it to None, to hide it from PR view
576 576
577 577 # empty version means latest, so we keep this to prevent
578 578 # double caching
579 579 version_normalized = version or PullRequest.LATEST_VER
580 580 from_version_normalized = from_version or PullRequest.LATEST_VER
581 581
582 582 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(target_repo)
583 583 cache_file_path = diff_cache_exist(
584 584 cache_path, 'pull_request', pull_request_id, version_normalized,
585 585 from_version_normalized, source_ref_id, target_ref_id,
586 586 hide_whitespace_changes, diff_context, c.fulldiff)
587 587
588 588 caching_enabled = self._is_diff_cache_enabled(c.target_repo)
589 589 force_recache = self.get_recache_flag()
590 590
591 591 cached_diff = None
592 592 if caching_enabled:
593 593 cached_diff = load_cached_diff(cache_file_path)
594 594
595 595 has_proper_commit_cache = (
596 596 cached_diff and cached_diff.get('commits')
597 597 and len(cached_diff.get('commits', [])) == 5
598 598 and cached_diff.get('commits')[0]
599 599 and cached_diff.get('commits')[3])
600 600
601 601 if not force_recache and not c.range_diff_on and has_proper_commit_cache:
602 602 diff_commit_cache = \
603 603 (ancestor_commit, commit_cache, missing_requirements,
604 604 source_commit, target_commit) = cached_diff['commits']
605 605 else:
606 606 # NOTE(marcink): we reach potentially unreachable errors when a PR has
607 607 # merge errors resulting in potentially hidden commits in the shadow repo.
608 608 maybe_unreachable = _merge_check.MERGE_CHECK in _merge_check.error_details \
609 609 and _merge_check.merge_response
610 610 maybe_unreachable = maybe_unreachable \
611 611 and _merge_check.merge_response.metadata.get('unresolved_files')
612 612 log.debug("Using unreachable commits due to MERGE_CHECK in merge simulation")
613 613 diff_commit_cache = \
614 614 (ancestor_commit, commit_cache, missing_requirements,
615 615 source_commit, target_commit) = self.get_commits(
616 616 commits_source_repo,
617 617 pull_request_at_ver,
618 618 source_commit,
619 619 source_ref_id,
620 620 source_scm,
621 621 target_commit,
622 622 target_ref_id,
623 623 target_scm,
624 624 maybe_unreachable=maybe_unreachable)
625 625
626 626 # register our commit range
627 627 for comm in commit_cache.values():
628 628 c.commit_ranges.append(comm)
629 629
630 630 c.missing_requirements = missing_requirements
631 631 c.ancestor_commit = ancestor_commit
632 632 c.statuses = source_repo.statuses(
633 633 [x.raw_id for x in c.commit_ranges])
634 634
635 635 # auto collapse if we have more than limit
636 636 collapse_limit = diffs.DiffProcessor._collapse_commits_over
637 637 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
638 638 c.compare_mode = compare
639 639
640 640 # diff_limit is the old behavior, will cut off the whole diff
641 641 # if the limit is applied otherwise will just hide the
642 642 # big files from the front-end
643 643 diff_limit = c.visual.cut_off_limit_diff
644 644 file_limit = c.visual.cut_off_limit_file
645 645
646 646 c.missing_commits = False
647 647 if (c.missing_requirements
648 648 or isinstance(source_commit, EmptyCommit)
649 649 or source_commit == target_commit):
650 650
651 651 c.missing_commits = True
652 652 else:
653 653 c.inline_comments = display_inline_comments
654 654
655 655 use_ancestor = True
656 656 if from_version_normalized != version_normalized:
657 657 use_ancestor = False
658 658
659 659 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
660 660 if not force_recache and has_proper_diff_cache:
661 661 c.diffset = cached_diff['diff']
662 662 else:
663 663 try:
664 664 c.diffset = self._get_diffset(
665 665 c.source_repo.repo_name, commits_source_repo,
666 666 c.ancestor_commit,
667 667 source_ref_id, target_ref_id,
668 668 target_commit, source_commit,
669 669 diff_limit, file_limit, c.fulldiff,
670 670 hide_whitespace_changes, diff_context,
671 671 use_ancestor=use_ancestor
672 672 )
673 673
674 674 # save cached diff
675 675 if caching_enabled:
676 676 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
677 677 except CommitDoesNotExistError:
678 678 log.exception('Failed to generate diffset')
679 679 c.missing_commits = True
680 680
681 681 if not c.missing_commits:
682 682
683 683 c.limited_diff = c.diffset.limited_diff
684 684
685 685 # calculate removed files that are bound to comments
686 686 comment_deleted_files = [
687 687 fname for fname in display_inline_comments
688 688 if fname not in c.diffset.file_stats]
689 689
690 690 c.deleted_files_comments = collections.defaultdict(dict)
691 691 for fname, per_line_comments in display_inline_comments.items():
692 692 if fname in comment_deleted_files:
693 693 c.deleted_files_comments[fname]['stats'] = 0
694 694 c.deleted_files_comments[fname]['comments'] = list()
695 695 for lno, comments in per_line_comments.items():
696 696 c.deleted_files_comments[fname]['comments'].extend(comments)
697 697
698 698 # maybe calculate the range diff
699 699 if c.range_diff_on:
700 700 # TODO(marcink): set whitespace/context
701 701 context_lcl = 3
702 702 ign_whitespace_lcl = False
703 703
704 704 for commit in c.commit_ranges:
705 705 commit2 = commit
706 706 commit1 = commit.first_parent
707 707
708 708 range_diff_cache_file_path = diff_cache_exist(
709 709 cache_path, 'diff', commit.raw_id,
710 710 ign_whitespace_lcl, context_lcl, c.fulldiff)
711 711
712 712 cached_diff = None
713 713 if caching_enabled:
714 714 cached_diff = load_cached_diff(range_diff_cache_file_path)
715 715
716 716 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
717 717 if not force_recache and has_proper_diff_cache:
718 718 diffset = cached_diff['diff']
719 719 else:
720 720 diffset = self._get_range_diffset(
721 721 commits_source_repo, source_repo,
722 722 commit1, commit2, diff_limit, file_limit,
723 723 c.fulldiff, ign_whitespace_lcl, context_lcl
724 724 )
725 725
726 726 # save cached diff
727 727 if caching_enabled:
728 728 cache_diff(range_diff_cache_file_path, diffset, None)
729 729
730 730 c.changes[commit.raw_id] = diffset
731 731
732 732 # this is a hack to properly display links, when creating PR, the
733 733 # compare view and others uses different notation, and
734 734 # compare_commits.mako renders links based on the target_repo.
735 735 # We need to swap that here to generate it properly on the html side
736 736 c.target_repo = c.source_repo
737 737
738 738 c.commit_statuses = ChangesetStatus.STATUSES
739 739
740 740 c.show_version_changes = not pr_closed
741 741 if c.show_version_changes:
742 742 cur_obj = pull_request_at_ver
743 743 prev_obj = prev_pull_request_at_ver
744 744
745 745 old_commit_ids = prev_obj.revisions
746 746 new_commit_ids = cur_obj.revisions
747 747 commit_changes = PullRequestModel()._calculate_commit_id_changes(
748 748 old_commit_ids, new_commit_ids)
749 749 c.commit_changes_summary = commit_changes
750 750
751 751 # calculate the diff for commits between versions
752 752 c.commit_changes = []
753 753
754 754 def mark(cs, fw):
755 755 return list(h.itertools.izip_longest([], cs, fillvalue=fw))
756 756
757 757 for c_type, raw_id in mark(commit_changes.added, 'a') \
758 758 + mark(commit_changes.removed, 'r') \
759 759 + mark(commit_changes.common, 'c'):
760 760
761 761 if raw_id in commit_cache:
762 762 commit = commit_cache[raw_id]
763 763 else:
764 764 try:
765 765 commit = commits_source_repo.get_commit(raw_id)
766 766 except CommitDoesNotExistError:
767 767 # in case we fail extracting still use "dummy" commit
768 768 # for display in commit diff
769 769 commit = h.AttributeDict(
770 770 {'raw_id': raw_id,
771 771 'message': 'EMPTY or MISSING COMMIT'})
772 772 c.commit_changes.append([c_type, commit])
773 773
774 774 # current user review statuses for each version
775 775 c.review_versions = {}
776 776 is_reviewer = PullRequestModel().is_user_reviewer(
777 777 pull_request, self._rhodecode_user)
778 778 if is_reviewer:
779 779 for co in general_comments:
780 780 if co.author.user_id == self._rhodecode_user.user_id:
781 781 status = co.status_change
782 782 if status:
783 783 _ver_pr = status[0].comment.pull_request_version_id
784 784 c.review_versions[_ver_pr] = status[0]
785 785
786 786 return self._get_template_context(c)
787 787
788 788 def get_commits(
789 789 self, commits_source_repo, pull_request_at_ver, source_commit,
790 790 source_ref_id, source_scm, target_commit, target_ref_id, target_scm,
791 791 maybe_unreachable=False):
792 792
793 793 commit_cache = collections.OrderedDict()
794 794 missing_requirements = False
795 795
796 796 try:
797 797 pre_load = ["author", "date", "message", "branch", "parents"]
798 798
799 799 pull_request_commits = pull_request_at_ver.revisions
800 800 log.debug('Loading %s commits from %s',
801 801 len(pull_request_commits), commits_source_repo)
802 802
803 803 for rev in pull_request_commits:
804 804 comm = commits_source_repo.get_commit(commit_id=rev, pre_load=pre_load,
805 805 maybe_unreachable=maybe_unreachable)
806 806 commit_cache[comm.raw_id] = comm
807 807
808 808 # Order here matters, we first need to get target, and then
809 809 # the source
810 810 target_commit = commits_source_repo.get_commit(
811 811 commit_id=safe_str(target_ref_id))
812 812
813 813 source_commit = commits_source_repo.get_commit(
814 814 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
815 815 except CommitDoesNotExistError:
816 816 log.warning('Failed to get commit from `{}` repo'.format(
817 817 commits_source_repo), exc_info=True)
818 818 except RepositoryRequirementError:
819 819 log.warning('Failed to get all required data from repo', exc_info=True)
820 820 missing_requirements = True
821 821
822 822 pr_ancestor_id = pull_request_at_ver.common_ancestor_id
823 823
824 824 try:
825 825 ancestor_commit = source_scm.get_commit(pr_ancestor_id)
826 826 except Exception:
827 827 ancestor_commit = None
828 828
829 829 return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit
830 830
831 831 def assure_not_empty_repo(self):
832 832 _ = self.request.translate
833 833
834 834 try:
835 835 self.db_repo.scm_instance().get_commit()
836 836 except EmptyRepositoryError:
837 837 h.flash(h.literal(_('There are no commits yet')),
838 838 category='warning')
839 839 raise HTTPFound(
840 840 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
841 841
842 842 @LoginRequired()
843 843 @NotAnonymous()
844 844 @HasRepoPermissionAnyDecorator(
845 845 'repository.read', 'repository.write', 'repository.admin')
846 846 def pull_request_new(self):
847 847 _ = self.request.translate
848 848 c = self.load_default_context()
849 849
850 850 self.assure_not_empty_repo()
851 851 source_repo = self.db_repo
852 852
853 853 commit_id = self.request.GET.get('commit')
854 854 branch_ref = self.request.GET.get('branch')
855 855 bookmark_ref = self.request.GET.get('bookmark')
856 856
857 857 try:
858 858 source_repo_data = PullRequestModel().generate_repo_data(
859 859 source_repo, commit_id=commit_id,
860 860 branch=branch_ref, bookmark=bookmark_ref,
861 861 translator=self.request.translate)
862 862 except CommitDoesNotExistError as e:
863 863 log.exception(e)
864 864 h.flash(_('Commit does not exist'), 'error')
865 865 raise HTTPFound(
866 866 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
867 867
868 868 default_target_repo = source_repo
869 869
870 870 if source_repo.parent and c.has_origin_repo_read_perm:
871 871 parent_vcs_obj = source_repo.parent.scm_instance()
872 872 if parent_vcs_obj and not parent_vcs_obj.is_empty():
873 873 # change default if we have a parent repo
874 874 default_target_repo = source_repo.parent
875 875
876 876 target_repo_data = PullRequestModel().generate_repo_data(
877 877 default_target_repo, translator=self.request.translate)
878 878
879 879 selected_source_ref = source_repo_data['refs']['selected_ref']
880 880 title_source_ref = ''
881 881 if selected_source_ref:
882 882 title_source_ref = selected_source_ref.split(':', 2)[1]
883 883 c.default_title = PullRequestModel().generate_pullrequest_title(
884 884 source=source_repo.repo_name,
885 885 source_ref=title_source_ref,
886 886 target=default_target_repo.repo_name
887 887 )
888 888
889 889 c.default_repo_data = {
890 890 'source_repo_name': source_repo.repo_name,
891 891 'source_refs_json': json.dumps(source_repo_data),
892 892 'target_repo_name': default_target_repo.repo_name,
893 893 'target_refs_json': json.dumps(target_repo_data),
894 894 }
895 895 c.default_source_ref = selected_source_ref
896 896
897 897 return self._get_template_context(c)
898 898
899 899 @LoginRequired()
900 900 @NotAnonymous()
901 901 @HasRepoPermissionAnyDecorator(
902 902 'repository.read', 'repository.write', 'repository.admin')
903 903 def pull_request_repo_refs(self):
904 904 self.load_default_context()
905 905 target_repo_name = self.request.matchdict['target_repo_name']
906 906 repo = Repository.get_by_repo_name(target_repo_name)
907 907 if not repo:
908 908 raise HTTPNotFound()
909 909
910 910 target_perm = HasRepoPermissionAny(
911 911 'repository.read', 'repository.write', 'repository.admin')(
912 912 target_repo_name)
913 913 if not target_perm:
914 914 raise HTTPNotFound()
915 915
916 916 return PullRequestModel().generate_repo_data(
917 917 repo, translator=self.request.translate)
918 918
919 919 @LoginRequired()
920 920 @NotAnonymous()
921 921 @HasRepoPermissionAnyDecorator(
922 922 'repository.read', 'repository.write', 'repository.admin')
923 923 def pullrequest_repo_targets(self):
924 924 _ = self.request.translate
925 925 filter_query = self.request.GET.get('query')
926 926
927 927 # get the parents
928 928 parent_target_repos = []
929 929 if self.db_repo.parent:
930 930 parents_query = Repository.query() \
931 931 .order_by(func.length(Repository.repo_name)) \
932 932 .filter(Repository.fork_id == self.db_repo.parent.repo_id)
933 933
934 934 if filter_query:
935 935 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
936 936 parents_query = parents_query.filter(
937 937 Repository.repo_name.ilike(ilike_expression))
938 938 parents = parents_query.limit(20).all()
939 939
940 940 for parent in parents:
941 941 parent_vcs_obj = parent.scm_instance()
942 942 if parent_vcs_obj and not parent_vcs_obj.is_empty():
943 943 parent_target_repos.append(parent)
944 944
945 945 # get other forks, and repo itself
946 946 query = Repository.query() \
947 947 .order_by(func.length(Repository.repo_name)) \
948 948 .filter(
949 949 or_(Repository.repo_id == self.db_repo.repo_id, # repo itself
950 950 Repository.fork_id == self.db_repo.repo_id) # forks of this repo
951 951 ) \
952 952 .filter(~Repository.repo_id.in_([x.repo_id for x in parent_target_repos]))
953 953
954 954 if filter_query:
955 955 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
956 956 query = query.filter(Repository.repo_name.ilike(ilike_expression))
957 957
958 958 limit = max(20 - len(parent_target_repos), 5) # not less then 5
959 959 target_repos = query.limit(limit).all()
960 960
961 961 all_target_repos = target_repos + parent_target_repos
962 962
963 963 repos = []
964 964 # This checks permissions to the repositories
965 965 for obj in ScmModel().get_repos(all_target_repos):
966 966 repos.append({
967 967 'id': obj['name'],
968 968 'text': obj['name'],
969 969 'type': 'repo',
970 970 'repo_id': obj['dbrepo']['repo_id'],
971 971 'repo_type': obj['dbrepo']['repo_type'],
972 972 'private': obj['dbrepo']['private'],
973 973
974 974 })
975 975
976 976 data = {
977 977 'more': False,
978 978 'results': [{
979 979 'text': _('Repositories'),
980 980 'children': repos
981 981 }] if repos else []
982 982 }
983 983 return data
984 984
985 985 @classmethod
986 986 def get_comment_ids(cls, post_data):
987 987 return filter(lambda e: e > 0, map(safe_int, aslist(post_data.get('comments'), ',')))
988 988
989 989 @LoginRequired()
990 990 @NotAnonymous()
991 991 @HasRepoPermissionAnyDecorator(
992 992 'repository.read', 'repository.write', 'repository.admin')
993 993 def pullrequest_comments(self):
994 994 self.load_default_context()
995 995
996 996 pull_request = PullRequest.get_or_404(
997 997 self.request.matchdict['pull_request_id'])
998 998 pull_request_id = pull_request.pull_request_id
999 999 version = self.request.GET.get('version')
1000 1000
1001 1001 _render = self.request.get_partial_renderer(
1002 1002 'rhodecode:templates/base/sidebar.mako')
1003 1003 c = _render.get_call_context()
1004 1004
1005 1005 (pull_request_latest,
1006 1006 pull_request_at_ver,
1007 1007 pull_request_display_obj,
1008 1008 at_version) = PullRequestModel().get_pr_version(
1009 1009 pull_request_id, version=version)
1010 1010 versions = pull_request_display_obj.versions()
1011 1011 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1012 1012 c.versions = versions + [latest_ver]
1013 1013
1014 1014 c.at_version = at_version
1015 1015 c.at_version_num = (at_version
1016 1016 if at_version and at_version != PullRequest.LATEST_VER
1017 1017 else None)
1018 1018
1019 1019 self.register_comments_vars(c, pull_request_latest, versions, include_drafts=False)
1020 1020 all_comments = c.inline_comments_flat + c.comments
1021 1021
1022 1022 existing_ids = self.get_comment_ids(self.request.POST)
1023 1023 return _render('comments_table', all_comments, len(all_comments),
1024 1024 existing_ids=existing_ids)
1025 1025
1026 1026 @LoginRequired()
1027 1027 @NotAnonymous()
1028 1028 @HasRepoPermissionAnyDecorator(
1029 1029 'repository.read', 'repository.write', 'repository.admin')
1030 1030 def pullrequest_todos(self):
1031 1031 self.load_default_context()
1032 1032
1033 1033 pull_request = PullRequest.get_or_404(
1034 1034 self.request.matchdict['pull_request_id'])
1035 1035 pull_request_id = pull_request.pull_request_id
1036 1036 version = self.request.GET.get('version')
1037 1037
1038 1038 _render = self.request.get_partial_renderer(
1039 1039 'rhodecode:templates/base/sidebar.mako')
1040 1040 c = _render.get_call_context()
1041 1041 (pull_request_latest,
1042 1042 pull_request_at_ver,
1043 1043 pull_request_display_obj,
1044 1044 at_version) = PullRequestModel().get_pr_version(
1045 1045 pull_request_id, version=version)
1046 1046 versions = pull_request_display_obj.versions()
1047 1047 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1048 1048 c.versions = versions + [latest_ver]
1049 1049
1050 1050 c.at_version = at_version
1051 1051 c.at_version_num = (at_version
1052 1052 if at_version and at_version != PullRequest.LATEST_VER
1053 1053 else None)
1054 1054
1055 1055 c.unresolved_comments = CommentsModel() \
1056 1056 .get_pull_request_unresolved_todos(pull_request, include_drafts=False)
1057 1057 c.resolved_comments = CommentsModel() \
1058 1058 .get_pull_request_resolved_todos(pull_request, include_drafts=False)
1059 1059
1060 1060 all_comments = c.unresolved_comments + c.resolved_comments
1061 1061 existing_ids = self.get_comment_ids(self.request.POST)
1062 1062 return _render('comments_table', all_comments, len(c.unresolved_comments),
1063 1063 todo_comments=True, existing_ids=existing_ids)
1064 1064
1065 1065 @LoginRequired()
1066 1066 @NotAnonymous()
1067 1067 @HasRepoPermissionAnyDecorator(
1068 1068 'repository.read', 'repository.write', 'repository.admin')
1069 1069 def pullrequest_drafts(self):
1070 1070 self.load_default_context()
1071 1071
1072 1072 pull_request = PullRequest.get_or_404(
1073 1073 self.request.matchdict['pull_request_id'])
1074 1074 pull_request_id = pull_request.pull_request_id
1075 1075 version = self.request.GET.get('version')
1076 1076
1077 1077 _render = self.request.get_partial_renderer(
1078 1078 'rhodecode:templates/base/sidebar.mako')
1079 1079 c = _render.get_call_context()
1080 1080
1081 1081 (pull_request_latest,
1082 1082 pull_request_at_ver,
1083 1083 pull_request_display_obj,
1084 1084 at_version) = PullRequestModel().get_pr_version(
1085 1085 pull_request_id, version=version)
1086 1086 versions = pull_request_display_obj.versions()
1087 1087 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1088 1088 c.versions = versions + [latest_ver]
1089 1089
1090 1090 c.at_version = at_version
1091 1091 c.at_version_num = (at_version
1092 1092 if at_version and at_version != PullRequest.LATEST_VER
1093 1093 else None)
1094 1094
1095 1095 c.draft_comments = CommentsModel() \
1096 1096 .get_pull_request_drafts(self._rhodecode_db_user.user_id, pull_request)
1097 1097
1098 1098 all_comments = c.draft_comments
1099 1099
1100 1100 existing_ids = self.get_comment_ids(self.request.POST)
1101 1101 return _render('comments_table', all_comments, len(all_comments),
1102 1102 existing_ids=existing_ids, draft_comments=True)
1103 1103
1104 1104 @LoginRequired()
1105 1105 @NotAnonymous()
1106 1106 @HasRepoPermissionAnyDecorator(
1107 1107 'repository.read', 'repository.write', 'repository.admin')
1108 1108 @CSRFRequired()
1109 1109 def pull_request_create(self):
1110 1110 _ = self.request.translate
1111 1111 self.assure_not_empty_repo()
1112 1112 self.load_default_context()
1113 1113
1114 1114 controls = peppercorn.parse(self.request.POST.items())
1115 1115
1116 1116 try:
1117 1117 form = PullRequestForm(
1118 1118 self.request.translate, self.db_repo.repo_id)()
1119 1119 _form = form.to_python(controls)
1120 1120 except formencode.Invalid as errors:
1121 1121 if errors.error_dict.get('revisions'):
1122 1122 msg = 'Revisions: %s' % errors.error_dict['revisions']
1123 1123 elif errors.error_dict.get('pullrequest_title'):
1124 1124 msg = errors.error_dict.get('pullrequest_title')
1125 1125 else:
1126 1126 msg = _('Error creating pull request: {}').format(errors)
1127 1127 log.exception(msg)
1128 1128 h.flash(msg, 'error')
1129 1129
1130 1130 # would rather just go back to form ...
1131 1131 raise HTTPFound(
1132 1132 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1133 1133
1134 1134 source_repo = _form['source_repo']
1135 1135 source_ref = _form['source_ref']
1136 1136 target_repo = _form['target_repo']
1137 1137 target_ref = _form['target_ref']
1138 1138 commit_ids = _form['revisions'][::-1]
1139 1139 common_ancestor_id = _form['common_ancestor']
1140 1140
1141 1141 # find the ancestor for this pr
1142 1142 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
1143 1143 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
1144 1144
1145 1145 if not (source_db_repo or target_db_repo):
1146 1146 h.flash(_('source_repo or target repo not found'), category='error')
1147 1147 raise HTTPFound(
1148 1148 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1149 1149
1150 1150 # re-check permissions again here
1151 1151 # source_repo we must have read permissions
1152 1152
1153 1153 source_perm = HasRepoPermissionAny(
1154 1154 'repository.read', 'repository.write', 'repository.admin')(
1155 1155 source_db_repo.repo_name)
1156 1156 if not source_perm:
1157 1157 msg = _('Not Enough permissions to source repo `{}`.'.format(
1158 1158 source_db_repo.repo_name))
1159 1159 h.flash(msg, category='error')
1160 1160 # copy the args back to redirect
1161 1161 org_query = self.request.GET.mixed()
1162 1162 raise HTTPFound(
1163 1163 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1164 1164 _query=org_query))
1165 1165
1166 1166 # target repo we must have read permissions, and also later on
1167 1167 # we want to check branch permissions here
1168 1168 target_perm = HasRepoPermissionAny(
1169 1169 'repository.read', 'repository.write', 'repository.admin')(
1170 1170 target_db_repo.repo_name)
1171 1171 if not target_perm:
1172 1172 msg = _('Not Enough permissions to target repo `{}`.'.format(
1173 1173 target_db_repo.repo_name))
1174 1174 h.flash(msg, category='error')
1175 1175 # copy the args back to redirect
1176 1176 org_query = self.request.GET.mixed()
1177 1177 raise HTTPFound(
1178 1178 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1179 1179 _query=org_query))
1180 1180
1181 1181 source_scm = source_db_repo.scm_instance()
1182 1182 target_scm = target_db_repo.scm_instance()
1183 1183
1184 1184 source_ref_obj = unicode_to_reference(source_ref)
1185 1185 target_ref_obj = unicode_to_reference(target_ref)
1186 1186
1187 1187 source_commit = source_scm.get_commit(source_ref_obj.commit_id)
1188 1188 target_commit = target_scm.get_commit(target_ref_obj.commit_id)
1189 1189
1190 1190 ancestor = source_scm.get_common_ancestor(
1191 1191 source_commit.raw_id, target_commit.raw_id, target_scm)
1192 1192
1193 1193 # recalculate target ref based on ancestor
1194 1194 target_ref = ':'.join((target_ref_obj.type, target_ref_obj.name, ancestor))
1195 1195
1196 1196 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1197 1197 PullRequestModel().get_reviewer_functions()
1198 1198
1199 1199 # recalculate reviewers logic, to make sure we can validate this
1200 1200 reviewer_rules = get_default_reviewers_data(
1201 1201 self._rhodecode_db_user,
1202 1202 source_db_repo,
1203 1203 source_ref_obj,
1204 1204 target_db_repo,
1205 1205 target_ref_obj,
1206 1206 include_diff_info=False)
1207 1207
1208 1208 reviewers = validate_default_reviewers(_form['review_members'], reviewer_rules)
1209 1209 observers = validate_observers(_form['observer_members'], reviewer_rules)
1210 1210
1211 1211 pullrequest_title = _form['pullrequest_title']
1212 1212 title_source_ref = source_ref_obj.name
1213 1213 if not pullrequest_title:
1214 1214 pullrequest_title = PullRequestModel().generate_pullrequest_title(
1215 1215 source=source_repo,
1216 1216 source_ref=title_source_ref,
1217 1217 target=target_repo
1218 1218 )
1219 1219
1220 1220 description = _form['pullrequest_desc']
1221 1221 description_renderer = _form['description_renderer']
1222 1222
1223 1223 try:
1224 1224 pull_request = PullRequestModel().create(
1225 1225 created_by=self._rhodecode_user.user_id,
1226 1226 source_repo=source_repo,
1227 1227 source_ref=source_ref,
1228 1228 target_repo=target_repo,
1229 1229 target_ref=target_ref,
1230 1230 revisions=commit_ids,
1231 1231 common_ancestor_id=common_ancestor_id,
1232 1232 reviewers=reviewers,
1233 1233 observers=observers,
1234 1234 title=pullrequest_title,
1235 1235 description=description,
1236 1236 description_renderer=description_renderer,
1237 1237 reviewer_data=reviewer_rules,
1238 1238 auth_user=self._rhodecode_user
1239 1239 )
1240 1240 Session().commit()
1241 1241
1242 1242 h.flash(_('Successfully opened new pull request'),
1243 1243 category='success')
1244 1244 except Exception:
1245 1245 msg = _('Error occurred during creation of this pull request.')
1246 1246 log.exception(msg)
1247 1247 h.flash(msg, category='error')
1248 1248
1249 1249 # copy the args back to redirect
1250 1250 org_query = self.request.GET.mixed()
1251 1251 raise HTTPFound(
1252 1252 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1253 1253 _query=org_query))
1254 1254
1255 1255 raise HTTPFound(
1256 1256 h.route_path('pullrequest_show', repo_name=target_repo,
1257 1257 pull_request_id=pull_request.pull_request_id))
1258 1258
1259 1259 @LoginRequired()
1260 1260 @NotAnonymous()
1261 1261 @HasRepoPermissionAnyDecorator(
1262 1262 'repository.read', 'repository.write', 'repository.admin')
1263 1263 @CSRFRequired()
1264 1264 def pull_request_update(self):
1265 1265 pull_request = PullRequest.get_or_404(
1266 1266 self.request.matchdict['pull_request_id'])
1267 1267 _ = self.request.translate
1268 1268
1269 1269 c = self.load_default_context()
1270 1270 redirect_url = None
1271 1271
1272 1272 if pull_request.is_closed():
1273 1273 log.debug('update: forbidden because pull request is closed')
1274 1274 msg = _(u'Cannot update closed pull requests.')
1275 1275 h.flash(msg, category='error')
1276 1276 return {'response': True,
1277 1277 'redirect_url': redirect_url}
1278 1278
1279 1279 is_state_changing = pull_request.is_state_changing()
1280 1280 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
1281 1281
1282 1282 # only owner or admin can update it
1283 1283 allowed_to_update = PullRequestModel().check_user_update(
1284 1284 pull_request, self._rhodecode_user)
1285 1285
1286 1286 if allowed_to_update:
1287 1287 controls = peppercorn.parse(self.request.POST.items())
1288 1288 force_refresh = str2bool(self.request.POST.get('force_refresh'))
1289 1289
1290 1290 if 'review_members' in controls:
1291 1291 self._update_reviewers(
1292 1292 c,
1293 1293 pull_request, controls['review_members'],
1294 1294 pull_request.reviewer_data,
1295 1295 PullRequestReviewers.ROLE_REVIEWER)
1296 1296 elif 'observer_members' in controls:
1297 1297 self._update_reviewers(
1298 1298 c,
1299 1299 pull_request, controls['observer_members'],
1300 1300 pull_request.reviewer_data,
1301 1301 PullRequestReviewers.ROLE_OBSERVER)
1302 1302 elif str2bool(self.request.POST.get('update_commits', 'false')):
1303 1303 if is_state_changing:
1304 1304 log.debug('commits update: forbidden because pull request is in state %s',
1305 1305 pull_request.pull_request_state)
1306 1306 msg = _(u'Cannot update pull requests commits in state other than `{}`. '
1307 1307 u'Current state is: `{}`').format(
1308 1308 PullRequest.STATE_CREATED, pull_request.pull_request_state)
1309 1309 h.flash(msg, category='error')
1310 1310 return {'response': True,
1311 1311 'redirect_url': redirect_url}
1312 1312
1313 1313 self._update_commits(c, pull_request)
1314 1314 if force_refresh:
1315 1315 redirect_url = h.route_path(
1316 1316 'pullrequest_show', repo_name=self.db_repo_name,
1317 1317 pull_request_id=pull_request.pull_request_id,
1318 1318 _query={"force_refresh": 1})
1319 1319 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
1320 1320 self._edit_pull_request(pull_request)
1321 1321 else:
1322 1322 log.error('Unhandled update data.')
1323 1323 raise HTTPBadRequest()
1324 1324
1325 1325 return {'response': True,
1326 1326 'redirect_url': redirect_url}
1327 1327 raise HTTPForbidden()
1328 1328
1329 1329 def _edit_pull_request(self, pull_request):
1330 1330 """
1331 1331 Edit title and description
1332 1332 """
1333 1333 _ = self.request.translate
1334 1334
1335 1335 try:
1336 1336 PullRequestModel().edit(
1337 1337 pull_request,
1338 1338 self.request.POST.get('title'),
1339 1339 self.request.POST.get('description'),
1340 1340 self.request.POST.get('description_renderer'),
1341 1341 self._rhodecode_user)
1342 1342 except ValueError:
1343 1343 msg = _(u'Cannot update closed pull requests.')
1344 1344 h.flash(msg, category='error')
1345 1345 return
1346 1346 else:
1347 1347 Session().commit()
1348 1348
1349 1349 msg = _(u'Pull request title & description updated.')
1350 1350 h.flash(msg, category='success')
1351 1351 return
1352 1352
1353 1353 def _update_commits(self, c, pull_request):
1354 1354 _ = self.request.translate
1355 1355
1356 @retry(exception=Exception, n_tries=3)
1357 def commits_update():
1358 return PullRequestModel().update_commits(
1359 pull_request, self._rhodecode_db_user)
1360
1356 1361 with pull_request.set_state(PullRequest.STATE_UPDATING):
1357 resp = PullRequestModel().update_commits(
1358 pull_request, self._rhodecode_db_user)
1362 resp = commits_update() # retry x3
1359 1363
1360 1364 if resp.executed:
1361 1365
1362 1366 if resp.target_changed and resp.source_changed:
1363 1367 changed = 'target and source repositories'
1364 1368 elif resp.target_changed and not resp.source_changed:
1365 1369 changed = 'target repository'
1366 1370 elif not resp.target_changed and resp.source_changed:
1367 1371 changed = 'source repository'
1368 1372 else:
1369 1373 changed = 'nothing'
1370 1374
1371 1375 msg = _(u'Pull request updated to "{source_commit_id}" with '
1372 1376 u'{count_added} added, {count_removed} removed commits. '
1373 1377 u'Source of changes: {change_source}.')
1374 1378 msg = msg.format(
1375 1379 source_commit_id=pull_request.source_ref_parts.commit_id,
1376 1380 count_added=len(resp.changes.added),
1377 1381 count_removed=len(resp.changes.removed),
1378 1382 change_source=changed)
1379 1383 h.flash(msg, category='success')
1380 1384 channelstream.pr_update_channelstream_push(
1381 1385 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1382 1386 else:
1383 1387 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1384 1388 warning_reasons = [
1385 1389 UpdateFailureReason.NO_CHANGE,
1386 1390 UpdateFailureReason.WRONG_REF_TYPE,
1387 1391 ]
1388 1392 category = 'warning' if resp.reason in warning_reasons else 'error'
1389 1393 h.flash(msg, category=category)
1390 1394
1391 1395 def _update_reviewers(self, c, pull_request, review_members, reviewer_rules, role):
1392 1396 _ = self.request.translate
1393 1397
1394 1398 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1395 1399 PullRequestModel().get_reviewer_functions()
1396 1400
1397 1401 if role == PullRequestReviewers.ROLE_REVIEWER:
1398 1402 try:
1399 1403 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1400 1404 except ValueError as e:
1401 1405 log.error('Reviewers Validation: {}'.format(e))
1402 1406 h.flash(e, category='error')
1403 1407 return
1404 1408
1405 1409 old_calculated_status = pull_request.calculated_review_status()
1406 1410 PullRequestModel().update_reviewers(
1407 1411 pull_request, reviewers, self._rhodecode_db_user)
1408 1412
1409 1413 Session().commit()
1410 1414
1411 1415 msg = _('Pull request reviewers updated.')
1412 1416 h.flash(msg, category='success')
1413 1417 channelstream.pr_update_channelstream_push(
1414 1418 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1415 1419
1416 1420 # trigger status changed if change in reviewers changes the status
1417 1421 calculated_status = pull_request.calculated_review_status()
1418 1422 if old_calculated_status != calculated_status:
1419 1423 PullRequestModel().trigger_pull_request_hook(
1420 1424 pull_request, self._rhodecode_user, 'review_status_change',
1421 1425 data={'status': calculated_status})
1422 1426
1423 1427 elif role == PullRequestReviewers.ROLE_OBSERVER:
1424 1428 try:
1425 1429 observers = validate_observers(review_members, reviewer_rules)
1426 1430 except ValueError as e:
1427 1431 log.error('Observers Validation: {}'.format(e))
1428 1432 h.flash(e, category='error')
1429 1433 return
1430 1434
1431 1435 PullRequestModel().update_observers(
1432 1436 pull_request, observers, self._rhodecode_db_user)
1433 1437
1434 1438 Session().commit()
1435 1439 msg = _('Pull request observers updated.')
1436 1440 h.flash(msg, category='success')
1437 1441 channelstream.pr_update_channelstream_push(
1438 1442 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1439 1443
1440 1444 @LoginRequired()
1441 1445 @NotAnonymous()
1442 1446 @HasRepoPermissionAnyDecorator(
1443 1447 'repository.read', 'repository.write', 'repository.admin')
1444 1448 @CSRFRequired()
1445 1449 def pull_request_merge(self):
1446 1450 """
1447 1451 Merge will perform a server-side merge of the specified
1448 1452 pull request, if the pull request is approved and mergeable.
1449 1453 After successful merging, the pull request is automatically
1450 1454 closed, with a relevant comment.
1451 1455 """
1452 1456 pull_request = PullRequest.get_or_404(
1453 1457 self.request.matchdict['pull_request_id'])
1454 1458 _ = self.request.translate
1455 1459
1456 1460 if pull_request.is_state_changing():
1457 1461 log.debug('show: forbidden because pull request is in state %s',
1458 1462 pull_request.pull_request_state)
1459 1463 msg = _(u'Cannot merge pull requests in state other than `{}`. '
1460 1464 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
1461 1465 pull_request.pull_request_state)
1462 1466 h.flash(msg, category='error')
1463 1467 raise HTTPFound(
1464 1468 h.route_path('pullrequest_show',
1465 1469 repo_name=pull_request.target_repo.repo_name,
1466 1470 pull_request_id=pull_request.pull_request_id))
1467 1471
1468 1472 self.load_default_context()
1469 1473
1470 1474 with pull_request.set_state(PullRequest.STATE_UPDATING):
1471 1475 check = MergeCheck.validate(
1472 1476 pull_request, auth_user=self._rhodecode_user,
1473 1477 translator=self.request.translate)
1474 1478 merge_possible = not check.failed
1475 1479
1476 1480 for err_type, error_msg in check.errors:
1477 1481 h.flash(error_msg, category=err_type)
1478 1482
1479 1483 if merge_possible:
1480 1484 log.debug("Pre-conditions checked, trying to merge.")
1481 1485 extras = vcs_operation_context(
1482 1486 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1483 1487 username=self._rhodecode_db_user.username, action='push',
1484 1488 scm=pull_request.target_repo.repo_type)
1485 1489 with pull_request.set_state(PullRequest.STATE_UPDATING):
1486 1490 self._merge_pull_request(
1487 1491 pull_request, self._rhodecode_db_user, extras)
1488 1492 else:
1489 1493 log.debug("Pre-conditions failed, NOT merging.")
1490 1494
1491 1495 raise HTTPFound(
1492 1496 h.route_path('pullrequest_show',
1493 1497 repo_name=pull_request.target_repo.repo_name,
1494 1498 pull_request_id=pull_request.pull_request_id))
1495 1499
1496 1500 def _merge_pull_request(self, pull_request, user, extras):
1497 1501 _ = self.request.translate
1498 1502 merge_resp = PullRequestModel().merge_repo(pull_request, user, extras=extras)
1499 1503
1500 1504 if merge_resp.executed:
1501 1505 log.debug("The merge was successful, closing the pull request.")
1502 1506 PullRequestModel().close_pull_request(
1503 1507 pull_request.pull_request_id, user)
1504 1508 Session().commit()
1505 1509 msg = _('Pull request was successfully merged and closed.')
1506 1510 h.flash(msg, category='success')
1507 1511 else:
1508 1512 log.debug(
1509 1513 "The merge was not successful. Merge response: %s", merge_resp)
1510 1514 msg = merge_resp.merge_status_message
1511 1515 h.flash(msg, category='error')
1512 1516
1513 1517 @LoginRequired()
1514 1518 @NotAnonymous()
1515 1519 @HasRepoPermissionAnyDecorator(
1516 1520 'repository.read', 'repository.write', 'repository.admin')
1517 1521 @CSRFRequired()
1518 1522 def pull_request_delete(self):
1519 1523 _ = self.request.translate
1520 1524
1521 1525 pull_request = PullRequest.get_or_404(
1522 1526 self.request.matchdict['pull_request_id'])
1523 1527 self.load_default_context()
1524 1528
1525 1529 pr_closed = pull_request.is_closed()
1526 1530 allowed_to_delete = PullRequestModel().check_user_delete(
1527 1531 pull_request, self._rhodecode_user) and not pr_closed
1528 1532
1529 1533 # only owner can delete it !
1530 1534 if allowed_to_delete:
1531 1535 PullRequestModel().delete(pull_request, self._rhodecode_user)
1532 1536 Session().commit()
1533 1537 h.flash(_('Successfully deleted pull request'),
1534 1538 category='success')
1535 1539 raise HTTPFound(h.route_path('pullrequest_show_all',
1536 1540 repo_name=self.db_repo_name))
1537 1541
1538 1542 log.warning('user %s tried to delete pull request without access',
1539 1543 self._rhodecode_user)
1540 1544 raise HTTPNotFound()
1541 1545
1542 1546 def _pull_request_comments_create(self, pull_request, comments):
1543 1547 _ = self.request.translate
1544 1548 data = {}
1545 1549 if not comments:
1546 1550 return
1547 1551 pull_request_id = pull_request.pull_request_id
1548 1552
1549 1553 all_drafts = len([x for x in comments if str2bool(x['is_draft'])]) == len(comments)
1550 1554
1551 1555 for entry in comments:
1552 1556 c = self.load_default_context()
1553 1557 comment_type = entry['comment_type']
1554 1558 text = entry['text']
1555 1559 status = entry['status']
1556 1560 is_draft = str2bool(entry['is_draft'])
1557 1561 resolves_comment_id = entry['resolves_comment_id']
1558 1562 close_pull_request = entry['close_pull_request']
1559 1563 f_path = entry['f_path']
1560 1564 line_no = entry['line']
1561 1565 target_elem_id = 'file-{}'.format(h.safeid(h.safe_unicode(f_path)))
1562 1566
1563 1567 # the logic here should work like following, if we submit close
1564 1568 # pr comment, use `close_pull_request_with_comment` function
1565 1569 # else handle regular comment logic
1566 1570
1567 1571 if close_pull_request:
1568 1572 # only owner or admin or person with write permissions
1569 1573 allowed_to_close = PullRequestModel().check_user_update(
1570 1574 pull_request, self._rhodecode_user)
1571 1575 if not allowed_to_close:
1572 1576 log.debug('comment: forbidden because not allowed to close '
1573 1577 'pull request %s', pull_request_id)
1574 1578 raise HTTPForbidden()
1575 1579
1576 1580 # This also triggers `review_status_change`
1577 1581 comment, status = PullRequestModel().close_pull_request_with_comment(
1578 1582 pull_request, self._rhodecode_user, self.db_repo, message=text,
1579 1583 auth_user=self._rhodecode_user)
1580 1584 Session().flush()
1581 1585 is_inline = comment.is_inline
1582 1586
1583 1587 PullRequestModel().trigger_pull_request_hook(
1584 1588 pull_request, self._rhodecode_user, 'comment',
1585 1589 data={'comment': comment})
1586 1590
1587 1591 else:
1588 1592 # regular comment case, could be inline, or one with status.
1589 1593 # for that one we check also permissions
1590 1594 # Additionally ENSURE if somehow draft is sent we're then unable to change status
1591 1595 allowed_to_change_status = PullRequestModel().check_user_change_status(
1592 1596 pull_request, self._rhodecode_user) and not is_draft
1593 1597
1594 1598 if status and allowed_to_change_status:
1595 1599 message = (_('Status change %(transition_icon)s %(status)s')
1596 1600 % {'transition_icon': '>',
1597 1601 'status': ChangesetStatus.get_status_lbl(status)})
1598 1602 text = text or message
1599 1603
1600 1604 comment = CommentsModel().create(
1601 1605 text=text,
1602 1606 repo=self.db_repo.repo_id,
1603 1607 user=self._rhodecode_user.user_id,
1604 1608 pull_request=pull_request,
1605 1609 f_path=f_path,
1606 1610 line_no=line_no,
1607 1611 status_change=(ChangesetStatus.get_status_lbl(status)
1608 1612 if status and allowed_to_change_status else None),
1609 1613 status_change_type=(status
1610 1614 if status and allowed_to_change_status else None),
1611 1615 comment_type=comment_type,
1612 1616 is_draft=is_draft,
1613 1617 resolves_comment_id=resolves_comment_id,
1614 1618 auth_user=self._rhodecode_user,
1615 1619 send_email=not is_draft, # skip notification for draft comments
1616 1620 )
1617 1621 is_inline = comment.is_inline
1618 1622
1619 1623 if allowed_to_change_status:
1620 1624 # calculate old status before we change it
1621 1625 old_calculated_status = pull_request.calculated_review_status()
1622 1626
1623 1627 # get status if set !
1624 1628 if status:
1625 1629 ChangesetStatusModel().set_status(
1626 1630 self.db_repo.repo_id,
1627 1631 status,
1628 1632 self._rhodecode_user.user_id,
1629 1633 comment,
1630 1634 pull_request=pull_request
1631 1635 )
1632 1636
1633 1637 Session().flush()
1634 1638 # this is somehow required to get access to some relationship
1635 1639 # loaded on comment
1636 1640 Session().refresh(comment)
1637 1641
1638 1642 # skip notifications for drafts
1639 1643 if not is_draft:
1640 1644 PullRequestModel().trigger_pull_request_hook(
1641 1645 pull_request, self._rhodecode_user, 'comment',
1642 1646 data={'comment': comment})
1643 1647
1644 1648 # we now calculate the status of pull request, and based on that
1645 1649 # calculation we set the commits status
1646 1650 calculated_status = pull_request.calculated_review_status()
1647 1651 if old_calculated_status != calculated_status:
1648 1652 PullRequestModel().trigger_pull_request_hook(
1649 1653 pull_request, self._rhodecode_user, 'review_status_change',
1650 1654 data={'status': calculated_status})
1651 1655
1652 1656 comment_id = comment.comment_id
1653 1657 data[comment_id] = {
1654 1658 'target_id': target_elem_id
1655 1659 }
1656 1660 Session().flush()
1657 1661
1658 1662 c.co = comment
1659 1663 c.at_version_num = None
1660 1664 c.is_new = True
1661 1665 rendered_comment = render(
1662 1666 'rhodecode:templates/changeset/changeset_comment_block.mako',
1663 1667 self._get_template_context(c), self.request)
1664 1668
1665 1669 data[comment_id].update(comment.get_dict())
1666 1670 data[comment_id].update({'rendered_text': rendered_comment})
1667 1671
1668 1672 Session().commit()
1669 1673
1670 1674 # skip channelstream for draft comments
1671 1675 if not all_drafts:
1672 1676 comment_broadcast_channel = channelstream.comment_channel(
1673 1677 self.db_repo_name, pull_request_obj=pull_request)
1674 1678
1675 1679 comment_data = data
1676 1680 posted_comment_type = 'inline' if is_inline else 'general'
1677 1681 if len(data) == 1:
1678 1682 msg = _('posted {} new {} comment').format(len(data), posted_comment_type)
1679 1683 else:
1680 1684 msg = _('posted {} new {} comments').format(len(data), posted_comment_type)
1681 1685
1682 1686 channelstream.comment_channelstream_push(
1683 1687 self.request, comment_broadcast_channel, self._rhodecode_user, msg,
1684 1688 comment_data=comment_data)
1685 1689
1686 1690 return data
1687 1691
1688 1692 @LoginRequired()
1689 1693 @NotAnonymous()
1690 1694 @HasRepoPermissionAnyDecorator(
1691 1695 'repository.read', 'repository.write', 'repository.admin')
1692 1696 @CSRFRequired()
1693 1697 def pull_request_comment_create(self):
1694 1698 _ = self.request.translate
1695 1699
1696 1700 pull_request = PullRequest.get_or_404(self.request.matchdict['pull_request_id'])
1697 1701
1698 1702 if pull_request.is_closed():
1699 1703 log.debug('comment: forbidden because pull request is closed')
1700 1704 raise HTTPForbidden()
1701 1705
1702 1706 allowed_to_comment = PullRequestModel().check_user_comment(
1703 1707 pull_request, self._rhodecode_user)
1704 1708 if not allowed_to_comment:
1705 1709 log.debug('comment: forbidden because pull request is from forbidden repo')
1706 1710 raise HTTPForbidden()
1707 1711
1708 1712 comment_data = {
1709 1713 'comment_type': self.request.POST.get('comment_type'),
1710 1714 'text': self.request.POST.get('text'),
1711 1715 'status': self.request.POST.get('changeset_status', None),
1712 1716 'is_draft': self.request.POST.get('draft'),
1713 1717 'resolves_comment_id': self.request.POST.get('resolves_comment_id', None),
1714 1718 'close_pull_request': self.request.POST.get('close_pull_request'),
1715 1719 'f_path': self.request.POST.get('f_path'),
1716 1720 'line': self.request.POST.get('line'),
1717 1721 }
1718 1722 data = self._pull_request_comments_create(pull_request, [comment_data])
1719 1723
1720 1724 return data
1721 1725
1722 1726 @LoginRequired()
1723 1727 @NotAnonymous()
1724 1728 @HasRepoPermissionAnyDecorator(
1725 1729 'repository.read', 'repository.write', 'repository.admin')
1726 1730 @CSRFRequired()
1727 1731 def pull_request_comment_delete(self):
1728 1732 pull_request = PullRequest.get_or_404(
1729 1733 self.request.matchdict['pull_request_id'])
1730 1734
1731 1735 comment = ChangesetComment.get_or_404(
1732 1736 self.request.matchdict['comment_id'])
1733 1737 comment_id = comment.comment_id
1734 1738
1735 1739 if comment.immutable:
1736 1740 # don't allow deleting comments that are immutable
1737 1741 raise HTTPForbidden()
1738 1742
1739 1743 if pull_request.is_closed():
1740 1744 log.debug('comment: forbidden because pull request is closed')
1741 1745 raise HTTPForbidden()
1742 1746
1743 1747 if not comment:
1744 1748 log.debug('Comment with id:%s not found, skipping', comment_id)
1745 1749 # comment already deleted in another call probably
1746 1750 return True
1747 1751
1748 1752 if comment.pull_request.is_closed():
1749 1753 # don't allow deleting comments on closed pull request
1750 1754 raise HTTPForbidden()
1751 1755
1752 1756 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1753 1757 super_admin = h.HasPermissionAny('hg.admin')()
1754 1758 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1755 1759 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1756 1760 comment_repo_admin = is_repo_admin and is_repo_comment
1757 1761
1758 1762 if comment.draft and not comment_owner:
1759 1763 # We never allow to delete draft comments for other than owners
1760 1764 raise HTTPNotFound()
1761 1765
1762 1766 if super_admin or comment_owner or comment_repo_admin:
1763 1767 old_calculated_status = comment.pull_request.calculated_review_status()
1764 1768 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
1765 1769 Session().commit()
1766 1770 calculated_status = comment.pull_request.calculated_review_status()
1767 1771 if old_calculated_status != calculated_status:
1768 1772 PullRequestModel().trigger_pull_request_hook(
1769 1773 comment.pull_request, self._rhodecode_user, 'review_status_change',
1770 1774 data={'status': calculated_status})
1771 1775 return True
1772 1776 else:
1773 1777 log.warning('No permissions for user %s to delete comment_id: %s',
1774 1778 self._rhodecode_db_user, comment_id)
1775 1779 raise HTTPNotFound()
1776 1780
1777 1781 @LoginRequired()
1778 1782 @NotAnonymous()
1779 1783 @HasRepoPermissionAnyDecorator(
1780 1784 'repository.read', 'repository.write', 'repository.admin')
1781 1785 @CSRFRequired()
1782 1786 def pull_request_comment_edit(self):
1783 1787 self.load_default_context()
1784 1788
1785 1789 pull_request = PullRequest.get_or_404(
1786 1790 self.request.matchdict['pull_request_id']
1787 1791 )
1788 1792 comment = ChangesetComment.get_or_404(
1789 1793 self.request.matchdict['comment_id']
1790 1794 )
1791 1795 comment_id = comment.comment_id
1792 1796
1793 1797 if comment.immutable:
1794 1798 # don't allow deleting comments that are immutable
1795 1799 raise HTTPForbidden()
1796 1800
1797 1801 if pull_request.is_closed():
1798 1802 log.debug('comment: forbidden because pull request is closed')
1799 1803 raise HTTPForbidden()
1800 1804
1801 1805 if comment.pull_request.is_closed():
1802 1806 # don't allow deleting comments on closed pull request
1803 1807 raise HTTPForbidden()
1804 1808
1805 1809 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1806 1810 super_admin = h.HasPermissionAny('hg.admin')()
1807 1811 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1808 1812 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1809 1813 comment_repo_admin = is_repo_admin and is_repo_comment
1810 1814
1811 1815 if super_admin or comment_owner or comment_repo_admin:
1812 1816 text = self.request.POST.get('text')
1813 1817 version = self.request.POST.get('version')
1814 1818 if text == comment.text:
1815 1819 log.warning(
1816 1820 'Comment(PR): '
1817 1821 'Trying to create new version '
1818 1822 'with the same comment body {}'.format(
1819 1823 comment_id,
1820 1824 )
1821 1825 )
1822 1826 raise HTTPNotFound()
1823 1827
1824 1828 if version.isdigit():
1825 1829 version = int(version)
1826 1830 else:
1827 1831 log.warning(
1828 1832 'Comment(PR): Wrong version type {} {} '
1829 1833 'for comment {}'.format(
1830 1834 version,
1831 1835 type(version),
1832 1836 comment_id,
1833 1837 )
1834 1838 )
1835 1839 raise HTTPNotFound()
1836 1840
1837 1841 try:
1838 1842 comment_history = CommentsModel().edit(
1839 1843 comment_id=comment_id,
1840 1844 text=text,
1841 1845 auth_user=self._rhodecode_user,
1842 1846 version=version,
1843 1847 )
1844 1848 except CommentVersionMismatch:
1845 1849 raise HTTPConflict()
1846 1850
1847 1851 if not comment_history:
1848 1852 raise HTTPNotFound()
1849 1853
1850 1854 Session().commit()
1851 1855 if not comment.draft:
1852 1856 PullRequestModel().trigger_pull_request_hook(
1853 1857 pull_request, self._rhodecode_user, 'comment_edit',
1854 1858 data={'comment': comment})
1855 1859
1856 1860 return {
1857 1861 'comment_history_id': comment_history.comment_history_id,
1858 1862 'comment_id': comment.comment_id,
1859 1863 'comment_version': comment_history.version,
1860 1864 'comment_author_username': comment_history.author.username,
1861 1865 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16),
1862 1866 'comment_created_on': h.age_component(comment_history.created_on,
1863 1867 time_is_local=True),
1864 1868 }
1865 1869 else:
1866 1870 log.warning('No permissions for user %s to edit comment_id: %s',
1867 1871 self._rhodecode_db_user, comment_id)
1868 1872 raise HTTPNotFound()
@@ -1,1074 +1,1148 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2020 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 Some simple helper functions
24 24 """
25 25
26 26 import collections
27 27 import datetime
28 28 import dateutil.relativedelta
29 29 import hashlib
30 30 import logging
31 31 import re
32 32 import sys
33 33 import time
34 34 import urllib
35 35 import urlobject
36 36 import uuid
37 37 import getpass
38 from functools import update_wrapper, partial
38 from functools import update_wrapper, partial, wraps
39 39
40 40 import pygments.lexers
41 41 import sqlalchemy
42 42 import sqlalchemy.engine.url
43 43 import sqlalchemy.exc
44 44 import sqlalchemy.sql
45 45 import webob
46 46 import pyramid.threadlocal
47 47 from pyramid import compat
48 48 from pyramid.settings import asbool
49 49
50 50 import rhodecode
51 51 from rhodecode.translation import _, _pluralize
52 52
53 53
54 54 def md5(s):
55 55 return hashlib.md5(s).hexdigest()
56 56
57 57
58 58 def md5_safe(s):
59 59 return md5(safe_str(s))
60 60
61 61
62 62 def sha1(s):
63 63 return hashlib.sha1(s).hexdigest()
64 64
65 65
66 66 def sha1_safe(s):
67 67 return sha1(safe_str(s))
68 68
69 69
70 70 def __get_lem(extra_mapping=None):
71 71 """
72 72 Get language extension map based on what's inside pygments lexers
73 73 """
74 74 d = collections.defaultdict(lambda: [])
75 75
76 76 def __clean(s):
77 77 s = s.lstrip('*')
78 78 s = s.lstrip('.')
79 79
80 80 if s.find('[') != -1:
81 81 exts = []
82 82 start, stop = s.find('['), s.find(']')
83 83
84 84 for suffix in s[start + 1:stop]:
85 85 exts.append(s[:s.find('[')] + suffix)
86 86 return [e.lower() for e in exts]
87 87 else:
88 88 return [s.lower()]
89 89
90 90 for lx, t in sorted(pygments.lexers.LEXERS.items()):
91 91 m = map(__clean, t[-2])
92 92 if m:
93 93 m = reduce(lambda x, y: x + y, m)
94 94 for ext in m:
95 95 desc = lx.replace('Lexer', '')
96 96 d[ext].append(desc)
97 97
98 98 data = dict(d)
99 99
100 100 extra_mapping = extra_mapping or {}
101 101 if extra_mapping:
102 102 for k, v in extra_mapping.items():
103 103 if k not in data:
104 104 # register new mapping2lexer
105 105 data[k] = [v]
106 106
107 107 return data
108 108
109 109
110 110 def str2bool(_str):
111 111 """
112 112 returns True/False value from given string, it tries to translate the
113 113 string into boolean
114 114
115 115 :param _str: string value to translate into boolean
116 116 :rtype: boolean
117 117 :returns: boolean from given string
118 118 """
119 119 if _str is None:
120 120 return False
121 121 if _str in (True, False):
122 122 return _str
123 123 _str = str(_str).strip().lower()
124 124 return _str in ('t', 'true', 'y', 'yes', 'on', '1')
125 125
126 126
127 127 def aslist(obj, sep=None, strip=True):
128 128 """
129 129 Returns given string separated by sep as list
130 130
131 131 :param obj:
132 132 :param sep:
133 133 :param strip:
134 134 """
135 135 if isinstance(obj, (basestring,)):
136 136 lst = obj.split(sep)
137 137 if strip:
138 138 lst = [v.strip() for v in lst]
139 139 return lst
140 140 elif isinstance(obj, (list, tuple)):
141 141 return obj
142 142 elif obj is None:
143 143 return []
144 144 else:
145 145 return [obj]
146 146
147 147
148 148 def convert_line_endings(line, mode):
149 149 """
150 150 Converts a given line "line end" accordingly to given mode
151 151
152 152 Available modes are::
153 153 0 - Unix
154 154 1 - Mac
155 155 2 - DOS
156 156
157 157 :param line: given line to convert
158 158 :param mode: mode to convert to
159 159 :rtype: str
160 160 :return: converted line according to mode
161 161 """
162 162 if mode == 0:
163 163 line = line.replace('\r\n', '\n')
164 164 line = line.replace('\r', '\n')
165 165 elif mode == 1:
166 166 line = line.replace('\r\n', '\r')
167 167 line = line.replace('\n', '\r')
168 168 elif mode == 2:
169 169 line = re.sub('\r(?!\n)|(?<!\r)\n', '\r\n', line)
170 170 return line
171 171
172 172
173 173 def detect_mode(line, default):
174 174 """
175 175 Detects line break for given line, if line break couldn't be found
176 176 given default value is returned
177 177
178 178 :param line: str line
179 179 :param default: default
180 180 :rtype: int
181 181 :return: value of line end on of 0 - Unix, 1 - Mac, 2 - DOS
182 182 """
183 183 if line.endswith('\r\n'):
184 184 return 2
185 185 elif line.endswith('\n'):
186 186 return 0
187 187 elif line.endswith('\r'):
188 188 return 1
189 189 else:
190 190 return default
191 191
192 192
193 193 def safe_int(val, default=None):
194 194 """
195 195 Returns int() of val if val is not convertable to int use default
196 196 instead
197 197
198 198 :param val:
199 199 :param default:
200 200 """
201 201
202 202 try:
203 203 val = int(val)
204 204 except (ValueError, TypeError):
205 205 val = default
206 206
207 207 return val
208 208
209 209
210 210 def safe_unicode(str_, from_encoding=None, use_chardet=False):
211 211 """
212 212 safe unicode function. Does few trick to turn str_ into unicode
213 213
214 214 In case of UnicodeDecode error, we try to return it with encoding detected
215 215 by chardet library if it fails fallback to unicode with errors replaced
216 216
217 217 :param str_: string to decode
218 218 :rtype: unicode
219 219 :returns: unicode object
220 220 """
221 221 if isinstance(str_, unicode):
222 222 return str_
223 223
224 224 if not from_encoding:
225 225 DEFAULT_ENCODINGS = aslist(rhodecode.CONFIG.get('default_encoding',
226 226 'utf8'), sep=',')
227 227 from_encoding = DEFAULT_ENCODINGS
228 228
229 229 if not isinstance(from_encoding, (list, tuple)):
230 230 from_encoding = [from_encoding]
231 231
232 232 try:
233 233 return unicode(str_)
234 234 except UnicodeDecodeError:
235 235 pass
236 236
237 237 for enc in from_encoding:
238 238 try:
239 239 return unicode(str_, enc)
240 240 except UnicodeDecodeError:
241 241 pass
242 242
243 243 if use_chardet:
244 244 try:
245 245 import chardet
246 246 encoding = chardet.detect(str_)['encoding']
247 247 if encoding is None:
248 248 raise Exception()
249 249 return str_.decode(encoding)
250 250 except (ImportError, UnicodeDecodeError, Exception):
251 251 return unicode(str_, from_encoding[0], 'replace')
252 252 else:
253 253 return unicode(str_, from_encoding[0], 'replace')
254 254
255 255 def safe_str(unicode_, to_encoding=None, use_chardet=False):
256 256 """
257 257 safe str function. Does few trick to turn unicode_ into string
258 258
259 259 In case of UnicodeEncodeError, we try to return it with encoding detected
260 260 by chardet library if it fails fallback to string with errors replaced
261 261
262 262 :param unicode_: unicode to encode
263 263 :rtype: str
264 264 :returns: str object
265 265 """
266 266
267 267 # if it's not basestr cast to str
268 268 if not isinstance(unicode_, compat.string_types):
269 269 return str(unicode_)
270 270
271 271 if isinstance(unicode_, str):
272 272 return unicode_
273 273
274 274 if not to_encoding:
275 275 DEFAULT_ENCODINGS = aslist(rhodecode.CONFIG.get('default_encoding',
276 276 'utf8'), sep=',')
277 277 to_encoding = DEFAULT_ENCODINGS
278 278
279 279 if not isinstance(to_encoding, (list, tuple)):
280 280 to_encoding = [to_encoding]
281 281
282 282 for enc in to_encoding:
283 283 try:
284 284 return unicode_.encode(enc)
285 285 except UnicodeEncodeError:
286 286 pass
287 287
288 288 if use_chardet:
289 289 try:
290 290 import chardet
291 291 encoding = chardet.detect(unicode_)['encoding']
292 292 if encoding is None:
293 293 raise UnicodeEncodeError()
294 294
295 295 return unicode_.encode(encoding)
296 296 except (ImportError, UnicodeEncodeError):
297 297 return unicode_.encode(to_encoding[0], 'replace')
298 298 else:
299 299 return unicode_.encode(to_encoding[0], 'replace')
300 300
301 301
302 302 def remove_suffix(s, suffix):
303 303 if s.endswith(suffix):
304 304 s = s[:-1 * len(suffix)]
305 305 return s
306 306
307 307
308 308 def remove_prefix(s, prefix):
309 309 if s.startswith(prefix):
310 310 s = s[len(prefix):]
311 311 return s
312 312
313 313
314 314 def find_calling_context(ignore_modules=None):
315 315 """
316 316 Look through the calling stack and return the frame which called
317 317 this function and is part of core module ( ie. rhodecode.* )
318 318
319 319 :param ignore_modules: list of modules to ignore eg. ['rhodecode.lib']
320 320 """
321 321
322 322 ignore_modules = ignore_modules or []
323 323
324 324 f = sys._getframe(2)
325 325 while f.f_back is not None:
326 326 name = f.f_globals.get('__name__')
327 327 if name and name.startswith(__name__.split('.')[0]):
328 328 if name not in ignore_modules:
329 329 return f
330 330 f = f.f_back
331 331 return None
332 332
333 333
334 334 def ping_connection(connection, branch):
335 335 if branch:
336 336 # "branch" refers to a sub-connection of a connection,
337 337 # we don't want to bother pinging on these.
338 338 return
339 339
340 340 # turn off "close with result". This flag is only used with
341 341 # "connectionless" execution, otherwise will be False in any case
342 342 save_should_close_with_result = connection.should_close_with_result
343 343 connection.should_close_with_result = False
344 344
345 345 try:
346 346 # run a SELECT 1. use a core select() so that
347 347 # the SELECT of a scalar value without a table is
348 348 # appropriately formatted for the backend
349 349 connection.scalar(sqlalchemy.sql.select([1]))
350 350 except sqlalchemy.exc.DBAPIError as err:
351 351 # catch SQLAlchemy's DBAPIError, which is a wrapper
352 352 # for the DBAPI's exception. It includes a .connection_invalidated
353 353 # attribute which specifies if this connection is a "disconnect"
354 354 # condition, which is based on inspection of the original exception
355 355 # by the dialect in use.
356 356 if err.connection_invalidated:
357 357 # run the same SELECT again - the connection will re-validate
358 358 # itself and establish a new connection. The disconnect detection
359 359 # here also causes the whole connection pool to be invalidated
360 360 # so that all stale connections are discarded.
361 361 connection.scalar(sqlalchemy.sql.select([1]))
362 362 else:
363 363 raise
364 364 finally:
365 365 # restore "close with result"
366 366 connection.should_close_with_result = save_should_close_with_result
367 367
368 368
369 369 def engine_from_config(configuration, prefix='sqlalchemy.', **kwargs):
370 370 """Custom engine_from_config functions."""
371 371 log = logging.getLogger('sqlalchemy.engine')
372 372 use_ping_connection = asbool(configuration.pop('sqlalchemy.db1.ping_connection', None))
373 373 debug = asbool(configuration.pop('sqlalchemy.db1.debug_query', None))
374 374
375 375 engine = sqlalchemy.engine_from_config(configuration, prefix, **kwargs)
376 376
377 377 def color_sql(sql):
378 378 color_seq = '\033[1;33m' # This is yellow: code 33
379 379 normal = '\x1b[0m'
380 380 return ''.join([color_seq, sql, normal])
381 381
382 382 if use_ping_connection:
383 383 log.debug('Adding ping_connection on the engine config.')
384 384 sqlalchemy.event.listen(engine, "engine_connect", ping_connection)
385 385
386 386 if debug:
387 387 # attach events only for debug configuration
388 388 def before_cursor_execute(conn, cursor, statement,
389 389 parameters, context, executemany):
390 390 setattr(conn, 'query_start_time', time.time())
391 391 log.info(color_sql(">>>>> STARTING QUERY >>>>>"))
392 392 calling_context = find_calling_context(ignore_modules=[
393 393 'rhodecode.lib.caching_query',
394 394 'rhodecode.model.settings',
395 395 ])
396 396 if calling_context:
397 397 log.info(color_sql('call context %s:%s' % (
398 398 calling_context.f_code.co_filename,
399 399 calling_context.f_lineno,
400 400 )))
401 401
402 402 def after_cursor_execute(conn, cursor, statement,
403 403 parameters, context, executemany):
404 404 delattr(conn, 'query_start_time')
405 405
406 406 sqlalchemy.event.listen(engine, "before_cursor_execute", before_cursor_execute)
407 407 sqlalchemy.event.listen(engine, "after_cursor_execute", after_cursor_execute)
408 408
409 409 return engine
410 410
411 411
412 412 def get_encryption_key(config):
413 413 secret = config.get('rhodecode.encrypted_values.secret')
414 414 default = config['beaker.session.secret']
415 415 return secret or default
416 416
417 417
418 418 def age(prevdate, now=None, show_short_version=False, show_suffix=True,
419 419 short_format=False):
420 420 """
421 421 Turns a datetime into an age string.
422 422 If show_short_version is True, this generates a shorter string with
423 423 an approximate age; ex. '1 day ago', rather than '1 day and 23 hours ago'.
424 424
425 425 * IMPORTANT*
426 426 Code of this function is written in special way so it's easier to
427 427 backport it to javascript. If you mean to update it, please also update
428 428 `jquery.timeago-extension.js` file
429 429
430 430 :param prevdate: datetime object
431 431 :param now: get current time, if not define we use
432 432 `datetime.datetime.now()`
433 433 :param show_short_version: if it should approximate the date and
434 434 return a shorter string
435 435 :param show_suffix:
436 436 :param short_format: show short format, eg 2D instead of 2 days
437 437 :rtype: unicode
438 438 :returns: unicode words describing age
439 439 """
440 440
441 441 def _get_relative_delta(now, prevdate):
442 442 base = dateutil.relativedelta.relativedelta(now, prevdate)
443 443 return {
444 444 'year': base.years,
445 445 'month': base.months,
446 446 'day': base.days,
447 447 'hour': base.hours,
448 448 'minute': base.minutes,
449 449 'second': base.seconds,
450 450 }
451 451
452 452 def _is_leap_year(year):
453 453 return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)
454 454
455 455 def get_month(prevdate):
456 456 return prevdate.month
457 457
458 458 def get_year(prevdate):
459 459 return prevdate.year
460 460
461 461 now = now or datetime.datetime.now()
462 462 order = ['year', 'month', 'day', 'hour', 'minute', 'second']
463 463 deltas = {}
464 464 future = False
465 465
466 466 if prevdate > now:
467 467 now_old = now
468 468 now = prevdate
469 469 prevdate = now_old
470 470 future = True
471 471 if future:
472 472 prevdate = prevdate.replace(microsecond=0)
473 473 # Get date parts deltas
474 474 for part in order:
475 475 rel_delta = _get_relative_delta(now, prevdate)
476 476 deltas[part] = rel_delta[part]
477 477
478 478 # Fix negative offsets (there is 1 second between 10:59:59 and 11:00:00,
479 479 # not 1 hour, -59 minutes and -59 seconds)
480 480 offsets = [[5, 60], [4, 60], [3, 24]]
481 481 for element in offsets: # seconds, minutes, hours
482 482 num = element[0]
483 483 length = element[1]
484 484
485 485 part = order[num]
486 486 carry_part = order[num - 1]
487 487
488 488 if deltas[part] < 0:
489 489 deltas[part] += length
490 490 deltas[carry_part] -= 1
491 491
492 492 # Same thing for days except that the increment depends on the (variable)
493 493 # number of days in the month
494 494 month_lengths = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
495 495 if deltas['day'] < 0:
496 496 if get_month(prevdate) == 2 and _is_leap_year(get_year(prevdate)):
497 497 deltas['day'] += 29
498 498 else:
499 499 deltas['day'] += month_lengths[get_month(prevdate) - 1]
500 500
501 501 deltas['month'] -= 1
502 502
503 503 if deltas['month'] < 0:
504 504 deltas['month'] += 12
505 505 deltas['year'] -= 1
506 506
507 507 # Format the result
508 508 if short_format:
509 509 fmt_funcs = {
510 510 'year': lambda d: u'%dy' % d,
511 511 'month': lambda d: u'%dm' % d,
512 512 'day': lambda d: u'%dd' % d,
513 513 'hour': lambda d: u'%dh' % d,
514 514 'minute': lambda d: u'%dmin' % d,
515 515 'second': lambda d: u'%dsec' % d,
516 516 }
517 517 else:
518 518 fmt_funcs = {
519 519 'year': lambda d: _pluralize(u'${num} year', u'${num} years', d, mapping={'num': d}).interpolate(),
520 520 'month': lambda d: _pluralize(u'${num} month', u'${num} months', d, mapping={'num': d}).interpolate(),
521 521 'day': lambda d: _pluralize(u'${num} day', u'${num} days', d, mapping={'num': d}).interpolate(),
522 522 'hour': lambda d: _pluralize(u'${num} hour', u'${num} hours', d, mapping={'num': d}).interpolate(),
523 523 'minute': lambda d: _pluralize(u'${num} minute', u'${num} minutes', d, mapping={'num': d}).interpolate(),
524 524 'second': lambda d: _pluralize(u'${num} second', u'${num} seconds', d, mapping={'num': d}).interpolate(),
525 525 }
526 526
527 527 i = 0
528 528 for part in order:
529 529 value = deltas[part]
530 530 if value != 0:
531 531
532 532 if i < 5:
533 533 sub_part = order[i + 1]
534 534 sub_value = deltas[sub_part]
535 535 else:
536 536 sub_value = 0
537 537
538 538 if sub_value == 0 or show_short_version:
539 539 _val = fmt_funcs[part](value)
540 540 if future:
541 541 if show_suffix:
542 542 return _(u'in ${ago}', mapping={'ago': _val})
543 543 else:
544 544 return _(_val)
545 545
546 546 else:
547 547 if show_suffix:
548 548 return _(u'${ago} ago', mapping={'ago': _val})
549 549 else:
550 550 return _(_val)
551 551
552 552 val = fmt_funcs[part](value)
553 553 val_detail = fmt_funcs[sub_part](sub_value)
554 554 mapping = {'val': val, 'detail': val_detail}
555 555
556 556 if short_format:
557 557 datetime_tmpl = _(u'${val}, ${detail}', mapping=mapping)
558 558 if show_suffix:
559 559 datetime_tmpl = _(u'${val}, ${detail} ago', mapping=mapping)
560 560 if future:
561 561 datetime_tmpl = _(u'in ${val}, ${detail}', mapping=mapping)
562 562 else:
563 563 datetime_tmpl = _(u'${val} and ${detail}', mapping=mapping)
564 564 if show_suffix:
565 565 datetime_tmpl = _(u'${val} and ${detail} ago', mapping=mapping)
566 566 if future:
567 567 datetime_tmpl = _(u'in ${val} and ${detail}', mapping=mapping)
568 568
569 569 return datetime_tmpl
570 570 i += 1
571 571 return _(u'just now')
572 572
573 573
574 574 def age_from_seconds(seconds):
575 575 seconds = safe_int(seconds) or 0
576 576 prevdate = time_to_datetime(time.time() + seconds)
577 577 return age(prevdate, show_suffix=False, show_short_version=True)
578 578
579 579
580 580 def cleaned_uri(uri):
581 581 """
582 582 Quotes '[' and ']' from uri if there is only one of them.
583 583 according to RFC3986 we cannot use such chars in uri
584 584 :param uri:
585 585 :return: uri without this chars
586 586 """
587 587 return urllib.quote(uri, safe='@$:/')
588 588
589 589
590 590 def credentials_filter(uri):
591 591 """
592 592 Returns a url with removed credentials
593 593
594 594 :param uri:
595 595 """
596 596 import urlobject
597 597 if isinstance(uri, rhodecode.lib.encrypt.InvalidDecryptedValue):
598 598 return 'InvalidDecryptionKey'
599 599
600 600 url_obj = urlobject.URLObject(cleaned_uri(uri))
601 601 url_obj = url_obj.without_password().without_username()
602 602
603 603 return url_obj
604 604
605 605
606 606 def get_host_info(request):
607 607 """
608 608 Generate host info, to obtain full url e.g https://server.com
609 609 use this
610 610 `{scheme}://{netloc}`
611 611 """
612 612 if not request:
613 613 return {}
614 614
615 615 qualified_home_url = request.route_url('home')
616 616 parsed_url = urlobject.URLObject(qualified_home_url)
617 617 decoded_path = safe_unicode(urllib.unquote(parsed_url.path.rstrip('/')))
618 618
619 619 return {
620 620 'scheme': parsed_url.scheme,
621 621 'netloc': parsed_url.netloc+decoded_path,
622 622 'hostname': parsed_url.hostname,
623 623 }
624 624
625 625
626 626 def get_clone_url(request, uri_tmpl, repo_name, repo_id, repo_type, **override):
627 627 qualified_home_url = request.route_url('home')
628 628 parsed_url = urlobject.URLObject(qualified_home_url)
629 629 decoded_path = safe_unicode(urllib.unquote(parsed_url.path.rstrip('/')))
630 630
631 631 args = {
632 632 'scheme': parsed_url.scheme,
633 633 'user': '',
634 634 'sys_user': getpass.getuser(),
635 635 # path if we use proxy-prefix
636 636 'netloc': parsed_url.netloc+decoded_path,
637 637 'hostname': parsed_url.hostname,
638 638 'prefix': decoded_path,
639 639 'repo': repo_name,
640 640 'repoid': str(repo_id),
641 641 'repo_type': repo_type
642 642 }
643 643 args.update(override)
644 644 args['user'] = urllib.quote(safe_str(args['user']))
645 645
646 646 for k, v in args.items():
647 647 uri_tmpl = uri_tmpl.replace('{%s}' % k, v)
648 648
649 649 # special case for SVN clone url
650 650 if repo_type == 'svn':
651 651 uri_tmpl = uri_tmpl.replace('ssh://', 'svn+ssh://')
652 652
653 653 # remove leading @ sign if it's present. Case of empty user
654 654 url_obj = urlobject.URLObject(uri_tmpl)
655 655 url = url_obj.with_netloc(url_obj.netloc.lstrip('@'))
656 656
657 657 return safe_unicode(url)
658 658
659 659
660 660 def get_commit_safe(repo, commit_id=None, commit_idx=None, pre_load=None,
661 661 maybe_unreachable=False, reference_obj=None):
662 662 """
663 663 Safe version of get_commit if this commit doesn't exists for a
664 664 repository it returns a Dummy one instead
665 665
666 666 :param repo: repository instance
667 667 :param commit_id: commit id as str
668 668 :param commit_idx: numeric commit index
669 669 :param pre_load: optional list of commit attributes to load
670 670 :param maybe_unreachable: translate unreachable commits on git repos
671 671 :param reference_obj: explicitly search via a reference obj in git. E.g "branch:123" would mean branch "123"
672 672 """
673 673 # TODO(skreft): remove these circular imports
674 674 from rhodecode.lib.vcs.backends.base import BaseRepository, EmptyCommit
675 675 from rhodecode.lib.vcs.exceptions import RepositoryError
676 676 if not isinstance(repo, BaseRepository):
677 677 raise Exception('You must pass an Repository '
678 678 'object as first argument got %s', type(repo))
679 679
680 680 try:
681 681 commit = repo.get_commit(
682 682 commit_id=commit_id, commit_idx=commit_idx, pre_load=pre_load,
683 683 maybe_unreachable=maybe_unreachable, reference_obj=reference_obj)
684 684 except (RepositoryError, LookupError):
685 685 commit = EmptyCommit()
686 686 return commit
687 687
688 688
689 689 def datetime_to_time(dt):
690 690 if dt:
691 691 return time.mktime(dt.timetuple())
692 692
693 693
694 694 def time_to_datetime(tm):
695 695 if tm:
696 696 if isinstance(tm, compat.string_types):
697 697 try:
698 698 tm = float(tm)
699 699 except ValueError:
700 700 return
701 701 return datetime.datetime.fromtimestamp(tm)
702 702
703 703
704 704 def time_to_utcdatetime(tm):
705 705 if tm:
706 706 if isinstance(tm, compat.string_types):
707 707 try:
708 708 tm = float(tm)
709 709 except ValueError:
710 710 return
711 711 return datetime.datetime.utcfromtimestamp(tm)
712 712
713 713
714 714 MENTIONS_REGEX = re.compile(
715 715 # ^@ or @ without any special chars in front
716 716 r'(?:^@|[^a-zA-Z0-9\-\_\.]@)'
717 717 # main body starts with letter, then can be . - _
718 718 r'([a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]+)',
719 719 re.VERBOSE | re.MULTILINE)
720 720
721 721
722 722 def extract_mentioned_users(s):
723 723 """
724 724 Returns unique usernames from given string s that have @mention
725 725
726 726 :param s: string to get mentions
727 727 """
728 728 usrs = set()
729 729 for username in MENTIONS_REGEX.findall(s):
730 730 usrs.add(username)
731 731
732 732 return sorted(list(usrs), key=lambda k: k.lower())
733 733
734 734
735 735 class AttributeDictBase(dict):
736 736 def __getstate__(self):
737 737 odict = self.__dict__ # get attribute dictionary
738 738 return odict
739 739
740 740 def __setstate__(self, dict):
741 741 self.__dict__ = dict
742 742
743 743 __setattr__ = dict.__setitem__
744 744 __delattr__ = dict.__delitem__
745 745
746 746
747 747 class StrictAttributeDict(AttributeDictBase):
748 748 """
749 749 Strict Version of Attribute dict which raises an Attribute error when
750 750 requested attribute is not set
751 751 """
752 752 def __getattr__(self, attr):
753 753 try:
754 754 return self[attr]
755 755 except KeyError:
756 756 raise AttributeError('%s object has no attribute %s' % (
757 757 self.__class__, attr))
758 758
759 759
760 760 class AttributeDict(AttributeDictBase):
761 761 def __getattr__(self, attr):
762 762 return self.get(attr, None)
763 763
764 764
765 765
766 766 class OrderedDefaultDict(collections.OrderedDict, collections.defaultdict):
767 767 def __init__(self, default_factory=None, *args, **kwargs):
768 768 # in python3 you can omit the args to super
769 769 super(OrderedDefaultDict, self).__init__(*args, **kwargs)
770 770 self.default_factory = default_factory
771 771
772 772
773 773 def fix_PATH(os_=None):
774 774 """
775 775 Get current active python path, and append it to PATH variable to fix
776 776 issues of subprocess calls and different python versions
777 777 """
778 778 if os_ is None:
779 779 import os
780 780 else:
781 781 os = os_
782 782
783 783 cur_path = os.path.split(sys.executable)[0]
784 784 if not os.environ['PATH'].startswith(cur_path):
785 785 os.environ['PATH'] = '%s:%s' % (cur_path, os.environ['PATH'])
786 786
787 787
788 788 def obfuscate_url_pw(engine):
789 789 _url = engine or ''
790 790 try:
791 791 _url = sqlalchemy.engine.url.make_url(engine)
792 792 if _url.password:
793 793 _url.password = 'XXXXX'
794 794 except Exception:
795 795 pass
796 796 return unicode(_url)
797 797
798 798
799 799 def get_server_url(environ):
800 800 req = webob.Request(environ)
801 801 return req.host_url + req.script_name
802 802
803 803
804 804 def unique_id(hexlen=32):
805 805 alphabet = "23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjklmnpqrstuvwxyz"
806 806 return suuid(truncate_to=hexlen, alphabet=alphabet)
807 807
808 808
809 809 def suuid(url=None, truncate_to=22, alphabet=None):
810 810 """
811 811 Generate and return a short URL safe UUID.
812 812
813 813 If the url parameter is provided, set the namespace to the provided
814 814 URL and generate a UUID.
815 815
816 816 :param url to get the uuid for
817 817 :truncate_to: truncate the basic 22 UUID to shorter version
818 818
819 819 The IDs won't be universally unique any longer, but the probability of
820 820 a collision will still be very low.
821 821 """
822 822 # Define our alphabet.
823 823 _ALPHABET = alphabet or "23456789ABCDEFGHJKLMNPQRSTUVWXYZ"
824 824
825 825 # If no URL is given, generate a random UUID.
826 826 if url is None:
827 827 unique_id = uuid.uuid4().int
828 828 else:
829 829 unique_id = uuid.uuid3(uuid.NAMESPACE_URL, url).int
830 830
831 831 alphabet_length = len(_ALPHABET)
832 832 output = []
833 833 while unique_id > 0:
834 834 digit = unique_id % alphabet_length
835 835 output.append(_ALPHABET[digit])
836 836 unique_id = int(unique_id / alphabet_length)
837 837 return "".join(output)[:truncate_to]
838 838
839 839
840 840 def get_current_rhodecode_user(request=None):
841 841 """
842 842 Gets rhodecode user from request
843 843 """
844 844 pyramid_request = request or pyramid.threadlocal.get_current_request()
845 845
846 846 # web case
847 847 if pyramid_request and hasattr(pyramid_request, 'user'):
848 848 return pyramid_request.user
849 849
850 850 # api case
851 851 if pyramid_request and hasattr(pyramid_request, 'rpc_user'):
852 852 return pyramid_request.rpc_user
853 853
854 854 return None
855 855
856 856
857 857 def action_logger_generic(action, namespace=''):
858 858 """
859 859 A generic logger for actions useful to the system overview, tries to find
860 860 an acting user for the context of the call otherwise reports unknown user
861 861
862 862 :param action: logging message eg 'comment 5 deleted'
863 863 :param type: string
864 864
865 865 :param namespace: namespace of the logging message eg. 'repo.comments'
866 866 :param type: string
867 867
868 868 """
869 869
870 870 logger_name = 'rhodecode.actions'
871 871
872 872 if namespace:
873 873 logger_name += '.' + namespace
874 874
875 875 log = logging.getLogger(logger_name)
876 876
877 877 # get a user if we can
878 878 user = get_current_rhodecode_user()
879 879
880 880 logfunc = log.info
881 881
882 882 if not user:
883 883 user = '<unknown user>'
884 884 logfunc = log.warning
885 885
886 886 logfunc('Logging action by {}: {}'.format(user, action))
887 887
888 888
889 889 def escape_split(text, sep=',', maxsplit=-1):
890 890 r"""
891 891 Allows for escaping of the separator: e.g. arg='foo\, bar'
892 892
893 893 It should be noted that the way bash et. al. do command line parsing, those
894 894 single quotes are required.
895 895 """
896 896 escaped_sep = r'\%s' % sep
897 897
898 898 if escaped_sep not in text:
899 899 return text.split(sep, maxsplit)
900 900
901 901 before, _mid, after = text.partition(escaped_sep)
902 902 startlist = before.split(sep, maxsplit) # a regular split is fine here
903 903 unfinished = startlist[-1]
904 904 startlist = startlist[:-1]
905 905
906 906 # recurse because there may be more escaped separators
907 907 endlist = escape_split(after, sep, maxsplit)
908 908
909 909 # finish building the escaped value. we use endlist[0] becaue the first
910 910 # part of the string sent in recursion is the rest of the escaped value.
911 911 unfinished += sep + endlist[0]
912 912
913 913 return startlist + [unfinished] + endlist[1:] # put together all the parts
914 914
915 915
916 916 class OptionalAttr(object):
917 917 """
918 918 Special Optional Option that defines other attribute. Example::
919 919
920 920 def test(apiuser, userid=Optional(OAttr('apiuser')):
921 921 user = Optional.extract(userid)
922 922 # calls
923 923
924 924 """
925 925
926 926 def __init__(self, attr_name):
927 927 self.attr_name = attr_name
928 928
929 929 def __repr__(self):
930 930 return '<OptionalAttr:%s>' % self.attr_name
931 931
932 932 def __call__(self):
933 933 return self
934 934
935 935
936 936 # alias
937 937 OAttr = OptionalAttr
938 938
939 939
940 940 class Optional(object):
941 941 """
942 942 Defines an optional parameter::
943 943
944 944 param = param.getval() if isinstance(param, Optional) else param
945 945 param = param() if isinstance(param, Optional) else param
946 946
947 947 is equivalent of::
948 948
949 949 param = Optional.extract(param)
950 950
951 951 """
952 952
953 953 def __init__(self, type_):
954 954 self.type_ = type_
955 955
956 956 def __repr__(self):
957 957 return '<Optional:%s>' % self.type_.__repr__()
958 958
959 959 def __call__(self):
960 960 return self.getval()
961 961
962 962 def getval(self):
963 963 """
964 964 returns value from this Optional instance
965 965 """
966 966 if isinstance(self.type_, OAttr):
967 967 # use params name
968 968 return self.type_.attr_name
969 969 return self.type_
970 970
971 971 @classmethod
972 972 def extract(cls, val):
973 973 """
974 974 Extracts value from Optional() instance
975 975
976 976 :param val:
977 977 :return: original value if it's not Optional instance else
978 978 value of instance
979 979 """
980 980 if isinstance(val, cls):
981 981 return val.getval()
982 982 return val
983 983
984 984
985 985 def glob2re(pat):
986 986 """
987 987 Translate a shell PATTERN to a regular expression.
988 988
989 989 There is no way to quote meta-characters.
990 990 """
991 991
992 992 i, n = 0, len(pat)
993 993 res = ''
994 994 while i < n:
995 995 c = pat[i]
996 996 i = i+1
997 997 if c == '*':
998 998 #res = res + '.*'
999 999 res = res + '[^/]*'
1000 1000 elif c == '?':
1001 1001 #res = res + '.'
1002 1002 res = res + '[^/]'
1003 1003 elif c == '[':
1004 1004 j = i
1005 1005 if j < n and pat[j] == '!':
1006 1006 j = j+1
1007 1007 if j < n and pat[j] == ']':
1008 1008 j = j+1
1009 1009 while j < n and pat[j] != ']':
1010 1010 j = j+1
1011 1011 if j >= n:
1012 1012 res = res + '\\['
1013 1013 else:
1014 1014 stuff = pat[i:j].replace('\\','\\\\')
1015 1015 i = j+1
1016 1016 if stuff[0] == '!':
1017 1017 stuff = '^' + stuff[1:]
1018 1018 elif stuff[0] == '^':
1019 1019 stuff = '\\' + stuff
1020 1020 res = '%s[%s]' % (res, stuff)
1021 1021 else:
1022 1022 res = res + re.escape(c)
1023 1023 return res + '\Z(?ms)'
1024 1024
1025 1025
1026 1026 def parse_byte_string(size_str):
1027 1027 match = re.match(r'(\d+)(MB|KB)', size_str, re.IGNORECASE)
1028 1028 if not match:
1029 1029 raise ValueError('Given size:%s is invalid, please make sure '
1030 1030 'to use format of <num>(MB|KB)' % size_str)
1031 1031
1032 1032 _parts = match.groups()
1033 1033 num, type_ = _parts
1034 1034 return long(num) * {'mb': 1024*1024, 'kb': 1024}[type_.lower()]
1035 1035
1036 1036
1037 1037 class CachedProperty(object):
1038 1038 """
1039 1039 Lazy Attributes. With option to invalidate the cache by running a method
1040 1040
1041 class Foo():
1041 >>> class Foo(object):
1042 ...
1043 ... @CachedProperty
1044 ... def heavy_func(self):
1045 ... return 'super-calculation'
1046 ...
1047 ... foo = Foo()
1048 ... foo.heavy_func() # first computation
1049 ... foo.heavy_func() # fetch from cache
1050 ... foo._invalidate_prop_cache('heavy_func')
1042 1051
1043 @CachedProperty
1044 def heavy_func():
1045 return 'super-calculation'
1046
1047 foo = Foo()
1048 foo.heavy_func() # first computions
1049 foo.heavy_func() # fetch from cache
1050 foo._invalidate_prop_cache('heavy_func')
1051 1052 # at this point calling foo.heavy_func() will be re-computed
1052 1053 """
1053 1054
1054 1055 def __init__(self, func, func_name=None):
1055 1056
1056 1057 if func_name is None:
1057 1058 func_name = func.__name__
1058 1059 self.data = (func, func_name)
1059 1060 update_wrapper(self, func)
1060 1061
1061 1062 def __get__(self, inst, class_):
1062 1063 if inst is None:
1063 1064 return self
1064 1065
1065 1066 func, func_name = self.data
1066 1067 value = func(inst)
1067 1068 inst.__dict__[func_name] = value
1068 1069 if '_invalidate_prop_cache' not in inst.__dict__:
1069 1070 inst.__dict__['_invalidate_prop_cache'] = partial(
1070 1071 self._invalidate_prop_cache, inst)
1071 1072 return value
1072 1073
1073 1074 def _invalidate_prop_cache(self, inst, name):
1074 1075 inst.__dict__.pop(name, None)
1076
1077
1078 def retry(func=None, exception=Exception, n_tries=5, delay=5, backoff=1, logger=True):
1079 """
1080 Retry decorator with exponential backoff.
1081
1082 Parameters
1083 ----------
1084 func : typing.Callable, optional
1085 Callable on which the decorator is applied, by default None
1086 exception : Exception or tuple of Exceptions, optional
1087 Exception(s) that invoke retry, by default Exception
1088 n_tries : int, optional
1089 Number of tries before giving up, by default 5
1090 delay : int, optional
1091 Initial delay between retries in seconds, by default 5
1092 backoff : int, optional
1093 Backoff multiplier e.g. value of 2 will double the delay, by default 1
1094 logger : bool, optional
1095 Option to log or print, by default False
1096
1097 Returns
1098 -------
1099 typing.Callable
1100 Decorated callable that calls itself when exception(s) occur.
1101
1102 Examples
1103 --------
1104 >>> import random
1105 >>> @retry(exception=Exception, n_tries=3)
1106 ... def test_random(text):
1107 ... x = random.random()
1108 ... if x < 0.5:
1109 ... raise Exception("Fail")
1110 ... else:
1111 ... print("Success: ", text)
1112 >>> test_random("It works!")
1113 """
1114
1115 if func is None:
1116 return partial(
1117 retry,
1118 exception=exception,
1119 n_tries=n_tries,
1120 delay=delay,
1121 backoff=backoff,
1122 logger=logger,
1123 )
1124
1125 @wraps(func)
1126 def wrapper(*args, **kwargs):
1127 _n_tries, n_delay = n_tries, delay
1128 log = logging.getLogger('rhodecode.retry')
1129
1130 while _n_tries > 1:
1131 try:
1132 return func(*args, **kwargs)
1133 except exception as e:
1134 e_details = repr(e)
1135 msg = "Exception on calling func {func}: {e}, " \
1136 "Retrying in {n_delay} seconds..."\
1137 .format(func=func, e=e_details, n_delay=n_delay)
1138 if logger:
1139 log.warning(msg)
1140 else:
1141 print(msg)
1142 time.sleep(n_delay)
1143 _n_tries -= 1
1144 n_delay *= backoff
1145
1146 return func(*args, **kwargs)
1147
1148 return wrapper
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
General Comments 0
You need to be logged in to leave comments. Login now