##// END OF EJS Templates
fix(pull-requests): fixes for rendering comments
super-admin -
r5211:5e903185 default
parent child Browse files
Show More
@@ -1,1875 +1,1878 b''
1 1 # Copyright (C) 2011-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import logging
20 20 import collections
21 21
22 22 import formencode
23 23 import formencode.htmlfill
24 24 import peppercorn
25 25 from pyramid.httpexceptions import (
26 26 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest, HTTPConflict)
27 27
28 28 from pyramid.renderers import render
29 29
30 30 from rhodecode.apps._base import RepoAppView, DataGridAppView
31 31
32 32 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
33 33 from rhodecode.lib.base import vcs_operation_context
34 34 from rhodecode.lib.diffs import load_cached_diff, cache_diff, diff_cache_exist
35 35 from rhodecode.lib.exceptions import CommentVersionMismatch
36 36 from rhodecode.lib import ext_json
37 37 from rhodecode.lib.auth import (
38 38 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
39 39 NotAnonymous, CSRFRequired)
40 40 from rhodecode.lib.utils2 import str2bool, safe_str, safe_int, aslist, retry
41 41 from rhodecode.lib.vcs.backends.base import (
42 42 EmptyCommit, UpdateFailureReason, unicode_to_reference)
43 43 from rhodecode.lib.vcs.exceptions import (
44 44 CommitDoesNotExistError, RepositoryRequirementError, EmptyRepositoryError)
45 45 from rhodecode.model.changeset_status import ChangesetStatusModel
46 46 from rhodecode.model.comment import CommentsModel
47 47 from rhodecode.model.db import (
48 48 func, false, or_, PullRequest, ChangesetComment, ChangesetStatus, Repository,
49 49 PullRequestReviewers)
50 50 from rhodecode.model.forms import PullRequestForm
51 51 from rhodecode.model.meta import Session
52 52 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
53 53 from rhodecode.model.scm import ScmModel
54 54
55 55 log = logging.getLogger(__name__)
56 56
57 57
58 58 class RepoPullRequestsView(RepoAppView, DataGridAppView):
59 59
60 60 def load_default_context(self):
61 61 c = self._get_local_tmpl_context(include_app_defaults=True)
62 62 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
63 63 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
64 64 # backward compat., we use for OLD PRs a plain renderer
65 65 c.renderer = 'plain'
66 66 return c
67 67
68 68 def _get_pull_requests_list(
69 69 self, repo_name, source, filter_type, opened_by, statuses):
70 70
71 71 draw, start, limit = self._extract_chunk(self.request)
72 72 search_q, order_by, order_dir = self._extract_ordering(self.request)
73 73 _render = self.request.get_partial_renderer(
74 74 'rhodecode:templates/data_table/_dt_elements.mako')
75 75
76 76 # pagination
77 77
78 78 if filter_type == 'awaiting_review':
79 79 pull_requests = PullRequestModel().get_awaiting_review(
80 80 repo_name,
81 81 search_q=search_q, statuses=statuses,
82 82 offset=start, length=limit, order_by=order_by, order_dir=order_dir)
83 83 pull_requests_total_count = PullRequestModel().count_awaiting_review(
84 84 repo_name,
85 85 search_q=search_q, statuses=statuses)
86 86 elif filter_type == 'awaiting_my_review':
87 87 pull_requests = PullRequestModel().get_awaiting_my_review(
88 88 repo_name, self._rhodecode_user.user_id,
89 89 search_q=search_q, statuses=statuses,
90 90 offset=start, length=limit, order_by=order_by, order_dir=order_dir)
91 91 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
92 92 repo_name, self._rhodecode_user.user_id,
93 93 search_q=search_q, statuses=statuses)
94 94 else:
95 95 pull_requests = PullRequestModel().get_all(
96 96 repo_name, search_q=search_q, source=source, opened_by=opened_by,
97 97 statuses=statuses, offset=start, length=limit,
98 98 order_by=order_by, order_dir=order_dir)
99 99 pull_requests_total_count = PullRequestModel().count_all(
100 100 repo_name, search_q=search_q, source=source, statuses=statuses,
101 101 opened_by=opened_by)
102 102
103 103 data = []
104 104 comments_model = CommentsModel()
105 105 for pr in pull_requests:
106 106 comments_count = comments_model.get_all_comments(
107 107 self.db_repo.repo_id, pull_request=pr,
108 108 include_drafts=False, count_only=True)
109 109
110 110 review_statuses = pr.reviewers_statuses(user=self._rhodecode_db_user)
111 111 my_review_status = ChangesetStatus.STATUS_NOT_REVIEWED
112 112 if review_statuses and review_statuses[4]:
113 113 _review_obj, _user, _reasons, _mandatory, statuses = review_statuses
114 114 my_review_status = statuses[0][1].status
115 115
116 116 data.append({
117 117 'name': _render('pullrequest_name',
118 118 pr.pull_request_id, pr.pull_request_state,
119 119 pr.work_in_progress, pr.target_repo.repo_name,
120 120 short=True),
121 121 'name_raw': pr.pull_request_id,
122 122 'status': _render('pullrequest_status',
123 123 pr.calculated_review_status()),
124 124 'my_status': _render('pullrequest_status',
125 125 my_review_status),
126 126 'title': _render('pullrequest_title', pr.title, pr.description),
127 127 'pr_flow': _render('pullrequest_commit_flow', pr),
128 128 'description': h.escape(pr.description),
129 129 'updated_on': _render('pullrequest_updated_on',
130 130 h.datetime_to_time(pr.updated_on),
131 131 pr.versions_count),
132 132 'updated_on_raw': h.datetime_to_time(pr.updated_on),
133 133 'created_on': _render('pullrequest_updated_on',
134 134 h.datetime_to_time(pr.created_on)),
135 135 'created_on_raw': h.datetime_to_time(pr.created_on),
136 136 'state': pr.pull_request_state,
137 137 'author': _render('pullrequest_author',
138 138 pr.author.full_contact, ),
139 139 'author_raw': pr.author.full_name,
140 140 'comments': _render('pullrequest_comments', comments_count),
141 141 'comments_raw': comments_count,
142 142 'closed': pr.is_closed(),
143 143 })
144 144
145 145 data = ({
146 146 'draw': draw,
147 147 'data': data,
148 148 'recordsTotal': pull_requests_total_count,
149 149 'recordsFiltered': pull_requests_total_count,
150 150 })
151 151 return data
152 152
153 153 @LoginRequired()
154 154 @HasRepoPermissionAnyDecorator(
155 155 'repository.read', 'repository.write', 'repository.admin')
156 156 def pull_request_list(self):
157 157 c = self.load_default_context()
158 158
159 159 req_get = self.request.GET
160 160 c.source = str2bool(req_get.get('source'))
161 161 c.closed = str2bool(req_get.get('closed'))
162 162 c.my = str2bool(req_get.get('my'))
163 163 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
164 164 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
165 165
166 166 c.active = 'open'
167 167 if c.my:
168 168 c.active = 'my'
169 169 if c.closed:
170 170 c.active = 'closed'
171 171 if c.awaiting_review and not c.source:
172 172 c.active = 'awaiting'
173 173 if c.source and not c.awaiting_review:
174 174 c.active = 'source'
175 175 if c.awaiting_my_review:
176 176 c.active = 'awaiting_my'
177 177
178 178 return self._get_template_context(c)
179 179
180 180 @LoginRequired()
181 181 @HasRepoPermissionAnyDecorator(
182 182 'repository.read', 'repository.write', 'repository.admin')
183 183 def pull_request_list_data(self):
184 184 self.load_default_context()
185 185
186 186 # additional filters
187 187 req_get = self.request.GET
188 188 source = str2bool(req_get.get('source'))
189 189 closed = str2bool(req_get.get('closed'))
190 190 my = str2bool(req_get.get('my'))
191 191 awaiting_review = str2bool(req_get.get('awaiting_review'))
192 192 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
193 193
194 194 filter_type = 'awaiting_review' if awaiting_review \
195 195 else 'awaiting_my_review' if awaiting_my_review \
196 196 else None
197 197
198 198 opened_by = None
199 199 if my:
200 200 opened_by = [self._rhodecode_user.user_id]
201 201
202 202 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
203 203 if closed:
204 204 statuses = [PullRequest.STATUS_CLOSED]
205 205
206 206 data = self._get_pull_requests_list(
207 207 repo_name=self.db_repo_name, source=source,
208 208 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
209 209
210 210 return data
211 211
212 212 def _is_diff_cache_enabled(self, target_repo):
213 213 caching_enabled = self._get_general_setting(
214 214 target_repo, 'rhodecode_diff_cache')
215 215 log.debug('Diff caching enabled: %s', caching_enabled)
216 216 return caching_enabled
217 217
218 218 def _get_diffset(self, source_repo_name, source_repo,
219 219 ancestor_commit,
220 220 source_ref_id, target_ref_id,
221 221 target_commit, source_commit, diff_limit, file_limit,
222 222 fulldiff, hide_whitespace_changes, diff_context, use_ancestor=True):
223 223
224 224 target_commit_final = target_commit
225 225 source_commit_final = source_commit
226 226
227 227 if use_ancestor:
228 228 # we might want to not use it for versions
229 229 target_ref_id = ancestor_commit.raw_id
230 230 target_commit_final = ancestor_commit
231 231
232 232 vcs_diff = PullRequestModel().get_diff(
233 233 source_repo, source_ref_id, target_ref_id,
234 234 hide_whitespace_changes, diff_context)
235 235
236 236 diff_processor = diffs.DiffProcessor(vcs_diff, diff_format='newdiff', diff_limit=diff_limit,
237 237 file_limit=file_limit, show_full_diff=fulldiff)
238 238
239 239 _parsed = diff_processor.prepare()
240 240
241 241 diffset = codeblocks.DiffSet(
242 242 repo_name=self.db_repo_name,
243 243 source_repo_name=source_repo_name,
244 244 source_node_getter=codeblocks.diffset_node_getter(target_commit_final),
245 245 target_node_getter=codeblocks.diffset_node_getter(source_commit_final),
246 246 )
247 247 diffset = self.path_filter.render_patchset_filtered(
248 248 diffset, _parsed, target_ref_id, source_ref_id)
249 249
250 250 return diffset
251 251
252 252 def _get_range_diffset(self, source_scm, source_repo,
253 253 commit1, commit2, diff_limit, file_limit,
254 254 fulldiff, hide_whitespace_changes, diff_context):
255 255 vcs_diff = source_scm.get_diff(
256 256 commit1, commit2,
257 257 ignore_whitespace=hide_whitespace_changes,
258 258 context=diff_context)
259 259
260 260 diff_processor = diffs.DiffProcessor(vcs_diff, diff_format='newdiff',
261 261 diff_limit=diff_limit,
262 262 file_limit=file_limit, show_full_diff=fulldiff)
263 263
264 264 _parsed = diff_processor.prepare()
265 265
266 266 diffset = codeblocks.DiffSet(
267 267 repo_name=source_repo.repo_name,
268 268 source_node_getter=codeblocks.diffset_node_getter(commit1),
269 269 target_node_getter=codeblocks.diffset_node_getter(commit2))
270 270
271 271 diffset = self.path_filter.render_patchset_filtered(
272 272 diffset, _parsed, commit1.raw_id, commit2.raw_id)
273 273
274 274 return diffset
275 275
276 276 def register_comments_vars(self, c, pull_request, versions, include_drafts=True):
277 277 comments_model = CommentsModel()
278 278
279 279 # GENERAL COMMENTS with versions #
280 280 q = comments_model._all_general_comments_of_pull_request(pull_request)
281 281 q = q.order_by(ChangesetComment.comment_id.asc())
282 282 if not include_drafts:
283 283 q = q.filter(ChangesetComment.draft == false())
284 284 general_comments = q
285 285
286 286 # pick comments we want to render at current version
287 287 c.comment_versions = comments_model.aggregate_comments(
288 288 general_comments, versions, c.at_version_num)
289 289
290 290 # INLINE COMMENTS with versions #
291 291 q = comments_model._all_inline_comments_of_pull_request(pull_request)
292 292 q = q.order_by(ChangesetComment.comment_id.asc())
293 293 if not include_drafts:
294 294 q = q.filter(ChangesetComment.draft == false())
295 295 inline_comments = q
296 296
297 297 c.inline_versions = comments_model.aggregate_comments(
298 298 inline_comments, versions, c.at_version_num, inline=True)
299 299
300 300 # Comments inline+general
301 301 if c.at_version:
302 302 c.inline_comments_flat = c.inline_versions[c.at_version_num]['display']
303 303 c.comments = c.comment_versions[c.at_version_num]['display']
304 304 else:
305 305 c.inline_comments_flat = c.inline_versions[c.at_version_num]['until']
306 306 c.comments = c.comment_versions[c.at_version_num]['until']
307 307
308 308 return general_comments, inline_comments
309 309
310 310 @LoginRequired()
311 311 @HasRepoPermissionAnyDecorator(
312 312 'repository.read', 'repository.write', 'repository.admin')
313 313 def pull_request_show(self):
314 314 _ = self.request.translate
315 315 c = self.load_default_context()
316 316
317 317 pull_request = PullRequest.get_or_404(
318 318 self.request.matchdict['pull_request_id'])
319 319 pull_request_id = pull_request.pull_request_id
320 320
321 321 c.state_progressing = pull_request.is_state_changing()
322 322 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
323 323
324 324 _new_state = {
325 325 'created': PullRequest.STATE_CREATED,
326 326 }.get(self.request.GET.get('force_state'))
327 327 can_force_state = c.is_super_admin or HasRepoPermissionAny('repository.admin')(c.repo_name)
328 328
329 329 if can_force_state and _new_state:
330 330 with pull_request.set_state(PullRequest.STATE_UPDATING, final_state=_new_state):
331 331 h.flash(
332 332 _('Pull Request state was force changed to `{}`').format(_new_state),
333 333 category='success')
334 334 Session().commit()
335 335
336 336 raise HTTPFound(h.route_path(
337 337 'pullrequest_show', repo_name=self.db_repo_name,
338 338 pull_request_id=pull_request_id))
339 339
340 340 version = self.request.GET.get('version')
341 341 from_version = self.request.GET.get('from_version') or version
342 342 merge_checks = self.request.GET.get('merge_checks')
343 343 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
344 344 force_refresh = str2bool(self.request.GET.get('force_refresh'))
345 345 c.range_diff_on = self.request.GET.get('range-diff') == "1"
346 346
347 347 # fetch global flags of ignore ws or context lines
348 348 diff_context = diffs.get_diff_context(self.request)
349 349 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
350 350
351 351 (pull_request_latest,
352 352 pull_request_at_ver,
353 353 pull_request_display_obj,
354 354 at_version) = PullRequestModel().get_pr_version(
355 355 pull_request_id, version=version)
356 356
357 357 pr_closed = pull_request_latest.is_closed()
358 358
359 359 if pr_closed and (version or from_version):
360 # not allow to browse versions for closed PR
360 # not allow browsing versions for closed PR
361 361 raise HTTPFound(h.route_path(
362 362 'pullrequest_show', repo_name=self.db_repo_name,
363 363 pull_request_id=pull_request_id))
364 364
365 365 versions = pull_request_display_obj.versions()
366 366
367 367 c.commit_versions = PullRequestModel().pr_commits_versions(versions)
368 368
369 369 # used to store per-commit range diffs
370 370 c.changes = collections.OrderedDict()
371 371
372 372 c.at_version = at_version
373 373 c.at_version_num = (at_version
374 374 if at_version and at_version != PullRequest.LATEST_VER
375 375 else None)
376 376
377 377 c.at_version_index = ChangesetComment.get_index_from_version(
378 378 c.at_version_num, versions)
379 379
380 380 (prev_pull_request_latest,
381 381 prev_pull_request_at_ver,
382 382 prev_pull_request_display_obj,
383 383 prev_at_version) = PullRequestModel().get_pr_version(
384 384 pull_request_id, version=from_version)
385 385
386 386 c.from_version = prev_at_version
387 387 c.from_version_num = (prev_at_version
388 388 if prev_at_version and prev_at_version != PullRequest.LATEST_VER
389 389 else None)
390 390 c.from_version_index = ChangesetComment.get_index_from_version(
391 391 c.from_version_num, versions)
392 392
393 393 # define if we're in COMPARE mode or VIEW at version mode
394 394 compare = at_version != prev_at_version
395 395
396 396 # pull_requests repo_name we opened it against
397 # ie. target_repo must match
397 # i.e., target_repo must match
398 398 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
399 399 log.warning('Mismatch between the current repo: %s, and target %s',
400 400 self.db_repo_name, pull_request_at_ver.target_repo.repo_name)
401 401 raise HTTPNotFound()
402 402
403 403 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(pull_request_at_ver)
404 404
405 405 c.pull_request = pull_request_display_obj
406 406 c.renderer = pull_request_at_ver.description_renderer or c.renderer
407 407 c.pull_request_latest = pull_request_latest
408 408
409 409 # inject latest version
410 410 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
411 411 c.versions = versions + [latest_ver]
412 412
413 413 if compare or (at_version and not at_version == PullRequest.LATEST_VER):
414 414 c.allowed_to_change_status = False
415 415 c.allowed_to_update = False
416 416 c.allowed_to_merge = False
417 417 c.allowed_to_delete = False
418 418 c.allowed_to_comment = False
419 419 c.allowed_to_close = False
420 420 else:
421 421 can_change_status = PullRequestModel().check_user_change_status(
422 422 pull_request_at_ver, self._rhodecode_user)
423 423 c.allowed_to_change_status = can_change_status and not pr_closed
424 424
425 425 c.allowed_to_update = PullRequestModel().check_user_update(
426 426 pull_request_latest, self._rhodecode_user) and not pr_closed
427 427 c.allowed_to_merge = PullRequestModel().check_user_merge(
428 428 pull_request_latest, self._rhodecode_user) and not pr_closed
429 429 c.allowed_to_delete = PullRequestModel().check_user_delete(
430 430 pull_request_latest, self._rhodecode_user) and not pr_closed
431 431 c.allowed_to_comment = not pr_closed
432 432 c.allowed_to_close = c.allowed_to_merge and not pr_closed
433 433
434 434 c.forbid_adding_reviewers = False
435 435
436 436 if pull_request_latest.reviewer_data and \
437 437 'rules' in pull_request_latest.reviewer_data:
438 438 rules = pull_request_latest.reviewer_data['rules'] or {}
439 439 try:
440 440 c.forbid_adding_reviewers = rules.get('forbid_adding_reviewers')
441 441 except Exception:
442 442 pass
443 443
444 444 # check merge capabilities
445 445 _merge_check = MergeCheck.validate(
446 446 pull_request_latest, auth_user=self._rhodecode_user,
447 447 translator=self.request.translate,
448 448 force_shadow_repo_refresh=force_refresh)
449 449
450 450 c.pr_merge_errors = _merge_check.error_details
451 451 c.pr_merge_possible = not _merge_check.failed
452 452 c.pr_merge_message = _merge_check.merge_msg
453 453 c.pr_merge_source_commit = _merge_check.source_commit
454 454 c.pr_merge_target_commit = _merge_check.target_commit
455 455
456 456 c.pr_merge_info = MergeCheck.get_merge_conditions(
457 457 pull_request_latest, translator=self.request.translate)
458 458
459 459 c.pull_request_review_status = _merge_check.review_status
460 460 if merge_checks:
461 461 self.request.override_renderer = \
462 462 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
463 463 return self._get_template_context(c)
464 464
465 465 c.reviewers_count = pull_request.reviewers_count
466 466 c.observers_count = pull_request.observers_count
467 467
468 468 # reviewers and statuses
469 469 c.pull_request_default_reviewers_data_json = ext_json.str_json(pull_request.reviewer_data)
470 470 c.pull_request_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
471 471 c.pull_request_set_observers_data_json = collections.OrderedDict({'observers': []})
472 472
473 # reviewers
473 474 for review_obj, member, reasons, mandatory, status in pull_request_at_ver.reviewers_statuses():
474 475 member_reviewer = h.reviewer_as_json(
475 476 member, reasons=reasons, mandatory=mandatory,
476 477 role=review_obj.role,
477 478 user_group=review_obj.rule_user_group_data()
478 479 )
479 480
480 481 current_review_status = status[0][1].status if status else ChangesetStatus.STATUS_NOT_REVIEWED
481 482 member_reviewer['review_status'] = current_review_status
482 483 member_reviewer['review_status_label'] = h.commit_status_lbl(current_review_status)
483 484 member_reviewer['allowed_to_update'] = c.allowed_to_update
484 485 c.pull_request_set_reviewers_data_json['reviewers'].append(member_reviewer)
485 486
486 487 c.pull_request_set_reviewers_data_json = ext_json.str_json(c.pull_request_set_reviewers_data_json)
487 488
489 # observers
488 490 for observer_obj, member in pull_request_at_ver.observers():
489 491 member_observer = h.reviewer_as_json(
490 492 member, reasons=[], mandatory=False,
491 493 role=observer_obj.role,
492 494 user_group=observer_obj.rule_user_group_data()
493 495 )
494 496 member_observer['allowed_to_update'] = c.allowed_to_update
495 497 c.pull_request_set_observers_data_json['observers'].append(member_observer)
496 498
497 499 c.pull_request_set_observers_data_json = ext_json.str_json(c.pull_request_set_observers_data_json)
498 500
499 501 general_comments, inline_comments = \
500 502 self.register_comments_vars(c, pull_request_latest, versions)
501 503
502 504 # TODOs
503 505 c.unresolved_comments = CommentsModel() \
504 506 .get_pull_request_unresolved_todos(pull_request_latest)
505 507 c.resolved_comments = CommentsModel() \
506 508 .get_pull_request_resolved_todos(pull_request_latest)
507 509
508 510 # Drafts
509 511 c.draft_comments = CommentsModel().get_pull_request_drafts(
510 512 self._rhodecode_db_user.user_id,
511 513 pull_request_latest)
512 514
513 515 # if we use version, then do not show later comments
514 516 # than current version
515 517 display_inline_comments = collections.defaultdict(
516 518 lambda: collections.defaultdict(list))
517 519 for co in inline_comments:
518 520 if c.at_version_num:
519 521 # pick comments that are at least UPTO given version, so we
520 522 # don't render comments for higher version
521 523 should_render = co.pull_request_version_id and \
522 524 co.pull_request_version_id <= c.at_version_num
523 525 else:
524 526 # showing all, for 'latest'
525 527 should_render = True
526 528
527 529 if should_render:
528 530 display_inline_comments[co.f_path][co.line_no].append(co)
529 531
530 532 # load diff data into template context, if we use compare mode then
531 533 # diff is calculated based on changes between versions of PR
532 534
533 535 source_repo = pull_request_at_ver.source_repo
534 536 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
535 537
536 538 target_repo = pull_request_at_ver.target_repo
537 539 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
538 540
539 541 if compare:
540 542 # in compare switch the diff base to latest commit from prev version
541 543 target_ref_id = prev_pull_request_display_obj.revisions[0]
542 544
543 545 # despite opening commits for bookmarks/branches/tags, we always
544 546 # convert this to rev to prevent changes after bookmark or branch change
545 547 c.source_ref_type = 'rev'
546 548 c.source_ref = source_ref_id
547 549
548 550 c.target_ref_type = 'rev'
549 551 c.target_ref = target_ref_id
550 552
551 553 c.source_repo = source_repo
552 554 c.target_repo = target_repo
553 555
554 556 c.commit_ranges = []
555 557 source_commit = EmptyCommit()
556 558 target_commit = EmptyCommit()
557 559 c.missing_requirements = False
558 560
559 561 source_scm = source_repo.scm_instance()
560 562 target_scm = target_repo.scm_instance()
561 563
562 564 shadow_scm = None
563 565 try:
564 566 shadow_scm = pull_request_latest.get_shadow_repo()
565 567 except Exception:
566 568 log.debug('Failed to get shadow repo', exc_info=True)
567 569 # try first the existing source_repo, and then shadow
568 570 # repo if we can obtain one
569 571 commits_source_repo = source_scm
570 572 if shadow_scm:
571 573 commits_source_repo = shadow_scm
572 574
573 575 c.commits_source_repo = commits_source_repo
574 576 c.ancestor = None # set it to None, to hide it from PR view
575 577
576 578 # empty version means latest, so we keep this to prevent
577 579 # double caching
578 580 version_normalized = version or PullRequest.LATEST_VER
579 581 from_version_normalized = from_version or PullRequest.LATEST_VER
580 582
581 583 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(target_repo)
582 584 cache_file_path = diff_cache_exist(
583 585 cache_path, 'pull_request', pull_request_id, version_normalized,
584 586 from_version_normalized, source_ref_id, target_ref_id,
585 587 hide_whitespace_changes, diff_context, c.fulldiff)
586 588
587 589 caching_enabled = self._is_diff_cache_enabled(c.target_repo)
588 590 force_recache = self.get_recache_flag()
589 591
590 592 cached_diff = None
591 593 if caching_enabled:
592 594 cached_diff = load_cached_diff(cache_file_path)
593 595
594 596 has_proper_commit_cache = (
595 597 cached_diff and cached_diff.get('commits')
596 598 and len(cached_diff.get('commits', [])) == 5
597 599 and cached_diff.get('commits')[0]
598 600 and cached_diff.get('commits')[3])
599 601
600 602 if not force_recache and not c.range_diff_on and has_proper_commit_cache:
601 603 diff_commit_cache = \
602 604 (ancestor_commit, commit_cache, missing_requirements,
603 605 source_commit, target_commit) = cached_diff['commits']
604 606 else:
605 607 # NOTE(marcink): we reach potentially unreachable errors when a PR has
606 608 # merge errors resulting in potentially hidden commits in the shadow repo.
607 609 maybe_unreachable = _merge_check.MERGE_CHECK in _merge_check.error_details \
608 610 and _merge_check.merge_response
609 611 maybe_unreachable = maybe_unreachable \
610 612 and _merge_check.merge_response.metadata.get('unresolved_files')
611 613 log.debug("Using unreachable commits due to MERGE_CHECK in merge simulation")
612 614 diff_commit_cache = \
613 615 (ancestor_commit, commit_cache, missing_requirements,
614 616 source_commit, target_commit) = self.get_commits(
615 617 commits_source_repo,
616 618 pull_request_at_ver,
617 619 source_commit,
618 620 source_ref_id,
619 621 source_scm,
620 622 target_commit,
621 623 target_ref_id,
622 624 target_scm,
623 maybe_unreachable=maybe_unreachable)
625 maybe_unreachable=maybe_unreachable)
624 626
625 627 # register our commit range
626 628 for comm in commit_cache.values():
627 629 c.commit_ranges.append(comm)
628 630
629 631 c.missing_requirements = missing_requirements
630 632 c.ancestor_commit = ancestor_commit
631 633 c.statuses = source_repo.statuses(
632 634 [x.raw_id for x in c.commit_ranges])
633 635
634 636 # auto collapse if we have more than limit
635 637 collapse_limit = diffs.DiffProcessor._collapse_commits_over
636 638 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
637 639 c.compare_mode = compare
638 640
639 641 # diff_limit is the old behavior, will cut off the whole diff
640 642 # if the limit is applied otherwise will just hide the
641 643 # big files from the front-end
642 644 diff_limit = c.visual.cut_off_limit_diff
643 645 file_limit = c.visual.cut_off_limit_file
644 646
645 647 c.missing_commits = False
646 648 if (c.missing_requirements
647 649 or isinstance(source_commit, EmptyCommit)
648 650 or source_commit == target_commit):
649 651
650 652 c.missing_commits = True
651 653 else:
652 654 c.inline_comments = display_inline_comments
653 655
654 656 use_ancestor = True
655 657 if from_version_normalized != version_normalized:
656 658 use_ancestor = False
657 659
658 660 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
659 661 if not force_recache and has_proper_diff_cache:
660 662 c.diffset = cached_diff['diff']
661 663 else:
662 664 try:
663 665 c.diffset = self._get_diffset(
664 666 c.source_repo.repo_name, commits_source_repo,
665 667 c.ancestor_commit,
666 668 source_ref_id, target_ref_id,
667 669 target_commit, source_commit,
668 670 diff_limit, file_limit, c.fulldiff,
669 671 hide_whitespace_changes, diff_context,
670 672 use_ancestor=use_ancestor
671 673 )
672 674
673 675 # save cached diff
674 676 if caching_enabled:
675 677 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
676 678 except CommitDoesNotExistError:
677 679 log.exception('Failed to generate diffset')
678 680 c.missing_commits = True
679 681
680 682 if not c.missing_commits:
681 683
682 684 c.limited_diff = c.diffset.limited_diff
683 685
684 686 # calculate removed files that are bound to comments
685 687 comment_deleted_files = [
686 688 fname for fname in display_inline_comments
687 689 if fname not in c.diffset.file_stats]
688 690
689 691 c.deleted_files_comments = collections.defaultdict(dict)
690 692 for fname, per_line_comments in display_inline_comments.items():
691 693 if fname in comment_deleted_files:
692 694 c.deleted_files_comments[fname]['stats'] = 0
693 695 c.deleted_files_comments[fname]['comments'] = list()
694 696 for lno, comments in per_line_comments.items():
695 697 c.deleted_files_comments[fname]['comments'].extend(comments)
696 698
697 699 # maybe calculate the range diff
698 700 if c.range_diff_on:
699 701 # TODO(marcink): set whitespace/context
700 702 context_lcl = 3
701 703 ign_whitespace_lcl = False
702 704
703 705 for commit in c.commit_ranges:
704 706 commit2 = commit
705 707 commit1 = commit.first_parent
706 708
707 709 range_diff_cache_file_path = diff_cache_exist(
708 710 cache_path, 'diff', commit.raw_id,
709 711 ign_whitespace_lcl, context_lcl, c.fulldiff)
710 712
711 713 cached_diff = None
712 714 if caching_enabled:
713 715 cached_diff = load_cached_diff(range_diff_cache_file_path)
714 716
715 717 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
716 718 if not force_recache and has_proper_diff_cache:
717 719 diffset = cached_diff['diff']
718 720 else:
719 721 diffset = self._get_range_diffset(
720 722 commits_source_repo, source_repo,
721 723 commit1, commit2, diff_limit, file_limit,
722 724 c.fulldiff, ign_whitespace_lcl, context_lcl
723 725 )
724 726
725 727 # save cached diff
726 728 if caching_enabled:
727 729 cache_diff(range_diff_cache_file_path, diffset, None)
728 730
729 731 c.changes[commit.raw_id] = diffset
730 732
731 733 # this is a hack to properly display links, when creating PR, the
732 734 # compare view and others uses different notation, and
733 735 # compare_commits.mako renders links based on the target_repo.
734 736 # We need to swap that here to generate it properly on the html side
735 737 c.target_repo = c.source_repo
736 738
737 739 c.commit_statuses = ChangesetStatus.STATUSES
738 740
739 741 c.show_version_changes = not pr_closed
740 742 if c.show_version_changes:
741 743 cur_obj = pull_request_at_ver
742 744 prev_obj = prev_pull_request_at_ver
743 745
744 746 old_commit_ids = prev_obj.revisions
745 747 new_commit_ids = cur_obj.revisions
746 748 commit_changes = PullRequestModel()._calculate_commit_id_changes(
747 749 old_commit_ids, new_commit_ids)
748 750 c.commit_changes_summary = commit_changes
749 751
750 752 # calculate the diff for commits between versions
751 753 c.commit_changes = []
752 754
753 755 def mark(cs, fw):
754 756 return list(h.itertools.zip_longest([], cs, fillvalue=fw))
755 757
756 758 for c_type, raw_id in mark(commit_changes.added, 'a') \
757 759 + mark(commit_changes.removed, 'r') \
758 760 + mark(commit_changes.common, 'c'):
759 761
760 762 if raw_id in commit_cache:
761 763 commit = commit_cache[raw_id]
762 764 else:
763 765 try:
764 766 commit = commits_source_repo.get_commit(raw_id)
765 767 except CommitDoesNotExistError:
766 # in case we fail extracting still use "dummy" commit
768 # in case we fail getting the commit, still use a dummy commit
767 769 # for display in commit diff
768 770 commit = h.AttributeDict(
769 771 {'raw_id': raw_id,
770 772 'message': 'EMPTY or MISSING COMMIT'})
771 773 c.commit_changes.append([c_type, commit])
772 774
773 775 # current user review statuses for each version
774 776 c.review_versions = {}
775 777 is_reviewer = PullRequestModel().is_user_reviewer(
776 778 pull_request, self._rhodecode_user)
777 779 if is_reviewer:
778 780 for co in general_comments:
779 781 if co.author.user_id == self._rhodecode_user.user_id:
780 782 status = co.status_change
781 783 if status:
782 784 _ver_pr = status[0].comment.pull_request_version_id
783 785 c.review_versions[_ver_pr] = status[0]
784 786
785 787 return self._get_template_context(c)
786 788
787 789 def get_commits(
788 790 self, commits_source_repo, pull_request_at_ver, source_commit,
789 791 source_ref_id, source_scm, target_commit, target_ref_id, target_scm,
790 792 maybe_unreachable=False):
791 793
792 794 commit_cache = collections.OrderedDict()
793 795 missing_requirements = False
794 796
795 797 try:
796 798 pre_load = ["author", "date", "message", "branch", "parents"]
797 799
798 800 pull_request_commits = pull_request_at_ver.revisions
799 801 log.debug('Loading %s commits from %s',
800 802 len(pull_request_commits), commits_source_repo)
801 803
802 804 for rev in pull_request_commits:
803 805 comm = commits_source_repo.get_commit(commit_id=rev, pre_load=pre_load,
804 806 maybe_unreachable=maybe_unreachable)
805 807 commit_cache[comm.raw_id] = comm
806 808
807 809 # Order here matters, we first need to get target, and then
808 810 # the source
809 811 target_commit = commits_source_repo.get_commit(
810 812 commit_id=safe_str(target_ref_id))
811 813
812 814 source_commit = commits_source_repo.get_commit(
813 815 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
814 816 except CommitDoesNotExistError:
815 817 log.warning('Failed to get commit from `{}` repo'.format(
816 818 commits_source_repo), exc_info=True)
817 819 except RepositoryRequirementError:
818 820 log.warning('Failed to get all required data from repo', exc_info=True)
819 821 missing_requirements = True
820 822
821 823 pr_ancestor_id = pull_request_at_ver.common_ancestor_id
822 824
823 825 try:
824 826 ancestor_commit = source_scm.get_commit(pr_ancestor_id)
825 827 except Exception:
826 828 ancestor_commit = None
827 829
828 830 return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit
829 831
830 832 def assure_not_empty_repo(self):
831 833 _ = self.request.translate
832 834
833 835 try:
834 836 self.db_repo.scm_instance().get_commit()
835 837 except EmptyRepositoryError:
836 838 h.flash(h.literal(_('There are no commits yet')),
837 839 category='warning')
838 840 raise HTTPFound(
839 841 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
840 842
841 843 @LoginRequired()
842 844 @NotAnonymous()
843 845 @HasRepoPermissionAnyDecorator(
844 846 'repository.read', 'repository.write', 'repository.admin')
845 847 def pull_request_new(self):
846 848 _ = self.request.translate
847 849 c = self.load_default_context()
848 850
849 851 self.assure_not_empty_repo()
850 852 source_repo = self.db_repo
851 853
852 854 commit_id = self.request.GET.get('commit')
853 855 branch_ref = self.request.GET.get('branch')
854 856 bookmark_ref = self.request.GET.get('bookmark')
855 857
856 858 try:
857 859 source_repo_data = PullRequestModel().generate_repo_data(
858 860 source_repo, commit_id=commit_id,
859 861 branch=branch_ref, bookmark=bookmark_ref,
860 862 translator=self.request.translate)
861 863 except CommitDoesNotExistError as e:
862 864 log.exception(e)
863 865 h.flash(_('Commit does not exist'), 'error')
864 866 raise HTTPFound(
865 867 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
866 868
867 869 default_target_repo = source_repo
868 870
869 871 if source_repo.parent and c.has_origin_repo_read_perm:
870 872 parent_vcs_obj = source_repo.parent.scm_instance()
871 873 if parent_vcs_obj and not parent_vcs_obj.is_empty():
872 874 # change default if we have a parent repo
873 875 default_target_repo = source_repo.parent
874 876
875 877 target_repo_data = PullRequestModel().generate_repo_data(
876 878 default_target_repo, translator=self.request.translate)
877 879
878 880 selected_source_ref = source_repo_data['refs']['selected_ref']
879 881 title_source_ref = ''
880 882 if selected_source_ref:
881 883 title_source_ref = selected_source_ref.split(':', 2)[1]
882 884 c.default_title = PullRequestModel().generate_pullrequest_title(
883 885 source=source_repo.repo_name,
884 886 source_ref=title_source_ref,
885 887 target=default_target_repo.repo_name
886 888 )
887 889
888 890 c.default_repo_data = {
889 891 'source_repo_name': source_repo.repo_name,
890 892 'source_refs_json': ext_json.str_json(source_repo_data),
891 893 'target_repo_name': default_target_repo.repo_name,
892 894 'target_refs_json': ext_json.str_json(target_repo_data),
893 895 }
894 896 c.default_source_ref = selected_source_ref
895 897
896 898 return self._get_template_context(c)
897 899
898 900 @LoginRequired()
899 901 @NotAnonymous()
900 902 @HasRepoPermissionAnyDecorator(
901 903 'repository.read', 'repository.write', 'repository.admin')
902 904 def pull_request_repo_refs(self):
903 905 self.load_default_context()
904 906 target_repo_name = self.request.matchdict['target_repo_name']
905 907 repo = Repository.get_by_repo_name(target_repo_name)
906 908 if not repo:
907 909 raise HTTPNotFound()
908 910
909 911 target_perm = HasRepoPermissionAny(
910 912 'repository.read', 'repository.write', 'repository.admin')(
911 913 target_repo_name)
912 914 if not target_perm:
913 915 raise HTTPNotFound()
914 916
915 917 return PullRequestModel().generate_repo_data(
916 918 repo, translator=self.request.translate)
917 919
918 920 @LoginRequired()
919 921 @NotAnonymous()
920 922 @HasRepoPermissionAnyDecorator(
921 923 'repository.read', 'repository.write', 'repository.admin')
922 924 def pullrequest_repo_targets(self):
923 925 _ = self.request.translate
924 926 filter_query = self.request.GET.get('query')
925 927
926 928 # get the parents
927 929 parent_target_repos = []
928 930 if self.db_repo.parent:
929 931 parents_query = Repository.query() \
930 932 .order_by(func.length(Repository.repo_name)) \
931 933 .filter(Repository.fork_id == self.db_repo.parent.repo_id)
932 934
933 935 if filter_query:
934 936 ilike_expression = f'%{safe_str(filter_query)}%'
935 937 parents_query = parents_query.filter(
936 938 Repository.repo_name.ilike(ilike_expression))
937 939 parents = parents_query.limit(20).all()
938 940
939 941 for parent in parents:
940 942 parent_vcs_obj = parent.scm_instance()
941 943 if parent_vcs_obj and not parent_vcs_obj.is_empty():
942 944 parent_target_repos.append(parent)
943 945
944 946 # get other forks, and repo itself
945 947 query = Repository.query() \
946 948 .order_by(func.length(Repository.repo_name)) \
947 949 .filter(
948 950 or_(Repository.repo_id == self.db_repo.repo_id, # repo itself
949 951 Repository.fork_id == self.db_repo.repo_id) # forks of this repo
950 952 ) \
951 953 .filter(~Repository.repo_id.in_([x.repo_id for x in parent_target_repos]))
952 954
953 955 if filter_query:
954 956 ilike_expression = f'%{safe_str(filter_query)}%'
955 957 query = query.filter(Repository.repo_name.ilike(ilike_expression))
956 958
957 959 limit = max(20 - len(parent_target_repos), 5) # not less then 5
958 960 target_repos = query.limit(limit).all()
959 961
960 962 all_target_repos = target_repos + parent_target_repos
961 963
962 964 repos = []
963 965 # This checks permissions to the repositories
964 966 for obj in ScmModel().get_repos(all_target_repos):
965 967 repos.append({
966 968 'id': obj['name'],
967 969 'text': obj['name'],
968 970 'type': 'repo',
969 971 'repo_id': obj['dbrepo']['repo_id'],
970 972 'repo_type': obj['dbrepo']['repo_type'],
971 973 'private': obj['dbrepo']['private'],
972 974
973 975 })
974 976
975 977 data = {
976 978 'more': False,
977 979 'results': [{
978 980 'text': _('Repositories'),
979 981 'children': repos
980 982 }] if repos else []
981 983 }
982 984 return data
983 985
984 986 @classmethod
985 987 def get_comment_ids(cls, post_data):
986 988 return filter(lambda e: e > 0, map(safe_int, aslist(post_data.get('comments'), ',')))
987 989
988 990 @LoginRequired()
989 991 @NotAnonymous()
990 992 @HasRepoPermissionAnyDecorator(
991 993 'repository.read', 'repository.write', 'repository.admin')
992 994 def pullrequest_comments(self):
993 995 self.load_default_context()
994 996
995 997 pull_request = PullRequest.get_or_404(
996 998 self.request.matchdict['pull_request_id'])
997 999 pull_request_id = pull_request.pull_request_id
998 1000 version = self.request.GET.get('version')
999 1001
1000 1002 _render = self.request.get_partial_renderer(
1001 1003 'rhodecode:templates/base/sidebar.mako')
1002 1004 c = _render.get_call_context()
1003 1005
1004 1006 (pull_request_latest,
1005 1007 pull_request_at_ver,
1006 1008 pull_request_display_obj,
1007 1009 at_version) = PullRequestModel().get_pr_version(
1008 1010 pull_request_id, version=version)
1009 1011 versions = pull_request_display_obj.versions()
1010 1012 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1011 1013 c.versions = versions + [latest_ver]
1012 1014
1013 1015 c.at_version = at_version
1014 1016 c.at_version_num = (at_version
1015 1017 if at_version and at_version != PullRequest.LATEST_VER
1016 1018 else None)
1017 1019
1018 1020 self.register_comments_vars(c, pull_request_latest, versions, include_drafts=False)
1019 1021 all_comments = c.inline_comments_flat + c.comments
1020 1022
1021 1023 existing_ids = self.get_comment_ids(self.request.POST)
1022 1024 return _render('comments_table', all_comments, len(all_comments),
1023 1025 existing_ids=existing_ids)
1024 1026
1025 1027 @LoginRequired()
1026 1028 @NotAnonymous()
1027 1029 @HasRepoPermissionAnyDecorator(
1028 1030 'repository.read', 'repository.write', 'repository.admin')
1029 1031 def pullrequest_todos(self):
1030 1032 self.load_default_context()
1031 1033
1032 1034 pull_request = PullRequest.get_or_404(
1033 1035 self.request.matchdict['pull_request_id'])
1034 1036 pull_request_id = pull_request.pull_request_id
1035 1037 version = self.request.GET.get('version')
1036 1038
1037 1039 _render = self.request.get_partial_renderer(
1038 1040 'rhodecode:templates/base/sidebar.mako')
1039 1041 c = _render.get_call_context()
1040 1042 (pull_request_latest,
1041 1043 pull_request_at_ver,
1042 1044 pull_request_display_obj,
1043 1045 at_version) = PullRequestModel().get_pr_version(
1044 1046 pull_request_id, version=version)
1045 1047 versions = pull_request_display_obj.versions()
1046 1048 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1047 1049 c.versions = versions + [latest_ver]
1048 1050
1049 1051 c.at_version = at_version
1050 1052 c.at_version_num = (at_version
1051 1053 if at_version and at_version != PullRequest.LATEST_VER
1052 1054 else None)
1053 1055
1054 1056 c.unresolved_comments = CommentsModel() \
1055 1057 .get_pull_request_unresolved_todos(pull_request, include_drafts=False)
1056 1058 c.resolved_comments = CommentsModel() \
1057 1059 .get_pull_request_resolved_todos(pull_request, include_drafts=False)
1058 1060
1059 1061 all_comments = c.unresolved_comments + c.resolved_comments
1060 1062 existing_ids = self.get_comment_ids(self.request.POST)
1061 1063 return _render('comments_table', all_comments, len(c.unresolved_comments),
1062 1064 todo_comments=True, existing_ids=existing_ids)
1063 1065
1064 1066 @LoginRequired()
1065 1067 @NotAnonymous()
1066 1068 @HasRepoPermissionAnyDecorator(
1067 1069 'repository.read', 'repository.write', 'repository.admin')
1068 1070 def pullrequest_drafts(self):
1069 1071 self.load_default_context()
1070 1072
1071 1073 pull_request = PullRequest.get_or_404(
1072 1074 self.request.matchdict['pull_request_id'])
1073 1075 pull_request_id = pull_request.pull_request_id
1074 1076 version = self.request.GET.get('version')
1075 1077
1076 1078 _render = self.request.get_partial_renderer(
1077 1079 'rhodecode:templates/base/sidebar.mako')
1078 1080 c = _render.get_call_context()
1079 1081
1080 1082 (pull_request_latest,
1081 1083 pull_request_at_ver,
1082 1084 pull_request_display_obj,
1083 1085 at_version) = PullRequestModel().get_pr_version(
1084 1086 pull_request_id, version=version)
1085 1087 versions = pull_request_display_obj.versions()
1086 1088 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1087 1089 c.versions = versions + [latest_ver]
1088 1090
1089 1091 c.at_version = at_version
1090 1092 c.at_version_num = (at_version
1091 1093 if at_version and at_version != PullRequest.LATEST_VER
1092 1094 else None)
1093 1095
1094 1096 c.draft_comments = CommentsModel() \
1095 1097 .get_pull_request_drafts(self._rhodecode_db_user.user_id, pull_request)
1096 1098
1097 1099 all_comments = c.draft_comments
1098 1100
1099 1101 existing_ids = self.get_comment_ids(self.request.POST)
1100 1102 return _render('comments_table', all_comments, len(all_comments),
1101 1103 existing_ids=existing_ids, draft_comments=True)
1102 1104
1103 1105 @LoginRequired()
1104 1106 @NotAnonymous()
1105 1107 @HasRepoPermissionAnyDecorator(
1106 1108 'repository.read', 'repository.write', 'repository.admin')
1107 1109 @CSRFRequired()
1108 1110 def pull_request_create(self):
1109 1111 _ = self.request.translate
1110 1112 self.assure_not_empty_repo()
1111 1113 self.load_default_context()
1112 1114
1113 1115 controls = peppercorn.parse(self.request.POST.items())
1114 1116
1115 1117 try:
1116 1118 form = PullRequestForm(
1117 1119 self.request.translate, self.db_repo.repo_id)()
1118 1120 _form = form.to_python(controls)
1119 1121 except formencode.Invalid as errors:
1120 1122 if errors.error_dict.get('revisions'):
1121 1123 msg = 'Revisions: {}'.format(errors.error_dict['revisions'])
1122 1124 elif errors.error_dict.get('pullrequest_title'):
1123 1125 msg = errors.error_dict.get('pullrequest_title')
1124 1126 else:
1125 1127 msg = _('Error creating pull request: {}').format(errors)
1126 1128 log.exception(msg)
1127 1129 h.flash(msg, 'error')
1128 1130
1129 1131 # would rather just go back to form ...
1130 1132 raise HTTPFound(
1131 1133 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1132 1134
1133 1135 source_repo = _form['source_repo']
1134 1136 source_ref = _form['source_ref']
1135 1137 target_repo = _form['target_repo']
1136 1138 target_ref = _form['target_ref']
1137 1139 commit_ids = _form['revisions'][::-1]
1138 1140 common_ancestor_id = _form['common_ancestor']
1139 1141
1140 1142 # find the ancestor for this pr
1141 1143 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
1142 1144 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
1143 1145
1144 1146 if not (source_db_repo or target_db_repo):
1145 1147 h.flash(_('source_repo or target repo not found'), category='error')
1146 1148 raise HTTPFound(
1147 1149 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1148 1150
1149 1151 # re-check permissions again here
1150 1152 # source_repo we must have read permissions
1151 1153
1152 1154 source_perm = HasRepoPermissionAny(
1153 1155 'repository.read', 'repository.write', 'repository.admin')(
1154 1156 source_db_repo.repo_name)
1155 1157 if not source_perm:
1156 1158 msg = _('Not Enough permissions to source repo `{}`.'.format(
1157 1159 source_db_repo.repo_name))
1158 1160 h.flash(msg, category='error')
1159 1161 # copy the args back to redirect
1160 1162 org_query = self.request.GET.mixed()
1161 1163 raise HTTPFound(
1162 1164 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1163 1165 _query=org_query))
1164 1166
1165 1167 # target repo we must have read permissions, and also later on
1166 1168 # we want to check branch permissions here
1167 1169 target_perm = HasRepoPermissionAny(
1168 1170 'repository.read', 'repository.write', 'repository.admin')(
1169 1171 target_db_repo.repo_name)
1170 1172 if not target_perm:
1171 1173 msg = _('Not Enough permissions to target repo `{}`.'.format(
1172 1174 target_db_repo.repo_name))
1173 1175 h.flash(msg, category='error')
1174 1176 # copy the args back to redirect
1175 1177 org_query = self.request.GET.mixed()
1176 1178 raise HTTPFound(
1177 1179 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1178 1180 _query=org_query))
1179 1181
1180 1182 source_scm = source_db_repo.scm_instance()
1181 1183 target_scm = target_db_repo.scm_instance()
1182 1184
1183 1185 source_ref_obj = unicode_to_reference(source_ref)
1184 1186 target_ref_obj = unicode_to_reference(target_ref)
1185 1187
1186 1188 source_commit = source_scm.get_commit(source_ref_obj.commit_id)
1187 1189 target_commit = target_scm.get_commit(target_ref_obj.commit_id)
1188 1190
1189 1191 ancestor = source_scm.get_common_ancestor(
1190 1192 source_commit.raw_id, target_commit.raw_id, target_scm)
1191 1193
1192 1194 # recalculate target ref based on ancestor
1193 1195 target_ref = ':'.join((target_ref_obj.type, target_ref_obj.name, ancestor))
1194 1196
1195 1197 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1196 1198 PullRequestModel().get_reviewer_functions()
1197 1199
1198 1200 # recalculate reviewers logic, to make sure we can validate this
1199 1201 reviewer_rules = get_default_reviewers_data(
1200 1202 self._rhodecode_db_user,
1201 1203 source_db_repo,
1202 1204 source_ref_obj,
1203 1205 target_db_repo,
1204 1206 target_ref_obj,
1205 1207 include_diff_info=False)
1206 1208
1207 1209 reviewers = validate_default_reviewers(_form['review_members'], reviewer_rules)
1208 1210 observers = validate_observers(_form['observer_members'], reviewer_rules)
1209 1211
1210 1212 pullrequest_title = _form['pullrequest_title']
1211 1213 title_source_ref = source_ref_obj.name
1212 1214 if not pullrequest_title:
1213 1215 pullrequest_title = PullRequestModel().generate_pullrequest_title(
1214 1216 source=source_repo,
1215 1217 source_ref=title_source_ref,
1216 1218 target=target_repo
1217 1219 )
1218 1220
1219 1221 description = _form['pullrequest_desc']
1220 1222 description_renderer = _form['description_renderer']
1221 1223
1222 1224 try:
1223 1225 pull_request = PullRequestModel().create(
1224 1226 created_by=self._rhodecode_user.user_id,
1225 1227 source_repo=source_repo,
1226 1228 source_ref=source_ref,
1227 1229 target_repo=target_repo,
1228 1230 target_ref=target_ref,
1229 1231 revisions=commit_ids,
1230 1232 common_ancestor_id=common_ancestor_id,
1231 1233 reviewers=reviewers,
1232 1234 observers=observers,
1233 1235 title=pullrequest_title,
1234 1236 description=description,
1235 1237 description_renderer=description_renderer,
1236 1238 reviewer_data=reviewer_rules,
1237 1239 auth_user=self._rhodecode_user
1238 1240 )
1239 1241 Session().commit()
1240 1242
1241 1243 h.flash(_('Successfully opened new pull request'),
1242 1244 category='success')
1243 1245 except Exception:
1244 1246 msg = _('Error occurred during creation of this pull request.')
1245 1247 log.exception(msg)
1246 1248 h.flash(msg, category='error')
1247 1249
1248 1250 # copy the args back to redirect
1249 1251 org_query = self.request.GET.mixed()
1250 1252 raise HTTPFound(
1251 1253 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1252 1254 _query=org_query))
1253 1255
1254 1256 raise HTTPFound(
1255 1257 h.route_path('pullrequest_show', repo_name=target_repo,
1256 1258 pull_request_id=pull_request.pull_request_id))
1257 1259
1258 1260 @LoginRequired()
1259 1261 @NotAnonymous()
1260 1262 @HasRepoPermissionAnyDecorator(
1261 1263 'repository.read', 'repository.write', 'repository.admin')
1262 1264 @CSRFRequired()
1263 1265 def pull_request_update(self):
1264 1266 pull_request = PullRequest.get_or_404(
1265 1267 self.request.matchdict['pull_request_id'])
1266 1268 _ = self.request.translate
1267 1269
1268 1270 c = self.load_default_context()
1269 1271 redirect_url = None
1270 1272 # we do this check as first, because we want to know ASAP in the flow that
1271 1273 # pr is updating currently
1272 1274 is_state_changing = pull_request.is_state_changing()
1273 1275
1274 1276 if pull_request.is_closed():
1275 1277 log.debug('update: forbidden because pull request is closed')
1276 1278 msg = _('Cannot update closed pull requests.')
1277 1279 h.flash(msg, category='error')
1278 1280 return {'response': True,
1279 1281 'redirect_url': redirect_url}
1280 1282
1281 1283 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
1282 1284
1283 1285 # only owner or admin can update it
1284 1286 allowed_to_update = PullRequestModel().check_user_update(
1285 1287 pull_request, self._rhodecode_user)
1286 1288
1287 1289 if allowed_to_update:
1288 1290 controls = peppercorn.parse(self.request.POST.items())
1289 1291 force_refresh = str2bool(self.request.POST.get('force_refresh', 'false'))
1290 1292 do_update_commits = str2bool(self.request.POST.get('update_commits', 'false'))
1291 1293
1292 1294 if 'review_members' in controls:
1293 1295 self._update_reviewers(
1294 1296 c,
1295 1297 pull_request, controls['review_members'],
1296 1298 pull_request.reviewer_data,
1297 1299 PullRequestReviewers.ROLE_REVIEWER)
1298 1300 elif 'observer_members' in controls:
1299 1301 self._update_reviewers(
1300 1302 c,
1301 1303 pull_request, controls['observer_members'],
1302 1304 pull_request.reviewer_data,
1303 1305 PullRequestReviewers.ROLE_OBSERVER)
1304 1306 elif do_update_commits:
1305 1307 if is_state_changing:
1306 1308 log.debug('commits update: forbidden because pull request is in state %s',
1307 1309 pull_request.pull_request_state)
1308 1310 msg = _('Cannot update pull requests commits in state other than `{}`. '
1309 1311 'Current state is: `{}`').format(
1310 1312 PullRequest.STATE_CREATED, pull_request.pull_request_state)
1311 1313 h.flash(msg, category='error')
1312 1314 return {'response': True,
1313 1315 'redirect_url': redirect_url}
1314 1316
1315 1317 self._update_commits(c, pull_request)
1316 1318 if force_refresh:
1317 1319 redirect_url = h.route_path(
1318 1320 'pullrequest_show', repo_name=self.db_repo_name,
1319 1321 pull_request_id=pull_request.pull_request_id,
1320 1322 _query={"force_refresh": 1})
1321 1323 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
1322 1324 self._edit_pull_request(pull_request)
1323 1325 else:
1324 1326 log.error('Unhandled update data.')
1325 1327 raise HTTPBadRequest()
1326 1328
1327 1329 return {'response': True,
1328 1330 'redirect_url': redirect_url}
1329 1331 raise HTTPForbidden()
1330 1332
1331 1333 def _edit_pull_request(self, pull_request):
1332 1334 """
1333 1335 Edit title and description
1334 1336 """
1335 1337 _ = self.request.translate
1336 1338
1337 1339 try:
1338 1340 PullRequestModel().edit(
1339 1341 pull_request,
1340 1342 self.request.POST.get('title'),
1341 1343 self.request.POST.get('description'),
1342 1344 self.request.POST.get('description_renderer'),
1343 1345 self._rhodecode_user)
1344 1346 except ValueError:
1345 1347 msg = _('Cannot update closed pull requests.')
1346 1348 h.flash(msg, category='error')
1347 1349 return
1348 1350 else:
1349 1351 Session().commit()
1350 1352
1351 1353 msg = _('Pull request title & description updated.')
1352 1354 h.flash(msg, category='success')
1353 1355 return
1354 1356
1355 1357 def _update_commits(self, c, pull_request):
1356 1358 _ = self.request.translate
1357 1359 log.debug('pull-request: running update commits actions')
1358 1360
1359 1361 @retry(exception=Exception, n_tries=3, delay=2)
1360 1362 def commits_update():
1361 1363 return PullRequestModel().update_commits(
1362 1364 pull_request, self._rhodecode_db_user)
1363 1365
1364 1366 with pull_request.set_state(PullRequest.STATE_UPDATING):
1365 1367 resp = commits_update() # retry x3
1366 1368
1367 1369 if resp.executed:
1368 1370
1369 1371 if resp.target_changed and resp.source_changed:
1370 1372 changed = 'target and source repositories'
1371 1373 elif resp.target_changed and not resp.source_changed:
1372 1374 changed = 'target repository'
1373 1375 elif not resp.target_changed and resp.source_changed:
1374 1376 changed = 'source repository'
1375 1377 else:
1376 1378 changed = 'nothing'
1377 1379
1378 1380 msg = _('Pull request updated to "{source_commit_id}" with '
1379 1381 '{count_added} added, {count_removed} removed commits. '
1380 1382 'Source of changes: {change_source}.')
1381 1383 msg = msg.format(
1382 1384 source_commit_id=pull_request.source_ref_parts.commit_id,
1383 1385 count_added=len(resp.changes.added),
1384 1386 count_removed=len(resp.changes.removed),
1385 1387 change_source=changed)
1386 1388 h.flash(msg, category='success')
1387 1389 channelstream.pr_update_channelstream_push(
1388 1390 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1389 1391 else:
1390 1392 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1391 1393 warning_reasons = [
1392 1394 UpdateFailureReason.NO_CHANGE,
1393 1395 UpdateFailureReason.WRONG_REF_TYPE,
1394 1396 ]
1395 1397 category = 'warning' if resp.reason in warning_reasons else 'error'
1396 1398 h.flash(msg, category=category)
1397 1399
1398 1400 def _update_reviewers(self, c, pull_request, review_members, reviewer_rules, role):
1399 1401 _ = self.request.translate
1400 1402
1401 1403 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1402 1404 PullRequestModel().get_reviewer_functions()
1403 1405
1404 1406 if role == PullRequestReviewers.ROLE_REVIEWER:
1405 1407 try:
1406 1408 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1407 1409 except ValueError as e:
1408 1410 log.error(f'Reviewers Validation: {e}')
1409 1411 h.flash(e, category='error')
1410 1412 return
1411 1413
1412 1414 old_calculated_status = pull_request.calculated_review_status()
1413 1415 PullRequestModel().update_reviewers(
1414 1416 pull_request, reviewers, self._rhodecode_db_user)
1415 1417
1416 1418 Session().commit()
1417 1419
1418 1420 msg = _('Pull request reviewers updated.')
1419 1421 h.flash(msg, category='success')
1420 1422 channelstream.pr_update_channelstream_push(
1421 1423 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1422 1424
1423 1425 # trigger status changed if change in reviewers changes the status
1424 1426 calculated_status = pull_request.calculated_review_status()
1425 1427 if old_calculated_status != calculated_status:
1426 1428 PullRequestModel().trigger_pull_request_hook(
1427 1429 pull_request, self._rhodecode_user, 'review_status_change',
1428 1430 data={'status': calculated_status})
1429 1431
1430 1432 elif role == PullRequestReviewers.ROLE_OBSERVER:
1431 1433 try:
1432 1434 observers = validate_observers(review_members, reviewer_rules)
1433 1435 except ValueError as e:
1434 1436 log.error(f'Observers Validation: {e}')
1435 1437 h.flash(e, category='error')
1436 1438 return
1437 1439
1438 1440 PullRequestModel().update_observers(
1439 1441 pull_request, observers, self._rhodecode_db_user)
1440 1442
1441 1443 Session().commit()
1442 1444 msg = _('Pull request observers updated.')
1443 1445 h.flash(msg, category='success')
1444 1446 channelstream.pr_update_channelstream_push(
1445 1447 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1446 1448
1447 1449 @LoginRequired()
1448 1450 @NotAnonymous()
1449 1451 @HasRepoPermissionAnyDecorator(
1450 1452 'repository.read', 'repository.write', 'repository.admin')
1451 1453 @CSRFRequired()
1452 1454 def pull_request_merge(self):
1453 1455 """
1454 1456 Merge will perform a server-side merge of the specified
1455 1457 pull request, if the pull request is approved and mergeable.
1456 1458 After successful merging, the pull request is automatically
1457 1459 closed, with a relevant comment.
1458 1460 """
1459 1461 pull_request = PullRequest.get_or_404(
1460 1462 self.request.matchdict['pull_request_id'])
1461 1463 _ = self.request.translate
1462 1464
1463 1465 if pull_request.is_state_changing():
1464 1466 log.debug('show: forbidden because pull request is in state %s',
1465 1467 pull_request.pull_request_state)
1466 1468 msg = _('Cannot merge pull requests in state other than `{}`. '
1467 1469 'Current state is: `{}`').format(PullRequest.STATE_CREATED,
1468 1470 pull_request.pull_request_state)
1469 1471 h.flash(msg, category='error')
1470 1472 raise HTTPFound(
1471 1473 h.route_path('pullrequest_show',
1472 1474 repo_name=pull_request.target_repo.repo_name,
1473 1475 pull_request_id=pull_request.pull_request_id))
1474 1476
1475 1477 self.load_default_context()
1476 1478
1477 1479 with pull_request.set_state(PullRequest.STATE_UPDATING):
1478 1480 check = MergeCheck.validate(
1479 1481 pull_request, auth_user=self._rhodecode_user,
1480 1482 translator=self.request.translate)
1481 1483 merge_possible = not check.failed
1482 1484
1483 1485 for err_type, error_msg in check.errors:
1484 1486 h.flash(error_msg, category=err_type)
1485 1487
1486 1488 if merge_possible:
1487 1489 log.debug("Pre-conditions checked, trying to merge.")
1488 1490 extras = vcs_operation_context(
1489 1491 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1490 1492 username=self._rhodecode_db_user.username, action='push',
1491 1493 scm=pull_request.target_repo.repo_type)
1492 1494 with pull_request.set_state(PullRequest.STATE_UPDATING):
1493 1495 self._merge_pull_request(
1494 1496 pull_request, self._rhodecode_db_user, extras)
1495 1497 else:
1496 1498 log.debug("Pre-conditions failed, NOT merging.")
1497 1499
1498 1500 raise HTTPFound(
1499 1501 h.route_path('pullrequest_show',
1500 1502 repo_name=pull_request.target_repo.repo_name,
1501 1503 pull_request_id=pull_request.pull_request_id))
1502 1504
1503 1505 def _merge_pull_request(self, pull_request, user, extras):
1504 1506 _ = self.request.translate
1505 1507 merge_resp = PullRequestModel().merge_repo(pull_request, user, extras=extras)
1506 1508
1507 1509 if merge_resp.executed:
1508 1510 log.debug("The merge was successful, closing the pull request.")
1509 1511 PullRequestModel().close_pull_request(
1510 1512 pull_request.pull_request_id, user)
1511 1513 Session().commit()
1512 1514 msg = _('Pull request was successfully merged and closed.')
1513 1515 h.flash(msg, category='success')
1514 1516 else:
1515 1517 log.debug(
1516 1518 "The merge was not successful. Merge response: %s", merge_resp)
1517 1519 msg = merge_resp.merge_status_message
1518 1520 h.flash(msg, category='error')
1519 1521
1520 1522 @LoginRequired()
1521 1523 @NotAnonymous()
1522 1524 @HasRepoPermissionAnyDecorator(
1523 1525 'repository.read', 'repository.write', 'repository.admin')
1524 1526 @CSRFRequired()
1525 1527 def pull_request_delete(self):
1526 1528 _ = self.request.translate
1527 1529
1528 1530 pull_request = PullRequest.get_or_404(
1529 1531 self.request.matchdict['pull_request_id'])
1530 1532 self.load_default_context()
1531 1533
1532 1534 pr_closed = pull_request.is_closed()
1533 1535 allowed_to_delete = PullRequestModel().check_user_delete(
1534 1536 pull_request, self._rhodecode_user) and not pr_closed
1535 1537
1536 1538 # only owner can delete it !
1537 1539 if allowed_to_delete:
1538 1540 PullRequestModel().delete(pull_request, self._rhodecode_user)
1539 1541 Session().commit()
1540 1542 h.flash(_('Successfully deleted pull request'),
1541 1543 category='success')
1542 1544 raise HTTPFound(h.route_path('pullrequest_show_all',
1543 1545 repo_name=self.db_repo_name))
1544 1546
1545 1547 log.warning('user %s tried to delete pull request without access',
1546 1548 self._rhodecode_user)
1547 1549 raise HTTPNotFound()
1548 1550
1549 1551 def _pull_request_comments_create(self, pull_request, comments):
1550 1552 _ = self.request.translate
1551 1553 data = {}
1552 1554 if not comments:
1553 1555 return
1554 1556 pull_request_id = pull_request.pull_request_id
1555 1557
1556 1558 all_drafts = len([x for x in comments if str2bool(x['is_draft'])]) == len(comments)
1557 1559
1558 1560 for entry in comments:
1559 1561 c = self.load_default_context()
1560 1562 comment_type = entry['comment_type']
1561 1563 text = entry['text']
1562 1564 status = entry['status']
1563 1565 is_draft = str2bool(entry['is_draft'])
1564 1566 resolves_comment_id = entry['resolves_comment_id']
1565 1567 close_pull_request = entry['close_pull_request']
1566 1568 f_path = entry['f_path']
1567 1569 line_no = entry['line']
1568 1570 target_elem_id = f'file-{h.safeid(h.safe_str(f_path))}'
1569 1571
1570 1572 # the logic here should work like following, if we submit close
1571 1573 # pr comment, use `close_pull_request_with_comment` function
1572 1574 # else handle regular comment logic
1573 1575
1574 1576 if close_pull_request:
1575 1577 # only owner or admin or person with write permissions
1576 1578 allowed_to_close = PullRequestModel().check_user_update(
1577 1579 pull_request, self._rhodecode_user)
1578 1580 if not allowed_to_close:
1579 1581 log.debug('comment: forbidden because not allowed to close '
1580 1582 'pull request %s', pull_request_id)
1581 1583 raise HTTPForbidden()
1582 1584
1583 1585 # This also triggers `review_status_change`
1584 1586 comment, status = PullRequestModel().close_pull_request_with_comment(
1585 1587 pull_request, self._rhodecode_user, self.db_repo, message=text,
1586 1588 auth_user=self._rhodecode_user)
1587 1589 Session().flush()
1588 1590 is_inline = comment.is_inline
1589 1591
1590 1592 PullRequestModel().trigger_pull_request_hook(
1591 1593 pull_request, self._rhodecode_user, 'comment',
1592 1594 data={'comment': comment})
1593 1595
1594 1596 else:
1595 1597 # regular comment case, could be inline, or one with status.
1596 1598 # for that one we check also permissions
1597 1599 # Additionally ENSURE if somehow draft is sent we're then unable to change status
1598 1600 allowed_to_change_status = PullRequestModel().check_user_change_status(
1599 1601 pull_request, self._rhodecode_user) and not is_draft
1600 1602
1601 1603 if status and allowed_to_change_status:
1602 1604 message = (_('Status change %(transition_icon)s %(status)s')
1603 1605 % {'transition_icon': '>',
1604 1606 'status': ChangesetStatus.get_status_lbl(status)})
1605 1607 text = text or message
1606 1608
1607 1609 comment = CommentsModel().create(
1608 1610 text=text,
1609 1611 repo=self.db_repo.repo_id,
1610 1612 user=self._rhodecode_user.user_id,
1611 1613 pull_request=pull_request,
1612 1614 f_path=f_path,
1613 1615 line_no=line_no,
1614 1616 status_change=(ChangesetStatus.get_status_lbl(status)
1615 1617 if status and allowed_to_change_status else None),
1616 1618 status_change_type=(status
1617 1619 if status and allowed_to_change_status else None),
1618 1620 comment_type=comment_type,
1619 1621 is_draft=is_draft,
1620 1622 resolves_comment_id=resolves_comment_id,
1621 1623 auth_user=self._rhodecode_user,
1622 1624 send_email=not is_draft, # skip notification for draft comments
1623 1625 )
1624 1626 is_inline = comment.is_inline
1625 1627
1626 1628 if allowed_to_change_status:
1627 1629 # calculate old status before we change it
1628 1630 old_calculated_status = pull_request.calculated_review_status()
1629 1631
1630 1632 # get status if set !
1631 1633 if status:
1632 1634 ChangesetStatusModel().set_status(
1633 1635 self.db_repo.repo_id,
1634 1636 status,
1635 1637 self._rhodecode_user.user_id,
1636 1638 comment,
1637 1639 pull_request=pull_request
1638 1640 )
1639 1641
1640 1642 Session().flush()
1641 1643 # this is somehow required to get access to some relationship
1642 1644 # loaded on comment
1643 1645 Session().refresh(comment)
1644 1646
1645 1647 # skip notifications for drafts
1646 1648 if not is_draft:
1647 1649 PullRequestModel().trigger_pull_request_hook(
1648 1650 pull_request, self._rhodecode_user, 'comment',
1649 1651 data={'comment': comment})
1650 1652
1651 1653 # we now calculate the status of pull request, and based on that
1652 1654 # calculation we set the commits status
1653 1655 calculated_status = pull_request.calculated_review_status()
1654 1656 if old_calculated_status != calculated_status:
1655 1657 PullRequestModel().trigger_pull_request_hook(
1656 1658 pull_request, self._rhodecode_user, 'review_status_change',
1657 1659 data={'status': calculated_status})
1658 1660
1659 1661 comment_id = comment.comment_id
1660 1662 data[comment_id] = {
1661 1663 'target_id': target_elem_id
1662 1664 }
1663 1665 Session().flush()
1664 1666
1665 1667 c.co = comment
1666 1668 c.at_version_num = None
1667 1669 c.is_new = True
1668 1670 rendered_comment = render(
1669 1671 'rhodecode:templates/changeset/changeset_comment_block.mako',
1670 1672 self._get_template_context(c), self.request)
1671 1673
1672 1674 data[comment_id].update(comment.get_dict())
1673 1675 data[comment_id].update({'rendered_text': rendered_comment})
1674 1676
1675 1677 Session().commit()
1676 1678
1677 1679 # skip channelstream for draft comments
1678 1680 if not all_drafts:
1679 1681 comment_broadcast_channel = channelstream.comment_channel(
1680 1682 self.db_repo_name, pull_request_obj=pull_request)
1681 1683
1682 1684 comment_data = data
1683 1685 posted_comment_type = 'inline' if is_inline else 'general'
1684 1686 if len(data) == 1:
1685 1687 msg = _('posted {} new {} comment').format(len(data), posted_comment_type)
1686 1688 else:
1687 1689 msg = _('posted {} new {} comments').format(len(data), posted_comment_type)
1688 1690
1689 1691 channelstream.comment_channelstream_push(
1690 1692 self.request, comment_broadcast_channel, self._rhodecode_user, msg,
1691 1693 comment_data=comment_data)
1692 1694
1693 1695 return data
1694 1696
1695 1697 @LoginRequired()
1696 1698 @NotAnonymous()
1697 1699 @HasRepoPermissionAnyDecorator(
1698 1700 'repository.read', 'repository.write', 'repository.admin')
1699 1701 @CSRFRequired()
1700 1702 def pull_request_comment_create(self):
1701 1703 _ = self.request.translate
1702 1704
1703 1705 pull_request = PullRequest.get_or_404(self.request.matchdict['pull_request_id'])
1704 1706
1705 1707 if pull_request.is_closed():
1706 1708 log.debug('comment: forbidden because pull request is closed')
1707 1709 raise HTTPForbidden()
1708 1710
1709 1711 allowed_to_comment = PullRequestModel().check_user_comment(
1710 1712 pull_request, self._rhodecode_user)
1711 1713 if not allowed_to_comment:
1712 1714 log.debug('comment: forbidden because pull request is from forbidden repo')
1713 1715 raise HTTPForbidden()
1714 1716
1715 1717 comment_data = {
1716 1718 'comment_type': self.request.POST.get('comment_type'),
1717 1719 'text': self.request.POST.get('text'),
1718 1720 'status': self.request.POST.get('changeset_status', None),
1719 1721 'is_draft': self.request.POST.get('draft'),
1720 1722 'resolves_comment_id': self.request.POST.get('resolves_comment_id', None),
1721 1723 'close_pull_request': self.request.POST.get('close_pull_request'),
1722 1724 'f_path': self.request.POST.get('f_path'),
1723 1725 'line': self.request.POST.get('line'),
1724 1726 }
1727
1725 1728 data = self._pull_request_comments_create(pull_request, [comment_data])
1726 1729
1727 1730 return data
1728 1731
1729 1732 @LoginRequired()
1730 1733 @NotAnonymous()
1731 1734 @HasRepoPermissionAnyDecorator(
1732 1735 'repository.read', 'repository.write', 'repository.admin')
1733 1736 @CSRFRequired()
1734 1737 def pull_request_comment_delete(self):
1735 1738 pull_request = PullRequest.get_or_404(
1736 1739 self.request.matchdict['pull_request_id'])
1737 1740
1738 1741 comment = ChangesetComment.get_or_404(
1739 1742 self.request.matchdict['comment_id'])
1740 1743 comment_id = comment.comment_id
1741 1744
1742 1745 if comment.immutable:
1743 1746 # don't allow deleting comments that are immutable
1744 1747 raise HTTPForbidden()
1745 1748
1746 1749 if pull_request.is_closed():
1747 1750 log.debug('comment: forbidden because pull request is closed')
1748 1751 raise HTTPForbidden()
1749 1752
1750 1753 if not comment:
1751 1754 log.debug('Comment with id:%s not found, skipping', comment_id)
1752 1755 # comment already deleted in another call probably
1753 1756 return True
1754 1757
1755 1758 if comment.pull_request.is_closed():
1756 1759 # don't allow deleting comments on closed pull request
1757 1760 raise HTTPForbidden()
1758 1761
1759 1762 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1760 1763 super_admin = h.HasPermissionAny('hg.admin')()
1761 1764 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1762 1765 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1763 1766 comment_repo_admin = is_repo_admin and is_repo_comment
1764 1767
1765 1768 if comment.draft and not comment_owner:
1766 1769 # We never allow to delete draft comments for other than owners
1767 1770 raise HTTPNotFound()
1768 1771
1769 1772 if super_admin or comment_owner or comment_repo_admin:
1770 1773 old_calculated_status = comment.pull_request.calculated_review_status()
1771 1774 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
1772 1775 Session().commit()
1773 1776 calculated_status = comment.pull_request.calculated_review_status()
1774 1777 if old_calculated_status != calculated_status:
1775 1778 PullRequestModel().trigger_pull_request_hook(
1776 1779 comment.pull_request, self._rhodecode_user, 'review_status_change',
1777 1780 data={'status': calculated_status})
1778 1781 return True
1779 1782 else:
1780 1783 log.warning('No permissions for user %s to delete comment_id: %s',
1781 1784 self._rhodecode_db_user, comment_id)
1782 1785 raise HTTPNotFound()
1783 1786
1784 1787 @LoginRequired()
1785 1788 @NotAnonymous()
1786 1789 @HasRepoPermissionAnyDecorator(
1787 1790 'repository.read', 'repository.write', 'repository.admin')
1788 1791 @CSRFRequired()
1789 1792 def pull_request_comment_edit(self):
1790 1793 self.load_default_context()
1791 1794
1792 1795 pull_request = PullRequest.get_or_404(
1793 1796 self.request.matchdict['pull_request_id']
1794 1797 )
1795 1798 comment = ChangesetComment.get_or_404(
1796 1799 self.request.matchdict['comment_id']
1797 1800 )
1798 1801 comment_id = comment.comment_id
1799 1802
1800 1803 if comment.immutable:
1801 1804 # don't allow deleting comments that are immutable
1802 1805 raise HTTPForbidden()
1803 1806
1804 1807 if pull_request.is_closed():
1805 1808 log.debug('comment: forbidden because pull request is closed')
1806 1809 raise HTTPForbidden()
1807 1810
1808 1811 if comment.pull_request.is_closed():
1809 1812 # don't allow deleting comments on closed pull request
1810 1813 raise HTTPForbidden()
1811 1814
1812 1815 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1813 1816 super_admin = h.HasPermissionAny('hg.admin')()
1814 1817 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1815 1818 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1816 1819 comment_repo_admin = is_repo_admin and is_repo_comment
1817 1820
1818 1821 if super_admin or comment_owner or comment_repo_admin:
1819 1822 text = self.request.POST.get('text')
1820 1823 version = self.request.POST.get('version')
1821 1824 if text == comment.text:
1822 1825 log.warning(
1823 1826 'Comment(PR): '
1824 1827 'Trying to create new version '
1825 1828 'with the same comment body {}'.format(
1826 1829 comment_id,
1827 1830 )
1828 1831 )
1829 1832 raise HTTPNotFound()
1830 1833
1831 1834 if version.isdigit():
1832 1835 version = int(version)
1833 1836 else:
1834 1837 log.warning(
1835 1838 'Comment(PR): Wrong version type {} {} '
1836 1839 'for comment {}'.format(
1837 1840 version,
1838 1841 type(version),
1839 1842 comment_id,
1840 1843 )
1841 1844 )
1842 1845 raise HTTPNotFound()
1843 1846
1844 1847 try:
1845 1848 comment_history = CommentsModel().edit(
1846 1849 comment_id=comment_id,
1847 1850 text=text,
1848 1851 auth_user=self._rhodecode_user,
1849 1852 version=version,
1850 1853 )
1851 1854 except CommentVersionMismatch:
1852 1855 raise HTTPConflict()
1853 1856
1854 1857 if not comment_history:
1855 1858 raise HTTPNotFound()
1856 1859
1857 1860 Session().commit()
1858 1861 if not comment.draft:
1859 1862 PullRequestModel().trigger_pull_request_hook(
1860 1863 pull_request, self._rhodecode_user, 'comment_edit',
1861 1864 data={'comment': comment})
1862 1865
1863 1866 return {
1864 1867 'comment_history_id': comment_history.comment_history_id,
1865 1868 'comment_id': comment.comment_id,
1866 1869 'comment_version': comment_history.version,
1867 1870 'comment_author_username': comment_history.author.username,
1868 1871 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16, request=self.request),
1869 1872 'comment_created_on': h.age_component(comment_history.created_on,
1870 1873 time_is_local=True),
1871 1874 }
1872 1875 else:
1873 1876 log.warning('No permissions for user %s to edit comment_id: %s',
1874 1877 self._rhodecode_db_user, comment_id)
1875 1878 raise HTTPNotFound()
@@ -1,5867 +1,5885 b''
1 1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 """
20 20 Database Models for RhodeCode Enterprise
21 21 """
22 22
23 23 import re
24 24 import os
25 25 import time
26 26 import string
27 27 import logging
28 28 import datetime
29 29 import uuid
30 30 import warnings
31 31 import ipaddress
32 32 import functools
33 33 import traceback
34 34 import collections
35 35
36 36 from sqlalchemy import (
37 37 or_, and_, not_, func, cast, TypeDecorator, event, select,
38 38 true, false, null,
39 39 Index, Sequence, UniqueConstraint, ForeignKey, CheckConstraint, Column,
40 40 Boolean, String, Unicode, UnicodeText, DateTime, Integer, LargeBinary,
41 41 Text, Float, PickleType, BigInteger)
42 42 from sqlalchemy.sql.expression import case
43 43 from sqlalchemy.sql.functions import coalesce, count # pragma: no cover
44 44 from sqlalchemy.orm import (
45 45 relationship, lazyload, joinedload, class_mapper, validates, aliased, load_only)
46 46 from sqlalchemy.ext.declarative import declared_attr
47 47 from sqlalchemy.ext.hybrid import hybrid_property
48 48 from sqlalchemy.exc import IntegrityError # pragma: no cover
49 49 from sqlalchemy.dialects.mysql import LONGTEXT
50 50 from zope.cachedescriptors.property import Lazy as LazyProperty
51 51 from pyramid.threadlocal import get_current_request
52 52 from webhelpers2.text import remove_formatting
53 53
54 54 from rhodecode.lib.str_utils import safe_bytes
55 55 from rhodecode.translation import _
56 56 from rhodecode.lib.vcs import get_vcs_instance, VCSError
57 57 from rhodecode.lib.vcs.backends.base import (
58 58 EmptyCommit, Reference, unicode_to_reference, reference_to_unicode)
59 59 from rhodecode.lib.utils2 import (
60 60 str2bool, safe_str, get_commit_safe, sha1_safe,
61 61 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
62 62 glob2re, StrictAttributeDict, cleaned_uri, datetime_to_time)
63 63 from rhodecode.lib.jsonalchemy import (
64 64 MutationObj, MutationList, JsonType, JsonRaw)
65 65 from rhodecode.lib.hash_utils import sha1
66 66 from rhodecode.lib import ext_json
67 67 from rhodecode.lib import enc_utils
68 from rhodecode.lib.ext_json import json
68 from rhodecode.lib.ext_json import json, str_json
69 69 from rhodecode.lib.caching_query import FromCache
70 70 from rhodecode.lib.exceptions import (
71 71 ArtifactMetadataDuplicate, ArtifactMetadataBadValueType)
72 72 from rhodecode.model.meta import Base, Session
73 73
74 74 URL_SEP = '/'
75 75 log = logging.getLogger(__name__)
76 76
77 77 # =============================================================================
78 78 # BASE CLASSES
79 79 # =============================================================================
80 80
81 81 # this is propagated from .ini file rhodecode.encrypted_values.secret or
82 82 # beaker.session.secret if first is not set.
83 83 # and initialized at environment.py
84 84 ENCRYPTION_KEY: bytes = b''
85 85
86 86 # used to sort permissions by types, '#' used here is not allowed to be in
87 87 # usernames, and it's very early in sorted string.printable table.
88 88 PERMISSION_TYPE_SORT = {
89 89 'admin': '####',
90 90 'write': '###',
91 91 'read': '##',
92 92 'none': '#',
93 93 }
94 94
95 95
96 96 def display_user_sort(obj):
97 97 """
98 98 Sort function used to sort permissions in .permissions() function of
99 99 Repository, RepoGroup, UserGroup. Also it put the default user in front
100 100 of all other resources
101 101 """
102 102
103 103 if obj.username == User.DEFAULT_USER:
104 104 return '#####'
105 105 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
106 106 extra_sort_num = '1' # default
107 107
108 108 # NOTE(dan): inactive duplicates goes last
109 109 if getattr(obj, 'duplicate_perm', None):
110 110 extra_sort_num = '9'
111 111 return prefix + extra_sort_num + obj.username
112 112
113 113
114 114 def display_user_group_sort(obj):
115 115 """
116 116 Sort function used to sort permissions in .permissions() function of
117 117 Repository, RepoGroup, UserGroup. Also it put the default user in front
118 118 of all other resources
119 119 """
120 120
121 121 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
122 122 return prefix + obj.users_group_name
123 123
124 124
125 125 def _hash_key(k):
126 126 return sha1_safe(k)
127 127
128 128
129 129 def in_filter_generator(qry, items, limit=500):
130 130 """
131 131 Splits IN() into multiple with OR
132 132 e.g.::
133 133 cnt = Repository.query().filter(
134 134 or_(
135 135 *in_filter_generator(Repository.repo_id, range(100000))
136 136 )).count()
137 137 """
138 138 if not items:
139 139 # empty list will cause empty query which might cause security issues
140 140 # this can lead to hidden unpleasant results
141 141 items = [-1]
142 142
143 143 parts = []
144 144 for chunk in range(0, len(items), limit):
145 145 parts.append(
146 146 qry.in_(items[chunk: chunk + limit])
147 147 )
148 148
149 149 return parts
150 150
151 151
152 152 base_table_args = {
153 153 'extend_existing': True,
154 154 'mysql_engine': 'InnoDB',
155 155 'mysql_charset': 'utf8',
156 156 'sqlite_autoincrement': True
157 157 }
158 158
159 159
160 160 class EncryptedTextValue(TypeDecorator):
161 161 """
162 162 Special column for encrypted long text data, use like::
163 163
164 164 value = Column("encrypted_value", EncryptedValue(), nullable=False)
165 165
166 166 This column is intelligent so if value is in unencrypted form it return
167 167 unencrypted form, but on save it always encrypts
168 168 """
169 169 cache_ok = True
170 170 impl = Text
171 171
172 172 def process_bind_param(self, value, dialect):
173 173 """
174 174 Setter for storing value
175 175 """
176 176 import rhodecode
177 177 if not value:
178 178 return value
179 179
180 180 # protect against double encrypting if values is already encrypted
181 181 if value.startswith('enc$aes$') \
182 182 or value.startswith('enc$aes_hmac$') \
183 183 or value.startswith('enc2$'):
184 184 raise ValueError('value needs to be in unencrypted format, '
185 185 'ie. not starting with enc$ or enc2$')
186 186
187 187 algo = rhodecode.CONFIG.get('rhodecode.encrypted_values.algorithm') or 'aes'
188 188 bytes_val = enc_utils.encrypt_value(value, enc_key=ENCRYPTION_KEY, algo=algo)
189 189 return safe_str(bytes_val)
190 190
191 191 def process_result_value(self, value, dialect):
192 192 """
193 193 Getter for retrieving value
194 194 """
195 195
196 196 import rhodecode
197 197 if not value:
198 198 return value
199 199
200 200 enc_strict_mode = rhodecode.ConfigGet().get_bool('rhodecode.encrypted_values.strict', missing=True)
201 201
202 202 bytes_val = enc_utils.decrypt_value(value, enc_key=ENCRYPTION_KEY, strict_mode=enc_strict_mode)
203 203
204 204 return safe_str(bytes_val)
205 205
206 206
207 207 class BaseModel(object):
208 208 """
209 209 Base Model for all classes
210 210 """
211 211
212 212 @classmethod
213 213 def _get_keys(cls):
214 214 """return column names for this model """
215 215 return class_mapper(cls).c.keys()
216 216
217 217 def get_dict(self):
218 218 """
219 219 return dict with keys and values corresponding
220 220 to this model data """
221 221
222 222 d = {}
223 223 for k in self._get_keys():
224 224 d[k] = getattr(self, k)
225 225
226 226 # also use __json__() if present to get additional fields
227 227 _json_attr = getattr(self, '__json__', None)
228 228 if _json_attr:
229 229 # update with attributes from __json__
230 230 if callable(_json_attr):
231 231 _json_attr = _json_attr()
232 232 for k, val in _json_attr.items():
233 233 d[k] = val
234 234 return d
235 235
236 236 def get_appstruct(self):
237 237 """return list with keys and values tuples corresponding
238 238 to this model data """
239 239
240 240 lst = []
241 241 for k in self._get_keys():
242 242 lst.append((k, getattr(self, k),))
243 243 return lst
244 244
245 245 def populate_obj(self, populate_dict):
246 246 """populate model with data from given populate_dict"""
247 247
248 248 for k in self._get_keys():
249 249 if k in populate_dict:
250 250 setattr(self, k, populate_dict[k])
251 251
252 252 @classmethod
253 253 def query(cls):
254 254 return Session().query(cls)
255 255
256 256 @classmethod
257 257 def select(cls, custom_cls=None):
258 258 """
259 259 stmt = cls.select().where(cls.user_id==1)
260 260 # optionally
261 261 stmt = cls.select(User.user_id).where(cls.user_id==1)
262 262 result = cls.execute(stmt) | cls.scalars(stmt)
263 263 """
264 264
265 265 if custom_cls:
266 266 stmt = select(custom_cls)
267 267 else:
268 268 stmt = select(cls)
269 269 return stmt
270 270
271 271 @classmethod
272 272 def execute(cls, stmt):
273 273 return Session().execute(stmt)
274 274
275 275 @classmethod
276 276 def scalars(cls, stmt):
277 277 return Session().scalars(stmt)
278 278
279 279 @classmethod
280 280 def get(cls, id_):
281 281 if id_:
282 282 return cls.query().get(id_)
283 283
284 284 @classmethod
285 285 def get_or_404(cls, id_):
286 286 from pyramid.httpexceptions import HTTPNotFound
287 287
288 288 try:
289 289 id_ = int(id_)
290 290 except (TypeError, ValueError):
291 291 raise HTTPNotFound()
292 292
293 293 res = cls.query().get(id_)
294 294 if not res:
295 295 raise HTTPNotFound()
296 296 return res
297 297
298 298 @classmethod
299 299 def getAll(cls):
300 300 # deprecated and left for backward compatibility
301 301 return cls.get_all()
302 302
303 303 @classmethod
304 304 def get_all(cls):
305 305 return cls.query().all()
306 306
307 307 @classmethod
308 308 def delete(cls, id_):
309 309 obj = cls.query().get(id_)
310 310 Session().delete(obj)
311 311
312 312 @classmethod
313 313 def identity_cache(cls, session, attr_name, value):
314 314 exist_in_session = []
315 315 for (item_cls, pkey), instance in session.identity_map.items():
316 316 if cls == item_cls and getattr(instance, attr_name) == value:
317 317 exist_in_session.append(instance)
318 318 if exist_in_session:
319 319 if len(exist_in_session) == 1:
320 320 return exist_in_session[0]
321 321 log.exception(
322 322 'multiple objects with attr %s and '
323 323 'value %s found with same name: %r',
324 324 attr_name, value, exist_in_session)
325 325
326 326 @property
327 327 def cls_name(self):
328 328 return self.__class__.__name__
329 329
330 330 def __repr__(self):
331 331 return f'<DB:{self.cls_name}>'
332 332
333 333
334 334 class RhodeCodeSetting(Base, BaseModel):
335 335 __tablename__ = 'rhodecode_settings'
336 336 __table_args__ = (
337 337 UniqueConstraint('app_settings_name'),
338 338 base_table_args
339 339 )
340 340
341 341 SETTINGS_TYPES = {
342 342 'str': safe_str,
343 343 'int': safe_int,
344 344 'unicode': safe_str,
345 345 'bool': str2bool,
346 346 'list': functools.partial(aslist, sep=',')
347 347 }
348 348 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
349 349 GLOBAL_CONF_KEY = 'app_settings'
350 350
351 351 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
352 352 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
353 353 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
354 354 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
355 355
356 356 def __init__(self, key='', val='', type='unicode'):
357 357 self.app_settings_name = key
358 358 self.app_settings_type = type
359 359 self.app_settings_value = val
360 360
361 361 @validates('_app_settings_value')
362 362 def validate_settings_value(self, key, val):
363 363 assert type(val) == str
364 364 return val
365 365
366 366 @hybrid_property
367 367 def app_settings_value(self):
368 368 v = self._app_settings_value
369 369 _type = self.app_settings_type
370 370 if _type:
371 371 _type = self.app_settings_type.split('.')[0]
372 372 # decode the encrypted value
373 373 if 'encrypted' in self.app_settings_type:
374 374 cipher = EncryptedTextValue()
375 375 v = safe_str(cipher.process_result_value(v, None))
376 376
377 377 converter = self.SETTINGS_TYPES.get(_type) or \
378 378 self.SETTINGS_TYPES['unicode']
379 379 return converter(v)
380 380
381 381 @app_settings_value.setter
382 382 def app_settings_value(self, val):
383 383 """
384 384 Setter that will always make sure we use unicode in app_settings_value
385 385
386 386 :param val:
387 387 """
388 388 val = safe_str(val)
389 389 # encode the encrypted value
390 390 if 'encrypted' in self.app_settings_type:
391 391 cipher = EncryptedTextValue()
392 392 val = safe_str(cipher.process_bind_param(val, None))
393 393 self._app_settings_value = val
394 394
395 395 @hybrid_property
396 396 def app_settings_type(self):
397 397 return self._app_settings_type
398 398
399 399 @app_settings_type.setter
400 400 def app_settings_type(self, val):
401 401 if val.split('.')[0] not in self.SETTINGS_TYPES:
402 402 raise Exception('type must be one of %s got %s'
403 403 % (self.SETTINGS_TYPES.keys(), val))
404 404 self._app_settings_type = val
405 405
406 406 @classmethod
407 407 def get_by_prefix(cls, prefix):
408 408 return RhodeCodeSetting.query()\
409 409 .filter(RhodeCodeSetting.app_settings_name.startswith(prefix))\
410 410 .all()
411 411
412 412 def __repr__(self):
413 413 return "<%s('%s:%s[%s]')>" % (
414 414 self.cls_name,
415 415 self.app_settings_name, self.app_settings_value,
416 416 self.app_settings_type
417 417 )
418 418
419 419
420 420 class RhodeCodeUi(Base, BaseModel):
421 421 __tablename__ = 'rhodecode_ui'
422 422 __table_args__ = (
423 423 UniqueConstraint('ui_key'),
424 424 base_table_args
425 425 )
426 426 # Sync those values with vcsserver.config.hooks
427 427
428 428 HOOK_REPO_SIZE = 'changegroup.repo_size'
429 429 # HG
430 430 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
431 431 HOOK_PULL = 'outgoing.pull_logger'
432 432 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
433 433 HOOK_PRETX_PUSH = 'pretxnchangegroup.pre_push'
434 434 HOOK_PUSH = 'changegroup.push_logger'
435 435 HOOK_PUSH_KEY = 'pushkey.key_push'
436 436
437 437 HOOKS_BUILTIN = [
438 438 HOOK_PRE_PULL,
439 439 HOOK_PULL,
440 440 HOOK_PRE_PUSH,
441 441 HOOK_PRETX_PUSH,
442 442 HOOK_PUSH,
443 443 HOOK_PUSH_KEY,
444 444 ]
445 445
446 446 # TODO: johbo: Unify way how hooks are configured for git and hg,
447 447 # git part is currently hardcoded.
448 448
449 449 # SVN PATTERNS
450 450 SVN_BRANCH_ID = 'vcs_svn_branch'
451 451 SVN_TAG_ID = 'vcs_svn_tag'
452 452
453 453 ui_id = Column(
454 454 "ui_id", Integer(), nullable=False, unique=True, default=None,
455 455 primary_key=True)
456 456 ui_section = Column(
457 457 "ui_section", String(255), nullable=True, unique=None, default=None)
458 458 ui_key = Column(
459 459 "ui_key", String(255), nullable=True, unique=None, default=None)
460 460 ui_value = Column(
461 461 "ui_value", String(255), nullable=True, unique=None, default=None)
462 462 ui_active = Column(
463 463 "ui_active", Boolean(), nullable=True, unique=None, default=True)
464 464
465 465 def __repr__(self):
466 466 return '<%s[%s]%s=>%s]>' % (self.cls_name, self.ui_section,
467 467 self.ui_key, self.ui_value)
468 468
469 469
470 470 class RepoRhodeCodeSetting(Base, BaseModel):
471 471 __tablename__ = 'repo_rhodecode_settings'
472 472 __table_args__ = (
473 473 UniqueConstraint(
474 474 'app_settings_name', 'repository_id',
475 475 name='uq_repo_rhodecode_setting_name_repo_id'),
476 476 base_table_args
477 477 )
478 478
479 479 repository_id = Column(
480 480 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
481 481 nullable=False)
482 482 app_settings_id = Column(
483 483 "app_settings_id", Integer(), nullable=False, unique=True,
484 484 default=None, primary_key=True)
485 485 app_settings_name = Column(
486 486 "app_settings_name", String(255), nullable=True, unique=None,
487 487 default=None)
488 488 _app_settings_value = Column(
489 489 "app_settings_value", String(4096), nullable=True, unique=None,
490 490 default=None)
491 491 _app_settings_type = Column(
492 492 "app_settings_type", String(255), nullable=True, unique=None,
493 493 default=None)
494 494
495 495 repository = relationship('Repository', viewonly=True)
496 496
497 497 def __init__(self, repository_id, key='', val='', type='unicode'):
498 498 self.repository_id = repository_id
499 499 self.app_settings_name = key
500 500 self.app_settings_type = type
501 501 self.app_settings_value = val
502 502
503 503 @validates('_app_settings_value')
504 504 def validate_settings_value(self, key, val):
505 505 assert type(val) == str
506 506 return val
507 507
508 508 @hybrid_property
509 509 def app_settings_value(self):
510 510 v = self._app_settings_value
511 511 type_ = self.app_settings_type
512 512 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
513 513 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
514 514 return converter(v)
515 515
516 516 @app_settings_value.setter
517 517 def app_settings_value(self, val):
518 518 """
519 519 Setter that will always make sure we use unicode in app_settings_value
520 520
521 521 :param val:
522 522 """
523 523 self._app_settings_value = safe_str(val)
524 524
525 525 @hybrid_property
526 526 def app_settings_type(self):
527 527 return self._app_settings_type
528 528
529 529 @app_settings_type.setter
530 530 def app_settings_type(self, val):
531 531 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
532 532 if val not in SETTINGS_TYPES:
533 533 raise Exception('type must be one of %s got %s'
534 534 % (SETTINGS_TYPES.keys(), val))
535 535 self._app_settings_type = val
536 536
537 537 def __repr__(self):
538 538 return "<%s('%s:%s:%s[%s]')>" % (
539 539 self.cls_name, self.repository.repo_name,
540 540 self.app_settings_name, self.app_settings_value,
541 541 self.app_settings_type
542 542 )
543 543
544 544
545 545 class RepoRhodeCodeUi(Base, BaseModel):
546 546 __tablename__ = 'repo_rhodecode_ui'
547 547 __table_args__ = (
548 548 UniqueConstraint(
549 549 'repository_id', 'ui_section', 'ui_key',
550 550 name='uq_repo_rhodecode_ui_repository_id_section_key'),
551 551 base_table_args
552 552 )
553 553
554 554 repository_id = Column(
555 555 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
556 556 nullable=False)
557 557 ui_id = Column(
558 558 "ui_id", Integer(), nullable=False, unique=True, default=None,
559 559 primary_key=True)
560 560 ui_section = Column(
561 561 "ui_section", String(255), nullable=True, unique=None, default=None)
562 562 ui_key = Column(
563 563 "ui_key", String(255), nullable=True, unique=None, default=None)
564 564 ui_value = Column(
565 565 "ui_value", String(255), nullable=True, unique=None, default=None)
566 566 ui_active = Column(
567 567 "ui_active", Boolean(), nullable=True, unique=None, default=True)
568 568
569 569 repository = relationship('Repository', viewonly=True)
570 570
571 571 def __repr__(self):
572 572 return '<%s[%s:%s]%s=>%s]>' % (
573 573 self.cls_name, self.repository.repo_name,
574 574 self.ui_section, self.ui_key, self.ui_value)
575 575
576 576
577 577 class User(Base, BaseModel):
578 578 __tablename__ = 'users'
579 579 __table_args__ = (
580 580 UniqueConstraint('username'), UniqueConstraint('email'),
581 581 Index('u_username_idx', 'username'),
582 582 Index('u_email_idx', 'email'),
583 583 base_table_args
584 584 )
585 585
586 586 DEFAULT_USER = 'default'
587 587 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
588 588 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
589 589
590 590 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
591 591 username = Column("username", String(255), nullable=True, unique=None, default=None)
592 592 password = Column("password", String(255), nullable=True, unique=None, default=None)
593 593 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
594 594 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
595 595 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
596 596 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
597 597 _email = Column("email", String(255), nullable=True, unique=None, default=None)
598 598 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
599 599 last_activity = Column('last_activity', DateTime(timezone=False), nullable=True, unique=None, default=None)
600 600 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
601 601
602 602 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
603 603 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
604 604 _api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
605 605 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
606 606 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
607 607 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
608 608
609 609 user_log = relationship('UserLog', back_populates='user')
610 610 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all, delete-orphan')
611 611
612 612 repositories = relationship('Repository', back_populates='user')
613 613 repository_groups = relationship('RepoGroup', back_populates='user')
614 614 user_groups = relationship('UserGroup', back_populates='user')
615 615
616 616 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all', back_populates='follows_user')
617 617 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all', back_populates='user')
618 618
619 619 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all, delete-orphan')
620 620 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all, delete-orphan', back_populates='user')
621 621 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all, delete-orphan', back_populates='user')
622 622
623 623 group_member = relationship('UserGroupMember', cascade='all', back_populates='user')
624 624
625 625 notifications = relationship('UserNotification', cascade='all', back_populates='user')
626 626 # notifications assigned to this user
627 627 user_created_notifications = relationship('Notification', cascade='all', back_populates='created_by_user')
628 628 # comments created by this user
629 629 user_comments = relationship('ChangesetComment', cascade='all', back_populates='author')
630 630 # user profile extra info
631 631 user_emails = relationship('UserEmailMap', cascade='all', back_populates='user')
632 632 user_ip_map = relationship('UserIpMap', cascade='all', back_populates='user')
633 633 user_auth_tokens = relationship('UserApiKeys', cascade='all', back_populates='user')
634 634 user_ssh_keys = relationship('UserSshKeys', cascade='all', back_populates='user')
635 635
636 636 # gists
637 637 user_gists = relationship('Gist', cascade='all', back_populates='owner')
638 638 # user pull requests
639 639 user_pull_requests = relationship('PullRequest', cascade='all', back_populates='author')
640 640
641 641 # external identities
642 642 external_identities = relationship('ExternalIdentity', primaryjoin="User.user_id==ExternalIdentity.local_user_id", cascade='all')
643 643 # review rules
644 644 user_review_rules = relationship('RepoReviewRuleUser', cascade='all', back_populates='user')
645 645
646 646 # artifacts owned
647 647 artifacts = relationship('FileStore', primaryjoin='FileStore.user_id==User.user_id', back_populates='upload_user')
648 648
649 649 # no cascade, set NULL
650 650 scope_artifacts = relationship('FileStore', primaryjoin='FileStore.scope_user_id==User.user_id', cascade='', back_populates='user')
651 651
652 652 def __repr__(self):
653 653 return f"<{self.cls_name}('id={self.user_id}, username={self.username}')>"
654 654
655 655 @hybrid_property
656 656 def email(self):
657 657 return self._email
658 658
659 659 @email.setter
660 660 def email(self, val):
661 661 self._email = val.lower() if val else None
662 662
663 663 @hybrid_property
664 664 def first_name(self):
665 665 from rhodecode.lib import helpers as h
666 666 if self.name:
667 667 return h.escape(self.name)
668 668 return self.name
669 669
670 670 @hybrid_property
671 671 def last_name(self):
672 672 from rhodecode.lib import helpers as h
673 673 if self.lastname:
674 674 return h.escape(self.lastname)
675 675 return self.lastname
676 676
677 677 @hybrid_property
678 678 def api_key(self):
679 679 """
680 680 Fetch if exist an auth-token with role ALL connected to this user
681 681 """
682 682 user_auth_token = UserApiKeys.query()\
683 683 .filter(UserApiKeys.user_id == self.user_id)\
684 684 .filter(or_(UserApiKeys.expires == -1,
685 685 UserApiKeys.expires >= time.time()))\
686 686 .filter(UserApiKeys.role == UserApiKeys.ROLE_ALL).first()
687 687 if user_auth_token:
688 688 user_auth_token = user_auth_token.api_key
689 689
690 690 return user_auth_token
691 691
692 692 @api_key.setter
693 693 def api_key(self, val):
694 694 # don't allow to set API key this is deprecated for now
695 695 self._api_key = None
696 696
697 697 @property
698 698 def reviewer_pull_requests(self):
699 699 return PullRequestReviewers.query() \
700 700 .options(joinedload(PullRequestReviewers.pull_request)) \
701 701 .filter(PullRequestReviewers.user_id == self.user_id) \
702 702 .all()
703 703
704 704 @property
705 705 def firstname(self):
706 706 # alias for future
707 707 return self.name
708 708
709 709 @property
710 710 def emails(self):
711 711 other = UserEmailMap.query()\
712 712 .filter(UserEmailMap.user == self) \
713 713 .order_by(UserEmailMap.email_id.asc()) \
714 714 .all()
715 715 return [self.email] + [x.email for x in other]
716 716
717 717 def emails_cached(self):
718 718 emails = []
719 719 if self.user_id != self.get_default_user_id():
720 720 emails = UserEmailMap.query()\
721 721 .filter(UserEmailMap.user == self) \
722 722 .order_by(UserEmailMap.email_id.asc())
723 723
724 724 emails = emails.options(
725 725 FromCache("sql_cache_short", f"get_user_{self.user_id}_emails")
726 726 )
727 727
728 728 return [self.email] + [x.email for x in emails]
729 729
730 730 @property
731 731 def auth_tokens(self):
732 732 auth_tokens = self.get_auth_tokens()
733 733 return [x.api_key for x in auth_tokens]
734 734
735 735 def get_auth_tokens(self):
736 736 return UserApiKeys.query()\
737 737 .filter(UserApiKeys.user == self)\
738 738 .order_by(UserApiKeys.user_api_key_id.asc())\
739 739 .all()
740 740
741 741 @LazyProperty
742 742 def feed_token(self):
743 743 return self.get_feed_token()
744 744
745 745 def get_feed_token(self, cache=True):
746 746 feed_tokens = UserApiKeys.query()\
747 747 .filter(UserApiKeys.user == self)\
748 748 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)
749 749 if cache:
750 750 feed_tokens = feed_tokens.options(
751 751 FromCache("sql_cache_short", f"get_user_feed_token_{self.user_id}"))
752 752
753 753 feed_tokens = feed_tokens.all()
754 754 if feed_tokens:
755 755 return feed_tokens[0].api_key
756 756 return 'NO_FEED_TOKEN_AVAILABLE'
757 757
758 758 @LazyProperty
759 759 def artifact_token(self):
760 760 return self.get_artifact_token()
761 761
762 762 def get_artifact_token(self, cache=True):
763 763 artifacts_tokens = UserApiKeys.query()\
764 764 .filter(UserApiKeys.user == self) \
765 765 .filter(or_(UserApiKeys.expires == -1,
766 766 UserApiKeys.expires >= time.time())) \
767 767 .filter(UserApiKeys.role == UserApiKeys.ROLE_ARTIFACT_DOWNLOAD)
768 768
769 769 if cache:
770 770 artifacts_tokens = artifacts_tokens.options(
771 771 FromCache("sql_cache_short", f"get_user_artifact_token_{self.user_id}"))
772 772
773 773 artifacts_tokens = artifacts_tokens.all()
774 774 if artifacts_tokens:
775 775 return artifacts_tokens[0].api_key
776 776 return 'NO_ARTIFACT_TOKEN_AVAILABLE'
777 777
778 778 def get_or_create_artifact_token(self):
779 779 artifacts_tokens = UserApiKeys.query()\
780 780 .filter(UserApiKeys.user == self) \
781 781 .filter(or_(UserApiKeys.expires == -1,
782 782 UserApiKeys.expires >= time.time())) \
783 783 .filter(UserApiKeys.role == UserApiKeys.ROLE_ARTIFACT_DOWNLOAD)
784 784
785 785 artifacts_tokens = artifacts_tokens.all()
786 786 if artifacts_tokens:
787 787 return artifacts_tokens[0].api_key
788 788 else:
789 789 from rhodecode.model.auth_token import AuthTokenModel
790 790 artifact_token = AuthTokenModel().create(
791 791 self, 'auto-generated-artifact-token',
792 792 lifetime=-1, role=UserApiKeys.ROLE_ARTIFACT_DOWNLOAD)
793 793 Session.commit()
794 794 return artifact_token.api_key
795 795
796 796 @classmethod
797 797 def get(cls, user_id, cache=False):
798 798 if not user_id:
799 799 return
800 800
801 801 user = cls.query()
802 802 if cache:
803 803 user = user.options(
804 804 FromCache("sql_cache_short", f"get_users_{user_id}"))
805 805 return user.get(user_id)
806 806
807 807 @classmethod
808 808 def extra_valid_auth_tokens(cls, user, role=None):
809 809 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
810 810 .filter(or_(UserApiKeys.expires == -1,
811 811 UserApiKeys.expires >= time.time()))
812 812 if role:
813 813 tokens = tokens.filter(or_(UserApiKeys.role == role,
814 814 UserApiKeys.role == UserApiKeys.ROLE_ALL))
815 815 return tokens.all()
816 816
817 817 def authenticate_by_token(self, auth_token, roles=None, scope_repo_id=None):
818 818 from rhodecode.lib import auth
819 819
820 820 log.debug('Trying to authenticate user: %s via auth-token, '
821 821 'and roles: %s', self, roles)
822 822
823 823 if not auth_token:
824 824 return False
825 825
826 826 roles = (roles or []) + [UserApiKeys.ROLE_ALL]
827 827 tokens_q = UserApiKeys.query()\
828 828 .filter(UserApiKeys.user_id == self.user_id)\
829 829 .filter(or_(UserApiKeys.expires == -1,
830 830 UserApiKeys.expires >= time.time()))
831 831
832 832 tokens_q = tokens_q.filter(UserApiKeys.role.in_(roles))
833 833
834 834 crypto_backend = auth.crypto_backend()
835 835 enc_token_map = {}
836 836 plain_token_map = {}
837 837 for token in tokens_q:
838 838 if token.api_key.startswith(crypto_backend.ENC_PREF):
839 839 enc_token_map[token.api_key] = token
840 840 else:
841 841 plain_token_map[token.api_key] = token
842 842 log.debug(
843 843 'Found %s plain and %s encrypted tokens to check for authentication for this user',
844 844 len(plain_token_map), len(enc_token_map))
845 845
846 846 # plain token match comes first
847 847 match = plain_token_map.get(auth_token)
848 848
849 849 # check encrypted tokens now
850 850 if not match:
851 851 for token_hash, token in enc_token_map.items():
852 852 # NOTE(marcink): this is expensive to calculate, but most secure
853 853 if crypto_backend.hash_check(auth_token, token_hash):
854 854 match = token
855 855 break
856 856
857 857 if match:
858 858 log.debug('Found matching token %s', match)
859 859 if match.repo_id:
860 860 log.debug('Found scope, checking for scope match of token %s', match)
861 861 if match.repo_id == scope_repo_id:
862 862 return True
863 863 else:
864 864 log.debug(
865 865 'AUTH_TOKEN: scope mismatch, token has a set repo scope: %s, '
866 866 'and calling scope is:%s, skipping further checks',
867 867 match.repo, scope_repo_id)
868 868 return False
869 869 else:
870 870 return True
871 871
872 872 return False
873 873
874 874 @property
875 875 def ip_addresses(self):
876 876 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
877 877 return [x.ip_addr for x in ret]
878 878
879 879 @property
880 880 def username_and_name(self):
881 881 return f'{self.username} ({self.first_name} {self.last_name})'
882 882
883 883 @property
884 884 def username_or_name_or_email(self):
885 885 full_name = self.full_name if self.full_name != ' ' else None
886 886 return self.username or full_name or self.email
887 887
888 888 @property
889 889 def full_name(self):
890 890 return f'{self.first_name} {self.last_name}'
891 891
892 892 @property
893 893 def full_name_or_username(self):
894 894 return (f'{self.first_name} {self.last_name}'
895 895 if (self.first_name and self.last_name) else self.username)
896 896
897 897 @property
898 898 def full_contact(self):
899 899 return f'{self.first_name} {self.last_name} <{self.email}>'
900 900
901 901 @property
902 902 def short_contact(self):
903 903 return f'{self.first_name} {self.last_name}'
904 904
905 905 @property
906 906 def is_admin(self):
907 907 return self.admin
908 908
909 909 @property
910 910 def language(self):
911 911 return self.user_data.get('language')
912 912
913 913 def AuthUser(self, **kwargs):
914 914 """
915 915 Returns instance of AuthUser for this user
916 916 """
917 917 from rhodecode.lib.auth import AuthUser
918 918 return AuthUser(user_id=self.user_id, username=self.username, **kwargs)
919 919
920 920 @hybrid_property
921 921 def user_data(self):
922 922 if not self._user_data:
923 923 return {}
924 924
925 925 try:
926 926 return json.loads(self._user_data) or {}
927 927 except TypeError:
928 928 return {}
929 929
930 930 @user_data.setter
931 931 def user_data(self, val):
932 932 if not isinstance(val, dict):
933 933 raise Exception('user_data must be dict, got %s' % type(val))
934 934 try:
935 935 self._user_data = safe_bytes(json.dumps(val))
936 936 except Exception:
937 937 log.error(traceback.format_exc())
938 938
939 939 @classmethod
940 940 def get_by_username(cls, username, case_insensitive=False,
941 941 cache=False):
942 942
943 943 if case_insensitive:
944 944 q = cls.select().where(
945 945 func.lower(cls.username) == func.lower(username))
946 946 else:
947 947 q = cls.select().where(cls.username == username)
948 948
949 949 if cache:
950 950 hash_key = _hash_key(username)
951 951 q = q.options(
952 952 FromCache("sql_cache_short", f"get_user_by_name_{hash_key}"))
953 953
954 954 return cls.execute(q).scalar_one_or_none()
955 955
956 956 @classmethod
957 957 def get_by_auth_token(cls, auth_token, cache=False):
958 958
959 959 q = cls.select(User)\
960 960 .join(UserApiKeys)\
961 961 .where(UserApiKeys.api_key == auth_token)\
962 962 .where(or_(UserApiKeys.expires == -1,
963 963 UserApiKeys.expires >= time.time()))
964 964
965 965 if cache:
966 966 q = q.options(
967 967 FromCache("sql_cache_short", f"get_auth_token_{auth_token}"))
968 968
969 969 matched_user = cls.execute(q).scalar_one_or_none()
970 970
971 971 return matched_user
972 972
973 973 @classmethod
974 974 def get_by_email(cls, email, case_insensitive=False, cache=False):
975 975
976 976 if case_insensitive:
977 977 q = cls.select().where(func.lower(cls.email) == func.lower(email))
978 978 else:
979 979 q = cls.select().where(cls.email == email)
980 980
981 981 if cache:
982 982 email_key = _hash_key(email)
983 983 q = q.options(
984 984 FromCache("sql_cache_short", f"get_email_key_{email_key}"))
985 985
986 986 ret = cls.execute(q).scalar_one_or_none()
987 987
988 988 if ret is None:
989 989 q = cls.select(UserEmailMap)
990 990 # try fetching in alternate email map
991 991 if case_insensitive:
992 992 q = q.where(func.lower(UserEmailMap.email) == func.lower(email))
993 993 else:
994 994 q = q.where(UserEmailMap.email == email)
995 995 q = q.options(joinedload(UserEmailMap.user))
996 996 if cache:
997 997 q = q.options(
998 998 FromCache("sql_cache_short", f"get_email_map_key_{email_key}"))
999 999
1000 1000 result = cls.execute(q).scalar_one_or_none()
1001 1001 ret = getattr(result, 'user', None)
1002 1002
1003 1003 return ret
1004 1004
1005 1005 @classmethod
1006 1006 def get_from_cs_author(cls, author):
1007 1007 """
1008 1008 Tries to get User objects out of commit author string
1009 1009
1010 1010 :param author:
1011 1011 """
1012 1012 from rhodecode.lib.helpers import email, author_name
1013 1013 # Valid email in the attribute passed, see if they're in the system
1014 1014 _email = email(author)
1015 1015 if _email:
1016 1016 user = cls.get_by_email(_email, case_insensitive=True)
1017 1017 if user:
1018 1018 return user
1019 1019 # Maybe we can match by username?
1020 1020 _author = author_name(author)
1021 1021 user = cls.get_by_username(_author, case_insensitive=True)
1022 1022 if user:
1023 1023 return user
1024 1024
1025 1025 def update_userdata(self, **kwargs):
1026 1026 usr = self
1027 1027 old = usr.user_data
1028 1028 old.update(**kwargs)
1029 1029 usr.user_data = old
1030 1030 Session().add(usr)
1031 1031 log.debug('updated userdata with %s', kwargs)
1032 1032
1033 1033 def update_lastlogin(self):
1034 1034 """Update user lastlogin"""
1035 1035 self.last_login = datetime.datetime.now()
1036 1036 Session().add(self)
1037 1037 log.debug('updated user %s lastlogin', self.username)
1038 1038
1039 1039 def update_password(self, new_password):
1040 1040 from rhodecode.lib.auth import get_crypt_password
1041 1041
1042 1042 self.password = get_crypt_password(new_password)
1043 1043 Session().add(self)
1044 1044
1045 1045 @classmethod
1046 1046 def get_first_super_admin(cls):
1047 1047 stmt = cls.select().where(User.admin == true()).order_by(User.user_id.asc())
1048 1048 user = cls.scalars(stmt).first()
1049 1049
1050 1050 if user is None:
1051 1051 raise Exception('FATAL: Missing administrative account!')
1052 1052 return user
1053 1053
1054 1054 @classmethod
1055 1055 def get_all_super_admins(cls, only_active=False):
1056 1056 """
1057 1057 Returns all admin accounts sorted by username
1058 1058 """
1059 1059 qry = User.query().filter(User.admin == true()).order_by(User.username.asc())
1060 1060 if only_active:
1061 1061 qry = qry.filter(User.active == true())
1062 1062 return qry.all()
1063 1063
1064 1064 @classmethod
1065 1065 def get_all_user_ids(cls, only_active=True):
1066 1066 """
1067 1067 Returns all users IDs
1068 1068 """
1069 1069 qry = Session().query(User.user_id)
1070 1070
1071 1071 if only_active:
1072 1072 qry = qry.filter(User.active == true())
1073 1073 return [x.user_id for x in qry]
1074 1074
1075 1075 @classmethod
1076 1076 def get_default_user(cls, cache=False, refresh=False):
1077 1077 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
1078 1078 if user is None:
1079 1079 raise Exception('FATAL: Missing default account!')
1080 1080 if refresh:
1081 1081 # The default user might be based on outdated state which
1082 1082 # has been loaded from the cache.
1083 1083 # A call to refresh() ensures that the
1084 1084 # latest state from the database is used.
1085 1085 Session().refresh(user)
1086 1086
1087 1087 return user
1088 1088
1089 1089 @classmethod
1090 1090 def get_default_user_id(cls):
1091 1091 import rhodecode
1092 1092 return rhodecode.CONFIG['default_user_id']
1093 1093
1094 1094 def _get_default_perms(self, user, suffix=''):
1095 1095 from rhodecode.model.permission import PermissionModel
1096 1096 return PermissionModel().get_default_perms(user.user_perms, suffix)
1097 1097
1098 1098 def get_default_perms(self, suffix=''):
1099 1099 return self._get_default_perms(self, suffix)
1100 1100
1101 1101 def get_api_data(self, include_secrets=False, details='full'):
1102 1102 """
1103 1103 Common function for generating user related data for API
1104 1104
1105 1105 :param include_secrets: By default secrets in the API data will be replaced
1106 1106 by a placeholder value to prevent exposing this data by accident. In case
1107 1107 this data shall be exposed, set this flag to ``True``.
1108 1108
1109 1109 :param details: details can be 'basic|full' basic gives only a subset of
1110 1110 the available user information that includes user_id, name and emails.
1111 1111 """
1112 1112 user = self
1113 1113 user_data = self.user_data
1114 1114 data = {
1115 1115 'user_id': user.user_id,
1116 1116 'username': user.username,
1117 1117 'firstname': user.name,
1118 1118 'lastname': user.lastname,
1119 1119 'description': user.description,
1120 1120 'email': user.email,
1121 1121 'emails': user.emails,
1122 1122 }
1123 1123 if details == 'basic':
1124 1124 return data
1125 1125
1126 1126 auth_token_length = 40
1127 1127 auth_token_replacement = '*' * auth_token_length
1128 1128
1129 1129 extras = {
1130 1130 'auth_tokens': [auth_token_replacement],
1131 1131 'active': user.active,
1132 1132 'admin': user.admin,
1133 1133 'extern_type': user.extern_type,
1134 1134 'extern_name': user.extern_name,
1135 1135 'last_login': user.last_login,
1136 1136 'last_activity': user.last_activity,
1137 1137 'ip_addresses': user.ip_addresses,
1138 1138 'language': user_data.get('language')
1139 1139 }
1140 1140 data.update(extras)
1141 1141
1142 1142 if include_secrets:
1143 1143 data['auth_tokens'] = user.auth_tokens
1144 1144 return data
1145 1145
1146 1146 def __json__(self):
1147 1147 data = {
1148 1148 'full_name': self.full_name,
1149 1149 'full_name_or_username': self.full_name_or_username,
1150 1150 'short_contact': self.short_contact,
1151 1151 'full_contact': self.full_contact,
1152 1152 }
1153 1153 data.update(self.get_api_data())
1154 1154 return data
1155 1155
1156 1156
1157 1157 class UserApiKeys(Base, BaseModel):
1158 1158 __tablename__ = 'user_api_keys'
1159 1159 __table_args__ = (
1160 1160 Index('uak_api_key_idx', 'api_key'),
1161 1161 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
1162 1162 base_table_args
1163 1163 )
1164 1164
1165 1165 # ApiKey role
1166 1166 ROLE_ALL = 'token_role_all'
1167 1167 ROLE_VCS = 'token_role_vcs'
1168 1168 ROLE_API = 'token_role_api'
1169 1169 ROLE_HTTP = 'token_role_http'
1170 1170 ROLE_FEED = 'token_role_feed'
1171 1171 ROLE_ARTIFACT_DOWNLOAD = 'role_artifact_download'
1172 1172 # The last one is ignored in the list as we only
1173 1173 # use it for one action, and cannot be created by users
1174 1174 ROLE_PASSWORD_RESET = 'token_password_reset'
1175 1175
1176 1176 ROLES = [ROLE_ALL, ROLE_VCS, ROLE_API, ROLE_HTTP, ROLE_FEED, ROLE_ARTIFACT_DOWNLOAD]
1177 1177
1178 1178 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1179 1179 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1180 1180 api_key = Column("api_key", String(255), nullable=False, unique=True)
1181 1181 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1182 1182 expires = Column('expires', Float(53), nullable=False)
1183 1183 role = Column('role', String(255), nullable=True)
1184 1184 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1185 1185
1186 1186 # scope columns
1187 1187 repo_id = Column(
1188 1188 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
1189 1189 nullable=True, unique=None, default=None)
1190 1190 repo = relationship('Repository', lazy='joined', back_populates='scoped_tokens')
1191 1191
1192 1192 repo_group_id = Column(
1193 1193 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
1194 1194 nullable=True, unique=None, default=None)
1195 1195 repo_group = relationship('RepoGroup', lazy='joined')
1196 1196
1197 1197 user = relationship('User', lazy='joined', back_populates='user_auth_tokens')
1198 1198
1199 1199 def __repr__(self):
1200 1200 return f"<{self.cls_name}('{self.role}')>"
1201 1201
1202 1202 def __json__(self):
1203 1203 data = {
1204 1204 'auth_token': self.api_key,
1205 1205 'role': self.role,
1206 1206 'scope': self.scope_humanized,
1207 1207 'expired': self.expired
1208 1208 }
1209 1209 return data
1210 1210
1211 1211 def get_api_data(self, include_secrets=False):
1212 1212 data = self.__json__()
1213 1213 if include_secrets:
1214 1214 return data
1215 1215 else:
1216 1216 data['auth_token'] = self.token_obfuscated
1217 1217 return data
1218 1218
1219 1219 @hybrid_property
1220 1220 def description_safe(self):
1221 1221 from rhodecode.lib import helpers as h
1222 1222 return h.escape(self.description)
1223 1223
1224 1224 @property
1225 1225 def expired(self):
1226 1226 if self.expires == -1:
1227 1227 return False
1228 1228 return time.time() > self.expires
1229 1229
1230 1230 @classmethod
1231 1231 def _get_role_name(cls, role):
1232 1232 return {
1233 1233 cls.ROLE_ALL: _('all'),
1234 1234 cls.ROLE_HTTP: _('http/web interface'),
1235 1235 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
1236 1236 cls.ROLE_API: _('api calls'),
1237 1237 cls.ROLE_FEED: _('feed access'),
1238 1238 cls.ROLE_ARTIFACT_DOWNLOAD: _('artifacts downloads'),
1239 1239 }.get(role, role)
1240 1240
1241 1241 @classmethod
1242 1242 def _get_role_description(cls, role):
1243 1243 return {
1244 1244 cls.ROLE_ALL: _('Token for all actions.'),
1245 1245 cls.ROLE_HTTP: _('Token to access RhodeCode pages via web interface without '
1246 1246 'login using `api_access_controllers_whitelist` functionality.'),
1247 1247 cls.ROLE_VCS: _('Token to interact over git/hg/svn protocols. '
1248 1248 'Requires auth_token authentication plugin to be active. <br/>'
1249 1249 'Such Token should be used then instead of a password to '
1250 1250 'interact with a repository, and additionally can be '
1251 1251 'limited to single repository using repo scope.'),
1252 1252 cls.ROLE_API: _('Token limited to api calls.'),
1253 1253 cls.ROLE_FEED: _('Token to read RSS/ATOM feed.'),
1254 1254 cls.ROLE_ARTIFACT_DOWNLOAD: _('Token for artifacts downloads.'),
1255 1255 }.get(role, role)
1256 1256
1257 1257 @property
1258 1258 def role_humanized(self):
1259 1259 return self._get_role_name(self.role)
1260 1260
1261 1261 def _get_scope(self):
1262 1262 if self.repo:
1263 1263 return 'Repository: {}'.format(self.repo.repo_name)
1264 1264 if self.repo_group:
1265 1265 return 'RepositoryGroup: {} (recursive)'.format(self.repo_group.group_name)
1266 1266 return 'Global'
1267 1267
1268 1268 @property
1269 1269 def scope_humanized(self):
1270 1270 return self._get_scope()
1271 1271
1272 1272 @property
1273 1273 def token_obfuscated(self):
1274 1274 if self.api_key:
1275 1275 return self.api_key[:4] + "****"
1276 1276
1277 1277
1278 1278 class UserEmailMap(Base, BaseModel):
1279 1279 __tablename__ = 'user_email_map'
1280 1280 __table_args__ = (
1281 1281 Index('uem_email_idx', 'email'),
1282 1282 Index('uem_user_id_idx', 'user_id'),
1283 1283 UniqueConstraint('email'),
1284 1284 base_table_args
1285 1285 )
1286 1286
1287 1287 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1288 1288 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1289 1289 _email = Column("email", String(255), nullable=True, unique=False, default=None)
1290 1290 user = relationship('User', lazy='joined', back_populates='user_emails')
1291 1291
1292 1292 @validates('_email')
1293 1293 def validate_email(self, key, email):
1294 1294 # check if this email is not main one
1295 1295 main_email = Session().query(User).filter(User.email == email).scalar()
1296 1296 if main_email is not None:
1297 1297 raise AttributeError('email %s is present is user table' % email)
1298 1298 return email
1299 1299
1300 1300 @hybrid_property
1301 1301 def email(self):
1302 1302 return self._email
1303 1303
1304 1304 @email.setter
1305 1305 def email(self, val):
1306 1306 self._email = val.lower() if val else None
1307 1307
1308 1308
1309 1309 class UserIpMap(Base, BaseModel):
1310 1310 __tablename__ = 'user_ip_map'
1311 1311 __table_args__ = (
1312 1312 UniqueConstraint('user_id', 'ip_addr'),
1313 1313 base_table_args
1314 1314 )
1315 1315
1316 1316 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1317 1317 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1318 1318 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
1319 1319 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
1320 1320 description = Column("description", String(10000), nullable=True, unique=None, default=None)
1321 1321 user = relationship('User', lazy='joined', back_populates='user_ip_map')
1322 1322
1323 1323 @hybrid_property
1324 1324 def description_safe(self):
1325 1325 from rhodecode.lib import helpers as h
1326 1326 return h.escape(self.description)
1327 1327
1328 1328 @classmethod
1329 1329 def _get_ip_range(cls, ip_addr):
1330 1330 net = ipaddress.ip_network(safe_str(ip_addr), strict=False)
1331 1331 return [str(net.network_address), str(net.broadcast_address)]
1332 1332
1333 1333 def __json__(self):
1334 1334 return {
1335 1335 'ip_addr': self.ip_addr,
1336 1336 'ip_range': self._get_ip_range(self.ip_addr),
1337 1337 }
1338 1338
1339 1339 def __repr__(self):
1340 1340 return f"<{self.cls_name}('user_id={self.user_id} => ip={self.ip_addr}')>"
1341 1341
1342 1342
1343 1343 class UserSshKeys(Base, BaseModel):
1344 1344 __tablename__ = 'user_ssh_keys'
1345 1345 __table_args__ = (
1346 1346 Index('usk_ssh_key_fingerprint_idx', 'ssh_key_fingerprint'),
1347 1347
1348 1348 UniqueConstraint('ssh_key_fingerprint'),
1349 1349
1350 1350 base_table_args
1351 1351 )
1352 1352
1353 1353 ssh_key_id = Column('ssh_key_id', Integer(), nullable=False, unique=True, default=None, primary_key=True)
1354 1354 ssh_key_data = Column('ssh_key_data', String(10240), nullable=False, unique=None, default=None)
1355 1355 ssh_key_fingerprint = Column('ssh_key_fingerprint', String(255), nullable=False, unique=None, default=None)
1356 1356
1357 1357 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1358 1358
1359 1359 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1360 1360 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True, default=None)
1361 1361 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1362 1362
1363 1363 user = relationship('User', lazy='joined', back_populates='user_ssh_keys')
1364 1364
1365 1365 def __json__(self):
1366 1366 data = {
1367 1367 'ssh_fingerprint': self.ssh_key_fingerprint,
1368 1368 'description': self.description,
1369 1369 'created_on': self.created_on
1370 1370 }
1371 1371 return data
1372 1372
1373 1373 def get_api_data(self):
1374 1374 data = self.__json__()
1375 1375 return data
1376 1376
1377 1377
1378 1378 class UserLog(Base, BaseModel):
1379 1379 __tablename__ = 'user_logs'
1380 1380 __table_args__ = (
1381 1381 base_table_args,
1382 1382 )
1383 1383
1384 1384 VERSION_1 = 'v1'
1385 1385 VERSION_2 = 'v2'
1386 1386 VERSIONS = [VERSION_1, VERSION_2]
1387 1387
1388 1388 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1389 1389 user_id = Column("user_id", Integer(), ForeignKey('users.user_id',ondelete='SET NULL'), nullable=True, unique=None, default=None)
1390 1390 username = Column("username", String(255), nullable=True, unique=None, default=None)
1391 1391 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id', ondelete='SET NULL'), nullable=True, unique=None, default=None)
1392 1392 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1393 1393 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1394 1394 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1395 1395 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1396 1396
1397 1397 version = Column("version", String(255), nullable=True, default=VERSION_1)
1398 1398 user_data = Column('user_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1399 1399 action_data = Column('action_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1400 1400 user = relationship('User', cascade='', back_populates='user_log')
1401 1401 repository = relationship('Repository', cascade='', back_populates='logs')
1402 1402
1403 1403 def __repr__(self):
1404 1404 return f"<{self.cls_name}('id:{self.repository_name}:{self.action}')>"
1405 1405
1406 1406 def __json__(self):
1407 1407 return {
1408 1408 'user_id': self.user_id,
1409 1409 'username': self.username,
1410 1410 'repository_id': self.repository_id,
1411 1411 'repository_name': self.repository_name,
1412 1412 'user_ip': self.user_ip,
1413 1413 'action_date': self.action_date,
1414 1414 'action': self.action,
1415 1415 }
1416 1416
1417 1417 @hybrid_property
1418 1418 def entry_id(self):
1419 1419 return self.user_log_id
1420 1420
1421 1421 @property
1422 1422 def action_as_day(self):
1423 1423 return datetime.date(*self.action_date.timetuple()[:3])
1424 1424
1425 1425
1426 1426 class UserGroup(Base, BaseModel):
1427 1427 __tablename__ = 'users_groups'
1428 1428 __table_args__ = (
1429 1429 base_table_args,
1430 1430 )
1431 1431
1432 1432 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1433 1433 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1434 1434 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1435 1435 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1436 1436 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1437 1437 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1438 1438 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1439 1439 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1440 1440
1441 1441 members = relationship('UserGroupMember', cascade="all, delete-orphan", lazy="joined", back_populates='users_group')
1442 1442 users_group_to_perm = relationship('UserGroupToPerm', cascade='all', back_populates='users_group')
1443 1443 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all', back_populates='users_group')
1444 1444 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all', back_populates='users_group')
1445 1445 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all', back_populates='user_group')
1446 1446
1447 1447 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all', back_populates='target_user_group')
1448 1448
1449 1449 user_group_review_rules = relationship('RepoReviewRuleUserGroup', cascade='all', back_populates='users_group')
1450 1450 user = relationship('User', primaryjoin="User.user_id==UserGroup.user_id", back_populates='user_groups')
1451 1451
1452 1452 @classmethod
1453 1453 def _load_group_data(cls, column):
1454 1454 if not column:
1455 1455 return {}
1456 1456
1457 1457 try:
1458 1458 return json.loads(column) or {}
1459 1459 except TypeError:
1460 1460 return {}
1461 1461
1462 1462 @hybrid_property
1463 1463 def description_safe(self):
1464 1464 from rhodecode.lib import helpers as h
1465 1465 return h.escape(self.user_group_description)
1466 1466
1467 1467 @hybrid_property
1468 1468 def group_data(self):
1469 1469 return self._load_group_data(self._group_data)
1470 1470
1471 1471 @group_data.expression
1472 1472 def group_data(self, **kwargs):
1473 1473 return self._group_data
1474 1474
1475 1475 @group_data.setter
1476 1476 def group_data(self, val):
1477 1477 try:
1478 1478 self._group_data = json.dumps(val)
1479 1479 except Exception:
1480 1480 log.error(traceback.format_exc())
1481 1481
1482 1482 @classmethod
1483 1483 def _load_sync(cls, group_data):
1484 1484 if group_data:
1485 1485 return group_data.get('extern_type')
1486 1486
1487 1487 @property
1488 1488 def sync(self):
1489 1489 return self._load_sync(self.group_data)
1490 1490
1491 1491 def __repr__(self):
1492 1492 return f"<{self.cls_name}('id:{self.users_group_id}:{self.users_group_name}')>"
1493 1493
1494 1494 @classmethod
1495 1495 def get_by_group_name(cls, group_name, cache=False,
1496 1496 case_insensitive=False):
1497 1497 if case_insensitive:
1498 1498 q = cls.query().filter(func.lower(cls.users_group_name) ==
1499 1499 func.lower(group_name))
1500 1500
1501 1501 else:
1502 1502 q = cls.query().filter(cls.users_group_name == group_name)
1503 1503 if cache:
1504 1504 name_key = _hash_key(group_name)
1505 1505 q = q.options(
1506 1506 FromCache("sql_cache_short", f"get_group_{name_key}"))
1507 1507 return q.scalar()
1508 1508
1509 1509 @classmethod
1510 1510 def get(cls, user_group_id, cache=False):
1511 1511 if not user_group_id:
1512 1512 return
1513 1513
1514 1514 user_group = cls.query()
1515 1515 if cache:
1516 1516 user_group = user_group.options(
1517 1517 FromCache("sql_cache_short", "get_users_group_%s" % user_group_id))
1518 1518 return user_group.get(user_group_id)
1519 1519
1520 1520 def permissions(self, with_admins=True, with_owner=True,
1521 1521 expand_from_user_groups=False):
1522 1522 """
1523 1523 Permissions for user groups
1524 1524 """
1525 1525 _admin_perm = 'usergroup.admin'
1526 1526
1527 1527 owner_row = []
1528 1528 if with_owner:
1529 1529 usr = AttributeDict(self.user.get_dict())
1530 1530 usr.owner_row = True
1531 1531 usr.permission = _admin_perm
1532 1532 owner_row.append(usr)
1533 1533
1534 1534 super_admin_ids = []
1535 1535 super_admin_rows = []
1536 1536 if with_admins:
1537 1537 for usr in User.get_all_super_admins():
1538 1538 super_admin_ids.append(usr.user_id)
1539 1539 # if this admin is also owner, don't double the record
1540 1540 if usr.user_id == owner_row[0].user_id:
1541 1541 owner_row[0].admin_row = True
1542 1542 else:
1543 1543 usr = AttributeDict(usr.get_dict())
1544 1544 usr.admin_row = True
1545 1545 usr.permission = _admin_perm
1546 1546 super_admin_rows.append(usr)
1547 1547
1548 1548 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1549 1549 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1550 1550 joinedload(UserUserGroupToPerm.user),
1551 1551 joinedload(UserUserGroupToPerm.permission),)
1552 1552
1553 1553 # get owners and admins and permissions. We do a trick of re-writing
1554 1554 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1555 1555 # has a global reference and changing one object propagates to all
1556 1556 # others. This means if admin is also an owner admin_row that change
1557 1557 # would propagate to both objects
1558 1558 perm_rows = []
1559 1559 for _usr in q.all():
1560 1560 usr = AttributeDict(_usr.user.get_dict())
1561 1561 # if this user is also owner/admin, mark as duplicate record
1562 1562 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
1563 1563 usr.duplicate_perm = True
1564 1564 usr.permission = _usr.permission.permission_name
1565 1565 perm_rows.append(usr)
1566 1566
1567 1567 # filter the perm rows by 'default' first and then sort them by
1568 1568 # admin,write,read,none permissions sorted again alphabetically in
1569 1569 # each group
1570 1570 perm_rows = sorted(perm_rows, key=display_user_sort)
1571 1571
1572 1572 user_groups_rows = []
1573 1573 if expand_from_user_groups:
1574 1574 for ug in self.permission_user_groups(with_members=True):
1575 1575 for user_data in ug.members:
1576 1576 user_groups_rows.append(user_data)
1577 1577
1578 1578 return super_admin_rows + owner_row + perm_rows + user_groups_rows
1579 1579
1580 1580 def permission_user_groups(self, with_members=False):
1581 1581 q = UserGroupUserGroupToPerm.query()\
1582 1582 .filter(UserGroupUserGroupToPerm.target_user_group == self)
1583 1583 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1584 1584 joinedload(UserGroupUserGroupToPerm.target_user_group),
1585 1585 joinedload(UserGroupUserGroupToPerm.permission),)
1586 1586
1587 1587 perm_rows = []
1588 1588 for _user_group in q.all():
1589 1589 entry = AttributeDict(_user_group.user_group.get_dict())
1590 1590 entry.permission = _user_group.permission.permission_name
1591 1591 if with_members:
1592 1592 entry.members = [x.user.get_dict()
1593 1593 for x in _user_group.user_group.members]
1594 1594 perm_rows.append(entry)
1595 1595
1596 1596 perm_rows = sorted(perm_rows, key=display_user_group_sort)
1597 1597 return perm_rows
1598 1598
1599 1599 def _get_default_perms(self, user_group, suffix=''):
1600 1600 from rhodecode.model.permission import PermissionModel
1601 1601 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1602 1602
1603 1603 def get_default_perms(self, suffix=''):
1604 1604 return self._get_default_perms(self, suffix)
1605 1605
1606 1606 def get_api_data(self, with_group_members=True, include_secrets=False):
1607 1607 """
1608 1608 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1609 1609 basically forwarded.
1610 1610
1611 1611 """
1612 1612 user_group = self
1613 1613 data = {
1614 1614 'users_group_id': user_group.users_group_id,
1615 1615 'group_name': user_group.users_group_name,
1616 1616 'group_description': user_group.user_group_description,
1617 1617 'active': user_group.users_group_active,
1618 1618 'owner': user_group.user.username,
1619 1619 'sync': user_group.sync,
1620 1620 'owner_email': user_group.user.email,
1621 1621 }
1622 1622
1623 1623 if with_group_members:
1624 1624 users = []
1625 1625 for user in user_group.members:
1626 1626 user = user.user
1627 1627 users.append(user.get_api_data(include_secrets=include_secrets))
1628 1628 data['users'] = users
1629 1629
1630 1630 return data
1631 1631
1632 1632
1633 1633 class UserGroupMember(Base, BaseModel):
1634 1634 __tablename__ = 'users_groups_members'
1635 1635 __table_args__ = (
1636 1636 base_table_args,
1637 1637 )
1638 1638
1639 1639 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1640 1640 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1641 1641 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1642 1642
1643 1643 user = relationship('User', lazy='joined', back_populates='group_member')
1644 1644 users_group = relationship('UserGroup', back_populates='members')
1645 1645
1646 1646 def __init__(self, gr_id='', u_id=''):
1647 1647 self.users_group_id = gr_id
1648 1648 self.user_id = u_id
1649 1649
1650 1650
1651 1651 class RepositoryField(Base, BaseModel):
1652 1652 __tablename__ = 'repositories_fields'
1653 1653 __table_args__ = (
1654 1654 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1655 1655 base_table_args,
1656 1656 )
1657 1657
1658 1658 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1659 1659
1660 1660 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1661 1661 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1662 1662 field_key = Column("field_key", String(250))
1663 1663 field_label = Column("field_label", String(1024), nullable=False)
1664 1664 field_value = Column("field_value", String(10000), nullable=False)
1665 1665 field_desc = Column("field_desc", String(1024), nullable=False)
1666 1666 field_type = Column("field_type", String(255), nullable=False, unique=None)
1667 1667 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1668 1668
1669 1669 repository = relationship('Repository', back_populates='extra_fields')
1670 1670
1671 1671 @property
1672 1672 def field_key_prefixed(self):
1673 1673 return 'ex_%s' % self.field_key
1674 1674
1675 1675 @classmethod
1676 1676 def un_prefix_key(cls, key):
1677 1677 if key.startswith(cls.PREFIX):
1678 1678 return key[len(cls.PREFIX):]
1679 1679 return key
1680 1680
1681 1681 @classmethod
1682 1682 def get_by_key_name(cls, key, repo):
1683 1683 row = cls.query()\
1684 1684 .filter(cls.repository == repo)\
1685 1685 .filter(cls.field_key == key).scalar()
1686 1686 return row
1687 1687
1688 1688
1689 1689 class Repository(Base, BaseModel):
1690 1690 __tablename__ = 'repositories'
1691 1691 __table_args__ = (
1692 1692 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1693 1693 base_table_args,
1694 1694 )
1695 1695 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1696 1696 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1697 1697 DEFAULT_CLONE_URI_SSH = 'ssh://{sys_user}@{hostname}/{repo}'
1698 1698
1699 1699 STATE_CREATED = 'repo_state_created'
1700 1700 STATE_PENDING = 'repo_state_pending'
1701 1701 STATE_ERROR = 'repo_state_error'
1702 1702
1703 1703 LOCK_AUTOMATIC = 'lock_auto'
1704 1704 LOCK_API = 'lock_api'
1705 1705 LOCK_WEB = 'lock_web'
1706 1706 LOCK_PULL = 'lock_pull'
1707 1707
1708 1708 NAME_SEP = URL_SEP
1709 1709
1710 1710 repo_id = Column(
1711 1711 "repo_id", Integer(), nullable=False, unique=True, default=None,
1712 1712 primary_key=True)
1713 1713 _repo_name = Column(
1714 1714 "repo_name", Text(), nullable=False, default=None)
1715 1715 repo_name_hash = Column(
1716 1716 "repo_name_hash", String(255), nullable=False, unique=True)
1717 1717 repo_state = Column("repo_state", String(255), nullable=True)
1718 1718
1719 1719 clone_uri = Column(
1720 1720 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1721 1721 default=None)
1722 1722 push_uri = Column(
1723 1723 "push_uri", EncryptedTextValue(), nullable=True, unique=False,
1724 1724 default=None)
1725 1725 repo_type = Column(
1726 1726 "repo_type", String(255), nullable=False, unique=False, default=None)
1727 1727 user_id = Column(
1728 1728 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1729 1729 unique=False, default=None)
1730 1730 private = Column(
1731 1731 "private", Boolean(), nullable=True, unique=None, default=None)
1732 1732 archived = Column(
1733 1733 "archived", Boolean(), nullable=True, unique=None, default=None)
1734 1734 enable_statistics = Column(
1735 1735 "statistics", Boolean(), nullable=True, unique=None, default=True)
1736 1736 enable_downloads = Column(
1737 1737 "downloads", Boolean(), nullable=True, unique=None, default=True)
1738 1738 description = Column(
1739 1739 "description", String(10000), nullable=True, unique=None, default=None)
1740 1740 created_on = Column(
1741 1741 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1742 1742 default=datetime.datetime.now)
1743 1743 updated_on = Column(
1744 1744 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1745 1745 default=datetime.datetime.now)
1746 1746 _landing_revision = Column(
1747 1747 "landing_revision", String(255), nullable=False, unique=False,
1748 1748 default=None)
1749 1749 enable_locking = Column(
1750 1750 "enable_locking", Boolean(), nullable=False, unique=None,
1751 1751 default=False)
1752 1752 _locked = Column(
1753 1753 "locked", String(255), nullable=True, unique=False, default=None)
1754 1754 _changeset_cache = Column(
1755 1755 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1756 1756
1757 1757 fork_id = Column(
1758 1758 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1759 1759 nullable=True, unique=False, default=None)
1760 1760 group_id = Column(
1761 1761 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1762 1762 unique=False, default=None)
1763 1763
1764 1764 user = relationship('User', lazy='joined', back_populates='repositories')
1765 1765 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1766 1766 group = relationship('RepoGroup', lazy='joined')
1767 1767 repo_to_perm = relationship('UserRepoToPerm', cascade='all', order_by='UserRepoToPerm.repo_to_perm_id')
1768 1768 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all', back_populates='repository')
1769 1769 stats = relationship('Statistics', cascade='all', uselist=False)
1770 1770
1771 1771 followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id', cascade='all', back_populates='follows_repository')
1772 1772 extra_fields = relationship('RepositoryField', cascade="all, delete-orphan", back_populates='repository')
1773 1773
1774 1774 logs = relationship('UserLog', back_populates='repository')
1775 1775
1776 1776 comments = relationship('ChangesetComment', cascade="all, delete-orphan", back_populates='repo')
1777 1777
1778 1778 pull_requests_source = relationship(
1779 1779 'PullRequest',
1780 1780 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1781 1781 cascade="all, delete-orphan",
1782 1782 #back_populates="pr_source"
1783 1783 )
1784 1784 pull_requests_target = relationship(
1785 1785 'PullRequest',
1786 1786 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1787 1787 cascade="all, delete-orphan",
1788 1788 #back_populates="pr_target"
1789 1789 )
1790 1790
1791 1791 ui = relationship('RepoRhodeCodeUi', cascade="all")
1792 1792 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1793 1793 integrations = relationship('Integration', cascade="all, delete-orphan", back_populates='repo')
1794 1794
1795 1795 scoped_tokens = relationship('UserApiKeys', cascade="all", back_populates='repo')
1796 1796
1797 1797 # no cascade, set NULL
1798 1798 artifacts = relationship('FileStore', primaryjoin='FileStore.scope_repo_id==Repository.repo_id', viewonly=True)
1799 1799
1800 1800 review_rules = relationship('RepoReviewRule')
1801 1801 user_branch_perms = relationship('UserToRepoBranchPermission')
1802 1802 user_group_branch_perms = relationship('UserGroupToRepoBranchPermission')
1803 1803
1804 1804 def __repr__(self):
1805 1805 return "<%s('%s:%s')>" % (self.cls_name, self.repo_id, self.repo_name)
1806 1806
1807 1807 @hybrid_property
1808 1808 def description_safe(self):
1809 1809 from rhodecode.lib import helpers as h
1810 1810 return h.escape(self.description)
1811 1811
1812 1812 @hybrid_property
1813 1813 def landing_rev(self):
1814 1814 # always should return [rev_type, rev], e.g ['branch', 'master']
1815 1815 if self._landing_revision:
1816 1816 _rev_info = self._landing_revision.split(':')
1817 1817 if len(_rev_info) < 2:
1818 1818 _rev_info.insert(0, 'rev')
1819 1819 return [_rev_info[0], _rev_info[1]]
1820 1820 return [None, None]
1821 1821
1822 1822 @property
1823 1823 def landing_ref_type(self):
1824 1824 return self.landing_rev[0]
1825 1825
1826 1826 @property
1827 1827 def landing_ref_name(self):
1828 1828 return self.landing_rev[1]
1829 1829
1830 1830 @landing_rev.setter
1831 1831 def landing_rev(self, val):
1832 1832 if ':' not in val:
1833 1833 raise ValueError('value must be delimited with `:` and consist '
1834 1834 'of <rev_type>:<rev>, got %s instead' % val)
1835 1835 self._landing_revision = val
1836 1836
1837 1837 @hybrid_property
1838 1838 def locked(self):
1839 1839 if self._locked:
1840 1840 user_id, timelocked, reason = self._locked.split(':')
1841 1841 lock_values = int(user_id), timelocked, reason
1842 1842 else:
1843 1843 lock_values = [None, None, None]
1844 1844 return lock_values
1845 1845
1846 1846 @locked.setter
1847 1847 def locked(self, val):
1848 1848 if val and isinstance(val, (list, tuple)):
1849 1849 self._locked = ':'.join(map(str, val))
1850 1850 else:
1851 1851 self._locked = None
1852 1852
1853 1853 @classmethod
1854 1854 def _load_changeset_cache(cls, repo_id, changeset_cache_raw):
1855 1855 from rhodecode.lib.vcs.backends.base import EmptyCommit
1856 1856 dummy = EmptyCommit().__json__()
1857 1857 if not changeset_cache_raw:
1858 1858 dummy['source_repo_id'] = repo_id
1859 1859 return json.loads(json.dumps(dummy))
1860 1860
1861 1861 try:
1862 1862 return json.loads(changeset_cache_raw)
1863 1863 except TypeError:
1864 1864 return dummy
1865 1865 except Exception:
1866 1866 log.error(traceback.format_exc())
1867 1867 return dummy
1868 1868
1869 1869 @hybrid_property
1870 1870 def changeset_cache(self):
1871 1871 return self._load_changeset_cache(self.repo_id, self._changeset_cache)
1872 1872
1873 1873 @changeset_cache.setter
1874 1874 def changeset_cache(self, val):
1875 1875 try:
1876 1876 self._changeset_cache = json.dumps(val)
1877 1877 except Exception:
1878 1878 log.error(traceback.format_exc())
1879 1879
1880 1880 @hybrid_property
1881 1881 def repo_name(self):
1882 1882 return self._repo_name
1883 1883
1884 1884 @repo_name.setter
1885 1885 def repo_name(self, value):
1886 1886 self._repo_name = value
1887 1887 self.repo_name_hash = sha1(safe_bytes(value))
1888 1888
1889 1889 @classmethod
1890 1890 def normalize_repo_name(cls, repo_name):
1891 1891 """
1892 1892 Normalizes os specific repo_name to the format internally stored inside
1893 1893 database using URL_SEP
1894 1894
1895 1895 :param cls:
1896 1896 :param repo_name:
1897 1897 """
1898 1898 return cls.NAME_SEP.join(repo_name.split(os.sep))
1899 1899
1900 1900 @classmethod
1901 1901 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1902 1902 session = Session()
1903 1903 q = session.query(cls).filter(cls.repo_name == repo_name)
1904 1904
1905 1905 if cache:
1906 1906 if identity_cache:
1907 1907 val = cls.identity_cache(session, 'repo_name', repo_name)
1908 1908 if val:
1909 1909 return val
1910 1910 else:
1911 1911 cache_key = "get_repo_by_name_%s" % _hash_key(repo_name)
1912 1912 q = q.options(
1913 1913 FromCache("sql_cache_short", cache_key))
1914 1914
1915 1915 return q.scalar()
1916 1916
1917 1917 @classmethod
1918 1918 def get_by_id_or_repo_name(cls, repoid):
1919 1919 if isinstance(repoid, int):
1920 1920 try:
1921 1921 repo = cls.get(repoid)
1922 1922 except ValueError:
1923 1923 repo = None
1924 1924 else:
1925 1925 repo = cls.get_by_repo_name(repoid)
1926 1926 return repo
1927 1927
1928 1928 @classmethod
1929 1929 def get_by_full_path(cls, repo_full_path):
1930 1930 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1931 1931 repo_name = cls.normalize_repo_name(repo_name)
1932 1932 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1933 1933
1934 1934 @classmethod
1935 1935 def get_repo_forks(cls, repo_id):
1936 1936 return cls.query().filter(Repository.fork_id == repo_id)
1937 1937
1938 1938 @classmethod
1939 1939 def base_path(cls):
1940 1940 """
1941 1941 Returns base path when all repos are stored
1942 1942
1943 1943 :param cls:
1944 1944 """
1945 1945 from rhodecode.lib.utils import get_rhodecode_base_path
1946 1946 return get_rhodecode_base_path()
1947 1947
1948 1948 @classmethod
1949 1949 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1950 1950 case_insensitive=True, archived=False):
1951 1951 q = Repository.query()
1952 1952
1953 1953 if not archived:
1954 1954 q = q.filter(Repository.archived.isnot(true()))
1955 1955
1956 1956 if not isinstance(user_id, Optional):
1957 1957 q = q.filter(Repository.user_id == user_id)
1958 1958
1959 1959 if not isinstance(group_id, Optional):
1960 1960 q = q.filter(Repository.group_id == group_id)
1961 1961
1962 1962 if case_insensitive:
1963 1963 q = q.order_by(func.lower(Repository.repo_name))
1964 1964 else:
1965 1965 q = q.order_by(Repository.repo_name)
1966 1966
1967 1967 return q.all()
1968 1968
1969 1969 @property
1970 1970 def repo_uid(self):
1971 1971 return '_{}'.format(self.repo_id)
1972 1972
1973 1973 @property
1974 1974 def forks(self):
1975 1975 """
1976 1976 Return forks of this repo
1977 1977 """
1978 1978 return Repository.get_repo_forks(self.repo_id)
1979 1979
1980 1980 @property
1981 1981 def parent(self):
1982 1982 """
1983 1983 Returns fork parent
1984 1984 """
1985 1985 return self.fork
1986 1986
1987 1987 @property
1988 1988 def just_name(self):
1989 1989 return self.repo_name.split(self.NAME_SEP)[-1]
1990 1990
1991 1991 @property
1992 1992 def groups_with_parents(self):
1993 1993 groups = []
1994 1994 if self.group is None:
1995 1995 return groups
1996 1996
1997 1997 cur_gr = self.group
1998 1998 groups.insert(0, cur_gr)
1999 1999 while 1:
2000 2000 gr = getattr(cur_gr, 'parent_group', None)
2001 2001 cur_gr = cur_gr.parent_group
2002 2002 if gr is None:
2003 2003 break
2004 2004 groups.insert(0, gr)
2005 2005
2006 2006 return groups
2007 2007
2008 2008 @property
2009 2009 def groups_and_repo(self):
2010 2010 return self.groups_with_parents, self
2011 2011
2012 2012 @LazyProperty
2013 2013 def repo_path(self):
2014 2014 """
2015 2015 Returns base full path for that repository means where it actually
2016 2016 exists on a filesystem
2017 2017 """
2018 2018 q = Session().query(RhodeCodeUi).filter(
2019 2019 RhodeCodeUi.ui_key == self.NAME_SEP)
2020 2020 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
2021 2021 return q.one().ui_value
2022 2022
2023 2023 @property
2024 2024 def repo_full_path(self):
2025 2025 p = [self.repo_path]
2026 2026 # we need to split the name by / since this is how we store the
2027 2027 # names in the database, but that eventually needs to be converted
2028 2028 # into a valid system path
2029 2029 p += self.repo_name.split(self.NAME_SEP)
2030 2030 return os.path.join(*map(safe_str, p))
2031 2031
2032 2032 @property
2033 2033 def cache_keys(self):
2034 2034 """
2035 2035 Returns associated cache keys for that repo
2036 2036 """
2037 2037 invalidation_namespace = CacheKey.REPO_INVALIDATION_NAMESPACE.format(
2038 2038 repo_id=self.repo_id)
2039 2039 return CacheKey.query()\
2040 2040 .filter(CacheKey.cache_args == invalidation_namespace)\
2041 2041 .order_by(CacheKey.cache_key)\
2042 2042 .all()
2043 2043
2044 2044 @property
2045 2045 def cached_diffs_relative_dir(self):
2046 2046 """
2047 2047 Return a relative to the repository store path of cached diffs
2048 2048 used for safe display for users, who shouldn't know the absolute store
2049 2049 path
2050 2050 """
2051 2051 return os.path.join(
2052 2052 os.path.dirname(self.repo_name),
2053 2053 self.cached_diffs_dir.split(os.path.sep)[-1])
2054 2054
2055 2055 @property
2056 2056 def cached_diffs_dir(self):
2057 2057 path = self.repo_full_path
2058 2058 return os.path.join(
2059 2059 os.path.dirname(path),
2060 2060 f'.__shadow_diff_cache_repo_{self.repo_id}')
2061 2061
2062 2062 def cached_diffs(self):
2063 2063 diff_cache_dir = self.cached_diffs_dir
2064 2064 if os.path.isdir(diff_cache_dir):
2065 2065 return os.listdir(diff_cache_dir)
2066 2066 return []
2067 2067
2068 2068 def shadow_repos(self):
2069 2069 shadow_repos_pattern = f'.__shadow_repo_{self.repo_id}'
2070 2070 return [
2071 2071 x for x in os.listdir(os.path.dirname(self.repo_full_path))
2072 2072 if x.startswith(shadow_repos_pattern)
2073 2073 ]
2074 2074
2075 2075 def get_new_name(self, repo_name):
2076 2076 """
2077 2077 returns new full repository name based on assigned group and new new
2078 2078
2079 2079 :param repo_name:
2080 2080 """
2081 2081 path_prefix = self.group.full_path_splitted if self.group else []
2082 2082 return self.NAME_SEP.join(path_prefix + [repo_name])
2083 2083
2084 2084 @property
2085 2085 def _config(self):
2086 2086 """
2087 2087 Returns db based config object.
2088 2088 """
2089 2089 from rhodecode.lib.utils import make_db_config
2090 2090 return make_db_config(clear_session=False, repo=self)
2091 2091
2092 2092 def permissions(self, with_admins=True, with_owner=True,
2093 2093 expand_from_user_groups=False):
2094 2094 """
2095 2095 Permissions for repositories
2096 2096 """
2097 2097 _admin_perm = 'repository.admin'
2098 2098
2099 2099 owner_row = []
2100 2100 if with_owner:
2101 2101 usr = AttributeDict(self.user.get_dict())
2102 2102 usr.owner_row = True
2103 2103 usr.permission = _admin_perm
2104 2104 usr.permission_id = None
2105 2105 owner_row.append(usr)
2106 2106
2107 2107 super_admin_ids = []
2108 2108 super_admin_rows = []
2109 2109 if with_admins:
2110 2110 for usr in User.get_all_super_admins():
2111 2111 super_admin_ids.append(usr.user_id)
2112 2112 # if this admin is also owner, don't double the record
2113 2113 if usr.user_id == owner_row[0].user_id:
2114 2114 owner_row[0].admin_row = True
2115 2115 else:
2116 2116 usr = AttributeDict(usr.get_dict())
2117 2117 usr.admin_row = True
2118 2118 usr.permission = _admin_perm
2119 2119 usr.permission_id = None
2120 2120 super_admin_rows.append(usr)
2121 2121
2122 2122 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
2123 2123 q = q.options(joinedload(UserRepoToPerm.repository),
2124 2124 joinedload(UserRepoToPerm.user),
2125 2125 joinedload(UserRepoToPerm.permission),)
2126 2126
2127 2127 # get owners and admins and permissions. We do a trick of re-writing
2128 2128 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2129 2129 # has a global reference and changing one object propagates to all
2130 2130 # others. This means if admin is also an owner admin_row that change
2131 2131 # would propagate to both objects
2132 2132 perm_rows = []
2133 2133 for _usr in q.all():
2134 2134 usr = AttributeDict(_usr.user.get_dict())
2135 2135 # if this user is also owner/admin, mark as duplicate record
2136 2136 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
2137 2137 usr.duplicate_perm = True
2138 2138 # also check if this permission is maybe used by branch_permissions
2139 2139 if _usr.branch_perm_entry:
2140 2140 usr.branch_rules = [x.branch_rule_id for x in _usr.branch_perm_entry]
2141 2141
2142 2142 usr.permission = _usr.permission.permission_name
2143 2143 usr.permission_id = _usr.repo_to_perm_id
2144 2144 perm_rows.append(usr)
2145 2145
2146 2146 # filter the perm rows by 'default' first and then sort them by
2147 2147 # admin,write,read,none permissions sorted again alphabetically in
2148 2148 # each group
2149 2149 perm_rows = sorted(perm_rows, key=display_user_sort)
2150 2150
2151 2151 user_groups_rows = []
2152 2152 if expand_from_user_groups:
2153 2153 for ug in self.permission_user_groups(with_members=True):
2154 2154 for user_data in ug.members:
2155 2155 user_groups_rows.append(user_data)
2156 2156
2157 2157 return super_admin_rows + owner_row + perm_rows + user_groups_rows
2158 2158
2159 2159 def permission_user_groups(self, with_members=True):
2160 2160 q = UserGroupRepoToPerm.query()\
2161 2161 .filter(UserGroupRepoToPerm.repository == self)
2162 2162 q = q.options(joinedload(UserGroupRepoToPerm.repository),
2163 2163 joinedload(UserGroupRepoToPerm.users_group),
2164 2164 joinedload(UserGroupRepoToPerm.permission),)
2165 2165
2166 2166 perm_rows = []
2167 2167 for _user_group in q.all():
2168 2168 entry = AttributeDict(_user_group.users_group.get_dict())
2169 2169 entry.permission = _user_group.permission.permission_name
2170 2170 if with_members:
2171 2171 entry.members = [x.user.get_dict()
2172 2172 for x in _user_group.users_group.members]
2173 2173 perm_rows.append(entry)
2174 2174
2175 2175 perm_rows = sorted(perm_rows, key=display_user_group_sort)
2176 2176 return perm_rows
2177 2177
2178 2178 def get_api_data(self, include_secrets=False):
2179 2179 """
2180 2180 Common function for generating repo api data
2181 2181
2182 2182 :param include_secrets: See :meth:`User.get_api_data`.
2183 2183
2184 2184 """
2185 2185 # TODO: mikhail: Here there is an anti-pattern, we probably need to
2186 2186 # move this methods on models level.
2187 2187 from rhodecode.model.settings import SettingsModel
2188 2188 from rhodecode.model.repo import RepoModel
2189 2189
2190 2190 repo = self
2191 2191 _user_id, _time, _reason = self.locked
2192 2192
2193 2193 data = {
2194 2194 'repo_id': repo.repo_id,
2195 2195 'repo_name': repo.repo_name,
2196 2196 'repo_type': repo.repo_type,
2197 2197 'clone_uri': repo.clone_uri or '',
2198 2198 'push_uri': repo.push_uri or '',
2199 2199 'url': RepoModel().get_url(self),
2200 2200 'private': repo.private,
2201 2201 'created_on': repo.created_on,
2202 2202 'description': repo.description_safe,
2203 2203 'landing_rev': repo.landing_rev,
2204 2204 'owner': repo.user.username,
2205 2205 'fork_of': repo.fork.repo_name if repo.fork else None,
2206 2206 'fork_of_id': repo.fork.repo_id if repo.fork else None,
2207 2207 'enable_statistics': repo.enable_statistics,
2208 2208 'enable_locking': repo.enable_locking,
2209 2209 'enable_downloads': repo.enable_downloads,
2210 2210 'last_changeset': repo.changeset_cache,
2211 2211 'locked_by': User.get(_user_id).get_api_data(
2212 2212 include_secrets=include_secrets) if _user_id else None,
2213 2213 'locked_date': time_to_datetime(_time) if _time else None,
2214 2214 'lock_reason': _reason if _reason else None,
2215 2215 }
2216 2216
2217 2217 # TODO: mikhail: should be per-repo settings here
2218 2218 rc_config = SettingsModel().get_all_settings()
2219 2219 repository_fields = str2bool(
2220 2220 rc_config.get('rhodecode_repository_fields'))
2221 2221 if repository_fields:
2222 2222 for f in self.extra_fields:
2223 2223 data[f.field_key_prefixed] = f.field_value
2224 2224
2225 2225 return data
2226 2226
2227 2227 @classmethod
2228 2228 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
2229 2229 if not lock_time:
2230 2230 lock_time = time.time()
2231 2231 if not lock_reason:
2232 2232 lock_reason = cls.LOCK_AUTOMATIC
2233 2233 repo.locked = [user_id, lock_time, lock_reason]
2234 2234 Session().add(repo)
2235 2235 Session().commit()
2236 2236
2237 2237 @classmethod
2238 2238 def unlock(cls, repo):
2239 2239 repo.locked = None
2240 2240 Session().add(repo)
2241 2241 Session().commit()
2242 2242
2243 2243 @classmethod
2244 2244 def getlock(cls, repo):
2245 2245 return repo.locked
2246 2246
2247 2247 def get_locking_state(self, action, user_id, only_when_enabled=True):
2248 2248 """
2249 2249 Checks locking on this repository, if locking is enabled and lock is
2250 2250 present returns a tuple of make_lock, locked, locked_by.
2251 2251 make_lock can have 3 states None (do nothing) True, make lock
2252 2252 False release lock, This value is later propagated to hooks, which
2253 2253 do the locking. Think about this as signals passed to hooks what to do.
2254 2254
2255 2255 """
2256 2256 # TODO: johbo: This is part of the business logic and should be moved
2257 2257 # into the RepositoryModel.
2258 2258
2259 2259 if action not in ('push', 'pull'):
2260 2260 raise ValueError("Invalid action value: %s" % repr(action))
2261 2261
2262 2262 # defines if locked error should be thrown to user
2263 2263 currently_locked = False
2264 2264 # defines if new lock should be made, tri-state
2265 2265 make_lock = None
2266 2266 repo = self
2267 2267 user = User.get(user_id)
2268 2268
2269 2269 lock_info = repo.locked
2270 2270
2271 2271 if repo and (repo.enable_locking or not only_when_enabled):
2272 2272 if action == 'push':
2273 2273 # check if it's already locked !, if it is compare users
2274 2274 locked_by_user_id = lock_info[0]
2275 2275 if user.user_id == locked_by_user_id:
2276 2276 log.debug(
2277 2277 'Got `push` action from user %s, now unlocking', user)
2278 2278 # unlock if we have push from user who locked
2279 2279 make_lock = False
2280 2280 else:
2281 2281 # we're not the same user who locked, ban with
2282 2282 # code defined in settings (default is 423 HTTP Locked) !
2283 2283 log.debug('Repo %s is currently locked by %s', repo, user)
2284 2284 currently_locked = True
2285 2285 elif action == 'pull':
2286 2286 # [0] user [1] date
2287 2287 if lock_info[0] and lock_info[1]:
2288 2288 log.debug('Repo %s is currently locked by %s', repo, user)
2289 2289 currently_locked = True
2290 2290 else:
2291 2291 log.debug('Setting lock on repo %s by %s', repo, user)
2292 2292 make_lock = True
2293 2293
2294 2294 else:
2295 2295 log.debug('Repository %s do not have locking enabled', repo)
2296 2296
2297 2297 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
2298 2298 make_lock, currently_locked, lock_info)
2299 2299
2300 2300 from rhodecode.lib.auth import HasRepoPermissionAny
2301 2301 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
2302 2302 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
2303 2303 # if we don't have at least write permission we cannot make a lock
2304 2304 log.debug('lock state reset back to FALSE due to lack '
2305 2305 'of at least read permission')
2306 2306 make_lock = False
2307 2307
2308 2308 return make_lock, currently_locked, lock_info
2309 2309
2310 2310 @property
2311 2311 def last_commit_cache_update_diff(self):
2312 2312 return time.time() - (safe_int(self.changeset_cache.get('updated_on')) or 0)
2313 2313
2314 2314 @classmethod
2315 2315 def _load_commit_change(cls, last_commit_cache):
2316 2316 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2317 2317 empty_date = datetime.datetime.fromtimestamp(0)
2318 2318 date_latest = last_commit_cache.get('date', empty_date)
2319 2319 try:
2320 2320 return parse_datetime(date_latest)
2321 2321 except Exception:
2322 2322 return empty_date
2323 2323
2324 2324 @property
2325 2325 def last_commit_change(self):
2326 2326 return self._load_commit_change(self.changeset_cache)
2327 2327
2328 2328 @property
2329 2329 def last_db_change(self):
2330 2330 return self.updated_on
2331 2331
2332 2332 @property
2333 2333 def clone_uri_hidden(self):
2334 2334 clone_uri = self.clone_uri
2335 2335 if clone_uri:
2336 2336 import urlobject
2337 2337 url_obj = urlobject.URLObject(cleaned_uri(clone_uri))
2338 2338 if url_obj.password:
2339 2339 clone_uri = url_obj.with_password('*****')
2340 2340 return clone_uri
2341 2341
2342 2342 @property
2343 2343 def push_uri_hidden(self):
2344 2344 push_uri = self.push_uri
2345 2345 if push_uri:
2346 2346 import urlobject
2347 2347 url_obj = urlobject.URLObject(cleaned_uri(push_uri))
2348 2348 if url_obj.password:
2349 2349 push_uri = url_obj.with_password('*****')
2350 2350 return push_uri
2351 2351
2352 2352 def clone_url(self, **override):
2353 2353 from rhodecode.model.settings import SettingsModel
2354 2354
2355 2355 uri_tmpl = None
2356 2356 if 'with_id' in override:
2357 2357 uri_tmpl = self.DEFAULT_CLONE_URI_ID
2358 2358 del override['with_id']
2359 2359
2360 2360 if 'uri_tmpl' in override:
2361 2361 uri_tmpl = override['uri_tmpl']
2362 2362 del override['uri_tmpl']
2363 2363
2364 2364 ssh = False
2365 2365 if 'ssh' in override:
2366 2366 ssh = True
2367 2367 del override['ssh']
2368 2368
2369 2369 # we didn't override our tmpl from **overrides
2370 2370 request = get_current_request()
2371 2371 if not uri_tmpl:
2372 2372 if hasattr(request, 'call_context') and hasattr(request.call_context, 'rc_config'):
2373 2373 rc_config = request.call_context.rc_config
2374 2374 else:
2375 2375 rc_config = SettingsModel().get_all_settings(cache=True)
2376 2376
2377 2377 if ssh:
2378 2378 uri_tmpl = rc_config.get(
2379 2379 'rhodecode_clone_uri_ssh_tmpl') or self.DEFAULT_CLONE_URI_SSH
2380 2380
2381 2381 else:
2382 2382 uri_tmpl = rc_config.get(
2383 2383 'rhodecode_clone_uri_tmpl') or self.DEFAULT_CLONE_URI
2384 2384
2385 2385 return get_clone_url(request=request,
2386 2386 uri_tmpl=uri_tmpl,
2387 2387 repo_name=self.repo_name,
2388 2388 repo_id=self.repo_id,
2389 2389 repo_type=self.repo_type,
2390 2390 **override)
2391 2391
2392 2392 def set_state(self, state):
2393 2393 self.repo_state = state
2394 2394 Session().add(self)
2395 2395 #==========================================================================
2396 2396 # SCM PROPERTIES
2397 2397 #==========================================================================
2398 2398
2399 2399 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None, maybe_unreachable=False, reference_obj=None):
2400 2400 return get_commit_safe(
2401 2401 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load,
2402 2402 maybe_unreachable=maybe_unreachable, reference_obj=reference_obj)
2403 2403
2404 2404 def get_changeset(self, rev=None, pre_load=None):
2405 2405 warnings.warn("Use get_commit", DeprecationWarning)
2406 2406 commit_id = None
2407 2407 commit_idx = None
2408 2408 if isinstance(rev, str):
2409 2409 commit_id = rev
2410 2410 else:
2411 2411 commit_idx = rev
2412 2412 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
2413 2413 pre_load=pre_load)
2414 2414
2415 2415 def get_landing_commit(self):
2416 2416 """
2417 2417 Returns landing commit, or if that doesn't exist returns the tip
2418 2418 """
2419 2419 _rev_type, _rev = self.landing_rev
2420 2420 commit = self.get_commit(_rev)
2421 2421 if isinstance(commit, EmptyCommit):
2422 2422 return self.get_commit()
2423 2423 return commit
2424 2424
2425 2425 def flush_commit_cache(self):
2426 2426 self.update_commit_cache(cs_cache={'raw_id':'0'})
2427 2427 self.update_commit_cache()
2428 2428
2429 2429 def update_commit_cache(self, cs_cache=None, config=None):
2430 2430 """
2431 2431 Update cache of last commit for repository
2432 2432 cache_keys should be::
2433 2433
2434 2434 source_repo_id
2435 2435 short_id
2436 2436 raw_id
2437 2437 revision
2438 2438 parents
2439 2439 message
2440 2440 date
2441 2441 author
2442 2442 updated_on
2443 2443
2444 2444 """
2445 2445 from rhodecode.lib.vcs.backends.base import BaseCommit
2446 2446 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2447 2447 empty_date = datetime.datetime.fromtimestamp(0)
2448 2448 repo_commit_count = 0
2449 2449
2450 2450 if cs_cache is None:
2451 2451 # use no-cache version here
2452 2452 try:
2453 2453 scm_repo = self.scm_instance(cache=False, config=config)
2454 2454 except VCSError:
2455 2455 scm_repo = None
2456 2456 empty = scm_repo is None or scm_repo.is_empty()
2457 2457
2458 2458 if not empty:
2459 2459 cs_cache = scm_repo.get_commit(
2460 2460 pre_load=["author", "date", "message", "parents", "branch"])
2461 2461 repo_commit_count = scm_repo.count()
2462 2462 else:
2463 2463 cs_cache = EmptyCommit()
2464 2464
2465 2465 if isinstance(cs_cache, BaseCommit):
2466 2466 cs_cache = cs_cache.__json__()
2467 2467
2468 2468 def is_outdated(new_cs_cache):
2469 2469 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
2470 2470 new_cs_cache['revision'] != self.changeset_cache['revision']):
2471 2471 return True
2472 2472 return False
2473 2473
2474 2474 # check if we have maybe already latest cached revision
2475 2475 if is_outdated(cs_cache) or not self.changeset_cache:
2476 2476 _current_datetime = datetime.datetime.utcnow()
2477 2477 last_change = cs_cache.get('date') or _current_datetime
2478 2478 # we check if last update is newer than the new value
2479 2479 # if yes, we use the current timestamp instead. Imagine you get
2480 2480 # old commit pushed 1y ago, we'd set last update 1y to ago.
2481 2481 last_change_timestamp = datetime_to_time(last_change)
2482 2482 current_timestamp = datetime_to_time(last_change)
2483 2483 if last_change_timestamp > current_timestamp and not empty:
2484 2484 cs_cache['date'] = _current_datetime
2485 2485
2486 2486 # also store size of repo
2487 2487 cs_cache['repo_commit_count'] = repo_commit_count
2488 2488
2489 2489 _date_latest = parse_datetime(cs_cache.get('date') or empty_date)
2490 2490 cs_cache['updated_on'] = time.time()
2491 2491 self.changeset_cache = cs_cache
2492 2492 self.updated_on = last_change
2493 2493 Session().add(self)
2494 2494 Session().commit()
2495 2495
2496 2496 else:
2497 2497 if empty:
2498 2498 cs_cache = EmptyCommit().__json__()
2499 2499 else:
2500 2500 cs_cache = self.changeset_cache
2501 2501
2502 2502 _date_latest = parse_datetime(cs_cache.get('date') or empty_date)
2503 2503
2504 2504 cs_cache['updated_on'] = time.time()
2505 2505 self.changeset_cache = cs_cache
2506 2506 self.updated_on = _date_latest
2507 2507 Session().add(self)
2508 2508 Session().commit()
2509 2509
2510 2510 log.debug('updated repo `%s` with new commit cache %s, and last update_date: %s',
2511 2511 self.repo_name, cs_cache, _date_latest)
2512 2512
2513 2513 @property
2514 2514 def tip(self):
2515 2515 return self.get_commit('tip')
2516 2516
2517 2517 @property
2518 2518 def author(self):
2519 2519 return self.tip.author
2520 2520
2521 2521 @property
2522 2522 def last_change(self):
2523 2523 return self.scm_instance().last_change
2524 2524
2525 2525 def get_comments(self, revisions=None):
2526 2526 """
2527 2527 Returns comments for this repository grouped by revisions
2528 2528
2529 2529 :param revisions: filter query by revisions only
2530 2530 """
2531 2531 cmts = ChangesetComment.query()\
2532 2532 .filter(ChangesetComment.repo == self)
2533 2533 if revisions:
2534 2534 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
2535 2535 grouped = collections.defaultdict(list)
2536 2536 for cmt in cmts.all():
2537 2537 grouped[cmt.revision].append(cmt)
2538 2538 return grouped
2539 2539
2540 2540 def statuses(self, revisions=None):
2541 2541 """
2542 2542 Returns statuses for this repository
2543 2543
2544 2544 :param revisions: list of revisions to get statuses for
2545 2545 """
2546 2546 statuses = ChangesetStatus.query()\
2547 2547 .filter(ChangesetStatus.repo == self)\
2548 2548 .filter(ChangesetStatus.version == 0)
2549 2549
2550 2550 if revisions:
2551 2551 # Try doing the filtering in chunks to avoid hitting limits
2552 2552 size = 500
2553 2553 status_results = []
2554 2554 for chunk in range(0, len(revisions), size):
2555 2555 status_results += statuses.filter(
2556 2556 ChangesetStatus.revision.in_(
2557 2557 revisions[chunk: chunk+size])
2558 2558 ).all()
2559 2559 else:
2560 2560 status_results = statuses.all()
2561 2561
2562 2562 grouped = {}
2563 2563
2564 2564 # maybe we have open new pullrequest without a status?
2565 2565 stat = ChangesetStatus.STATUS_UNDER_REVIEW
2566 2566 status_lbl = ChangesetStatus.get_status_lbl(stat)
2567 2567 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
2568 2568 for rev in pr.revisions:
2569 2569 pr_id = pr.pull_request_id
2570 2570 pr_repo = pr.target_repo.repo_name
2571 2571 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
2572 2572
2573 2573 for stat in status_results:
2574 2574 pr_id = pr_repo = None
2575 2575 if stat.pull_request:
2576 2576 pr_id = stat.pull_request.pull_request_id
2577 2577 pr_repo = stat.pull_request.target_repo.repo_name
2578 2578 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
2579 2579 pr_id, pr_repo]
2580 2580 return grouped
2581 2581
2582 2582 # ==========================================================================
2583 2583 # SCM CACHE INSTANCE
2584 2584 # ==========================================================================
2585 2585
2586 2586 def scm_instance(self, **kwargs):
2587 2587 import rhodecode
2588 2588
2589 2589 # Passing a config will not hit the cache currently only used
2590 2590 # for repo2dbmapper
2591 2591 config = kwargs.pop('config', None)
2592 2592 cache = kwargs.pop('cache', None)
2593 2593 vcs_full_cache = kwargs.pop('vcs_full_cache', None)
2594 2594 if vcs_full_cache is not None:
2595 2595 # allows override global config
2596 2596 full_cache = vcs_full_cache
2597 2597 else:
2598 2598 full_cache = rhodecode.ConfigGet().get_bool('vcs_full_cache')
2599 2599 # if cache is NOT defined use default global, else we have a full
2600 2600 # control over cache behaviour
2601 2601 if cache is None and full_cache and not config:
2602 2602 log.debug('Initializing pure cached instance for %s', self.repo_path)
2603 2603 return self._get_instance_cached()
2604 2604
2605 2605 # cache here is sent to the "vcs server"
2606 2606 return self._get_instance(cache=bool(cache), config=config)
2607 2607
2608 2608 def _get_instance_cached(self):
2609 2609 from rhodecode.lib import rc_cache
2610 2610
2611 2611 cache_namespace_uid = f'repo_instance.{self.repo_id}'
2612 2612 invalidation_namespace = CacheKey.REPO_INVALIDATION_NAMESPACE.format(
2613 2613 repo_id=self.repo_id)
2614 2614 region = rc_cache.get_or_create_region('cache_repo_longterm', cache_namespace_uid)
2615 2615
2616 2616 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid)
2617 2617 def get_instance_cached(repo_id, context_id, _cache_state_uid):
2618 2618 return self._get_instance(repo_state_uid=_cache_state_uid)
2619 2619
2620 2620 # we must use thread scoped cache here,
2621 2621 # because each thread of gevent needs it's own not shared connection and cache
2622 2622 # we also alter `args` so the cache key is individual for every green thread.
2623 2623 inv_context_manager = rc_cache.InvalidationContext(
2624 2624 uid=cache_namespace_uid, invalidation_namespace=invalidation_namespace,
2625 2625 thread_scoped=True)
2626 2626 with inv_context_manager as invalidation_context:
2627 2627 cache_state_uid = invalidation_context.cache_data['cache_state_uid']
2628 2628 args = (self.repo_id, inv_context_manager.cache_key, cache_state_uid)
2629 2629
2630 2630 # re-compute and store cache if we get invalidate signal
2631 2631 if invalidation_context.should_invalidate():
2632 2632 instance = get_instance_cached.refresh(*args)
2633 2633 else:
2634 2634 instance = get_instance_cached(*args)
2635 2635
2636 2636 log.debug('Repo instance fetched in %.4fs', inv_context_manager.compute_time)
2637 2637 return instance
2638 2638
2639 2639 def _get_instance(self, cache=True, config=None, repo_state_uid=None):
2640 2640 log.debug('Initializing %s instance `%s` with cache flag set to: %s',
2641 2641 self.repo_type, self.repo_path, cache)
2642 2642 config = config or self._config
2643 2643 custom_wire = {
2644 2644 'cache': cache, # controls the vcs.remote cache
2645 2645 'repo_state_uid': repo_state_uid
2646 2646 }
2647 2647 repo = get_vcs_instance(
2648 2648 repo_path=safe_str(self.repo_full_path),
2649 2649 config=config,
2650 2650 with_wire=custom_wire,
2651 2651 create=False,
2652 2652 _vcs_alias=self.repo_type)
2653 2653 if repo is not None:
2654 2654 repo.count() # cache rebuild
2655 2655 return repo
2656 2656
2657 2657 def get_shadow_repository_path(self, workspace_id):
2658 2658 from rhodecode.lib.vcs.backends.base import BaseRepository
2659 2659 shadow_repo_path = BaseRepository._get_shadow_repository_path(
2660 2660 self.repo_full_path, self.repo_id, workspace_id)
2661 2661 return shadow_repo_path
2662 2662
2663 2663 def __json__(self):
2664 2664 return {'landing_rev': self.landing_rev}
2665 2665
2666 2666 def get_dict(self):
2667 2667
2668 2668 # Since we transformed `repo_name` to a hybrid property, we need to
2669 2669 # keep compatibility with the code which uses `repo_name` field.
2670 2670
2671 2671 result = super(Repository, self).get_dict()
2672 2672 result['repo_name'] = result.pop('_repo_name', None)
2673 2673 result.pop('_changeset_cache', '')
2674 2674 return result
2675 2675
2676 2676
2677 2677 class RepoGroup(Base, BaseModel):
2678 2678 __tablename__ = 'groups'
2679 2679 __table_args__ = (
2680 2680 UniqueConstraint('group_name', 'group_parent_id'),
2681 2681 base_table_args,
2682 2682 )
2683 2683
2684 2684 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2685 2685
2686 2686 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2687 2687 _group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2688 2688 group_name_hash = Column("repo_group_name_hash", String(1024), nullable=False, unique=False)
2689 2689 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2690 2690 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2691 2691 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2692 2692 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2693 2693 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2694 2694 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2695 2695 personal = Column('personal', Boolean(), nullable=True, unique=None, default=None)
2696 2696 _changeset_cache = Column("changeset_cache", LargeBinary(), nullable=True) # JSON data
2697 2697
2698 2698 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id', back_populates='group')
2699 2699 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all', back_populates='group')
2700 2700 parent_group = relationship('RepoGroup', remote_side=group_id)
2701 2701 user = relationship('User', back_populates='repository_groups')
2702 2702 integrations = relationship('Integration', cascade="all, delete-orphan", back_populates='repo_group')
2703 2703
2704 2704 # no cascade, set NULL
2705 2705 scope_artifacts = relationship('FileStore', primaryjoin='FileStore.scope_repo_group_id==RepoGroup.group_id', viewonly=True)
2706 2706
2707 2707 def __init__(self, group_name='', parent_group=None):
2708 2708 self.group_name = group_name
2709 2709 self.parent_group = parent_group
2710 2710
2711 2711 def __repr__(self):
2712 2712 return f"<{self.cls_name}('id:{self.group_id}:{self.group_name}')>"
2713 2713
2714 2714 @hybrid_property
2715 2715 def group_name(self):
2716 2716 return self._group_name
2717 2717
2718 2718 @group_name.setter
2719 2719 def group_name(self, value):
2720 2720 self._group_name = value
2721 2721 self.group_name_hash = self.hash_repo_group_name(value)
2722 2722
2723 2723 @classmethod
2724 2724 def _load_changeset_cache(cls, repo_id, changeset_cache_raw):
2725 2725 from rhodecode.lib.vcs.backends.base import EmptyCommit
2726 2726 dummy = EmptyCommit().__json__()
2727 2727 if not changeset_cache_raw:
2728 2728 dummy['source_repo_id'] = repo_id
2729 2729 return json.loads(json.dumps(dummy))
2730 2730
2731 2731 try:
2732 2732 return json.loads(changeset_cache_raw)
2733 2733 except TypeError:
2734 2734 return dummy
2735 2735 except Exception:
2736 2736 log.error(traceback.format_exc())
2737 2737 return dummy
2738 2738
2739 2739 @hybrid_property
2740 2740 def changeset_cache(self):
2741 2741 return self._load_changeset_cache('', self._changeset_cache)
2742 2742
2743 2743 @changeset_cache.setter
2744 2744 def changeset_cache(self, val):
2745 2745 try:
2746 2746 self._changeset_cache = json.dumps(val)
2747 2747 except Exception:
2748 2748 log.error(traceback.format_exc())
2749 2749
2750 2750 @validates('group_parent_id')
2751 2751 def validate_group_parent_id(self, key, val):
2752 2752 """
2753 2753 Check cycle references for a parent group to self
2754 2754 """
2755 2755 if self.group_id and val:
2756 2756 assert val != self.group_id
2757 2757
2758 2758 return val
2759 2759
2760 2760 @hybrid_property
2761 2761 def description_safe(self):
2762 2762 from rhodecode.lib import helpers as h
2763 2763 return h.escape(self.group_description)
2764 2764
2765 2765 @classmethod
2766 2766 def hash_repo_group_name(cls, repo_group_name):
2767 2767 val = remove_formatting(repo_group_name)
2768 2768 val = safe_str(val).lower()
2769 2769 chars = []
2770 2770 for c in val:
2771 2771 if c not in string.ascii_letters:
2772 2772 c = str(ord(c))
2773 2773 chars.append(c)
2774 2774
2775 2775 return ''.join(chars)
2776 2776
2777 2777 @classmethod
2778 2778 def _generate_choice(cls, repo_group):
2779 2779 from webhelpers2.html import literal as _literal
2780 2780
2781 2781 def _name(k):
2782 2782 return _literal(cls.CHOICES_SEPARATOR.join(k))
2783 2783
2784 2784 return repo_group.group_id, _name(repo_group.full_path_splitted)
2785 2785
2786 2786 @classmethod
2787 2787 def groups_choices(cls, groups=None, show_empty_group=True):
2788 2788 if not groups:
2789 2789 groups = cls.query().all()
2790 2790
2791 2791 repo_groups = []
2792 2792 if show_empty_group:
2793 2793 repo_groups = [(-1, '-- %s --' % _('No parent'))]
2794 2794
2795 2795 repo_groups.extend([cls._generate_choice(x) for x in groups])
2796 2796
2797 2797 repo_groups = sorted(
2798 2798 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2799 2799 return repo_groups
2800 2800
2801 2801 @classmethod
2802 2802 def url_sep(cls):
2803 2803 return URL_SEP
2804 2804
2805 2805 @classmethod
2806 2806 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2807 2807 if case_insensitive:
2808 2808 gr = cls.query().filter(func.lower(cls.group_name)
2809 2809 == func.lower(group_name))
2810 2810 else:
2811 2811 gr = cls.query().filter(cls.group_name == group_name)
2812 2812 if cache:
2813 2813 name_key = _hash_key(group_name)
2814 2814 gr = gr.options(
2815 2815 FromCache("sql_cache_short", f"get_group_{name_key}"))
2816 2816 return gr.scalar()
2817 2817
2818 2818 @classmethod
2819 2819 def get_user_personal_repo_group(cls, user_id):
2820 2820 user = User.get(user_id)
2821 2821 if user.username == User.DEFAULT_USER:
2822 2822 return None
2823 2823
2824 2824 return cls.query()\
2825 2825 .filter(cls.personal == true()) \
2826 2826 .filter(cls.user == user) \
2827 2827 .order_by(cls.group_id.asc()) \
2828 2828 .first()
2829 2829
2830 2830 @classmethod
2831 2831 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2832 2832 case_insensitive=True):
2833 2833 q = RepoGroup.query()
2834 2834
2835 2835 if not isinstance(user_id, Optional):
2836 2836 q = q.filter(RepoGroup.user_id == user_id)
2837 2837
2838 2838 if not isinstance(group_id, Optional):
2839 2839 q = q.filter(RepoGroup.group_parent_id == group_id)
2840 2840
2841 2841 if case_insensitive:
2842 2842 q = q.order_by(func.lower(RepoGroup.group_name))
2843 2843 else:
2844 2844 q = q.order_by(RepoGroup.group_name)
2845 2845 return q.all()
2846 2846
2847 2847 @property
2848 2848 def parents(self, parents_recursion_limit=10):
2849 2849 groups = []
2850 2850 if self.parent_group is None:
2851 2851 return groups
2852 2852 cur_gr = self.parent_group
2853 2853 groups.insert(0, cur_gr)
2854 2854 cnt = 0
2855 2855 while 1:
2856 2856 cnt += 1
2857 2857 gr = getattr(cur_gr, 'parent_group', None)
2858 2858 cur_gr = cur_gr.parent_group
2859 2859 if gr is None:
2860 2860 break
2861 2861 if cnt == parents_recursion_limit:
2862 2862 # this will prevent accidental infinit loops
2863 2863 log.error('more than %s parents found for group %s, stopping '
2864 2864 'recursive parent fetching', parents_recursion_limit, self)
2865 2865 break
2866 2866
2867 2867 groups.insert(0, gr)
2868 2868 return groups
2869 2869
2870 2870 @property
2871 2871 def last_commit_cache_update_diff(self):
2872 2872 return time.time() - (safe_int(self.changeset_cache.get('updated_on')) or 0)
2873 2873
2874 2874 @classmethod
2875 2875 def _load_commit_change(cls, last_commit_cache):
2876 2876 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2877 2877 empty_date = datetime.datetime.fromtimestamp(0)
2878 2878 date_latest = last_commit_cache.get('date', empty_date)
2879 2879 try:
2880 2880 return parse_datetime(date_latest)
2881 2881 except Exception:
2882 2882 return empty_date
2883 2883
2884 2884 @property
2885 2885 def last_commit_change(self):
2886 2886 return self._load_commit_change(self.changeset_cache)
2887 2887
2888 2888 @property
2889 2889 def last_db_change(self):
2890 2890 return self.updated_on
2891 2891
2892 2892 @property
2893 2893 def children(self):
2894 2894 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2895 2895
2896 2896 @property
2897 2897 def name(self):
2898 2898 return self.group_name.split(RepoGroup.url_sep())[-1]
2899 2899
2900 2900 @property
2901 2901 def full_path(self):
2902 2902 return self.group_name
2903 2903
2904 2904 @property
2905 2905 def full_path_splitted(self):
2906 2906 return self.group_name.split(RepoGroup.url_sep())
2907 2907
2908 2908 @property
2909 2909 def repositories(self):
2910 2910 return Repository.query()\
2911 2911 .filter(Repository.group == self)\
2912 2912 .order_by(Repository.repo_name)
2913 2913
2914 2914 @property
2915 2915 def repositories_recursive_count(self):
2916 2916 cnt = self.repositories.count()
2917 2917
2918 2918 def children_count(group):
2919 2919 cnt = 0
2920 2920 for child in group.children:
2921 2921 cnt += child.repositories.count()
2922 2922 cnt += children_count(child)
2923 2923 return cnt
2924 2924
2925 2925 return cnt + children_count(self)
2926 2926
2927 2927 def _recursive_objects(self, include_repos=True, include_groups=True):
2928 2928 all_ = []
2929 2929
2930 2930 def _get_members(root_gr):
2931 2931 if include_repos:
2932 2932 for r in root_gr.repositories:
2933 2933 all_.append(r)
2934 2934 childs = root_gr.children.all()
2935 2935 if childs:
2936 2936 for gr in childs:
2937 2937 if include_groups:
2938 2938 all_.append(gr)
2939 2939 _get_members(gr)
2940 2940
2941 2941 root_group = []
2942 2942 if include_groups:
2943 2943 root_group = [self]
2944 2944
2945 2945 _get_members(self)
2946 2946 return root_group + all_
2947 2947
2948 2948 def recursive_groups_and_repos(self):
2949 2949 """
2950 2950 Recursive return all groups, with repositories in those groups
2951 2951 """
2952 2952 return self._recursive_objects()
2953 2953
2954 2954 def recursive_groups(self):
2955 2955 """
2956 2956 Returns all children groups for this group including children of children
2957 2957 """
2958 2958 return self._recursive_objects(include_repos=False)
2959 2959
2960 2960 def recursive_repos(self):
2961 2961 """
2962 2962 Returns all children repositories for this group
2963 2963 """
2964 2964 return self._recursive_objects(include_groups=False)
2965 2965
2966 2966 def get_new_name(self, group_name):
2967 2967 """
2968 2968 returns new full group name based on parent and new name
2969 2969
2970 2970 :param group_name:
2971 2971 """
2972 2972 path_prefix = (self.parent_group.full_path_splitted if
2973 2973 self.parent_group else [])
2974 2974 return RepoGroup.url_sep().join(path_prefix + [group_name])
2975 2975
2976 2976 def update_commit_cache(self, config=None):
2977 2977 """
2978 2978 Update cache of last commit for newest repository inside this repository group.
2979 2979 cache_keys should be::
2980 2980
2981 2981 source_repo_id
2982 2982 short_id
2983 2983 raw_id
2984 2984 revision
2985 2985 parents
2986 2986 message
2987 2987 date
2988 2988 author
2989 2989
2990 2990 """
2991 2991 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2992 2992 empty_date = datetime.datetime.fromtimestamp(0)
2993 2993
2994 2994 def repo_groups_and_repos(root_gr):
2995 2995 for _repo in root_gr.repositories:
2996 2996 yield _repo
2997 2997 for child_group in root_gr.children.all():
2998 2998 yield child_group
2999 2999
3000 3000 latest_repo_cs_cache = {}
3001 3001 for obj in repo_groups_and_repos(self):
3002 3002 repo_cs_cache = obj.changeset_cache
3003 3003 date_latest = latest_repo_cs_cache.get('date', empty_date)
3004 3004 date_current = repo_cs_cache.get('date', empty_date)
3005 3005 current_timestamp = datetime_to_time(parse_datetime(date_latest))
3006 3006 if current_timestamp < datetime_to_time(parse_datetime(date_current)):
3007 3007 latest_repo_cs_cache = repo_cs_cache
3008 3008 if hasattr(obj, 'repo_id'):
3009 3009 latest_repo_cs_cache['source_repo_id'] = obj.repo_id
3010 3010 else:
3011 3011 latest_repo_cs_cache['source_repo_id'] = repo_cs_cache.get('source_repo_id')
3012 3012
3013 3013 _date_latest = parse_datetime(latest_repo_cs_cache.get('date') or empty_date)
3014 3014
3015 3015 latest_repo_cs_cache['updated_on'] = time.time()
3016 3016 self.changeset_cache = latest_repo_cs_cache
3017 3017 self.updated_on = _date_latest
3018 3018 Session().add(self)
3019 3019 Session().commit()
3020 3020
3021 3021 log.debug('updated repo group `%s` with new commit cache %s, and last update_date: %s',
3022 3022 self.group_name, latest_repo_cs_cache, _date_latest)
3023 3023
3024 3024 def permissions(self, with_admins=True, with_owner=True,
3025 3025 expand_from_user_groups=False):
3026 3026 """
3027 3027 Permissions for repository groups
3028 3028 """
3029 3029 _admin_perm = 'group.admin'
3030 3030
3031 3031 owner_row = []
3032 3032 if with_owner:
3033 3033 usr = AttributeDict(self.user.get_dict())
3034 3034 usr.owner_row = True
3035 3035 usr.permission = _admin_perm
3036 3036 owner_row.append(usr)
3037 3037
3038 3038 super_admin_ids = []
3039 3039 super_admin_rows = []
3040 3040 if with_admins:
3041 3041 for usr in User.get_all_super_admins():
3042 3042 super_admin_ids.append(usr.user_id)
3043 3043 # if this admin is also owner, don't double the record
3044 3044 if usr.user_id == owner_row[0].user_id:
3045 3045 owner_row[0].admin_row = True
3046 3046 else:
3047 3047 usr = AttributeDict(usr.get_dict())
3048 3048 usr.admin_row = True
3049 3049 usr.permission = _admin_perm
3050 3050 super_admin_rows.append(usr)
3051 3051
3052 3052 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
3053 3053 q = q.options(joinedload(UserRepoGroupToPerm.group),
3054 3054 joinedload(UserRepoGroupToPerm.user),
3055 3055 joinedload(UserRepoGroupToPerm.permission),)
3056 3056
3057 3057 # get owners and admins and permissions. We do a trick of re-writing
3058 3058 # objects from sqlalchemy to named-tuples due to sqlalchemy session
3059 3059 # has a global reference and changing one object propagates to all
3060 3060 # others. This means if admin is also an owner admin_row that change
3061 3061 # would propagate to both objects
3062 3062 perm_rows = []
3063 3063 for _usr in q.all():
3064 3064 usr = AttributeDict(_usr.user.get_dict())
3065 3065 # if this user is also owner/admin, mark as duplicate record
3066 3066 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
3067 3067 usr.duplicate_perm = True
3068 3068 usr.permission = _usr.permission.permission_name
3069 3069 perm_rows.append(usr)
3070 3070
3071 3071 # filter the perm rows by 'default' first and then sort them by
3072 3072 # admin,write,read,none permissions sorted again alphabetically in
3073 3073 # each group
3074 3074 perm_rows = sorted(perm_rows, key=display_user_sort)
3075 3075
3076 3076 user_groups_rows = []
3077 3077 if expand_from_user_groups:
3078 3078 for ug in self.permission_user_groups(with_members=True):
3079 3079 for user_data in ug.members:
3080 3080 user_groups_rows.append(user_data)
3081 3081
3082 3082 return super_admin_rows + owner_row + perm_rows + user_groups_rows
3083 3083
3084 3084 def permission_user_groups(self, with_members=False):
3085 3085 q = UserGroupRepoGroupToPerm.query()\
3086 3086 .filter(UserGroupRepoGroupToPerm.group == self)
3087 3087 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
3088 3088 joinedload(UserGroupRepoGroupToPerm.users_group),
3089 3089 joinedload(UserGroupRepoGroupToPerm.permission),)
3090 3090
3091 3091 perm_rows = []
3092 3092 for _user_group in q.all():
3093 3093 entry = AttributeDict(_user_group.users_group.get_dict())
3094 3094 entry.permission = _user_group.permission.permission_name
3095 3095 if with_members:
3096 3096 entry.members = [x.user.get_dict()
3097 3097 for x in _user_group.users_group.members]
3098 3098 perm_rows.append(entry)
3099 3099
3100 3100 perm_rows = sorted(perm_rows, key=display_user_group_sort)
3101 3101 return perm_rows
3102 3102
3103 3103 def get_api_data(self):
3104 3104 """
3105 3105 Common function for generating api data
3106 3106
3107 3107 """
3108 3108 group = self
3109 3109 data = {
3110 3110 'group_id': group.group_id,
3111 3111 'group_name': group.group_name,
3112 3112 'group_description': group.description_safe,
3113 3113 'parent_group': group.parent_group.group_name if group.parent_group else None,
3114 3114 'repositories': [x.repo_name for x in group.repositories],
3115 3115 'owner': group.user.username,
3116 3116 }
3117 3117 return data
3118 3118
3119 3119 def get_dict(self):
3120 3120 # Since we transformed `group_name` to a hybrid property, we need to
3121 3121 # keep compatibility with the code which uses `group_name` field.
3122 3122 result = super(RepoGroup, self).get_dict()
3123 3123 result['group_name'] = result.pop('_group_name', None)
3124 3124 result.pop('_changeset_cache', '')
3125 3125 return result
3126 3126
3127 3127
3128 3128 class Permission(Base, BaseModel):
3129 3129 __tablename__ = 'permissions'
3130 3130 __table_args__ = (
3131 3131 Index('p_perm_name_idx', 'permission_name'),
3132 3132 base_table_args,
3133 3133 )
3134 3134
3135 3135 PERMS = [
3136 3136 ('hg.admin', _('RhodeCode Super Administrator')),
3137 3137
3138 3138 ('repository.none', _('Repository no access')),
3139 3139 ('repository.read', _('Repository read access')),
3140 3140 ('repository.write', _('Repository write access')),
3141 3141 ('repository.admin', _('Repository admin access')),
3142 3142
3143 3143 ('group.none', _('Repository group no access')),
3144 3144 ('group.read', _('Repository group read access')),
3145 3145 ('group.write', _('Repository group write access')),
3146 3146 ('group.admin', _('Repository group admin access')),
3147 3147
3148 3148 ('usergroup.none', _('User group no access')),
3149 3149 ('usergroup.read', _('User group read access')),
3150 3150 ('usergroup.write', _('User group write access')),
3151 3151 ('usergroup.admin', _('User group admin access')),
3152 3152
3153 3153 ('branch.none', _('Branch no permissions')),
3154 3154 ('branch.merge', _('Branch access by web merge')),
3155 3155 ('branch.push', _('Branch access by push')),
3156 3156 ('branch.push_force', _('Branch access by push with force')),
3157 3157
3158 3158 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
3159 3159 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
3160 3160
3161 3161 ('hg.usergroup.create.false', _('User Group creation disabled')),
3162 3162 ('hg.usergroup.create.true', _('User Group creation enabled')),
3163 3163
3164 3164 ('hg.create.none', _('Repository creation disabled')),
3165 3165 ('hg.create.repository', _('Repository creation enabled')),
3166 3166 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
3167 3167 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
3168 3168
3169 3169 ('hg.fork.none', _('Repository forking disabled')),
3170 3170 ('hg.fork.repository', _('Repository forking enabled')),
3171 3171
3172 3172 ('hg.register.none', _('Registration disabled')),
3173 3173 ('hg.register.manual_activate', _('User Registration with manual account activation')),
3174 3174 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
3175 3175
3176 3176 ('hg.password_reset.enabled', _('Password reset enabled')),
3177 3177 ('hg.password_reset.hidden', _('Password reset hidden')),
3178 3178 ('hg.password_reset.disabled', _('Password reset disabled')),
3179 3179
3180 3180 ('hg.extern_activate.manual', _('Manual activation of external account')),
3181 3181 ('hg.extern_activate.auto', _('Automatic activation of external account')),
3182 3182
3183 3183 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
3184 3184 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
3185 3185 ]
3186 3186
3187 3187 # definition of system default permissions for DEFAULT user, created on
3188 3188 # system setup
3189 3189 DEFAULT_USER_PERMISSIONS = [
3190 3190 # object perms
3191 3191 'repository.read',
3192 3192 'group.read',
3193 3193 'usergroup.read',
3194 3194 # branch, for backward compat we need same value as before so forced pushed
3195 3195 'branch.push_force',
3196 3196 # global
3197 3197 'hg.create.repository',
3198 3198 'hg.repogroup.create.false',
3199 3199 'hg.usergroup.create.false',
3200 3200 'hg.create.write_on_repogroup.true',
3201 3201 'hg.fork.repository',
3202 3202 'hg.register.manual_activate',
3203 3203 'hg.password_reset.enabled',
3204 3204 'hg.extern_activate.auto',
3205 3205 'hg.inherit_default_perms.true',
3206 3206 ]
3207 3207
3208 3208 # defines which permissions are more important higher the more important
3209 3209 # Weight defines which permissions are more important.
3210 3210 # The higher number the more important.
3211 3211 PERM_WEIGHTS = {
3212 3212 'repository.none': 0,
3213 3213 'repository.read': 1,
3214 3214 'repository.write': 3,
3215 3215 'repository.admin': 4,
3216 3216
3217 3217 'group.none': 0,
3218 3218 'group.read': 1,
3219 3219 'group.write': 3,
3220 3220 'group.admin': 4,
3221 3221
3222 3222 'usergroup.none': 0,
3223 3223 'usergroup.read': 1,
3224 3224 'usergroup.write': 3,
3225 3225 'usergroup.admin': 4,
3226 3226
3227 3227 'branch.none': 0,
3228 3228 'branch.merge': 1,
3229 3229 'branch.push': 3,
3230 3230 'branch.push_force': 4,
3231 3231
3232 3232 'hg.repogroup.create.false': 0,
3233 3233 'hg.repogroup.create.true': 1,
3234 3234
3235 3235 'hg.usergroup.create.false': 0,
3236 3236 'hg.usergroup.create.true': 1,
3237 3237
3238 3238 'hg.fork.none': 0,
3239 3239 'hg.fork.repository': 1,
3240 3240 'hg.create.none': 0,
3241 3241 'hg.create.repository': 1
3242 3242 }
3243 3243
3244 3244 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3245 3245 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
3246 3246 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
3247 3247
3248 3248 def __repr__(self):
3249 3249 return "<%s('%s:%s')>" % (
3250 3250 self.cls_name, self.permission_id, self.permission_name
3251 3251 )
3252 3252
3253 3253 @classmethod
3254 3254 def get_by_key(cls, key):
3255 3255 return cls.query().filter(cls.permission_name == key).scalar()
3256 3256
3257 3257 @classmethod
3258 3258 def get_default_repo_perms(cls, user_id, repo_id=None):
3259 3259 q = Session().query(UserRepoToPerm, Repository, Permission)\
3260 3260 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
3261 3261 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
3262 3262 .filter(UserRepoToPerm.user_id == user_id)
3263 3263 if repo_id:
3264 3264 q = q.filter(UserRepoToPerm.repository_id == repo_id)
3265 3265 return q.all()
3266 3266
3267 3267 @classmethod
3268 3268 def get_default_repo_branch_perms(cls, user_id, repo_id=None):
3269 3269 q = Session().query(UserToRepoBranchPermission, UserRepoToPerm, Permission) \
3270 3270 .join(
3271 3271 Permission,
3272 3272 UserToRepoBranchPermission.permission_id == Permission.permission_id) \
3273 3273 .join(
3274 3274 UserRepoToPerm,
3275 3275 UserToRepoBranchPermission.rule_to_perm_id == UserRepoToPerm.repo_to_perm_id) \
3276 3276 .filter(UserRepoToPerm.user_id == user_id)
3277 3277
3278 3278 if repo_id:
3279 3279 q = q.filter(UserToRepoBranchPermission.repository_id == repo_id)
3280 3280 return q.order_by(UserToRepoBranchPermission.rule_order).all()
3281 3281
3282 3282 @classmethod
3283 3283 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
3284 3284 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
3285 3285 .join(
3286 3286 Permission,
3287 3287 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
3288 3288 .join(
3289 3289 Repository,
3290 3290 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
3291 3291 .join(
3292 3292 UserGroup,
3293 3293 UserGroupRepoToPerm.users_group_id ==
3294 3294 UserGroup.users_group_id)\
3295 3295 .join(
3296 3296 UserGroupMember,
3297 3297 UserGroupRepoToPerm.users_group_id ==
3298 3298 UserGroupMember.users_group_id)\
3299 3299 .filter(
3300 3300 UserGroupMember.user_id == user_id,
3301 3301 UserGroup.users_group_active == true())
3302 3302 if repo_id:
3303 3303 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
3304 3304 return q.all()
3305 3305
3306 3306 @classmethod
3307 3307 def get_default_repo_branch_perms_from_user_group(cls, user_id, repo_id=None):
3308 3308 q = Session().query(UserGroupToRepoBranchPermission, UserGroupRepoToPerm, Permission) \
3309 3309 .join(
3310 3310 Permission,
3311 3311 UserGroupToRepoBranchPermission.permission_id == Permission.permission_id) \
3312 3312 .join(
3313 3313 UserGroupRepoToPerm,
3314 3314 UserGroupToRepoBranchPermission.rule_to_perm_id == UserGroupRepoToPerm.users_group_to_perm_id) \
3315 3315 .join(
3316 3316 UserGroup,
3317 3317 UserGroupRepoToPerm.users_group_id == UserGroup.users_group_id) \
3318 3318 .join(
3319 3319 UserGroupMember,
3320 3320 UserGroupRepoToPerm.users_group_id == UserGroupMember.users_group_id) \
3321 3321 .filter(
3322 3322 UserGroupMember.user_id == user_id,
3323 3323 UserGroup.users_group_active == true())
3324 3324
3325 3325 if repo_id:
3326 3326 q = q.filter(UserGroupToRepoBranchPermission.repository_id == repo_id)
3327 3327 return q.order_by(UserGroupToRepoBranchPermission.rule_order).all()
3328 3328
3329 3329 @classmethod
3330 3330 def get_default_group_perms(cls, user_id, repo_group_id=None):
3331 3331 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
3332 3332 .join(
3333 3333 Permission,
3334 3334 UserRepoGroupToPerm.permission_id == Permission.permission_id)\
3335 3335 .join(
3336 3336 RepoGroup,
3337 3337 UserRepoGroupToPerm.group_id == RepoGroup.group_id)\
3338 3338 .filter(UserRepoGroupToPerm.user_id == user_id)
3339 3339 if repo_group_id:
3340 3340 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
3341 3341 return q.all()
3342 3342
3343 3343 @classmethod
3344 3344 def get_default_group_perms_from_user_group(
3345 3345 cls, user_id, repo_group_id=None):
3346 3346 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
3347 3347 .join(
3348 3348 Permission,
3349 3349 UserGroupRepoGroupToPerm.permission_id ==
3350 3350 Permission.permission_id)\
3351 3351 .join(
3352 3352 RepoGroup,
3353 3353 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
3354 3354 .join(
3355 3355 UserGroup,
3356 3356 UserGroupRepoGroupToPerm.users_group_id ==
3357 3357 UserGroup.users_group_id)\
3358 3358 .join(
3359 3359 UserGroupMember,
3360 3360 UserGroupRepoGroupToPerm.users_group_id ==
3361 3361 UserGroupMember.users_group_id)\
3362 3362 .filter(
3363 3363 UserGroupMember.user_id == user_id,
3364 3364 UserGroup.users_group_active == true())
3365 3365 if repo_group_id:
3366 3366 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
3367 3367 return q.all()
3368 3368
3369 3369 @classmethod
3370 3370 def get_default_user_group_perms(cls, user_id, user_group_id=None):
3371 3371 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
3372 3372 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
3373 3373 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
3374 3374 .filter(UserUserGroupToPerm.user_id == user_id)
3375 3375 if user_group_id:
3376 3376 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
3377 3377 return q.all()
3378 3378
3379 3379 @classmethod
3380 3380 def get_default_user_group_perms_from_user_group(
3381 3381 cls, user_id, user_group_id=None):
3382 3382 TargetUserGroup = aliased(UserGroup, name='target_user_group')
3383 3383 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
3384 3384 .join(
3385 3385 Permission,
3386 3386 UserGroupUserGroupToPerm.permission_id ==
3387 3387 Permission.permission_id)\
3388 3388 .join(
3389 3389 TargetUserGroup,
3390 3390 UserGroupUserGroupToPerm.target_user_group_id ==
3391 3391 TargetUserGroup.users_group_id)\
3392 3392 .join(
3393 3393 UserGroup,
3394 3394 UserGroupUserGroupToPerm.user_group_id ==
3395 3395 UserGroup.users_group_id)\
3396 3396 .join(
3397 3397 UserGroupMember,
3398 3398 UserGroupUserGroupToPerm.user_group_id ==
3399 3399 UserGroupMember.users_group_id)\
3400 3400 .filter(
3401 3401 UserGroupMember.user_id == user_id,
3402 3402 UserGroup.users_group_active == true())
3403 3403 if user_group_id:
3404 3404 q = q.filter(
3405 3405 UserGroupUserGroupToPerm.user_group_id == user_group_id)
3406 3406
3407 3407 return q.all()
3408 3408
3409 3409
3410 3410 class UserRepoToPerm(Base, BaseModel):
3411 3411 __tablename__ = 'repo_to_perm'
3412 3412 __table_args__ = (
3413 3413 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
3414 3414 base_table_args
3415 3415 )
3416 3416
3417 3417 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3418 3418 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3419 3419 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3420 3420 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
3421 3421
3422 3422 user = relationship('User', back_populates="repo_to_perm")
3423 3423 repository = relationship('Repository', back_populates="repo_to_perm")
3424 3424 permission = relationship('Permission')
3425 3425
3426 3426 branch_perm_entry = relationship('UserToRepoBranchPermission', cascade="all, delete-orphan", lazy='joined', back_populates='user_repo_to_perm')
3427 3427
3428 3428 @classmethod
3429 3429 def create(cls, user, repository, permission):
3430 3430 n = cls()
3431 3431 n.user = user
3432 3432 n.repository = repository
3433 3433 n.permission = permission
3434 3434 Session().add(n)
3435 3435 return n
3436 3436
3437 3437 def __repr__(self):
3438 3438 return f'<{self.user} => {self.repository} >'
3439 3439
3440 3440
3441 3441 class UserUserGroupToPerm(Base, BaseModel):
3442 3442 __tablename__ = 'user_user_group_to_perm'
3443 3443 __table_args__ = (
3444 3444 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
3445 3445 base_table_args
3446 3446 )
3447 3447
3448 3448 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3449 3449 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3450 3450 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3451 3451 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3452 3452
3453 3453 user = relationship('User', back_populates='user_group_to_perm')
3454 3454 user_group = relationship('UserGroup', back_populates='user_user_group_to_perm')
3455 3455 permission = relationship('Permission')
3456 3456
3457 3457 @classmethod
3458 3458 def create(cls, user, user_group, permission):
3459 3459 n = cls()
3460 3460 n.user = user
3461 3461 n.user_group = user_group
3462 3462 n.permission = permission
3463 3463 Session().add(n)
3464 3464 return n
3465 3465
3466 3466 def __repr__(self):
3467 3467 return f'<{self.user} => {self.user_group} >'
3468 3468
3469 3469
3470 3470 class UserToPerm(Base, BaseModel):
3471 3471 __tablename__ = 'user_to_perm'
3472 3472 __table_args__ = (
3473 3473 UniqueConstraint('user_id', 'permission_id'),
3474 3474 base_table_args
3475 3475 )
3476 3476
3477 3477 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3478 3478 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3479 3479 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3480 3480
3481 3481 user = relationship('User', back_populates='user_perms')
3482 3482 permission = relationship('Permission', lazy='joined')
3483 3483
3484 3484 def __repr__(self):
3485 3485 return f'<{self.user} => {self.permission} >'
3486 3486
3487 3487
3488 3488 class UserGroupRepoToPerm(Base, BaseModel):
3489 3489 __tablename__ = 'users_group_repo_to_perm'
3490 3490 __table_args__ = (
3491 3491 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
3492 3492 base_table_args
3493 3493 )
3494 3494
3495 3495 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3496 3496 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3497 3497 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3498 3498 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
3499 3499
3500 3500 users_group = relationship('UserGroup', back_populates='users_group_repo_to_perm')
3501 3501 permission = relationship('Permission')
3502 3502 repository = relationship('Repository', back_populates='users_group_to_perm')
3503 3503 user_group_branch_perms = relationship('UserGroupToRepoBranchPermission', cascade='all', back_populates='user_group_repo_to_perm')
3504 3504
3505 3505 @classmethod
3506 3506 def create(cls, users_group, repository, permission):
3507 3507 n = cls()
3508 3508 n.users_group = users_group
3509 3509 n.repository = repository
3510 3510 n.permission = permission
3511 3511 Session().add(n)
3512 3512 return n
3513 3513
3514 3514 def __repr__(self):
3515 3515 return f'<UserGroupRepoToPerm:{self.users_group} => {self.repository} >'
3516 3516
3517 3517
3518 3518 class UserGroupUserGroupToPerm(Base, BaseModel):
3519 3519 __tablename__ = 'user_group_user_group_to_perm'
3520 3520 __table_args__ = (
3521 3521 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
3522 3522 CheckConstraint('target_user_group_id != user_group_id'),
3523 3523 base_table_args
3524 3524 )
3525 3525
3526 3526 user_group_user_group_to_perm_id = Column("user_group_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3527 3527 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3528 3528 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3529 3529 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3530 3530
3531 3531 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id', back_populates='user_group_user_group_to_perm')
3532 3532 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
3533 3533 permission = relationship('Permission')
3534 3534
3535 3535 @classmethod
3536 3536 def create(cls, target_user_group, user_group, permission):
3537 3537 n = cls()
3538 3538 n.target_user_group = target_user_group
3539 3539 n.user_group = user_group
3540 3540 n.permission = permission
3541 3541 Session().add(n)
3542 3542 return n
3543 3543
3544 3544 def __repr__(self):
3545 3545 return f'<UserGroupUserGroup:{self.target_user_group} => {self.user_group} >'
3546 3546
3547 3547
3548 3548 class UserGroupToPerm(Base, BaseModel):
3549 3549 __tablename__ = 'users_group_to_perm'
3550 3550 __table_args__ = (
3551 3551 UniqueConstraint('users_group_id', 'permission_id',),
3552 3552 base_table_args
3553 3553 )
3554 3554
3555 3555 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3556 3556 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3557 3557 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3558 3558
3559 3559 users_group = relationship('UserGroup', back_populates='users_group_to_perm')
3560 3560 permission = relationship('Permission')
3561 3561
3562 3562
3563 3563 class UserRepoGroupToPerm(Base, BaseModel):
3564 3564 __tablename__ = 'user_repo_group_to_perm'
3565 3565 __table_args__ = (
3566 3566 UniqueConstraint('user_id', 'group_id', 'permission_id'),
3567 3567 base_table_args
3568 3568 )
3569 3569
3570 3570 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3571 3571 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3572 3572 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
3573 3573 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3574 3574
3575 3575 user = relationship('User', back_populates='repo_group_to_perm')
3576 3576 group = relationship('RepoGroup', back_populates='repo_group_to_perm')
3577 3577 permission = relationship('Permission')
3578 3578
3579 3579 @classmethod
3580 3580 def create(cls, user, repository_group, permission):
3581 3581 n = cls()
3582 3582 n.user = user
3583 3583 n.group = repository_group
3584 3584 n.permission = permission
3585 3585 Session().add(n)
3586 3586 return n
3587 3587
3588 3588
3589 3589 class UserGroupRepoGroupToPerm(Base, BaseModel):
3590 3590 __tablename__ = 'users_group_repo_group_to_perm'
3591 3591 __table_args__ = (
3592 3592 UniqueConstraint('users_group_id', 'group_id'),
3593 3593 base_table_args
3594 3594 )
3595 3595
3596 3596 users_group_repo_group_to_perm_id = Column("users_group_repo_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3597 3597 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3598 3598 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
3599 3599 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3600 3600
3601 3601 users_group = relationship('UserGroup', back_populates='users_group_repo_group_to_perm')
3602 3602 permission = relationship('Permission')
3603 3603 group = relationship('RepoGroup', back_populates='users_group_to_perm')
3604 3604
3605 3605 @classmethod
3606 3606 def create(cls, user_group, repository_group, permission):
3607 3607 n = cls()
3608 3608 n.users_group = user_group
3609 3609 n.group = repository_group
3610 3610 n.permission = permission
3611 3611 Session().add(n)
3612 3612 return n
3613 3613
3614 3614 def __repr__(self):
3615 3615 return '<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
3616 3616
3617 3617
3618 3618 class Statistics(Base, BaseModel):
3619 3619 __tablename__ = 'statistics'
3620 3620 __table_args__ = (
3621 3621 base_table_args
3622 3622 )
3623 3623
3624 3624 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3625 3625 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
3626 3626 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
3627 3627 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False) #JSON data
3628 3628 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False) #JSON data
3629 3629 languages = Column("languages", LargeBinary(1000000), nullable=False) #JSON data
3630 3630
3631 3631 repository = relationship('Repository', single_parent=True, viewonly=True)
3632 3632
3633 3633
3634 3634 class UserFollowing(Base, BaseModel):
3635 3635 __tablename__ = 'user_followings'
3636 3636 __table_args__ = (
3637 3637 UniqueConstraint('user_id', 'follows_repository_id'),
3638 3638 UniqueConstraint('user_id', 'follows_user_id'),
3639 3639 base_table_args
3640 3640 )
3641 3641
3642 3642 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3643 3643 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3644 3644 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
3645 3645 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
3646 3646 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
3647 3647
3648 3648 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id', back_populates='followings')
3649 3649
3650 3650 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
3651 3651 follows_repository = relationship('Repository', order_by='Repository.repo_name', back_populates='followers')
3652 3652
3653 3653 @classmethod
3654 3654 def get_repo_followers(cls, repo_id):
3655 3655 return cls.query().filter(cls.follows_repo_id == repo_id)
3656 3656
3657 3657
3658 3658 class CacheKey(Base, BaseModel):
3659 3659 __tablename__ = 'cache_invalidation'
3660 3660 __table_args__ = (
3661 3661 UniqueConstraint('cache_key'),
3662 3662 Index('key_idx', 'cache_key'),
3663 3663 Index('cache_args_idx', 'cache_args'),
3664 3664 base_table_args,
3665 3665 )
3666 3666
3667 3667 CACHE_TYPE_FEED = 'FEED'
3668 3668
3669 3669 # namespaces used to register process/thread aware caches
3670 3670 REPO_INVALIDATION_NAMESPACE = 'repo_cache.v1:{repo_id}'
3671 3671
3672 3672 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3673 3673 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
3674 3674 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
3675 3675 cache_state_uid = Column("cache_state_uid", String(255), nullable=True, unique=None, default=None)
3676 3676 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
3677 3677
3678 3678 def __init__(self, cache_key, cache_args='', cache_state_uid=None):
3679 3679 self.cache_key = cache_key
3680 3680 self.cache_args = cache_args
3681 3681 self.cache_active = False
3682 3682 # first key should be same for all entries, since all workers should share it
3683 3683 self.cache_state_uid = cache_state_uid or self.generate_new_state_uid()
3684 3684
3685 3685 def __repr__(self):
3686 3686 return "<%s('%s:%s[%s]')>" % (
3687 3687 self.cls_name,
3688 3688 self.cache_id, self.cache_key, self.cache_active)
3689 3689
3690 3690 def _cache_key_partition(self):
3691 3691 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
3692 3692 return prefix, repo_name, suffix
3693 3693
3694 3694 def get_prefix(self):
3695 3695 """
3696 3696 Try to extract prefix from existing cache key. The key could consist
3697 3697 of prefix, repo_name, suffix
3698 3698 """
3699 3699 # this returns prefix, repo_name, suffix
3700 3700 return self._cache_key_partition()[0]
3701 3701
3702 3702 def get_suffix(self):
3703 3703 """
3704 3704 get suffix that might have been used in _get_cache_key to
3705 3705 generate self.cache_key. Only used for informational purposes
3706 3706 in repo_edit.mako.
3707 3707 """
3708 3708 # prefix, repo_name, suffix
3709 3709 return self._cache_key_partition()[2]
3710 3710
3711 3711 @classmethod
3712 3712 def generate_new_state_uid(cls, based_on=None):
3713 3713 if based_on:
3714 3714 return str(uuid.uuid5(uuid.NAMESPACE_URL, safe_str(based_on)))
3715 3715 else:
3716 3716 return str(uuid.uuid4())
3717 3717
3718 3718 @classmethod
3719 3719 def delete_all_cache(cls):
3720 3720 """
3721 3721 Delete all cache keys from database.
3722 3722 Should only be run when all instances are down and all entries
3723 3723 thus stale.
3724 3724 """
3725 3725 cls.query().delete()
3726 3726 Session().commit()
3727 3727
3728 3728 @classmethod
3729 3729 def set_invalidate(cls, cache_uid, delete=False):
3730 3730 """
3731 3731 Mark all caches of a repo as invalid in the database.
3732 3732 """
3733 3733
3734 3734 try:
3735 3735 qry = Session().query(cls).filter(cls.cache_args == cache_uid)
3736 3736 if delete:
3737 3737 qry.delete()
3738 3738 log.debug('cache objects deleted for cache args %s',
3739 3739 safe_str(cache_uid))
3740 3740 else:
3741 3741 qry.update({"cache_active": False,
3742 3742 "cache_state_uid": cls.generate_new_state_uid()})
3743 3743 log.debug('cache objects marked as invalid for cache args %s',
3744 3744 safe_str(cache_uid))
3745 3745
3746 3746 Session().commit()
3747 3747 except Exception:
3748 3748 log.exception(
3749 3749 'Cache key invalidation failed for cache args %s',
3750 3750 safe_str(cache_uid))
3751 3751 Session().rollback()
3752 3752
3753 3753 @classmethod
3754 3754 def get_active_cache(cls, cache_key):
3755 3755 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
3756 3756 if inv_obj:
3757 3757 return inv_obj
3758 3758 return None
3759 3759
3760 3760 @classmethod
3761 3761 def get_namespace_map(cls, namespace):
3762 3762 return {
3763 3763 x.cache_key: x
3764 3764 for x in cls.query().filter(cls.cache_args == namespace)}
3765 3765
3766 3766
3767 3767 class ChangesetComment(Base, BaseModel):
3768 3768 __tablename__ = 'changeset_comments'
3769 3769 __table_args__ = (
3770 3770 Index('cc_revision_idx', 'revision'),
3771 3771 base_table_args,
3772 3772 )
3773 3773
3774 3774 COMMENT_OUTDATED = 'comment_outdated'
3775 3775 COMMENT_TYPE_NOTE = 'note'
3776 3776 COMMENT_TYPE_TODO = 'todo'
3777 3777 COMMENT_TYPES = [COMMENT_TYPE_NOTE, COMMENT_TYPE_TODO]
3778 3778
3779 3779 OP_IMMUTABLE = 'immutable'
3780 3780 OP_CHANGEABLE = 'changeable'
3781 3781
3782 3782 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
3783 3783 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3784 3784 revision = Column('revision', String(40), nullable=True)
3785 3785 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3786 3786 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
3787 3787 line_no = Column('line_no', Unicode(10), nullable=True)
3788 3788 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
3789 3789 f_path = Column('f_path', Unicode(1000), nullable=True)
3790 3790 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
3791 3791 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
3792 3792 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3793 3793 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3794 3794 renderer = Column('renderer', Unicode(64), nullable=True)
3795 3795 display_state = Column('display_state', Unicode(128), nullable=True)
3796 3796 immutable_state = Column('immutable_state', Unicode(128), nullable=True, default=OP_CHANGEABLE)
3797 3797 draft = Column('draft', Boolean(), nullable=True, default=False)
3798 3798
3799 3799 comment_type = Column('comment_type', Unicode(128), nullable=True, default=COMMENT_TYPE_NOTE)
3800 3800 resolved_comment_id = Column('resolved_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=True)
3801 3801
3802 3802 resolved_comment = relationship('ChangesetComment', remote_side=comment_id, back_populates='resolved_by')
3803 3803 resolved_by = relationship('ChangesetComment', back_populates='resolved_comment')
3804 3804
3805 3805 author = relationship('User', lazy='select', back_populates='user_comments')
3806 3806 repo = relationship('Repository', back_populates='comments')
3807 3807 status_change = relationship('ChangesetStatus', cascade="all, delete-orphan", lazy='select', back_populates='comment')
3808 3808 pull_request = relationship('PullRequest', lazy='select', back_populates='comments')
3809 3809 pull_request_version = relationship('PullRequestVersion', lazy='select')
3810 3810 history = relationship('ChangesetCommentHistory', cascade='all, delete-orphan', lazy='select', order_by='ChangesetCommentHistory.version', back_populates="comment")
3811 3811
3812 3812 @classmethod
3813 3813 def get_users(cls, revision=None, pull_request_id=None):
3814 3814 """
3815 3815 Returns user associated with this ChangesetComment. ie those
3816 3816 who actually commented
3817 3817
3818 3818 :param cls:
3819 3819 :param revision:
3820 3820 """
3821 3821 q = Session().query(User).join(ChangesetComment.author)
3822 3822 if revision:
3823 3823 q = q.filter(cls.revision == revision)
3824 3824 elif pull_request_id:
3825 3825 q = q.filter(cls.pull_request_id == pull_request_id)
3826 3826 return q.all()
3827 3827
3828 3828 @classmethod
3829 3829 def get_index_from_version(cls, pr_version, versions=None, num_versions=None) -> int:
3830 3830 if pr_version is None:
3831 3831 return 0
3832 3832
3833 3833 if versions is not None:
3834 3834 num_versions = [x.pull_request_version_id for x in versions]
3835 3835
3836 3836 num_versions = num_versions or []
3837 3837 try:
3838 3838 return num_versions.index(pr_version) + 1
3839 3839 except (IndexError, ValueError):
3840 3840 return 0
3841 3841
3842 3842 @property
3843 3843 def outdated(self):
3844 3844 return self.display_state == self.COMMENT_OUTDATED
3845 3845
3846 3846 @property
3847 3847 def outdated_js(self):
3848 return json.dumps(self.display_state == self.COMMENT_OUTDATED)
3848 return str_json(self.display_state == self.COMMENT_OUTDATED)
3849 3849
3850 3850 @property
3851 3851 def immutable(self):
3852 3852 return self.immutable_state == self.OP_IMMUTABLE
3853 3853
3854 3854 def outdated_at_version(self, version):
3855 3855 """
3856 3856 Checks if comment is outdated for given pull request version
3857 3857 """
3858
3859 # If version is None, return False as the current version cannot be less than None
3860 if version is None:
3861 return False
3862
3863 # Ensure that the version is an integer to prevent TypeError on comparison
3864 if not isinstance(version, int):
3865 raise ValueError("The provided version must be an integer.")
3866
3858 3867 def version_check():
3859 3868 return self.pull_request_version_id and self.pull_request_version_id != version
3860 3869
3861 3870 if self.is_inline:
3862 3871 return self.outdated and version_check()
3863 3872 else:
3864 3873 # general comments don't have .outdated set, also latest don't have a version
3865 3874 return version_check()
3866 3875
3867 3876 def outdated_at_version_js(self, version):
3868 3877 """
3869 3878 Checks if comment is outdated for given pull request version
3870 3879 """
3871 return json.dumps(self.outdated_at_version(version))
3872
3873 def older_than_version(self, version):
3880 return str_json(self.outdated_at_version(version))
3881
3882 def older_than_version(self, version: int) -> bool:
3883 """
3884 Checks if comment is made from a previous version than given.
3885 Assumes self.pull_request_version.pull_request_version_id is an integer if not None.
3874 3886 """
3875 Checks if comment is made from previous version than given
3876 """
3887
3888 # If version is None, return False as the current version cannot be less than None
3889 if version is None:
3890 return False
3891
3892 # Ensure that the version is an integer to prevent TypeError on comparison
3893 if not isinstance(version, int):
3894 raise ValueError("The provided version must be an integer.")
3895
3896 # Initialize current version to 0 or pull_request_version_id if it's available
3877 3897 cur_ver = 0
3878 if self.pull_request_version:
3879 cur_ver = self.pull_request_version.pull_request_version_id or cur_ver
3880
3881 if version is None:
3882 return cur_ver != version
3883
3898 if self.pull_request_version and self.pull_request_version.pull_request_version_id is not None:
3899 cur_ver = self.pull_request_version.pull_request_version_id
3900
3901 # Return True if the current version is less than the given version
3884 3902 return cur_ver < version
3885 3903
3886 3904 def older_than_version_js(self, version):
3887 3905 """
3888 3906 Checks if comment is made from previous version than given
3889 3907 """
3890 return json.dumps(self.older_than_version(version))
3908 return str_json(self.older_than_version(version))
3891 3909
3892 3910 @property
3893 3911 def commit_id(self):
3894 3912 """New style naming to stop using .revision"""
3895 3913 return self.revision
3896 3914
3897 3915 @property
3898 3916 def resolved(self):
3899 3917 return self.resolved_by[0] if self.resolved_by else None
3900 3918
3901 3919 @property
3902 3920 def is_todo(self):
3903 3921 return self.comment_type == self.COMMENT_TYPE_TODO
3904 3922
3905 3923 @property
3906 3924 def is_inline(self):
3907 3925 if self.line_no and self.f_path:
3908 3926 return True
3909 3927 return False
3910 3928
3911 3929 @property
3912 3930 def last_version(self):
3913 3931 version = 0
3914 3932 if self.history:
3915 3933 version = self.history[-1].version
3916 3934 return version
3917 3935
3918 3936 def get_index_version(self, versions):
3919 3937 return self.get_index_from_version(
3920 3938 self.pull_request_version_id, versions)
3921 3939
3922 3940 @property
3923 3941 def review_status(self):
3924 3942 if self.status_change:
3925 3943 return self.status_change[0].status
3926 3944
3927 3945 @property
3928 3946 def review_status_lbl(self):
3929 3947 if self.status_change:
3930 3948 return self.status_change[0].status_lbl
3931 3949
3932 3950 def __repr__(self):
3933 3951 if self.comment_id:
3934 3952 return f'<DB:Comment #{self.comment_id}>'
3935 3953 else:
3936 3954 return f'<DB:Comment at {id(self)!r}>'
3937 3955
3938 3956 def get_api_data(self):
3939 3957 comment = self
3940 3958
3941 3959 data = {
3942 3960 'comment_id': comment.comment_id,
3943 3961 'comment_type': comment.comment_type,
3944 3962 'comment_text': comment.text,
3945 3963 'comment_status': comment.status_change,
3946 3964 'comment_f_path': comment.f_path,
3947 3965 'comment_lineno': comment.line_no,
3948 3966 'comment_author': comment.author,
3949 3967 'comment_created_on': comment.created_on,
3950 3968 'comment_resolved_by': self.resolved,
3951 3969 'comment_commit_id': comment.revision,
3952 3970 'comment_pull_request_id': comment.pull_request_id,
3953 3971 'comment_last_version': self.last_version
3954 3972 }
3955 3973 return data
3956 3974
3957 3975 def __json__(self):
3958 3976 data = dict()
3959 3977 data.update(self.get_api_data())
3960 3978 return data
3961 3979
3962 3980
3963 3981 class ChangesetCommentHistory(Base, BaseModel):
3964 3982 __tablename__ = 'changeset_comments_history'
3965 3983 __table_args__ = (
3966 3984 Index('cch_comment_id_idx', 'comment_id'),
3967 3985 base_table_args,
3968 3986 )
3969 3987
3970 3988 comment_history_id = Column('comment_history_id', Integer(), nullable=False, primary_key=True)
3971 3989 comment_id = Column('comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=False)
3972 3990 version = Column("version", Integer(), nullable=False, default=0)
3973 3991 created_by_user_id = Column('created_by_user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
3974 3992 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
3975 3993 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3976 3994 deleted = Column('deleted', Boolean(), default=False)
3977 3995
3978 3996 author = relationship('User', lazy='joined')
3979 3997 comment = relationship('ChangesetComment', cascade="all, delete", back_populates="history")
3980 3998
3981 3999 @classmethod
3982 4000 def get_version(cls, comment_id):
3983 4001 q = Session().query(ChangesetCommentHistory).filter(
3984 4002 ChangesetCommentHistory.comment_id == comment_id).order_by(ChangesetCommentHistory.version.desc())
3985 4003 if q.count() == 0:
3986 4004 return 1
3987 4005 elif q.count() >= q[0].version:
3988 4006 return q.count() + 1
3989 4007 else:
3990 4008 return q[0].version + 1
3991 4009
3992 4010
3993 4011 class ChangesetStatus(Base, BaseModel):
3994 4012 __tablename__ = 'changeset_statuses'
3995 4013 __table_args__ = (
3996 4014 Index('cs_revision_idx', 'revision'),
3997 4015 Index('cs_version_idx', 'version'),
3998 4016 UniqueConstraint('repo_id', 'revision', 'version'),
3999 4017 base_table_args
4000 4018 )
4001 4019
4002 4020 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
4003 4021 STATUS_APPROVED = 'approved'
4004 4022 STATUS_REJECTED = 'rejected'
4005 4023 STATUS_UNDER_REVIEW = 'under_review'
4006 4024
4007 4025 STATUSES = [
4008 4026 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
4009 4027 (STATUS_APPROVED, _("Approved")),
4010 4028 (STATUS_REJECTED, _("Rejected")),
4011 4029 (STATUS_UNDER_REVIEW, _("Under Review")),
4012 4030 ]
4013 4031
4014 4032 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
4015 4033 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
4016 4034 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
4017 4035 revision = Column('revision', String(40), nullable=False)
4018 4036 status = Column('status', String(128), nullable=False, default=DEFAULT)
4019 4037 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
4020 4038 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
4021 4039 version = Column('version', Integer(), nullable=False, default=0)
4022 4040 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
4023 4041
4024 4042 author = relationship('User', lazy='select')
4025 4043 repo = relationship('Repository', lazy='select')
4026 4044 comment = relationship('ChangesetComment', lazy='select', back_populates='status_change')
4027 4045 pull_request = relationship('PullRequest', lazy='select', back_populates='statuses')
4028 4046
4029 4047 def __repr__(self):
4030 4048 return f"<{self.cls_name}('{self.status}[v{self.version}]:{self.author}')>"
4031 4049
4032 4050 @classmethod
4033 4051 def get_status_lbl(cls, value):
4034 4052 return dict(cls.STATUSES).get(value)
4035 4053
4036 4054 @property
4037 4055 def status_lbl(self):
4038 4056 return ChangesetStatus.get_status_lbl(self.status)
4039 4057
4040 4058 def get_api_data(self):
4041 4059 status = self
4042 4060 data = {
4043 4061 'status_id': status.changeset_status_id,
4044 4062 'status': status.status,
4045 4063 }
4046 4064 return data
4047 4065
4048 4066 def __json__(self):
4049 4067 data = dict()
4050 4068 data.update(self.get_api_data())
4051 4069 return data
4052 4070
4053 4071
4054 4072 class _SetState(object):
4055 4073 """
4056 4074 Context processor allowing changing state for sensitive operation such as
4057 4075 pull request update or merge
4058 4076 """
4059 4077
4060 4078 def __init__(self, pull_request, pr_state, back_state=None):
4061 4079 self._pr = pull_request
4062 4080 self._org_state = back_state or pull_request.pull_request_state
4063 4081 self._pr_state = pr_state
4064 4082 self._current_state = None
4065 4083
4066 4084 def __enter__(self):
4067 4085 log.debug('StateLock: entering set state context of pr %s, setting state to: `%s`',
4068 4086 self._pr, self._pr_state)
4069 4087 self.set_pr_state(self._pr_state)
4070 4088 return self
4071 4089
4072 4090 def __exit__(self, exc_type, exc_val, exc_tb):
4073 4091 if exc_val is not None or exc_type is not None:
4074 4092 log.error(traceback.format_tb(exc_tb))
4075 4093 return None
4076 4094
4077 4095 self.set_pr_state(self._org_state)
4078 4096 log.debug('StateLock: exiting set state context of pr %s, setting state to: `%s`',
4079 4097 self._pr, self._org_state)
4080 4098
4081 4099 @property
4082 4100 def state(self):
4083 4101 return self._current_state
4084 4102
4085 4103 def set_pr_state(self, pr_state):
4086 4104 try:
4087 4105 self._pr.pull_request_state = pr_state
4088 4106 Session().add(self._pr)
4089 4107 Session().commit()
4090 4108 self._current_state = pr_state
4091 4109 except Exception:
4092 4110 log.exception('Failed to set PullRequest %s state to %s', self._pr, pr_state)
4093 4111 raise
4094 4112
4095 4113
4096 4114 class _PullRequestBase(BaseModel):
4097 4115 """
4098 4116 Common attributes of pull request and version entries.
4099 4117 """
4100 4118
4101 4119 # .status values
4102 4120 STATUS_NEW = 'new'
4103 4121 STATUS_OPEN = 'open'
4104 4122 STATUS_CLOSED = 'closed'
4105 4123
4106 4124 # available states
4107 4125 STATE_CREATING = 'creating'
4108 4126 STATE_UPDATING = 'updating'
4109 4127 STATE_MERGING = 'merging'
4110 4128 STATE_CREATED = 'created'
4111 4129
4112 4130 title = Column('title', Unicode(255), nullable=True)
4113 4131 description = Column(
4114 4132 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
4115 4133 nullable=True)
4116 4134 description_renderer = Column('description_renderer', Unicode(64), nullable=True)
4117 4135
4118 4136 # new/open/closed status of pull request (not approve/reject/etc)
4119 4137 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
4120 4138 created_on = Column(
4121 4139 'created_on', DateTime(timezone=False), nullable=False,
4122 4140 default=datetime.datetime.now)
4123 4141 updated_on = Column(
4124 4142 'updated_on', DateTime(timezone=False), nullable=False,
4125 4143 default=datetime.datetime.now)
4126 4144
4127 4145 pull_request_state = Column("pull_request_state", String(255), nullable=True)
4128 4146
4129 4147 @declared_attr
4130 4148 def user_id(cls):
4131 4149 return Column(
4132 4150 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
4133 4151 unique=None)
4134 4152
4135 4153 # 500 revisions max
4136 4154 _revisions = Column(
4137 4155 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
4138 4156
4139 4157 common_ancestor_id = Column('common_ancestor_id', Unicode(255), nullable=True)
4140 4158
4141 4159 @declared_attr
4142 4160 def source_repo_id(cls):
4143 4161 # TODO: dan: rename column to source_repo_id
4144 4162 return Column(
4145 4163 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
4146 4164 nullable=False)
4147 4165
4148 4166 @declared_attr
4149 4167 def pr_source(cls):
4150 4168 return relationship(
4151 4169 'Repository',
4152 4170 primaryjoin=f'{cls.__name__}.source_repo_id==Repository.repo_id',
4153 4171 overlaps="pull_requests_source"
4154 4172 )
4155 4173
4156 4174 _source_ref = Column('org_ref', Unicode(255), nullable=False)
4157 4175
4158 4176 @hybrid_property
4159 4177 def source_ref(self):
4160 4178 return self._source_ref
4161 4179
4162 4180 @source_ref.setter
4163 4181 def source_ref(self, val):
4164 4182 parts = (val or '').split(':')
4165 4183 if len(parts) != 3:
4166 4184 raise ValueError(
4167 4185 'Invalid reference format given: {}, expected X:Y:Z'.format(val))
4168 4186 self._source_ref = safe_str(val)
4169 4187
4170 4188 _target_ref = Column('other_ref', Unicode(255), nullable=False)
4171 4189
4172 4190 @hybrid_property
4173 4191 def target_ref(self):
4174 4192 return self._target_ref
4175 4193
4176 4194 @target_ref.setter
4177 4195 def target_ref(self, val):
4178 4196 parts = (val or '').split(':')
4179 4197 if len(parts) != 3:
4180 4198 raise ValueError(
4181 4199 'Invalid reference format given: {}, expected X:Y:Z'.format(val))
4182 4200 self._target_ref = safe_str(val)
4183 4201
4184 4202 @declared_attr
4185 4203 def target_repo_id(cls):
4186 4204 # TODO: dan: rename column to target_repo_id
4187 4205 return Column(
4188 4206 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
4189 4207 nullable=False)
4190 4208
4191 4209 @declared_attr
4192 4210 def pr_target(cls):
4193 4211 return relationship(
4194 4212 'Repository',
4195 4213 primaryjoin=f'{cls.__name__}.target_repo_id==Repository.repo_id',
4196 4214 overlaps="pull_requests_target"
4197 4215 )
4198 4216
4199 4217 _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True)
4200 4218
4201 4219 # TODO: dan: rename column to last_merge_source_rev
4202 4220 _last_merge_source_rev = Column(
4203 4221 'last_merge_org_rev', String(40), nullable=True)
4204 4222 # TODO: dan: rename column to last_merge_target_rev
4205 4223 _last_merge_target_rev = Column(
4206 4224 'last_merge_other_rev', String(40), nullable=True)
4207 4225 _last_merge_status = Column('merge_status', Integer(), nullable=True)
4208 4226 last_merge_metadata = Column(
4209 4227 'last_merge_metadata', MutationObj.as_mutable(
4210 4228 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4211 4229
4212 4230 merge_rev = Column('merge_rev', String(40), nullable=True)
4213 4231
4214 4232 reviewer_data = Column(
4215 4233 'reviewer_data_json', MutationObj.as_mutable(
4216 4234 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4217 4235
4218 4236 @property
4219 4237 def reviewer_data_json(self):
4220 return json.dumps(self.reviewer_data)
4238 return str_json(self.reviewer_data)
4221 4239
4222 4240 @property
4223 4241 def last_merge_metadata_parsed(self):
4224 4242 metadata = {}
4225 4243 if not self.last_merge_metadata:
4226 4244 return metadata
4227 4245
4228 4246 if hasattr(self.last_merge_metadata, 'de_coerce'):
4229 4247 for k, v in self.last_merge_metadata.de_coerce().items():
4230 4248 if k in ['target_ref', 'source_ref']:
4231 4249 metadata[k] = Reference(v['type'], v['name'], v['commit_id'])
4232 4250 else:
4233 4251 if hasattr(v, 'de_coerce'):
4234 4252 metadata[k] = v.de_coerce()
4235 4253 else:
4236 4254 metadata[k] = v
4237 4255 return metadata
4238 4256
4239 4257 @property
4240 4258 def work_in_progress(self):
4241 4259 """checks if pull request is work in progress by checking the title"""
4242 4260 title = self.title.upper()
4243 4261 if re.match(r'^(\[WIP\]\s*|WIP:\s*|WIP\s+)', title):
4244 4262 return True
4245 4263 return False
4246 4264
4247 4265 @property
4248 4266 def title_safe(self):
4249 4267 return self.title\
4250 4268 .replace('{', '{{')\
4251 4269 .replace('}', '}}')
4252 4270
4253 4271 @hybrid_property
4254 4272 def description_safe(self):
4255 4273 from rhodecode.lib import helpers as h
4256 4274 return h.escape(self.description)
4257 4275
4258 4276 @hybrid_property
4259 4277 def revisions(self):
4260 4278 return self._revisions.split(':') if self._revisions else []
4261 4279
4262 4280 @revisions.setter
4263 4281 def revisions(self, val):
4264 4282 self._revisions = ':'.join(val)
4265 4283
4266 4284 @hybrid_property
4267 4285 def last_merge_status(self):
4268 4286 return safe_int(self._last_merge_status)
4269 4287
4270 4288 @last_merge_status.setter
4271 4289 def last_merge_status(self, val):
4272 4290 self._last_merge_status = val
4273 4291
4274 4292 @declared_attr
4275 4293 def author(cls):
4276 4294 return relationship(
4277 4295 'User', lazy='joined',
4278 4296 #TODO, problem that is somehow :?
4279 4297 #back_populates='user_pull_requests'
4280 4298 )
4281 4299
4282 4300 @declared_attr
4283 4301 def source_repo(cls):
4284 4302 return relationship(
4285 4303 'Repository',
4286 4304 primaryjoin=f'{cls.__name__}.source_repo_id==Repository.repo_id',
4287 4305 #back_populates=''
4288 4306 )
4289 4307
4290 4308 @property
4291 4309 def source_ref_parts(self):
4292 4310 return self.unicode_to_reference(self.source_ref)
4293 4311
4294 4312 @declared_attr
4295 4313 def target_repo(cls):
4296 4314 return relationship(
4297 4315 'Repository',
4298 4316 primaryjoin=f'{cls.__name__}.target_repo_id==Repository.repo_id'
4299 4317 )
4300 4318
4301 4319 @property
4302 4320 def target_ref_parts(self):
4303 4321 return self.unicode_to_reference(self.target_ref)
4304 4322
4305 4323 @property
4306 4324 def shadow_merge_ref(self):
4307 4325 return self.unicode_to_reference(self._shadow_merge_ref)
4308 4326
4309 4327 @shadow_merge_ref.setter
4310 4328 def shadow_merge_ref(self, ref):
4311 4329 self._shadow_merge_ref = self.reference_to_unicode(ref)
4312 4330
4313 4331 @staticmethod
4314 4332 def unicode_to_reference(raw):
4315 4333 return unicode_to_reference(raw)
4316 4334
4317 4335 @staticmethod
4318 4336 def reference_to_unicode(ref):
4319 4337 return reference_to_unicode(ref)
4320 4338
4321 4339 def get_api_data(self, with_merge_state=True):
4322 4340 from rhodecode.model.pull_request import PullRequestModel
4323 4341
4324 4342 pull_request = self
4325 4343 if with_merge_state:
4326 4344 merge_response, merge_status, msg = \
4327 4345 PullRequestModel().merge_status(pull_request)
4328 4346 merge_state = {
4329 4347 'status': merge_status,
4330 4348 'message': safe_str(msg),
4331 4349 }
4332 4350 else:
4333 4351 merge_state = {'status': 'not_available',
4334 4352 'message': 'not_available'}
4335 4353
4336 4354 merge_data = {
4337 4355 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
4338 4356 'reference': (
4339 4357 pull_request.shadow_merge_ref.asdict()
4340 4358 if pull_request.shadow_merge_ref else None),
4341 4359 }
4342 4360
4343 4361 data = {
4344 4362 'pull_request_id': pull_request.pull_request_id,
4345 4363 'url': PullRequestModel().get_url(pull_request),
4346 4364 'title': pull_request.title,
4347 4365 'description': pull_request.description,
4348 4366 'status': pull_request.status,
4349 4367 'state': pull_request.pull_request_state,
4350 4368 'created_on': pull_request.created_on,
4351 4369 'updated_on': pull_request.updated_on,
4352 4370 'commit_ids': pull_request.revisions,
4353 4371 'review_status': pull_request.calculated_review_status(),
4354 4372 'mergeable': merge_state,
4355 4373 'source': {
4356 4374 'clone_url': pull_request.source_repo.clone_url(),
4357 4375 'repository': pull_request.source_repo.repo_name,
4358 4376 'reference': {
4359 4377 'name': pull_request.source_ref_parts.name,
4360 4378 'type': pull_request.source_ref_parts.type,
4361 4379 'commit_id': pull_request.source_ref_parts.commit_id,
4362 4380 },
4363 4381 },
4364 4382 'target': {
4365 4383 'clone_url': pull_request.target_repo.clone_url(),
4366 4384 'repository': pull_request.target_repo.repo_name,
4367 4385 'reference': {
4368 4386 'name': pull_request.target_ref_parts.name,
4369 4387 'type': pull_request.target_ref_parts.type,
4370 4388 'commit_id': pull_request.target_ref_parts.commit_id,
4371 4389 },
4372 4390 },
4373 4391 'merge': merge_data,
4374 4392 'author': pull_request.author.get_api_data(include_secrets=False,
4375 4393 details='basic'),
4376 4394 'reviewers': [
4377 4395 {
4378 4396 'user': reviewer.get_api_data(include_secrets=False,
4379 4397 details='basic'),
4380 4398 'reasons': reasons,
4381 4399 'review_status': st[0][1].status if st else 'not_reviewed',
4382 4400 }
4383 4401 for obj, reviewer, reasons, mandatory, st in
4384 4402 pull_request.reviewers_statuses()
4385 4403 ]
4386 4404 }
4387 4405
4388 4406 return data
4389 4407
4390 4408 def set_state(self, pull_request_state, final_state=None):
4391 4409 """
4392 4410 # goes from initial state to updating to initial state.
4393 4411 # initial state can be changed by specifying back_state=
4394 4412 with pull_request_obj.set_state(PullRequest.STATE_UPDATING):
4395 4413 pull_request.merge()
4396 4414
4397 4415 :param pull_request_state:
4398 4416 :param final_state:
4399 4417
4400 4418 """
4401 4419
4402 4420 return _SetState(self, pull_request_state, back_state=final_state)
4403 4421
4404 4422
4405 4423 class PullRequest(Base, _PullRequestBase):
4406 4424 __tablename__ = 'pull_requests'
4407 4425 __table_args__ = (
4408 4426 base_table_args,
4409 4427 )
4410 4428 LATEST_VER = 'latest'
4411 4429
4412 4430 pull_request_id = Column(
4413 4431 'pull_request_id', Integer(), nullable=False, primary_key=True)
4414 4432
4415 4433 def __repr__(self):
4416 4434 if self.pull_request_id:
4417 4435 return f'<DB:PullRequest #{self.pull_request_id}>'
4418 4436 else:
4419 4437 return f'<DB:PullRequest at {id(self)!r}>'
4420 4438
4421 4439 reviewers = relationship('PullRequestReviewers', cascade="all, delete-orphan", back_populates='pull_request')
4422 4440 statuses = relationship('ChangesetStatus', cascade="all, delete-orphan", back_populates='pull_request')
4423 4441 comments = relationship('ChangesetComment', cascade="all, delete-orphan", back_populates='pull_request')
4424 4442 versions = relationship('PullRequestVersion', cascade="all, delete-orphan", lazy='dynamic', back_populates='pull_request')
4425 4443
4426 4444 @classmethod
4427 4445 def get_pr_display_object(cls, pull_request_obj, org_pull_request_obj,
4428 4446 internal_methods=None):
4429 4447
4430 4448 class PullRequestDisplay(object):
4431 4449 """
4432 4450 Special object wrapper for showing PullRequest data via Versions
4433 4451 It mimics PR object as close as possible. This is read only object
4434 4452 just for display
4435 4453 """
4436 4454
4437 4455 def __init__(self, attrs, internal=None):
4438 4456 self.attrs = attrs
4439 4457 # internal have priority over the given ones via attrs
4440 4458 self.internal = internal or ['versions']
4441 4459
4442 4460 def __getattr__(self, item):
4443 4461 if item in self.internal:
4444 4462 return getattr(self, item)
4445 4463 try:
4446 4464 return self.attrs[item]
4447 4465 except KeyError:
4448 4466 raise AttributeError(
4449 4467 '%s object has no attribute %s' % (self, item))
4450 4468
4451 4469 def __repr__(self):
4452 4470 pr_id = self.attrs.get('pull_request_id')
4453 4471 return f'<DB:PullRequestDisplay #{pr_id}>'
4454 4472
4455 4473 def versions(self):
4456 4474 return pull_request_obj.versions.order_by(
4457 4475 PullRequestVersion.pull_request_version_id).all()
4458 4476
4459 4477 def is_closed(self):
4460 4478 return pull_request_obj.is_closed()
4461 4479
4462 4480 def is_state_changing(self):
4463 4481 return pull_request_obj.is_state_changing()
4464 4482
4465 4483 @property
4466 4484 def pull_request_version_id(self):
4467 4485 return getattr(pull_request_obj, 'pull_request_version_id', None)
4468 4486
4469 4487 @property
4470 4488 def pull_request_last_version(self):
4471 4489 return pull_request_obj.pull_request_last_version
4472 4490
4473 4491 attrs = StrictAttributeDict(pull_request_obj.get_api_data(with_merge_state=False))
4474 4492
4475 4493 attrs.author = StrictAttributeDict(
4476 4494 pull_request_obj.author.get_api_data())
4477 4495 if pull_request_obj.target_repo:
4478 4496 attrs.target_repo = StrictAttributeDict(
4479 4497 pull_request_obj.target_repo.get_api_data())
4480 4498 attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url
4481 4499
4482 4500 if pull_request_obj.source_repo:
4483 4501 attrs.source_repo = StrictAttributeDict(
4484 4502 pull_request_obj.source_repo.get_api_data())
4485 4503 attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url
4486 4504
4487 4505 attrs.source_ref_parts = pull_request_obj.source_ref_parts
4488 4506 attrs.target_ref_parts = pull_request_obj.target_ref_parts
4489 4507 attrs.revisions = pull_request_obj.revisions
4490 4508 attrs.common_ancestor_id = pull_request_obj.common_ancestor_id
4491 4509 attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref
4492 4510 attrs.reviewer_data = org_pull_request_obj.reviewer_data
4493 4511 attrs.reviewer_data_json = org_pull_request_obj.reviewer_data_json
4494 4512
4495 4513 return PullRequestDisplay(attrs, internal=internal_methods)
4496 4514
4497 4515 def is_closed(self):
4498 4516 return self.status == self.STATUS_CLOSED
4499 4517
4500 4518 def is_state_changing(self):
4501 4519 return self.pull_request_state != PullRequest.STATE_CREATED
4502 4520
4503 4521 def __json__(self):
4504 4522 return {
4505 4523 'revisions': self.revisions,
4506 4524 'versions': self.versions_count
4507 4525 }
4508 4526
4509 4527 def calculated_review_status(self):
4510 4528 from rhodecode.model.changeset_status import ChangesetStatusModel
4511 4529 return ChangesetStatusModel().calculated_review_status(self)
4512 4530
4513 4531 def reviewers_statuses(self, user=None):
4514 4532 from rhodecode.model.changeset_status import ChangesetStatusModel
4515 4533 return ChangesetStatusModel().reviewers_statuses(self, user=user)
4516 4534
4517 4535 def get_pull_request_reviewers(self, role=None):
4518 4536 qry = PullRequestReviewers.query()\
4519 4537 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)
4520 4538 if role:
4521 4539 qry = qry.filter(PullRequestReviewers.role == role)
4522 4540
4523 4541 return qry.all()
4524 4542
4525 4543 @property
4526 4544 def reviewers_count(self):
4527 4545 qry = PullRequestReviewers.query()\
4528 4546 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)\
4529 4547 .filter(PullRequestReviewers.role == PullRequestReviewers.ROLE_REVIEWER)
4530 4548 return qry.count()
4531 4549
4532 4550 @property
4533 4551 def observers_count(self):
4534 4552 qry = PullRequestReviewers.query()\
4535 4553 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)\
4536 4554 .filter(PullRequestReviewers.role == PullRequestReviewers.ROLE_OBSERVER)
4537 4555 return qry.count()
4538 4556
4539 4557 def observers(self):
4540 4558 qry = PullRequestReviewers.query()\
4541 4559 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)\
4542 4560 .filter(PullRequestReviewers.role == PullRequestReviewers.ROLE_OBSERVER)\
4543 4561 .all()
4544 4562
4545 4563 for entry in qry:
4546 4564 yield entry, entry.user
4547 4565
4548 4566 @property
4549 4567 def workspace_id(self):
4550 4568 from rhodecode.model.pull_request import PullRequestModel
4551 4569 return PullRequestModel()._workspace_id(self)
4552 4570
4553 4571 def get_shadow_repo(self):
4554 4572 workspace_id = self.workspace_id
4555 4573 shadow_repository_path = self.target_repo.get_shadow_repository_path(workspace_id)
4556 4574 if os.path.isdir(shadow_repository_path):
4557 4575 vcs_obj = self.target_repo.scm_instance()
4558 4576 return vcs_obj.get_shadow_instance(shadow_repository_path)
4559 4577
4560 4578 @property
4561 4579 def versions_count(self):
4562 4580 """
4563 4581 return number of versions this PR have, e.g a PR that once been
4564 4582 updated will have 2 versions
4565 4583 """
4566 4584 return self.versions.count() + 1
4567 4585
4568 4586 @property
4569 4587 def pull_request_last_version(self):
4570 4588 return self.versions_count
4571 4589
4572 4590
4573 4591 class PullRequestVersion(Base, _PullRequestBase):
4574 4592 __tablename__ = 'pull_request_versions'
4575 4593 __table_args__ = (
4576 4594 base_table_args,
4577 4595 )
4578 4596
4579 4597 pull_request_version_id = Column('pull_request_version_id', Integer(), nullable=False, primary_key=True)
4580 4598 pull_request_id = Column('pull_request_id', Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=False)
4581 4599 pull_request = relationship('PullRequest', back_populates='versions')
4582 4600
4583 4601 def __repr__(self):
4584 4602 if self.pull_request_version_id:
4585 4603 return f'<DB:PullRequestVersion #{self.pull_request_version_id}>'
4586 4604 else:
4587 4605 return f'<DB:PullRequestVersion at {id(self)!r}>'
4588 4606
4589 4607 @property
4590 4608 def reviewers(self):
4591 4609 return self.pull_request.reviewers
4592 4610
4593 4611 @property
4594 4612 def versions(self):
4595 4613 return self.pull_request.versions
4596 4614
4597 4615 def is_closed(self):
4598 4616 # calculate from original
4599 4617 return self.pull_request.status == self.STATUS_CLOSED
4600 4618
4601 4619 def is_state_changing(self):
4602 4620 return self.pull_request.pull_request_state != PullRequest.STATE_CREATED
4603 4621
4604 4622 def calculated_review_status(self):
4605 4623 return self.pull_request.calculated_review_status()
4606 4624
4607 4625 def reviewers_statuses(self):
4608 4626 return self.pull_request.reviewers_statuses()
4609 4627
4610 4628 def observers(self):
4611 4629 return self.pull_request.observers()
4612 4630
4613 4631
4614 4632 class PullRequestReviewers(Base, BaseModel):
4615 4633 __tablename__ = 'pull_request_reviewers'
4616 4634 __table_args__ = (
4617 4635 base_table_args,
4618 4636 )
4619 4637 ROLE_REVIEWER = 'reviewer'
4620 4638 ROLE_OBSERVER = 'observer'
4621 4639 ROLES = [ROLE_REVIEWER, ROLE_OBSERVER]
4622 4640
4623 4641 @hybrid_property
4624 4642 def reasons(self):
4625 4643 if not self._reasons:
4626 4644 return []
4627 4645 return self._reasons
4628 4646
4629 4647 @reasons.setter
4630 4648 def reasons(self, val):
4631 4649 val = val or []
4632 4650 if any(not isinstance(x, str) for x in val):
4633 4651 raise Exception('invalid reasons type, must be list of strings')
4634 4652 self._reasons = val
4635 4653
4636 4654 pull_requests_reviewers_id = Column(
4637 4655 'pull_requests_reviewers_id', Integer(), nullable=False,
4638 4656 primary_key=True)
4639 4657 pull_request_id = Column(
4640 4658 "pull_request_id", Integer(),
4641 4659 ForeignKey('pull_requests.pull_request_id'), nullable=False)
4642 4660 user_id = Column(
4643 4661 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
4644 4662 _reasons = Column(
4645 4663 'reason', MutationList.as_mutable(
4646 4664 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
4647 4665
4648 4666 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4649 4667 role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
4650 4668
4651 4669 user = relationship('User')
4652 4670 pull_request = relationship('PullRequest', back_populates='reviewers')
4653 4671
4654 4672 rule_data = Column(
4655 4673 'rule_data_json',
4656 4674 JsonType(dialect_map=dict(mysql=UnicodeText(16384))))
4657 4675
4658 4676 def rule_user_group_data(self):
4659 4677 """
4660 4678 Returns the voting user group rule data for this reviewer
4661 4679 """
4662 4680
4663 4681 if self.rule_data and 'vote_rule' in self.rule_data:
4664 4682 user_group_data = {}
4665 4683 if 'rule_user_group_entry_id' in self.rule_data:
4666 4684 # means a group with voting rules !
4667 4685 user_group_data['id'] = self.rule_data['rule_user_group_entry_id']
4668 4686 user_group_data['name'] = self.rule_data['rule_name']
4669 4687 user_group_data['vote_rule'] = self.rule_data['vote_rule']
4670 4688
4671 4689 return user_group_data
4672 4690
4673 4691 @classmethod
4674 4692 def get_pull_request_reviewers(cls, pull_request_id, role=None):
4675 4693 qry = PullRequestReviewers.query()\
4676 4694 .filter(PullRequestReviewers.pull_request_id == pull_request_id)
4677 4695 if role:
4678 4696 qry = qry.filter(PullRequestReviewers.role == role)
4679 4697
4680 4698 return qry.all()
4681 4699
4682 4700 def __repr__(self):
4683 4701 return f"<{self.cls_name}('id:{self.pull_requests_reviewers_id}')>"
4684 4702
4685 4703
4686 4704 class Notification(Base, BaseModel):
4687 4705 __tablename__ = 'notifications'
4688 4706 __table_args__ = (
4689 4707 Index('notification_type_idx', 'type'),
4690 4708 base_table_args,
4691 4709 )
4692 4710
4693 4711 TYPE_CHANGESET_COMMENT = 'cs_comment'
4694 4712 TYPE_MESSAGE = 'message'
4695 4713 TYPE_MENTION = 'mention'
4696 4714 TYPE_REGISTRATION = 'registration'
4697 4715 TYPE_PULL_REQUEST = 'pull_request'
4698 4716 TYPE_PULL_REQUEST_COMMENT = 'pull_request_comment'
4699 4717 TYPE_PULL_REQUEST_UPDATE = 'pull_request_update'
4700 4718
4701 4719 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
4702 4720 subject = Column('subject', Unicode(512), nullable=True)
4703 4721 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
4704 4722 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
4705 4723 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4706 4724 type_ = Column('type', Unicode(255))
4707 4725
4708 4726 created_by_user = relationship('User', back_populates='user_created_notifications')
4709 4727 notifications_to_users = relationship('UserNotification', lazy='joined', cascade="all, delete-orphan", back_populates='notification')
4710 4728
4711 4729 @property
4712 4730 def recipients(self):
4713 4731 return [x.user for x in UserNotification.query()\
4714 4732 .filter(UserNotification.notification == self)\
4715 4733 .order_by(UserNotification.user_id.asc()).all()]
4716 4734
4717 4735 @classmethod
4718 4736 def create(cls, created_by, subject, body, recipients, type_=None):
4719 4737 if type_ is None:
4720 4738 type_ = Notification.TYPE_MESSAGE
4721 4739
4722 4740 notification = cls()
4723 4741 notification.created_by_user = created_by
4724 4742 notification.subject = subject
4725 4743 notification.body = body
4726 4744 notification.type_ = type_
4727 4745 notification.created_on = datetime.datetime.now()
4728 4746
4729 4747 # For each recipient link the created notification to his account
4730 4748 for u in recipients:
4731 4749 assoc = UserNotification()
4732 4750 assoc.user_id = u.user_id
4733 4751 assoc.notification = notification
4734 4752
4735 4753 # if created_by is inside recipients mark his notification
4736 4754 # as read
4737 4755 if u.user_id == created_by.user_id:
4738 4756 assoc.read = True
4739 4757 Session().add(assoc)
4740 4758
4741 4759 Session().add(notification)
4742 4760
4743 4761 return notification
4744 4762
4745 4763
4746 4764 class UserNotification(Base, BaseModel):
4747 4765 __tablename__ = 'user_to_notification'
4748 4766 __table_args__ = (
4749 4767 UniqueConstraint('user_id', 'notification_id'),
4750 4768 base_table_args
4751 4769 )
4752 4770
4753 4771 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
4754 4772 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
4755 4773 read = Column('read', Boolean, default=False)
4756 4774 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
4757 4775
4758 4776 user = relationship('User', lazy="joined", back_populates='notifications')
4759 4777 notification = relationship('Notification', lazy="joined", order_by=lambda: Notification.created_on.desc(), back_populates='notifications_to_users')
4760 4778
4761 4779 def mark_as_read(self):
4762 4780 self.read = True
4763 4781 Session().add(self)
4764 4782
4765 4783
4766 4784 class UserNotice(Base, BaseModel):
4767 4785 __tablename__ = 'user_notices'
4768 4786 __table_args__ = (
4769 4787 base_table_args
4770 4788 )
4771 4789
4772 4790 NOTIFICATION_TYPE_MESSAGE = 'message'
4773 4791 NOTIFICATION_TYPE_NOTICE = 'notice'
4774 4792
4775 4793 NOTIFICATION_LEVEL_INFO = 'info'
4776 4794 NOTIFICATION_LEVEL_WARNING = 'warning'
4777 4795 NOTIFICATION_LEVEL_ERROR = 'error'
4778 4796
4779 4797 user_notice_id = Column('gist_id', Integer(), primary_key=True)
4780 4798
4781 4799 notice_subject = Column('notice_subject', Unicode(512), nullable=True)
4782 4800 notice_body = Column('notice_body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
4783 4801
4784 4802 notice_read = Column('notice_read', Boolean, default=False)
4785 4803
4786 4804 notification_level = Column('notification_level', String(1024), default=NOTIFICATION_LEVEL_INFO)
4787 4805 notification_type = Column('notification_type', String(1024), default=NOTIFICATION_TYPE_NOTICE)
4788 4806
4789 4807 notice_created_by = Column('notice_created_by', Integer(), ForeignKey('users.user_id'), nullable=True)
4790 4808 notice_created_on = Column('notice_created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4791 4809
4792 4810 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'))
4793 4811 user = relationship('User', lazy="joined", primaryjoin='User.user_id==UserNotice.user_id')
4794 4812
4795 4813 @classmethod
4796 4814 def create_for_user(cls, user, subject, body, notice_level=NOTIFICATION_LEVEL_INFO, allow_duplicate=False):
4797 4815
4798 4816 if notice_level not in [cls.NOTIFICATION_LEVEL_ERROR,
4799 4817 cls.NOTIFICATION_LEVEL_WARNING,
4800 4818 cls.NOTIFICATION_LEVEL_INFO]:
4801 4819 return
4802 4820
4803 4821 from rhodecode.model.user import UserModel
4804 4822 user = UserModel().get_user(user)
4805 4823
4806 4824 new_notice = UserNotice()
4807 4825 if not allow_duplicate:
4808 4826 existing_msg = UserNotice().query() \
4809 4827 .filter(UserNotice.user == user) \
4810 4828 .filter(UserNotice.notice_body == body) \
4811 4829 .filter(UserNotice.notice_read == false()) \
4812 4830 .scalar()
4813 4831 if existing_msg:
4814 4832 log.warning('Ignoring duplicate notice for user %s', user)
4815 4833 return
4816 4834
4817 4835 new_notice.user = user
4818 4836 new_notice.notice_subject = subject
4819 4837 new_notice.notice_body = body
4820 4838 new_notice.notification_level = notice_level
4821 4839 Session().add(new_notice)
4822 4840 Session().commit()
4823 4841
4824 4842
4825 4843 class Gist(Base, BaseModel):
4826 4844 __tablename__ = 'gists'
4827 4845 __table_args__ = (
4828 4846 Index('g_gist_access_id_idx', 'gist_access_id'),
4829 4847 Index('g_created_on_idx', 'created_on'),
4830 4848 base_table_args
4831 4849 )
4832 4850
4833 4851 GIST_PUBLIC = 'public'
4834 4852 GIST_PRIVATE = 'private'
4835 4853 DEFAULT_FILENAME = 'gistfile1.txt'
4836 4854
4837 4855 ACL_LEVEL_PUBLIC = 'acl_public'
4838 4856 ACL_LEVEL_PRIVATE = 'acl_private'
4839 4857
4840 4858 gist_id = Column('gist_id', Integer(), primary_key=True)
4841 4859 gist_access_id = Column('gist_access_id', Unicode(250))
4842 4860 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
4843 4861 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
4844 4862 gist_expires = Column('gist_expires', Float(53), nullable=False)
4845 4863 gist_type = Column('gist_type', Unicode(128), nullable=False)
4846 4864 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4847 4865 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4848 4866 acl_level = Column('acl_level', Unicode(128), nullable=True)
4849 4867
4850 4868 owner = relationship('User', back_populates='user_gists')
4851 4869
4852 4870 def __repr__(self):
4853 4871 return f'<Gist:[{self.gist_type}]{self.gist_access_id}>'
4854 4872
4855 4873 @hybrid_property
4856 4874 def description_safe(self):
4857 4875 from rhodecode.lib import helpers as h
4858 4876 return h.escape(self.gist_description)
4859 4877
4860 4878 @classmethod
4861 4879 def get_or_404(cls, id_):
4862 4880 from pyramid.httpexceptions import HTTPNotFound
4863 4881
4864 4882 res = cls.query().filter(cls.gist_access_id == id_).scalar()
4865 4883 if not res:
4866 4884 log.debug('WARN: No DB entry with id %s', id_)
4867 4885 raise HTTPNotFound()
4868 4886 return res
4869 4887
4870 4888 @classmethod
4871 4889 def get_by_access_id(cls, gist_access_id):
4872 4890 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
4873 4891
4874 4892 def gist_url(self):
4875 4893 from rhodecode.model.gist import GistModel
4876 4894 return GistModel().get_url(self)
4877 4895
4878 4896 @classmethod
4879 4897 def base_path(cls):
4880 4898 """
4881 4899 Returns base path when all gists are stored
4882 4900
4883 4901 :param cls:
4884 4902 """
4885 4903 from rhodecode.model.gist import GIST_STORE_LOC
4886 4904 q = Session().query(RhodeCodeUi)\
4887 4905 .filter(RhodeCodeUi.ui_key == URL_SEP)
4888 4906 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
4889 4907 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
4890 4908
4891 4909 def get_api_data(self):
4892 4910 """
4893 4911 Common function for generating gist related data for API
4894 4912 """
4895 4913 gist = self
4896 4914 data = {
4897 4915 'gist_id': gist.gist_id,
4898 4916 'type': gist.gist_type,
4899 4917 'access_id': gist.gist_access_id,
4900 4918 'description': gist.gist_description,
4901 4919 'url': gist.gist_url(),
4902 4920 'expires': gist.gist_expires,
4903 4921 'created_on': gist.created_on,
4904 4922 'modified_at': gist.modified_at,
4905 4923 'content': None,
4906 4924 'acl_level': gist.acl_level,
4907 4925 }
4908 4926 return data
4909 4927
4910 4928 def __json__(self):
4911 4929 data = dict(
4912 4930 )
4913 4931 data.update(self.get_api_data())
4914 4932 return data
4915 4933 # SCM functions
4916 4934
4917 4935 def scm_instance(self, **kwargs):
4918 4936 """
4919 4937 Get an instance of VCS Repository
4920 4938
4921 4939 :param kwargs:
4922 4940 """
4923 4941 from rhodecode.model.gist import GistModel
4924 4942 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
4925 4943 return get_vcs_instance(
4926 4944 repo_path=safe_str(full_repo_path), create=False,
4927 4945 _vcs_alias=GistModel.vcs_backend)
4928 4946
4929 4947
4930 4948 class ExternalIdentity(Base, BaseModel):
4931 4949 __tablename__ = 'external_identities'
4932 4950 __table_args__ = (
4933 4951 Index('local_user_id_idx', 'local_user_id'),
4934 4952 Index('external_id_idx', 'external_id'),
4935 4953 base_table_args
4936 4954 )
4937 4955
4938 4956 external_id = Column('external_id', Unicode(255), default='', primary_key=True)
4939 4957 external_username = Column('external_username', Unicode(1024), default='')
4940 4958 local_user_id = Column('local_user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
4941 4959 provider_name = Column('provider_name', Unicode(255), default='', primary_key=True)
4942 4960 access_token = Column('access_token', String(1024), default='')
4943 4961 alt_token = Column('alt_token', String(1024), default='')
4944 4962 token_secret = Column('token_secret', String(1024), default='')
4945 4963
4946 4964 @classmethod
4947 4965 def by_external_id_and_provider(cls, external_id, provider_name, local_user_id=None):
4948 4966 """
4949 4967 Returns ExternalIdentity instance based on search params
4950 4968
4951 4969 :param external_id:
4952 4970 :param provider_name:
4953 4971 :return: ExternalIdentity
4954 4972 """
4955 4973 query = cls.query()
4956 4974 query = query.filter(cls.external_id == external_id)
4957 4975 query = query.filter(cls.provider_name == provider_name)
4958 4976 if local_user_id:
4959 4977 query = query.filter(cls.local_user_id == local_user_id)
4960 4978 return query.first()
4961 4979
4962 4980 @classmethod
4963 4981 def user_by_external_id_and_provider(cls, external_id, provider_name):
4964 4982 """
4965 4983 Returns User instance based on search params
4966 4984
4967 4985 :param external_id:
4968 4986 :param provider_name:
4969 4987 :return: User
4970 4988 """
4971 4989 query = User.query()
4972 4990 query = query.filter(cls.external_id == external_id)
4973 4991 query = query.filter(cls.provider_name == provider_name)
4974 4992 query = query.filter(User.user_id == cls.local_user_id)
4975 4993 return query.first()
4976 4994
4977 4995 @classmethod
4978 4996 def by_local_user_id(cls, local_user_id):
4979 4997 """
4980 4998 Returns all tokens for user
4981 4999
4982 5000 :param local_user_id:
4983 5001 :return: ExternalIdentity
4984 5002 """
4985 5003 query = cls.query()
4986 5004 query = query.filter(cls.local_user_id == local_user_id)
4987 5005 return query
4988 5006
4989 5007 @classmethod
4990 5008 def load_provider_plugin(cls, plugin_id):
4991 5009 from rhodecode.authentication.base import loadplugin
4992 5010 _plugin_id = 'egg:rhodecode-enterprise-ee#{}'.format(plugin_id)
4993 5011 auth_plugin = loadplugin(_plugin_id)
4994 5012 return auth_plugin
4995 5013
4996 5014
4997 5015 class Integration(Base, BaseModel):
4998 5016 __tablename__ = 'integrations'
4999 5017 __table_args__ = (
5000 5018 base_table_args
5001 5019 )
5002 5020
5003 5021 integration_id = Column('integration_id', Integer(), primary_key=True)
5004 5022 integration_type = Column('integration_type', String(255))
5005 5023 enabled = Column('enabled', Boolean(), nullable=False)
5006 5024 name = Column('name', String(255), nullable=False)
5007 5025 child_repos_only = Column('child_repos_only', Boolean(), nullable=False, default=False)
5008 5026
5009 5027 settings = Column(
5010 5028 'settings_json', MutationObj.as_mutable(
5011 5029 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
5012 5030 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
5013 5031 repo = relationship('Repository', lazy='joined', back_populates='integrations')
5014 5032
5015 5033 repo_group_id = Column('repo_group_id', Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
5016 5034 repo_group = relationship('RepoGroup', lazy='joined', back_populates='integrations')
5017 5035
5018 5036 @property
5019 5037 def scope(self):
5020 5038 if self.repo:
5021 5039 return repr(self.repo)
5022 5040 if self.repo_group:
5023 5041 if self.child_repos_only:
5024 5042 return repr(self.repo_group) + ' (child repos only)'
5025 5043 else:
5026 5044 return repr(self.repo_group) + ' (recursive)'
5027 5045 if self.child_repos_only:
5028 5046 return 'root_repos'
5029 5047 return 'global'
5030 5048
5031 5049 def __repr__(self):
5032 5050 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
5033 5051
5034 5052
5035 5053 class RepoReviewRuleUser(Base, BaseModel):
5036 5054 __tablename__ = 'repo_review_rules_users'
5037 5055 __table_args__ = (
5038 5056 base_table_args
5039 5057 )
5040 5058 ROLE_REVIEWER = 'reviewer'
5041 5059 ROLE_OBSERVER = 'observer'
5042 5060 ROLES = [ROLE_REVIEWER, ROLE_OBSERVER]
5043 5061
5044 5062 repo_review_rule_user_id = Column('repo_review_rule_user_id', Integer(), primary_key=True)
5045 5063 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
5046 5064 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False)
5047 5065 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
5048 5066 role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
5049 5067 user = relationship('User', back_populates='user_review_rules')
5050 5068
5051 5069 def rule_data(self):
5052 5070 return {
5053 5071 'mandatory': self.mandatory,
5054 5072 'role': self.role,
5055 5073 }
5056 5074
5057 5075
5058 5076 class RepoReviewRuleUserGroup(Base, BaseModel):
5059 5077 __tablename__ = 'repo_review_rules_users_groups'
5060 5078 __table_args__ = (
5061 5079 base_table_args
5062 5080 )
5063 5081
5064 5082 VOTE_RULE_ALL = -1
5065 5083 ROLE_REVIEWER = 'reviewer'
5066 5084 ROLE_OBSERVER = 'observer'
5067 5085 ROLES = [ROLE_REVIEWER, ROLE_OBSERVER]
5068 5086
5069 5087 repo_review_rule_users_group_id = Column('repo_review_rule_users_group_id', Integer(), primary_key=True)
5070 5088 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
5071 5089 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False)
5072 5090 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
5073 5091 role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
5074 5092 vote_rule = Column("vote_rule", Integer(), nullable=True, default=VOTE_RULE_ALL)
5075 5093 users_group = relationship('UserGroup')
5076 5094
5077 5095 def rule_data(self):
5078 5096 return {
5079 5097 'mandatory': self.mandatory,
5080 5098 'role': self.role,
5081 5099 'vote_rule': self.vote_rule
5082 5100 }
5083 5101
5084 5102 @property
5085 5103 def vote_rule_label(self):
5086 5104 if not self.vote_rule or self.vote_rule == self.VOTE_RULE_ALL:
5087 5105 return 'all must vote'
5088 5106 else:
5089 5107 return 'min. vote {}'.format(self.vote_rule)
5090 5108
5091 5109
5092 5110 class RepoReviewRule(Base, BaseModel):
5093 5111 __tablename__ = 'repo_review_rules'
5094 5112 __table_args__ = (
5095 5113 base_table_args
5096 5114 )
5097 5115
5098 5116 repo_review_rule_id = Column(
5099 5117 'repo_review_rule_id', Integer(), primary_key=True)
5100 5118 repo_id = Column(
5101 5119 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
5102 5120 repo = relationship('Repository', back_populates='review_rules')
5103 5121
5104 5122 review_rule_name = Column('review_rule_name', String(255))
5105 5123 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default='*') # glob
5106 5124 _target_branch_pattern = Column("target_branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default='*') # glob
5107 5125 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default='*') # glob
5108 5126
5109 5127 use_authors_for_review = Column("use_authors_for_review", Boolean(), nullable=False, default=False)
5110 5128
5111 5129 # Legacy fields, just for backward compat
5112 5130 _forbid_author_to_review = Column("forbid_author_to_review", Boolean(), nullable=False, default=False)
5113 5131 _forbid_commit_author_to_review = Column("forbid_commit_author_to_review", Boolean(), nullable=False, default=False)
5114 5132
5115 5133 pr_author = Column("pr_author", UnicodeText().with_variant(UnicodeText(255), 'mysql'), nullable=True)
5116 5134 commit_author = Column("commit_author", UnicodeText().with_variant(UnicodeText(255), 'mysql'), nullable=True)
5117 5135
5118 5136 forbid_adding_reviewers = Column("forbid_adding_reviewers", Boolean(), nullable=False, default=False)
5119 5137
5120 5138 rule_users = relationship('RepoReviewRuleUser')
5121 5139 rule_user_groups = relationship('RepoReviewRuleUserGroup')
5122 5140
5123 5141 def _validate_pattern(self, value):
5124 5142 re.compile('^' + glob2re(value) + '$')
5125 5143
5126 5144 @hybrid_property
5127 5145 def source_branch_pattern(self):
5128 5146 return self._branch_pattern or '*'
5129 5147
5130 5148 @source_branch_pattern.setter
5131 5149 def source_branch_pattern(self, value):
5132 5150 self._validate_pattern(value)
5133 5151 self._branch_pattern = value or '*'
5134 5152
5135 5153 @hybrid_property
5136 5154 def target_branch_pattern(self):
5137 5155 return self._target_branch_pattern or '*'
5138 5156
5139 5157 @target_branch_pattern.setter
5140 5158 def target_branch_pattern(self, value):
5141 5159 self._validate_pattern(value)
5142 5160 self._target_branch_pattern = value or '*'
5143 5161
5144 5162 @hybrid_property
5145 5163 def file_pattern(self):
5146 5164 return self._file_pattern or '*'
5147 5165
5148 5166 @file_pattern.setter
5149 5167 def file_pattern(self, value):
5150 5168 self._validate_pattern(value)
5151 5169 self._file_pattern = value or '*'
5152 5170
5153 5171 @hybrid_property
5154 5172 def forbid_pr_author_to_review(self):
5155 5173 return self.pr_author == 'forbid_pr_author'
5156 5174
5157 5175 @hybrid_property
5158 5176 def include_pr_author_to_review(self):
5159 5177 return self.pr_author == 'include_pr_author'
5160 5178
5161 5179 @hybrid_property
5162 5180 def forbid_commit_author_to_review(self):
5163 5181 return self.commit_author == 'forbid_commit_author'
5164 5182
5165 5183 @hybrid_property
5166 5184 def include_commit_author_to_review(self):
5167 5185 return self.commit_author == 'include_commit_author'
5168 5186
5169 5187 def matches(self, source_branch, target_branch, files_changed):
5170 5188 """
5171 5189 Check if this review rule matches a branch/files in a pull request
5172 5190
5173 5191 :param source_branch: source branch name for the commit
5174 5192 :param target_branch: target branch name for the commit
5175 5193 :param files_changed: list of file paths changed in the pull request
5176 5194 """
5177 5195
5178 5196 source_branch = source_branch or ''
5179 5197 target_branch = target_branch or ''
5180 5198 files_changed = files_changed or []
5181 5199
5182 5200 branch_matches = True
5183 5201 if source_branch or target_branch:
5184 5202 if self.source_branch_pattern == '*':
5185 5203 source_branch_match = True
5186 5204 else:
5187 5205 if self.source_branch_pattern.startswith('re:'):
5188 5206 source_pattern = self.source_branch_pattern[3:]
5189 5207 else:
5190 5208 source_pattern = '^' + glob2re(self.source_branch_pattern) + '$'
5191 5209 source_branch_regex = re.compile(source_pattern)
5192 5210 source_branch_match = bool(source_branch_regex.search(source_branch))
5193 5211 if self.target_branch_pattern == '*':
5194 5212 target_branch_match = True
5195 5213 else:
5196 5214 if self.target_branch_pattern.startswith('re:'):
5197 5215 target_pattern = self.target_branch_pattern[3:]
5198 5216 else:
5199 5217 target_pattern = '^' + glob2re(self.target_branch_pattern) + '$'
5200 5218 target_branch_regex = re.compile(target_pattern)
5201 5219 target_branch_match = bool(target_branch_regex.search(target_branch))
5202 5220
5203 5221 branch_matches = source_branch_match and target_branch_match
5204 5222
5205 5223 files_matches = True
5206 5224 if self.file_pattern != '*':
5207 5225 files_matches = False
5208 5226 if self.file_pattern.startswith('re:'):
5209 5227 file_pattern = self.file_pattern[3:]
5210 5228 else:
5211 5229 file_pattern = glob2re(self.file_pattern)
5212 5230 file_regex = re.compile(file_pattern)
5213 5231 for file_data in files_changed:
5214 5232 filename = file_data.get('filename')
5215 5233
5216 5234 if file_regex.search(filename):
5217 5235 files_matches = True
5218 5236 break
5219 5237
5220 5238 return branch_matches and files_matches
5221 5239
5222 5240 @property
5223 5241 def review_users(self):
5224 5242 """ Returns the users which this rule applies to """
5225 5243
5226 5244 users = collections.OrderedDict()
5227 5245
5228 5246 for rule_user in self.rule_users:
5229 5247 if rule_user.user.active:
5230 5248 if rule_user.user not in users:
5231 5249 users[rule_user.user.username] = {
5232 5250 'user': rule_user.user,
5233 5251 'source': 'user',
5234 5252 'source_data': {},
5235 5253 'data': rule_user.rule_data()
5236 5254 }
5237 5255
5238 5256 for rule_user_group in self.rule_user_groups:
5239 5257 source_data = {
5240 5258 'user_group_id': rule_user_group.users_group.users_group_id,
5241 5259 'name': rule_user_group.users_group.users_group_name,
5242 5260 'members': len(rule_user_group.users_group.members)
5243 5261 }
5244 5262 for member in rule_user_group.users_group.members:
5245 5263 if member.user.active:
5246 5264 key = member.user.username
5247 5265 if key in users:
5248 5266 # skip this member as we have him already
5249 5267 # this prevents from override the "first" matched
5250 5268 # users with duplicates in multiple groups
5251 5269 continue
5252 5270
5253 5271 users[key] = {
5254 5272 'user': member.user,
5255 5273 'source': 'user_group',
5256 5274 'source_data': source_data,
5257 5275 'data': rule_user_group.rule_data()
5258 5276 }
5259 5277
5260 5278 return users
5261 5279
5262 5280 def user_group_vote_rule(self, user_id):
5263 5281
5264 5282 rules = []
5265 5283 if not self.rule_user_groups:
5266 5284 return rules
5267 5285
5268 5286 for user_group in self.rule_user_groups:
5269 5287 user_group_members = [x.user_id for x in user_group.users_group.members]
5270 5288 if user_id in user_group_members:
5271 5289 rules.append(user_group)
5272 5290 return rules
5273 5291
5274 5292 def __repr__(self):
5275 5293 return f'<RepoReviewerRule(id={self.repo_review_rule_id}, repo={self.repo!r})>'
5276 5294
5277 5295
5278 5296 class ScheduleEntry(Base, BaseModel):
5279 5297 __tablename__ = 'schedule_entries'
5280 5298 __table_args__ = (
5281 5299 UniqueConstraint('schedule_name', name='s_schedule_name_idx'),
5282 5300 UniqueConstraint('task_uid', name='s_task_uid_idx'),
5283 5301 base_table_args,
5284 5302 )
5285 5303 SCHEDULE_TYPE_INTEGER = "integer"
5286 5304 SCHEDULE_TYPE_CRONTAB = "crontab"
5287 5305
5288 5306 schedule_types = [SCHEDULE_TYPE_CRONTAB, SCHEDULE_TYPE_INTEGER]
5289 5307 schedule_entry_id = Column('schedule_entry_id', Integer(), primary_key=True)
5290 5308
5291 5309 schedule_name = Column("schedule_name", String(255), nullable=False, unique=None, default=None)
5292 5310 schedule_description = Column("schedule_description", String(10000), nullable=True, unique=None, default=None)
5293 5311 schedule_enabled = Column("schedule_enabled", Boolean(), nullable=False, unique=None, default=True)
5294 5312
5295 5313 _schedule_type = Column("schedule_type", String(255), nullable=False, unique=None, default=None)
5296 5314 schedule_definition = Column('schedule_definition_json', MutationObj.as_mutable(JsonType(default=lambda: "", dialect_map=dict(mysql=LONGTEXT()))))
5297 5315
5298 5316 schedule_last_run = Column('schedule_last_run', DateTime(timezone=False), nullable=True, unique=None, default=None)
5299 5317 schedule_total_run_count = Column('schedule_total_run_count', Integer(), nullable=True, unique=None, default=0)
5300 5318
5301 5319 # task
5302 5320 task_uid = Column("task_uid", String(255), nullable=False, unique=None, default=None)
5303 5321 task_dot_notation = Column("task_dot_notation", String(4096), nullable=False, unique=None, default=None)
5304 5322 task_args = Column('task_args_json', MutationObj.as_mutable(JsonType(default=list, dialect_map=dict(mysql=LONGTEXT()))))
5305 5323 task_kwargs = Column('task_kwargs_json', MutationObj.as_mutable(JsonType(default=dict, dialect_map=dict(mysql=LONGTEXT()))))
5306 5324
5307 5325 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5308 5326 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=None)
5309 5327
5310 5328 @hybrid_property
5311 5329 def schedule_type(self):
5312 5330 return self._schedule_type
5313 5331
5314 5332 @schedule_type.setter
5315 5333 def schedule_type(self, val):
5316 5334 if val not in self.schedule_types:
5317 5335 raise ValueError('Value must be on of `{}` and got `{}`'.format(
5318 5336 val, self.schedule_type))
5319 5337
5320 5338 self._schedule_type = val
5321 5339
5322 5340 @classmethod
5323 5341 def get_uid(cls, obj):
5324 5342 args = obj.task_args
5325 5343 kwargs = obj.task_kwargs
5326 5344 if isinstance(args, JsonRaw):
5327 5345 try:
5328 5346 args = json.loads(args)
5329 5347 except ValueError:
5330 5348 args = tuple()
5331 5349
5332 5350 if isinstance(kwargs, JsonRaw):
5333 5351 try:
5334 5352 kwargs = json.loads(kwargs)
5335 5353 except ValueError:
5336 5354 kwargs = dict()
5337 5355
5338 5356 dot_notation = obj.task_dot_notation
5339 5357 val = '.'.join(map(safe_str, [
5340 5358 sorted(dot_notation), args, sorted(kwargs.items())]))
5341 5359 return sha1(safe_bytes(val))
5342 5360
5343 5361 @classmethod
5344 5362 def get_by_schedule_name(cls, schedule_name):
5345 5363 return cls.query().filter(cls.schedule_name == schedule_name).scalar()
5346 5364
5347 5365 @classmethod
5348 5366 def get_by_schedule_id(cls, schedule_id):
5349 5367 return cls.query().filter(cls.schedule_entry_id == schedule_id).scalar()
5350 5368
5351 5369 @property
5352 5370 def task(self):
5353 5371 return self.task_dot_notation
5354 5372
5355 5373 @property
5356 5374 def schedule(self):
5357 5375 from rhodecode.lib.celerylib.utils import raw_2_schedule
5358 5376 schedule = raw_2_schedule(self.schedule_definition, self.schedule_type)
5359 5377 return schedule
5360 5378
5361 5379 @property
5362 5380 def args(self):
5363 5381 try:
5364 5382 return list(self.task_args or [])
5365 5383 except ValueError:
5366 5384 return list()
5367 5385
5368 5386 @property
5369 5387 def kwargs(self):
5370 5388 try:
5371 5389 return dict(self.task_kwargs or {})
5372 5390 except ValueError:
5373 5391 return dict()
5374 5392
5375 5393 def _as_raw(self, val, indent=False):
5376 5394 if hasattr(val, 'de_coerce'):
5377 5395 val = val.de_coerce()
5378 5396 if val:
5379 5397 if indent:
5380 5398 val = ext_json.formatted_str_json(val)
5381 5399 else:
5382 5400 val = ext_json.str_json(val)
5383 5401
5384 5402 return val
5385 5403
5386 5404 @property
5387 5405 def schedule_definition_raw(self):
5388 5406 return self._as_raw(self.schedule_definition)
5389 5407
5390 5408 def args_raw(self, indent=False):
5391 5409 return self._as_raw(self.task_args, indent)
5392 5410
5393 5411 def kwargs_raw(self, indent=False):
5394 5412 return self._as_raw(self.task_kwargs, indent)
5395 5413
5396 5414 def __repr__(self):
5397 5415 return f'<DB:ScheduleEntry({self.schedule_entry_id}:{self.schedule_name})>'
5398 5416
5399 5417
5400 5418 @event.listens_for(ScheduleEntry, 'before_update')
5401 5419 def update_task_uid(mapper, connection, target):
5402 5420 target.task_uid = ScheduleEntry.get_uid(target)
5403 5421
5404 5422
5405 5423 @event.listens_for(ScheduleEntry, 'before_insert')
5406 5424 def set_task_uid(mapper, connection, target):
5407 5425 target.task_uid = ScheduleEntry.get_uid(target)
5408 5426
5409 5427
5410 5428 class _BaseBranchPerms(BaseModel):
5411 5429 @classmethod
5412 5430 def compute_hash(cls, value):
5413 5431 return sha1_safe(value)
5414 5432
5415 5433 @hybrid_property
5416 5434 def branch_pattern(self):
5417 5435 return self._branch_pattern or '*'
5418 5436
5419 5437 @hybrid_property
5420 5438 def branch_hash(self):
5421 5439 return self._branch_hash
5422 5440
5423 5441 def _validate_glob(self, value):
5424 5442 re.compile('^' + glob2re(value) + '$')
5425 5443
5426 5444 @branch_pattern.setter
5427 5445 def branch_pattern(self, value):
5428 5446 self._validate_glob(value)
5429 5447 self._branch_pattern = value or '*'
5430 5448 # set the Hash when setting the branch pattern
5431 5449 self._branch_hash = self.compute_hash(self._branch_pattern)
5432 5450
5433 5451 def matches(self, branch):
5434 5452 """
5435 5453 Check if this the branch matches entry
5436 5454
5437 5455 :param branch: branch name for the commit
5438 5456 """
5439 5457
5440 5458 branch = branch or ''
5441 5459
5442 5460 branch_matches = True
5443 5461 if branch:
5444 5462 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
5445 5463 branch_matches = bool(branch_regex.search(branch))
5446 5464
5447 5465 return branch_matches
5448 5466
5449 5467
5450 5468 class UserToRepoBranchPermission(Base, _BaseBranchPerms):
5451 5469 __tablename__ = 'user_to_repo_branch_permissions'
5452 5470 __table_args__ = (
5453 5471 base_table_args
5454 5472 )
5455 5473
5456 5474 branch_rule_id = Column('branch_rule_id', Integer(), primary_key=True)
5457 5475
5458 5476 repository_id = Column('repository_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
5459 5477 repo = relationship('Repository', back_populates='user_branch_perms')
5460 5478
5461 5479 permission_id = Column('permission_id', Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
5462 5480 permission = relationship('Permission')
5463 5481
5464 5482 rule_to_perm_id = Column('rule_to_perm_id', Integer(), ForeignKey('repo_to_perm.repo_to_perm_id'), nullable=False, unique=None, default=None)
5465 5483 user_repo_to_perm = relationship('UserRepoToPerm', back_populates='branch_perm_entry')
5466 5484
5467 5485 rule_order = Column('rule_order', Integer(), nullable=False)
5468 5486 _branch_pattern = Column('branch_pattern', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), default='*') # glob
5469 5487 _branch_hash = Column('branch_hash', UnicodeText().with_variant(UnicodeText(2048), 'mysql'))
5470 5488
5471 5489 def __repr__(self):
5472 5490 return f'<UserBranchPermission({self.user_repo_to_perm} => {self.branch_pattern!r})>'
5473 5491
5474 5492
5475 5493 class UserGroupToRepoBranchPermission(Base, _BaseBranchPerms):
5476 5494 __tablename__ = 'user_group_to_repo_branch_permissions'
5477 5495 __table_args__ = (
5478 5496 base_table_args
5479 5497 )
5480 5498
5481 5499 branch_rule_id = Column('branch_rule_id', Integer(), primary_key=True)
5482 5500
5483 5501 repository_id = Column('repository_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
5484 5502 repo = relationship('Repository', back_populates='user_group_branch_perms')
5485 5503
5486 5504 permission_id = Column('permission_id', Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
5487 5505 permission = relationship('Permission')
5488 5506
5489 5507 rule_to_perm_id = Column('rule_to_perm_id', Integer(), ForeignKey('users_group_repo_to_perm.users_group_to_perm_id'), nullable=False, unique=None, default=None)
5490 5508 user_group_repo_to_perm = relationship('UserGroupRepoToPerm', back_populates='user_group_branch_perms')
5491 5509
5492 5510 rule_order = Column('rule_order', Integer(), nullable=False)
5493 5511 _branch_pattern = Column('branch_pattern', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), default='*') # glob
5494 5512 _branch_hash = Column('branch_hash', UnicodeText().with_variant(UnicodeText(2048), 'mysql'))
5495 5513
5496 5514 def __repr__(self):
5497 5515 return f'<UserBranchPermission({self.user_group_repo_to_perm} => {self.branch_pattern!r})>'
5498 5516
5499 5517
5500 5518 class UserBookmark(Base, BaseModel):
5501 5519 __tablename__ = 'user_bookmarks'
5502 5520 __table_args__ = (
5503 5521 UniqueConstraint('user_id', 'bookmark_repo_id'),
5504 5522 UniqueConstraint('user_id', 'bookmark_repo_group_id'),
5505 5523 UniqueConstraint('user_id', 'bookmark_position'),
5506 5524 base_table_args
5507 5525 )
5508 5526
5509 5527 user_bookmark_id = Column("user_bookmark_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
5510 5528 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
5511 5529 position = Column("bookmark_position", Integer(), nullable=False)
5512 5530 title = Column("bookmark_title", String(255), nullable=True, unique=None, default=None)
5513 5531 redirect_url = Column("bookmark_redirect_url", String(10240), nullable=True, unique=None, default=None)
5514 5532 created_on = Column("created_on", DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5515 5533
5516 5534 bookmark_repo_id = Column("bookmark_repo_id", Integer(), ForeignKey("repositories.repo_id"), nullable=True, unique=None, default=None)
5517 5535 bookmark_repo_group_id = Column("bookmark_repo_group_id", Integer(), ForeignKey("groups.group_id"), nullable=True, unique=None, default=None)
5518 5536
5519 5537 user = relationship("User")
5520 5538
5521 5539 repository = relationship("Repository")
5522 5540 repository_group = relationship("RepoGroup")
5523 5541
5524 5542 @classmethod
5525 5543 def get_by_position_for_user(cls, position, user_id):
5526 5544 return cls.query() \
5527 5545 .filter(UserBookmark.user_id == user_id) \
5528 5546 .filter(UserBookmark.position == position).scalar()
5529 5547
5530 5548 @classmethod
5531 5549 def get_bookmarks_for_user(cls, user_id, cache=True):
5532 5550 bookmarks = cls.query() \
5533 5551 .filter(UserBookmark.user_id == user_id) \
5534 5552 .options(joinedload(UserBookmark.repository)) \
5535 5553 .options(joinedload(UserBookmark.repository_group)) \
5536 5554 .order_by(UserBookmark.position.asc())
5537 5555
5538 5556 if cache:
5539 5557 bookmarks = bookmarks.options(
5540 5558 FromCache("sql_cache_short", "get_user_{}_bookmarks".format(user_id))
5541 5559 )
5542 5560
5543 5561 return bookmarks.all()
5544 5562
5545 5563 def __repr__(self):
5546 5564 return f'<UserBookmark({self.position} @ {self.redirect_url!r})>'
5547 5565
5548 5566
5549 5567 class FileStore(Base, BaseModel):
5550 5568 __tablename__ = 'file_store'
5551 5569 __table_args__ = (
5552 5570 base_table_args
5553 5571 )
5554 5572
5555 5573 file_store_id = Column('file_store_id', Integer(), primary_key=True)
5556 5574 file_uid = Column('file_uid', String(1024), nullable=False)
5557 5575 file_display_name = Column('file_display_name', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), nullable=True)
5558 5576 file_description = Column('file_description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'), nullable=True)
5559 5577 file_org_name = Column('file_org_name', UnicodeText().with_variant(UnicodeText(10240), 'mysql'), nullable=False)
5560 5578
5561 5579 # sha256 hash
5562 5580 file_hash = Column('file_hash', String(512), nullable=False)
5563 5581 file_size = Column('file_size', BigInteger(), nullable=False)
5564 5582
5565 5583 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5566 5584 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True)
5567 5585 accessed_count = Column('accessed_count', Integer(), default=0)
5568 5586
5569 5587 enabled = Column('enabled', Boolean(), nullable=False, default=True)
5570 5588
5571 5589 # if repo/repo_group reference is set, check for permissions
5572 5590 check_acl = Column('check_acl', Boolean(), nullable=False, default=True)
5573 5591
5574 5592 # hidden defines an attachment that should be hidden from showing in artifact listing
5575 5593 hidden = Column('hidden', Boolean(), nullable=False, default=False)
5576 5594
5577 5595 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
5578 5596 upload_user = relationship('User', lazy='joined', primaryjoin='User.user_id==FileStore.user_id', back_populates='artifacts')
5579 5597
5580 5598 file_metadata = relationship('FileStoreMetadata', lazy='joined')
5581 5599
5582 5600 # scope limited to user, which requester have access to
5583 5601 scope_user_id = Column(
5584 5602 'scope_user_id', Integer(), ForeignKey('users.user_id'),
5585 5603 nullable=True, unique=None, default=None)
5586 5604 user = relationship('User', lazy='joined', primaryjoin='User.user_id==FileStore.scope_user_id', back_populates='scope_artifacts')
5587 5605
5588 5606 # scope limited to user group, which requester have access to
5589 5607 scope_user_group_id = Column(
5590 5608 'scope_user_group_id', Integer(), ForeignKey('users_groups.users_group_id'),
5591 5609 nullable=True, unique=None, default=None)
5592 5610 user_group = relationship('UserGroup', lazy='joined')
5593 5611
5594 5612 # scope limited to repo, which requester have access to
5595 5613 scope_repo_id = Column(
5596 5614 'scope_repo_id', Integer(), ForeignKey('repositories.repo_id'),
5597 5615 nullable=True, unique=None, default=None)
5598 5616 repo = relationship('Repository', lazy='joined')
5599 5617
5600 5618 # scope limited to repo group, which requester have access to
5601 5619 scope_repo_group_id = Column(
5602 5620 'scope_repo_group_id', Integer(), ForeignKey('groups.group_id'),
5603 5621 nullable=True, unique=None, default=None)
5604 5622 repo_group = relationship('RepoGroup', lazy='joined')
5605 5623
5606 5624 @classmethod
5607 5625 def get_scope(cls, scope_type, scope_id):
5608 5626 if scope_type == 'repo':
5609 5627 return f'repo:{scope_id}'
5610 5628 elif scope_type == 'repo-group':
5611 5629 return f'repo-group:{scope_id}'
5612 5630 elif scope_type == 'user':
5613 5631 return f'user:{scope_id}'
5614 5632 elif scope_type == 'user-group':
5615 5633 return f'user-group:{scope_id}'
5616 5634 else:
5617 5635 return scope_type
5618 5636
5619 5637 @classmethod
5620 5638 def get_by_store_uid(cls, file_store_uid, safe=False):
5621 5639 if safe:
5622 5640 return FileStore.query().filter(FileStore.file_uid == file_store_uid).first()
5623 5641 else:
5624 5642 return FileStore.query().filter(FileStore.file_uid == file_store_uid).scalar()
5625 5643
5626 5644 @classmethod
5627 5645 def create(cls, file_uid, filename, file_hash, file_size, file_display_name='',
5628 5646 file_description='', enabled=True, hidden=False, check_acl=True,
5629 5647 user_id=None, scope_user_id=None, scope_repo_id=None, scope_repo_group_id=None):
5630 5648
5631 5649 store_entry = FileStore()
5632 5650 store_entry.file_uid = file_uid
5633 5651 store_entry.file_display_name = file_display_name
5634 5652 store_entry.file_org_name = filename
5635 5653 store_entry.file_size = file_size
5636 5654 store_entry.file_hash = file_hash
5637 5655 store_entry.file_description = file_description
5638 5656
5639 5657 store_entry.check_acl = check_acl
5640 5658 store_entry.enabled = enabled
5641 5659 store_entry.hidden = hidden
5642 5660
5643 5661 store_entry.user_id = user_id
5644 5662 store_entry.scope_user_id = scope_user_id
5645 5663 store_entry.scope_repo_id = scope_repo_id
5646 5664 store_entry.scope_repo_group_id = scope_repo_group_id
5647 5665
5648 5666 return store_entry
5649 5667
5650 5668 @classmethod
5651 5669 def store_metadata(cls, file_store_id, args, commit=True):
5652 5670 file_store = FileStore.get(file_store_id)
5653 5671 if file_store is None:
5654 5672 return
5655 5673
5656 5674 for section, key, value, value_type in args:
5657 5675 has_key = FileStoreMetadata().query() \
5658 5676 .filter(FileStoreMetadata.file_store_id == file_store.file_store_id) \
5659 5677 .filter(FileStoreMetadata.file_store_meta_section == section) \
5660 5678 .filter(FileStoreMetadata.file_store_meta_key == key) \
5661 5679 .scalar()
5662 5680 if has_key:
5663 5681 msg = 'key `{}` already defined under section `{}` for this file.'\
5664 5682 .format(key, section)
5665 5683 raise ArtifactMetadataDuplicate(msg, err_section=section, err_key=key)
5666 5684
5667 5685 # NOTE(marcink): raises ArtifactMetadataBadValueType
5668 5686 FileStoreMetadata.valid_value_type(value_type)
5669 5687
5670 5688 meta_entry = FileStoreMetadata()
5671 5689 meta_entry.file_store = file_store
5672 5690 meta_entry.file_store_meta_section = section
5673 5691 meta_entry.file_store_meta_key = key
5674 5692 meta_entry.file_store_meta_value_type = value_type
5675 5693 meta_entry.file_store_meta_value = value
5676 5694
5677 5695 Session().add(meta_entry)
5678 5696
5679 5697 try:
5680 5698 if commit:
5681 5699 Session().commit()
5682 5700 except IntegrityError:
5683 5701 Session().rollback()
5684 5702 raise ArtifactMetadataDuplicate('Duplicate section/key found for this file.')
5685 5703
5686 5704 @classmethod
5687 5705 def bump_access_counter(cls, file_uid, commit=True):
5688 5706 FileStore().query()\
5689 5707 .filter(FileStore.file_uid == file_uid)\
5690 5708 .update({FileStore.accessed_count: (FileStore.accessed_count + 1),
5691 5709 FileStore.accessed_on: datetime.datetime.now()})
5692 5710 if commit:
5693 5711 Session().commit()
5694 5712
5695 5713 def __json__(self):
5696 5714 data = {
5697 5715 'filename': self.file_display_name,
5698 5716 'filename_org': self.file_org_name,
5699 5717 'file_uid': self.file_uid,
5700 5718 'description': self.file_description,
5701 5719 'hidden': self.hidden,
5702 5720 'size': self.file_size,
5703 5721 'created_on': self.created_on,
5704 5722 'uploaded_by': self.upload_user.get_api_data(details='basic'),
5705 5723 'downloaded_times': self.accessed_count,
5706 5724 'sha256': self.file_hash,
5707 5725 'metadata': self.file_metadata,
5708 5726 }
5709 5727
5710 5728 return data
5711 5729
5712 5730 def __repr__(self):
5713 5731 return f'<FileStore({self.file_store_id})>'
5714 5732
5715 5733
5716 5734 class FileStoreMetadata(Base, BaseModel):
5717 5735 __tablename__ = 'file_store_metadata'
5718 5736 __table_args__ = (
5719 5737 UniqueConstraint('file_store_id', 'file_store_meta_section_hash', 'file_store_meta_key_hash'),
5720 5738 Index('file_store_meta_section_idx', 'file_store_meta_section', mysql_length=255),
5721 5739 Index('file_store_meta_key_idx', 'file_store_meta_key', mysql_length=255),
5722 5740 base_table_args
5723 5741 )
5724 5742 SETTINGS_TYPES = {
5725 5743 'str': safe_str,
5726 5744 'int': safe_int,
5727 5745 'unicode': safe_str,
5728 5746 'bool': str2bool,
5729 5747 'list': functools.partial(aslist, sep=',')
5730 5748 }
5731 5749
5732 5750 file_store_meta_id = Column(
5733 5751 "file_store_meta_id", Integer(), nullable=False, unique=True, default=None,
5734 5752 primary_key=True)
5735 5753 _file_store_meta_section = Column(
5736 5754 "file_store_meta_section", UnicodeText().with_variant(UnicodeText(1024), 'mysql'),
5737 5755 nullable=True, unique=None, default=None)
5738 5756 _file_store_meta_section_hash = Column(
5739 5757 "file_store_meta_section_hash", String(255),
5740 5758 nullable=True, unique=None, default=None)
5741 5759 _file_store_meta_key = Column(
5742 5760 "file_store_meta_key", UnicodeText().with_variant(UnicodeText(1024), 'mysql'),
5743 5761 nullable=True, unique=None, default=None)
5744 5762 _file_store_meta_key_hash = Column(
5745 5763 "file_store_meta_key_hash", String(255), nullable=True, unique=None, default=None)
5746 5764 _file_store_meta_value = Column(
5747 5765 "file_store_meta_value", UnicodeText().with_variant(UnicodeText(20480), 'mysql'),
5748 5766 nullable=True, unique=None, default=None)
5749 5767 _file_store_meta_value_type = Column(
5750 5768 "file_store_meta_value_type", String(255), nullable=True, unique=None,
5751 5769 default='unicode')
5752 5770
5753 5771 file_store_id = Column(
5754 5772 'file_store_id', Integer(), ForeignKey('file_store.file_store_id'),
5755 5773 nullable=True, unique=None, default=None)
5756 5774
5757 5775 file_store = relationship('FileStore', lazy='joined', viewonly=True)
5758 5776
5759 5777 @classmethod
5760 5778 def valid_value_type(cls, value):
5761 5779 if value.split('.')[0] not in cls.SETTINGS_TYPES:
5762 5780 raise ArtifactMetadataBadValueType(
5763 5781 'value_type must be one of %s got %s' % (cls.SETTINGS_TYPES.keys(), value))
5764 5782
5765 5783 @hybrid_property
5766 5784 def file_store_meta_section(self):
5767 5785 return self._file_store_meta_section
5768 5786
5769 5787 @file_store_meta_section.setter
5770 5788 def file_store_meta_section(self, value):
5771 5789 self._file_store_meta_section = value
5772 5790 self._file_store_meta_section_hash = _hash_key(value)
5773 5791
5774 5792 @hybrid_property
5775 5793 def file_store_meta_key(self):
5776 5794 return self._file_store_meta_key
5777 5795
5778 5796 @file_store_meta_key.setter
5779 5797 def file_store_meta_key(self, value):
5780 5798 self._file_store_meta_key = value
5781 5799 self._file_store_meta_key_hash = _hash_key(value)
5782 5800
5783 5801 @hybrid_property
5784 5802 def file_store_meta_value(self):
5785 5803 val = self._file_store_meta_value
5786 5804
5787 5805 if self._file_store_meta_value_type:
5788 5806 # e.g unicode.encrypted == unicode
5789 5807 _type = self._file_store_meta_value_type.split('.')[0]
5790 5808 # decode the encrypted value if it's encrypted field type
5791 5809 if '.encrypted' in self._file_store_meta_value_type:
5792 5810 cipher = EncryptedTextValue()
5793 5811 val = safe_str(cipher.process_result_value(val, None))
5794 5812 # do final type conversion
5795 5813 converter = self.SETTINGS_TYPES.get(_type) or self.SETTINGS_TYPES['unicode']
5796 5814 val = converter(val)
5797 5815
5798 5816 return val
5799 5817
5800 5818 @file_store_meta_value.setter
5801 5819 def file_store_meta_value(self, val):
5802 5820 val = safe_str(val)
5803 5821 # encode the encrypted value
5804 5822 if '.encrypted' in self.file_store_meta_value_type:
5805 5823 cipher = EncryptedTextValue()
5806 5824 val = safe_str(cipher.process_bind_param(val, None))
5807 5825 self._file_store_meta_value = val
5808 5826
5809 5827 @hybrid_property
5810 5828 def file_store_meta_value_type(self):
5811 5829 return self._file_store_meta_value_type
5812 5830
5813 5831 @file_store_meta_value_type.setter
5814 5832 def file_store_meta_value_type(self, val):
5815 5833 # e.g unicode.encrypted
5816 5834 self.valid_value_type(val)
5817 5835 self._file_store_meta_value_type = val
5818 5836
5819 5837 def __json__(self):
5820 5838 data = {
5821 5839 'artifact': self.file_store.file_uid,
5822 5840 'section': self.file_store_meta_section,
5823 5841 'key': self.file_store_meta_key,
5824 5842 'value': self.file_store_meta_value,
5825 5843 }
5826 5844
5827 5845 return data
5828 5846
5829 5847 def __repr__(self):
5830 5848 return '<%s[%s]%s=>%s]>' % (self.cls_name, self.file_store_meta_section,
5831 5849 self.file_store_meta_key, self.file_store_meta_value)
5832 5850
5833 5851
5834 5852 class DbMigrateVersion(Base, BaseModel):
5835 5853 __tablename__ = 'db_migrate_version'
5836 5854 __table_args__ = (
5837 5855 base_table_args,
5838 5856 )
5839 5857
5840 5858 repository_id = Column('repository_id', String(250), primary_key=True)
5841 5859 repository_path = Column('repository_path', Text)
5842 5860 version = Column('version', Integer)
5843 5861
5844 5862 @classmethod
5845 5863 def set_version(cls, version):
5846 5864 """
5847 5865 Helper for forcing a different version, usually for debugging purposes via ishell.
5848 5866 """
5849 5867 ver = DbMigrateVersion.query().first()
5850 5868 ver.version = version
5851 5869 Session().commit()
5852 5870
5853 5871
5854 5872 class DbSession(Base, BaseModel):
5855 5873 __tablename__ = 'db_session'
5856 5874 __table_args__ = (
5857 5875 base_table_args,
5858 5876 )
5859 5877
5860 5878 def __repr__(self):
5861 5879 return f'<DB:DbSession({self.id})>'
5862 5880
5863 5881 id = Column('id', Integer())
5864 5882 namespace = Column('namespace', String(255), primary_key=True)
5865 5883 accessed = Column('accessed', DateTime, nullable=False)
5866 5884 created = Column('created', DateTime, nullable=False)
5867 5885 data = Column('data', PickleType, nullable=False)
@@ -1,1404 +1,1403 b''
1 1 <%namespace name="base" file="/base/base.mako"/>
2 2 <%namespace name="commentblock" file="/changeset/changeset_file_comment.mako"/>
3 3
4 4 <%def name="diff_line_anchor(commit, filename, line, type)"><%
5 5 return '%s_%s_%i' % (h.md5_safe(commit+filename), type, line)
6 6 %></%def>
7 7
8 8 <%def name="action_class(action)">
9 9 <%
10 10 return {
11 11 '-': 'cb-deletion',
12 12 '+': 'cb-addition',
13 13 ' ': 'cb-context',
14 14 }.get(action, 'cb-empty')
15 15 %>
16 16 </%def>
17 17
18 18 <%def name="op_class(op_id)">
19 19 <%
20 20 return {
21 21 DEL_FILENODE: 'deletion', # file deleted
22 22 BIN_FILENODE: 'warning' # binary diff hidden
23 23 }.get(op_id, 'addition')
24 24 %>
25 25 </%def>
26 26
27 27
28 28
29 29 <%def name="render_diffset(diffset, commit=None,
30 30
31 31 # collapse all file diff entries when there are more than this amount of files in the diff
32 32 collapse_when_files_over=20,
33 33
34 34 # collapse lines in the diff when more than this amount of lines changed in the file diff
35 35 lines_changed_limit=500,
36 36
37 37 # add a ruler at to the output
38 38 ruler_at_chars=0,
39 39
40 40 # show inline comments
41 41 use_comments=False,
42 42
43 43 # disable new comments
44 44 disable_new_comments=False,
45 45
46 46 # special file-comments that were deleted in previous versions
47 47 # it's used for showing outdated comments for deleted files in a PR
48 48 deleted_files_comments=None,
49 49
50 50 # for cache purpose
51 51 inline_comments=None,
52 52
53 53 # additional menu for PRs
54 54 pull_request_menu=None,
55 55
56 56 # show/hide todo next to comments
57 57 show_todos=True,
58 58
59 59 )">
60 60
61 61 <%
62 62 diffset_container_id = h.md5_safe(diffset.target_ref)
63 63 collapse_all = len(diffset.files) > collapse_when_files_over
64 64 active_pattern_entries = h.get_active_pattern_entries(getattr(c, 'repo_name', None))
65 65 from rhodecode.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
66 66 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE, COPIED_FILENODE
67 67 %>
68 68
69 69 %if use_comments:
70 70
71 71 ## Template for injecting comments
72 72 <div id="cb-comments-inline-container-template" class="js-template">
73 73 ${inline_comments_container([])}
74 74 </div>
75 75
76 76 <div class="js-template" id="cb-comment-inline-form-template">
77 77 <div class="comment-inline-form ac">
78 78 %if not c.rhodecode_user.is_default:
79 79 ## render template for inline comments
80 80 ${commentblock.comment_form(form_type='inline')}
81 81 %endif
82 82 </div>
83 83 </div>
84 84
85 85 %endif
86 86
87 87 %if c.user_session_attrs["diffmode"] == 'sideside':
88 88 <style>
89 89 .wrapper {
90 90 max-width: 1600px !important;
91 91 }
92 92 </style>
93 93 %endif
94 94
95 95 %if ruler_at_chars:
96 96 <style>
97 97 .diff table.cb .cb-content:after {
98 98 content: "";
99 99 border-left: 1px solid blue;
100 100 position: absolute;
101 101 top: 0;
102 102 height: 18px;
103 103 opacity: .2;
104 104 z-index: 10;
105 105 //## +5 to account for diff action (+/-)
106 106 left: ${ruler_at_chars + 5}ch;
107 107 </style>
108 108 %endif
109 109
110 110 <div class="diffset ${disable_new_comments and 'diffset-comments-disabled'}">
111 111
112 112 <div style="height: 20px; line-height: 20px">
113 113 ## expand/collapse action
114 114 <div class="pull-left">
115 115 <a class="${'collapsed' if collapse_all else ''}" href="#expand-files" onclick="toggleExpand(this, '${diffset_container_id}'); return false">
116 116 % if collapse_all:
117 117 <i class="icon-plus-squared-alt icon-no-margin"></i>${_('Expand all files')}
118 118 % else:
119 119 <i class="icon-minus-squared-alt icon-no-margin"></i>${_('Collapse all files')}
120 120 % endif
121 121 </a>
122 122
123 123 </div>
124 124
125 125 ## todos
126 126 % if show_todos and getattr(c, 'at_version', None):
127 127 <div class="pull-right">
128 128 <i class="icon-flag-filled" style="color: #949494">TODOs:</i>
129 129 ${_('not available in this view')}
130 130 </div>
131 131 % elif show_todos:
132 132 <div class="pull-right">
133 133 <div class="comments-number" style="padding-left: 10px">
134 134 % if hasattr(c, 'unresolved_comments') and hasattr(c, 'resolved_comments'):
135 135 <i class="icon-flag-filled" style="color: #949494">TODOs:</i>
136 136 % if c.unresolved_comments:
137 137 <a href="#show-todos" onclick="$('#todo-box').toggle(); return false">
138 138 ${_('{} unresolved').format(len(c.unresolved_comments))}
139 139 </a>
140 140 % else:
141 141 ${_('0 unresolved')}
142 142 % endif
143 143
144 144 ${_('{} Resolved').format(len(c.resolved_comments))}
145 145 % endif
146 146 </div>
147 147 </div>
148 148 % endif
149 149
150 150 ## ## comments
151 151 ## <div class="pull-right">
152 152 ## <div class="comments-number" style="padding-left: 10px">
153 153 ## % if hasattr(c, 'comments') and hasattr(c, 'inline_cnt'):
154 154 ## <i class="icon-comment" style="color: #949494">COMMENTS:</i>
155 155 ## % if c.comments:
156 156 ## <a href="#comments">${_ungettext("{} General", "{} General", len(c.comments)).format(len(c.comments))}</a>,
157 157 ## % else:
158 158 ## ${_('0 General')}
159 159 ## % endif
160 160 ##
161 161 ## % if c.inline_cnt:
162 162 ## <a href="#" onclick="return Rhodecode.comments.nextComment();"
163 163 ## id="inline-comments-counter">${_ungettext("{} Inline", "{} Inline", c.inline_cnt).format(c.inline_cnt)}
164 164 ## </a>
165 165 ## % else:
166 166 ## ${_('0 Inline')}
167 167 ## % endif
168 168 ## % endif
169 169 ##
170 170 ## % if pull_request_menu:
171 171 ## <%
172 172 ## outdated_comm_count_ver = pull_request_menu['outdated_comm_count_ver']
173 173 ## %>
174 174 ##
175 175 ## % if outdated_comm_count_ver:
176 176 ## <a href="#" onclick="showOutdated(); Rhodecode.comments.nextOutdatedComment(); return false;">
177 177 ## (${_("{} Outdated").format(outdated_comm_count_ver)})
178 178 ## </a>
179 179 ## <a href="#" class="showOutdatedComments" onclick="showOutdated(this); return false;"> | ${_('show outdated')}</a>
180 180 ## <a href="#" class="hideOutdatedComments" style="display: none" onclick="hideOutdated(this); return false;"> | ${_('hide outdated')}</a>
181 181 ## % else:
182 182 ## (${_("{} Outdated").format(outdated_comm_count_ver)})
183 183 ## % endif
184 184 ##
185 185 ## % endif
186 186 ##
187 187 ## </div>
188 188 ## </div>
189 189
190 190 </div>
191 191
192 192 % if diffset.limited_diff:
193 193 <div class="diffset-heading ${(diffset.limited_diff and 'diffset-heading-warning' or '')}">
194 194 <h2 class="clearinner">
195 195 ${_('The requested changes are too big and content was truncated.')}
196 196 <a href="${h.current_route_path(request, fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a>
197 197 </h2>
198 198 </div>
199 199 % endif
200 200
201 201 <div id="todo-box">
202 202 % if hasattr(c, 'unresolved_comments') and c.unresolved_comments:
203 203 % for co in c.unresolved_comments:
204 204 <a class="permalink" href="#comment-${co.comment_id}"
205 205 onclick="Rhodecode.comments.scrollToComment($('#comment-${co.comment_id}'))">
206 206 <i class="icon-flag-filled-red"></i>
207 207 ${co.comment_id}</a>${('' if loop.last else ',')}
208 208 % endfor
209 209 % endif
210 210 </div>
211 211 %if diffset.has_hidden_changes:
212 212 <p class="empty_data">${_('Some changes may be hidden')}</p>
213 213 %elif not diffset.files:
214 214 <p class="empty_data">${_('No files')}</p>
215 215 %endif
216 216
217 217 <div class="filediffs">
218 218
219 219 ## initial value could be marked as False later on
220 220 <% over_lines_changed_limit = False %>
221 221 %for i, filediff in enumerate(diffset.files):
222 222
223 223 %if filediff.source_file_path and filediff.target_file_path:
224 224 %if filediff.source_file_path != filediff.target_file_path:
225 225 ## file was renamed, or copied
226 226 %if RENAMED_FILENODE in filediff.patch['stats']['ops']:
227 227 <%
228 final_file_name = h.literal(u'{} <i class="icon-angle-left"></i> <del>{}</del>'.format(filediff.target_file_path, filediff.source_file_path))
228 final_file_name = h.literal('{} <i class="icon-angle-left"></i> <del>{}</del>'.format(filediff.target_file_path, filediff.source_file_path))
229 229 final_path = filediff.target_file_path
230 230 %>
231 231 %elif COPIED_FILENODE in filediff.patch['stats']['ops']:
232 232 <%
233 final_file_name = h.literal(u'{} <i class="icon-angle-left"></i> {}'.format(filediff.target_file_path, filediff.source_file_path))
233 final_file_name = h.literal('{} <i class="icon-angle-left"></i> {}'.format(filediff.target_file_path, filediff.source_file_path))
234 234 final_path = filediff.target_file_path
235 235 %>
236 236 %endif
237 237 %else:
238 238 ## file was modified
239 239 <%
240 240 final_file_name = filediff.source_file_path
241 241 final_path = final_file_name
242 242 %>
243 243 %endif
244 244 %else:
245 245 %if filediff.source_file_path:
246 246 ## file was deleted
247 247 <%
248 248 final_file_name = filediff.source_file_path
249 249 final_path = final_file_name
250 250 %>
251 251 %else:
252 252 ## file was added
253 253 <%
254 254 final_file_name = filediff.target_file_path
255 255 final_path = final_file_name
256 256 %>
257 257 %endif
258 258 %endif
259 259
260 260 <%
261 261 lines_changed = filediff.patch['stats']['added'] + filediff.patch['stats']['deleted']
262 262 over_lines_changed_limit = lines_changed > lines_changed_limit
263 263 %>
264 264 ## anchor with support of sticky header
265 265 <div class="anchor" id="a_${h.FID(filediff.raw_id, filediff.patch['filename'])}"></div>
266 266
267 267 <input ${(collapse_all and 'checked' or '')} class="filediff-collapse-state collapse-${diffset_container_id}" id="filediff-collapse-${id(filediff)}" type="checkbox" onchange="updateSticky();">
268 268 <div
269 269 class="filediff"
270 270 data-f-path="${filediff.patch['filename']}"
271 271 data-anchor-id="${h.FID(filediff.raw_id, filediff.patch['filename'])}"
272 272 >
273 273 <label for="filediff-collapse-${id(filediff)}" class="filediff-heading">
274 274 <%
275 file_comments = (get_inline_comments(inline_comments, filediff.patch['filename']) or {}).values()
275 file_comments = list((get_inline_comments(inline_comments, filediff.patch['filename']) or {}).values())
276 276 total_file_comments = [_c for _c in h.itertools.chain.from_iterable(file_comments) if not (_c.outdated or _c.draft)]
277 277 %>
278 278 <div class="filediff-collapse-indicator icon-"></div>
279 279
280 280 ## Comments/Options PILL
281 281 <span class="pill-group pull-right">
282 282 <span class="pill" op="comments">
283 283 <i class="icon-comment"></i> ${len(total_file_comments)}
284 284 </span>
285 285
286 286 <details class="details-reset details-inline-block">
287 287 <summary class="noselect">
288 288 <i class="pill icon-options cursor-pointer" op="options"></i>
289 289 </summary>
290 290 <details-menu class="details-dropdown">
291 291
292 292 <div class="dropdown-item">
293 293 <span>${final_path}</span>
294 294 <span class="pull-right icon-clipboard clipboard-action" data-clipboard-text="${final_path}" title="Copy file path"></span>
295 295 </div>
296 296
297 297 <div class="dropdown-divider"></div>
298 298
299 299 <div class="dropdown-item">
300 300 <% permalink = request.current_route_url(_anchor='a_{}'.format(h.FID(filediff.raw_id, filediff.patch['filename']))) %>
301 301 <a href="${permalink}">ΒΆ permalink</a>
302 302 <span class="pull-right icon-clipboard clipboard-action" data-clipboard-text="${permalink}" title="Copy permalink"></span>
303 303 </div>
304 304
305
306 305 </details-menu>
307 306 </details>
308 307
309 308 </span>
310 309
311 310 ${diff_ops(final_file_name, filediff)}
312 311
313 312 </label>
314 313
315 314 ${diff_menu(filediff, use_comments=use_comments)}
316 315 <table id="file-${h.safeid(h.safe_str(filediff.patch['filename']))}" data-f-path="${filediff.patch['filename']}" data-anchor-id="${h.FID(filediff.raw_id, filediff.patch['filename'])}" class="code-visible-block cb cb-diff-${c.user_session_attrs["diffmode"]} code-highlight ${(over_lines_changed_limit and 'cb-collapsed' or '')}">
317 316
318 317 ## new/deleted/empty content case
319 318 % if not filediff.hunks:
320 319 ## Comment container, on "fakes" hunk that contains all data to render comments
321 320 ${render_hunk_lines(filediff, c.user_session_attrs["diffmode"], filediff.hunk_ops, use_comments=use_comments, inline_comments=inline_comments, active_pattern_entries=active_pattern_entries)}
322 321 % endif
323 322
324 323 %if filediff.limited_diff:
325 324 <tr class="cb-warning cb-collapser">
326 325 <td class="cb-text" ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=4' or 'colspan=6')}>
327 326 ${_('The requested commit or file is too big and content was truncated.')} <a href="${h.current_route_path(request, fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a>
328 327 </td>
329 328 </tr>
330 329 %else:
331 330 %if over_lines_changed_limit:
332 331 <tr class="cb-warning cb-collapser">
333 332 <td class="cb-text" ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=4' or 'colspan=6')}>
334 333 ${_('This diff has been collapsed as it changes many lines, (%i lines changed)' % lines_changed)}
335 334 <a href="#" class="cb-expand"
336 335 onclick="$(this).closest('table').removeClass('cb-collapsed'); updateSticky(); return false;">${_('Show them')}
337 336 </a>
338 337 <a href="#" class="cb-collapse"
339 338 onclick="$(this).closest('table').addClass('cb-collapsed'); updateSticky(); return false;">${_('Hide them')}
340 339 </a>
341 340 </td>
342 341 </tr>
343 342 %endif
344 343 %endif
345 344
346 345 % for hunk in filediff.hunks:
347 346 <tr class="cb-hunk">
348 347 <td ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=3' or '')}>
349 348 ## TODO: dan: add ajax loading of more context here
350 349 ## <a href="#">
351 350 <i class="icon-more"></i>
352 351 ## </a>
353 352 </td>
354 353 <td ${(c.user_session_attrs["diffmode"] == 'sideside' and 'colspan=5' or '')}>
355 354 @@
356 355 -${hunk.source_start},${hunk.source_length}
357 356 +${hunk.target_start},${hunk.target_length}
358 357 ${hunk.section_header}
359 358 </td>
360 359 </tr>
361 360
362 361 ${render_hunk_lines(filediff, c.user_session_attrs["diffmode"], hunk, use_comments=use_comments, inline_comments=inline_comments, active_pattern_entries=active_pattern_entries)}
363 362 % endfor
364 363
365 364 <% unmatched_comments = (inline_comments or {}).get(filediff.patch['filename'], {}) %>
366 365
367 366 ## outdated comments that do not fit into currently displayed lines
368 367 % for lineno, comments in unmatched_comments.items():
369 368
370 369 %if c.user_session_attrs["diffmode"] == 'unified':
371 370 % if loop.index == 0:
372 371 <tr class="cb-hunk">
373 372 <td colspan="3"></td>
374 373 <td>
375 374 <div>
376 375 ${_('Unmatched/outdated inline comments below')}
377 376 </div>
378 377 </td>
379 378 </tr>
380 379 % endif
381 380 <tr class="cb-line">
382 381 <td class="cb-data cb-context"></td>
383 382 <td class="cb-lineno cb-context"></td>
384 383 <td class="cb-lineno cb-context"></td>
385 384 <td class="cb-content cb-context">
386 385 ${inline_comments_container(comments, active_pattern_entries=active_pattern_entries)}
387 386 </td>
388 387 </tr>
389 388 %elif c.user_session_attrs["diffmode"] == 'sideside':
390 389 % if loop.index == 0:
391 390 <tr class="cb-comment-info">
392 391 <td colspan="2"></td>
393 392 <td class="cb-line">
394 393 <div>
395 394 ${_('Unmatched/outdated inline comments below')}
396 395 </div>
397 396 </td>
398 397 <td colspan="2"></td>
399 398 <td class="cb-line">
400 399 <div>
401 400 ${_('Unmatched/outdated comments below')}
402 401 </div>
403 402 </td>
404 403 </tr>
405 404 % endif
406 405 <tr class="cb-line">
407 406 <td class="cb-data cb-context"></td>
408 407 <td class="cb-lineno cb-context"></td>
409 408 <td class="cb-content cb-context">
410 409 % if lineno.startswith('o'):
411 410 ${inline_comments_container(comments, active_pattern_entries=active_pattern_entries)}
412 411 % endif
413 412 </td>
414 413
415 414 <td class="cb-data cb-context"></td>
416 415 <td class="cb-lineno cb-context"></td>
417 416 <td class="cb-content cb-context">
418 417 % if lineno.startswith('n'):
419 418 ${inline_comments_container(comments, active_pattern_entries=active_pattern_entries)}
420 419 % endif
421 420 </td>
422 421 </tr>
423 422 %endif
424 423
425 424 % endfor
426 425
427 426 </table>
428 427 </div>
429 428 %endfor
430 429
431 430 ## outdated comments that are made for a file that has been deleted
432 431 % for filename, comments_dict in (deleted_files_comments or {}).items():
433 432
434 433 <%
435 434 display_state = 'display: none'
436 435 open_comments_in_file = [x for x in comments_dict['comments'] if x.outdated is False]
437 436 if open_comments_in_file:
438 437 display_state = ''
439 438 fid = str(id(filename))
440 439 %>
441 440 <div class="filediffs filediff-outdated" style="${display_state}">
442 441 <input ${(collapse_all and 'checked' or '')} class="filediff-collapse-state collapse-${diffset_container_id}" id="filediff-collapse-${id(filename)}" type="checkbox" onchange="updateSticky();">
443 442 <div class="filediff" data-f-path="${filename}" id="a_${h.FID(fid, filename)}">
444 443 <label for="filediff-collapse-${id(filename)}" class="filediff-heading">
445 444 <div class="filediff-collapse-indicator icon-"></div>
446 445
447 446 <span class="pill">
448 447 ## file was deleted
449 448 ${filename}
450 449 </span>
451 450 <span class="pill-group pull-left" >
452 451 ## file op, doesn't need translation
453 452 <span class="pill" op="removed">unresolved comments</span>
454 453 </span>
455 454 <a class="pill filediff-anchor" href="#a_${h.FID(fid, filename)}">ΒΆ</a>
456 455 <span class="pill-group pull-right">
457 456 <span class="pill" op="deleted">
458 457 % if comments_dict['stats'] >0:
459 458 -${comments_dict['stats']}
460 459 % else:
461 460 ${comments_dict['stats']}
462 461 % endif
463 462 </span>
464 463 </span>
465 464 </label>
466 465
467 466 <table class="cb cb-diff-${c.user_session_attrs["diffmode"]} code-highlight ${(over_lines_changed_limit and 'cb-collapsed' or '')}">
468 467 <tr>
469 468 % if c.user_session_attrs["diffmode"] == 'unified':
470 469 <td></td>
471 470 %endif
472 471
473 472 <td></td>
474 473 <td class="cb-text cb-${op_class(BIN_FILENODE)}" ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=4' or 'colspan=5')}>
475 474 <strong>${_('This file was removed from diff during updates to this pull-request.')}</strong><br/>
476 475 ${_('There are still outdated/unresolved comments attached to it.')}
477 476 </td>
478 477 </tr>
479 478 %if c.user_session_attrs["diffmode"] == 'unified':
480 479 <tr class="cb-line">
481 480 <td class="cb-data cb-context"></td>
482 481 <td class="cb-lineno cb-context"></td>
483 482 <td class="cb-lineno cb-context"></td>
484 483 <td class="cb-content cb-context">
485 484 ${inline_comments_container(comments_dict['comments'], active_pattern_entries=active_pattern_entries)}
486 485 </td>
487 486 </tr>
488 487 %elif c.user_session_attrs["diffmode"] == 'sideside':
489 488 <tr class="cb-line">
490 489 <td class="cb-data cb-context"></td>
491 490 <td class="cb-lineno cb-context"></td>
492 491 <td class="cb-content cb-context"></td>
493 492
494 493 <td class="cb-data cb-context"></td>
495 494 <td class="cb-lineno cb-context"></td>
496 495 <td class="cb-content cb-context">
497 496 ${inline_comments_container(comments_dict['comments'], active_pattern_entries=active_pattern_entries)}
498 497 </td>
499 498 </tr>
500 499 %endif
501 500 </table>
502 501 </div>
503 502 </div>
504 503 % endfor
505 504
506 505 </div>
507 506 </div>
508 507 </%def>
509 508
510 509 <%def name="diff_ops(file_name, filediff)">
511 510 <%
512 511 from rhodecode.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
513 512 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE, COPIED_FILENODE
514 513 %>
515 514 <span class="pill">
516 515 <i class="icon-file-text"></i>
517 516 ${file_name}
518 517 </span>
519 518
520 519 <span class="pill-group pull-right">
521 520
522 521 ## ops pills
523 522 %if filediff.limited_diff:
524 523 <span class="pill tooltip" op="limited" title="The stats for this diff are not complete">limited diff</span>
525 524 %endif
526 525
527 526 %if NEW_FILENODE in filediff.patch['stats']['ops']:
528 527 <span class="pill" op="created">created</span>
529 528 %if filediff['target_mode'].startswith('120'):
530 529 <span class="pill" op="symlink">symlink</span>
531 530 %else:
532 531 <span class="pill" op="mode">${nice_mode(filediff['target_mode'])}</span>
533 532 %endif
534 533 %endif
535 534
536 535 %if RENAMED_FILENODE in filediff.patch['stats']['ops']:
537 536 <span class="pill" op="renamed">renamed</span>
538 537 %endif
539 538
540 539 %if COPIED_FILENODE in filediff.patch['stats']['ops']:
541 540 <span class="pill" op="copied">copied</span>
542 541 %endif
543 542
544 543 %if DEL_FILENODE in filediff.patch['stats']['ops']:
545 544 <span class="pill" op="removed">removed</span>
546 545 %endif
547 546
548 547 %if CHMOD_FILENODE in filediff.patch['stats']['ops']:
549 548 <span class="pill" op="mode">
550 549 ${nice_mode(filediff['source_mode'])} ➑ ${nice_mode(filediff['target_mode'])}
551 550 </span>
552 551 %endif
553 552
554 553 %if BIN_FILENODE in filediff.patch['stats']['ops']:
555 554 <span class="pill" op="binary">binary</span>
556 555 %if MOD_FILENODE in filediff.patch['stats']['ops']:
557 556 <span class="pill" op="modified">modified</span>
558 557 %endif
559 558 %endif
560 559
561 560 <span class="pill" op="added">${('+' if filediff.patch['stats']['added'] else '')}${filediff.patch['stats']['added']}</span>
562 561 <span class="pill" op="deleted">${((h.safe_int(filediff.patch['stats']['deleted']) or 0) * -1)}</span>
563 562
564 563 </span>
565 564
566 565 </%def>
567 566
568 567 <%def name="nice_mode(filemode)">
569 568 ${(filemode.startswith('100') and filemode[3:] or filemode)}
570 569 </%def>
571 570
572 571 <%def name="diff_menu(filediff, use_comments=False)">
573 572 <div class="filediff-menu">
574 573
575 574 %if filediff.diffset.source_ref:
576 575
577 576 ## FILE BEFORE CHANGES
578 577 %if filediff.operation in ['D', 'M']:
579 578 <a
580 579 class="tooltip"
581 580 href="${h.route_path('repo_files',repo_name=filediff.diffset.target_repo_name,commit_id=filediff.diffset.source_ref,f_path=filediff.source_file_path)}"
582 581 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
583 582 >
584 583 ${_('Show file before')}
585 584 </a> |
586 585 %else:
587 586 <span
588 587 class="tooltip"
589 588 title="${h.tooltip(_('File not present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
590 589 >
591 590 ${_('Show file before')}
592 591 </span> |
593 592 %endif
594 593
595 594 ## FILE AFTER CHANGES
596 595 %if filediff.operation in ['A', 'M']:
597 596 <a
598 597 class="tooltip"
599 598 href="${h.route_path('repo_files',repo_name=filediff.diffset.source_repo_name,commit_id=filediff.diffset.target_ref,f_path=filediff.target_file_path)}"
600 599 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
601 600 >
602 601 ${_('Show file after')}
603 602 </a>
604 603 %else:
605 604 <span
606 605 class="tooltip"
607 606 title="${h.tooltip(_('File not present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
608 607 >
609 608 ${_('Show file after')}
610 609 </span>
611 610 %endif
612 611
613 612 % if use_comments:
614 613 |
615 614 <a href="#" onclick="Rhodecode.comments.toggleDiffComments(this);return toggleElement(this)"
616 615 data-toggle-on="${_('Hide comments')}"
617 616 data-toggle-off="${_('Show comments')}">
618 617 <span class="hide-comment-button">${_('Hide comments')}</span>
619 618 </a>
620 619 % endif
621 620
622 621 %endif
623 622
624 623 </div>
625 624 </%def>
626 625
627 626
628 627 <%def name="inline_comments_container(comments, active_pattern_entries=None, line_no='', f_path='')">
629 628
630 629 <div class="inline-comments">
631 630 %for comment in comments:
632 631 ${commentblock.comment_block(comment, inline=True, active_pattern_entries=active_pattern_entries)}
633 632 %endfor
634 633
635 634 <%
636 635 extra_class = ''
637 636 extra_style = ''
638 637
639 638 if comments and comments[-1].outdated_at_version(c.at_version_num):
640 639 extra_class = ' comment-outdated'
641 640 extra_style = 'display: none;'
642 641
643 642 %>
644 643
645 644 <div class="reply-thread-container-wrapper${extra_class}" style="${extra_style}">
646 645 <div class="reply-thread-container${extra_class}">
647 646 <div class="reply-thread-gravatar">
648 647 % if c.rhodecode_user.username != h.DEFAULT_USER:
649 648 ${base.gravatar(c.rhodecode_user.email, 20, tooltip=True, user=c.rhodecode_user)}
650 649 % endif
651 650 </div>
652 651
653 652 <div class="reply-thread-reply-button">
654 653 % if c.rhodecode_user.username != h.DEFAULT_USER:
655 654 ## initial reply button, some JS logic can append here a FORM to leave a first comment.
656 655 <button class="cb-comment-add-button" onclick="return Rhodecode.comments.createComment(this, '${f_path}', '${line_no}', null)">Reply...</button>
657 656 % endif
658 657 </div>
659 658 ##% endif
660 659 <div class="reply-thread-last"></div>
661 660 </div>
662 661 </div>
663 662 </div>
664 663
665 664 </%def>
666 665
667 666 <%!
668 667
669 668 def get_inline_comments(comments, filename):
670 if hasattr(filename, 'unicode_path'):
671 filename = filename.unicode_path
669 if hasattr(filename, 'str_path'):
670 filename = filename.str_path
672 671
673 672 if not isinstance(filename, str):
674 673 return None
675 674
676 675 if comments and filename in comments:
677 676 return comments[filename]
678 677
679 678 return None
680 679
681 680 def get_comments_for(diff_type, comments, filename, line_version, line_number):
682 if hasattr(filename, 'unicode_path'):
683 filename = filename.unicode_path
681 if hasattr(filename, 'str_path'):
682 filename = filename.str_path
684 683
685 684 if not isinstance(filename, str):
686 685 return None
687 686
688 687 file_comments = get_inline_comments(comments, filename)
689 688 if file_comments is None:
690 689 return None
691 690
692 line_key = '{}{}'.format(line_version, line_number) ## e.g o37, n12
691 line_key = f'{line_version}{line_number}' ## e.g o37, n12
693 692 if line_key in file_comments:
694 693 data = file_comments.pop(line_key)
695 694 return data
696 695 %>
697 696
698 697 <%def name="render_hunk_lines_sideside(filediff, hunk, use_comments=False, inline_comments=None, active_pattern_entries=None)">
699 698
700 699 <% chunk_count = 1 %>
701 700 %for loop_obj, item in h.looper(hunk.sideside):
702 701 <%
703 702 line = item
704 703 i = loop_obj.index
705 704 prev_line = loop_obj.previous
706 705 old_line_anchor, new_line_anchor = None, None
707 706
708 707 if line.original.lineno:
709 708 old_line_anchor = diff_line_anchor(filediff.raw_id, hunk.source_file_path, line.original.lineno, 'o')
710 709 if line.modified.lineno:
711 710 new_line_anchor = diff_line_anchor(filediff.raw_id, hunk.target_file_path, line.modified.lineno, 'n')
712 711
713 712 line_action = line.modified.action or line.original.action
714 713 prev_line_action = prev_line and (prev_line.modified.action or prev_line.original.action)
715 714 %>
716 715
717 716 <tr class="cb-line">
718 717 <td class="cb-data ${action_class(line.original.action)}"
719 718 data-line-no="${line.original.lineno}"
720 719 >
721 720
722 721 <% line_old_comments, line_old_comments_no_drafts = None, None %>
723 722 %if line.original.get_comment_args:
724 723 <%
725 724 line_old_comments = get_comments_for('side-by-side', inline_comments, *line.original.get_comment_args)
726 725 line_old_comments_no_drafts = [c for c in line_old_comments if not c.draft] if line_old_comments else []
727 726 has_outdated = any([x.outdated for x in line_old_comments_no_drafts])
728 727 %>
729 728 %endif
730 729 %if line_old_comments_no_drafts:
731 730 % if has_outdated:
732 731 <i class="tooltip toggle-comment-action icon-comment-toggle" title="${_('Comments including outdated: {}. Click here to toggle them.').format(len(line_old_comments_no_drafts))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
733 732 % else:
734 733 <i class="tooltip toggle-comment-action icon-comment" title="${_('Comments: {}. Click to toggle them.').format(len(line_old_comments_no_drafts))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
735 734 % endif
736 735 %endif
737 736 </td>
738 737 <td class="cb-lineno ${action_class(line.original.action)}"
739 738 data-line-no="${line.original.lineno}"
740 739 %if old_line_anchor:
741 740 id="${old_line_anchor}"
742 741 %endif
743 742 >
744 743 %if line.original.lineno:
745 744 <a name="${old_line_anchor}" href="#${old_line_anchor}">${line.original.lineno}</a>
746 745 %endif
747 746 </td>
748 747
749 748 <% line_no = 'o{}'.format(line.original.lineno) %>
750 749 <td class="cb-content ${action_class(line.original.action)}"
751 750 data-line-no="${line_no}"
752 751 >
753 752 %if use_comments and line.original.lineno:
754 753 ${render_add_comment_button(line_no=line_no, f_path=filediff.patch['filename'])}
755 754 %endif
756 755 <span class="cb-code"><span class="cb-action ${action_class(line.original.action)}"></span>${line.original.content or '' | n}</span>
757 756
758 757 %if use_comments and line.original.lineno and line_old_comments:
759 758 ${inline_comments_container(line_old_comments, active_pattern_entries=active_pattern_entries, line_no=line_no, f_path=filediff.patch['filename'])}
760 759 %endif
761 760
762 761 </td>
763 762 <td class="cb-data ${action_class(line.modified.action)}"
764 763 data-line-no="${line.modified.lineno}"
765 764 >
766 765 <div>
767 766
768 767 <% line_new_comments, line_new_comments_no_drafts = None, None %>
769 768 %if line.modified.get_comment_args:
770 769 <%
771 770 line_new_comments = get_comments_for('side-by-side', inline_comments, *line.modified.get_comment_args)
772 771 line_new_comments_no_drafts = [c for c in line_new_comments if not c.draft] if line_new_comments else []
773 772 has_outdated = any([x.outdated for x in line_new_comments_no_drafts])
774 773 %>
775 774 %endif
776 775
777 776 %if line_new_comments_no_drafts:
778 777 % if has_outdated:
779 778 <i class="tooltip toggle-comment-action icon-comment-toggle" title="${_('Comments including outdated: {}. Click here to toggle them.').format(len(line_new_comments_no_drafts))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
780 779 % else:
781 780 <i class="tooltip toggle-comment-action icon-comment" title="${_('Comments: {}. Click to toggle them.').format(len(line_new_comments_no_drafts))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
782 781 % endif
783 782 %endif
784 783 </div>
785 784 </td>
786 785 <td class="cb-lineno ${action_class(line.modified.action)}"
787 786 data-line-no="${line.modified.lineno}"
788 787 %if new_line_anchor:
789 788 id="${new_line_anchor}"
790 789 %endif
791 790 >
792 791 %if line.modified.lineno:
793 792 <a name="${new_line_anchor}" href="#${new_line_anchor}">${line.modified.lineno}</a>
794 793 %endif
795 794 </td>
796 795
797 796 <% line_no = 'n{}'.format(line.modified.lineno) %>
798 797 <td class="cb-content ${action_class(line.modified.action)}"
799 798 data-line-no="${line_no}"
800 799 >
801 800 %if use_comments and line.modified.lineno:
802 801 ${render_add_comment_button(line_no=line_no, f_path=filediff.patch['filename'])}
803 802 %endif
804 803 <span class="cb-code"><span class="cb-action ${action_class(line.modified.action)}"></span>${line.modified.content or '' | n}</span>
805 804 % if line_action in ['+', '-'] and prev_line_action not in ['+', '-']:
806 805 <div class="nav-chunk" style="visibility: hidden">
807 806 <i class="icon-eye" title="viewing diff hunk-${hunk.index}-${chunk_count}"></i>
808 807 </div>
809 808 <% chunk_count +=1 %>
810 809 % endif
811 810 %if use_comments and line.modified.lineno and line_new_comments:
812 811 ${inline_comments_container(line_new_comments, active_pattern_entries=active_pattern_entries, line_no=line_no, f_path=filediff.patch['filename'])}
813 812 %endif
814 813
815 814 </td>
816 815 </tr>
817 816 %endfor
818 817 </%def>
819 818
820 819
821 820 <%def name="render_hunk_lines_unified(filediff, hunk, use_comments=False, inline_comments=None, active_pattern_entries=None)">
822 821 %for old_line_no, new_line_no, action, content, comments_args in hunk.unified:
823 822
824 823 <%
825 824 old_line_anchor, new_line_anchor = None, None
826 825 if old_line_no:
827 826 old_line_anchor = diff_line_anchor(filediff.raw_id, hunk.source_file_path, old_line_no, 'o')
828 827 if new_line_no:
829 828 new_line_anchor = diff_line_anchor(filediff.raw_id, hunk.target_file_path, new_line_no, 'n')
830 829 %>
831 830 <tr class="cb-line">
832 831 <td class="cb-data ${action_class(action)}">
833 832 <div>
834 833
835 834 <% comments, comments_no_drafts = None, None %>
836 835 %if comments_args:
837 836 <%
838 837 comments = get_comments_for('unified', inline_comments, *comments_args)
839 838 comments_no_drafts = [c for c in line_new_comments if not c.draft] if line_new_comments else []
840 839 has_outdated = any([x.outdated for x in comments_no_drafts])
841 840 %>
842 841 %endif
843 842
844 843 % if comments_no_drafts:
845 844 % if has_outdated:
846 845 <i class="tooltip toggle-comment-action icon-comment-toggle" title="${_('Comments including outdated: {}. Click here to toggle them.').format(len(comments_no_drafts))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
847 846 % else:
848 847 <i class="tooltip toggle-comment-action icon-comment" title="${_('Comments: {}. Click to toggle them.').format(len(comments_no_drafts))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
849 848 % endif
850 849 % endif
851 850 </div>
852 851 </td>
853 852 <td class="cb-lineno ${action_class(action)}"
854 853 data-line-no="${old_line_no}"
855 854 %if old_line_anchor:
856 855 id="${old_line_anchor}"
857 856 %endif
858 857 >
859 858 %if old_line_anchor:
860 859 <a name="${old_line_anchor}" href="#${old_line_anchor}">${old_line_no}</a>
861 860 %endif
862 861 </td>
863 862 <td class="cb-lineno ${action_class(action)}"
864 863 data-line-no="${new_line_no}"
865 864 %if new_line_anchor:
866 865 id="${new_line_anchor}"
867 866 %endif
868 867 >
869 868 %if new_line_anchor:
870 869 <a name="${new_line_anchor}" href="#${new_line_anchor}">${new_line_no}</a>
871 870 %endif
872 871 </td>
873 872 <% line_no = '{}{}'.format(new_line_no and 'n' or 'o', new_line_no or old_line_no) %>
874 873 <td class="cb-content ${action_class(action)}"
875 874 data-line-no="${line_no}"
876 875 >
877 876 %if use_comments:
878 877 ${render_add_comment_button(line_no=line_no, f_path=filediff.patch['filename'])}
879 878 %endif
880 879 <span class="cb-code"><span class="cb-action ${action_class(action)}"></span> ${content or '' | n}</span>
881 880 %if use_comments and comments:
882 881 ${inline_comments_container(comments, active_pattern_entries=active_pattern_entries, line_no=line_no, f_path=filediff.patch['filename'])}
883 882 %endif
884 883 </td>
885 884 </tr>
886 885 %endfor
887 886 </%def>
888 887
889 888
890 889 <%def name="render_hunk_lines(filediff, diff_mode, hunk, use_comments, inline_comments, active_pattern_entries)">
891 890 % if diff_mode == 'unified':
892 891 ${render_hunk_lines_unified(filediff, hunk, use_comments=use_comments, inline_comments=inline_comments, active_pattern_entries=active_pattern_entries)}
893 892 % elif diff_mode == 'sideside':
894 893 ${render_hunk_lines_sideside(filediff, hunk, use_comments=use_comments, inline_comments=inline_comments, active_pattern_entries=active_pattern_entries)}
895 894 % else:
896 895 <tr class="cb-line">
897 896 <td>unknown diff mode</td>
898 897 </tr>
899 898 % endif
900 899 </%def>file changes
901 900
902 901
903 902 <%def name="render_add_comment_button(line_no='', f_path='')">
904 903 % if not c.rhodecode_user.is_default:
905 904 <button class="btn btn-small btn-primary cb-comment-box-opener" onclick="return Rhodecode.comments.createComment(this, '${f_path}', '${line_no}', null)">
906 905 <span><i class="icon-comment"></i></span>
907 906 </button>
908 907 % endif
909 908 </%def>
910 909
911 910 <%def name="render_diffset_menu(diffset, range_diff_on=None, commit=None, pull_request_menu=None)">
912 911 <% diffset_container_id = h.md5_safe(diffset.target_ref) %>
913 912
914 913 <div id="diff-file-sticky" class="diffset-menu clearinner">
915 914 ## auto adjustable
916 915 <div class="sidebar__inner">
917 916 <div class="sidebar__bar">
918 917 <div class="pull-right">
919 918
920 919 <div class="btn-group" style="margin-right: 5px;">
921 920 <a class="tooltip btn" onclick="scrollDown();return false" title="${_('Scroll to page bottom')}">
922 921 <i class="icon-arrow_down"></i>
923 922 </a>
924 923 <a class="tooltip btn" onclick="scrollUp();return false" title="${_('Scroll to page top')}">
925 924 <i class="icon-arrow_up"></i>
926 925 </a>
927 926 </div>
928 927
929 928 <div class="btn-group">
930 929 <a class="btn tooltip toggle-wide-diff" href="#toggle-wide-diff" onclick="toggleWideDiff(this); return false" title="${h.tooltip(_('Toggle wide diff'))}">
931 930 <i class="icon-wide-mode"></i>
932 931 </a>
933 932 </div>
934 933 <div class="btn-group">
935 934
936 935 <a
937 936 class="btn ${(c.user_session_attrs["diffmode"] == 'sideside' and 'btn-active')} tooltip"
938 937 title="${h.tooltip(_('View diff as side by side'))}"
939 938 href="${h.current_route_path(request, diffmode='sideside')}">
940 939 <span>${_('Side by Side')}</span>
941 940 </a>
942 941
943 942 <a
944 943 class="btn ${(c.user_session_attrs["diffmode"] == 'unified' and 'btn-active')} tooltip"
945 944 title="${h.tooltip(_('View diff as unified'))}" href="${h.current_route_path(request, diffmode='unified')}">
946 945 <span>${_('Unified')}</span>
947 946 </a>
948 947
949 948 % if range_diff_on is True:
950 949 <a
951 950 title="${_('Turn off: Show the diff as commit range')}"
952 951 class="btn btn-primary"
953 952 href="${h.current_route_path(request, **{"range-diff":"0"})}">
954 953 <span>${_('Range Diff')}</span>
955 954 </a>
956 955 % elif range_diff_on is False:
957 956 <a
958 957 title="${_('Show the diff as commit range')}"
959 958 class="btn"
960 959 href="${h.current_route_path(request, **{"range-diff":"1"})}">
961 960 <span>${_('Range Diff')}</span>
962 961 </a>
963 962 % endif
964 963 </div>
965 964 <div class="btn-group">
966 965
967 966 <details class="details-reset details-inline-block">
968 967 <summary class="noselect btn">
969 968 <i class="icon-options cursor-pointer" op="options"></i>
970 969 </summary>
971 970
972 971 <div>
973 972 <details-menu class="details-dropdown" style="top: 35px;">
974 973
975 974 <div class="dropdown-item">
976 975 <div style="padding: 2px 0px">
977 976 % if request.GET.get('ignorews', '') == '1':
978 977 <a href="${h.current_route_path(request, ignorews=0)}">${_('Show whitespace changes')}</a>
979 978 % else:
980 979 <a href="${h.current_route_path(request, ignorews=1)}">${_('Hide whitespace changes')}</a>
981 980 % endif
982 981 </div>
983 982 </div>
984 983
985 984 <div class="dropdown-item">
986 985 <div style="padding: 2px 0px">
987 986 % if request.GET.get('fullcontext', '') == '1':
988 987 <a href="${h.current_route_path(request, fullcontext=0)}">${_('Hide full context diff')}</a>
989 988 % else:
990 989 <a href="${h.current_route_path(request, fullcontext=1)}">${_('Show full context diff')}</a>
991 990 % endif
992 991 </div>
993 992 </div>
994 993
995 994 </details-menu>
996 995 </div>
997 996 </details>
998 997
999 998 </div>
1000 999 </div>
1001 1000 <div class="pull-left">
1002 1001 <div class="btn-group">
1003 1002 <div class="pull-left">
1004 1003 ${h.hidden('file_filter_{}'.format(diffset_container_id))}
1005 1004 </div>
1006 1005
1007 1006 </div>
1008 1007 </div>
1009 1008 </div>
1010 1009 <div class="fpath-placeholder pull-left">
1011 1010 <i class="icon-file-text"></i>
1012 1011 <strong class="fpath-placeholder-text">
1013 1012 Context file:
1014 1013 </strong>
1015 1014 </div>
1016 1015 <div class="pull-right noselect">
1017 1016 %if commit:
1018 1017 <span>
1019 1018 <code>${h.show_id(commit)}</code>
1020 1019 </span>
1021 1020 %elif pull_request_menu and pull_request_menu.get('pull_request'):
1022 1021 <span>
1023 1022 <code>!${pull_request_menu['pull_request'].pull_request_id}</code>
1024 1023 </span>
1025 1024 %endif
1026 1025 % if commit or pull_request_menu:
1027 1026 <span class="tooltip" title="Navigate to previous or next change inside files." id="diff_nav">Loading diff...:</span>
1028 1027 <span class="cursor-pointer" onclick="scrollToPrevChunk(); return false">
1029 1028 <i class="icon-angle-up"></i>
1030 1029 </span>
1031 1030 <span class="cursor-pointer" onclick="scrollToNextChunk(); return false">
1032 1031 <i class="icon-angle-down"></i>
1033 1032 </span>
1034 1033 % endif
1035 1034 </div>
1036 1035 <div class="sidebar_inner_shadow"></div>
1037 1036 </div>
1038 1037 </div>
1039 1038
1040 1039 % if diffset:
1041 1040 %if diffset.limited_diff:
1042 1041 <% file_placeholder = _ungettext('%(num)s file changed', '%(num)s files changed', diffset.changed_files) % {'num': diffset.changed_files} %>
1043 1042 %else:
1044 1043 <% file_placeholder = h.literal(_ungettext('%(num)s file changed: <span class="op-added">%(linesadd)s inserted</span>, <span class="op-deleted">%(linesdel)s deleted</span>', '%(num)s files changed: <span class="op-added">%(linesadd)s inserted</span>, <span class="op-deleted">%(linesdel)s deleted</span>',
1045 1044 diffset.changed_files) % {'num': diffset.changed_files, 'linesadd': diffset.lines_added, 'linesdel': diffset.lines_deleted}) %>
1046 1045
1047 1046 %endif
1048 1047 ## case on range-diff placeholder needs to be updated
1049 1048 % if range_diff_on is True:
1050 1049 <% file_placeholder = _('Disabled on range diff') %>
1051 1050 % endif
1052 1051
1053 1052 <script type="text/javascript">
1054 1053 var feedFilesOptions = function (query, initialData) {
1055 1054 var data = {results: []};
1056 1055 var isQuery = typeof query.term !== 'undefined';
1057 1056
1058 1057 var section = _gettext('Changed files');
1059 1058 var filteredData = [];
1060 1059
1061 1060 //filter results
1062 1061 $.each(initialData.results, function (idx, value) {
1063 1062
1064 1063 if (!isQuery || query.term.length === 0 || value.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0) {
1065 1064 filteredData.push({
1066 1065 'id': this.id,
1067 1066 'text': this.text,
1068 1067 "ops": this.ops,
1069 1068 })
1070 1069 }
1071 1070
1072 1071 });
1073 1072
1074 1073 data.results = filteredData;
1075 1074
1076 1075 query.callback(data);
1077 1076 };
1078 1077
1079 1078 var selectionFormatter = function(data, escapeMarkup) {
1080 1079 var container = '<div class="filelist" style="padding-right:100px">{0}</div>';
1081 1080 var tmpl = '<div><strong>{0}</strong></div>'.format(escapeMarkup(data['text']));
1082 1081 var pill = '<div class="pill-group" style="position: absolute; top:7px; right: 0">' +
1083 1082 '<span class="pill" op="added">{0}</span>' +
1084 1083 '<span class="pill" op="deleted">{1}</span>' +
1085 1084 '</div>'
1086 1085 ;
1087 1086 var added = data['ops']['added'];
1088 1087 if (added === 0) {
1089 1088 // don't show +0
1090 1089 added = 0;
1091 1090 } else {
1092 1091 added = '+' + added;
1093 1092 }
1094 1093
1095 1094 var deleted = -1*data['ops']['deleted'];
1096 1095
1097 1096 tmpl += pill.format(added, deleted);
1098 1097 return container.format(tmpl);
1099 1098 };
1100 1099 var formatFileResult = function(result, container, query, escapeMarkup) {
1101 1100 return selectionFormatter(result, escapeMarkup);
1102 1101 };
1103 1102
1104 1103 var formatSelection = function (data, container) {
1105 1104 return '${file_placeholder}'
1106 1105 };
1107 1106
1108 1107 if (window.preloadFileFilterData === undefined) {
1109 1108 window.preloadFileFilterData = {}
1110 1109 }
1111 1110
1112 1111 preloadFileFilterData["${diffset_container_id}"] = {
1113 1112 results: [
1114 1113 % for filediff in diffset.files:
1115 1114 {id:"a_${h.FID(filediff.raw_id, filediff.patch['filename'])}",
1116 1115 text:"${filediff.patch['filename']}",
1117 1116 ops:${h.str_json(filediff.patch['stats'])|n}}${('' if loop.last else ',')}
1118 1117 % endfor
1119 1118 ]
1120 1119 };
1121 1120
1122 1121 var diffFileFilterId = "#file_filter_" + "${diffset_container_id}";
1123 1122 var diffFileFilter = $(diffFileFilterId).select2({
1124 1123 'dropdownAutoWidth': true,
1125 1124 'width': 'auto',
1126 1125
1127 1126 containerCssClass: "drop-menu",
1128 1127 dropdownCssClass: "drop-menu-dropdown",
1129 1128 data: preloadFileFilterData["${diffset_container_id}"],
1130 1129 query: function(query) {
1131 1130 feedFilesOptions(query, preloadFileFilterData["${diffset_container_id}"]);
1132 1131 },
1133 1132 initSelection: function(element, callback) {
1134 1133 callback({'init': true});
1135 1134 },
1136 1135 formatResult: formatFileResult,
1137 1136 formatSelection: formatSelection
1138 1137 });
1139 1138
1140 1139 % if range_diff_on is True:
1141 1140 diffFileFilter.select2("enable", false);
1142 1141 % endif
1143 1142
1144 1143 $(diffFileFilterId).on('select2-selecting', function (e) {
1145 1144 var idSelector = e.choice.id;
1146 1145
1147 1146 // expand the container if we quick-select the field
1148 1147 $('#'+idSelector).next().prop('checked', false);
1149 1148 // hide the mast as we later do preventDefault()
1150 1149 $("#select2-drop-mask").click();
1151 1150
1152 1151 window.location.hash = '#'+idSelector;
1153 1152 updateSticky();
1154 1153
1155 1154 e.preventDefault();
1156 1155 });
1157 1156
1158 1157 diffNavText = 'diff navigation:'
1159 1158
1160 1159 getCurrentChunk = function () {
1161 1160
1162 1161 var chunksAll = $('.nav-chunk').filter(function () {
1163 1162 return $(this).parents('.filediff').prev().get(0).checked !== true
1164 1163 })
1165 1164 var chunkSelected = $('.nav-chunk.selected');
1166 1165 var initial = false;
1167 1166
1168 1167 if (chunkSelected.length === 0) {
1169 1168 // no initial chunk selected, we pick first
1170 1169 chunkSelected = $(chunksAll.get(0));
1171 1170 var initial = true;
1172 1171 }
1173 1172
1174 1173 return {
1175 1174 'all': chunksAll,
1176 1175 'selected': chunkSelected,
1177 1176 'initial': initial,
1178 1177 }
1179 1178 }
1180 1179
1181 1180 animateDiffNavText = function () {
1182 1181 var $diffNav = $('#diff_nav')
1183 1182
1184 1183 var callback = function () {
1185 1184 $diffNav.animate({'opacity': 1.00}, 200)
1186 1185 };
1187 1186 $diffNav.animate({'opacity': 0.15}, 200, callback);
1188 1187 }
1189 1188
1190 1189 scrollToChunk = function (moveBy) {
1191 1190 var chunk = getCurrentChunk();
1192 1191 var all = chunk.all
1193 1192 var selected = chunk.selected
1194 1193
1195 1194 var curPos = all.index(selected);
1196 1195 var newPos = curPos;
1197 1196 if (!chunk.initial) {
1198 1197 var newPos = curPos + moveBy;
1199 1198 }
1200 1199
1201 1200 var curElem = all.get(newPos);
1202 1201
1203 1202 if (curElem === undefined) {
1204 1203 // end or back
1205 1204 $('#diff_nav').html('no next diff element:')
1206 1205 animateDiffNavText()
1207 1206 return
1208 1207 } else if (newPos < 0) {
1209 1208 $('#diff_nav').html('no previous diff element:')
1210 1209 animateDiffNavText()
1211 1210 return
1212 1211 } else {
1213 1212 $('#diff_nav').html(diffNavText)
1214 1213 }
1215 1214
1216 1215 curElem = $(curElem)
1217 1216 var offset = 100;
1218 1217 $(window).scrollTop(curElem.position().top - offset);
1219 1218
1220 1219 //clear selection
1221 1220 all.removeClass('selected')
1222 1221 curElem.addClass('selected')
1223 1222 }
1224 1223
1225 1224 scrollToPrevChunk = function () {
1226 1225 scrollToChunk(-1)
1227 1226 }
1228 1227 scrollToNextChunk = function () {
1229 1228 scrollToChunk(1)
1230 1229 }
1231 1230
1232 1231 </script>
1233 1232 % endif
1234 1233
1235 1234 <script type="text/javascript">
1236 1235 $('#diff_nav').html('loading diff...') // wait until whole page is loaded
1237 1236
1238 1237 $(document).ready(function () {
1239 1238
1240 1239 var contextPrefix = _gettext('Context file: ');
1241 1240 ## sticky sidebar
1242 1241 var sidebarElement = document.getElementById('diff-file-sticky');
1243 1242 sidebar = new StickySidebar(sidebarElement, {
1244 1243 topSpacing: 0,
1245 1244 bottomSpacing: 0,
1246 1245 innerWrapperSelector: '.sidebar__inner'
1247 1246 });
1248 1247 sidebarElement.addEventListener('affixed.static.stickySidebar', function () {
1249 1248 // reset our file so it's not holding new value
1250 1249 $('.fpath-placeholder-text').html(contextPrefix + ' - ')
1251 1250 });
1252 1251
1253 1252 updateSticky = function () {
1254 1253 sidebar.updateSticky();
1255 1254 Waypoint.refreshAll();
1256 1255 };
1257 1256
1258 1257 var animateText = function (fPath, anchorId) {
1259 1258 fPath = Select2.util.escapeMarkup(fPath);
1260 1259 $('.fpath-placeholder-text').html(contextPrefix + '<a href="#a_' + anchorId + '">' + fPath + '</a>')
1261 1260 };
1262 1261
1263 1262 ## dynamic file waypoints
1264 1263 var setFPathInfo = function(fPath, anchorId){
1265 1264 animateText(fPath, anchorId)
1266 1265 };
1267 1266
1268 1267 var codeBlock = $('.filediff');
1269 1268
1270 1269 // forward waypoint
1271 1270 codeBlock.waypoint(
1272 1271 function(direction) {
1273 1272 if (direction === "down"){
1274 1273 setFPathInfo($(this.element).data('fPath'), $(this.element).data('anchorId'))
1275 1274 }
1276 1275 }, {
1277 1276 offset: function () {
1278 1277 return 70;
1279 1278 },
1280 1279 context: '.fpath-placeholder'
1281 1280 }
1282 1281 );
1283 1282
1284 1283 // backward waypoint
1285 1284 codeBlock.waypoint(
1286 1285 function(direction) {
1287 1286 if (direction === "up"){
1288 1287 setFPathInfo($(this.element).data('fPath'), $(this.element).data('anchorId'))
1289 1288 }
1290 1289 }, {
1291 1290 offset: function () {
1292 1291 return -this.element.clientHeight + 90;
1293 1292 },
1294 1293 context: '.fpath-placeholder'
1295 1294 }
1296 1295 );
1297 1296
1298 1297 toggleWideDiff = function (el) {
1299 1298 updateSticky();
1300 1299 var wide = Rhodecode.comments.toggleWideMode(this);
1301 1300 storeUserSessionAttr('rc_user_session_attr.wide_diff_mode', wide);
1302 1301 if (wide === true) {
1303 1302 $(el).addClass('btn-active');
1304 1303 } else {
1305 1304 $(el).removeClass('btn-active');
1306 1305 }
1307 1306 return null;
1308 1307 };
1309 1308
1310 1309 toggleExpand = function (el, diffsetEl) {
1311 1310 var el = $(el);
1312 1311 if (el.hasClass('collapsed')) {
1313 1312 $('.filediff-collapse-state.collapse-{0}'.format(diffsetEl)).prop('checked', false);
1314 1313 el.removeClass('collapsed');
1315 1314 el.html(
1316 1315 '<i class="icon-minus-squared-alt icon-no-margin"></i>' +
1317 1316 _gettext('Collapse all files'));
1318 1317 }
1319 1318 else {
1320 1319 $('.filediff-collapse-state.collapse-{0}'.format(diffsetEl)).prop('checked', true);
1321 1320 el.addClass('collapsed');
1322 1321 el.html(
1323 1322 '<i class="icon-plus-squared-alt icon-no-margin"></i>' +
1324 1323 _gettext('Expand all files'));
1325 1324 }
1326 1325 updateSticky()
1327 1326 };
1328 1327
1329 1328 toggleCommitExpand = function (el) {
1330 1329 var $el = $(el);
1331 1330 var commits = $el.data('toggleCommitsCnt');
1332 1331 var collapseMsg = _ngettext('Collapse {0} commit', 'Collapse {0} commits', commits).format(commits);
1333 1332 var expandMsg = _ngettext('Expand {0} commit', 'Expand {0} commits', commits).format(commits);
1334 1333
1335 1334 if ($el.hasClass('collapsed')) {
1336 1335 $('.compare_select').show();
1337 1336 $('.compare_select_hidden').hide();
1338 1337
1339 1338 $el.removeClass('collapsed');
1340 1339 $el.html(
1341 1340 '<i class="icon-minus-squared-alt icon-no-margin"></i>' +
1342 1341 collapseMsg);
1343 1342 }
1344 1343 else {
1345 1344 $('.compare_select').hide();
1346 1345 $('.compare_select_hidden').show();
1347 1346 $el.addClass('collapsed');
1348 1347 $el.html(
1349 1348 '<i class="icon-plus-squared-alt icon-no-margin"></i>' +
1350 1349 expandMsg);
1351 1350 }
1352 1351 updateSticky();
1353 1352 };
1354 1353
1355 1354 // get stored diff mode and pre-enable it
1356 1355 if (templateContext.session_attrs.wide_diff_mode === "true") {
1357 1356 Rhodecode.comments.toggleWideMode(null);
1358 1357 $('.toggle-wide-diff').addClass('btn-active');
1359 1358 updateSticky();
1360 1359 }
1361 1360
1362 1361 // DIFF NAV //
1363 1362
1364 1363 // element to detect scroll direction of
1365 1364 var $window = $(window);
1366 1365
1367 1366 // initialize last scroll position
1368 1367 var lastScrollY = $window.scrollTop();
1369 1368
1370 1369 $window.on('resize scrollstop', {latency: 350}, function () {
1371 1370 var visibleChunks = $('.nav-chunk').withinviewport({top: 75});
1372 1371
1373 1372 // get current scroll position
1374 1373 var currentScrollY = $window.scrollTop();
1375 1374
1376 1375 // determine current scroll direction
1377 1376 if (currentScrollY > lastScrollY) {
1378 1377 var y = 'down'
1379 1378 } else if (currentScrollY !== lastScrollY) {
1380 1379 var y = 'up';
1381 1380 }
1382 1381
1383 1382 var pos = -1; // by default we use last element in viewport
1384 1383 if (y === 'down') {
1385 1384 pos = -1;
1386 1385 } else if (y === 'up') {
1387 1386 pos = 0;
1388 1387 }
1389 1388
1390 1389 if (visibleChunks.length > 0) {
1391 1390 $('.nav-chunk').removeClass('selected');
1392 1391 $(visibleChunks.get(pos)).addClass('selected');
1393 1392 }
1394 1393
1395 1394 // update last scroll position to current position
1396 1395 lastScrollY = currentScrollY;
1397 1396
1398 1397 });
1399 1398 $('#diff_nav').html(diffNavText);
1400 1399
1401 1400 });
1402 1401 </script>
1403 1402
1404 1403 </%def>
General Comments 0
You need to be logged in to leave comments. Login now