##// 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 # Copyright (C) 2011-2023 RhodeCode GmbH
1 # Copyright (C) 2011-2023 RhodeCode GmbH
2 #
2 #
3 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
14 #
15 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19 import logging
19 import logging
20 import collections
20 import collections
21
21
22 import formencode
22 import formencode
23 import formencode.htmlfill
23 import formencode.htmlfill
24 import peppercorn
24 import peppercorn
25 from pyramid.httpexceptions import (
25 from pyramid.httpexceptions import (
26 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest, HTTPConflict)
26 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest, HTTPConflict)
27
27
28 from pyramid.renderers import render
28 from pyramid.renderers import render
29
29
30 from rhodecode.apps._base import RepoAppView, DataGridAppView
30 from rhodecode.apps._base import RepoAppView, DataGridAppView
31
31
32 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
32 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
33 from rhodecode.lib.base import vcs_operation_context
33 from rhodecode.lib.base import vcs_operation_context
34 from rhodecode.lib.diffs import load_cached_diff, cache_diff, diff_cache_exist
34 from rhodecode.lib.diffs import load_cached_diff, cache_diff, diff_cache_exist
35 from rhodecode.lib.exceptions import CommentVersionMismatch
35 from rhodecode.lib.exceptions import CommentVersionMismatch
36 from rhodecode.lib import ext_json
36 from rhodecode.lib import ext_json
37 from rhodecode.lib.auth import (
37 from rhodecode.lib.auth import (
38 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
38 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
39 NotAnonymous, CSRFRequired)
39 NotAnonymous, CSRFRequired)
40 from rhodecode.lib.utils2 import str2bool, safe_str, safe_int, aslist, retry
40 from rhodecode.lib.utils2 import str2bool, safe_str, safe_int, aslist, retry
41 from rhodecode.lib.vcs.backends.base import (
41 from rhodecode.lib.vcs.backends.base import (
42 EmptyCommit, UpdateFailureReason, unicode_to_reference)
42 EmptyCommit, UpdateFailureReason, unicode_to_reference)
43 from rhodecode.lib.vcs.exceptions import (
43 from rhodecode.lib.vcs.exceptions import (
44 CommitDoesNotExistError, RepositoryRequirementError, EmptyRepositoryError)
44 CommitDoesNotExistError, RepositoryRequirementError, EmptyRepositoryError)
45 from rhodecode.model.changeset_status import ChangesetStatusModel
45 from rhodecode.model.changeset_status import ChangesetStatusModel
46 from rhodecode.model.comment import CommentsModel
46 from rhodecode.model.comment import CommentsModel
47 from rhodecode.model.db import (
47 from rhodecode.model.db import (
48 func, false, or_, PullRequest, ChangesetComment, ChangesetStatus, Repository,
48 func, false, or_, PullRequest, ChangesetComment, ChangesetStatus, Repository,
49 PullRequestReviewers)
49 PullRequestReviewers)
50 from rhodecode.model.forms import PullRequestForm
50 from rhodecode.model.forms import PullRequestForm
51 from rhodecode.model.meta import Session
51 from rhodecode.model.meta import Session
52 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
52 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
53 from rhodecode.model.scm import ScmModel
53 from rhodecode.model.scm import ScmModel
54
54
55 log = logging.getLogger(__name__)
55 log = logging.getLogger(__name__)
56
56
57
57
58 class RepoPullRequestsView(RepoAppView, DataGridAppView):
58 class RepoPullRequestsView(RepoAppView, DataGridAppView):
59
59
60 def load_default_context(self):
60 def load_default_context(self):
61 c = self._get_local_tmpl_context(include_app_defaults=True)
61 c = self._get_local_tmpl_context(include_app_defaults=True)
62 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
62 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
63 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
63 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
64 # backward compat., we use for OLD PRs a plain renderer
64 # backward compat., we use for OLD PRs a plain renderer
65 c.renderer = 'plain'
65 c.renderer = 'plain'
66 return c
66 return c
67
67
68 def _get_pull_requests_list(
68 def _get_pull_requests_list(
69 self, repo_name, source, filter_type, opened_by, statuses):
69 self, repo_name, source, filter_type, opened_by, statuses):
70
70
71 draw, start, limit = self._extract_chunk(self.request)
71 draw, start, limit = self._extract_chunk(self.request)
72 search_q, order_by, order_dir = self._extract_ordering(self.request)
72 search_q, order_by, order_dir = self._extract_ordering(self.request)
73 _render = self.request.get_partial_renderer(
73 _render = self.request.get_partial_renderer(
74 'rhodecode:templates/data_table/_dt_elements.mako')
74 'rhodecode:templates/data_table/_dt_elements.mako')
75
75
76 # pagination
76 # pagination
77
77
78 if filter_type == 'awaiting_review':
78 if filter_type == 'awaiting_review':
79 pull_requests = PullRequestModel().get_awaiting_review(
79 pull_requests = PullRequestModel().get_awaiting_review(
80 repo_name,
80 repo_name,
81 search_q=search_q, statuses=statuses,
81 search_q=search_q, statuses=statuses,
82 offset=start, length=limit, order_by=order_by, order_dir=order_dir)
82 offset=start, length=limit, order_by=order_by, order_dir=order_dir)
83 pull_requests_total_count = PullRequestModel().count_awaiting_review(
83 pull_requests_total_count = PullRequestModel().count_awaiting_review(
84 repo_name,
84 repo_name,
85 search_q=search_q, statuses=statuses)
85 search_q=search_q, statuses=statuses)
86 elif filter_type == 'awaiting_my_review':
86 elif filter_type == 'awaiting_my_review':
87 pull_requests = PullRequestModel().get_awaiting_my_review(
87 pull_requests = PullRequestModel().get_awaiting_my_review(
88 repo_name, self._rhodecode_user.user_id,
88 repo_name, self._rhodecode_user.user_id,
89 search_q=search_q, statuses=statuses,
89 search_q=search_q, statuses=statuses,
90 offset=start, length=limit, order_by=order_by, order_dir=order_dir)
90 offset=start, length=limit, order_by=order_by, order_dir=order_dir)
91 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
91 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
92 repo_name, self._rhodecode_user.user_id,
92 repo_name, self._rhodecode_user.user_id,
93 search_q=search_q, statuses=statuses)
93 search_q=search_q, statuses=statuses)
94 else:
94 else:
95 pull_requests = PullRequestModel().get_all(
95 pull_requests = PullRequestModel().get_all(
96 repo_name, search_q=search_q, source=source, opened_by=opened_by,
96 repo_name, search_q=search_q, source=source, opened_by=opened_by,
97 statuses=statuses, offset=start, length=limit,
97 statuses=statuses, offset=start, length=limit,
98 order_by=order_by, order_dir=order_dir)
98 order_by=order_by, order_dir=order_dir)
99 pull_requests_total_count = PullRequestModel().count_all(
99 pull_requests_total_count = PullRequestModel().count_all(
100 repo_name, search_q=search_q, source=source, statuses=statuses,
100 repo_name, search_q=search_q, source=source, statuses=statuses,
101 opened_by=opened_by)
101 opened_by=opened_by)
102
102
103 data = []
103 data = []
104 comments_model = CommentsModel()
104 comments_model = CommentsModel()
105 for pr in pull_requests:
105 for pr in pull_requests:
106 comments_count = comments_model.get_all_comments(
106 comments_count = comments_model.get_all_comments(
107 self.db_repo.repo_id, pull_request=pr,
107 self.db_repo.repo_id, pull_request=pr,
108 include_drafts=False, count_only=True)
108 include_drafts=False, count_only=True)
109
109
110 review_statuses = pr.reviewers_statuses(user=self._rhodecode_db_user)
110 review_statuses = pr.reviewers_statuses(user=self._rhodecode_db_user)
111 my_review_status = ChangesetStatus.STATUS_NOT_REVIEWED
111 my_review_status = ChangesetStatus.STATUS_NOT_REVIEWED
112 if review_statuses and review_statuses[4]:
112 if review_statuses and review_statuses[4]:
113 _review_obj, _user, _reasons, _mandatory, statuses = review_statuses
113 _review_obj, _user, _reasons, _mandatory, statuses = review_statuses
114 my_review_status = statuses[0][1].status
114 my_review_status = statuses[0][1].status
115
115
116 data.append({
116 data.append({
117 'name': _render('pullrequest_name',
117 'name': _render('pullrequest_name',
118 pr.pull_request_id, pr.pull_request_state,
118 pr.pull_request_id, pr.pull_request_state,
119 pr.work_in_progress, pr.target_repo.repo_name,
119 pr.work_in_progress, pr.target_repo.repo_name,
120 short=True),
120 short=True),
121 'name_raw': pr.pull_request_id,
121 'name_raw': pr.pull_request_id,
122 'status': _render('pullrequest_status',
122 'status': _render('pullrequest_status',
123 pr.calculated_review_status()),
123 pr.calculated_review_status()),
124 'my_status': _render('pullrequest_status',
124 'my_status': _render('pullrequest_status',
125 my_review_status),
125 my_review_status),
126 'title': _render('pullrequest_title', pr.title, pr.description),
126 'title': _render('pullrequest_title', pr.title, pr.description),
127 'pr_flow': _render('pullrequest_commit_flow', pr),
127 'pr_flow': _render('pullrequest_commit_flow', pr),
128 'description': h.escape(pr.description),
128 'description': h.escape(pr.description),
129 'updated_on': _render('pullrequest_updated_on',
129 'updated_on': _render('pullrequest_updated_on',
130 h.datetime_to_time(pr.updated_on),
130 h.datetime_to_time(pr.updated_on),
131 pr.versions_count),
131 pr.versions_count),
132 'updated_on_raw': h.datetime_to_time(pr.updated_on),
132 'updated_on_raw': h.datetime_to_time(pr.updated_on),
133 'created_on': _render('pullrequest_updated_on',
133 'created_on': _render('pullrequest_updated_on',
134 h.datetime_to_time(pr.created_on)),
134 h.datetime_to_time(pr.created_on)),
135 'created_on_raw': h.datetime_to_time(pr.created_on),
135 'created_on_raw': h.datetime_to_time(pr.created_on),
136 'state': pr.pull_request_state,
136 'state': pr.pull_request_state,
137 'author': _render('pullrequest_author',
137 'author': _render('pullrequest_author',
138 pr.author.full_contact, ),
138 pr.author.full_contact, ),
139 'author_raw': pr.author.full_name,
139 'author_raw': pr.author.full_name,
140 'comments': _render('pullrequest_comments', comments_count),
140 'comments': _render('pullrequest_comments', comments_count),
141 'comments_raw': comments_count,
141 'comments_raw': comments_count,
142 'closed': pr.is_closed(),
142 'closed': pr.is_closed(),
143 })
143 })
144
144
145 data = ({
145 data = ({
146 'draw': draw,
146 'draw': draw,
147 'data': data,
147 'data': data,
148 'recordsTotal': pull_requests_total_count,
148 'recordsTotal': pull_requests_total_count,
149 'recordsFiltered': pull_requests_total_count,
149 'recordsFiltered': pull_requests_total_count,
150 })
150 })
151 return data
151 return data
152
152
153 @LoginRequired()
153 @LoginRequired()
154 @HasRepoPermissionAnyDecorator(
154 @HasRepoPermissionAnyDecorator(
155 'repository.read', 'repository.write', 'repository.admin')
155 'repository.read', 'repository.write', 'repository.admin')
156 def pull_request_list(self):
156 def pull_request_list(self):
157 c = self.load_default_context()
157 c = self.load_default_context()
158
158
159 req_get = self.request.GET
159 req_get = self.request.GET
160 c.source = str2bool(req_get.get('source'))
160 c.source = str2bool(req_get.get('source'))
161 c.closed = str2bool(req_get.get('closed'))
161 c.closed = str2bool(req_get.get('closed'))
162 c.my = str2bool(req_get.get('my'))
162 c.my = str2bool(req_get.get('my'))
163 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
163 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
164 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
164 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
165
165
166 c.active = 'open'
166 c.active = 'open'
167 if c.my:
167 if c.my:
168 c.active = 'my'
168 c.active = 'my'
169 if c.closed:
169 if c.closed:
170 c.active = 'closed'
170 c.active = 'closed'
171 if c.awaiting_review and not c.source:
171 if c.awaiting_review and not c.source:
172 c.active = 'awaiting'
172 c.active = 'awaiting'
173 if c.source and not c.awaiting_review:
173 if c.source and not c.awaiting_review:
174 c.active = 'source'
174 c.active = 'source'
175 if c.awaiting_my_review:
175 if c.awaiting_my_review:
176 c.active = 'awaiting_my'
176 c.active = 'awaiting_my'
177
177
178 return self._get_template_context(c)
178 return self._get_template_context(c)
179
179
180 @LoginRequired()
180 @LoginRequired()
181 @HasRepoPermissionAnyDecorator(
181 @HasRepoPermissionAnyDecorator(
182 'repository.read', 'repository.write', 'repository.admin')
182 'repository.read', 'repository.write', 'repository.admin')
183 def pull_request_list_data(self):
183 def pull_request_list_data(self):
184 self.load_default_context()
184 self.load_default_context()
185
185
186 # additional filters
186 # additional filters
187 req_get = self.request.GET
187 req_get = self.request.GET
188 source = str2bool(req_get.get('source'))
188 source = str2bool(req_get.get('source'))
189 closed = str2bool(req_get.get('closed'))
189 closed = str2bool(req_get.get('closed'))
190 my = str2bool(req_get.get('my'))
190 my = str2bool(req_get.get('my'))
191 awaiting_review = str2bool(req_get.get('awaiting_review'))
191 awaiting_review = str2bool(req_get.get('awaiting_review'))
192 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
192 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
193
193
194 filter_type = 'awaiting_review' if awaiting_review \
194 filter_type = 'awaiting_review' if awaiting_review \
195 else 'awaiting_my_review' if awaiting_my_review \
195 else 'awaiting_my_review' if awaiting_my_review \
196 else None
196 else None
197
197
198 opened_by = None
198 opened_by = None
199 if my:
199 if my:
200 opened_by = [self._rhodecode_user.user_id]
200 opened_by = [self._rhodecode_user.user_id]
201
201
202 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
202 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
203 if closed:
203 if closed:
204 statuses = [PullRequest.STATUS_CLOSED]
204 statuses = [PullRequest.STATUS_CLOSED]
205
205
206 data = self._get_pull_requests_list(
206 data = self._get_pull_requests_list(
207 repo_name=self.db_repo_name, source=source,
207 repo_name=self.db_repo_name, source=source,
208 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
208 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
209
209
210 return data
210 return data
211
211
212 def _is_diff_cache_enabled(self, target_repo):
212 def _is_diff_cache_enabled(self, target_repo):
213 caching_enabled = self._get_general_setting(
213 caching_enabled = self._get_general_setting(
214 target_repo, 'rhodecode_diff_cache')
214 target_repo, 'rhodecode_diff_cache')
215 log.debug('Diff caching enabled: %s', caching_enabled)
215 log.debug('Diff caching enabled: %s', caching_enabled)
216 return caching_enabled
216 return caching_enabled
217
217
218 def _get_diffset(self, source_repo_name, source_repo,
218 def _get_diffset(self, source_repo_name, source_repo,
219 ancestor_commit,
219 ancestor_commit,
220 source_ref_id, target_ref_id,
220 source_ref_id, target_ref_id,
221 target_commit, source_commit, diff_limit, file_limit,
221 target_commit, source_commit, diff_limit, file_limit,
222 fulldiff, hide_whitespace_changes, diff_context, use_ancestor=True):
222 fulldiff, hide_whitespace_changes, diff_context, use_ancestor=True):
223
223
224 target_commit_final = target_commit
224 target_commit_final = target_commit
225 source_commit_final = source_commit
225 source_commit_final = source_commit
226
226
227 if use_ancestor:
227 if use_ancestor:
228 # we might want to not use it for versions
228 # we might want to not use it for versions
229 target_ref_id = ancestor_commit.raw_id
229 target_ref_id = ancestor_commit.raw_id
230 target_commit_final = ancestor_commit
230 target_commit_final = ancestor_commit
231
231
232 vcs_diff = PullRequestModel().get_diff(
232 vcs_diff = PullRequestModel().get_diff(
233 source_repo, source_ref_id, target_ref_id,
233 source_repo, source_ref_id, target_ref_id,
234 hide_whitespace_changes, diff_context)
234 hide_whitespace_changes, diff_context)
235
235
236 diff_processor = diffs.DiffProcessor(vcs_diff, diff_format='newdiff', diff_limit=diff_limit,
236 diff_processor = diffs.DiffProcessor(vcs_diff, diff_format='newdiff', diff_limit=diff_limit,
237 file_limit=file_limit, show_full_diff=fulldiff)
237 file_limit=file_limit, show_full_diff=fulldiff)
238
238
239 _parsed = diff_processor.prepare()
239 _parsed = diff_processor.prepare()
240
240
241 diffset = codeblocks.DiffSet(
241 diffset = codeblocks.DiffSet(
242 repo_name=self.db_repo_name,
242 repo_name=self.db_repo_name,
243 source_repo_name=source_repo_name,
243 source_repo_name=source_repo_name,
244 source_node_getter=codeblocks.diffset_node_getter(target_commit_final),
244 source_node_getter=codeblocks.diffset_node_getter(target_commit_final),
245 target_node_getter=codeblocks.diffset_node_getter(source_commit_final),
245 target_node_getter=codeblocks.diffset_node_getter(source_commit_final),
246 )
246 )
247 diffset = self.path_filter.render_patchset_filtered(
247 diffset = self.path_filter.render_patchset_filtered(
248 diffset, _parsed, target_ref_id, source_ref_id)
248 diffset, _parsed, target_ref_id, source_ref_id)
249
249
250 return diffset
250 return diffset
251
251
252 def _get_range_diffset(self, source_scm, source_repo,
252 def _get_range_diffset(self, source_scm, source_repo,
253 commit1, commit2, diff_limit, file_limit,
253 commit1, commit2, diff_limit, file_limit,
254 fulldiff, hide_whitespace_changes, diff_context):
254 fulldiff, hide_whitespace_changes, diff_context):
255 vcs_diff = source_scm.get_diff(
255 vcs_diff = source_scm.get_diff(
256 commit1, commit2,
256 commit1, commit2,
257 ignore_whitespace=hide_whitespace_changes,
257 ignore_whitespace=hide_whitespace_changes,
258 context=diff_context)
258 context=diff_context)
259
259
260 diff_processor = diffs.DiffProcessor(vcs_diff, diff_format='newdiff',
260 diff_processor = diffs.DiffProcessor(vcs_diff, diff_format='newdiff',
261 diff_limit=diff_limit,
261 diff_limit=diff_limit,
262 file_limit=file_limit, show_full_diff=fulldiff)
262 file_limit=file_limit, show_full_diff=fulldiff)
263
263
264 _parsed = diff_processor.prepare()
264 _parsed = diff_processor.prepare()
265
265
266 diffset = codeblocks.DiffSet(
266 diffset = codeblocks.DiffSet(
267 repo_name=source_repo.repo_name,
267 repo_name=source_repo.repo_name,
268 source_node_getter=codeblocks.diffset_node_getter(commit1),
268 source_node_getter=codeblocks.diffset_node_getter(commit1),
269 target_node_getter=codeblocks.diffset_node_getter(commit2))
269 target_node_getter=codeblocks.diffset_node_getter(commit2))
270
270
271 diffset = self.path_filter.render_patchset_filtered(
271 diffset = self.path_filter.render_patchset_filtered(
272 diffset, _parsed, commit1.raw_id, commit2.raw_id)
272 diffset, _parsed, commit1.raw_id, commit2.raw_id)
273
273
274 return diffset
274 return diffset
275
275
276 def register_comments_vars(self, c, pull_request, versions, include_drafts=True):
276 def register_comments_vars(self, c, pull_request, versions, include_drafts=True):
277 comments_model = CommentsModel()
277 comments_model = CommentsModel()
278
278
279 # GENERAL COMMENTS with versions #
279 # GENERAL COMMENTS with versions #
280 q = comments_model._all_general_comments_of_pull_request(pull_request)
280 q = comments_model._all_general_comments_of_pull_request(pull_request)
281 q = q.order_by(ChangesetComment.comment_id.asc())
281 q = q.order_by(ChangesetComment.comment_id.asc())
282 if not include_drafts:
282 if not include_drafts:
283 q = q.filter(ChangesetComment.draft == false())
283 q = q.filter(ChangesetComment.draft == false())
284 general_comments = q
284 general_comments = q
285
285
286 # pick comments we want to render at current version
286 # pick comments we want to render at current version
287 c.comment_versions = comments_model.aggregate_comments(
287 c.comment_versions = comments_model.aggregate_comments(
288 general_comments, versions, c.at_version_num)
288 general_comments, versions, c.at_version_num)
289
289
290 # INLINE COMMENTS with versions #
290 # INLINE COMMENTS with versions #
291 q = comments_model._all_inline_comments_of_pull_request(pull_request)
291 q = comments_model._all_inline_comments_of_pull_request(pull_request)
292 q = q.order_by(ChangesetComment.comment_id.asc())
292 q = q.order_by(ChangesetComment.comment_id.asc())
293 if not include_drafts:
293 if not include_drafts:
294 q = q.filter(ChangesetComment.draft == false())
294 q = q.filter(ChangesetComment.draft == false())
295 inline_comments = q
295 inline_comments = q
296
296
297 c.inline_versions = comments_model.aggregate_comments(
297 c.inline_versions = comments_model.aggregate_comments(
298 inline_comments, versions, c.at_version_num, inline=True)
298 inline_comments, versions, c.at_version_num, inline=True)
299
299
300 # Comments inline+general
300 # Comments inline+general
301 if c.at_version:
301 if c.at_version:
302 c.inline_comments_flat = c.inline_versions[c.at_version_num]['display']
302 c.inline_comments_flat = c.inline_versions[c.at_version_num]['display']
303 c.comments = c.comment_versions[c.at_version_num]['display']
303 c.comments = c.comment_versions[c.at_version_num]['display']
304 else:
304 else:
305 c.inline_comments_flat = c.inline_versions[c.at_version_num]['until']
305 c.inline_comments_flat = c.inline_versions[c.at_version_num]['until']
306 c.comments = c.comment_versions[c.at_version_num]['until']
306 c.comments = c.comment_versions[c.at_version_num]['until']
307
307
308 return general_comments, inline_comments
308 return general_comments, inline_comments
309
309
310 @LoginRequired()
310 @LoginRequired()
311 @HasRepoPermissionAnyDecorator(
311 @HasRepoPermissionAnyDecorator(
312 'repository.read', 'repository.write', 'repository.admin')
312 'repository.read', 'repository.write', 'repository.admin')
313 def pull_request_show(self):
313 def pull_request_show(self):
314 _ = self.request.translate
314 _ = self.request.translate
315 c = self.load_default_context()
315 c = self.load_default_context()
316
316
317 pull_request = PullRequest.get_or_404(
317 pull_request = PullRequest.get_or_404(
318 self.request.matchdict['pull_request_id'])
318 self.request.matchdict['pull_request_id'])
319 pull_request_id = pull_request.pull_request_id
319 pull_request_id = pull_request.pull_request_id
320
320
321 c.state_progressing = pull_request.is_state_changing()
321 c.state_progressing = pull_request.is_state_changing()
322 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
322 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
323
323
324 _new_state = {
324 _new_state = {
325 'created': PullRequest.STATE_CREATED,
325 'created': PullRequest.STATE_CREATED,
326 }.get(self.request.GET.get('force_state'))
326 }.get(self.request.GET.get('force_state'))
327 can_force_state = c.is_super_admin or HasRepoPermissionAny('repository.admin')(c.repo_name)
327 can_force_state = c.is_super_admin or HasRepoPermissionAny('repository.admin')(c.repo_name)
328
328
329 if can_force_state and _new_state:
329 if can_force_state and _new_state:
330 with pull_request.set_state(PullRequest.STATE_UPDATING, final_state=_new_state):
330 with pull_request.set_state(PullRequest.STATE_UPDATING, final_state=_new_state):
331 h.flash(
331 h.flash(
332 _('Pull Request state was force changed to `{}`').format(_new_state),
332 _('Pull Request state was force changed to `{}`').format(_new_state),
333 category='success')
333 category='success')
334 Session().commit()
334 Session().commit()
335
335
336 raise HTTPFound(h.route_path(
336 raise HTTPFound(h.route_path(
337 'pullrequest_show', repo_name=self.db_repo_name,
337 'pullrequest_show', repo_name=self.db_repo_name,
338 pull_request_id=pull_request_id))
338 pull_request_id=pull_request_id))
339
339
340 version = self.request.GET.get('version')
340 version = self.request.GET.get('version')
341 from_version = self.request.GET.get('from_version') or version
341 from_version = self.request.GET.get('from_version') or version
342 merge_checks = self.request.GET.get('merge_checks')
342 merge_checks = self.request.GET.get('merge_checks')
343 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
343 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
344 force_refresh = str2bool(self.request.GET.get('force_refresh'))
344 force_refresh = str2bool(self.request.GET.get('force_refresh'))
345 c.range_diff_on = self.request.GET.get('range-diff') == "1"
345 c.range_diff_on = self.request.GET.get('range-diff') == "1"
346
346
347 # fetch global flags of ignore ws or context lines
347 # fetch global flags of ignore ws or context lines
348 diff_context = diffs.get_diff_context(self.request)
348 diff_context = diffs.get_diff_context(self.request)
349 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
349 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
350
350
351 (pull_request_latest,
351 (pull_request_latest,
352 pull_request_at_ver,
352 pull_request_at_ver,
353 pull_request_display_obj,
353 pull_request_display_obj,
354 at_version) = PullRequestModel().get_pr_version(
354 at_version) = PullRequestModel().get_pr_version(
355 pull_request_id, version=version)
355 pull_request_id, version=version)
356
356
357 pr_closed = pull_request_latest.is_closed()
357 pr_closed = pull_request_latest.is_closed()
358
358
359 if pr_closed and (version or from_version):
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 raise HTTPFound(h.route_path(
361 raise HTTPFound(h.route_path(
362 'pullrequest_show', repo_name=self.db_repo_name,
362 'pullrequest_show', repo_name=self.db_repo_name,
363 pull_request_id=pull_request_id))
363 pull_request_id=pull_request_id))
364
364
365 versions = pull_request_display_obj.versions()
365 versions = pull_request_display_obj.versions()
366
366
367 c.commit_versions = PullRequestModel().pr_commits_versions(versions)
367 c.commit_versions = PullRequestModel().pr_commits_versions(versions)
368
368
369 # used to store per-commit range diffs
369 # used to store per-commit range diffs
370 c.changes = collections.OrderedDict()
370 c.changes = collections.OrderedDict()
371
371
372 c.at_version = at_version
372 c.at_version = at_version
373 c.at_version_num = (at_version
373 c.at_version_num = (at_version
374 if at_version and at_version != PullRequest.LATEST_VER
374 if at_version and at_version != PullRequest.LATEST_VER
375 else None)
375 else None)
376
376
377 c.at_version_index = ChangesetComment.get_index_from_version(
377 c.at_version_index = ChangesetComment.get_index_from_version(
378 c.at_version_num, versions)
378 c.at_version_num, versions)
379
379
380 (prev_pull_request_latest,
380 (prev_pull_request_latest,
381 prev_pull_request_at_ver,
381 prev_pull_request_at_ver,
382 prev_pull_request_display_obj,
382 prev_pull_request_display_obj,
383 prev_at_version) = PullRequestModel().get_pr_version(
383 prev_at_version) = PullRequestModel().get_pr_version(
384 pull_request_id, version=from_version)
384 pull_request_id, version=from_version)
385
385
386 c.from_version = prev_at_version
386 c.from_version = prev_at_version
387 c.from_version_num = (prev_at_version
387 c.from_version_num = (prev_at_version
388 if prev_at_version and prev_at_version != PullRequest.LATEST_VER
388 if prev_at_version and prev_at_version != PullRequest.LATEST_VER
389 else None)
389 else None)
390 c.from_version_index = ChangesetComment.get_index_from_version(
390 c.from_version_index = ChangesetComment.get_index_from_version(
391 c.from_version_num, versions)
391 c.from_version_num, versions)
392
392
393 # define if we're in COMPARE mode or VIEW at version mode
393 # define if we're in COMPARE mode or VIEW at version mode
394 compare = at_version != prev_at_version
394 compare = at_version != prev_at_version
395
395
396 # pull_requests repo_name we opened it against
396 # pull_requests repo_name we opened it against
397 # ie. target_repo must match
397 # i.e., target_repo must match
398 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
398 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
399 log.warning('Mismatch between the current repo: %s, and target %s',
399 log.warning('Mismatch between the current repo: %s, and target %s',
400 self.db_repo_name, pull_request_at_ver.target_repo.repo_name)
400 self.db_repo_name, pull_request_at_ver.target_repo.repo_name)
401 raise HTTPNotFound()
401 raise HTTPNotFound()
402
402
403 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(pull_request_at_ver)
403 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(pull_request_at_ver)
404
404
405 c.pull_request = pull_request_display_obj
405 c.pull_request = pull_request_display_obj
406 c.renderer = pull_request_at_ver.description_renderer or c.renderer
406 c.renderer = pull_request_at_ver.description_renderer or c.renderer
407 c.pull_request_latest = pull_request_latest
407 c.pull_request_latest = pull_request_latest
408
408
409 # inject latest version
409 # inject latest version
410 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
410 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
411 c.versions = versions + [latest_ver]
411 c.versions = versions + [latest_ver]
412
412
413 if compare or (at_version and not at_version == PullRequest.LATEST_VER):
413 if compare or (at_version and not at_version == PullRequest.LATEST_VER):
414 c.allowed_to_change_status = False
414 c.allowed_to_change_status = False
415 c.allowed_to_update = False
415 c.allowed_to_update = False
416 c.allowed_to_merge = False
416 c.allowed_to_merge = False
417 c.allowed_to_delete = False
417 c.allowed_to_delete = False
418 c.allowed_to_comment = False
418 c.allowed_to_comment = False
419 c.allowed_to_close = False
419 c.allowed_to_close = False
420 else:
420 else:
421 can_change_status = PullRequestModel().check_user_change_status(
421 can_change_status = PullRequestModel().check_user_change_status(
422 pull_request_at_ver, self._rhodecode_user)
422 pull_request_at_ver, self._rhodecode_user)
423 c.allowed_to_change_status = can_change_status and not pr_closed
423 c.allowed_to_change_status = can_change_status and not pr_closed
424
424
425 c.allowed_to_update = PullRequestModel().check_user_update(
425 c.allowed_to_update = PullRequestModel().check_user_update(
426 pull_request_latest, self._rhodecode_user) and not pr_closed
426 pull_request_latest, self._rhodecode_user) and not pr_closed
427 c.allowed_to_merge = PullRequestModel().check_user_merge(
427 c.allowed_to_merge = PullRequestModel().check_user_merge(
428 pull_request_latest, self._rhodecode_user) and not pr_closed
428 pull_request_latest, self._rhodecode_user) and not pr_closed
429 c.allowed_to_delete = PullRequestModel().check_user_delete(
429 c.allowed_to_delete = PullRequestModel().check_user_delete(
430 pull_request_latest, self._rhodecode_user) and not pr_closed
430 pull_request_latest, self._rhodecode_user) and not pr_closed
431 c.allowed_to_comment = not pr_closed
431 c.allowed_to_comment = not pr_closed
432 c.allowed_to_close = c.allowed_to_merge and not pr_closed
432 c.allowed_to_close = c.allowed_to_merge and not pr_closed
433
433
434 c.forbid_adding_reviewers = False
434 c.forbid_adding_reviewers = False
435
435
436 if pull_request_latest.reviewer_data and \
436 if pull_request_latest.reviewer_data and \
437 'rules' in pull_request_latest.reviewer_data:
437 'rules' in pull_request_latest.reviewer_data:
438 rules = pull_request_latest.reviewer_data['rules'] or {}
438 rules = pull_request_latest.reviewer_data['rules'] or {}
439 try:
439 try:
440 c.forbid_adding_reviewers = rules.get('forbid_adding_reviewers')
440 c.forbid_adding_reviewers = rules.get('forbid_adding_reviewers')
441 except Exception:
441 except Exception:
442 pass
442 pass
443
443
444 # check merge capabilities
444 # check merge capabilities
445 _merge_check = MergeCheck.validate(
445 _merge_check = MergeCheck.validate(
446 pull_request_latest, auth_user=self._rhodecode_user,
446 pull_request_latest, auth_user=self._rhodecode_user,
447 translator=self.request.translate,
447 translator=self.request.translate,
448 force_shadow_repo_refresh=force_refresh)
448 force_shadow_repo_refresh=force_refresh)
449
449
450 c.pr_merge_errors = _merge_check.error_details
450 c.pr_merge_errors = _merge_check.error_details
451 c.pr_merge_possible = not _merge_check.failed
451 c.pr_merge_possible = not _merge_check.failed
452 c.pr_merge_message = _merge_check.merge_msg
452 c.pr_merge_message = _merge_check.merge_msg
453 c.pr_merge_source_commit = _merge_check.source_commit
453 c.pr_merge_source_commit = _merge_check.source_commit
454 c.pr_merge_target_commit = _merge_check.target_commit
454 c.pr_merge_target_commit = _merge_check.target_commit
455
455
456 c.pr_merge_info = MergeCheck.get_merge_conditions(
456 c.pr_merge_info = MergeCheck.get_merge_conditions(
457 pull_request_latest, translator=self.request.translate)
457 pull_request_latest, translator=self.request.translate)
458
458
459 c.pull_request_review_status = _merge_check.review_status
459 c.pull_request_review_status = _merge_check.review_status
460 if merge_checks:
460 if merge_checks:
461 self.request.override_renderer = \
461 self.request.override_renderer = \
462 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
462 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
463 return self._get_template_context(c)
463 return self._get_template_context(c)
464
464
465 c.reviewers_count = pull_request.reviewers_count
465 c.reviewers_count = pull_request.reviewers_count
466 c.observers_count = pull_request.observers_count
466 c.observers_count = pull_request.observers_count
467
467
468 # reviewers and statuses
468 # reviewers and statuses
469 c.pull_request_default_reviewers_data_json = ext_json.str_json(pull_request.reviewer_data)
469 c.pull_request_default_reviewers_data_json = ext_json.str_json(pull_request.reviewer_data)
470 c.pull_request_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
470 c.pull_request_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
471 c.pull_request_set_observers_data_json = collections.OrderedDict({'observers': []})
471 c.pull_request_set_observers_data_json = collections.OrderedDict({'observers': []})
472
472
473 # reviewers
473 for review_obj, member, reasons, mandatory, status in pull_request_at_ver.reviewers_statuses():
474 for review_obj, member, reasons, mandatory, status in pull_request_at_ver.reviewers_statuses():
474 member_reviewer = h.reviewer_as_json(
475 member_reviewer = h.reviewer_as_json(
475 member, reasons=reasons, mandatory=mandatory,
476 member, reasons=reasons, mandatory=mandatory,
476 role=review_obj.role,
477 role=review_obj.role,
477 user_group=review_obj.rule_user_group_data()
478 user_group=review_obj.rule_user_group_data()
478 )
479 )
479
480
480 current_review_status = status[0][1].status if status else ChangesetStatus.STATUS_NOT_REVIEWED
481 current_review_status = status[0][1].status if status else ChangesetStatus.STATUS_NOT_REVIEWED
481 member_reviewer['review_status'] = current_review_status
482 member_reviewer['review_status'] = current_review_status
482 member_reviewer['review_status_label'] = h.commit_status_lbl(current_review_status)
483 member_reviewer['review_status_label'] = h.commit_status_lbl(current_review_status)
483 member_reviewer['allowed_to_update'] = c.allowed_to_update
484 member_reviewer['allowed_to_update'] = c.allowed_to_update
484 c.pull_request_set_reviewers_data_json['reviewers'].append(member_reviewer)
485 c.pull_request_set_reviewers_data_json['reviewers'].append(member_reviewer)
485
486
486 c.pull_request_set_reviewers_data_json = ext_json.str_json(c.pull_request_set_reviewers_data_json)
487 c.pull_request_set_reviewers_data_json = ext_json.str_json(c.pull_request_set_reviewers_data_json)
487
488
489 # observers
488 for observer_obj, member in pull_request_at_ver.observers():
490 for observer_obj, member in pull_request_at_ver.observers():
489 member_observer = h.reviewer_as_json(
491 member_observer = h.reviewer_as_json(
490 member, reasons=[], mandatory=False,
492 member, reasons=[], mandatory=False,
491 role=observer_obj.role,
493 role=observer_obj.role,
492 user_group=observer_obj.rule_user_group_data()
494 user_group=observer_obj.rule_user_group_data()
493 )
495 )
494 member_observer['allowed_to_update'] = c.allowed_to_update
496 member_observer['allowed_to_update'] = c.allowed_to_update
495 c.pull_request_set_observers_data_json['observers'].append(member_observer)
497 c.pull_request_set_observers_data_json['observers'].append(member_observer)
496
498
497 c.pull_request_set_observers_data_json = ext_json.str_json(c.pull_request_set_observers_data_json)
499 c.pull_request_set_observers_data_json = ext_json.str_json(c.pull_request_set_observers_data_json)
498
500
499 general_comments, inline_comments = \
501 general_comments, inline_comments = \
500 self.register_comments_vars(c, pull_request_latest, versions)
502 self.register_comments_vars(c, pull_request_latest, versions)
501
503
502 # TODOs
504 # TODOs
503 c.unresolved_comments = CommentsModel() \
505 c.unresolved_comments = CommentsModel() \
504 .get_pull_request_unresolved_todos(pull_request_latest)
506 .get_pull_request_unresolved_todos(pull_request_latest)
505 c.resolved_comments = CommentsModel() \
507 c.resolved_comments = CommentsModel() \
506 .get_pull_request_resolved_todos(pull_request_latest)
508 .get_pull_request_resolved_todos(pull_request_latest)
507
509
508 # Drafts
510 # Drafts
509 c.draft_comments = CommentsModel().get_pull_request_drafts(
511 c.draft_comments = CommentsModel().get_pull_request_drafts(
510 self._rhodecode_db_user.user_id,
512 self._rhodecode_db_user.user_id,
511 pull_request_latest)
513 pull_request_latest)
512
514
513 # if we use version, then do not show later comments
515 # if we use version, then do not show later comments
514 # than current version
516 # than current version
515 display_inline_comments = collections.defaultdict(
517 display_inline_comments = collections.defaultdict(
516 lambda: collections.defaultdict(list))
518 lambda: collections.defaultdict(list))
517 for co in inline_comments:
519 for co in inline_comments:
518 if c.at_version_num:
520 if c.at_version_num:
519 # pick comments that are at least UPTO given version, so we
521 # pick comments that are at least UPTO given version, so we
520 # don't render comments for higher version
522 # don't render comments for higher version
521 should_render = co.pull_request_version_id and \
523 should_render = co.pull_request_version_id and \
522 co.pull_request_version_id <= c.at_version_num
524 co.pull_request_version_id <= c.at_version_num
523 else:
525 else:
524 # showing all, for 'latest'
526 # showing all, for 'latest'
525 should_render = True
527 should_render = True
526
528
527 if should_render:
529 if should_render:
528 display_inline_comments[co.f_path][co.line_no].append(co)
530 display_inline_comments[co.f_path][co.line_no].append(co)
529
531
530 # load diff data into template context, if we use compare mode then
532 # load diff data into template context, if we use compare mode then
531 # diff is calculated based on changes between versions of PR
533 # diff is calculated based on changes between versions of PR
532
534
533 source_repo = pull_request_at_ver.source_repo
535 source_repo = pull_request_at_ver.source_repo
534 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
536 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
535
537
536 target_repo = pull_request_at_ver.target_repo
538 target_repo = pull_request_at_ver.target_repo
537 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
539 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
538
540
539 if compare:
541 if compare:
540 # in compare switch the diff base to latest commit from prev version
542 # in compare switch the diff base to latest commit from prev version
541 target_ref_id = prev_pull_request_display_obj.revisions[0]
543 target_ref_id = prev_pull_request_display_obj.revisions[0]
542
544
543 # despite opening commits for bookmarks/branches/tags, we always
545 # despite opening commits for bookmarks/branches/tags, we always
544 # convert this to rev to prevent changes after bookmark or branch change
546 # convert this to rev to prevent changes after bookmark or branch change
545 c.source_ref_type = 'rev'
547 c.source_ref_type = 'rev'
546 c.source_ref = source_ref_id
548 c.source_ref = source_ref_id
547
549
548 c.target_ref_type = 'rev'
550 c.target_ref_type = 'rev'
549 c.target_ref = target_ref_id
551 c.target_ref = target_ref_id
550
552
551 c.source_repo = source_repo
553 c.source_repo = source_repo
552 c.target_repo = target_repo
554 c.target_repo = target_repo
553
555
554 c.commit_ranges = []
556 c.commit_ranges = []
555 source_commit = EmptyCommit()
557 source_commit = EmptyCommit()
556 target_commit = EmptyCommit()
558 target_commit = EmptyCommit()
557 c.missing_requirements = False
559 c.missing_requirements = False
558
560
559 source_scm = source_repo.scm_instance()
561 source_scm = source_repo.scm_instance()
560 target_scm = target_repo.scm_instance()
562 target_scm = target_repo.scm_instance()
561
563
562 shadow_scm = None
564 shadow_scm = None
563 try:
565 try:
564 shadow_scm = pull_request_latest.get_shadow_repo()
566 shadow_scm = pull_request_latest.get_shadow_repo()
565 except Exception:
567 except Exception:
566 log.debug('Failed to get shadow repo', exc_info=True)
568 log.debug('Failed to get shadow repo', exc_info=True)
567 # try first the existing source_repo, and then shadow
569 # try first the existing source_repo, and then shadow
568 # repo if we can obtain one
570 # repo if we can obtain one
569 commits_source_repo = source_scm
571 commits_source_repo = source_scm
570 if shadow_scm:
572 if shadow_scm:
571 commits_source_repo = shadow_scm
573 commits_source_repo = shadow_scm
572
574
573 c.commits_source_repo = commits_source_repo
575 c.commits_source_repo = commits_source_repo
574 c.ancestor = None # set it to None, to hide it from PR view
576 c.ancestor = None # set it to None, to hide it from PR view
575
577
576 # empty version means latest, so we keep this to prevent
578 # empty version means latest, so we keep this to prevent
577 # double caching
579 # double caching
578 version_normalized = version or PullRequest.LATEST_VER
580 version_normalized = version or PullRequest.LATEST_VER
579 from_version_normalized = from_version or PullRequest.LATEST_VER
581 from_version_normalized = from_version or PullRequest.LATEST_VER
580
582
581 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(target_repo)
583 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(target_repo)
582 cache_file_path = diff_cache_exist(
584 cache_file_path = diff_cache_exist(
583 cache_path, 'pull_request', pull_request_id, version_normalized,
585 cache_path, 'pull_request', pull_request_id, version_normalized,
584 from_version_normalized, source_ref_id, target_ref_id,
586 from_version_normalized, source_ref_id, target_ref_id,
585 hide_whitespace_changes, diff_context, c.fulldiff)
587 hide_whitespace_changes, diff_context, c.fulldiff)
586
588
587 caching_enabled = self._is_diff_cache_enabled(c.target_repo)
589 caching_enabled = self._is_diff_cache_enabled(c.target_repo)
588 force_recache = self.get_recache_flag()
590 force_recache = self.get_recache_flag()
589
591
590 cached_diff = None
592 cached_diff = None
591 if caching_enabled:
593 if caching_enabled:
592 cached_diff = load_cached_diff(cache_file_path)
594 cached_diff = load_cached_diff(cache_file_path)
593
595
594 has_proper_commit_cache = (
596 has_proper_commit_cache = (
595 cached_diff and cached_diff.get('commits')
597 cached_diff and cached_diff.get('commits')
596 and len(cached_diff.get('commits', [])) == 5
598 and len(cached_diff.get('commits', [])) == 5
597 and cached_diff.get('commits')[0]
599 and cached_diff.get('commits')[0]
598 and cached_diff.get('commits')[3])
600 and cached_diff.get('commits')[3])
599
601
600 if not force_recache and not c.range_diff_on and has_proper_commit_cache:
602 if not force_recache and not c.range_diff_on and has_proper_commit_cache:
601 diff_commit_cache = \
603 diff_commit_cache = \
602 (ancestor_commit, commit_cache, missing_requirements,
604 (ancestor_commit, commit_cache, missing_requirements,
603 source_commit, target_commit) = cached_diff['commits']
605 source_commit, target_commit) = cached_diff['commits']
604 else:
606 else:
605 # NOTE(marcink): we reach potentially unreachable errors when a PR has
607 # NOTE(marcink): we reach potentially unreachable errors when a PR has
606 # merge errors resulting in potentially hidden commits in the shadow repo.
608 # merge errors resulting in potentially hidden commits in the shadow repo.
607 maybe_unreachable = _merge_check.MERGE_CHECK in _merge_check.error_details \
609 maybe_unreachable = _merge_check.MERGE_CHECK in _merge_check.error_details \
608 and _merge_check.merge_response
610 and _merge_check.merge_response
609 maybe_unreachable = maybe_unreachable \
611 maybe_unreachable = maybe_unreachable \
610 and _merge_check.merge_response.metadata.get('unresolved_files')
612 and _merge_check.merge_response.metadata.get('unresolved_files')
611 log.debug("Using unreachable commits due to MERGE_CHECK in merge simulation")
613 log.debug("Using unreachable commits due to MERGE_CHECK in merge simulation")
612 diff_commit_cache = \
614 diff_commit_cache = \
613 (ancestor_commit, commit_cache, missing_requirements,
615 (ancestor_commit, commit_cache, missing_requirements,
614 source_commit, target_commit) = self.get_commits(
616 source_commit, target_commit) = self.get_commits(
615 commits_source_repo,
617 commits_source_repo,
616 pull_request_at_ver,
618 pull_request_at_ver,
617 source_commit,
619 source_commit,
618 source_ref_id,
620 source_ref_id,
619 source_scm,
621 source_scm,
620 target_commit,
622 target_commit,
621 target_ref_id,
623 target_ref_id,
622 target_scm,
624 target_scm,
623 maybe_unreachable=maybe_unreachable)
625 maybe_unreachable=maybe_unreachable)
624
626
625 # register our commit range
627 # register our commit range
626 for comm in commit_cache.values():
628 for comm in commit_cache.values():
627 c.commit_ranges.append(comm)
629 c.commit_ranges.append(comm)
628
630
629 c.missing_requirements = missing_requirements
631 c.missing_requirements = missing_requirements
630 c.ancestor_commit = ancestor_commit
632 c.ancestor_commit = ancestor_commit
631 c.statuses = source_repo.statuses(
633 c.statuses = source_repo.statuses(
632 [x.raw_id for x in c.commit_ranges])
634 [x.raw_id for x in c.commit_ranges])
633
635
634 # auto collapse if we have more than limit
636 # auto collapse if we have more than limit
635 collapse_limit = diffs.DiffProcessor._collapse_commits_over
637 collapse_limit = diffs.DiffProcessor._collapse_commits_over
636 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
638 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
637 c.compare_mode = compare
639 c.compare_mode = compare
638
640
639 # diff_limit is the old behavior, will cut off the whole diff
641 # diff_limit is the old behavior, will cut off the whole diff
640 # if the limit is applied otherwise will just hide the
642 # if the limit is applied otherwise will just hide the
641 # big files from the front-end
643 # big files from the front-end
642 diff_limit = c.visual.cut_off_limit_diff
644 diff_limit = c.visual.cut_off_limit_diff
643 file_limit = c.visual.cut_off_limit_file
645 file_limit = c.visual.cut_off_limit_file
644
646
645 c.missing_commits = False
647 c.missing_commits = False
646 if (c.missing_requirements
648 if (c.missing_requirements
647 or isinstance(source_commit, EmptyCommit)
649 or isinstance(source_commit, EmptyCommit)
648 or source_commit == target_commit):
650 or source_commit == target_commit):
649
651
650 c.missing_commits = True
652 c.missing_commits = True
651 else:
653 else:
652 c.inline_comments = display_inline_comments
654 c.inline_comments = display_inline_comments
653
655
654 use_ancestor = True
656 use_ancestor = True
655 if from_version_normalized != version_normalized:
657 if from_version_normalized != version_normalized:
656 use_ancestor = False
658 use_ancestor = False
657
659
658 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
660 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
659 if not force_recache and has_proper_diff_cache:
661 if not force_recache and has_proper_diff_cache:
660 c.diffset = cached_diff['diff']
662 c.diffset = cached_diff['diff']
661 else:
663 else:
662 try:
664 try:
663 c.diffset = self._get_diffset(
665 c.diffset = self._get_diffset(
664 c.source_repo.repo_name, commits_source_repo,
666 c.source_repo.repo_name, commits_source_repo,
665 c.ancestor_commit,
667 c.ancestor_commit,
666 source_ref_id, target_ref_id,
668 source_ref_id, target_ref_id,
667 target_commit, source_commit,
669 target_commit, source_commit,
668 diff_limit, file_limit, c.fulldiff,
670 diff_limit, file_limit, c.fulldiff,
669 hide_whitespace_changes, diff_context,
671 hide_whitespace_changes, diff_context,
670 use_ancestor=use_ancestor
672 use_ancestor=use_ancestor
671 )
673 )
672
674
673 # save cached diff
675 # save cached diff
674 if caching_enabled:
676 if caching_enabled:
675 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
677 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
676 except CommitDoesNotExistError:
678 except CommitDoesNotExistError:
677 log.exception('Failed to generate diffset')
679 log.exception('Failed to generate diffset')
678 c.missing_commits = True
680 c.missing_commits = True
679
681
680 if not c.missing_commits:
682 if not c.missing_commits:
681
683
682 c.limited_diff = c.diffset.limited_diff
684 c.limited_diff = c.diffset.limited_diff
683
685
684 # calculate removed files that are bound to comments
686 # calculate removed files that are bound to comments
685 comment_deleted_files = [
687 comment_deleted_files = [
686 fname for fname in display_inline_comments
688 fname for fname in display_inline_comments
687 if fname not in c.diffset.file_stats]
689 if fname not in c.diffset.file_stats]
688
690
689 c.deleted_files_comments = collections.defaultdict(dict)
691 c.deleted_files_comments = collections.defaultdict(dict)
690 for fname, per_line_comments in display_inline_comments.items():
692 for fname, per_line_comments in display_inline_comments.items():
691 if fname in comment_deleted_files:
693 if fname in comment_deleted_files:
692 c.deleted_files_comments[fname]['stats'] = 0
694 c.deleted_files_comments[fname]['stats'] = 0
693 c.deleted_files_comments[fname]['comments'] = list()
695 c.deleted_files_comments[fname]['comments'] = list()
694 for lno, comments in per_line_comments.items():
696 for lno, comments in per_line_comments.items():
695 c.deleted_files_comments[fname]['comments'].extend(comments)
697 c.deleted_files_comments[fname]['comments'].extend(comments)
696
698
697 # maybe calculate the range diff
699 # maybe calculate the range diff
698 if c.range_diff_on:
700 if c.range_diff_on:
699 # TODO(marcink): set whitespace/context
701 # TODO(marcink): set whitespace/context
700 context_lcl = 3
702 context_lcl = 3
701 ign_whitespace_lcl = False
703 ign_whitespace_lcl = False
702
704
703 for commit in c.commit_ranges:
705 for commit in c.commit_ranges:
704 commit2 = commit
706 commit2 = commit
705 commit1 = commit.first_parent
707 commit1 = commit.first_parent
706
708
707 range_diff_cache_file_path = diff_cache_exist(
709 range_diff_cache_file_path = diff_cache_exist(
708 cache_path, 'diff', commit.raw_id,
710 cache_path, 'diff', commit.raw_id,
709 ign_whitespace_lcl, context_lcl, c.fulldiff)
711 ign_whitespace_lcl, context_lcl, c.fulldiff)
710
712
711 cached_diff = None
713 cached_diff = None
712 if caching_enabled:
714 if caching_enabled:
713 cached_diff = load_cached_diff(range_diff_cache_file_path)
715 cached_diff = load_cached_diff(range_diff_cache_file_path)
714
716
715 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
717 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
716 if not force_recache and has_proper_diff_cache:
718 if not force_recache and has_proper_diff_cache:
717 diffset = cached_diff['diff']
719 diffset = cached_diff['diff']
718 else:
720 else:
719 diffset = self._get_range_diffset(
721 diffset = self._get_range_diffset(
720 commits_source_repo, source_repo,
722 commits_source_repo, source_repo,
721 commit1, commit2, diff_limit, file_limit,
723 commit1, commit2, diff_limit, file_limit,
722 c.fulldiff, ign_whitespace_lcl, context_lcl
724 c.fulldiff, ign_whitespace_lcl, context_lcl
723 )
725 )
724
726
725 # save cached diff
727 # save cached diff
726 if caching_enabled:
728 if caching_enabled:
727 cache_diff(range_diff_cache_file_path, diffset, None)
729 cache_diff(range_diff_cache_file_path, diffset, None)
728
730
729 c.changes[commit.raw_id] = diffset
731 c.changes[commit.raw_id] = diffset
730
732
731 # this is a hack to properly display links, when creating PR, the
733 # this is a hack to properly display links, when creating PR, the
732 # compare view and others uses different notation, and
734 # compare view and others uses different notation, and
733 # compare_commits.mako renders links based on the target_repo.
735 # compare_commits.mako renders links based on the target_repo.
734 # We need to swap that here to generate it properly on the html side
736 # We need to swap that here to generate it properly on the html side
735 c.target_repo = c.source_repo
737 c.target_repo = c.source_repo
736
738
737 c.commit_statuses = ChangesetStatus.STATUSES
739 c.commit_statuses = ChangesetStatus.STATUSES
738
740
739 c.show_version_changes = not pr_closed
741 c.show_version_changes = not pr_closed
740 if c.show_version_changes:
742 if c.show_version_changes:
741 cur_obj = pull_request_at_ver
743 cur_obj = pull_request_at_ver
742 prev_obj = prev_pull_request_at_ver
744 prev_obj = prev_pull_request_at_ver
743
745
744 old_commit_ids = prev_obj.revisions
746 old_commit_ids = prev_obj.revisions
745 new_commit_ids = cur_obj.revisions
747 new_commit_ids = cur_obj.revisions
746 commit_changes = PullRequestModel()._calculate_commit_id_changes(
748 commit_changes = PullRequestModel()._calculate_commit_id_changes(
747 old_commit_ids, new_commit_ids)
749 old_commit_ids, new_commit_ids)
748 c.commit_changes_summary = commit_changes
750 c.commit_changes_summary = commit_changes
749
751
750 # calculate the diff for commits between versions
752 # calculate the diff for commits between versions
751 c.commit_changes = []
753 c.commit_changes = []
752
754
753 def mark(cs, fw):
755 def mark(cs, fw):
754 return list(h.itertools.zip_longest([], cs, fillvalue=fw))
756 return list(h.itertools.zip_longest([], cs, fillvalue=fw))
755
757
756 for c_type, raw_id in mark(commit_changes.added, 'a') \
758 for c_type, raw_id in mark(commit_changes.added, 'a') \
757 + mark(commit_changes.removed, 'r') \
759 + mark(commit_changes.removed, 'r') \
758 + mark(commit_changes.common, 'c'):
760 + mark(commit_changes.common, 'c'):
759
761
760 if raw_id in commit_cache:
762 if raw_id in commit_cache:
761 commit = commit_cache[raw_id]
763 commit = commit_cache[raw_id]
762 else:
764 else:
763 try:
765 try:
764 commit = commits_source_repo.get_commit(raw_id)
766 commit = commits_source_repo.get_commit(raw_id)
765 except CommitDoesNotExistError:
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 # for display in commit diff
769 # for display in commit diff
768 commit = h.AttributeDict(
770 commit = h.AttributeDict(
769 {'raw_id': raw_id,
771 {'raw_id': raw_id,
770 'message': 'EMPTY or MISSING COMMIT'})
772 'message': 'EMPTY or MISSING COMMIT'})
771 c.commit_changes.append([c_type, commit])
773 c.commit_changes.append([c_type, commit])
772
774
773 # current user review statuses for each version
775 # current user review statuses for each version
774 c.review_versions = {}
776 c.review_versions = {}
775 is_reviewer = PullRequestModel().is_user_reviewer(
777 is_reviewer = PullRequestModel().is_user_reviewer(
776 pull_request, self._rhodecode_user)
778 pull_request, self._rhodecode_user)
777 if is_reviewer:
779 if is_reviewer:
778 for co in general_comments:
780 for co in general_comments:
779 if co.author.user_id == self._rhodecode_user.user_id:
781 if co.author.user_id == self._rhodecode_user.user_id:
780 status = co.status_change
782 status = co.status_change
781 if status:
783 if status:
782 _ver_pr = status[0].comment.pull_request_version_id
784 _ver_pr = status[0].comment.pull_request_version_id
783 c.review_versions[_ver_pr] = status[0]
785 c.review_versions[_ver_pr] = status[0]
784
786
785 return self._get_template_context(c)
787 return self._get_template_context(c)
786
788
787 def get_commits(
789 def get_commits(
788 self, commits_source_repo, pull_request_at_ver, source_commit,
790 self, commits_source_repo, pull_request_at_ver, source_commit,
789 source_ref_id, source_scm, target_commit, target_ref_id, target_scm,
791 source_ref_id, source_scm, target_commit, target_ref_id, target_scm,
790 maybe_unreachable=False):
792 maybe_unreachable=False):
791
793
792 commit_cache = collections.OrderedDict()
794 commit_cache = collections.OrderedDict()
793 missing_requirements = False
795 missing_requirements = False
794
796
795 try:
797 try:
796 pre_load = ["author", "date", "message", "branch", "parents"]
798 pre_load = ["author", "date", "message", "branch", "parents"]
797
799
798 pull_request_commits = pull_request_at_ver.revisions
800 pull_request_commits = pull_request_at_ver.revisions
799 log.debug('Loading %s commits from %s',
801 log.debug('Loading %s commits from %s',
800 len(pull_request_commits), commits_source_repo)
802 len(pull_request_commits), commits_source_repo)
801
803
802 for rev in pull_request_commits:
804 for rev in pull_request_commits:
803 comm = commits_source_repo.get_commit(commit_id=rev, pre_load=pre_load,
805 comm = commits_source_repo.get_commit(commit_id=rev, pre_load=pre_load,
804 maybe_unreachable=maybe_unreachable)
806 maybe_unreachable=maybe_unreachable)
805 commit_cache[comm.raw_id] = comm
807 commit_cache[comm.raw_id] = comm
806
808
807 # Order here matters, we first need to get target, and then
809 # Order here matters, we first need to get target, and then
808 # the source
810 # the source
809 target_commit = commits_source_repo.get_commit(
811 target_commit = commits_source_repo.get_commit(
810 commit_id=safe_str(target_ref_id))
812 commit_id=safe_str(target_ref_id))
811
813
812 source_commit = commits_source_repo.get_commit(
814 source_commit = commits_source_repo.get_commit(
813 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
815 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
814 except CommitDoesNotExistError:
816 except CommitDoesNotExistError:
815 log.warning('Failed to get commit from `{}` repo'.format(
817 log.warning('Failed to get commit from `{}` repo'.format(
816 commits_source_repo), exc_info=True)
818 commits_source_repo), exc_info=True)
817 except RepositoryRequirementError:
819 except RepositoryRequirementError:
818 log.warning('Failed to get all required data from repo', exc_info=True)
820 log.warning('Failed to get all required data from repo', exc_info=True)
819 missing_requirements = True
821 missing_requirements = True
820
822
821 pr_ancestor_id = pull_request_at_ver.common_ancestor_id
823 pr_ancestor_id = pull_request_at_ver.common_ancestor_id
822
824
823 try:
825 try:
824 ancestor_commit = source_scm.get_commit(pr_ancestor_id)
826 ancestor_commit = source_scm.get_commit(pr_ancestor_id)
825 except Exception:
827 except Exception:
826 ancestor_commit = None
828 ancestor_commit = None
827
829
828 return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit
830 return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit
829
831
830 def assure_not_empty_repo(self):
832 def assure_not_empty_repo(self):
831 _ = self.request.translate
833 _ = self.request.translate
832
834
833 try:
835 try:
834 self.db_repo.scm_instance().get_commit()
836 self.db_repo.scm_instance().get_commit()
835 except EmptyRepositoryError:
837 except EmptyRepositoryError:
836 h.flash(h.literal(_('There are no commits yet')),
838 h.flash(h.literal(_('There are no commits yet')),
837 category='warning')
839 category='warning')
838 raise HTTPFound(
840 raise HTTPFound(
839 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
841 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
840
842
841 @LoginRequired()
843 @LoginRequired()
842 @NotAnonymous()
844 @NotAnonymous()
843 @HasRepoPermissionAnyDecorator(
845 @HasRepoPermissionAnyDecorator(
844 'repository.read', 'repository.write', 'repository.admin')
846 'repository.read', 'repository.write', 'repository.admin')
845 def pull_request_new(self):
847 def pull_request_new(self):
846 _ = self.request.translate
848 _ = self.request.translate
847 c = self.load_default_context()
849 c = self.load_default_context()
848
850
849 self.assure_not_empty_repo()
851 self.assure_not_empty_repo()
850 source_repo = self.db_repo
852 source_repo = self.db_repo
851
853
852 commit_id = self.request.GET.get('commit')
854 commit_id = self.request.GET.get('commit')
853 branch_ref = self.request.GET.get('branch')
855 branch_ref = self.request.GET.get('branch')
854 bookmark_ref = self.request.GET.get('bookmark')
856 bookmark_ref = self.request.GET.get('bookmark')
855
857
856 try:
858 try:
857 source_repo_data = PullRequestModel().generate_repo_data(
859 source_repo_data = PullRequestModel().generate_repo_data(
858 source_repo, commit_id=commit_id,
860 source_repo, commit_id=commit_id,
859 branch=branch_ref, bookmark=bookmark_ref,
861 branch=branch_ref, bookmark=bookmark_ref,
860 translator=self.request.translate)
862 translator=self.request.translate)
861 except CommitDoesNotExistError as e:
863 except CommitDoesNotExistError as e:
862 log.exception(e)
864 log.exception(e)
863 h.flash(_('Commit does not exist'), 'error')
865 h.flash(_('Commit does not exist'), 'error')
864 raise HTTPFound(
866 raise HTTPFound(
865 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
867 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
866
868
867 default_target_repo = source_repo
869 default_target_repo = source_repo
868
870
869 if source_repo.parent and c.has_origin_repo_read_perm:
871 if source_repo.parent and c.has_origin_repo_read_perm:
870 parent_vcs_obj = source_repo.parent.scm_instance()
872 parent_vcs_obj = source_repo.parent.scm_instance()
871 if parent_vcs_obj and not parent_vcs_obj.is_empty():
873 if parent_vcs_obj and not parent_vcs_obj.is_empty():
872 # change default if we have a parent repo
874 # change default if we have a parent repo
873 default_target_repo = source_repo.parent
875 default_target_repo = source_repo.parent
874
876
875 target_repo_data = PullRequestModel().generate_repo_data(
877 target_repo_data = PullRequestModel().generate_repo_data(
876 default_target_repo, translator=self.request.translate)
878 default_target_repo, translator=self.request.translate)
877
879
878 selected_source_ref = source_repo_data['refs']['selected_ref']
880 selected_source_ref = source_repo_data['refs']['selected_ref']
879 title_source_ref = ''
881 title_source_ref = ''
880 if selected_source_ref:
882 if selected_source_ref:
881 title_source_ref = selected_source_ref.split(':', 2)[1]
883 title_source_ref = selected_source_ref.split(':', 2)[1]
882 c.default_title = PullRequestModel().generate_pullrequest_title(
884 c.default_title = PullRequestModel().generate_pullrequest_title(
883 source=source_repo.repo_name,
885 source=source_repo.repo_name,
884 source_ref=title_source_ref,
886 source_ref=title_source_ref,
885 target=default_target_repo.repo_name
887 target=default_target_repo.repo_name
886 )
888 )
887
889
888 c.default_repo_data = {
890 c.default_repo_data = {
889 'source_repo_name': source_repo.repo_name,
891 'source_repo_name': source_repo.repo_name,
890 'source_refs_json': ext_json.str_json(source_repo_data),
892 'source_refs_json': ext_json.str_json(source_repo_data),
891 'target_repo_name': default_target_repo.repo_name,
893 'target_repo_name': default_target_repo.repo_name,
892 'target_refs_json': ext_json.str_json(target_repo_data),
894 'target_refs_json': ext_json.str_json(target_repo_data),
893 }
895 }
894 c.default_source_ref = selected_source_ref
896 c.default_source_ref = selected_source_ref
895
897
896 return self._get_template_context(c)
898 return self._get_template_context(c)
897
899
898 @LoginRequired()
900 @LoginRequired()
899 @NotAnonymous()
901 @NotAnonymous()
900 @HasRepoPermissionAnyDecorator(
902 @HasRepoPermissionAnyDecorator(
901 'repository.read', 'repository.write', 'repository.admin')
903 'repository.read', 'repository.write', 'repository.admin')
902 def pull_request_repo_refs(self):
904 def pull_request_repo_refs(self):
903 self.load_default_context()
905 self.load_default_context()
904 target_repo_name = self.request.matchdict['target_repo_name']
906 target_repo_name = self.request.matchdict['target_repo_name']
905 repo = Repository.get_by_repo_name(target_repo_name)
907 repo = Repository.get_by_repo_name(target_repo_name)
906 if not repo:
908 if not repo:
907 raise HTTPNotFound()
909 raise HTTPNotFound()
908
910
909 target_perm = HasRepoPermissionAny(
911 target_perm = HasRepoPermissionAny(
910 'repository.read', 'repository.write', 'repository.admin')(
912 'repository.read', 'repository.write', 'repository.admin')(
911 target_repo_name)
913 target_repo_name)
912 if not target_perm:
914 if not target_perm:
913 raise HTTPNotFound()
915 raise HTTPNotFound()
914
916
915 return PullRequestModel().generate_repo_data(
917 return PullRequestModel().generate_repo_data(
916 repo, translator=self.request.translate)
918 repo, translator=self.request.translate)
917
919
918 @LoginRequired()
920 @LoginRequired()
919 @NotAnonymous()
921 @NotAnonymous()
920 @HasRepoPermissionAnyDecorator(
922 @HasRepoPermissionAnyDecorator(
921 'repository.read', 'repository.write', 'repository.admin')
923 'repository.read', 'repository.write', 'repository.admin')
922 def pullrequest_repo_targets(self):
924 def pullrequest_repo_targets(self):
923 _ = self.request.translate
925 _ = self.request.translate
924 filter_query = self.request.GET.get('query')
926 filter_query = self.request.GET.get('query')
925
927
926 # get the parents
928 # get the parents
927 parent_target_repos = []
929 parent_target_repos = []
928 if self.db_repo.parent:
930 if self.db_repo.parent:
929 parents_query = Repository.query() \
931 parents_query = Repository.query() \
930 .order_by(func.length(Repository.repo_name)) \
932 .order_by(func.length(Repository.repo_name)) \
931 .filter(Repository.fork_id == self.db_repo.parent.repo_id)
933 .filter(Repository.fork_id == self.db_repo.parent.repo_id)
932
934
933 if filter_query:
935 if filter_query:
934 ilike_expression = f'%{safe_str(filter_query)}%'
936 ilike_expression = f'%{safe_str(filter_query)}%'
935 parents_query = parents_query.filter(
937 parents_query = parents_query.filter(
936 Repository.repo_name.ilike(ilike_expression))
938 Repository.repo_name.ilike(ilike_expression))
937 parents = parents_query.limit(20).all()
939 parents = parents_query.limit(20).all()
938
940
939 for parent in parents:
941 for parent in parents:
940 parent_vcs_obj = parent.scm_instance()
942 parent_vcs_obj = parent.scm_instance()
941 if parent_vcs_obj and not parent_vcs_obj.is_empty():
943 if parent_vcs_obj and not parent_vcs_obj.is_empty():
942 parent_target_repos.append(parent)
944 parent_target_repos.append(parent)
943
945
944 # get other forks, and repo itself
946 # get other forks, and repo itself
945 query = Repository.query() \
947 query = Repository.query() \
946 .order_by(func.length(Repository.repo_name)) \
948 .order_by(func.length(Repository.repo_name)) \
947 .filter(
949 .filter(
948 or_(Repository.repo_id == self.db_repo.repo_id, # repo itself
950 or_(Repository.repo_id == self.db_repo.repo_id, # repo itself
949 Repository.fork_id == self.db_repo.repo_id) # forks of this repo
951 Repository.fork_id == self.db_repo.repo_id) # forks of this repo
950 ) \
952 ) \
951 .filter(~Repository.repo_id.in_([x.repo_id for x in parent_target_repos]))
953 .filter(~Repository.repo_id.in_([x.repo_id for x in parent_target_repos]))
952
954
953 if filter_query:
955 if filter_query:
954 ilike_expression = f'%{safe_str(filter_query)}%'
956 ilike_expression = f'%{safe_str(filter_query)}%'
955 query = query.filter(Repository.repo_name.ilike(ilike_expression))
957 query = query.filter(Repository.repo_name.ilike(ilike_expression))
956
958
957 limit = max(20 - len(parent_target_repos), 5) # not less then 5
959 limit = max(20 - len(parent_target_repos), 5) # not less then 5
958 target_repos = query.limit(limit).all()
960 target_repos = query.limit(limit).all()
959
961
960 all_target_repos = target_repos + parent_target_repos
962 all_target_repos = target_repos + parent_target_repos
961
963
962 repos = []
964 repos = []
963 # This checks permissions to the repositories
965 # This checks permissions to the repositories
964 for obj in ScmModel().get_repos(all_target_repos):
966 for obj in ScmModel().get_repos(all_target_repos):
965 repos.append({
967 repos.append({
966 'id': obj['name'],
968 'id': obj['name'],
967 'text': obj['name'],
969 'text': obj['name'],
968 'type': 'repo',
970 'type': 'repo',
969 'repo_id': obj['dbrepo']['repo_id'],
971 'repo_id': obj['dbrepo']['repo_id'],
970 'repo_type': obj['dbrepo']['repo_type'],
972 'repo_type': obj['dbrepo']['repo_type'],
971 'private': obj['dbrepo']['private'],
973 'private': obj['dbrepo']['private'],
972
974
973 })
975 })
974
976
975 data = {
977 data = {
976 'more': False,
978 'more': False,
977 'results': [{
979 'results': [{
978 'text': _('Repositories'),
980 'text': _('Repositories'),
979 'children': repos
981 'children': repos
980 }] if repos else []
982 }] if repos else []
981 }
983 }
982 return data
984 return data
983
985
984 @classmethod
986 @classmethod
985 def get_comment_ids(cls, post_data):
987 def get_comment_ids(cls, post_data):
986 return filter(lambda e: e > 0, map(safe_int, aslist(post_data.get('comments'), ',')))
988 return filter(lambda e: e > 0, map(safe_int, aslist(post_data.get('comments'), ',')))
987
989
988 @LoginRequired()
990 @LoginRequired()
989 @NotAnonymous()
991 @NotAnonymous()
990 @HasRepoPermissionAnyDecorator(
992 @HasRepoPermissionAnyDecorator(
991 'repository.read', 'repository.write', 'repository.admin')
993 'repository.read', 'repository.write', 'repository.admin')
992 def pullrequest_comments(self):
994 def pullrequest_comments(self):
993 self.load_default_context()
995 self.load_default_context()
994
996
995 pull_request = PullRequest.get_or_404(
997 pull_request = PullRequest.get_or_404(
996 self.request.matchdict['pull_request_id'])
998 self.request.matchdict['pull_request_id'])
997 pull_request_id = pull_request.pull_request_id
999 pull_request_id = pull_request.pull_request_id
998 version = self.request.GET.get('version')
1000 version = self.request.GET.get('version')
999
1001
1000 _render = self.request.get_partial_renderer(
1002 _render = self.request.get_partial_renderer(
1001 'rhodecode:templates/base/sidebar.mako')
1003 'rhodecode:templates/base/sidebar.mako')
1002 c = _render.get_call_context()
1004 c = _render.get_call_context()
1003
1005
1004 (pull_request_latest,
1006 (pull_request_latest,
1005 pull_request_at_ver,
1007 pull_request_at_ver,
1006 pull_request_display_obj,
1008 pull_request_display_obj,
1007 at_version) = PullRequestModel().get_pr_version(
1009 at_version) = PullRequestModel().get_pr_version(
1008 pull_request_id, version=version)
1010 pull_request_id, version=version)
1009 versions = pull_request_display_obj.versions()
1011 versions = pull_request_display_obj.versions()
1010 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1012 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1011 c.versions = versions + [latest_ver]
1013 c.versions = versions + [latest_ver]
1012
1014
1013 c.at_version = at_version
1015 c.at_version = at_version
1014 c.at_version_num = (at_version
1016 c.at_version_num = (at_version
1015 if at_version and at_version != PullRequest.LATEST_VER
1017 if at_version and at_version != PullRequest.LATEST_VER
1016 else None)
1018 else None)
1017
1019
1018 self.register_comments_vars(c, pull_request_latest, versions, include_drafts=False)
1020 self.register_comments_vars(c, pull_request_latest, versions, include_drafts=False)
1019 all_comments = c.inline_comments_flat + c.comments
1021 all_comments = c.inline_comments_flat + c.comments
1020
1022
1021 existing_ids = self.get_comment_ids(self.request.POST)
1023 existing_ids = self.get_comment_ids(self.request.POST)
1022 return _render('comments_table', all_comments, len(all_comments),
1024 return _render('comments_table', all_comments, len(all_comments),
1023 existing_ids=existing_ids)
1025 existing_ids=existing_ids)
1024
1026
1025 @LoginRequired()
1027 @LoginRequired()
1026 @NotAnonymous()
1028 @NotAnonymous()
1027 @HasRepoPermissionAnyDecorator(
1029 @HasRepoPermissionAnyDecorator(
1028 'repository.read', 'repository.write', 'repository.admin')
1030 'repository.read', 'repository.write', 'repository.admin')
1029 def pullrequest_todos(self):
1031 def pullrequest_todos(self):
1030 self.load_default_context()
1032 self.load_default_context()
1031
1033
1032 pull_request = PullRequest.get_or_404(
1034 pull_request = PullRequest.get_or_404(
1033 self.request.matchdict['pull_request_id'])
1035 self.request.matchdict['pull_request_id'])
1034 pull_request_id = pull_request.pull_request_id
1036 pull_request_id = pull_request.pull_request_id
1035 version = self.request.GET.get('version')
1037 version = self.request.GET.get('version')
1036
1038
1037 _render = self.request.get_partial_renderer(
1039 _render = self.request.get_partial_renderer(
1038 'rhodecode:templates/base/sidebar.mako')
1040 'rhodecode:templates/base/sidebar.mako')
1039 c = _render.get_call_context()
1041 c = _render.get_call_context()
1040 (pull_request_latest,
1042 (pull_request_latest,
1041 pull_request_at_ver,
1043 pull_request_at_ver,
1042 pull_request_display_obj,
1044 pull_request_display_obj,
1043 at_version) = PullRequestModel().get_pr_version(
1045 at_version) = PullRequestModel().get_pr_version(
1044 pull_request_id, version=version)
1046 pull_request_id, version=version)
1045 versions = pull_request_display_obj.versions()
1047 versions = pull_request_display_obj.versions()
1046 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1048 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1047 c.versions = versions + [latest_ver]
1049 c.versions = versions + [latest_ver]
1048
1050
1049 c.at_version = at_version
1051 c.at_version = at_version
1050 c.at_version_num = (at_version
1052 c.at_version_num = (at_version
1051 if at_version and at_version != PullRequest.LATEST_VER
1053 if at_version and at_version != PullRequest.LATEST_VER
1052 else None)
1054 else None)
1053
1055
1054 c.unresolved_comments = CommentsModel() \
1056 c.unresolved_comments = CommentsModel() \
1055 .get_pull_request_unresolved_todos(pull_request, include_drafts=False)
1057 .get_pull_request_unresolved_todos(pull_request, include_drafts=False)
1056 c.resolved_comments = CommentsModel() \
1058 c.resolved_comments = CommentsModel() \
1057 .get_pull_request_resolved_todos(pull_request, include_drafts=False)
1059 .get_pull_request_resolved_todos(pull_request, include_drafts=False)
1058
1060
1059 all_comments = c.unresolved_comments + c.resolved_comments
1061 all_comments = c.unresolved_comments + c.resolved_comments
1060 existing_ids = self.get_comment_ids(self.request.POST)
1062 existing_ids = self.get_comment_ids(self.request.POST)
1061 return _render('comments_table', all_comments, len(c.unresolved_comments),
1063 return _render('comments_table', all_comments, len(c.unresolved_comments),
1062 todo_comments=True, existing_ids=existing_ids)
1064 todo_comments=True, existing_ids=existing_ids)
1063
1065
1064 @LoginRequired()
1066 @LoginRequired()
1065 @NotAnonymous()
1067 @NotAnonymous()
1066 @HasRepoPermissionAnyDecorator(
1068 @HasRepoPermissionAnyDecorator(
1067 'repository.read', 'repository.write', 'repository.admin')
1069 'repository.read', 'repository.write', 'repository.admin')
1068 def pullrequest_drafts(self):
1070 def pullrequest_drafts(self):
1069 self.load_default_context()
1071 self.load_default_context()
1070
1072
1071 pull_request = PullRequest.get_or_404(
1073 pull_request = PullRequest.get_or_404(
1072 self.request.matchdict['pull_request_id'])
1074 self.request.matchdict['pull_request_id'])
1073 pull_request_id = pull_request.pull_request_id
1075 pull_request_id = pull_request.pull_request_id
1074 version = self.request.GET.get('version')
1076 version = self.request.GET.get('version')
1075
1077
1076 _render = self.request.get_partial_renderer(
1078 _render = self.request.get_partial_renderer(
1077 'rhodecode:templates/base/sidebar.mako')
1079 'rhodecode:templates/base/sidebar.mako')
1078 c = _render.get_call_context()
1080 c = _render.get_call_context()
1079
1081
1080 (pull_request_latest,
1082 (pull_request_latest,
1081 pull_request_at_ver,
1083 pull_request_at_ver,
1082 pull_request_display_obj,
1084 pull_request_display_obj,
1083 at_version) = PullRequestModel().get_pr_version(
1085 at_version) = PullRequestModel().get_pr_version(
1084 pull_request_id, version=version)
1086 pull_request_id, version=version)
1085 versions = pull_request_display_obj.versions()
1087 versions = pull_request_display_obj.versions()
1086 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1088 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1087 c.versions = versions + [latest_ver]
1089 c.versions = versions + [latest_ver]
1088
1090
1089 c.at_version = at_version
1091 c.at_version = at_version
1090 c.at_version_num = (at_version
1092 c.at_version_num = (at_version
1091 if at_version and at_version != PullRequest.LATEST_VER
1093 if at_version and at_version != PullRequest.LATEST_VER
1092 else None)
1094 else None)
1093
1095
1094 c.draft_comments = CommentsModel() \
1096 c.draft_comments = CommentsModel() \
1095 .get_pull_request_drafts(self._rhodecode_db_user.user_id, pull_request)
1097 .get_pull_request_drafts(self._rhodecode_db_user.user_id, pull_request)
1096
1098
1097 all_comments = c.draft_comments
1099 all_comments = c.draft_comments
1098
1100
1099 existing_ids = self.get_comment_ids(self.request.POST)
1101 existing_ids = self.get_comment_ids(self.request.POST)
1100 return _render('comments_table', all_comments, len(all_comments),
1102 return _render('comments_table', all_comments, len(all_comments),
1101 existing_ids=existing_ids, draft_comments=True)
1103 existing_ids=existing_ids, draft_comments=True)
1102
1104
1103 @LoginRequired()
1105 @LoginRequired()
1104 @NotAnonymous()
1106 @NotAnonymous()
1105 @HasRepoPermissionAnyDecorator(
1107 @HasRepoPermissionAnyDecorator(
1106 'repository.read', 'repository.write', 'repository.admin')
1108 'repository.read', 'repository.write', 'repository.admin')
1107 @CSRFRequired()
1109 @CSRFRequired()
1108 def pull_request_create(self):
1110 def pull_request_create(self):
1109 _ = self.request.translate
1111 _ = self.request.translate
1110 self.assure_not_empty_repo()
1112 self.assure_not_empty_repo()
1111 self.load_default_context()
1113 self.load_default_context()
1112
1114
1113 controls = peppercorn.parse(self.request.POST.items())
1115 controls = peppercorn.parse(self.request.POST.items())
1114
1116
1115 try:
1117 try:
1116 form = PullRequestForm(
1118 form = PullRequestForm(
1117 self.request.translate, self.db_repo.repo_id)()
1119 self.request.translate, self.db_repo.repo_id)()
1118 _form = form.to_python(controls)
1120 _form = form.to_python(controls)
1119 except formencode.Invalid as errors:
1121 except formencode.Invalid as errors:
1120 if errors.error_dict.get('revisions'):
1122 if errors.error_dict.get('revisions'):
1121 msg = 'Revisions: {}'.format(errors.error_dict['revisions'])
1123 msg = 'Revisions: {}'.format(errors.error_dict['revisions'])
1122 elif errors.error_dict.get('pullrequest_title'):
1124 elif errors.error_dict.get('pullrequest_title'):
1123 msg = errors.error_dict.get('pullrequest_title')
1125 msg = errors.error_dict.get('pullrequest_title')
1124 else:
1126 else:
1125 msg = _('Error creating pull request: {}').format(errors)
1127 msg = _('Error creating pull request: {}').format(errors)
1126 log.exception(msg)
1128 log.exception(msg)
1127 h.flash(msg, 'error')
1129 h.flash(msg, 'error')
1128
1130
1129 # would rather just go back to form ...
1131 # would rather just go back to form ...
1130 raise HTTPFound(
1132 raise HTTPFound(
1131 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1133 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1132
1134
1133 source_repo = _form['source_repo']
1135 source_repo = _form['source_repo']
1134 source_ref = _form['source_ref']
1136 source_ref = _form['source_ref']
1135 target_repo = _form['target_repo']
1137 target_repo = _form['target_repo']
1136 target_ref = _form['target_ref']
1138 target_ref = _form['target_ref']
1137 commit_ids = _form['revisions'][::-1]
1139 commit_ids = _form['revisions'][::-1]
1138 common_ancestor_id = _form['common_ancestor']
1140 common_ancestor_id = _form['common_ancestor']
1139
1141
1140 # find the ancestor for this pr
1142 # find the ancestor for this pr
1141 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
1143 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
1142 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
1144 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
1143
1145
1144 if not (source_db_repo or target_db_repo):
1146 if not (source_db_repo or target_db_repo):
1145 h.flash(_('source_repo or target repo not found'), category='error')
1147 h.flash(_('source_repo or target repo not found'), category='error')
1146 raise HTTPFound(
1148 raise HTTPFound(
1147 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1149 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1148
1150
1149 # re-check permissions again here
1151 # re-check permissions again here
1150 # source_repo we must have read permissions
1152 # source_repo we must have read permissions
1151
1153
1152 source_perm = HasRepoPermissionAny(
1154 source_perm = HasRepoPermissionAny(
1153 'repository.read', 'repository.write', 'repository.admin')(
1155 'repository.read', 'repository.write', 'repository.admin')(
1154 source_db_repo.repo_name)
1156 source_db_repo.repo_name)
1155 if not source_perm:
1157 if not source_perm:
1156 msg = _('Not Enough permissions to source repo `{}`.'.format(
1158 msg = _('Not Enough permissions to source repo `{}`.'.format(
1157 source_db_repo.repo_name))
1159 source_db_repo.repo_name))
1158 h.flash(msg, category='error')
1160 h.flash(msg, category='error')
1159 # copy the args back to redirect
1161 # copy the args back to redirect
1160 org_query = self.request.GET.mixed()
1162 org_query = self.request.GET.mixed()
1161 raise HTTPFound(
1163 raise HTTPFound(
1162 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1164 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1163 _query=org_query))
1165 _query=org_query))
1164
1166
1165 # target repo we must have read permissions, and also later on
1167 # target repo we must have read permissions, and also later on
1166 # we want to check branch permissions here
1168 # we want to check branch permissions here
1167 target_perm = HasRepoPermissionAny(
1169 target_perm = HasRepoPermissionAny(
1168 'repository.read', 'repository.write', 'repository.admin')(
1170 'repository.read', 'repository.write', 'repository.admin')(
1169 target_db_repo.repo_name)
1171 target_db_repo.repo_name)
1170 if not target_perm:
1172 if not target_perm:
1171 msg = _('Not Enough permissions to target repo `{}`.'.format(
1173 msg = _('Not Enough permissions to target repo `{}`.'.format(
1172 target_db_repo.repo_name))
1174 target_db_repo.repo_name))
1173 h.flash(msg, category='error')
1175 h.flash(msg, category='error')
1174 # copy the args back to redirect
1176 # copy the args back to redirect
1175 org_query = self.request.GET.mixed()
1177 org_query = self.request.GET.mixed()
1176 raise HTTPFound(
1178 raise HTTPFound(
1177 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1179 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1178 _query=org_query))
1180 _query=org_query))
1179
1181
1180 source_scm = source_db_repo.scm_instance()
1182 source_scm = source_db_repo.scm_instance()
1181 target_scm = target_db_repo.scm_instance()
1183 target_scm = target_db_repo.scm_instance()
1182
1184
1183 source_ref_obj = unicode_to_reference(source_ref)
1185 source_ref_obj = unicode_to_reference(source_ref)
1184 target_ref_obj = unicode_to_reference(target_ref)
1186 target_ref_obj = unicode_to_reference(target_ref)
1185
1187
1186 source_commit = source_scm.get_commit(source_ref_obj.commit_id)
1188 source_commit = source_scm.get_commit(source_ref_obj.commit_id)
1187 target_commit = target_scm.get_commit(target_ref_obj.commit_id)
1189 target_commit = target_scm.get_commit(target_ref_obj.commit_id)
1188
1190
1189 ancestor = source_scm.get_common_ancestor(
1191 ancestor = source_scm.get_common_ancestor(
1190 source_commit.raw_id, target_commit.raw_id, target_scm)
1192 source_commit.raw_id, target_commit.raw_id, target_scm)
1191
1193
1192 # recalculate target ref based on ancestor
1194 # recalculate target ref based on ancestor
1193 target_ref = ':'.join((target_ref_obj.type, target_ref_obj.name, ancestor))
1195 target_ref = ':'.join((target_ref_obj.type, target_ref_obj.name, ancestor))
1194
1196
1195 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1197 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1196 PullRequestModel().get_reviewer_functions()
1198 PullRequestModel().get_reviewer_functions()
1197
1199
1198 # recalculate reviewers logic, to make sure we can validate this
1200 # recalculate reviewers logic, to make sure we can validate this
1199 reviewer_rules = get_default_reviewers_data(
1201 reviewer_rules = get_default_reviewers_data(
1200 self._rhodecode_db_user,
1202 self._rhodecode_db_user,
1201 source_db_repo,
1203 source_db_repo,
1202 source_ref_obj,
1204 source_ref_obj,
1203 target_db_repo,
1205 target_db_repo,
1204 target_ref_obj,
1206 target_ref_obj,
1205 include_diff_info=False)
1207 include_diff_info=False)
1206
1208
1207 reviewers = validate_default_reviewers(_form['review_members'], reviewer_rules)
1209 reviewers = validate_default_reviewers(_form['review_members'], reviewer_rules)
1208 observers = validate_observers(_form['observer_members'], reviewer_rules)
1210 observers = validate_observers(_form['observer_members'], reviewer_rules)
1209
1211
1210 pullrequest_title = _form['pullrequest_title']
1212 pullrequest_title = _form['pullrequest_title']
1211 title_source_ref = source_ref_obj.name
1213 title_source_ref = source_ref_obj.name
1212 if not pullrequest_title:
1214 if not pullrequest_title:
1213 pullrequest_title = PullRequestModel().generate_pullrequest_title(
1215 pullrequest_title = PullRequestModel().generate_pullrequest_title(
1214 source=source_repo,
1216 source=source_repo,
1215 source_ref=title_source_ref,
1217 source_ref=title_source_ref,
1216 target=target_repo
1218 target=target_repo
1217 )
1219 )
1218
1220
1219 description = _form['pullrequest_desc']
1221 description = _form['pullrequest_desc']
1220 description_renderer = _form['description_renderer']
1222 description_renderer = _form['description_renderer']
1221
1223
1222 try:
1224 try:
1223 pull_request = PullRequestModel().create(
1225 pull_request = PullRequestModel().create(
1224 created_by=self._rhodecode_user.user_id,
1226 created_by=self._rhodecode_user.user_id,
1225 source_repo=source_repo,
1227 source_repo=source_repo,
1226 source_ref=source_ref,
1228 source_ref=source_ref,
1227 target_repo=target_repo,
1229 target_repo=target_repo,
1228 target_ref=target_ref,
1230 target_ref=target_ref,
1229 revisions=commit_ids,
1231 revisions=commit_ids,
1230 common_ancestor_id=common_ancestor_id,
1232 common_ancestor_id=common_ancestor_id,
1231 reviewers=reviewers,
1233 reviewers=reviewers,
1232 observers=observers,
1234 observers=observers,
1233 title=pullrequest_title,
1235 title=pullrequest_title,
1234 description=description,
1236 description=description,
1235 description_renderer=description_renderer,
1237 description_renderer=description_renderer,
1236 reviewer_data=reviewer_rules,
1238 reviewer_data=reviewer_rules,
1237 auth_user=self._rhodecode_user
1239 auth_user=self._rhodecode_user
1238 )
1240 )
1239 Session().commit()
1241 Session().commit()
1240
1242
1241 h.flash(_('Successfully opened new pull request'),
1243 h.flash(_('Successfully opened new pull request'),
1242 category='success')
1244 category='success')
1243 except Exception:
1245 except Exception:
1244 msg = _('Error occurred during creation of this pull request.')
1246 msg = _('Error occurred during creation of this pull request.')
1245 log.exception(msg)
1247 log.exception(msg)
1246 h.flash(msg, category='error')
1248 h.flash(msg, category='error')
1247
1249
1248 # copy the args back to redirect
1250 # copy the args back to redirect
1249 org_query = self.request.GET.mixed()
1251 org_query = self.request.GET.mixed()
1250 raise HTTPFound(
1252 raise HTTPFound(
1251 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1253 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1252 _query=org_query))
1254 _query=org_query))
1253
1255
1254 raise HTTPFound(
1256 raise HTTPFound(
1255 h.route_path('pullrequest_show', repo_name=target_repo,
1257 h.route_path('pullrequest_show', repo_name=target_repo,
1256 pull_request_id=pull_request.pull_request_id))
1258 pull_request_id=pull_request.pull_request_id))
1257
1259
1258 @LoginRequired()
1260 @LoginRequired()
1259 @NotAnonymous()
1261 @NotAnonymous()
1260 @HasRepoPermissionAnyDecorator(
1262 @HasRepoPermissionAnyDecorator(
1261 'repository.read', 'repository.write', 'repository.admin')
1263 'repository.read', 'repository.write', 'repository.admin')
1262 @CSRFRequired()
1264 @CSRFRequired()
1263 def pull_request_update(self):
1265 def pull_request_update(self):
1264 pull_request = PullRequest.get_or_404(
1266 pull_request = PullRequest.get_or_404(
1265 self.request.matchdict['pull_request_id'])
1267 self.request.matchdict['pull_request_id'])
1266 _ = self.request.translate
1268 _ = self.request.translate
1267
1269
1268 c = self.load_default_context()
1270 c = self.load_default_context()
1269 redirect_url = None
1271 redirect_url = None
1270 # we do this check as first, because we want to know ASAP in the flow that
1272 # we do this check as first, because we want to know ASAP in the flow that
1271 # pr is updating currently
1273 # pr is updating currently
1272 is_state_changing = pull_request.is_state_changing()
1274 is_state_changing = pull_request.is_state_changing()
1273
1275
1274 if pull_request.is_closed():
1276 if pull_request.is_closed():
1275 log.debug('update: forbidden because pull request is closed')
1277 log.debug('update: forbidden because pull request is closed')
1276 msg = _('Cannot update closed pull requests.')
1278 msg = _('Cannot update closed pull requests.')
1277 h.flash(msg, category='error')
1279 h.flash(msg, category='error')
1278 return {'response': True,
1280 return {'response': True,
1279 'redirect_url': redirect_url}
1281 'redirect_url': redirect_url}
1280
1282
1281 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
1283 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
1282
1284
1283 # only owner or admin can update it
1285 # only owner or admin can update it
1284 allowed_to_update = PullRequestModel().check_user_update(
1286 allowed_to_update = PullRequestModel().check_user_update(
1285 pull_request, self._rhodecode_user)
1287 pull_request, self._rhodecode_user)
1286
1288
1287 if allowed_to_update:
1289 if allowed_to_update:
1288 controls = peppercorn.parse(self.request.POST.items())
1290 controls = peppercorn.parse(self.request.POST.items())
1289 force_refresh = str2bool(self.request.POST.get('force_refresh', 'false'))
1291 force_refresh = str2bool(self.request.POST.get('force_refresh', 'false'))
1290 do_update_commits = str2bool(self.request.POST.get('update_commits', 'false'))
1292 do_update_commits = str2bool(self.request.POST.get('update_commits', 'false'))
1291
1293
1292 if 'review_members' in controls:
1294 if 'review_members' in controls:
1293 self._update_reviewers(
1295 self._update_reviewers(
1294 c,
1296 c,
1295 pull_request, controls['review_members'],
1297 pull_request, controls['review_members'],
1296 pull_request.reviewer_data,
1298 pull_request.reviewer_data,
1297 PullRequestReviewers.ROLE_REVIEWER)
1299 PullRequestReviewers.ROLE_REVIEWER)
1298 elif 'observer_members' in controls:
1300 elif 'observer_members' in controls:
1299 self._update_reviewers(
1301 self._update_reviewers(
1300 c,
1302 c,
1301 pull_request, controls['observer_members'],
1303 pull_request, controls['observer_members'],
1302 pull_request.reviewer_data,
1304 pull_request.reviewer_data,
1303 PullRequestReviewers.ROLE_OBSERVER)
1305 PullRequestReviewers.ROLE_OBSERVER)
1304 elif do_update_commits:
1306 elif do_update_commits:
1305 if is_state_changing:
1307 if is_state_changing:
1306 log.debug('commits update: forbidden because pull request is in state %s',
1308 log.debug('commits update: forbidden because pull request is in state %s',
1307 pull_request.pull_request_state)
1309 pull_request.pull_request_state)
1308 msg = _('Cannot update pull requests commits in state other than `{}`. '
1310 msg = _('Cannot update pull requests commits in state other than `{}`. '
1309 'Current state is: `{}`').format(
1311 'Current state is: `{}`').format(
1310 PullRequest.STATE_CREATED, pull_request.pull_request_state)
1312 PullRequest.STATE_CREATED, pull_request.pull_request_state)
1311 h.flash(msg, category='error')
1313 h.flash(msg, category='error')
1312 return {'response': True,
1314 return {'response': True,
1313 'redirect_url': redirect_url}
1315 'redirect_url': redirect_url}
1314
1316
1315 self._update_commits(c, pull_request)
1317 self._update_commits(c, pull_request)
1316 if force_refresh:
1318 if force_refresh:
1317 redirect_url = h.route_path(
1319 redirect_url = h.route_path(
1318 'pullrequest_show', repo_name=self.db_repo_name,
1320 'pullrequest_show', repo_name=self.db_repo_name,
1319 pull_request_id=pull_request.pull_request_id,
1321 pull_request_id=pull_request.pull_request_id,
1320 _query={"force_refresh": 1})
1322 _query={"force_refresh": 1})
1321 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
1323 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
1322 self._edit_pull_request(pull_request)
1324 self._edit_pull_request(pull_request)
1323 else:
1325 else:
1324 log.error('Unhandled update data.')
1326 log.error('Unhandled update data.')
1325 raise HTTPBadRequest()
1327 raise HTTPBadRequest()
1326
1328
1327 return {'response': True,
1329 return {'response': True,
1328 'redirect_url': redirect_url}
1330 'redirect_url': redirect_url}
1329 raise HTTPForbidden()
1331 raise HTTPForbidden()
1330
1332
1331 def _edit_pull_request(self, pull_request):
1333 def _edit_pull_request(self, pull_request):
1332 """
1334 """
1333 Edit title and description
1335 Edit title and description
1334 """
1336 """
1335 _ = self.request.translate
1337 _ = self.request.translate
1336
1338
1337 try:
1339 try:
1338 PullRequestModel().edit(
1340 PullRequestModel().edit(
1339 pull_request,
1341 pull_request,
1340 self.request.POST.get('title'),
1342 self.request.POST.get('title'),
1341 self.request.POST.get('description'),
1343 self.request.POST.get('description'),
1342 self.request.POST.get('description_renderer'),
1344 self.request.POST.get('description_renderer'),
1343 self._rhodecode_user)
1345 self._rhodecode_user)
1344 except ValueError:
1346 except ValueError:
1345 msg = _('Cannot update closed pull requests.')
1347 msg = _('Cannot update closed pull requests.')
1346 h.flash(msg, category='error')
1348 h.flash(msg, category='error')
1347 return
1349 return
1348 else:
1350 else:
1349 Session().commit()
1351 Session().commit()
1350
1352
1351 msg = _('Pull request title & description updated.')
1353 msg = _('Pull request title & description updated.')
1352 h.flash(msg, category='success')
1354 h.flash(msg, category='success')
1353 return
1355 return
1354
1356
1355 def _update_commits(self, c, pull_request):
1357 def _update_commits(self, c, pull_request):
1356 _ = self.request.translate
1358 _ = self.request.translate
1357 log.debug('pull-request: running update commits actions')
1359 log.debug('pull-request: running update commits actions')
1358
1360
1359 @retry(exception=Exception, n_tries=3, delay=2)
1361 @retry(exception=Exception, n_tries=3, delay=2)
1360 def commits_update():
1362 def commits_update():
1361 return PullRequestModel().update_commits(
1363 return PullRequestModel().update_commits(
1362 pull_request, self._rhodecode_db_user)
1364 pull_request, self._rhodecode_db_user)
1363
1365
1364 with pull_request.set_state(PullRequest.STATE_UPDATING):
1366 with pull_request.set_state(PullRequest.STATE_UPDATING):
1365 resp = commits_update() # retry x3
1367 resp = commits_update() # retry x3
1366
1368
1367 if resp.executed:
1369 if resp.executed:
1368
1370
1369 if resp.target_changed and resp.source_changed:
1371 if resp.target_changed and resp.source_changed:
1370 changed = 'target and source repositories'
1372 changed = 'target and source repositories'
1371 elif resp.target_changed and not resp.source_changed:
1373 elif resp.target_changed and not resp.source_changed:
1372 changed = 'target repository'
1374 changed = 'target repository'
1373 elif not resp.target_changed and resp.source_changed:
1375 elif not resp.target_changed and resp.source_changed:
1374 changed = 'source repository'
1376 changed = 'source repository'
1375 else:
1377 else:
1376 changed = 'nothing'
1378 changed = 'nothing'
1377
1379
1378 msg = _('Pull request updated to "{source_commit_id}" with '
1380 msg = _('Pull request updated to "{source_commit_id}" with '
1379 '{count_added} added, {count_removed} removed commits. '
1381 '{count_added} added, {count_removed} removed commits. '
1380 'Source of changes: {change_source}.')
1382 'Source of changes: {change_source}.')
1381 msg = msg.format(
1383 msg = msg.format(
1382 source_commit_id=pull_request.source_ref_parts.commit_id,
1384 source_commit_id=pull_request.source_ref_parts.commit_id,
1383 count_added=len(resp.changes.added),
1385 count_added=len(resp.changes.added),
1384 count_removed=len(resp.changes.removed),
1386 count_removed=len(resp.changes.removed),
1385 change_source=changed)
1387 change_source=changed)
1386 h.flash(msg, category='success')
1388 h.flash(msg, category='success')
1387 channelstream.pr_update_channelstream_push(
1389 channelstream.pr_update_channelstream_push(
1388 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1390 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1389 else:
1391 else:
1390 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1392 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1391 warning_reasons = [
1393 warning_reasons = [
1392 UpdateFailureReason.NO_CHANGE,
1394 UpdateFailureReason.NO_CHANGE,
1393 UpdateFailureReason.WRONG_REF_TYPE,
1395 UpdateFailureReason.WRONG_REF_TYPE,
1394 ]
1396 ]
1395 category = 'warning' if resp.reason in warning_reasons else 'error'
1397 category = 'warning' if resp.reason in warning_reasons else 'error'
1396 h.flash(msg, category=category)
1398 h.flash(msg, category=category)
1397
1399
1398 def _update_reviewers(self, c, pull_request, review_members, reviewer_rules, role):
1400 def _update_reviewers(self, c, pull_request, review_members, reviewer_rules, role):
1399 _ = self.request.translate
1401 _ = self.request.translate
1400
1402
1401 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1403 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1402 PullRequestModel().get_reviewer_functions()
1404 PullRequestModel().get_reviewer_functions()
1403
1405
1404 if role == PullRequestReviewers.ROLE_REVIEWER:
1406 if role == PullRequestReviewers.ROLE_REVIEWER:
1405 try:
1407 try:
1406 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1408 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1407 except ValueError as e:
1409 except ValueError as e:
1408 log.error(f'Reviewers Validation: {e}')
1410 log.error(f'Reviewers Validation: {e}')
1409 h.flash(e, category='error')
1411 h.flash(e, category='error')
1410 return
1412 return
1411
1413
1412 old_calculated_status = pull_request.calculated_review_status()
1414 old_calculated_status = pull_request.calculated_review_status()
1413 PullRequestModel().update_reviewers(
1415 PullRequestModel().update_reviewers(
1414 pull_request, reviewers, self._rhodecode_db_user)
1416 pull_request, reviewers, self._rhodecode_db_user)
1415
1417
1416 Session().commit()
1418 Session().commit()
1417
1419
1418 msg = _('Pull request reviewers updated.')
1420 msg = _('Pull request reviewers updated.')
1419 h.flash(msg, category='success')
1421 h.flash(msg, category='success')
1420 channelstream.pr_update_channelstream_push(
1422 channelstream.pr_update_channelstream_push(
1421 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1423 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1422
1424
1423 # trigger status changed if change in reviewers changes the status
1425 # trigger status changed if change in reviewers changes the status
1424 calculated_status = pull_request.calculated_review_status()
1426 calculated_status = pull_request.calculated_review_status()
1425 if old_calculated_status != calculated_status:
1427 if old_calculated_status != calculated_status:
1426 PullRequestModel().trigger_pull_request_hook(
1428 PullRequestModel().trigger_pull_request_hook(
1427 pull_request, self._rhodecode_user, 'review_status_change',
1429 pull_request, self._rhodecode_user, 'review_status_change',
1428 data={'status': calculated_status})
1430 data={'status': calculated_status})
1429
1431
1430 elif role == PullRequestReviewers.ROLE_OBSERVER:
1432 elif role == PullRequestReviewers.ROLE_OBSERVER:
1431 try:
1433 try:
1432 observers = validate_observers(review_members, reviewer_rules)
1434 observers = validate_observers(review_members, reviewer_rules)
1433 except ValueError as e:
1435 except ValueError as e:
1434 log.error(f'Observers Validation: {e}')
1436 log.error(f'Observers Validation: {e}')
1435 h.flash(e, category='error')
1437 h.flash(e, category='error')
1436 return
1438 return
1437
1439
1438 PullRequestModel().update_observers(
1440 PullRequestModel().update_observers(
1439 pull_request, observers, self._rhodecode_db_user)
1441 pull_request, observers, self._rhodecode_db_user)
1440
1442
1441 Session().commit()
1443 Session().commit()
1442 msg = _('Pull request observers updated.')
1444 msg = _('Pull request observers updated.')
1443 h.flash(msg, category='success')
1445 h.flash(msg, category='success')
1444 channelstream.pr_update_channelstream_push(
1446 channelstream.pr_update_channelstream_push(
1445 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1447 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1446
1448
1447 @LoginRequired()
1449 @LoginRequired()
1448 @NotAnonymous()
1450 @NotAnonymous()
1449 @HasRepoPermissionAnyDecorator(
1451 @HasRepoPermissionAnyDecorator(
1450 'repository.read', 'repository.write', 'repository.admin')
1452 'repository.read', 'repository.write', 'repository.admin')
1451 @CSRFRequired()
1453 @CSRFRequired()
1452 def pull_request_merge(self):
1454 def pull_request_merge(self):
1453 """
1455 """
1454 Merge will perform a server-side merge of the specified
1456 Merge will perform a server-side merge of the specified
1455 pull request, if the pull request is approved and mergeable.
1457 pull request, if the pull request is approved and mergeable.
1456 After successful merging, the pull request is automatically
1458 After successful merging, the pull request is automatically
1457 closed, with a relevant comment.
1459 closed, with a relevant comment.
1458 """
1460 """
1459 pull_request = PullRequest.get_or_404(
1461 pull_request = PullRequest.get_or_404(
1460 self.request.matchdict['pull_request_id'])
1462 self.request.matchdict['pull_request_id'])
1461 _ = self.request.translate
1463 _ = self.request.translate
1462
1464
1463 if pull_request.is_state_changing():
1465 if pull_request.is_state_changing():
1464 log.debug('show: forbidden because pull request is in state %s',
1466 log.debug('show: forbidden because pull request is in state %s',
1465 pull_request.pull_request_state)
1467 pull_request.pull_request_state)
1466 msg = _('Cannot merge pull requests in state other than `{}`. '
1468 msg = _('Cannot merge pull requests in state other than `{}`. '
1467 'Current state is: `{}`').format(PullRequest.STATE_CREATED,
1469 'Current state is: `{}`').format(PullRequest.STATE_CREATED,
1468 pull_request.pull_request_state)
1470 pull_request.pull_request_state)
1469 h.flash(msg, category='error')
1471 h.flash(msg, category='error')
1470 raise HTTPFound(
1472 raise HTTPFound(
1471 h.route_path('pullrequest_show',
1473 h.route_path('pullrequest_show',
1472 repo_name=pull_request.target_repo.repo_name,
1474 repo_name=pull_request.target_repo.repo_name,
1473 pull_request_id=pull_request.pull_request_id))
1475 pull_request_id=pull_request.pull_request_id))
1474
1476
1475 self.load_default_context()
1477 self.load_default_context()
1476
1478
1477 with pull_request.set_state(PullRequest.STATE_UPDATING):
1479 with pull_request.set_state(PullRequest.STATE_UPDATING):
1478 check = MergeCheck.validate(
1480 check = MergeCheck.validate(
1479 pull_request, auth_user=self._rhodecode_user,
1481 pull_request, auth_user=self._rhodecode_user,
1480 translator=self.request.translate)
1482 translator=self.request.translate)
1481 merge_possible = not check.failed
1483 merge_possible = not check.failed
1482
1484
1483 for err_type, error_msg in check.errors:
1485 for err_type, error_msg in check.errors:
1484 h.flash(error_msg, category=err_type)
1486 h.flash(error_msg, category=err_type)
1485
1487
1486 if merge_possible:
1488 if merge_possible:
1487 log.debug("Pre-conditions checked, trying to merge.")
1489 log.debug("Pre-conditions checked, trying to merge.")
1488 extras = vcs_operation_context(
1490 extras = vcs_operation_context(
1489 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1491 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1490 username=self._rhodecode_db_user.username, action='push',
1492 username=self._rhodecode_db_user.username, action='push',
1491 scm=pull_request.target_repo.repo_type)
1493 scm=pull_request.target_repo.repo_type)
1492 with pull_request.set_state(PullRequest.STATE_UPDATING):
1494 with pull_request.set_state(PullRequest.STATE_UPDATING):
1493 self._merge_pull_request(
1495 self._merge_pull_request(
1494 pull_request, self._rhodecode_db_user, extras)
1496 pull_request, self._rhodecode_db_user, extras)
1495 else:
1497 else:
1496 log.debug("Pre-conditions failed, NOT merging.")
1498 log.debug("Pre-conditions failed, NOT merging.")
1497
1499
1498 raise HTTPFound(
1500 raise HTTPFound(
1499 h.route_path('pullrequest_show',
1501 h.route_path('pullrequest_show',
1500 repo_name=pull_request.target_repo.repo_name,
1502 repo_name=pull_request.target_repo.repo_name,
1501 pull_request_id=pull_request.pull_request_id))
1503 pull_request_id=pull_request.pull_request_id))
1502
1504
1503 def _merge_pull_request(self, pull_request, user, extras):
1505 def _merge_pull_request(self, pull_request, user, extras):
1504 _ = self.request.translate
1506 _ = self.request.translate
1505 merge_resp = PullRequestModel().merge_repo(pull_request, user, extras=extras)
1507 merge_resp = PullRequestModel().merge_repo(pull_request, user, extras=extras)
1506
1508
1507 if merge_resp.executed:
1509 if merge_resp.executed:
1508 log.debug("The merge was successful, closing the pull request.")
1510 log.debug("The merge was successful, closing the pull request.")
1509 PullRequestModel().close_pull_request(
1511 PullRequestModel().close_pull_request(
1510 pull_request.pull_request_id, user)
1512 pull_request.pull_request_id, user)
1511 Session().commit()
1513 Session().commit()
1512 msg = _('Pull request was successfully merged and closed.')
1514 msg = _('Pull request was successfully merged and closed.')
1513 h.flash(msg, category='success')
1515 h.flash(msg, category='success')
1514 else:
1516 else:
1515 log.debug(
1517 log.debug(
1516 "The merge was not successful. Merge response: %s", merge_resp)
1518 "The merge was not successful. Merge response: %s", merge_resp)
1517 msg = merge_resp.merge_status_message
1519 msg = merge_resp.merge_status_message
1518 h.flash(msg, category='error')
1520 h.flash(msg, category='error')
1519
1521
1520 @LoginRequired()
1522 @LoginRequired()
1521 @NotAnonymous()
1523 @NotAnonymous()
1522 @HasRepoPermissionAnyDecorator(
1524 @HasRepoPermissionAnyDecorator(
1523 'repository.read', 'repository.write', 'repository.admin')
1525 'repository.read', 'repository.write', 'repository.admin')
1524 @CSRFRequired()
1526 @CSRFRequired()
1525 def pull_request_delete(self):
1527 def pull_request_delete(self):
1526 _ = self.request.translate
1528 _ = self.request.translate
1527
1529
1528 pull_request = PullRequest.get_or_404(
1530 pull_request = PullRequest.get_or_404(
1529 self.request.matchdict['pull_request_id'])
1531 self.request.matchdict['pull_request_id'])
1530 self.load_default_context()
1532 self.load_default_context()
1531
1533
1532 pr_closed = pull_request.is_closed()
1534 pr_closed = pull_request.is_closed()
1533 allowed_to_delete = PullRequestModel().check_user_delete(
1535 allowed_to_delete = PullRequestModel().check_user_delete(
1534 pull_request, self._rhodecode_user) and not pr_closed
1536 pull_request, self._rhodecode_user) and not pr_closed
1535
1537
1536 # only owner can delete it !
1538 # only owner can delete it !
1537 if allowed_to_delete:
1539 if allowed_to_delete:
1538 PullRequestModel().delete(pull_request, self._rhodecode_user)
1540 PullRequestModel().delete(pull_request, self._rhodecode_user)
1539 Session().commit()
1541 Session().commit()
1540 h.flash(_('Successfully deleted pull request'),
1542 h.flash(_('Successfully deleted pull request'),
1541 category='success')
1543 category='success')
1542 raise HTTPFound(h.route_path('pullrequest_show_all',
1544 raise HTTPFound(h.route_path('pullrequest_show_all',
1543 repo_name=self.db_repo_name))
1545 repo_name=self.db_repo_name))
1544
1546
1545 log.warning('user %s tried to delete pull request without access',
1547 log.warning('user %s tried to delete pull request without access',
1546 self._rhodecode_user)
1548 self._rhodecode_user)
1547 raise HTTPNotFound()
1549 raise HTTPNotFound()
1548
1550
1549 def _pull_request_comments_create(self, pull_request, comments):
1551 def _pull_request_comments_create(self, pull_request, comments):
1550 _ = self.request.translate
1552 _ = self.request.translate
1551 data = {}
1553 data = {}
1552 if not comments:
1554 if not comments:
1553 return
1555 return
1554 pull_request_id = pull_request.pull_request_id
1556 pull_request_id = pull_request.pull_request_id
1555
1557
1556 all_drafts = len([x for x in comments if str2bool(x['is_draft'])]) == len(comments)
1558 all_drafts = len([x for x in comments if str2bool(x['is_draft'])]) == len(comments)
1557
1559
1558 for entry in comments:
1560 for entry in comments:
1559 c = self.load_default_context()
1561 c = self.load_default_context()
1560 comment_type = entry['comment_type']
1562 comment_type = entry['comment_type']
1561 text = entry['text']
1563 text = entry['text']
1562 status = entry['status']
1564 status = entry['status']
1563 is_draft = str2bool(entry['is_draft'])
1565 is_draft = str2bool(entry['is_draft'])
1564 resolves_comment_id = entry['resolves_comment_id']
1566 resolves_comment_id = entry['resolves_comment_id']
1565 close_pull_request = entry['close_pull_request']
1567 close_pull_request = entry['close_pull_request']
1566 f_path = entry['f_path']
1568 f_path = entry['f_path']
1567 line_no = entry['line']
1569 line_no = entry['line']
1568 target_elem_id = f'file-{h.safeid(h.safe_str(f_path))}'
1570 target_elem_id = f'file-{h.safeid(h.safe_str(f_path))}'
1569
1571
1570 # the logic here should work like following, if we submit close
1572 # the logic here should work like following, if we submit close
1571 # pr comment, use `close_pull_request_with_comment` function
1573 # pr comment, use `close_pull_request_with_comment` function
1572 # else handle regular comment logic
1574 # else handle regular comment logic
1573
1575
1574 if close_pull_request:
1576 if close_pull_request:
1575 # only owner or admin or person with write permissions
1577 # only owner or admin or person with write permissions
1576 allowed_to_close = PullRequestModel().check_user_update(
1578 allowed_to_close = PullRequestModel().check_user_update(
1577 pull_request, self._rhodecode_user)
1579 pull_request, self._rhodecode_user)
1578 if not allowed_to_close:
1580 if not allowed_to_close:
1579 log.debug('comment: forbidden because not allowed to close '
1581 log.debug('comment: forbidden because not allowed to close '
1580 'pull request %s', pull_request_id)
1582 'pull request %s', pull_request_id)
1581 raise HTTPForbidden()
1583 raise HTTPForbidden()
1582
1584
1583 # This also triggers `review_status_change`
1585 # This also triggers `review_status_change`
1584 comment, status = PullRequestModel().close_pull_request_with_comment(
1586 comment, status = PullRequestModel().close_pull_request_with_comment(
1585 pull_request, self._rhodecode_user, self.db_repo, message=text,
1587 pull_request, self._rhodecode_user, self.db_repo, message=text,
1586 auth_user=self._rhodecode_user)
1588 auth_user=self._rhodecode_user)
1587 Session().flush()
1589 Session().flush()
1588 is_inline = comment.is_inline
1590 is_inline = comment.is_inline
1589
1591
1590 PullRequestModel().trigger_pull_request_hook(
1592 PullRequestModel().trigger_pull_request_hook(
1591 pull_request, self._rhodecode_user, 'comment',
1593 pull_request, self._rhodecode_user, 'comment',
1592 data={'comment': comment})
1594 data={'comment': comment})
1593
1595
1594 else:
1596 else:
1595 # regular comment case, could be inline, or one with status.
1597 # regular comment case, could be inline, or one with status.
1596 # for that one we check also permissions
1598 # for that one we check also permissions
1597 # Additionally ENSURE if somehow draft is sent we're then unable to change status
1599 # Additionally ENSURE if somehow draft is sent we're then unable to change status
1598 allowed_to_change_status = PullRequestModel().check_user_change_status(
1600 allowed_to_change_status = PullRequestModel().check_user_change_status(
1599 pull_request, self._rhodecode_user) and not is_draft
1601 pull_request, self._rhodecode_user) and not is_draft
1600
1602
1601 if status and allowed_to_change_status:
1603 if status and allowed_to_change_status:
1602 message = (_('Status change %(transition_icon)s %(status)s')
1604 message = (_('Status change %(transition_icon)s %(status)s')
1603 % {'transition_icon': '>',
1605 % {'transition_icon': '>',
1604 'status': ChangesetStatus.get_status_lbl(status)})
1606 'status': ChangesetStatus.get_status_lbl(status)})
1605 text = text or message
1607 text = text or message
1606
1608
1607 comment = CommentsModel().create(
1609 comment = CommentsModel().create(
1608 text=text,
1610 text=text,
1609 repo=self.db_repo.repo_id,
1611 repo=self.db_repo.repo_id,
1610 user=self._rhodecode_user.user_id,
1612 user=self._rhodecode_user.user_id,
1611 pull_request=pull_request,
1613 pull_request=pull_request,
1612 f_path=f_path,
1614 f_path=f_path,
1613 line_no=line_no,
1615 line_no=line_no,
1614 status_change=(ChangesetStatus.get_status_lbl(status)
1616 status_change=(ChangesetStatus.get_status_lbl(status)
1615 if status and allowed_to_change_status else None),
1617 if status and allowed_to_change_status else None),
1616 status_change_type=(status
1618 status_change_type=(status
1617 if status and allowed_to_change_status else None),
1619 if status and allowed_to_change_status else None),
1618 comment_type=comment_type,
1620 comment_type=comment_type,
1619 is_draft=is_draft,
1621 is_draft=is_draft,
1620 resolves_comment_id=resolves_comment_id,
1622 resolves_comment_id=resolves_comment_id,
1621 auth_user=self._rhodecode_user,
1623 auth_user=self._rhodecode_user,
1622 send_email=not is_draft, # skip notification for draft comments
1624 send_email=not is_draft, # skip notification for draft comments
1623 )
1625 )
1624 is_inline = comment.is_inline
1626 is_inline = comment.is_inline
1625
1627
1626 if allowed_to_change_status:
1628 if allowed_to_change_status:
1627 # calculate old status before we change it
1629 # calculate old status before we change it
1628 old_calculated_status = pull_request.calculated_review_status()
1630 old_calculated_status = pull_request.calculated_review_status()
1629
1631
1630 # get status if set !
1632 # get status if set !
1631 if status:
1633 if status:
1632 ChangesetStatusModel().set_status(
1634 ChangesetStatusModel().set_status(
1633 self.db_repo.repo_id,
1635 self.db_repo.repo_id,
1634 status,
1636 status,
1635 self._rhodecode_user.user_id,
1637 self._rhodecode_user.user_id,
1636 comment,
1638 comment,
1637 pull_request=pull_request
1639 pull_request=pull_request
1638 )
1640 )
1639
1641
1640 Session().flush()
1642 Session().flush()
1641 # this is somehow required to get access to some relationship
1643 # this is somehow required to get access to some relationship
1642 # loaded on comment
1644 # loaded on comment
1643 Session().refresh(comment)
1645 Session().refresh(comment)
1644
1646
1645 # skip notifications for drafts
1647 # skip notifications for drafts
1646 if not is_draft:
1648 if not is_draft:
1647 PullRequestModel().trigger_pull_request_hook(
1649 PullRequestModel().trigger_pull_request_hook(
1648 pull_request, self._rhodecode_user, 'comment',
1650 pull_request, self._rhodecode_user, 'comment',
1649 data={'comment': comment})
1651 data={'comment': comment})
1650
1652
1651 # we now calculate the status of pull request, and based on that
1653 # we now calculate the status of pull request, and based on that
1652 # calculation we set the commits status
1654 # calculation we set the commits status
1653 calculated_status = pull_request.calculated_review_status()
1655 calculated_status = pull_request.calculated_review_status()
1654 if old_calculated_status != calculated_status:
1656 if old_calculated_status != calculated_status:
1655 PullRequestModel().trigger_pull_request_hook(
1657 PullRequestModel().trigger_pull_request_hook(
1656 pull_request, self._rhodecode_user, 'review_status_change',
1658 pull_request, self._rhodecode_user, 'review_status_change',
1657 data={'status': calculated_status})
1659 data={'status': calculated_status})
1658
1660
1659 comment_id = comment.comment_id
1661 comment_id = comment.comment_id
1660 data[comment_id] = {
1662 data[comment_id] = {
1661 'target_id': target_elem_id
1663 'target_id': target_elem_id
1662 }
1664 }
1663 Session().flush()
1665 Session().flush()
1664
1666
1665 c.co = comment
1667 c.co = comment
1666 c.at_version_num = None
1668 c.at_version_num = None
1667 c.is_new = True
1669 c.is_new = True
1668 rendered_comment = render(
1670 rendered_comment = render(
1669 'rhodecode:templates/changeset/changeset_comment_block.mako',
1671 'rhodecode:templates/changeset/changeset_comment_block.mako',
1670 self._get_template_context(c), self.request)
1672 self._get_template_context(c), self.request)
1671
1673
1672 data[comment_id].update(comment.get_dict())
1674 data[comment_id].update(comment.get_dict())
1673 data[comment_id].update({'rendered_text': rendered_comment})
1675 data[comment_id].update({'rendered_text': rendered_comment})
1674
1676
1675 Session().commit()
1677 Session().commit()
1676
1678
1677 # skip channelstream for draft comments
1679 # skip channelstream for draft comments
1678 if not all_drafts:
1680 if not all_drafts:
1679 comment_broadcast_channel = channelstream.comment_channel(
1681 comment_broadcast_channel = channelstream.comment_channel(
1680 self.db_repo_name, pull_request_obj=pull_request)
1682 self.db_repo_name, pull_request_obj=pull_request)
1681
1683
1682 comment_data = data
1684 comment_data = data
1683 posted_comment_type = 'inline' if is_inline else 'general'
1685 posted_comment_type = 'inline' if is_inline else 'general'
1684 if len(data) == 1:
1686 if len(data) == 1:
1685 msg = _('posted {} new {} comment').format(len(data), posted_comment_type)
1687 msg = _('posted {} new {} comment').format(len(data), posted_comment_type)
1686 else:
1688 else:
1687 msg = _('posted {} new {} comments').format(len(data), posted_comment_type)
1689 msg = _('posted {} new {} comments').format(len(data), posted_comment_type)
1688
1690
1689 channelstream.comment_channelstream_push(
1691 channelstream.comment_channelstream_push(
1690 self.request, comment_broadcast_channel, self._rhodecode_user, msg,
1692 self.request, comment_broadcast_channel, self._rhodecode_user, msg,
1691 comment_data=comment_data)
1693 comment_data=comment_data)
1692
1694
1693 return data
1695 return data
1694
1696
1695 @LoginRequired()
1697 @LoginRequired()
1696 @NotAnonymous()
1698 @NotAnonymous()
1697 @HasRepoPermissionAnyDecorator(
1699 @HasRepoPermissionAnyDecorator(
1698 'repository.read', 'repository.write', 'repository.admin')
1700 'repository.read', 'repository.write', 'repository.admin')
1699 @CSRFRequired()
1701 @CSRFRequired()
1700 def pull_request_comment_create(self):
1702 def pull_request_comment_create(self):
1701 _ = self.request.translate
1703 _ = self.request.translate
1702
1704
1703 pull_request = PullRequest.get_or_404(self.request.matchdict['pull_request_id'])
1705 pull_request = PullRequest.get_or_404(self.request.matchdict['pull_request_id'])
1704
1706
1705 if pull_request.is_closed():
1707 if pull_request.is_closed():
1706 log.debug('comment: forbidden because pull request is closed')
1708 log.debug('comment: forbidden because pull request is closed')
1707 raise HTTPForbidden()
1709 raise HTTPForbidden()
1708
1710
1709 allowed_to_comment = PullRequestModel().check_user_comment(
1711 allowed_to_comment = PullRequestModel().check_user_comment(
1710 pull_request, self._rhodecode_user)
1712 pull_request, self._rhodecode_user)
1711 if not allowed_to_comment:
1713 if not allowed_to_comment:
1712 log.debug('comment: forbidden because pull request is from forbidden repo')
1714 log.debug('comment: forbidden because pull request is from forbidden repo')
1713 raise HTTPForbidden()
1715 raise HTTPForbidden()
1714
1716
1715 comment_data = {
1717 comment_data = {
1716 'comment_type': self.request.POST.get('comment_type'),
1718 'comment_type': self.request.POST.get('comment_type'),
1717 'text': self.request.POST.get('text'),
1719 'text': self.request.POST.get('text'),
1718 'status': self.request.POST.get('changeset_status', None),
1720 'status': self.request.POST.get('changeset_status', None),
1719 'is_draft': self.request.POST.get('draft'),
1721 'is_draft': self.request.POST.get('draft'),
1720 'resolves_comment_id': self.request.POST.get('resolves_comment_id', None),
1722 'resolves_comment_id': self.request.POST.get('resolves_comment_id', None),
1721 'close_pull_request': self.request.POST.get('close_pull_request'),
1723 'close_pull_request': self.request.POST.get('close_pull_request'),
1722 'f_path': self.request.POST.get('f_path'),
1724 'f_path': self.request.POST.get('f_path'),
1723 'line': self.request.POST.get('line'),
1725 'line': self.request.POST.get('line'),
1724 }
1726 }
1727
1725 data = self._pull_request_comments_create(pull_request, [comment_data])
1728 data = self._pull_request_comments_create(pull_request, [comment_data])
1726
1729
1727 return data
1730 return data
1728
1731
1729 @LoginRequired()
1732 @LoginRequired()
1730 @NotAnonymous()
1733 @NotAnonymous()
1731 @HasRepoPermissionAnyDecorator(
1734 @HasRepoPermissionAnyDecorator(
1732 'repository.read', 'repository.write', 'repository.admin')
1735 'repository.read', 'repository.write', 'repository.admin')
1733 @CSRFRequired()
1736 @CSRFRequired()
1734 def pull_request_comment_delete(self):
1737 def pull_request_comment_delete(self):
1735 pull_request = PullRequest.get_or_404(
1738 pull_request = PullRequest.get_or_404(
1736 self.request.matchdict['pull_request_id'])
1739 self.request.matchdict['pull_request_id'])
1737
1740
1738 comment = ChangesetComment.get_or_404(
1741 comment = ChangesetComment.get_or_404(
1739 self.request.matchdict['comment_id'])
1742 self.request.matchdict['comment_id'])
1740 comment_id = comment.comment_id
1743 comment_id = comment.comment_id
1741
1744
1742 if comment.immutable:
1745 if comment.immutable:
1743 # don't allow deleting comments that are immutable
1746 # don't allow deleting comments that are immutable
1744 raise HTTPForbidden()
1747 raise HTTPForbidden()
1745
1748
1746 if pull_request.is_closed():
1749 if pull_request.is_closed():
1747 log.debug('comment: forbidden because pull request is closed')
1750 log.debug('comment: forbidden because pull request is closed')
1748 raise HTTPForbidden()
1751 raise HTTPForbidden()
1749
1752
1750 if not comment:
1753 if not comment:
1751 log.debug('Comment with id:%s not found, skipping', comment_id)
1754 log.debug('Comment with id:%s not found, skipping', comment_id)
1752 # comment already deleted in another call probably
1755 # comment already deleted in another call probably
1753 return True
1756 return True
1754
1757
1755 if comment.pull_request.is_closed():
1758 if comment.pull_request.is_closed():
1756 # don't allow deleting comments on closed pull request
1759 # don't allow deleting comments on closed pull request
1757 raise HTTPForbidden()
1760 raise HTTPForbidden()
1758
1761
1759 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1762 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1760 super_admin = h.HasPermissionAny('hg.admin')()
1763 super_admin = h.HasPermissionAny('hg.admin')()
1761 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1764 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1762 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1765 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1763 comment_repo_admin = is_repo_admin and is_repo_comment
1766 comment_repo_admin = is_repo_admin and is_repo_comment
1764
1767
1765 if comment.draft and not comment_owner:
1768 if comment.draft and not comment_owner:
1766 # We never allow to delete draft comments for other than owners
1769 # We never allow to delete draft comments for other than owners
1767 raise HTTPNotFound()
1770 raise HTTPNotFound()
1768
1771
1769 if super_admin or comment_owner or comment_repo_admin:
1772 if super_admin or comment_owner or comment_repo_admin:
1770 old_calculated_status = comment.pull_request.calculated_review_status()
1773 old_calculated_status = comment.pull_request.calculated_review_status()
1771 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
1774 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
1772 Session().commit()
1775 Session().commit()
1773 calculated_status = comment.pull_request.calculated_review_status()
1776 calculated_status = comment.pull_request.calculated_review_status()
1774 if old_calculated_status != calculated_status:
1777 if old_calculated_status != calculated_status:
1775 PullRequestModel().trigger_pull_request_hook(
1778 PullRequestModel().trigger_pull_request_hook(
1776 comment.pull_request, self._rhodecode_user, 'review_status_change',
1779 comment.pull_request, self._rhodecode_user, 'review_status_change',
1777 data={'status': calculated_status})
1780 data={'status': calculated_status})
1778 return True
1781 return True
1779 else:
1782 else:
1780 log.warning('No permissions for user %s to delete comment_id: %s',
1783 log.warning('No permissions for user %s to delete comment_id: %s',
1781 self._rhodecode_db_user, comment_id)
1784 self._rhodecode_db_user, comment_id)
1782 raise HTTPNotFound()
1785 raise HTTPNotFound()
1783
1786
1784 @LoginRequired()
1787 @LoginRequired()
1785 @NotAnonymous()
1788 @NotAnonymous()
1786 @HasRepoPermissionAnyDecorator(
1789 @HasRepoPermissionAnyDecorator(
1787 'repository.read', 'repository.write', 'repository.admin')
1790 'repository.read', 'repository.write', 'repository.admin')
1788 @CSRFRequired()
1791 @CSRFRequired()
1789 def pull_request_comment_edit(self):
1792 def pull_request_comment_edit(self):
1790 self.load_default_context()
1793 self.load_default_context()
1791
1794
1792 pull_request = PullRequest.get_or_404(
1795 pull_request = PullRequest.get_or_404(
1793 self.request.matchdict['pull_request_id']
1796 self.request.matchdict['pull_request_id']
1794 )
1797 )
1795 comment = ChangesetComment.get_or_404(
1798 comment = ChangesetComment.get_or_404(
1796 self.request.matchdict['comment_id']
1799 self.request.matchdict['comment_id']
1797 )
1800 )
1798 comment_id = comment.comment_id
1801 comment_id = comment.comment_id
1799
1802
1800 if comment.immutable:
1803 if comment.immutable:
1801 # don't allow deleting comments that are immutable
1804 # don't allow deleting comments that are immutable
1802 raise HTTPForbidden()
1805 raise HTTPForbidden()
1803
1806
1804 if pull_request.is_closed():
1807 if pull_request.is_closed():
1805 log.debug('comment: forbidden because pull request is closed')
1808 log.debug('comment: forbidden because pull request is closed')
1806 raise HTTPForbidden()
1809 raise HTTPForbidden()
1807
1810
1808 if comment.pull_request.is_closed():
1811 if comment.pull_request.is_closed():
1809 # don't allow deleting comments on closed pull request
1812 # don't allow deleting comments on closed pull request
1810 raise HTTPForbidden()
1813 raise HTTPForbidden()
1811
1814
1812 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1815 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1813 super_admin = h.HasPermissionAny('hg.admin')()
1816 super_admin = h.HasPermissionAny('hg.admin')()
1814 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1817 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1815 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1818 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1816 comment_repo_admin = is_repo_admin and is_repo_comment
1819 comment_repo_admin = is_repo_admin and is_repo_comment
1817
1820
1818 if super_admin or comment_owner or comment_repo_admin:
1821 if super_admin or comment_owner or comment_repo_admin:
1819 text = self.request.POST.get('text')
1822 text = self.request.POST.get('text')
1820 version = self.request.POST.get('version')
1823 version = self.request.POST.get('version')
1821 if text == comment.text:
1824 if text == comment.text:
1822 log.warning(
1825 log.warning(
1823 'Comment(PR): '
1826 'Comment(PR): '
1824 'Trying to create new version '
1827 'Trying to create new version '
1825 'with the same comment body {}'.format(
1828 'with the same comment body {}'.format(
1826 comment_id,
1829 comment_id,
1827 )
1830 )
1828 )
1831 )
1829 raise HTTPNotFound()
1832 raise HTTPNotFound()
1830
1833
1831 if version.isdigit():
1834 if version.isdigit():
1832 version = int(version)
1835 version = int(version)
1833 else:
1836 else:
1834 log.warning(
1837 log.warning(
1835 'Comment(PR): Wrong version type {} {} '
1838 'Comment(PR): Wrong version type {} {} '
1836 'for comment {}'.format(
1839 'for comment {}'.format(
1837 version,
1840 version,
1838 type(version),
1841 type(version),
1839 comment_id,
1842 comment_id,
1840 )
1843 )
1841 )
1844 )
1842 raise HTTPNotFound()
1845 raise HTTPNotFound()
1843
1846
1844 try:
1847 try:
1845 comment_history = CommentsModel().edit(
1848 comment_history = CommentsModel().edit(
1846 comment_id=comment_id,
1849 comment_id=comment_id,
1847 text=text,
1850 text=text,
1848 auth_user=self._rhodecode_user,
1851 auth_user=self._rhodecode_user,
1849 version=version,
1852 version=version,
1850 )
1853 )
1851 except CommentVersionMismatch:
1854 except CommentVersionMismatch:
1852 raise HTTPConflict()
1855 raise HTTPConflict()
1853
1856
1854 if not comment_history:
1857 if not comment_history:
1855 raise HTTPNotFound()
1858 raise HTTPNotFound()
1856
1859
1857 Session().commit()
1860 Session().commit()
1858 if not comment.draft:
1861 if not comment.draft:
1859 PullRequestModel().trigger_pull_request_hook(
1862 PullRequestModel().trigger_pull_request_hook(
1860 pull_request, self._rhodecode_user, 'comment_edit',
1863 pull_request, self._rhodecode_user, 'comment_edit',
1861 data={'comment': comment})
1864 data={'comment': comment})
1862
1865
1863 return {
1866 return {
1864 'comment_history_id': comment_history.comment_history_id,
1867 'comment_history_id': comment_history.comment_history_id,
1865 'comment_id': comment.comment_id,
1868 'comment_id': comment.comment_id,
1866 'comment_version': comment_history.version,
1869 'comment_version': comment_history.version,
1867 'comment_author_username': comment_history.author.username,
1870 'comment_author_username': comment_history.author.username,
1868 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16, request=self.request),
1871 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16, request=self.request),
1869 'comment_created_on': h.age_component(comment_history.created_on,
1872 'comment_created_on': h.age_component(comment_history.created_on,
1870 time_is_local=True),
1873 time_is_local=True),
1871 }
1874 }
1872 else:
1875 else:
1873 log.warning('No permissions for user %s to edit comment_id: %s',
1876 log.warning('No permissions for user %s to edit comment_id: %s',
1874 self._rhodecode_db_user, comment_id)
1877 self._rhodecode_db_user, comment_id)
1875 raise HTTPNotFound()
1878 raise HTTPNotFound()
@@ -1,5867 +1,5885 b''
1 # Copyright (C) 2010-2023 RhodeCode GmbH
1 # Copyright (C) 2010-2023 RhodeCode GmbH
2 #
2 #
3 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
14 #
15 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19 """
19 """
20 Database Models for RhodeCode Enterprise
20 Database Models for RhodeCode Enterprise
21 """
21 """
22
22
23 import re
23 import re
24 import os
24 import os
25 import time
25 import time
26 import string
26 import string
27 import logging
27 import logging
28 import datetime
28 import datetime
29 import uuid
29 import uuid
30 import warnings
30 import warnings
31 import ipaddress
31 import ipaddress
32 import functools
32 import functools
33 import traceback
33 import traceback
34 import collections
34 import collections
35
35
36 from sqlalchemy import (
36 from sqlalchemy import (
37 or_, and_, not_, func, cast, TypeDecorator, event, select,
37 or_, and_, not_, func, cast, TypeDecorator, event, select,
38 true, false, null,
38 true, false, null,
39 Index, Sequence, UniqueConstraint, ForeignKey, CheckConstraint, Column,
39 Index, Sequence, UniqueConstraint, ForeignKey, CheckConstraint, Column,
40 Boolean, String, Unicode, UnicodeText, DateTime, Integer, LargeBinary,
40 Boolean, String, Unicode, UnicodeText, DateTime, Integer, LargeBinary,
41 Text, Float, PickleType, BigInteger)
41 Text, Float, PickleType, BigInteger)
42 from sqlalchemy.sql.expression import case
42 from sqlalchemy.sql.expression import case
43 from sqlalchemy.sql.functions import coalesce, count # pragma: no cover
43 from sqlalchemy.sql.functions import coalesce, count # pragma: no cover
44 from sqlalchemy.orm import (
44 from sqlalchemy.orm import (
45 relationship, lazyload, joinedload, class_mapper, validates, aliased, load_only)
45 relationship, lazyload, joinedload, class_mapper, validates, aliased, load_only)
46 from sqlalchemy.ext.declarative import declared_attr
46 from sqlalchemy.ext.declarative import declared_attr
47 from sqlalchemy.ext.hybrid import hybrid_property
47 from sqlalchemy.ext.hybrid import hybrid_property
48 from sqlalchemy.exc import IntegrityError # pragma: no cover
48 from sqlalchemy.exc import IntegrityError # pragma: no cover
49 from sqlalchemy.dialects.mysql import LONGTEXT
49 from sqlalchemy.dialects.mysql import LONGTEXT
50 from zope.cachedescriptors.property import Lazy as LazyProperty
50 from zope.cachedescriptors.property import Lazy as LazyProperty
51 from pyramid.threadlocal import get_current_request
51 from pyramid.threadlocal import get_current_request
52 from webhelpers2.text import remove_formatting
52 from webhelpers2.text import remove_formatting
53
53
54 from rhodecode.lib.str_utils import safe_bytes
54 from rhodecode.lib.str_utils import safe_bytes
55 from rhodecode.translation import _
55 from rhodecode.translation import _
56 from rhodecode.lib.vcs import get_vcs_instance, VCSError
56 from rhodecode.lib.vcs import get_vcs_instance, VCSError
57 from rhodecode.lib.vcs.backends.base import (
57 from rhodecode.lib.vcs.backends.base import (
58 EmptyCommit, Reference, unicode_to_reference, reference_to_unicode)
58 EmptyCommit, Reference, unicode_to_reference, reference_to_unicode)
59 from rhodecode.lib.utils2 import (
59 from rhodecode.lib.utils2 import (
60 str2bool, safe_str, get_commit_safe, sha1_safe,
60 str2bool, safe_str, get_commit_safe, sha1_safe,
61 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
61 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
62 glob2re, StrictAttributeDict, cleaned_uri, datetime_to_time)
62 glob2re, StrictAttributeDict, cleaned_uri, datetime_to_time)
63 from rhodecode.lib.jsonalchemy import (
63 from rhodecode.lib.jsonalchemy import (
64 MutationObj, MutationList, JsonType, JsonRaw)
64 MutationObj, MutationList, JsonType, JsonRaw)
65 from rhodecode.lib.hash_utils import sha1
65 from rhodecode.lib.hash_utils import sha1
66 from rhodecode.lib import ext_json
66 from rhodecode.lib import ext_json
67 from rhodecode.lib import enc_utils
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 from rhodecode.lib.caching_query import FromCache
69 from rhodecode.lib.caching_query import FromCache
70 from rhodecode.lib.exceptions import (
70 from rhodecode.lib.exceptions import (
71 ArtifactMetadataDuplicate, ArtifactMetadataBadValueType)
71 ArtifactMetadataDuplicate, ArtifactMetadataBadValueType)
72 from rhodecode.model.meta import Base, Session
72 from rhodecode.model.meta import Base, Session
73
73
74 URL_SEP = '/'
74 URL_SEP = '/'
75 log = logging.getLogger(__name__)
75 log = logging.getLogger(__name__)
76
76
77 # =============================================================================
77 # =============================================================================
78 # BASE CLASSES
78 # BASE CLASSES
79 # =============================================================================
79 # =============================================================================
80
80
81 # this is propagated from .ini file rhodecode.encrypted_values.secret or
81 # this is propagated from .ini file rhodecode.encrypted_values.secret or
82 # beaker.session.secret if first is not set.
82 # beaker.session.secret if first is not set.
83 # and initialized at environment.py
83 # and initialized at environment.py
84 ENCRYPTION_KEY: bytes = b''
84 ENCRYPTION_KEY: bytes = b''
85
85
86 # used to sort permissions by types, '#' used here is not allowed to be in
86 # used to sort permissions by types, '#' used here is not allowed to be in
87 # usernames, and it's very early in sorted string.printable table.
87 # usernames, and it's very early in sorted string.printable table.
88 PERMISSION_TYPE_SORT = {
88 PERMISSION_TYPE_SORT = {
89 'admin': '####',
89 'admin': '####',
90 'write': '###',
90 'write': '###',
91 'read': '##',
91 'read': '##',
92 'none': '#',
92 'none': '#',
93 }
93 }
94
94
95
95
96 def display_user_sort(obj):
96 def display_user_sort(obj):
97 """
97 """
98 Sort function used to sort permissions in .permissions() function of
98 Sort function used to sort permissions in .permissions() function of
99 Repository, RepoGroup, UserGroup. Also it put the default user in front
99 Repository, RepoGroup, UserGroup. Also it put the default user in front
100 of all other resources
100 of all other resources
101 """
101 """
102
102
103 if obj.username == User.DEFAULT_USER:
103 if obj.username == User.DEFAULT_USER:
104 return '#####'
104 return '#####'
105 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
105 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
106 extra_sort_num = '1' # default
106 extra_sort_num = '1' # default
107
107
108 # NOTE(dan): inactive duplicates goes last
108 # NOTE(dan): inactive duplicates goes last
109 if getattr(obj, 'duplicate_perm', None):
109 if getattr(obj, 'duplicate_perm', None):
110 extra_sort_num = '9'
110 extra_sort_num = '9'
111 return prefix + extra_sort_num + obj.username
111 return prefix + extra_sort_num + obj.username
112
112
113
113
114 def display_user_group_sort(obj):
114 def display_user_group_sort(obj):
115 """
115 """
116 Sort function used to sort permissions in .permissions() function of
116 Sort function used to sort permissions in .permissions() function of
117 Repository, RepoGroup, UserGroup. Also it put the default user in front
117 Repository, RepoGroup, UserGroup. Also it put the default user in front
118 of all other resources
118 of all other resources
119 """
119 """
120
120
121 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
121 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
122 return prefix + obj.users_group_name
122 return prefix + obj.users_group_name
123
123
124
124
125 def _hash_key(k):
125 def _hash_key(k):
126 return sha1_safe(k)
126 return sha1_safe(k)
127
127
128
128
129 def in_filter_generator(qry, items, limit=500):
129 def in_filter_generator(qry, items, limit=500):
130 """
130 """
131 Splits IN() into multiple with OR
131 Splits IN() into multiple with OR
132 e.g.::
132 e.g.::
133 cnt = Repository.query().filter(
133 cnt = Repository.query().filter(
134 or_(
134 or_(
135 *in_filter_generator(Repository.repo_id, range(100000))
135 *in_filter_generator(Repository.repo_id, range(100000))
136 )).count()
136 )).count()
137 """
137 """
138 if not items:
138 if not items:
139 # empty list will cause empty query which might cause security issues
139 # empty list will cause empty query which might cause security issues
140 # this can lead to hidden unpleasant results
140 # this can lead to hidden unpleasant results
141 items = [-1]
141 items = [-1]
142
142
143 parts = []
143 parts = []
144 for chunk in range(0, len(items), limit):
144 for chunk in range(0, len(items), limit):
145 parts.append(
145 parts.append(
146 qry.in_(items[chunk: chunk + limit])
146 qry.in_(items[chunk: chunk + limit])
147 )
147 )
148
148
149 return parts
149 return parts
150
150
151
151
152 base_table_args = {
152 base_table_args = {
153 'extend_existing': True,
153 'extend_existing': True,
154 'mysql_engine': 'InnoDB',
154 'mysql_engine': 'InnoDB',
155 'mysql_charset': 'utf8',
155 'mysql_charset': 'utf8',
156 'sqlite_autoincrement': True
156 'sqlite_autoincrement': True
157 }
157 }
158
158
159
159
160 class EncryptedTextValue(TypeDecorator):
160 class EncryptedTextValue(TypeDecorator):
161 """
161 """
162 Special column for encrypted long text data, use like::
162 Special column for encrypted long text data, use like::
163
163
164 value = Column("encrypted_value", EncryptedValue(), nullable=False)
164 value = Column("encrypted_value", EncryptedValue(), nullable=False)
165
165
166 This column is intelligent so if value is in unencrypted form it return
166 This column is intelligent so if value is in unencrypted form it return
167 unencrypted form, but on save it always encrypts
167 unencrypted form, but on save it always encrypts
168 """
168 """
169 cache_ok = True
169 cache_ok = True
170 impl = Text
170 impl = Text
171
171
172 def process_bind_param(self, value, dialect):
172 def process_bind_param(self, value, dialect):
173 """
173 """
174 Setter for storing value
174 Setter for storing value
175 """
175 """
176 import rhodecode
176 import rhodecode
177 if not value:
177 if not value:
178 return value
178 return value
179
179
180 # protect against double encrypting if values is already encrypted
180 # protect against double encrypting if values is already encrypted
181 if value.startswith('enc$aes$') \
181 if value.startswith('enc$aes$') \
182 or value.startswith('enc$aes_hmac$') \
182 or value.startswith('enc$aes_hmac$') \
183 or value.startswith('enc2$'):
183 or value.startswith('enc2$'):
184 raise ValueError('value needs to be in unencrypted format, '
184 raise ValueError('value needs to be in unencrypted format, '
185 'ie. not starting with enc$ or enc2$')
185 'ie. not starting with enc$ or enc2$')
186
186
187 algo = rhodecode.CONFIG.get('rhodecode.encrypted_values.algorithm') or 'aes'
187 algo = rhodecode.CONFIG.get('rhodecode.encrypted_values.algorithm') or 'aes'
188 bytes_val = enc_utils.encrypt_value(value, enc_key=ENCRYPTION_KEY, algo=algo)
188 bytes_val = enc_utils.encrypt_value(value, enc_key=ENCRYPTION_KEY, algo=algo)
189 return safe_str(bytes_val)
189 return safe_str(bytes_val)
190
190
191 def process_result_value(self, value, dialect):
191 def process_result_value(self, value, dialect):
192 """
192 """
193 Getter for retrieving value
193 Getter for retrieving value
194 """
194 """
195
195
196 import rhodecode
196 import rhodecode
197 if not value:
197 if not value:
198 return value
198 return value
199
199
200 enc_strict_mode = rhodecode.ConfigGet().get_bool('rhodecode.encrypted_values.strict', missing=True)
200 enc_strict_mode = rhodecode.ConfigGet().get_bool('rhodecode.encrypted_values.strict', missing=True)
201
201
202 bytes_val = enc_utils.decrypt_value(value, enc_key=ENCRYPTION_KEY, strict_mode=enc_strict_mode)
202 bytes_val = enc_utils.decrypt_value(value, enc_key=ENCRYPTION_KEY, strict_mode=enc_strict_mode)
203
203
204 return safe_str(bytes_val)
204 return safe_str(bytes_val)
205
205
206
206
207 class BaseModel(object):
207 class BaseModel(object):
208 """
208 """
209 Base Model for all classes
209 Base Model for all classes
210 """
210 """
211
211
212 @classmethod
212 @classmethod
213 def _get_keys(cls):
213 def _get_keys(cls):
214 """return column names for this model """
214 """return column names for this model """
215 return class_mapper(cls).c.keys()
215 return class_mapper(cls).c.keys()
216
216
217 def get_dict(self):
217 def get_dict(self):
218 """
218 """
219 return dict with keys and values corresponding
219 return dict with keys and values corresponding
220 to this model data """
220 to this model data """
221
221
222 d = {}
222 d = {}
223 for k in self._get_keys():
223 for k in self._get_keys():
224 d[k] = getattr(self, k)
224 d[k] = getattr(self, k)
225
225
226 # also use __json__() if present to get additional fields
226 # also use __json__() if present to get additional fields
227 _json_attr = getattr(self, '__json__', None)
227 _json_attr = getattr(self, '__json__', None)
228 if _json_attr:
228 if _json_attr:
229 # update with attributes from __json__
229 # update with attributes from __json__
230 if callable(_json_attr):
230 if callable(_json_attr):
231 _json_attr = _json_attr()
231 _json_attr = _json_attr()
232 for k, val in _json_attr.items():
232 for k, val in _json_attr.items():
233 d[k] = val
233 d[k] = val
234 return d
234 return d
235
235
236 def get_appstruct(self):
236 def get_appstruct(self):
237 """return list with keys and values tuples corresponding
237 """return list with keys and values tuples corresponding
238 to this model data """
238 to this model data """
239
239
240 lst = []
240 lst = []
241 for k in self._get_keys():
241 for k in self._get_keys():
242 lst.append((k, getattr(self, k),))
242 lst.append((k, getattr(self, k),))
243 return lst
243 return lst
244
244
245 def populate_obj(self, populate_dict):
245 def populate_obj(self, populate_dict):
246 """populate model with data from given populate_dict"""
246 """populate model with data from given populate_dict"""
247
247
248 for k in self._get_keys():
248 for k in self._get_keys():
249 if k in populate_dict:
249 if k in populate_dict:
250 setattr(self, k, populate_dict[k])
250 setattr(self, k, populate_dict[k])
251
251
252 @classmethod
252 @classmethod
253 def query(cls):
253 def query(cls):
254 return Session().query(cls)
254 return Session().query(cls)
255
255
256 @classmethod
256 @classmethod
257 def select(cls, custom_cls=None):
257 def select(cls, custom_cls=None):
258 """
258 """
259 stmt = cls.select().where(cls.user_id==1)
259 stmt = cls.select().where(cls.user_id==1)
260 # optionally
260 # optionally
261 stmt = cls.select(User.user_id).where(cls.user_id==1)
261 stmt = cls.select(User.user_id).where(cls.user_id==1)
262 result = cls.execute(stmt) | cls.scalars(stmt)
262 result = cls.execute(stmt) | cls.scalars(stmt)
263 """
263 """
264
264
265 if custom_cls:
265 if custom_cls:
266 stmt = select(custom_cls)
266 stmt = select(custom_cls)
267 else:
267 else:
268 stmt = select(cls)
268 stmt = select(cls)
269 return stmt
269 return stmt
270
270
271 @classmethod
271 @classmethod
272 def execute(cls, stmt):
272 def execute(cls, stmt):
273 return Session().execute(stmt)
273 return Session().execute(stmt)
274
274
275 @classmethod
275 @classmethod
276 def scalars(cls, stmt):
276 def scalars(cls, stmt):
277 return Session().scalars(stmt)
277 return Session().scalars(stmt)
278
278
279 @classmethod
279 @classmethod
280 def get(cls, id_):
280 def get(cls, id_):
281 if id_:
281 if id_:
282 return cls.query().get(id_)
282 return cls.query().get(id_)
283
283
284 @classmethod
284 @classmethod
285 def get_or_404(cls, id_):
285 def get_or_404(cls, id_):
286 from pyramid.httpexceptions import HTTPNotFound
286 from pyramid.httpexceptions import HTTPNotFound
287
287
288 try:
288 try:
289 id_ = int(id_)
289 id_ = int(id_)
290 except (TypeError, ValueError):
290 except (TypeError, ValueError):
291 raise HTTPNotFound()
291 raise HTTPNotFound()
292
292
293 res = cls.query().get(id_)
293 res = cls.query().get(id_)
294 if not res:
294 if not res:
295 raise HTTPNotFound()
295 raise HTTPNotFound()
296 return res
296 return res
297
297
298 @classmethod
298 @classmethod
299 def getAll(cls):
299 def getAll(cls):
300 # deprecated and left for backward compatibility
300 # deprecated and left for backward compatibility
301 return cls.get_all()
301 return cls.get_all()
302
302
303 @classmethod
303 @classmethod
304 def get_all(cls):
304 def get_all(cls):
305 return cls.query().all()
305 return cls.query().all()
306
306
307 @classmethod
307 @classmethod
308 def delete(cls, id_):
308 def delete(cls, id_):
309 obj = cls.query().get(id_)
309 obj = cls.query().get(id_)
310 Session().delete(obj)
310 Session().delete(obj)
311
311
312 @classmethod
312 @classmethod
313 def identity_cache(cls, session, attr_name, value):
313 def identity_cache(cls, session, attr_name, value):
314 exist_in_session = []
314 exist_in_session = []
315 for (item_cls, pkey), instance in session.identity_map.items():
315 for (item_cls, pkey), instance in session.identity_map.items():
316 if cls == item_cls and getattr(instance, attr_name) == value:
316 if cls == item_cls and getattr(instance, attr_name) == value:
317 exist_in_session.append(instance)
317 exist_in_session.append(instance)
318 if exist_in_session:
318 if exist_in_session:
319 if len(exist_in_session) == 1:
319 if len(exist_in_session) == 1:
320 return exist_in_session[0]
320 return exist_in_session[0]
321 log.exception(
321 log.exception(
322 'multiple objects with attr %s and '
322 'multiple objects with attr %s and '
323 'value %s found with same name: %r',
323 'value %s found with same name: %r',
324 attr_name, value, exist_in_session)
324 attr_name, value, exist_in_session)
325
325
326 @property
326 @property
327 def cls_name(self):
327 def cls_name(self):
328 return self.__class__.__name__
328 return self.__class__.__name__
329
329
330 def __repr__(self):
330 def __repr__(self):
331 return f'<DB:{self.cls_name}>'
331 return f'<DB:{self.cls_name}>'
332
332
333
333
334 class RhodeCodeSetting(Base, BaseModel):
334 class RhodeCodeSetting(Base, BaseModel):
335 __tablename__ = 'rhodecode_settings'
335 __tablename__ = 'rhodecode_settings'
336 __table_args__ = (
336 __table_args__ = (
337 UniqueConstraint('app_settings_name'),
337 UniqueConstraint('app_settings_name'),
338 base_table_args
338 base_table_args
339 )
339 )
340
340
341 SETTINGS_TYPES = {
341 SETTINGS_TYPES = {
342 'str': safe_str,
342 'str': safe_str,
343 'int': safe_int,
343 'int': safe_int,
344 'unicode': safe_str,
344 'unicode': safe_str,
345 'bool': str2bool,
345 'bool': str2bool,
346 'list': functools.partial(aslist, sep=',')
346 'list': functools.partial(aslist, sep=',')
347 }
347 }
348 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
348 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
349 GLOBAL_CONF_KEY = 'app_settings'
349 GLOBAL_CONF_KEY = 'app_settings'
350
350
351 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
351 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
352 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
352 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
353 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
353 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
354 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
354 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
355
355
356 def __init__(self, key='', val='', type='unicode'):
356 def __init__(self, key='', val='', type='unicode'):
357 self.app_settings_name = key
357 self.app_settings_name = key
358 self.app_settings_type = type
358 self.app_settings_type = type
359 self.app_settings_value = val
359 self.app_settings_value = val
360
360
361 @validates('_app_settings_value')
361 @validates('_app_settings_value')
362 def validate_settings_value(self, key, val):
362 def validate_settings_value(self, key, val):
363 assert type(val) == str
363 assert type(val) == str
364 return val
364 return val
365
365
366 @hybrid_property
366 @hybrid_property
367 def app_settings_value(self):
367 def app_settings_value(self):
368 v = self._app_settings_value
368 v = self._app_settings_value
369 _type = self.app_settings_type
369 _type = self.app_settings_type
370 if _type:
370 if _type:
371 _type = self.app_settings_type.split('.')[0]
371 _type = self.app_settings_type.split('.')[0]
372 # decode the encrypted value
372 # decode the encrypted value
373 if 'encrypted' in self.app_settings_type:
373 if 'encrypted' in self.app_settings_type:
374 cipher = EncryptedTextValue()
374 cipher = EncryptedTextValue()
375 v = safe_str(cipher.process_result_value(v, None))
375 v = safe_str(cipher.process_result_value(v, None))
376
376
377 converter = self.SETTINGS_TYPES.get(_type) or \
377 converter = self.SETTINGS_TYPES.get(_type) or \
378 self.SETTINGS_TYPES['unicode']
378 self.SETTINGS_TYPES['unicode']
379 return converter(v)
379 return converter(v)
380
380
381 @app_settings_value.setter
381 @app_settings_value.setter
382 def app_settings_value(self, val):
382 def app_settings_value(self, val):
383 """
383 """
384 Setter that will always make sure we use unicode in app_settings_value
384 Setter that will always make sure we use unicode in app_settings_value
385
385
386 :param val:
386 :param val:
387 """
387 """
388 val = safe_str(val)
388 val = safe_str(val)
389 # encode the encrypted value
389 # encode the encrypted value
390 if 'encrypted' in self.app_settings_type:
390 if 'encrypted' in self.app_settings_type:
391 cipher = EncryptedTextValue()
391 cipher = EncryptedTextValue()
392 val = safe_str(cipher.process_bind_param(val, None))
392 val = safe_str(cipher.process_bind_param(val, None))
393 self._app_settings_value = val
393 self._app_settings_value = val
394
394
395 @hybrid_property
395 @hybrid_property
396 def app_settings_type(self):
396 def app_settings_type(self):
397 return self._app_settings_type
397 return self._app_settings_type
398
398
399 @app_settings_type.setter
399 @app_settings_type.setter
400 def app_settings_type(self, val):
400 def app_settings_type(self, val):
401 if val.split('.')[0] not in self.SETTINGS_TYPES:
401 if val.split('.')[0] not in self.SETTINGS_TYPES:
402 raise Exception('type must be one of %s got %s'
402 raise Exception('type must be one of %s got %s'
403 % (self.SETTINGS_TYPES.keys(), val))
403 % (self.SETTINGS_TYPES.keys(), val))
404 self._app_settings_type = val
404 self._app_settings_type = val
405
405
406 @classmethod
406 @classmethod
407 def get_by_prefix(cls, prefix):
407 def get_by_prefix(cls, prefix):
408 return RhodeCodeSetting.query()\
408 return RhodeCodeSetting.query()\
409 .filter(RhodeCodeSetting.app_settings_name.startswith(prefix))\
409 .filter(RhodeCodeSetting.app_settings_name.startswith(prefix))\
410 .all()
410 .all()
411
411
412 def __repr__(self):
412 def __repr__(self):
413 return "<%s('%s:%s[%s]')>" % (
413 return "<%s('%s:%s[%s]')>" % (
414 self.cls_name,
414 self.cls_name,
415 self.app_settings_name, self.app_settings_value,
415 self.app_settings_name, self.app_settings_value,
416 self.app_settings_type
416 self.app_settings_type
417 )
417 )
418
418
419
419
420 class RhodeCodeUi(Base, BaseModel):
420 class RhodeCodeUi(Base, BaseModel):
421 __tablename__ = 'rhodecode_ui'
421 __tablename__ = 'rhodecode_ui'
422 __table_args__ = (
422 __table_args__ = (
423 UniqueConstraint('ui_key'),
423 UniqueConstraint('ui_key'),
424 base_table_args
424 base_table_args
425 )
425 )
426 # Sync those values with vcsserver.config.hooks
426 # Sync those values with vcsserver.config.hooks
427
427
428 HOOK_REPO_SIZE = 'changegroup.repo_size'
428 HOOK_REPO_SIZE = 'changegroup.repo_size'
429 # HG
429 # HG
430 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
430 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
431 HOOK_PULL = 'outgoing.pull_logger'
431 HOOK_PULL = 'outgoing.pull_logger'
432 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
432 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
433 HOOK_PRETX_PUSH = 'pretxnchangegroup.pre_push'
433 HOOK_PRETX_PUSH = 'pretxnchangegroup.pre_push'
434 HOOK_PUSH = 'changegroup.push_logger'
434 HOOK_PUSH = 'changegroup.push_logger'
435 HOOK_PUSH_KEY = 'pushkey.key_push'
435 HOOK_PUSH_KEY = 'pushkey.key_push'
436
436
437 HOOKS_BUILTIN = [
437 HOOKS_BUILTIN = [
438 HOOK_PRE_PULL,
438 HOOK_PRE_PULL,
439 HOOK_PULL,
439 HOOK_PULL,
440 HOOK_PRE_PUSH,
440 HOOK_PRE_PUSH,
441 HOOK_PRETX_PUSH,
441 HOOK_PRETX_PUSH,
442 HOOK_PUSH,
442 HOOK_PUSH,
443 HOOK_PUSH_KEY,
443 HOOK_PUSH_KEY,
444 ]
444 ]
445
445
446 # TODO: johbo: Unify way how hooks are configured for git and hg,
446 # TODO: johbo: Unify way how hooks are configured for git and hg,
447 # git part is currently hardcoded.
447 # git part is currently hardcoded.
448
448
449 # SVN PATTERNS
449 # SVN PATTERNS
450 SVN_BRANCH_ID = 'vcs_svn_branch'
450 SVN_BRANCH_ID = 'vcs_svn_branch'
451 SVN_TAG_ID = 'vcs_svn_tag'
451 SVN_TAG_ID = 'vcs_svn_tag'
452
452
453 ui_id = Column(
453 ui_id = Column(
454 "ui_id", Integer(), nullable=False, unique=True, default=None,
454 "ui_id", Integer(), nullable=False, unique=True, default=None,
455 primary_key=True)
455 primary_key=True)
456 ui_section = Column(
456 ui_section = Column(
457 "ui_section", String(255), nullable=True, unique=None, default=None)
457 "ui_section", String(255), nullable=True, unique=None, default=None)
458 ui_key = Column(
458 ui_key = Column(
459 "ui_key", String(255), nullable=True, unique=None, default=None)
459 "ui_key", String(255), nullable=True, unique=None, default=None)
460 ui_value = Column(
460 ui_value = Column(
461 "ui_value", String(255), nullable=True, unique=None, default=None)
461 "ui_value", String(255), nullable=True, unique=None, default=None)
462 ui_active = Column(
462 ui_active = Column(
463 "ui_active", Boolean(), nullable=True, unique=None, default=True)
463 "ui_active", Boolean(), nullable=True, unique=None, default=True)
464
464
465 def __repr__(self):
465 def __repr__(self):
466 return '<%s[%s]%s=>%s]>' % (self.cls_name, self.ui_section,
466 return '<%s[%s]%s=>%s]>' % (self.cls_name, self.ui_section,
467 self.ui_key, self.ui_value)
467 self.ui_key, self.ui_value)
468
468
469
469
470 class RepoRhodeCodeSetting(Base, BaseModel):
470 class RepoRhodeCodeSetting(Base, BaseModel):
471 __tablename__ = 'repo_rhodecode_settings'
471 __tablename__ = 'repo_rhodecode_settings'
472 __table_args__ = (
472 __table_args__ = (
473 UniqueConstraint(
473 UniqueConstraint(
474 'app_settings_name', 'repository_id',
474 'app_settings_name', 'repository_id',
475 name='uq_repo_rhodecode_setting_name_repo_id'),
475 name='uq_repo_rhodecode_setting_name_repo_id'),
476 base_table_args
476 base_table_args
477 )
477 )
478
478
479 repository_id = Column(
479 repository_id = Column(
480 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
480 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
481 nullable=False)
481 nullable=False)
482 app_settings_id = Column(
482 app_settings_id = Column(
483 "app_settings_id", Integer(), nullable=False, unique=True,
483 "app_settings_id", Integer(), nullable=False, unique=True,
484 default=None, primary_key=True)
484 default=None, primary_key=True)
485 app_settings_name = Column(
485 app_settings_name = Column(
486 "app_settings_name", String(255), nullable=True, unique=None,
486 "app_settings_name", String(255), nullable=True, unique=None,
487 default=None)
487 default=None)
488 _app_settings_value = Column(
488 _app_settings_value = Column(
489 "app_settings_value", String(4096), nullable=True, unique=None,
489 "app_settings_value", String(4096), nullable=True, unique=None,
490 default=None)
490 default=None)
491 _app_settings_type = Column(
491 _app_settings_type = Column(
492 "app_settings_type", String(255), nullable=True, unique=None,
492 "app_settings_type", String(255), nullable=True, unique=None,
493 default=None)
493 default=None)
494
494
495 repository = relationship('Repository', viewonly=True)
495 repository = relationship('Repository', viewonly=True)
496
496
497 def __init__(self, repository_id, key='', val='', type='unicode'):
497 def __init__(self, repository_id, key='', val='', type='unicode'):
498 self.repository_id = repository_id
498 self.repository_id = repository_id
499 self.app_settings_name = key
499 self.app_settings_name = key
500 self.app_settings_type = type
500 self.app_settings_type = type
501 self.app_settings_value = val
501 self.app_settings_value = val
502
502
503 @validates('_app_settings_value')
503 @validates('_app_settings_value')
504 def validate_settings_value(self, key, val):
504 def validate_settings_value(self, key, val):
505 assert type(val) == str
505 assert type(val) == str
506 return val
506 return val
507
507
508 @hybrid_property
508 @hybrid_property
509 def app_settings_value(self):
509 def app_settings_value(self):
510 v = self._app_settings_value
510 v = self._app_settings_value
511 type_ = self.app_settings_type
511 type_ = self.app_settings_type
512 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
512 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
513 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
513 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
514 return converter(v)
514 return converter(v)
515
515
516 @app_settings_value.setter
516 @app_settings_value.setter
517 def app_settings_value(self, val):
517 def app_settings_value(self, val):
518 """
518 """
519 Setter that will always make sure we use unicode in app_settings_value
519 Setter that will always make sure we use unicode in app_settings_value
520
520
521 :param val:
521 :param val:
522 """
522 """
523 self._app_settings_value = safe_str(val)
523 self._app_settings_value = safe_str(val)
524
524
525 @hybrid_property
525 @hybrid_property
526 def app_settings_type(self):
526 def app_settings_type(self):
527 return self._app_settings_type
527 return self._app_settings_type
528
528
529 @app_settings_type.setter
529 @app_settings_type.setter
530 def app_settings_type(self, val):
530 def app_settings_type(self, val):
531 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
531 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
532 if val not in SETTINGS_TYPES:
532 if val not in SETTINGS_TYPES:
533 raise Exception('type must be one of %s got %s'
533 raise Exception('type must be one of %s got %s'
534 % (SETTINGS_TYPES.keys(), val))
534 % (SETTINGS_TYPES.keys(), val))
535 self._app_settings_type = val
535 self._app_settings_type = val
536
536
537 def __repr__(self):
537 def __repr__(self):
538 return "<%s('%s:%s:%s[%s]')>" % (
538 return "<%s('%s:%s:%s[%s]')>" % (
539 self.cls_name, self.repository.repo_name,
539 self.cls_name, self.repository.repo_name,
540 self.app_settings_name, self.app_settings_value,
540 self.app_settings_name, self.app_settings_value,
541 self.app_settings_type
541 self.app_settings_type
542 )
542 )
543
543
544
544
545 class RepoRhodeCodeUi(Base, BaseModel):
545 class RepoRhodeCodeUi(Base, BaseModel):
546 __tablename__ = 'repo_rhodecode_ui'
546 __tablename__ = 'repo_rhodecode_ui'
547 __table_args__ = (
547 __table_args__ = (
548 UniqueConstraint(
548 UniqueConstraint(
549 'repository_id', 'ui_section', 'ui_key',
549 'repository_id', 'ui_section', 'ui_key',
550 name='uq_repo_rhodecode_ui_repository_id_section_key'),
550 name='uq_repo_rhodecode_ui_repository_id_section_key'),
551 base_table_args
551 base_table_args
552 )
552 )
553
553
554 repository_id = Column(
554 repository_id = Column(
555 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
555 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
556 nullable=False)
556 nullable=False)
557 ui_id = Column(
557 ui_id = Column(
558 "ui_id", Integer(), nullable=False, unique=True, default=None,
558 "ui_id", Integer(), nullable=False, unique=True, default=None,
559 primary_key=True)
559 primary_key=True)
560 ui_section = Column(
560 ui_section = Column(
561 "ui_section", String(255), nullable=True, unique=None, default=None)
561 "ui_section", String(255), nullable=True, unique=None, default=None)
562 ui_key = Column(
562 ui_key = Column(
563 "ui_key", String(255), nullable=True, unique=None, default=None)
563 "ui_key", String(255), nullable=True, unique=None, default=None)
564 ui_value = Column(
564 ui_value = Column(
565 "ui_value", String(255), nullable=True, unique=None, default=None)
565 "ui_value", String(255), nullable=True, unique=None, default=None)
566 ui_active = Column(
566 ui_active = Column(
567 "ui_active", Boolean(), nullable=True, unique=None, default=True)
567 "ui_active", Boolean(), nullable=True, unique=None, default=True)
568
568
569 repository = relationship('Repository', viewonly=True)
569 repository = relationship('Repository', viewonly=True)
570
570
571 def __repr__(self):
571 def __repr__(self):
572 return '<%s[%s:%s]%s=>%s]>' % (
572 return '<%s[%s:%s]%s=>%s]>' % (
573 self.cls_name, self.repository.repo_name,
573 self.cls_name, self.repository.repo_name,
574 self.ui_section, self.ui_key, self.ui_value)
574 self.ui_section, self.ui_key, self.ui_value)
575
575
576
576
577 class User(Base, BaseModel):
577 class User(Base, BaseModel):
578 __tablename__ = 'users'
578 __tablename__ = 'users'
579 __table_args__ = (
579 __table_args__ = (
580 UniqueConstraint('username'), UniqueConstraint('email'),
580 UniqueConstraint('username'), UniqueConstraint('email'),
581 Index('u_username_idx', 'username'),
581 Index('u_username_idx', 'username'),
582 Index('u_email_idx', 'email'),
582 Index('u_email_idx', 'email'),
583 base_table_args
583 base_table_args
584 )
584 )
585
585
586 DEFAULT_USER = 'default'
586 DEFAULT_USER = 'default'
587 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
587 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
588 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
588 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
589
589
590 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
590 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
591 username = Column("username", String(255), nullable=True, unique=None, default=None)
591 username = Column("username", String(255), nullable=True, unique=None, default=None)
592 password = Column("password", String(255), nullable=True, unique=None, default=None)
592 password = Column("password", String(255), nullable=True, unique=None, default=None)
593 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
593 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
594 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
594 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
595 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
595 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
596 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
596 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
597 _email = Column("email", String(255), nullable=True, unique=None, default=None)
597 _email = Column("email", String(255), nullable=True, unique=None, default=None)
598 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
598 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
599 last_activity = Column('last_activity', DateTime(timezone=False), nullable=True, unique=None, default=None)
599 last_activity = Column('last_activity', DateTime(timezone=False), nullable=True, unique=None, default=None)
600 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
600 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
601
601
602 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
602 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
603 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
603 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
604 _api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
604 _api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
605 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
605 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
606 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
606 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
607 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
607 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
608
608
609 user_log = relationship('UserLog', back_populates='user')
609 user_log = relationship('UserLog', back_populates='user')
610 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all, delete-orphan')
610 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all, delete-orphan')
611
611
612 repositories = relationship('Repository', back_populates='user')
612 repositories = relationship('Repository', back_populates='user')
613 repository_groups = relationship('RepoGroup', back_populates='user')
613 repository_groups = relationship('RepoGroup', back_populates='user')
614 user_groups = relationship('UserGroup', back_populates='user')
614 user_groups = relationship('UserGroup', back_populates='user')
615
615
616 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all', back_populates='follows_user')
616 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all', back_populates='follows_user')
617 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all', back_populates='user')
617 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all', back_populates='user')
618
618
619 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all, delete-orphan')
619 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all, delete-orphan')
620 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all, delete-orphan', back_populates='user')
620 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all, delete-orphan', back_populates='user')
621 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all, delete-orphan', back_populates='user')
621 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all, delete-orphan', back_populates='user')
622
622
623 group_member = relationship('UserGroupMember', cascade='all', back_populates='user')
623 group_member = relationship('UserGroupMember', cascade='all', back_populates='user')
624
624
625 notifications = relationship('UserNotification', cascade='all', back_populates='user')
625 notifications = relationship('UserNotification', cascade='all', back_populates='user')
626 # notifications assigned to this user
626 # notifications assigned to this user
627 user_created_notifications = relationship('Notification', cascade='all', back_populates='created_by_user')
627 user_created_notifications = relationship('Notification', cascade='all', back_populates='created_by_user')
628 # comments created by this user
628 # comments created by this user
629 user_comments = relationship('ChangesetComment', cascade='all', back_populates='author')
629 user_comments = relationship('ChangesetComment', cascade='all', back_populates='author')
630 # user profile extra info
630 # user profile extra info
631 user_emails = relationship('UserEmailMap', cascade='all', back_populates='user')
631 user_emails = relationship('UserEmailMap', cascade='all', back_populates='user')
632 user_ip_map = relationship('UserIpMap', cascade='all', back_populates='user')
632 user_ip_map = relationship('UserIpMap', cascade='all', back_populates='user')
633 user_auth_tokens = relationship('UserApiKeys', cascade='all', back_populates='user')
633 user_auth_tokens = relationship('UserApiKeys', cascade='all', back_populates='user')
634 user_ssh_keys = relationship('UserSshKeys', cascade='all', back_populates='user')
634 user_ssh_keys = relationship('UserSshKeys', cascade='all', back_populates='user')
635
635
636 # gists
636 # gists
637 user_gists = relationship('Gist', cascade='all', back_populates='owner')
637 user_gists = relationship('Gist', cascade='all', back_populates='owner')
638 # user pull requests
638 # user pull requests
639 user_pull_requests = relationship('PullRequest', cascade='all', back_populates='author')
639 user_pull_requests = relationship('PullRequest', cascade='all', back_populates='author')
640
640
641 # external identities
641 # external identities
642 external_identities = relationship('ExternalIdentity', primaryjoin="User.user_id==ExternalIdentity.local_user_id", cascade='all')
642 external_identities = relationship('ExternalIdentity', primaryjoin="User.user_id==ExternalIdentity.local_user_id", cascade='all')
643 # review rules
643 # review rules
644 user_review_rules = relationship('RepoReviewRuleUser', cascade='all', back_populates='user')
644 user_review_rules = relationship('RepoReviewRuleUser', cascade='all', back_populates='user')
645
645
646 # artifacts owned
646 # artifacts owned
647 artifacts = relationship('FileStore', primaryjoin='FileStore.user_id==User.user_id', back_populates='upload_user')
647 artifacts = relationship('FileStore', primaryjoin='FileStore.user_id==User.user_id', back_populates='upload_user')
648
648
649 # no cascade, set NULL
649 # no cascade, set NULL
650 scope_artifacts = relationship('FileStore', primaryjoin='FileStore.scope_user_id==User.user_id', cascade='', back_populates='user')
650 scope_artifacts = relationship('FileStore', primaryjoin='FileStore.scope_user_id==User.user_id', cascade='', back_populates='user')
651
651
652 def __repr__(self):
652 def __repr__(self):
653 return f"<{self.cls_name}('id={self.user_id}, username={self.username}')>"
653 return f"<{self.cls_name}('id={self.user_id}, username={self.username}')>"
654
654
655 @hybrid_property
655 @hybrid_property
656 def email(self):
656 def email(self):
657 return self._email
657 return self._email
658
658
659 @email.setter
659 @email.setter
660 def email(self, val):
660 def email(self, val):
661 self._email = val.lower() if val else None
661 self._email = val.lower() if val else None
662
662
663 @hybrid_property
663 @hybrid_property
664 def first_name(self):
664 def first_name(self):
665 from rhodecode.lib import helpers as h
665 from rhodecode.lib import helpers as h
666 if self.name:
666 if self.name:
667 return h.escape(self.name)
667 return h.escape(self.name)
668 return self.name
668 return self.name
669
669
670 @hybrid_property
670 @hybrid_property
671 def last_name(self):
671 def last_name(self):
672 from rhodecode.lib import helpers as h
672 from rhodecode.lib import helpers as h
673 if self.lastname:
673 if self.lastname:
674 return h.escape(self.lastname)
674 return h.escape(self.lastname)
675 return self.lastname
675 return self.lastname
676
676
677 @hybrid_property
677 @hybrid_property
678 def api_key(self):
678 def api_key(self):
679 """
679 """
680 Fetch if exist an auth-token with role ALL connected to this user
680 Fetch if exist an auth-token with role ALL connected to this user
681 """
681 """
682 user_auth_token = UserApiKeys.query()\
682 user_auth_token = UserApiKeys.query()\
683 .filter(UserApiKeys.user_id == self.user_id)\
683 .filter(UserApiKeys.user_id == self.user_id)\
684 .filter(or_(UserApiKeys.expires == -1,
684 .filter(or_(UserApiKeys.expires == -1,
685 UserApiKeys.expires >= time.time()))\
685 UserApiKeys.expires >= time.time()))\
686 .filter(UserApiKeys.role == UserApiKeys.ROLE_ALL).first()
686 .filter(UserApiKeys.role == UserApiKeys.ROLE_ALL).first()
687 if user_auth_token:
687 if user_auth_token:
688 user_auth_token = user_auth_token.api_key
688 user_auth_token = user_auth_token.api_key
689
689
690 return user_auth_token
690 return user_auth_token
691
691
692 @api_key.setter
692 @api_key.setter
693 def api_key(self, val):
693 def api_key(self, val):
694 # don't allow to set API key this is deprecated for now
694 # don't allow to set API key this is deprecated for now
695 self._api_key = None
695 self._api_key = None
696
696
697 @property
697 @property
698 def reviewer_pull_requests(self):
698 def reviewer_pull_requests(self):
699 return PullRequestReviewers.query() \
699 return PullRequestReviewers.query() \
700 .options(joinedload(PullRequestReviewers.pull_request)) \
700 .options(joinedload(PullRequestReviewers.pull_request)) \
701 .filter(PullRequestReviewers.user_id == self.user_id) \
701 .filter(PullRequestReviewers.user_id == self.user_id) \
702 .all()
702 .all()
703
703
704 @property
704 @property
705 def firstname(self):
705 def firstname(self):
706 # alias for future
706 # alias for future
707 return self.name
707 return self.name
708
708
709 @property
709 @property
710 def emails(self):
710 def emails(self):
711 other = UserEmailMap.query()\
711 other = UserEmailMap.query()\
712 .filter(UserEmailMap.user == self) \
712 .filter(UserEmailMap.user == self) \
713 .order_by(UserEmailMap.email_id.asc()) \
713 .order_by(UserEmailMap.email_id.asc()) \
714 .all()
714 .all()
715 return [self.email] + [x.email for x in other]
715 return [self.email] + [x.email for x in other]
716
716
717 def emails_cached(self):
717 def emails_cached(self):
718 emails = []
718 emails = []
719 if self.user_id != self.get_default_user_id():
719 if self.user_id != self.get_default_user_id():
720 emails = UserEmailMap.query()\
720 emails = UserEmailMap.query()\
721 .filter(UserEmailMap.user == self) \
721 .filter(UserEmailMap.user == self) \
722 .order_by(UserEmailMap.email_id.asc())
722 .order_by(UserEmailMap.email_id.asc())
723
723
724 emails = emails.options(
724 emails = emails.options(
725 FromCache("sql_cache_short", f"get_user_{self.user_id}_emails")
725 FromCache("sql_cache_short", f"get_user_{self.user_id}_emails")
726 )
726 )
727
727
728 return [self.email] + [x.email for x in emails]
728 return [self.email] + [x.email for x in emails]
729
729
730 @property
730 @property
731 def auth_tokens(self):
731 def auth_tokens(self):
732 auth_tokens = self.get_auth_tokens()
732 auth_tokens = self.get_auth_tokens()
733 return [x.api_key for x in auth_tokens]
733 return [x.api_key for x in auth_tokens]
734
734
735 def get_auth_tokens(self):
735 def get_auth_tokens(self):
736 return UserApiKeys.query()\
736 return UserApiKeys.query()\
737 .filter(UserApiKeys.user == self)\
737 .filter(UserApiKeys.user == self)\
738 .order_by(UserApiKeys.user_api_key_id.asc())\
738 .order_by(UserApiKeys.user_api_key_id.asc())\
739 .all()
739 .all()
740
740
741 @LazyProperty
741 @LazyProperty
742 def feed_token(self):
742 def feed_token(self):
743 return self.get_feed_token()
743 return self.get_feed_token()
744
744
745 def get_feed_token(self, cache=True):
745 def get_feed_token(self, cache=True):
746 feed_tokens = UserApiKeys.query()\
746 feed_tokens = UserApiKeys.query()\
747 .filter(UserApiKeys.user == self)\
747 .filter(UserApiKeys.user == self)\
748 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)
748 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)
749 if cache:
749 if cache:
750 feed_tokens = feed_tokens.options(
750 feed_tokens = feed_tokens.options(
751 FromCache("sql_cache_short", f"get_user_feed_token_{self.user_id}"))
751 FromCache("sql_cache_short", f"get_user_feed_token_{self.user_id}"))
752
752
753 feed_tokens = feed_tokens.all()
753 feed_tokens = feed_tokens.all()
754 if feed_tokens:
754 if feed_tokens:
755 return feed_tokens[0].api_key
755 return feed_tokens[0].api_key
756 return 'NO_FEED_TOKEN_AVAILABLE'
756 return 'NO_FEED_TOKEN_AVAILABLE'
757
757
758 @LazyProperty
758 @LazyProperty
759 def artifact_token(self):
759 def artifact_token(self):
760 return self.get_artifact_token()
760 return self.get_artifact_token()
761
761
762 def get_artifact_token(self, cache=True):
762 def get_artifact_token(self, cache=True):
763 artifacts_tokens = UserApiKeys.query()\
763 artifacts_tokens = UserApiKeys.query()\
764 .filter(UserApiKeys.user == self) \
764 .filter(UserApiKeys.user == self) \
765 .filter(or_(UserApiKeys.expires == -1,
765 .filter(or_(UserApiKeys.expires == -1,
766 UserApiKeys.expires >= time.time())) \
766 UserApiKeys.expires >= time.time())) \
767 .filter(UserApiKeys.role == UserApiKeys.ROLE_ARTIFACT_DOWNLOAD)
767 .filter(UserApiKeys.role == UserApiKeys.ROLE_ARTIFACT_DOWNLOAD)
768
768
769 if cache:
769 if cache:
770 artifacts_tokens = artifacts_tokens.options(
770 artifacts_tokens = artifacts_tokens.options(
771 FromCache("sql_cache_short", f"get_user_artifact_token_{self.user_id}"))
771 FromCache("sql_cache_short", f"get_user_artifact_token_{self.user_id}"))
772
772
773 artifacts_tokens = artifacts_tokens.all()
773 artifacts_tokens = artifacts_tokens.all()
774 if artifacts_tokens:
774 if artifacts_tokens:
775 return artifacts_tokens[0].api_key
775 return artifacts_tokens[0].api_key
776 return 'NO_ARTIFACT_TOKEN_AVAILABLE'
776 return 'NO_ARTIFACT_TOKEN_AVAILABLE'
777
777
778 def get_or_create_artifact_token(self):
778 def get_or_create_artifact_token(self):
779 artifacts_tokens = UserApiKeys.query()\
779 artifacts_tokens = UserApiKeys.query()\
780 .filter(UserApiKeys.user == self) \
780 .filter(UserApiKeys.user == self) \
781 .filter(or_(UserApiKeys.expires == -1,
781 .filter(or_(UserApiKeys.expires == -1,
782 UserApiKeys.expires >= time.time())) \
782 UserApiKeys.expires >= time.time())) \
783 .filter(UserApiKeys.role == UserApiKeys.ROLE_ARTIFACT_DOWNLOAD)
783 .filter(UserApiKeys.role == UserApiKeys.ROLE_ARTIFACT_DOWNLOAD)
784
784
785 artifacts_tokens = artifacts_tokens.all()
785 artifacts_tokens = artifacts_tokens.all()
786 if artifacts_tokens:
786 if artifacts_tokens:
787 return artifacts_tokens[0].api_key
787 return artifacts_tokens[0].api_key
788 else:
788 else:
789 from rhodecode.model.auth_token import AuthTokenModel
789 from rhodecode.model.auth_token import AuthTokenModel
790 artifact_token = AuthTokenModel().create(
790 artifact_token = AuthTokenModel().create(
791 self, 'auto-generated-artifact-token',
791 self, 'auto-generated-artifact-token',
792 lifetime=-1, role=UserApiKeys.ROLE_ARTIFACT_DOWNLOAD)
792 lifetime=-1, role=UserApiKeys.ROLE_ARTIFACT_DOWNLOAD)
793 Session.commit()
793 Session.commit()
794 return artifact_token.api_key
794 return artifact_token.api_key
795
795
796 @classmethod
796 @classmethod
797 def get(cls, user_id, cache=False):
797 def get(cls, user_id, cache=False):
798 if not user_id:
798 if not user_id:
799 return
799 return
800
800
801 user = cls.query()
801 user = cls.query()
802 if cache:
802 if cache:
803 user = user.options(
803 user = user.options(
804 FromCache("sql_cache_short", f"get_users_{user_id}"))
804 FromCache("sql_cache_short", f"get_users_{user_id}"))
805 return user.get(user_id)
805 return user.get(user_id)
806
806
807 @classmethod
807 @classmethod
808 def extra_valid_auth_tokens(cls, user, role=None):
808 def extra_valid_auth_tokens(cls, user, role=None):
809 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
809 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
810 .filter(or_(UserApiKeys.expires == -1,
810 .filter(or_(UserApiKeys.expires == -1,
811 UserApiKeys.expires >= time.time()))
811 UserApiKeys.expires >= time.time()))
812 if role:
812 if role:
813 tokens = tokens.filter(or_(UserApiKeys.role == role,
813 tokens = tokens.filter(or_(UserApiKeys.role == role,
814 UserApiKeys.role == UserApiKeys.ROLE_ALL))
814 UserApiKeys.role == UserApiKeys.ROLE_ALL))
815 return tokens.all()
815 return tokens.all()
816
816
817 def authenticate_by_token(self, auth_token, roles=None, scope_repo_id=None):
817 def authenticate_by_token(self, auth_token, roles=None, scope_repo_id=None):
818 from rhodecode.lib import auth
818 from rhodecode.lib import auth
819
819
820 log.debug('Trying to authenticate user: %s via auth-token, '
820 log.debug('Trying to authenticate user: %s via auth-token, '
821 'and roles: %s', self, roles)
821 'and roles: %s', self, roles)
822
822
823 if not auth_token:
823 if not auth_token:
824 return False
824 return False
825
825
826 roles = (roles or []) + [UserApiKeys.ROLE_ALL]
826 roles = (roles or []) + [UserApiKeys.ROLE_ALL]
827 tokens_q = UserApiKeys.query()\
827 tokens_q = UserApiKeys.query()\
828 .filter(UserApiKeys.user_id == self.user_id)\
828 .filter(UserApiKeys.user_id == self.user_id)\
829 .filter(or_(UserApiKeys.expires == -1,
829 .filter(or_(UserApiKeys.expires == -1,
830 UserApiKeys.expires >= time.time()))
830 UserApiKeys.expires >= time.time()))
831
831
832 tokens_q = tokens_q.filter(UserApiKeys.role.in_(roles))
832 tokens_q = tokens_q.filter(UserApiKeys.role.in_(roles))
833
833
834 crypto_backend = auth.crypto_backend()
834 crypto_backend = auth.crypto_backend()
835 enc_token_map = {}
835 enc_token_map = {}
836 plain_token_map = {}
836 plain_token_map = {}
837 for token in tokens_q:
837 for token in tokens_q:
838 if token.api_key.startswith(crypto_backend.ENC_PREF):
838 if token.api_key.startswith(crypto_backend.ENC_PREF):
839 enc_token_map[token.api_key] = token
839 enc_token_map[token.api_key] = token
840 else:
840 else:
841 plain_token_map[token.api_key] = token
841 plain_token_map[token.api_key] = token
842 log.debug(
842 log.debug(
843 'Found %s plain and %s encrypted tokens to check for authentication for this user',
843 'Found %s plain and %s encrypted tokens to check for authentication for this user',
844 len(plain_token_map), len(enc_token_map))
844 len(plain_token_map), len(enc_token_map))
845
845
846 # plain token match comes first
846 # plain token match comes first
847 match = plain_token_map.get(auth_token)
847 match = plain_token_map.get(auth_token)
848
848
849 # check encrypted tokens now
849 # check encrypted tokens now
850 if not match:
850 if not match:
851 for token_hash, token in enc_token_map.items():
851 for token_hash, token in enc_token_map.items():
852 # NOTE(marcink): this is expensive to calculate, but most secure
852 # NOTE(marcink): this is expensive to calculate, but most secure
853 if crypto_backend.hash_check(auth_token, token_hash):
853 if crypto_backend.hash_check(auth_token, token_hash):
854 match = token
854 match = token
855 break
855 break
856
856
857 if match:
857 if match:
858 log.debug('Found matching token %s', match)
858 log.debug('Found matching token %s', match)
859 if match.repo_id:
859 if match.repo_id:
860 log.debug('Found scope, checking for scope match of token %s', match)
860 log.debug('Found scope, checking for scope match of token %s', match)
861 if match.repo_id == scope_repo_id:
861 if match.repo_id == scope_repo_id:
862 return True
862 return True
863 else:
863 else:
864 log.debug(
864 log.debug(
865 'AUTH_TOKEN: scope mismatch, token has a set repo scope: %s, '
865 'AUTH_TOKEN: scope mismatch, token has a set repo scope: %s, '
866 'and calling scope is:%s, skipping further checks',
866 'and calling scope is:%s, skipping further checks',
867 match.repo, scope_repo_id)
867 match.repo, scope_repo_id)
868 return False
868 return False
869 else:
869 else:
870 return True
870 return True
871
871
872 return False
872 return False
873
873
874 @property
874 @property
875 def ip_addresses(self):
875 def ip_addresses(self):
876 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
876 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
877 return [x.ip_addr for x in ret]
877 return [x.ip_addr for x in ret]
878
878
879 @property
879 @property
880 def username_and_name(self):
880 def username_and_name(self):
881 return f'{self.username} ({self.first_name} {self.last_name})'
881 return f'{self.username} ({self.first_name} {self.last_name})'
882
882
883 @property
883 @property
884 def username_or_name_or_email(self):
884 def username_or_name_or_email(self):
885 full_name = self.full_name if self.full_name != ' ' else None
885 full_name = self.full_name if self.full_name != ' ' else None
886 return self.username or full_name or self.email
886 return self.username or full_name or self.email
887
887
888 @property
888 @property
889 def full_name(self):
889 def full_name(self):
890 return f'{self.first_name} {self.last_name}'
890 return f'{self.first_name} {self.last_name}'
891
891
892 @property
892 @property
893 def full_name_or_username(self):
893 def full_name_or_username(self):
894 return (f'{self.first_name} {self.last_name}'
894 return (f'{self.first_name} {self.last_name}'
895 if (self.first_name and self.last_name) else self.username)
895 if (self.first_name and self.last_name) else self.username)
896
896
897 @property
897 @property
898 def full_contact(self):
898 def full_contact(self):
899 return f'{self.first_name} {self.last_name} <{self.email}>'
899 return f'{self.first_name} {self.last_name} <{self.email}>'
900
900
901 @property
901 @property
902 def short_contact(self):
902 def short_contact(self):
903 return f'{self.first_name} {self.last_name}'
903 return f'{self.first_name} {self.last_name}'
904
904
905 @property
905 @property
906 def is_admin(self):
906 def is_admin(self):
907 return self.admin
907 return self.admin
908
908
909 @property
909 @property
910 def language(self):
910 def language(self):
911 return self.user_data.get('language')
911 return self.user_data.get('language')
912
912
913 def AuthUser(self, **kwargs):
913 def AuthUser(self, **kwargs):
914 """
914 """
915 Returns instance of AuthUser for this user
915 Returns instance of AuthUser for this user
916 """
916 """
917 from rhodecode.lib.auth import AuthUser
917 from rhodecode.lib.auth import AuthUser
918 return AuthUser(user_id=self.user_id, username=self.username, **kwargs)
918 return AuthUser(user_id=self.user_id, username=self.username, **kwargs)
919
919
920 @hybrid_property
920 @hybrid_property
921 def user_data(self):
921 def user_data(self):
922 if not self._user_data:
922 if not self._user_data:
923 return {}
923 return {}
924
924
925 try:
925 try:
926 return json.loads(self._user_data) or {}
926 return json.loads(self._user_data) or {}
927 except TypeError:
927 except TypeError:
928 return {}
928 return {}
929
929
930 @user_data.setter
930 @user_data.setter
931 def user_data(self, val):
931 def user_data(self, val):
932 if not isinstance(val, dict):
932 if not isinstance(val, dict):
933 raise Exception('user_data must be dict, got %s' % type(val))
933 raise Exception('user_data must be dict, got %s' % type(val))
934 try:
934 try:
935 self._user_data = safe_bytes(json.dumps(val))
935 self._user_data = safe_bytes(json.dumps(val))
936 except Exception:
936 except Exception:
937 log.error(traceback.format_exc())
937 log.error(traceback.format_exc())
938
938
939 @classmethod
939 @classmethod
940 def get_by_username(cls, username, case_insensitive=False,
940 def get_by_username(cls, username, case_insensitive=False,
941 cache=False):
941 cache=False):
942
942
943 if case_insensitive:
943 if case_insensitive:
944 q = cls.select().where(
944 q = cls.select().where(
945 func.lower(cls.username) == func.lower(username))
945 func.lower(cls.username) == func.lower(username))
946 else:
946 else:
947 q = cls.select().where(cls.username == username)
947 q = cls.select().where(cls.username == username)
948
948
949 if cache:
949 if cache:
950 hash_key = _hash_key(username)
950 hash_key = _hash_key(username)
951 q = q.options(
951 q = q.options(
952 FromCache("sql_cache_short", f"get_user_by_name_{hash_key}"))
952 FromCache("sql_cache_short", f"get_user_by_name_{hash_key}"))
953
953
954 return cls.execute(q).scalar_one_or_none()
954 return cls.execute(q).scalar_one_or_none()
955
955
956 @classmethod
956 @classmethod
957 def get_by_auth_token(cls, auth_token, cache=False):
957 def get_by_auth_token(cls, auth_token, cache=False):
958
958
959 q = cls.select(User)\
959 q = cls.select(User)\
960 .join(UserApiKeys)\
960 .join(UserApiKeys)\
961 .where(UserApiKeys.api_key == auth_token)\
961 .where(UserApiKeys.api_key == auth_token)\
962 .where(or_(UserApiKeys.expires == -1,
962 .where(or_(UserApiKeys.expires == -1,
963 UserApiKeys.expires >= time.time()))
963 UserApiKeys.expires >= time.time()))
964
964
965 if cache:
965 if cache:
966 q = q.options(
966 q = q.options(
967 FromCache("sql_cache_short", f"get_auth_token_{auth_token}"))
967 FromCache("sql_cache_short", f"get_auth_token_{auth_token}"))
968
968
969 matched_user = cls.execute(q).scalar_one_or_none()
969 matched_user = cls.execute(q).scalar_one_or_none()
970
970
971 return matched_user
971 return matched_user
972
972
973 @classmethod
973 @classmethod
974 def get_by_email(cls, email, case_insensitive=False, cache=False):
974 def get_by_email(cls, email, case_insensitive=False, cache=False):
975
975
976 if case_insensitive:
976 if case_insensitive:
977 q = cls.select().where(func.lower(cls.email) == func.lower(email))
977 q = cls.select().where(func.lower(cls.email) == func.lower(email))
978 else:
978 else:
979 q = cls.select().where(cls.email == email)
979 q = cls.select().where(cls.email == email)
980
980
981 if cache:
981 if cache:
982 email_key = _hash_key(email)
982 email_key = _hash_key(email)
983 q = q.options(
983 q = q.options(
984 FromCache("sql_cache_short", f"get_email_key_{email_key}"))
984 FromCache("sql_cache_short", f"get_email_key_{email_key}"))
985
985
986 ret = cls.execute(q).scalar_one_or_none()
986 ret = cls.execute(q).scalar_one_or_none()
987
987
988 if ret is None:
988 if ret is None:
989 q = cls.select(UserEmailMap)
989 q = cls.select(UserEmailMap)
990 # try fetching in alternate email map
990 # try fetching in alternate email map
991 if case_insensitive:
991 if case_insensitive:
992 q = q.where(func.lower(UserEmailMap.email) == func.lower(email))
992 q = q.where(func.lower(UserEmailMap.email) == func.lower(email))
993 else:
993 else:
994 q = q.where(UserEmailMap.email == email)
994 q = q.where(UserEmailMap.email == email)
995 q = q.options(joinedload(UserEmailMap.user))
995 q = q.options(joinedload(UserEmailMap.user))
996 if cache:
996 if cache:
997 q = q.options(
997 q = q.options(
998 FromCache("sql_cache_short", f"get_email_map_key_{email_key}"))
998 FromCache("sql_cache_short", f"get_email_map_key_{email_key}"))
999
999
1000 result = cls.execute(q).scalar_one_or_none()
1000 result = cls.execute(q).scalar_one_or_none()
1001 ret = getattr(result, 'user', None)
1001 ret = getattr(result, 'user', None)
1002
1002
1003 return ret
1003 return ret
1004
1004
1005 @classmethod
1005 @classmethod
1006 def get_from_cs_author(cls, author):
1006 def get_from_cs_author(cls, author):
1007 """
1007 """
1008 Tries to get User objects out of commit author string
1008 Tries to get User objects out of commit author string
1009
1009
1010 :param author:
1010 :param author:
1011 """
1011 """
1012 from rhodecode.lib.helpers import email, author_name
1012 from rhodecode.lib.helpers import email, author_name
1013 # Valid email in the attribute passed, see if they're in the system
1013 # Valid email in the attribute passed, see if they're in the system
1014 _email = email(author)
1014 _email = email(author)
1015 if _email:
1015 if _email:
1016 user = cls.get_by_email(_email, case_insensitive=True)
1016 user = cls.get_by_email(_email, case_insensitive=True)
1017 if user:
1017 if user:
1018 return user
1018 return user
1019 # Maybe we can match by username?
1019 # Maybe we can match by username?
1020 _author = author_name(author)
1020 _author = author_name(author)
1021 user = cls.get_by_username(_author, case_insensitive=True)
1021 user = cls.get_by_username(_author, case_insensitive=True)
1022 if user:
1022 if user:
1023 return user
1023 return user
1024
1024
1025 def update_userdata(self, **kwargs):
1025 def update_userdata(self, **kwargs):
1026 usr = self
1026 usr = self
1027 old = usr.user_data
1027 old = usr.user_data
1028 old.update(**kwargs)
1028 old.update(**kwargs)
1029 usr.user_data = old
1029 usr.user_data = old
1030 Session().add(usr)
1030 Session().add(usr)
1031 log.debug('updated userdata with %s', kwargs)
1031 log.debug('updated userdata with %s', kwargs)
1032
1032
1033 def update_lastlogin(self):
1033 def update_lastlogin(self):
1034 """Update user lastlogin"""
1034 """Update user lastlogin"""
1035 self.last_login = datetime.datetime.now()
1035 self.last_login = datetime.datetime.now()
1036 Session().add(self)
1036 Session().add(self)
1037 log.debug('updated user %s lastlogin', self.username)
1037 log.debug('updated user %s lastlogin', self.username)
1038
1038
1039 def update_password(self, new_password):
1039 def update_password(self, new_password):
1040 from rhodecode.lib.auth import get_crypt_password
1040 from rhodecode.lib.auth import get_crypt_password
1041
1041
1042 self.password = get_crypt_password(new_password)
1042 self.password = get_crypt_password(new_password)
1043 Session().add(self)
1043 Session().add(self)
1044
1044
1045 @classmethod
1045 @classmethod
1046 def get_first_super_admin(cls):
1046 def get_first_super_admin(cls):
1047 stmt = cls.select().where(User.admin == true()).order_by(User.user_id.asc())
1047 stmt = cls.select().where(User.admin == true()).order_by(User.user_id.asc())
1048 user = cls.scalars(stmt).first()
1048 user = cls.scalars(stmt).first()
1049
1049
1050 if user is None:
1050 if user is None:
1051 raise Exception('FATAL: Missing administrative account!')
1051 raise Exception('FATAL: Missing administrative account!')
1052 return user
1052 return user
1053
1053
1054 @classmethod
1054 @classmethod
1055 def get_all_super_admins(cls, only_active=False):
1055 def get_all_super_admins(cls, only_active=False):
1056 """
1056 """
1057 Returns all admin accounts sorted by username
1057 Returns all admin accounts sorted by username
1058 """
1058 """
1059 qry = User.query().filter(User.admin == true()).order_by(User.username.asc())
1059 qry = User.query().filter(User.admin == true()).order_by(User.username.asc())
1060 if only_active:
1060 if only_active:
1061 qry = qry.filter(User.active == true())
1061 qry = qry.filter(User.active == true())
1062 return qry.all()
1062 return qry.all()
1063
1063
1064 @classmethod
1064 @classmethod
1065 def get_all_user_ids(cls, only_active=True):
1065 def get_all_user_ids(cls, only_active=True):
1066 """
1066 """
1067 Returns all users IDs
1067 Returns all users IDs
1068 """
1068 """
1069 qry = Session().query(User.user_id)
1069 qry = Session().query(User.user_id)
1070
1070
1071 if only_active:
1071 if only_active:
1072 qry = qry.filter(User.active == true())
1072 qry = qry.filter(User.active == true())
1073 return [x.user_id for x in qry]
1073 return [x.user_id for x in qry]
1074
1074
1075 @classmethod
1075 @classmethod
1076 def get_default_user(cls, cache=False, refresh=False):
1076 def get_default_user(cls, cache=False, refresh=False):
1077 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
1077 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
1078 if user is None:
1078 if user is None:
1079 raise Exception('FATAL: Missing default account!')
1079 raise Exception('FATAL: Missing default account!')
1080 if refresh:
1080 if refresh:
1081 # The default user might be based on outdated state which
1081 # The default user might be based on outdated state which
1082 # has been loaded from the cache.
1082 # has been loaded from the cache.
1083 # A call to refresh() ensures that the
1083 # A call to refresh() ensures that the
1084 # latest state from the database is used.
1084 # latest state from the database is used.
1085 Session().refresh(user)
1085 Session().refresh(user)
1086
1086
1087 return user
1087 return user
1088
1088
1089 @classmethod
1089 @classmethod
1090 def get_default_user_id(cls):
1090 def get_default_user_id(cls):
1091 import rhodecode
1091 import rhodecode
1092 return rhodecode.CONFIG['default_user_id']
1092 return rhodecode.CONFIG['default_user_id']
1093
1093
1094 def _get_default_perms(self, user, suffix=''):
1094 def _get_default_perms(self, user, suffix=''):
1095 from rhodecode.model.permission import PermissionModel
1095 from rhodecode.model.permission import PermissionModel
1096 return PermissionModel().get_default_perms(user.user_perms, suffix)
1096 return PermissionModel().get_default_perms(user.user_perms, suffix)
1097
1097
1098 def get_default_perms(self, suffix=''):
1098 def get_default_perms(self, suffix=''):
1099 return self._get_default_perms(self, suffix)
1099 return self._get_default_perms(self, suffix)
1100
1100
1101 def get_api_data(self, include_secrets=False, details='full'):
1101 def get_api_data(self, include_secrets=False, details='full'):
1102 """
1102 """
1103 Common function for generating user related data for API
1103 Common function for generating user related data for API
1104
1104
1105 :param include_secrets: By default secrets in the API data will be replaced
1105 :param include_secrets: By default secrets in the API data will be replaced
1106 by a placeholder value to prevent exposing this data by accident. In case
1106 by a placeholder value to prevent exposing this data by accident. In case
1107 this data shall be exposed, set this flag to ``True``.
1107 this data shall be exposed, set this flag to ``True``.
1108
1108
1109 :param details: details can be 'basic|full' basic gives only a subset of
1109 :param details: details can be 'basic|full' basic gives only a subset of
1110 the available user information that includes user_id, name and emails.
1110 the available user information that includes user_id, name and emails.
1111 """
1111 """
1112 user = self
1112 user = self
1113 user_data = self.user_data
1113 user_data = self.user_data
1114 data = {
1114 data = {
1115 'user_id': user.user_id,
1115 'user_id': user.user_id,
1116 'username': user.username,
1116 'username': user.username,
1117 'firstname': user.name,
1117 'firstname': user.name,
1118 'lastname': user.lastname,
1118 'lastname': user.lastname,
1119 'description': user.description,
1119 'description': user.description,
1120 'email': user.email,
1120 'email': user.email,
1121 'emails': user.emails,
1121 'emails': user.emails,
1122 }
1122 }
1123 if details == 'basic':
1123 if details == 'basic':
1124 return data
1124 return data
1125
1125
1126 auth_token_length = 40
1126 auth_token_length = 40
1127 auth_token_replacement = '*' * auth_token_length
1127 auth_token_replacement = '*' * auth_token_length
1128
1128
1129 extras = {
1129 extras = {
1130 'auth_tokens': [auth_token_replacement],
1130 'auth_tokens': [auth_token_replacement],
1131 'active': user.active,
1131 'active': user.active,
1132 'admin': user.admin,
1132 'admin': user.admin,
1133 'extern_type': user.extern_type,
1133 'extern_type': user.extern_type,
1134 'extern_name': user.extern_name,
1134 'extern_name': user.extern_name,
1135 'last_login': user.last_login,
1135 'last_login': user.last_login,
1136 'last_activity': user.last_activity,
1136 'last_activity': user.last_activity,
1137 'ip_addresses': user.ip_addresses,
1137 'ip_addresses': user.ip_addresses,
1138 'language': user_data.get('language')
1138 'language': user_data.get('language')
1139 }
1139 }
1140 data.update(extras)
1140 data.update(extras)
1141
1141
1142 if include_secrets:
1142 if include_secrets:
1143 data['auth_tokens'] = user.auth_tokens
1143 data['auth_tokens'] = user.auth_tokens
1144 return data
1144 return data
1145
1145
1146 def __json__(self):
1146 def __json__(self):
1147 data = {
1147 data = {
1148 'full_name': self.full_name,
1148 'full_name': self.full_name,
1149 'full_name_or_username': self.full_name_or_username,
1149 'full_name_or_username': self.full_name_or_username,
1150 'short_contact': self.short_contact,
1150 'short_contact': self.short_contact,
1151 'full_contact': self.full_contact,
1151 'full_contact': self.full_contact,
1152 }
1152 }
1153 data.update(self.get_api_data())
1153 data.update(self.get_api_data())
1154 return data
1154 return data
1155
1155
1156
1156
1157 class UserApiKeys(Base, BaseModel):
1157 class UserApiKeys(Base, BaseModel):
1158 __tablename__ = 'user_api_keys'
1158 __tablename__ = 'user_api_keys'
1159 __table_args__ = (
1159 __table_args__ = (
1160 Index('uak_api_key_idx', 'api_key'),
1160 Index('uak_api_key_idx', 'api_key'),
1161 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
1161 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
1162 base_table_args
1162 base_table_args
1163 )
1163 )
1164
1164
1165 # ApiKey role
1165 # ApiKey role
1166 ROLE_ALL = 'token_role_all'
1166 ROLE_ALL = 'token_role_all'
1167 ROLE_VCS = 'token_role_vcs'
1167 ROLE_VCS = 'token_role_vcs'
1168 ROLE_API = 'token_role_api'
1168 ROLE_API = 'token_role_api'
1169 ROLE_HTTP = 'token_role_http'
1169 ROLE_HTTP = 'token_role_http'
1170 ROLE_FEED = 'token_role_feed'
1170 ROLE_FEED = 'token_role_feed'
1171 ROLE_ARTIFACT_DOWNLOAD = 'role_artifact_download'
1171 ROLE_ARTIFACT_DOWNLOAD = 'role_artifact_download'
1172 # The last one is ignored in the list as we only
1172 # The last one is ignored in the list as we only
1173 # use it for one action, and cannot be created by users
1173 # use it for one action, and cannot be created by users
1174 ROLE_PASSWORD_RESET = 'token_password_reset'
1174 ROLE_PASSWORD_RESET = 'token_password_reset'
1175
1175
1176 ROLES = [ROLE_ALL, ROLE_VCS, ROLE_API, ROLE_HTTP, ROLE_FEED, ROLE_ARTIFACT_DOWNLOAD]
1176 ROLES = [ROLE_ALL, ROLE_VCS, ROLE_API, ROLE_HTTP, ROLE_FEED, ROLE_ARTIFACT_DOWNLOAD]
1177
1177
1178 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1178 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1179 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1179 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1180 api_key = Column("api_key", String(255), nullable=False, unique=True)
1180 api_key = Column("api_key", String(255), nullable=False, unique=True)
1181 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1181 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1182 expires = Column('expires', Float(53), nullable=False)
1182 expires = Column('expires', Float(53), nullable=False)
1183 role = Column('role', String(255), nullable=True)
1183 role = Column('role', String(255), nullable=True)
1184 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1184 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1185
1185
1186 # scope columns
1186 # scope columns
1187 repo_id = Column(
1187 repo_id = Column(
1188 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
1188 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
1189 nullable=True, unique=None, default=None)
1189 nullable=True, unique=None, default=None)
1190 repo = relationship('Repository', lazy='joined', back_populates='scoped_tokens')
1190 repo = relationship('Repository', lazy='joined', back_populates='scoped_tokens')
1191
1191
1192 repo_group_id = Column(
1192 repo_group_id = Column(
1193 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
1193 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
1194 nullable=True, unique=None, default=None)
1194 nullable=True, unique=None, default=None)
1195 repo_group = relationship('RepoGroup', lazy='joined')
1195 repo_group = relationship('RepoGroup', lazy='joined')
1196
1196
1197 user = relationship('User', lazy='joined', back_populates='user_auth_tokens')
1197 user = relationship('User', lazy='joined', back_populates='user_auth_tokens')
1198
1198
1199 def __repr__(self):
1199 def __repr__(self):
1200 return f"<{self.cls_name}('{self.role}')>"
1200 return f"<{self.cls_name}('{self.role}')>"
1201
1201
1202 def __json__(self):
1202 def __json__(self):
1203 data = {
1203 data = {
1204 'auth_token': self.api_key,
1204 'auth_token': self.api_key,
1205 'role': self.role,
1205 'role': self.role,
1206 'scope': self.scope_humanized,
1206 'scope': self.scope_humanized,
1207 'expired': self.expired
1207 'expired': self.expired
1208 }
1208 }
1209 return data
1209 return data
1210
1210
1211 def get_api_data(self, include_secrets=False):
1211 def get_api_data(self, include_secrets=False):
1212 data = self.__json__()
1212 data = self.__json__()
1213 if include_secrets:
1213 if include_secrets:
1214 return data
1214 return data
1215 else:
1215 else:
1216 data['auth_token'] = self.token_obfuscated
1216 data['auth_token'] = self.token_obfuscated
1217 return data
1217 return data
1218
1218
1219 @hybrid_property
1219 @hybrid_property
1220 def description_safe(self):
1220 def description_safe(self):
1221 from rhodecode.lib import helpers as h
1221 from rhodecode.lib import helpers as h
1222 return h.escape(self.description)
1222 return h.escape(self.description)
1223
1223
1224 @property
1224 @property
1225 def expired(self):
1225 def expired(self):
1226 if self.expires == -1:
1226 if self.expires == -1:
1227 return False
1227 return False
1228 return time.time() > self.expires
1228 return time.time() > self.expires
1229
1229
1230 @classmethod
1230 @classmethod
1231 def _get_role_name(cls, role):
1231 def _get_role_name(cls, role):
1232 return {
1232 return {
1233 cls.ROLE_ALL: _('all'),
1233 cls.ROLE_ALL: _('all'),
1234 cls.ROLE_HTTP: _('http/web interface'),
1234 cls.ROLE_HTTP: _('http/web interface'),
1235 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
1235 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
1236 cls.ROLE_API: _('api calls'),
1236 cls.ROLE_API: _('api calls'),
1237 cls.ROLE_FEED: _('feed access'),
1237 cls.ROLE_FEED: _('feed access'),
1238 cls.ROLE_ARTIFACT_DOWNLOAD: _('artifacts downloads'),
1238 cls.ROLE_ARTIFACT_DOWNLOAD: _('artifacts downloads'),
1239 }.get(role, role)
1239 }.get(role, role)
1240
1240
1241 @classmethod
1241 @classmethod
1242 def _get_role_description(cls, role):
1242 def _get_role_description(cls, role):
1243 return {
1243 return {
1244 cls.ROLE_ALL: _('Token for all actions.'),
1244 cls.ROLE_ALL: _('Token for all actions.'),
1245 cls.ROLE_HTTP: _('Token to access RhodeCode pages via web interface without '
1245 cls.ROLE_HTTP: _('Token to access RhodeCode pages via web interface without '
1246 'login using `api_access_controllers_whitelist` functionality.'),
1246 'login using `api_access_controllers_whitelist` functionality.'),
1247 cls.ROLE_VCS: _('Token to interact over git/hg/svn protocols. '
1247 cls.ROLE_VCS: _('Token to interact over git/hg/svn protocols. '
1248 'Requires auth_token authentication plugin to be active. <br/>'
1248 'Requires auth_token authentication plugin to be active. <br/>'
1249 'Such Token should be used then instead of a password to '
1249 'Such Token should be used then instead of a password to '
1250 'interact with a repository, and additionally can be '
1250 'interact with a repository, and additionally can be '
1251 'limited to single repository using repo scope.'),
1251 'limited to single repository using repo scope.'),
1252 cls.ROLE_API: _('Token limited to api calls.'),
1252 cls.ROLE_API: _('Token limited to api calls.'),
1253 cls.ROLE_FEED: _('Token to read RSS/ATOM feed.'),
1253 cls.ROLE_FEED: _('Token to read RSS/ATOM feed.'),
1254 cls.ROLE_ARTIFACT_DOWNLOAD: _('Token for artifacts downloads.'),
1254 cls.ROLE_ARTIFACT_DOWNLOAD: _('Token for artifacts downloads.'),
1255 }.get(role, role)
1255 }.get(role, role)
1256
1256
1257 @property
1257 @property
1258 def role_humanized(self):
1258 def role_humanized(self):
1259 return self._get_role_name(self.role)
1259 return self._get_role_name(self.role)
1260
1260
1261 def _get_scope(self):
1261 def _get_scope(self):
1262 if self.repo:
1262 if self.repo:
1263 return 'Repository: {}'.format(self.repo.repo_name)
1263 return 'Repository: {}'.format(self.repo.repo_name)
1264 if self.repo_group:
1264 if self.repo_group:
1265 return 'RepositoryGroup: {} (recursive)'.format(self.repo_group.group_name)
1265 return 'RepositoryGroup: {} (recursive)'.format(self.repo_group.group_name)
1266 return 'Global'
1266 return 'Global'
1267
1267
1268 @property
1268 @property
1269 def scope_humanized(self):
1269 def scope_humanized(self):
1270 return self._get_scope()
1270 return self._get_scope()
1271
1271
1272 @property
1272 @property
1273 def token_obfuscated(self):
1273 def token_obfuscated(self):
1274 if self.api_key:
1274 if self.api_key:
1275 return self.api_key[:4] + "****"
1275 return self.api_key[:4] + "****"
1276
1276
1277
1277
1278 class UserEmailMap(Base, BaseModel):
1278 class UserEmailMap(Base, BaseModel):
1279 __tablename__ = 'user_email_map'
1279 __tablename__ = 'user_email_map'
1280 __table_args__ = (
1280 __table_args__ = (
1281 Index('uem_email_idx', 'email'),
1281 Index('uem_email_idx', 'email'),
1282 Index('uem_user_id_idx', 'user_id'),
1282 Index('uem_user_id_idx', 'user_id'),
1283 UniqueConstraint('email'),
1283 UniqueConstraint('email'),
1284 base_table_args
1284 base_table_args
1285 )
1285 )
1286
1286
1287 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1287 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1288 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1288 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1289 _email = Column("email", String(255), nullable=True, unique=False, default=None)
1289 _email = Column("email", String(255), nullable=True, unique=False, default=None)
1290 user = relationship('User', lazy='joined', back_populates='user_emails')
1290 user = relationship('User', lazy='joined', back_populates='user_emails')
1291
1291
1292 @validates('_email')
1292 @validates('_email')
1293 def validate_email(self, key, email):
1293 def validate_email(self, key, email):
1294 # check if this email is not main one
1294 # check if this email is not main one
1295 main_email = Session().query(User).filter(User.email == email).scalar()
1295 main_email = Session().query(User).filter(User.email == email).scalar()
1296 if main_email is not None:
1296 if main_email is not None:
1297 raise AttributeError('email %s is present is user table' % email)
1297 raise AttributeError('email %s is present is user table' % email)
1298 return email
1298 return email
1299
1299
1300 @hybrid_property
1300 @hybrid_property
1301 def email(self):
1301 def email(self):
1302 return self._email
1302 return self._email
1303
1303
1304 @email.setter
1304 @email.setter
1305 def email(self, val):
1305 def email(self, val):
1306 self._email = val.lower() if val else None
1306 self._email = val.lower() if val else None
1307
1307
1308
1308
1309 class UserIpMap(Base, BaseModel):
1309 class UserIpMap(Base, BaseModel):
1310 __tablename__ = 'user_ip_map'
1310 __tablename__ = 'user_ip_map'
1311 __table_args__ = (
1311 __table_args__ = (
1312 UniqueConstraint('user_id', 'ip_addr'),
1312 UniqueConstraint('user_id', 'ip_addr'),
1313 base_table_args
1313 base_table_args
1314 )
1314 )
1315
1315
1316 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1316 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1317 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1317 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1318 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
1318 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
1319 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
1319 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
1320 description = Column("description", String(10000), nullable=True, unique=None, default=None)
1320 description = Column("description", String(10000), nullable=True, unique=None, default=None)
1321 user = relationship('User', lazy='joined', back_populates='user_ip_map')
1321 user = relationship('User', lazy='joined', back_populates='user_ip_map')
1322
1322
1323 @hybrid_property
1323 @hybrid_property
1324 def description_safe(self):
1324 def description_safe(self):
1325 from rhodecode.lib import helpers as h
1325 from rhodecode.lib import helpers as h
1326 return h.escape(self.description)
1326 return h.escape(self.description)
1327
1327
1328 @classmethod
1328 @classmethod
1329 def _get_ip_range(cls, ip_addr):
1329 def _get_ip_range(cls, ip_addr):
1330 net = ipaddress.ip_network(safe_str(ip_addr), strict=False)
1330 net = ipaddress.ip_network(safe_str(ip_addr), strict=False)
1331 return [str(net.network_address), str(net.broadcast_address)]
1331 return [str(net.network_address), str(net.broadcast_address)]
1332
1332
1333 def __json__(self):
1333 def __json__(self):
1334 return {
1334 return {
1335 'ip_addr': self.ip_addr,
1335 'ip_addr': self.ip_addr,
1336 'ip_range': self._get_ip_range(self.ip_addr),
1336 'ip_range': self._get_ip_range(self.ip_addr),
1337 }
1337 }
1338
1338
1339 def __repr__(self):
1339 def __repr__(self):
1340 return f"<{self.cls_name}('user_id={self.user_id} => ip={self.ip_addr}')>"
1340 return f"<{self.cls_name}('user_id={self.user_id} => ip={self.ip_addr}')>"
1341
1341
1342
1342
1343 class UserSshKeys(Base, BaseModel):
1343 class UserSshKeys(Base, BaseModel):
1344 __tablename__ = 'user_ssh_keys'
1344 __tablename__ = 'user_ssh_keys'
1345 __table_args__ = (
1345 __table_args__ = (
1346 Index('usk_ssh_key_fingerprint_idx', 'ssh_key_fingerprint'),
1346 Index('usk_ssh_key_fingerprint_idx', 'ssh_key_fingerprint'),
1347
1347
1348 UniqueConstraint('ssh_key_fingerprint'),
1348 UniqueConstraint('ssh_key_fingerprint'),
1349
1349
1350 base_table_args
1350 base_table_args
1351 )
1351 )
1352
1352
1353 ssh_key_id = Column('ssh_key_id', Integer(), nullable=False, unique=True, default=None, primary_key=True)
1353 ssh_key_id = Column('ssh_key_id', Integer(), nullable=False, unique=True, default=None, primary_key=True)
1354 ssh_key_data = Column('ssh_key_data', String(10240), nullable=False, unique=None, default=None)
1354 ssh_key_data = Column('ssh_key_data', String(10240), nullable=False, unique=None, default=None)
1355 ssh_key_fingerprint = Column('ssh_key_fingerprint', String(255), nullable=False, unique=None, default=None)
1355 ssh_key_fingerprint = Column('ssh_key_fingerprint', String(255), nullable=False, unique=None, default=None)
1356
1356
1357 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1357 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1358
1358
1359 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1359 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1360 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True, default=None)
1360 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True, default=None)
1361 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1361 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1362
1362
1363 user = relationship('User', lazy='joined', back_populates='user_ssh_keys')
1363 user = relationship('User', lazy='joined', back_populates='user_ssh_keys')
1364
1364
1365 def __json__(self):
1365 def __json__(self):
1366 data = {
1366 data = {
1367 'ssh_fingerprint': self.ssh_key_fingerprint,
1367 'ssh_fingerprint': self.ssh_key_fingerprint,
1368 'description': self.description,
1368 'description': self.description,
1369 'created_on': self.created_on
1369 'created_on': self.created_on
1370 }
1370 }
1371 return data
1371 return data
1372
1372
1373 def get_api_data(self):
1373 def get_api_data(self):
1374 data = self.__json__()
1374 data = self.__json__()
1375 return data
1375 return data
1376
1376
1377
1377
1378 class UserLog(Base, BaseModel):
1378 class UserLog(Base, BaseModel):
1379 __tablename__ = 'user_logs'
1379 __tablename__ = 'user_logs'
1380 __table_args__ = (
1380 __table_args__ = (
1381 base_table_args,
1381 base_table_args,
1382 )
1382 )
1383
1383
1384 VERSION_1 = 'v1'
1384 VERSION_1 = 'v1'
1385 VERSION_2 = 'v2'
1385 VERSION_2 = 'v2'
1386 VERSIONS = [VERSION_1, VERSION_2]
1386 VERSIONS = [VERSION_1, VERSION_2]
1387
1387
1388 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1388 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1389 user_id = Column("user_id", Integer(), ForeignKey('users.user_id',ondelete='SET NULL'), nullable=True, unique=None, default=None)
1389 user_id = Column("user_id", Integer(), ForeignKey('users.user_id',ondelete='SET NULL'), nullable=True, unique=None, default=None)
1390 username = Column("username", String(255), nullable=True, unique=None, default=None)
1390 username = Column("username", String(255), nullable=True, unique=None, default=None)
1391 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id', ondelete='SET NULL'), nullable=True, unique=None, default=None)
1391 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id', ondelete='SET NULL'), nullable=True, unique=None, default=None)
1392 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1392 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1393 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1393 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1394 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1394 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1395 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1395 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1396
1396
1397 version = Column("version", String(255), nullable=True, default=VERSION_1)
1397 version = Column("version", String(255), nullable=True, default=VERSION_1)
1398 user_data = Column('user_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1398 user_data = Column('user_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1399 action_data = Column('action_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1399 action_data = Column('action_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1400 user = relationship('User', cascade='', back_populates='user_log')
1400 user = relationship('User', cascade='', back_populates='user_log')
1401 repository = relationship('Repository', cascade='', back_populates='logs')
1401 repository = relationship('Repository', cascade='', back_populates='logs')
1402
1402
1403 def __repr__(self):
1403 def __repr__(self):
1404 return f"<{self.cls_name}('id:{self.repository_name}:{self.action}')>"
1404 return f"<{self.cls_name}('id:{self.repository_name}:{self.action}')>"
1405
1405
1406 def __json__(self):
1406 def __json__(self):
1407 return {
1407 return {
1408 'user_id': self.user_id,
1408 'user_id': self.user_id,
1409 'username': self.username,
1409 'username': self.username,
1410 'repository_id': self.repository_id,
1410 'repository_id': self.repository_id,
1411 'repository_name': self.repository_name,
1411 'repository_name': self.repository_name,
1412 'user_ip': self.user_ip,
1412 'user_ip': self.user_ip,
1413 'action_date': self.action_date,
1413 'action_date': self.action_date,
1414 'action': self.action,
1414 'action': self.action,
1415 }
1415 }
1416
1416
1417 @hybrid_property
1417 @hybrid_property
1418 def entry_id(self):
1418 def entry_id(self):
1419 return self.user_log_id
1419 return self.user_log_id
1420
1420
1421 @property
1421 @property
1422 def action_as_day(self):
1422 def action_as_day(self):
1423 return datetime.date(*self.action_date.timetuple()[:3])
1423 return datetime.date(*self.action_date.timetuple()[:3])
1424
1424
1425
1425
1426 class UserGroup(Base, BaseModel):
1426 class UserGroup(Base, BaseModel):
1427 __tablename__ = 'users_groups'
1427 __tablename__ = 'users_groups'
1428 __table_args__ = (
1428 __table_args__ = (
1429 base_table_args,
1429 base_table_args,
1430 )
1430 )
1431
1431
1432 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1432 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1433 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1433 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1434 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1434 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1435 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1435 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1436 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1436 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1437 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1437 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1438 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1438 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1439 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1439 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1440
1440
1441 members = relationship('UserGroupMember', cascade="all, delete-orphan", lazy="joined", back_populates='users_group')
1441 members = relationship('UserGroupMember', cascade="all, delete-orphan", lazy="joined", back_populates='users_group')
1442 users_group_to_perm = relationship('UserGroupToPerm', cascade='all', back_populates='users_group')
1442 users_group_to_perm = relationship('UserGroupToPerm', cascade='all', back_populates='users_group')
1443 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all', back_populates='users_group')
1443 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all', back_populates='users_group')
1444 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all', back_populates='users_group')
1444 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all', back_populates='users_group')
1445 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all', back_populates='user_group')
1445 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all', back_populates='user_group')
1446
1446
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')
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 user_group_review_rules = relationship('RepoReviewRuleUserGroup', cascade='all', back_populates='users_group')
1449 user_group_review_rules = relationship('RepoReviewRuleUserGroup', cascade='all', back_populates='users_group')
1450 user = relationship('User', primaryjoin="User.user_id==UserGroup.user_id", back_populates='user_groups')
1450 user = relationship('User', primaryjoin="User.user_id==UserGroup.user_id", back_populates='user_groups')
1451
1451
1452 @classmethod
1452 @classmethod
1453 def _load_group_data(cls, column):
1453 def _load_group_data(cls, column):
1454 if not column:
1454 if not column:
1455 return {}
1455 return {}
1456
1456
1457 try:
1457 try:
1458 return json.loads(column) or {}
1458 return json.loads(column) or {}
1459 except TypeError:
1459 except TypeError:
1460 return {}
1460 return {}
1461
1461
1462 @hybrid_property
1462 @hybrid_property
1463 def description_safe(self):
1463 def description_safe(self):
1464 from rhodecode.lib import helpers as h
1464 from rhodecode.lib import helpers as h
1465 return h.escape(self.user_group_description)
1465 return h.escape(self.user_group_description)
1466
1466
1467 @hybrid_property
1467 @hybrid_property
1468 def group_data(self):
1468 def group_data(self):
1469 return self._load_group_data(self._group_data)
1469 return self._load_group_data(self._group_data)
1470
1470
1471 @group_data.expression
1471 @group_data.expression
1472 def group_data(self, **kwargs):
1472 def group_data(self, **kwargs):
1473 return self._group_data
1473 return self._group_data
1474
1474
1475 @group_data.setter
1475 @group_data.setter
1476 def group_data(self, val):
1476 def group_data(self, val):
1477 try:
1477 try:
1478 self._group_data = json.dumps(val)
1478 self._group_data = json.dumps(val)
1479 except Exception:
1479 except Exception:
1480 log.error(traceback.format_exc())
1480 log.error(traceback.format_exc())
1481
1481
1482 @classmethod
1482 @classmethod
1483 def _load_sync(cls, group_data):
1483 def _load_sync(cls, group_data):
1484 if group_data:
1484 if group_data:
1485 return group_data.get('extern_type')
1485 return group_data.get('extern_type')
1486
1486
1487 @property
1487 @property
1488 def sync(self):
1488 def sync(self):
1489 return self._load_sync(self.group_data)
1489 return self._load_sync(self.group_data)
1490
1490
1491 def __repr__(self):
1491 def __repr__(self):
1492 return f"<{self.cls_name}('id:{self.users_group_id}:{self.users_group_name}')>"
1492 return f"<{self.cls_name}('id:{self.users_group_id}:{self.users_group_name}')>"
1493
1493
1494 @classmethod
1494 @classmethod
1495 def get_by_group_name(cls, group_name, cache=False,
1495 def get_by_group_name(cls, group_name, cache=False,
1496 case_insensitive=False):
1496 case_insensitive=False):
1497 if case_insensitive:
1497 if case_insensitive:
1498 q = cls.query().filter(func.lower(cls.users_group_name) ==
1498 q = cls.query().filter(func.lower(cls.users_group_name) ==
1499 func.lower(group_name))
1499 func.lower(group_name))
1500
1500
1501 else:
1501 else:
1502 q = cls.query().filter(cls.users_group_name == group_name)
1502 q = cls.query().filter(cls.users_group_name == group_name)
1503 if cache:
1503 if cache:
1504 name_key = _hash_key(group_name)
1504 name_key = _hash_key(group_name)
1505 q = q.options(
1505 q = q.options(
1506 FromCache("sql_cache_short", f"get_group_{name_key}"))
1506 FromCache("sql_cache_short", f"get_group_{name_key}"))
1507 return q.scalar()
1507 return q.scalar()
1508
1508
1509 @classmethod
1509 @classmethod
1510 def get(cls, user_group_id, cache=False):
1510 def get(cls, user_group_id, cache=False):
1511 if not user_group_id:
1511 if not user_group_id:
1512 return
1512 return
1513
1513
1514 user_group = cls.query()
1514 user_group = cls.query()
1515 if cache:
1515 if cache:
1516 user_group = user_group.options(
1516 user_group = user_group.options(
1517 FromCache("sql_cache_short", "get_users_group_%s" % user_group_id))
1517 FromCache("sql_cache_short", "get_users_group_%s" % user_group_id))
1518 return user_group.get(user_group_id)
1518 return user_group.get(user_group_id)
1519
1519
1520 def permissions(self, with_admins=True, with_owner=True,
1520 def permissions(self, with_admins=True, with_owner=True,
1521 expand_from_user_groups=False):
1521 expand_from_user_groups=False):
1522 """
1522 """
1523 Permissions for user groups
1523 Permissions for user groups
1524 """
1524 """
1525 _admin_perm = 'usergroup.admin'
1525 _admin_perm = 'usergroup.admin'
1526
1526
1527 owner_row = []
1527 owner_row = []
1528 if with_owner:
1528 if with_owner:
1529 usr = AttributeDict(self.user.get_dict())
1529 usr = AttributeDict(self.user.get_dict())
1530 usr.owner_row = True
1530 usr.owner_row = True
1531 usr.permission = _admin_perm
1531 usr.permission = _admin_perm
1532 owner_row.append(usr)
1532 owner_row.append(usr)
1533
1533
1534 super_admin_ids = []
1534 super_admin_ids = []
1535 super_admin_rows = []
1535 super_admin_rows = []
1536 if with_admins:
1536 if with_admins:
1537 for usr in User.get_all_super_admins():
1537 for usr in User.get_all_super_admins():
1538 super_admin_ids.append(usr.user_id)
1538 super_admin_ids.append(usr.user_id)
1539 # if this admin is also owner, don't double the record
1539 # if this admin is also owner, don't double the record
1540 if usr.user_id == owner_row[0].user_id:
1540 if usr.user_id == owner_row[0].user_id:
1541 owner_row[0].admin_row = True
1541 owner_row[0].admin_row = True
1542 else:
1542 else:
1543 usr = AttributeDict(usr.get_dict())
1543 usr = AttributeDict(usr.get_dict())
1544 usr.admin_row = True
1544 usr.admin_row = True
1545 usr.permission = _admin_perm
1545 usr.permission = _admin_perm
1546 super_admin_rows.append(usr)
1546 super_admin_rows.append(usr)
1547
1547
1548 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1548 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1549 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1549 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1550 joinedload(UserUserGroupToPerm.user),
1550 joinedload(UserUserGroupToPerm.user),
1551 joinedload(UserUserGroupToPerm.permission),)
1551 joinedload(UserUserGroupToPerm.permission),)
1552
1552
1553 # get owners and admins and permissions. We do a trick of re-writing
1553 # get owners and admins and permissions. We do a trick of re-writing
1554 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1554 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1555 # has a global reference and changing one object propagates to all
1555 # has a global reference and changing one object propagates to all
1556 # others. This means if admin is also an owner admin_row that change
1556 # others. This means if admin is also an owner admin_row that change
1557 # would propagate to both objects
1557 # would propagate to both objects
1558 perm_rows = []
1558 perm_rows = []
1559 for _usr in q.all():
1559 for _usr in q.all():
1560 usr = AttributeDict(_usr.user.get_dict())
1560 usr = AttributeDict(_usr.user.get_dict())
1561 # if this user is also owner/admin, mark as duplicate record
1561 # if this user is also owner/admin, mark as duplicate record
1562 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
1562 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
1563 usr.duplicate_perm = True
1563 usr.duplicate_perm = True
1564 usr.permission = _usr.permission.permission_name
1564 usr.permission = _usr.permission.permission_name
1565 perm_rows.append(usr)
1565 perm_rows.append(usr)
1566
1566
1567 # filter the perm rows by 'default' first and then sort them by
1567 # filter the perm rows by 'default' first and then sort them by
1568 # admin,write,read,none permissions sorted again alphabetically in
1568 # admin,write,read,none permissions sorted again alphabetically in
1569 # each group
1569 # each group
1570 perm_rows = sorted(perm_rows, key=display_user_sort)
1570 perm_rows = sorted(perm_rows, key=display_user_sort)
1571
1571
1572 user_groups_rows = []
1572 user_groups_rows = []
1573 if expand_from_user_groups:
1573 if expand_from_user_groups:
1574 for ug in self.permission_user_groups(with_members=True):
1574 for ug in self.permission_user_groups(with_members=True):
1575 for user_data in ug.members:
1575 for user_data in ug.members:
1576 user_groups_rows.append(user_data)
1576 user_groups_rows.append(user_data)
1577
1577
1578 return super_admin_rows + owner_row + perm_rows + user_groups_rows
1578 return super_admin_rows + owner_row + perm_rows + user_groups_rows
1579
1579
1580 def permission_user_groups(self, with_members=False):
1580 def permission_user_groups(self, with_members=False):
1581 q = UserGroupUserGroupToPerm.query()\
1581 q = UserGroupUserGroupToPerm.query()\
1582 .filter(UserGroupUserGroupToPerm.target_user_group == self)
1582 .filter(UserGroupUserGroupToPerm.target_user_group == self)
1583 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1583 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1584 joinedload(UserGroupUserGroupToPerm.target_user_group),
1584 joinedload(UserGroupUserGroupToPerm.target_user_group),
1585 joinedload(UserGroupUserGroupToPerm.permission),)
1585 joinedload(UserGroupUserGroupToPerm.permission),)
1586
1586
1587 perm_rows = []
1587 perm_rows = []
1588 for _user_group in q.all():
1588 for _user_group in q.all():
1589 entry = AttributeDict(_user_group.user_group.get_dict())
1589 entry = AttributeDict(_user_group.user_group.get_dict())
1590 entry.permission = _user_group.permission.permission_name
1590 entry.permission = _user_group.permission.permission_name
1591 if with_members:
1591 if with_members:
1592 entry.members = [x.user.get_dict()
1592 entry.members = [x.user.get_dict()
1593 for x in _user_group.user_group.members]
1593 for x in _user_group.user_group.members]
1594 perm_rows.append(entry)
1594 perm_rows.append(entry)
1595
1595
1596 perm_rows = sorted(perm_rows, key=display_user_group_sort)
1596 perm_rows = sorted(perm_rows, key=display_user_group_sort)
1597 return perm_rows
1597 return perm_rows
1598
1598
1599 def _get_default_perms(self, user_group, suffix=''):
1599 def _get_default_perms(self, user_group, suffix=''):
1600 from rhodecode.model.permission import PermissionModel
1600 from rhodecode.model.permission import PermissionModel
1601 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1601 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1602
1602
1603 def get_default_perms(self, suffix=''):
1603 def get_default_perms(self, suffix=''):
1604 return self._get_default_perms(self, suffix)
1604 return self._get_default_perms(self, suffix)
1605
1605
1606 def get_api_data(self, with_group_members=True, include_secrets=False):
1606 def get_api_data(self, with_group_members=True, include_secrets=False):
1607 """
1607 """
1608 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1608 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1609 basically forwarded.
1609 basically forwarded.
1610
1610
1611 """
1611 """
1612 user_group = self
1612 user_group = self
1613 data = {
1613 data = {
1614 'users_group_id': user_group.users_group_id,
1614 'users_group_id': user_group.users_group_id,
1615 'group_name': user_group.users_group_name,
1615 'group_name': user_group.users_group_name,
1616 'group_description': user_group.user_group_description,
1616 'group_description': user_group.user_group_description,
1617 'active': user_group.users_group_active,
1617 'active': user_group.users_group_active,
1618 'owner': user_group.user.username,
1618 'owner': user_group.user.username,
1619 'sync': user_group.sync,
1619 'sync': user_group.sync,
1620 'owner_email': user_group.user.email,
1620 'owner_email': user_group.user.email,
1621 }
1621 }
1622
1622
1623 if with_group_members:
1623 if with_group_members:
1624 users = []
1624 users = []
1625 for user in user_group.members:
1625 for user in user_group.members:
1626 user = user.user
1626 user = user.user
1627 users.append(user.get_api_data(include_secrets=include_secrets))
1627 users.append(user.get_api_data(include_secrets=include_secrets))
1628 data['users'] = users
1628 data['users'] = users
1629
1629
1630 return data
1630 return data
1631
1631
1632
1632
1633 class UserGroupMember(Base, BaseModel):
1633 class UserGroupMember(Base, BaseModel):
1634 __tablename__ = 'users_groups_members'
1634 __tablename__ = 'users_groups_members'
1635 __table_args__ = (
1635 __table_args__ = (
1636 base_table_args,
1636 base_table_args,
1637 )
1637 )
1638
1638
1639 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1639 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1640 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1640 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1641 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1641 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1642
1642
1643 user = relationship('User', lazy='joined', back_populates='group_member')
1643 user = relationship('User', lazy='joined', back_populates='group_member')
1644 users_group = relationship('UserGroup', back_populates='members')
1644 users_group = relationship('UserGroup', back_populates='members')
1645
1645
1646 def __init__(self, gr_id='', u_id=''):
1646 def __init__(self, gr_id='', u_id=''):
1647 self.users_group_id = gr_id
1647 self.users_group_id = gr_id
1648 self.user_id = u_id
1648 self.user_id = u_id
1649
1649
1650
1650
1651 class RepositoryField(Base, BaseModel):
1651 class RepositoryField(Base, BaseModel):
1652 __tablename__ = 'repositories_fields'
1652 __tablename__ = 'repositories_fields'
1653 __table_args__ = (
1653 __table_args__ = (
1654 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1654 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1655 base_table_args,
1655 base_table_args,
1656 )
1656 )
1657
1657
1658 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1658 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1659
1659
1660 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1660 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1661 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1661 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1662 field_key = Column("field_key", String(250))
1662 field_key = Column("field_key", String(250))
1663 field_label = Column("field_label", String(1024), nullable=False)
1663 field_label = Column("field_label", String(1024), nullable=False)
1664 field_value = Column("field_value", String(10000), nullable=False)
1664 field_value = Column("field_value", String(10000), nullable=False)
1665 field_desc = Column("field_desc", String(1024), nullable=False)
1665 field_desc = Column("field_desc", String(1024), nullable=False)
1666 field_type = Column("field_type", String(255), nullable=False, unique=None)
1666 field_type = Column("field_type", String(255), nullable=False, unique=None)
1667 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1667 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1668
1668
1669 repository = relationship('Repository', back_populates='extra_fields')
1669 repository = relationship('Repository', back_populates='extra_fields')
1670
1670
1671 @property
1671 @property
1672 def field_key_prefixed(self):
1672 def field_key_prefixed(self):
1673 return 'ex_%s' % self.field_key
1673 return 'ex_%s' % self.field_key
1674
1674
1675 @classmethod
1675 @classmethod
1676 def un_prefix_key(cls, key):
1676 def un_prefix_key(cls, key):
1677 if key.startswith(cls.PREFIX):
1677 if key.startswith(cls.PREFIX):
1678 return key[len(cls.PREFIX):]
1678 return key[len(cls.PREFIX):]
1679 return key
1679 return key
1680
1680
1681 @classmethod
1681 @classmethod
1682 def get_by_key_name(cls, key, repo):
1682 def get_by_key_name(cls, key, repo):
1683 row = cls.query()\
1683 row = cls.query()\
1684 .filter(cls.repository == repo)\
1684 .filter(cls.repository == repo)\
1685 .filter(cls.field_key == key).scalar()
1685 .filter(cls.field_key == key).scalar()
1686 return row
1686 return row
1687
1687
1688
1688
1689 class Repository(Base, BaseModel):
1689 class Repository(Base, BaseModel):
1690 __tablename__ = 'repositories'
1690 __tablename__ = 'repositories'
1691 __table_args__ = (
1691 __table_args__ = (
1692 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1692 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1693 base_table_args,
1693 base_table_args,
1694 )
1694 )
1695 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1695 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1696 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1696 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1697 DEFAULT_CLONE_URI_SSH = 'ssh://{sys_user}@{hostname}/{repo}'
1697 DEFAULT_CLONE_URI_SSH = 'ssh://{sys_user}@{hostname}/{repo}'
1698
1698
1699 STATE_CREATED = 'repo_state_created'
1699 STATE_CREATED = 'repo_state_created'
1700 STATE_PENDING = 'repo_state_pending'
1700 STATE_PENDING = 'repo_state_pending'
1701 STATE_ERROR = 'repo_state_error'
1701 STATE_ERROR = 'repo_state_error'
1702
1702
1703 LOCK_AUTOMATIC = 'lock_auto'
1703 LOCK_AUTOMATIC = 'lock_auto'
1704 LOCK_API = 'lock_api'
1704 LOCK_API = 'lock_api'
1705 LOCK_WEB = 'lock_web'
1705 LOCK_WEB = 'lock_web'
1706 LOCK_PULL = 'lock_pull'
1706 LOCK_PULL = 'lock_pull'
1707
1707
1708 NAME_SEP = URL_SEP
1708 NAME_SEP = URL_SEP
1709
1709
1710 repo_id = Column(
1710 repo_id = Column(
1711 "repo_id", Integer(), nullable=False, unique=True, default=None,
1711 "repo_id", Integer(), nullable=False, unique=True, default=None,
1712 primary_key=True)
1712 primary_key=True)
1713 _repo_name = Column(
1713 _repo_name = Column(
1714 "repo_name", Text(), nullable=False, default=None)
1714 "repo_name", Text(), nullable=False, default=None)
1715 repo_name_hash = Column(
1715 repo_name_hash = Column(
1716 "repo_name_hash", String(255), nullable=False, unique=True)
1716 "repo_name_hash", String(255), nullable=False, unique=True)
1717 repo_state = Column("repo_state", String(255), nullable=True)
1717 repo_state = Column("repo_state", String(255), nullable=True)
1718
1718
1719 clone_uri = Column(
1719 clone_uri = Column(
1720 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1720 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1721 default=None)
1721 default=None)
1722 push_uri = Column(
1722 push_uri = Column(
1723 "push_uri", EncryptedTextValue(), nullable=True, unique=False,
1723 "push_uri", EncryptedTextValue(), nullable=True, unique=False,
1724 default=None)
1724 default=None)
1725 repo_type = Column(
1725 repo_type = Column(
1726 "repo_type", String(255), nullable=False, unique=False, default=None)
1726 "repo_type", String(255), nullable=False, unique=False, default=None)
1727 user_id = Column(
1727 user_id = Column(
1728 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1728 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1729 unique=False, default=None)
1729 unique=False, default=None)
1730 private = Column(
1730 private = Column(
1731 "private", Boolean(), nullable=True, unique=None, default=None)
1731 "private", Boolean(), nullable=True, unique=None, default=None)
1732 archived = Column(
1732 archived = Column(
1733 "archived", Boolean(), nullable=True, unique=None, default=None)
1733 "archived", Boolean(), nullable=True, unique=None, default=None)
1734 enable_statistics = Column(
1734 enable_statistics = Column(
1735 "statistics", Boolean(), nullable=True, unique=None, default=True)
1735 "statistics", Boolean(), nullable=True, unique=None, default=True)
1736 enable_downloads = Column(
1736 enable_downloads = Column(
1737 "downloads", Boolean(), nullable=True, unique=None, default=True)
1737 "downloads", Boolean(), nullable=True, unique=None, default=True)
1738 description = Column(
1738 description = Column(
1739 "description", String(10000), nullable=True, unique=None, default=None)
1739 "description", String(10000), nullable=True, unique=None, default=None)
1740 created_on = Column(
1740 created_on = Column(
1741 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1741 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1742 default=datetime.datetime.now)
1742 default=datetime.datetime.now)
1743 updated_on = Column(
1743 updated_on = Column(
1744 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1744 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1745 default=datetime.datetime.now)
1745 default=datetime.datetime.now)
1746 _landing_revision = Column(
1746 _landing_revision = Column(
1747 "landing_revision", String(255), nullable=False, unique=False,
1747 "landing_revision", String(255), nullable=False, unique=False,
1748 default=None)
1748 default=None)
1749 enable_locking = Column(
1749 enable_locking = Column(
1750 "enable_locking", Boolean(), nullable=False, unique=None,
1750 "enable_locking", Boolean(), nullable=False, unique=None,
1751 default=False)
1751 default=False)
1752 _locked = Column(
1752 _locked = Column(
1753 "locked", String(255), nullable=True, unique=False, default=None)
1753 "locked", String(255), nullable=True, unique=False, default=None)
1754 _changeset_cache = Column(
1754 _changeset_cache = Column(
1755 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1755 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1756
1756
1757 fork_id = Column(
1757 fork_id = Column(
1758 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1758 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1759 nullable=True, unique=False, default=None)
1759 nullable=True, unique=False, default=None)
1760 group_id = Column(
1760 group_id = Column(
1761 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1761 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1762 unique=False, default=None)
1762 unique=False, default=None)
1763
1763
1764 user = relationship('User', lazy='joined', back_populates='repositories')
1764 user = relationship('User', lazy='joined', back_populates='repositories')
1765 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1765 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1766 group = relationship('RepoGroup', lazy='joined')
1766 group = relationship('RepoGroup', lazy='joined')
1767 repo_to_perm = relationship('UserRepoToPerm', cascade='all', order_by='UserRepoToPerm.repo_to_perm_id')
1767 repo_to_perm = relationship('UserRepoToPerm', cascade='all', order_by='UserRepoToPerm.repo_to_perm_id')
1768 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all', back_populates='repository')
1768 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all', back_populates='repository')
1769 stats = relationship('Statistics', cascade='all', uselist=False)
1769 stats = relationship('Statistics', cascade='all', uselist=False)
1770
1770
1771 followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id', cascade='all', back_populates='follows_repository')
1771 followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id', cascade='all', back_populates='follows_repository')
1772 extra_fields = relationship('RepositoryField', cascade="all, delete-orphan", back_populates='repository')
1772 extra_fields = relationship('RepositoryField', cascade="all, delete-orphan", back_populates='repository')
1773
1773
1774 logs = relationship('UserLog', back_populates='repository')
1774 logs = relationship('UserLog', back_populates='repository')
1775
1775
1776 comments = relationship('ChangesetComment', cascade="all, delete-orphan", back_populates='repo')
1776 comments = relationship('ChangesetComment', cascade="all, delete-orphan", back_populates='repo')
1777
1777
1778 pull_requests_source = relationship(
1778 pull_requests_source = relationship(
1779 'PullRequest',
1779 'PullRequest',
1780 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1780 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1781 cascade="all, delete-orphan",
1781 cascade="all, delete-orphan",
1782 #back_populates="pr_source"
1782 #back_populates="pr_source"
1783 )
1783 )
1784 pull_requests_target = relationship(
1784 pull_requests_target = relationship(
1785 'PullRequest',
1785 'PullRequest',
1786 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1786 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1787 cascade="all, delete-orphan",
1787 cascade="all, delete-orphan",
1788 #back_populates="pr_target"
1788 #back_populates="pr_target"
1789 )
1789 )
1790
1790
1791 ui = relationship('RepoRhodeCodeUi', cascade="all")
1791 ui = relationship('RepoRhodeCodeUi', cascade="all")
1792 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1792 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1793 integrations = relationship('Integration', cascade="all, delete-orphan", back_populates='repo')
1793 integrations = relationship('Integration', cascade="all, delete-orphan", back_populates='repo')
1794
1794
1795 scoped_tokens = relationship('UserApiKeys', cascade="all", back_populates='repo')
1795 scoped_tokens = relationship('UserApiKeys', cascade="all", back_populates='repo')
1796
1796
1797 # no cascade, set NULL
1797 # no cascade, set NULL
1798 artifacts = relationship('FileStore', primaryjoin='FileStore.scope_repo_id==Repository.repo_id', viewonly=True)
1798 artifacts = relationship('FileStore', primaryjoin='FileStore.scope_repo_id==Repository.repo_id', viewonly=True)
1799
1799
1800 review_rules = relationship('RepoReviewRule')
1800 review_rules = relationship('RepoReviewRule')
1801 user_branch_perms = relationship('UserToRepoBranchPermission')
1801 user_branch_perms = relationship('UserToRepoBranchPermission')
1802 user_group_branch_perms = relationship('UserGroupToRepoBranchPermission')
1802 user_group_branch_perms = relationship('UserGroupToRepoBranchPermission')
1803
1803
1804 def __repr__(self):
1804 def __repr__(self):
1805 return "<%s('%s:%s')>" % (self.cls_name, self.repo_id, self.repo_name)
1805 return "<%s('%s:%s')>" % (self.cls_name, self.repo_id, self.repo_name)
1806
1806
1807 @hybrid_property
1807 @hybrid_property
1808 def description_safe(self):
1808 def description_safe(self):
1809 from rhodecode.lib import helpers as h
1809 from rhodecode.lib import helpers as h
1810 return h.escape(self.description)
1810 return h.escape(self.description)
1811
1811
1812 @hybrid_property
1812 @hybrid_property
1813 def landing_rev(self):
1813 def landing_rev(self):
1814 # always should return [rev_type, rev], e.g ['branch', 'master']
1814 # always should return [rev_type, rev], e.g ['branch', 'master']
1815 if self._landing_revision:
1815 if self._landing_revision:
1816 _rev_info = self._landing_revision.split(':')
1816 _rev_info = self._landing_revision.split(':')
1817 if len(_rev_info) < 2:
1817 if len(_rev_info) < 2:
1818 _rev_info.insert(0, 'rev')
1818 _rev_info.insert(0, 'rev')
1819 return [_rev_info[0], _rev_info[1]]
1819 return [_rev_info[0], _rev_info[1]]
1820 return [None, None]
1820 return [None, None]
1821
1821
1822 @property
1822 @property
1823 def landing_ref_type(self):
1823 def landing_ref_type(self):
1824 return self.landing_rev[0]
1824 return self.landing_rev[0]
1825
1825
1826 @property
1826 @property
1827 def landing_ref_name(self):
1827 def landing_ref_name(self):
1828 return self.landing_rev[1]
1828 return self.landing_rev[1]
1829
1829
1830 @landing_rev.setter
1830 @landing_rev.setter
1831 def landing_rev(self, val):
1831 def landing_rev(self, val):
1832 if ':' not in val:
1832 if ':' not in val:
1833 raise ValueError('value must be delimited with `:` and consist '
1833 raise ValueError('value must be delimited with `:` and consist '
1834 'of <rev_type>:<rev>, got %s instead' % val)
1834 'of <rev_type>:<rev>, got %s instead' % val)
1835 self._landing_revision = val
1835 self._landing_revision = val
1836
1836
1837 @hybrid_property
1837 @hybrid_property
1838 def locked(self):
1838 def locked(self):
1839 if self._locked:
1839 if self._locked:
1840 user_id, timelocked, reason = self._locked.split(':')
1840 user_id, timelocked, reason = self._locked.split(':')
1841 lock_values = int(user_id), timelocked, reason
1841 lock_values = int(user_id), timelocked, reason
1842 else:
1842 else:
1843 lock_values = [None, None, None]
1843 lock_values = [None, None, None]
1844 return lock_values
1844 return lock_values
1845
1845
1846 @locked.setter
1846 @locked.setter
1847 def locked(self, val):
1847 def locked(self, val):
1848 if val and isinstance(val, (list, tuple)):
1848 if val and isinstance(val, (list, tuple)):
1849 self._locked = ':'.join(map(str, val))
1849 self._locked = ':'.join(map(str, val))
1850 else:
1850 else:
1851 self._locked = None
1851 self._locked = None
1852
1852
1853 @classmethod
1853 @classmethod
1854 def _load_changeset_cache(cls, repo_id, changeset_cache_raw):
1854 def _load_changeset_cache(cls, repo_id, changeset_cache_raw):
1855 from rhodecode.lib.vcs.backends.base import EmptyCommit
1855 from rhodecode.lib.vcs.backends.base import EmptyCommit
1856 dummy = EmptyCommit().__json__()
1856 dummy = EmptyCommit().__json__()
1857 if not changeset_cache_raw:
1857 if not changeset_cache_raw:
1858 dummy['source_repo_id'] = repo_id
1858 dummy['source_repo_id'] = repo_id
1859 return json.loads(json.dumps(dummy))
1859 return json.loads(json.dumps(dummy))
1860
1860
1861 try:
1861 try:
1862 return json.loads(changeset_cache_raw)
1862 return json.loads(changeset_cache_raw)
1863 except TypeError:
1863 except TypeError:
1864 return dummy
1864 return dummy
1865 except Exception:
1865 except Exception:
1866 log.error(traceback.format_exc())
1866 log.error(traceback.format_exc())
1867 return dummy
1867 return dummy
1868
1868
1869 @hybrid_property
1869 @hybrid_property
1870 def changeset_cache(self):
1870 def changeset_cache(self):
1871 return self._load_changeset_cache(self.repo_id, self._changeset_cache)
1871 return self._load_changeset_cache(self.repo_id, self._changeset_cache)
1872
1872
1873 @changeset_cache.setter
1873 @changeset_cache.setter
1874 def changeset_cache(self, val):
1874 def changeset_cache(self, val):
1875 try:
1875 try:
1876 self._changeset_cache = json.dumps(val)
1876 self._changeset_cache = json.dumps(val)
1877 except Exception:
1877 except Exception:
1878 log.error(traceback.format_exc())
1878 log.error(traceback.format_exc())
1879
1879
1880 @hybrid_property
1880 @hybrid_property
1881 def repo_name(self):
1881 def repo_name(self):
1882 return self._repo_name
1882 return self._repo_name
1883
1883
1884 @repo_name.setter
1884 @repo_name.setter
1885 def repo_name(self, value):
1885 def repo_name(self, value):
1886 self._repo_name = value
1886 self._repo_name = value
1887 self.repo_name_hash = sha1(safe_bytes(value))
1887 self.repo_name_hash = sha1(safe_bytes(value))
1888
1888
1889 @classmethod
1889 @classmethod
1890 def normalize_repo_name(cls, repo_name):
1890 def normalize_repo_name(cls, repo_name):
1891 """
1891 """
1892 Normalizes os specific repo_name to the format internally stored inside
1892 Normalizes os specific repo_name to the format internally stored inside
1893 database using URL_SEP
1893 database using URL_SEP
1894
1894
1895 :param cls:
1895 :param cls:
1896 :param repo_name:
1896 :param repo_name:
1897 """
1897 """
1898 return cls.NAME_SEP.join(repo_name.split(os.sep))
1898 return cls.NAME_SEP.join(repo_name.split(os.sep))
1899
1899
1900 @classmethod
1900 @classmethod
1901 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1901 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1902 session = Session()
1902 session = Session()
1903 q = session.query(cls).filter(cls.repo_name == repo_name)
1903 q = session.query(cls).filter(cls.repo_name == repo_name)
1904
1904
1905 if cache:
1905 if cache:
1906 if identity_cache:
1906 if identity_cache:
1907 val = cls.identity_cache(session, 'repo_name', repo_name)
1907 val = cls.identity_cache(session, 'repo_name', repo_name)
1908 if val:
1908 if val:
1909 return val
1909 return val
1910 else:
1910 else:
1911 cache_key = "get_repo_by_name_%s" % _hash_key(repo_name)
1911 cache_key = "get_repo_by_name_%s" % _hash_key(repo_name)
1912 q = q.options(
1912 q = q.options(
1913 FromCache("sql_cache_short", cache_key))
1913 FromCache("sql_cache_short", cache_key))
1914
1914
1915 return q.scalar()
1915 return q.scalar()
1916
1916
1917 @classmethod
1917 @classmethod
1918 def get_by_id_or_repo_name(cls, repoid):
1918 def get_by_id_or_repo_name(cls, repoid):
1919 if isinstance(repoid, int):
1919 if isinstance(repoid, int):
1920 try:
1920 try:
1921 repo = cls.get(repoid)
1921 repo = cls.get(repoid)
1922 except ValueError:
1922 except ValueError:
1923 repo = None
1923 repo = None
1924 else:
1924 else:
1925 repo = cls.get_by_repo_name(repoid)
1925 repo = cls.get_by_repo_name(repoid)
1926 return repo
1926 return repo
1927
1927
1928 @classmethod
1928 @classmethod
1929 def get_by_full_path(cls, repo_full_path):
1929 def get_by_full_path(cls, repo_full_path):
1930 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1930 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1931 repo_name = cls.normalize_repo_name(repo_name)
1931 repo_name = cls.normalize_repo_name(repo_name)
1932 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1932 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1933
1933
1934 @classmethod
1934 @classmethod
1935 def get_repo_forks(cls, repo_id):
1935 def get_repo_forks(cls, repo_id):
1936 return cls.query().filter(Repository.fork_id == repo_id)
1936 return cls.query().filter(Repository.fork_id == repo_id)
1937
1937
1938 @classmethod
1938 @classmethod
1939 def base_path(cls):
1939 def base_path(cls):
1940 """
1940 """
1941 Returns base path when all repos are stored
1941 Returns base path when all repos are stored
1942
1942
1943 :param cls:
1943 :param cls:
1944 """
1944 """
1945 from rhodecode.lib.utils import get_rhodecode_base_path
1945 from rhodecode.lib.utils import get_rhodecode_base_path
1946 return get_rhodecode_base_path()
1946 return get_rhodecode_base_path()
1947
1947
1948 @classmethod
1948 @classmethod
1949 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1949 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1950 case_insensitive=True, archived=False):
1950 case_insensitive=True, archived=False):
1951 q = Repository.query()
1951 q = Repository.query()
1952
1952
1953 if not archived:
1953 if not archived:
1954 q = q.filter(Repository.archived.isnot(true()))
1954 q = q.filter(Repository.archived.isnot(true()))
1955
1955
1956 if not isinstance(user_id, Optional):
1956 if not isinstance(user_id, Optional):
1957 q = q.filter(Repository.user_id == user_id)
1957 q = q.filter(Repository.user_id == user_id)
1958
1958
1959 if not isinstance(group_id, Optional):
1959 if not isinstance(group_id, Optional):
1960 q = q.filter(Repository.group_id == group_id)
1960 q = q.filter(Repository.group_id == group_id)
1961
1961
1962 if case_insensitive:
1962 if case_insensitive:
1963 q = q.order_by(func.lower(Repository.repo_name))
1963 q = q.order_by(func.lower(Repository.repo_name))
1964 else:
1964 else:
1965 q = q.order_by(Repository.repo_name)
1965 q = q.order_by(Repository.repo_name)
1966
1966
1967 return q.all()
1967 return q.all()
1968
1968
1969 @property
1969 @property
1970 def repo_uid(self):
1970 def repo_uid(self):
1971 return '_{}'.format(self.repo_id)
1971 return '_{}'.format(self.repo_id)
1972
1972
1973 @property
1973 @property
1974 def forks(self):
1974 def forks(self):
1975 """
1975 """
1976 Return forks of this repo
1976 Return forks of this repo
1977 """
1977 """
1978 return Repository.get_repo_forks(self.repo_id)
1978 return Repository.get_repo_forks(self.repo_id)
1979
1979
1980 @property
1980 @property
1981 def parent(self):
1981 def parent(self):
1982 """
1982 """
1983 Returns fork parent
1983 Returns fork parent
1984 """
1984 """
1985 return self.fork
1985 return self.fork
1986
1986
1987 @property
1987 @property
1988 def just_name(self):
1988 def just_name(self):
1989 return self.repo_name.split(self.NAME_SEP)[-1]
1989 return self.repo_name.split(self.NAME_SEP)[-1]
1990
1990
1991 @property
1991 @property
1992 def groups_with_parents(self):
1992 def groups_with_parents(self):
1993 groups = []
1993 groups = []
1994 if self.group is None:
1994 if self.group is None:
1995 return groups
1995 return groups
1996
1996
1997 cur_gr = self.group
1997 cur_gr = self.group
1998 groups.insert(0, cur_gr)
1998 groups.insert(0, cur_gr)
1999 while 1:
1999 while 1:
2000 gr = getattr(cur_gr, 'parent_group', None)
2000 gr = getattr(cur_gr, 'parent_group', None)
2001 cur_gr = cur_gr.parent_group
2001 cur_gr = cur_gr.parent_group
2002 if gr is None:
2002 if gr is None:
2003 break
2003 break
2004 groups.insert(0, gr)
2004 groups.insert(0, gr)
2005
2005
2006 return groups
2006 return groups
2007
2007
2008 @property
2008 @property
2009 def groups_and_repo(self):
2009 def groups_and_repo(self):
2010 return self.groups_with_parents, self
2010 return self.groups_with_parents, self
2011
2011
2012 @LazyProperty
2012 @LazyProperty
2013 def repo_path(self):
2013 def repo_path(self):
2014 """
2014 """
2015 Returns base full path for that repository means where it actually
2015 Returns base full path for that repository means where it actually
2016 exists on a filesystem
2016 exists on a filesystem
2017 """
2017 """
2018 q = Session().query(RhodeCodeUi).filter(
2018 q = Session().query(RhodeCodeUi).filter(
2019 RhodeCodeUi.ui_key == self.NAME_SEP)
2019 RhodeCodeUi.ui_key == self.NAME_SEP)
2020 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
2020 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
2021 return q.one().ui_value
2021 return q.one().ui_value
2022
2022
2023 @property
2023 @property
2024 def repo_full_path(self):
2024 def repo_full_path(self):
2025 p = [self.repo_path]
2025 p = [self.repo_path]
2026 # we need to split the name by / since this is how we store the
2026 # we need to split the name by / since this is how we store the
2027 # names in the database, but that eventually needs to be converted
2027 # names in the database, but that eventually needs to be converted
2028 # into a valid system path
2028 # into a valid system path
2029 p += self.repo_name.split(self.NAME_SEP)
2029 p += self.repo_name.split(self.NAME_SEP)
2030 return os.path.join(*map(safe_str, p))
2030 return os.path.join(*map(safe_str, p))
2031
2031
2032 @property
2032 @property
2033 def cache_keys(self):
2033 def cache_keys(self):
2034 """
2034 """
2035 Returns associated cache keys for that repo
2035 Returns associated cache keys for that repo
2036 """
2036 """
2037 invalidation_namespace = CacheKey.REPO_INVALIDATION_NAMESPACE.format(
2037 invalidation_namespace = CacheKey.REPO_INVALIDATION_NAMESPACE.format(
2038 repo_id=self.repo_id)
2038 repo_id=self.repo_id)
2039 return CacheKey.query()\
2039 return CacheKey.query()\
2040 .filter(CacheKey.cache_args == invalidation_namespace)\
2040 .filter(CacheKey.cache_args == invalidation_namespace)\
2041 .order_by(CacheKey.cache_key)\
2041 .order_by(CacheKey.cache_key)\
2042 .all()
2042 .all()
2043
2043
2044 @property
2044 @property
2045 def cached_diffs_relative_dir(self):
2045 def cached_diffs_relative_dir(self):
2046 """
2046 """
2047 Return a relative to the repository store path of cached diffs
2047 Return a relative to the repository store path of cached diffs
2048 used for safe display for users, who shouldn't know the absolute store
2048 used for safe display for users, who shouldn't know the absolute store
2049 path
2049 path
2050 """
2050 """
2051 return os.path.join(
2051 return os.path.join(
2052 os.path.dirname(self.repo_name),
2052 os.path.dirname(self.repo_name),
2053 self.cached_diffs_dir.split(os.path.sep)[-1])
2053 self.cached_diffs_dir.split(os.path.sep)[-1])
2054
2054
2055 @property
2055 @property
2056 def cached_diffs_dir(self):
2056 def cached_diffs_dir(self):
2057 path = self.repo_full_path
2057 path = self.repo_full_path
2058 return os.path.join(
2058 return os.path.join(
2059 os.path.dirname(path),
2059 os.path.dirname(path),
2060 f'.__shadow_diff_cache_repo_{self.repo_id}')
2060 f'.__shadow_diff_cache_repo_{self.repo_id}')
2061
2061
2062 def cached_diffs(self):
2062 def cached_diffs(self):
2063 diff_cache_dir = self.cached_diffs_dir
2063 diff_cache_dir = self.cached_diffs_dir
2064 if os.path.isdir(diff_cache_dir):
2064 if os.path.isdir(diff_cache_dir):
2065 return os.listdir(diff_cache_dir)
2065 return os.listdir(diff_cache_dir)
2066 return []
2066 return []
2067
2067
2068 def shadow_repos(self):
2068 def shadow_repos(self):
2069 shadow_repos_pattern = f'.__shadow_repo_{self.repo_id}'
2069 shadow_repos_pattern = f'.__shadow_repo_{self.repo_id}'
2070 return [
2070 return [
2071 x for x in os.listdir(os.path.dirname(self.repo_full_path))
2071 x for x in os.listdir(os.path.dirname(self.repo_full_path))
2072 if x.startswith(shadow_repos_pattern)
2072 if x.startswith(shadow_repos_pattern)
2073 ]
2073 ]
2074
2074
2075 def get_new_name(self, repo_name):
2075 def get_new_name(self, repo_name):
2076 """
2076 """
2077 returns new full repository name based on assigned group and new new
2077 returns new full repository name based on assigned group and new new
2078
2078
2079 :param repo_name:
2079 :param repo_name:
2080 """
2080 """
2081 path_prefix = self.group.full_path_splitted if self.group else []
2081 path_prefix = self.group.full_path_splitted if self.group else []
2082 return self.NAME_SEP.join(path_prefix + [repo_name])
2082 return self.NAME_SEP.join(path_prefix + [repo_name])
2083
2083
2084 @property
2084 @property
2085 def _config(self):
2085 def _config(self):
2086 """
2086 """
2087 Returns db based config object.
2087 Returns db based config object.
2088 """
2088 """
2089 from rhodecode.lib.utils import make_db_config
2089 from rhodecode.lib.utils import make_db_config
2090 return make_db_config(clear_session=False, repo=self)
2090 return make_db_config(clear_session=False, repo=self)
2091
2091
2092 def permissions(self, with_admins=True, with_owner=True,
2092 def permissions(self, with_admins=True, with_owner=True,
2093 expand_from_user_groups=False):
2093 expand_from_user_groups=False):
2094 """
2094 """
2095 Permissions for repositories
2095 Permissions for repositories
2096 """
2096 """
2097 _admin_perm = 'repository.admin'
2097 _admin_perm = 'repository.admin'
2098
2098
2099 owner_row = []
2099 owner_row = []
2100 if with_owner:
2100 if with_owner:
2101 usr = AttributeDict(self.user.get_dict())
2101 usr = AttributeDict(self.user.get_dict())
2102 usr.owner_row = True
2102 usr.owner_row = True
2103 usr.permission = _admin_perm
2103 usr.permission = _admin_perm
2104 usr.permission_id = None
2104 usr.permission_id = None
2105 owner_row.append(usr)
2105 owner_row.append(usr)
2106
2106
2107 super_admin_ids = []
2107 super_admin_ids = []
2108 super_admin_rows = []
2108 super_admin_rows = []
2109 if with_admins:
2109 if with_admins:
2110 for usr in User.get_all_super_admins():
2110 for usr in User.get_all_super_admins():
2111 super_admin_ids.append(usr.user_id)
2111 super_admin_ids.append(usr.user_id)
2112 # if this admin is also owner, don't double the record
2112 # if this admin is also owner, don't double the record
2113 if usr.user_id == owner_row[0].user_id:
2113 if usr.user_id == owner_row[0].user_id:
2114 owner_row[0].admin_row = True
2114 owner_row[0].admin_row = True
2115 else:
2115 else:
2116 usr = AttributeDict(usr.get_dict())
2116 usr = AttributeDict(usr.get_dict())
2117 usr.admin_row = True
2117 usr.admin_row = True
2118 usr.permission = _admin_perm
2118 usr.permission = _admin_perm
2119 usr.permission_id = None
2119 usr.permission_id = None
2120 super_admin_rows.append(usr)
2120 super_admin_rows.append(usr)
2121
2121
2122 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
2122 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
2123 q = q.options(joinedload(UserRepoToPerm.repository),
2123 q = q.options(joinedload(UserRepoToPerm.repository),
2124 joinedload(UserRepoToPerm.user),
2124 joinedload(UserRepoToPerm.user),
2125 joinedload(UserRepoToPerm.permission),)
2125 joinedload(UserRepoToPerm.permission),)
2126
2126
2127 # get owners and admins and permissions. We do a trick of re-writing
2127 # get owners and admins and permissions. We do a trick of re-writing
2128 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2128 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2129 # has a global reference and changing one object propagates to all
2129 # has a global reference and changing one object propagates to all
2130 # others. This means if admin is also an owner admin_row that change
2130 # others. This means if admin is also an owner admin_row that change
2131 # would propagate to both objects
2131 # would propagate to both objects
2132 perm_rows = []
2132 perm_rows = []
2133 for _usr in q.all():
2133 for _usr in q.all():
2134 usr = AttributeDict(_usr.user.get_dict())
2134 usr = AttributeDict(_usr.user.get_dict())
2135 # if this user is also owner/admin, mark as duplicate record
2135 # if this user is also owner/admin, mark as duplicate record
2136 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
2136 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
2137 usr.duplicate_perm = True
2137 usr.duplicate_perm = True
2138 # also check if this permission is maybe used by branch_permissions
2138 # also check if this permission is maybe used by branch_permissions
2139 if _usr.branch_perm_entry:
2139 if _usr.branch_perm_entry:
2140 usr.branch_rules = [x.branch_rule_id for x in _usr.branch_perm_entry]
2140 usr.branch_rules = [x.branch_rule_id for x in _usr.branch_perm_entry]
2141
2141
2142 usr.permission = _usr.permission.permission_name
2142 usr.permission = _usr.permission.permission_name
2143 usr.permission_id = _usr.repo_to_perm_id
2143 usr.permission_id = _usr.repo_to_perm_id
2144 perm_rows.append(usr)
2144 perm_rows.append(usr)
2145
2145
2146 # filter the perm rows by 'default' first and then sort them by
2146 # filter the perm rows by 'default' first and then sort them by
2147 # admin,write,read,none permissions sorted again alphabetically in
2147 # admin,write,read,none permissions sorted again alphabetically in
2148 # each group
2148 # each group
2149 perm_rows = sorted(perm_rows, key=display_user_sort)
2149 perm_rows = sorted(perm_rows, key=display_user_sort)
2150
2150
2151 user_groups_rows = []
2151 user_groups_rows = []
2152 if expand_from_user_groups:
2152 if expand_from_user_groups:
2153 for ug in self.permission_user_groups(with_members=True):
2153 for ug in self.permission_user_groups(with_members=True):
2154 for user_data in ug.members:
2154 for user_data in ug.members:
2155 user_groups_rows.append(user_data)
2155 user_groups_rows.append(user_data)
2156
2156
2157 return super_admin_rows + owner_row + perm_rows + user_groups_rows
2157 return super_admin_rows + owner_row + perm_rows + user_groups_rows
2158
2158
2159 def permission_user_groups(self, with_members=True):
2159 def permission_user_groups(self, with_members=True):
2160 q = UserGroupRepoToPerm.query()\
2160 q = UserGroupRepoToPerm.query()\
2161 .filter(UserGroupRepoToPerm.repository == self)
2161 .filter(UserGroupRepoToPerm.repository == self)
2162 q = q.options(joinedload(UserGroupRepoToPerm.repository),
2162 q = q.options(joinedload(UserGroupRepoToPerm.repository),
2163 joinedload(UserGroupRepoToPerm.users_group),
2163 joinedload(UserGroupRepoToPerm.users_group),
2164 joinedload(UserGroupRepoToPerm.permission),)
2164 joinedload(UserGroupRepoToPerm.permission),)
2165
2165
2166 perm_rows = []
2166 perm_rows = []
2167 for _user_group in q.all():
2167 for _user_group in q.all():
2168 entry = AttributeDict(_user_group.users_group.get_dict())
2168 entry = AttributeDict(_user_group.users_group.get_dict())
2169 entry.permission = _user_group.permission.permission_name
2169 entry.permission = _user_group.permission.permission_name
2170 if with_members:
2170 if with_members:
2171 entry.members = [x.user.get_dict()
2171 entry.members = [x.user.get_dict()
2172 for x in _user_group.users_group.members]
2172 for x in _user_group.users_group.members]
2173 perm_rows.append(entry)
2173 perm_rows.append(entry)
2174
2174
2175 perm_rows = sorted(perm_rows, key=display_user_group_sort)
2175 perm_rows = sorted(perm_rows, key=display_user_group_sort)
2176 return perm_rows
2176 return perm_rows
2177
2177
2178 def get_api_data(self, include_secrets=False):
2178 def get_api_data(self, include_secrets=False):
2179 """
2179 """
2180 Common function for generating repo api data
2180 Common function for generating repo api data
2181
2181
2182 :param include_secrets: See :meth:`User.get_api_data`.
2182 :param include_secrets: See :meth:`User.get_api_data`.
2183
2183
2184 """
2184 """
2185 # TODO: mikhail: Here there is an anti-pattern, we probably need to
2185 # TODO: mikhail: Here there is an anti-pattern, we probably need to
2186 # move this methods on models level.
2186 # move this methods on models level.
2187 from rhodecode.model.settings import SettingsModel
2187 from rhodecode.model.settings import SettingsModel
2188 from rhodecode.model.repo import RepoModel
2188 from rhodecode.model.repo import RepoModel
2189
2189
2190 repo = self
2190 repo = self
2191 _user_id, _time, _reason = self.locked
2191 _user_id, _time, _reason = self.locked
2192
2192
2193 data = {
2193 data = {
2194 'repo_id': repo.repo_id,
2194 'repo_id': repo.repo_id,
2195 'repo_name': repo.repo_name,
2195 'repo_name': repo.repo_name,
2196 'repo_type': repo.repo_type,
2196 'repo_type': repo.repo_type,
2197 'clone_uri': repo.clone_uri or '',
2197 'clone_uri': repo.clone_uri or '',
2198 'push_uri': repo.push_uri or '',
2198 'push_uri': repo.push_uri or '',
2199 'url': RepoModel().get_url(self),
2199 'url': RepoModel().get_url(self),
2200 'private': repo.private,
2200 'private': repo.private,
2201 'created_on': repo.created_on,
2201 'created_on': repo.created_on,
2202 'description': repo.description_safe,
2202 'description': repo.description_safe,
2203 'landing_rev': repo.landing_rev,
2203 'landing_rev': repo.landing_rev,
2204 'owner': repo.user.username,
2204 'owner': repo.user.username,
2205 'fork_of': repo.fork.repo_name if repo.fork else None,
2205 'fork_of': repo.fork.repo_name if repo.fork else None,
2206 'fork_of_id': repo.fork.repo_id if repo.fork else None,
2206 'fork_of_id': repo.fork.repo_id if repo.fork else None,
2207 'enable_statistics': repo.enable_statistics,
2207 'enable_statistics': repo.enable_statistics,
2208 'enable_locking': repo.enable_locking,
2208 'enable_locking': repo.enable_locking,
2209 'enable_downloads': repo.enable_downloads,
2209 'enable_downloads': repo.enable_downloads,
2210 'last_changeset': repo.changeset_cache,
2210 'last_changeset': repo.changeset_cache,
2211 'locked_by': User.get(_user_id).get_api_data(
2211 'locked_by': User.get(_user_id).get_api_data(
2212 include_secrets=include_secrets) if _user_id else None,
2212 include_secrets=include_secrets) if _user_id else None,
2213 'locked_date': time_to_datetime(_time) if _time else None,
2213 'locked_date': time_to_datetime(_time) if _time else None,
2214 'lock_reason': _reason if _reason else None,
2214 'lock_reason': _reason if _reason else None,
2215 }
2215 }
2216
2216
2217 # TODO: mikhail: should be per-repo settings here
2217 # TODO: mikhail: should be per-repo settings here
2218 rc_config = SettingsModel().get_all_settings()
2218 rc_config = SettingsModel().get_all_settings()
2219 repository_fields = str2bool(
2219 repository_fields = str2bool(
2220 rc_config.get('rhodecode_repository_fields'))
2220 rc_config.get('rhodecode_repository_fields'))
2221 if repository_fields:
2221 if repository_fields:
2222 for f in self.extra_fields:
2222 for f in self.extra_fields:
2223 data[f.field_key_prefixed] = f.field_value
2223 data[f.field_key_prefixed] = f.field_value
2224
2224
2225 return data
2225 return data
2226
2226
2227 @classmethod
2227 @classmethod
2228 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
2228 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
2229 if not lock_time:
2229 if not lock_time:
2230 lock_time = time.time()
2230 lock_time = time.time()
2231 if not lock_reason:
2231 if not lock_reason:
2232 lock_reason = cls.LOCK_AUTOMATIC
2232 lock_reason = cls.LOCK_AUTOMATIC
2233 repo.locked = [user_id, lock_time, lock_reason]
2233 repo.locked = [user_id, lock_time, lock_reason]
2234 Session().add(repo)
2234 Session().add(repo)
2235 Session().commit()
2235 Session().commit()
2236
2236
2237 @classmethod
2237 @classmethod
2238 def unlock(cls, repo):
2238 def unlock(cls, repo):
2239 repo.locked = None
2239 repo.locked = None
2240 Session().add(repo)
2240 Session().add(repo)
2241 Session().commit()
2241 Session().commit()
2242
2242
2243 @classmethod
2243 @classmethod
2244 def getlock(cls, repo):
2244 def getlock(cls, repo):
2245 return repo.locked
2245 return repo.locked
2246
2246
2247 def get_locking_state(self, action, user_id, only_when_enabled=True):
2247 def get_locking_state(self, action, user_id, only_when_enabled=True):
2248 """
2248 """
2249 Checks locking on this repository, if locking is enabled and lock is
2249 Checks locking on this repository, if locking is enabled and lock is
2250 present returns a tuple of make_lock, locked, locked_by.
2250 present returns a tuple of make_lock, locked, locked_by.
2251 make_lock can have 3 states None (do nothing) True, make lock
2251 make_lock can have 3 states None (do nothing) True, make lock
2252 False release lock, This value is later propagated to hooks, which
2252 False release lock, This value is later propagated to hooks, which
2253 do the locking. Think about this as signals passed to hooks what to do.
2253 do the locking. Think about this as signals passed to hooks what to do.
2254
2254
2255 """
2255 """
2256 # TODO: johbo: This is part of the business logic and should be moved
2256 # TODO: johbo: This is part of the business logic and should be moved
2257 # into the RepositoryModel.
2257 # into the RepositoryModel.
2258
2258
2259 if action not in ('push', 'pull'):
2259 if action not in ('push', 'pull'):
2260 raise ValueError("Invalid action value: %s" % repr(action))
2260 raise ValueError("Invalid action value: %s" % repr(action))
2261
2261
2262 # defines if locked error should be thrown to user
2262 # defines if locked error should be thrown to user
2263 currently_locked = False
2263 currently_locked = False
2264 # defines if new lock should be made, tri-state
2264 # defines if new lock should be made, tri-state
2265 make_lock = None
2265 make_lock = None
2266 repo = self
2266 repo = self
2267 user = User.get(user_id)
2267 user = User.get(user_id)
2268
2268
2269 lock_info = repo.locked
2269 lock_info = repo.locked
2270
2270
2271 if repo and (repo.enable_locking or not only_when_enabled):
2271 if repo and (repo.enable_locking or not only_when_enabled):
2272 if action == 'push':
2272 if action == 'push':
2273 # check if it's already locked !, if it is compare users
2273 # check if it's already locked !, if it is compare users
2274 locked_by_user_id = lock_info[0]
2274 locked_by_user_id = lock_info[0]
2275 if user.user_id == locked_by_user_id:
2275 if user.user_id == locked_by_user_id:
2276 log.debug(
2276 log.debug(
2277 'Got `push` action from user %s, now unlocking', user)
2277 'Got `push` action from user %s, now unlocking', user)
2278 # unlock if we have push from user who locked
2278 # unlock if we have push from user who locked
2279 make_lock = False
2279 make_lock = False
2280 else:
2280 else:
2281 # we're not the same user who locked, ban with
2281 # we're not the same user who locked, ban with
2282 # code defined in settings (default is 423 HTTP Locked) !
2282 # code defined in settings (default is 423 HTTP Locked) !
2283 log.debug('Repo %s is currently locked by %s', repo, user)
2283 log.debug('Repo %s is currently locked by %s', repo, user)
2284 currently_locked = True
2284 currently_locked = True
2285 elif action == 'pull':
2285 elif action == 'pull':
2286 # [0] user [1] date
2286 # [0] user [1] date
2287 if lock_info[0] and lock_info[1]:
2287 if lock_info[0] and lock_info[1]:
2288 log.debug('Repo %s is currently locked by %s', repo, user)
2288 log.debug('Repo %s is currently locked by %s', repo, user)
2289 currently_locked = True
2289 currently_locked = True
2290 else:
2290 else:
2291 log.debug('Setting lock on repo %s by %s', repo, user)
2291 log.debug('Setting lock on repo %s by %s', repo, user)
2292 make_lock = True
2292 make_lock = True
2293
2293
2294 else:
2294 else:
2295 log.debug('Repository %s do not have locking enabled', repo)
2295 log.debug('Repository %s do not have locking enabled', repo)
2296
2296
2297 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
2297 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
2298 make_lock, currently_locked, lock_info)
2298 make_lock, currently_locked, lock_info)
2299
2299
2300 from rhodecode.lib.auth import HasRepoPermissionAny
2300 from rhodecode.lib.auth import HasRepoPermissionAny
2301 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
2301 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
2302 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
2302 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
2303 # if we don't have at least write permission we cannot make a lock
2303 # if we don't have at least write permission we cannot make a lock
2304 log.debug('lock state reset back to FALSE due to lack '
2304 log.debug('lock state reset back to FALSE due to lack '
2305 'of at least read permission')
2305 'of at least read permission')
2306 make_lock = False
2306 make_lock = False
2307
2307
2308 return make_lock, currently_locked, lock_info
2308 return make_lock, currently_locked, lock_info
2309
2309
2310 @property
2310 @property
2311 def last_commit_cache_update_diff(self):
2311 def last_commit_cache_update_diff(self):
2312 return time.time() - (safe_int(self.changeset_cache.get('updated_on')) or 0)
2312 return time.time() - (safe_int(self.changeset_cache.get('updated_on')) or 0)
2313
2313
2314 @classmethod
2314 @classmethod
2315 def _load_commit_change(cls, last_commit_cache):
2315 def _load_commit_change(cls, last_commit_cache):
2316 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2316 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2317 empty_date = datetime.datetime.fromtimestamp(0)
2317 empty_date = datetime.datetime.fromtimestamp(0)
2318 date_latest = last_commit_cache.get('date', empty_date)
2318 date_latest = last_commit_cache.get('date', empty_date)
2319 try:
2319 try:
2320 return parse_datetime(date_latest)
2320 return parse_datetime(date_latest)
2321 except Exception:
2321 except Exception:
2322 return empty_date
2322 return empty_date
2323
2323
2324 @property
2324 @property
2325 def last_commit_change(self):
2325 def last_commit_change(self):
2326 return self._load_commit_change(self.changeset_cache)
2326 return self._load_commit_change(self.changeset_cache)
2327
2327
2328 @property
2328 @property
2329 def last_db_change(self):
2329 def last_db_change(self):
2330 return self.updated_on
2330 return self.updated_on
2331
2331
2332 @property
2332 @property
2333 def clone_uri_hidden(self):
2333 def clone_uri_hidden(self):
2334 clone_uri = self.clone_uri
2334 clone_uri = self.clone_uri
2335 if clone_uri:
2335 if clone_uri:
2336 import urlobject
2336 import urlobject
2337 url_obj = urlobject.URLObject(cleaned_uri(clone_uri))
2337 url_obj = urlobject.URLObject(cleaned_uri(clone_uri))
2338 if url_obj.password:
2338 if url_obj.password:
2339 clone_uri = url_obj.with_password('*****')
2339 clone_uri = url_obj.with_password('*****')
2340 return clone_uri
2340 return clone_uri
2341
2341
2342 @property
2342 @property
2343 def push_uri_hidden(self):
2343 def push_uri_hidden(self):
2344 push_uri = self.push_uri
2344 push_uri = self.push_uri
2345 if push_uri:
2345 if push_uri:
2346 import urlobject
2346 import urlobject
2347 url_obj = urlobject.URLObject(cleaned_uri(push_uri))
2347 url_obj = urlobject.URLObject(cleaned_uri(push_uri))
2348 if url_obj.password:
2348 if url_obj.password:
2349 push_uri = url_obj.with_password('*****')
2349 push_uri = url_obj.with_password('*****')
2350 return push_uri
2350 return push_uri
2351
2351
2352 def clone_url(self, **override):
2352 def clone_url(self, **override):
2353 from rhodecode.model.settings import SettingsModel
2353 from rhodecode.model.settings import SettingsModel
2354
2354
2355 uri_tmpl = None
2355 uri_tmpl = None
2356 if 'with_id' in override:
2356 if 'with_id' in override:
2357 uri_tmpl = self.DEFAULT_CLONE_URI_ID
2357 uri_tmpl = self.DEFAULT_CLONE_URI_ID
2358 del override['with_id']
2358 del override['with_id']
2359
2359
2360 if 'uri_tmpl' in override:
2360 if 'uri_tmpl' in override:
2361 uri_tmpl = override['uri_tmpl']
2361 uri_tmpl = override['uri_tmpl']
2362 del override['uri_tmpl']
2362 del override['uri_tmpl']
2363
2363
2364 ssh = False
2364 ssh = False
2365 if 'ssh' in override:
2365 if 'ssh' in override:
2366 ssh = True
2366 ssh = True
2367 del override['ssh']
2367 del override['ssh']
2368
2368
2369 # we didn't override our tmpl from **overrides
2369 # we didn't override our tmpl from **overrides
2370 request = get_current_request()
2370 request = get_current_request()
2371 if not uri_tmpl:
2371 if not uri_tmpl:
2372 if hasattr(request, 'call_context') and hasattr(request.call_context, 'rc_config'):
2372 if hasattr(request, 'call_context') and hasattr(request.call_context, 'rc_config'):
2373 rc_config = request.call_context.rc_config
2373 rc_config = request.call_context.rc_config
2374 else:
2374 else:
2375 rc_config = SettingsModel().get_all_settings(cache=True)
2375 rc_config = SettingsModel().get_all_settings(cache=True)
2376
2376
2377 if ssh:
2377 if ssh:
2378 uri_tmpl = rc_config.get(
2378 uri_tmpl = rc_config.get(
2379 'rhodecode_clone_uri_ssh_tmpl') or self.DEFAULT_CLONE_URI_SSH
2379 'rhodecode_clone_uri_ssh_tmpl') or self.DEFAULT_CLONE_URI_SSH
2380
2380
2381 else:
2381 else:
2382 uri_tmpl = rc_config.get(
2382 uri_tmpl = rc_config.get(
2383 'rhodecode_clone_uri_tmpl') or self.DEFAULT_CLONE_URI
2383 'rhodecode_clone_uri_tmpl') or self.DEFAULT_CLONE_URI
2384
2384
2385 return get_clone_url(request=request,
2385 return get_clone_url(request=request,
2386 uri_tmpl=uri_tmpl,
2386 uri_tmpl=uri_tmpl,
2387 repo_name=self.repo_name,
2387 repo_name=self.repo_name,
2388 repo_id=self.repo_id,
2388 repo_id=self.repo_id,
2389 repo_type=self.repo_type,
2389 repo_type=self.repo_type,
2390 **override)
2390 **override)
2391
2391
2392 def set_state(self, state):
2392 def set_state(self, state):
2393 self.repo_state = state
2393 self.repo_state = state
2394 Session().add(self)
2394 Session().add(self)
2395 #==========================================================================
2395 #==========================================================================
2396 # SCM PROPERTIES
2396 # SCM PROPERTIES
2397 #==========================================================================
2397 #==========================================================================
2398
2398
2399 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None, maybe_unreachable=False, reference_obj=None):
2399 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None, maybe_unreachable=False, reference_obj=None):
2400 return get_commit_safe(
2400 return get_commit_safe(
2401 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load,
2401 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load,
2402 maybe_unreachable=maybe_unreachable, reference_obj=reference_obj)
2402 maybe_unreachable=maybe_unreachable, reference_obj=reference_obj)
2403
2403
2404 def get_changeset(self, rev=None, pre_load=None):
2404 def get_changeset(self, rev=None, pre_load=None):
2405 warnings.warn("Use get_commit", DeprecationWarning)
2405 warnings.warn("Use get_commit", DeprecationWarning)
2406 commit_id = None
2406 commit_id = None
2407 commit_idx = None
2407 commit_idx = None
2408 if isinstance(rev, str):
2408 if isinstance(rev, str):
2409 commit_id = rev
2409 commit_id = rev
2410 else:
2410 else:
2411 commit_idx = rev
2411 commit_idx = rev
2412 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
2412 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
2413 pre_load=pre_load)
2413 pre_load=pre_load)
2414
2414
2415 def get_landing_commit(self):
2415 def get_landing_commit(self):
2416 """
2416 """
2417 Returns landing commit, or if that doesn't exist returns the tip
2417 Returns landing commit, or if that doesn't exist returns the tip
2418 """
2418 """
2419 _rev_type, _rev = self.landing_rev
2419 _rev_type, _rev = self.landing_rev
2420 commit = self.get_commit(_rev)
2420 commit = self.get_commit(_rev)
2421 if isinstance(commit, EmptyCommit):
2421 if isinstance(commit, EmptyCommit):
2422 return self.get_commit()
2422 return self.get_commit()
2423 return commit
2423 return commit
2424
2424
2425 def flush_commit_cache(self):
2425 def flush_commit_cache(self):
2426 self.update_commit_cache(cs_cache={'raw_id':'0'})
2426 self.update_commit_cache(cs_cache={'raw_id':'0'})
2427 self.update_commit_cache()
2427 self.update_commit_cache()
2428
2428
2429 def update_commit_cache(self, cs_cache=None, config=None):
2429 def update_commit_cache(self, cs_cache=None, config=None):
2430 """
2430 """
2431 Update cache of last commit for repository
2431 Update cache of last commit for repository
2432 cache_keys should be::
2432 cache_keys should be::
2433
2433
2434 source_repo_id
2434 source_repo_id
2435 short_id
2435 short_id
2436 raw_id
2436 raw_id
2437 revision
2437 revision
2438 parents
2438 parents
2439 message
2439 message
2440 date
2440 date
2441 author
2441 author
2442 updated_on
2442 updated_on
2443
2443
2444 """
2444 """
2445 from rhodecode.lib.vcs.backends.base import BaseCommit
2445 from rhodecode.lib.vcs.backends.base import BaseCommit
2446 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2446 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2447 empty_date = datetime.datetime.fromtimestamp(0)
2447 empty_date = datetime.datetime.fromtimestamp(0)
2448 repo_commit_count = 0
2448 repo_commit_count = 0
2449
2449
2450 if cs_cache is None:
2450 if cs_cache is None:
2451 # use no-cache version here
2451 # use no-cache version here
2452 try:
2452 try:
2453 scm_repo = self.scm_instance(cache=False, config=config)
2453 scm_repo = self.scm_instance(cache=False, config=config)
2454 except VCSError:
2454 except VCSError:
2455 scm_repo = None
2455 scm_repo = None
2456 empty = scm_repo is None or scm_repo.is_empty()
2456 empty = scm_repo is None or scm_repo.is_empty()
2457
2457
2458 if not empty:
2458 if not empty:
2459 cs_cache = scm_repo.get_commit(
2459 cs_cache = scm_repo.get_commit(
2460 pre_load=["author", "date", "message", "parents", "branch"])
2460 pre_load=["author", "date", "message", "parents", "branch"])
2461 repo_commit_count = scm_repo.count()
2461 repo_commit_count = scm_repo.count()
2462 else:
2462 else:
2463 cs_cache = EmptyCommit()
2463 cs_cache = EmptyCommit()
2464
2464
2465 if isinstance(cs_cache, BaseCommit):
2465 if isinstance(cs_cache, BaseCommit):
2466 cs_cache = cs_cache.__json__()
2466 cs_cache = cs_cache.__json__()
2467
2467
2468 def is_outdated(new_cs_cache):
2468 def is_outdated(new_cs_cache):
2469 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
2469 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
2470 new_cs_cache['revision'] != self.changeset_cache['revision']):
2470 new_cs_cache['revision'] != self.changeset_cache['revision']):
2471 return True
2471 return True
2472 return False
2472 return False
2473
2473
2474 # check if we have maybe already latest cached revision
2474 # check if we have maybe already latest cached revision
2475 if is_outdated(cs_cache) or not self.changeset_cache:
2475 if is_outdated(cs_cache) or not self.changeset_cache:
2476 _current_datetime = datetime.datetime.utcnow()
2476 _current_datetime = datetime.datetime.utcnow()
2477 last_change = cs_cache.get('date') or _current_datetime
2477 last_change = cs_cache.get('date') or _current_datetime
2478 # we check if last update is newer than the new value
2478 # we check if last update is newer than the new value
2479 # if yes, we use the current timestamp instead. Imagine you get
2479 # if yes, we use the current timestamp instead. Imagine you get
2480 # old commit pushed 1y ago, we'd set last update 1y to ago.
2480 # old commit pushed 1y ago, we'd set last update 1y to ago.
2481 last_change_timestamp = datetime_to_time(last_change)
2481 last_change_timestamp = datetime_to_time(last_change)
2482 current_timestamp = datetime_to_time(last_change)
2482 current_timestamp = datetime_to_time(last_change)
2483 if last_change_timestamp > current_timestamp and not empty:
2483 if last_change_timestamp > current_timestamp and not empty:
2484 cs_cache['date'] = _current_datetime
2484 cs_cache['date'] = _current_datetime
2485
2485
2486 # also store size of repo
2486 # also store size of repo
2487 cs_cache['repo_commit_count'] = repo_commit_count
2487 cs_cache['repo_commit_count'] = repo_commit_count
2488
2488
2489 _date_latest = parse_datetime(cs_cache.get('date') or empty_date)
2489 _date_latest = parse_datetime(cs_cache.get('date') or empty_date)
2490 cs_cache['updated_on'] = time.time()
2490 cs_cache['updated_on'] = time.time()
2491 self.changeset_cache = cs_cache
2491 self.changeset_cache = cs_cache
2492 self.updated_on = last_change
2492 self.updated_on = last_change
2493 Session().add(self)
2493 Session().add(self)
2494 Session().commit()
2494 Session().commit()
2495
2495
2496 else:
2496 else:
2497 if empty:
2497 if empty:
2498 cs_cache = EmptyCommit().__json__()
2498 cs_cache = EmptyCommit().__json__()
2499 else:
2499 else:
2500 cs_cache = self.changeset_cache
2500 cs_cache = self.changeset_cache
2501
2501
2502 _date_latest = parse_datetime(cs_cache.get('date') or empty_date)
2502 _date_latest = parse_datetime(cs_cache.get('date') or empty_date)
2503
2503
2504 cs_cache['updated_on'] = time.time()
2504 cs_cache['updated_on'] = time.time()
2505 self.changeset_cache = cs_cache
2505 self.changeset_cache = cs_cache
2506 self.updated_on = _date_latest
2506 self.updated_on = _date_latest
2507 Session().add(self)
2507 Session().add(self)
2508 Session().commit()
2508 Session().commit()
2509
2509
2510 log.debug('updated repo `%s` with new commit cache %s, and last update_date: %s',
2510 log.debug('updated repo `%s` with new commit cache %s, and last update_date: %s',
2511 self.repo_name, cs_cache, _date_latest)
2511 self.repo_name, cs_cache, _date_latest)
2512
2512
2513 @property
2513 @property
2514 def tip(self):
2514 def tip(self):
2515 return self.get_commit('tip')
2515 return self.get_commit('tip')
2516
2516
2517 @property
2517 @property
2518 def author(self):
2518 def author(self):
2519 return self.tip.author
2519 return self.tip.author
2520
2520
2521 @property
2521 @property
2522 def last_change(self):
2522 def last_change(self):
2523 return self.scm_instance().last_change
2523 return self.scm_instance().last_change
2524
2524
2525 def get_comments(self, revisions=None):
2525 def get_comments(self, revisions=None):
2526 """
2526 """
2527 Returns comments for this repository grouped by revisions
2527 Returns comments for this repository grouped by revisions
2528
2528
2529 :param revisions: filter query by revisions only
2529 :param revisions: filter query by revisions only
2530 """
2530 """
2531 cmts = ChangesetComment.query()\
2531 cmts = ChangesetComment.query()\
2532 .filter(ChangesetComment.repo == self)
2532 .filter(ChangesetComment.repo == self)
2533 if revisions:
2533 if revisions:
2534 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
2534 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
2535 grouped = collections.defaultdict(list)
2535 grouped = collections.defaultdict(list)
2536 for cmt in cmts.all():
2536 for cmt in cmts.all():
2537 grouped[cmt.revision].append(cmt)
2537 grouped[cmt.revision].append(cmt)
2538 return grouped
2538 return grouped
2539
2539
2540 def statuses(self, revisions=None):
2540 def statuses(self, revisions=None):
2541 """
2541 """
2542 Returns statuses for this repository
2542 Returns statuses for this repository
2543
2543
2544 :param revisions: list of revisions to get statuses for
2544 :param revisions: list of revisions to get statuses for
2545 """
2545 """
2546 statuses = ChangesetStatus.query()\
2546 statuses = ChangesetStatus.query()\
2547 .filter(ChangesetStatus.repo == self)\
2547 .filter(ChangesetStatus.repo == self)\
2548 .filter(ChangesetStatus.version == 0)
2548 .filter(ChangesetStatus.version == 0)
2549
2549
2550 if revisions:
2550 if revisions:
2551 # Try doing the filtering in chunks to avoid hitting limits
2551 # Try doing the filtering in chunks to avoid hitting limits
2552 size = 500
2552 size = 500
2553 status_results = []
2553 status_results = []
2554 for chunk in range(0, len(revisions), size):
2554 for chunk in range(0, len(revisions), size):
2555 status_results += statuses.filter(
2555 status_results += statuses.filter(
2556 ChangesetStatus.revision.in_(
2556 ChangesetStatus.revision.in_(
2557 revisions[chunk: chunk+size])
2557 revisions[chunk: chunk+size])
2558 ).all()
2558 ).all()
2559 else:
2559 else:
2560 status_results = statuses.all()
2560 status_results = statuses.all()
2561
2561
2562 grouped = {}
2562 grouped = {}
2563
2563
2564 # maybe we have open new pullrequest without a status?
2564 # maybe we have open new pullrequest without a status?
2565 stat = ChangesetStatus.STATUS_UNDER_REVIEW
2565 stat = ChangesetStatus.STATUS_UNDER_REVIEW
2566 status_lbl = ChangesetStatus.get_status_lbl(stat)
2566 status_lbl = ChangesetStatus.get_status_lbl(stat)
2567 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
2567 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
2568 for rev in pr.revisions:
2568 for rev in pr.revisions:
2569 pr_id = pr.pull_request_id
2569 pr_id = pr.pull_request_id
2570 pr_repo = pr.target_repo.repo_name
2570 pr_repo = pr.target_repo.repo_name
2571 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
2571 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
2572
2572
2573 for stat in status_results:
2573 for stat in status_results:
2574 pr_id = pr_repo = None
2574 pr_id = pr_repo = None
2575 if stat.pull_request:
2575 if stat.pull_request:
2576 pr_id = stat.pull_request.pull_request_id
2576 pr_id = stat.pull_request.pull_request_id
2577 pr_repo = stat.pull_request.target_repo.repo_name
2577 pr_repo = stat.pull_request.target_repo.repo_name
2578 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
2578 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
2579 pr_id, pr_repo]
2579 pr_id, pr_repo]
2580 return grouped
2580 return grouped
2581
2581
2582 # ==========================================================================
2582 # ==========================================================================
2583 # SCM CACHE INSTANCE
2583 # SCM CACHE INSTANCE
2584 # ==========================================================================
2584 # ==========================================================================
2585
2585
2586 def scm_instance(self, **kwargs):
2586 def scm_instance(self, **kwargs):
2587 import rhodecode
2587 import rhodecode
2588
2588
2589 # Passing a config will not hit the cache currently only used
2589 # Passing a config will not hit the cache currently only used
2590 # for repo2dbmapper
2590 # for repo2dbmapper
2591 config = kwargs.pop('config', None)
2591 config = kwargs.pop('config', None)
2592 cache = kwargs.pop('cache', None)
2592 cache = kwargs.pop('cache', None)
2593 vcs_full_cache = kwargs.pop('vcs_full_cache', None)
2593 vcs_full_cache = kwargs.pop('vcs_full_cache', None)
2594 if vcs_full_cache is not None:
2594 if vcs_full_cache is not None:
2595 # allows override global config
2595 # allows override global config
2596 full_cache = vcs_full_cache
2596 full_cache = vcs_full_cache
2597 else:
2597 else:
2598 full_cache = rhodecode.ConfigGet().get_bool('vcs_full_cache')
2598 full_cache = rhodecode.ConfigGet().get_bool('vcs_full_cache')
2599 # if cache is NOT defined use default global, else we have a full
2599 # if cache is NOT defined use default global, else we have a full
2600 # control over cache behaviour
2600 # control over cache behaviour
2601 if cache is None and full_cache and not config:
2601 if cache is None and full_cache and not config:
2602 log.debug('Initializing pure cached instance for %s', self.repo_path)
2602 log.debug('Initializing pure cached instance for %s', self.repo_path)
2603 return self._get_instance_cached()
2603 return self._get_instance_cached()
2604
2604
2605 # cache here is sent to the "vcs server"
2605 # cache here is sent to the "vcs server"
2606 return self._get_instance(cache=bool(cache), config=config)
2606 return self._get_instance(cache=bool(cache), config=config)
2607
2607
2608 def _get_instance_cached(self):
2608 def _get_instance_cached(self):
2609 from rhodecode.lib import rc_cache
2609 from rhodecode.lib import rc_cache
2610
2610
2611 cache_namespace_uid = f'repo_instance.{self.repo_id}'
2611 cache_namespace_uid = f'repo_instance.{self.repo_id}'
2612 invalidation_namespace = CacheKey.REPO_INVALIDATION_NAMESPACE.format(
2612 invalidation_namespace = CacheKey.REPO_INVALIDATION_NAMESPACE.format(
2613 repo_id=self.repo_id)
2613 repo_id=self.repo_id)
2614 region = rc_cache.get_or_create_region('cache_repo_longterm', cache_namespace_uid)
2614 region = rc_cache.get_or_create_region('cache_repo_longterm', cache_namespace_uid)
2615
2615
2616 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid)
2616 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid)
2617 def get_instance_cached(repo_id, context_id, _cache_state_uid):
2617 def get_instance_cached(repo_id, context_id, _cache_state_uid):
2618 return self._get_instance(repo_state_uid=_cache_state_uid)
2618 return self._get_instance(repo_state_uid=_cache_state_uid)
2619
2619
2620 # we must use thread scoped cache here,
2620 # we must use thread scoped cache here,
2621 # because each thread of gevent needs it's own not shared connection and cache
2621 # because each thread of gevent needs it's own not shared connection and cache
2622 # we also alter `args` so the cache key is individual for every green thread.
2622 # we also alter `args` so the cache key is individual for every green thread.
2623 inv_context_manager = rc_cache.InvalidationContext(
2623 inv_context_manager = rc_cache.InvalidationContext(
2624 uid=cache_namespace_uid, invalidation_namespace=invalidation_namespace,
2624 uid=cache_namespace_uid, invalidation_namespace=invalidation_namespace,
2625 thread_scoped=True)
2625 thread_scoped=True)
2626 with inv_context_manager as invalidation_context:
2626 with inv_context_manager as invalidation_context:
2627 cache_state_uid = invalidation_context.cache_data['cache_state_uid']
2627 cache_state_uid = invalidation_context.cache_data['cache_state_uid']
2628 args = (self.repo_id, inv_context_manager.cache_key, cache_state_uid)
2628 args = (self.repo_id, inv_context_manager.cache_key, cache_state_uid)
2629
2629
2630 # re-compute and store cache if we get invalidate signal
2630 # re-compute and store cache if we get invalidate signal
2631 if invalidation_context.should_invalidate():
2631 if invalidation_context.should_invalidate():
2632 instance = get_instance_cached.refresh(*args)
2632 instance = get_instance_cached.refresh(*args)
2633 else:
2633 else:
2634 instance = get_instance_cached(*args)
2634 instance = get_instance_cached(*args)
2635
2635
2636 log.debug('Repo instance fetched in %.4fs', inv_context_manager.compute_time)
2636 log.debug('Repo instance fetched in %.4fs', inv_context_manager.compute_time)
2637 return instance
2637 return instance
2638
2638
2639 def _get_instance(self, cache=True, config=None, repo_state_uid=None):
2639 def _get_instance(self, cache=True, config=None, repo_state_uid=None):
2640 log.debug('Initializing %s instance `%s` with cache flag set to: %s',
2640 log.debug('Initializing %s instance `%s` with cache flag set to: %s',
2641 self.repo_type, self.repo_path, cache)
2641 self.repo_type, self.repo_path, cache)
2642 config = config or self._config
2642 config = config or self._config
2643 custom_wire = {
2643 custom_wire = {
2644 'cache': cache, # controls the vcs.remote cache
2644 'cache': cache, # controls the vcs.remote cache
2645 'repo_state_uid': repo_state_uid
2645 'repo_state_uid': repo_state_uid
2646 }
2646 }
2647 repo = get_vcs_instance(
2647 repo = get_vcs_instance(
2648 repo_path=safe_str(self.repo_full_path),
2648 repo_path=safe_str(self.repo_full_path),
2649 config=config,
2649 config=config,
2650 with_wire=custom_wire,
2650 with_wire=custom_wire,
2651 create=False,
2651 create=False,
2652 _vcs_alias=self.repo_type)
2652 _vcs_alias=self.repo_type)
2653 if repo is not None:
2653 if repo is not None:
2654 repo.count() # cache rebuild
2654 repo.count() # cache rebuild
2655 return repo
2655 return repo
2656
2656
2657 def get_shadow_repository_path(self, workspace_id):
2657 def get_shadow_repository_path(self, workspace_id):
2658 from rhodecode.lib.vcs.backends.base import BaseRepository
2658 from rhodecode.lib.vcs.backends.base import BaseRepository
2659 shadow_repo_path = BaseRepository._get_shadow_repository_path(
2659 shadow_repo_path = BaseRepository._get_shadow_repository_path(
2660 self.repo_full_path, self.repo_id, workspace_id)
2660 self.repo_full_path, self.repo_id, workspace_id)
2661 return shadow_repo_path
2661 return shadow_repo_path
2662
2662
2663 def __json__(self):
2663 def __json__(self):
2664 return {'landing_rev': self.landing_rev}
2664 return {'landing_rev': self.landing_rev}
2665
2665
2666 def get_dict(self):
2666 def get_dict(self):
2667
2667
2668 # Since we transformed `repo_name` to a hybrid property, we need to
2668 # Since we transformed `repo_name` to a hybrid property, we need to
2669 # keep compatibility with the code which uses `repo_name` field.
2669 # keep compatibility with the code which uses `repo_name` field.
2670
2670
2671 result = super(Repository, self).get_dict()
2671 result = super(Repository, self).get_dict()
2672 result['repo_name'] = result.pop('_repo_name', None)
2672 result['repo_name'] = result.pop('_repo_name', None)
2673 result.pop('_changeset_cache', '')
2673 result.pop('_changeset_cache', '')
2674 return result
2674 return result
2675
2675
2676
2676
2677 class RepoGroup(Base, BaseModel):
2677 class RepoGroup(Base, BaseModel):
2678 __tablename__ = 'groups'
2678 __tablename__ = 'groups'
2679 __table_args__ = (
2679 __table_args__ = (
2680 UniqueConstraint('group_name', 'group_parent_id'),
2680 UniqueConstraint('group_name', 'group_parent_id'),
2681 base_table_args,
2681 base_table_args,
2682 )
2682 )
2683
2683
2684 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2684 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2685
2685
2686 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2686 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2687 _group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2687 _group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2688 group_name_hash = Column("repo_group_name_hash", String(1024), nullable=False, unique=False)
2688 group_name_hash = Column("repo_group_name_hash", String(1024), nullable=False, unique=False)
2689 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2689 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2690 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2690 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2691 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2691 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2692 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2692 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2693 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2693 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2694 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2694 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2695 personal = Column('personal', Boolean(), nullable=True, unique=None, default=None)
2695 personal = Column('personal', Boolean(), nullable=True, unique=None, default=None)
2696 _changeset_cache = Column("changeset_cache", LargeBinary(), nullable=True) # JSON data
2696 _changeset_cache = Column("changeset_cache", LargeBinary(), nullable=True) # JSON data
2697
2697
2698 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id', back_populates='group')
2698 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id', back_populates='group')
2699 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all', back_populates='group')
2699 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all', back_populates='group')
2700 parent_group = relationship('RepoGroup', remote_side=group_id)
2700 parent_group = relationship('RepoGroup', remote_side=group_id)
2701 user = relationship('User', back_populates='repository_groups')
2701 user = relationship('User', back_populates='repository_groups')
2702 integrations = relationship('Integration', cascade="all, delete-orphan", back_populates='repo_group')
2702 integrations = relationship('Integration', cascade="all, delete-orphan", back_populates='repo_group')
2703
2703
2704 # no cascade, set NULL
2704 # no cascade, set NULL
2705 scope_artifacts = relationship('FileStore', primaryjoin='FileStore.scope_repo_group_id==RepoGroup.group_id', viewonly=True)
2705 scope_artifacts = relationship('FileStore', primaryjoin='FileStore.scope_repo_group_id==RepoGroup.group_id', viewonly=True)
2706
2706
2707 def __init__(self, group_name='', parent_group=None):
2707 def __init__(self, group_name='', parent_group=None):
2708 self.group_name = group_name
2708 self.group_name = group_name
2709 self.parent_group = parent_group
2709 self.parent_group = parent_group
2710
2710
2711 def __repr__(self):
2711 def __repr__(self):
2712 return f"<{self.cls_name}('id:{self.group_id}:{self.group_name}')>"
2712 return f"<{self.cls_name}('id:{self.group_id}:{self.group_name}')>"
2713
2713
2714 @hybrid_property
2714 @hybrid_property
2715 def group_name(self):
2715 def group_name(self):
2716 return self._group_name
2716 return self._group_name
2717
2717
2718 @group_name.setter
2718 @group_name.setter
2719 def group_name(self, value):
2719 def group_name(self, value):
2720 self._group_name = value
2720 self._group_name = value
2721 self.group_name_hash = self.hash_repo_group_name(value)
2721 self.group_name_hash = self.hash_repo_group_name(value)
2722
2722
2723 @classmethod
2723 @classmethod
2724 def _load_changeset_cache(cls, repo_id, changeset_cache_raw):
2724 def _load_changeset_cache(cls, repo_id, changeset_cache_raw):
2725 from rhodecode.lib.vcs.backends.base import EmptyCommit
2725 from rhodecode.lib.vcs.backends.base import EmptyCommit
2726 dummy = EmptyCommit().__json__()
2726 dummy = EmptyCommit().__json__()
2727 if not changeset_cache_raw:
2727 if not changeset_cache_raw:
2728 dummy['source_repo_id'] = repo_id
2728 dummy['source_repo_id'] = repo_id
2729 return json.loads(json.dumps(dummy))
2729 return json.loads(json.dumps(dummy))
2730
2730
2731 try:
2731 try:
2732 return json.loads(changeset_cache_raw)
2732 return json.loads(changeset_cache_raw)
2733 except TypeError:
2733 except TypeError:
2734 return dummy
2734 return dummy
2735 except Exception:
2735 except Exception:
2736 log.error(traceback.format_exc())
2736 log.error(traceback.format_exc())
2737 return dummy
2737 return dummy
2738
2738
2739 @hybrid_property
2739 @hybrid_property
2740 def changeset_cache(self):
2740 def changeset_cache(self):
2741 return self._load_changeset_cache('', self._changeset_cache)
2741 return self._load_changeset_cache('', self._changeset_cache)
2742
2742
2743 @changeset_cache.setter
2743 @changeset_cache.setter
2744 def changeset_cache(self, val):
2744 def changeset_cache(self, val):
2745 try:
2745 try:
2746 self._changeset_cache = json.dumps(val)
2746 self._changeset_cache = json.dumps(val)
2747 except Exception:
2747 except Exception:
2748 log.error(traceback.format_exc())
2748 log.error(traceback.format_exc())
2749
2749
2750 @validates('group_parent_id')
2750 @validates('group_parent_id')
2751 def validate_group_parent_id(self, key, val):
2751 def validate_group_parent_id(self, key, val):
2752 """
2752 """
2753 Check cycle references for a parent group to self
2753 Check cycle references for a parent group to self
2754 """
2754 """
2755 if self.group_id and val:
2755 if self.group_id and val:
2756 assert val != self.group_id
2756 assert val != self.group_id
2757
2757
2758 return val
2758 return val
2759
2759
2760 @hybrid_property
2760 @hybrid_property
2761 def description_safe(self):
2761 def description_safe(self):
2762 from rhodecode.lib import helpers as h
2762 from rhodecode.lib import helpers as h
2763 return h.escape(self.group_description)
2763 return h.escape(self.group_description)
2764
2764
2765 @classmethod
2765 @classmethod
2766 def hash_repo_group_name(cls, repo_group_name):
2766 def hash_repo_group_name(cls, repo_group_name):
2767 val = remove_formatting(repo_group_name)
2767 val = remove_formatting(repo_group_name)
2768 val = safe_str(val).lower()
2768 val = safe_str(val).lower()
2769 chars = []
2769 chars = []
2770 for c in val:
2770 for c in val:
2771 if c not in string.ascii_letters:
2771 if c not in string.ascii_letters:
2772 c = str(ord(c))
2772 c = str(ord(c))
2773 chars.append(c)
2773 chars.append(c)
2774
2774
2775 return ''.join(chars)
2775 return ''.join(chars)
2776
2776
2777 @classmethod
2777 @classmethod
2778 def _generate_choice(cls, repo_group):
2778 def _generate_choice(cls, repo_group):
2779 from webhelpers2.html import literal as _literal
2779 from webhelpers2.html import literal as _literal
2780
2780
2781 def _name(k):
2781 def _name(k):
2782 return _literal(cls.CHOICES_SEPARATOR.join(k))
2782 return _literal(cls.CHOICES_SEPARATOR.join(k))
2783
2783
2784 return repo_group.group_id, _name(repo_group.full_path_splitted)
2784 return repo_group.group_id, _name(repo_group.full_path_splitted)
2785
2785
2786 @classmethod
2786 @classmethod
2787 def groups_choices(cls, groups=None, show_empty_group=True):
2787 def groups_choices(cls, groups=None, show_empty_group=True):
2788 if not groups:
2788 if not groups:
2789 groups = cls.query().all()
2789 groups = cls.query().all()
2790
2790
2791 repo_groups = []
2791 repo_groups = []
2792 if show_empty_group:
2792 if show_empty_group:
2793 repo_groups = [(-1, '-- %s --' % _('No parent'))]
2793 repo_groups = [(-1, '-- %s --' % _('No parent'))]
2794
2794
2795 repo_groups.extend([cls._generate_choice(x) for x in groups])
2795 repo_groups.extend([cls._generate_choice(x) for x in groups])
2796
2796
2797 repo_groups = sorted(
2797 repo_groups = sorted(
2798 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2798 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2799 return repo_groups
2799 return repo_groups
2800
2800
2801 @classmethod
2801 @classmethod
2802 def url_sep(cls):
2802 def url_sep(cls):
2803 return URL_SEP
2803 return URL_SEP
2804
2804
2805 @classmethod
2805 @classmethod
2806 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2806 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2807 if case_insensitive:
2807 if case_insensitive:
2808 gr = cls.query().filter(func.lower(cls.group_name)
2808 gr = cls.query().filter(func.lower(cls.group_name)
2809 == func.lower(group_name))
2809 == func.lower(group_name))
2810 else:
2810 else:
2811 gr = cls.query().filter(cls.group_name == group_name)
2811 gr = cls.query().filter(cls.group_name == group_name)
2812 if cache:
2812 if cache:
2813 name_key = _hash_key(group_name)
2813 name_key = _hash_key(group_name)
2814 gr = gr.options(
2814 gr = gr.options(
2815 FromCache("sql_cache_short", f"get_group_{name_key}"))
2815 FromCache("sql_cache_short", f"get_group_{name_key}"))
2816 return gr.scalar()
2816 return gr.scalar()
2817
2817
2818 @classmethod
2818 @classmethod
2819 def get_user_personal_repo_group(cls, user_id):
2819 def get_user_personal_repo_group(cls, user_id):
2820 user = User.get(user_id)
2820 user = User.get(user_id)
2821 if user.username == User.DEFAULT_USER:
2821 if user.username == User.DEFAULT_USER:
2822 return None
2822 return None
2823
2823
2824 return cls.query()\
2824 return cls.query()\
2825 .filter(cls.personal == true()) \
2825 .filter(cls.personal == true()) \
2826 .filter(cls.user == user) \
2826 .filter(cls.user == user) \
2827 .order_by(cls.group_id.asc()) \
2827 .order_by(cls.group_id.asc()) \
2828 .first()
2828 .first()
2829
2829
2830 @classmethod
2830 @classmethod
2831 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2831 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2832 case_insensitive=True):
2832 case_insensitive=True):
2833 q = RepoGroup.query()
2833 q = RepoGroup.query()
2834
2834
2835 if not isinstance(user_id, Optional):
2835 if not isinstance(user_id, Optional):
2836 q = q.filter(RepoGroup.user_id == user_id)
2836 q = q.filter(RepoGroup.user_id == user_id)
2837
2837
2838 if not isinstance(group_id, Optional):
2838 if not isinstance(group_id, Optional):
2839 q = q.filter(RepoGroup.group_parent_id == group_id)
2839 q = q.filter(RepoGroup.group_parent_id == group_id)
2840
2840
2841 if case_insensitive:
2841 if case_insensitive:
2842 q = q.order_by(func.lower(RepoGroup.group_name))
2842 q = q.order_by(func.lower(RepoGroup.group_name))
2843 else:
2843 else:
2844 q = q.order_by(RepoGroup.group_name)
2844 q = q.order_by(RepoGroup.group_name)
2845 return q.all()
2845 return q.all()
2846
2846
2847 @property
2847 @property
2848 def parents(self, parents_recursion_limit=10):
2848 def parents(self, parents_recursion_limit=10):
2849 groups = []
2849 groups = []
2850 if self.parent_group is None:
2850 if self.parent_group is None:
2851 return groups
2851 return groups
2852 cur_gr = self.parent_group
2852 cur_gr = self.parent_group
2853 groups.insert(0, cur_gr)
2853 groups.insert(0, cur_gr)
2854 cnt = 0
2854 cnt = 0
2855 while 1:
2855 while 1:
2856 cnt += 1
2856 cnt += 1
2857 gr = getattr(cur_gr, 'parent_group', None)
2857 gr = getattr(cur_gr, 'parent_group', None)
2858 cur_gr = cur_gr.parent_group
2858 cur_gr = cur_gr.parent_group
2859 if gr is None:
2859 if gr is None:
2860 break
2860 break
2861 if cnt == parents_recursion_limit:
2861 if cnt == parents_recursion_limit:
2862 # this will prevent accidental infinit loops
2862 # this will prevent accidental infinit loops
2863 log.error('more than %s parents found for group %s, stopping '
2863 log.error('more than %s parents found for group %s, stopping '
2864 'recursive parent fetching', parents_recursion_limit, self)
2864 'recursive parent fetching', parents_recursion_limit, self)
2865 break
2865 break
2866
2866
2867 groups.insert(0, gr)
2867 groups.insert(0, gr)
2868 return groups
2868 return groups
2869
2869
2870 @property
2870 @property
2871 def last_commit_cache_update_diff(self):
2871 def last_commit_cache_update_diff(self):
2872 return time.time() - (safe_int(self.changeset_cache.get('updated_on')) or 0)
2872 return time.time() - (safe_int(self.changeset_cache.get('updated_on')) or 0)
2873
2873
2874 @classmethod
2874 @classmethod
2875 def _load_commit_change(cls, last_commit_cache):
2875 def _load_commit_change(cls, last_commit_cache):
2876 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2876 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2877 empty_date = datetime.datetime.fromtimestamp(0)
2877 empty_date = datetime.datetime.fromtimestamp(0)
2878 date_latest = last_commit_cache.get('date', empty_date)
2878 date_latest = last_commit_cache.get('date', empty_date)
2879 try:
2879 try:
2880 return parse_datetime(date_latest)
2880 return parse_datetime(date_latest)
2881 except Exception:
2881 except Exception:
2882 return empty_date
2882 return empty_date
2883
2883
2884 @property
2884 @property
2885 def last_commit_change(self):
2885 def last_commit_change(self):
2886 return self._load_commit_change(self.changeset_cache)
2886 return self._load_commit_change(self.changeset_cache)
2887
2887
2888 @property
2888 @property
2889 def last_db_change(self):
2889 def last_db_change(self):
2890 return self.updated_on
2890 return self.updated_on
2891
2891
2892 @property
2892 @property
2893 def children(self):
2893 def children(self):
2894 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2894 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2895
2895
2896 @property
2896 @property
2897 def name(self):
2897 def name(self):
2898 return self.group_name.split(RepoGroup.url_sep())[-1]
2898 return self.group_name.split(RepoGroup.url_sep())[-1]
2899
2899
2900 @property
2900 @property
2901 def full_path(self):
2901 def full_path(self):
2902 return self.group_name
2902 return self.group_name
2903
2903
2904 @property
2904 @property
2905 def full_path_splitted(self):
2905 def full_path_splitted(self):
2906 return self.group_name.split(RepoGroup.url_sep())
2906 return self.group_name.split(RepoGroup.url_sep())
2907
2907
2908 @property
2908 @property
2909 def repositories(self):
2909 def repositories(self):
2910 return Repository.query()\
2910 return Repository.query()\
2911 .filter(Repository.group == self)\
2911 .filter(Repository.group == self)\
2912 .order_by(Repository.repo_name)
2912 .order_by(Repository.repo_name)
2913
2913
2914 @property
2914 @property
2915 def repositories_recursive_count(self):
2915 def repositories_recursive_count(self):
2916 cnt = self.repositories.count()
2916 cnt = self.repositories.count()
2917
2917
2918 def children_count(group):
2918 def children_count(group):
2919 cnt = 0
2919 cnt = 0
2920 for child in group.children:
2920 for child in group.children:
2921 cnt += child.repositories.count()
2921 cnt += child.repositories.count()
2922 cnt += children_count(child)
2922 cnt += children_count(child)
2923 return cnt
2923 return cnt
2924
2924
2925 return cnt + children_count(self)
2925 return cnt + children_count(self)
2926
2926
2927 def _recursive_objects(self, include_repos=True, include_groups=True):
2927 def _recursive_objects(self, include_repos=True, include_groups=True):
2928 all_ = []
2928 all_ = []
2929
2929
2930 def _get_members(root_gr):
2930 def _get_members(root_gr):
2931 if include_repos:
2931 if include_repos:
2932 for r in root_gr.repositories:
2932 for r in root_gr.repositories:
2933 all_.append(r)
2933 all_.append(r)
2934 childs = root_gr.children.all()
2934 childs = root_gr.children.all()
2935 if childs:
2935 if childs:
2936 for gr in childs:
2936 for gr in childs:
2937 if include_groups:
2937 if include_groups:
2938 all_.append(gr)
2938 all_.append(gr)
2939 _get_members(gr)
2939 _get_members(gr)
2940
2940
2941 root_group = []
2941 root_group = []
2942 if include_groups:
2942 if include_groups:
2943 root_group = [self]
2943 root_group = [self]
2944
2944
2945 _get_members(self)
2945 _get_members(self)
2946 return root_group + all_
2946 return root_group + all_
2947
2947
2948 def recursive_groups_and_repos(self):
2948 def recursive_groups_and_repos(self):
2949 """
2949 """
2950 Recursive return all groups, with repositories in those groups
2950 Recursive return all groups, with repositories in those groups
2951 """
2951 """
2952 return self._recursive_objects()
2952 return self._recursive_objects()
2953
2953
2954 def recursive_groups(self):
2954 def recursive_groups(self):
2955 """
2955 """
2956 Returns all children groups for this group including children of children
2956 Returns all children groups for this group including children of children
2957 """
2957 """
2958 return self._recursive_objects(include_repos=False)
2958 return self._recursive_objects(include_repos=False)
2959
2959
2960 def recursive_repos(self):
2960 def recursive_repos(self):
2961 """
2961 """
2962 Returns all children repositories for this group
2962 Returns all children repositories for this group
2963 """
2963 """
2964 return self._recursive_objects(include_groups=False)
2964 return self._recursive_objects(include_groups=False)
2965
2965
2966 def get_new_name(self, group_name):
2966 def get_new_name(self, group_name):
2967 """
2967 """
2968 returns new full group name based on parent and new name
2968 returns new full group name based on parent and new name
2969
2969
2970 :param group_name:
2970 :param group_name:
2971 """
2971 """
2972 path_prefix = (self.parent_group.full_path_splitted if
2972 path_prefix = (self.parent_group.full_path_splitted if
2973 self.parent_group else [])
2973 self.parent_group else [])
2974 return RepoGroup.url_sep().join(path_prefix + [group_name])
2974 return RepoGroup.url_sep().join(path_prefix + [group_name])
2975
2975
2976 def update_commit_cache(self, config=None):
2976 def update_commit_cache(self, config=None):
2977 """
2977 """
2978 Update cache of last commit for newest repository inside this repository group.
2978 Update cache of last commit for newest repository inside this repository group.
2979 cache_keys should be::
2979 cache_keys should be::
2980
2980
2981 source_repo_id
2981 source_repo_id
2982 short_id
2982 short_id
2983 raw_id
2983 raw_id
2984 revision
2984 revision
2985 parents
2985 parents
2986 message
2986 message
2987 date
2987 date
2988 author
2988 author
2989
2989
2990 """
2990 """
2991 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2991 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2992 empty_date = datetime.datetime.fromtimestamp(0)
2992 empty_date = datetime.datetime.fromtimestamp(0)
2993
2993
2994 def repo_groups_and_repos(root_gr):
2994 def repo_groups_and_repos(root_gr):
2995 for _repo in root_gr.repositories:
2995 for _repo in root_gr.repositories:
2996 yield _repo
2996 yield _repo
2997 for child_group in root_gr.children.all():
2997 for child_group in root_gr.children.all():
2998 yield child_group
2998 yield child_group
2999
2999
3000 latest_repo_cs_cache = {}
3000 latest_repo_cs_cache = {}
3001 for obj in repo_groups_and_repos(self):
3001 for obj in repo_groups_and_repos(self):
3002 repo_cs_cache = obj.changeset_cache
3002 repo_cs_cache = obj.changeset_cache
3003 date_latest = latest_repo_cs_cache.get('date', empty_date)
3003 date_latest = latest_repo_cs_cache.get('date', empty_date)
3004 date_current = repo_cs_cache.get('date', empty_date)
3004 date_current = repo_cs_cache.get('date', empty_date)
3005 current_timestamp = datetime_to_time(parse_datetime(date_latest))
3005 current_timestamp = datetime_to_time(parse_datetime(date_latest))
3006 if current_timestamp < datetime_to_time(parse_datetime(date_current)):
3006 if current_timestamp < datetime_to_time(parse_datetime(date_current)):
3007 latest_repo_cs_cache = repo_cs_cache
3007 latest_repo_cs_cache = repo_cs_cache
3008 if hasattr(obj, 'repo_id'):
3008 if hasattr(obj, 'repo_id'):
3009 latest_repo_cs_cache['source_repo_id'] = obj.repo_id
3009 latest_repo_cs_cache['source_repo_id'] = obj.repo_id
3010 else:
3010 else:
3011 latest_repo_cs_cache['source_repo_id'] = repo_cs_cache.get('source_repo_id')
3011 latest_repo_cs_cache['source_repo_id'] = repo_cs_cache.get('source_repo_id')
3012
3012
3013 _date_latest = parse_datetime(latest_repo_cs_cache.get('date') or empty_date)
3013 _date_latest = parse_datetime(latest_repo_cs_cache.get('date') or empty_date)
3014
3014
3015 latest_repo_cs_cache['updated_on'] = time.time()
3015 latest_repo_cs_cache['updated_on'] = time.time()
3016 self.changeset_cache = latest_repo_cs_cache
3016 self.changeset_cache = latest_repo_cs_cache
3017 self.updated_on = _date_latest
3017 self.updated_on = _date_latest
3018 Session().add(self)
3018 Session().add(self)
3019 Session().commit()
3019 Session().commit()
3020
3020
3021 log.debug('updated repo group `%s` with new commit cache %s, and last update_date: %s',
3021 log.debug('updated repo group `%s` with new commit cache %s, and last update_date: %s',
3022 self.group_name, latest_repo_cs_cache, _date_latest)
3022 self.group_name, latest_repo_cs_cache, _date_latest)
3023
3023
3024 def permissions(self, with_admins=True, with_owner=True,
3024 def permissions(self, with_admins=True, with_owner=True,
3025 expand_from_user_groups=False):
3025 expand_from_user_groups=False):
3026 """
3026 """
3027 Permissions for repository groups
3027 Permissions for repository groups
3028 """
3028 """
3029 _admin_perm = 'group.admin'
3029 _admin_perm = 'group.admin'
3030
3030
3031 owner_row = []
3031 owner_row = []
3032 if with_owner:
3032 if with_owner:
3033 usr = AttributeDict(self.user.get_dict())
3033 usr = AttributeDict(self.user.get_dict())
3034 usr.owner_row = True
3034 usr.owner_row = True
3035 usr.permission = _admin_perm
3035 usr.permission = _admin_perm
3036 owner_row.append(usr)
3036 owner_row.append(usr)
3037
3037
3038 super_admin_ids = []
3038 super_admin_ids = []
3039 super_admin_rows = []
3039 super_admin_rows = []
3040 if with_admins:
3040 if with_admins:
3041 for usr in User.get_all_super_admins():
3041 for usr in User.get_all_super_admins():
3042 super_admin_ids.append(usr.user_id)
3042 super_admin_ids.append(usr.user_id)
3043 # if this admin is also owner, don't double the record
3043 # if this admin is also owner, don't double the record
3044 if usr.user_id == owner_row[0].user_id:
3044 if usr.user_id == owner_row[0].user_id:
3045 owner_row[0].admin_row = True
3045 owner_row[0].admin_row = True
3046 else:
3046 else:
3047 usr = AttributeDict(usr.get_dict())
3047 usr = AttributeDict(usr.get_dict())
3048 usr.admin_row = True
3048 usr.admin_row = True
3049 usr.permission = _admin_perm
3049 usr.permission = _admin_perm
3050 super_admin_rows.append(usr)
3050 super_admin_rows.append(usr)
3051
3051
3052 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
3052 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
3053 q = q.options(joinedload(UserRepoGroupToPerm.group),
3053 q = q.options(joinedload(UserRepoGroupToPerm.group),
3054 joinedload(UserRepoGroupToPerm.user),
3054 joinedload(UserRepoGroupToPerm.user),
3055 joinedload(UserRepoGroupToPerm.permission),)
3055 joinedload(UserRepoGroupToPerm.permission),)
3056
3056
3057 # get owners and admins and permissions. We do a trick of re-writing
3057 # get owners and admins and permissions. We do a trick of re-writing
3058 # objects from sqlalchemy to named-tuples due to sqlalchemy session
3058 # objects from sqlalchemy to named-tuples due to sqlalchemy session
3059 # has a global reference and changing one object propagates to all
3059 # has a global reference and changing one object propagates to all
3060 # others. This means if admin is also an owner admin_row that change
3060 # others. This means if admin is also an owner admin_row that change
3061 # would propagate to both objects
3061 # would propagate to both objects
3062 perm_rows = []
3062 perm_rows = []
3063 for _usr in q.all():
3063 for _usr in q.all():
3064 usr = AttributeDict(_usr.user.get_dict())
3064 usr = AttributeDict(_usr.user.get_dict())
3065 # if this user is also owner/admin, mark as duplicate record
3065 # if this user is also owner/admin, mark as duplicate record
3066 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
3066 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
3067 usr.duplicate_perm = True
3067 usr.duplicate_perm = True
3068 usr.permission = _usr.permission.permission_name
3068 usr.permission = _usr.permission.permission_name
3069 perm_rows.append(usr)
3069 perm_rows.append(usr)
3070
3070
3071 # filter the perm rows by 'default' first and then sort them by
3071 # filter the perm rows by 'default' first and then sort them by
3072 # admin,write,read,none permissions sorted again alphabetically in
3072 # admin,write,read,none permissions sorted again alphabetically in
3073 # each group
3073 # each group
3074 perm_rows = sorted(perm_rows, key=display_user_sort)
3074 perm_rows = sorted(perm_rows, key=display_user_sort)
3075
3075
3076 user_groups_rows = []
3076 user_groups_rows = []
3077 if expand_from_user_groups:
3077 if expand_from_user_groups:
3078 for ug in self.permission_user_groups(with_members=True):
3078 for ug in self.permission_user_groups(with_members=True):
3079 for user_data in ug.members:
3079 for user_data in ug.members:
3080 user_groups_rows.append(user_data)
3080 user_groups_rows.append(user_data)
3081
3081
3082 return super_admin_rows + owner_row + perm_rows + user_groups_rows
3082 return super_admin_rows + owner_row + perm_rows + user_groups_rows
3083
3083
3084 def permission_user_groups(self, with_members=False):
3084 def permission_user_groups(self, with_members=False):
3085 q = UserGroupRepoGroupToPerm.query()\
3085 q = UserGroupRepoGroupToPerm.query()\
3086 .filter(UserGroupRepoGroupToPerm.group == self)
3086 .filter(UserGroupRepoGroupToPerm.group == self)
3087 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
3087 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
3088 joinedload(UserGroupRepoGroupToPerm.users_group),
3088 joinedload(UserGroupRepoGroupToPerm.users_group),
3089 joinedload(UserGroupRepoGroupToPerm.permission),)
3089 joinedload(UserGroupRepoGroupToPerm.permission),)
3090
3090
3091 perm_rows = []
3091 perm_rows = []
3092 for _user_group in q.all():
3092 for _user_group in q.all():
3093 entry = AttributeDict(_user_group.users_group.get_dict())
3093 entry = AttributeDict(_user_group.users_group.get_dict())
3094 entry.permission = _user_group.permission.permission_name
3094 entry.permission = _user_group.permission.permission_name
3095 if with_members:
3095 if with_members:
3096 entry.members = [x.user.get_dict()
3096 entry.members = [x.user.get_dict()
3097 for x in _user_group.users_group.members]
3097 for x in _user_group.users_group.members]
3098 perm_rows.append(entry)
3098 perm_rows.append(entry)
3099
3099
3100 perm_rows = sorted(perm_rows, key=display_user_group_sort)
3100 perm_rows = sorted(perm_rows, key=display_user_group_sort)
3101 return perm_rows
3101 return perm_rows
3102
3102
3103 def get_api_data(self):
3103 def get_api_data(self):
3104 """
3104 """
3105 Common function for generating api data
3105 Common function for generating api data
3106
3106
3107 """
3107 """
3108 group = self
3108 group = self
3109 data = {
3109 data = {
3110 'group_id': group.group_id,
3110 'group_id': group.group_id,
3111 'group_name': group.group_name,
3111 'group_name': group.group_name,
3112 'group_description': group.description_safe,
3112 'group_description': group.description_safe,
3113 'parent_group': group.parent_group.group_name if group.parent_group else None,
3113 'parent_group': group.parent_group.group_name if group.parent_group else None,
3114 'repositories': [x.repo_name for x in group.repositories],
3114 'repositories': [x.repo_name for x in group.repositories],
3115 'owner': group.user.username,
3115 'owner': group.user.username,
3116 }
3116 }
3117 return data
3117 return data
3118
3118
3119 def get_dict(self):
3119 def get_dict(self):
3120 # Since we transformed `group_name` to a hybrid property, we need to
3120 # Since we transformed `group_name` to a hybrid property, we need to
3121 # keep compatibility with the code which uses `group_name` field.
3121 # keep compatibility with the code which uses `group_name` field.
3122 result = super(RepoGroup, self).get_dict()
3122 result = super(RepoGroup, self).get_dict()
3123 result['group_name'] = result.pop('_group_name', None)
3123 result['group_name'] = result.pop('_group_name', None)
3124 result.pop('_changeset_cache', '')
3124 result.pop('_changeset_cache', '')
3125 return result
3125 return result
3126
3126
3127
3127
3128 class Permission(Base, BaseModel):
3128 class Permission(Base, BaseModel):
3129 __tablename__ = 'permissions'
3129 __tablename__ = 'permissions'
3130 __table_args__ = (
3130 __table_args__ = (
3131 Index('p_perm_name_idx', 'permission_name'),
3131 Index('p_perm_name_idx', 'permission_name'),
3132 base_table_args,
3132 base_table_args,
3133 )
3133 )
3134
3134
3135 PERMS = [
3135 PERMS = [
3136 ('hg.admin', _('RhodeCode Super Administrator')),
3136 ('hg.admin', _('RhodeCode Super Administrator')),
3137
3137
3138 ('repository.none', _('Repository no access')),
3138 ('repository.none', _('Repository no access')),
3139 ('repository.read', _('Repository read access')),
3139 ('repository.read', _('Repository read access')),
3140 ('repository.write', _('Repository write access')),
3140 ('repository.write', _('Repository write access')),
3141 ('repository.admin', _('Repository admin access')),
3141 ('repository.admin', _('Repository admin access')),
3142
3142
3143 ('group.none', _('Repository group no access')),
3143 ('group.none', _('Repository group no access')),
3144 ('group.read', _('Repository group read access')),
3144 ('group.read', _('Repository group read access')),
3145 ('group.write', _('Repository group write access')),
3145 ('group.write', _('Repository group write access')),
3146 ('group.admin', _('Repository group admin access')),
3146 ('group.admin', _('Repository group admin access')),
3147
3147
3148 ('usergroup.none', _('User group no access')),
3148 ('usergroup.none', _('User group no access')),
3149 ('usergroup.read', _('User group read access')),
3149 ('usergroup.read', _('User group read access')),
3150 ('usergroup.write', _('User group write access')),
3150 ('usergroup.write', _('User group write access')),
3151 ('usergroup.admin', _('User group admin access')),
3151 ('usergroup.admin', _('User group admin access')),
3152
3152
3153 ('branch.none', _('Branch no permissions')),
3153 ('branch.none', _('Branch no permissions')),
3154 ('branch.merge', _('Branch access by web merge')),
3154 ('branch.merge', _('Branch access by web merge')),
3155 ('branch.push', _('Branch access by push')),
3155 ('branch.push', _('Branch access by push')),
3156 ('branch.push_force', _('Branch access by push with force')),
3156 ('branch.push_force', _('Branch access by push with force')),
3157
3157
3158 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
3158 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
3159 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
3159 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
3160
3160
3161 ('hg.usergroup.create.false', _('User Group creation disabled')),
3161 ('hg.usergroup.create.false', _('User Group creation disabled')),
3162 ('hg.usergroup.create.true', _('User Group creation enabled')),
3162 ('hg.usergroup.create.true', _('User Group creation enabled')),
3163
3163
3164 ('hg.create.none', _('Repository creation disabled')),
3164 ('hg.create.none', _('Repository creation disabled')),
3165 ('hg.create.repository', _('Repository creation enabled')),
3165 ('hg.create.repository', _('Repository creation enabled')),
3166 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
3166 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
3167 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
3167 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
3168
3168
3169 ('hg.fork.none', _('Repository forking disabled')),
3169 ('hg.fork.none', _('Repository forking disabled')),
3170 ('hg.fork.repository', _('Repository forking enabled')),
3170 ('hg.fork.repository', _('Repository forking enabled')),
3171
3171
3172 ('hg.register.none', _('Registration disabled')),
3172 ('hg.register.none', _('Registration disabled')),
3173 ('hg.register.manual_activate', _('User Registration with manual account activation')),
3173 ('hg.register.manual_activate', _('User Registration with manual account activation')),
3174 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
3174 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
3175
3175
3176 ('hg.password_reset.enabled', _('Password reset enabled')),
3176 ('hg.password_reset.enabled', _('Password reset enabled')),
3177 ('hg.password_reset.hidden', _('Password reset hidden')),
3177 ('hg.password_reset.hidden', _('Password reset hidden')),
3178 ('hg.password_reset.disabled', _('Password reset disabled')),
3178 ('hg.password_reset.disabled', _('Password reset disabled')),
3179
3179
3180 ('hg.extern_activate.manual', _('Manual activation of external account')),
3180 ('hg.extern_activate.manual', _('Manual activation of external account')),
3181 ('hg.extern_activate.auto', _('Automatic activation of external account')),
3181 ('hg.extern_activate.auto', _('Automatic activation of external account')),
3182
3182
3183 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
3183 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
3184 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
3184 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
3185 ]
3185 ]
3186
3186
3187 # definition of system default permissions for DEFAULT user, created on
3187 # definition of system default permissions for DEFAULT user, created on
3188 # system setup
3188 # system setup
3189 DEFAULT_USER_PERMISSIONS = [
3189 DEFAULT_USER_PERMISSIONS = [
3190 # object perms
3190 # object perms
3191 'repository.read',
3191 'repository.read',
3192 'group.read',
3192 'group.read',
3193 'usergroup.read',
3193 'usergroup.read',
3194 # branch, for backward compat we need same value as before so forced pushed
3194 # branch, for backward compat we need same value as before so forced pushed
3195 'branch.push_force',
3195 'branch.push_force',
3196 # global
3196 # global
3197 'hg.create.repository',
3197 'hg.create.repository',
3198 'hg.repogroup.create.false',
3198 'hg.repogroup.create.false',
3199 'hg.usergroup.create.false',
3199 'hg.usergroup.create.false',
3200 'hg.create.write_on_repogroup.true',
3200 'hg.create.write_on_repogroup.true',
3201 'hg.fork.repository',
3201 'hg.fork.repository',
3202 'hg.register.manual_activate',
3202 'hg.register.manual_activate',
3203 'hg.password_reset.enabled',
3203 'hg.password_reset.enabled',
3204 'hg.extern_activate.auto',
3204 'hg.extern_activate.auto',
3205 'hg.inherit_default_perms.true',
3205 'hg.inherit_default_perms.true',
3206 ]
3206 ]
3207
3207
3208 # defines which permissions are more important higher the more important
3208 # defines which permissions are more important higher the more important
3209 # Weight defines which permissions are more important.
3209 # Weight defines which permissions are more important.
3210 # The higher number the more important.
3210 # The higher number the more important.
3211 PERM_WEIGHTS = {
3211 PERM_WEIGHTS = {
3212 'repository.none': 0,
3212 'repository.none': 0,
3213 'repository.read': 1,
3213 'repository.read': 1,
3214 'repository.write': 3,
3214 'repository.write': 3,
3215 'repository.admin': 4,
3215 'repository.admin': 4,
3216
3216
3217 'group.none': 0,
3217 'group.none': 0,
3218 'group.read': 1,
3218 'group.read': 1,
3219 'group.write': 3,
3219 'group.write': 3,
3220 'group.admin': 4,
3220 'group.admin': 4,
3221
3221
3222 'usergroup.none': 0,
3222 'usergroup.none': 0,
3223 'usergroup.read': 1,
3223 'usergroup.read': 1,
3224 'usergroup.write': 3,
3224 'usergroup.write': 3,
3225 'usergroup.admin': 4,
3225 'usergroup.admin': 4,
3226
3226
3227 'branch.none': 0,
3227 'branch.none': 0,
3228 'branch.merge': 1,
3228 'branch.merge': 1,
3229 'branch.push': 3,
3229 'branch.push': 3,
3230 'branch.push_force': 4,
3230 'branch.push_force': 4,
3231
3231
3232 'hg.repogroup.create.false': 0,
3232 'hg.repogroup.create.false': 0,
3233 'hg.repogroup.create.true': 1,
3233 'hg.repogroup.create.true': 1,
3234
3234
3235 'hg.usergroup.create.false': 0,
3235 'hg.usergroup.create.false': 0,
3236 'hg.usergroup.create.true': 1,
3236 'hg.usergroup.create.true': 1,
3237
3237
3238 'hg.fork.none': 0,
3238 'hg.fork.none': 0,
3239 'hg.fork.repository': 1,
3239 'hg.fork.repository': 1,
3240 'hg.create.none': 0,
3240 'hg.create.none': 0,
3241 'hg.create.repository': 1
3241 'hg.create.repository': 1
3242 }
3242 }
3243
3243
3244 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3244 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3245 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
3245 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
3246 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
3246 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
3247
3247
3248 def __repr__(self):
3248 def __repr__(self):
3249 return "<%s('%s:%s')>" % (
3249 return "<%s('%s:%s')>" % (
3250 self.cls_name, self.permission_id, self.permission_name
3250 self.cls_name, self.permission_id, self.permission_name
3251 )
3251 )
3252
3252
3253 @classmethod
3253 @classmethod
3254 def get_by_key(cls, key):
3254 def get_by_key(cls, key):
3255 return cls.query().filter(cls.permission_name == key).scalar()
3255 return cls.query().filter(cls.permission_name == key).scalar()
3256
3256
3257 @classmethod
3257 @classmethod
3258 def get_default_repo_perms(cls, user_id, repo_id=None):
3258 def get_default_repo_perms(cls, user_id, repo_id=None):
3259 q = Session().query(UserRepoToPerm, Repository, Permission)\
3259 q = Session().query(UserRepoToPerm, Repository, Permission)\
3260 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
3260 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
3261 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
3261 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
3262 .filter(UserRepoToPerm.user_id == user_id)
3262 .filter(UserRepoToPerm.user_id == user_id)
3263 if repo_id:
3263 if repo_id:
3264 q = q.filter(UserRepoToPerm.repository_id == repo_id)
3264 q = q.filter(UserRepoToPerm.repository_id == repo_id)
3265 return q.all()
3265 return q.all()
3266
3266
3267 @classmethod
3267 @classmethod
3268 def get_default_repo_branch_perms(cls, user_id, repo_id=None):
3268 def get_default_repo_branch_perms(cls, user_id, repo_id=None):
3269 q = Session().query(UserToRepoBranchPermission, UserRepoToPerm, Permission) \
3269 q = Session().query(UserToRepoBranchPermission, UserRepoToPerm, Permission) \
3270 .join(
3270 .join(
3271 Permission,
3271 Permission,
3272 UserToRepoBranchPermission.permission_id == Permission.permission_id) \
3272 UserToRepoBranchPermission.permission_id == Permission.permission_id) \
3273 .join(
3273 .join(
3274 UserRepoToPerm,
3274 UserRepoToPerm,
3275 UserToRepoBranchPermission.rule_to_perm_id == UserRepoToPerm.repo_to_perm_id) \
3275 UserToRepoBranchPermission.rule_to_perm_id == UserRepoToPerm.repo_to_perm_id) \
3276 .filter(UserRepoToPerm.user_id == user_id)
3276 .filter(UserRepoToPerm.user_id == user_id)
3277
3277
3278 if repo_id:
3278 if repo_id:
3279 q = q.filter(UserToRepoBranchPermission.repository_id == repo_id)
3279 q = q.filter(UserToRepoBranchPermission.repository_id == repo_id)
3280 return q.order_by(UserToRepoBranchPermission.rule_order).all()
3280 return q.order_by(UserToRepoBranchPermission.rule_order).all()
3281
3281
3282 @classmethod
3282 @classmethod
3283 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
3283 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
3284 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
3284 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
3285 .join(
3285 .join(
3286 Permission,
3286 Permission,
3287 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
3287 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
3288 .join(
3288 .join(
3289 Repository,
3289 Repository,
3290 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
3290 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
3291 .join(
3291 .join(
3292 UserGroup,
3292 UserGroup,
3293 UserGroupRepoToPerm.users_group_id ==
3293 UserGroupRepoToPerm.users_group_id ==
3294 UserGroup.users_group_id)\
3294 UserGroup.users_group_id)\
3295 .join(
3295 .join(
3296 UserGroupMember,
3296 UserGroupMember,
3297 UserGroupRepoToPerm.users_group_id ==
3297 UserGroupRepoToPerm.users_group_id ==
3298 UserGroupMember.users_group_id)\
3298 UserGroupMember.users_group_id)\
3299 .filter(
3299 .filter(
3300 UserGroupMember.user_id == user_id,
3300 UserGroupMember.user_id == user_id,
3301 UserGroup.users_group_active == true())
3301 UserGroup.users_group_active == true())
3302 if repo_id:
3302 if repo_id:
3303 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
3303 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
3304 return q.all()
3304 return q.all()
3305
3305
3306 @classmethod
3306 @classmethod
3307 def get_default_repo_branch_perms_from_user_group(cls, user_id, repo_id=None):
3307 def get_default_repo_branch_perms_from_user_group(cls, user_id, repo_id=None):
3308 q = Session().query(UserGroupToRepoBranchPermission, UserGroupRepoToPerm, Permission) \
3308 q = Session().query(UserGroupToRepoBranchPermission, UserGroupRepoToPerm, Permission) \
3309 .join(
3309 .join(
3310 Permission,
3310 Permission,
3311 UserGroupToRepoBranchPermission.permission_id == Permission.permission_id) \
3311 UserGroupToRepoBranchPermission.permission_id == Permission.permission_id) \
3312 .join(
3312 .join(
3313 UserGroupRepoToPerm,
3313 UserGroupRepoToPerm,
3314 UserGroupToRepoBranchPermission.rule_to_perm_id == UserGroupRepoToPerm.users_group_to_perm_id) \
3314 UserGroupToRepoBranchPermission.rule_to_perm_id == UserGroupRepoToPerm.users_group_to_perm_id) \
3315 .join(
3315 .join(
3316 UserGroup,
3316 UserGroup,
3317 UserGroupRepoToPerm.users_group_id == UserGroup.users_group_id) \
3317 UserGroupRepoToPerm.users_group_id == UserGroup.users_group_id) \
3318 .join(
3318 .join(
3319 UserGroupMember,
3319 UserGroupMember,
3320 UserGroupRepoToPerm.users_group_id == UserGroupMember.users_group_id) \
3320 UserGroupRepoToPerm.users_group_id == UserGroupMember.users_group_id) \
3321 .filter(
3321 .filter(
3322 UserGroupMember.user_id == user_id,
3322 UserGroupMember.user_id == user_id,
3323 UserGroup.users_group_active == true())
3323 UserGroup.users_group_active == true())
3324
3324
3325 if repo_id:
3325 if repo_id:
3326 q = q.filter(UserGroupToRepoBranchPermission.repository_id == repo_id)
3326 q = q.filter(UserGroupToRepoBranchPermission.repository_id == repo_id)
3327 return q.order_by(UserGroupToRepoBranchPermission.rule_order).all()
3327 return q.order_by(UserGroupToRepoBranchPermission.rule_order).all()
3328
3328
3329 @classmethod
3329 @classmethod
3330 def get_default_group_perms(cls, user_id, repo_group_id=None):
3330 def get_default_group_perms(cls, user_id, repo_group_id=None):
3331 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
3331 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
3332 .join(
3332 .join(
3333 Permission,
3333 Permission,
3334 UserRepoGroupToPerm.permission_id == Permission.permission_id)\
3334 UserRepoGroupToPerm.permission_id == Permission.permission_id)\
3335 .join(
3335 .join(
3336 RepoGroup,
3336 RepoGroup,
3337 UserRepoGroupToPerm.group_id == RepoGroup.group_id)\
3337 UserRepoGroupToPerm.group_id == RepoGroup.group_id)\
3338 .filter(UserRepoGroupToPerm.user_id == user_id)
3338 .filter(UserRepoGroupToPerm.user_id == user_id)
3339 if repo_group_id:
3339 if repo_group_id:
3340 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
3340 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
3341 return q.all()
3341 return q.all()
3342
3342
3343 @classmethod
3343 @classmethod
3344 def get_default_group_perms_from_user_group(
3344 def get_default_group_perms_from_user_group(
3345 cls, user_id, repo_group_id=None):
3345 cls, user_id, repo_group_id=None):
3346 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
3346 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
3347 .join(
3347 .join(
3348 Permission,
3348 Permission,
3349 UserGroupRepoGroupToPerm.permission_id ==
3349 UserGroupRepoGroupToPerm.permission_id ==
3350 Permission.permission_id)\
3350 Permission.permission_id)\
3351 .join(
3351 .join(
3352 RepoGroup,
3352 RepoGroup,
3353 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
3353 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
3354 .join(
3354 .join(
3355 UserGroup,
3355 UserGroup,
3356 UserGroupRepoGroupToPerm.users_group_id ==
3356 UserGroupRepoGroupToPerm.users_group_id ==
3357 UserGroup.users_group_id)\
3357 UserGroup.users_group_id)\
3358 .join(
3358 .join(
3359 UserGroupMember,
3359 UserGroupMember,
3360 UserGroupRepoGroupToPerm.users_group_id ==
3360 UserGroupRepoGroupToPerm.users_group_id ==
3361 UserGroupMember.users_group_id)\
3361 UserGroupMember.users_group_id)\
3362 .filter(
3362 .filter(
3363 UserGroupMember.user_id == user_id,
3363 UserGroupMember.user_id == user_id,
3364 UserGroup.users_group_active == true())
3364 UserGroup.users_group_active == true())
3365 if repo_group_id:
3365 if repo_group_id:
3366 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
3366 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
3367 return q.all()
3367 return q.all()
3368
3368
3369 @classmethod
3369 @classmethod
3370 def get_default_user_group_perms(cls, user_id, user_group_id=None):
3370 def get_default_user_group_perms(cls, user_id, user_group_id=None):
3371 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
3371 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
3372 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
3372 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
3373 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
3373 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
3374 .filter(UserUserGroupToPerm.user_id == user_id)
3374 .filter(UserUserGroupToPerm.user_id == user_id)
3375 if user_group_id:
3375 if user_group_id:
3376 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
3376 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
3377 return q.all()
3377 return q.all()
3378
3378
3379 @classmethod
3379 @classmethod
3380 def get_default_user_group_perms_from_user_group(
3380 def get_default_user_group_perms_from_user_group(
3381 cls, user_id, user_group_id=None):
3381 cls, user_id, user_group_id=None):
3382 TargetUserGroup = aliased(UserGroup, name='target_user_group')
3382 TargetUserGroup = aliased(UserGroup, name='target_user_group')
3383 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
3383 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
3384 .join(
3384 .join(
3385 Permission,
3385 Permission,
3386 UserGroupUserGroupToPerm.permission_id ==
3386 UserGroupUserGroupToPerm.permission_id ==
3387 Permission.permission_id)\
3387 Permission.permission_id)\
3388 .join(
3388 .join(
3389 TargetUserGroup,
3389 TargetUserGroup,
3390 UserGroupUserGroupToPerm.target_user_group_id ==
3390 UserGroupUserGroupToPerm.target_user_group_id ==
3391 TargetUserGroup.users_group_id)\
3391 TargetUserGroup.users_group_id)\
3392 .join(
3392 .join(
3393 UserGroup,
3393 UserGroup,
3394 UserGroupUserGroupToPerm.user_group_id ==
3394 UserGroupUserGroupToPerm.user_group_id ==
3395 UserGroup.users_group_id)\
3395 UserGroup.users_group_id)\
3396 .join(
3396 .join(
3397 UserGroupMember,
3397 UserGroupMember,
3398 UserGroupUserGroupToPerm.user_group_id ==
3398 UserGroupUserGroupToPerm.user_group_id ==
3399 UserGroupMember.users_group_id)\
3399 UserGroupMember.users_group_id)\
3400 .filter(
3400 .filter(
3401 UserGroupMember.user_id == user_id,
3401 UserGroupMember.user_id == user_id,
3402 UserGroup.users_group_active == true())
3402 UserGroup.users_group_active == true())
3403 if user_group_id:
3403 if user_group_id:
3404 q = q.filter(
3404 q = q.filter(
3405 UserGroupUserGroupToPerm.user_group_id == user_group_id)
3405 UserGroupUserGroupToPerm.user_group_id == user_group_id)
3406
3406
3407 return q.all()
3407 return q.all()
3408
3408
3409
3409
3410 class UserRepoToPerm(Base, BaseModel):
3410 class UserRepoToPerm(Base, BaseModel):
3411 __tablename__ = 'repo_to_perm'
3411 __tablename__ = 'repo_to_perm'
3412 __table_args__ = (
3412 __table_args__ = (
3413 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
3413 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
3414 base_table_args
3414 base_table_args
3415 )
3415 )
3416
3416
3417 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3417 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3418 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3418 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3419 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3419 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3420 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
3420 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
3421
3421
3422 user = relationship('User', back_populates="repo_to_perm")
3422 user = relationship('User', back_populates="repo_to_perm")
3423 repository = relationship('Repository', back_populates="repo_to_perm")
3423 repository = relationship('Repository', back_populates="repo_to_perm")
3424 permission = relationship('Permission')
3424 permission = relationship('Permission')
3425
3425
3426 branch_perm_entry = relationship('UserToRepoBranchPermission', cascade="all, delete-orphan", lazy='joined', back_populates='user_repo_to_perm')
3426 branch_perm_entry = relationship('UserToRepoBranchPermission', cascade="all, delete-orphan", lazy='joined', back_populates='user_repo_to_perm')
3427
3427
3428 @classmethod
3428 @classmethod
3429 def create(cls, user, repository, permission):
3429 def create(cls, user, repository, permission):
3430 n = cls()
3430 n = cls()
3431 n.user = user
3431 n.user = user
3432 n.repository = repository
3432 n.repository = repository
3433 n.permission = permission
3433 n.permission = permission
3434 Session().add(n)
3434 Session().add(n)
3435 return n
3435 return n
3436
3436
3437 def __repr__(self):
3437 def __repr__(self):
3438 return f'<{self.user} => {self.repository} >'
3438 return f'<{self.user} => {self.repository} >'
3439
3439
3440
3440
3441 class UserUserGroupToPerm(Base, BaseModel):
3441 class UserUserGroupToPerm(Base, BaseModel):
3442 __tablename__ = 'user_user_group_to_perm'
3442 __tablename__ = 'user_user_group_to_perm'
3443 __table_args__ = (
3443 __table_args__ = (
3444 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
3444 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
3445 base_table_args
3445 base_table_args
3446 )
3446 )
3447
3447
3448 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
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 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3449 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3450 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3450 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3451 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3451 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3452
3452
3453 user = relationship('User', back_populates='user_group_to_perm')
3453 user = relationship('User', back_populates='user_group_to_perm')
3454 user_group = relationship('UserGroup', back_populates='user_user_group_to_perm')
3454 user_group = relationship('UserGroup', back_populates='user_user_group_to_perm')
3455 permission = relationship('Permission')
3455 permission = relationship('Permission')
3456
3456
3457 @classmethod
3457 @classmethod
3458 def create(cls, user, user_group, permission):
3458 def create(cls, user, user_group, permission):
3459 n = cls()
3459 n = cls()
3460 n.user = user
3460 n.user = user
3461 n.user_group = user_group
3461 n.user_group = user_group
3462 n.permission = permission
3462 n.permission = permission
3463 Session().add(n)
3463 Session().add(n)
3464 return n
3464 return n
3465
3465
3466 def __repr__(self):
3466 def __repr__(self):
3467 return f'<{self.user} => {self.user_group} >'
3467 return f'<{self.user} => {self.user_group} >'
3468
3468
3469
3469
3470 class UserToPerm(Base, BaseModel):
3470 class UserToPerm(Base, BaseModel):
3471 __tablename__ = 'user_to_perm'
3471 __tablename__ = 'user_to_perm'
3472 __table_args__ = (
3472 __table_args__ = (
3473 UniqueConstraint('user_id', 'permission_id'),
3473 UniqueConstraint('user_id', 'permission_id'),
3474 base_table_args
3474 base_table_args
3475 )
3475 )
3476
3476
3477 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3477 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3478 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3478 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3479 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3479 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3480
3480
3481 user = relationship('User', back_populates='user_perms')
3481 user = relationship('User', back_populates='user_perms')
3482 permission = relationship('Permission', lazy='joined')
3482 permission = relationship('Permission', lazy='joined')
3483
3483
3484 def __repr__(self):
3484 def __repr__(self):
3485 return f'<{self.user} => {self.permission} >'
3485 return f'<{self.user} => {self.permission} >'
3486
3486
3487
3487
3488 class UserGroupRepoToPerm(Base, BaseModel):
3488 class UserGroupRepoToPerm(Base, BaseModel):
3489 __tablename__ = 'users_group_repo_to_perm'
3489 __tablename__ = 'users_group_repo_to_perm'
3490 __table_args__ = (
3490 __table_args__ = (
3491 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
3491 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
3492 base_table_args
3492 base_table_args
3493 )
3493 )
3494
3494
3495 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3495 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3496 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3496 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3497 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3497 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3498 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
3498 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
3499
3499
3500 users_group = relationship('UserGroup', back_populates='users_group_repo_to_perm')
3500 users_group = relationship('UserGroup', back_populates='users_group_repo_to_perm')
3501 permission = relationship('Permission')
3501 permission = relationship('Permission')
3502 repository = relationship('Repository', back_populates='users_group_to_perm')
3502 repository = relationship('Repository', back_populates='users_group_to_perm')
3503 user_group_branch_perms = relationship('UserGroupToRepoBranchPermission', cascade='all', back_populates='user_group_repo_to_perm')
3503 user_group_branch_perms = relationship('UserGroupToRepoBranchPermission', cascade='all', back_populates='user_group_repo_to_perm')
3504
3504
3505 @classmethod
3505 @classmethod
3506 def create(cls, users_group, repository, permission):
3506 def create(cls, users_group, repository, permission):
3507 n = cls()
3507 n = cls()
3508 n.users_group = users_group
3508 n.users_group = users_group
3509 n.repository = repository
3509 n.repository = repository
3510 n.permission = permission
3510 n.permission = permission
3511 Session().add(n)
3511 Session().add(n)
3512 return n
3512 return n
3513
3513
3514 def __repr__(self):
3514 def __repr__(self):
3515 return f'<UserGroupRepoToPerm:{self.users_group} => {self.repository} >'
3515 return f'<UserGroupRepoToPerm:{self.users_group} => {self.repository} >'
3516
3516
3517
3517
3518 class UserGroupUserGroupToPerm(Base, BaseModel):
3518 class UserGroupUserGroupToPerm(Base, BaseModel):
3519 __tablename__ = 'user_group_user_group_to_perm'
3519 __tablename__ = 'user_group_user_group_to_perm'
3520 __table_args__ = (
3520 __table_args__ = (
3521 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
3521 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
3522 CheckConstraint('target_user_group_id != user_group_id'),
3522 CheckConstraint('target_user_group_id != user_group_id'),
3523 base_table_args
3523 base_table_args
3524 )
3524 )
3525
3525
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)
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 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3527 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3528 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3528 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3529 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3529 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3530
3530
3531 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id', back_populates='user_group_user_group_to_perm')
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 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
3532 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
3533 permission = relationship('Permission')
3533 permission = relationship('Permission')
3534
3534
3535 @classmethod
3535 @classmethod
3536 def create(cls, target_user_group, user_group, permission):
3536 def create(cls, target_user_group, user_group, permission):
3537 n = cls()
3537 n = cls()
3538 n.target_user_group = target_user_group
3538 n.target_user_group = target_user_group
3539 n.user_group = user_group
3539 n.user_group = user_group
3540 n.permission = permission
3540 n.permission = permission
3541 Session().add(n)
3541 Session().add(n)
3542 return n
3542 return n
3543
3543
3544 def __repr__(self):
3544 def __repr__(self):
3545 return f'<UserGroupUserGroup:{self.target_user_group} => {self.user_group} >'
3545 return f'<UserGroupUserGroup:{self.target_user_group} => {self.user_group} >'
3546
3546
3547
3547
3548 class UserGroupToPerm(Base, BaseModel):
3548 class UserGroupToPerm(Base, BaseModel):
3549 __tablename__ = 'users_group_to_perm'
3549 __tablename__ = 'users_group_to_perm'
3550 __table_args__ = (
3550 __table_args__ = (
3551 UniqueConstraint('users_group_id', 'permission_id',),
3551 UniqueConstraint('users_group_id', 'permission_id',),
3552 base_table_args
3552 base_table_args
3553 )
3553 )
3554
3554
3555 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3555 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3556 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3556 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3557 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3557 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3558
3558
3559 users_group = relationship('UserGroup', back_populates='users_group_to_perm')
3559 users_group = relationship('UserGroup', back_populates='users_group_to_perm')
3560 permission = relationship('Permission')
3560 permission = relationship('Permission')
3561
3561
3562
3562
3563 class UserRepoGroupToPerm(Base, BaseModel):
3563 class UserRepoGroupToPerm(Base, BaseModel):
3564 __tablename__ = 'user_repo_group_to_perm'
3564 __tablename__ = 'user_repo_group_to_perm'
3565 __table_args__ = (
3565 __table_args__ = (
3566 UniqueConstraint('user_id', 'group_id', 'permission_id'),
3566 UniqueConstraint('user_id', 'group_id', 'permission_id'),
3567 base_table_args
3567 base_table_args
3568 )
3568 )
3569
3569
3570 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3570 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3571 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3571 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3572 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
3572 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
3573 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3573 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3574
3574
3575 user = relationship('User', back_populates='repo_group_to_perm')
3575 user = relationship('User', back_populates='repo_group_to_perm')
3576 group = relationship('RepoGroup', back_populates='repo_group_to_perm')
3576 group = relationship('RepoGroup', back_populates='repo_group_to_perm')
3577 permission = relationship('Permission')
3577 permission = relationship('Permission')
3578
3578
3579 @classmethod
3579 @classmethod
3580 def create(cls, user, repository_group, permission):
3580 def create(cls, user, repository_group, permission):
3581 n = cls()
3581 n = cls()
3582 n.user = user
3582 n.user = user
3583 n.group = repository_group
3583 n.group = repository_group
3584 n.permission = permission
3584 n.permission = permission
3585 Session().add(n)
3585 Session().add(n)
3586 return n
3586 return n
3587
3587
3588
3588
3589 class UserGroupRepoGroupToPerm(Base, BaseModel):
3589 class UserGroupRepoGroupToPerm(Base, BaseModel):
3590 __tablename__ = 'users_group_repo_group_to_perm'
3590 __tablename__ = 'users_group_repo_group_to_perm'
3591 __table_args__ = (
3591 __table_args__ = (
3592 UniqueConstraint('users_group_id', 'group_id'),
3592 UniqueConstraint('users_group_id', 'group_id'),
3593 base_table_args
3593 base_table_args
3594 )
3594 )
3595
3595
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)
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 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3597 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3598 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
3598 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
3599 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3599 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3600
3600
3601 users_group = relationship('UserGroup', back_populates='users_group_repo_group_to_perm')
3601 users_group = relationship('UserGroup', back_populates='users_group_repo_group_to_perm')
3602 permission = relationship('Permission')
3602 permission = relationship('Permission')
3603 group = relationship('RepoGroup', back_populates='users_group_to_perm')
3603 group = relationship('RepoGroup', back_populates='users_group_to_perm')
3604
3604
3605 @classmethod
3605 @classmethod
3606 def create(cls, user_group, repository_group, permission):
3606 def create(cls, user_group, repository_group, permission):
3607 n = cls()
3607 n = cls()
3608 n.users_group = user_group
3608 n.users_group = user_group
3609 n.group = repository_group
3609 n.group = repository_group
3610 n.permission = permission
3610 n.permission = permission
3611 Session().add(n)
3611 Session().add(n)
3612 return n
3612 return n
3613
3613
3614 def __repr__(self):
3614 def __repr__(self):
3615 return '<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
3615 return '<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
3616
3616
3617
3617
3618 class Statistics(Base, BaseModel):
3618 class Statistics(Base, BaseModel):
3619 __tablename__ = 'statistics'
3619 __tablename__ = 'statistics'
3620 __table_args__ = (
3620 __table_args__ = (
3621 base_table_args
3621 base_table_args
3622 )
3622 )
3623
3623
3624 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3624 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3625 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
3625 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
3626 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
3626 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
3627 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False) #JSON data
3627 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False) #JSON data
3628 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False) #JSON data
3628 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False) #JSON data
3629 languages = Column("languages", LargeBinary(1000000), nullable=False) #JSON data
3629 languages = Column("languages", LargeBinary(1000000), nullable=False) #JSON data
3630
3630
3631 repository = relationship('Repository', single_parent=True, viewonly=True)
3631 repository = relationship('Repository', single_parent=True, viewonly=True)
3632
3632
3633
3633
3634 class UserFollowing(Base, BaseModel):
3634 class UserFollowing(Base, BaseModel):
3635 __tablename__ = 'user_followings'
3635 __tablename__ = 'user_followings'
3636 __table_args__ = (
3636 __table_args__ = (
3637 UniqueConstraint('user_id', 'follows_repository_id'),
3637 UniqueConstraint('user_id', 'follows_repository_id'),
3638 UniqueConstraint('user_id', 'follows_user_id'),
3638 UniqueConstraint('user_id', 'follows_user_id'),
3639 base_table_args
3639 base_table_args
3640 )
3640 )
3641
3641
3642 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3642 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3643 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3643 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3644 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
3644 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
3645 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
3645 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
3646 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
3646 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
3647
3647
3648 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id', back_populates='followings')
3648 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id', back_populates='followings')
3649
3649
3650 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
3650 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
3651 follows_repository = relationship('Repository', order_by='Repository.repo_name', back_populates='followers')
3651 follows_repository = relationship('Repository', order_by='Repository.repo_name', back_populates='followers')
3652
3652
3653 @classmethod
3653 @classmethod
3654 def get_repo_followers(cls, repo_id):
3654 def get_repo_followers(cls, repo_id):
3655 return cls.query().filter(cls.follows_repo_id == repo_id)
3655 return cls.query().filter(cls.follows_repo_id == repo_id)
3656
3656
3657
3657
3658 class CacheKey(Base, BaseModel):
3658 class CacheKey(Base, BaseModel):
3659 __tablename__ = 'cache_invalidation'
3659 __tablename__ = 'cache_invalidation'
3660 __table_args__ = (
3660 __table_args__ = (
3661 UniqueConstraint('cache_key'),
3661 UniqueConstraint('cache_key'),
3662 Index('key_idx', 'cache_key'),
3662 Index('key_idx', 'cache_key'),
3663 Index('cache_args_idx', 'cache_args'),
3663 Index('cache_args_idx', 'cache_args'),
3664 base_table_args,
3664 base_table_args,
3665 )
3665 )
3666
3666
3667 CACHE_TYPE_FEED = 'FEED'
3667 CACHE_TYPE_FEED = 'FEED'
3668
3668
3669 # namespaces used to register process/thread aware caches
3669 # namespaces used to register process/thread aware caches
3670 REPO_INVALIDATION_NAMESPACE = 'repo_cache.v1:{repo_id}'
3670 REPO_INVALIDATION_NAMESPACE = 'repo_cache.v1:{repo_id}'
3671
3671
3672 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3672 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3673 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
3673 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
3674 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
3674 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
3675 cache_state_uid = Column("cache_state_uid", String(255), nullable=True, unique=None, default=None)
3675 cache_state_uid = Column("cache_state_uid", String(255), nullable=True, unique=None, default=None)
3676 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
3676 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
3677
3677
3678 def __init__(self, cache_key, cache_args='', cache_state_uid=None):
3678 def __init__(self, cache_key, cache_args='', cache_state_uid=None):
3679 self.cache_key = cache_key
3679 self.cache_key = cache_key
3680 self.cache_args = cache_args
3680 self.cache_args = cache_args
3681 self.cache_active = False
3681 self.cache_active = False
3682 # first key should be same for all entries, since all workers should share it
3682 # first key should be same for all entries, since all workers should share it
3683 self.cache_state_uid = cache_state_uid or self.generate_new_state_uid()
3683 self.cache_state_uid = cache_state_uid or self.generate_new_state_uid()
3684
3684
3685 def __repr__(self):
3685 def __repr__(self):
3686 return "<%s('%s:%s[%s]')>" % (
3686 return "<%s('%s:%s[%s]')>" % (
3687 self.cls_name,
3687 self.cls_name,
3688 self.cache_id, self.cache_key, self.cache_active)
3688 self.cache_id, self.cache_key, self.cache_active)
3689
3689
3690 def _cache_key_partition(self):
3690 def _cache_key_partition(self):
3691 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
3691 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
3692 return prefix, repo_name, suffix
3692 return prefix, repo_name, suffix
3693
3693
3694 def get_prefix(self):
3694 def get_prefix(self):
3695 """
3695 """
3696 Try to extract prefix from existing cache key. The key could consist
3696 Try to extract prefix from existing cache key. The key could consist
3697 of prefix, repo_name, suffix
3697 of prefix, repo_name, suffix
3698 """
3698 """
3699 # this returns prefix, repo_name, suffix
3699 # this returns prefix, repo_name, suffix
3700 return self._cache_key_partition()[0]
3700 return self._cache_key_partition()[0]
3701
3701
3702 def get_suffix(self):
3702 def get_suffix(self):
3703 """
3703 """
3704 get suffix that might have been used in _get_cache_key to
3704 get suffix that might have been used in _get_cache_key to
3705 generate self.cache_key. Only used for informational purposes
3705 generate self.cache_key. Only used for informational purposes
3706 in repo_edit.mako.
3706 in repo_edit.mako.
3707 """
3707 """
3708 # prefix, repo_name, suffix
3708 # prefix, repo_name, suffix
3709 return self._cache_key_partition()[2]
3709 return self._cache_key_partition()[2]
3710
3710
3711 @classmethod
3711 @classmethod
3712 def generate_new_state_uid(cls, based_on=None):
3712 def generate_new_state_uid(cls, based_on=None):
3713 if based_on:
3713 if based_on:
3714 return str(uuid.uuid5(uuid.NAMESPACE_URL, safe_str(based_on)))
3714 return str(uuid.uuid5(uuid.NAMESPACE_URL, safe_str(based_on)))
3715 else:
3715 else:
3716 return str(uuid.uuid4())
3716 return str(uuid.uuid4())
3717
3717
3718 @classmethod
3718 @classmethod
3719 def delete_all_cache(cls):
3719 def delete_all_cache(cls):
3720 """
3720 """
3721 Delete all cache keys from database.
3721 Delete all cache keys from database.
3722 Should only be run when all instances are down and all entries
3722 Should only be run when all instances are down and all entries
3723 thus stale.
3723 thus stale.
3724 """
3724 """
3725 cls.query().delete()
3725 cls.query().delete()
3726 Session().commit()
3726 Session().commit()
3727
3727
3728 @classmethod
3728 @classmethod
3729 def set_invalidate(cls, cache_uid, delete=False):
3729 def set_invalidate(cls, cache_uid, delete=False):
3730 """
3730 """
3731 Mark all caches of a repo as invalid in the database.
3731 Mark all caches of a repo as invalid in the database.
3732 """
3732 """
3733
3733
3734 try:
3734 try:
3735 qry = Session().query(cls).filter(cls.cache_args == cache_uid)
3735 qry = Session().query(cls).filter(cls.cache_args == cache_uid)
3736 if delete:
3736 if delete:
3737 qry.delete()
3737 qry.delete()
3738 log.debug('cache objects deleted for cache args %s',
3738 log.debug('cache objects deleted for cache args %s',
3739 safe_str(cache_uid))
3739 safe_str(cache_uid))
3740 else:
3740 else:
3741 qry.update({"cache_active": False,
3741 qry.update({"cache_active": False,
3742 "cache_state_uid": cls.generate_new_state_uid()})
3742 "cache_state_uid": cls.generate_new_state_uid()})
3743 log.debug('cache objects marked as invalid for cache args %s',
3743 log.debug('cache objects marked as invalid for cache args %s',
3744 safe_str(cache_uid))
3744 safe_str(cache_uid))
3745
3745
3746 Session().commit()
3746 Session().commit()
3747 except Exception:
3747 except Exception:
3748 log.exception(
3748 log.exception(
3749 'Cache key invalidation failed for cache args %s',
3749 'Cache key invalidation failed for cache args %s',
3750 safe_str(cache_uid))
3750 safe_str(cache_uid))
3751 Session().rollback()
3751 Session().rollback()
3752
3752
3753 @classmethod
3753 @classmethod
3754 def get_active_cache(cls, cache_key):
3754 def get_active_cache(cls, cache_key):
3755 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
3755 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
3756 if inv_obj:
3756 if inv_obj:
3757 return inv_obj
3757 return inv_obj
3758 return None
3758 return None
3759
3759
3760 @classmethod
3760 @classmethod
3761 def get_namespace_map(cls, namespace):
3761 def get_namespace_map(cls, namespace):
3762 return {
3762 return {
3763 x.cache_key: x
3763 x.cache_key: x
3764 for x in cls.query().filter(cls.cache_args == namespace)}
3764 for x in cls.query().filter(cls.cache_args == namespace)}
3765
3765
3766
3766
3767 class ChangesetComment(Base, BaseModel):
3767 class ChangesetComment(Base, BaseModel):
3768 __tablename__ = 'changeset_comments'
3768 __tablename__ = 'changeset_comments'
3769 __table_args__ = (
3769 __table_args__ = (
3770 Index('cc_revision_idx', 'revision'),
3770 Index('cc_revision_idx', 'revision'),
3771 base_table_args,
3771 base_table_args,
3772 )
3772 )
3773
3773
3774 COMMENT_OUTDATED = 'comment_outdated'
3774 COMMENT_OUTDATED = 'comment_outdated'
3775 COMMENT_TYPE_NOTE = 'note'
3775 COMMENT_TYPE_NOTE = 'note'
3776 COMMENT_TYPE_TODO = 'todo'
3776 COMMENT_TYPE_TODO = 'todo'
3777 COMMENT_TYPES = [COMMENT_TYPE_NOTE, COMMENT_TYPE_TODO]
3777 COMMENT_TYPES = [COMMENT_TYPE_NOTE, COMMENT_TYPE_TODO]
3778
3778
3779 OP_IMMUTABLE = 'immutable'
3779 OP_IMMUTABLE = 'immutable'
3780 OP_CHANGEABLE = 'changeable'
3780 OP_CHANGEABLE = 'changeable'
3781
3781
3782 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
3782 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
3783 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3783 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3784 revision = Column('revision', String(40), nullable=True)
3784 revision = Column('revision', String(40), nullable=True)
3785 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3785 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3786 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
3786 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
3787 line_no = Column('line_no', Unicode(10), nullable=True)
3787 line_no = Column('line_no', Unicode(10), nullable=True)
3788 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
3788 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
3789 f_path = Column('f_path', Unicode(1000), nullable=True)
3789 f_path = Column('f_path', Unicode(1000), nullable=True)
3790 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
3790 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
3791 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
3791 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
3792 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3792 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3793 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3793 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3794 renderer = Column('renderer', Unicode(64), nullable=True)
3794 renderer = Column('renderer', Unicode(64), nullable=True)
3795 display_state = Column('display_state', Unicode(128), nullable=True)
3795 display_state = Column('display_state', Unicode(128), nullable=True)
3796 immutable_state = Column('immutable_state', Unicode(128), nullable=True, default=OP_CHANGEABLE)
3796 immutable_state = Column('immutable_state', Unicode(128), nullable=True, default=OP_CHANGEABLE)
3797 draft = Column('draft', Boolean(), nullable=True, default=False)
3797 draft = Column('draft', Boolean(), nullable=True, default=False)
3798
3798
3799 comment_type = Column('comment_type', Unicode(128), nullable=True, default=COMMENT_TYPE_NOTE)
3799 comment_type = Column('comment_type', Unicode(128), nullable=True, default=COMMENT_TYPE_NOTE)
3800 resolved_comment_id = Column('resolved_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=True)
3800 resolved_comment_id = Column('resolved_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=True)
3801
3801
3802 resolved_comment = relationship('ChangesetComment', remote_side=comment_id, back_populates='resolved_by')
3802 resolved_comment = relationship('ChangesetComment', remote_side=comment_id, back_populates='resolved_by')
3803 resolved_by = relationship('ChangesetComment', back_populates='resolved_comment')
3803 resolved_by = relationship('ChangesetComment', back_populates='resolved_comment')
3804
3804
3805 author = relationship('User', lazy='select', back_populates='user_comments')
3805 author = relationship('User', lazy='select', back_populates='user_comments')
3806 repo = relationship('Repository', back_populates='comments')
3806 repo = relationship('Repository', back_populates='comments')
3807 status_change = relationship('ChangesetStatus', cascade="all, delete-orphan", lazy='select', back_populates='comment')
3807 status_change = relationship('ChangesetStatus', cascade="all, delete-orphan", lazy='select', back_populates='comment')
3808 pull_request = relationship('PullRequest', lazy='select', back_populates='comments')
3808 pull_request = relationship('PullRequest', lazy='select', back_populates='comments')
3809 pull_request_version = relationship('PullRequestVersion', lazy='select')
3809 pull_request_version = relationship('PullRequestVersion', lazy='select')
3810 history = relationship('ChangesetCommentHistory', cascade='all, delete-orphan', lazy='select', order_by='ChangesetCommentHistory.version', back_populates="comment")
3810 history = relationship('ChangesetCommentHistory', cascade='all, delete-orphan', lazy='select', order_by='ChangesetCommentHistory.version', back_populates="comment")
3811
3811
3812 @classmethod
3812 @classmethod
3813 def get_users(cls, revision=None, pull_request_id=None):
3813 def get_users(cls, revision=None, pull_request_id=None):
3814 """
3814 """
3815 Returns user associated with this ChangesetComment. ie those
3815 Returns user associated with this ChangesetComment. ie those
3816 who actually commented
3816 who actually commented
3817
3817
3818 :param cls:
3818 :param cls:
3819 :param revision:
3819 :param revision:
3820 """
3820 """
3821 q = Session().query(User).join(ChangesetComment.author)
3821 q = Session().query(User).join(ChangesetComment.author)
3822 if revision:
3822 if revision:
3823 q = q.filter(cls.revision == revision)
3823 q = q.filter(cls.revision == revision)
3824 elif pull_request_id:
3824 elif pull_request_id:
3825 q = q.filter(cls.pull_request_id == pull_request_id)
3825 q = q.filter(cls.pull_request_id == pull_request_id)
3826 return q.all()
3826 return q.all()
3827
3827
3828 @classmethod
3828 @classmethod
3829 def get_index_from_version(cls, pr_version, versions=None, num_versions=None) -> int:
3829 def get_index_from_version(cls, pr_version, versions=None, num_versions=None) -> int:
3830 if pr_version is None:
3830 if pr_version is None:
3831 return 0
3831 return 0
3832
3832
3833 if versions is not None:
3833 if versions is not None:
3834 num_versions = [x.pull_request_version_id for x in versions]
3834 num_versions = [x.pull_request_version_id for x in versions]
3835
3835
3836 num_versions = num_versions or []
3836 num_versions = num_versions or []
3837 try:
3837 try:
3838 return num_versions.index(pr_version) + 1
3838 return num_versions.index(pr_version) + 1
3839 except (IndexError, ValueError):
3839 except (IndexError, ValueError):
3840 return 0
3840 return 0
3841
3841
3842 @property
3842 @property
3843 def outdated(self):
3843 def outdated(self):
3844 return self.display_state == self.COMMENT_OUTDATED
3844 return self.display_state == self.COMMENT_OUTDATED
3845
3845
3846 @property
3846 @property
3847 def outdated_js(self):
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 @property
3850 @property
3851 def immutable(self):
3851 def immutable(self):
3852 return self.immutable_state == self.OP_IMMUTABLE
3852 return self.immutable_state == self.OP_IMMUTABLE
3853
3853
3854 def outdated_at_version(self, version):
3854 def outdated_at_version(self, version):
3855 """
3855 """
3856 Checks if comment is outdated for given pull request version
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 def version_check():
3867 def version_check():
3859 return self.pull_request_version_id and self.pull_request_version_id != version
3868 return self.pull_request_version_id and self.pull_request_version_id != version
3860
3869
3861 if self.is_inline:
3870 if self.is_inline:
3862 return self.outdated and version_check()
3871 return self.outdated and version_check()
3863 else:
3872 else:
3864 # general comments don't have .outdated set, also latest don't have a version
3873 # general comments don't have .outdated set, also latest don't have a version
3865 return version_check()
3874 return version_check()
3866
3875
3867 def outdated_at_version_js(self, version):
3876 def outdated_at_version_js(self, version):
3868 """
3877 """
3869 Checks if comment is outdated for given pull request version
3878 Checks if comment is outdated for given pull request version
3870 """
3879 """
3871 return json.dumps(self.outdated_at_version(version))
3880 return str_json(self.outdated_at_version(version))
3872
3881
3873 def older_than_version(self, version):
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
3887
3876 """
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 cur_ver = 0
3897 cur_ver = 0
3878 if self.pull_request_version:
3898 if self.pull_request_version and self.pull_request_version.pull_request_version_id is not None:
3879 cur_ver = self.pull_request_version.pull_request_version_id or cur_ver
3899 cur_ver = self.pull_request_version.pull_request_version_id
3880
3900
3881 if version is None:
3901 # Return True if the current version is less than the given version
3882 return cur_ver != version
3883
3884 return cur_ver < version
3902 return cur_ver < version
3885
3903
3886 def older_than_version_js(self, version):
3904 def older_than_version_js(self, version):
3887 """
3905 """
3888 Checks if comment is made from previous version than given
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 @property
3910 @property
3893 def commit_id(self):
3911 def commit_id(self):
3894 """New style naming to stop using .revision"""
3912 """New style naming to stop using .revision"""
3895 return self.revision
3913 return self.revision
3896
3914
3897 @property
3915 @property
3898 def resolved(self):
3916 def resolved(self):
3899 return self.resolved_by[0] if self.resolved_by else None
3917 return self.resolved_by[0] if self.resolved_by else None
3900
3918
3901 @property
3919 @property
3902 def is_todo(self):
3920 def is_todo(self):
3903 return self.comment_type == self.COMMENT_TYPE_TODO
3921 return self.comment_type == self.COMMENT_TYPE_TODO
3904
3922
3905 @property
3923 @property
3906 def is_inline(self):
3924 def is_inline(self):
3907 if self.line_no and self.f_path:
3925 if self.line_no and self.f_path:
3908 return True
3926 return True
3909 return False
3927 return False
3910
3928
3911 @property
3929 @property
3912 def last_version(self):
3930 def last_version(self):
3913 version = 0
3931 version = 0
3914 if self.history:
3932 if self.history:
3915 version = self.history[-1].version
3933 version = self.history[-1].version
3916 return version
3934 return version
3917
3935
3918 def get_index_version(self, versions):
3936 def get_index_version(self, versions):
3919 return self.get_index_from_version(
3937 return self.get_index_from_version(
3920 self.pull_request_version_id, versions)
3938 self.pull_request_version_id, versions)
3921
3939
3922 @property
3940 @property
3923 def review_status(self):
3941 def review_status(self):
3924 if self.status_change:
3942 if self.status_change:
3925 return self.status_change[0].status
3943 return self.status_change[0].status
3926
3944
3927 @property
3945 @property
3928 def review_status_lbl(self):
3946 def review_status_lbl(self):
3929 if self.status_change:
3947 if self.status_change:
3930 return self.status_change[0].status_lbl
3948 return self.status_change[0].status_lbl
3931
3949
3932 def __repr__(self):
3950 def __repr__(self):
3933 if self.comment_id:
3951 if self.comment_id:
3934 return f'<DB:Comment #{self.comment_id}>'
3952 return f'<DB:Comment #{self.comment_id}>'
3935 else:
3953 else:
3936 return f'<DB:Comment at {id(self)!r}>'
3954 return f'<DB:Comment at {id(self)!r}>'
3937
3955
3938 def get_api_data(self):
3956 def get_api_data(self):
3939 comment = self
3957 comment = self
3940
3958
3941 data = {
3959 data = {
3942 'comment_id': comment.comment_id,
3960 'comment_id': comment.comment_id,
3943 'comment_type': comment.comment_type,
3961 'comment_type': comment.comment_type,
3944 'comment_text': comment.text,
3962 'comment_text': comment.text,
3945 'comment_status': comment.status_change,
3963 'comment_status': comment.status_change,
3946 'comment_f_path': comment.f_path,
3964 'comment_f_path': comment.f_path,
3947 'comment_lineno': comment.line_no,
3965 'comment_lineno': comment.line_no,
3948 'comment_author': comment.author,
3966 'comment_author': comment.author,
3949 'comment_created_on': comment.created_on,
3967 'comment_created_on': comment.created_on,
3950 'comment_resolved_by': self.resolved,
3968 'comment_resolved_by': self.resolved,
3951 'comment_commit_id': comment.revision,
3969 'comment_commit_id': comment.revision,
3952 'comment_pull_request_id': comment.pull_request_id,
3970 'comment_pull_request_id': comment.pull_request_id,
3953 'comment_last_version': self.last_version
3971 'comment_last_version': self.last_version
3954 }
3972 }
3955 return data
3973 return data
3956
3974
3957 def __json__(self):
3975 def __json__(self):
3958 data = dict()
3976 data = dict()
3959 data.update(self.get_api_data())
3977 data.update(self.get_api_data())
3960 return data
3978 return data
3961
3979
3962
3980
3963 class ChangesetCommentHistory(Base, BaseModel):
3981 class ChangesetCommentHistory(Base, BaseModel):
3964 __tablename__ = 'changeset_comments_history'
3982 __tablename__ = 'changeset_comments_history'
3965 __table_args__ = (
3983 __table_args__ = (
3966 Index('cch_comment_id_idx', 'comment_id'),
3984 Index('cch_comment_id_idx', 'comment_id'),
3967 base_table_args,
3985 base_table_args,
3968 )
3986 )
3969
3987
3970 comment_history_id = Column('comment_history_id', Integer(), nullable=False, primary_key=True)
3988 comment_history_id = Column('comment_history_id', Integer(), nullable=False, primary_key=True)
3971 comment_id = Column('comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=False)
3989 comment_id = Column('comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=False)
3972 version = Column("version", Integer(), nullable=False, default=0)
3990 version = Column("version", Integer(), nullable=False, default=0)
3973 created_by_user_id = Column('created_by_user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
3991 created_by_user_id = Column('created_by_user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
3974 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
3992 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
3975 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3993 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3976 deleted = Column('deleted', Boolean(), default=False)
3994 deleted = Column('deleted', Boolean(), default=False)
3977
3995
3978 author = relationship('User', lazy='joined')
3996 author = relationship('User', lazy='joined')
3979 comment = relationship('ChangesetComment', cascade="all, delete", back_populates="history")
3997 comment = relationship('ChangesetComment', cascade="all, delete", back_populates="history")
3980
3998
3981 @classmethod
3999 @classmethod
3982 def get_version(cls, comment_id):
4000 def get_version(cls, comment_id):
3983 q = Session().query(ChangesetCommentHistory).filter(
4001 q = Session().query(ChangesetCommentHistory).filter(
3984 ChangesetCommentHistory.comment_id == comment_id).order_by(ChangesetCommentHistory.version.desc())
4002 ChangesetCommentHistory.comment_id == comment_id).order_by(ChangesetCommentHistory.version.desc())
3985 if q.count() == 0:
4003 if q.count() == 0:
3986 return 1
4004 return 1
3987 elif q.count() >= q[0].version:
4005 elif q.count() >= q[0].version:
3988 return q.count() + 1
4006 return q.count() + 1
3989 else:
4007 else:
3990 return q[0].version + 1
4008 return q[0].version + 1
3991
4009
3992
4010
3993 class ChangesetStatus(Base, BaseModel):
4011 class ChangesetStatus(Base, BaseModel):
3994 __tablename__ = 'changeset_statuses'
4012 __tablename__ = 'changeset_statuses'
3995 __table_args__ = (
4013 __table_args__ = (
3996 Index('cs_revision_idx', 'revision'),
4014 Index('cs_revision_idx', 'revision'),
3997 Index('cs_version_idx', 'version'),
4015 Index('cs_version_idx', 'version'),
3998 UniqueConstraint('repo_id', 'revision', 'version'),
4016 UniqueConstraint('repo_id', 'revision', 'version'),
3999 base_table_args
4017 base_table_args
4000 )
4018 )
4001
4019
4002 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
4020 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
4003 STATUS_APPROVED = 'approved'
4021 STATUS_APPROVED = 'approved'
4004 STATUS_REJECTED = 'rejected'
4022 STATUS_REJECTED = 'rejected'
4005 STATUS_UNDER_REVIEW = 'under_review'
4023 STATUS_UNDER_REVIEW = 'under_review'
4006
4024
4007 STATUSES = [
4025 STATUSES = [
4008 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
4026 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
4009 (STATUS_APPROVED, _("Approved")),
4027 (STATUS_APPROVED, _("Approved")),
4010 (STATUS_REJECTED, _("Rejected")),
4028 (STATUS_REJECTED, _("Rejected")),
4011 (STATUS_UNDER_REVIEW, _("Under Review")),
4029 (STATUS_UNDER_REVIEW, _("Under Review")),
4012 ]
4030 ]
4013
4031
4014 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
4032 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
4015 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
4033 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
4016 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
4034 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
4017 revision = Column('revision', String(40), nullable=False)
4035 revision = Column('revision', String(40), nullable=False)
4018 status = Column('status', String(128), nullable=False, default=DEFAULT)
4036 status = Column('status', String(128), nullable=False, default=DEFAULT)
4019 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
4037 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
4020 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
4038 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
4021 version = Column('version', Integer(), nullable=False, default=0)
4039 version = Column('version', Integer(), nullable=False, default=0)
4022 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
4040 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
4023
4041
4024 author = relationship('User', lazy='select')
4042 author = relationship('User', lazy='select')
4025 repo = relationship('Repository', lazy='select')
4043 repo = relationship('Repository', lazy='select')
4026 comment = relationship('ChangesetComment', lazy='select', back_populates='status_change')
4044 comment = relationship('ChangesetComment', lazy='select', back_populates='status_change')
4027 pull_request = relationship('PullRequest', lazy='select', back_populates='statuses')
4045 pull_request = relationship('PullRequest', lazy='select', back_populates='statuses')
4028
4046
4029 def __repr__(self):
4047 def __repr__(self):
4030 return f"<{self.cls_name}('{self.status}[v{self.version}]:{self.author}')>"
4048 return f"<{self.cls_name}('{self.status}[v{self.version}]:{self.author}')>"
4031
4049
4032 @classmethod
4050 @classmethod
4033 def get_status_lbl(cls, value):
4051 def get_status_lbl(cls, value):
4034 return dict(cls.STATUSES).get(value)
4052 return dict(cls.STATUSES).get(value)
4035
4053
4036 @property
4054 @property
4037 def status_lbl(self):
4055 def status_lbl(self):
4038 return ChangesetStatus.get_status_lbl(self.status)
4056 return ChangesetStatus.get_status_lbl(self.status)
4039
4057
4040 def get_api_data(self):
4058 def get_api_data(self):
4041 status = self
4059 status = self
4042 data = {
4060 data = {
4043 'status_id': status.changeset_status_id,
4061 'status_id': status.changeset_status_id,
4044 'status': status.status,
4062 'status': status.status,
4045 }
4063 }
4046 return data
4064 return data
4047
4065
4048 def __json__(self):
4066 def __json__(self):
4049 data = dict()
4067 data = dict()
4050 data.update(self.get_api_data())
4068 data.update(self.get_api_data())
4051 return data
4069 return data
4052
4070
4053
4071
4054 class _SetState(object):
4072 class _SetState(object):
4055 """
4073 """
4056 Context processor allowing changing state for sensitive operation such as
4074 Context processor allowing changing state for sensitive operation such as
4057 pull request update or merge
4075 pull request update or merge
4058 """
4076 """
4059
4077
4060 def __init__(self, pull_request, pr_state, back_state=None):
4078 def __init__(self, pull_request, pr_state, back_state=None):
4061 self._pr = pull_request
4079 self._pr = pull_request
4062 self._org_state = back_state or pull_request.pull_request_state
4080 self._org_state = back_state or pull_request.pull_request_state
4063 self._pr_state = pr_state
4081 self._pr_state = pr_state
4064 self._current_state = None
4082 self._current_state = None
4065
4083
4066 def __enter__(self):
4084 def __enter__(self):
4067 log.debug('StateLock: entering set state context of pr %s, setting state to: `%s`',
4085 log.debug('StateLock: entering set state context of pr %s, setting state to: `%s`',
4068 self._pr, self._pr_state)
4086 self._pr, self._pr_state)
4069 self.set_pr_state(self._pr_state)
4087 self.set_pr_state(self._pr_state)
4070 return self
4088 return self
4071
4089
4072 def __exit__(self, exc_type, exc_val, exc_tb):
4090 def __exit__(self, exc_type, exc_val, exc_tb):
4073 if exc_val is not None or exc_type is not None:
4091 if exc_val is not None or exc_type is not None:
4074 log.error(traceback.format_tb(exc_tb))
4092 log.error(traceback.format_tb(exc_tb))
4075 return None
4093 return None
4076
4094
4077 self.set_pr_state(self._org_state)
4095 self.set_pr_state(self._org_state)
4078 log.debug('StateLock: exiting set state context of pr %s, setting state to: `%s`',
4096 log.debug('StateLock: exiting set state context of pr %s, setting state to: `%s`',
4079 self._pr, self._org_state)
4097 self._pr, self._org_state)
4080
4098
4081 @property
4099 @property
4082 def state(self):
4100 def state(self):
4083 return self._current_state
4101 return self._current_state
4084
4102
4085 def set_pr_state(self, pr_state):
4103 def set_pr_state(self, pr_state):
4086 try:
4104 try:
4087 self._pr.pull_request_state = pr_state
4105 self._pr.pull_request_state = pr_state
4088 Session().add(self._pr)
4106 Session().add(self._pr)
4089 Session().commit()
4107 Session().commit()
4090 self._current_state = pr_state
4108 self._current_state = pr_state
4091 except Exception:
4109 except Exception:
4092 log.exception('Failed to set PullRequest %s state to %s', self._pr, pr_state)
4110 log.exception('Failed to set PullRequest %s state to %s', self._pr, pr_state)
4093 raise
4111 raise
4094
4112
4095
4113
4096 class _PullRequestBase(BaseModel):
4114 class _PullRequestBase(BaseModel):
4097 """
4115 """
4098 Common attributes of pull request and version entries.
4116 Common attributes of pull request and version entries.
4099 """
4117 """
4100
4118
4101 # .status values
4119 # .status values
4102 STATUS_NEW = 'new'
4120 STATUS_NEW = 'new'
4103 STATUS_OPEN = 'open'
4121 STATUS_OPEN = 'open'
4104 STATUS_CLOSED = 'closed'
4122 STATUS_CLOSED = 'closed'
4105
4123
4106 # available states
4124 # available states
4107 STATE_CREATING = 'creating'
4125 STATE_CREATING = 'creating'
4108 STATE_UPDATING = 'updating'
4126 STATE_UPDATING = 'updating'
4109 STATE_MERGING = 'merging'
4127 STATE_MERGING = 'merging'
4110 STATE_CREATED = 'created'
4128 STATE_CREATED = 'created'
4111
4129
4112 title = Column('title', Unicode(255), nullable=True)
4130 title = Column('title', Unicode(255), nullable=True)
4113 description = Column(
4131 description = Column(
4114 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
4132 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
4115 nullable=True)
4133 nullable=True)
4116 description_renderer = Column('description_renderer', Unicode(64), nullable=True)
4134 description_renderer = Column('description_renderer', Unicode(64), nullable=True)
4117
4135
4118 # new/open/closed status of pull request (not approve/reject/etc)
4136 # new/open/closed status of pull request (not approve/reject/etc)
4119 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
4137 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
4120 created_on = Column(
4138 created_on = Column(
4121 'created_on', DateTime(timezone=False), nullable=False,
4139 'created_on', DateTime(timezone=False), nullable=False,
4122 default=datetime.datetime.now)
4140 default=datetime.datetime.now)
4123 updated_on = Column(
4141 updated_on = Column(
4124 'updated_on', DateTime(timezone=False), nullable=False,
4142 'updated_on', DateTime(timezone=False), nullable=False,
4125 default=datetime.datetime.now)
4143 default=datetime.datetime.now)
4126
4144
4127 pull_request_state = Column("pull_request_state", String(255), nullable=True)
4145 pull_request_state = Column("pull_request_state", String(255), nullable=True)
4128
4146
4129 @declared_attr
4147 @declared_attr
4130 def user_id(cls):
4148 def user_id(cls):
4131 return Column(
4149 return Column(
4132 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
4150 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
4133 unique=None)
4151 unique=None)
4134
4152
4135 # 500 revisions max
4153 # 500 revisions max
4136 _revisions = Column(
4154 _revisions = Column(
4137 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
4155 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
4138
4156
4139 common_ancestor_id = Column('common_ancestor_id', Unicode(255), nullable=True)
4157 common_ancestor_id = Column('common_ancestor_id', Unicode(255), nullable=True)
4140
4158
4141 @declared_attr
4159 @declared_attr
4142 def source_repo_id(cls):
4160 def source_repo_id(cls):
4143 # TODO: dan: rename column to source_repo_id
4161 # TODO: dan: rename column to source_repo_id
4144 return Column(
4162 return Column(
4145 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
4163 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
4146 nullable=False)
4164 nullable=False)
4147
4165
4148 @declared_attr
4166 @declared_attr
4149 def pr_source(cls):
4167 def pr_source(cls):
4150 return relationship(
4168 return relationship(
4151 'Repository',
4169 'Repository',
4152 primaryjoin=f'{cls.__name__}.source_repo_id==Repository.repo_id',
4170 primaryjoin=f'{cls.__name__}.source_repo_id==Repository.repo_id',
4153 overlaps="pull_requests_source"
4171 overlaps="pull_requests_source"
4154 )
4172 )
4155
4173
4156 _source_ref = Column('org_ref', Unicode(255), nullable=False)
4174 _source_ref = Column('org_ref', Unicode(255), nullable=False)
4157
4175
4158 @hybrid_property
4176 @hybrid_property
4159 def source_ref(self):
4177 def source_ref(self):
4160 return self._source_ref
4178 return self._source_ref
4161
4179
4162 @source_ref.setter
4180 @source_ref.setter
4163 def source_ref(self, val):
4181 def source_ref(self, val):
4164 parts = (val or '').split(':')
4182 parts = (val or '').split(':')
4165 if len(parts) != 3:
4183 if len(parts) != 3:
4166 raise ValueError(
4184 raise ValueError(
4167 'Invalid reference format given: {}, expected X:Y:Z'.format(val))
4185 'Invalid reference format given: {}, expected X:Y:Z'.format(val))
4168 self._source_ref = safe_str(val)
4186 self._source_ref = safe_str(val)
4169
4187
4170 _target_ref = Column('other_ref', Unicode(255), nullable=False)
4188 _target_ref = Column('other_ref', Unicode(255), nullable=False)
4171
4189
4172 @hybrid_property
4190 @hybrid_property
4173 def target_ref(self):
4191 def target_ref(self):
4174 return self._target_ref
4192 return self._target_ref
4175
4193
4176 @target_ref.setter
4194 @target_ref.setter
4177 def target_ref(self, val):
4195 def target_ref(self, val):
4178 parts = (val or '').split(':')
4196 parts = (val or '').split(':')
4179 if len(parts) != 3:
4197 if len(parts) != 3:
4180 raise ValueError(
4198 raise ValueError(
4181 'Invalid reference format given: {}, expected X:Y:Z'.format(val))
4199 'Invalid reference format given: {}, expected X:Y:Z'.format(val))
4182 self._target_ref = safe_str(val)
4200 self._target_ref = safe_str(val)
4183
4201
4184 @declared_attr
4202 @declared_attr
4185 def target_repo_id(cls):
4203 def target_repo_id(cls):
4186 # TODO: dan: rename column to target_repo_id
4204 # TODO: dan: rename column to target_repo_id
4187 return Column(
4205 return Column(
4188 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
4206 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
4189 nullable=False)
4207 nullable=False)
4190
4208
4191 @declared_attr
4209 @declared_attr
4192 def pr_target(cls):
4210 def pr_target(cls):
4193 return relationship(
4211 return relationship(
4194 'Repository',
4212 'Repository',
4195 primaryjoin=f'{cls.__name__}.target_repo_id==Repository.repo_id',
4213 primaryjoin=f'{cls.__name__}.target_repo_id==Repository.repo_id',
4196 overlaps="pull_requests_target"
4214 overlaps="pull_requests_target"
4197 )
4215 )
4198
4216
4199 _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True)
4217 _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True)
4200
4218
4201 # TODO: dan: rename column to last_merge_source_rev
4219 # TODO: dan: rename column to last_merge_source_rev
4202 _last_merge_source_rev = Column(
4220 _last_merge_source_rev = Column(
4203 'last_merge_org_rev', String(40), nullable=True)
4221 'last_merge_org_rev', String(40), nullable=True)
4204 # TODO: dan: rename column to last_merge_target_rev
4222 # TODO: dan: rename column to last_merge_target_rev
4205 _last_merge_target_rev = Column(
4223 _last_merge_target_rev = Column(
4206 'last_merge_other_rev', String(40), nullable=True)
4224 'last_merge_other_rev', String(40), nullable=True)
4207 _last_merge_status = Column('merge_status', Integer(), nullable=True)
4225 _last_merge_status = Column('merge_status', Integer(), nullable=True)
4208 last_merge_metadata = Column(
4226 last_merge_metadata = Column(
4209 'last_merge_metadata', MutationObj.as_mutable(
4227 'last_merge_metadata', MutationObj.as_mutable(
4210 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4228 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4211
4229
4212 merge_rev = Column('merge_rev', String(40), nullable=True)
4230 merge_rev = Column('merge_rev', String(40), nullable=True)
4213
4231
4214 reviewer_data = Column(
4232 reviewer_data = Column(
4215 'reviewer_data_json', MutationObj.as_mutable(
4233 'reviewer_data_json', MutationObj.as_mutable(
4216 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4234 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4217
4235
4218 @property
4236 @property
4219 def reviewer_data_json(self):
4237 def reviewer_data_json(self):
4220 return json.dumps(self.reviewer_data)
4238 return str_json(self.reviewer_data)
4221
4239
4222 @property
4240 @property
4223 def last_merge_metadata_parsed(self):
4241 def last_merge_metadata_parsed(self):
4224 metadata = {}
4242 metadata = {}
4225 if not self.last_merge_metadata:
4243 if not self.last_merge_metadata:
4226 return metadata
4244 return metadata
4227
4245
4228 if hasattr(self.last_merge_metadata, 'de_coerce'):
4246 if hasattr(self.last_merge_metadata, 'de_coerce'):
4229 for k, v in self.last_merge_metadata.de_coerce().items():
4247 for k, v in self.last_merge_metadata.de_coerce().items():
4230 if k in ['target_ref', 'source_ref']:
4248 if k in ['target_ref', 'source_ref']:
4231 metadata[k] = Reference(v['type'], v['name'], v['commit_id'])
4249 metadata[k] = Reference(v['type'], v['name'], v['commit_id'])
4232 else:
4250 else:
4233 if hasattr(v, 'de_coerce'):
4251 if hasattr(v, 'de_coerce'):
4234 metadata[k] = v.de_coerce()
4252 metadata[k] = v.de_coerce()
4235 else:
4253 else:
4236 metadata[k] = v
4254 metadata[k] = v
4237 return metadata
4255 return metadata
4238
4256
4239 @property
4257 @property
4240 def work_in_progress(self):
4258 def work_in_progress(self):
4241 """checks if pull request is work in progress by checking the title"""
4259 """checks if pull request is work in progress by checking the title"""
4242 title = self.title.upper()
4260 title = self.title.upper()
4243 if re.match(r'^(\[WIP\]\s*|WIP:\s*|WIP\s+)', title):
4261 if re.match(r'^(\[WIP\]\s*|WIP:\s*|WIP\s+)', title):
4244 return True
4262 return True
4245 return False
4263 return False
4246
4264
4247 @property
4265 @property
4248 def title_safe(self):
4266 def title_safe(self):
4249 return self.title\
4267 return self.title\
4250 .replace('{', '{{')\
4268 .replace('{', '{{')\
4251 .replace('}', '}}')
4269 .replace('}', '}}')
4252
4270
4253 @hybrid_property
4271 @hybrid_property
4254 def description_safe(self):
4272 def description_safe(self):
4255 from rhodecode.lib import helpers as h
4273 from rhodecode.lib import helpers as h
4256 return h.escape(self.description)
4274 return h.escape(self.description)
4257
4275
4258 @hybrid_property
4276 @hybrid_property
4259 def revisions(self):
4277 def revisions(self):
4260 return self._revisions.split(':') if self._revisions else []
4278 return self._revisions.split(':') if self._revisions else []
4261
4279
4262 @revisions.setter
4280 @revisions.setter
4263 def revisions(self, val):
4281 def revisions(self, val):
4264 self._revisions = ':'.join(val)
4282 self._revisions = ':'.join(val)
4265
4283
4266 @hybrid_property
4284 @hybrid_property
4267 def last_merge_status(self):
4285 def last_merge_status(self):
4268 return safe_int(self._last_merge_status)
4286 return safe_int(self._last_merge_status)
4269
4287
4270 @last_merge_status.setter
4288 @last_merge_status.setter
4271 def last_merge_status(self, val):
4289 def last_merge_status(self, val):
4272 self._last_merge_status = val
4290 self._last_merge_status = val
4273
4291
4274 @declared_attr
4292 @declared_attr
4275 def author(cls):
4293 def author(cls):
4276 return relationship(
4294 return relationship(
4277 'User', lazy='joined',
4295 'User', lazy='joined',
4278 #TODO, problem that is somehow :?
4296 #TODO, problem that is somehow :?
4279 #back_populates='user_pull_requests'
4297 #back_populates='user_pull_requests'
4280 )
4298 )
4281
4299
4282 @declared_attr
4300 @declared_attr
4283 def source_repo(cls):
4301 def source_repo(cls):
4284 return relationship(
4302 return relationship(
4285 'Repository',
4303 'Repository',
4286 primaryjoin=f'{cls.__name__}.source_repo_id==Repository.repo_id',
4304 primaryjoin=f'{cls.__name__}.source_repo_id==Repository.repo_id',
4287 #back_populates=''
4305 #back_populates=''
4288 )
4306 )
4289
4307
4290 @property
4308 @property
4291 def source_ref_parts(self):
4309 def source_ref_parts(self):
4292 return self.unicode_to_reference(self.source_ref)
4310 return self.unicode_to_reference(self.source_ref)
4293
4311
4294 @declared_attr
4312 @declared_attr
4295 def target_repo(cls):
4313 def target_repo(cls):
4296 return relationship(
4314 return relationship(
4297 'Repository',
4315 'Repository',
4298 primaryjoin=f'{cls.__name__}.target_repo_id==Repository.repo_id'
4316 primaryjoin=f'{cls.__name__}.target_repo_id==Repository.repo_id'
4299 )
4317 )
4300
4318
4301 @property
4319 @property
4302 def target_ref_parts(self):
4320 def target_ref_parts(self):
4303 return self.unicode_to_reference(self.target_ref)
4321 return self.unicode_to_reference(self.target_ref)
4304
4322
4305 @property
4323 @property
4306 def shadow_merge_ref(self):
4324 def shadow_merge_ref(self):
4307 return self.unicode_to_reference(self._shadow_merge_ref)
4325 return self.unicode_to_reference(self._shadow_merge_ref)
4308
4326
4309 @shadow_merge_ref.setter
4327 @shadow_merge_ref.setter
4310 def shadow_merge_ref(self, ref):
4328 def shadow_merge_ref(self, ref):
4311 self._shadow_merge_ref = self.reference_to_unicode(ref)
4329 self._shadow_merge_ref = self.reference_to_unicode(ref)
4312
4330
4313 @staticmethod
4331 @staticmethod
4314 def unicode_to_reference(raw):
4332 def unicode_to_reference(raw):
4315 return unicode_to_reference(raw)
4333 return unicode_to_reference(raw)
4316
4334
4317 @staticmethod
4335 @staticmethod
4318 def reference_to_unicode(ref):
4336 def reference_to_unicode(ref):
4319 return reference_to_unicode(ref)
4337 return reference_to_unicode(ref)
4320
4338
4321 def get_api_data(self, with_merge_state=True):
4339 def get_api_data(self, with_merge_state=True):
4322 from rhodecode.model.pull_request import PullRequestModel
4340 from rhodecode.model.pull_request import PullRequestModel
4323
4341
4324 pull_request = self
4342 pull_request = self
4325 if with_merge_state:
4343 if with_merge_state:
4326 merge_response, merge_status, msg = \
4344 merge_response, merge_status, msg = \
4327 PullRequestModel().merge_status(pull_request)
4345 PullRequestModel().merge_status(pull_request)
4328 merge_state = {
4346 merge_state = {
4329 'status': merge_status,
4347 'status': merge_status,
4330 'message': safe_str(msg),
4348 'message': safe_str(msg),
4331 }
4349 }
4332 else:
4350 else:
4333 merge_state = {'status': 'not_available',
4351 merge_state = {'status': 'not_available',
4334 'message': 'not_available'}
4352 'message': 'not_available'}
4335
4353
4336 merge_data = {
4354 merge_data = {
4337 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
4355 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
4338 'reference': (
4356 'reference': (
4339 pull_request.shadow_merge_ref.asdict()
4357 pull_request.shadow_merge_ref.asdict()
4340 if pull_request.shadow_merge_ref else None),
4358 if pull_request.shadow_merge_ref else None),
4341 }
4359 }
4342
4360
4343 data = {
4361 data = {
4344 'pull_request_id': pull_request.pull_request_id,
4362 'pull_request_id': pull_request.pull_request_id,
4345 'url': PullRequestModel().get_url(pull_request),
4363 'url': PullRequestModel().get_url(pull_request),
4346 'title': pull_request.title,
4364 'title': pull_request.title,
4347 'description': pull_request.description,
4365 'description': pull_request.description,
4348 'status': pull_request.status,
4366 'status': pull_request.status,
4349 'state': pull_request.pull_request_state,
4367 'state': pull_request.pull_request_state,
4350 'created_on': pull_request.created_on,
4368 'created_on': pull_request.created_on,
4351 'updated_on': pull_request.updated_on,
4369 'updated_on': pull_request.updated_on,
4352 'commit_ids': pull_request.revisions,
4370 'commit_ids': pull_request.revisions,
4353 'review_status': pull_request.calculated_review_status(),
4371 'review_status': pull_request.calculated_review_status(),
4354 'mergeable': merge_state,
4372 'mergeable': merge_state,
4355 'source': {
4373 'source': {
4356 'clone_url': pull_request.source_repo.clone_url(),
4374 'clone_url': pull_request.source_repo.clone_url(),
4357 'repository': pull_request.source_repo.repo_name,
4375 'repository': pull_request.source_repo.repo_name,
4358 'reference': {
4376 'reference': {
4359 'name': pull_request.source_ref_parts.name,
4377 'name': pull_request.source_ref_parts.name,
4360 'type': pull_request.source_ref_parts.type,
4378 'type': pull_request.source_ref_parts.type,
4361 'commit_id': pull_request.source_ref_parts.commit_id,
4379 'commit_id': pull_request.source_ref_parts.commit_id,
4362 },
4380 },
4363 },
4381 },
4364 'target': {
4382 'target': {
4365 'clone_url': pull_request.target_repo.clone_url(),
4383 'clone_url': pull_request.target_repo.clone_url(),
4366 'repository': pull_request.target_repo.repo_name,
4384 'repository': pull_request.target_repo.repo_name,
4367 'reference': {
4385 'reference': {
4368 'name': pull_request.target_ref_parts.name,
4386 'name': pull_request.target_ref_parts.name,
4369 'type': pull_request.target_ref_parts.type,
4387 'type': pull_request.target_ref_parts.type,
4370 'commit_id': pull_request.target_ref_parts.commit_id,
4388 'commit_id': pull_request.target_ref_parts.commit_id,
4371 },
4389 },
4372 },
4390 },
4373 'merge': merge_data,
4391 'merge': merge_data,
4374 'author': pull_request.author.get_api_data(include_secrets=False,
4392 'author': pull_request.author.get_api_data(include_secrets=False,
4375 details='basic'),
4393 details='basic'),
4376 'reviewers': [
4394 'reviewers': [
4377 {
4395 {
4378 'user': reviewer.get_api_data(include_secrets=False,
4396 'user': reviewer.get_api_data(include_secrets=False,
4379 details='basic'),
4397 details='basic'),
4380 'reasons': reasons,
4398 'reasons': reasons,
4381 'review_status': st[0][1].status if st else 'not_reviewed',
4399 'review_status': st[0][1].status if st else 'not_reviewed',
4382 }
4400 }
4383 for obj, reviewer, reasons, mandatory, st in
4401 for obj, reviewer, reasons, mandatory, st in
4384 pull_request.reviewers_statuses()
4402 pull_request.reviewers_statuses()
4385 ]
4403 ]
4386 }
4404 }
4387
4405
4388 return data
4406 return data
4389
4407
4390 def set_state(self, pull_request_state, final_state=None):
4408 def set_state(self, pull_request_state, final_state=None):
4391 """
4409 """
4392 # goes from initial state to updating to initial state.
4410 # goes from initial state to updating to initial state.
4393 # initial state can be changed by specifying back_state=
4411 # initial state can be changed by specifying back_state=
4394 with pull_request_obj.set_state(PullRequest.STATE_UPDATING):
4412 with pull_request_obj.set_state(PullRequest.STATE_UPDATING):
4395 pull_request.merge()
4413 pull_request.merge()
4396
4414
4397 :param pull_request_state:
4415 :param pull_request_state:
4398 :param final_state:
4416 :param final_state:
4399
4417
4400 """
4418 """
4401
4419
4402 return _SetState(self, pull_request_state, back_state=final_state)
4420 return _SetState(self, pull_request_state, back_state=final_state)
4403
4421
4404
4422
4405 class PullRequest(Base, _PullRequestBase):
4423 class PullRequest(Base, _PullRequestBase):
4406 __tablename__ = 'pull_requests'
4424 __tablename__ = 'pull_requests'
4407 __table_args__ = (
4425 __table_args__ = (
4408 base_table_args,
4426 base_table_args,
4409 )
4427 )
4410 LATEST_VER = 'latest'
4428 LATEST_VER = 'latest'
4411
4429
4412 pull_request_id = Column(
4430 pull_request_id = Column(
4413 'pull_request_id', Integer(), nullable=False, primary_key=True)
4431 'pull_request_id', Integer(), nullable=False, primary_key=True)
4414
4432
4415 def __repr__(self):
4433 def __repr__(self):
4416 if self.pull_request_id:
4434 if self.pull_request_id:
4417 return f'<DB:PullRequest #{self.pull_request_id}>'
4435 return f'<DB:PullRequest #{self.pull_request_id}>'
4418 else:
4436 else:
4419 return f'<DB:PullRequest at {id(self)!r}>'
4437 return f'<DB:PullRequest at {id(self)!r}>'
4420
4438
4421 reviewers = relationship('PullRequestReviewers', cascade="all, delete-orphan", back_populates='pull_request')
4439 reviewers = relationship('PullRequestReviewers', cascade="all, delete-orphan", back_populates='pull_request')
4422 statuses = relationship('ChangesetStatus', cascade="all, delete-orphan", back_populates='pull_request')
4440 statuses = relationship('ChangesetStatus', cascade="all, delete-orphan", back_populates='pull_request')
4423 comments = relationship('ChangesetComment', cascade="all, delete-orphan", back_populates='pull_request')
4441 comments = relationship('ChangesetComment', cascade="all, delete-orphan", back_populates='pull_request')
4424 versions = relationship('PullRequestVersion', cascade="all, delete-orphan", lazy='dynamic', back_populates='pull_request')
4442 versions = relationship('PullRequestVersion', cascade="all, delete-orphan", lazy='dynamic', back_populates='pull_request')
4425
4443
4426 @classmethod
4444 @classmethod
4427 def get_pr_display_object(cls, pull_request_obj, org_pull_request_obj,
4445 def get_pr_display_object(cls, pull_request_obj, org_pull_request_obj,
4428 internal_methods=None):
4446 internal_methods=None):
4429
4447
4430 class PullRequestDisplay(object):
4448 class PullRequestDisplay(object):
4431 """
4449 """
4432 Special object wrapper for showing PullRequest data via Versions
4450 Special object wrapper for showing PullRequest data via Versions
4433 It mimics PR object as close as possible. This is read only object
4451 It mimics PR object as close as possible. This is read only object
4434 just for display
4452 just for display
4435 """
4453 """
4436
4454
4437 def __init__(self, attrs, internal=None):
4455 def __init__(self, attrs, internal=None):
4438 self.attrs = attrs
4456 self.attrs = attrs
4439 # internal have priority over the given ones via attrs
4457 # internal have priority over the given ones via attrs
4440 self.internal = internal or ['versions']
4458 self.internal = internal or ['versions']
4441
4459
4442 def __getattr__(self, item):
4460 def __getattr__(self, item):
4443 if item in self.internal:
4461 if item in self.internal:
4444 return getattr(self, item)
4462 return getattr(self, item)
4445 try:
4463 try:
4446 return self.attrs[item]
4464 return self.attrs[item]
4447 except KeyError:
4465 except KeyError:
4448 raise AttributeError(
4466 raise AttributeError(
4449 '%s object has no attribute %s' % (self, item))
4467 '%s object has no attribute %s' % (self, item))
4450
4468
4451 def __repr__(self):
4469 def __repr__(self):
4452 pr_id = self.attrs.get('pull_request_id')
4470 pr_id = self.attrs.get('pull_request_id')
4453 return f'<DB:PullRequestDisplay #{pr_id}>'
4471 return f'<DB:PullRequestDisplay #{pr_id}>'
4454
4472
4455 def versions(self):
4473 def versions(self):
4456 return pull_request_obj.versions.order_by(
4474 return pull_request_obj.versions.order_by(
4457 PullRequestVersion.pull_request_version_id).all()
4475 PullRequestVersion.pull_request_version_id).all()
4458
4476
4459 def is_closed(self):
4477 def is_closed(self):
4460 return pull_request_obj.is_closed()
4478 return pull_request_obj.is_closed()
4461
4479
4462 def is_state_changing(self):
4480 def is_state_changing(self):
4463 return pull_request_obj.is_state_changing()
4481 return pull_request_obj.is_state_changing()
4464
4482
4465 @property
4483 @property
4466 def pull_request_version_id(self):
4484 def pull_request_version_id(self):
4467 return getattr(pull_request_obj, 'pull_request_version_id', None)
4485 return getattr(pull_request_obj, 'pull_request_version_id', None)
4468
4486
4469 @property
4487 @property
4470 def pull_request_last_version(self):
4488 def pull_request_last_version(self):
4471 return pull_request_obj.pull_request_last_version
4489 return pull_request_obj.pull_request_last_version
4472
4490
4473 attrs = StrictAttributeDict(pull_request_obj.get_api_data(with_merge_state=False))
4491 attrs = StrictAttributeDict(pull_request_obj.get_api_data(with_merge_state=False))
4474
4492
4475 attrs.author = StrictAttributeDict(
4493 attrs.author = StrictAttributeDict(
4476 pull_request_obj.author.get_api_data())
4494 pull_request_obj.author.get_api_data())
4477 if pull_request_obj.target_repo:
4495 if pull_request_obj.target_repo:
4478 attrs.target_repo = StrictAttributeDict(
4496 attrs.target_repo = StrictAttributeDict(
4479 pull_request_obj.target_repo.get_api_data())
4497 pull_request_obj.target_repo.get_api_data())
4480 attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url
4498 attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url
4481
4499
4482 if pull_request_obj.source_repo:
4500 if pull_request_obj.source_repo:
4483 attrs.source_repo = StrictAttributeDict(
4501 attrs.source_repo = StrictAttributeDict(
4484 pull_request_obj.source_repo.get_api_data())
4502 pull_request_obj.source_repo.get_api_data())
4485 attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url
4503 attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url
4486
4504
4487 attrs.source_ref_parts = pull_request_obj.source_ref_parts
4505 attrs.source_ref_parts = pull_request_obj.source_ref_parts
4488 attrs.target_ref_parts = pull_request_obj.target_ref_parts
4506 attrs.target_ref_parts = pull_request_obj.target_ref_parts
4489 attrs.revisions = pull_request_obj.revisions
4507 attrs.revisions = pull_request_obj.revisions
4490 attrs.common_ancestor_id = pull_request_obj.common_ancestor_id
4508 attrs.common_ancestor_id = pull_request_obj.common_ancestor_id
4491 attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref
4509 attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref
4492 attrs.reviewer_data = org_pull_request_obj.reviewer_data
4510 attrs.reviewer_data = org_pull_request_obj.reviewer_data
4493 attrs.reviewer_data_json = org_pull_request_obj.reviewer_data_json
4511 attrs.reviewer_data_json = org_pull_request_obj.reviewer_data_json
4494
4512
4495 return PullRequestDisplay(attrs, internal=internal_methods)
4513 return PullRequestDisplay(attrs, internal=internal_methods)
4496
4514
4497 def is_closed(self):
4515 def is_closed(self):
4498 return self.status == self.STATUS_CLOSED
4516 return self.status == self.STATUS_CLOSED
4499
4517
4500 def is_state_changing(self):
4518 def is_state_changing(self):
4501 return self.pull_request_state != PullRequest.STATE_CREATED
4519 return self.pull_request_state != PullRequest.STATE_CREATED
4502
4520
4503 def __json__(self):
4521 def __json__(self):
4504 return {
4522 return {
4505 'revisions': self.revisions,
4523 'revisions': self.revisions,
4506 'versions': self.versions_count
4524 'versions': self.versions_count
4507 }
4525 }
4508
4526
4509 def calculated_review_status(self):
4527 def calculated_review_status(self):
4510 from rhodecode.model.changeset_status import ChangesetStatusModel
4528 from rhodecode.model.changeset_status import ChangesetStatusModel
4511 return ChangesetStatusModel().calculated_review_status(self)
4529 return ChangesetStatusModel().calculated_review_status(self)
4512
4530
4513 def reviewers_statuses(self, user=None):
4531 def reviewers_statuses(self, user=None):
4514 from rhodecode.model.changeset_status import ChangesetStatusModel
4532 from rhodecode.model.changeset_status import ChangesetStatusModel
4515 return ChangesetStatusModel().reviewers_statuses(self, user=user)
4533 return ChangesetStatusModel().reviewers_statuses(self, user=user)
4516
4534
4517 def get_pull_request_reviewers(self, role=None):
4535 def get_pull_request_reviewers(self, role=None):
4518 qry = PullRequestReviewers.query()\
4536 qry = PullRequestReviewers.query()\
4519 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)
4537 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)
4520 if role:
4538 if role:
4521 qry = qry.filter(PullRequestReviewers.role == role)
4539 qry = qry.filter(PullRequestReviewers.role == role)
4522
4540
4523 return qry.all()
4541 return qry.all()
4524
4542
4525 @property
4543 @property
4526 def reviewers_count(self):
4544 def reviewers_count(self):
4527 qry = PullRequestReviewers.query()\
4545 qry = PullRequestReviewers.query()\
4528 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)\
4546 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)\
4529 .filter(PullRequestReviewers.role == PullRequestReviewers.ROLE_REVIEWER)
4547 .filter(PullRequestReviewers.role == PullRequestReviewers.ROLE_REVIEWER)
4530 return qry.count()
4548 return qry.count()
4531
4549
4532 @property
4550 @property
4533 def observers_count(self):
4551 def observers_count(self):
4534 qry = PullRequestReviewers.query()\
4552 qry = PullRequestReviewers.query()\
4535 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)\
4553 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)\
4536 .filter(PullRequestReviewers.role == PullRequestReviewers.ROLE_OBSERVER)
4554 .filter(PullRequestReviewers.role == PullRequestReviewers.ROLE_OBSERVER)
4537 return qry.count()
4555 return qry.count()
4538
4556
4539 def observers(self):
4557 def observers(self):
4540 qry = PullRequestReviewers.query()\
4558 qry = PullRequestReviewers.query()\
4541 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)\
4559 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)\
4542 .filter(PullRequestReviewers.role == PullRequestReviewers.ROLE_OBSERVER)\
4560 .filter(PullRequestReviewers.role == PullRequestReviewers.ROLE_OBSERVER)\
4543 .all()
4561 .all()
4544
4562
4545 for entry in qry:
4563 for entry in qry:
4546 yield entry, entry.user
4564 yield entry, entry.user
4547
4565
4548 @property
4566 @property
4549 def workspace_id(self):
4567 def workspace_id(self):
4550 from rhodecode.model.pull_request import PullRequestModel
4568 from rhodecode.model.pull_request import PullRequestModel
4551 return PullRequestModel()._workspace_id(self)
4569 return PullRequestModel()._workspace_id(self)
4552
4570
4553 def get_shadow_repo(self):
4571 def get_shadow_repo(self):
4554 workspace_id = self.workspace_id
4572 workspace_id = self.workspace_id
4555 shadow_repository_path = self.target_repo.get_shadow_repository_path(workspace_id)
4573 shadow_repository_path = self.target_repo.get_shadow_repository_path(workspace_id)
4556 if os.path.isdir(shadow_repository_path):
4574 if os.path.isdir(shadow_repository_path):
4557 vcs_obj = self.target_repo.scm_instance()
4575 vcs_obj = self.target_repo.scm_instance()
4558 return vcs_obj.get_shadow_instance(shadow_repository_path)
4576 return vcs_obj.get_shadow_instance(shadow_repository_path)
4559
4577
4560 @property
4578 @property
4561 def versions_count(self):
4579 def versions_count(self):
4562 """
4580 """
4563 return number of versions this PR have, e.g a PR that once been
4581 return number of versions this PR have, e.g a PR that once been
4564 updated will have 2 versions
4582 updated will have 2 versions
4565 """
4583 """
4566 return self.versions.count() + 1
4584 return self.versions.count() + 1
4567
4585
4568 @property
4586 @property
4569 def pull_request_last_version(self):
4587 def pull_request_last_version(self):
4570 return self.versions_count
4588 return self.versions_count
4571
4589
4572
4590
4573 class PullRequestVersion(Base, _PullRequestBase):
4591 class PullRequestVersion(Base, _PullRequestBase):
4574 __tablename__ = 'pull_request_versions'
4592 __tablename__ = 'pull_request_versions'
4575 __table_args__ = (
4593 __table_args__ = (
4576 base_table_args,
4594 base_table_args,
4577 )
4595 )
4578
4596
4579 pull_request_version_id = Column('pull_request_version_id', Integer(), nullable=False, primary_key=True)
4597 pull_request_version_id = Column('pull_request_version_id', Integer(), nullable=False, primary_key=True)
4580 pull_request_id = Column('pull_request_id', Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=False)
4598 pull_request_id = Column('pull_request_id', Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=False)
4581 pull_request = relationship('PullRequest', back_populates='versions')
4599 pull_request = relationship('PullRequest', back_populates='versions')
4582
4600
4583 def __repr__(self):
4601 def __repr__(self):
4584 if self.pull_request_version_id:
4602 if self.pull_request_version_id:
4585 return f'<DB:PullRequestVersion #{self.pull_request_version_id}>'
4603 return f'<DB:PullRequestVersion #{self.pull_request_version_id}>'
4586 else:
4604 else:
4587 return f'<DB:PullRequestVersion at {id(self)!r}>'
4605 return f'<DB:PullRequestVersion at {id(self)!r}>'
4588
4606
4589 @property
4607 @property
4590 def reviewers(self):
4608 def reviewers(self):
4591 return self.pull_request.reviewers
4609 return self.pull_request.reviewers
4592
4610
4593 @property
4611 @property
4594 def versions(self):
4612 def versions(self):
4595 return self.pull_request.versions
4613 return self.pull_request.versions
4596
4614
4597 def is_closed(self):
4615 def is_closed(self):
4598 # calculate from original
4616 # calculate from original
4599 return self.pull_request.status == self.STATUS_CLOSED
4617 return self.pull_request.status == self.STATUS_CLOSED
4600
4618
4601 def is_state_changing(self):
4619 def is_state_changing(self):
4602 return self.pull_request.pull_request_state != PullRequest.STATE_CREATED
4620 return self.pull_request.pull_request_state != PullRequest.STATE_CREATED
4603
4621
4604 def calculated_review_status(self):
4622 def calculated_review_status(self):
4605 return self.pull_request.calculated_review_status()
4623 return self.pull_request.calculated_review_status()
4606
4624
4607 def reviewers_statuses(self):
4625 def reviewers_statuses(self):
4608 return self.pull_request.reviewers_statuses()
4626 return self.pull_request.reviewers_statuses()
4609
4627
4610 def observers(self):
4628 def observers(self):
4611 return self.pull_request.observers()
4629 return self.pull_request.observers()
4612
4630
4613
4631
4614 class PullRequestReviewers(Base, BaseModel):
4632 class PullRequestReviewers(Base, BaseModel):
4615 __tablename__ = 'pull_request_reviewers'
4633 __tablename__ = 'pull_request_reviewers'
4616 __table_args__ = (
4634 __table_args__ = (
4617 base_table_args,
4635 base_table_args,
4618 )
4636 )
4619 ROLE_REVIEWER = 'reviewer'
4637 ROLE_REVIEWER = 'reviewer'
4620 ROLE_OBSERVER = 'observer'
4638 ROLE_OBSERVER = 'observer'
4621 ROLES = [ROLE_REVIEWER, ROLE_OBSERVER]
4639 ROLES = [ROLE_REVIEWER, ROLE_OBSERVER]
4622
4640
4623 @hybrid_property
4641 @hybrid_property
4624 def reasons(self):
4642 def reasons(self):
4625 if not self._reasons:
4643 if not self._reasons:
4626 return []
4644 return []
4627 return self._reasons
4645 return self._reasons
4628
4646
4629 @reasons.setter
4647 @reasons.setter
4630 def reasons(self, val):
4648 def reasons(self, val):
4631 val = val or []
4649 val = val or []
4632 if any(not isinstance(x, str) for x in val):
4650 if any(not isinstance(x, str) for x in val):
4633 raise Exception('invalid reasons type, must be list of strings')
4651 raise Exception('invalid reasons type, must be list of strings')
4634 self._reasons = val
4652 self._reasons = val
4635
4653
4636 pull_requests_reviewers_id = Column(
4654 pull_requests_reviewers_id = Column(
4637 'pull_requests_reviewers_id', Integer(), nullable=False,
4655 'pull_requests_reviewers_id', Integer(), nullable=False,
4638 primary_key=True)
4656 primary_key=True)
4639 pull_request_id = Column(
4657 pull_request_id = Column(
4640 "pull_request_id", Integer(),
4658 "pull_request_id", Integer(),
4641 ForeignKey('pull_requests.pull_request_id'), nullable=False)
4659 ForeignKey('pull_requests.pull_request_id'), nullable=False)
4642 user_id = Column(
4660 user_id = Column(
4643 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
4661 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
4644 _reasons = Column(
4662 _reasons = Column(
4645 'reason', MutationList.as_mutable(
4663 'reason', MutationList.as_mutable(
4646 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
4664 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
4647
4665
4648 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4666 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4649 role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
4667 role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
4650
4668
4651 user = relationship('User')
4669 user = relationship('User')
4652 pull_request = relationship('PullRequest', back_populates='reviewers')
4670 pull_request = relationship('PullRequest', back_populates='reviewers')
4653
4671
4654 rule_data = Column(
4672 rule_data = Column(
4655 'rule_data_json',
4673 'rule_data_json',
4656 JsonType(dialect_map=dict(mysql=UnicodeText(16384))))
4674 JsonType(dialect_map=dict(mysql=UnicodeText(16384))))
4657
4675
4658 def rule_user_group_data(self):
4676 def rule_user_group_data(self):
4659 """
4677 """
4660 Returns the voting user group rule data for this reviewer
4678 Returns the voting user group rule data for this reviewer
4661 """
4679 """
4662
4680
4663 if self.rule_data and 'vote_rule' in self.rule_data:
4681 if self.rule_data and 'vote_rule' in self.rule_data:
4664 user_group_data = {}
4682 user_group_data = {}
4665 if 'rule_user_group_entry_id' in self.rule_data:
4683 if 'rule_user_group_entry_id' in self.rule_data:
4666 # means a group with voting rules !
4684 # means a group with voting rules !
4667 user_group_data['id'] = self.rule_data['rule_user_group_entry_id']
4685 user_group_data['id'] = self.rule_data['rule_user_group_entry_id']
4668 user_group_data['name'] = self.rule_data['rule_name']
4686 user_group_data['name'] = self.rule_data['rule_name']
4669 user_group_data['vote_rule'] = self.rule_data['vote_rule']
4687 user_group_data['vote_rule'] = self.rule_data['vote_rule']
4670
4688
4671 return user_group_data
4689 return user_group_data
4672
4690
4673 @classmethod
4691 @classmethod
4674 def get_pull_request_reviewers(cls, pull_request_id, role=None):
4692 def get_pull_request_reviewers(cls, pull_request_id, role=None):
4675 qry = PullRequestReviewers.query()\
4693 qry = PullRequestReviewers.query()\
4676 .filter(PullRequestReviewers.pull_request_id == pull_request_id)
4694 .filter(PullRequestReviewers.pull_request_id == pull_request_id)
4677 if role:
4695 if role:
4678 qry = qry.filter(PullRequestReviewers.role == role)
4696 qry = qry.filter(PullRequestReviewers.role == role)
4679
4697
4680 return qry.all()
4698 return qry.all()
4681
4699
4682 def __repr__(self):
4700 def __repr__(self):
4683 return f"<{self.cls_name}('id:{self.pull_requests_reviewers_id}')>"
4701 return f"<{self.cls_name}('id:{self.pull_requests_reviewers_id}')>"
4684
4702
4685
4703
4686 class Notification(Base, BaseModel):
4704 class Notification(Base, BaseModel):
4687 __tablename__ = 'notifications'
4705 __tablename__ = 'notifications'
4688 __table_args__ = (
4706 __table_args__ = (
4689 Index('notification_type_idx', 'type'),
4707 Index('notification_type_idx', 'type'),
4690 base_table_args,
4708 base_table_args,
4691 )
4709 )
4692
4710
4693 TYPE_CHANGESET_COMMENT = 'cs_comment'
4711 TYPE_CHANGESET_COMMENT = 'cs_comment'
4694 TYPE_MESSAGE = 'message'
4712 TYPE_MESSAGE = 'message'
4695 TYPE_MENTION = 'mention'
4713 TYPE_MENTION = 'mention'
4696 TYPE_REGISTRATION = 'registration'
4714 TYPE_REGISTRATION = 'registration'
4697 TYPE_PULL_REQUEST = 'pull_request'
4715 TYPE_PULL_REQUEST = 'pull_request'
4698 TYPE_PULL_REQUEST_COMMENT = 'pull_request_comment'
4716 TYPE_PULL_REQUEST_COMMENT = 'pull_request_comment'
4699 TYPE_PULL_REQUEST_UPDATE = 'pull_request_update'
4717 TYPE_PULL_REQUEST_UPDATE = 'pull_request_update'
4700
4718
4701 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
4719 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
4702 subject = Column('subject', Unicode(512), nullable=True)
4720 subject = Column('subject', Unicode(512), nullable=True)
4703 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
4721 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
4704 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
4722 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
4705 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4723 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4706 type_ = Column('type', Unicode(255))
4724 type_ = Column('type', Unicode(255))
4707
4725
4708 created_by_user = relationship('User', back_populates='user_created_notifications')
4726 created_by_user = relationship('User', back_populates='user_created_notifications')
4709 notifications_to_users = relationship('UserNotification', lazy='joined', cascade="all, delete-orphan", back_populates='notification')
4727 notifications_to_users = relationship('UserNotification', lazy='joined', cascade="all, delete-orphan", back_populates='notification')
4710
4728
4711 @property
4729 @property
4712 def recipients(self):
4730 def recipients(self):
4713 return [x.user for x in UserNotification.query()\
4731 return [x.user for x in UserNotification.query()\
4714 .filter(UserNotification.notification == self)\
4732 .filter(UserNotification.notification == self)\
4715 .order_by(UserNotification.user_id.asc()).all()]
4733 .order_by(UserNotification.user_id.asc()).all()]
4716
4734
4717 @classmethod
4735 @classmethod
4718 def create(cls, created_by, subject, body, recipients, type_=None):
4736 def create(cls, created_by, subject, body, recipients, type_=None):
4719 if type_ is None:
4737 if type_ is None:
4720 type_ = Notification.TYPE_MESSAGE
4738 type_ = Notification.TYPE_MESSAGE
4721
4739
4722 notification = cls()
4740 notification = cls()
4723 notification.created_by_user = created_by
4741 notification.created_by_user = created_by
4724 notification.subject = subject
4742 notification.subject = subject
4725 notification.body = body
4743 notification.body = body
4726 notification.type_ = type_
4744 notification.type_ = type_
4727 notification.created_on = datetime.datetime.now()
4745 notification.created_on = datetime.datetime.now()
4728
4746
4729 # For each recipient link the created notification to his account
4747 # For each recipient link the created notification to his account
4730 for u in recipients:
4748 for u in recipients:
4731 assoc = UserNotification()
4749 assoc = UserNotification()
4732 assoc.user_id = u.user_id
4750 assoc.user_id = u.user_id
4733 assoc.notification = notification
4751 assoc.notification = notification
4734
4752
4735 # if created_by is inside recipients mark his notification
4753 # if created_by is inside recipients mark his notification
4736 # as read
4754 # as read
4737 if u.user_id == created_by.user_id:
4755 if u.user_id == created_by.user_id:
4738 assoc.read = True
4756 assoc.read = True
4739 Session().add(assoc)
4757 Session().add(assoc)
4740
4758
4741 Session().add(notification)
4759 Session().add(notification)
4742
4760
4743 return notification
4761 return notification
4744
4762
4745
4763
4746 class UserNotification(Base, BaseModel):
4764 class UserNotification(Base, BaseModel):
4747 __tablename__ = 'user_to_notification'
4765 __tablename__ = 'user_to_notification'
4748 __table_args__ = (
4766 __table_args__ = (
4749 UniqueConstraint('user_id', 'notification_id'),
4767 UniqueConstraint('user_id', 'notification_id'),
4750 base_table_args
4768 base_table_args
4751 )
4769 )
4752
4770
4753 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
4771 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
4754 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
4772 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
4755 read = Column('read', Boolean, default=False)
4773 read = Column('read', Boolean, default=False)
4756 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
4774 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
4757
4775
4758 user = relationship('User', lazy="joined", back_populates='notifications')
4776 user = relationship('User', lazy="joined", back_populates='notifications')
4759 notification = relationship('Notification', lazy="joined", order_by=lambda: Notification.created_on.desc(), back_populates='notifications_to_users')
4777 notification = relationship('Notification', lazy="joined", order_by=lambda: Notification.created_on.desc(), back_populates='notifications_to_users')
4760
4778
4761 def mark_as_read(self):
4779 def mark_as_read(self):
4762 self.read = True
4780 self.read = True
4763 Session().add(self)
4781 Session().add(self)
4764
4782
4765
4783
4766 class UserNotice(Base, BaseModel):
4784 class UserNotice(Base, BaseModel):
4767 __tablename__ = 'user_notices'
4785 __tablename__ = 'user_notices'
4768 __table_args__ = (
4786 __table_args__ = (
4769 base_table_args
4787 base_table_args
4770 )
4788 )
4771
4789
4772 NOTIFICATION_TYPE_MESSAGE = 'message'
4790 NOTIFICATION_TYPE_MESSAGE = 'message'
4773 NOTIFICATION_TYPE_NOTICE = 'notice'
4791 NOTIFICATION_TYPE_NOTICE = 'notice'
4774
4792
4775 NOTIFICATION_LEVEL_INFO = 'info'
4793 NOTIFICATION_LEVEL_INFO = 'info'
4776 NOTIFICATION_LEVEL_WARNING = 'warning'
4794 NOTIFICATION_LEVEL_WARNING = 'warning'
4777 NOTIFICATION_LEVEL_ERROR = 'error'
4795 NOTIFICATION_LEVEL_ERROR = 'error'
4778
4796
4779 user_notice_id = Column('gist_id', Integer(), primary_key=True)
4797 user_notice_id = Column('gist_id', Integer(), primary_key=True)
4780
4798
4781 notice_subject = Column('notice_subject', Unicode(512), nullable=True)
4799 notice_subject = Column('notice_subject', Unicode(512), nullable=True)
4782 notice_body = Column('notice_body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
4800 notice_body = Column('notice_body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
4783
4801
4784 notice_read = Column('notice_read', Boolean, default=False)
4802 notice_read = Column('notice_read', Boolean, default=False)
4785
4803
4786 notification_level = Column('notification_level', String(1024), default=NOTIFICATION_LEVEL_INFO)
4804 notification_level = Column('notification_level', String(1024), default=NOTIFICATION_LEVEL_INFO)
4787 notification_type = Column('notification_type', String(1024), default=NOTIFICATION_TYPE_NOTICE)
4805 notification_type = Column('notification_type', String(1024), default=NOTIFICATION_TYPE_NOTICE)
4788
4806
4789 notice_created_by = Column('notice_created_by', Integer(), ForeignKey('users.user_id'), nullable=True)
4807 notice_created_by = Column('notice_created_by', Integer(), ForeignKey('users.user_id'), nullable=True)
4790 notice_created_on = Column('notice_created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4808 notice_created_on = Column('notice_created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4791
4809
4792 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'))
4810 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'))
4793 user = relationship('User', lazy="joined", primaryjoin='User.user_id==UserNotice.user_id')
4811 user = relationship('User', lazy="joined", primaryjoin='User.user_id==UserNotice.user_id')
4794
4812
4795 @classmethod
4813 @classmethod
4796 def create_for_user(cls, user, subject, body, notice_level=NOTIFICATION_LEVEL_INFO, allow_duplicate=False):
4814 def create_for_user(cls, user, subject, body, notice_level=NOTIFICATION_LEVEL_INFO, allow_duplicate=False):
4797
4815
4798 if notice_level not in [cls.NOTIFICATION_LEVEL_ERROR,
4816 if notice_level not in [cls.NOTIFICATION_LEVEL_ERROR,
4799 cls.NOTIFICATION_LEVEL_WARNING,
4817 cls.NOTIFICATION_LEVEL_WARNING,
4800 cls.NOTIFICATION_LEVEL_INFO]:
4818 cls.NOTIFICATION_LEVEL_INFO]:
4801 return
4819 return
4802
4820
4803 from rhodecode.model.user import UserModel
4821 from rhodecode.model.user import UserModel
4804 user = UserModel().get_user(user)
4822 user = UserModel().get_user(user)
4805
4823
4806 new_notice = UserNotice()
4824 new_notice = UserNotice()
4807 if not allow_duplicate:
4825 if not allow_duplicate:
4808 existing_msg = UserNotice().query() \
4826 existing_msg = UserNotice().query() \
4809 .filter(UserNotice.user == user) \
4827 .filter(UserNotice.user == user) \
4810 .filter(UserNotice.notice_body == body) \
4828 .filter(UserNotice.notice_body == body) \
4811 .filter(UserNotice.notice_read == false()) \
4829 .filter(UserNotice.notice_read == false()) \
4812 .scalar()
4830 .scalar()
4813 if existing_msg:
4831 if existing_msg:
4814 log.warning('Ignoring duplicate notice for user %s', user)
4832 log.warning('Ignoring duplicate notice for user %s', user)
4815 return
4833 return
4816
4834
4817 new_notice.user = user
4835 new_notice.user = user
4818 new_notice.notice_subject = subject
4836 new_notice.notice_subject = subject
4819 new_notice.notice_body = body
4837 new_notice.notice_body = body
4820 new_notice.notification_level = notice_level
4838 new_notice.notification_level = notice_level
4821 Session().add(new_notice)
4839 Session().add(new_notice)
4822 Session().commit()
4840 Session().commit()
4823
4841
4824
4842
4825 class Gist(Base, BaseModel):
4843 class Gist(Base, BaseModel):
4826 __tablename__ = 'gists'
4844 __tablename__ = 'gists'
4827 __table_args__ = (
4845 __table_args__ = (
4828 Index('g_gist_access_id_idx', 'gist_access_id'),
4846 Index('g_gist_access_id_idx', 'gist_access_id'),
4829 Index('g_created_on_idx', 'created_on'),
4847 Index('g_created_on_idx', 'created_on'),
4830 base_table_args
4848 base_table_args
4831 )
4849 )
4832
4850
4833 GIST_PUBLIC = 'public'
4851 GIST_PUBLIC = 'public'
4834 GIST_PRIVATE = 'private'
4852 GIST_PRIVATE = 'private'
4835 DEFAULT_FILENAME = 'gistfile1.txt'
4853 DEFAULT_FILENAME = 'gistfile1.txt'
4836
4854
4837 ACL_LEVEL_PUBLIC = 'acl_public'
4855 ACL_LEVEL_PUBLIC = 'acl_public'
4838 ACL_LEVEL_PRIVATE = 'acl_private'
4856 ACL_LEVEL_PRIVATE = 'acl_private'
4839
4857
4840 gist_id = Column('gist_id', Integer(), primary_key=True)
4858 gist_id = Column('gist_id', Integer(), primary_key=True)
4841 gist_access_id = Column('gist_access_id', Unicode(250))
4859 gist_access_id = Column('gist_access_id', Unicode(250))
4842 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
4860 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
4843 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
4861 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
4844 gist_expires = Column('gist_expires', Float(53), nullable=False)
4862 gist_expires = Column('gist_expires', Float(53), nullable=False)
4845 gist_type = Column('gist_type', Unicode(128), nullable=False)
4863 gist_type = Column('gist_type', Unicode(128), nullable=False)
4846 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4864 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4847 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4865 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4848 acl_level = Column('acl_level', Unicode(128), nullable=True)
4866 acl_level = Column('acl_level', Unicode(128), nullable=True)
4849
4867
4850 owner = relationship('User', back_populates='user_gists')
4868 owner = relationship('User', back_populates='user_gists')
4851
4869
4852 def __repr__(self):
4870 def __repr__(self):
4853 return f'<Gist:[{self.gist_type}]{self.gist_access_id}>'
4871 return f'<Gist:[{self.gist_type}]{self.gist_access_id}>'
4854
4872
4855 @hybrid_property
4873 @hybrid_property
4856 def description_safe(self):
4874 def description_safe(self):
4857 from rhodecode.lib import helpers as h
4875 from rhodecode.lib import helpers as h
4858 return h.escape(self.gist_description)
4876 return h.escape(self.gist_description)
4859
4877
4860 @classmethod
4878 @classmethod
4861 def get_or_404(cls, id_):
4879 def get_or_404(cls, id_):
4862 from pyramid.httpexceptions import HTTPNotFound
4880 from pyramid.httpexceptions import HTTPNotFound
4863
4881
4864 res = cls.query().filter(cls.gist_access_id == id_).scalar()
4882 res = cls.query().filter(cls.gist_access_id == id_).scalar()
4865 if not res:
4883 if not res:
4866 log.debug('WARN: No DB entry with id %s', id_)
4884 log.debug('WARN: No DB entry with id %s', id_)
4867 raise HTTPNotFound()
4885 raise HTTPNotFound()
4868 return res
4886 return res
4869
4887
4870 @classmethod
4888 @classmethod
4871 def get_by_access_id(cls, gist_access_id):
4889 def get_by_access_id(cls, gist_access_id):
4872 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
4890 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
4873
4891
4874 def gist_url(self):
4892 def gist_url(self):
4875 from rhodecode.model.gist import GistModel
4893 from rhodecode.model.gist import GistModel
4876 return GistModel().get_url(self)
4894 return GistModel().get_url(self)
4877
4895
4878 @classmethod
4896 @classmethod
4879 def base_path(cls):
4897 def base_path(cls):
4880 """
4898 """
4881 Returns base path when all gists are stored
4899 Returns base path when all gists are stored
4882
4900
4883 :param cls:
4901 :param cls:
4884 """
4902 """
4885 from rhodecode.model.gist import GIST_STORE_LOC
4903 from rhodecode.model.gist import GIST_STORE_LOC
4886 q = Session().query(RhodeCodeUi)\
4904 q = Session().query(RhodeCodeUi)\
4887 .filter(RhodeCodeUi.ui_key == URL_SEP)
4905 .filter(RhodeCodeUi.ui_key == URL_SEP)
4888 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
4906 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
4889 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
4907 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
4890
4908
4891 def get_api_data(self):
4909 def get_api_data(self):
4892 """
4910 """
4893 Common function for generating gist related data for API
4911 Common function for generating gist related data for API
4894 """
4912 """
4895 gist = self
4913 gist = self
4896 data = {
4914 data = {
4897 'gist_id': gist.gist_id,
4915 'gist_id': gist.gist_id,
4898 'type': gist.gist_type,
4916 'type': gist.gist_type,
4899 'access_id': gist.gist_access_id,
4917 'access_id': gist.gist_access_id,
4900 'description': gist.gist_description,
4918 'description': gist.gist_description,
4901 'url': gist.gist_url(),
4919 'url': gist.gist_url(),
4902 'expires': gist.gist_expires,
4920 'expires': gist.gist_expires,
4903 'created_on': gist.created_on,
4921 'created_on': gist.created_on,
4904 'modified_at': gist.modified_at,
4922 'modified_at': gist.modified_at,
4905 'content': None,
4923 'content': None,
4906 'acl_level': gist.acl_level,
4924 'acl_level': gist.acl_level,
4907 }
4925 }
4908 return data
4926 return data
4909
4927
4910 def __json__(self):
4928 def __json__(self):
4911 data = dict(
4929 data = dict(
4912 )
4930 )
4913 data.update(self.get_api_data())
4931 data.update(self.get_api_data())
4914 return data
4932 return data
4915 # SCM functions
4933 # SCM functions
4916
4934
4917 def scm_instance(self, **kwargs):
4935 def scm_instance(self, **kwargs):
4918 """
4936 """
4919 Get an instance of VCS Repository
4937 Get an instance of VCS Repository
4920
4938
4921 :param kwargs:
4939 :param kwargs:
4922 """
4940 """
4923 from rhodecode.model.gist import GistModel
4941 from rhodecode.model.gist import GistModel
4924 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
4942 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
4925 return get_vcs_instance(
4943 return get_vcs_instance(
4926 repo_path=safe_str(full_repo_path), create=False,
4944 repo_path=safe_str(full_repo_path), create=False,
4927 _vcs_alias=GistModel.vcs_backend)
4945 _vcs_alias=GistModel.vcs_backend)
4928
4946
4929
4947
4930 class ExternalIdentity(Base, BaseModel):
4948 class ExternalIdentity(Base, BaseModel):
4931 __tablename__ = 'external_identities'
4949 __tablename__ = 'external_identities'
4932 __table_args__ = (
4950 __table_args__ = (
4933 Index('local_user_id_idx', 'local_user_id'),
4951 Index('local_user_id_idx', 'local_user_id'),
4934 Index('external_id_idx', 'external_id'),
4952 Index('external_id_idx', 'external_id'),
4935 base_table_args
4953 base_table_args
4936 )
4954 )
4937
4955
4938 external_id = Column('external_id', Unicode(255), default='', primary_key=True)
4956 external_id = Column('external_id', Unicode(255), default='', primary_key=True)
4939 external_username = Column('external_username', Unicode(1024), default='')
4957 external_username = Column('external_username', Unicode(1024), default='')
4940 local_user_id = Column('local_user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
4958 local_user_id = Column('local_user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
4941 provider_name = Column('provider_name', Unicode(255), default='', primary_key=True)
4959 provider_name = Column('provider_name', Unicode(255), default='', primary_key=True)
4942 access_token = Column('access_token', String(1024), default='')
4960 access_token = Column('access_token', String(1024), default='')
4943 alt_token = Column('alt_token', String(1024), default='')
4961 alt_token = Column('alt_token', String(1024), default='')
4944 token_secret = Column('token_secret', String(1024), default='')
4962 token_secret = Column('token_secret', String(1024), default='')
4945
4963
4946 @classmethod
4964 @classmethod
4947 def by_external_id_and_provider(cls, external_id, provider_name, local_user_id=None):
4965 def by_external_id_and_provider(cls, external_id, provider_name, local_user_id=None):
4948 """
4966 """
4949 Returns ExternalIdentity instance based on search params
4967 Returns ExternalIdentity instance based on search params
4950
4968
4951 :param external_id:
4969 :param external_id:
4952 :param provider_name:
4970 :param provider_name:
4953 :return: ExternalIdentity
4971 :return: ExternalIdentity
4954 """
4972 """
4955 query = cls.query()
4973 query = cls.query()
4956 query = query.filter(cls.external_id == external_id)
4974 query = query.filter(cls.external_id == external_id)
4957 query = query.filter(cls.provider_name == provider_name)
4975 query = query.filter(cls.provider_name == provider_name)
4958 if local_user_id:
4976 if local_user_id:
4959 query = query.filter(cls.local_user_id == local_user_id)
4977 query = query.filter(cls.local_user_id == local_user_id)
4960 return query.first()
4978 return query.first()
4961
4979
4962 @classmethod
4980 @classmethod
4963 def user_by_external_id_and_provider(cls, external_id, provider_name):
4981 def user_by_external_id_and_provider(cls, external_id, provider_name):
4964 """
4982 """
4965 Returns User instance based on search params
4983 Returns User instance based on search params
4966
4984
4967 :param external_id:
4985 :param external_id:
4968 :param provider_name:
4986 :param provider_name:
4969 :return: User
4987 :return: User
4970 """
4988 """
4971 query = User.query()
4989 query = User.query()
4972 query = query.filter(cls.external_id == external_id)
4990 query = query.filter(cls.external_id == external_id)
4973 query = query.filter(cls.provider_name == provider_name)
4991 query = query.filter(cls.provider_name == provider_name)
4974 query = query.filter(User.user_id == cls.local_user_id)
4992 query = query.filter(User.user_id == cls.local_user_id)
4975 return query.first()
4993 return query.first()
4976
4994
4977 @classmethod
4995 @classmethod
4978 def by_local_user_id(cls, local_user_id):
4996 def by_local_user_id(cls, local_user_id):
4979 """
4997 """
4980 Returns all tokens for user
4998 Returns all tokens for user
4981
4999
4982 :param local_user_id:
5000 :param local_user_id:
4983 :return: ExternalIdentity
5001 :return: ExternalIdentity
4984 """
5002 """
4985 query = cls.query()
5003 query = cls.query()
4986 query = query.filter(cls.local_user_id == local_user_id)
5004 query = query.filter(cls.local_user_id == local_user_id)
4987 return query
5005 return query
4988
5006
4989 @classmethod
5007 @classmethod
4990 def load_provider_plugin(cls, plugin_id):
5008 def load_provider_plugin(cls, plugin_id):
4991 from rhodecode.authentication.base import loadplugin
5009 from rhodecode.authentication.base import loadplugin
4992 _plugin_id = 'egg:rhodecode-enterprise-ee#{}'.format(plugin_id)
5010 _plugin_id = 'egg:rhodecode-enterprise-ee#{}'.format(plugin_id)
4993 auth_plugin = loadplugin(_plugin_id)
5011 auth_plugin = loadplugin(_plugin_id)
4994 return auth_plugin
5012 return auth_plugin
4995
5013
4996
5014
4997 class Integration(Base, BaseModel):
5015 class Integration(Base, BaseModel):
4998 __tablename__ = 'integrations'
5016 __tablename__ = 'integrations'
4999 __table_args__ = (
5017 __table_args__ = (
5000 base_table_args
5018 base_table_args
5001 )
5019 )
5002
5020
5003 integration_id = Column('integration_id', Integer(), primary_key=True)
5021 integration_id = Column('integration_id', Integer(), primary_key=True)
5004 integration_type = Column('integration_type', String(255))
5022 integration_type = Column('integration_type', String(255))
5005 enabled = Column('enabled', Boolean(), nullable=False)
5023 enabled = Column('enabled', Boolean(), nullable=False)
5006 name = Column('name', String(255), nullable=False)
5024 name = Column('name', String(255), nullable=False)
5007 child_repos_only = Column('child_repos_only', Boolean(), nullable=False, default=False)
5025 child_repos_only = Column('child_repos_only', Boolean(), nullable=False, default=False)
5008
5026
5009 settings = Column(
5027 settings = Column(
5010 'settings_json', MutationObj.as_mutable(
5028 'settings_json', MutationObj.as_mutable(
5011 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
5029 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
5012 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
5030 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
5013 repo = relationship('Repository', lazy='joined', back_populates='integrations')
5031 repo = relationship('Repository', lazy='joined', back_populates='integrations')
5014
5032
5015 repo_group_id = Column('repo_group_id', Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
5033 repo_group_id = Column('repo_group_id', Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
5016 repo_group = relationship('RepoGroup', lazy='joined', back_populates='integrations')
5034 repo_group = relationship('RepoGroup', lazy='joined', back_populates='integrations')
5017
5035
5018 @property
5036 @property
5019 def scope(self):
5037 def scope(self):
5020 if self.repo:
5038 if self.repo:
5021 return repr(self.repo)
5039 return repr(self.repo)
5022 if self.repo_group:
5040 if self.repo_group:
5023 if self.child_repos_only:
5041 if self.child_repos_only:
5024 return repr(self.repo_group) + ' (child repos only)'
5042 return repr(self.repo_group) + ' (child repos only)'
5025 else:
5043 else:
5026 return repr(self.repo_group) + ' (recursive)'
5044 return repr(self.repo_group) + ' (recursive)'
5027 if self.child_repos_only:
5045 if self.child_repos_only:
5028 return 'root_repos'
5046 return 'root_repos'
5029 return 'global'
5047 return 'global'
5030
5048
5031 def __repr__(self):
5049 def __repr__(self):
5032 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
5050 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
5033
5051
5034
5052
5035 class RepoReviewRuleUser(Base, BaseModel):
5053 class RepoReviewRuleUser(Base, BaseModel):
5036 __tablename__ = 'repo_review_rules_users'
5054 __tablename__ = 'repo_review_rules_users'
5037 __table_args__ = (
5055 __table_args__ = (
5038 base_table_args
5056 base_table_args
5039 )
5057 )
5040 ROLE_REVIEWER = 'reviewer'
5058 ROLE_REVIEWER = 'reviewer'
5041 ROLE_OBSERVER = 'observer'
5059 ROLE_OBSERVER = 'observer'
5042 ROLES = [ROLE_REVIEWER, ROLE_OBSERVER]
5060 ROLES = [ROLE_REVIEWER, ROLE_OBSERVER]
5043
5061
5044 repo_review_rule_user_id = Column('repo_review_rule_user_id', Integer(), primary_key=True)
5062 repo_review_rule_user_id = Column('repo_review_rule_user_id', Integer(), primary_key=True)
5045 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
5063 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
5046 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False)
5064 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False)
5047 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
5065 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
5048 role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
5066 role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
5049 user = relationship('User', back_populates='user_review_rules')
5067 user = relationship('User', back_populates='user_review_rules')
5050
5068
5051 def rule_data(self):
5069 def rule_data(self):
5052 return {
5070 return {
5053 'mandatory': self.mandatory,
5071 'mandatory': self.mandatory,
5054 'role': self.role,
5072 'role': self.role,
5055 }
5073 }
5056
5074
5057
5075
5058 class RepoReviewRuleUserGroup(Base, BaseModel):
5076 class RepoReviewRuleUserGroup(Base, BaseModel):
5059 __tablename__ = 'repo_review_rules_users_groups'
5077 __tablename__ = 'repo_review_rules_users_groups'
5060 __table_args__ = (
5078 __table_args__ = (
5061 base_table_args
5079 base_table_args
5062 )
5080 )
5063
5081
5064 VOTE_RULE_ALL = -1
5082 VOTE_RULE_ALL = -1
5065 ROLE_REVIEWER = 'reviewer'
5083 ROLE_REVIEWER = 'reviewer'
5066 ROLE_OBSERVER = 'observer'
5084 ROLE_OBSERVER = 'observer'
5067 ROLES = [ROLE_REVIEWER, ROLE_OBSERVER]
5085 ROLES = [ROLE_REVIEWER, ROLE_OBSERVER]
5068
5086
5069 repo_review_rule_users_group_id = Column('repo_review_rule_users_group_id', Integer(), primary_key=True)
5087 repo_review_rule_users_group_id = Column('repo_review_rule_users_group_id', Integer(), primary_key=True)
5070 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
5088 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
5071 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False)
5089 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False)
5072 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
5090 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
5073 role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
5091 role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
5074 vote_rule = Column("vote_rule", Integer(), nullable=True, default=VOTE_RULE_ALL)
5092 vote_rule = Column("vote_rule", Integer(), nullable=True, default=VOTE_RULE_ALL)
5075 users_group = relationship('UserGroup')
5093 users_group = relationship('UserGroup')
5076
5094
5077 def rule_data(self):
5095 def rule_data(self):
5078 return {
5096 return {
5079 'mandatory': self.mandatory,
5097 'mandatory': self.mandatory,
5080 'role': self.role,
5098 'role': self.role,
5081 'vote_rule': self.vote_rule
5099 'vote_rule': self.vote_rule
5082 }
5100 }
5083
5101
5084 @property
5102 @property
5085 def vote_rule_label(self):
5103 def vote_rule_label(self):
5086 if not self.vote_rule or self.vote_rule == self.VOTE_RULE_ALL:
5104 if not self.vote_rule or self.vote_rule == self.VOTE_RULE_ALL:
5087 return 'all must vote'
5105 return 'all must vote'
5088 else:
5106 else:
5089 return 'min. vote {}'.format(self.vote_rule)
5107 return 'min. vote {}'.format(self.vote_rule)
5090
5108
5091
5109
5092 class RepoReviewRule(Base, BaseModel):
5110 class RepoReviewRule(Base, BaseModel):
5093 __tablename__ = 'repo_review_rules'
5111 __tablename__ = 'repo_review_rules'
5094 __table_args__ = (
5112 __table_args__ = (
5095 base_table_args
5113 base_table_args
5096 )
5114 )
5097
5115
5098 repo_review_rule_id = Column(
5116 repo_review_rule_id = Column(
5099 'repo_review_rule_id', Integer(), primary_key=True)
5117 'repo_review_rule_id', Integer(), primary_key=True)
5100 repo_id = Column(
5118 repo_id = Column(
5101 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
5119 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
5102 repo = relationship('Repository', back_populates='review_rules')
5120 repo = relationship('Repository', back_populates='review_rules')
5103
5121
5104 review_rule_name = Column('review_rule_name', String(255))
5122 review_rule_name = Column('review_rule_name', String(255))
5105 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default='*') # glob
5123 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default='*') # glob
5106 _target_branch_pattern = Column("target_branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default='*') # glob
5124 _target_branch_pattern = Column("target_branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default='*') # glob
5107 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default='*') # glob
5125 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default='*') # glob
5108
5126
5109 use_authors_for_review = Column("use_authors_for_review", Boolean(), nullable=False, default=False)
5127 use_authors_for_review = Column("use_authors_for_review", Boolean(), nullable=False, default=False)
5110
5128
5111 # Legacy fields, just for backward compat
5129 # Legacy fields, just for backward compat
5112 _forbid_author_to_review = Column("forbid_author_to_review", Boolean(), nullable=False, default=False)
5130 _forbid_author_to_review = Column("forbid_author_to_review", Boolean(), nullable=False, default=False)
5113 _forbid_commit_author_to_review = Column("forbid_commit_author_to_review", Boolean(), nullable=False, default=False)
5131 _forbid_commit_author_to_review = Column("forbid_commit_author_to_review", Boolean(), nullable=False, default=False)
5114
5132
5115 pr_author = Column("pr_author", UnicodeText().with_variant(UnicodeText(255), 'mysql'), nullable=True)
5133 pr_author = Column("pr_author", UnicodeText().with_variant(UnicodeText(255), 'mysql'), nullable=True)
5116 commit_author = Column("commit_author", UnicodeText().with_variant(UnicodeText(255), 'mysql'), nullable=True)
5134 commit_author = Column("commit_author", UnicodeText().with_variant(UnicodeText(255), 'mysql'), nullable=True)
5117
5135
5118 forbid_adding_reviewers = Column("forbid_adding_reviewers", Boolean(), nullable=False, default=False)
5136 forbid_adding_reviewers = Column("forbid_adding_reviewers", Boolean(), nullable=False, default=False)
5119
5137
5120 rule_users = relationship('RepoReviewRuleUser')
5138 rule_users = relationship('RepoReviewRuleUser')
5121 rule_user_groups = relationship('RepoReviewRuleUserGroup')
5139 rule_user_groups = relationship('RepoReviewRuleUserGroup')
5122
5140
5123 def _validate_pattern(self, value):
5141 def _validate_pattern(self, value):
5124 re.compile('^' + glob2re(value) + '$')
5142 re.compile('^' + glob2re(value) + '$')
5125
5143
5126 @hybrid_property
5144 @hybrid_property
5127 def source_branch_pattern(self):
5145 def source_branch_pattern(self):
5128 return self._branch_pattern or '*'
5146 return self._branch_pattern or '*'
5129
5147
5130 @source_branch_pattern.setter
5148 @source_branch_pattern.setter
5131 def source_branch_pattern(self, value):
5149 def source_branch_pattern(self, value):
5132 self._validate_pattern(value)
5150 self._validate_pattern(value)
5133 self._branch_pattern = value or '*'
5151 self._branch_pattern = value or '*'
5134
5152
5135 @hybrid_property
5153 @hybrid_property
5136 def target_branch_pattern(self):
5154 def target_branch_pattern(self):
5137 return self._target_branch_pattern or '*'
5155 return self._target_branch_pattern or '*'
5138
5156
5139 @target_branch_pattern.setter
5157 @target_branch_pattern.setter
5140 def target_branch_pattern(self, value):
5158 def target_branch_pattern(self, value):
5141 self._validate_pattern(value)
5159 self._validate_pattern(value)
5142 self._target_branch_pattern = value or '*'
5160 self._target_branch_pattern = value or '*'
5143
5161
5144 @hybrid_property
5162 @hybrid_property
5145 def file_pattern(self):
5163 def file_pattern(self):
5146 return self._file_pattern or '*'
5164 return self._file_pattern or '*'
5147
5165
5148 @file_pattern.setter
5166 @file_pattern.setter
5149 def file_pattern(self, value):
5167 def file_pattern(self, value):
5150 self._validate_pattern(value)
5168 self._validate_pattern(value)
5151 self._file_pattern = value or '*'
5169 self._file_pattern = value or '*'
5152
5170
5153 @hybrid_property
5171 @hybrid_property
5154 def forbid_pr_author_to_review(self):
5172 def forbid_pr_author_to_review(self):
5155 return self.pr_author == 'forbid_pr_author'
5173 return self.pr_author == 'forbid_pr_author'
5156
5174
5157 @hybrid_property
5175 @hybrid_property
5158 def include_pr_author_to_review(self):
5176 def include_pr_author_to_review(self):
5159 return self.pr_author == 'include_pr_author'
5177 return self.pr_author == 'include_pr_author'
5160
5178
5161 @hybrid_property
5179 @hybrid_property
5162 def forbid_commit_author_to_review(self):
5180 def forbid_commit_author_to_review(self):
5163 return self.commit_author == 'forbid_commit_author'
5181 return self.commit_author == 'forbid_commit_author'
5164
5182
5165 @hybrid_property
5183 @hybrid_property
5166 def include_commit_author_to_review(self):
5184 def include_commit_author_to_review(self):
5167 return self.commit_author == 'include_commit_author'
5185 return self.commit_author == 'include_commit_author'
5168
5186
5169 def matches(self, source_branch, target_branch, files_changed):
5187 def matches(self, source_branch, target_branch, files_changed):
5170 """
5188 """
5171 Check if this review rule matches a branch/files in a pull request
5189 Check if this review rule matches a branch/files in a pull request
5172
5190
5173 :param source_branch: source branch name for the commit
5191 :param source_branch: source branch name for the commit
5174 :param target_branch: target branch name for the commit
5192 :param target_branch: target branch name for the commit
5175 :param files_changed: list of file paths changed in the pull request
5193 :param files_changed: list of file paths changed in the pull request
5176 """
5194 """
5177
5195
5178 source_branch = source_branch or ''
5196 source_branch = source_branch or ''
5179 target_branch = target_branch or ''
5197 target_branch = target_branch or ''
5180 files_changed = files_changed or []
5198 files_changed = files_changed or []
5181
5199
5182 branch_matches = True
5200 branch_matches = True
5183 if source_branch or target_branch:
5201 if source_branch or target_branch:
5184 if self.source_branch_pattern == '*':
5202 if self.source_branch_pattern == '*':
5185 source_branch_match = True
5203 source_branch_match = True
5186 else:
5204 else:
5187 if self.source_branch_pattern.startswith('re:'):
5205 if self.source_branch_pattern.startswith('re:'):
5188 source_pattern = self.source_branch_pattern[3:]
5206 source_pattern = self.source_branch_pattern[3:]
5189 else:
5207 else:
5190 source_pattern = '^' + glob2re(self.source_branch_pattern) + '$'
5208 source_pattern = '^' + glob2re(self.source_branch_pattern) + '$'
5191 source_branch_regex = re.compile(source_pattern)
5209 source_branch_regex = re.compile(source_pattern)
5192 source_branch_match = bool(source_branch_regex.search(source_branch))
5210 source_branch_match = bool(source_branch_regex.search(source_branch))
5193 if self.target_branch_pattern == '*':
5211 if self.target_branch_pattern == '*':
5194 target_branch_match = True
5212 target_branch_match = True
5195 else:
5213 else:
5196 if self.target_branch_pattern.startswith('re:'):
5214 if self.target_branch_pattern.startswith('re:'):
5197 target_pattern = self.target_branch_pattern[3:]
5215 target_pattern = self.target_branch_pattern[3:]
5198 else:
5216 else:
5199 target_pattern = '^' + glob2re(self.target_branch_pattern) + '$'
5217 target_pattern = '^' + glob2re(self.target_branch_pattern) + '$'
5200 target_branch_regex = re.compile(target_pattern)
5218 target_branch_regex = re.compile(target_pattern)
5201 target_branch_match = bool(target_branch_regex.search(target_branch))
5219 target_branch_match = bool(target_branch_regex.search(target_branch))
5202
5220
5203 branch_matches = source_branch_match and target_branch_match
5221 branch_matches = source_branch_match and target_branch_match
5204
5222
5205 files_matches = True
5223 files_matches = True
5206 if self.file_pattern != '*':
5224 if self.file_pattern != '*':
5207 files_matches = False
5225 files_matches = False
5208 if self.file_pattern.startswith('re:'):
5226 if self.file_pattern.startswith('re:'):
5209 file_pattern = self.file_pattern[3:]
5227 file_pattern = self.file_pattern[3:]
5210 else:
5228 else:
5211 file_pattern = glob2re(self.file_pattern)
5229 file_pattern = glob2re(self.file_pattern)
5212 file_regex = re.compile(file_pattern)
5230 file_regex = re.compile(file_pattern)
5213 for file_data in files_changed:
5231 for file_data in files_changed:
5214 filename = file_data.get('filename')
5232 filename = file_data.get('filename')
5215
5233
5216 if file_regex.search(filename):
5234 if file_regex.search(filename):
5217 files_matches = True
5235 files_matches = True
5218 break
5236 break
5219
5237
5220 return branch_matches and files_matches
5238 return branch_matches and files_matches
5221
5239
5222 @property
5240 @property
5223 def review_users(self):
5241 def review_users(self):
5224 """ Returns the users which this rule applies to """
5242 """ Returns the users which this rule applies to """
5225
5243
5226 users = collections.OrderedDict()
5244 users = collections.OrderedDict()
5227
5245
5228 for rule_user in self.rule_users:
5246 for rule_user in self.rule_users:
5229 if rule_user.user.active:
5247 if rule_user.user.active:
5230 if rule_user.user not in users:
5248 if rule_user.user not in users:
5231 users[rule_user.user.username] = {
5249 users[rule_user.user.username] = {
5232 'user': rule_user.user,
5250 'user': rule_user.user,
5233 'source': 'user',
5251 'source': 'user',
5234 'source_data': {},
5252 'source_data': {},
5235 'data': rule_user.rule_data()
5253 'data': rule_user.rule_data()
5236 }
5254 }
5237
5255
5238 for rule_user_group in self.rule_user_groups:
5256 for rule_user_group in self.rule_user_groups:
5239 source_data = {
5257 source_data = {
5240 'user_group_id': rule_user_group.users_group.users_group_id,
5258 'user_group_id': rule_user_group.users_group.users_group_id,
5241 'name': rule_user_group.users_group.users_group_name,
5259 'name': rule_user_group.users_group.users_group_name,
5242 'members': len(rule_user_group.users_group.members)
5260 'members': len(rule_user_group.users_group.members)
5243 }
5261 }
5244 for member in rule_user_group.users_group.members:
5262 for member in rule_user_group.users_group.members:
5245 if member.user.active:
5263 if member.user.active:
5246 key = member.user.username
5264 key = member.user.username
5247 if key in users:
5265 if key in users:
5248 # skip this member as we have him already
5266 # skip this member as we have him already
5249 # this prevents from override the "first" matched
5267 # this prevents from override the "first" matched
5250 # users with duplicates in multiple groups
5268 # users with duplicates in multiple groups
5251 continue
5269 continue
5252
5270
5253 users[key] = {
5271 users[key] = {
5254 'user': member.user,
5272 'user': member.user,
5255 'source': 'user_group',
5273 'source': 'user_group',
5256 'source_data': source_data,
5274 'source_data': source_data,
5257 'data': rule_user_group.rule_data()
5275 'data': rule_user_group.rule_data()
5258 }
5276 }
5259
5277
5260 return users
5278 return users
5261
5279
5262 def user_group_vote_rule(self, user_id):
5280 def user_group_vote_rule(self, user_id):
5263
5281
5264 rules = []
5282 rules = []
5265 if not self.rule_user_groups:
5283 if not self.rule_user_groups:
5266 return rules
5284 return rules
5267
5285
5268 for user_group in self.rule_user_groups:
5286 for user_group in self.rule_user_groups:
5269 user_group_members = [x.user_id for x in user_group.users_group.members]
5287 user_group_members = [x.user_id for x in user_group.users_group.members]
5270 if user_id in user_group_members:
5288 if user_id in user_group_members:
5271 rules.append(user_group)
5289 rules.append(user_group)
5272 return rules
5290 return rules
5273
5291
5274 def __repr__(self):
5292 def __repr__(self):
5275 return f'<RepoReviewerRule(id={self.repo_review_rule_id}, repo={self.repo!r})>'
5293 return f'<RepoReviewerRule(id={self.repo_review_rule_id}, repo={self.repo!r})>'
5276
5294
5277
5295
5278 class ScheduleEntry(Base, BaseModel):
5296 class ScheduleEntry(Base, BaseModel):
5279 __tablename__ = 'schedule_entries'
5297 __tablename__ = 'schedule_entries'
5280 __table_args__ = (
5298 __table_args__ = (
5281 UniqueConstraint('schedule_name', name='s_schedule_name_idx'),
5299 UniqueConstraint('schedule_name', name='s_schedule_name_idx'),
5282 UniqueConstraint('task_uid', name='s_task_uid_idx'),
5300 UniqueConstraint('task_uid', name='s_task_uid_idx'),
5283 base_table_args,
5301 base_table_args,
5284 )
5302 )
5285 SCHEDULE_TYPE_INTEGER = "integer"
5303 SCHEDULE_TYPE_INTEGER = "integer"
5286 SCHEDULE_TYPE_CRONTAB = "crontab"
5304 SCHEDULE_TYPE_CRONTAB = "crontab"
5287
5305
5288 schedule_types = [SCHEDULE_TYPE_CRONTAB, SCHEDULE_TYPE_INTEGER]
5306 schedule_types = [SCHEDULE_TYPE_CRONTAB, SCHEDULE_TYPE_INTEGER]
5289 schedule_entry_id = Column('schedule_entry_id', Integer(), primary_key=True)
5307 schedule_entry_id = Column('schedule_entry_id', Integer(), primary_key=True)
5290
5308
5291 schedule_name = Column("schedule_name", String(255), nullable=False, unique=None, default=None)
5309 schedule_name = Column("schedule_name", String(255), nullable=False, unique=None, default=None)
5292 schedule_description = Column("schedule_description", String(10000), nullable=True, unique=None, default=None)
5310 schedule_description = Column("schedule_description", String(10000), nullable=True, unique=None, default=None)
5293 schedule_enabled = Column("schedule_enabled", Boolean(), nullable=False, unique=None, default=True)
5311 schedule_enabled = Column("schedule_enabled", Boolean(), nullable=False, unique=None, default=True)
5294
5312
5295 _schedule_type = Column("schedule_type", String(255), nullable=False, unique=None, default=None)
5313 _schedule_type = Column("schedule_type", String(255), nullable=False, unique=None, default=None)
5296 schedule_definition = Column('schedule_definition_json', MutationObj.as_mutable(JsonType(default=lambda: "", dialect_map=dict(mysql=LONGTEXT()))))
5314 schedule_definition = Column('schedule_definition_json', MutationObj.as_mutable(JsonType(default=lambda: "", dialect_map=dict(mysql=LONGTEXT()))))
5297
5315
5298 schedule_last_run = Column('schedule_last_run', DateTime(timezone=False), nullable=True, unique=None, default=None)
5316 schedule_last_run = Column('schedule_last_run', DateTime(timezone=False), nullable=True, unique=None, default=None)
5299 schedule_total_run_count = Column('schedule_total_run_count', Integer(), nullable=True, unique=None, default=0)
5317 schedule_total_run_count = Column('schedule_total_run_count', Integer(), nullable=True, unique=None, default=0)
5300
5318
5301 # task
5319 # task
5302 task_uid = Column("task_uid", String(255), nullable=False, unique=None, default=None)
5320 task_uid = Column("task_uid", String(255), nullable=False, unique=None, default=None)
5303 task_dot_notation = Column("task_dot_notation", String(4096), nullable=False, unique=None, default=None)
5321 task_dot_notation = Column("task_dot_notation", String(4096), nullable=False, unique=None, default=None)
5304 task_args = Column('task_args_json', MutationObj.as_mutable(JsonType(default=list, dialect_map=dict(mysql=LONGTEXT()))))
5322 task_args = Column('task_args_json', MutationObj.as_mutable(JsonType(default=list, dialect_map=dict(mysql=LONGTEXT()))))
5305 task_kwargs = Column('task_kwargs_json', MutationObj.as_mutable(JsonType(default=dict, dialect_map=dict(mysql=LONGTEXT()))))
5323 task_kwargs = Column('task_kwargs_json', MutationObj.as_mutable(JsonType(default=dict, dialect_map=dict(mysql=LONGTEXT()))))
5306
5324
5307 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5325 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5308 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=None)
5326 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=None)
5309
5327
5310 @hybrid_property
5328 @hybrid_property
5311 def schedule_type(self):
5329 def schedule_type(self):
5312 return self._schedule_type
5330 return self._schedule_type
5313
5331
5314 @schedule_type.setter
5332 @schedule_type.setter
5315 def schedule_type(self, val):
5333 def schedule_type(self, val):
5316 if val not in self.schedule_types:
5334 if val not in self.schedule_types:
5317 raise ValueError('Value must be on of `{}` and got `{}`'.format(
5335 raise ValueError('Value must be on of `{}` and got `{}`'.format(
5318 val, self.schedule_type))
5336 val, self.schedule_type))
5319
5337
5320 self._schedule_type = val
5338 self._schedule_type = val
5321
5339
5322 @classmethod
5340 @classmethod
5323 def get_uid(cls, obj):
5341 def get_uid(cls, obj):
5324 args = obj.task_args
5342 args = obj.task_args
5325 kwargs = obj.task_kwargs
5343 kwargs = obj.task_kwargs
5326 if isinstance(args, JsonRaw):
5344 if isinstance(args, JsonRaw):
5327 try:
5345 try:
5328 args = json.loads(args)
5346 args = json.loads(args)
5329 except ValueError:
5347 except ValueError:
5330 args = tuple()
5348 args = tuple()
5331
5349
5332 if isinstance(kwargs, JsonRaw):
5350 if isinstance(kwargs, JsonRaw):
5333 try:
5351 try:
5334 kwargs = json.loads(kwargs)
5352 kwargs = json.loads(kwargs)
5335 except ValueError:
5353 except ValueError:
5336 kwargs = dict()
5354 kwargs = dict()
5337
5355
5338 dot_notation = obj.task_dot_notation
5356 dot_notation = obj.task_dot_notation
5339 val = '.'.join(map(safe_str, [
5357 val = '.'.join(map(safe_str, [
5340 sorted(dot_notation), args, sorted(kwargs.items())]))
5358 sorted(dot_notation), args, sorted(kwargs.items())]))
5341 return sha1(safe_bytes(val))
5359 return sha1(safe_bytes(val))
5342
5360
5343 @classmethod
5361 @classmethod
5344 def get_by_schedule_name(cls, schedule_name):
5362 def get_by_schedule_name(cls, schedule_name):
5345 return cls.query().filter(cls.schedule_name == schedule_name).scalar()
5363 return cls.query().filter(cls.schedule_name == schedule_name).scalar()
5346
5364
5347 @classmethod
5365 @classmethod
5348 def get_by_schedule_id(cls, schedule_id):
5366 def get_by_schedule_id(cls, schedule_id):
5349 return cls.query().filter(cls.schedule_entry_id == schedule_id).scalar()
5367 return cls.query().filter(cls.schedule_entry_id == schedule_id).scalar()
5350
5368
5351 @property
5369 @property
5352 def task(self):
5370 def task(self):
5353 return self.task_dot_notation
5371 return self.task_dot_notation
5354
5372
5355 @property
5373 @property
5356 def schedule(self):
5374 def schedule(self):
5357 from rhodecode.lib.celerylib.utils import raw_2_schedule
5375 from rhodecode.lib.celerylib.utils import raw_2_schedule
5358 schedule = raw_2_schedule(self.schedule_definition, self.schedule_type)
5376 schedule = raw_2_schedule(self.schedule_definition, self.schedule_type)
5359 return schedule
5377 return schedule
5360
5378
5361 @property
5379 @property
5362 def args(self):
5380 def args(self):
5363 try:
5381 try:
5364 return list(self.task_args or [])
5382 return list(self.task_args or [])
5365 except ValueError:
5383 except ValueError:
5366 return list()
5384 return list()
5367
5385
5368 @property
5386 @property
5369 def kwargs(self):
5387 def kwargs(self):
5370 try:
5388 try:
5371 return dict(self.task_kwargs or {})
5389 return dict(self.task_kwargs or {})
5372 except ValueError:
5390 except ValueError:
5373 return dict()
5391 return dict()
5374
5392
5375 def _as_raw(self, val, indent=False):
5393 def _as_raw(self, val, indent=False):
5376 if hasattr(val, 'de_coerce'):
5394 if hasattr(val, 'de_coerce'):
5377 val = val.de_coerce()
5395 val = val.de_coerce()
5378 if val:
5396 if val:
5379 if indent:
5397 if indent:
5380 val = ext_json.formatted_str_json(val)
5398 val = ext_json.formatted_str_json(val)
5381 else:
5399 else:
5382 val = ext_json.str_json(val)
5400 val = ext_json.str_json(val)
5383
5401
5384 return val
5402 return val
5385
5403
5386 @property
5404 @property
5387 def schedule_definition_raw(self):
5405 def schedule_definition_raw(self):
5388 return self._as_raw(self.schedule_definition)
5406 return self._as_raw(self.schedule_definition)
5389
5407
5390 def args_raw(self, indent=False):
5408 def args_raw(self, indent=False):
5391 return self._as_raw(self.task_args, indent)
5409 return self._as_raw(self.task_args, indent)
5392
5410
5393 def kwargs_raw(self, indent=False):
5411 def kwargs_raw(self, indent=False):
5394 return self._as_raw(self.task_kwargs, indent)
5412 return self._as_raw(self.task_kwargs, indent)
5395
5413
5396 def __repr__(self):
5414 def __repr__(self):
5397 return f'<DB:ScheduleEntry({self.schedule_entry_id}:{self.schedule_name})>'
5415 return f'<DB:ScheduleEntry({self.schedule_entry_id}:{self.schedule_name})>'
5398
5416
5399
5417
5400 @event.listens_for(ScheduleEntry, 'before_update')
5418 @event.listens_for(ScheduleEntry, 'before_update')
5401 def update_task_uid(mapper, connection, target):
5419 def update_task_uid(mapper, connection, target):
5402 target.task_uid = ScheduleEntry.get_uid(target)
5420 target.task_uid = ScheduleEntry.get_uid(target)
5403
5421
5404
5422
5405 @event.listens_for(ScheduleEntry, 'before_insert')
5423 @event.listens_for(ScheduleEntry, 'before_insert')
5406 def set_task_uid(mapper, connection, target):
5424 def set_task_uid(mapper, connection, target):
5407 target.task_uid = ScheduleEntry.get_uid(target)
5425 target.task_uid = ScheduleEntry.get_uid(target)
5408
5426
5409
5427
5410 class _BaseBranchPerms(BaseModel):
5428 class _BaseBranchPerms(BaseModel):
5411 @classmethod
5429 @classmethod
5412 def compute_hash(cls, value):
5430 def compute_hash(cls, value):
5413 return sha1_safe(value)
5431 return sha1_safe(value)
5414
5432
5415 @hybrid_property
5433 @hybrid_property
5416 def branch_pattern(self):
5434 def branch_pattern(self):
5417 return self._branch_pattern or '*'
5435 return self._branch_pattern or '*'
5418
5436
5419 @hybrid_property
5437 @hybrid_property
5420 def branch_hash(self):
5438 def branch_hash(self):
5421 return self._branch_hash
5439 return self._branch_hash
5422
5440
5423 def _validate_glob(self, value):
5441 def _validate_glob(self, value):
5424 re.compile('^' + glob2re(value) + '$')
5442 re.compile('^' + glob2re(value) + '$')
5425
5443
5426 @branch_pattern.setter
5444 @branch_pattern.setter
5427 def branch_pattern(self, value):
5445 def branch_pattern(self, value):
5428 self._validate_glob(value)
5446 self._validate_glob(value)
5429 self._branch_pattern = value or '*'
5447 self._branch_pattern = value or '*'
5430 # set the Hash when setting the branch pattern
5448 # set the Hash when setting the branch pattern
5431 self._branch_hash = self.compute_hash(self._branch_pattern)
5449 self._branch_hash = self.compute_hash(self._branch_pattern)
5432
5450
5433 def matches(self, branch):
5451 def matches(self, branch):
5434 """
5452 """
5435 Check if this the branch matches entry
5453 Check if this the branch matches entry
5436
5454
5437 :param branch: branch name for the commit
5455 :param branch: branch name for the commit
5438 """
5456 """
5439
5457
5440 branch = branch or ''
5458 branch = branch or ''
5441
5459
5442 branch_matches = True
5460 branch_matches = True
5443 if branch:
5461 if branch:
5444 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
5462 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
5445 branch_matches = bool(branch_regex.search(branch))
5463 branch_matches = bool(branch_regex.search(branch))
5446
5464
5447 return branch_matches
5465 return branch_matches
5448
5466
5449
5467
5450 class UserToRepoBranchPermission(Base, _BaseBranchPerms):
5468 class UserToRepoBranchPermission(Base, _BaseBranchPerms):
5451 __tablename__ = 'user_to_repo_branch_permissions'
5469 __tablename__ = 'user_to_repo_branch_permissions'
5452 __table_args__ = (
5470 __table_args__ = (
5453 base_table_args
5471 base_table_args
5454 )
5472 )
5455
5473
5456 branch_rule_id = Column('branch_rule_id', Integer(), primary_key=True)
5474 branch_rule_id = Column('branch_rule_id', Integer(), primary_key=True)
5457
5475
5458 repository_id = Column('repository_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
5476 repository_id = Column('repository_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
5459 repo = relationship('Repository', back_populates='user_branch_perms')
5477 repo = relationship('Repository', back_populates='user_branch_perms')
5460
5478
5461 permission_id = Column('permission_id', Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
5479 permission_id = Column('permission_id', Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
5462 permission = relationship('Permission')
5480 permission = relationship('Permission')
5463
5481
5464 rule_to_perm_id = Column('rule_to_perm_id', Integer(), ForeignKey('repo_to_perm.repo_to_perm_id'), nullable=False, unique=None, default=None)
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 user_repo_to_perm = relationship('UserRepoToPerm', back_populates='branch_perm_entry')
5483 user_repo_to_perm = relationship('UserRepoToPerm', back_populates='branch_perm_entry')
5466
5484
5467 rule_order = Column('rule_order', Integer(), nullable=False)
5485 rule_order = Column('rule_order', Integer(), nullable=False)
5468 _branch_pattern = Column('branch_pattern', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), default='*') # glob
5486 _branch_pattern = Column('branch_pattern', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), default='*') # glob
5469 _branch_hash = Column('branch_hash', UnicodeText().with_variant(UnicodeText(2048), 'mysql'))
5487 _branch_hash = Column('branch_hash', UnicodeText().with_variant(UnicodeText(2048), 'mysql'))
5470
5488
5471 def __repr__(self):
5489 def __repr__(self):
5472 return f'<UserBranchPermission({self.user_repo_to_perm} => {self.branch_pattern!r})>'
5490 return f'<UserBranchPermission({self.user_repo_to_perm} => {self.branch_pattern!r})>'
5473
5491
5474
5492
5475 class UserGroupToRepoBranchPermission(Base, _BaseBranchPerms):
5493 class UserGroupToRepoBranchPermission(Base, _BaseBranchPerms):
5476 __tablename__ = 'user_group_to_repo_branch_permissions'
5494 __tablename__ = 'user_group_to_repo_branch_permissions'
5477 __table_args__ = (
5495 __table_args__ = (
5478 base_table_args
5496 base_table_args
5479 )
5497 )
5480
5498
5481 branch_rule_id = Column('branch_rule_id', Integer(), primary_key=True)
5499 branch_rule_id = Column('branch_rule_id', Integer(), primary_key=True)
5482
5500
5483 repository_id = Column('repository_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
5501 repository_id = Column('repository_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
5484 repo = relationship('Repository', back_populates='user_group_branch_perms')
5502 repo = relationship('Repository', back_populates='user_group_branch_perms')
5485
5503
5486 permission_id = Column('permission_id', Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
5504 permission_id = Column('permission_id', Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
5487 permission = relationship('Permission')
5505 permission = relationship('Permission')
5488
5506
5489 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)
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 user_group_repo_to_perm = relationship('UserGroupRepoToPerm', back_populates='user_group_branch_perms')
5508 user_group_repo_to_perm = relationship('UserGroupRepoToPerm', back_populates='user_group_branch_perms')
5491
5509
5492 rule_order = Column('rule_order', Integer(), nullable=False)
5510 rule_order = Column('rule_order', Integer(), nullable=False)
5493 _branch_pattern = Column('branch_pattern', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), default='*') # glob
5511 _branch_pattern = Column('branch_pattern', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), default='*') # glob
5494 _branch_hash = Column('branch_hash', UnicodeText().with_variant(UnicodeText(2048), 'mysql'))
5512 _branch_hash = Column('branch_hash', UnicodeText().with_variant(UnicodeText(2048), 'mysql'))
5495
5513
5496 def __repr__(self):
5514 def __repr__(self):
5497 return f'<UserBranchPermission({self.user_group_repo_to_perm} => {self.branch_pattern!r})>'
5515 return f'<UserBranchPermission({self.user_group_repo_to_perm} => {self.branch_pattern!r})>'
5498
5516
5499
5517
5500 class UserBookmark(Base, BaseModel):
5518 class UserBookmark(Base, BaseModel):
5501 __tablename__ = 'user_bookmarks'
5519 __tablename__ = 'user_bookmarks'
5502 __table_args__ = (
5520 __table_args__ = (
5503 UniqueConstraint('user_id', 'bookmark_repo_id'),
5521 UniqueConstraint('user_id', 'bookmark_repo_id'),
5504 UniqueConstraint('user_id', 'bookmark_repo_group_id'),
5522 UniqueConstraint('user_id', 'bookmark_repo_group_id'),
5505 UniqueConstraint('user_id', 'bookmark_position'),
5523 UniqueConstraint('user_id', 'bookmark_position'),
5506 base_table_args
5524 base_table_args
5507 )
5525 )
5508
5526
5509 user_bookmark_id = Column("user_bookmark_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
5527 user_bookmark_id = Column("user_bookmark_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
5510 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
5528 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
5511 position = Column("bookmark_position", Integer(), nullable=False)
5529 position = Column("bookmark_position", Integer(), nullable=False)
5512 title = Column("bookmark_title", String(255), nullable=True, unique=None, default=None)
5530 title = Column("bookmark_title", String(255), nullable=True, unique=None, default=None)
5513 redirect_url = Column("bookmark_redirect_url", String(10240), nullable=True, unique=None, default=None)
5531 redirect_url = Column("bookmark_redirect_url", String(10240), nullable=True, unique=None, default=None)
5514 created_on = Column("created_on", DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5532 created_on = Column("created_on", DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5515
5533
5516 bookmark_repo_id = Column("bookmark_repo_id", Integer(), ForeignKey("repositories.repo_id"), nullable=True, unique=None, default=None)
5534 bookmark_repo_id = Column("bookmark_repo_id", Integer(), ForeignKey("repositories.repo_id"), nullable=True, unique=None, default=None)
5517 bookmark_repo_group_id = Column("bookmark_repo_group_id", Integer(), ForeignKey("groups.group_id"), nullable=True, unique=None, default=None)
5535 bookmark_repo_group_id = Column("bookmark_repo_group_id", Integer(), ForeignKey("groups.group_id"), nullable=True, unique=None, default=None)
5518
5536
5519 user = relationship("User")
5537 user = relationship("User")
5520
5538
5521 repository = relationship("Repository")
5539 repository = relationship("Repository")
5522 repository_group = relationship("RepoGroup")
5540 repository_group = relationship("RepoGroup")
5523
5541
5524 @classmethod
5542 @classmethod
5525 def get_by_position_for_user(cls, position, user_id):
5543 def get_by_position_for_user(cls, position, user_id):
5526 return cls.query() \
5544 return cls.query() \
5527 .filter(UserBookmark.user_id == user_id) \
5545 .filter(UserBookmark.user_id == user_id) \
5528 .filter(UserBookmark.position == position).scalar()
5546 .filter(UserBookmark.position == position).scalar()
5529
5547
5530 @classmethod
5548 @classmethod
5531 def get_bookmarks_for_user(cls, user_id, cache=True):
5549 def get_bookmarks_for_user(cls, user_id, cache=True):
5532 bookmarks = cls.query() \
5550 bookmarks = cls.query() \
5533 .filter(UserBookmark.user_id == user_id) \
5551 .filter(UserBookmark.user_id == user_id) \
5534 .options(joinedload(UserBookmark.repository)) \
5552 .options(joinedload(UserBookmark.repository)) \
5535 .options(joinedload(UserBookmark.repository_group)) \
5553 .options(joinedload(UserBookmark.repository_group)) \
5536 .order_by(UserBookmark.position.asc())
5554 .order_by(UserBookmark.position.asc())
5537
5555
5538 if cache:
5556 if cache:
5539 bookmarks = bookmarks.options(
5557 bookmarks = bookmarks.options(
5540 FromCache("sql_cache_short", "get_user_{}_bookmarks".format(user_id))
5558 FromCache("sql_cache_short", "get_user_{}_bookmarks".format(user_id))
5541 )
5559 )
5542
5560
5543 return bookmarks.all()
5561 return bookmarks.all()
5544
5562
5545 def __repr__(self):
5563 def __repr__(self):
5546 return f'<UserBookmark({self.position} @ {self.redirect_url!r})>'
5564 return f'<UserBookmark({self.position} @ {self.redirect_url!r})>'
5547
5565
5548
5566
5549 class FileStore(Base, BaseModel):
5567 class FileStore(Base, BaseModel):
5550 __tablename__ = 'file_store'
5568 __tablename__ = 'file_store'
5551 __table_args__ = (
5569 __table_args__ = (
5552 base_table_args
5570 base_table_args
5553 )
5571 )
5554
5572
5555 file_store_id = Column('file_store_id', Integer(), primary_key=True)
5573 file_store_id = Column('file_store_id', Integer(), primary_key=True)
5556 file_uid = Column('file_uid', String(1024), nullable=False)
5574 file_uid = Column('file_uid', String(1024), nullable=False)
5557 file_display_name = Column('file_display_name', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), nullable=True)
5575 file_display_name = Column('file_display_name', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), nullable=True)
5558 file_description = Column('file_description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'), nullable=True)
5576 file_description = Column('file_description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'), nullable=True)
5559 file_org_name = Column('file_org_name', UnicodeText().with_variant(UnicodeText(10240), 'mysql'), nullable=False)
5577 file_org_name = Column('file_org_name', UnicodeText().with_variant(UnicodeText(10240), 'mysql'), nullable=False)
5560
5578
5561 # sha256 hash
5579 # sha256 hash
5562 file_hash = Column('file_hash', String(512), nullable=False)
5580 file_hash = Column('file_hash', String(512), nullable=False)
5563 file_size = Column('file_size', BigInteger(), nullable=False)
5581 file_size = Column('file_size', BigInteger(), nullable=False)
5564
5582
5565 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5583 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5566 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True)
5584 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True)
5567 accessed_count = Column('accessed_count', Integer(), default=0)
5585 accessed_count = Column('accessed_count', Integer(), default=0)
5568
5586
5569 enabled = Column('enabled', Boolean(), nullable=False, default=True)
5587 enabled = Column('enabled', Boolean(), nullable=False, default=True)
5570
5588
5571 # if repo/repo_group reference is set, check for permissions
5589 # if repo/repo_group reference is set, check for permissions
5572 check_acl = Column('check_acl', Boolean(), nullable=False, default=True)
5590 check_acl = Column('check_acl', Boolean(), nullable=False, default=True)
5573
5591
5574 # hidden defines an attachment that should be hidden from showing in artifact listing
5592 # hidden defines an attachment that should be hidden from showing in artifact listing
5575 hidden = Column('hidden', Boolean(), nullable=False, default=False)
5593 hidden = Column('hidden', Boolean(), nullable=False, default=False)
5576
5594
5577 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
5595 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
5578 upload_user = relationship('User', lazy='joined', primaryjoin='User.user_id==FileStore.user_id', back_populates='artifacts')
5596 upload_user = relationship('User', lazy='joined', primaryjoin='User.user_id==FileStore.user_id', back_populates='artifacts')
5579
5597
5580 file_metadata = relationship('FileStoreMetadata', lazy='joined')
5598 file_metadata = relationship('FileStoreMetadata', lazy='joined')
5581
5599
5582 # scope limited to user, which requester have access to
5600 # scope limited to user, which requester have access to
5583 scope_user_id = Column(
5601 scope_user_id = Column(
5584 'scope_user_id', Integer(), ForeignKey('users.user_id'),
5602 'scope_user_id', Integer(), ForeignKey('users.user_id'),
5585 nullable=True, unique=None, default=None)
5603 nullable=True, unique=None, default=None)
5586 user = relationship('User', lazy='joined', primaryjoin='User.user_id==FileStore.scope_user_id', back_populates='scope_artifacts')
5604 user = relationship('User', lazy='joined', primaryjoin='User.user_id==FileStore.scope_user_id', back_populates='scope_artifacts')
5587
5605
5588 # scope limited to user group, which requester have access to
5606 # scope limited to user group, which requester have access to
5589 scope_user_group_id = Column(
5607 scope_user_group_id = Column(
5590 'scope_user_group_id', Integer(), ForeignKey('users_groups.users_group_id'),
5608 'scope_user_group_id', Integer(), ForeignKey('users_groups.users_group_id'),
5591 nullable=True, unique=None, default=None)
5609 nullable=True, unique=None, default=None)
5592 user_group = relationship('UserGroup', lazy='joined')
5610 user_group = relationship('UserGroup', lazy='joined')
5593
5611
5594 # scope limited to repo, which requester have access to
5612 # scope limited to repo, which requester have access to
5595 scope_repo_id = Column(
5613 scope_repo_id = Column(
5596 'scope_repo_id', Integer(), ForeignKey('repositories.repo_id'),
5614 'scope_repo_id', Integer(), ForeignKey('repositories.repo_id'),
5597 nullable=True, unique=None, default=None)
5615 nullable=True, unique=None, default=None)
5598 repo = relationship('Repository', lazy='joined')
5616 repo = relationship('Repository', lazy='joined')
5599
5617
5600 # scope limited to repo group, which requester have access to
5618 # scope limited to repo group, which requester have access to
5601 scope_repo_group_id = Column(
5619 scope_repo_group_id = Column(
5602 'scope_repo_group_id', Integer(), ForeignKey('groups.group_id'),
5620 'scope_repo_group_id', Integer(), ForeignKey('groups.group_id'),
5603 nullable=True, unique=None, default=None)
5621 nullable=True, unique=None, default=None)
5604 repo_group = relationship('RepoGroup', lazy='joined')
5622 repo_group = relationship('RepoGroup', lazy='joined')
5605
5623
5606 @classmethod
5624 @classmethod
5607 def get_scope(cls, scope_type, scope_id):
5625 def get_scope(cls, scope_type, scope_id):
5608 if scope_type == 'repo':
5626 if scope_type == 'repo':
5609 return f'repo:{scope_id}'
5627 return f'repo:{scope_id}'
5610 elif scope_type == 'repo-group':
5628 elif scope_type == 'repo-group':
5611 return f'repo-group:{scope_id}'
5629 return f'repo-group:{scope_id}'
5612 elif scope_type == 'user':
5630 elif scope_type == 'user':
5613 return f'user:{scope_id}'
5631 return f'user:{scope_id}'
5614 elif scope_type == 'user-group':
5632 elif scope_type == 'user-group':
5615 return f'user-group:{scope_id}'
5633 return f'user-group:{scope_id}'
5616 else:
5634 else:
5617 return scope_type
5635 return scope_type
5618
5636
5619 @classmethod
5637 @classmethod
5620 def get_by_store_uid(cls, file_store_uid, safe=False):
5638 def get_by_store_uid(cls, file_store_uid, safe=False):
5621 if safe:
5639 if safe:
5622 return FileStore.query().filter(FileStore.file_uid == file_store_uid).first()
5640 return FileStore.query().filter(FileStore.file_uid == file_store_uid).first()
5623 else:
5641 else:
5624 return FileStore.query().filter(FileStore.file_uid == file_store_uid).scalar()
5642 return FileStore.query().filter(FileStore.file_uid == file_store_uid).scalar()
5625
5643
5626 @classmethod
5644 @classmethod
5627 def create(cls, file_uid, filename, file_hash, file_size, file_display_name='',
5645 def create(cls, file_uid, filename, file_hash, file_size, file_display_name='',
5628 file_description='', enabled=True, hidden=False, check_acl=True,
5646 file_description='', enabled=True, hidden=False, check_acl=True,
5629 user_id=None, scope_user_id=None, scope_repo_id=None, scope_repo_group_id=None):
5647 user_id=None, scope_user_id=None, scope_repo_id=None, scope_repo_group_id=None):
5630
5648
5631 store_entry = FileStore()
5649 store_entry = FileStore()
5632 store_entry.file_uid = file_uid
5650 store_entry.file_uid = file_uid
5633 store_entry.file_display_name = file_display_name
5651 store_entry.file_display_name = file_display_name
5634 store_entry.file_org_name = filename
5652 store_entry.file_org_name = filename
5635 store_entry.file_size = file_size
5653 store_entry.file_size = file_size
5636 store_entry.file_hash = file_hash
5654 store_entry.file_hash = file_hash
5637 store_entry.file_description = file_description
5655 store_entry.file_description = file_description
5638
5656
5639 store_entry.check_acl = check_acl
5657 store_entry.check_acl = check_acl
5640 store_entry.enabled = enabled
5658 store_entry.enabled = enabled
5641 store_entry.hidden = hidden
5659 store_entry.hidden = hidden
5642
5660
5643 store_entry.user_id = user_id
5661 store_entry.user_id = user_id
5644 store_entry.scope_user_id = scope_user_id
5662 store_entry.scope_user_id = scope_user_id
5645 store_entry.scope_repo_id = scope_repo_id
5663 store_entry.scope_repo_id = scope_repo_id
5646 store_entry.scope_repo_group_id = scope_repo_group_id
5664 store_entry.scope_repo_group_id = scope_repo_group_id
5647
5665
5648 return store_entry
5666 return store_entry
5649
5667
5650 @classmethod
5668 @classmethod
5651 def store_metadata(cls, file_store_id, args, commit=True):
5669 def store_metadata(cls, file_store_id, args, commit=True):
5652 file_store = FileStore.get(file_store_id)
5670 file_store = FileStore.get(file_store_id)
5653 if file_store is None:
5671 if file_store is None:
5654 return
5672 return
5655
5673
5656 for section, key, value, value_type in args:
5674 for section, key, value, value_type in args:
5657 has_key = FileStoreMetadata().query() \
5675 has_key = FileStoreMetadata().query() \
5658 .filter(FileStoreMetadata.file_store_id == file_store.file_store_id) \
5676 .filter(FileStoreMetadata.file_store_id == file_store.file_store_id) \
5659 .filter(FileStoreMetadata.file_store_meta_section == section) \
5677 .filter(FileStoreMetadata.file_store_meta_section == section) \
5660 .filter(FileStoreMetadata.file_store_meta_key == key) \
5678 .filter(FileStoreMetadata.file_store_meta_key == key) \
5661 .scalar()
5679 .scalar()
5662 if has_key:
5680 if has_key:
5663 msg = 'key `{}` already defined under section `{}` for this file.'\
5681 msg = 'key `{}` already defined under section `{}` for this file.'\
5664 .format(key, section)
5682 .format(key, section)
5665 raise ArtifactMetadataDuplicate(msg, err_section=section, err_key=key)
5683 raise ArtifactMetadataDuplicate(msg, err_section=section, err_key=key)
5666
5684
5667 # NOTE(marcink): raises ArtifactMetadataBadValueType
5685 # NOTE(marcink): raises ArtifactMetadataBadValueType
5668 FileStoreMetadata.valid_value_type(value_type)
5686 FileStoreMetadata.valid_value_type(value_type)
5669
5687
5670 meta_entry = FileStoreMetadata()
5688 meta_entry = FileStoreMetadata()
5671 meta_entry.file_store = file_store
5689 meta_entry.file_store = file_store
5672 meta_entry.file_store_meta_section = section
5690 meta_entry.file_store_meta_section = section
5673 meta_entry.file_store_meta_key = key
5691 meta_entry.file_store_meta_key = key
5674 meta_entry.file_store_meta_value_type = value_type
5692 meta_entry.file_store_meta_value_type = value_type
5675 meta_entry.file_store_meta_value = value
5693 meta_entry.file_store_meta_value = value
5676
5694
5677 Session().add(meta_entry)
5695 Session().add(meta_entry)
5678
5696
5679 try:
5697 try:
5680 if commit:
5698 if commit:
5681 Session().commit()
5699 Session().commit()
5682 except IntegrityError:
5700 except IntegrityError:
5683 Session().rollback()
5701 Session().rollback()
5684 raise ArtifactMetadataDuplicate('Duplicate section/key found for this file.')
5702 raise ArtifactMetadataDuplicate('Duplicate section/key found for this file.')
5685
5703
5686 @classmethod
5704 @classmethod
5687 def bump_access_counter(cls, file_uid, commit=True):
5705 def bump_access_counter(cls, file_uid, commit=True):
5688 FileStore().query()\
5706 FileStore().query()\
5689 .filter(FileStore.file_uid == file_uid)\
5707 .filter(FileStore.file_uid == file_uid)\
5690 .update({FileStore.accessed_count: (FileStore.accessed_count + 1),
5708 .update({FileStore.accessed_count: (FileStore.accessed_count + 1),
5691 FileStore.accessed_on: datetime.datetime.now()})
5709 FileStore.accessed_on: datetime.datetime.now()})
5692 if commit:
5710 if commit:
5693 Session().commit()
5711 Session().commit()
5694
5712
5695 def __json__(self):
5713 def __json__(self):
5696 data = {
5714 data = {
5697 'filename': self.file_display_name,
5715 'filename': self.file_display_name,
5698 'filename_org': self.file_org_name,
5716 'filename_org': self.file_org_name,
5699 'file_uid': self.file_uid,
5717 'file_uid': self.file_uid,
5700 'description': self.file_description,
5718 'description': self.file_description,
5701 'hidden': self.hidden,
5719 'hidden': self.hidden,
5702 'size': self.file_size,
5720 'size': self.file_size,
5703 'created_on': self.created_on,
5721 'created_on': self.created_on,
5704 'uploaded_by': self.upload_user.get_api_data(details='basic'),
5722 'uploaded_by': self.upload_user.get_api_data(details='basic'),
5705 'downloaded_times': self.accessed_count,
5723 'downloaded_times': self.accessed_count,
5706 'sha256': self.file_hash,
5724 'sha256': self.file_hash,
5707 'metadata': self.file_metadata,
5725 'metadata': self.file_metadata,
5708 }
5726 }
5709
5727
5710 return data
5728 return data
5711
5729
5712 def __repr__(self):
5730 def __repr__(self):
5713 return f'<FileStore({self.file_store_id})>'
5731 return f'<FileStore({self.file_store_id})>'
5714
5732
5715
5733
5716 class FileStoreMetadata(Base, BaseModel):
5734 class FileStoreMetadata(Base, BaseModel):
5717 __tablename__ = 'file_store_metadata'
5735 __tablename__ = 'file_store_metadata'
5718 __table_args__ = (
5736 __table_args__ = (
5719 UniqueConstraint('file_store_id', 'file_store_meta_section_hash', 'file_store_meta_key_hash'),
5737 UniqueConstraint('file_store_id', 'file_store_meta_section_hash', 'file_store_meta_key_hash'),
5720 Index('file_store_meta_section_idx', 'file_store_meta_section', mysql_length=255),
5738 Index('file_store_meta_section_idx', 'file_store_meta_section', mysql_length=255),
5721 Index('file_store_meta_key_idx', 'file_store_meta_key', mysql_length=255),
5739 Index('file_store_meta_key_idx', 'file_store_meta_key', mysql_length=255),
5722 base_table_args
5740 base_table_args
5723 )
5741 )
5724 SETTINGS_TYPES = {
5742 SETTINGS_TYPES = {
5725 'str': safe_str,
5743 'str': safe_str,
5726 'int': safe_int,
5744 'int': safe_int,
5727 'unicode': safe_str,
5745 'unicode': safe_str,
5728 'bool': str2bool,
5746 'bool': str2bool,
5729 'list': functools.partial(aslist, sep=',')
5747 'list': functools.partial(aslist, sep=',')
5730 }
5748 }
5731
5749
5732 file_store_meta_id = Column(
5750 file_store_meta_id = Column(
5733 "file_store_meta_id", Integer(), nullable=False, unique=True, default=None,
5751 "file_store_meta_id", Integer(), nullable=False, unique=True, default=None,
5734 primary_key=True)
5752 primary_key=True)
5735 _file_store_meta_section = Column(
5753 _file_store_meta_section = Column(
5736 "file_store_meta_section", UnicodeText().with_variant(UnicodeText(1024), 'mysql'),
5754 "file_store_meta_section", UnicodeText().with_variant(UnicodeText(1024), 'mysql'),
5737 nullable=True, unique=None, default=None)
5755 nullable=True, unique=None, default=None)
5738 _file_store_meta_section_hash = Column(
5756 _file_store_meta_section_hash = Column(
5739 "file_store_meta_section_hash", String(255),
5757 "file_store_meta_section_hash", String(255),
5740 nullable=True, unique=None, default=None)
5758 nullable=True, unique=None, default=None)
5741 _file_store_meta_key = Column(
5759 _file_store_meta_key = Column(
5742 "file_store_meta_key", UnicodeText().with_variant(UnicodeText(1024), 'mysql'),
5760 "file_store_meta_key", UnicodeText().with_variant(UnicodeText(1024), 'mysql'),
5743 nullable=True, unique=None, default=None)
5761 nullable=True, unique=None, default=None)
5744 _file_store_meta_key_hash = Column(
5762 _file_store_meta_key_hash = Column(
5745 "file_store_meta_key_hash", String(255), nullable=True, unique=None, default=None)
5763 "file_store_meta_key_hash", String(255), nullable=True, unique=None, default=None)
5746 _file_store_meta_value = Column(
5764 _file_store_meta_value = Column(
5747 "file_store_meta_value", UnicodeText().with_variant(UnicodeText(20480), 'mysql'),
5765 "file_store_meta_value", UnicodeText().with_variant(UnicodeText(20480), 'mysql'),
5748 nullable=True, unique=None, default=None)
5766 nullable=True, unique=None, default=None)
5749 _file_store_meta_value_type = Column(
5767 _file_store_meta_value_type = Column(
5750 "file_store_meta_value_type", String(255), nullable=True, unique=None,
5768 "file_store_meta_value_type", String(255), nullable=True, unique=None,
5751 default='unicode')
5769 default='unicode')
5752
5770
5753 file_store_id = Column(
5771 file_store_id = Column(
5754 'file_store_id', Integer(), ForeignKey('file_store.file_store_id'),
5772 'file_store_id', Integer(), ForeignKey('file_store.file_store_id'),
5755 nullable=True, unique=None, default=None)
5773 nullable=True, unique=None, default=None)
5756
5774
5757 file_store = relationship('FileStore', lazy='joined', viewonly=True)
5775 file_store = relationship('FileStore', lazy='joined', viewonly=True)
5758
5776
5759 @classmethod
5777 @classmethod
5760 def valid_value_type(cls, value):
5778 def valid_value_type(cls, value):
5761 if value.split('.')[0] not in cls.SETTINGS_TYPES:
5779 if value.split('.')[0] not in cls.SETTINGS_TYPES:
5762 raise ArtifactMetadataBadValueType(
5780 raise ArtifactMetadataBadValueType(
5763 'value_type must be one of %s got %s' % (cls.SETTINGS_TYPES.keys(), value))
5781 'value_type must be one of %s got %s' % (cls.SETTINGS_TYPES.keys(), value))
5764
5782
5765 @hybrid_property
5783 @hybrid_property
5766 def file_store_meta_section(self):
5784 def file_store_meta_section(self):
5767 return self._file_store_meta_section
5785 return self._file_store_meta_section
5768
5786
5769 @file_store_meta_section.setter
5787 @file_store_meta_section.setter
5770 def file_store_meta_section(self, value):
5788 def file_store_meta_section(self, value):
5771 self._file_store_meta_section = value
5789 self._file_store_meta_section = value
5772 self._file_store_meta_section_hash = _hash_key(value)
5790 self._file_store_meta_section_hash = _hash_key(value)
5773
5791
5774 @hybrid_property
5792 @hybrid_property
5775 def file_store_meta_key(self):
5793 def file_store_meta_key(self):
5776 return self._file_store_meta_key
5794 return self._file_store_meta_key
5777
5795
5778 @file_store_meta_key.setter
5796 @file_store_meta_key.setter
5779 def file_store_meta_key(self, value):
5797 def file_store_meta_key(self, value):
5780 self._file_store_meta_key = value
5798 self._file_store_meta_key = value
5781 self._file_store_meta_key_hash = _hash_key(value)
5799 self._file_store_meta_key_hash = _hash_key(value)
5782
5800
5783 @hybrid_property
5801 @hybrid_property
5784 def file_store_meta_value(self):
5802 def file_store_meta_value(self):
5785 val = self._file_store_meta_value
5803 val = self._file_store_meta_value
5786
5804
5787 if self._file_store_meta_value_type:
5805 if self._file_store_meta_value_type:
5788 # e.g unicode.encrypted == unicode
5806 # e.g unicode.encrypted == unicode
5789 _type = self._file_store_meta_value_type.split('.')[0]
5807 _type = self._file_store_meta_value_type.split('.')[0]
5790 # decode the encrypted value if it's encrypted field type
5808 # decode the encrypted value if it's encrypted field type
5791 if '.encrypted' in self._file_store_meta_value_type:
5809 if '.encrypted' in self._file_store_meta_value_type:
5792 cipher = EncryptedTextValue()
5810 cipher = EncryptedTextValue()
5793 val = safe_str(cipher.process_result_value(val, None))
5811 val = safe_str(cipher.process_result_value(val, None))
5794 # do final type conversion
5812 # do final type conversion
5795 converter = self.SETTINGS_TYPES.get(_type) or self.SETTINGS_TYPES['unicode']
5813 converter = self.SETTINGS_TYPES.get(_type) or self.SETTINGS_TYPES['unicode']
5796 val = converter(val)
5814 val = converter(val)
5797
5815
5798 return val
5816 return val
5799
5817
5800 @file_store_meta_value.setter
5818 @file_store_meta_value.setter
5801 def file_store_meta_value(self, val):
5819 def file_store_meta_value(self, val):
5802 val = safe_str(val)
5820 val = safe_str(val)
5803 # encode the encrypted value
5821 # encode the encrypted value
5804 if '.encrypted' in self.file_store_meta_value_type:
5822 if '.encrypted' in self.file_store_meta_value_type:
5805 cipher = EncryptedTextValue()
5823 cipher = EncryptedTextValue()
5806 val = safe_str(cipher.process_bind_param(val, None))
5824 val = safe_str(cipher.process_bind_param(val, None))
5807 self._file_store_meta_value = val
5825 self._file_store_meta_value = val
5808
5826
5809 @hybrid_property
5827 @hybrid_property
5810 def file_store_meta_value_type(self):
5828 def file_store_meta_value_type(self):
5811 return self._file_store_meta_value_type
5829 return self._file_store_meta_value_type
5812
5830
5813 @file_store_meta_value_type.setter
5831 @file_store_meta_value_type.setter
5814 def file_store_meta_value_type(self, val):
5832 def file_store_meta_value_type(self, val):
5815 # e.g unicode.encrypted
5833 # e.g unicode.encrypted
5816 self.valid_value_type(val)
5834 self.valid_value_type(val)
5817 self._file_store_meta_value_type = val
5835 self._file_store_meta_value_type = val
5818
5836
5819 def __json__(self):
5837 def __json__(self):
5820 data = {
5838 data = {
5821 'artifact': self.file_store.file_uid,
5839 'artifact': self.file_store.file_uid,
5822 'section': self.file_store_meta_section,
5840 'section': self.file_store_meta_section,
5823 'key': self.file_store_meta_key,
5841 'key': self.file_store_meta_key,
5824 'value': self.file_store_meta_value,
5842 'value': self.file_store_meta_value,
5825 }
5843 }
5826
5844
5827 return data
5845 return data
5828
5846
5829 def __repr__(self):
5847 def __repr__(self):
5830 return '<%s[%s]%s=>%s]>' % (self.cls_name, self.file_store_meta_section,
5848 return '<%s[%s]%s=>%s]>' % (self.cls_name, self.file_store_meta_section,
5831 self.file_store_meta_key, self.file_store_meta_value)
5849 self.file_store_meta_key, self.file_store_meta_value)
5832
5850
5833
5851
5834 class DbMigrateVersion(Base, BaseModel):
5852 class DbMigrateVersion(Base, BaseModel):
5835 __tablename__ = 'db_migrate_version'
5853 __tablename__ = 'db_migrate_version'
5836 __table_args__ = (
5854 __table_args__ = (
5837 base_table_args,
5855 base_table_args,
5838 )
5856 )
5839
5857
5840 repository_id = Column('repository_id', String(250), primary_key=True)
5858 repository_id = Column('repository_id', String(250), primary_key=True)
5841 repository_path = Column('repository_path', Text)
5859 repository_path = Column('repository_path', Text)
5842 version = Column('version', Integer)
5860 version = Column('version', Integer)
5843
5861
5844 @classmethod
5862 @classmethod
5845 def set_version(cls, version):
5863 def set_version(cls, version):
5846 """
5864 """
5847 Helper for forcing a different version, usually for debugging purposes via ishell.
5865 Helper for forcing a different version, usually for debugging purposes via ishell.
5848 """
5866 """
5849 ver = DbMigrateVersion.query().first()
5867 ver = DbMigrateVersion.query().first()
5850 ver.version = version
5868 ver.version = version
5851 Session().commit()
5869 Session().commit()
5852
5870
5853
5871
5854 class DbSession(Base, BaseModel):
5872 class DbSession(Base, BaseModel):
5855 __tablename__ = 'db_session'
5873 __tablename__ = 'db_session'
5856 __table_args__ = (
5874 __table_args__ = (
5857 base_table_args,
5875 base_table_args,
5858 )
5876 )
5859
5877
5860 def __repr__(self):
5878 def __repr__(self):
5861 return f'<DB:DbSession({self.id})>'
5879 return f'<DB:DbSession({self.id})>'
5862
5880
5863 id = Column('id', Integer())
5881 id = Column('id', Integer())
5864 namespace = Column('namespace', String(255), primary_key=True)
5882 namespace = Column('namespace', String(255), primary_key=True)
5865 accessed = Column('accessed', DateTime, nullable=False)
5883 accessed = Column('accessed', DateTime, nullable=False)
5866 created = Column('created', DateTime, nullable=False)
5884 created = Column('created', DateTime, nullable=False)
5867 data = Column('data', PickleType, nullable=False)
5885 data = Column('data', PickleType, nullable=False)
@@ -1,1404 +1,1403 b''
1 <%namespace name="base" file="/base/base.mako"/>
1 <%namespace name="base" file="/base/base.mako"/>
2 <%namespace name="commentblock" file="/changeset/changeset_file_comment.mako"/>
2 <%namespace name="commentblock" file="/changeset/changeset_file_comment.mako"/>
3
3
4 <%def name="diff_line_anchor(commit, filename, line, type)"><%
4 <%def name="diff_line_anchor(commit, filename, line, type)"><%
5 return '%s_%s_%i' % (h.md5_safe(commit+filename), type, line)
5 return '%s_%s_%i' % (h.md5_safe(commit+filename), type, line)
6 %></%def>
6 %></%def>
7
7
8 <%def name="action_class(action)">
8 <%def name="action_class(action)">
9 <%
9 <%
10 return {
10 return {
11 '-': 'cb-deletion',
11 '-': 'cb-deletion',
12 '+': 'cb-addition',
12 '+': 'cb-addition',
13 ' ': 'cb-context',
13 ' ': 'cb-context',
14 }.get(action, 'cb-empty')
14 }.get(action, 'cb-empty')
15 %>
15 %>
16 </%def>
16 </%def>
17
17
18 <%def name="op_class(op_id)">
18 <%def name="op_class(op_id)">
19 <%
19 <%
20 return {
20 return {
21 DEL_FILENODE: 'deletion', # file deleted
21 DEL_FILENODE: 'deletion', # file deleted
22 BIN_FILENODE: 'warning' # binary diff hidden
22 BIN_FILENODE: 'warning' # binary diff hidden
23 }.get(op_id, 'addition')
23 }.get(op_id, 'addition')
24 %>
24 %>
25 </%def>
25 </%def>
26
26
27
27
28
28
29 <%def name="render_diffset(diffset, commit=None,
29 <%def name="render_diffset(diffset, commit=None,
30
30
31 # collapse all file diff entries when there are more than this amount of files in the diff
31 # collapse all file diff entries when there are more than this amount of files in the diff
32 collapse_when_files_over=20,
32 collapse_when_files_over=20,
33
33
34 # collapse lines in the diff when more than this amount of lines changed in the file diff
34 # collapse lines in the diff when more than this amount of lines changed in the file diff
35 lines_changed_limit=500,
35 lines_changed_limit=500,
36
36
37 # add a ruler at to the output
37 # add a ruler at to the output
38 ruler_at_chars=0,
38 ruler_at_chars=0,
39
39
40 # show inline comments
40 # show inline comments
41 use_comments=False,
41 use_comments=False,
42
42
43 # disable new comments
43 # disable new comments
44 disable_new_comments=False,
44 disable_new_comments=False,
45
45
46 # special file-comments that were deleted in previous versions
46 # special file-comments that were deleted in previous versions
47 # it's used for showing outdated comments for deleted files in a PR
47 # it's used for showing outdated comments for deleted files in a PR
48 deleted_files_comments=None,
48 deleted_files_comments=None,
49
49
50 # for cache purpose
50 # for cache purpose
51 inline_comments=None,
51 inline_comments=None,
52
52
53 # additional menu for PRs
53 # additional menu for PRs
54 pull_request_menu=None,
54 pull_request_menu=None,
55
55
56 # show/hide todo next to comments
56 # show/hide todo next to comments
57 show_todos=True,
57 show_todos=True,
58
58
59 )">
59 )">
60
60
61 <%
61 <%
62 diffset_container_id = h.md5_safe(diffset.target_ref)
62 diffset_container_id = h.md5_safe(diffset.target_ref)
63 collapse_all = len(diffset.files) > collapse_when_files_over
63 collapse_all = len(diffset.files) > collapse_when_files_over
64 active_pattern_entries = h.get_active_pattern_entries(getattr(c, 'repo_name', None))
64 active_pattern_entries = h.get_active_pattern_entries(getattr(c, 'repo_name', None))
65 from rhodecode.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
65 from rhodecode.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
66 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE, COPIED_FILENODE
66 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE, COPIED_FILENODE
67 %>
67 %>
68
68
69 %if use_comments:
69 %if use_comments:
70
70
71 ## Template for injecting comments
71 ## Template for injecting comments
72 <div id="cb-comments-inline-container-template" class="js-template">
72 <div id="cb-comments-inline-container-template" class="js-template">
73 ${inline_comments_container([])}
73 ${inline_comments_container([])}
74 </div>
74 </div>
75
75
76 <div class="js-template" id="cb-comment-inline-form-template">
76 <div class="js-template" id="cb-comment-inline-form-template">
77 <div class="comment-inline-form ac">
77 <div class="comment-inline-form ac">
78 %if not c.rhodecode_user.is_default:
78 %if not c.rhodecode_user.is_default:
79 ## render template for inline comments
79 ## render template for inline comments
80 ${commentblock.comment_form(form_type='inline')}
80 ${commentblock.comment_form(form_type='inline')}
81 %endif
81 %endif
82 </div>
82 </div>
83 </div>
83 </div>
84
84
85 %endif
85 %endif
86
86
87 %if c.user_session_attrs["diffmode"] == 'sideside':
87 %if c.user_session_attrs["diffmode"] == 'sideside':
88 <style>
88 <style>
89 .wrapper {
89 .wrapper {
90 max-width: 1600px !important;
90 max-width: 1600px !important;
91 }
91 }
92 </style>
92 </style>
93 %endif
93 %endif
94
94
95 %if ruler_at_chars:
95 %if ruler_at_chars:
96 <style>
96 <style>
97 .diff table.cb .cb-content:after {
97 .diff table.cb .cb-content:after {
98 content: "";
98 content: "";
99 border-left: 1px solid blue;
99 border-left: 1px solid blue;
100 position: absolute;
100 position: absolute;
101 top: 0;
101 top: 0;
102 height: 18px;
102 height: 18px;
103 opacity: .2;
103 opacity: .2;
104 z-index: 10;
104 z-index: 10;
105 //## +5 to account for diff action (+/-)
105 //## +5 to account for diff action (+/-)
106 left: ${ruler_at_chars + 5}ch;
106 left: ${ruler_at_chars + 5}ch;
107 </style>
107 </style>
108 %endif
108 %endif
109
109
110 <div class="diffset ${disable_new_comments and 'diffset-comments-disabled'}">
110 <div class="diffset ${disable_new_comments and 'diffset-comments-disabled'}">
111
111
112 <div style="height: 20px; line-height: 20px">
112 <div style="height: 20px; line-height: 20px">
113 ## expand/collapse action
113 ## expand/collapse action
114 <div class="pull-left">
114 <div class="pull-left">
115 <a class="${'collapsed' if collapse_all else ''}" href="#expand-files" onclick="toggleExpand(this, '${diffset_container_id}'); return false">
115 <a class="${'collapsed' if collapse_all else ''}" href="#expand-files" onclick="toggleExpand(this, '${diffset_container_id}'); return false">
116 % if collapse_all:
116 % if collapse_all:
117 <i class="icon-plus-squared-alt icon-no-margin"></i>${_('Expand all files')}
117 <i class="icon-plus-squared-alt icon-no-margin"></i>${_('Expand all files')}
118 % else:
118 % else:
119 <i class="icon-minus-squared-alt icon-no-margin"></i>${_('Collapse all files')}
119 <i class="icon-minus-squared-alt icon-no-margin"></i>${_('Collapse all files')}
120 % endif
120 % endif
121 </a>
121 </a>
122
122
123 </div>
123 </div>
124
124
125 ## todos
125 ## todos
126 % if show_todos and getattr(c, 'at_version', None):
126 % if show_todos and getattr(c, 'at_version', None):
127 <div class="pull-right">
127 <div class="pull-right">
128 <i class="icon-flag-filled" style="color: #949494">TODOs:</i>
128 <i class="icon-flag-filled" style="color: #949494">TODOs:</i>
129 ${_('not available in this view')}
129 ${_('not available in this view')}
130 </div>
130 </div>
131 % elif show_todos:
131 % elif show_todos:
132 <div class="pull-right">
132 <div class="pull-right">
133 <div class="comments-number" style="padding-left: 10px">
133 <div class="comments-number" style="padding-left: 10px">
134 % if hasattr(c, 'unresolved_comments') and hasattr(c, 'resolved_comments'):
134 % if hasattr(c, 'unresolved_comments') and hasattr(c, 'resolved_comments'):
135 <i class="icon-flag-filled" style="color: #949494">TODOs:</i>
135 <i class="icon-flag-filled" style="color: #949494">TODOs:</i>
136 % if c.unresolved_comments:
136 % if c.unresolved_comments:
137 <a href="#show-todos" onclick="$('#todo-box').toggle(); return false">
137 <a href="#show-todos" onclick="$('#todo-box').toggle(); return false">
138 ${_('{} unresolved').format(len(c.unresolved_comments))}
138 ${_('{} unresolved').format(len(c.unresolved_comments))}
139 </a>
139 </a>
140 % else:
140 % else:
141 ${_('0 unresolved')}
141 ${_('0 unresolved')}
142 % endif
142 % endif
143
143
144 ${_('{} Resolved').format(len(c.resolved_comments))}
144 ${_('{} Resolved').format(len(c.resolved_comments))}
145 % endif
145 % endif
146 </div>
146 </div>
147 </div>
147 </div>
148 % endif
148 % endif
149
149
150 ## ## comments
150 ## ## comments
151 ## <div class="pull-right">
151 ## <div class="pull-right">
152 ## <div class="comments-number" style="padding-left: 10px">
152 ## <div class="comments-number" style="padding-left: 10px">
153 ## % if hasattr(c, 'comments') and hasattr(c, 'inline_cnt'):
153 ## % if hasattr(c, 'comments') and hasattr(c, 'inline_cnt'):
154 ## <i class="icon-comment" style="color: #949494">COMMENTS:</i>
154 ## <i class="icon-comment" style="color: #949494">COMMENTS:</i>
155 ## % if c.comments:
155 ## % if c.comments:
156 ## <a href="#comments">${_ungettext("{} General", "{} General", len(c.comments)).format(len(c.comments))}</a>,
156 ## <a href="#comments">${_ungettext("{} General", "{} General", len(c.comments)).format(len(c.comments))}</a>,
157 ## % else:
157 ## % else:
158 ## ${_('0 General')}
158 ## ${_('0 General')}
159 ## % endif
159 ## % endif
160 ##
160 ##
161 ## % if c.inline_cnt:
161 ## % if c.inline_cnt:
162 ## <a href="#" onclick="return Rhodecode.comments.nextComment();"
162 ## <a href="#" onclick="return Rhodecode.comments.nextComment();"
163 ## id="inline-comments-counter">${_ungettext("{} Inline", "{} Inline", c.inline_cnt).format(c.inline_cnt)}
163 ## id="inline-comments-counter">${_ungettext("{} Inline", "{} Inline", c.inline_cnt).format(c.inline_cnt)}
164 ## </a>
164 ## </a>
165 ## % else:
165 ## % else:
166 ## ${_('0 Inline')}
166 ## ${_('0 Inline')}
167 ## % endif
167 ## % endif
168 ## % endif
168 ## % endif
169 ##
169 ##
170 ## % if pull_request_menu:
170 ## % if pull_request_menu:
171 ## <%
171 ## <%
172 ## outdated_comm_count_ver = pull_request_menu['outdated_comm_count_ver']
172 ## outdated_comm_count_ver = pull_request_menu['outdated_comm_count_ver']
173 ## %>
173 ## %>
174 ##
174 ##
175 ## % if outdated_comm_count_ver:
175 ## % if outdated_comm_count_ver:
176 ## <a href="#" onclick="showOutdated(); Rhodecode.comments.nextOutdatedComment(); return false;">
176 ## <a href="#" onclick="showOutdated(); Rhodecode.comments.nextOutdatedComment(); return false;">
177 ## (${_("{} Outdated").format(outdated_comm_count_ver)})
177 ## (${_("{} Outdated").format(outdated_comm_count_ver)})
178 ## </a>
178 ## </a>
179 ## <a href="#" class="showOutdatedComments" onclick="showOutdated(this); return false;"> | ${_('show outdated')}</a>
179 ## <a href="#" class="showOutdatedComments" onclick="showOutdated(this); return false;"> | ${_('show outdated')}</a>
180 ## <a href="#" class="hideOutdatedComments" style="display: none" onclick="hideOutdated(this); return false;"> | ${_('hide outdated')}</a>
180 ## <a href="#" class="hideOutdatedComments" style="display: none" onclick="hideOutdated(this); return false;"> | ${_('hide outdated')}</a>
181 ## % else:
181 ## % else:
182 ## (${_("{} Outdated").format(outdated_comm_count_ver)})
182 ## (${_("{} Outdated").format(outdated_comm_count_ver)})
183 ## % endif
183 ## % endif
184 ##
184 ##
185 ## % endif
185 ## % endif
186 ##
186 ##
187 ## </div>
187 ## </div>
188 ## </div>
188 ## </div>
189
189
190 </div>
190 </div>
191
191
192 % if diffset.limited_diff:
192 % if diffset.limited_diff:
193 <div class="diffset-heading ${(diffset.limited_diff and 'diffset-heading-warning' or '')}">
193 <div class="diffset-heading ${(diffset.limited_diff and 'diffset-heading-warning' or '')}">
194 <h2 class="clearinner">
194 <h2 class="clearinner">
195 ${_('The requested changes are too big and content was truncated.')}
195 ${_('The requested changes are too big and content was truncated.')}
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>
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 </h2>
197 </h2>
198 </div>
198 </div>
199 % endif
199 % endif
200
200
201 <div id="todo-box">
201 <div id="todo-box">
202 % if hasattr(c, 'unresolved_comments') and c.unresolved_comments:
202 % if hasattr(c, 'unresolved_comments') and c.unresolved_comments:
203 % for co in c.unresolved_comments:
203 % for co in c.unresolved_comments:
204 <a class="permalink" href="#comment-${co.comment_id}"
204 <a class="permalink" href="#comment-${co.comment_id}"
205 onclick="Rhodecode.comments.scrollToComment($('#comment-${co.comment_id}'))">
205 onclick="Rhodecode.comments.scrollToComment($('#comment-${co.comment_id}'))">
206 <i class="icon-flag-filled-red"></i>
206 <i class="icon-flag-filled-red"></i>
207 ${co.comment_id}</a>${('' if loop.last else ',')}
207 ${co.comment_id}</a>${('' if loop.last else ',')}
208 % endfor
208 % endfor
209 % endif
209 % endif
210 </div>
210 </div>
211 %if diffset.has_hidden_changes:
211 %if diffset.has_hidden_changes:
212 <p class="empty_data">${_('Some changes may be hidden')}</p>
212 <p class="empty_data">${_('Some changes may be hidden')}</p>
213 %elif not diffset.files:
213 %elif not diffset.files:
214 <p class="empty_data">${_('No files')}</p>
214 <p class="empty_data">${_('No files')}</p>
215 %endif
215 %endif
216
216
217 <div class="filediffs">
217 <div class="filediffs">
218
218
219 ## initial value could be marked as False later on
219 ## initial value could be marked as False later on
220 <% over_lines_changed_limit = False %>
220 <% over_lines_changed_limit = False %>
221 %for i, filediff in enumerate(diffset.files):
221 %for i, filediff in enumerate(diffset.files):
222
222
223 %if filediff.source_file_path and filediff.target_file_path:
223 %if filediff.source_file_path and filediff.target_file_path:
224 %if filediff.source_file_path != filediff.target_file_path:
224 %if filediff.source_file_path != filediff.target_file_path:
225 ## file was renamed, or copied
225 ## file was renamed, or copied
226 %if RENAMED_FILENODE in filediff.patch['stats']['ops']:
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 final_path = filediff.target_file_path
229 final_path = filediff.target_file_path
230 %>
230 %>
231 %elif COPIED_FILENODE in filediff.patch['stats']['ops']:
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 final_path = filediff.target_file_path
234 final_path = filediff.target_file_path
235 %>
235 %>
236 %endif
236 %endif
237 %else:
237 %else:
238 ## file was modified
238 ## file was modified
239 <%
239 <%
240 final_file_name = filediff.source_file_path
240 final_file_name = filediff.source_file_path
241 final_path = final_file_name
241 final_path = final_file_name
242 %>
242 %>
243 %endif
243 %endif
244 %else:
244 %else:
245 %if filediff.source_file_path:
245 %if filediff.source_file_path:
246 ## file was deleted
246 ## file was deleted
247 <%
247 <%
248 final_file_name = filediff.source_file_path
248 final_file_name = filediff.source_file_path
249 final_path = final_file_name
249 final_path = final_file_name
250 %>
250 %>
251 %else:
251 %else:
252 ## file was added
252 ## file was added
253 <%
253 <%
254 final_file_name = filediff.target_file_path
254 final_file_name = filediff.target_file_path
255 final_path = final_file_name
255 final_path = final_file_name
256 %>
256 %>
257 %endif
257 %endif
258 %endif
258 %endif
259
259
260 <%
260 <%
261 lines_changed = filediff.patch['stats']['added'] + filediff.patch['stats']['deleted']
261 lines_changed = filediff.patch['stats']['added'] + filediff.patch['stats']['deleted']
262 over_lines_changed_limit = lines_changed > lines_changed_limit
262 over_lines_changed_limit = lines_changed > lines_changed_limit
263 %>
263 %>
264 ## anchor with support of sticky header
264 ## anchor with support of sticky header
265 <div class="anchor" id="a_${h.FID(filediff.raw_id, filediff.patch['filename'])}"></div>
265 <div class="anchor" id="a_${h.FID(filediff.raw_id, filediff.patch['filename'])}"></div>
266
266
267 <input ${(collapse_all and 'checked' or '')} class="filediff-collapse-state collapse-${diffset_container_id}" id="filediff-collapse-${id(filediff)}" type="checkbox" onchange="updateSticky();">
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 <div
268 <div
269 class="filediff"
269 class="filediff"
270 data-f-path="${filediff.patch['filename']}"
270 data-f-path="${filediff.patch['filename']}"
271 data-anchor-id="${h.FID(filediff.raw_id, filediff.patch['filename'])}"
271 data-anchor-id="${h.FID(filediff.raw_id, filediff.patch['filename'])}"
272 >
272 >
273 <label for="filediff-collapse-${id(filediff)}" class="filediff-heading">
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 total_file_comments = [_c for _c in h.itertools.chain.from_iterable(file_comments) if not (_c.outdated or _c.draft)]
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 <div class="filediff-collapse-indicator icon-"></div>
278 <div class="filediff-collapse-indicator icon-"></div>
279
279
280 ## Comments/Options PILL
280 ## Comments/Options PILL
281 <span class="pill-group pull-right">
281 <span class="pill-group pull-right">
282 <span class="pill" op="comments">
282 <span class="pill" op="comments">
283 <i class="icon-comment"></i> ${len(total_file_comments)}
283 <i class="icon-comment"></i> ${len(total_file_comments)}
284 </span>
284 </span>
285
285
286 <details class="details-reset details-inline-block">
286 <details class="details-reset details-inline-block">
287 <summary class="noselect">
287 <summary class="noselect">
288 <i class="pill icon-options cursor-pointer" op="options"></i>
288 <i class="pill icon-options cursor-pointer" op="options"></i>
289 </summary>
289 </summary>
290 <details-menu class="details-dropdown">
290 <details-menu class="details-dropdown">
291
291
292 <div class="dropdown-item">
292 <div class="dropdown-item">
293 <span>${final_path}</span>
293 <span>${final_path}</span>
294 <span class="pull-right icon-clipboard clipboard-action" data-clipboard-text="${final_path}" title="Copy file path"></span>
294 <span class="pull-right icon-clipboard clipboard-action" data-clipboard-text="${final_path}" title="Copy file path"></span>
295 </div>
295 </div>
296
296
297 <div class="dropdown-divider"></div>
297 <div class="dropdown-divider"></div>
298
298
299 <div class="dropdown-item">
299 <div class="dropdown-item">
300 <% permalink = request.current_route_url(_anchor='a_{}'.format(h.FID(filediff.raw_id, filediff.patch['filename']))) %>
300 <% permalink = request.current_route_url(_anchor='a_{}'.format(h.FID(filediff.raw_id, filediff.patch['filename']))) %>
301 <a href="${permalink}">ΒΆ permalink</a>
301 <a href="${permalink}">ΒΆ permalink</a>
302 <span class="pull-right icon-clipboard clipboard-action" data-clipboard-text="${permalink}" title="Copy permalink"></span>
302 <span class="pull-right icon-clipboard clipboard-action" data-clipboard-text="${permalink}" title="Copy permalink"></span>
303 </div>
303 </div>
304
304
305
306 </details-menu>
305 </details-menu>
307 </details>
306 </details>
308
307
309 </span>
308 </span>
310
309
311 ${diff_ops(final_file_name, filediff)}
310 ${diff_ops(final_file_name, filediff)}
312
311
313 </label>
312 </label>
314
313
315 ${diff_menu(filediff, use_comments=use_comments)}
314 ${diff_menu(filediff, use_comments=use_comments)}
316 <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 '')}">
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 ## new/deleted/empty content case
317 ## new/deleted/empty content case
319 % if not filediff.hunks:
318 % if not filediff.hunks:
320 ## Comment container, on "fakes" hunk that contains all data to render comments
319 ## Comment container, on "fakes" hunk that contains all data to render comments
321 ${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)}
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 % endif
321 % endif
323
322
324 %if filediff.limited_diff:
323 %if filediff.limited_diff:
325 <tr class="cb-warning cb-collapser">
324 <tr class="cb-warning cb-collapser">
326 <td class="cb-text" ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=4' or 'colspan=6')}>
325 <td class="cb-text" ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=4' or 'colspan=6')}>
327 ${_('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>
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 </td>
327 </td>
329 </tr>
328 </tr>
330 %else:
329 %else:
331 %if over_lines_changed_limit:
330 %if over_lines_changed_limit:
332 <tr class="cb-warning cb-collapser">
331 <tr class="cb-warning cb-collapser">
333 <td class="cb-text" ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=4' or 'colspan=6')}>
332 <td class="cb-text" ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=4' or 'colspan=6')}>
334 ${_('This diff has been collapsed as it changes many lines, (%i lines changed)' % lines_changed)}
333 ${_('This diff has been collapsed as it changes many lines, (%i lines changed)' % lines_changed)}
335 <a href="#" class="cb-expand"
334 <a href="#" class="cb-expand"
336 onclick="$(this).closest('table').removeClass('cb-collapsed'); updateSticky(); return false;">${_('Show them')}
335 onclick="$(this).closest('table').removeClass('cb-collapsed'); updateSticky(); return false;">${_('Show them')}
337 </a>
336 </a>
338 <a href="#" class="cb-collapse"
337 <a href="#" class="cb-collapse"
339 onclick="$(this).closest('table').addClass('cb-collapsed'); updateSticky(); return false;">${_('Hide them')}
338 onclick="$(this).closest('table').addClass('cb-collapsed'); updateSticky(); return false;">${_('Hide them')}
340 </a>
339 </a>
341 </td>
340 </td>
342 </tr>
341 </tr>
343 %endif
342 %endif
344 %endif
343 %endif
345
344
346 % for hunk in filediff.hunks:
345 % for hunk in filediff.hunks:
347 <tr class="cb-hunk">
346 <tr class="cb-hunk">
348 <td ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=3' or '')}>
347 <td ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=3' or '')}>
349 ## TODO: dan: add ajax loading of more context here
348 ## TODO: dan: add ajax loading of more context here
350 ## <a href="#">
349 ## <a href="#">
351 <i class="icon-more"></i>
350 <i class="icon-more"></i>
352 ## </a>
351 ## </a>
353 </td>
352 </td>
354 <td ${(c.user_session_attrs["diffmode"] == 'sideside' and 'colspan=5' or '')}>
353 <td ${(c.user_session_attrs["diffmode"] == 'sideside' and 'colspan=5' or '')}>
355 @@
354 @@
356 -${hunk.source_start},${hunk.source_length}
355 -${hunk.source_start},${hunk.source_length}
357 +${hunk.target_start},${hunk.target_length}
356 +${hunk.target_start},${hunk.target_length}
358 ${hunk.section_header}
357 ${hunk.section_header}
359 </td>
358 </td>
360 </tr>
359 </tr>
361
360
362 ${render_hunk_lines(filediff, c.user_session_attrs["diffmode"], hunk, use_comments=use_comments, inline_comments=inline_comments, active_pattern_entries=active_pattern_entries)}
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 % endfor
362 % endfor
364
363
365 <% unmatched_comments = (inline_comments or {}).get(filediff.patch['filename'], {}) %>
364 <% unmatched_comments = (inline_comments or {}).get(filediff.patch['filename'], {}) %>
366
365
367 ## outdated comments that do not fit into currently displayed lines
366 ## outdated comments that do not fit into currently displayed lines
368 % for lineno, comments in unmatched_comments.items():
367 % for lineno, comments in unmatched_comments.items():
369
368
370 %if c.user_session_attrs["diffmode"] == 'unified':
369 %if c.user_session_attrs["diffmode"] == 'unified':
371 % if loop.index == 0:
370 % if loop.index == 0:
372 <tr class="cb-hunk">
371 <tr class="cb-hunk">
373 <td colspan="3"></td>
372 <td colspan="3"></td>
374 <td>
373 <td>
375 <div>
374 <div>
376 ${_('Unmatched/outdated inline comments below')}
375 ${_('Unmatched/outdated inline comments below')}
377 </div>
376 </div>
378 </td>
377 </td>
379 </tr>
378 </tr>
380 % endif
379 % endif
381 <tr class="cb-line">
380 <tr class="cb-line">
382 <td class="cb-data cb-context"></td>
381 <td class="cb-data cb-context"></td>
383 <td class="cb-lineno cb-context"></td>
382 <td class="cb-lineno cb-context"></td>
384 <td class="cb-lineno cb-context"></td>
383 <td class="cb-lineno cb-context"></td>
385 <td class="cb-content cb-context">
384 <td class="cb-content cb-context">
386 ${inline_comments_container(comments, active_pattern_entries=active_pattern_entries)}
385 ${inline_comments_container(comments, active_pattern_entries=active_pattern_entries)}
387 </td>
386 </td>
388 </tr>
387 </tr>
389 %elif c.user_session_attrs["diffmode"] == 'sideside':
388 %elif c.user_session_attrs["diffmode"] == 'sideside':
390 % if loop.index == 0:
389 % if loop.index == 0:
391 <tr class="cb-comment-info">
390 <tr class="cb-comment-info">
392 <td colspan="2"></td>
391 <td colspan="2"></td>
393 <td class="cb-line">
392 <td class="cb-line">
394 <div>
393 <div>
395 ${_('Unmatched/outdated inline comments below')}
394 ${_('Unmatched/outdated inline comments below')}
396 </div>
395 </div>
397 </td>
396 </td>
398 <td colspan="2"></td>
397 <td colspan="2"></td>
399 <td class="cb-line">
398 <td class="cb-line">
400 <div>
399 <div>
401 ${_('Unmatched/outdated comments below')}
400 ${_('Unmatched/outdated comments below')}
402 </div>
401 </div>
403 </td>
402 </td>
404 </tr>
403 </tr>
405 % endif
404 % endif
406 <tr class="cb-line">
405 <tr class="cb-line">
407 <td class="cb-data cb-context"></td>
406 <td class="cb-data cb-context"></td>
408 <td class="cb-lineno cb-context"></td>
407 <td class="cb-lineno cb-context"></td>
409 <td class="cb-content cb-context">
408 <td class="cb-content cb-context">
410 % if lineno.startswith('o'):
409 % if lineno.startswith('o'):
411 ${inline_comments_container(comments, active_pattern_entries=active_pattern_entries)}
410 ${inline_comments_container(comments, active_pattern_entries=active_pattern_entries)}
412 % endif
411 % endif
413 </td>
412 </td>
414
413
415 <td class="cb-data cb-context"></td>
414 <td class="cb-data cb-context"></td>
416 <td class="cb-lineno cb-context"></td>
415 <td class="cb-lineno cb-context"></td>
417 <td class="cb-content cb-context">
416 <td class="cb-content cb-context">
418 % if lineno.startswith('n'):
417 % if lineno.startswith('n'):
419 ${inline_comments_container(comments, active_pattern_entries=active_pattern_entries)}
418 ${inline_comments_container(comments, active_pattern_entries=active_pattern_entries)}
420 % endif
419 % endif
421 </td>
420 </td>
422 </tr>
421 </tr>
423 %endif
422 %endif
424
423
425 % endfor
424 % endfor
426
425
427 </table>
426 </table>
428 </div>
427 </div>
429 %endfor
428 %endfor
430
429
431 ## outdated comments that are made for a file that has been deleted
430 ## outdated comments that are made for a file that has been deleted
432 % for filename, comments_dict in (deleted_files_comments or {}).items():
431 % for filename, comments_dict in (deleted_files_comments or {}).items():
433
432
434 <%
433 <%
435 display_state = 'display: none'
434 display_state = 'display: none'
436 open_comments_in_file = [x for x in comments_dict['comments'] if x.outdated is False]
435 open_comments_in_file = [x for x in comments_dict['comments'] if x.outdated is False]
437 if open_comments_in_file:
436 if open_comments_in_file:
438 display_state = ''
437 display_state = ''
439 fid = str(id(filename))
438 fid = str(id(filename))
440 %>
439 %>
441 <div class="filediffs filediff-outdated" style="${display_state}">
440 <div class="filediffs filediff-outdated" style="${display_state}">
442 <input ${(collapse_all and 'checked' or '')} class="filediff-collapse-state collapse-${diffset_container_id}" id="filediff-collapse-${id(filename)}" type="checkbox" onchange="updateSticky();">
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 <div class="filediff" data-f-path="${filename}" id="a_${h.FID(fid, filename)}">
442 <div class="filediff" data-f-path="${filename}" id="a_${h.FID(fid, filename)}">
444 <label for="filediff-collapse-${id(filename)}" class="filediff-heading">
443 <label for="filediff-collapse-${id(filename)}" class="filediff-heading">
445 <div class="filediff-collapse-indicator icon-"></div>
444 <div class="filediff-collapse-indicator icon-"></div>
446
445
447 <span class="pill">
446 <span class="pill">
448 ## file was deleted
447 ## file was deleted
449 ${filename}
448 ${filename}
450 </span>
449 </span>
451 <span class="pill-group pull-left" >
450 <span class="pill-group pull-left" >
452 ## file op, doesn't need translation
451 ## file op, doesn't need translation
453 <span class="pill" op="removed">unresolved comments</span>
452 <span class="pill" op="removed">unresolved comments</span>
454 </span>
453 </span>
455 <a class="pill filediff-anchor" href="#a_${h.FID(fid, filename)}">ΒΆ</a>
454 <a class="pill filediff-anchor" href="#a_${h.FID(fid, filename)}">ΒΆ</a>
456 <span class="pill-group pull-right">
455 <span class="pill-group pull-right">
457 <span class="pill" op="deleted">
456 <span class="pill" op="deleted">
458 % if comments_dict['stats'] >0:
457 % if comments_dict['stats'] >0:
459 -${comments_dict['stats']}
458 -${comments_dict['stats']}
460 % else:
459 % else:
461 ${comments_dict['stats']}
460 ${comments_dict['stats']}
462 % endif
461 % endif
463 </span>
462 </span>
464 </span>
463 </span>
465 </label>
464 </label>
466
465
467 <table class="cb cb-diff-${c.user_session_attrs["diffmode"]} code-highlight ${(over_lines_changed_limit and 'cb-collapsed' or '')}">
466 <table class="cb cb-diff-${c.user_session_attrs["diffmode"]} code-highlight ${(over_lines_changed_limit and 'cb-collapsed' or '')}">
468 <tr>
467 <tr>
469 % if c.user_session_attrs["diffmode"] == 'unified':
468 % if c.user_session_attrs["diffmode"] == 'unified':
470 <td></td>
469 <td></td>
471 %endif
470 %endif
472
471
473 <td></td>
472 <td></td>
474 <td class="cb-text cb-${op_class(BIN_FILENODE)}" ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=4' or 'colspan=5')}>
473 <td class="cb-text cb-${op_class(BIN_FILENODE)}" ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=4' or 'colspan=5')}>
475 <strong>${_('This file was removed from diff during updates to this pull-request.')}</strong><br/>
474 <strong>${_('This file was removed from diff during updates to this pull-request.')}</strong><br/>
476 ${_('There are still outdated/unresolved comments attached to it.')}
475 ${_('There are still outdated/unresolved comments attached to it.')}
477 </td>
476 </td>
478 </tr>
477 </tr>
479 %if c.user_session_attrs["diffmode"] == 'unified':
478 %if c.user_session_attrs["diffmode"] == 'unified':
480 <tr class="cb-line">
479 <tr class="cb-line">
481 <td class="cb-data cb-context"></td>
480 <td class="cb-data cb-context"></td>
482 <td class="cb-lineno cb-context"></td>
481 <td class="cb-lineno cb-context"></td>
483 <td class="cb-lineno cb-context"></td>
482 <td class="cb-lineno cb-context"></td>
484 <td class="cb-content cb-context">
483 <td class="cb-content cb-context">
485 ${inline_comments_container(comments_dict['comments'], active_pattern_entries=active_pattern_entries)}
484 ${inline_comments_container(comments_dict['comments'], active_pattern_entries=active_pattern_entries)}
486 </td>
485 </td>
487 </tr>
486 </tr>
488 %elif c.user_session_attrs["diffmode"] == 'sideside':
487 %elif c.user_session_attrs["diffmode"] == 'sideside':
489 <tr class="cb-line">
488 <tr class="cb-line">
490 <td class="cb-data cb-context"></td>
489 <td class="cb-data cb-context"></td>
491 <td class="cb-lineno cb-context"></td>
490 <td class="cb-lineno cb-context"></td>
492 <td class="cb-content cb-context"></td>
491 <td class="cb-content cb-context"></td>
493
492
494 <td class="cb-data cb-context"></td>
493 <td class="cb-data cb-context"></td>
495 <td class="cb-lineno cb-context"></td>
494 <td class="cb-lineno cb-context"></td>
496 <td class="cb-content cb-context">
495 <td class="cb-content cb-context">
497 ${inline_comments_container(comments_dict['comments'], active_pattern_entries=active_pattern_entries)}
496 ${inline_comments_container(comments_dict['comments'], active_pattern_entries=active_pattern_entries)}
498 </td>
497 </td>
499 </tr>
498 </tr>
500 %endif
499 %endif
501 </table>
500 </table>
502 </div>
501 </div>
503 </div>
502 </div>
504 % endfor
503 % endfor
505
504
506 </div>
505 </div>
507 </div>
506 </div>
508 </%def>
507 </%def>
509
508
510 <%def name="diff_ops(file_name, filediff)">
509 <%def name="diff_ops(file_name, filediff)">
511 <%
510 <%
512 from rhodecode.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
511 from rhodecode.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
513 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE, COPIED_FILENODE
512 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE, COPIED_FILENODE
514 %>
513 %>
515 <span class="pill">
514 <span class="pill">
516 <i class="icon-file-text"></i>
515 <i class="icon-file-text"></i>
517 ${file_name}
516 ${file_name}
518 </span>
517 </span>
519
518
520 <span class="pill-group pull-right">
519 <span class="pill-group pull-right">
521
520
522 ## ops pills
521 ## ops pills
523 %if filediff.limited_diff:
522 %if filediff.limited_diff:
524 <span class="pill tooltip" op="limited" title="The stats for this diff are not complete">limited diff</span>
523 <span class="pill tooltip" op="limited" title="The stats for this diff are not complete">limited diff</span>
525 %endif
524 %endif
526
525
527 %if NEW_FILENODE in filediff.patch['stats']['ops']:
526 %if NEW_FILENODE in filediff.patch['stats']['ops']:
528 <span class="pill" op="created">created</span>
527 <span class="pill" op="created">created</span>
529 %if filediff['target_mode'].startswith('120'):
528 %if filediff['target_mode'].startswith('120'):
530 <span class="pill" op="symlink">symlink</span>
529 <span class="pill" op="symlink">symlink</span>
531 %else:
530 %else:
532 <span class="pill" op="mode">${nice_mode(filediff['target_mode'])}</span>
531 <span class="pill" op="mode">${nice_mode(filediff['target_mode'])}</span>
533 %endif
532 %endif
534 %endif
533 %endif
535
534
536 %if RENAMED_FILENODE in filediff.patch['stats']['ops']:
535 %if RENAMED_FILENODE in filediff.patch['stats']['ops']:
537 <span class="pill" op="renamed">renamed</span>
536 <span class="pill" op="renamed">renamed</span>
538 %endif
537 %endif
539
538
540 %if COPIED_FILENODE in filediff.patch['stats']['ops']:
539 %if COPIED_FILENODE in filediff.patch['stats']['ops']:
541 <span class="pill" op="copied">copied</span>
540 <span class="pill" op="copied">copied</span>
542 %endif
541 %endif
543
542
544 %if DEL_FILENODE in filediff.patch['stats']['ops']:
543 %if DEL_FILENODE in filediff.patch['stats']['ops']:
545 <span class="pill" op="removed">removed</span>
544 <span class="pill" op="removed">removed</span>
546 %endif
545 %endif
547
546
548 %if CHMOD_FILENODE in filediff.patch['stats']['ops']:
547 %if CHMOD_FILENODE in filediff.patch['stats']['ops']:
549 <span class="pill" op="mode">
548 <span class="pill" op="mode">
550 ${nice_mode(filediff['source_mode'])} ➑ ${nice_mode(filediff['target_mode'])}
549 ${nice_mode(filediff['source_mode'])} ➑ ${nice_mode(filediff['target_mode'])}
551 </span>
550 </span>
552 %endif
551 %endif
553
552
554 %if BIN_FILENODE in filediff.patch['stats']['ops']:
553 %if BIN_FILENODE in filediff.patch['stats']['ops']:
555 <span class="pill" op="binary">binary</span>
554 <span class="pill" op="binary">binary</span>
556 %if MOD_FILENODE in filediff.patch['stats']['ops']:
555 %if MOD_FILENODE in filediff.patch['stats']['ops']:
557 <span class="pill" op="modified">modified</span>
556 <span class="pill" op="modified">modified</span>
558 %endif
557 %endif
559 %endif
558 %endif
560
559
561 <span class="pill" op="added">${('+' if filediff.patch['stats']['added'] else '')}${filediff.patch['stats']['added']}</span>
560 <span class="pill" op="added">${('+' if filediff.patch['stats']['added'] else '')}${filediff.patch['stats']['added']}</span>
562 <span class="pill" op="deleted">${((h.safe_int(filediff.patch['stats']['deleted']) or 0) * -1)}</span>
561 <span class="pill" op="deleted">${((h.safe_int(filediff.patch['stats']['deleted']) or 0) * -1)}</span>
563
562
564 </span>
563 </span>
565
564
566 </%def>
565 </%def>
567
566
568 <%def name="nice_mode(filemode)">
567 <%def name="nice_mode(filemode)">
569 ${(filemode.startswith('100') and filemode[3:] or filemode)}
568 ${(filemode.startswith('100') and filemode[3:] or filemode)}
570 </%def>
569 </%def>
571
570
572 <%def name="diff_menu(filediff, use_comments=False)">
571 <%def name="diff_menu(filediff, use_comments=False)">
573 <div class="filediff-menu">
572 <div class="filediff-menu">
574
573
575 %if filediff.diffset.source_ref:
574 %if filediff.diffset.source_ref:
576
575
577 ## FILE BEFORE CHANGES
576 ## FILE BEFORE CHANGES
578 %if filediff.operation in ['D', 'M']:
577 %if filediff.operation in ['D', 'M']:
579 <a
578 <a
580 class="tooltip"
579 class="tooltip"
581 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)}"
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 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
581 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
583 >
582 >
584 ${_('Show file before')}
583 ${_('Show file before')}
585 </a> |
584 </a> |
586 %else:
585 %else:
587 <span
586 <span
588 class="tooltip"
587 class="tooltip"
589 title="${h.tooltip(_('File not present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
588 title="${h.tooltip(_('File not present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
590 >
589 >
591 ${_('Show file before')}
590 ${_('Show file before')}
592 </span> |
591 </span> |
593 %endif
592 %endif
594
593
595 ## FILE AFTER CHANGES
594 ## FILE AFTER CHANGES
596 %if filediff.operation in ['A', 'M']:
595 %if filediff.operation in ['A', 'M']:
597 <a
596 <a
598 class="tooltip"
597 class="tooltip"
599 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)}"
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 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
599 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
601 >
600 >
602 ${_('Show file after')}
601 ${_('Show file after')}
603 </a>
602 </a>
604 %else:
603 %else:
605 <span
604 <span
606 class="tooltip"
605 class="tooltip"
607 title="${h.tooltip(_('File not present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
606 title="${h.tooltip(_('File not present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
608 >
607 >
609 ${_('Show file after')}
608 ${_('Show file after')}
610 </span>
609 </span>
611 %endif
610 %endif
612
611
613 % if use_comments:
612 % if use_comments:
614 |
613 |
615 <a href="#" onclick="Rhodecode.comments.toggleDiffComments(this);return toggleElement(this)"
614 <a href="#" onclick="Rhodecode.comments.toggleDiffComments(this);return toggleElement(this)"
616 data-toggle-on="${_('Hide comments')}"
615 data-toggle-on="${_('Hide comments')}"
617 data-toggle-off="${_('Show comments')}">
616 data-toggle-off="${_('Show comments')}">
618 <span class="hide-comment-button">${_('Hide comments')}</span>
617 <span class="hide-comment-button">${_('Hide comments')}</span>
619 </a>
618 </a>
620 % endif
619 % endif
621
620
622 %endif
621 %endif
623
622
624 </div>
623 </div>
625 </%def>
624 </%def>
626
625
627
626
628 <%def name="inline_comments_container(comments, active_pattern_entries=None, line_no='', f_path='')">
627 <%def name="inline_comments_container(comments, active_pattern_entries=None, line_no='', f_path='')">
629
628
630 <div class="inline-comments">
629 <div class="inline-comments">
631 %for comment in comments:
630 %for comment in comments:
632 ${commentblock.comment_block(comment, inline=True, active_pattern_entries=active_pattern_entries)}
631 ${commentblock.comment_block(comment, inline=True, active_pattern_entries=active_pattern_entries)}
633 %endfor
632 %endfor
634
633
635 <%
634 <%
636 extra_class = ''
635 extra_class = ''
637 extra_style = ''
636 extra_style = ''
638
637
639 if comments and comments[-1].outdated_at_version(c.at_version_num):
638 if comments and comments[-1].outdated_at_version(c.at_version_num):
640 extra_class = ' comment-outdated'
639 extra_class = ' comment-outdated'
641 extra_style = 'display: none;'
640 extra_style = 'display: none;'
642
641
643 %>
642 %>
644
643
645 <div class="reply-thread-container-wrapper${extra_class}" style="${extra_style}">
644 <div class="reply-thread-container-wrapper${extra_class}" style="${extra_style}">
646 <div class="reply-thread-container${extra_class}">
645 <div class="reply-thread-container${extra_class}">
647 <div class="reply-thread-gravatar">
646 <div class="reply-thread-gravatar">
648 % if c.rhodecode_user.username != h.DEFAULT_USER:
647 % if c.rhodecode_user.username != h.DEFAULT_USER:
649 ${base.gravatar(c.rhodecode_user.email, 20, tooltip=True, user=c.rhodecode_user)}
648 ${base.gravatar(c.rhodecode_user.email, 20, tooltip=True, user=c.rhodecode_user)}
650 % endif
649 % endif
651 </div>
650 </div>
652
651
653 <div class="reply-thread-reply-button">
652 <div class="reply-thread-reply-button">
654 % if c.rhodecode_user.username != h.DEFAULT_USER:
653 % if c.rhodecode_user.username != h.DEFAULT_USER:
655 ## initial reply button, some JS logic can append here a FORM to leave a first comment.
654 ## initial reply button, some JS logic can append here a FORM to leave a first comment.
656 <button class="cb-comment-add-button" onclick="return Rhodecode.comments.createComment(this, '${f_path}', '${line_no}', null)">Reply...</button>
655 <button class="cb-comment-add-button" onclick="return Rhodecode.comments.createComment(this, '${f_path}', '${line_no}', null)">Reply...</button>
657 % endif
656 % endif
658 </div>
657 </div>
659 ##% endif
658 ##% endif
660 <div class="reply-thread-last"></div>
659 <div class="reply-thread-last"></div>
661 </div>
660 </div>
662 </div>
661 </div>
663 </div>
662 </div>
664
663
665 </%def>
664 </%def>
666
665
667 <%!
666 <%!
668
667
669 def get_inline_comments(comments, filename):
668 def get_inline_comments(comments, filename):
670 if hasattr(filename, 'unicode_path'):
669 if hasattr(filename, 'str_path'):
671 filename = filename.unicode_path
670 filename = filename.str_path
672
671
673 if not isinstance(filename, str):
672 if not isinstance(filename, str):
674 return None
673 return None
675
674
676 if comments and filename in comments:
675 if comments and filename in comments:
677 return comments[filename]
676 return comments[filename]
678
677
679 return None
678 return None
680
679
681 def get_comments_for(diff_type, comments, filename, line_version, line_number):
680 def get_comments_for(diff_type, comments, filename, line_version, line_number):
682 if hasattr(filename, 'unicode_path'):
681 if hasattr(filename, 'str_path'):
683 filename = filename.unicode_path
682 filename = filename.str_path
684
683
685 if not isinstance(filename, str):
684 if not isinstance(filename, str):
686 return None
685 return None
687
686
688 file_comments = get_inline_comments(comments, filename)
687 file_comments = get_inline_comments(comments, filename)
689 if file_comments is None:
688 if file_comments is None:
690 return None
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 if line_key in file_comments:
692 if line_key in file_comments:
694 data = file_comments.pop(line_key)
693 data = file_comments.pop(line_key)
695 return data
694 return data
696 %>
695 %>
697
696
698 <%def name="render_hunk_lines_sideside(filediff, hunk, use_comments=False, inline_comments=None, active_pattern_entries=None)">
697 <%def name="render_hunk_lines_sideside(filediff, hunk, use_comments=False, inline_comments=None, active_pattern_entries=None)">
699
698
700 <% chunk_count = 1 %>
699 <% chunk_count = 1 %>
701 %for loop_obj, item in h.looper(hunk.sideside):
700 %for loop_obj, item in h.looper(hunk.sideside):
702 <%
701 <%
703 line = item
702 line = item
704 i = loop_obj.index
703 i = loop_obj.index
705 prev_line = loop_obj.previous
704 prev_line = loop_obj.previous
706 old_line_anchor, new_line_anchor = None, None
705 old_line_anchor, new_line_anchor = None, None
707
706
708 if line.original.lineno:
707 if line.original.lineno:
709 old_line_anchor = diff_line_anchor(filediff.raw_id, hunk.source_file_path, line.original.lineno, 'o')
708 old_line_anchor = diff_line_anchor(filediff.raw_id, hunk.source_file_path, line.original.lineno, 'o')
710 if line.modified.lineno:
709 if line.modified.lineno:
711 new_line_anchor = diff_line_anchor(filediff.raw_id, hunk.target_file_path, line.modified.lineno, 'n')
710 new_line_anchor = diff_line_anchor(filediff.raw_id, hunk.target_file_path, line.modified.lineno, 'n')
712
711
713 line_action = line.modified.action or line.original.action
712 line_action = line.modified.action or line.original.action
714 prev_line_action = prev_line and (prev_line.modified.action or prev_line.original.action)
713 prev_line_action = prev_line and (prev_line.modified.action or prev_line.original.action)
715 %>
714 %>
716
715
717 <tr class="cb-line">
716 <tr class="cb-line">
718 <td class="cb-data ${action_class(line.original.action)}"
717 <td class="cb-data ${action_class(line.original.action)}"
719 data-line-no="${line.original.lineno}"
718 data-line-no="${line.original.lineno}"
720 >
719 >
721
720
722 <% line_old_comments, line_old_comments_no_drafts = None, None %>
721 <% line_old_comments, line_old_comments_no_drafts = None, None %>
723 %if line.original.get_comment_args:
722 %if line.original.get_comment_args:
724 <%
723 <%
725 line_old_comments = get_comments_for('side-by-side', inline_comments, *line.original.get_comment_args)
724 line_old_comments = get_comments_for('side-by-side', inline_comments, *line.original.get_comment_args)
726 line_old_comments_no_drafts = [c for c in line_old_comments if not c.draft] if line_old_comments else []
725 line_old_comments_no_drafts = [c for c in line_old_comments if not c.draft] if line_old_comments else []
727 has_outdated = any([x.outdated for x in line_old_comments_no_drafts])
726 has_outdated = any([x.outdated for x in line_old_comments_no_drafts])
728 %>
727 %>
729 %endif
728 %endif
730 %if line_old_comments_no_drafts:
729 %if line_old_comments_no_drafts:
731 % if has_outdated:
730 % if has_outdated:
732 <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>
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 % else:
732 % else:
734 <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>
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 % endif
734 % endif
736 %endif
735 %endif
737 </td>
736 </td>
738 <td class="cb-lineno ${action_class(line.original.action)}"
737 <td class="cb-lineno ${action_class(line.original.action)}"
739 data-line-no="${line.original.lineno}"
738 data-line-no="${line.original.lineno}"
740 %if old_line_anchor:
739 %if old_line_anchor:
741 id="${old_line_anchor}"
740 id="${old_line_anchor}"
742 %endif
741 %endif
743 >
742 >
744 %if line.original.lineno:
743 %if line.original.lineno:
745 <a name="${old_line_anchor}" href="#${old_line_anchor}">${line.original.lineno}</a>
744 <a name="${old_line_anchor}" href="#${old_line_anchor}">${line.original.lineno}</a>
746 %endif
745 %endif
747 </td>
746 </td>
748
747
749 <% line_no = 'o{}'.format(line.original.lineno) %>
748 <% line_no = 'o{}'.format(line.original.lineno) %>
750 <td class="cb-content ${action_class(line.original.action)}"
749 <td class="cb-content ${action_class(line.original.action)}"
751 data-line-no="${line_no}"
750 data-line-no="${line_no}"
752 >
751 >
753 %if use_comments and line.original.lineno:
752 %if use_comments and line.original.lineno:
754 ${render_add_comment_button(line_no=line_no, f_path=filediff.patch['filename'])}
753 ${render_add_comment_button(line_no=line_no, f_path=filediff.patch['filename'])}
755 %endif
754 %endif
756 <span class="cb-code"><span class="cb-action ${action_class(line.original.action)}"></span>${line.original.content or '' | n}</span>
755 <span class="cb-code"><span class="cb-action ${action_class(line.original.action)}"></span>${line.original.content or '' | n}</span>
757
756
758 %if use_comments and line.original.lineno and line_old_comments:
757 %if use_comments and line.original.lineno and line_old_comments:
759 ${inline_comments_container(line_old_comments, active_pattern_entries=active_pattern_entries, line_no=line_no, f_path=filediff.patch['filename'])}
758 ${inline_comments_container(line_old_comments, active_pattern_entries=active_pattern_entries, line_no=line_no, f_path=filediff.patch['filename'])}
760 %endif
759 %endif
761
760
762 </td>
761 </td>
763 <td class="cb-data ${action_class(line.modified.action)}"
762 <td class="cb-data ${action_class(line.modified.action)}"
764 data-line-no="${line.modified.lineno}"
763 data-line-no="${line.modified.lineno}"
765 >
764 >
766 <div>
765 <div>
767
766
768 <% line_new_comments, line_new_comments_no_drafts = None, None %>
767 <% line_new_comments, line_new_comments_no_drafts = None, None %>
769 %if line.modified.get_comment_args:
768 %if line.modified.get_comment_args:
770 <%
769 <%
771 line_new_comments = get_comments_for('side-by-side', inline_comments, *line.modified.get_comment_args)
770 line_new_comments = get_comments_for('side-by-side', inline_comments, *line.modified.get_comment_args)
772 line_new_comments_no_drafts = [c for c in line_new_comments if not c.draft] if line_new_comments else []
771 line_new_comments_no_drafts = [c for c in line_new_comments if not c.draft] if line_new_comments else []
773 has_outdated = any([x.outdated for x in line_new_comments_no_drafts])
772 has_outdated = any([x.outdated for x in line_new_comments_no_drafts])
774 %>
773 %>
775 %endif
774 %endif
776
775
777 %if line_new_comments_no_drafts:
776 %if line_new_comments_no_drafts:
778 % if has_outdated:
777 % if has_outdated:
779 <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>
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 % else:
779 % else:
781 <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>
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 % endif
781 % endif
783 %endif
782 %endif
784 </div>
783 </div>
785 </td>
784 </td>
786 <td class="cb-lineno ${action_class(line.modified.action)}"
785 <td class="cb-lineno ${action_class(line.modified.action)}"
787 data-line-no="${line.modified.lineno}"
786 data-line-no="${line.modified.lineno}"
788 %if new_line_anchor:
787 %if new_line_anchor:
789 id="${new_line_anchor}"
788 id="${new_line_anchor}"
790 %endif
789 %endif
791 >
790 >
792 %if line.modified.lineno:
791 %if line.modified.lineno:
793 <a name="${new_line_anchor}" href="#${new_line_anchor}">${line.modified.lineno}</a>
792 <a name="${new_line_anchor}" href="#${new_line_anchor}">${line.modified.lineno}</a>
794 %endif
793 %endif
795 </td>
794 </td>
796
795
797 <% line_no = 'n{}'.format(line.modified.lineno) %>
796 <% line_no = 'n{}'.format(line.modified.lineno) %>
798 <td class="cb-content ${action_class(line.modified.action)}"
797 <td class="cb-content ${action_class(line.modified.action)}"
799 data-line-no="${line_no}"
798 data-line-no="${line_no}"
800 >
799 >
801 %if use_comments and line.modified.lineno:
800 %if use_comments and line.modified.lineno:
802 ${render_add_comment_button(line_no=line_no, f_path=filediff.patch['filename'])}
801 ${render_add_comment_button(line_no=line_no, f_path=filediff.patch['filename'])}
803 %endif
802 %endif
804 <span class="cb-code"><span class="cb-action ${action_class(line.modified.action)}"></span>${line.modified.content or '' | n}</span>
803 <span class="cb-code"><span class="cb-action ${action_class(line.modified.action)}"></span>${line.modified.content or '' | n}</span>
805 % if line_action in ['+', '-'] and prev_line_action not in ['+', '-']:
804 % if line_action in ['+', '-'] and prev_line_action not in ['+', '-']:
806 <div class="nav-chunk" style="visibility: hidden">
805 <div class="nav-chunk" style="visibility: hidden">
807 <i class="icon-eye" title="viewing diff hunk-${hunk.index}-${chunk_count}"></i>
806 <i class="icon-eye" title="viewing diff hunk-${hunk.index}-${chunk_count}"></i>
808 </div>
807 </div>
809 <% chunk_count +=1 %>
808 <% chunk_count +=1 %>
810 % endif
809 % endif
811 %if use_comments and line.modified.lineno and line_new_comments:
810 %if use_comments and line.modified.lineno and line_new_comments:
812 ${inline_comments_container(line_new_comments, active_pattern_entries=active_pattern_entries, line_no=line_no, f_path=filediff.patch['filename'])}
811 ${inline_comments_container(line_new_comments, active_pattern_entries=active_pattern_entries, line_no=line_no, f_path=filediff.patch['filename'])}
813 %endif
812 %endif
814
813
815 </td>
814 </td>
816 </tr>
815 </tr>
817 %endfor
816 %endfor
818 </%def>
817 </%def>
819
818
820
819
821 <%def name="render_hunk_lines_unified(filediff, hunk, use_comments=False, inline_comments=None, active_pattern_entries=None)">
820 <%def name="render_hunk_lines_unified(filediff, hunk, use_comments=False, inline_comments=None, active_pattern_entries=None)">
822 %for old_line_no, new_line_no, action, content, comments_args in hunk.unified:
821 %for old_line_no, new_line_no, action, content, comments_args in hunk.unified:
823
822
824 <%
823 <%
825 old_line_anchor, new_line_anchor = None, None
824 old_line_anchor, new_line_anchor = None, None
826 if old_line_no:
825 if old_line_no:
827 old_line_anchor = diff_line_anchor(filediff.raw_id, hunk.source_file_path, old_line_no, 'o')
826 old_line_anchor = diff_line_anchor(filediff.raw_id, hunk.source_file_path, old_line_no, 'o')
828 if new_line_no:
827 if new_line_no:
829 new_line_anchor = diff_line_anchor(filediff.raw_id, hunk.target_file_path, new_line_no, 'n')
828 new_line_anchor = diff_line_anchor(filediff.raw_id, hunk.target_file_path, new_line_no, 'n')
830 %>
829 %>
831 <tr class="cb-line">
830 <tr class="cb-line">
832 <td class="cb-data ${action_class(action)}">
831 <td class="cb-data ${action_class(action)}">
833 <div>
832 <div>
834
833
835 <% comments, comments_no_drafts = None, None %>
834 <% comments, comments_no_drafts = None, None %>
836 %if comments_args:
835 %if comments_args:
837 <%
836 <%
838 comments = get_comments_for('unified', inline_comments, *comments_args)
837 comments = get_comments_for('unified', inline_comments, *comments_args)
839 comments_no_drafts = [c for c in line_new_comments if not c.draft] if line_new_comments else []
838 comments_no_drafts = [c for c in line_new_comments if not c.draft] if line_new_comments else []
840 has_outdated = any([x.outdated for x in comments_no_drafts])
839 has_outdated = any([x.outdated for x in comments_no_drafts])
841 %>
840 %>
842 %endif
841 %endif
843
842
844 % if comments_no_drafts:
843 % if comments_no_drafts:
845 % if has_outdated:
844 % if has_outdated:
846 <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>
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 % else:
846 % else:
848 <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>
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 % endif
848 % endif
850 % endif
849 % endif
851 </div>
850 </div>
852 </td>
851 </td>
853 <td class="cb-lineno ${action_class(action)}"
852 <td class="cb-lineno ${action_class(action)}"
854 data-line-no="${old_line_no}"
853 data-line-no="${old_line_no}"
855 %if old_line_anchor:
854 %if old_line_anchor:
856 id="${old_line_anchor}"
855 id="${old_line_anchor}"
857 %endif
856 %endif
858 >
857 >
859 %if old_line_anchor:
858 %if old_line_anchor:
860 <a name="${old_line_anchor}" href="#${old_line_anchor}">${old_line_no}</a>
859 <a name="${old_line_anchor}" href="#${old_line_anchor}">${old_line_no}</a>
861 %endif
860 %endif
862 </td>
861 </td>
863 <td class="cb-lineno ${action_class(action)}"
862 <td class="cb-lineno ${action_class(action)}"
864 data-line-no="${new_line_no}"
863 data-line-no="${new_line_no}"
865 %if new_line_anchor:
864 %if new_line_anchor:
866 id="${new_line_anchor}"
865 id="${new_line_anchor}"
867 %endif
866 %endif
868 >
867 >
869 %if new_line_anchor:
868 %if new_line_anchor:
870 <a name="${new_line_anchor}" href="#${new_line_anchor}">${new_line_no}</a>
869 <a name="${new_line_anchor}" href="#${new_line_anchor}">${new_line_no}</a>
871 %endif
870 %endif
872 </td>
871 </td>
873 <% line_no = '{}{}'.format(new_line_no and 'n' or 'o', new_line_no or old_line_no) %>
872 <% line_no = '{}{}'.format(new_line_no and 'n' or 'o', new_line_no or old_line_no) %>
874 <td class="cb-content ${action_class(action)}"
873 <td class="cb-content ${action_class(action)}"
875 data-line-no="${line_no}"
874 data-line-no="${line_no}"
876 >
875 >
877 %if use_comments:
876 %if use_comments:
878 ${render_add_comment_button(line_no=line_no, f_path=filediff.patch['filename'])}
877 ${render_add_comment_button(line_no=line_no, f_path=filediff.patch['filename'])}
879 %endif
878 %endif
880 <span class="cb-code"><span class="cb-action ${action_class(action)}"></span> ${content or '' | n}</span>
879 <span class="cb-code"><span class="cb-action ${action_class(action)}"></span> ${content or '' | n}</span>
881 %if use_comments and comments:
880 %if use_comments and comments:
882 ${inline_comments_container(comments, active_pattern_entries=active_pattern_entries, line_no=line_no, f_path=filediff.patch['filename'])}
881 ${inline_comments_container(comments, active_pattern_entries=active_pattern_entries, line_no=line_no, f_path=filediff.patch['filename'])}
883 %endif
882 %endif
884 </td>
883 </td>
885 </tr>
884 </tr>
886 %endfor
885 %endfor
887 </%def>
886 </%def>
888
887
889
888
890 <%def name="render_hunk_lines(filediff, diff_mode, hunk, use_comments, inline_comments, active_pattern_entries)">
889 <%def name="render_hunk_lines(filediff, diff_mode, hunk, use_comments, inline_comments, active_pattern_entries)">
891 % if diff_mode == 'unified':
890 % if diff_mode == 'unified':
892 ${render_hunk_lines_unified(filediff, hunk, use_comments=use_comments, inline_comments=inline_comments, active_pattern_entries=active_pattern_entries)}
891 ${render_hunk_lines_unified(filediff, hunk, use_comments=use_comments, inline_comments=inline_comments, active_pattern_entries=active_pattern_entries)}
893 % elif diff_mode == 'sideside':
892 % elif diff_mode == 'sideside':
894 ${render_hunk_lines_sideside(filediff, hunk, use_comments=use_comments, inline_comments=inline_comments, active_pattern_entries=active_pattern_entries)}
893 ${render_hunk_lines_sideside(filediff, hunk, use_comments=use_comments, inline_comments=inline_comments, active_pattern_entries=active_pattern_entries)}
895 % else:
894 % else:
896 <tr class="cb-line">
895 <tr class="cb-line">
897 <td>unknown diff mode</td>
896 <td>unknown diff mode</td>
898 </tr>
897 </tr>
899 % endif
898 % endif
900 </%def>file changes
899 </%def>file changes
901
900
902
901
903 <%def name="render_add_comment_button(line_no='', f_path='')">
902 <%def name="render_add_comment_button(line_no='', f_path='')">
904 % if not c.rhodecode_user.is_default:
903 % if not c.rhodecode_user.is_default:
905 <button class="btn btn-small btn-primary cb-comment-box-opener" onclick="return Rhodecode.comments.createComment(this, '${f_path}', '${line_no}', null)">
904 <button class="btn btn-small btn-primary cb-comment-box-opener" onclick="return Rhodecode.comments.createComment(this, '${f_path}', '${line_no}', null)">
906 <span><i class="icon-comment"></i></span>
905 <span><i class="icon-comment"></i></span>
907 </button>
906 </button>
908 % endif
907 % endif
909 </%def>
908 </%def>
910
909
911 <%def name="render_diffset_menu(diffset, range_diff_on=None, commit=None, pull_request_menu=None)">
910 <%def name="render_diffset_menu(diffset, range_diff_on=None, commit=None, pull_request_menu=None)">
912 <% diffset_container_id = h.md5_safe(diffset.target_ref) %>
911 <% diffset_container_id = h.md5_safe(diffset.target_ref) %>
913
912
914 <div id="diff-file-sticky" class="diffset-menu clearinner">
913 <div id="diff-file-sticky" class="diffset-menu clearinner">
915 ## auto adjustable
914 ## auto adjustable
916 <div class="sidebar__inner">
915 <div class="sidebar__inner">
917 <div class="sidebar__bar">
916 <div class="sidebar__bar">
918 <div class="pull-right">
917 <div class="pull-right">
919
918
920 <div class="btn-group" style="margin-right: 5px;">
919 <div class="btn-group" style="margin-right: 5px;">
921 <a class="tooltip btn" onclick="scrollDown();return false" title="${_('Scroll to page bottom')}">
920 <a class="tooltip btn" onclick="scrollDown();return false" title="${_('Scroll to page bottom')}">
922 <i class="icon-arrow_down"></i>
921 <i class="icon-arrow_down"></i>
923 </a>
922 </a>
924 <a class="tooltip btn" onclick="scrollUp();return false" title="${_('Scroll to page top')}">
923 <a class="tooltip btn" onclick="scrollUp();return false" title="${_('Scroll to page top')}">
925 <i class="icon-arrow_up"></i>
924 <i class="icon-arrow_up"></i>
926 </a>
925 </a>
927 </div>
926 </div>
928
927
929 <div class="btn-group">
928 <div class="btn-group">
930 <a class="btn tooltip toggle-wide-diff" href="#toggle-wide-diff" onclick="toggleWideDiff(this); return false" title="${h.tooltip(_('Toggle wide diff'))}">
929 <a class="btn tooltip toggle-wide-diff" href="#toggle-wide-diff" onclick="toggleWideDiff(this); return false" title="${h.tooltip(_('Toggle wide diff'))}">
931 <i class="icon-wide-mode"></i>
930 <i class="icon-wide-mode"></i>
932 </a>
931 </a>
933 </div>
932 </div>
934 <div class="btn-group">
933 <div class="btn-group">
935
934
936 <a
935 <a
937 class="btn ${(c.user_session_attrs["diffmode"] == 'sideside' and 'btn-active')} tooltip"
936 class="btn ${(c.user_session_attrs["diffmode"] == 'sideside' and 'btn-active')} tooltip"
938 title="${h.tooltip(_('View diff as side by side'))}"
937 title="${h.tooltip(_('View diff as side by side'))}"
939 href="${h.current_route_path(request, diffmode='sideside')}">
938 href="${h.current_route_path(request, diffmode='sideside')}">
940 <span>${_('Side by Side')}</span>
939 <span>${_('Side by Side')}</span>
941 </a>
940 </a>
942
941
943 <a
942 <a
944 class="btn ${(c.user_session_attrs["diffmode"] == 'unified' and 'btn-active')} tooltip"
943 class="btn ${(c.user_session_attrs["diffmode"] == 'unified' and 'btn-active')} tooltip"
945 title="${h.tooltip(_('View diff as unified'))}" href="${h.current_route_path(request, diffmode='unified')}">
944 title="${h.tooltip(_('View diff as unified'))}" href="${h.current_route_path(request, diffmode='unified')}">
946 <span>${_('Unified')}</span>
945 <span>${_('Unified')}</span>
947 </a>
946 </a>
948
947
949 % if range_diff_on is True:
948 % if range_diff_on is True:
950 <a
949 <a
951 title="${_('Turn off: Show the diff as commit range')}"
950 title="${_('Turn off: Show the diff as commit range')}"
952 class="btn btn-primary"
951 class="btn btn-primary"
953 href="${h.current_route_path(request, **{"range-diff":"0"})}">
952 href="${h.current_route_path(request, **{"range-diff":"0"})}">
954 <span>${_('Range Diff')}</span>
953 <span>${_('Range Diff')}</span>
955 </a>
954 </a>
956 % elif range_diff_on is False:
955 % elif range_diff_on is False:
957 <a
956 <a
958 title="${_('Show the diff as commit range')}"
957 title="${_('Show the diff as commit range')}"
959 class="btn"
958 class="btn"
960 href="${h.current_route_path(request, **{"range-diff":"1"})}">
959 href="${h.current_route_path(request, **{"range-diff":"1"})}">
961 <span>${_('Range Diff')}</span>
960 <span>${_('Range Diff')}</span>
962 </a>
961 </a>
963 % endif
962 % endif
964 </div>
963 </div>
965 <div class="btn-group">
964 <div class="btn-group">
966
965
967 <details class="details-reset details-inline-block">
966 <details class="details-reset details-inline-block">
968 <summary class="noselect btn">
967 <summary class="noselect btn">
969 <i class="icon-options cursor-pointer" op="options"></i>
968 <i class="icon-options cursor-pointer" op="options"></i>
970 </summary>
969 </summary>
971
970
972 <div>
971 <div>
973 <details-menu class="details-dropdown" style="top: 35px;">
972 <details-menu class="details-dropdown" style="top: 35px;">
974
973
975 <div class="dropdown-item">
974 <div class="dropdown-item">
976 <div style="padding: 2px 0px">
975 <div style="padding: 2px 0px">
977 % if request.GET.get('ignorews', '') == '1':
976 % if request.GET.get('ignorews', '') == '1':
978 <a href="${h.current_route_path(request, ignorews=0)}">${_('Show whitespace changes')}</a>
977 <a href="${h.current_route_path(request, ignorews=0)}">${_('Show whitespace changes')}</a>
979 % else:
978 % else:
980 <a href="${h.current_route_path(request, ignorews=1)}">${_('Hide whitespace changes')}</a>
979 <a href="${h.current_route_path(request, ignorews=1)}">${_('Hide whitespace changes')}</a>
981 % endif
980 % endif
982 </div>
981 </div>
983 </div>
982 </div>
984
983
985 <div class="dropdown-item">
984 <div class="dropdown-item">
986 <div style="padding: 2px 0px">
985 <div style="padding: 2px 0px">
987 % if request.GET.get('fullcontext', '') == '1':
986 % if request.GET.get('fullcontext', '') == '1':
988 <a href="${h.current_route_path(request, fullcontext=0)}">${_('Hide full context diff')}</a>
987 <a href="${h.current_route_path(request, fullcontext=0)}">${_('Hide full context diff')}</a>
989 % else:
988 % else:
990 <a href="${h.current_route_path(request, fullcontext=1)}">${_('Show full context diff')}</a>
989 <a href="${h.current_route_path(request, fullcontext=1)}">${_('Show full context diff')}</a>
991 % endif
990 % endif
992 </div>
991 </div>
993 </div>
992 </div>
994
993
995 </details-menu>
994 </details-menu>
996 </div>
995 </div>
997 </details>
996 </details>
998
997
999 </div>
998 </div>
1000 </div>
999 </div>
1001 <div class="pull-left">
1000 <div class="pull-left">
1002 <div class="btn-group">
1001 <div class="btn-group">
1003 <div class="pull-left">
1002 <div class="pull-left">
1004 ${h.hidden('file_filter_{}'.format(diffset_container_id))}
1003 ${h.hidden('file_filter_{}'.format(diffset_container_id))}
1005 </div>
1004 </div>
1006
1005
1007 </div>
1006 </div>
1008 </div>
1007 </div>
1009 </div>
1008 </div>
1010 <div class="fpath-placeholder pull-left">
1009 <div class="fpath-placeholder pull-left">
1011 <i class="icon-file-text"></i>
1010 <i class="icon-file-text"></i>
1012 <strong class="fpath-placeholder-text">
1011 <strong class="fpath-placeholder-text">
1013 Context file:
1012 Context file:
1014 </strong>
1013 </strong>
1015 </div>
1014 </div>
1016 <div class="pull-right noselect">
1015 <div class="pull-right noselect">
1017 %if commit:
1016 %if commit:
1018 <span>
1017 <span>
1019 <code>${h.show_id(commit)}</code>
1018 <code>${h.show_id(commit)}</code>
1020 </span>
1019 </span>
1021 %elif pull_request_menu and pull_request_menu.get('pull_request'):
1020 %elif pull_request_menu and pull_request_menu.get('pull_request'):
1022 <span>
1021 <span>
1023 <code>!${pull_request_menu['pull_request'].pull_request_id}</code>
1022 <code>!${pull_request_menu['pull_request'].pull_request_id}</code>
1024 </span>
1023 </span>
1025 %endif
1024 %endif
1026 % if commit or pull_request_menu:
1025 % if commit or pull_request_menu:
1027 <span class="tooltip" title="Navigate to previous or next change inside files." id="diff_nav">Loading diff...:</span>
1026 <span class="tooltip" title="Navigate to previous or next change inside files." id="diff_nav">Loading diff...:</span>
1028 <span class="cursor-pointer" onclick="scrollToPrevChunk(); return false">
1027 <span class="cursor-pointer" onclick="scrollToPrevChunk(); return false">
1029 <i class="icon-angle-up"></i>
1028 <i class="icon-angle-up"></i>
1030 </span>
1029 </span>
1031 <span class="cursor-pointer" onclick="scrollToNextChunk(); return false">
1030 <span class="cursor-pointer" onclick="scrollToNextChunk(); return false">
1032 <i class="icon-angle-down"></i>
1031 <i class="icon-angle-down"></i>
1033 </span>
1032 </span>
1034 % endif
1033 % endif
1035 </div>
1034 </div>
1036 <div class="sidebar_inner_shadow"></div>
1035 <div class="sidebar_inner_shadow"></div>
1037 </div>
1036 </div>
1038 </div>
1037 </div>
1039
1038
1040 % if diffset:
1039 % if diffset:
1041 %if diffset.limited_diff:
1040 %if diffset.limited_diff:
1042 <% file_placeholder = _ungettext('%(num)s file changed', '%(num)s files changed', diffset.changed_files) % {'num': diffset.changed_files} %>
1041 <% file_placeholder = _ungettext('%(num)s file changed', '%(num)s files changed', diffset.changed_files) % {'num': diffset.changed_files} %>
1043 %else:
1042 %else:
1044 <% 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>',
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 diffset.changed_files) % {'num': diffset.changed_files, 'linesadd': diffset.lines_added, 'linesdel': diffset.lines_deleted}) %>
1044 diffset.changed_files) % {'num': diffset.changed_files, 'linesadd': diffset.lines_added, 'linesdel': diffset.lines_deleted}) %>
1046
1045
1047 %endif
1046 %endif
1048 ## case on range-diff placeholder needs to be updated
1047 ## case on range-diff placeholder needs to be updated
1049 % if range_diff_on is True:
1048 % if range_diff_on is True:
1050 <% file_placeholder = _('Disabled on range diff') %>
1049 <% file_placeholder = _('Disabled on range diff') %>
1051 % endif
1050 % endif
1052
1051
1053 <script type="text/javascript">
1052 <script type="text/javascript">
1054 var feedFilesOptions = function (query, initialData) {
1053 var feedFilesOptions = function (query, initialData) {
1055 var data = {results: []};
1054 var data = {results: []};
1056 var isQuery = typeof query.term !== 'undefined';
1055 var isQuery = typeof query.term !== 'undefined';
1057
1056
1058 var section = _gettext('Changed files');
1057 var section = _gettext('Changed files');
1059 var filteredData = [];
1058 var filteredData = [];
1060
1059
1061 //filter results
1060 //filter results
1062 $.each(initialData.results, function (idx, value) {
1061 $.each(initialData.results, function (idx, value) {
1063
1062
1064 if (!isQuery || query.term.length === 0 || value.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0) {
1063 if (!isQuery || query.term.length === 0 || value.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0) {
1065 filteredData.push({
1064 filteredData.push({
1066 'id': this.id,
1065 'id': this.id,
1067 'text': this.text,
1066 'text': this.text,
1068 "ops": this.ops,
1067 "ops": this.ops,
1069 })
1068 })
1070 }
1069 }
1071
1070
1072 });
1071 });
1073
1072
1074 data.results = filteredData;
1073 data.results = filteredData;
1075
1074
1076 query.callback(data);
1075 query.callback(data);
1077 };
1076 };
1078
1077
1079 var selectionFormatter = function(data, escapeMarkup) {
1078 var selectionFormatter = function(data, escapeMarkup) {
1080 var container = '<div class="filelist" style="padding-right:100px">{0}</div>';
1079 var container = '<div class="filelist" style="padding-right:100px">{0}</div>';
1081 var tmpl = '<div><strong>{0}</strong></div>'.format(escapeMarkup(data['text']));
1080 var tmpl = '<div><strong>{0}</strong></div>'.format(escapeMarkup(data['text']));
1082 var pill = '<div class="pill-group" style="position: absolute; top:7px; right: 0">' +
1081 var pill = '<div class="pill-group" style="position: absolute; top:7px; right: 0">' +
1083 '<span class="pill" op="added">{0}</span>' +
1082 '<span class="pill" op="added">{0}</span>' +
1084 '<span class="pill" op="deleted">{1}</span>' +
1083 '<span class="pill" op="deleted">{1}</span>' +
1085 '</div>'
1084 '</div>'
1086 ;
1085 ;
1087 var added = data['ops']['added'];
1086 var added = data['ops']['added'];
1088 if (added === 0) {
1087 if (added === 0) {
1089 // don't show +0
1088 // don't show +0
1090 added = 0;
1089 added = 0;
1091 } else {
1090 } else {
1092 added = '+' + added;
1091 added = '+' + added;
1093 }
1092 }
1094
1093
1095 var deleted = -1*data['ops']['deleted'];
1094 var deleted = -1*data['ops']['deleted'];
1096
1095
1097 tmpl += pill.format(added, deleted);
1096 tmpl += pill.format(added, deleted);
1098 return container.format(tmpl);
1097 return container.format(tmpl);
1099 };
1098 };
1100 var formatFileResult = function(result, container, query, escapeMarkup) {
1099 var formatFileResult = function(result, container, query, escapeMarkup) {
1101 return selectionFormatter(result, escapeMarkup);
1100 return selectionFormatter(result, escapeMarkup);
1102 };
1101 };
1103
1102
1104 var formatSelection = function (data, container) {
1103 var formatSelection = function (data, container) {
1105 return '${file_placeholder}'
1104 return '${file_placeholder}'
1106 };
1105 };
1107
1106
1108 if (window.preloadFileFilterData === undefined) {
1107 if (window.preloadFileFilterData === undefined) {
1109 window.preloadFileFilterData = {}
1108 window.preloadFileFilterData = {}
1110 }
1109 }
1111
1110
1112 preloadFileFilterData["${diffset_container_id}"] = {
1111 preloadFileFilterData["${diffset_container_id}"] = {
1113 results: [
1112 results: [
1114 % for filediff in diffset.files:
1113 % for filediff in diffset.files:
1115 {id:"a_${h.FID(filediff.raw_id, filediff.patch['filename'])}",
1114 {id:"a_${h.FID(filediff.raw_id, filediff.patch['filename'])}",
1116 text:"${filediff.patch['filename']}",
1115 text:"${filediff.patch['filename']}",
1117 ops:${h.str_json(filediff.patch['stats'])|n}}${('' if loop.last else ',')}
1116 ops:${h.str_json(filediff.patch['stats'])|n}}${('' if loop.last else ',')}
1118 % endfor
1117 % endfor
1119 ]
1118 ]
1120 };
1119 };
1121
1120
1122 var diffFileFilterId = "#file_filter_" + "${diffset_container_id}";
1121 var diffFileFilterId = "#file_filter_" + "${diffset_container_id}";
1123 var diffFileFilter = $(diffFileFilterId).select2({
1122 var diffFileFilter = $(diffFileFilterId).select2({
1124 'dropdownAutoWidth': true,
1123 'dropdownAutoWidth': true,
1125 'width': 'auto',
1124 'width': 'auto',
1126
1125
1127 containerCssClass: "drop-menu",
1126 containerCssClass: "drop-menu",
1128 dropdownCssClass: "drop-menu-dropdown",
1127 dropdownCssClass: "drop-menu-dropdown",
1129 data: preloadFileFilterData["${diffset_container_id}"],
1128 data: preloadFileFilterData["${diffset_container_id}"],
1130 query: function(query) {
1129 query: function(query) {
1131 feedFilesOptions(query, preloadFileFilterData["${diffset_container_id}"]);
1130 feedFilesOptions(query, preloadFileFilterData["${diffset_container_id}"]);
1132 },
1131 },
1133 initSelection: function(element, callback) {
1132 initSelection: function(element, callback) {
1134 callback({'init': true});
1133 callback({'init': true});
1135 },
1134 },
1136 formatResult: formatFileResult,
1135 formatResult: formatFileResult,
1137 formatSelection: formatSelection
1136 formatSelection: formatSelection
1138 });
1137 });
1139
1138
1140 % if range_diff_on is True:
1139 % if range_diff_on is True:
1141 diffFileFilter.select2("enable", false);
1140 diffFileFilter.select2("enable", false);
1142 % endif
1141 % endif
1143
1142
1144 $(diffFileFilterId).on('select2-selecting', function (e) {
1143 $(diffFileFilterId).on('select2-selecting', function (e) {
1145 var idSelector = e.choice.id;
1144 var idSelector = e.choice.id;
1146
1145
1147 // expand the container if we quick-select the field
1146 // expand the container if we quick-select the field
1148 $('#'+idSelector).next().prop('checked', false);
1147 $('#'+idSelector).next().prop('checked', false);
1149 // hide the mast as we later do preventDefault()
1148 // hide the mast as we later do preventDefault()
1150 $("#select2-drop-mask").click();
1149 $("#select2-drop-mask").click();
1151
1150
1152 window.location.hash = '#'+idSelector;
1151 window.location.hash = '#'+idSelector;
1153 updateSticky();
1152 updateSticky();
1154
1153
1155 e.preventDefault();
1154 e.preventDefault();
1156 });
1155 });
1157
1156
1158 diffNavText = 'diff navigation:'
1157 diffNavText = 'diff navigation:'
1159
1158
1160 getCurrentChunk = function () {
1159 getCurrentChunk = function () {
1161
1160
1162 var chunksAll = $('.nav-chunk').filter(function () {
1161 var chunksAll = $('.nav-chunk').filter(function () {
1163 return $(this).parents('.filediff').prev().get(0).checked !== true
1162 return $(this).parents('.filediff').prev().get(0).checked !== true
1164 })
1163 })
1165 var chunkSelected = $('.nav-chunk.selected');
1164 var chunkSelected = $('.nav-chunk.selected');
1166 var initial = false;
1165 var initial = false;
1167
1166
1168 if (chunkSelected.length === 0) {
1167 if (chunkSelected.length === 0) {
1169 // no initial chunk selected, we pick first
1168 // no initial chunk selected, we pick first
1170 chunkSelected = $(chunksAll.get(0));
1169 chunkSelected = $(chunksAll.get(0));
1171 var initial = true;
1170 var initial = true;
1172 }
1171 }
1173
1172
1174 return {
1173 return {
1175 'all': chunksAll,
1174 'all': chunksAll,
1176 'selected': chunkSelected,
1175 'selected': chunkSelected,
1177 'initial': initial,
1176 'initial': initial,
1178 }
1177 }
1179 }
1178 }
1180
1179
1181 animateDiffNavText = function () {
1180 animateDiffNavText = function () {
1182 var $diffNav = $('#diff_nav')
1181 var $diffNav = $('#diff_nav')
1183
1182
1184 var callback = function () {
1183 var callback = function () {
1185 $diffNav.animate({'opacity': 1.00}, 200)
1184 $diffNav.animate({'opacity': 1.00}, 200)
1186 };
1185 };
1187 $diffNav.animate({'opacity': 0.15}, 200, callback);
1186 $diffNav.animate({'opacity': 0.15}, 200, callback);
1188 }
1187 }
1189
1188
1190 scrollToChunk = function (moveBy) {
1189 scrollToChunk = function (moveBy) {
1191 var chunk = getCurrentChunk();
1190 var chunk = getCurrentChunk();
1192 var all = chunk.all
1191 var all = chunk.all
1193 var selected = chunk.selected
1192 var selected = chunk.selected
1194
1193
1195 var curPos = all.index(selected);
1194 var curPos = all.index(selected);
1196 var newPos = curPos;
1195 var newPos = curPos;
1197 if (!chunk.initial) {
1196 if (!chunk.initial) {
1198 var newPos = curPos + moveBy;
1197 var newPos = curPos + moveBy;
1199 }
1198 }
1200
1199
1201 var curElem = all.get(newPos);
1200 var curElem = all.get(newPos);
1202
1201
1203 if (curElem === undefined) {
1202 if (curElem === undefined) {
1204 // end or back
1203 // end or back
1205 $('#diff_nav').html('no next diff element:')
1204 $('#diff_nav').html('no next diff element:')
1206 animateDiffNavText()
1205 animateDiffNavText()
1207 return
1206 return
1208 } else if (newPos < 0) {
1207 } else if (newPos < 0) {
1209 $('#diff_nav').html('no previous diff element:')
1208 $('#diff_nav').html('no previous diff element:')
1210 animateDiffNavText()
1209 animateDiffNavText()
1211 return
1210 return
1212 } else {
1211 } else {
1213 $('#diff_nav').html(diffNavText)
1212 $('#diff_nav').html(diffNavText)
1214 }
1213 }
1215
1214
1216 curElem = $(curElem)
1215 curElem = $(curElem)
1217 var offset = 100;
1216 var offset = 100;
1218 $(window).scrollTop(curElem.position().top - offset);
1217 $(window).scrollTop(curElem.position().top - offset);
1219
1218
1220 //clear selection
1219 //clear selection
1221 all.removeClass('selected')
1220 all.removeClass('selected')
1222 curElem.addClass('selected')
1221 curElem.addClass('selected')
1223 }
1222 }
1224
1223
1225 scrollToPrevChunk = function () {
1224 scrollToPrevChunk = function () {
1226 scrollToChunk(-1)
1225 scrollToChunk(-1)
1227 }
1226 }
1228 scrollToNextChunk = function () {
1227 scrollToNextChunk = function () {
1229 scrollToChunk(1)
1228 scrollToChunk(1)
1230 }
1229 }
1231
1230
1232 </script>
1231 </script>
1233 % endif
1232 % endif
1234
1233
1235 <script type="text/javascript">
1234 <script type="text/javascript">
1236 $('#diff_nav').html('loading diff...') // wait until whole page is loaded
1235 $('#diff_nav').html('loading diff...') // wait until whole page is loaded
1237
1236
1238 $(document).ready(function () {
1237 $(document).ready(function () {
1239
1238
1240 var contextPrefix = _gettext('Context file: ');
1239 var contextPrefix = _gettext('Context file: ');
1241 ## sticky sidebar
1240 ## sticky sidebar
1242 var sidebarElement = document.getElementById('diff-file-sticky');
1241 var sidebarElement = document.getElementById('diff-file-sticky');
1243 sidebar = new StickySidebar(sidebarElement, {
1242 sidebar = new StickySidebar(sidebarElement, {
1244 topSpacing: 0,
1243 topSpacing: 0,
1245 bottomSpacing: 0,
1244 bottomSpacing: 0,
1246 innerWrapperSelector: '.sidebar__inner'
1245 innerWrapperSelector: '.sidebar__inner'
1247 });
1246 });
1248 sidebarElement.addEventListener('affixed.static.stickySidebar', function () {
1247 sidebarElement.addEventListener('affixed.static.stickySidebar', function () {
1249 // reset our file so it's not holding new value
1248 // reset our file so it's not holding new value
1250 $('.fpath-placeholder-text').html(contextPrefix + ' - ')
1249 $('.fpath-placeholder-text').html(contextPrefix + ' - ')
1251 });
1250 });
1252
1251
1253 updateSticky = function () {
1252 updateSticky = function () {
1254 sidebar.updateSticky();
1253 sidebar.updateSticky();
1255 Waypoint.refreshAll();
1254 Waypoint.refreshAll();
1256 };
1255 };
1257
1256
1258 var animateText = function (fPath, anchorId) {
1257 var animateText = function (fPath, anchorId) {
1259 fPath = Select2.util.escapeMarkup(fPath);
1258 fPath = Select2.util.escapeMarkup(fPath);
1260 $('.fpath-placeholder-text').html(contextPrefix + '<a href="#a_' + anchorId + '">' + fPath + '</a>')
1259 $('.fpath-placeholder-text').html(contextPrefix + '<a href="#a_' + anchorId + '">' + fPath + '</a>')
1261 };
1260 };
1262
1261
1263 ## dynamic file waypoints
1262 ## dynamic file waypoints
1264 var setFPathInfo = function(fPath, anchorId){
1263 var setFPathInfo = function(fPath, anchorId){
1265 animateText(fPath, anchorId)
1264 animateText(fPath, anchorId)
1266 };
1265 };
1267
1266
1268 var codeBlock = $('.filediff');
1267 var codeBlock = $('.filediff');
1269
1268
1270 // forward waypoint
1269 // forward waypoint
1271 codeBlock.waypoint(
1270 codeBlock.waypoint(
1272 function(direction) {
1271 function(direction) {
1273 if (direction === "down"){
1272 if (direction === "down"){
1274 setFPathInfo($(this.element).data('fPath'), $(this.element).data('anchorId'))
1273 setFPathInfo($(this.element).data('fPath'), $(this.element).data('anchorId'))
1275 }
1274 }
1276 }, {
1275 }, {
1277 offset: function () {
1276 offset: function () {
1278 return 70;
1277 return 70;
1279 },
1278 },
1280 context: '.fpath-placeholder'
1279 context: '.fpath-placeholder'
1281 }
1280 }
1282 );
1281 );
1283
1282
1284 // backward waypoint
1283 // backward waypoint
1285 codeBlock.waypoint(
1284 codeBlock.waypoint(
1286 function(direction) {
1285 function(direction) {
1287 if (direction === "up"){
1286 if (direction === "up"){
1288 setFPathInfo($(this.element).data('fPath'), $(this.element).data('anchorId'))
1287 setFPathInfo($(this.element).data('fPath'), $(this.element).data('anchorId'))
1289 }
1288 }
1290 }, {
1289 }, {
1291 offset: function () {
1290 offset: function () {
1292 return -this.element.clientHeight + 90;
1291 return -this.element.clientHeight + 90;
1293 },
1292 },
1294 context: '.fpath-placeholder'
1293 context: '.fpath-placeholder'
1295 }
1294 }
1296 );
1295 );
1297
1296
1298 toggleWideDiff = function (el) {
1297 toggleWideDiff = function (el) {
1299 updateSticky();
1298 updateSticky();
1300 var wide = Rhodecode.comments.toggleWideMode(this);
1299 var wide = Rhodecode.comments.toggleWideMode(this);
1301 storeUserSessionAttr('rc_user_session_attr.wide_diff_mode', wide);
1300 storeUserSessionAttr('rc_user_session_attr.wide_diff_mode', wide);
1302 if (wide === true) {
1301 if (wide === true) {
1303 $(el).addClass('btn-active');
1302 $(el).addClass('btn-active');
1304 } else {
1303 } else {
1305 $(el).removeClass('btn-active');
1304 $(el).removeClass('btn-active');
1306 }
1305 }
1307 return null;
1306 return null;
1308 };
1307 };
1309
1308
1310 toggleExpand = function (el, diffsetEl) {
1309 toggleExpand = function (el, diffsetEl) {
1311 var el = $(el);
1310 var el = $(el);
1312 if (el.hasClass('collapsed')) {
1311 if (el.hasClass('collapsed')) {
1313 $('.filediff-collapse-state.collapse-{0}'.format(diffsetEl)).prop('checked', false);
1312 $('.filediff-collapse-state.collapse-{0}'.format(diffsetEl)).prop('checked', false);
1314 el.removeClass('collapsed');
1313 el.removeClass('collapsed');
1315 el.html(
1314 el.html(
1316 '<i class="icon-minus-squared-alt icon-no-margin"></i>' +
1315 '<i class="icon-minus-squared-alt icon-no-margin"></i>' +
1317 _gettext('Collapse all files'));
1316 _gettext('Collapse all files'));
1318 }
1317 }
1319 else {
1318 else {
1320 $('.filediff-collapse-state.collapse-{0}'.format(diffsetEl)).prop('checked', true);
1319 $('.filediff-collapse-state.collapse-{0}'.format(diffsetEl)).prop('checked', true);
1321 el.addClass('collapsed');
1320 el.addClass('collapsed');
1322 el.html(
1321 el.html(
1323 '<i class="icon-plus-squared-alt icon-no-margin"></i>' +
1322 '<i class="icon-plus-squared-alt icon-no-margin"></i>' +
1324 _gettext('Expand all files'));
1323 _gettext('Expand all files'));
1325 }
1324 }
1326 updateSticky()
1325 updateSticky()
1327 };
1326 };
1328
1327
1329 toggleCommitExpand = function (el) {
1328 toggleCommitExpand = function (el) {
1330 var $el = $(el);
1329 var $el = $(el);
1331 var commits = $el.data('toggleCommitsCnt');
1330 var commits = $el.data('toggleCommitsCnt');
1332 var collapseMsg = _ngettext('Collapse {0} commit', 'Collapse {0} commits', commits).format(commits);
1331 var collapseMsg = _ngettext('Collapse {0} commit', 'Collapse {0} commits', commits).format(commits);
1333 var expandMsg = _ngettext('Expand {0} commit', 'Expand {0} commits', commits).format(commits);
1332 var expandMsg = _ngettext('Expand {0} commit', 'Expand {0} commits', commits).format(commits);
1334
1333
1335 if ($el.hasClass('collapsed')) {
1334 if ($el.hasClass('collapsed')) {
1336 $('.compare_select').show();
1335 $('.compare_select').show();
1337 $('.compare_select_hidden').hide();
1336 $('.compare_select_hidden').hide();
1338
1337
1339 $el.removeClass('collapsed');
1338 $el.removeClass('collapsed');
1340 $el.html(
1339 $el.html(
1341 '<i class="icon-minus-squared-alt icon-no-margin"></i>' +
1340 '<i class="icon-minus-squared-alt icon-no-margin"></i>' +
1342 collapseMsg);
1341 collapseMsg);
1343 }
1342 }
1344 else {
1343 else {
1345 $('.compare_select').hide();
1344 $('.compare_select').hide();
1346 $('.compare_select_hidden').show();
1345 $('.compare_select_hidden').show();
1347 $el.addClass('collapsed');
1346 $el.addClass('collapsed');
1348 $el.html(
1347 $el.html(
1349 '<i class="icon-plus-squared-alt icon-no-margin"></i>' +
1348 '<i class="icon-plus-squared-alt icon-no-margin"></i>' +
1350 expandMsg);
1349 expandMsg);
1351 }
1350 }
1352 updateSticky();
1351 updateSticky();
1353 };
1352 };
1354
1353
1355 // get stored diff mode and pre-enable it
1354 // get stored diff mode and pre-enable it
1356 if (templateContext.session_attrs.wide_diff_mode === "true") {
1355 if (templateContext.session_attrs.wide_diff_mode === "true") {
1357 Rhodecode.comments.toggleWideMode(null);
1356 Rhodecode.comments.toggleWideMode(null);
1358 $('.toggle-wide-diff').addClass('btn-active');
1357 $('.toggle-wide-diff').addClass('btn-active');
1359 updateSticky();
1358 updateSticky();
1360 }
1359 }
1361
1360
1362 // DIFF NAV //
1361 // DIFF NAV //
1363
1362
1364 // element to detect scroll direction of
1363 // element to detect scroll direction of
1365 var $window = $(window);
1364 var $window = $(window);
1366
1365
1367 // initialize last scroll position
1366 // initialize last scroll position
1368 var lastScrollY = $window.scrollTop();
1367 var lastScrollY = $window.scrollTop();
1369
1368
1370 $window.on('resize scrollstop', {latency: 350}, function () {
1369 $window.on('resize scrollstop', {latency: 350}, function () {
1371 var visibleChunks = $('.nav-chunk').withinviewport({top: 75});
1370 var visibleChunks = $('.nav-chunk').withinviewport({top: 75});
1372
1371
1373 // get current scroll position
1372 // get current scroll position
1374 var currentScrollY = $window.scrollTop();
1373 var currentScrollY = $window.scrollTop();
1375
1374
1376 // determine current scroll direction
1375 // determine current scroll direction
1377 if (currentScrollY > lastScrollY) {
1376 if (currentScrollY > lastScrollY) {
1378 var y = 'down'
1377 var y = 'down'
1379 } else if (currentScrollY !== lastScrollY) {
1378 } else if (currentScrollY !== lastScrollY) {
1380 var y = 'up';
1379 var y = 'up';
1381 }
1380 }
1382
1381
1383 var pos = -1; // by default we use last element in viewport
1382 var pos = -1; // by default we use last element in viewport
1384 if (y === 'down') {
1383 if (y === 'down') {
1385 pos = -1;
1384 pos = -1;
1386 } else if (y === 'up') {
1385 } else if (y === 'up') {
1387 pos = 0;
1386 pos = 0;
1388 }
1387 }
1389
1388
1390 if (visibleChunks.length > 0) {
1389 if (visibleChunks.length > 0) {
1391 $('.nav-chunk').removeClass('selected');
1390 $('.nav-chunk').removeClass('selected');
1392 $(visibleChunks.get(pos)).addClass('selected');
1391 $(visibleChunks.get(pos)).addClass('selected');
1393 }
1392 }
1394
1393
1395 // update last scroll position to current position
1394 // update last scroll position to current position
1396 lastScrollY = currentScrollY;
1395 lastScrollY = currentScrollY;
1397
1396
1398 });
1397 });
1399 $('#diff_nav').html(diffNavText);
1398 $('#diff_nav').html(diffNavText);
1400
1399
1401 });
1400 });
1402 </script>
1401 </script>
1403
1402
1404 </%def>
1403 </%def>
General Comments 0
You need to be logged in to leave comments. Login now