##// END OF EJS Templates
reviewers: store reviewer reasons to database, fixes #4238
dan -
r873:930d1a1f default
parent child Browse files
Show More

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

@@ -1,121 +1,122 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 import mock
23 23 import pytest
24 24 import urlobject
25 25 from pylons import url
26 26
27 27 from rhodecode.api.tests.utils import (
28 28 build_data, api_call, assert_error, assert_ok)
29 29
30 30 pytestmark = pytest.mark.backends("git", "hg")
31 31
32 32
33 33 @pytest.mark.usefixtures("testuser_api", "app")
34 34 class TestGetPullRequest(object):
35 35
36 36 def test_api_get_pull_request(self, pr_util):
37 37 pull_request = pr_util.create_pull_request(mergeable=True)
38 38 id_, params = build_data(
39 39 self.apikey, 'get_pull_request',
40 40 repoid=pull_request.target_repo.repo_name,
41 41 pullrequestid=pull_request.pull_request_id)
42 42
43 43 response = api_call(self.app, params)
44 44
45 45 assert response.status == '200 OK'
46 46
47 47 url_obj = urlobject.URLObject(
48 48 url(
49 49 'pullrequest_show',
50 50 repo_name=pull_request.target_repo.repo_name,
51 51 pull_request_id=pull_request.pull_request_id, qualified=True))
52 52 pr_url = unicode(
53 53 url_obj.with_netloc('test.example.com:80'))
54 54 source_url = unicode(
55 55 pull_request.source_repo.clone_url()
56 56 .with_netloc('test.example.com:80'))
57 57 target_url = unicode(
58 58 pull_request.target_repo.clone_url()
59 59 .with_netloc('test.example.com:80'))
60 60 expected = {
61 61 'pull_request_id': pull_request.pull_request_id,
62 62 'url': pr_url,
63 63 'title': pull_request.title,
64 64 'description': pull_request.description,
65 65 'status': pull_request.status,
66 66 'created_on': pull_request.created_on,
67 67 'updated_on': pull_request.updated_on,
68 68 'commit_ids': pull_request.revisions,
69 69 'review_status': pull_request.calculated_review_status(),
70 70 'mergeable': {
71 71 'status': True,
72 72 'message': 'This pull request can be automatically merged.',
73 73 },
74 74 'source': {
75 75 'clone_url': source_url,
76 76 'repository': pull_request.source_repo.repo_name,
77 77 'reference': {
78 78 'name': pull_request.source_ref_parts.name,
79 79 'type': pull_request.source_ref_parts.type,
80 80 'commit_id': pull_request.source_ref_parts.commit_id,
81 81 },
82 82 },
83 83 'target': {
84 84 'clone_url': target_url,
85 85 'repository': pull_request.target_repo.repo_name,
86 86 'reference': {
87 87 'name': pull_request.target_ref_parts.name,
88 88 'type': pull_request.target_ref_parts.type,
89 89 'commit_id': pull_request.target_ref_parts.commit_id,
90 90 },
91 91 },
92 92 'author': pull_request.author.get_api_data(include_secrets=False,
93 93 details='basic'),
94 94 'reviewers': [
95 95 {
96 96 'user': reviewer.get_api_data(include_secrets=False,
97 97 details='basic'),
98 'reasons': reasons,
98 99 'review_status': st[0][1].status if st else 'not_reviewed',
99 100 }
100 for reviewer, st in pull_request.reviewers_statuses()
101 for reviewer, reasons, st in pull_request.reviewers_statuses()
101 102 ]
102 103 }
103 104 assert_ok(id_, expected, response.body)
104 105
105 106 def test_api_get_pull_request_repo_error(self):
106 107 id_, params = build_data(
107 108 self.apikey, 'get_pull_request',
108 109 repoid=666, pullrequestid=1)
109 110 response = api_call(self.app, params)
110 111
111 112 expected = 'repository `666` does not exist'
112 113 assert_error(id_, expected, given=response.body)
113 114
114 115 def test_api_get_pull_request_pull_request_error(self):
115 116 id_, params = build_data(
116 117 self.apikey, 'get_pull_request',
117 118 repoid=1, pullrequestid=666)
118 119 response = api_call(self.app, params)
119 120
120 121 expected = 'pull request `666` does not exist'
121 122 assert_error(id_, expected, given=response.body)
@@ -1,635 +1,660 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 import logging
23 23
24 24 from rhodecode.api import jsonrpc_method, JSONRPCError
25 25 from rhodecode.api.utils import (
26 26 has_superadmin_permission, Optional, OAttr, get_repo_or_error,
27 27 get_pull_request_or_error, get_commit_or_error, get_user_or_error,
28 28 has_repo_permissions, resolve_ref_or_error)
29 29 from rhodecode.lib.auth import (HasRepoPermissionAnyApi)
30 30 from rhodecode.lib.base import vcs_operation_context
31 31 from rhodecode.lib.utils2 import str2bool
32 32 from rhodecode.model.changeset_status import ChangesetStatusModel
33 33 from rhodecode.model.comment import ChangesetCommentsModel
34 34 from rhodecode.model.db import Session, ChangesetStatus
35 35 from rhodecode.model.pull_request import PullRequestModel
36 36 from rhodecode.model.settings import SettingsModel
37 37
38 38 log = logging.getLogger(__name__)
39 39
40 40
41 41 @jsonrpc_method()
42 42 def get_pull_request(request, apiuser, repoid, pullrequestid):
43 43 """
44 44 Get a pull request based on the given ID.
45 45
46 46 :param apiuser: This is filled automatically from the |authtoken|.
47 47 :type apiuser: AuthUser
48 48 :param repoid: Repository name or repository ID from where the pull
49 49 request was opened.
50 50 :type repoid: str or int
51 51 :param pullrequestid: ID of the requested pull request.
52 52 :type pullrequestid: int
53 53
54 54 Example output:
55 55
56 56 .. code-block:: bash
57 57
58 58 "id": <id_given_in_input>,
59 59 "result":
60 60 {
61 61 "pull_request_id": "<pull_request_id>",
62 62 "url": "<url>",
63 63 "title": "<title>",
64 64 "description": "<description>",
65 65 "status" : "<status>",
66 66 "created_on": "<date_time_created>",
67 67 "updated_on": "<date_time_updated>",
68 68 "commit_ids": [
69 69 ...
70 70 "<commit_id>",
71 71 "<commit_id>",
72 72 ...
73 73 ],
74 74 "review_status": "<review_status>",
75 75 "mergeable": {
76 76 "status": "<bool>",
77 77 "message": "<message>",
78 78 },
79 79 "source": {
80 80 "clone_url": "<clone_url>",
81 81 "repository": "<repository_name>",
82 82 "reference":
83 83 {
84 84 "name": "<name>",
85 85 "type": "<type>",
86 86 "commit_id": "<commit_id>",
87 87 }
88 88 },
89 89 "target": {
90 90 "clone_url": "<clone_url>",
91 91 "repository": "<repository_name>",
92 92 "reference":
93 93 {
94 94 "name": "<name>",
95 95 "type": "<type>",
96 96 "commit_id": "<commit_id>",
97 97 }
98 98 },
99 99 "author": <user_obj>,
100 100 "reviewers": [
101 101 ...
102 102 {
103 103 "user": "<user_obj>",
104 104 "review_status": "<review_status>",
105 105 }
106 106 ...
107 107 ]
108 108 },
109 109 "error": null
110 110 """
111 111 get_repo_or_error(repoid)
112 112 pull_request = get_pull_request_or_error(pullrequestid)
113 113 if not PullRequestModel().check_user_read(
114 114 pull_request, apiuser, api=True):
115 115 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
116 116 data = pull_request.get_api_data()
117 117 return data
118 118
119 119
120 120 @jsonrpc_method()
121 121 def get_pull_requests(request, apiuser, repoid, status=Optional('new')):
122 122 """
123 123 Get all pull requests from the repository specified in `repoid`.
124 124
125 125 :param apiuser: This is filled automatically from the |authtoken|.
126 126 :type apiuser: AuthUser
127 127 :param repoid: Repository name or repository ID.
128 128 :type repoid: str or int
129 129 :param status: Only return pull requests with the specified status.
130 130 Valid options are.
131 131 * ``new`` (default)
132 132 * ``open``
133 133 * ``closed``
134 134 :type status: str
135 135
136 136 Example output:
137 137
138 138 .. code-block:: bash
139 139
140 140 "id": <id_given_in_input>,
141 141 "result":
142 142 [
143 143 ...
144 144 {
145 145 "pull_request_id": "<pull_request_id>",
146 146 "url": "<url>",
147 147 "title" : "<title>",
148 148 "description": "<description>",
149 149 "status": "<status>",
150 150 "created_on": "<date_time_created>",
151 151 "updated_on": "<date_time_updated>",
152 152 "commit_ids": [
153 153 ...
154 154 "<commit_id>",
155 155 "<commit_id>",
156 156 ...
157 157 ],
158 158 "review_status": "<review_status>",
159 159 "mergeable": {
160 160 "status": "<bool>",
161 161 "message: "<message>",
162 162 },
163 163 "source": {
164 164 "clone_url": "<clone_url>",
165 165 "reference":
166 166 {
167 167 "name": "<name>",
168 168 "type": "<type>",
169 169 "commit_id": "<commit_id>",
170 170 }
171 171 },
172 172 "target": {
173 173 "clone_url": "<clone_url>",
174 174 "reference":
175 175 {
176 176 "name": "<name>",
177 177 "type": "<type>",
178 178 "commit_id": "<commit_id>",
179 179 }
180 180 },
181 181 "author": <user_obj>,
182 182 "reviewers": [
183 183 ...
184 184 {
185 185 "user": "<user_obj>",
186 186 "review_status": "<review_status>",
187 187 }
188 188 ...
189 189 ]
190 190 }
191 191 ...
192 192 ],
193 193 "error": null
194 194
195 195 """
196 196 repo = get_repo_or_error(repoid)
197 197 if not has_superadmin_permission(apiuser):
198 198 _perms = (
199 199 'repository.admin', 'repository.write', 'repository.read',)
200 200 has_repo_permissions(apiuser, repoid, repo, _perms)
201 201
202 202 status = Optional.extract(status)
203 203 pull_requests = PullRequestModel().get_all(repo, statuses=[status])
204 204 data = [pr.get_api_data() for pr in pull_requests]
205 205 return data
206 206
207 207
208 208 @jsonrpc_method()
209 209 def merge_pull_request(request, apiuser, repoid, pullrequestid,
210 210 userid=Optional(OAttr('apiuser'))):
211 211 """
212 212 Merge the pull request specified by `pullrequestid` into its target
213 213 repository.
214 214
215 215 :param apiuser: This is filled automatically from the |authtoken|.
216 216 :type apiuser: AuthUser
217 217 :param repoid: The Repository name or repository ID of the
218 218 target repository to which the |pr| is to be merged.
219 219 :type repoid: str or int
220 220 :param pullrequestid: ID of the pull request which shall be merged.
221 221 :type pullrequestid: int
222 222 :param userid: Merge the pull request as this user.
223 223 :type userid: Optional(str or int)
224 224
225 225 Example output:
226 226
227 227 .. code-block:: bash
228 228
229 229 "id": <id_given_in_input>,
230 230 "result":
231 231 {
232 232 "executed": "<bool>",
233 233 "failure_reason": "<int>",
234 234 "merge_commit_id": "<merge_commit_id>",
235 235 "possible": "<bool>"
236 236 },
237 237 "error": null
238 238
239 239 """
240 240 repo = get_repo_or_error(repoid)
241 241 if not isinstance(userid, Optional):
242 242 if (has_superadmin_permission(apiuser) or
243 243 HasRepoPermissionAnyApi('repository.admin')(
244 244 user=apiuser, repo_name=repo.repo_name)):
245 245 apiuser = get_user_or_error(userid)
246 246 else:
247 247 raise JSONRPCError('userid is not the same as your user')
248 248
249 249 pull_request = get_pull_request_or_error(pullrequestid)
250 250 if not PullRequestModel().check_user_merge(
251 251 pull_request, apiuser, api=True):
252 252 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
253 253 if pull_request.is_closed():
254 254 raise JSONRPCError(
255 255 'pull request `%s` merge failed, pull request is closed' % (
256 256 pullrequestid,))
257 257
258 258 target_repo = pull_request.target_repo
259 259 extras = vcs_operation_context(
260 260 request.environ, repo_name=target_repo.repo_name,
261 261 username=apiuser.username, action='push',
262 262 scm=target_repo.repo_type)
263 263 data = PullRequestModel().merge(pull_request, apiuser, extras=extras)
264 264 if data.executed:
265 265 PullRequestModel().close_pull_request(
266 266 pull_request.pull_request_id, apiuser)
267 267
268 268 Session().commit()
269 269 return data
270 270
271 271
272 272 @jsonrpc_method()
273 273 def close_pull_request(request, apiuser, repoid, pullrequestid,
274 274 userid=Optional(OAttr('apiuser'))):
275 275 """
276 276 Close the pull request specified by `pullrequestid`.
277 277
278 278 :param apiuser: This is filled automatically from the |authtoken|.
279 279 :type apiuser: AuthUser
280 280 :param repoid: Repository name or repository ID to which the pull
281 281 request belongs.
282 282 :type repoid: str or int
283 283 :param pullrequestid: ID of the pull request to be closed.
284 284 :type pullrequestid: int
285 285 :param userid: Close the pull request as this user.
286 286 :type userid: Optional(str or int)
287 287
288 288 Example output:
289 289
290 290 .. code-block:: bash
291 291
292 292 "id": <id_given_in_input>,
293 293 "result":
294 294 {
295 295 "pull_request_id": "<int>",
296 296 "closed": "<bool>"
297 297 },
298 298 "error": null
299 299
300 300 """
301 301 repo = get_repo_or_error(repoid)
302 302 if not isinstance(userid, Optional):
303 303 if (has_superadmin_permission(apiuser) or
304 304 HasRepoPermissionAnyApi('repository.admin')(
305 305 user=apiuser, repo_name=repo.repo_name)):
306 306 apiuser = get_user_or_error(userid)
307 307 else:
308 308 raise JSONRPCError('userid is not the same as your user')
309 309
310 310 pull_request = get_pull_request_or_error(pullrequestid)
311 311 if not PullRequestModel().check_user_update(
312 312 pull_request, apiuser, api=True):
313 313 raise JSONRPCError(
314 314 'pull request `%s` close failed, no permission to close.' % (
315 315 pullrequestid,))
316 316 if pull_request.is_closed():
317 317 raise JSONRPCError(
318 318 'pull request `%s` is already closed' % (pullrequestid,))
319 319
320 320 PullRequestModel().close_pull_request(
321 321 pull_request.pull_request_id, apiuser)
322 322 Session().commit()
323 323 data = {
324 324 'pull_request_id': pull_request.pull_request_id,
325 325 'closed': True,
326 326 }
327 327 return data
328 328
329 329
330 330 @jsonrpc_method()
331 331 def comment_pull_request(request, apiuser, repoid, pullrequestid,
332 332 message=Optional(None), status=Optional(None),
333 333 userid=Optional(OAttr('apiuser'))):
334 334 """
335 335 Comment on the pull request specified with the `pullrequestid`,
336 336 in the |repo| specified by the `repoid`, and optionally change the
337 337 review status.
338 338
339 339 :param apiuser: This is filled automatically from the |authtoken|.
340 340 :type apiuser: AuthUser
341 341 :param repoid: The repository name or repository ID.
342 342 :type repoid: str or int
343 343 :param pullrequestid: The pull request ID.
344 344 :type pullrequestid: int
345 345 :param message: The text content of the comment.
346 346 :type message: str
347 347 :param status: (**Optional**) Set the approval status of the pull
348 348 request. Valid options are:
349 349 * not_reviewed
350 350 * approved
351 351 * rejected
352 352 * under_review
353 353 :type status: str
354 354 :param userid: Comment on the pull request as this user
355 355 :type userid: Optional(str or int)
356 356
357 357 Example output:
358 358
359 359 .. code-block:: bash
360 360
361 361 id : <id_given_in_input>
362 362 result :
363 363 {
364 364 "pull_request_id": "<Integer>",
365 365 "comment_id": "<Integer>"
366 366 }
367 367 error : null
368 368 """
369 369 repo = get_repo_or_error(repoid)
370 370 if not isinstance(userid, Optional):
371 371 if (has_superadmin_permission(apiuser) or
372 372 HasRepoPermissionAnyApi('repository.admin')(
373 373 user=apiuser, repo_name=repo.repo_name)):
374 374 apiuser = get_user_or_error(userid)
375 375 else:
376 376 raise JSONRPCError('userid is not the same as your user')
377 377
378 378 pull_request = get_pull_request_or_error(pullrequestid)
379 379 if not PullRequestModel().check_user_read(
380 380 pull_request, apiuser, api=True):
381 381 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
382 382 message = Optional.extract(message)
383 383 status = Optional.extract(status)
384 384 if not message and not status:
385 385 raise JSONRPCError('message and status parameter missing')
386 386
387 387 if (status not in (st[0] for st in ChangesetStatus.STATUSES) and
388 388 status is not None):
389 389 raise JSONRPCError('unknown comment status`%s`' % status)
390 390
391 391 allowed_to_change_status = PullRequestModel().check_user_change_status(
392 392 pull_request, apiuser)
393 393 text = message
394 394 if status and allowed_to_change_status:
395 395 st_message = (('Status change %(transition_icon)s %(status)s')
396 396 % {'transition_icon': '>',
397 397 'status': ChangesetStatus.get_status_lbl(status)})
398 398 text = message or st_message
399 399
400 400 rc_config = SettingsModel().get_all_settings()
401 401 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
402 402 comment = ChangesetCommentsModel().create(
403 403 text=text,
404 404 repo=pull_request.target_repo.repo_id,
405 405 user=apiuser.user_id,
406 406 pull_request=pull_request.pull_request_id,
407 407 f_path=None,
408 408 line_no=None,
409 409 status_change=(ChangesetStatus.get_status_lbl(status)
410 410 if status and allowed_to_change_status else None),
411 411 status_change_type=(status
412 412 if status and allowed_to_change_status else None),
413 413 closing_pr=False,
414 414 renderer=renderer
415 415 )
416 416
417 417 if allowed_to_change_status and status:
418 418 ChangesetStatusModel().set_status(
419 419 pull_request.target_repo.repo_id,
420 420 status,
421 421 apiuser.user_id,
422 422 comment,
423 423 pull_request=pull_request.pull_request_id
424 424 )
425 425 Session().flush()
426 426
427 427 Session().commit()
428 428 data = {
429 429 'pull_request_id': pull_request.pull_request_id,
430 430 'comment_id': comment.comment_id,
431 431 'status': status
432 432 }
433 433 return data
434 434
435 435
436 436 @jsonrpc_method()
437 437 def create_pull_request(
438 438 request, apiuser, source_repo, target_repo, source_ref, target_ref,
439 439 title, description=Optional(''), reviewers=Optional(None)):
440 440 """
441 441 Creates a new pull request.
442 442
443 443 Accepts refs in the following formats:
444 444
445 445 * branch:<branch_name>:<sha>
446 446 * branch:<branch_name>
447 447 * bookmark:<bookmark_name>:<sha> (Mercurial only)
448 448 * bookmark:<bookmark_name> (Mercurial only)
449 449
450 450 :param apiuser: This is filled automatically from the |authtoken|.
451 451 :type apiuser: AuthUser
452 452 :param source_repo: Set the source repository name.
453 453 :type source_repo: str
454 454 :param target_repo: Set the target repository name.
455 455 :type target_repo: str
456 456 :param source_ref: Set the source ref name.
457 457 :type source_ref: str
458 458 :param target_ref: Set the target ref name.
459 459 :type target_ref: str
460 460 :param title: Set the pull request title.
461 461 :type title: str
462 462 :param description: Set the pull request description.
463 463 :type description: Optional(str)
464 464 :param reviewers: Set the new pull request reviewers list.
465 465 :type reviewers: Optional(list)
466 Accepts username strings or objects of the format:
467 {
468 'username': 'nick', 'reasons': ['original author']
469 }
466 470 """
471
467 472 source = get_repo_or_error(source_repo)
468 473 target = get_repo_or_error(target_repo)
469 474 if not has_superadmin_permission(apiuser):
470 475 _perms = ('repository.admin', 'repository.write', 'repository.read',)
471 476 has_repo_permissions(apiuser, source_repo, source, _perms)
472 477
473 478 full_source_ref = resolve_ref_or_error(source_ref, source)
474 479 full_target_ref = resolve_ref_or_error(target_ref, target)
475 480 source_commit = get_commit_or_error(full_source_ref, source)
476 481 target_commit = get_commit_or_error(full_target_ref, target)
477 482 source_scm = source.scm_instance()
478 483 target_scm = target.scm_instance()
479 484
480 485 commit_ranges = target_scm.compare(
481 486 target_commit.raw_id, source_commit.raw_id, source_scm,
482 487 merge=True, pre_load=[])
483 488
484 489 ancestor = target_scm.get_common_ancestor(
485 490 target_commit.raw_id, source_commit.raw_id, source_scm)
486 491
487 492 if not commit_ranges:
488 493 raise JSONRPCError('no commits found')
489 494
490 495 if not ancestor:
491 496 raise JSONRPCError('no common ancestor found')
492 497
493 reviewer_names = Optional.extract(reviewers) or []
494 if not isinstance(reviewer_names, list):
498 reviewer_objects = Optional.extract(reviewers) or []
499 if not isinstance(reviewer_objects, list):
495 500 raise JSONRPCError('reviewers should be specified as a list')
496 501
497 reviewer_users = [get_user_or_error(n) for n in reviewer_names]
498 reviewer_ids = [u.user_id for u in reviewer_users]
502 reviewers_reasons = []
503 for reviewer_object in reviewer_objects:
504 reviewer_reasons = []
505 if isinstance(reviewer_object, (basestring, int)):
506 reviewer_username = reviewer_object
507 else:
508 reviewer_username = reviewer_object['username']
509 reviewer_reasons = reviewer_object.get('reasons', [])
510
511 user = get_user_or_error(reviewer_username)
512 reviewers_reasons.append((user.user_id, reviewer_reasons))
499 513
500 514 pull_request_model = PullRequestModel()
501 515 pull_request = pull_request_model.create(
502 516 created_by=apiuser.user_id,
503 517 source_repo=source_repo,
504 518 source_ref=full_source_ref,
505 519 target_repo=target_repo,
506 520 target_ref=full_target_ref,
507 521 revisions=reversed(
508 522 [commit.raw_id for commit in reversed(commit_ranges)]),
509 reviewers=reviewer_ids,
523 reviewers=reviewers_reasons,
510 524 title=title,
511 525 description=Optional.extract(description)
512 526 )
513 527
514 528 Session().commit()
515 529 data = {
516 530 'msg': 'Created new pull request `{}`'.format(title),
517 531 'pull_request_id': pull_request.pull_request_id,
518 532 }
519 533 return data
520 534
521 535
522 536 @jsonrpc_method()
523 537 def update_pull_request(
524 538 request, apiuser, repoid, pullrequestid, title=Optional(''),
525 539 description=Optional(''), reviewers=Optional(None),
526 540 update_commits=Optional(None), close_pull_request=Optional(None)):
527 541 """
528 542 Updates a pull request.
529 543
530 544 :param apiuser: This is filled automatically from the |authtoken|.
531 545 :type apiuser: AuthUser
532 546 :param repoid: The repository name or repository ID.
533 547 :type repoid: str or int
534 548 :param pullrequestid: The pull request ID.
535 549 :type pullrequestid: int
536 550 :param title: Set the pull request title.
537 551 :type title: str
538 552 :param description: Update pull request description.
539 553 :type description: Optional(str)
540 554 :param reviewers: Update pull request reviewers list with new value.
541 555 :type reviewers: Optional(list)
542 556 :param update_commits: Trigger update of commits for this pull request
543 557 :type: update_commits: Optional(bool)
544 558 :param close_pull_request: Close this pull request with rejected state
545 559 :type: close_pull_request: Optional(bool)
546 560
547 561 Example output:
548 562
549 563 .. code-block:: bash
550 564
551 565 id : <id_given_in_input>
552 566 result :
553 567 {
554 568 "msg": "Updated pull request `63`",
555 569 "pull_request": <pull_request_object>,
556 570 "updated_reviewers": {
557 571 "added": [
558 572 "username"
559 573 ],
560 574 "removed": []
561 575 },
562 576 "updated_commits": {
563 577 "added": [
564 578 "<sha1_hash>"
565 579 ],
566 580 "common": [
567 581 "<sha1_hash>",
568 582 "<sha1_hash>",
569 583 ],
570 584 "removed": []
571 585 }
572 586 }
573 587 error : null
574 588 """
575 589
576 590 repo = get_repo_or_error(repoid)
577 591 pull_request = get_pull_request_or_error(pullrequestid)
578 592 if not PullRequestModel().check_user_update(
579 593 pull_request, apiuser, api=True):
580 594 raise JSONRPCError(
581 595 'pull request `%s` update failed, no permission to update.' % (
582 596 pullrequestid,))
583 597 if pull_request.is_closed():
584 598 raise JSONRPCError(
585 599 'pull request `%s` update failed, pull request is closed' % (
586 600 pullrequestid,))
587 601
588 reviewer_names = Optional.extract(reviewers) or []
589 if not isinstance(reviewer_names, list):
602 reviewer_objects = Optional.extract(reviewers) or []
603 if not isinstance(reviewer_objects, list):
590 604 raise JSONRPCError('reviewers should be specified as a list')
591 605
592 reviewer_users = [get_user_or_error(n) for n in reviewer_names]
593 reviewer_ids = [u.user_id for u in reviewer_users]
606 reviewers_reasons = []
607 reviewer_ids = set()
608 for reviewer_object in reviewer_objects:
609 reviewer_reasons = []
610 if isinstance(reviewer_object, (int, basestring)):
611 reviewer_username = reviewer_object
612 else:
613 reviewer_username = reviewer_object['username']
614 reviewer_reasons = reviewer_object.get('reasons', [])
615
616 user = get_user_or_error(reviewer_username)
617 reviewer_ids.add(user.user_id)
618 reviewers_reasons.append((user.user_id, reviewer_reasons))
594 619
595 620 title = Optional.extract(title)
596 621 description = Optional.extract(description)
597 622 if title or description:
598 623 PullRequestModel().edit(
599 624 pull_request, title or pull_request.title,
600 625 description or pull_request.description)
601 626 Session().commit()
602 627
603 628 commit_changes = {"added": [], "common": [], "removed": []}
604 629 if str2bool(Optional.extract(update_commits)):
605 630 if PullRequestModel().has_valid_update_type(pull_request):
606 631 _version, _commit_changes = PullRequestModel().update_commits(
607 632 pull_request)
608 633 commit_changes = _commit_changes or commit_changes
609 634 Session().commit()
610 635
611 636 reviewers_changes = {"added": [], "removed": []}
612 637 if reviewer_ids:
613 638 added_reviewers, removed_reviewers = \
614 PullRequestModel().update_reviewers(pull_request, reviewer_ids)
639 PullRequestModel().update_reviewers(pull_request, reviewers_reasons)
615 640
616 641 reviewers_changes['added'] = sorted(
617 642 [get_user_or_error(n).username for n in added_reviewers])
618 643 reviewers_changes['removed'] = sorted(
619 644 [get_user_or_error(n).username for n in removed_reviewers])
620 645 Session().commit()
621 646
622 647 if str2bool(Optional.extract(close_pull_request)):
623 648 PullRequestModel().close_pull_request_with_comment(
624 649 pull_request, apiuser, repo)
625 650 Session().commit()
626 651
627 652 data = {
628 653 'msg': 'Updated pull request `{}`'.format(
629 654 pull_request.pull_request_id),
630 655 'pull_request': pull_request.get_api_data(),
631 656 'updated_commits': commit_changes,
632 657 'updated_reviewers': reviewers_changes
633 658 }
634 659 return data
635 660
@@ -1,885 +1,891 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 pull requests controller for rhodecode for initializing pull requests
23 23 """
24 24
25 import peppercorn
25 26 import formencode
26 27 import logging
27 28
28 29 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPBadRequest
29 30 from pylons import request, tmpl_context as c, url
30 31 from pylons.controllers.util import redirect
31 32 from pylons.i18n.translation import _
32 33 from pyramid.threadlocal import get_current_registry
33 34 from sqlalchemy.sql import func
34 35 from sqlalchemy.sql.expression import or_
35 36
36 37 from rhodecode import events
37 38 from rhodecode.lib import auth, diffs, helpers as h
38 39 from rhodecode.lib.ext_json import json
39 40 from rhodecode.lib.base import (
40 41 BaseRepoController, render, vcs_operation_context)
41 42 from rhodecode.lib.auth import (
42 43 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous,
43 44 HasAcceptedRepoType, XHRRequired)
44 45 from rhodecode.lib.channelstream import channelstream_request
45 46 from rhodecode.lib.utils import jsonify
46 47 from rhodecode.lib.utils2 import safe_int, safe_str, str2bool, safe_unicode
47 48 from rhodecode.lib.vcs.backends.base import EmptyCommit
48 49 from rhodecode.lib.vcs.exceptions import (
49 50 EmptyRepositoryError, CommitDoesNotExistError, RepositoryRequirementError)
50 51 from rhodecode.lib.diffs import LimitedDiffContainer
51 52 from rhodecode.model.changeset_status import ChangesetStatusModel
52 53 from rhodecode.model.comment import ChangesetCommentsModel
53 54 from rhodecode.model.db import PullRequest, ChangesetStatus, ChangesetComment, \
54 55 Repository
55 56 from rhodecode.model.forms import PullRequestForm
56 57 from rhodecode.model.meta import Session
57 58 from rhodecode.model.pull_request import PullRequestModel
58 59
59 60 log = logging.getLogger(__name__)
60 61
61 62
62 63 class PullrequestsController(BaseRepoController):
63 64 def __before__(self):
64 65 super(PullrequestsController, self).__before__()
65 66
66 67 def _load_compare_data(self, pull_request, enable_comments=True):
67 68 """
68 69 Load context data needed for generating compare diff
69 70
70 71 :param pull_request: object related to the request
71 72 :param enable_comments: flag to determine if comments are included
72 73 """
73 74 source_repo = pull_request.source_repo
74 75 source_ref_id = pull_request.source_ref_parts.commit_id
75 76
76 77 target_repo = pull_request.target_repo
77 78 target_ref_id = pull_request.target_ref_parts.commit_id
78 79
79 80 # despite opening commits for bookmarks/branches/tags, we always
80 81 # convert this to rev to prevent changes after bookmark or branch change
81 82 c.source_ref_type = 'rev'
82 83 c.source_ref = source_ref_id
83 84
84 85 c.target_ref_type = 'rev'
85 86 c.target_ref = target_ref_id
86 87
87 88 c.source_repo = source_repo
88 89 c.target_repo = target_repo
89 90
90 91 c.fulldiff = bool(request.GET.get('fulldiff'))
91 92
92 93 # diff_limit is the old behavior, will cut off the whole diff
93 94 # if the limit is applied otherwise will just hide the
94 95 # big files from the front-end
95 96 diff_limit = self.cut_off_limit_diff
96 97 file_limit = self.cut_off_limit_file
97 98
98 99 pre_load = ["author", "branch", "date", "message"]
99 100
100 101 c.commit_ranges = []
101 102 source_commit = EmptyCommit()
102 103 target_commit = EmptyCommit()
103 104 c.missing_requirements = False
104 105 try:
105 106 c.commit_ranges = [
106 107 source_repo.get_commit(commit_id=rev, pre_load=pre_load)
107 108 for rev in pull_request.revisions]
108 109
109 110 c.statuses = source_repo.statuses(
110 111 [x.raw_id for x in c.commit_ranges])
111 112
112 113 target_commit = source_repo.get_commit(
113 114 commit_id=safe_str(target_ref_id))
114 115 source_commit = source_repo.get_commit(
115 116 commit_id=safe_str(source_ref_id))
116 117 except RepositoryRequirementError:
117 118 c.missing_requirements = True
118 119
119 120 c.missing_commits = False
120 121 if (c.missing_requirements or
121 122 isinstance(source_commit, EmptyCommit) or
122 123 source_commit == target_commit):
123 124 _parsed = []
124 125 c.missing_commits = True
125 126 else:
126 127 vcs_diff = PullRequestModel().get_diff(pull_request)
127 128 diff_processor = diffs.DiffProcessor(
128 129 vcs_diff, format='gitdiff', diff_limit=diff_limit,
129 130 file_limit=file_limit, show_full_diff=c.fulldiff)
130 131 _parsed = diff_processor.prepare()
131 132
132 133 c.limited_diff = isinstance(_parsed, LimitedDiffContainer)
133 134
134 135 c.files = []
135 136 c.changes = {}
136 137 c.lines_added = 0
137 138 c.lines_deleted = 0
138 139 c.included_files = []
139 140 c.deleted_files = []
140 141
141 142 for f in _parsed:
142 143 st = f['stats']
143 144 c.lines_added += st['added']
144 145 c.lines_deleted += st['deleted']
145 146
146 147 fid = h.FID('', f['filename'])
147 148 c.files.append([fid, f['operation'], f['filename'], f['stats']])
148 149 c.included_files.append(f['filename'])
149 150 html_diff = diff_processor.as_html(enable_comments=enable_comments,
150 151 parsed_lines=[f])
151 152 c.changes[fid] = [f['operation'], f['filename'], html_diff, f]
152 153
153 154 def _extract_ordering(self, request):
154 155 column_index = safe_int(request.GET.get('order[0][column]'))
155 156 order_dir = request.GET.get('order[0][dir]', 'desc')
156 157 order_by = request.GET.get(
157 158 'columns[%s][data][sort]' % column_index, 'name_raw')
158 159 return order_by, order_dir
159 160
160 161 @LoginRequired()
161 162 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
162 163 'repository.admin')
163 164 @HasAcceptedRepoType('git', 'hg')
164 165 def show_all(self, repo_name):
165 166 # filter types
166 167 c.active = 'open'
167 168 c.source = str2bool(request.GET.get('source'))
168 169 c.closed = str2bool(request.GET.get('closed'))
169 170 c.my = str2bool(request.GET.get('my'))
170 171 c.awaiting_review = str2bool(request.GET.get('awaiting_review'))
171 172 c.awaiting_my_review = str2bool(request.GET.get('awaiting_my_review'))
172 173 c.repo_name = repo_name
173 174
174 175 opened_by = None
175 176 if c.my:
176 177 c.active = 'my'
177 178 opened_by = [c.rhodecode_user.user_id]
178 179
179 180 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
180 181 if c.closed:
181 182 c.active = 'closed'
182 183 statuses = [PullRequest.STATUS_CLOSED]
183 184
184 185 if c.awaiting_review and not c.source:
185 186 c.active = 'awaiting'
186 187 if c.source and not c.awaiting_review:
187 188 c.active = 'source'
188 189 if c.awaiting_my_review:
189 190 c.active = 'awaiting_my'
190 191
191 192 data = self._get_pull_requests_list(
192 193 repo_name=repo_name, opened_by=opened_by, statuses=statuses)
193 194 if not request.is_xhr:
194 195 c.data = json.dumps(data['data'])
195 196 c.records_total = data['recordsTotal']
196 197 return render('/pullrequests/pullrequests.html')
197 198 else:
198 199 return json.dumps(data)
199 200
200 201 def _get_pull_requests_list(self, repo_name, opened_by, statuses):
201 202 # pagination
202 203 start = safe_int(request.GET.get('start'), 0)
203 204 length = safe_int(request.GET.get('length'), c.visual.dashboard_items)
204 205 order_by, order_dir = self._extract_ordering(request)
205 206
206 207 if c.awaiting_review:
207 208 pull_requests = PullRequestModel().get_awaiting_review(
208 209 repo_name, source=c.source, opened_by=opened_by,
209 210 statuses=statuses, offset=start, length=length,
210 211 order_by=order_by, order_dir=order_dir)
211 212 pull_requests_total_count = PullRequestModel(
212 213 ).count_awaiting_review(
213 214 repo_name, source=c.source, statuses=statuses,
214 215 opened_by=opened_by)
215 216 elif c.awaiting_my_review:
216 217 pull_requests = PullRequestModel().get_awaiting_my_review(
217 218 repo_name, source=c.source, opened_by=opened_by,
218 219 user_id=c.rhodecode_user.user_id, statuses=statuses,
219 220 offset=start, length=length, order_by=order_by,
220 221 order_dir=order_dir)
221 222 pull_requests_total_count = PullRequestModel(
222 223 ).count_awaiting_my_review(
223 224 repo_name, source=c.source, user_id=c.rhodecode_user.user_id,
224 225 statuses=statuses, opened_by=opened_by)
225 226 else:
226 227 pull_requests = PullRequestModel().get_all(
227 228 repo_name, source=c.source, opened_by=opened_by,
228 229 statuses=statuses, offset=start, length=length,
229 230 order_by=order_by, order_dir=order_dir)
230 231 pull_requests_total_count = PullRequestModel().count_all(
231 232 repo_name, source=c.source, statuses=statuses,
232 233 opened_by=opened_by)
233 234
234 235 from rhodecode.lib.utils import PartialRenderer
235 236 _render = PartialRenderer('data_table/_dt_elements.html')
236 237 data = []
237 238 for pr in pull_requests:
238 239 comments = ChangesetCommentsModel().get_all_comments(
239 240 c.rhodecode_db_repo.repo_id, pull_request=pr)
240 241
241 242 data.append({
242 243 'name': _render('pullrequest_name',
243 244 pr.pull_request_id, pr.target_repo.repo_name),
244 245 'name_raw': pr.pull_request_id,
245 246 'status': _render('pullrequest_status',
246 247 pr.calculated_review_status()),
247 248 'title': _render(
248 249 'pullrequest_title', pr.title, pr.description),
249 250 'description': h.escape(pr.description),
250 251 'updated_on': _render('pullrequest_updated_on',
251 252 h.datetime_to_time(pr.updated_on)),
252 253 'updated_on_raw': h.datetime_to_time(pr.updated_on),
253 254 'created_on': _render('pullrequest_updated_on',
254 255 h.datetime_to_time(pr.created_on)),
255 256 'created_on_raw': h.datetime_to_time(pr.created_on),
256 257 'author': _render('pullrequest_author',
257 258 pr.author.full_contact, ),
258 259 'author_raw': pr.author.full_name,
259 260 'comments': _render('pullrequest_comments', len(comments)),
260 261 'comments_raw': len(comments),
261 262 'closed': pr.is_closed(),
262 263 })
263 264 # json used to render the grid
264 265 data = ({
265 266 'data': data,
266 267 'recordsTotal': pull_requests_total_count,
267 268 'recordsFiltered': pull_requests_total_count,
268 269 })
269 270 return data
270 271
271 272 @LoginRequired()
272 273 @NotAnonymous()
273 274 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
274 275 'repository.admin')
275 276 @HasAcceptedRepoType('git', 'hg')
276 277 def index(self):
277 278 source_repo = c.rhodecode_db_repo
278 279
279 280 try:
280 281 source_repo.scm_instance().get_commit()
281 282 except EmptyRepositoryError:
282 283 h.flash(h.literal(_('There are no commits yet')),
283 284 category='warning')
284 285 redirect(url('summary_home', repo_name=source_repo.repo_name))
285 286
286 287 commit_id = request.GET.get('commit')
287 288 branch_ref = request.GET.get('branch')
288 289 bookmark_ref = request.GET.get('bookmark')
289 290
290 291 try:
291 292 source_repo_data = PullRequestModel().generate_repo_data(
292 293 source_repo, commit_id=commit_id,
293 294 branch=branch_ref, bookmark=bookmark_ref)
294 295 except CommitDoesNotExistError as e:
295 296 log.exception(e)
296 297 h.flash(_('Commit does not exist'), 'error')
297 298 redirect(url('pullrequest_home', repo_name=source_repo.repo_name))
298 299
299 300 default_target_repo = source_repo
300 301 if (source_repo.parent and
301 302 not source_repo.parent.scm_instance().is_empty()):
302 303 # change default if we have a parent repo
303 304 default_target_repo = source_repo.parent
304 305
305 306 target_repo_data = PullRequestModel().generate_repo_data(
306 307 default_target_repo)
307 308
308 309 selected_source_ref = source_repo_data['refs']['selected_ref']
309 310
310 311 title_source_ref = selected_source_ref.split(':', 2)[1]
311 312 c.default_title = PullRequestModel().generate_pullrequest_title(
312 313 source=source_repo.repo_name,
313 314 source_ref=title_source_ref,
314 315 target=default_target_repo.repo_name
315 316 )
316 317
317 318 c.default_repo_data = {
318 319 'source_repo_name': source_repo.repo_name,
319 320 'source_refs_json': json.dumps(source_repo_data),
320 321 'target_repo_name': default_target_repo.repo_name,
321 322 'target_refs_json': json.dumps(target_repo_data),
322 323 }
323 324 c.default_source_ref = selected_source_ref
324 325
325 326 return render('/pullrequests/pullrequest.html')
326 327
327 328 @LoginRequired()
328 329 @NotAnonymous()
329 330 @XHRRequired()
330 331 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
331 332 'repository.admin')
332 333 @jsonify
333 334 def get_repo_refs(self, repo_name, target_repo_name):
334 335 repo = Repository.get_by_repo_name(target_repo_name)
335 336 if not repo:
336 337 raise HTTPNotFound
337 338 return PullRequestModel().generate_repo_data(repo)
338 339
339 340 @LoginRequired()
340 341 @NotAnonymous()
341 342 @XHRRequired()
342 343 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
343 344 'repository.admin')
344 345 @jsonify
345 346 def get_repo_destinations(self, repo_name):
346 347 repo = Repository.get_by_repo_name(repo_name)
347 348 if not repo:
348 349 raise HTTPNotFound
349 350 filter_query = request.GET.get('query')
350 351
351 352 query = Repository.query() \
352 353 .order_by(func.length(Repository.repo_name)) \
353 354 .filter(or_(
354 355 Repository.repo_name == repo.repo_name,
355 356 Repository.fork_id == repo.repo_id))
356 357
357 358 if filter_query:
358 359 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
359 360 query = query.filter(
360 361 Repository.repo_name.ilike(ilike_expression))
361 362
362 363 add_parent = False
363 364 if repo.parent:
364 365 if filter_query in repo.parent.repo_name:
365 366 if not repo.parent.scm_instance().is_empty():
366 367 add_parent = True
367 368
368 369 limit = 20 - 1 if add_parent else 20
369 370 all_repos = query.limit(limit).all()
370 371 if add_parent:
371 372 all_repos += [repo.parent]
372 373
373 374 repos = []
374 375 for obj in self.scm_model.get_repos(all_repos):
375 376 repos.append({
376 377 'id': obj['name'],
377 378 'text': obj['name'],
378 379 'type': 'repo',
379 380 'obj': obj['dbrepo']
380 381 })
381 382
382 383 data = {
383 384 'more': False,
384 385 'results': [{
385 386 'text': _('Repositories'),
386 387 'children': repos
387 388 }] if repos else []
388 389 }
389 390 return data
390 391
391 392 @LoginRequired()
392 393 @NotAnonymous()
393 394 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
394 395 'repository.admin')
395 396 @HasAcceptedRepoType('git', 'hg')
396 397 @auth.CSRFRequired()
397 398 def create(self, repo_name):
398 399 repo = Repository.get_by_repo_name(repo_name)
399 400 if not repo:
400 401 raise HTTPNotFound
401 402
403 controls = peppercorn.parse(request.POST.items())
404
402 405 try:
403 _form = PullRequestForm(repo.repo_id)().to_python(request.POST)
406 _form = PullRequestForm(repo.repo_id)().to_python(controls)
404 407 except formencode.Invalid as errors:
405 408 if errors.error_dict.get('revisions'):
406 409 msg = 'Revisions: %s' % errors.error_dict['revisions']
407 410 elif errors.error_dict.get('pullrequest_title'):
408 411 msg = _('Pull request requires a title with min. 3 chars')
409 412 else:
410 413 msg = _('Error creating pull request: {}').format(errors)
411 414 log.exception(msg)
412 415 h.flash(msg, 'error')
413 416
414 417 # would rather just go back to form ...
415 418 return redirect(url('pullrequest_home', repo_name=repo_name))
416 419
417 420 source_repo = _form['source_repo']
418 421 source_ref = _form['source_ref']
419 422 target_repo = _form['target_repo']
420 423 target_ref = _form['target_ref']
421 424 commit_ids = _form['revisions'][::-1]
422 reviewers = _form['review_members']
425 reviewers = [
426 (r['user_id'], r['reasons']) for r in _form['review_members']]
423 427
424 428 # find the ancestor for this pr
425 429 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
426 430 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
427 431
428 432 source_scm = source_db_repo.scm_instance()
429 433 target_scm = target_db_repo.scm_instance()
430 434
431 435 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
432 436 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
433 437
434 438 ancestor = source_scm.get_common_ancestor(
435 439 source_commit.raw_id, target_commit.raw_id, target_scm)
436 440
437 441 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
438 442 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
439 443
440 444 pullrequest_title = _form['pullrequest_title']
441 445 title_source_ref = source_ref.split(':', 2)[1]
442 446 if not pullrequest_title:
443 447 pullrequest_title = PullRequestModel().generate_pullrequest_title(
444 448 source=source_repo,
445 449 source_ref=title_source_ref,
446 450 target=target_repo
447 451 )
448 452
449 453 description = _form['pullrequest_desc']
450 454 try:
451 455 pull_request = PullRequestModel().create(
452 456 c.rhodecode_user.user_id, source_repo, source_ref, target_repo,
453 457 target_ref, commit_ids, reviewers, pullrequest_title,
454 458 description
455 459 )
456 460 Session().commit()
457 461 h.flash(_('Successfully opened new pull request'),
458 462 category='success')
459 463 except Exception as e:
460 464 msg = _('Error occurred during sending pull request')
461 465 log.exception(msg)
462 466 h.flash(msg, category='error')
463 467 return redirect(url('pullrequest_home', repo_name=repo_name))
464 468
465 469 return redirect(url('pullrequest_show', repo_name=target_repo,
466 470 pull_request_id=pull_request.pull_request_id))
467 471
468 472 @LoginRequired()
469 473 @NotAnonymous()
470 474 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
471 475 'repository.admin')
472 476 @auth.CSRFRequired()
473 477 @jsonify
474 478 def update(self, repo_name, pull_request_id):
475 479 pull_request_id = safe_int(pull_request_id)
476 480 pull_request = PullRequest.get_or_404(pull_request_id)
477 481 # only owner or admin can update it
478 482 allowed_to_update = PullRequestModel().check_user_update(
479 483 pull_request, c.rhodecode_user)
480 484 if allowed_to_update:
481 if 'reviewers_ids' in request.POST:
482 self._update_reviewers(pull_request_id)
485 controls = peppercorn.parse(request.POST.items())
486
487 if 'review_members' in controls:
488 self._update_reviewers(
489 pull_request_id, controls['review_members'])
483 490 elif str2bool(request.POST.get('update_commits', 'false')):
484 491 self._update_commits(pull_request)
485 492 elif str2bool(request.POST.get('close_pull_request', 'false')):
486 493 self._reject_close(pull_request)
487 494 elif str2bool(request.POST.get('edit_pull_request', 'false')):
488 495 self._edit_pull_request(pull_request)
489 496 else:
490 497 raise HTTPBadRequest()
491 498 return True
492 499 raise HTTPForbidden()
493 500
494 501 def _edit_pull_request(self, pull_request):
495 502 try:
496 503 PullRequestModel().edit(
497 504 pull_request, request.POST.get('title'),
498 505 request.POST.get('description'))
499 506 except ValueError:
500 507 msg = _(u'Cannot update closed pull requests.')
501 508 h.flash(msg, category='error')
502 509 return
503 510 else:
504 511 Session().commit()
505 512
506 513 msg = _(u'Pull request title & description updated.')
507 514 h.flash(msg, category='success')
508 515 return
509 516
510 517 def _update_commits(self, pull_request):
511 518 try:
512 519 if PullRequestModel().has_valid_update_type(pull_request):
513 520 updated_version, changes = PullRequestModel().update_commits(
514 521 pull_request)
515 522 if updated_version:
516 523 msg = _(
517 524 u'Pull request updated to "{source_commit_id}" with '
518 525 u'{count_added} added, {count_removed} removed '
519 526 u'commits.'
520 527 ).format(
521 528 source_commit_id=pull_request.source_ref_parts.commit_id,
522 529 count_added=len(changes.added),
523 530 count_removed=len(changes.removed))
524 531 h.flash(msg, category='success')
525 532 registry = get_current_registry()
526 533 rhodecode_plugins = getattr(registry,
527 534 'rhodecode_plugins', {})
528 535 channelstream_config = rhodecode_plugins.get(
529 536 'channelstream', {})
530 537 if channelstream_config.get('enabled'):
531 538 message = msg + ' - <a onclick="' \
532 539 'window.location.reload()">' \
533 540 '<strong>{}</strong></a>'.format(
534 541 _('Reload page')
535 542 )
536 543 channel = '/repo${}$/pr/{}'.format(
537 544 pull_request.target_repo.repo_name,
538 545 pull_request.pull_request_id
539 546 )
540 547 payload = {
541 548 'type': 'message',
542 549 'user': 'system',
543 550 'exclude_users': [request.user.username],
544 551 'channel': channel,
545 552 'message': {
546 553 'message': message,
547 554 'level': 'success',
548 555 'topic': '/notifications'
549 556 }
550 557 }
551 558 channelstream_request(channelstream_config, [payload],
552 559 '/message', raise_exc=False)
553 560 else:
554 561 h.flash(_("Nothing changed in pull request."),
555 562 category='warning')
556 563 else:
557 564 msg = _(
558 565 u"Skipping update of pull request due to reference "
559 566 u"type: {reference_type}"
560 567 ).format(reference_type=pull_request.source_ref_parts.type)
561 568 h.flash(msg, category='warning')
562 569 except CommitDoesNotExistError:
563 570 h.flash(
564 571 _(u'Update failed due to missing commits.'), category='error')
565 572
566 573 @auth.CSRFRequired()
567 574 @LoginRequired()
568 575 @NotAnonymous()
569 576 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
570 577 'repository.admin')
571 578 def merge(self, repo_name, pull_request_id):
572 579 """
573 580 POST /{repo_name}/pull-request/{pull_request_id}
574 581
575 582 Merge will perform a server-side merge of the specified
576 583 pull request, if the pull request is approved and mergeable.
577 584 After succesfull merging, the pull request is automatically
578 585 closed, with a relevant comment.
579 586 """
580 587 pull_request_id = safe_int(pull_request_id)
581 588 pull_request = PullRequest.get_or_404(pull_request_id)
582 589 user = c.rhodecode_user
583 590
584 591 if self._meets_merge_pre_conditions(pull_request, user):
585 592 log.debug("Pre-conditions checked, trying to merge.")
586 593 extras = vcs_operation_context(
587 594 request.environ, repo_name=pull_request.target_repo.repo_name,
588 595 username=user.username, action='push',
589 596 scm=pull_request.target_repo.repo_type)
590 597 self._merge_pull_request(pull_request, user, extras)
591 598
592 599 return redirect(url(
593 600 'pullrequest_show',
594 601 repo_name=pull_request.target_repo.repo_name,
595 602 pull_request_id=pull_request.pull_request_id))
596 603
597 604 def _meets_merge_pre_conditions(self, pull_request, user):
598 605 if not PullRequestModel().check_user_merge(pull_request, user):
599 606 raise HTTPForbidden()
600 607
601 608 merge_status, msg = PullRequestModel().merge_status(pull_request)
602 609 if not merge_status:
603 610 log.debug("Cannot merge, not mergeable.")
604 611 h.flash(msg, category='error')
605 612 return False
606 613
607 614 if (pull_request.calculated_review_status()
608 615 is not ChangesetStatus.STATUS_APPROVED):
609 616 log.debug("Cannot merge, approval is pending.")
610 617 msg = _('Pull request reviewer approval is pending.')
611 618 h.flash(msg, category='error')
612 619 return False
613 620 return True
614 621
615 622 def _merge_pull_request(self, pull_request, user, extras):
616 623 merge_resp = PullRequestModel().merge(
617 624 pull_request, user, extras=extras)
618 625
619 626 if merge_resp.executed:
620 627 log.debug("The merge was successful, closing the pull request.")
621 628 PullRequestModel().close_pull_request(
622 629 pull_request.pull_request_id, user)
623 630 Session().commit()
624 631 msg = _('Pull request was successfully merged and closed.')
625 632 h.flash(msg, category='success')
626 633 else:
627 634 log.debug(
628 635 "The merge was not successful. Merge response: %s",
629 636 merge_resp)
630 637 msg = PullRequestModel().merge_status_message(
631 638 merge_resp.failure_reason)
632 639 h.flash(msg, category='error')
633 640
634 def _update_reviewers(self, pull_request_id):
635 reviewers_ids = map(int, filter(
636 lambda v: v not in [None, ''],
637 request.POST.get('reviewers_ids', '').split(',')))
638 PullRequestModel().update_reviewers(pull_request_id, reviewers_ids)
641 def _update_reviewers(self, pull_request_id, review_members):
642 reviewers = [
643 (int(r['user_id']), r['reasons']) for r in review_members]
644 PullRequestModel().update_reviewers(pull_request_id, reviewers)
639 645 Session().commit()
640 646
641 647 def _reject_close(self, pull_request):
642 648 if pull_request.is_closed():
643 649 raise HTTPForbidden()
644 650
645 651 PullRequestModel().close_pull_request_with_comment(
646 652 pull_request, c.rhodecode_user, c.rhodecode_db_repo)
647 653 Session().commit()
648 654
649 655 @LoginRequired()
650 656 @NotAnonymous()
651 657 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
652 658 'repository.admin')
653 659 @auth.CSRFRequired()
654 660 @jsonify
655 661 def delete(self, repo_name, pull_request_id):
656 662 pull_request_id = safe_int(pull_request_id)
657 663 pull_request = PullRequest.get_or_404(pull_request_id)
658 664 # only owner can delete it !
659 665 if pull_request.author.user_id == c.rhodecode_user.user_id:
660 666 PullRequestModel().delete(pull_request)
661 667 Session().commit()
662 668 h.flash(_('Successfully deleted pull request'),
663 669 category='success')
664 670 return redirect(url('my_account_pullrequests'))
665 671 raise HTTPForbidden()
666 672
667 673 @LoginRequired()
668 674 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
669 675 'repository.admin')
670 676 def show(self, repo_name, pull_request_id):
671 677 pull_request_id = safe_int(pull_request_id)
672 678 c.pull_request = PullRequest.get_or_404(pull_request_id)
673 679
674 680 c.template_context['pull_request_data']['pull_request_id'] = \
675 681 pull_request_id
676 682
677 683 # pull_requests repo_name we opened it against
678 684 # ie. target_repo must match
679 685 if repo_name != c.pull_request.target_repo.repo_name:
680 686 raise HTTPNotFound
681 687
682 688 c.allowed_to_change_status = PullRequestModel(). \
683 689 check_user_change_status(c.pull_request, c.rhodecode_user)
684 690 c.allowed_to_update = PullRequestModel().check_user_update(
685 691 c.pull_request, c.rhodecode_user) and not c.pull_request.is_closed()
686 692 c.allowed_to_merge = PullRequestModel().check_user_merge(
687 693 c.pull_request, c.rhodecode_user) and not c.pull_request.is_closed()
688 694
689 695 cc_model = ChangesetCommentsModel()
690 696
691 697 c.pull_request_reviewers = c.pull_request.reviewers_statuses()
692 698
693 699 c.pull_request_review_status = c.pull_request.calculated_review_status()
694 700 c.pr_merge_status, c.pr_merge_msg = PullRequestModel().merge_status(
695 701 c.pull_request)
696 702 c.approval_msg = None
697 703 if c.pull_request_review_status != ChangesetStatus.STATUS_APPROVED:
698 704 c.approval_msg = _('Reviewer approval is pending.')
699 705 c.pr_merge_status = False
700 706 # load compare data into template context
701 707 enable_comments = not c.pull_request.is_closed()
702 708 self._load_compare_data(c.pull_request, enable_comments=enable_comments)
703 709
704 710 # this is a hack to properly display links, when creating PR, the
705 711 # compare view and others uses different notation, and
706 712 # compare_commits.html renders links based on the target_repo.
707 713 # We need to swap that here to generate it properly on the html side
708 714 c.target_repo = c.source_repo
709 715
710 716 # inline comments
711 717 c.inline_cnt = 0
712 718 c.inline_comments = cc_model.get_inline_comments(
713 719 c.rhodecode_db_repo.repo_id,
714 720 pull_request=pull_request_id).items()
715 721 # count inline comments
716 722 for __, lines in c.inline_comments:
717 723 for comments in lines.values():
718 724 c.inline_cnt += len(comments)
719 725
720 726 # outdated comments
721 727 c.outdated_cnt = 0
722 728 if ChangesetCommentsModel.use_outdated_comments(c.pull_request):
723 729 c.outdated_comments = cc_model.get_outdated_comments(
724 730 c.rhodecode_db_repo.repo_id,
725 731 pull_request=c.pull_request)
726 732 # Count outdated comments and check for deleted files
727 733 for file_name, lines in c.outdated_comments.iteritems():
728 734 for comments in lines.values():
729 735 c.outdated_cnt += len(comments)
730 736 if file_name not in c.included_files:
731 737 c.deleted_files.append(file_name)
732 738 else:
733 739 c.outdated_comments = {}
734 740
735 741 # comments
736 742 c.comments = cc_model.get_comments(c.rhodecode_db_repo.repo_id,
737 743 pull_request=pull_request_id)
738 744
739 745 if c.allowed_to_update:
740 746 force_close = ('forced_closed', _('Close Pull Request'))
741 747 statuses = ChangesetStatus.STATUSES + [force_close]
742 748 else:
743 749 statuses = ChangesetStatus.STATUSES
744 750 c.commit_statuses = statuses
745 751
746 752 c.ancestor = None # TODO: add ancestor here
747 753
748 754 return render('/pullrequests/pullrequest_show.html')
749 755
750 756 @LoginRequired()
751 757 @NotAnonymous()
752 758 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
753 759 'repository.admin')
754 760 @auth.CSRFRequired()
755 761 @jsonify
756 762 def comment(self, repo_name, pull_request_id):
757 763 pull_request_id = safe_int(pull_request_id)
758 764 pull_request = PullRequest.get_or_404(pull_request_id)
759 765 if pull_request.is_closed():
760 766 raise HTTPForbidden()
761 767
762 768 # TODO: johbo: Re-think this bit, "approved_closed" does not exist
763 769 # as a changeset status, still we want to send it in one value.
764 770 status = request.POST.get('changeset_status', None)
765 771 text = request.POST.get('text')
766 772 if status and '_closed' in status:
767 773 close_pr = True
768 774 status = status.replace('_closed', '')
769 775 else:
770 776 close_pr = False
771 777
772 778 forced = (status == 'forced')
773 779 if forced:
774 780 status = 'rejected'
775 781
776 782 allowed_to_change_status = PullRequestModel().check_user_change_status(
777 783 pull_request, c.rhodecode_user)
778 784
779 785 if status and allowed_to_change_status:
780 786 message = (_('Status change %(transition_icon)s %(status)s')
781 787 % {'transition_icon': '>',
782 788 'status': ChangesetStatus.get_status_lbl(status)})
783 789 if close_pr:
784 790 message = _('Closing with') + ' ' + message
785 791 text = text or message
786 792 comm = ChangesetCommentsModel().create(
787 793 text=text,
788 794 repo=c.rhodecode_db_repo.repo_id,
789 795 user=c.rhodecode_user.user_id,
790 796 pull_request=pull_request_id,
791 797 f_path=request.POST.get('f_path'),
792 798 line_no=request.POST.get('line'),
793 799 status_change=(ChangesetStatus.get_status_lbl(status)
794 800 if status and allowed_to_change_status else None),
795 801 status_change_type=(status
796 802 if status and allowed_to_change_status else None),
797 803 closing_pr=close_pr
798 804 )
799 805
800 806
801 807
802 808 if allowed_to_change_status:
803 809 old_calculated_status = pull_request.calculated_review_status()
804 810 # get status if set !
805 811 if status:
806 812 ChangesetStatusModel().set_status(
807 813 c.rhodecode_db_repo.repo_id,
808 814 status,
809 815 c.rhodecode_user.user_id,
810 816 comm,
811 817 pull_request=pull_request_id
812 818 )
813 819
814 820 Session().flush()
815 821 events.trigger(events.PullRequestCommentEvent(pull_request, comm))
816 822 # we now calculate the status of pull request, and based on that
817 823 # calculation we set the commits status
818 824 calculated_status = pull_request.calculated_review_status()
819 825 if old_calculated_status != calculated_status:
820 826 PullRequestModel()._trigger_pull_request_hook(
821 827 pull_request, c.rhodecode_user, 'review_status_change')
822 828
823 829 calculated_status_lbl = ChangesetStatus.get_status_lbl(
824 830 calculated_status)
825 831
826 832 if close_pr:
827 833 status_completed = (
828 834 calculated_status in [ChangesetStatus.STATUS_APPROVED,
829 835 ChangesetStatus.STATUS_REJECTED])
830 836 if forced or status_completed:
831 837 PullRequestModel().close_pull_request(
832 838 pull_request_id, c.rhodecode_user)
833 839 else:
834 840 h.flash(_('Closing pull request on other statuses than '
835 841 'rejected or approved is forbidden. '
836 842 'Calculated status from all reviewers '
837 843 'is currently: %s') % calculated_status_lbl,
838 844 category='warning')
839 845
840 846 Session().commit()
841 847
842 848 if not request.is_xhr:
843 849 return redirect(h.url('pullrequest_show', repo_name=repo_name,
844 850 pull_request_id=pull_request_id))
845 851
846 852 data = {
847 853 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
848 854 }
849 855 if comm:
850 856 c.co = comm
851 857 data.update(comm.get_dict())
852 858 data.update({'rendered_text':
853 859 render('changeset/changeset_comment_block.html')})
854 860
855 861 return data
856 862
857 863 @LoginRequired()
858 864 @NotAnonymous()
859 865 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
860 866 'repository.admin')
861 867 @auth.CSRFRequired()
862 868 @jsonify
863 869 def delete_comment(self, repo_name, comment_id):
864 870 return self._delete_comment(comment_id)
865 871
866 872 def _delete_comment(self, comment_id):
867 873 comment_id = safe_int(comment_id)
868 874 co = ChangesetComment.get_or_404(comment_id)
869 875 if co.pull_request.is_closed():
870 876 # don't allow deleting comments on closed pull request
871 877 raise HTTPForbidden()
872 878
873 879 is_owner = co.author.user_id == c.rhodecode_user.user_id
874 880 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
875 881 if h.HasPermissionAny('hg.admin')() or is_repo_admin or is_owner:
876 882 old_calculated_status = co.pull_request.calculated_review_status()
877 883 ChangesetCommentsModel().delete(comment=co)
878 884 Session().commit()
879 885 calculated_status = co.pull_request.calculated_review_status()
880 886 if old_calculated_status != calculated_status:
881 887 PullRequestModel()._trigger_pull_request_hook(
882 888 co.pull_request, c.rhodecode_user, 'review_status_change')
883 889 return True
884 890 else:
885 891 raise HTTPForbidden()
@@ -1,3642 +1,3658 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Database Models for RhodeCode Enterprise
23 23 """
24 24
25 25 import re
26 26 import os
27 27 import sys
28 28 import time
29 29 import hashlib
30 30 import logging
31 31 import datetime
32 32 import warnings
33 33 import ipaddress
34 34 import functools
35 35 import traceback
36 36 import collections
37 37
38 38
39 39 from sqlalchemy import *
40 40 from sqlalchemy.exc import IntegrityError
41 41 from sqlalchemy.ext.declarative import declared_attr
42 42 from sqlalchemy.ext.hybrid import hybrid_property
43 43 from sqlalchemy.orm import (
44 44 relationship, joinedload, class_mapper, validates, aliased)
45 45 from sqlalchemy.sql.expression import true
46 46 from beaker.cache import cache_region, region_invalidate
47 47 from webob.exc import HTTPNotFound
48 48 from zope.cachedescriptors.property import Lazy as LazyProperty
49 49
50 50 from pylons import url
51 51 from pylons.i18n.translation import lazy_ugettext as _
52 52
53 53 from rhodecode.lib.vcs import get_backend, get_vcs_instance
54 54 from rhodecode.lib.vcs.utils.helpers import get_scm
55 55 from rhodecode.lib.vcs.exceptions import VCSError
56 56 from rhodecode.lib.vcs.backends.base import (
57 57 EmptyCommit, Reference, MergeFailureReason)
58 58 from rhodecode.lib.utils2 import (
59 59 str2bool, safe_str, get_commit_safe, safe_unicode, remove_prefix, md5_safe,
60 60 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
61 61 glob2re)
62 from rhodecode.lib.jsonalchemy import MutationObj, JsonType, JSONDict
62 from rhodecode.lib.jsonalchemy import MutationObj, MutationList, JsonType, JSONDict
63 63 from rhodecode.lib.ext_json import json
64 64 from rhodecode.lib.caching_query import FromCache
65 65 from rhodecode.lib.encrypt import AESCipher
66 66
67 67 from rhodecode.model.meta import Base, Session
68 68
69 69 URL_SEP = '/'
70 70 log = logging.getLogger(__name__)
71 71
72 72 # =============================================================================
73 73 # BASE CLASSES
74 74 # =============================================================================
75 75
76 76 # this is propagated from .ini file rhodecode.encrypted_values.secret or
77 77 # beaker.session.secret if first is not set.
78 78 # and initialized at environment.py
79 79 ENCRYPTION_KEY = None
80 80
81 81 # used to sort permissions by types, '#' used here is not allowed to be in
82 82 # usernames, and it's very early in sorted string.printable table.
83 83 PERMISSION_TYPE_SORT = {
84 84 'admin': '####',
85 85 'write': '###',
86 86 'read': '##',
87 87 'none': '#',
88 88 }
89 89
90 90
91 91 def display_sort(obj):
92 92 """
93 93 Sort function used to sort permissions in .permissions() function of
94 94 Repository, RepoGroup, UserGroup. Also it put the default user in front
95 95 of all other resources
96 96 """
97 97
98 98 if obj.username == User.DEFAULT_USER:
99 99 return '#####'
100 100 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
101 101 return prefix + obj.username
102 102
103 103
104 104 def _hash_key(k):
105 105 return md5_safe(k)
106 106
107 107
108 108 class EncryptedTextValue(TypeDecorator):
109 109 """
110 110 Special column for encrypted long text data, use like::
111 111
112 112 value = Column("encrypted_value", EncryptedValue(), nullable=False)
113 113
114 114 This column is intelligent so if value is in unencrypted form it return
115 115 unencrypted form, but on save it always encrypts
116 116 """
117 117 impl = Text
118 118
119 119 def process_bind_param(self, value, dialect):
120 120 if not value:
121 121 return value
122 122 if value.startswith('enc$aes$') or value.startswith('enc$aes_hmac$'):
123 123 # protect against double encrypting if someone manually starts
124 124 # doing
125 125 raise ValueError('value needs to be in unencrypted format, ie. '
126 126 'not starting with enc$aes')
127 127 return 'enc$aes_hmac$%s' % AESCipher(
128 128 ENCRYPTION_KEY, hmac=True).encrypt(value)
129 129
130 130 def process_result_value(self, value, dialect):
131 131 import rhodecode
132 132
133 133 if not value:
134 134 return value
135 135
136 136 parts = value.split('$', 3)
137 137 if not len(parts) == 3:
138 138 # probably not encrypted values
139 139 return value
140 140 else:
141 141 if parts[0] != 'enc':
142 142 # parts ok but without our header ?
143 143 return value
144 144 enc_strict_mode = str2bool(rhodecode.CONFIG.get(
145 145 'rhodecode.encrypted_values.strict') or True)
146 146 # at that stage we know it's our encryption
147 147 if parts[1] == 'aes':
148 148 decrypted_data = AESCipher(ENCRYPTION_KEY).decrypt(parts[2])
149 149 elif parts[1] == 'aes_hmac':
150 150 decrypted_data = AESCipher(
151 151 ENCRYPTION_KEY, hmac=True,
152 152 strict_verification=enc_strict_mode).decrypt(parts[2])
153 153 else:
154 154 raise ValueError(
155 155 'Encryption type part is wrong, must be `aes` '
156 156 'or `aes_hmac`, got `%s` instead' % (parts[1]))
157 157 return decrypted_data
158 158
159 159
160 160 class BaseModel(object):
161 161 """
162 162 Base Model for all classes
163 163 """
164 164
165 165 @classmethod
166 166 def _get_keys(cls):
167 167 """return column names for this model """
168 168 return class_mapper(cls).c.keys()
169 169
170 170 def get_dict(self):
171 171 """
172 172 return dict with keys and values corresponding
173 173 to this model data """
174 174
175 175 d = {}
176 176 for k in self._get_keys():
177 177 d[k] = getattr(self, k)
178 178
179 179 # also use __json__() if present to get additional fields
180 180 _json_attr = getattr(self, '__json__', None)
181 181 if _json_attr:
182 182 # update with attributes from __json__
183 183 if callable(_json_attr):
184 184 _json_attr = _json_attr()
185 185 for k, val in _json_attr.iteritems():
186 186 d[k] = val
187 187 return d
188 188
189 189 def get_appstruct(self):
190 190 """return list with keys and values tuples corresponding
191 191 to this model data """
192 192
193 193 l = []
194 194 for k in self._get_keys():
195 195 l.append((k, getattr(self, k),))
196 196 return l
197 197
198 198 def populate_obj(self, populate_dict):
199 199 """populate model with data from given populate_dict"""
200 200
201 201 for k in self._get_keys():
202 202 if k in populate_dict:
203 203 setattr(self, k, populate_dict[k])
204 204
205 205 @classmethod
206 206 def query(cls):
207 207 return Session().query(cls)
208 208
209 209 @classmethod
210 210 def get(cls, id_):
211 211 if id_:
212 212 return cls.query().get(id_)
213 213
214 214 @classmethod
215 215 def get_or_404(cls, id_):
216 216 try:
217 217 id_ = int(id_)
218 218 except (TypeError, ValueError):
219 219 raise HTTPNotFound
220 220
221 221 res = cls.query().get(id_)
222 222 if not res:
223 223 raise HTTPNotFound
224 224 return res
225 225
226 226 @classmethod
227 227 def getAll(cls):
228 228 # deprecated and left for backward compatibility
229 229 return cls.get_all()
230 230
231 231 @classmethod
232 232 def get_all(cls):
233 233 return cls.query().all()
234 234
235 235 @classmethod
236 236 def delete(cls, id_):
237 237 obj = cls.query().get(id_)
238 238 Session().delete(obj)
239 239
240 240 @classmethod
241 241 def identity_cache(cls, session, attr_name, value):
242 242 exist_in_session = []
243 243 for (item_cls, pkey), instance in session.identity_map.items():
244 244 if cls == item_cls and getattr(instance, attr_name) == value:
245 245 exist_in_session.append(instance)
246 246 if exist_in_session:
247 247 if len(exist_in_session) == 1:
248 248 return exist_in_session[0]
249 249 log.exception(
250 250 'multiple objects with attr %s and '
251 251 'value %s found with same name: %r',
252 252 attr_name, value, exist_in_session)
253 253
254 254 def __repr__(self):
255 255 if hasattr(self, '__unicode__'):
256 256 # python repr needs to return str
257 257 try:
258 258 return safe_str(self.__unicode__())
259 259 except UnicodeDecodeError:
260 260 pass
261 261 return '<DB:%s>' % (self.__class__.__name__)
262 262
263 263
264 264 class RhodeCodeSetting(Base, BaseModel):
265 265 __tablename__ = 'rhodecode_settings'
266 266 __table_args__ = (
267 267 UniqueConstraint('app_settings_name'),
268 268 {'extend_existing': True, 'mysql_engine': 'InnoDB',
269 269 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
270 270 )
271 271
272 272 SETTINGS_TYPES = {
273 273 'str': safe_str,
274 274 'int': safe_int,
275 275 'unicode': safe_unicode,
276 276 'bool': str2bool,
277 277 'list': functools.partial(aslist, sep=',')
278 278 }
279 279 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
280 280 GLOBAL_CONF_KEY = 'app_settings'
281 281
282 282 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
283 283 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
284 284 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
285 285 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
286 286
287 287 def __init__(self, key='', val='', type='unicode'):
288 288 self.app_settings_name = key
289 289 self.app_settings_type = type
290 290 self.app_settings_value = val
291 291
292 292 @validates('_app_settings_value')
293 293 def validate_settings_value(self, key, val):
294 294 assert type(val) == unicode
295 295 return val
296 296
297 297 @hybrid_property
298 298 def app_settings_value(self):
299 299 v = self._app_settings_value
300 300 _type = self.app_settings_type
301 301 if _type:
302 302 _type = self.app_settings_type.split('.')[0]
303 303 # decode the encrypted value
304 304 if 'encrypted' in self.app_settings_type:
305 305 cipher = EncryptedTextValue()
306 306 v = safe_unicode(cipher.process_result_value(v, None))
307 307
308 308 converter = self.SETTINGS_TYPES.get(_type) or \
309 309 self.SETTINGS_TYPES['unicode']
310 310 return converter(v)
311 311
312 312 @app_settings_value.setter
313 313 def app_settings_value(self, val):
314 314 """
315 315 Setter that will always make sure we use unicode in app_settings_value
316 316
317 317 :param val:
318 318 """
319 319 val = safe_unicode(val)
320 320 # encode the encrypted value
321 321 if 'encrypted' in self.app_settings_type:
322 322 cipher = EncryptedTextValue()
323 323 val = safe_unicode(cipher.process_bind_param(val, None))
324 324 self._app_settings_value = val
325 325
326 326 @hybrid_property
327 327 def app_settings_type(self):
328 328 return self._app_settings_type
329 329
330 330 @app_settings_type.setter
331 331 def app_settings_type(self, val):
332 332 if val.split('.')[0] not in self.SETTINGS_TYPES:
333 333 raise Exception('type must be one of %s got %s'
334 334 % (self.SETTINGS_TYPES.keys(), val))
335 335 self._app_settings_type = val
336 336
337 337 def __unicode__(self):
338 338 return u"<%s('%s:%s[%s]')>" % (
339 339 self.__class__.__name__,
340 340 self.app_settings_name, self.app_settings_value,
341 341 self.app_settings_type
342 342 )
343 343
344 344
345 345 class RhodeCodeUi(Base, BaseModel):
346 346 __tablename__ = 'rhodecode_ui'
347 347 __table_args__ = (
348 348 UniqueConstraint('ui_key'),
349 349 {'extend_existing': True, 'mysql_engine': 'InnoDB',
350 350 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
351 351 )
352 352
353 353 HOOK_REPO_SIZE = 'changegroup.repo_size'
354 354 # HG
355 355 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
356 356 HOOK_PULL = 'outgoing.pull_logger'
357 357 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
358 358 HOOK_PUSH = 'changegroup.push_logger'
359 359
360 360 # TODO: johbo: Unify way how hooks are configured for git and hg,
361 361 # git part is currently hardcoded.
362 362
363 363 # SVN PATTERNS
364 364 SVN_BRANCH_ID = 'vcs_svn_branch'
365 365 SVN_TAG_ID = 'vcs_svn_tag'
366 366
367 367 ui_id = Column(
368 368 "ui_id", Integer(), nullable=False, unique=True, default=None,
369 369 primary_key=True)
370 370 ui_section = Column(
371 371 "ui_section", String(255), nullable=True, unique=None, default=None)
372 372 ui_key = Column(
373 373 "ui_key", String(255), nullable=True, unique=None, default=None)
374 374 ui_value = Column(
375 375 "ui_value", String(255), nullable=True, unique=None, default=None)
376 376 ui_active = Column(
377 377 "ui_active", Boolean(), nullable=True, unique=None, default=True)
378 378
379 379 def __repr__(self):
380 380 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
381 381 self.ui_key, self.ui_value)
382 382
383 383
384 384 class RepoRhodeCodeSetting(Base, BaseModel):
385 385 __tablename__ = 'repo_rhodecode_settings'
386 386 __table_args__ = (
387 387 UniqueConstraint(
388 388 'app_settings_name', 'repository_id',
389 389 name='uq_repo_rhodecode_setting_name_repo_id'),
390 390 {'extend_existing': True, 'mysql_engine': 'InnoDB',
391 391 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
392 392 )
393 393
394 394 repository_id = Column(
395 395 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
396 396 nullable=False)
397 397 app_settings_id = Column(
398 398 "app_settings_id", Integer(), nullable=False, unique=True,
399 399 default=None, primary_key=True)
400 400 app_settings_name = Column(
401 401 "app_settings_name", String(255), nullable=True, unique=None,
402 402 default=None)
403 403 _app_settings_value = Column(
404 404 "app_settings_value", String(4096), nullable=True, unique=None,
405 405 default=None)
406 406 _app_settings_type = Column(
407 407 "app_settings_type", String(255), nullable=True, unique=None,
408 408 default=None)
409 409
410 410 repository = relationship('Repository')
411 411
412 412 def __init__(self, repository_id, key='', val='', type='unicode'):
413 413 self.repository_id = repository_id
414 414 self.app_settings_name = key
415 415 self.app_settings_type = type
416 416 self.app_settings_value = val
417 417
418 418 @validates('_app_settings_value')
419 419 def validate_settings_value(self, key, val):
420 420 assert type(val) == unicode
421 421 return val
422 422
423 423 @hybrid_property
424 424 def app_settings_value(self):
425 425 v = self._app_settings_value
426 426 type_ = self.app_settings_type
427 427 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
428 428 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
429 429 return converter(v)
430 430
431 431 @app_settings_value.setter
432 432 def app_settings_value(self, val):
433 433 """
434 434 Setter that will always make sure we use unicode in app_settings_value
435 435
436 436 :param val:
437 437 """
438 438 self._app_settings_value = safe_unicode(val)
439 439
440 440 @hybrid_property
441 441 def app_settings_type(self):
442 442 return self._app_settings_type
443 443
444 444 @app_settings_type.setter
445 445 def app_settings_type(self, val):
446 446 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
447 447 if val not in SETTINGS_TYPES:
448 448 raise Exception('type must be one of %s got %s'
449 449 % (SETTINGS_TYPES.keys(), val))
450 450 self._app_settings_type = val
451 451
452 452 def __unicode__(self):
453 453 return u"<%s('%s:%s:%s[%s]')>" % (
454 454 self.__class__.__name__, self.repository.repo_name,
455 455 self.app_settings_name, self.app_settings_value,
456 456 self.app_settings_type
457 457 )
458 458
459 459
460 460 class RepoRhodeCodeUi(Base, BaseModel):
461 461 __tablename__ = 'repo_rhodecode_ui'
462 462 __table_args__ = (
463 463 UniqueConstraint(
464 464 'repository_id', 'ui_section', 'ui_key',
465 465 name='uq_repo_rhodecode_ui_repository_id_section_key'),
466 466 {'extend_existing': True, 'mysql_engine': 'InnoDB',
467 467 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
468 468 )
469 469
470 470 repository_id = Column(
471 471 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
472 472 nullable=False)
473 473 ui_id = Column(
474 474 "ui_id", Integer(), nullable=False, unique=True, default=None,
475 475 primary_key=True)
476 476 ui_section = Column(
477 477 "ui_section", String(255), nullable=True, unique=None, default=None)
478 478 ui_key = Column(
479 479 "ui_key", String(255), nullable=True, unique=None, default=None)
480 480 ui_value = Column(
481 481 "ui_value", String(255), nullable=True, unique=None, default=None)
482 482 ui_active = Column(
483 483 "ui_active", Boolean(), nullable=True, unique=None, default=True)
484 484
485 485 repository = relationship('Repository')
486 486
487 487 def __repr__(self):
488 488 return '<%s[%s:%s]%s=>%s]>' % (
489 489 self.__class__.__name__, self.repository.repo_name,
490 490 self.ui_section, self.ui_key, self.ui_value)
491 491
492 492
493 493 class User(Base, BaseModel):
494 494 __tablename__ = 'users'
495 495 __table_args__ = (
496 496 UniqueConstraint('username'), UniqueConstraint('email'),
497 497 Index('u_username_idx', 'username'),
498 498 Index('u_email_idx', 'email'),
499 499 {'extend_existing': True, 'mysql_engine': 'InnoDB',
500 500 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
501 501 )
502 502 DEFAULT_USER = 'default'
503 503 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
504 504 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
505 505
506 506 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
507 507 username = Column("username", String(255), nullable=True, unique=None, default=None)
508 508 password = Column("password", String(255), nullable=True, unique=None, default=None)
509 509 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
510 510 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
511 511 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
512 512 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
513 513 _email = Column("email", String(255), nullable=True, unique=None, default=None)
514 514 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
515 515 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
516 516 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
517 517 api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
518 518 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
519 519 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
520 520 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
521 521
522 522 user_log = relationship('UserLog')
523 523 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
524 524
525 525 repositories = relationship('Repository')
526 526 repository_groups = relationship('RepoGroup')
527 527 user_groups = relationship('UserGroup')
528 528
529 529 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
530 530 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
531 531
532 532 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
533 533 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
534 534 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all')
535 535
536 536 group_member = relationship('UserGroupMember', cascade='all')
537 537
538 538 notifications = relationship('UserNotification', cascade='all')
539 539 # notifications assigned to this user
540 540 user_created_notifications = relationship('Notification', cascade='all')
541 541 # comments created by this user
542 542 user_comments = relationship('ChangesetComment', cascade='all')
543 543 # user profile extra info
544 544 user_emails = relationship('UserEmailMap', cascade='all')
545 545 user_ip_map = relationship('UserIpMap', cascade='all')
546 546 user_auth_tokens = relationship('UserApiKeys', cascade='all')
547 547 # gists
548 548 user_gists = relationship('Gist', cascade='all')
549 549 # user pull requests
550 550 user_pull_requests = relationship('PullRequest', cascade='all')
551 551 # external identities
552 552 extenal_identities = relationship(
553 553 'ExternalIdentity',
554 554 primaryjoin="User.user_id==ExternalIdentity.local_user_id",
555 555 cascade='all')
556 556
557 557 def __unicode__(self):
558 558 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
559 559 self.user_id, self.username)
560 560
561 561 @hybrid_property
562 562 def email(self):
563 563 return self._email
564 564
565 565 @email.setter
566 566 def email(self, val):
567 567 self._email = val.lower() if val else None
568 568
569 569 @property
570 570 def firstname(self):
571 571 # alias for future
572 572 return self.name
573 573
574 574 @property
575 575 def emails(self):
576 576 other = UserEmailMap.query().filter(UserEmailMap.user==self).all()
577 577 return [self.email] + [x.email for x in other]
578 578
579 579 @property
580 580 def auth_tokens(self):
581 581 return [self.api_key] + [x.api_key for x in self.extra_auth_tokens]
582 582
583 583 @property
584 584 def extra_auth_tokens(self):
585 585 return UserApiKeys.query().filter(UserApiKeys.user == self).all()
586 586
587 587 @property
588 588 def feed_token(self):
589 589 feed_tokens = UserApiKeys.query()\
590 590 .filter(UserApiKeys.user == self)\
591 591 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)\
592 592 .all()
593 593 if feed_tokens:
594 594 return feed_tokens[0].api_key
595 595 else:
596 596 # use the main token so we don't end up with nothing...
597 597 return self.api_key
598 598
599 599 @classmethod
600 600 def extra_valid_auth_tokens(cls, user, role=None):
601 601 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
602 602 .filter(or_(UserApiKeys.expires == -1,
603 603 UserApiKeys.expires >= time.time()))
604 604 if role:
605 605 tokens = tokens.filter(or_(UserApiKeys.role == role,
606 606 UserApiKeys.role == UserApiKeys.ROLE_ALL))
607 607 return tokens.all()
608 608
609 609 @property
610 610 def ip_addresses(self):
611 611 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
612 612 return [x.ip_addr for x in ret]
613 613
614 614 @property
615 615 def username_and_name(self):
616 616 return '%s (%s %s)' % (self.username, self.firstname, self.lastname)
617 617
618 618 @property
619 619 def username_or_name_or_email(self):
620 620 full_name = self.full_name if self.full_name is not ' ' else None
621 621 return self.username or full_name or self.email
622 622
623 623 @property
624 624 def full_name(self):
625 625 return '%s %s' % (self.firstname, self.lastname)
626 626
627 627 @property
628 628 def full_name_or_username(self):
629 629 return ('%s %s' % (self.firstname, self.lastname)
630 630 if (self.firstname and self.lastname) else self.username)
631 631
632 632 @property
633 633 def full_contact(self):
634 634 return '%s %s <%s>' % (self.firstname, self.lastname, self.email)
635 635
636 636 @property
637 637 def short_contact(self):
638 638 return '%s %s' % (self.firstname, self.lastname)
639 639
640 640 @property
641 641 def is_admin(self):
642 642 return self.admin
643 643
644 644 @property
645 645 def AuthUser(self):
646 646 """
647 647 Returns instance of AuthUser for this user
648 648 """
649 649 from rhodecode.lib.auth import AuthUser
650 650 return AuthUser(user_id=self.user_id, api_key=self.api_key,
651 651 username=self.username)
652 652
653 653 @hybrid_property
654 654 def user_data(self):
655 655 if not self._user_data:
656 656 return {}
657 657
658 658 try:
659 659 return json.loads(self._user_data)
660 660 except TypeError:
661 661 return {}
662 662
663 663 @user_data.setter
664 664 def user_data(self, val):
665 665 if not isinstance(val, dict):
666 666 raise Exception('user_data must be dict, got %s' % type(val))
667 667 try:
668 668 self._user_data = json.dumps(val)
669 669 except Exception:
670 670 log.error(traceback.format_exc())
671 671
672 672 @classmethod
673 673 def get_by_username(cls, username, case_insensitive=False,
674 674 cache=False, identity_cache=False):
675 675 session = Session()
676 676
677 677 if case_insensitive:
678 678 q = cls.query().filter(
679 679 func.lower(cls.username) == func.lower(username))
680 680 else:
681 681 q = cls.query().filter(cls.username == username)
682 682
683 683 if cache:
684 684 if identity_cache:
685 685 val = cls.identity_cache(session, 'username', username)
686 686 if val:
687 687 return val
688 688 else:
689 689 q = q.options(
690 690 FromCache("sql_cache_short",
691 691 "get_user_by_name_%s" % _hash_key(username)))
692 692
693 693 return q.scalar()
694 694
695 695 @classmethod
696 696 def get_by_auth_token(cls, auth_token, cache=False, fallback=True):
697 697 q = cls.query().filter(cls.api_key == auth_token)
698 698
699 699 if cache:
700 700 q = q.options(FromCache("sql_cache_short",
701 701 "get_auth_token_%s" % auth_token))
702 702 res = q.scalar()
703 703
704 704 if fallback and not res:
705 705 #fallback to additional keys
706 706 _res = UserApiKeys.query()\
707 707 .filter(UserApiKeys.api_key == auth_token)\
708 708 .filter(or_(UserApiKeys.expires == -1,
709 709 UserApiKeys.expires >= time.time()))\
710 710 .first()
711 711 if _res:
712 712 res = _res.user
713 713 return res
714 714
715 715 @classmethod
716 716 def get_by_email(cls, email, case_insensitive=False, cache=False):
717 717
718 718 if case_insensitive:
719 719 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
720 720
721 721 else:
722 722 q = cls.query().filter(cls.email == email)
723 723
724 724 if cache:
725 725 q = q.options(FromCache("sql_cache_short",
726 726 "get_email_key_%s" % _hash_key(email)))
727 727
728 728 ret = q.scalar()
729 729 if ret is None:
730 730 q = UserEmailMap.query()
731 731 # try fetching in alternate email map
732 732 if case_insensitive:
733 733 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
734 734 else:
735 735 q = q.filter(UserEmailMap.email == email)
736 736 q = q.options(joinedload(UserEmailMap.user))
737 737 if cache:
738 738 q = q.options(FromCache("sql_cache_short",
739 739 "get_email_map_key_%s" % email))
740 740 ret = getattr(q.scalar(), 'user', None)
741 741
742 742 return ret
743 743
744 744 @classmethod
745 745 def get_from_cs_author(cls, author):
746 746 """
747 747 Tries to get User objects out of commit author string
748 748
749 749 :param author:
750 750 """
751 751 from rhodecode.lib.helpers import email, author_name
752 752 # Valid email in the attribute passed, see if they're in the system
753 753 _email = email(author)
754 754 if _email:
755 755 user = cls.get_by_email(_email, case_insensitive=True)
756 756 if user:
757 757 return user
758 758 # Maybe we can match by username?
759 759 _author = author_name(author)
760 760 user = cls.get_by_username(_author, case_insensitive=True)
761 761 if user:
762 762 return user
763 763
764 764 def update_userdata(self, **kwargs):
765 765 usr = self
766 766 old = usr.user_data
767 767 old.update(**kwargs)
768 768 usr.user_data = old
769 769 Session().add(usr)
770 770 log.debug('updated userdata with ', kwargs)
771 771
772 772 def update_lastlogin(self):
773 773 """Update user lastlogin"""
774 774 self.last_login = datetime.datetime.now()
775 775 Session().add(self)
776 776 log.debug('updated user %s lastlogin', self.username)
777 777
778 778 def update_lastactivity(self):
779 779 """Update user lastactivity"""
780 780 usr = self
781 781 old = usr.user_data
782 782 old.update({'last_activity': time.time()})
783 783 usr.user_data = old
784 784 Session().add(usr)
785 785 log.debug('updated user %s lastactivity', usr.username)
786 786
787 787 def update_password(self, new_password, change_api_key=False):
788 788 from rhodecode.lib.auth import get_crypt_password,generate_auth_token
789 789
790 790 self.password = get_crypt_password(new_password)
791 791 if change_api_key:
792 792 self.api_key = generate_auth_token(self.username)
793 793 Session().add(self)
794 794
795 795 @classmethod
796 796 def get_first_super_admin(cls):
797 797 user = User.query().filter(User.admin == true()).first()
798 798 if user is None:
799 799 raise Exception('FATAL: Missing administrative account!')
800 800 return user
801 801
802 802 @classmethod
803 803 def get_all_super_admins(cls):
804 804 """
805 805 Returns all admin accounts sorted by username
806 806 """
807 807 return User.query().filter(User.admin == true())\
808 808 .order_by(User.username.asc()).all()
809 809
810 810 @classmethod
811 811 def get_default_user(cls, cache=False):
812 812 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
813 813 if user is None:
814 814 raise Exception('FATAL: Missing default account!')
815 815 return user
816 816
817 817 def _get_default_perms(self, user, suffix=''):
818 818 from rhodecode.model.permission import PermissionModel
819 819 return PermissionModel().get_default_perms(user.user_perms, suffix)
820 820
821 821 def get_default_perms(self, suffix=''):
822 822 return self._get_default_perms(self, suffix)
823 823
824 824 def get_api_data(self, include_secrets=False, details='full'):
825 825 """
826 826 Common function for generating user related data for API
827 827
828 828 :param include_secrets: By default secrets in the API data will be replaced
829 829 by a placeholder value to prevent exposing this data by accident. In case
830 830 this data shall be exposed, set this flag to ``True``.
831 831
832 832 :param details: details can be 'basic|full' basic gives only a subset of
833 833 the available user information that includes user_id, name and emails.
834 834 """
835 835 user = self
836 836 user_data = self.user_data
837 837 data = {
838 838 'user_id': user.user_id,
839 839 'username': user.username,
840 840 'firstname': user.name,
841 841 'lastname': user.lastname,
842 842 'email': user.email,
843 843 'emails': user.emails,
844 844 }
845 845 if details == 'basic':
846 846 return data
847 847
848 848 api_key_length = 40
849 849 api_key_replacement = '*' * api_key_length
850 850
851 851 extras = {
852 852 'api_key': api_key_replacement,
853 853 'api_keys': [api_key_replacement],
854 854 'active': user.active,
855 855 'admin': user.admin,
856 856 'extern_type': user.extern_type,
857 857 'extern_name': user.extern_name,
858 858 'last_login': user.last_login,
859 859 'ip_addresses': user.ip_addresses,
860 860 'language': user_data.get('language')
861 861 }
862 862 data.update(extras)
863 863
864 864 if include_secrets:
865 865 data['api_key'] = user.api_key
866 866 data['api_keys'] = user.auth_tokens
867 867 return data
868 868
869 869 def __json__(self):
870 870 data = {
871 871 'full_name': self.full_name,
872 872 'full_name_or_username': self.full_name_or_username,
873 873 'short_contact': self.short_contact,
874 874 'full_contact': self.full_contact,
875 875 }
876 876 data.update(self.get_api_data())
877 877 return data
878 878
879 879
880 880 class UserApiKeys(Base, BaseModel):
881 881 __tablename__ = 'user_api_keys'
882 882 __table_args__ = (
883 883 Index('uak_api_key_idx', 'api_key'),
884 884 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
885 885 UniqueConstraint('api_key'),
886 886 {'extend_existing': True, 'mysql_engine': 'InnoDB',
887 887 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
888 888 )
889 889 __mapper_args__ = {}
890 890
891 891 # ApiKey role
892 892 ROLE_ALL = 'token_role_all'
893 893 ROLE_HTTP = 'token_role_http'
894 894 ROLE_VCS = 'token_role_vcs'
895 895 ROLE_API = 'token_role_api'
896 896 ROLE_FEED = 'token_role_feed'
897 897 ROLES = [ROLE_ALL, ROLE_HTTP, ROLE_VCS, ROLE_API, ROLE_FEED]
898 898
899 899 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
900 900 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
901 901 api_key = Column("api_key", String(255), nullable=False, unique=True)
902 902 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
903 903 expires = Column('expires', Float(53), nullable=False)
904 904 role = Column('role', String(255), nullable=True)
905 905 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
906 906
907 907 user = relationship('User', lazy='joined')
908 908
909 909 @classmethod
910 910 def _get_role_name(cls, role):
911 911 return {
912 912 cls.ROLE_ALL: _('all'),
913 913 cls.ROLE_HTTP: _('http/web interface'),
914 914 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
915 915 cls.ROLE_API: _('api calls'),
916 916 cls.ROLE_FEED: _('feed access'),
917 917 }.get(role, role)
918 918
919 919 @property
920 920 def expired(self):
921 921 if self.expires == -1:
922 922 return False
923 923 return time.time() > self.expires
924 924
925 925 @property
926 926 def role_humanized(self):
927 927 return self._get_role_name(self.role)
928 928
929 929
930 930 class UserEmailMap(Base, BaseModel):
931 931 __tablename__ = 'user_email_map'
932 932 __table_args__ = (
933 933 Index('uem_email_idx', 'email'),
934 934 UniqueConstraint('email'),
935 935 {'extend_existing': True, 'mysql_engine': 'InnoDB',
936 936 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
937 937 )
938 938 __mapper_args__ = {}
939 939
940 940 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
941 941 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
942 942 _email = Column("email", String(255), nullable=True, unique=False, default=None)
943 943 user = relationship('User', lazy='joined')
944 944
945 945 @validates('_email')
946 946 def validate_email(self, key, email):
947 947 # check if this email is not main one
948 948 main_email = Session().query(User).filter(User.email == email).scalar()
949 949 if main_email is not None:
950 950 raise AttributeError('email %s is present is user table' % email)
951 951 return email
952 952
953 953 @hybrid_property
954 954 def email(self):
955 955 return self._email
956 956
957 957 @email.setter
958 958 def email(self, val):
959 959 self._email = val.lower() if val else None
960 960
961 961
962 962 class UserIpMap(Base, BaseModel):
963 963 __tablename__ = 'user_ip_map'
964 964 __table_args__ = (
965 965 UniqueConstraint('user_id', 'ip_addr'),
966 966 {'extend_existing': True, 'mysql_engine': 'InnoDB',
967 967 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
968 968 )
969 969 __mapper_args__ = {}
970 970
971 971 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
972 972 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
973 973 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
974 974 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
975 975 description = Column("description", String(10000), nullable=True, unique=None, default=None)
976 976 user = relationship('User', lazy='joined')
977 977
978 978 @classmethod
979 979 def _get_ip_range(cls, ip_addr):
980 980 net = ipaddress.ip_network(ip_addr, strict=False)
981 981 return [str(net.network_address), str(net.broadcast_address)]
982 982
983 983 def __json__(self):
984 984 return {
985 985 'ip_addr': self.ip_addr,
986 986 'ip_range': self._get_ip_range(self.ip_addr),
987 987 }
988 988
989 989 def __unicode__(self):
990 990 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
991 991 self.user_id, self.ip_addr)
992 992
993 993 class UserLog(Base, BaseModel):
994 994 __tablename__ = 'user_logs'
995 995 __table_args__ = (
996 996 {'extend_existing': True, 'mysql_engine': 'InnoDB',
997 997 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
998 998 )
999 999 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1000 1000 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1001 1001 username = Column("username", String(255), nullable=True, unique=None, default=None)
1002 1002 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True)
1003 1003 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1004 1004 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1005 1005 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1006 1006 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1007 1007
1008 1008 def __unicode__(self):
1009 1009 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1010 1010 self.repository_name,
1011 1011 self.action)
1012 1012
1013 1013 @property
1014 1014 def action_as_day(self):
1015 1015 return datetime.date(*self.action_date.timetuple()[:3])
1016 1016
1017 1017 user = relationship('User')
1018 1018 repository = relationship('Repository', cascade='')
1019 1019
1020 1020
1021 1021 class UserGroup(Base, BaseModel):
1022 1022 __tablename__ = 'users_groups'
1023 1023 __table_args__ = (
1024 1024 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1025 1025 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1026 1026 )
1027 1027
1028 1028 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1029 1029 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1030 1030 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1031 1031 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1032 1032 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1033 1033 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1034 1034 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1035 1035 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1036 1036
1037 1037 members = relationship('UserGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
1038 1038 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
1039 1039 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1040 1040 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1041 1041 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all')
1042 1042 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
1043 1043
1044 1044 user = relationship('User')
1045 1045
1046 1046 @hybrid_property
1047 1047 def group_data(self):
1048 1048 if not self._group_data:
1049 1049 return {}
1050 1050
1051 1051 try:
1052 1052 return json.loads(self._group_data)
1053 1053 except TypeError:
1054 1054 return {}
1055 1055
1056 1056 @group_data.setter
1057 1057 def group_data(self, val):
1058 1058 try:
1059 1059 self._group_data = json.dumps(val)
1060 1060 except Exception:
1061 1061 log.error(traceback.format_exc())
1062 1062
1063 1063 def __unicode__(self):
1064 1064 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1065 1065 self.users_group_id,
1066 1066 self.users_group_name)
1067 1067
1068 1068 @classmethod
1069 1069 def get_by_group_name(cls, group_name, cache=False,
1070 1070 case_insensitive=False):
1071 1071 if case_insensitive:
1072 1072 q = cls.query().filter(func.lower(cls.users_group_name) ==
1073 1073 func.lower(group_name))
1074 1074
1075 1075 else:
1076 1076 q = cls.query().filter(cls.users_group_name == group_name)
1077 1077 if cache:
1078 1078 q = q.options(FromCache(
1079 1079 "sql_cache_short",
1080 1080 "get_group_%s" % _hash_key(group_name)))
1081 1081 return q.scalar()
1082 1082
1083 1083 @classmethod
1084 1084 def get(cls, user_group_id, cache=False):
1085 1085 user_group = cls.query()
1086 1086 if cache:
1087 1087 user_group = user_group.options(FromCache("sql_cache_short",
1088 1088 "get_users_group_%s" % user_group_id))
1089 1089 return user_group.get(user_group_id)
1090 1090
1091 1091 def permissions(self, with_admins=True, with_owner=True):
1092 1092 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1093 1093 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1094 1094 joinedload(UserUserGroupToPerm.user),
1095 1095 joinedload(UserUserGroupToPerm.permission),)
1096 1096
1097 1097 # get owners and admins and permissions. We do a trick of re-writing
1098 1098 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1099 1099 # has a global reference and changing one object propagates to all
1100 1100 # others. This means if admin is also an owner admin_row that change
1101 1101 # would propagate to both objects
1102 1102 perm_rows = []
1103 1103 for _usr in q.all():
1104 1104 usr = AttributeDict(_usr.user.get_dict())
1105 1105 usr.permission = _usr.permission.permission_name
1106 1106 perm_rows.append(usr)
1107 1107
1108 1108 # filter the perm rows by 'default' first and then sort them by
1109 1109 # admin,write,read,none permissions sorted again alphabetically in
1110 1110 # each group
1111 1111 perm_rows = sorted(perm_rows, key=display_sort)
1112 1112
1113 1113 _admin_perm = 'usergroup.admin'
1114 1114 owner_row = []
1115 1115 if with_owner:
1116 1116 usr = AttributeDict(self.user.get_dict())
1117 1117 usr.owner_row = True
1118 1118 usr.permission = _admin_perm
1119 1119 owner_row.append(usr)
1120 1120
1121 1121 super_admin_rows = []
1122 1122 if with_admins:
1123 1123 for usr in User.get_all_super_admins():
1124 1124 # if this admin is also owner, don't double the record
1125 1125 if usr.user_id == owner_row[0].user_id:
1126 1126 owner_row[0].admin_row = True
1127 1127 else:
1128 1128 usr = AttributeDict(usr.get_dict())
1129 1129 usr.admin_row = True
1130 1130 usr.permission = _admin_perm
1131 1131 super_admin_rows.append(usr)
1132 1132
1133 1133 return super_admin_rows + owner_row + perm_rows
1134 1134
1135 1135 def permission_user_groups(self):
1136 1136 q = UserGroupUserGroupToPerm.query().filter(UserGroupUserGroupToPerm.target_user_group == self)
1137 1137 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1138 1138 joinedload(UserGroupUserGroupToPerm.target_user_group),
1139 1139 joinedload(UserGroupUserGroupToPerm.permission),)
1140 1140
1141 1141 perm_rows = []
1142 1142 for _user_group in q.all():
1143 1143 usr = AttributeDict(_user_group.user_group.get_dict())
1144 1144 usr.permission = _user_group.permission.permission_name
1145 1145 perm_rows.append(usr)
1146 1146
1147 1147 return perm_rows
1148 1148
1149 1149 def _get_default_perms(self, user_group, suffix=''):
1150 1150 from rhodecode.model.permission import PermissionModel
1151 1151 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1152 1152
1153 1153 def get_default_perms(self, suffix=''):
1154 1154 return self._get_default_perms(self, suffix)
1155 1155
1156 1156 def get_api_data(self, with_group_members=True, include_secrets=False):
1157 1157 """
1158 1158 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1159 1159 basically forwarded.
1160 1160
1161 1161 """
1162 1162 user_group = self
1163 1163
1164 1164 data = {
1165 1165 'users_group_id': user_group.users_group_id,
1166 1166 'group_name': user_group.users_group_name,
1167 1167 'group_description': user_group.user_group_description,
1168 1168 'active': user_group.users_group_active,
1169 1169 'owner': user_group.user.username,
1170 1170 }
1171 1171 if with_group_members:
1172 1172 users = []
1173 1173 for user in user_group.members:
1174 1174 user = user.user
1175 1175 users.append(user.get_api_data(include_secrets=include_secrets))
1176 1176 data['users'] = users
1177 1177
1178 1178 return data
1179 1179
1180 1180
1181 1181 class UserGroupMember(Base, BaseModel):
1182 1182 __tablename__ = 'users_groups_members'
1183 1183 __table_args__ = (
1184 1184 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1185 1185 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1186 1186 )
1187 1187
1188 1188 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1189 1189 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1190 1190 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1191 1191
1192 1192 user = relationship('User', lazy='joined')
1193 1193 users_group = relationship('UserGroup')
1194 1194
1195 1195 def __init__(self, gr_id='', u_id=''):
1196 1196 self.users_group_id = gr_id
1197 1197 self.user_id = u_id
1198 1198
1199 1199
1200 1200 class RepositoryField(Base, BaseModel):
1201 1201 __tablename__ = 'repositories_fields'
1202 1202 __table_args__ = (
1203 1203 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1204 1204 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1205 1205 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1206 1206 )
1207 1207 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1208 1208
1209 1209 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1210 1210 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1211 1211 field_key = Column("field_key", String(250))
1212 1212 field_label = Column("field_label", String(1024), nullable=False)
1213 1213 field_value = Column("field_value", String(10000), nullable=False)
1214 1214 field_desc = Column("field_desc", String(1024), nullable=False)
1215 1215 field_type = Column("field_type", String(255), nullable=False, unique=None)
1216 1216 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1217 1217
1218 1218 repository = relationship('Repository')
1219 1219
1220 1220 @property
1221 1221 def field_key_prefixed(self):
1222 1222 return 'ex_%s' % self.field_key
1223 1223
1224 1224 @classmethod
1225 1225 def un_prefix_key(cls, key):
1226 1226 if key.startswith(cls.PREFIX):
1227 1227 return key[len(cls.PREFIX):]
1228 1228 return key
1229 1229
1230 1230 @classmethod
1231 1231 def get_by_key_name(cls, key, repo):
1232 1232 row = cls.query()\
1233 1233 .filter(cls.repository == repo)\
1234 1234 .filter(cls.field_key == key).scalar()
1235 1235 return row
1236 1236
1237 1237
1238 1238 class Repository(Base, BaseModel):
1239 1239 __tablename__ = 'repositories'
1240 1240 __table_args__ = (
1241 1241 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1242 1242 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1243 1243 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1244 1244 )
1245 1245 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1246 1246 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1247 1247
1248 1248 STATE_CREATED = 'repo_state_created'
1249 1249 STATE_PENDING = 'repo_state_pending'
1250 1250 STATE_ERROR = 'repo_state_error'
1251 1251
1252 1252 LOCK_AUTOMATIC = 'lock_auto'
1253 1253 LOCK_API = 'lock_api'
1254 1254 LOCK_WEB = 'lock_web'
1255 1255 LOCK_PULL = 'lock_pull'
1256 1256
1257 1257 NAME_SEP = URL_SEP
1258 1258
1259 1259 repo_id = Column(
1260 1260 "repo_id", Integer(), nullable=False, unique=True, default=None,
1261 1261 primary_key=True)
1262 1262 _repo_name = Column(
1263 1263 "repo_name", Text(), nullable=False, default=None)
1264 1264 _repo_name_hash = Column(
1265 1265 "repo_name_hash", String(255), nullable=False, unique=True)
1266 1266 repo_state = Column("repo_state", String(255), nullable=True)
1267 1267
1268 1268 clone_uri = Column(
1269 1269 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1270 1270 default=None)
1271 1271 repo_type = Column(
1272 1272 "repo_type", String(255), nullable=False, unique=False, default=None)
1273 1273 user_id = Column(
1274 1274 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1275 1275 unique=False, default=None)
1276 1276 private = Column(
1277 1277 "private", Boolean(), nullable=True, unique=None, default=None)
1278 1278 enable_statistics = Column(
1279 1279 "statistics", Boolean(), nullable=True, unique=None, default=True)
1280 1280 enable_downloads = Column(
1281 1281 "downloads", Boolean(), nullable=True, unique=None, default=True)
1282 1282 description = Column(
1283 1283 "description", String(10000), nullable=True, unique=None, default=None)
1284 1284 created_on = Column(
1285 1285 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1286 1286 default=datetime.datetime.now)
1287 1287 updated_on = Column(
1288 1288 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1289 1289 default=datetime.datetime.now)
1290 1290 _landing_revision = Column(
1291 1291 "landing_revision", String(255), nullable=False, unique=False,
1292 1292 default=None)
1293 1293 enable_locking = Column(
1294 1294 "enable_locking", Boolean(), nullable=False, unique=None,
1295 1295 default=False)
1296 1296 _locked = Column(
1297 1297 "locked", String(255), nullable=True, unique=False, default=None)
1298 1298 _changeset_cache = Column(
1299 1299 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1300 1300
1301 1301 fork_id = Column(
1302 1302 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1303 1303 nullable=True, unique=False, default=None)
1304 1304 group_id = Column(
1305 1305 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1306 1306 unique=False, default=None)
1307 1307
1308 1308 user = relationship('User', lazy='joined')
1309 1309 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1310 1310 group = relationship('RepoGroup', lazy='joined')
1311 1311 repo_to_perm = relationship(
1312 1312 'UserRepoToPerm', cascade='all',
1313 1313 order_by='UserRepoToPerm.repo_to_perm_id')
1314 1314 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1315 1315 stats = relationship('Statistics', cascade='all', uselist=False)
1316 1316
1317 1317 followers = relationship(
1318 1318 'UserFollowing',
1319 1319 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
1320 1320 cascade='all')
1321 1321 extra_fields = relationship(
1322 1322 'RepositoryField', cascade="all, delete, delete-orphan")
1323 1323 logs = relationship('UserLog')
1324 1324 comments = relationship(
1325 1325 'ChangesetComment', cascade="all, delete, delete-orphan")
1326 1326 pull_requests_source = relationship(
1327 1327 'PullRequest',
1328 1328 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1329 1329 cascade="all, delete, delete-orphan")
1330 1330 pull_requests_target = relationship(
1331 1331 'PullRequest',
1332 1332 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1333 1333 cascade="all, delete, delete-orphan")
1334 1334 ui = relationship('RepoRhodeCodeUi', cascade="all")
1335 1335 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1336 1336 integrations = relationship('Integration',
1337 1337 cascade="all, delete, delete-orphan")
1338 1338
1339 1339 def __unicode__(self):
1340 1340 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1341 1341 safe_unicode(self.repo_name))
1342 1342
1343 1343 @hybrid_property
1344 1344 def landing_rev(self):
1345 1345 # always should return [rev_type, rev]
1346 1346 if self._landing_revision:
1347 1347 _rev_info = self._landing_revision.split(':')
1348 1348 if len(_rev_info) < 2:
1349 1349 _rev_info.insert(0, 'rev')
1350 1350 return [_rev_info[0], _rev_info[1]]
1351 1351 return [None, None]
1352 1352
1353 1353 @landing_rev.setter
1354 1354 def landing_rev(self, val):
1355 1355 if ':' not in val:
1356 1356 raise ValueError('value must be delimited with `:` and consist '
1357 1357 'of <rev_type>:<rev>, got %s instead' % val)
1358 1358 self._landing_revision = val
1359 1359
1360 1360 @hybrid_property
1361 1361 def locked(self):
1362 1362 if self._locked:
1363 1363 user_id, timelocked, reason = self._locked.split(':')
1364 1364 lock_values = int(user_id), timelocked, reason
1365 1365 else:
1366 1366 lock_values = [None, None, None]
1367 1367 return lock_values
1368 1368
1369 1369 @locked.setter
1370 1370 def locked(self, val):
1371 1371 if val and isinstance(val, (list, tuple)):
1372 1372 self._locked = ':'.join(map(str, val))
1373 1373 else:
1374 1374 self._locked = None
1375 1375
1376 1376 @hybrid_property
1377 1377 def changeset_cache(self):
1378 1378 from rhodecode.lib.vcs.backends.base import EmptyCommit
1379 1379 dummy = EmptyCommit().__json__()
1380 1380 if not self._changeset_cache:
1381 1381 return dummy
1382 1382 try:
1383 1383 return json.loads(self._changeset_cache)
1384 1384 except TypeError:
1385 1385 return dummy
1386 1386 except Exception:
1387 1387 log.error(traceback.format_exc())
1388 1388 return dummy
1389 1389
1390 1390 @changeset_cache.setter
1391 1391 def changeset_cache(self, val):
1392 1392 try:
1393 1393 self._changeset_cache = json.dumps(val)
1394 1394 except Exception:
1395 1395 log.error(traceback.format_exc())
1396 1396
1397 1397 @hybrid_property
1398 1398 def repo_name(self):
1399 1399 return self._repo_name
1400 1400
1401 1401 @repo_name.setter
1402 1402 def repo_name(self, value):
1403 1403 self._repo_name = value
1404 1404 self._repo_name_hash = hashlib.sha1(safe_str(value)).hexdigest()
1405 1405
1406 1406 @classmethod
1407 1407 def normalize_repo_name(cls, repo_name):
1408 1408 """
1409 1409 Normalizes os specific repo_name to the format internally stored inside
1410 1410 database using URL_SEP
1411 1411
1412 1412 :param cls:
1413 1413 :param repo_name:
1414 1414 """
1415 1415 return cls.NAME_SEP.join(repo_name.split(os.sep))
1416 1416
1417 1417 @classmethod
1418 1418 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1419 1419 session = Session()
1420 1420 q = session.query(cls).filter(cls.repo_name == repo_name)
1421 1421
1422 1422 if cache:
1423 1423 if identity_cache:
1424 1424 val = cls.identity_cache(session, 'repo_name', repo_name)
1425 1425 if val:
1426 1426 return val
1427 1427 else:
1428 1428 q = q.options(
1429 1429 FromCache("sql_cache_short",
1430 1430 "get_repo_by_name_%s" % _hash_key(repo_name)))
1431 1431
1432 1432 return q.scalar()
1433 1433
1434 1434 @classmethod
1435 1435 def get_by_full_path(cls, repo_full_path):
1436 1436 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1437 1437 repo_name = cls.normalize_repo_name(repo_name)
1438 1438 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1439 1439
1440 1440 @classmethod
1441 1441 def get_repo_forks(cls, repo_id):
1442 1442 return cls.query().filter(Repository.fork_id == repo_id)
1443 1443
1444 1444 @classmethod
1445 1445 def base_path(cls):
1446 1446 """
1447 1447 Returns base path when all repos are stored
1448 1448
1449 1449 :param cls:
1450 1450 """
1451 1451 q = Session().query(RhodeCodeUi)\
1452 1452 .filter(RhodeCodeUi.ui_key == cls.NAME_SEP)
1453 1453 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1454 1454 return q.one().ui_value
1455 1455
1456 1456 @classmethod
1457 1457 def is_valid(cls, repo_name):
1458 1458 """
1459 1459 returns True if given repo name is a valid filesystem repository
1460 1460
1461 1461 :param cls:
1462 1462 :param repo_name:
1463 1463 """
1464 1464 from rhodecode.lib.utils import is_valid_repo
1465 1465
1466 1466 return is_valid_repo(repo_name, cls.base_path())
1467 1467
1468 1468 @classmethod
1469 1469 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1470 1470 case_insensitive=True):
1471 1471 q = Repository.query()
1472 1472
1473 1473 if not isinstance(user_id, Optional):
1474 1474 q = q.filter(Repository.user_id == user_id)
1475 1475
1476 1476 if not isinstance(group_id, Optional):
1477 1477 q = q.filter(Repository.group_id == group_id)
1478 1478
1479 1479 if case_insensitive:
1480 1480 q = q.order_by(func.lower(Repository.repo_name))
1481 1481 else:
1482 1482 q = q.order_by(Repository.repo_name)
1483 1483 return q.all()
1484 1484
1485 1485 @property
1486 1486 def forks(self):
1487 1487 """
1488 1488 Return forks of this repo
1489 1489 """
1490 1490 return Repository.get_repo_forks(self.repo_id)
1491 1491
1492 1492 @property
1493 1493 def parent(self):
1494 1494 """
1495 1495 Returns fork parent
1496 1496 """
1497 1497 return self.fork
1498 1498
1499 1499 @property
1500 1500 def just_name(self):
1501 1501 return self.repo_name.split(self.NAME_SEP)[-1]
1502 1502
1503 1503 @property
1504 1504 def groups_with_parents(self):
1505 1505 groups = []
1506 1506 if self.group is None:
1507 1507 return groups
1508 1508
1509 1509 cur_gr = self.group
1510 1510 groups.insert(0, cur_gr)
1511 1511 while 1:
1512 1512 gr = getattr(cur_gr, 'parent_group', None)
1513 1513 cur_gr = cur_gr.parent_group
1514 1514 if gr is None:
1515 1515 break
1516 1516 groups.insert(0, gr)
1517 1517
1518 1518 return groups
1519 1519
1520 1520 @property
1521 1521 def groups_and_repo(self):
1522 1522 return self.groups_with_parents, self
1523 1523
1524 1524 @LazyProperty
1525 1525 def repo_path(self):
1526 1526 """
1527 1527 Returns base full path for that repository means where it actually
1528 1528 exists on a filesystem
1529 1529 """
1530 1530 q = Session().query(RhodeCodeUi).filter(
1531 1531 RhodeCodeUi.ui_key == self.NAME_SEP)
1532 1532 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1533 1533 return q.one().ui_value
1534 1534
1535 1535 @property
1536 1536 def repo_full_path(self):
1537 1537 p = [self.repo_path]
1538 1538 # we need to split the name by / since this is how we store the
1539 1539 # names in the database, but that eventually needs to be converted
1540 1540 # into a valid system path
1541 1541 p += self.repo_name.split(self.NAME_SEP)
1542 1542 return os.path.join(*map(safe_unicode, p))
1543 1543
1544 1544 @property
1545 1545 def cache_keys(self):
1546 1546 """
1547 1547 Returns associated cache keys for that repo
1548 1548 """
1549 1549 return CacheKey.query()\
1550 1550 .filter(CacheKey.cache_args == self.repo_name)\
1551 1551 .order_by(CacheKey.cache_key)\
1552 1552 .all()
1553 1553
1554 1554 def get_new_name(self, repo_name):
1555 1555 """
1556 1556 returns new full repository name based on assigned group and new new
1557 1557
1558 1558 :param group_name:
1559 1559 """
1560 1560 path_prefix = self.group.full_path_splitted if self.group else []
1561 1561 return self.NAME_SEP.join(path_prefix + [repo_name])
1562 1562
1563 1563 @property
1564 1564 def _config(self):
1565 1565 """
1566 1566 Returns db based config object.
1567 1567 """
1568 1568 from rhodecode.lib.utils import make_db_config
1569 1569 return make_db_config(clear_session=False, repo=self)
1570 1570
1571 1571 def permissions(self, with_admins=True, with_owner=True):
1572 1572 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
1573 1573 q = q.options(joinedload(UserRepoToPerm.repository),
1574 1574 joinedload(UserRepoToPerm.user),
1575 1575 joinedload(UserRepoToPerm.permission),)
1576 1576
1577 1577 # get owners and admins and permissions. We do a trick of re-writing
1578 1578 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1579 1579 # has a global reference and changing one object propagates to all
1580 1580 # others. This means if admin is also an owner admin_row that change
1581 1581 # would propagate to both objects
1582 1582 perm_rows = []
1583 1583 for _usr in q.all():
1584 1584 usr = AttributeDict(_usr.user.get_dict())
1585 1585 usr.permission = _usr.permission.permission_name
1586 1586 perm_rows.append(usr)
1587 1587
1588 1588 # filter the perm rows by 'default' first and then sort them by
1589 1589 # admin,write,read,none permissions sorted again alphabetically in
1590 1590 # each group
1591 1591 perm_rows = sorted(perm_rows, key=display_sort)
1592 1592
1593 1593 _admin_perm = 'repository.admin'
1594 1594 owner_row = []
1595 1595 if with_owner:
1596 1596 usr = AttributeDict(self.user.get_dict())
1597 1597 usr.owner_row = True
1598 1598 usr.permission = _admin_perm
1599 1599 owner_row.append(usr)
1600 1600
1601 1601 super_admin_rows = []
1602 1602 if with_admins:
1603 1603 for usr in User.get_all_super_admins():
1604 1604 # if this admin is also owner, don't double the record
1605 1605 if usr.user_id == owner_row[0].user_id:
1606 1606 owner_row[0].admin_row = True
1607 1607 else:
1608 1608 usr = AttributeDict(usr.get_dict())
1609 1609 usr.admin_row = True
1610 1610 usr.permission = _admin_perm
1611 1611 super_admin_rows.append(usr)
1612 1612
1613 1613 return super_admin_rows + owner_row + perm_rows
1614 1614
1615 1615 def permission_user_groups(self):
1616 1616 q = UserGroupRepoToPerm.query().filter(
1617 1617 UserGroupRepoToPerm.repository == self)
1618 1618 q = q.options(joinedload(UserGroupRepoToPerm.repository),
1619 1619 joinedload(UserGroupRepoToPerm.users_group),
1620 1620 joinedload(UserGroupRepoToPerm.permission),)
1621 1621
1622 1622 perm_rows = []
1623 1623 for _user_group in q.all():
1624 1624 usr = AttributeDict(_user_group.users_group.get_dict())
1625 1625 usr.permission = _user_group.permission.permission_name
1626 1626 perm_rows.append(usr)
1627 1627
1628 1628 return perm_rows
1629 1629
1630 1630 def get_api_data(self, include_secrets=False):
1631 1631 """
1632 1632 Common function for generating repo api data
1633 1633
1634 1634 :param include_secrets: See :meth:`User.get_api_data`.
1635 1635
1636 1636 """
1637 1637 # TODO: mikhail: Here there is an anti-pattern, we probably need to
1638 1638 # move this methods on models level.
1639 1639 from rhodecode.model.settings import SettingsModel
1640 1640
1641 1641 repo = self
1642 1642 _user_id, _time, _reason = self.locked
1643 1643
1644 1644 data = {
1645 1645 'repo_id': repo.repo_id,
1646 1646 'repo_name': repo.repo_name,
1647 1647 'repo_type': repo.repo_type,
1648 1648 'clone_uri': repo.clone_uri or '',
1649 1649 'url': url('summary_home', repo_name=self.repo_name, qualified=True),
1650 1650 'private': repo.private,
1651 1651 'created_on': repo.created_on,
1652 1652 'description': repo.description,
1653 1653 'landing_rev': repo.landing_rev,
1654 1654 'owner': repo.user.username,
1655 1655 'fork_of': repo.fork.repo_name if repo.fork else None,
1656 1656 'enable_statistics': repo.enable_statistics,
1657 1657 'enable_locking': repo.enable_locking,
1658 1658 'enable_downloads': repo.enable_downloads,
1659 1659 'last_changeset': repo.changeset_cache,
1660 1660 'locked_by': User.get(_user_id).get_api_data(
1661 1661 include_secrets=include_secrets) if _user_id else None,
1662 1662 'locked_date': time_to_datetime(_time) if _time else None,
1663 1663 'lock_reason': _reason if _reason else None,
1664 1664 }
1665 1665
1666 1666 # TODO: mikhail: should be per-repo settings here
1667 1667 rc_config = SettingsModel().get_all_settings()
1668 1668 repository_fields = str2bool(
1669 1669 rc_config.get('rhodecode_repository_fields'))
1670 1670 if repository_fields:
1671 1671 for f in self.extra_fields:
1672 1672 data[f.field_key_prefixed] = f.field_value
1673 1673
1674 1674 return data
1675 1675
1676 1676 @classmethod
1677 1677 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
1678 1678 if not lock_time:
1679 1679 lock_time = time.time()
1680 1680 if not lock_reason:
1681 1681 lock_reason = cls.LOCK_AUTOMATIC
1682 1682 repo.locked = [user_id, lock_time, lock_reason]
1683 1683 Session().add(repo)
1684 1684 Session().commit()
1685 1685
1686 1686 @classmethod
1687 1687 def unlock(cls, repo):
1688 1688 repo.locked = None
1689 1689 Session().add(repo)
1690 1690 Session().commit()
1691 1691
1692 1692 @classmethod
1693 1693 def getlock(cls, repo):
1694 1694 return repo.locked
1695 1695
1696 1696 def is_user_lock(self, user_id):
1697 1697 if self.lock[0]:
1698 1698 lock_user_id = safe_int(self.lock[0])
1699 1699 user_id = safe_int(user_id)
1700 1700 # both are ints, and they are equal
1701 1701 return all([lock_user_id, user_id]) and lock_user_id == user_id
1702 1702
1703 1703 return False
1704 1704
1705 1705 def get_locking_state(self, action, user_id, only_when_enabled=True):
1706 1706 """
1707 1707 Checks locking on this repository, if locking is enabled and lock is
1708 1708 present returns a tuple of make_lock, locked, locked_by.
1709 1709 make_lock can have 3 states None (do nothing) True, make lock
1710 1710 False release lock, This value is later propagated to hooks, which
1711 1711 do the locking. Think about this as signals passed to hooks what to do.
1712 1712
1713 1713 """
1714 1714 # TODO: johbo: This is part of the business logic and should be moved
1715 1715 # into the RepositoryModel.
1716 1716
1717 1717 if action not in ('push', 'pull'):
1718 1718 raise ValueError("Invalid action value: %s" % repr(action))
1719 1719
1720 1720 # defines if locked error should be thrown to user
1721 1721 currently_locked = False
1722 1722 # defines if new lock should be made, tri-state
1723 1723 make_lock = None
1724 1724 repo = self
1725 1725 user = User.get(user_id)
1726 1726
1727 1727 lock_info = repo.locked
1728 1728
1729 1729 if repo and (repo.enable_locking or not only_when_enabled):
1730 1730 if action == 'push':
1731 1731 # check if it's already locked !, if it is compare users
1732 1732 locked_by_user_id = lock_info[0]
1733 1733 if user.user_id == locked_by_user_id:
1734 1734 log.debug(
1735 1735 'Got `push` action from user %s, now unlocking', user)
1736 1736 # unlock if we have push from user who locked
1737 1737 make_lock = False
1738 1738 else:
1739 1739 # we're not the same user who locked, ban with
1740 1740 # code defined in settings (default is 423 HTTP Locked) !
1741 1741 log.debug('Repo %s is currently locked by %s', repo, user)
1742 1742 currently_locked = True
1743 1743 elif action == 'pull':
1744 1744 # [0] user [1] date
1745 1745 if lock_info[0] and lock_info[1]:
1746 1746 log.debug('Repo %s is currently locked by %s', repo, user)
1747 1747 currently_locked = True
1748 1748 else:
1749 1749 log.debug('Setting lock on repo %s by %s', repo, user)
1750 1750 make_lock = True
1751 1751
1752 1752 else:
1753 1753 log.debug('Repository %s do not have locking enabled', repo)
1754 1754
1755 1755 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
1756 1756 make_lock, currently_locked, lock_info)
1757 1757
1758 1758 from rhodecode.lib.auth import HasRepoPermissionAny
1759 1759 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
1760 1760 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
1761 1761 # if we don't have at least write permission we cannot make a lock
1762 1762 log.debug('lock state reset back to FALSE due to lack '
1763 1763 'of at least read permission')
1764 1764 make_lock = False
1765 1765
1766 1766 return make_lock, currently_locked, lock_info
1767 1767
1768 1768 @property
1769 1769 def last_db_change(self):
1770 1770 return self.updated_on
1771 1771
1772 1772 @property
1773 1773 def clone_uri_hidden(self):
1774 1774 clone_uri = self.clone_uri
1775 1775 if clone_uri:
1776 1776 import urlobject
1777 1777 url_obj = urlobject.URLObject(clone_uri)
1778 1778 if url_obj.password:
1779 1779 clone_uri = url_obj.with_password('*****')
1780 1780 return clone_uri
1781 1781
1782 1782 def clone_url(self, **override):
1783 1783 qualified_home_url = url('home', qualified=True)
1784 1784
1785 1785 uri_tmpl = None
1786 1786 if 'with_id' in override:
1787 1787 uri_tmpl = self.DEFAULT_CLONE_URI_ID
1788 1788 del override['with_id']
1789 1789
1790 1790 if 'uri_tmpl' in override:
1791 1791 uri_tmpl = override['uri_tmpl']
1792 1792 del override['uri_tmpl']
1793 1793
1794 1794 # we didn't override our tmpl from **overrides
1795 1795 if not uri_tmpl:
1796 1796 uri_tmpl = self.DEFAULT_CLONE_URI
1797 1797 try:
1798 1798 from pylons import tmpl_context as c
1799 1799 uri_tmpl = c.clone_uri_tmpl
1800 1800 except Exception:
1801 1801 # in any case if we call this outside of request context,
1802 1802 # ie, not having tmpl_context set up
1803 1803 pass
1804 1804
1805 1805 return get_clone_url(uri_tmpl=uri_tmpl,
1806 1806 qualifed_home_url=qualified_home_url,
1807 1807 repo_name=self.repo_name,
1808 1808 repo_id=self.repo_id, **override)
1809 1809
1810 1810 def set_state(self, state):
1811 1811 self.repo_state = state
1812 1812 Session().add(self)
1813 1813 #==========================================================================
1814 1814 # SCM PROPERTIES
1815 1815 #==========================================================================
1816 1816
1817 1817 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
1818 1818 return get_commit_safe(
1819 1819 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load)
1820 1820
1821 1821 def get_changeset(self, rev=None, pre_load=None):
1822 1822 warnings.warn("Use get_commit", DeprecationWarning)
1823 1823 commit_id = None
1824 1824 commit_idx = None
1825 1825 if isinstance(rev, basestring):
1826 1826 commit_id = rev
1827 1827 else:
1828 1828 commit_idx = rev
1829 1829 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
1830 1830 pre_load=pre_load)
1831 1831
1832 1832 def get_landing_commit(self):
1833 1833 """
1834 1834 Returns landing commit, or if that doesn't exist returns the tip
1835 1835 """
1836 1836 _rev_type, _rev = self.landing_rev
1837 1837 commit = self.get_commit(_rev)
1838 1838 if isinstance(commit, EmptyCommit):
1839 1839 return self.get_commit()
1840 1840 return commit
1841 1841
1842 1842 def update_commit_cache(self, cs_cache=None, config=None):
1843 1843 """
1844 1844 Update cache of last changeset for repository, keys should be::
1845 1845
1846 1846 short_id
1847 1847 raw_id
1848 1848 revision
1849 1849 parents
1850 1850 message
1851 1851 date
1852 1852 author
1853 1853
1854 1854 :param cs_cache:
1855 1855 """
1856 1856 from rhodecode.lib.vcs.backends.base import BaseChangeset
1857 1857 if cs_cache is None:
1858 1858 # use no-cache version here
1859 1859 scm_repo = self.scm_instance(cache=False, config=config)
1860 1860 if scm_repo:
1861 1861 cs_cache = scm_repo.get_commit(
1862 1862 pre_load=["author", "date", "message", "parents"])
1863 1863 else:
1864 1864 cs_cache = EmptyCommit()
1865 1865
1866 1866 if isinstance(cs_cache, BaseChangeset):
1867 1867 cs_cache = cs_cache.__json__()
1868 1868
1869 1869 def is_outdated(new_cs_cache):
1870 1870 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
1871 1871 new_cs_cache['revision'] != self.changeset_cache['revision']):
1872 1872 return True
1873 1873 return False
1874 1874
1875 1875 # check if we have maybe already latest cached revision
1876 1876 if is_outdated(cs_cache) or not self.changeset_cache:
1877 1877 _default = datetime.datetime.fromtimestamp(0)
1878 1878 last_change = cs_cache.get('date') or _default
1879 1879 log.debug('updated repo %s with new cs cache %s',
1880 1880 self.repo_name, cs_cache)
1881 1881 self.updated_on = last_change
1882 1882 self.changeset_cache = cs_cache
1883 1883 Session().add(self)
1884 1884 Session().commit()
1885 1885 else:
1886 1886 log.debug('Skipping update_commit_cache for repo:`%s` '
1887 1887 'commit already with latest changes', self.repo_name)
1888 1888
1889 1889 @property
1890 1890 def tip(self):
1891 1891 return self.get_commit('tip')
1892 1892
1893 1893 @property
1894 1894 def author(self):
1895 1895 return self.tip.author
1896 1896
1897 1897 @property
1898 1898 def last_change(self):
1899 1899 return self.scm_instance().last_change
1900 1900
1901 1901 def get_comments(self, revisions=None):
1902 1902 """
1903 1903 Returns comments for this repository grouped by revisions
1904 1904
1905 1905 :param revisions: filter query by revisions only
1906 1906 """
1907 1907 cmts = ChangesetComment.query()\
1908 1908 .filter(ChangesetComment.repo == self)
1909 1909 if revisions:
1910 1910 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
1911 1911 grouped = collections.defaultdict(list)
1912 1912 for cmt in cmts.all():
1913 1913 grouped[cmt.revision].append(cmt)
1914 1914 return grouped
1915 1915
1916 1916 def statuses(self, revisions=None):
1917 1917 """
1918 1918 Returns statuses for this repository
1919 1919
1920 1920 :param revisions: list of revisions to get statuses for
1921 1921 """
1922 1922 statuses = ChangesetStatus.query()\
1923 1923 .filter(ChangesetStatus.repo == self)\
1924 1924 .filter(ChangesetStatus.version == 0)
1925 1925
1926 1926 if revisions:
1927 1927 # Try doing the filtering in chunks to avoid hitting limits
1928 1928 size = 500
1929 1929 status_results = []
1930 1930 for chunk in xrange(0, len(revisions), size):
1931 1931 status_results += statuses.filter(
1932 1932 ChangesetStatus.revision.in_(
1933 1933 revisions[chunk: chunk+size])
1934 1934 ).all()
1935 1935 else:
1936 1936 status_results = statuses.all()
1937 1937
1938 1938 grouped = {}
1939 1939
1940 1940 # maybe we have open new pullrequest without a status?
1941 1941 stat = ChangesetStatus.STATUS_UNDER_REVIEW
1942 1942 status_lbl = ChangesetStatus.get_status_lbl(stat)
1943 1943 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
1944 1944 for rev in pr.revisions:
1945 1945 pr_id = pr.pull_request_id
1946 1946 pr_repo = pr.target_repo.repo_name
1947 1947 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
1948 1948
1949 1949 for stat in status_results:
1950 1950 pr_id = pr_repo = None
1951 1951 if stat.pull_request:
1952 1952 pr_id = stat.pull_request.pull_request_id
1953 1953 pr_repo = stat.pull_request.target_repo.repo_name
1954 1954 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
1955 1955 pr_id, pr_repo]
1956 1956 return grouped
1957 1957
1958 1958 # ==========================================================================
1959 1959 # SCM CACHE INSTANCE
1960 1960 # ==========================================================================
1961 1961
1962 1962 def scm_instance(self, **kwargs):
1963 1963 import rhodecode
1964 1964
1965 1965 # Passing a config will not hit the cache currently only used
1966 1966 # for repo2dbmapper
1967 1967 config = kwargs.pop('config', None)
1968 1968 cache = kwargs.pop('cache', None)
1969 1969 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
1970 1970 # if cache is NOT defined use default global, else we have a full
1971 1971 # control over cache behaviour
1972 1972 if cache is None and full_cache and not config:
1973 1973 return self._get_instance_cached()
1974 1974 return self._get_instance(cache=bool(cache), config=config)
1975 1975
1976 1976 def _get_instance_cached(self):
1977 1977 @cache_region('long_term')
1978 1978 def _get_repo(cache_key):
1979 1979 return self._get_instance()
1980 1980
1981 1981 invalidator_context = CacheKey.repo_context_cache(
1982 1982 _get_repo, self.repo_name, None, thread_scoped=True)
1983 1983
1984 1984 with invalidator_context as context:
1985 1985 context.invalidate()
1986 1986 repo = context.compute()
1987 1987
1988 1988 return repo
1989 1989
1990 1990 def _get_instance(self, cache=True, config=None):
1991 1991 config = config or self._config
1992 1992 custom_wire = {
1993 1993 'cache': cache # controls the vcs.remote cache
1994 1994 }
1995 1995
1996 1996 repo = get_vcs_instance(
1997 1997 repo_path=safe_str(self.repo_full_path),
1998 1998 config=config,
1999 1999 with_wire=custom_wire,
2000 2000 create=False)
2001 2001
2002 2002 return repo
2003 2003
2004 2004 def __json__(self):
2005 2005 return {'landing_rev': self.landing_rev}
2006 2006
2007 2007 def get_dict(self):
2008 2008
2009 2009 # Since we transformed `repo_name` to a hybrid property, we need to
2010 2010 # keep compatibility with the code which uses `repo_name` field.
2011 2011
2012 2012 result = super(Repository, self).get_dict()
2013 2013 result['repo_name'] = result.pop('_repo_name', None)
2014 2014 return result
2015 2015
2016 2016
2017 2017 class RepoGroup(Base, BaseModel):
2018 2018 __tablename__ = 'groups'
2019 2019 __table_args__ = (
2020 2020 UniqueConstraint('group_name', 'group_parent_id'),
2021 2021 CheckConstraint('group_id != group_parent_id'),
2022 2022 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2023 2023 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2024 2024 )
2025 2025 __mapper_args__ = {'order_by': 'group_name'}
2026 2026
2027 2027 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2028 2028
2029 2029 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2030 2030 group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2031 2031 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2032 2032 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2033 2033 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2034 2034 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2035 2035 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2036 2036
2037 2037 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
2038 2038 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
2039 2039 parent_group = relationship('RepoGroup', remote_side=group_id)
2040 2040 user = relationship('User')
2041 2041 integrations = relationship('Integration',
2042 2042 cascade="all, delete, delete-orphan")
2043 2043
2044 2044 def __init__(self, group_name='', parent_group=None):
2045 2045 self.group_name = group_name
2046 2046 self.parent_group = parent_group
2047 2047
2048 2048 def __unicode__(self):
2049 2049 return u"<%s('id:%s:%s')>" % (self.__class__.__name__, self.group_id,
2050 2050 self.group_name)
2051 2051
2052 2052 @classmethod
2053 2053 def _generate_choice(cls, repo_group):
2054 2054 from webhelpers.html import literal as _literal
2055 2055 _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k))
2056 2056 return repo_group.group_id, _name(repo_group.full_path_splitted)
2057 2057
2058 2058 @classmethod
2059 2059 def groups_choices(cls, groups=None, show_empty_group=True):
2060 2060 if not groups:
2061 2061 groups = cls.query().all()
2062 2062
2063 2063 repo_groups = []
2064 2064 if show_empty_group:
2065 2065 repo_groups = [('-1', u'-- %s --' % _('No parent'))]
2066 2066
2067 2067 repo_groups.extend([cls._generate_choice(x) for x in groups])
2068 2068
2069 2069 repo_groups = sorted(
2070 2070 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2071 2071 return repo_groups
2072 2072
2073 2073 @classmethod
2074 2074 def url_sep(cls):
2075 2075 return URL_SEP
2076 2076
2077 2077 @classmethod
2078 2078 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2079 2079 if case_insensitive:
2080 2080 gr = cls.query().filter(func.lower(cls.group_name)
2081 2081 == func.lower(group_name))
2082 2082 else:
2083 2083 gr = cls.query().filter(cls.group_name == group_name)
2084 2084 if cache:
2085 2085 gr = gr.options(FromCache(
2086 2086 "sql_cache_short",
2087 2087 "get_group_%s" % _hash_key(group_name)))
2088 2088 return gr.scalar()
2089 2089
2090 2090 @classmethod
2091 2091 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2092 2092 case_insensitive=True):
2093 2093 q = RepoGroup.query()
2094 2094
2095 2095 if not isinstance(user_id, Optional):
2096 2096 q = q.filter(RepoGroup.user_id == user_id)
2097 2097
2098 2098 if not isinstance(group_id, Optional):
2099 2099 q = q.filter(RepoGroup.group_parent_id == group_id)
2100 2100
2101 2101 if case_insensitive:
2102 2102 q = q.order_by(func.lower(RepoGroup.group_name))
2103 2103 else:
2104 2104 q = q.order_by(RepoGroup.group_name)
2105 2105 return q.all()
2106 2106
2107 2107 @property
2108 2108 def parents(self):
2109 2109 parents_recursion_limit = 10
2110 2110 groups = []
2111 2111 if self.parent_group is None:
2112 2112 return groups
2113 2113 cur_gr = self.parent_group
2114 2114 groups.insert(0, cur_gr)
2115 2115 cnt = 0
2116 2116 while 1:
2117 2117 cnt += 1
2118 2118 gr = getattr(cur_gr, 'parent_group', None)
2119 2119 cur_gr = cur_gr.parent_group
2120 2120 if gr is None:
2121 2121 break
2122 2122 if cnt == parents_recursion_limit:
2123 2123 # this will prevent accidental infinit loops
2124 2124 log.error(('more than %s parents found for group %s, stopping '
2125 2125 'recursive parent fetching' % (parents_recursion_limit, self)))
2126 2126 break
2127 2127
2128 2128 groups.insert(0, gr)
2129 2129 return groups
2130 2130
2131 2131 @property
2132 2132 def children(self):
2133 2133 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2134 2134
2135 2135 @property
2136 2136 def name(self):
2137 2137 return self.group_name.split(RepoGroup.url_sep())[-1]
2138 2138
2139 2139 @property
2140 2140 def full_path(self):
2141 2141 return self.group_name
2142 2142
2143 2143 @property
2144 2144 def full_path_splitted(self):
2145 2145 return self.group_name.split(RepoGroup.url_sep())
2146 2146
2147 2147 @property
2148 2148 def repositories(self):
2149 2149 return Repository.query()\
2150 2150 .filter(Repository.group == self)\
2151 2151 .order_by(Repository.repo_name)
2152 2152
2153 2153 @property
2154 2154 def repositories_recursive_count(self):
2155 2155 cnt = self.repositories.count()
2156 2156
2157 2157 def children_count(group):
2158 2158 cnt = 0
2159 2159 for child in group.children:
2160 2160 cnt += child.repositories.count()
2161 2161 cnt += children_count(child)
2162 2162 return cnt
2163 2163
2164 2164 return cnt + children_count(self)
2165 2165
2166 2166 def _recursive_objects(self, include_repos=True):
2167 2167 all_ = []
2168 2168
2169 2169 def _get_members(root_gr):
2170 2170 if include_repos:
2171 2171 for r in root_gr.repositories:
2172 2172 all_.append(r)
2173 2173 childs = root_gr.children.all()
2174 2174 if childs:
2175 2175 for gr in childs:
2176 2176 all_.append(gr)
2177 2177 _get_members(gr)
2178 2178
2179 2179 _get_members(self)
2180 2180 return [self] + all_
2181 2181
2182 2182 def recursive_groups_and_repos(self):
2183 2183 """
2184 2184 Recursive return all groups, with repositories in those groups
2185 2185 """
2186 2186 return self._recursive_objects()
2187 2187
2188 2188 def recursive_groups(self):
2189 2189 """
2190 2190 Returns all children groups for this group including children of children
2191 2191 """
2192 2192 return self._recursive_objects(include_repos=False)
2193 2193
2194 2194 def get_new_name(self, group_name):
2195 2195 """
2196 2196 returns new full group name based on parent and new name
2197 2197
2198 2198 :param group_name:
2199 2199 """
2200 2200 path_prefix = (self.parent_group.full_path_splitted if
2201 2201 self.parent_group else [])
2202 2202 return RepoGroup.url_sep().join(path_prefix + [group_name])
2203 2203
2204 2204 def permissions(self, with_admins=True, with_owner=True):
2205 2205 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
2206 2206 q = q.options(joinedload(UserRepoGroupToPerm.group),
2207 2207 joinedload(UserRepoGroupToPerm.user),
2208 2208 joinedload(UserRepoGroupToPerm.permission),)
2209 2209
2210 2210 # get owners and admins and permissions. We do a trick of re-writing
2211 2211 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2212 2212 # has a global reference and changing one object propagates to all
2213 2213 # others. This means if admin is also an owner admin_row that change
2214 2214 # would propagate to both objects
2215 2215 perm_rows = []
2216 2216 for _usr in q.all():
2217 2217 usr = AttributeDict(_usr.user.get_dict())
2218 2218 usr.permission = _usr.permission.permission_name
2219 2219 perm_rows.append(usr)
2220 2220
2221 2221 # filter the perm rows by 'default' first and then sort them by
2222 2222 # admin,write,read,none permissions sorted again alphabetically in
2223 2223 # each group
2224 2224 perm_rows = sorted(perm_rows, key=display_sort)
2225 2225
2226 2226 _admin_perm = 'group.admin'
2227 2227 owner_row = []
2228 2228 if with_owner:
2229 2229 usr = AttributeDict(self.user.get_dict())
2230 2230 usr.owner_row = True
2231 2231 usr.permission = _admin_perm
2232 2232 owner_row.append(usr)
2233 2233
2234 2234 super_admin_rows = []
2235 2235 if with_admins:
2236 2236 for usr in User.get_all_super_admins():
2237 2237 # if this admin is also owner, don't double the record
2238 2238 if usr.user_id == owner_row[0].user_id:
2239 2239 owner_row[0].admin_row = True
2240 2240 else:
2241 2241 usr = AttributeDict(usr.get_dict())
2242 2242 usr.admin_row = True
2243 2243 usr.permission = _admin_perm
2244 2244 super_admin_rows.append(usr)
2245 2245
2246 2246 return super_admin_rows + owner_row + perm_rows
2247 2247
2248 2248 def permission_user_groups(self):
2249 2249 q = UserGroupRepoGroupToPerm.query().filter(UserGroupRepoGroupToPerm.group == self)
2250 2250 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
2251 2251 joinedload(UserGroupRepoGroupToPerm.users_group),
2252 2252 joinedload(UserGroupRepoGroupToPerm.permission),)
2253 2253
2254 2254 perm_rows = []
2255 2255 for _user_group in q.all():
2256 2256 usr = AttributeDict(_user_group.users_group.get_dict())
2257 2257 usr.permission = _user_group.permission.permission_name
2258 2258 perm_rows.append(usr)
2259 2259
2260 2260 return perm_rows
2261 2261
2262 2262 def get_api_data(self):
2263 2263 """
2264 2264 Common function for generating api data
2265 2265
2266 2266 """
2267 2267 group = self
2268 2268 data = {
2269 2269 'group_id': group.group_id,
2270 2270 'group_name': group.group_name,
2271 2271 'group_description': group.group_description,
2272 2272 'parent_group': group.parent_group.group_name if group.parent_group else None,
2273 2273 'repositories': [x.repo_name for x in group.repositories],
2274 2274 'owner': group.user.username,
2275 2275 }
2276 2276 return data
2277 2277
2278 2278
2279 2279 class Permission(Base, BaseModel):
2280 2280 __tablename__ = 'permissions'
2281 2281 __table_args__ = (
2282 2282 Index('p_perm_name_idx', 'permission_name'),
2283 2283 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2284 2284 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2285 2285 )
2286 2286 PERMS = [
2287 2287 ('hg.admin', _('RhodeCode Super Administrator')),
2288 2288
2289 2289 ('repository.none', _('Repository no access')),
2290 2290 ('repository.read', _('Repository read access')),
2291 2291 ('repository.write', _('Repository write access')),
2292 2292 ('repository.admin', _('Repository admin access')),
2293 2293
2294 2294 ('group.none', _('Repository group no access')),
2295 2295 ('group.read', _('Repository group read access')),
2296 2296 ('group.write', _('Repository group write access')),
2297 2297 ('group.admin', _('Repository group admin access')),
2298 2298
2299 2299 ('usergroup.none', _('User group no access')),
2300 2300 ('usergroup.read', _('User group read access')),
2301 2301 ('usergroup.write', _('User group write access')),
2302 2302 ('usergroup.admin', _('User group admin access')),
2303 2303
2304 2304 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
2305 2305 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
2306 2306
2307 2307 ('hg.usergroup.create.false', _('User Group creation disabled')),
2308 2308 ('hg.usergroup.create.true', _('User Group creation enabled')),
2309 2309
2310 2310 ('hg.create.none', _('Repository creation disabled')),
2311 2311 ('hg.create.repository', _('Repository creation enabled')),
2312 2312 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
2313 2313 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
2314 2314
2315 2315 ('hg.fork.none', _('Repository forking disabled')),
2316 2316 ('hg.fork.repository', _('Repository forking enabled')),
2317 2317
2318 2318 ('hg.register.none', _('Registration disabled')),
2319 2319 ('hg.register.manual_activate', _('User Registration with manual account activation')),
2320 2320 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
2321 2321
2322 2322 ('hg.extern_activate.manual', _('Manual activation of external account')),
2323 2323 ('hg.extern_activate.auto', _('Automatic activation of external account')),
2324 2324
2325 2325 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
2326 2326 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
2327 2327 ]
2328 2328
2329 2329 # definition of system default permissions for DEFAULT user
2330 2330 DEFAULT_USER_PERMISSIONS = [
2331 2331 'repository.read',
2332 2332 'group.read',
2333 2333 'usergroup.read',
2334 2334 'hg.create.repository',
2335 2335 'hg.repogroup.create.false',
2336 2336 'hg.usergroup.create.false',
2337 2337 'hg.create.write_on_repogroup.true',
2338 2338 'hg.fork.repository',
2339 2339 'hg.register.manual_activate',
2340 2340 'hg.extern_activate.auto',
2341 2341 'hg.inherit_default_perms.true',
2342 2342 ]
2343 2343
2344 2344 # defines which permissions are more important higher the more important
2345 2345 # Weight defines which permissions are more important.
2346 2346 # The higher number the more important.
2347 2347 PERM_WEIGHTS = {
2348 2348 'repository.none': 0,
2349 2349 'repository.read': 1,
2350 2350 'repository.write': 3,
2351 2351 'repository.admin': 4,
2352 2352
2353 2353 'group.none': 0,
2354 2354 'group.read': 1,
2355 2355 'group.write': 3,
2356 2356 'group.admin': 4,
2357 2357
2358 2358 'usergroup.none': 0,
2359 2359 'usergroup.read': 1,
2360 2360 'usergroup.write': 3,
2361 2361 'usergroup.admin': 4,
2362 2362
2363 2363 'hg.repogroup.create.false': 0,
2364 2364 'hg.repogroup.create.true': 1,
2365 2365
2366 2366 'hg.usergroup.create.false': 0,
2367 2367 'hg.usergroup.create.true': 1,
2368 2368
2369 2369 'hg.fork.none': 0,
2370 2370 'hg.fork.repository': 1,
2371 2371 'hg.create.none': 0,
2372 2372 'hg.create.repository': 1
2373 2373 }
2374 2374
2375 2375 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2376 2376 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
2377 2377 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
2378 2378
2379 2379 def __unicode__(self):
2380 2380 return u"<%s('%s:%s')>" % (
2381 2381 self.__class__.__name__, self.permission_id, self.permission_name
2382 2382 )
2383 2383
2384 2384 @classmethod
2385 2385 def get_by_key(cls, key):
2386 2386 return cls.query().filter(cls.permission_name == key).scalar()
2387 2387
2388 2388 @classmethod
2389 2389 def get_default_repo_perms(cls, user_id, repo_id=None):
2390 2390 q = Session().query(UserRepoToPerm, Repository, Permission)\
2391 2391 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
2392 2392 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
2393 2393 .filter(UserRepoToPerm.user_id == user_id)
2394 2394 if repo_id:
2395 2395 q = q.filter(UserRepoToPerm.repository_id == repo_id)
2396 2396 return q.all()
2397 2397
2398 2398 @classmethod
2399 2399 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
2400 2400 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
2401 2401 .join(
2402 2402 Permission,
2403 2403 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
2404 2404 .join(
2405 2405 Repository,
2406 2406 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
2407 2407 .join(
2408 2408 UserGroup,
2409 2409 UserGroupRepoToPerm.users_group_id ==
2410 2410 UserGroup.users_group_id)\
2411 2411 .join(
2412 2412 UserGroupMember,
2413 2413 UserGroupRepoToPerm.users_group_id ==
2414 2414 UserGroupMember.users_group_id)\
2415 2415 .filter(
2416 2416 UserGroupMember.user_id == user_id,
2417 2417 UserGroup.users_group_active == true())
2418 2418 if repo_id:
2419 2419 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
2420 2420 return q.all()
2421 2421
2422 2422 @classmethod
2423 2423 def get_default_group_perms(cls, user_id, repo_group_id=None):
2424 2424 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
2425 2425 .join((Permission, UserRepoGroupToPerm.permission_id == Permission.permission_id))\
2426 2426 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
2427 2427 .filter(UserRepoGroupToPerm.user_id == user_id)
2428 2428 if repo_group_id:
2429 2429 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
2430 2430 return q.all()
2431 2431
2432 2432 @classmethod
2433 2433 def get_default_group_perms_from_user_group(
2434 2434 cls, user_id, repo_group_id=None):
2435 2435 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
2436 2436 .join(
2437 2437 Permission,
2438 2438 UserGroupRepoGroupToPerm.permission_id ==
2439 2439 Permission.permission_id)\
2440 2440 .join(
2441 2441 RepoGroup,
2442 2442 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
2443 2443 .join(
2444 2444 UserGroup,
2445 2445 UserGroupRepoGroupToPerm.users_group_id ==
2446 2446 UserGroup.users_group_id)\
2447 2447 .join(
2448 2448 UserGroupMember,
2449 2449 UserGroupRepoGroupToPerm.users_group_id ==
2450 2450 UserGroupMember.users_group_id)\
2451 2451 .filter(
2452 2452 UserGroupMember.user_id == user_id,
2453 2453 UserGroup.users_group_active == true())
2454 2454 if repo_group_id:
2455 2455 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
2456 2456 return q.all()
2457 2457
2458 2458 @classmethod
2459 2459 def get_default_user_group_perms(cls, user_id, user_group_id=None):
2460 2460 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
2461 2461 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
2462 2462 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
2463 2463 .filter(UserUserGroupToPerm.user_id == user_id)
2464 2464 if user_group_id:
2465 2465 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
2466 2466 return q.all()
2467 2467
2468 2468 @classmethod
2469 2469 def get_default_user_group_perms_from_user_group(
2470 2470 cls, user_id, user_group_id=None):
2471 2471 TargetUserGroup = aliased(UserGroup, name='target_user_group')
2472 2472 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
2473 2473 .join(
2474 2474 Permission,
2475 2475 UserGroupUserGroupToPerm.permission_id ==
2476 2476 Permission.permission_id)\
2477 2477 .join(
2478 2478 TargetUserGroup,
2479 2479 UserGroupUserGroupToPerm.target_user_group_id ==
2480 2480 TargetUserGroup.users_group_id)\
2481 2481 .join(
2482 2482 UserGroup,
2483 2483 UserGroupUserGroupToPerm.user_group_id ==
2484 2484 UserGroup.users_group_id)\
2485 2485 .join(
2486 2486 UserGroupMember,
2487 2487 UserGroupUserGroupToPerm.user_group_id ==
2488 2488 UserGroupMember.users_group_id)\
2489 2489 .filter(
2490 2490 UserGroupMember.user_id == user_id,
2491 2491 UserGroup.users_group_active == true())
2492 2492 if user_group_id:
2493 2493 q = q.filter(
2494 2494 UserGroupUserGroupToPerm.user_group_id == user_group_id)
2495 2495
2496 2496 return q.all()
2497 2497
2498 2498
2499 2499 class UserRepoToPerm(Base, BaseModel):
2500 2500 __tablename__ = 'repo_to_perm'
2501 2501 __table_args__ = (
2502 2502 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
2503 2503 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2504 2504 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2505 2505 )
2506 2506 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2507 2507 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2508 2508 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2509 2509 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2510 2510
2511 2511 user = relationship('User')
2512 2512 repository = relationship('Repository')
2513 2513 permission = relationship('Permission')
2514 2514
2515 2515 @classmethod
2516 2516 def create(cls, user, repository, permission):
2517 2517 n = cls()
2518 2518 n.user = user
2519 2519 n.repository = repository
2520 2520 n.permission = permission
2521 2521 Session().add(n)
2522 2522 return n
2523 2523
2524 2524 def __unicode__(self):
2525 2525 return u'<%s => %s >' % (self.user, self.repository)
2526 2526
2527 2527
2528 2528 class UserUserGroupToPerm(Base, BaseModel):
2529 2529 __tablename__ = 'user_user_group_to_perm'
2530 2530 __table_args__ = (
2531 2531 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
2532 2532 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2533 2533 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2534 2534 )
2535 2535 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2536 2536 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2537 2537 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2538 2538 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2539 2539
2540 2540 user = relationship('User')
2541 2541 user_group = relationship('UserGroup')
2542 2542 permission = relationship('Permission')
2543 2543
2544 2544 @classmethod
2545 2545 def create(cls, user, user_group, permission):
2546 2546 n = cls()
2547 2547 n.user = user
2548 2548 n.user_group = user_group
2549 2549 n.permission = permission
2550 2550 Session().add(n)
2551 2551 return n
2552 2552
2553 2553 def __unicode__(self):
2554 2554 return u'<%s => %s >' % (self.user, self.user_group)
2555 2555
2556 2556
2557 2557 class UserToPerm(Base, BaseModel):
2558 2558 __tablename__ = 'user_to_perm'
2559 2559 __table_args__ = (
2560 2560 UniqueConstraint('user_id', 'permission_id'),
2561 2561 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2562 2562 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2563 2563 )
2564 2564 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2565 2565 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2566 2566 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2567 2567
2568 2568 user = relationship('User')
2569 2569 permission = relationship('Permission', lazy='joined')
2570 2570
2571 2571 def __unicode__(self):
2572 2572 return u'<%s => %s >' % (self.user, self.permission)
2573 2573
2574 2574
2575 2575 class UserGroupRepoToPerm(Base, BaseModel):
2576 2576 __tablename__ = 'users_group_repo_to_perm'
2577 2577 __table_args__ = (
2578 2578 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
2579 2579 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2580 2580 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2581 2581 )
2582 2582 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2583 2583 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2584 2584 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2585 2585 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2586 2586
2587 2587 users_group = relationship('UserGroup')
2588 2588 permission = relationship('Permission')
2589 2589 repository = relationship('Repository')
2590 2590
2591 2591 @classmethod
2592 2592 def create(cls, users_group, repository, permission):
2593 2593 n = cls()
2594 2594 n.users_group = users_group
2595 2595 n.repository = repository
2596 2596 n.permission = permission
2597 2597 Session().add(n)
2598 2598 return n
2599 2599
2600 2600 def __unicode__(self):
2601 2601 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
2602 2602
2603 2603
2604 2604 class UserGroupUserGroupToPerm(Base, BaseModel):
2605 2605 __tablename__ = 'user_group_user_group_to_perm'
2606 2606 __table_args__ = (
2607 2607 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
2608 2608 CheckConstraint('target_user_group_id != user_group_id'),
2609 2609 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2610 2610 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2611 2611 )
2612 2612 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)
2613 2613 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2614 2614 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2615 2615 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2616 2616
2617 2617 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
2618 2618 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
2619 2619 permission = relationship('Permission')
2620 2620
2621 2621 @classmethod
2622 2622 def create(cls, target_user_group, user_group, permission):
2623 2623 n = cls()
2624 2624 n.target_user_group = target_user_group
2625 2625 n.user_group = user_group
2626 2626 n.permission = permission
2627 2627 Session().add(n)
2628 2628 return n
2629 2629
2630 2630 def __unicode__(self):
2631 2631 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
2632 2632
2633 2633
2634 2634 class UserGroupToPerm(Base, BaseModel):
2635 2635 __tablename__ = 'users_group_to_perm'
2636 2636 __table_args__ = (
2637 2637 UniqueConstraint('users_group_id', 'permission_id',),
2638 2638 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2639 2639 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2640 2640 )
2641 2641 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2642 2642 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2643 2643 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2644 2644
2645 2645 users_group = relationship('UserGroup')
2646 2646 permission = relationship('Permission')
2647 2647
2648 2648
2649 2649 class UserRepoGroupToPerm(Base, BaseModel):
2650 2650 __tablename__ = 'user_repo_group_to_perm'
2651 2651 __table_args__ = (
2652 2652 UniqueConstraint('user_id', 'group_id', 'permission_id'),
2653 2653 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2654 2654 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2655 2655 )
2656 2656
2657 2657 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2658 2658 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2659 2659 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2660 2660 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2661 2661
2662 2662 user = relationship('User')
2663 2663 group = relationship('RepoGroup')
2664 2664 permission = relationship('Permission')
2665 2665
2666 2666 @classmethod
2667 2667 def create(cls, user, repository_group, permission):
2668 2668 n = cls()
2669 2669 n.user = user
2670 2670 n.group = repository_group
2671 2671 n.permission = permission
2672 2672 Session().add(n)
2673 2673 return n
2674 2674
2675 2675
2676 2676 class UserGroupRepoGroupToPerm(Base, BaseModel):
2677 2677 __tablename__ = 'users_group_repo_group_to_perm'
2678 2678 __table_args__ = (
2679 2679 UniqueConstraint('users_group_id', 'group_id'),
2680 2680 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2681 2681 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2682 2682 )
2683 2683
2684 2684 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)
2685 2685 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2686 2686 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2687 2687 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2688 2688
2689 2689 users_group = relationship('UserGroup')
2690 2690 permission = relationship('Permission')
2691 2691 group = relationship('RepoGroup')
2692 2692
2693 2693 @classmethod
2694 2694 def create(cls, user_group, repository_group, permission):
2695 2695 n = cls()
2696 2696 n.users_group = user_group
2697 2697 n.group = repository_group
2698 2698 n.permission = permission
2699 2699 Session().add(n)
2700 2700 return n
2701 2701
2702 2702 def __unicode__(self):
2703 2703 return u'<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
2704 2704
2705 2705
2706 2706 class Statistics(Base, BaseModel):
2707 2707 __tablename__ = 'statistics'
2708 2708 __table_args__ = (
2709 2709 UniqueConstraint('repository_id'),
2710 2710 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2711 2711 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2712 2712 )
2713 2713 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2714 2714 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
2715 2715 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
2716 2716 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
2717 2717 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
2718 2718 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
2719 2719
2720 2720 repository = relationship('Repository', single_parent=True)
2721 2721
2722 2722
2723 2723 class UserFollowing(Base, BaseModel):
2724 2724 __tablename__ = 'user_followings'
2725 2725 __table_args__ = (
2726 2726 UniqueConstraint('user_id', 'follows_repository_id'),
2727 2727 UniqueConstraint('user_id', 'follows_user_id'),
2728 2728 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2729 2729 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2730 2730 )
2731 2731
2732 2732 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2733 2733 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2734 2734 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
2735 2735 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
2736 2736 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2737 2737
2738 2738 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
2739 2739
2740 2740 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
2741 2741 follows_repository = relationship('Repository', order_by='Repository.repo_name')
2742 2742
2743 2743 @classmethod
2744 2744 def get_repo_followers(cls, repo_id):
2745 2745 return cls.query().filter(cls.follows_repo_id == repo_id)
2746 2746
2747 2747
2748 2748 class CacheKey(Base, BaseModel):
2749 2749 __tablename__ = 'cache_invalidation'
2750 2750 __table_args__ = (
2751 2751 UniqueConstraint('cache_key'),
2752 2752 Index('key_idx', 'cache_key'),
2753 2753 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2754 2754 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2755 2755 )
2756 2756 CACHE_TYPE_ATOM = 'ATOM'
2757 2757 CACHE_TYPE_RSS = 'RSS'
2758 2758 CACHE_TYPE_README = 'README'
2759 2759
2760 2760 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2761 2761 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
2762 2762 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
2763 2763 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
2764 2764
2765 2765 def __init__(self, cache_key, cache_args=''):
2766 2766 self.cache_key = cache_key
2767 2767 self.cache_args = cache_args
2768 2768 self.cache_active = False
2769 2769
2770 2770 def __unicode__(self):
2771 2771 return u"<%s('%s:%s[%s]')>" % (
2772 2772 self.__class__.__name__,
2773 2773 self.cache_id, self.cache_key, self.cache_active)
2774 2774
2775 2775 def _cache_key_partition(self):
2776 2776 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
2777 2777 return prefix, repo_name, suffix
2778 2778
2779 2779 def get_prefix(self):
2780 2780 """
2781 2781 Try to extract prefix from existing cache key. The key could consist
2782 2782 of prefix, repo_name, suffix
2783 2783 """
2784 2784 # this returns prefix, repo_name, suffix
2785 2785 return self._cache_key_partition()[0]
2786 2786
2787 2787 def get_suffix(self):
2788 2788 """
2789 2789 get suffix that might have been used in _get_cache_key to
2790 2790 generate self.cache_key. Only used for informational purposes
2791 2791 in repo_edit.html.
2792 2792 """
2793 2793 # prefix, repo_name, suffix
2794 2794 return self._cache_key_partition()[2]
2795 2795
2796 2796 @classmethod
2797 2797 def delete_all_cache(cls):
2798 2798 """
2799 2799 Delete all cache keys from database.
2800 2800 Should only be run when all instances are down and all entries
2801 2801 thus stale.
2802 2802 """
2803 2803 cls.query().delete()
2804 2804 Session().commit()
2805 2805
2806 2806 @classmethod
2807 2807 def get_cache_key(cls, repo_name, cache_type):
2808 2808 """
2809 2809
2810 2810 Generate a cache key for this process of RhodeCode instance.
2811 2811 Prefix most likely will be process id or maybe explicitly set
2812 2812 instance_id from .ini file.
2813 2813 """
2814 2814 import rhodecode
2815 2815 prefix = safe_unicode(rhodecode.CONFIG.get('instance_id') or '')
2816 2816
2817 2817 repo_as_unicode = safe_unicode(repo_name)
2818 2818 key = u'{}_{}'.format(repo_as_unicode, cache_type) \
2819 2819 if cache_type else repo_as_unicode
2820 2820
2821 2821 return u'{}{}'.format(prefix, key)
2822 2822
2823 2823 @classmethod
2824 2824 def set_invalidate(cls, repo_name, delete=False):
2825 2825 """
2826 2826 Mark all caches of a repo as invalid in the database.
2827 2827 """
2828 2828
2829 2829 try:
2830 2830 qry = Session().query(cls).filter(cls.cache_args == repo_name)
2831 2831 if delete:
2832 2832 log.debug('cache objects deleted for repo %s',
2833 2833 safe_str(repo_name))
2834 2834 qry.delete()
2835 2835 else:
2836 2836 log.debug('cache objects marked as invalid for repo %s',
2837 2837 safe_str(repo_name))
2838 2838 qry.update({"cache_active": False})
2839 2839
2840 2840 Session().commit()
2841 2841 except Exception:
2842 2842 log.exception(
2843 2843 'Cache key invalidation failed for repository %s',
2844 2844 safe_str(repo_name))
2845 2845 Session().rollback()
2846 2846
2847 2847 @classmethod
2848 2848 def get_active_cache(cls, cache_key):
2849 2849 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
2850 2850 if inv_obj:
2851 2851 return inv_obj
2852 2852 return None
2853 2853
2854 2854 @classmethod
2855 2855 def repo_context_cache(cls, compute_func, repo_name, cache_type,
2856 2856 thread_scoped=False):
2857 2857 """
2858 2858 @cache_region('long_term')
2859 2859 def _heavy_calculation(cache_key):
2860 2860 return 'result'
2861 2861
2862 2862 cache_context = CacheKey.repo_context_cache(
2863 2863 _heavy_calculation, repo_name, cache_type)
2864 2864
2865 2865 with cache_context as context:
2866 2866 context.invalidate()
2867 2867 computed = context.compute()
2868 2868
2869 2869 assert computed == 'result'
2870 2870 """
2871 2871 from rhodecode.lib import caches
2872 2872 return caches.InvalidationContext(
2873 2873 compute_func, repo_name, cache_type, thread_scoped=thread_scoped)
2874 2874
2875 2875
2876 2876 class ChangesetComment(Base, BaseModel):
2877 2877 __tablename__ = 'changeset_comments'
2878 2878 __table_args__ = (
2879 2879 Index('cc_revision_idx', 'revision'),
2880 2880 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2881 2881 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2882 2882 )
2883 2883
2884 2884 COMMENT_OUTDATED = u'comment_outdated'
2885 2885
2886 2886 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
2887 2887 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2888 2888 revision = Column('revision', String(40), nullable=True)
2889 2889 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
2890 2890 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
2891 2891 line_no = Column('line_no', Unicode(10), nullable=True)
2892 2892 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
2893 2893 f_path = Column('f_path', Unicode(1000), nullable=True)
2894 2894 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
2895 2895 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
2896 2896 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2897 2897 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2898 2898 renderer = Column('renderer', Unicode(64), nullable=True)
2899 2899 display_state = Column('display_state', Unicode(128), nullable=True)
2900 2900
2901 2901 author = relationship('User', lazy='joined')
2902 2902 repo = relationship('Repository')
2903 2903 status_change = relationship('ChangesetStatus', cascade="all, delete, delete-orphan")
2904 2904 pull_request = relationship('PullRequest', lazy='joined')
2905 2905 pull_request_version = relationship('PullRequestVersion')
2906 2906
2907 2907 @classmethod
2908 2908 def get_users(cls, revision=None, pull_request_id=None):
2909 2909 """
2910 2910 Returns user associated with this ChangesetComment. ie those
2911 2911 who actually commented
2912 2912
2913 2913 :param cls:
2914 2914 :param revision:
2915 2915 """
2916 2916 q = Session().query(User)\
2917 2917 .join(ChangesetComment.author)
2918 2918 if revision:
2919 2919 q = q.filter(cls.revision == revision)
2920 2920 elif pull_request_id:
2921 2921 q = q.filter(cls.pull_request_id == pull_request_id)
2922 2922 return q.all()
2923 2923
2924 2924 def render(self, mentions=False):
2925 2925 from rhodecode.lib import helpers as h
2926 2926 return h.render(self.text, renderer=self.renderer, mentions=mentions)
2927 2927
2928 2928 def __repr__(self):
2929 2929 if self.comment_id:
2930 2930 return '<DB:ChangesetComment #%s>' % self.comment_id
2931 2931 else:
2932 2932 return '<DB:ChangesetComment at %#x>' % id(self)
2933 2933
2934 2934
2935 2935 class ChangesetStatus(Base, BaseModel):
2936 2936 __tablename__ = 'changeset_statuses'
2937 2937 __table_args__ = (
2938 2938 Index('cs_revision_idx', 'revision'),
2939 2939 Index('cs_version_idx', 'version'),
2940 2940 UniqueConstraint('repo_id', 'revision', 'version'),
2941 2941 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2942 2942 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2943 2943 )
2944 2944 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
2945 2945 STATUS_APPROVED = 'approved'
2946 2946 STATUS_REJECTED = 'rejected'
2947 2947 STATUS_UNDER_REVIEW = 'under_review'
2948 2948
2949 2949 STATUSES = [
2950 2950 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
2951 2951 (STATUS_APPROVED, _("Approved")),
2952 2952 (STATUS_REJECTED, _("Rejected")),
2953 2953 (STATUS_UNDER_REVIEW, _("Under Review")),
2954 2954 ]
2955 2955
2956 2956 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
2957 2957 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2958 2958 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
2959 2959 revision = Column('revision', String(40), nullable=False)
2960 2960 status = Column('status', String(128), nullable=False, default=DEFAULT)
2961 2961 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
2962 2962 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
2963 2963 version = Column('version', Integer(), nullable=False, default=0)
2964 2964 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
2965 2965
2966 2966 author = relationship('User', lazy='joined')
2967 2967 repo = relationship('Repository')
2968 2968 comment = relationship('ChangesetComment', lazy='joined')
2969 2969 pull_request = relationship('PullRequest', lazy='joined')
2970 2970
2971 2971 def __unicode__(self):
2972 2972 return u"<%s('%s[%s]:%s')>" % (
2973 2973 self.__class__.__name__,
2974 2974 self.status, self.version, self.author
2975 2975 )
2976 2976
2977 2977 @classmethod
2978 2978 def get_status_lbl(cls, value):
2979 2979 return dict(cls.STATUSES).get(value)
2980 2980
2981 2981 @property
2982 2982 def status_lbl(self):
2983 2983 return ChangesetStatus.get_status_lbl(self.status)
2984 2984
2985 2985
2986 2986 class _PullRequestBase(BaseModel):
2987 2987 """
2988 2988 Common attributes of pull request and version entries.
2989 2989 """
2990 2990
2991 2991 # .status values
2992 2992 STATUS_NEW = u'new'
2993 2993 STATUS_OPEN = u'open'
2994 2994 STATUS_CLOSED = u'closed'
2995 2995
2996 2996 title = Column('title', Unicode(255), nullable=True)
2997 2997 description = Column(
2998 2998 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
2999 2999 nullable=True)
3000 3000 # new/open/closed status of pull request (not approve/reject/etc)
3001 3001 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
3002 3002 created_on = Column(
3003 3003 'created_on', DateTime(timezone=False), nullable=False,
3004 3004 default=datetime.datetime.now)
3005 3005 updated_on = Column(
3006 3006 'updated_on', DateTime(timezone=False), nullable=False,
3007 3007 default=datetime.datetime.now)
3008 3008
3009 3009 @declared_attr
3010 3010 def user_id(cls):
3011 3011 return Column(
3012 3012 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
3013 3013 unique=None)
3014 3014
3015 3015 # 500 revisions max
3016 3016 _revisions = Column(
3017 3017 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
3018 3018
3019 3019 @declared_attr
3020 3020 def source_repo_id(cls):
3021 3021 # TODO: dan: rename column to source_repo_id
3022 3022 return Column(
3023 3023 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3024 3024 nullable=False)
3025 3025
3026 3026 source_ref = Column('org_ref', Unicode(255), nullable=False)
3027 3027
3028 3028 @declared_attr
3029 3029 def target_repo_id(cls):
3030 3030 # TODO: dan: rename column to target_repo_id
3031 3031 return Column(
3032 3032 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3033 3033 nullable=False)
3034 3034
3035 3035 target_ref = Column('other_ref', Unicode(255), nullable=False)
3036 3036
3037 3037 # TODO: dan: rename column to last_merge_source_rev
3038 3038 _last_merge_source_rev = Column(
3039 3039 'last_merge_org_rev', String(40), nullable=True)
3040 3040 # TODO: dan: rename column to last_merge_target_rev
3041 3041 _last_merge_target_rev = Column(
3042 3042 'last_merge_other_rev', String(40), nullable=True)
3043 3043 _last_merge_status = Column('merge_status', Integer(), nullable=True)
3044 3044 merge_rev = Column('merge_rev', String(40), nullable=True)
3045 3045
3046 3046 @hybrid_property
3047 3047 def revisions(self):
3048 3048 return self._revisions.split(':') if self._revisions else []
3049 3049
3050 3050 @revisions.setter
3051 3051 def revisions(self, val):
3052 3052 self._revisions = ':'.join(val)
3053 3053
3054 3054 @declared_attr
3055 3055 def author(cls):
3056 3056 return relationship('User', lazy='joined')
3057 3057
3058 3058 @declared_attr
3059 3059 def source_repo(cls):
3060 3060 return relationship(
3061 3061 'Repository',
3062 3062 primaryjoin='%s.source_repo_id==Repository.repo_id' % cls.__name__)
3063 3063
3064 3064 @property
3065 3065 def source_ref_parts(self):
3066 3066 refs = self.source_ref.split(':')
3067 3067 return Reference(refs[0], refs[1], refs[2])
3068 3068
3069 3069 @declared_attr
3070 3070 def target_repo(cls):
3071 3071 return relationship(
3072 3072 'Repository',
3073 3073 primaryjoin='%s.target_repo_id==Repository.repo_id' % cls.__name__)
3074 3074
3075 3075 @property
3076 3076 def target_ref_parts(self):
3077 3077 refs = self.target_ref.split(':')
3078 3078 return Reference(refs[0], refs[1], refs[2])
3079 3079
3080 3080
3081 3081 class PullRequest(Base, _PullRequestBase):
3082 3082 __tablename__ = 'pull_requests'
3083 3083 __table_args__ = (
3084 3084 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3085 3085 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3086 3086 )
3087 3087
3088 3088 pull_request_id = Column(
3089 3089 'pull_request_id', Integer(), nullable=False, primary_key=True)
3090 3090
3091 3091 def __repr__(self):
3092 3092 if self.pull_request_id:
3093 3093 return '<DB:PullRequest #%s>' % self.pull_request_id
3094 3094 else:
3095 3095 return '<DB:PullRequest at %#x>' % id(self)
3096 3096
3097 3097 reviewers = relationship('PullRequestReviewers',
3098 3098 cascade="all, delete, delete-orphan")
3099 3099 statuses = relationship('ChangesetStatus')
3100 3100 comments = relationship('ChangesetComment',
3101 3101 cascade="all, delete, delete-orphan")
3102 3102 versions = relationship('PullRequestVersion',
3103 3103 cascade="all, delete, delete-orphan")
3104 3104
3105 3105 def is_closed(self):
3106 3106 return self.status == self.STATUS_CLOSED
3107 3107
3108 3108 def get_api_data(self):
3109 3109 from rhodecode.model.pull_request import PullRequestModel
3110 3110 pull_request = self
3111 3111 merge_status = PullRequestModel().merge_status(pull_request)
3112 3112 data = {
3113 3113 'pull_request_id': pull_request.pull_request_id,
3114 3114 'url': url('pullrequest_show', repo_name=self.target_repo.repo_name,
3115 3115 pull_request_id=self.pull_request_id,
3116 3116 qualified=True),
3117 3117 'title': pull_request.title,
3118 3118 'description': pull_request.description,
3119 3119 'status': pull_request.status,
3120 3120 'created_on': pull_request.created_on,
3121 3121 'updated_on': pull_request.updated_on,
3122 3122 'commit_ids': pull_request.revisions,
3123 3123 'review_status': pull_request.calculated_review_status(),
3124 3124 'mergeable': {
3125 3125 'status': merge_status[0],
3126 3126 'message': unicode(merge_status[1]),
3127 3127 },
3128 3128 'source': {
3129 3129 'clone_url': pull_request.source_repo.clone_url(),
3130 3130 'repository': pull_request.source_repo.repo_name,
3131 3131 'reference': {
3132 3132 'name': pull_request.source_ref_parts.name,
3133 3133 'type': pull_request.source_ref_parts.type,
3134 3134 'commit_id': pull_request.source_ref_parts.commit_id,
3135 3135 },
3136 3136 },
3137 3137 'target': {
3138 3138 'clone_url': pull_request.target_repo.clone_url(),
3139 3139 'repository': pull_request.target_repo.repo_name,
3140 3140 'reference': {
3141 3141 'name': pull_request.target_ref_parts.name,
3142 3142 'type': pull_request.target_ref_parts.type,
3143 3143 'commit_id': pull_request.target_ref_parts.commit_id,
3144 3144 },
3145 3145 },
3146 3146 'author': pull_request.author.get_api_data(include_secrets=False,
3147 3147 details='basic'),
3148 3148 'reviewers': [
3149 3149 {
3150 3150 'user': reviewer.get_api_data(include_secrets=False,
3151 3151 details='basic'),
3152 'reasons': reasons,
3152 3153 'review_status': st[0][1].status if st else 'not_reviewed',
3153 3154 }
3154 for reviewer, st in pull_request.reviewers_statuses()
3155 for reviewer, reasons, st in pull_request.reviewers_statuses()
3155 3156 ]
3156 3157 }
3157 3158
3158 3159 return data
3159 3160
3160 3161 def __json__(self):
3161 3162 return {
3162 3163 'revisions': self.revisions,
3163 3164 }
3164 3165
3165 3166 def calculated_review_status(self):
3166 3167 # TODO: anderson: 13.05.15 Used only on templates/my_account_pullrequests.html
3167 3168 # because it's tricky on how to use ChangesetStatusModel from there
3168 3169 warnings.warn("Use calculated_review_status from ChangesetStatusModel", DeprecationWarning)
3169 3170 from rhodecode.model.changeset_status import ChangesetStatusModel
3170 3171 return ChangesetStatusModel().calculated_review_status(self)
3171 3172
3172 3173 def reviewers_statuses(self):
3173 3174 warnings.warn("Use reviewers_statuses from ChangesetStatusModel", DeprecationWarning)
3174 3175 from rhodecode.model.changeset_status import ChangesetStatusModel
3175 3176 return ChangesetStatusModel().reviewers_statuses(self)
3176 3177
3177 3178
3178 3179 class PullRequestVersion(Base, _PullRequestBase):
3179 3180 __tablename__ = 'pull_request_versions'
3180 3181 __table_args__ = (
3181 3182 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3182 3183 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3183 3184 )
3184 3185
3185 3186 pull_request_version_id = Column(
3186 3187 'pull_request_version_id', Integer(), nullable=False, primary_key=True)
3187 3188 pull_request_id = Column(
3188 3189 'pull_request_id', Integer(),
3189 3190 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3190 3191 pull_request = relationship('PullRequest')
3191 3192
3192 3193 def __repr__(self):
3193 3194 if self.pull_request_version_id:
3194 3195 return '<DB:PullRequestVersion #%s>' % self.pull_request_version_id
3195 3196 else:
3196 3197 return '<DB:PullRequestVersion at %#x>' % id(self)
3197 3198
3198 3199
3199 3200 class PullRequestReviewers(Base, BaseModel):
3200 3201 __tablename__ = 'pull_request_reviewers'
3201 3202 __table_args__ = (
3202 3203 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3203 3204 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3204 3205 )
3205 3206
3206 def __init__(self, user=None, pull_request=None):
3207 def __init__(self, user=None, pull_request=None, reasons=None):
3207 3208 self.user = user
3208 3209 self.pull_request = pull_request
3210 self.reasons = reasons or []
3211
3212 @hybrid_property
3213 def reasons(self):
3214 if not self._reasons:
3215 return []
3216 return self._reasons
3217
3218 @reasons.setter
3219 def reasons(self, val):
3220 val = val or []
3221 if any(not isinstance(x, basestring) for x in val):
3222 raise Exception('invalid reasons type, must be list of strings')
3223 self._reasons = val
3209 3224
3210 3225 pull_requests_reviewers_id = Column(
3211 3226 'pull_requests_reviewers_id', Integer(), nullable=False,
3212 3227 primary_key=True)
3213 3228 pull_request_id = Column(
3214 3229 "pull_request_id", Integer(),
3215 3230 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3216 3231 user_id = Column(
3217 3232 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
3218 reason = Column('reason',
3219 UnicodeText().with_variant(UnicodeText(255), 'mysql'), nullable=True)
3233 _reasons = Column(
3234 'reason', MutationList.as_mutable(
3235 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
3220 3236
3221 3237 user = relationship('User')
3222 3238 pull_request = relationship('PullRequest')
3223 3239
3224 3240
3225 3241 class Notification(Base, BaseModel):
3226 3242 __tablename__ = 'notifications'
3227 3243 __table_args__ = (
3228 3244 Index('notification_type_idx', 'type'),
3229 3245 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3230 3246 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3231 3247 )
3232 3248
3233 3249 TYPE_CHANGESET_COMMENT = u'cs_comment'
3234 3250 TYPE_MESSAGE = u'message'
3235 3251 TYPE_MENTION = u'mention'
3236 3252 TYPE_REGISTRATION = u'registration'
3237 3253 TYPE_PULL_REQUEST = u'pull_request'
3238 3254 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
3239 3255
3240 3256 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
3241 3257 subject = Column('subject', Unicode(512), nullable=True)
3242 3258 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
3243 3259 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
3244 3260 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3245 3261 type_ = Column('type', Unicode(255))
3246 3262
3247 3263 created_by_user = relationship('User')
3248 3264 notifications_to_users = relationship('UserNotification', lazy='joined',
3249 3265 cascade="all, delete, delete-orphan")
3250 3266
3251 3267 @property
3252 3268 def recipients(self):
3253 3269 return [x.user for x in UserNotification.query()\
3254 3270 .filter(UserNotification.notification == self)\
3255 3271 .order_by(UserNotification.user_id.asc()).all()]
3256 3272
3257 3273 @classmethod
3258 3274 def create(cls, created_by, subject, body, recipients, type_=None):
3259 3275 if type_ is None:
3260 3276 type_ = Notification.TYPE_MESSAGE
3261 3277
3262 3278 notification = cls()
3263 3279 notification.created_by_user = created_by
3264 3280 notification.subject = subject
3265 3281 notification.body = body
3266 3282 notification.type_ = type_
3267 3283 notification.created_on = datetime.datetime.now()
3268 3284
3269 3285 for u in recipients:
3270 3286 assoc = UserNotification()
3271 3287 assoc.notification = notification
3272 3288
3273 3289 # if created_by is inside recipients mark his notification
3274 3290 # as read
3275 3291 if u.user_id == created_by.user_id:
3276 3292 assoc.read = True
3277 3293
3278 3294 u.notifications.append(assoc)
3279 3295 Session().add(notification)
3280 3296
3281 3297 return notification
3282 3298
3283 3299 @property
3284 3300 def description(self):
3285 3301 from rhodecode.model.notification import NotificationModel
3286 3302 return NotificationModel().make_description(self)
3287 3303
3288 3304
3289 3305 class UserNotification(Base, BaseModel):
3290 3306 __tablename__ = 'user_to_notification'
3291 3307 __table_args__ = (
3292 3308 UniqueConstraint('user_id', 'notification_id'),
3293 3309 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3294 3310 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3295 3311 )
3296 3312 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
3297 3313 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
3298 3314 read = Column('read', Boolean, default=False)
3299 3315 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
3300 3316
3301 3317 user = relationship('User', lazy="joined")
3302 3318 notification = relationship('Notification', lazy="joined",
3303 3319 order_by=lambda: Notification.created_on.desc(),)
3304 3320
3305 3321 def mark_as_read(self):
3306 3322 self.read = True
3307 3323 Session().add(self)
3308 3324
3309 3325
3310 3326 class Gist(Base, BaseModel):
3311 3327 __tablename__ = 'gists'
3312 3328 __table_args__ = (
3313 3329 Index('g_gist_access_id_idx', 'gist_access_id'),
3314 3330 Index('g_created_on_idx', 'created_on'),
3315 3331 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3316 3332 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3317 3333 )
3318 3334 GIST_PUBLIC = u'public'
3319 3335 GIST_PRIVATE = u'private'
3320 3336 DEFAULT_FILENAME = u'gistfile1.txt'
3321 3337
3322 3338 ACL_LEVEL_PUBLIC = u'acl_public'
3323 3339 ACL_LEVEL_PRIVATE = u'acl_private'
3324 3340
3325 3341 gist_id = Column('gist_id', Integer(), primary_key=True)
3326 3342 gist_access_id = Column('gist_access_id', Unicode(250))
3327 3343 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
3328 3344 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
3329 3345 gist_expires = Column('gist_expires', Float(53), nullable=False)
3330 3346 gist_type = Column('gist_type', Unicode(128), nullable=False)
3331 3347 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3332 3348 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3333 3349 acl_level = Column('acl_level', Unicode(128), nullable=True)
3334 3350
3335 3351 owner = relationship('User')
3336 3352
3337 3353 def __repr__(self):
3338 3354 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
3339 3355
3340 3356 @classmethod
3341 3357 def get_or_404(cls, id_):
3342 3358 res = cls.query().filter(cls.gist_access_id == id_).scalar()
3343 3359 if not res:
3344 3360 raise HTTPNotFound
3345 3361 return res
3346 3362
3347 3363 @classmethod
3348 3364 def get_by_access_id(cls, gist_access_id):
3349 3365 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
3350 3366
3351 3367 def gist_url(self):
3352 3368 import rhodecode
3353 3369 alias_url = rhodecode.CONFIG.get('gist_alias_url')
3354 3370 if alias_url:
3355 3371 return alias_url.replace('{gistid}', self.gist_access_id)
3356 3372
3357 3373 return url('gist', gist_id=self.gist_access_id, qualified=True)
3358 3374
3359 3375 @classmethod
3360 3376 def base_path(cls):
3361 3377 """
3362 3378 Returns base path when all gists are stored
3363 3379
3364 3380 :param cls:
3365 3381 """
3366 3382 from rhodecode.model.gist import GIST_STORE_LOC
3367 3383 q = Session().query(RhodeCodeUi)\
3368 3384 .filter(RhodeCodeUi.ui_key == URL_SEP)
3369 3385 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
3370 3386 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
3371 3387
3372 3388 def get_api_data(self):
3373 3389 """
3374 3390 Common function for generating gist related data for API
3375 3391 """
3376 3392 gist = self
3377 3393 data = {
3378 3394 'gist_id': gist.gist_id,
3379 3395 'type': gist.gist_type,
3380 3396 'access_id': gist.gist_access_id,
3381 3397 'description': gist.gist_description,
3382 3398 'url': gist.gist_url(),
3383 3399 'expires': gist.gist_expires,
3384 3400 'created_on': gist.created_on,
3385 3401 'modified_at': gist.modified_at,
3386 3402 'content': None,
3387 3403 'acl_level': gist.acl_level,
3388 3404 }
3389 3405 return data
3390 3406
3391 3407 def __json__(self):
3392 3408 data = dict(
3393 3409 )
3394 3410 data.update(self.get_api_data())
3395 3411 return data
3396 3412 # SCM functions
3397 3413
3398 3414 def scm_instance(self, **kwargs):
3399 3415 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
3400 3416 return get_vcs_instance(
3401 3417 repo_path=safe_str(full_repo_path), create=False)
3402 3418
3403 3419
3404 3420 class DbMigrateVersion(Base, BaseModel):
3405 3421 __tablename__ = 'db_migrate_version'
3406 3422 __table_args__ = (
3407 3423 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3408 3424 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3409 3425 )
3410 3426 repository_id = Column('repository_id', String(250), primary_key=True)
3411 3427 repository_path = Column('repository_path', Text)
3412 3428 version = Column('version', Integer)
3413 3429
3414 3430
3415 3431 class ExternalIdentity(Base, BaseModel):
3416 3432 __tablename__ = 'external_identities'
3417 3433 __table_args__ = (
3418 3434 Index('local_user_id_idx', 'local_user_id'),
3419 3435 Index('external_id_idx', 'external_id'),
3420 3436 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3421 3437 'mysql_charset': 'utf8'})
3422 3438
3423 3439 external_id = Column('external_id', Unicode(255), default=u'',
3424 3440 primary_key=True)
3425 3441 external_username = Column('external_username', Unicode(1024), default=u'')
3426 3442 local_user_id = Column('local_user_id', Integer(),
3427 3443 ForeignKey('users.user_id'), primary_key=True)
3428 3444 provider_name = Column('provider_name', Unicode(255), default=u'',
3429 3445 primary_key=True)
3430 3446 access_token = Column('access_token', String(1024), default=u'')
3431 3447 alt_token = Column('alt_token', String(1024), default=u'')
3432 3448 token_secret = Column('token_secret', String(1024), default=u'')
3433 3449
3434 3450 @classmethod
3435 3451 def by_external_id_and_provider(cls, external_id, provider_name,
3436 3452 local_user_id=None):
3437 3453 """
3438 3454 Returns ExternalIdentity instance based on search params
3439 3455
3440 3456 :param external_id:
3441 3457 :param provider_name:
3442 3458 :return: ExternalIdentity
3443 3459 """
3444 3460 query = cls.query()
3445 3461 query = query.filter(cls.external_id == external_id)
3446 3462 query = query.filter(cls.provider_name == provider_name)
3447 3463 if local_user_id:
3448 3464 query = query.filter(cls.local_user_id == local_user_id)
3449 3465 return query.first()
3450 3466
3451 3467 @classmethod
3452 3468 def user_by_external_id_and_provider(cls, external_id, provider_name):
3453 3469 """
3454 3470 Returns User instance based on search params
3455 3471
3456 3472 :param external_id:
3457 3473 :param provider_name:
3458 3474 :return: User
3459 3475 """
3460 3476 query = User.query()
3461 3477 query = query.filter(cls.external_id == external_id)
3462 3478 query = query.filter(cls.provider_name == provider_name)
3463 3479 query = query.filter(User.user_id == cls.local_user_id)
3464 3480 return query.first()
3465 3481
3466 3482 @classmethod
3467 3483 def by_local_user_id(cls, local_user_id):
3468 3484 """
3469 3485 Returns all tokens for user
3470 3486
3471 3487 :param local_user_id:
3472 3488 :return: ExternalIdentity
3473 3489 """
3474 3490 query = cls.query()
3475 3491 query = query.filter(cls.local_user_id == local_user_id)
3476 3492 return query
3477 3493
3478 3494
3479 3495 class Integration(Base, BaseModel):
3480 3496 __tablename__ = 'integrations'
3481 3497 __table_args__ = (
3482 3498 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3483 3499 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3484 3500 )
3485 3501
3486 3502 integration_id = Column('integration_id', Integer(), primary_key=True)
3487 3503 integration_type = Column('integration_type', String(255))
3488 3504 enabled = Column('enabled', Boolean(), nullable=False)
3489 3505 name = Column('name', String(255), nullable=False)
3490 3506 child_repos_only = Column('child_repos_only', Boolean(), nullable=False,
3491 3507 default=False)
3492 3508
3493 3509 settings = Column(
3494 3510 'settings_json', MutationObj.as_mutable(
3495 3511 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
3496 3512 repo_id = Column(
3497 3513 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
3498 3514 nullable=True, unique=None, default=None)
3499 3515 repo = relationship('Repository', lazy='joined')
3500 3516
3501 3517 repo_group_id = Column(
3502 3518 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
3503 3519 nullable=True, unique=None, default=None)
3504 3520 repo_group = relationship('RepoGroup', lazy='joined')
3505 3521
3506 3522 @property
3507 3523 def scope(self):
3508 3524 if self.repo:
3509 3525 return repr(self.repo)
3510 3526 if self.repo_group:
3511 3527 if self.child_repos_only:
3512 3528 return repr(self.repo_group) + ' (child repos only)'
3513 3529 else:
3514 3530 return repr(self.repo_group) + ' (recursive)'
3515 3531 if self.child_repos_only:
3516 3532 return 'root_repos'
3517 3533 return 'global'
3518 3534
3519 3535 def __repr__(self):
3520 3536 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
3521 3537
3522 3538
3523 3539 class RepoReviewRuleUser(Base, BaseModel):
3524 3540 __tablename__ = 'repo_review_rules_users'
3525 3541 __table_args__ = (
3526 3542 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3527 3543 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3528 3544 )
3529 3545 repo_review_rule_user_id = Column(
3530 3546 'repo_review_rule_user_id', Integer(), primary_key=True)
3531 3547 repo_review_rule_id = Column("repo_review_rule_id",
3532 3548 Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3533 3549 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'),
3534 3550 nullable=False)
3535 3551 user = relationship('User')
3536 3552
3537 3553
3538 3554 class RepoReviewRuleUserGroup(Base, BaseModel):
3539 3555 __tablename__ = 'repo_review_rules_users_groups'
3540 3556 __table_args__ = (
3541 3557 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3542 3558 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3543 3559 )
3544 3560 repo_review_rule_users_group_id = Column(
3545 3561 'repo_review_rule_users_group_id', Integer(), primary_key=True)
3546 3562 repo_review_rule_id = Column("repo_review_rule_id",
3547 3563 Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3548 3564 users_group_id = Column("users_group_id", Integer(),
3549 3565 ForeignKey('users_groups.users_group_id'), nullable=False)
3550 3566 users_group = relationship('UserGroup')
3551 3567
3552 3568
3553 3569 class RepoReviewRule(Base, BaseModel):
3554 3570 __tablename__ = 'repo_review_rules'
3555 3571 __table_args__ = (
3556 3572 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3557 3573 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3558 3574 )
3559 3575
3560 3576 repo_review_rule_id = Column(
3561 3577 'repo_review_rule_id', Integer(), primary_key=True)
3562 3578 repo_id = Column(
3563 3579 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
3564 3580 repo = relationship('Repository', backref='review_rules')
3565 3581
3566 3582 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'),
3567 3583 default=u'*') # glob
3568 3584 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'),
3569 3585 default=u'*') # glob
3570 3586
3571 3587 use_authors_for_review = Column("use_authors_for_review", Boolean(),
3572 3588 nullable=False, default=False)
3573 3589 rule_users = relationship('RepoReviewRuleUser')
3574 3590 rule_user_groups = relationship('RepoReviewRuleUserGroup')
3575 3591
3576 3592 @hybrid_property
3577 3593 def branch_pattern(self):
3578 3594 return self._branch_pattern or '*'
3579 3595
3580 3596 def _validate_glob(self, value):
3581 3597 re.compile('^' + glob2re(value) + '$')
3582 3598
3583 3599 @branch_pattern.setter
3584 3600 def branch_pattern(self, value):
3585 3601 self._validate_glob(value)
3586 3602 self._branch_pattern = value or '*'
3587 3603
3588 3604 @hybrid_property
3589 3605 def file_pattern(self):
3590 3606 return self._file_pattern or '*'
3591 3607
3592 3608 @file_pattern.setter
3593 3609 def file_pattern(self, value):
3594 3610 self._validate_glob(value)
3595 3611 self._file_pattern = value or '*'
3596 3612
3597 3613 def matches(self, branch, files_changed):
3598 3614 """
3599 3615 Check if this review rule matches a branch/files in a pull request
3600 3616
3601 3617 :param branch: branch name for the commit
3602 3618 :param files_changed: list of file paths changed in the pull request
3603 3619 """
3604 3620
3605 3621 branch = branch or ''
3606 3622 files_changed = files_changed or []
3607 3623
3608 3624 branch_matches = True
3609 3625 if branch:
3610 3626 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
3611 3627 branch_matches = bool(branch_regex.search(branch))
3612 3628
3613 3629 files_matches = True
3614 3630 if self.file_pattern != '*':
3615 3631 files_matches = False
3616 3632 file_regex = re.compile(glob2re(self.file_pattern))
3617 3633 for filename in files_changed:
3618 3634 if file_regex.search(filename):
3619 3635 files_matches = True
3620 3636 break
3621 3637
3622 3638 return branch_matches and files_matches
3623 3639
3624 3640 @property
3625 3641 def review_users(self):
3626 3642 """ Returns the users which this rule applies to """
3627 3643
3628 3644 users = set()
3629 3645 users |= set([
3630 3646 rule_user.user for rule_user in self.rule_users
3631 3647 if rule_user.user.active])
3632 3648 users |= set(
3633 3649 member.user
3634 3650 for rule_user_group in self.rule_user_groups
3635 3651 for member in rule_user_group.users_group.members
3636 3652 if member.user.active
3637 3653 )
3638 3654 return users
3639 3655
3640 3656 def __repr__(self):
3641 3657 return '<RepoReviewerRule(id=%r, repo=%r)>' % (
3642 3658 self.repo_review_rule_id, self.repo)
@@ -1,34 +1,34 b''
1 1 import logging
2 2 import datetime
3 3
4 4 from sqlalchemy import *
5 5 from sqlalchemy.exc import DatabaseError
6 6 from sqlalchemy.orm import relation, backref, class_mapper, joinedload
7 7 from sqlalchemy.orm.session import Session
8 8 from sqlalchemy.ext.declarative import declarative_base
9 9
10 10 from rhodecode.lib.dbmigrate.migrate import *
11 11 from rhodecode.lib.dbmigrate.migrate.changeset import *
12 12 from rhodecode.lib.utils2 import str2bool
13 13
14 14 from rhodecode.model.meta import Base
15 15 from rhodecode.model import meta
16 16 from rhodecode.lib.dbmigrate.versions import _reset_base, notify
17 17
18 18 log = logging.getLogger(__name__)
19 19
20 20
21 21 def upgrade(migrate_engine):
22 22 """
23 23 Upgrade operations go here.
24 24 Don't create your own engine; bind migrate_engine to your metadata
25 25 """
26 26 _reset_base(migrate_engine)
27 27 from rhodecode.lib.dbmigrate.schema import db_4_5_0_0
28 28
29 db_4_5_0_0.PullRequestReviewers.reason.create(
29 db_4_5_0_0.PullRequestReviewers.reasons.create(
30 30 table=db_4_5_0_0.PullRequestReviewers.__table__)
31 31
32 32 def downgrade(migrate_engine):
33 33 meta = MetaData()
34 34 meta.bind = migrate_engine
@@ -1,268 +1,268 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Changeset status conttroller
23 23 """
24 24
25 25 import itertools
26 26 import logging
27 27 from collections import defaultdict
28 28
29 29 from rhodecode.model import BaseModel
30 30 from rhodecode.model.db import ChangesetStatus, ChangesetComment, PullRequest
31 31 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
32 32 from rhodecode.lib.markup_renderer import (
33 33 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
34 34
35 35 log = logging.getLogger(__name__)
36 36
37 37
38 38 class ChangesetStatusModel(BaseModel):
39 39
40 40 cls = ChangesetStatus
41 41
42 42 def __get_changeset_status(self, changeset_status):
43 43 return self._get_instance(ChangesetStatus, changeset_status)
44 44
45 45 def __get_pull_request(self, pull_request):
46 46 return self._get_instance(PullRequest, pull_request)
47 47
48 48 def _get_status_query(self, repo, revision, pull_request,
49 49 with_revisions=False):
50 50 repo = self._get_repo(repo)
51 51
52 52 q = ChangesetStatus.query()\
53 53 .filter(ChangesetStatus.repo == repo)
54 54 if not with_revisions:
55 55 q = q.filter(ChangesetStatus.version == 0)
56 56
57 57 if revision:
58 58 q = q.filter(ChangesetStatus.revision == revision)
59 59 elif pull_request:
60 60 pull_request = self.__get_pull_request(pull_request)
61 61 # TODO: johbo: Think about the impact of this join, there must
62 62 # be a reason why ChangesetStatus and ChanagesetComment is linked
63 63 # to the pull request. Might be that we want to do the same for
64 64 # the pull_request_version_id.
65 65 q = q.join(ChangesetComment).filter(
66 66 ChangesetStatus.pull_request == pull_request,
67 67 ChangesetComment.pull_request_version_id == None)
68 68 else:
69 69 raise Exception('Please specify revision or pull_request')
70 70 q = q.order_by(ChangesetStatus.version.asc())
71 71 return q
72 72
73 73 def calculate_status(self, statuses_by_reviewers):
74 74 """
75 75 Given the approval statuses from reviewers, calculates final approval
76 76 status. There can only be 3 results, all approved, all rejected. If
77 77 there is no consensus the PR is under review.
78 78
79 79 :param statuses_by_reviewers:
80 80 """
81 81 votes = defaultdict(int)
82 82 reviewers_number = len(statuses_by_reviewers)
83 for user, statuses in statuses_by_reviewers:
83 for user, reasons, statuses in statuses_by_reviewers:
84 84 if statuses:
85 85 ver, latest = statuses[0]
86 86 votes[latest.status] += 1
87 87 else:
88 88 votes[ChangesetStatus.DEFAULT] += 1
89 89
90 90 # all approved
91 91 if votes.get(ChangesetStatus.STATUS_APPROVED) == reviewers_number:
92 92 return ChangesetStatus.STATUS_APPROVED
93 93
94 94 # all rejected
95 95 if votes.get(ChangesetStatus.STATUS_REJECTED) == reviewers_number:
96 96 return ChangesetStatus.STATUS_REJECTED
97 97
98 98 return ChangesetStatus.STATUS_UNDER_REVIEW
99 99
100 100 def get_statuses(self, repo, revision=None, pull_request=None,
101 101 with_revisions=False):
102 102 q = self._get_status_query(repo, revision, pull_request,
103 103 with_revisions)
104 104 return q.all()
105 105
106 106 def get_status(self, repo, revision=None, pull_request=None, as_str=True):
107 107 """
108 108 Returns latest status of changeset for given revision or for given
109 109 pull request. Statuses are versioned inside a table itself and
110 110 version == 0 is always the current one
111 111
112 112 :param repo:
113 113 :param revision: 40char hash or None
114 114 :param pull_request: pull_request reference
115 115 :param as_str: return status as string not object
116 116 """
117 117 q = self._get_status_query(repo, revision, pull_request)
118 118
119 119 # need to use first here since there can be multiple statuses
120 120 # returned from pull_request
121 121 status = q.first()
122 122 if as_str:
123 123 status = status.status if status else status
124 124 st = status or ChangesetStatus.DEFAULT
125 125 return str(st)
126 126 return status
127 127
128 128 def _render_auto_status_message(
129 129 self, status, commit_id=None, pull_request=None):
130 130 """
131 131 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
132 132 so it's always looking the same disregarding on which default
133 133 renderer system is using.
134 134
135 135 :param status: status text to change into
136 136 :param commit_id: the commit_id we change the status for
137 137 :param pull_request: the pull request we change the status for
138 138 """
139 139
140 140 new_status = ChangesetStatus.get_status_lbl(status)
141 141
142 142 params = {
143 143 'new_status_label': new_status,
144 144 'pull_request': pull_request,
145 145 'commit_id': commit_id,
146 146 }
147 147 renderer = RstTemplateRenderer()
148 148 return renderer.render('auto_status_change.mako', **params)
149 149
150 150 def set_status(self, repo, status, user, comment=None, revision=None,
151 151 pull_request=None, dont_allow_on_closed_pull_request=False):
152 152 """
153 153 Creates new status for changeset or updates the old ones bumping their
154 154 version, leaving the current status at
155 155
156 156 :param repo:
157 157 :param revision:
158 158 :param status:
159 159 :param user:
160 160 :param comment:
161 161 :param dont_allow_on_closed_pull_request: don't allow a status change
162 162 if last status was for pull request and it's closed. We shouldn't
163 163 mess around this manually
164 164 """
165 165 repo = self._get_repo(repo)
166 166
167 167 q = ChangesetStatus.query()
168 168
169 169 if revision:
170 170 q = q.filter(ChangesetStatus.repo == repo)
171 171 q = q.filter(ChangesetStatus.revision == revision)
172 172 elif pull_request:
173 173 pull_request = self.__get_pull_request(pull_request)
174 174 q = q.filter(ChangesetStatus.repo == pull_request.source_repo)
175 175 q = q.filter(ChangesetStatus.revision.in_(pull_request.revisions))
176 176 cur_statuses = q.all()
177 177
178 178 # if statuses exists and last is associated with a closed pull request
179 179 # we need to check if we can allow this status change
180 180 if (dont_allow_on_closed_pull_request and cur_statuses
181 181 and getattr(cur_statuses[0].pull_request, 'status', '')
182 182 == PullRequest.STATUS_CLOSED):
183 183 raise StatusChangeOnClosedPullRequestError(
184 184 'Changing status on closed pull request is not allowed'
185 185 )
186 186
187 187 # update all current statuses with older version
188 188 if cur_statuses:
189 189 for st in cur_statuses:
190 190 st.version += 1
191 191 self.sa.add(st)
192 192
193 193 def _create_status(user, repo, status, comment, revision, pull_request):
194 194 new_status = ChangesetStatus()
195 195 new_status.author = self._get_user(user)
196 196 new_status.repo = self._get_repo(repo)
197 197 new_status.status = status
198 198 new_status.comment = comment
199 199 new_status.revision = revision
200 200 new_status.pull_request = pull_request
201 201 return new_status
202 202
203 203 if not comment:
204 204 from rhodecode.model.comment import ChangesetCommentsModel
205 205 comment = ChangesetCommentsModel().create(
206 206 text=self._render_auto_status_message(
207 207 status, commit_id=revision, pull_request=pull_request),
208 208 repo=repo,
209 209 user=user,
210 210 pull_request=pull_request,
211 211 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER
212 212 )
213 213
214 214 if revision:
215 215 new_status = _create_status(
216 216 user=user, repo=repo, status=status, comment=comment,
217 217 revision=revision, pull_request=pull_request)
218 218 self.sa.add(new_status)
219 219 return new_status
220 220 elif pull_request:
221 221 # pull request can have more than one revision associated to it
222 222 # we need to create new version for each one
223 223 new_statuses = []
224 224 repo = pull_request.source_repo
225 225 for rev in pull_request.revisions:
226 226 new_status = _create_status(
227 227 user=user, repo=repo, status=status, comment=comment,
228 228 revision=rev, pull_request=pull_request)
229 229 new_statuses.append(new_status)
230 230 self.sa.add(new_status)
231 231 return new_statuses
232 232
233 233 def reviewers_statuses(self, pull_request):
234 234 _commit_statuses = self.get_statuses(
235 235 pull_request.source_repo,
236 236 pull_request=pull_request,
237 237 with_revisions=True)
238 238
239 239 commit_statuses = defaultdict(list)
240 240 for st in _commit_statuses:
241 241 commit_statuses[st.author.username] += [st]
242 242
243 243 pull_request_reviewers = []
244 244
245 245 def version(commit_status):
246 246 return commit_status.version
247 247
248 248 for o in pull_request.reviewers:
249 249 if not o.user:
250 250 continue
251 251 st = commit_statuses.get(o.user.username, None)
252 252 if st:
253 253 st = [(x, list(y)[0])
254 254 for x, y in (itertools.groupby(sorted(st, key=version),
255 255 version))]
256 256
257 pull_request_reviewers.append([o.user, st])
257 pull_request_reviewers.append((o.user, o.reasons, st))
258 258 return pull_request_reviewers
259 259
260 260 def calculated_review_status(self, pull_request, reviewers_statuses=None):
261 261 """
262 262 calculate pull request status based on reviewers, it should be a list
263 263 of two element lists.
264 264
265 265 :param reviewers_statuses:
266 266 """
267 267 reviewers = reviewers_statuses or self.reviewers_statuses(pull_request)
268 268 return self.calculate_status(reviewers)
@@ -1,3642 +1,3658 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Database Models for RhodeCode Enterprise
23 23 """
24 24
25 25 import re
26 26 import os
27 27 import sys
28 28 import time
29 29 import hashlib
30 30 import logging
31 31 import datetime
32 32 import warnings
33 33 import ipaddress
34 34 import functools
35 35 import traceback
36 36 import collections
37 37
38 38
39 39 from sqlalchemy import *
40 40 from sqlalchemy.exc import IntegrityError
41 41 from sqlalchemy.ext.declarative import declared_attr
42 42 from sqlalchemy.ext.hybrid import hybrid_property
43 43 from sqlalchemy.orm import (
44 44 relationship, joinedload, class_mapper, validates, aliased)
45 45 from sqlalchemy.sql.expression import true
46 46 from beaker.cache import cache_region, region_invalidate
47 47 from webob.exc import HTTPNotFound
48 48 from zope.cachedescriptors.property import Lazy as LazyProperty
49 49
50 50 from pylons import url
51 51 from pylons.i18n.translation import lazy_ugettext as _
52 52
53 53 from rhodecode.lib.vcs import get_backend, get_vcs_instance
54 54 from rhodecode.lib.vcs.utils.helpers import get_scm
55 55 from rhodecode.lib.vcs.exceptions import VCSError
56 56 from rhodecode.lib.vcs.backends.base import (
57 57 EmptyCommit, Reference, MergeFailureReason)
58 58 from rhodecode.lib.utils2 import (
59 59 str2bool, safe_str, get_commit_safe, safe_unicode, remove_prefix, md5_safe,
60 60 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
61 61 glob2re)
62 from rhodecode.lib.jsonalchemy import MutationObj, JsonType, JSONDict
62 from rhodecode.lib.jsonalchemy import MutationObj, MutationList, JsonType, JSONDict
63 63 from rhodecode.lib.ext_json import json
64 64 from rhodecode.lib.caching_query import FromCache
65 65 from rhodecode.lib.encrypt import AESCipher
66 66
67 67 from rhodecode.model.meta import Base, Session
68 68
69 69 URL_SEP = '/'
70 70 log = logging.getLogger(__name__)
71 71
72 72 # =============================================================================
73 73 # BASE CLASSES
74 74 # =============================================================================
75 75
76 76 # this is propagated from .ini file rhodecode.encrypted_values.secret or
77 77 # beaker.session.secret if first is not set.
78 78 # and initialized at environment.py
79 79 ENCRYPTION_KEY = None
80 80
81 81 # used to sort permissions by types, '#' used here is not allowed to be in
82 82 # usernames, and it's very early in sorted string.printable table.
83 83 PERMISSION_TYPE_SORT = {
84 84 'admin': '####',
85 85 'write': '###',
86 86 'read': '##',
87 87 'none': '#',
88 88 }
89 89
90 90
91 91 def display_sort(obj):
92 92 """
93 93 Sort function used to sort permissions in .permissions() function of
94 94 Repository, RepoGroup, UserGroup. Also it put the default user in front
95 95 of all other resources
96 96 """
97 97
98 98 if obj.username == User.DEFAULT_USER:
99 99 return '#####'
100 100 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
101 101 return prefix + obj.username
102 102
103 103
104 104 def _hash_key(k):
105 105 return md5_safe(k)
106 106
107 107
108 108 class EncryptedTextValue(TypeDecorator):
109 109 """
110 110 Special column for encrypted long text data, use like::
111 111
112 112 value = Column("encrypted_value", EncryptedValue(), nullable=False)
113 113
114 114 This column is intelligent so if value is in unencrypted form it return
115 115 unencrypted form, but on save it always encrypts
116 116 """
117 117 impl = Text
118 118
119 119 def process_bind_param(self, value, dialect):
120 120 if not value:
121 121 return value
122 122 if value.startswith('enc$aes$') or value.startswith('enc$aes_hmac$'):
123 123 # protect against double encrypting if someone manually starts
124 124 # doing
125 125 raise ValueError('value needs to be in unencrypted format, ie. '
126 126 'not starting with enc$aes')
127 127 return 'enc$aes_hmac$%s' % AESCipher(
128 128 ENCRYPTION_KEY, hmac=True).encrypt(value)
129 129
130 130 def process_result_value(self, value, dialect):
131 131 import rhodecode
132 132
133 133 if not value:
134 134 return value
135 135
136 136 parts = value.split('$', 3)
137 137 if not len(parts) == 3:
138 138 # probably not encrypted values
139 139 return value
140 140 else:
141 141 if parts[0] != 'enc':
142 142 # parts ok but without our header ?
143 143 return value
144 144 enc_strict_mode = str2bool(rhodecode.CONFIG.get(
145 145 'rhodecode.encrypted_values.strict') or True)
146 146 # at that stage we know it's our encryption
147 147 if parts[1] == 'aes':
148 148 decrypted_data = AESCipher(ENCRYPTION_KEY).decrypt(parts[2])
149 149 elif parts[1] == 'aes_hmac':
150 150 decrypted_data = AESCipher(
151 151 ENCRYPTION_KEY, hmac=True,
152 152 strict_verification=enc_strict_mode).decrypt(parts[2])
153 153 else:
154 154 raise ValueError(
155 155 'Encryption type part is wrong, must be `aes` '
156 156 'or `aes_hmac`, got `%s` instead' % (parts[1]))
157 157 return decrypted_data
158 158
159 159
160 160 class BaseModel(object):
161 161 """
162 162 Base Model for all classes
163 163 """
164 164
165 165 @classmethod
166 166 def _get_keys(cls):
167 167 """return column names for this model """
168 168 return class_mapper(cls).c.keys()
169 169
170 170 def get_dict(self):
171 171 """
172 172 return dict with keys and values corresponding
173 173 to this model data """
174 174
175 175 d = {}
176 176 for k in self._get_keys():
177 177 d[k] = getattr(self, k)
178 178
179 179 # also use __json__() if present to get additional fields
180 180 _json_attr = getattr(self, '__json__', None)
181 181 if _json_attr:
182 182 # update with attributes from __json__
183 183 if callable(_json_attr):
184 184 _json_attr = _json_attr()
185 185 for k, val in _json_attr.iteritems():
186 186 d[k] = val
187 187 return d
188 188
189 189 def get_appstruct(self):
190 190 """return list with keys and values tuples corresponding
191 191 to this model data """
192 192
193 193 l = []
194 194 for k in self._get_keys():
195 195 l.append((k, getattr(self, k),))
196 196 return l
197 197
198 198 def populate_obj(self, populate_dict):
199 199 """populate model with data from given populate_dict"""
200 200
201 201 for k in self._get_keys():
202 202 if k in populate_dict:
203 203 setattr(self, k, populate_dict[k])
204 204
205 205 @classmethod
206 206 def query(cls):
207 207 return Session().query(cls)
208 208
209 209 @classmethod
210 210 def get(cls, id_):
211 211 if id_:
212 212 return cls.query().get(id_)
213 213
214 214 @classmethod
215 215 def get_or_404(cls, id_):
216 216 try:
217 217 id_ = int(id_)
218 218 except (TypeError, ValueError):
219 219 raise HTTPNotFound
220 220
221 221 res = cls.query().get(id_)
222 222 if not res:
223 223 raise HTTPNotFound
224 224 return res
225 225
226 226 @classmethod
227 227 def getAll(cls):
228 228 # deprecated and left for backward compatibility
229 229 return cls.get_all()
230 230
231 231 @classmethod
232 232 def get_all(cls):
233 233 return cls.query().all()
234 234
235 235 @classmethod
236 236 def delete(cls, id_):
237 237 obj = cls.query().get(id_)
238 238 Session().delete(obj)
239 239
240 240 @classmethod
241 241 def identity_cache(cls, session, attr_name, value):
242 242 exist_in_session = []
243 243 for (item_cls, pkey), instance in session.identity_map.items():
244 244 if cls == item_cls and getattr(instance, attr_name) == value:
245 245 exist_in_session.append(instance)
246 246 if exist_in_session:
247 247 if len(exist_in_session) == 1:
248 248 return exist_in_session[0]
249 249 log.exception(
250 250 'multiple objects with attr %s and '
251 251 'value %s found with same name: %r',
252 252 attr_name, value, exist_in_session)
253 253
254 254 def __repr__(self):
255 255 if hasattr(self, '__unicode__'):
256 256 # python repr needs to return str
257 257 try:
258 258 return safe_str(self.__unicode__())
259 259 except UnicodeDecodeError:
260 260 pass
261 261 return '<DB:%s>' % (self.__class__.__name__)
262 262
263 263
264 264 class RhodeCodeSetting(Base, BaseModel):
265 265 __tablename__ = 'rhodecode_settings'
266 266 __table_args__ = (
267 267 UniqueConstraint('app_settings_name'),
268 268 {'extend_existing': True, 'mysql_engine': 'InnoDB',
269 269 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
270 270 )
271 271
272 272 SETTINGS_TYPES = {
273 273 'str': safe_str,
274 274 'int': safe_int,
275 275 'unicode': safe_unicode,
276 276 'bool': str2bool,
277 277 'list': functools.partial(aslist, sep=',')
278 278 }
279 279 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
280 280 GLOBAL_CONF_KEY = 'app_settings'
281 281
282 282 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
283 283 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
284 284 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
285 285 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
286 286
287 287 def __init__(self, key='', val='', type='unicode'):
288 288 self.app_settings_name = key
289 289 self.app_settings_type = type
290 290 self.app_settings_value = val
291 291
292 292 @validates('_app_settings_value')
293 293 def validate_settings_value(self, key, val):
294 294 assert type(val) == unicode
295 295 return val
296 296
297 297 @hybrid_property
298 298 def app_settings_value(self):
299 299 v = self._app_settings_value
300 300 _type = self.app_settings_type
301 301 if _type:
302 302 _type = self.app_settings_type.split('.')[0]
303 303 # decode the encrypted value
304 304 if 'encrypted' in self.app_settings_type:
305 305 cipher = EncryptedTextValue()
306 306 v = safe_unicode(cipher.process_result_value(v, None))
307 307
308 308 converter = self.SETTINGS_TYPES.get(_type) or \
309 309 self.SETTINGS_TYPES['unicode']
310 310 return converter(v)
311 311
312 312 @app_settings_value.setter
313 313 def app_settings_value(self, val):
314 314 """
315 315 Setter that will always make sure we use unicode in app_settings_value
316 316
317 317 :param val:
318 318 """
319 319 val = safe_unicode(val)
320 320 # encode the encrypted value
321 321 if 'encrypted' in self.app_settings_type:
322 322 cipher = EncryptedTextValue()
323 323 val = safe_unicode(cipher.process_bind_param(val, None))
324 324 self._app_settings_value = val
325 325
326 326 @hybrid_property
327 327 def app_settings_type(self):
328 328 return self._app_settings_type
329 329
330 330 @app_settings_type.setter
331 331 def app_settings_type(self, val):
332 332 if val.split('.')[0] not in self.SETTINGS_TYPES:
333 333 raise Exception('type must be one of %s got %s'
334 334 % (self.SETTINGS_TYPES.keys(), val))
335 335 self._app_settings_type = val
336 336
337 337 def __unicode__(self):
338 338 return u"<%s('%s:%s[%s]')>" % (
339 339 self.__class__.__name__,
340 340 self.app_settings_name, self.app_settings_value,
341 341 self.app_settings_type
342 342 )
343 343
344 344
345 345 class RhodeCodeUi(Base, BaseModel):
346 346 __tablename__ = 'rhodecode_ui'
347 347 __table_args__ = (
348 348 UniqueConstraint('ui_key'),
349 349 {'extend_existing': True, 'mysql_engine': 'InnoDB',
350 350 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
351 351 )
352 352
353 353 HOOK_REPO_SIZE = 'changegroup.repo_size'
354 354 # HG
355 355 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
356 356 HOOK_PULL = 'outgoing.pull_logger'
357 357 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
358 358 HOOK_PUSH = 'changegroup.push_logger'
359 359
360 360 # TODO: johbo: Unify way how hooks are configured for git and hg,
361 361 # git part is currently hardcoded.
362 362
363 363 # SVN PATTERNS
364 364 SVN_BRANCH_ID = 'vcs_svn_branch'
365 365 SVN_TAG_ID = 'vcs_svn_tag'
366 366
367 367 ui_id = Column(
368 368 "ui_id", Integer(), nullable=False, unique=True, default=None,
369 369 primary_key=True)
370 370 ui_section = Column(
371 371 "ui_section", String(255), nullable=True, unique=None, default=None)
372 372 ui_key = Column(
373 373 "ui_key", String(255), nullable=True, unique=None, default=None)
374 374 ui_value = Column(
375 375 "ui_value", String(255), nullable=True, unique=None, default=None)
376 376 ui_active = Column(
377 377 "ui_active", Boolean(), nullable=True, unique=None, default=True)
378 378
379 379 def __repr__(self):
380 380 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
381 381 self.ui_key, self.ui_value)
382 382
383 383
384 384 class RepoRhodeCodeSetting(Base, BaseModel):
385 385 __tablename__ = 'repo_rhodecode_settings'
386 386 __table_args__ = (
387 387 UniqueConstraint(
388 388 'app_settings_name', 'repository_id',
389 389 name='uq_repo_rhodecode_setting_name_repo_id'),
390 390 {'extend_existing': True, 'mysql_engine': 'InnoDB',
391 391 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
392 392 )
393 393
394 394 repository_id = Column(
395 395 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
396 396 nullable=False)
397 397 app_settings_id = Column(
398 398 "app_settings_id", Integer(), nullable=False, unique=True,
399 399 default=None, primary_key=True)
400 400 app_settings_name = Column(
401 401 "app_settings_name", String(255), nullable=True, unique=None,
402 402 default=None)
403 403 _app_settings_value = Column(
404 404 "app_settings_value", String(4096), nullable=True, unique=None,
405 405 default=None)
406 406 _app_settings_type = Column(
407 407 "app_settings_type", String(255), nullable=True, unique=None,
408 408 default=None)
409 409
410 410 repository = relationship('Repository')
411 411
412 412 def __init__(self, repository_id, key='', val='', type='unicode'):
413 413 self.repository_id = repository_id
414 414 self.app_settings_name = key
415 415 self.app_settings_type = type
416 416 self.app_settings_value = val
417 417
418 418 @validates('_app_settings_value')
419 419 def validate_settings_value(self, key, val):
420 420 assert type(val) == unicode
421 421 return val
422 422
423 423 @hybrid_property
424 424 def app_settings_value(self):
425 425 v = self._app_settings_value
426 426 type_ = self.app_settings_type
427 427 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
428 428 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
429 429 return converter(v)
430 430
431 431 @app_settings_value.setter
432 432 def app_settings_value(self, val):
433 433 """
434 434 Setter that will always make sure we use unicode in app_settings_value
435 435
436 436 :param val:
437 437 """
438 438 self._app_settings_value = safe_unicode(val)
439 439
440 440 @hybrid_property
441 441 def app_settings_type(self):
442 442 return self._app_settings_type
443 443
444 444 @app_settings_type.setter
445 445 def app_settings_type(self, val):
446 446 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
447 447 if val not in SETTINGS_TYPES:
448 448 raise Exception('type must be one of %s got %s'
449 449 % (SETTINGS_TYPES.keys(), val))
450 450 self._app_settings_type = val
451 451
452 452 def __unicode__(self):
453 453 return u"<%s('%s:%s:%s[%s]')>" % (
454 454 self.__class__.__name__, self.repository.repo_name,
455 455 self.app_settings_name, self.app_settings_value,
456 456 self.app_settings_type
457 457 )
458 458
459 459
460 460 class RepoRhodeCodeUi(Base, BaseModel):
461 461 __tablename__ = 'repo_rhodecode_ui'
462 462 __table_args__ = (
463 463 UniqueConstraint(
464 464 'repository_id', 'ui_section', 'ui_key',
465 465 name='uq_repo_rhodecode_ui_repository_id_section_key'),
466 466 {'extend_existing': True, 'mysql_engine': 'InnoDB',
467 467 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
468 468 )
469 469
470 470 repository_id = Column(
471 471 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
472 472 nullable=False)
473 473 ui_id = Column(
474 474 "ui_id", Integer(), nullable=False, unique=True, default=None,
475 475 primary_key=True)
476 476 ui_section = Column(
477 477 "ui_section", String(255), nullable=True, unique=None, default=None)
478 478 ui_key = Column(
479 479 "ui_key", String(255), nullable=True, unique=None, default=None)
480 480 ui_value = Column(
481 481 "ui_value", String(255), nullable=True, unique=None, default=None)
482 482 ui_active = Column(
483 483 "ui_active", Boolean(), nullable=True, unique=None, default=True)
484 484
485 485 repository = relationship('Repository')
486 486
487 487 def __repr__(self):
488 488 return '<%s[%s:%s]%s=>%s]>' % (
489 489 self.__class__.__name__, self.repository.repo_name,
490 490 self.ui_section, self.ui_key, self.ui_value)
491 491
492 492
493 493 class User(Base, BaseModel):
494 494 __tablename__ = 'users'
495 495 __table_args__ = (
496 496 UniqueConstraint('username'), UniqueConstraint('email'),
497 497 Index('u_username_idx', 'username'),
498 498 Index('u_email_idx', 'email'),
499 499 {'extend_existing': True, 'mysql_engine': 'InnoDB',
500 500 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
501 501 )
502 502 DEFAULT_USER = 'default'
503 503 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
504 504 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
505 505
506 506 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
507 507 username = Column("username", String(255), nullable=True, unique=None, default=None)
508 508 password = Column("password", String(255), nullable=True, unique=None, default=None)
509 509 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
510 510 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
511 511 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
512 512 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
513 513 _email = Column("email", String(255), nullable=True, unique=None, default=None)
514 514 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
515 515 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
516 516 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
517 517 api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
518 518 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
519 519 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
520 520 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
521 521
522 522 user_log = relationship('UserLog')
523 523 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
524 524
525 525 repositories = relationship('Repository')
526 526 repository_groups = relationship('RepoGroup')
527 527 user_groups = relationship('UserGroup')
528 528
529 529 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
530 530 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
531 531
532 532 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
533 533 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
534 534 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all')
535 535
536 536 group_member = relationship('UserGroupMember', cascade='all')
537 537
538 538 notifications = relationship('UserNotification', cascade='all')
539 539 # notifications assigned to this user
540 540 user_created_notifications = relationship('Notification', cascade='all')
541 541 # comments created by this user
542 542 user_comments = relationship('ChangesetComment', cascade='all')
543 543 # user profile extra info
544 544 user_emails = relationship('UserEmailMap', cascade='all')
545 545 user_ip_map = relationship('UserIpMap', cascade='all')
546 546 user_auth_tokens = relationship('UserApiKeys', cascade='all')
547 547 # gists
548 548 user_gists = relationship('Gist', cascade='all')
549 549 # user pull requests
550 550 user_pull_requests = relationship('PullRequest', cascade='all')
551 551 # external identities
552 552 extenal_identities = relationship(
553 553 'ExternalIdentity',
554 554 primaryjoin="User.user_id==ExternalIdentity.local_user_id",
555 555 cascade='all')
556 556
557 557 def __unicode__(self):
558 558 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
559 559 self.user_id, self.username)
560 560
561 561 @hybrid_property
562 562 def email(self):
563 563 return self._email
564 564
565 565 @email.setter
566 566 def email(self, val):
567 567 self._email = val.lower() if val else None
568 568
569 569 @property
570 570 def firstname(self):
571 571 # alias for future
572 572 return self.name
573 573
574 574 @property
575 575 def emails(self):
576 576 other = UserEmailMap.query().filter(UserEmailMap.user==self).all()
577 577 return [self.email] + [x.email for x in other]
578 578
579 579 @property
580 580 def auth_tokens(self):
581 581 return [self.api_key] + [x.api_key for x in self.extra_auth_tokens]
582 582
583 583 @property
584 584 def extra_auth_tokens(self):
585 585 return UserApiKeys.query().filter(UserApiKeys.user == self).all()
586 586
587 587 @property
588 588 def feed_token(self):
589 589 feed_tokens = UserApiKeys.query()\
590 590 .filter(UserApiKeys.user == self)\
591 591 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)\
592 592 .all()
593 593 if feed_tokens:
594 594 return feed_tokens[0].api_key
595 595 else:
596 596 # use the main token so we don't end up with nothing...
597 597 return self.api_key
598 598
599 599 @classmethod
600 600 def extra_valid_auth_tokens(cls, user, role=None):
601 601 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
602 602 .filter(or_(UserApiKeys.expires == -1,
603 603 UserApiKeys.expires >= time.time()))
604 604 if role:
605 605 tokens = tokens.filter(or_(UserApiKeys.role == role,
606 606 UserApiKeys.role == UserApiKeys.ROLE_ALL))
607 607 return tokens.all()
608 608
609 609 @property
610 610 def ip_addresses(self):
611 611 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
612 612 return [x.ip_addr for x in ret]
613 613
614 614 @property
615 615 def username_and_name(self):
616 616 return '%s (%s %s)' % (self.username, self.firstname, self.lastname)
617 617
618 618 @property
619 619 def username_or_name_or_email(self):
620 620 full_name = self.full_name if self.full_name is not ' ' else None
621 621 return self.username or full_name or self.email
622 622
623 623 @property
624 624 def full_name(self):
625 625 return '%s %s' % (self.firstname, self.lastname)
626 626
627 627 @property
628 628 def full_name_or_username(self):
629 629 return ('%s %s' % (self.firstname, self.lastname)
630 630 if (self.firstname and self.lastname) else self.username)
631 631
632 632 @property
633 633 def full_contact(self):
634 634 return '%s %s <%s>' % (self.firstname, self.lastname, self.email)
635 635
636 636 @property
637 637 def short_contact(self):
638 638 return '%s %s' % (self.firstname, self.lastname)
639 639
640 640 @property
641 641 def is_admin(self):
642 642 return self.admin
643 643
644 644 @property
645 645 def AuthUser(self):
646 646 """
647 647 Returns instance of AuthUser for this user
648 648 """
649 649 from rhodecode.lib.auth import AuthUser
650 650 return AuthUser(user_id=self.user_id, api_key=self.api_key,
651 651 username=self.username)
652 652
653 653 @hybrid_property
654 654 def user_data(self):
655 655 if not self._user_data:
656 656 return {}
657 657
658 658 try:
659 659 return json.loads(self._user_data)
660 660 except TypeError:
661 661 return {}
662 662
663 663 @user_data.setter
664 664 def user_data(self, val):
665 665 if not isinstance(val, dict):
666 666 raise Exception('user_data must be dict, got %s' % type(val))
667 667 try:
668 668 self._user_data = json.dumps(val)
669 669 except Exception:
670 670 log.error(traceback.format_exc())
671 671
672 672 @classmethod
673 673 def get_by_username(cls, username, case_insensitive=False,
674 674 cache=False, identity_cache=False):
675 675 session = Session()
676 676
677 677 if case_insensitive:
678 678 q = cls.query().filter(
679 679 func.lower(cls.username) == func.lower(username))
680 680 else:
681 681 q = cls.query().filter(cls.username == username)
682 682
683 683 if cache:
684 684 if identity_cache:
685 685 val = cls.identity_cache(session, 'username', username)
686 686 if val:
687 687 return val
688 688 else:
689 689 q = q.options(
690 690 FromCache("sql_cache_short",
691 691 "get_user_by_name_%s" % _hash_key(username)))
692 692
693 693 return q.scalar()
694 694
695 695 @classmethod
696 696 def get_by_auth_token(cls, auth_token, cache=False, fallback=True):
697 697 q = cls.query().filter(cls.api_key == auth_token)
698 698
699 699 if cache:
700 700 q = q.options(FromCache("sql_cache_short",
701 701 "get_auth_token_%s" % auth_token))
702 702 res = q.scalar()
703 703
704 704 if fallback and not res:
705 705 #fallback to additional keys
706 706 _res = UserApiKeys.query()\
707 707 .filter(UserApiKeys.api_key == auth_token)\
708 708 .filter(or_(UserApiKeys.expires == -1,
709 709 UserApiKeys.expires >= time.time()))\
710 710 .first()
711 711 if _res:
712 712 res = _res.user
713 713 return res
714 714
715 715 @classmethod
716 716 def get_by_email(cls, email, case_insensitive=False, cache=False):
717 717
718 718 if case_insensitive:
719 719 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
720 720
721 721 else:
722 722 q = cls.query().filter(cls.email == email)
723 723
724 724 if cache:
725 725 q = q.options(FromCache("sql_cache_short",
726 726 "get_email_key_%s" % _hash_key(email)))
727 727
728 728 ret = q.scalar()
729 729 if ret is None:
730 730 q = UserEmailMap.query()
731 731 # try fetching in alternate email map
732 732 if case_insensitive:
733 733 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
734 734 else:
735 735 q = q.filter(UserEmailMap.email == email)
736 736 q = q.options(joinedload(UserEmailMap.user))
737 737 if cache:
738 738 q = q.options(FromCache("sql_cache_short",
739 739 "get_email_map_key_%s" % email))
740 740 ret = getattr(q.scalar(), 'user', None)
741 741
742 742 return ret
743 743
744 744 @classmethod
745 745 def get_from_cs_author(cls, author):
746 746 """
747 747 Tries to get User objects out of commit author string
748 748
749 749 :param author:
750 750 """
751 751 from rhodecode.lib.helpers import email, author_name
752 752 # Valid email in the attribute passed, see if they're in the system
753 753 _email = email(author)
754 754 if _email:
755 755 user = cls.get_by_email(_email, case_insensitive=True)
756 756 if user:
757 757 return user
758 758 # Maybe we can match by username?
759 759 _author = author_name(author)
760 760 user = cls.get_by_username(_author, case_insensitive=True)
761 761 if user:
762 762 return user
763 763
764 764 def update_userdata(self, **kwargs):
765 765 usr = self
766 766 old = usr.user_data
767 767 old.update(**kwargs)
768 768 usr.user_data = old
769 769 Session().add(usr)
770 770 log.debug('updated userdata with ', kwargs)
771 771
772 772 def update_lastlogin(self):
773 773 """Update user lastlogin"""
774 774 self.last_login = datetime.datetime.now()
775 775 Session().add(self)
776 776 log.debug('updated user %s lastlogin', self.username)
777 777
778 778 def update_lastactivity(self):
779 779 """Update user lastactivity"""
780 780 usr = self
781 781 old = usr.user_data
782 782 old.update({'last_activity': time.time()})
783 783 usr.user_data = old
784 784 Session().add(usr)
785 785 log.debug('updated user %s lastactivity', usr.username)
786 786
787 787 def update_password(self, new_password, change_api_key=False):
788 788 from rhodecode.lib.auth import get_crypt_password,generate_auth_token
789 789
790 790 self.password = get_crypt_password(new_password)
791 791 if change_api_key:
792 792 self.api_key = generate_auth_token(self.username)
793 793 Session().add(self)
794 794
795 795 @classmethod
796 796 def get_first_super_admin(cls):
797 797 user = User.query().filter(User.admin == true()).first()
798 798 if user is None:
799 799 raise Exception('FATAL: Missing administrative account!')
800 800 return user
801 801
802 802 @classmethod
803 803 def get_all_super_admins(cls):
804 804 """
805 805 Returns all admin accounts sorted by username
806 806 """
807 807 return User.query().filter(User.admin == true())\
808 808 .order_by(User.username.asc()).all()
809 809
810 810 @classmethod
811 811 def get_default_user(cls, cache=False):
812 812 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
813 813 if user is None:
814 814 raise Exception('FATAL: Missing default account!')
815 815 return user
816 816
817 817 def _get_default_perms(self, user, suffix=''):
818 818 from rhodecode.model.permission import PermissionModel
819 819 return PermissionModel().get_default_perms(user.user_perms, suffix)
820 820
821 821 def get_default_perms(self, suffix=''):
822 822 return self._get_default_perms(self, suffix)
823 823
824 824 def get_api_data(self, include_secrets=False, details='full'):
825 825 """
826 826 Common function for generating user related data for API
827 827
828 828 :param include_secrets: By default secrets in the API data will be replaced
829 829 by a placeholder value to prevent exposing this data by accident. In case
830 830 this data shall be exposed, set this flag to ``True``.
831 831
832 832 :param details: details can be 'basic|full' basic gives only a subset of
833 833 the available user information that includes user_id, name and emails.
834 834 """
835 835 user = self
836 836 user_data = self.user_data
837 837 data = {
838 838 'user_id': user.user_id,
839 839 'username': user.username,
840 840 'firstname': user.name,
841 841 'lastname': user.lastname,
842 842 'email': user.email,
843 843 'emails': user.emails,
844 844 }
845 845 if details == 'basic':
846 846 return data
847 847
848 848 api_key_length = 40
849 849 api_key_replacement = '*' * api_key_length
850 850
851 851 extras = {
852 852 'api_key': api_key_replacement,
853 853 'api_keys': [api_key_replacement],
854 854 'active': user.active,
855 855 'admin': user.admin,
856 856 'extern_type': user.extern_type,
857 857 'extern_name': user.extern_name,
858 858 'last_login': user.last_login,
859 859 'ip_addresses': user.ip_addresses,
860 860 'language': user_data.get('language')
861 861 }
862 862 data.update(extras)
863 863
864 864 if include_secrets:
865 865 data['api_key'] = user.api_key
866 866 data['api_keys'] = user.auth_tokens
867 867 return data
868 868
869 869 def __json__(self):
870 870 data = {
871 871 'full_name': self.full_name,
872 872 'full_name_or_username': self.full_name_or_username,
873 873 'short_contact': self.short_contact,
874 874 'full_contact': self.full_contact,
875 875 }
876 876 data.update(self.get_api_data())
877 877 return data
878 878
879 879
880 880 class UserApiKeys(Base, BaseModel):
881 881 __tablename__ = 'user_api_keys'
882 882 __table_args__ = (
883 883 Index('uak_api_key_idx', 'api_key'),
884 884 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
885 885 UniqueConstraint('api_key'),
886 886 {'extend_existing': True, 'mysql_engine': 'InnoDB',
887 887 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
888 888 )
889 889 __mapper_args__ = {}
890 890
891 891 # ApiKey role
892 892 ROLE_ALL = 'token_role_all'
893 893 ROLE_HTTP = 'token_role_http'
894 894 ROLE_VCS = 'token_role_vcs'
895 895 ROLE_API = 'token_role_api'
896 896 ROLE_FEED = 'token_role_feed'
897 897 ROLES = [ROLE_ALL, ROLE_HTTP, ROLE_VCS, ROLE_API, ROLE_FEED]
898 898
899 899 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
900 900 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
901 901 api_key = Column("api_key", String(255), nullable=False, unique=True)
902 902 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
903 903 expires = Column('expires', Float(53), nullable=False)
904 904 role = Column('role', String(255), nullable=True)
905 905 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
906 906
907 907 user = relationship('User', lazy='joined')
908 908
909 909 @classmethod
910 910 def _get_role_name(cls, role):
911 911 return {
912 912 cls.ROLE_ALL: _('all'),
913 913 cls.ROLE_HTTP: _('http/web interface'),
914 914 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
915 915 cls.ROLE_API: _('api calls'),
916 916 cls.ROLE_FEED: _('feed access'),
917 917 }.get(role, role)
918 918
919 919 @property
920 920 def expired(self):
921 921 if self.expires == -1:
922 922 return False
923 923 return time.time() > self.expires
924 924
925 925 @property
926 926 def role_humanized(self):
927 927 return self._get_role_name(self.role)
928 928
929 929
930 930 class UserEmailMap(Base, BaseModel):
931 931 __tablename__ = 'user_email_map'
932 932 __table_args__ = (
933 933 Index('uem_email_idx', 'email'),
934 934 UniqueConstraint('email'),
935 935 {'extend_existing': True, 'mysql_engine': 'InnoDB',
936 936 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
937 937 )
938 938 __mapper_args__ = {}
939 939
940 940 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
941 941 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
942 942 _email = Column("email", String(255), nullable=True, unique=False, default=None)
943 943 user = relationship('User', lazy='joined')
944 944
945 945 @validates('_email')
946 946 def validate_email(self, key, email):
947 947 # check if this email is not main one
948 948 main_email = Session().query(User).filter(User.email == email).scalar()
949 949 if main_email is not None:
950 950 raise AttributeError('email %s is present is user table' % email)
951 951 return email
952 952
953 953 @hybrid_property
954 954 def email(self):
955 955 return self._email
956 956
957 957 @email.setter
958 958 def email(self, val):
959 959 self._email = val.lower() if val else None
960 960
961 961
962 962 class UserIpMap(Base, BaseModel):
963 963 __tablename__ = 'user_ip_map'
964 964 __table_args__ = (
965 965 UniqueConstraint('user_id', 'ip_addr'),
966 966 {'extend_existing': True, 'mysql_engine': 'InnoDB',
967 967 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
968 968 )
969 969 __mapper_args__ = {}
970 970
971 971 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
972 972 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
973 973 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
974 974 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
975 975 description = Column("description", String(10000), nullable=True, unique=None, default=None)
976 976 user = relationship('User', lazy='joined')
977 977
978 978 @classmethod
979 979 def _get_ip_range(cls, ip_addr):
980 980 net = ipaddress.ip_network(ip_addr, strict=False)
981 981 return [str(net.network_address), str(net.broadcast_address)]
982 982
983 983 def __json__(self):
984 984 return {
985 985 'ip_addr': self.ip_addr,
986 986 'ip_range': self._get_ip_range(self.ip_addr),
987 987 }
988 988
989 989 def __unicode__(self):
990 990 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
991 991 self.user_id, self.ip_addr)
992 992
993 993 class UserLog(Base, BaseModel):
994 994 __tablename__ = 'user_logs'
995 995 __table_args__ = (
996 996 {'extend_existing': True, 'mysql_engine': 'InnoDB',
997 997 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
998 998 )
999 999 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1000 1000 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1001 1001 username = Column("username", String(255), nullable=True, unique=None, default=None)
1002 1002 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True)
1003 1003 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1004 1004 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1005 1005 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1006 1006 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1007 1007
1008 1008 def __unicode__(self):
1009 1009 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1010 1010 self.repository_name,
1011 1011 self.action)
1012 1012
1013 1013 @property
1014 1014 def action_as_day(self):
1015 1015 return datetime.date(*self.action_date.timetuple()[:3])
1016 1016
1017 1017 user = relationship('User')
1018 1018 repository = relationship('Repository', cascade='')
1019 1019
1020 1020
1021 1021 class UserGroup(Base, BaseModel):
1022 1022 __tablename__ = 'users_groups'
1023 1023 __table_args__ = (
1024 1024 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1025 1025 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1026 1026 )
1027 1027
1028 1028 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1029 1029 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1030 1030 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1031 1031 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1032 1032 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1033 1033 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1034 1034 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1035 1035 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1036 1036
1037 1037 members = relationship('UserGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
1038 1038 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
1039 1039 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1040 1040 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1041 1041 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all')
1042 1042 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
1043 1043
1044 1044 user = relationship('User')
1045 1045
1046 1046 @hybrid_property
1047 1047 def group_data(self):
1048 1048 if not self._group_data:
1049 1049 return {}
1050 1050
1051 1051 try:
1052 1052 return json.loads(self._group_data)
1053 1053 except TypeError:
1054 1054 return {}
1055 1055
1056 1056 @group_data.setter
1057 1057 def group_data(self, val):
1058 1058 try:
1059 1059 self._group_data = json.dumps(val)
1060 1060 except Exception:
1061 1061 log.error(traceback.format_exc())
1062 1062
1063 1063 def __unicode__(self):
1064 1064 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1065 1065 self.users_group_id,
1066 1066 self.users_group_name)
1067 1067
1068 1068 @classmethod
1069 1069 def get_by_group_name(cls, group_name, cache=False,
1070 1070 case_insensitive=False):
1071 1071 if case_insensitive:
1072 1072 q = cls.query().filter(func.lower(cls.users_group_name) ==
1073 1073 func.lower(group_name))
1074 1074
1075 1075 else:
1076 1076 q = cls.query().filter(cls.users_group_name == group_name)
1077 1077 if cache:
1078 1078 q = q.options(FromCache(
1079 1079 "sql_cache_short",
1080 1080 "get_group_%s" % _hash_key(group_name)))
1081 1081 return q.scalar()
1082 1082
1083 1083 @classmethod
1084 1084 def get(cls, user_group_id, cache=False):
1085 1085 user_group = cls.query()
1086 1086 if cache:
1087 1087 user_group = user_group.options(FromCache("sql_cache_short",
1088 1088 "get_users_group_%s" % user_group_id))
1089 1089 return user_group.get(user_group_id)
1090 1090
1091 1091 def permissions(self, with_admins=True, with_owner=True):
1092 1092 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1093 1093 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1094 1094 joinedload(UserUserGroupToPerm.user),
1095 1095 joinedload(UserUserGroupToPerm.permission),)
1096 1096
1097 1097 # get owners and admins and permissions. We do a trick of re-writing
1098 1098 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1099 1099 # has a global reference and changing one object propagates to all
1100 1100 # others. This means if admin is also an owner admin_row that change
1101 1101 # would propagate to both objects
1102 1102 perm_rows = []
1103 1103 for _usr in q.all():
1104 1104 usr = AttributeDict(_usr.user.get_dict())
1105 1105 usr.permission = _usr.permission.permission_name
1106 1106 perm_rows.append(usr)
1107 1107
1108 1108 # filter the perm rows by 'default' first and then sort them by
1109 1109 # admin,write,read,none permissions sorted again alphabetically in
1110 1110 # each group
1111 1111 perm_rows = sorted(perm_rows, key=display_sort)
1112 1112
1113 1113 _admin_perm = 'usergroup.admin'
1114 1114 owner_row = []
1115 1115 if with_owner:
1116 1116 usr = AttributeDict(self.user.get_dict())
1117 1117 usr.owner_row = True
1118 1118 usr.permission = _admin_perm
1119 1119 owner_row.append(usr)
1120 1120
1121 1121 super_admin_rows = []
1122 1122 if with_admins:
1123 1123 for usr in User.get_all_super_admins():
1124 1124 # if this admin is also owner, don't double the record
1125 1125 if usr.user_id == owner_row[0].user_id:
1126 1126 owner_row[0].admin_row = True
1127 1127 else:
1128 1128 usr = AttributeDict(usr.get_dict())
1129 1129 usr.admin_row = True
1130 1130 usr.permission = _admin_perm
1131 1131 super_admin_rows.append(usr)
1132 1132
1133 1133 return super_admin_rows + owner_row + perm_rows
1134 1134
1135 1135 def permission_user_groups(self):
1136 1136 q = UserGroupUserGroupToPerm.query().filter(UserGroupUserGroupToPerm.target_user_group == self)
1137 1137 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1138 1138 joinedload(UserGroupUserGroupToPerm.target_user_group),
1139 1139 joinedload(UserGroupUserGroupToPerm.permission),)
1140 1140
1141 1141 perm_rows = []
1142 1142 for _user_group in q.all():
1143 1143 usr = AttributeDict(_user_group.user_group.get_dict())
1144 1144 usr.permission = _user_group.permission.permission_name
1145 1145 perm_rows.append(usr)
1146 1146
1147 1147 return perm_rows
1148 1148
1149 1149 def _get_default_perms(self, user_group, suffix=''):
1150 1150 from rhodecode.model.permission import PermissionModel
1151 1151 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1152 1152
1153 1153 def get_default_perms(self, suffix=''):
1154 1154 return self._get_default_perms(self, suffix)
1155 1155
1156 1156 def get_api_data(self, with_group_members=True, include_secrets=False):
1157 1157 """
1158 1158 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1159 1159 basically forwarded.
1160 1160
1161 1161 """
1162 1162 user_group = self
1163 1163
1164 1164 data = {
1165 1165 'users_group_id': user_group.users_group_id,
1166 1166 'group_name': user_group.users_group_name,
1167 1167 'group_description': user_group.user_group_description,
1168 1168 'active': user_group.users_group_active,
1169 1169 'owner': user_group.user.username,
1170 1170 }
1171 1171 if with_group_members:
1172 1172 users = []
1173 1173 for user in user_group.members:
1174 1174 user = user.user
1175 1175 users.append(user.get_api_data(include_secrets=include_secrets))
1176 1176 data['users'] = users
1177 1177
1178 1178 return data
1179 1179
1180 1180
1181 1181 class UserGroupMember(Base, BaseModel):
1182 1182 __tablename__ = 'users_groups_members'
1183 1183 __table_args__ = (
1184 1184 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1185 1185 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1186 1186 )
1187 1187
1188 1188 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1189 1189 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1190 1190 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1191 1191
1192 1192 user = relationship('User', lazy='joined')
1193 1193 users_group = relationship('UserGroup')
1194 1194
1195 1195 def __init__(self, gr_id='', u_id=''):
1196 1196 self.users_group_id = gr_id
1197 1197 self.user_id = u_id
1198 1198
1199 1199
1200 1200 class RepositoryField(Base, BaseModel):
1201 1201 __tablename__ = 'repositories_fields'
1202 1202 __table_args__ = (
1203 1203 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1204 1204 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1205 1205 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1206 1206 )
1207 1207 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1208 1208
1209 1209 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1210 1210 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1211 1211 field_key = Column("field_key", String(250))
1212 1212 field_label = Column("field_label", String(1024), nullable=False)
1213 1213 field_value = Column("field_value", String(10000), nullable=False)
1214 1214 field_desc = Column("field_desc", String(1024), nullable=False)
1215 1215 field_type = Column("field_type", String(255), nullable=False, unique=None)
1216 1216 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1217 1217
1218 1218 repository = relationship('Repository')
1219 1219
1220 1220 @property
1221 1221 def field_key_prefixed(self):
1222 1222 return 'ex_%s' % self.field_key
1223 1223
1224 1224 @classmethod
1225 1225 def un_prefix_key(cls, key):
1226 1226 if key.startswith(cls.PREFIX):
1227 1227 return key[len(cls.PREFIX):]
1228 1228 return key
1229 1229
1230 1230 @classmethod
1231 1231 def get_by_key_name(cls, key, repo):
1232 1232 row = cls.query()\
1233 1233 .filter(cls.repository == repo)\
1234 1234 .filter(cls.field_key == key).scalar()
1235 1235 return row
1236 1236
1237 1237
1238 1238 class Repository(Base, BaseModel):
1239 1239 __tablename__ = 'repositories'
1240 1240 __table_args__ = (
1241 1241 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1242 1242 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1243 1243 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1244 1244 )
1245 1245 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1246 1246 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1247 1247
1248 1248 STATE_CREATED = 'repo_state_created'
1249 1249 STATE_PENDING = 'repo_state_pending'
1250 1250 STATE_ERROR = 'repo_state_error'
1251 1251
1252 1252 LOCK_AUTOMATIC = 'lock_auto'
1253 1253 LOCK_API = 'lock_api'
1254 1254 LOCK_WEB = 'lock_web'
1255 1255 LOCK_PULL = 'lock_pull'
1256 1256
1257 1257 NAME_SEP = URL_SEP
1258 1258
1259 1259 repo_id = Column(
1260 1260 "repo_id", Integer(), nullable=False, unique=True, default=None,
1261 1261 primary_key=True)
1262 1262 _repo_name = Column(
1263 1263 "repo_name", Text(), nullable=False, default=None)
1264 1264 _repo_name_hash = Column(
1265 1265 "repo_name_hash", String(255), nullable=False, unique=True)
1266 1266 repo_state = Column("repo_state", String(255), nullable=True)
1267 1267
1268 1268 clone_uri = Column(
1269 1269 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1270 1270 default=None)
1271 1271 repo_type = Column(
1272 1272 "repo_type", String(255), nullable=False, unique=False, default=None)
1273 1273 user_id = Column(
1274 1274 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1275 1275 unique=False, default=None)
1276 1276 private = Column(
1277 1277 "private", Boolean(), nullable=True, unique=None, default=None)
1278 1278 enable_statistics = Column(
1279 1279 "statistics", Boolean(), nullable=True, unique=None, default=True)
1280 1280 enable_downloads = Column(
1281 1281 "downloads", Boolean(), nullable=True, unique=None, default=True)
1282 1282 description = Column(
1283 1283 "description", String(10000), nullable=True, unique=None, default=None)
1284 1284 created_on = Column(
1285 1285 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1286 1286 default=datetime.datetime.now)
1287 1287 updated_on = Column(
1288 1288 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1289 1289 default=datetime.datetime.now)
1290 1290 _landing_revision = Column(
1291 1291 "landing_revision", String(255), nullable=False, unique=False,
1292 1292 default=None)
1293 1293 enable_locking = Column(
1294 1294 "enable_locking", Boolean(), nullable=False, unique=None,
1295 1295 default=False)
1296 1296 _locked = Column(
1297 1297 "locked", String(255), nullable=True, unique=False, default=None)
1298 1298 _changeset_cache = Column(
1299 1299 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1300 1300
1301 1301 fork_id = Column(
1302 1302 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1303 1303 nullable=True, unique=False, default=None)
1304 1304 group_id = Column(
1305 1305 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1306 1306 unique=False, default=None)
1307 1307
1308 1308 user = relationship('User', lazy='joined')
1309 1309 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1310 1310 group = relationship('RepoGroup', lazy='joined')
1311 1311 repo_to_perm = relationship(
1312 1312 'UserRepoToPerm', cascade='all',
1313 1313 order_by='UserRepoToPerm.repo_to_perm_id')
1314 1314 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1315 1315 stats = relationship('Statistics', cascade='all', uselist=False)
1316 1316
1317 1317 followers = relationship(
1318 1318 'UserFollowing',
1319 1319 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
1320 1320 cascade='all')
1321 1321 extra_fields = relationship(
1322 1322 'RepositoryField', cascade="all, delete, delete-orphan")
1323 1323 logs = relationship('UserLog')
1324 1324 comments = relationship(
1325 1325 'ChangesetComment', cascade="all, delete, delete-orphan")
1326 1326 pull_requests_source = relationship(
1327 1327 'PullRequest',
1328 1328 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1329 1329 cascade="all, delete, delete-orphan")
1330 1330 pull_requests_target = relationship(
1331 1331 'PullRequest',
1332 1332 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1333 1333 cascade="all, delete, delete-orphan")
1334 1334 ui = relationship('RepoRhodeCodeUi', cascade="all")
1335 1335 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1336 1336 integrations = relationship('Integration',
1337 1337 cascade="all, delete, delete-orphan")
1338 1338
1339 1339 def __unicode__(self):
1340 1340 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1341 1341 safe_unicode(self.repo_name))
1342 1342
1343 1343 @hybrid_property
1344 1344 def landing_rev(self):
1345 1345 # always should return [rev_type, rev]
1346 1346 if self._landing_revision:
1347 1347 _rev_info = self._landing_revision.split(':')
1348 1348 if len(_rev_info) < 2:
1349 1349 _rev_info.insert(0, 'rev')
1350 1350 return [_rev_info[0], _rev_info[1]]
1351 1351 return [None, None]
1352 1352
1353 1353 @landing_rev.setter
1354 1354 def landing_rev(self, val):
1355 1355 if ':' not in val:
1356 1356 raise ValueError('value must be delimited with `:` and consist '
1357 1357 'of <rev_type>:<rev>, got %s instead' % val)
1358 1358 self._landing_revision = val
1359 1359
1360 1360 @hybrid_property
1361 1361 def locked(self):
1362 1362 if self._locked:
1363 1363 user_id, timelocked, reason = self._locked.split(':')
1364 1364 lock_values = int(user_id), timelocked, reason
1365 1365 else:
1366 1366 lock_values = [None, None, None]
1367 1367 return lock_values
1368 1368
1369 1369 @locked.setter
1370 1370 def locked(self, val):
1371 1371 if val and isinstance(val, (list, tuple)):
1372 1372 self._locked = ':'.join(map(str, val))
1373 1373 else:
1374 1374 self._locked = None
1375 1375
1376 1376 @hybrid_property
1377 1377 def changeset_cache(self):
1378 1378 from rhodecode.lib.vcs.backends.base import EmptyCommit
1379 1379 dummy = EmptyCommit().__json__()
1380 1380 if not self._changeset_cache:
1381 1381 return dummy
1382 1382 try:
1383 1383 return json.loads(self._changeset_cache)
1384 1384 except TypeError:
1385 1385 return dummy
1386 1386 except Exception:
1387 1387 log.error(traceback.format_exc())
1388 1388 return dummy
1389 1389
1390 1390 @changeset_cache.setter
1391 1391 def changeset_cache(self, val):
1392 1392 try:
1393 1393 self._changeset_cache = json.dumps(val)
1394 1394 except Exception:
1395 1395 log.error(traceback.format_exc())
1396 1396
1397 1397 @hybrid_property
1398 1398 def repo_name(self):
1399 1399 return self._repo_name
1400 1400
1401 1401 @repo_name.setter
1402 1402 def repo_name(self, value):
1403 1403 self._repo_name = value
1404 1404 self._repo_name_hash = hashlib.sha1(safe_str(value)).hexdigest()
1405 1405
1406 1406 @classmethod
1407 1407 def normalize_repo_name(cls, repo_name):
1408 1408 """
1409 1409 Normalizes os specific repo_name to the format internally stored inside
1410 1410 database using URL_SEP
1411 1411
1412 1412 :param cls:
1413 1413 :param repo_name:
1414 1414 """
1415 1415 return cls.NAME_SEP.join(repo_name.split(os.sep))
1416 1416
1417 1417 @classmethod
1418 1418 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1419 1419 session = Session()
1420 1420 q = session.query(cls).filter(cls.repo_name == repo_name)
1421 1421
1422 1422 if cache:
1423 1423 if identity_cache:
1424 1424 val = cls.identity_cache(session, 'repo_name', repo_name)
1425 1425 if val:
1426 1426 return val
1427 1427 else:
1428 1428 q = q.options(
1429 1429 FromCache("sql_cache_short",
1430 1430 "get_repo_by_name_%s" % _hash_key(repo_name)))
1431 1431
1432 1432 return q.scalar()
1433 1433
1434 1434 @classmethod
1435 1435 def get_by_full_path(cls, repo_full_path):
1436 1436 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1437 1437 repo_name = cls.normalize_repo_name(repo_name)
1438 1438 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1439 1439
1440 1440 @classmethod
1441 1441 def get_repo_forks(cls, repo_id):
1442 1442 return cls.query().filter(Repository.fork_id == repo_id)
1443 1443
1444 1444 @classmethod
1445 1445 def base_path(cls):
1446 1446 """
1447 1447 Returns base path when all repos are stored
1448 1448
1449 1449 :param cls:
1450 1450 """
1451 1451 q = Session().query(RhodeCodeUi)\
1452 1452 .filter(RhodeCodeUi.ui_key == cls.NAME_SEP)
1453 1453 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1454 1454 return q.one().ui_value
1455 1455
1456 1456 @classmethod
1457 1457 def is_valid(cls, repo_name):
1458 1458 """
1459 1459 returns True if given repo name is a valid filesystem repository
1460 1460
1461 1461 :param cls:
1462 1462 :param repo_name:
1463 1463 """
1464 1464 from rhodecode.lib.utils import is_valid_repo
1465 1465
1466 1466 return is_valid_repo(repo_name, cls.base_path())
1467 1467
1468 1468 @classmethod
1469 1469 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1470 1470 case_insensitive=True):
1471 1471 q = Repository.query()
1472 1472
1473 1473 if not isinstance(user_id, Optional):
1474 1474 q = q.filter(Repository.user_id == user_id)
1475 1475
1476 1476 if not isinstance(group_id, Optional):
1477 1477 q = q.filter(Repository.group_id == group_id)
1478 1478
1479 1479 if case_insensitive:
1480 1480 q = q.order_by(func.lower(Repository.repo_name))
1481 1481 else:
1482 1482 q = q.order_by(Repository.repo_name)
1483 1483 return q.all()
1484 1484
1485 1485 @property
1486 1486 def forks(self):
1487 1487 """
1488 1488 Return forks of this repo
1489 1489 """
1490 1490 return Repository.get_repo_forks(self.repo_id)
1491 1491
1492 1492 @property
1493 1493 def parent(self):
1494 1494 """
1495 1495 Returns fork parent
1496 1496 """
1497 1497 return self.fork
1498 1498
1499 1499 @property
1500 1500 def just_name(self):
1501 1501 return self.repo_name.split(self.NAME_SEP)[-1]
1502 1502
1503 1503 @property
1504 1504 def groups_with_parents(self):
1505 1505 groups = []
1506 1506 if self.group is None:
1507 1507 return groups
1508 1508
1509 1509 cur_gr = self.group
1510 1510 groups.insert(0, cur_gr)
1511 1511 while 1:
1512 1512 gr = getattr(cur_gr, 'parent_group', None)
1513 1513 cur_gr = cur_gr.parent_group
1514 1514 if gr is None:
1515 1515 break
1516 1516 groups.insert(0, gr)
1517 1517
1518 1518 return groups
1519 1519
1520 1520 @property
1521 1521 def groups_and_repo(self):
1522 1522 return self.groups_with_parents, self
1523 1523
1524 1524 @LazyProperty
1525 1525 def repo_path(self):
1526 1526 """
1527 1527 Returns base full path for that repository means where it actually
1528 1528 exists on a filesystem
1529 1529 """
1530 1530 q = Session().query(RhodeCodeUi).filter(
1531 1531 RhodeCodeUi.ui_key == self.NAME_SEP)
1532 1532 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1533 1533 return q.one().ui_value
1534 1534
1535 1535 @property
1536 1536 def repo_full_path(self):
1537 1537 p = [self.repo_path]
1538 1538 # we need to split the name by / since this is how we store the
1539 1539 # names in the database, but that eventually needs to be converted
1540 1540 # into a valid system path
1541 1541 p += self.repo_name.split(self.NAME_SEP)
1542 1542 return os.path.join(*map(safe_unicode, p))
1543 1543
1544 1544 @property
1545 1545 def cache_keys(self):
1546 1546 """
1547 1547 Returns associated cache keys for that repo
1548 1548 """
1549 1549 return CacheKey.query()\
1550 1550 .filter(CacheKey.cache_args == self.repo_name)\
1551 1551 .order_by(CacheKey.cache_key)\
1552 1552 .all()
1553 1553
1554 1554 def get_new_name(self, repo_name):
1555 1555 """
1556 1556 returns new full repository name based on assigned group and new new
1557 1557
1558 1558 :param group_name:
1559 1559 """
1560 1560 path_prefix = self.group.full_path_splitted if self.group else []
1561 1561 return self.NAME_SEP.join(path_prefix + [repo_name])
1562 1562
1563 1563 @property
1564 1564 def _config(self):
1565 1565 """
1566 1566 Returns db based config object.
1567 1567 """
1568 1568 from rhodecode.lib.utils import make_db_config
1569 1569 return make_db_config(clear_session=False, repo=self)
1570 1570
1571 1571 def permissions(self, with_admins=True, with_owner=True):
1572 1572 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
1573 1573 q = q.options(joinedload(UserRepoToPerm.repository),
1574 1574 joinedload(UserRepoToPerm.user),
1575 1575 joinedload(UserRepoToPerm.permission),)
1576 1576
1577 1577 # get owners and admins and permissions. We do a trick of re-writing
1578 1578 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1579 1579 # has a global reference and changing one object propagates to all
1580 1580 # others. This means if admin is also an owner admin_row that change
1581 1581 # would propagate to both objects
1582 1582 perm_rows = []
1583 1583 for _usr in q.all():
1584 1584 usr = AttributeDict(_usr.user.get_dict())
1585 1585 usr.permission = _usr.permission.permission_name
1586 1586 perm_rows.append(usr)
1587 1587
1588 1588 # filter the perm rows by 'default' first and then sort them by
1589 1589 # admin,write,read,none permissions sorted again alphabetically in
1590 1590 # each group
1591 1591 perm_rows = sorted(perm_rows, key=display_sort)
1592 1592
1593 1593 _admin_perm = 'repository.admin'
1594 1594 owner_row = []
1595 1595 if with_owner:
1596 1596 usr = AttributeDict(self.user.get_dict())
1597 1597 usr.owner_row = True
1598 1598 usr.permission = _admin_perm
1599 1599 owner_row.append(usr)
1600 1600
1601 1601 super_admin_rows = []
1602 1602 if with_admins:
1603 1603 for usr in User.get_all_super_admins():
1604 1604 # if this admin is also owner, don't double the record
1605 1605 if usr.user_id == owner_row[0].user_id:
1606 1606 owner_row[0].admin_row = True
1607 1607 else:
1608 1608 usr = AttributeDict(usr.get_dict())
1609 1609 usr.admin_row = True
1610 1610 usr.permission = _admin_perm
1611 1611 super_admin_rows.append(usr)
1612 1612
1613 1613 return super_admin_rows + owner_row + perm_rows
1614 1614
1615 1615 def permission_user_groups(self):
1616 1616 q = UserGroupRepoToPerm.query().filter(
1617 1617 UserGroupRepoToPerm.repository == self)
1618 1618 q = q.options(joinedload(UserGroupRepoToPerm.repository),
1619 1619 joinedload(UserGroupRepoToPerm.users_group),
1620 1620 joinedload(UserGroupRepoToPerm.permission),)
1621 1621
1622 1622 perm_rows = []
1623 1623 for _user_group in q.all():
1624 1624 usr = AttributeDict(_user_group.users_group.get_dict())
1625 1625 usr.permission = _user_group.permission.permission_name
1626 1626 perm_rows.append(usr)
1627 1627
1628 1628 return perm_rows
1629 1629
1630 1630 def get_api_data(self, include_secrets=False):
1631 1631 """
1632 1632 Common function for generating repo api data
1633 1633
1634 1634 :param include_secrets: See :meth:`User.get_api_data`.
1635 1635
1636 1636 """
1637 1637 # TODO: mikhail: Here there is an anti-pattern, we probably need to
1638 1638 # move this methods on models level.
1639 1639 from rhodecode.model.settings import SettingsModel
1640 1640
1641 1641 repo = self
1642 1642 _user_id, _time, _reason = self.locked
1643 1643
1644 1644 data = {
1645 1645 'repo_id': repo.repo_id,
1646 1646 'repo_name': repo.repo_name,
1647 1647 'repo_type': repo.repo_type,
1648 1648 'clone_uri': repo.clone_uri or '',
1649 1649 'url': url('summary_home', repo_name=self.repo_name, qualified=True),
1650 1650 'private': repo.private,
1651 1651 'created_on': repo.created_on,
1652 1652 'description': repo.description,
1653 1653 'landing_rev': repo.landing_rev,
1654 1654 'owner': repo.user.username,
1655 1655 'fork_of': repo.fork.repo_name if repo.fork else None,
1656 1656 'enable_statistics': repo.enable_statistics,
1657 1657 'enable_locking': repo.enable_locking,
1658 1658 'enable_downloads': repo.enable_downloads,
1659 1659 'last_changeset': repo.changeset_cache,
1660 1660 'locked_by': User.get(_user_id).get_api_data(
1661 1661 include_secrets=include_secrets) if _user_id else None,
1662 1662 'locked_date': time_to_datetime(_time) if _time else None,
1663 1663 'lock_reason': _reason if _reason else None,
1664 1664 }
1665 1665
1666 1666 # TODO: mikhail: should be per-repo settings here
1667 1667 rc_config = SettingsModel().get_all_settings()
1668 1668 repository_fields = str2bool(
1669 1669 rc_config.get('rhodecode_repository_fields'))
1670 1670 if repository_fields:
1671 1671 for f in self.extra_fields:
1672 1672 data[f.field_key_prefixed] = f.field_value
1673 1673
1674 1674 return data
1675 1675
1676 1676 @classmethod
1677 1677 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
1678 1678 if not lock_time:
1679 1679 lock_time = time.time()
1680 1680 if not lock_reason:
1681 1681 lock_reason = cls.LOCK_AUTOMATIC
1682 1682 repo.locked = [user_id, lock_time, lock_reason]
1683 1683 Session().add(repo)
1684 1684 Session().commit()
1685 1685
1686 1686 @classmethod
1687 1687 def unlock(cls, repo):
1688 1688 repo.locked = None
1689 1689 Session().add(repo)
1690 1690 Session().commit()
1691 1691
1692 1692 @classmethod
1693 1693 def getlock(cls, repo):
1694 1694 return repo.locked
1695 1695
1696 1696 def is_user_lock(self, user_id):
1697 1697 if self.lock[0]:
1698 1698 lock_user_id = safe_int(self.lock[0])
1699 1699 user_id = safe_int(user_id)
1700 1700 # both are ints, and they are equal
1701 1701 return all([lock_user_id, user_id]) and lock_user_id == user_id
1702 1702
1703 1703 return False
1704 1704
1705 1705 def get_locking_state(self, action, user_id, only_when_enabled=True):
1706 1706 """
1707 1707 Checks locking on this repository, if locking is enabled and lock is
1708 1708 present returns a tuple of make_lock, locked, locked_by.
1709 1709 make_lock can have 3 states None (do nothing) True, make lock
1710 1710 False release lock, This value is later propagated to hooks, which
1711 1711 do the locking. Think about this as signals passed to hooks what to do.
1712 1712
1713 1713 """
1714 1714 # TODO: johbo: This is part of the business logic and should be moved
1715 1715 # into the RepositoryModel.
1716 1716
1717 1717 if action not in ('push', 'pull'):
1718 1718 raise ValueError("Invalid action value: %s" % repr(action))
1719 1719
1720 1720 # defines if locked error should be thrown to user
1721 1721 currently_locked = False
1722 1722 # defines if new lock should be made, tri-state
1723 1723 make_lock = None
1724 1724 repo = self
1725 1725 user = User.get(user_id)
1726 1726
1727 1727 lock_info = repo.locked
1728 1728
1729 1729 if repo and (repo.enable_locking or not only_when_enabled):
1730 1730 if action == 'push':
1731 1731 # check if it's already locked !, if it is compare users
1732 1732 locked_by_user_id = lock_info[0]
1733 1733 if user.user_id == locked_by_user_id:
1734 1734 log.debug(
1735 1735 'Got `push` action from user %s, now unlocking', user)
1736 1736 # unlock if we have push from user who locked
1737 1737 make_lock = False
1738 1738 else:
1739 1739 # we're not the same user who locked, ban with
1740 1740 # code defined in settings (default is 423 HTTP Locked) !
1741 1741 log.debug('Repo %s is currently locked by %s', repo, user)
1742 1742 currently_locked = True
1743 1743 elif action == 'pull':
1744 1744 # [0] user [1] date
1745 1745 if lock_info[0] and lock_info[1]:
1746 1746 log.debug('Repo %s is currently locked by %s', repo, user)
1747 1747 currently_locked = True
1748 1748 else:
1749 1749 log.debug('Setting lock on repo %s by %s', repo, user)
1750 1750 make_lock = True
1751 1751
1752 1752 else:
1753 1753 log.debug('Repository %s do not have locking enabled', repo)
1754 1754
1755 1755 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
1756 1756 make_lock, currently_locked, lock_info)
1757 1757
1758 1758 from rhodecode.lib.auth import HasRepoPermissionAny
1759 1759 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
1760 1760 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
1761 1761 # if we don't have at least write permission we cannot make a lock
1762 1762 log.debug('lock state reset back to FALSE due to lack '
1763 1763 'of at least read permission')
1764 1764 make_lock = False
1765 1765
1766 1766 return make_lock, currently_locked, lock_info
1767 1767
1768 1768 @property
1769 1769 def last_db_change(self):
1770 1770 return self.updated_on
1771 1771
1772 1772 @property
1773 1773 def clone_uri_hidden(self):
1774 1774 clone_uri = self.clone_uri
1775 1775 if clone_uri:
1776 1776 import urlobject
1777 1777 url_obj = urlobject.URLObject(clone_uri)
1778 1778 if url_obj.password:
1779 1779 clone_uri = url_obj.with_password('*****')
1780 1780 return clone_uri
1781 1781
1782 1782 def clone_url(self, **override):
1783 1783 qualified_home_url = url('home', qualified=True)
1784 1784
1785 1785 uri_tmpl = None
1786 1786 if 'with_id' in override:
1787 1787 uri_tmpl = self.DEFAULT_CLONE_URI_ID
1788 1788 del override['with_id']
1789 1789
1790 1790 if 'uri_tmpl' in override:
1791 1791 uri_tmpl = override['uri_tmpl']
1792 1792 del override['uri_tmpl']
1793 1793
1794 1794 # we didn't override our tmpl from **overrides
1795 1795 if not uri_tmpl:
1796 1796 uri_tmpl = self.DEFAULT_CLONE_URI
1797 1797 try:
1798 1798 from pylons import tmpl_context as c
1799 1799 uri_tmpl = c.clone_uri_tmpl
1800 1800 except Exception:
1801 1801 # in any case if we call this outside of request context,
1802 1802 # ie, not having tmpl_context set up
1803 1803 pass
1804 1804
1805 1805 return get_clone_url(uri_tmpl=uri_tmpl,
1806 1806 qualifed_home_url=qualified_home_url,
1807 1807 repo_name=self.repo_name,
1808 1808 repo_id=self.repo_id, **override)
1809 1809
1810 1810 def set_state(self, state):
1811 1811 self.repo_state = state
1812 1812 Session().add(self)
1813 1813 #==========================================================================
1814 1814 # SCM PROPERTIES
1815 1815 #==========================================================================
1816 1816
1817 1817 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
1818 1818 return get_commit_safe(
1819 1819 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load)
1820 1820
1821 1821 def get_changeset(self, rev=None, pre_load=None):
1822 1822 warnings.warn("Use get_commit", DeprecationWarning)
1823 1823 commit_id = None
1824 1824 commit_idx = None
1825 1825 if isinstance(rev, basestring):
1826 1826 commit_id = rev
1827 1827 else:
1828 1828 commit_idx = rev
1829 1829 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
1830 1830 pre_load=pre_load)
1831 1831
1832 1832 def get_landing_commit(self):
1833 1833 """
1834 1834 Returns landing commit, or if that doesn't exist returns the tip
1835 1835 """
1836 1836 _rev_type, _rev = self.landing_rev
1837 1837 commit = self.get_commit(_rev)
1838 1838 if isinstance(commit, EmptyCommit):
1839 1839 return self.get_commit()
1840 1840 return commit
1841 1841
1842 1842 def update_commit_cache(self, cs_cache=None, config=None):
1843 1843 """
1844 1844 Update cache of last changeset for repository, keys should be::
1845 1845
1846 1846 short_id
1847 1847 raw_id
1848 1848 revision
1849 1849 parents
1850 1850 message
1851 1851 date
1852 1852 author
1853 1853
1854 1854 :param cs_cache:
1855 1855 """
1856 1856 from rhodecode.lib.vcs.backends.base import BaseChangeset
1857 1857 if cs_cache is None:
1858 1858 # use no-cache version here
1859 1859 scm_repo = self.scm_instance(cache=False, config=config)
1860 1860 if scm_repo:
1861 1861 cs_cache = scm_repo.get_commit(
1862 1862 pre_load=["author", "date", "message", "parents"])
1863 1863 else:
1864 1864 cs_cache = EmptyCommit()
1865 1865
1866 1866 if isinstance(cs_cache, BaseChangeset):
1867 1867 cs_cache = cs_cache.__json__()
1868 1868
1869 1869 def is_outdated(new_cs_cache):
1870 1870 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
1871 1871 new_cs_cache['revision'] != self.changeset_cache['revision']):
1872 1872 return True
1873 1873 return False
1874 1874
1875 1875 # check if we have maybe already latest cached revision
1876 1876 if is_outdated(cs_cache) or not self.changeset_cache:
1877 1877 _default = datetime.datetime.fromtimestamp(0)
1878 1878 last_change = cs_cache.get('date') or _default
1879 1879 log.debug('updated repo %s with new cs cache %s',
1880 1880 self.repo_name, cs_cache)
1881 1881 self.updated_on = last_change
1882 1882 self.changeset_cache = cs_cache
1883 1883 Session().add(self)
1884 1884 Session().commit()
1885 1885 else:
1886 1886 log.debug('Skipping update_commit_cache for repo:`%s` '
1887 1887 'commit already with latest changes', self.repo_name)
1888 1888
1889 1889 @property
1890 1890 def tip(self):
1891 1891 return self.get_commit('tip')
1892 1892
1893 1893 @property
1894 1894 def author(self):
1895 1895 return self.tip.author
1896 1896
1897 1897 @property
1898 1898 def last_change(self):
1899 1899 return self.scm_instance().last_change
1900 1900
1901 1901 def get_comments(self, revisions=None):
1902 1902 """
1903 1903 Returns comments for this repository grouped by revisions
1904 1904
1905 1905 :param revisions: filter query by revisions only
1906 1906 """
1907 1907 cmts = ChangesetComment.query()\
1908 1908 .filter(ChangesetComment.repo == self)
1909 1909 if revisions:
1910 1910 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
1911 1911 grouped = collections.defaultdict(list)
1912 1912 for cmt in cmts.all():
1913 1913 grouped[cmt.revision].append(cmt)
1914 1914 return grouped
1915 1915
1916 1916 def statuses(self, revisions=None):
1917 1917 """
1918 1918 Returns statuses for this repository
1919 1919
1920 1920 :param revisions: list of revisions to get statuses for
1921 1921 """
1922 1922 statuses = ChangesetStatus.query()\
1923 1923 .filter(ChangesetStatus.repo == self)\
1924 1924 .filter(ChangesetStatus.version == 0)
1925 1925
1926 1926 if revisions:
1927 1927 # Try doing the filtering in chunks to avoid hitting limits
1928 1928 size = 500
1929 1929 status_results = []
1930 1930 for chunk in xrange(0, len(revisions), size):
1931 1931 status_results += statuses.filter(
1932 1932 ChangesetStatus.revision.in_(
1933 1933 revisions[chunk: chunk+size])
1934 1934 ).all()
1935 1935 else:
1936 1936 status_results = statuses.all()
1937 1937
1938 1938 grouped = {}
1939 1939
1940 1940 # maybe we have open new pullrequest without a status?
1941 1941 stat = ChangesetStatus.STATUS_UNDER_REVIEW
1942 1942 status_lbl = ChangesetStatus.get_status_lbl(stat)
1943 1943 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
1944 1944 for rev in pr.revisions:
1945 1945 pr_id = pr.pull_request_id
1946 1946 pr_repo = pr.target_repo.repo_name
1947 1947 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
1948 1948
1949 1949 for stat in status_results:
1950 1950 pr_id = pr_repo = None
1951 1951 if stat.pull_request:
1952 1952 pr_id = stat.pull_request.pull_request_id
1953 1953 pr_repo = stat.pull_request.target_repo.repo_name
1954 1954 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
1955 1955 pr_id, pr_repo]
1956 1956 return grouped
1957 1957
1958 1958 # ==========================================================================
1959 1959 # SCM CACHE INSTANCE
1960 1960 # ==========================================================================
1961 1961
1962 1962 def scm_instance(self, **kwargs):
1963 1963 import rhodecode
1964 1964
1965 1965 # Passing a config will not hit the cache currently only used
1966 1966 # for repo2dbmapper
1967 1967 config = kwargs.pop('config', None)
1968 1968 cache = kwargs.pop('cache', None)
1969 1969 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
1970 1970 # if cache is NOT defined use default global, else we have a full
1971 1971 # control over cache behaviour
1972 1972 if cache is None and full_cache and not config:
1973 1973 return self._get_instance_cached()
1974 1974 return self._get_instance(cache=bool(cache), config=config)
1975 1975
1976 1976 def _get_instance_cached(self):
1977 1977 @cache_region('long_term')
1978 1978 def _get_repo(cache_key):
1979 1979 return self._get_instance()
1980 1980
1981 1981 invalidator_context = CacheKey.repo_context_cache(
1982 1982 _get_repo, self.repo_name, None, thread_scoped=True)
1983 1983
1984 1984 with invalidator_context as context:
1985 1985 context.invalidate()
1986 1986 repo = context.compute()
1987 1987
1988 1988 return repo
1989 1989
1990 1990 def _get_instance(self, cache=True, config=None):
1991 1991 config = config or self._config
1992 1992 custom_wire = {
1993 1993 'cache': cache # controls the vcs.remote cache
1994 1994 }
1995 1995
1996 1996 repo = get_vcs_instance(
1997 1997 repo_path=safe_str(self.repo_full_path),
1998 1998 config=config,
1999 1999 with_wire=custom_wire,
2000 2000 create=False)
2001 2001
2002 2002 return repo
2003 2003
2004 2004 def __json__(self):
2005 2005 return {'landing_rev': self.landing_rev}
2006 2006
2007 2007 def get_dict(self):
2008 2008
2009 2009 # Since we transformed `repo_name` to a hybrid property, we need to
2010 2010 # keep compatibility with the code which uses `repo_name` field.
2011 2011
2012 2012 result = super(Repository, self).get_dict()
2013 2013 result['repo_name'] = result.pop('_repo_name', None)
2014 2014 return result
2015 2015
2016 2016
2017 2017 class RepoGroup(Base, BaseModel):
2018 2018 __tablename__ = 'groups'
2019 2019 __table_args__ = (
2020 2020 UniqueConstraint('group_name', 'group_parent_id'),
2021 2021 CheckConstraint('group_id != group_parent_id'),
2022 2022 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2023 2023 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2024 2024 )
2025 2025 __mapper_args__ = {'order_by': 'group_name'}
2026 2026
2027 2027 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2028 2028
2029 2029 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2030 2030 group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2031 2031 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2032 2032 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2033 2033 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2034 2034 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2035 2035 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2036 2036
2037 2037 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
2038 2038 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
2039 2039 parent_group = relationship('RepoGroup', remote_side=group_id)
2040 2040 user = relationship('User')
2041 2041 integrations = relationship('Integration',
2042 2042 cascade="all, delete, delete-orphan")
2043 2043
2044 2044 def __init__(self, group_name='', parent_group=None):
2045 2045 self.group_name = group_name
2046 2046 self.parent_group = parent_group
2047 2047
2048 2048 def __unicode__(self):
2049 2049 return u"<%s('id:%s:%s')>" % (self.__class__.__name__, self.group_id,
2050 2050 self.group_name)
2051 2051
2052 2052 @classmethod
2053 2053 def _generate_choice(cls, repo_group):
2054 2054 from webhelpers.html import literal as _literal
2055 2055 _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k))
2056 2056 return repo_group.group_id, _name(repo_group.full_path_splitted)
2057 2057
2058 2058 @classmethod
2059 2059 def groups_choices(cls, groups=None, show_empty_group=True):
2060 2060 if not groups:
2061 2061 groups = cls.query().all()
2062 2062
2063 2063 repo_groups = []
2064 2064 if show_empty_group:
2065 2065 repo_groups = [('-1', u'-- %s --' % _('No parent'))]
2066 2066
2067 2067 repo_groups.extend([cls._generate_choice(x) for x in groups])
2068 2068
2069 2069 repo_groups = sorted(
2070 2070 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2071 2071 return repo_groups
2072 2072
2073 2073 @classmethod
2074 2074 def url_sep(cls):
2075 2075 return URL_SEP
2076 2076
2077 2077 @classmethod
2078 2078 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2079 2079 if case_insensitive:
2080 2080 gr = cls.query().filter(func.lower(cls.group_name)
2081 2081 == func.lower(group_name))
2082 2082 else:
2083 2083 gr = cls.query().filter(cls.group_name == group_name)
2084 2084 if cache:
2085 2085 gr = gr.options(FromCache(
2086 2086 "sql_cache_short",
2087 2087 "get_group_%s" % _hash_key(group_name)))
2088 2088 return gr.scalar()
2089 2089
2090 2090 @classmethod
2091 2091 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2092 2092 case_insensitive=True):
2093 2093 q = RepoGroup.query()
2094 2094
2095 2095 if not isinstance(user_id, Optional):
2096 2096 q = q.filter(RepoGroup.user_id == user_id)
2097 2097
2098 2098 if not isinstance(group_id, Optional):
2099 2099 q = q.filter(RepoGroup.group_parent_id == group_id)
2100 2100
2101 2101 if case_insensitive:
2102 2102 q = q.order_by(func.lower(RepoGroup.group_name))
2103 2103 else:
2104 2104 q = q.order_by(RepoGroup.group_name)
2105 2105 return q.all()
2106 2106
2107 2107 @property
2108 2108 def parents(self):
2109 2109 parents_recursion_limit = 10
2110 2110 groups = []
2111 2111 if self.parent_group is None:
2112 2112 return groups
2113 2113 cur_gr = self.parent_group
2114 2114 groups.insert(0, cur_gr)
2115 2115 cnt = 0
2116 2116 while 1:
2117 2117 cnt += 1
2118 2118 gr = getattr(cur_gr, 'parent_group', None)
2119 2119 cur_gr = cur_gr.parent_group
2120 2120 if gr is None:
2121 2121 break
2122 2122 if cnt == parents_recursion_limit:
2123 2123 # this will prevent accidental infinit loops
2124 2124 log.error(('more than %s parents found for group %s, stopping '
2125 2125 'recursive parent fetching' % (parents_recursion_limit, self)))
2126 2126 break
2127 2127
2128 2128 groups.insert(0, gr)
2129 2129 return groups
2130 2130
2131 2131 @property
2132 2132 def children(self):
2133 2133 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2134 2134
2135 2135 @property
2136 2136 def name(self):
2137 2137 return self.group_name.split(RepoGroup.url_sep())[-1]
2138 2138
2139 2139 @property
2140 2140 def full_path(self):
2141 2141 return self.group_name
2142 2142
2143 2143 @property
2144 2144 def full_path_splitted(self):
2145 2145 return self.group_name.split(RepoGroup.url_sep())
2146 2146
2147 2147 @property
2148 2148 def repositories(self):
2149 2149 return Repository.query()\
2150 2150 .filter(Repository.group == self)\
2151 2151 .order_by(Repository.repo_name)
2152 2152
2153 2153 @property
2154 2154 def repositories_recursive_count(self):
2155 2155 cnt = self.repositories.count()
2156 2156
2157 2157 def children_count(group):
2158 2158 cnt = 0
2159 2159 for child in group.children:
2160 2160 cnt += child.repositories.count()
2161 2161 cnt += children_count(child)
2162 2162 return cnt
2163 2163
2164 2164 return cnt + children_count(self)
2165 2165
2166 2166 def _recursive_objects(self, include_repos=True):
2167 2167 all_ = []
2168 2168
2169 2169 def _get_members(root_gr):
2170 2170 if include_repos:
2171 2171 for r in root_gr.repositories:
2172 2172 all_.append(r)
2173 2173 childs = root_gr.children.all()
2174 2174 if childs:
2175 2175 for gr in childs:
2176 2176 all_.append(gr)
2177 2177 _get_members(gr)
2178 2178
2179 2179 _get_members(self)
2180 2180 return [self] + all_
2181 2181
2182 2182 def recursive_groups_and_repos(self):
2183 2183 """
2184 2184 Recursive return all groups, with repositories in those groups
2185 2185 """
2186 2186 return self._recursive_objects()
2187 2187
2188 2188 def recursive_groups(self):
2189 2189 """
2190 2190 Returns all children groups for this group including children of children
2191 2191 """
2192 2192 return self._recursive_objects(include_repos=False)
2193 2193
2194 2194 def get_new_name(self, group_name):
2195 2195 """
2196 2196 returns new full group name based on parent and new name
2197 2197
2198 2198 :param group_name:
2199 2199 """
2200 2200 path_prefix = (self.parent_group.full_path_splitted if
2201 2201 self.parent_group else [])
2202 2202 return RepoGroup.url_sep().join(path_prefix + [group_name])
2203 2203
2204 2204 def permissions(self, with_admins=True, with_owner=True):
2205 2205 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
2206 2206 q = q.options(joinedload(UserRepoGroupToPerm.group),
2207 2207 joinedload(UserRepoGroupToPerm.user),
2208 2208 joinedload(UserRepoGroupToPerm.permission),)
2209 2209
2210 2210 # get owners and admins and permissions. We do a trick of re-writing
2211 2211 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2212 2212 # has a global reference and changing one object propagates to all
2213 2213 # others. This means if admin is also an owner admin_row that change
2214 2214 # would propagate to both objects
2215 2215 perm_rows = []
2216 2216 for _usr in q.all():
2217 2217 usr = AttributeDict(_usr.user.get_dict())
2218 2218 usr.permission = _usr.permission.permission_name
2219 2219 perm_rows.append(usr)
2220 2220
2221 2221 # filter the perm rows by 'default' first and then sort them by
2222 2222 # admin,write,read,none permissions sorted again alphabetically in
2223 2223 # each group
2224 2224 perm_rows = sorted(perm_rows, key=display_sort)
2225 2225
2226 2226 _admin_perm = 'group.admin'
2227 2227 owner_row = []
2228 2228 if with_owner:
2229 2229 usr = AttributeDict(self.user.get_dict())
2230 2230 usr.owner_row = True
2231 2231 usr.permission = _admin_perm
2232 2232 owner_row.append(usr)
2233 2233
2234 2234 super_admin_rows = []
2235 2235 if with_admins:
2236 2236 for usr in User.get_all_super_admins():
2237 2237 # if this admin is also owner, don't double the record
2238 2238 if usr.user_id == owner_row[0].user_id:
2239 2239 owner_row[0].admin_row = True
2240 2240 else:
2241 2241 usr = AttributeDict(usr.get_dict())
2242 2242 usr.admin_row = True
2243 2243 usr.permission = _admin_perm
2244 2244 super_admin_rows.append(usr)
2245 2245
2246 2246 return super_admin_rows + owner_row + perm_rows
2247 2247
2248 2248 def permission_user_groups(self):
2249 2249 q = UserGroupRepoGroupToPerm.query().filter(UserGroupRepoGroupToPerm.group == self)
2250 2250 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
2251 2251 joinedload(UserGroupRepoGroupToPerm.users_group),
2252 2252 joinedload(UserGroupRepoGroupToPerm.permission),)
2253 2253
2254 2254 perm_rows = []
2255 2255 for _user_group in q.all():
2256 2256 usr = AttributeDict(_user_group.users_group.get_dict())
2257 2257 usr.permission = _user_group.permission.permission_name
2258 2258 perm_rows.append(usr)
2259 2259
2260 2260 return perm_rows
2261 2261
2262 2262 def get_api_data(self):
2263 2263 """
2264 2264 Common function for generating api data
2265 2265
2266 2266 """
2267 2267 group = self
2268 2268 data = {
2269 2269 'group_id': group.group_id,
2270 2270 'group_name': group.group_name,
2271 2271 'group_description': group.group_description,
2272 2272 'parent_group': group.parent_group.group_name if group.parent_group else None,
2273 2273 'repositories': [x.repo_name for x in group.repositories],
2274 2274 'owner': group.user.username,
2275 2275 }
2276 2276 return data
2277 2277
2278 2278
2279 2279 class Permission(Base, BaseModel):
2280 2280 __tablename__ = 'permissions'
2281 2281 __table_args__ = (
2282 2282 Index('p_perm_name_idx', 'permission_name'),
2283 2283 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2284 2284 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2285 2285 )
2286 2286 PERMS = [
2287 2287 ('hg.admin', _('RhodeCode Super Administrator')),
2288 2288
2289 2289 ('repository.none', _('Repository no access')),
2290 2290 ('repository.read', _('Repository read access')),
2291 2291 ('repository.write', _('Repository write access')),
2292 2292 ('repository.admin', _('Repository admin access')),
2293 2293
2294 2294 ('group.none', _('Repository group no access')),
2295 2295 ('group.read', _('Repository group read access')),
2296 2296 ('group.write', _('Repository group write access')),
2297 2297 ('group.admin', _('Repository group admin access')),
2298 2298
2299 2299 ('usergroup.none', _('User group no access')),
2300 2300 ('usergroup.read', _('User group read access')),
2301 2301 ('usergroup.write', _('User group write access')),
2302 2302 ('usergroup.admin', _('User group admin access')),
2303 2303
2304 2304 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
2305 2305 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
2306 2306
2307 2307 ('hg.usergroup.create.false', _('User Group creation disabled')),
2308 2308 ('hg.usergroup.create.true', _('User Group creation enabled')),
2309 2309
2310 2310 ('hg.create.none', _('Repository creation disabled')),
2311 2311 ('hg.create.repository', _('Repository creation enabled')),
2312 2312 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
2313 2313 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
2314 2314
2315 2315 ('hg.fork.none', _('Repository forking disabled')),
2316 2316 ('hg.fork.repository', _('Repository forking enabled')),
2317 2317
2318 2318 ('hg.register.none', _('Registration disabled')),
2319 2319 ('hg.register.manual_activate', _('User Registration with manual account activation')),
2320 2320 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
2321 2321
2322 2322 ('hg.extern_activate.manual', _('Manual activation of external account')),
2323 2323 ('hg.extern_activate.auto', _('Automatic activation of external account')),
2324 2324
2325 2325 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
2326 2326 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
2327 2327 ]
2328 2328
2329 2329 # definition of system default permissions for DEFAULT user
2330 2330 DEFAULT_USER_PERMISSIONS = [
2331 2331 'repository.read',
2332 2332 'group.read',
2333 2333 'usergroup.read',
2334 2334 'hg.create.repository',
2335 2335 'hg.repogroup.create.false',
2336 2336 'hg.usergroup.create.false',
2337 2337 'hg.create.write_on_repogroup.true',
2338 2338 'hg.fork.repository',
2339 2339 'hg.register.manual_activate',
2340 2340 'hg.extern_activate.auto',
2341 2341 'hg.inherit_default_perms.true',
2342 2342 ]
2343 2343
2344 2344 # defines which permissions are more important higher the more important
2345 2345 # Weight defines which permissions are more important.
2346 2346 # The higher number the more important.
2347 2347 PERM_WEIGHTS = {
2348 2348 'repository.none': 0,
2349 2349 'repository.read': 1,
2350 2350 'repository.write': 3,
2351 2351 'repository.admin': 4,
2352 2352
2353 2353 'group.none': 0,
2354 2354 'group.read': 1,
2355 2355 'group.write': 3,
2356 2356 'group.admin': 4,
2357 2357
2358 2358 'usergroup.none': 0,
2359 2359 'usergroup.read': 1,
2360 2360 'usergroup.write': 3,
2361 2361 'usergroup.admin': 4,
2362 2362
2363 2363 'hg.repogroup.create.false': 0,
2364 2364 'hg.repogroup.create.true': 1,
2365 2365
2366 2366 'hg.usergroup.create.false': 0,
2367 2367 'hg.usergroup.create.true': 1,
2368 2368
2369 2369 'hg.fork.none': 0,
2370 2370 'hg.fork.repository': 1,
2371 2371 'hg.create.none': 0,
2372 2372 'hg.create.repository': 1
2373 2373 }
2374 2374
2375 2375 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2376 2376 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
2377 2377 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
2378 2378
2379 2379 def __unicode__(self):
2380 2380 return u"<%s('%s:%s')>" % (
2381 2381 self.__class__.__name__, self.permission_id, self.permission_name
2382 2382 )
2383 2383
2384 2384 @classmethod
2385 2385 def get_by_key(cls, key):
2386 2386 return cls.query().filter(cls.permission_name == key).scalar()
2387 2387
2388 2388 @classmethod
2389 2389 def get_default_repo_perms(cls, user_id, repo_id=None):
2390 2390 q = Session().query(UserRepoToPerm, Repository, Permission)\
2391 2391 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
2392 2392 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
2393 2393 .filter(UserRepoToPerm.user_id == user_id)
2394 2394 if repo_id:
2395 2395 q = q.filter(UserRepoToPerm.repository_id == repo_id)
2396 2396 return q.all()
2397 2397
2398 2398 @classmethod
2399 2399 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
2400 2400 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
2401 2401 .join(
2402 2402 Permission,
2403 2403 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
2404 2404 .join(
2405 2405 Repository,
2406 2406 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
2407 2407 .join(
2408 2408 UserGroup,
2409 2409 UserGroupRepoToPerm.users_group_id ==
2410 2410 UserGroup.users_group_id)\
2411 2411 .join(
2412 2412 UserGroupMember,
2413 2413 UserGroupRepoToPerm.users_group_id ==
2414 2414 UserGroupMember.users_group_id)\
2415 2415 .filter(
2416 2416 UserGroupMember.user_id == user_id,
2417 2417 UserGroup.users_group_active == true())
2418 2418 if repo_id:
2419 2419 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
2420 2420 return q.all()
2421 2421
2422 2422 @classmethod
2423 2423 def get_default_group_perms(cls, user_id, repo_group_id=None):
2424 2424 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
2425 2425 .join((Permission, UserRepoGroupToPerm.permission_id == Permission.permission_id))\
2426 2426 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
2427 2427 .filter(UserRepoGroupToPerm.user_id == user_id)
2428 2428 if repo_group_id:
2429 2429 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
2430 2430 return q.all()
2431 2431
2432 2432 @classmethod
2433 2433 def get_default_group_perms_from_user_group(
2434 2434 cls, user_id, repo_group_id=None):
2435 2435 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
2436 2436 .join(
2437 2437 Permission,
2438 2438 UserGroupRepoGroupToPerm.permission_id ==
2439 2439 Permission.permission_id)\
2440 2440 .join(
2441 2441 RepoGroup,
2442 2442 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
2443 2443 .join(
2444 2444 UserGroup,
2445 2445 UserGroupRepoGroupToPerm.users_group_id ==
2446 2446 UserGroup.users_group_id)\
2447 2447 .join(
2448 2448 UserGroupMember,
2449 2449 UserGroupRepoGroupToPerm.users_group_id ==
2450 2450 UserGroupMember.users_group_id)\
2451 2451 .filter(
2452 2452 UserGroupMember.user_id == user_id,
2453 2453 UserGroup.users_group_active == true())
2454 2454 if repo_group_id:
2455 2455 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
2456 2456 return q.all()
2457 2457
2458 2458 @classmethod
2459 2459 def get_default_user_group_perms(cls, user_id, user_group_id=None):
2460 2460 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
2461 2461 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
2462 2462 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
2463 2463 .filter(UserUserGroupToPerm.user_id == user_id)
2464 2464 if user_group_id:
2465 2465 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
2466 2466 return q.all()
2467 2467
2468 2468 @classmethod
2469 2469 def get_default_user_group_perms_from_user_group(
2470 2470 cls, user_id, user_group_id=None):
2471 2471 TargetUserGroup = aliased(UserGroup, name='target_user_group')
2472 2472 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
2473 2473 .join(
2474 2474 Permission,
2475 2475 UserGroupUserGroupToPerm.permission_id ==
2476 2476 Permission.permission_id)\
2477 2477 .join(
2478 2478 TargetUserGroup,
2479 2479 UserGroupUserGroupToPerm.target_user_group_id ==
2480 2480 TargetUserGroup.users_group_id)\
2481 2481 .join(
2482 2482 UserGroup,
2483 2483 UserGroupUserGroupToPerm.user_group_id ==
2484 2484 UserGroup.users_group_id)\
2485 2485 .join(
2486 2486 UserGroupMember,
2487 2487 UserGroupUserGroupToPerm.user_group_id ==
2488 2488 UserGroupMember.users_group_id)\
2489 2489 .filter(
2490 2490 UserGroupMember.user_id == user_id,
2491 2491 UserGroup.users_group_active == true())
2492 2492 if user_group_id:
2493 2493 q = q.filter(
2494 2494 UserGroupUserGroupToPerm.user_group_id == user_group_id)
2495 2495
2496 2496 return q.all()
2497 2497
2498 2498
2499 2499 class UserRepoToPerm(Base, BaseModel):
2500 2500 __tablename__ = 'repo_to_perm'
2501 2501 __table_args__ = (
2502 2502 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
2503 2503 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2504 2504 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2505 2505 )
2506 2506 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2507 2507 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2508 2508 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2509 2509 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2510 2510
2511 2511 user = relationship('User')
2512 2512 repository = relationship('Repository')
2513 2513 permission = relationship('Permission')
2514 2514
2515 2515 @classmethod
2516 2516 def create(cls, user, repository, permission):
2517 2517 n = cls()
2518 2518 n.user = user
2519 2519 n.repository = repository
2520 2520 n.permission = permission
2521 2521 Session().add(n)
2522 2522 return n
2523 2523
2524 2524 def __unicode__(self):
2525 2525 return u'<%s => %s >' % (self.user, self.repository)
2526 2526
2527 2527
2528 2528 class UserUserGroupToPerm(Base, BaseModel):
2529 2529 __tablename__ = 'user_user_group_to_perm'
2530 2530 __table_args__ = (
2531 2531 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
2532 2532 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2533 2533 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2534 2534 )
2535 2535 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2536 2536 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2537 2537 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2538 2538 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2539 2539
2540 2540 user = relationship('User')
2541 2541 user_group = relationship('UserGroup')
2542 2542 permission = relationship('Permission')
2543 2543
2544 2544 @classmethod
2545 2545 def create(cls, user, user_group, permission):
2546 2546 n = cls()
2547 2547 n.user = user
2548 2548 n.user_group = user_group
2549 2549 n.permission = permission
2550 2550 Session().add(n)
2551 2551 return n
2552 2552
2553 2553 def __unicode__(self):
2554 2554 return u'<%s => %s >' % (self.user, self.user_group)
2555 2555
2556 2556
2557 2557 class UserToPerm(Base, BaseModel):
2558 2558 __tablename__ = 'user_to_perm'
2559 2559 __table_args__ = (
2560 2560 UniqueConstraint('user_id', 'permission_id'),
2561 2561 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2562 2562 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2563 2563 )
2564 2564 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2565 2565 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2566 2566 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2567 2567
2568 2568 user = relationship('User')
2569 2569 permission = relationship('Permission', lazy='joined')
2570 2570
2571 2571 def __unicode__(self):
2572 2572 return u'<%s => %s >' % (self.user, self.permission)
2573 2573
2574 2574
2575 2575 class UserGroupRepoToPerm(Base, BaseModel):
2576 2576 __tablename__ = 'users_group_repo_to_perm'
2577 2577 __table_args__ = (
2578 2578 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
2579 2579 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2580 2580 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2581 2581 )
2582 2582 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2583 2583 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2584 2584 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2585 2585 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2586 2586
2587 2587 users_group = relationship('UserGroup')
2588 2588 permission = relationship('Permission')
2589 2589 repository = relationship('Repository')
2590 2590
2591 2591 @classmethod
2592 2592 def create(cls, users_group, repository, permission):
2593 2593 n = cls()
2594 2594 n.users_group = users_group
2595 2595 n.repository = repository
2596 2596 n.permission = permission
2597 2597 Session().add(n)
2598 2598 return n
2599 2599
2600 2600 def __unicode__(self):
2601 2601 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
2602 2602
2603 2603
2604 2604 class UserGroupUserGroupToPerm(Base, BaseModel):
2605 2605 __tablename__ = 'user_group_user_group_to_perm'
2606 2606 __table_args__ = (
2607 2607 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
2608 2608 CheckConstraint('target_user_group_id != user_group_id'),
2609 2609 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2610 2610 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2611 2611 )
2612 2612 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)
2613 2613 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2614 2614 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2615 2615 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2616 2616
2617 2617 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
2618 2618 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
2619 2619 permission = relationship('Permission')
2620 2620
2621 2621 @classmethod
2622 2622 def create(cls, target_user_group, user_group, permission):
2623 2623 n = cls()
2624 2624 n.target_user_group = target_user_group
2625 2625 n.user_group = user_group
2626 2626 n.permission = permission
2627 2627 Session().add(n)
2628 2628 return n
2629 2629
2630 2630 def __unicode__(self):
2631 2631 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
2632 2632
2633 2633
2634 2634 class UserGroupToPerm(Base, BaseModel):
2635 2635 __tablename__ = 'users_group_to_perm'
2636 2636 __table_args__ = (
2637 2637 UniqueConstraint('users_group_id', 'permission_id',),
2638 2638 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2639 2639 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2640 2640 )
2641 2641 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2642 2642 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2643 2643 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2644 2644
2645 2645 users_group = relationship('UserGroup')
2646 2646 permission = relationship('Permission')
2647 2647
2648 2648
2649 2649 class UserRepoGroupToPerm(Base, BaseModel):
2650 2650 __tablename__ = 'user_repo_group_to_perm'
2651 2651 __table_args__ = (
2652 2652 UniqueConstraint('user_id', 'group_id', 'permission_id'),
2653 2653 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2654 2654 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2655 2655 )
2656 2656
2657 2657 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2658 2658 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2659 2659 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2660 2660 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2661 2661
2662 2662 user = relationship('User')
2663 2663 group = relationship('RepoGroup')
2664 2664 permission = relationship('Permission')
2665 2665
2666 2666 @classmethod
2667 2667 def create(cls, user, repository_group, permission):
2668 2668 n = cls()
2669 2669 n.user = user
2670 2670 n.group = repository_group
2671 2671 n.permission = permission
2672 2672 Session().add(n)
2673 2673 return n
2674 2674
2675 2675
2676 2676 class UserGroupRepoGroupToPerm(Base, BaseModel):
2677 2677 __tablename__ = 'users_group_repo_group_to_perm'
2678 2678 __table_args__ = (
2679 2679 UniqueConstraint('users_group_id', 'group_id'),
2680 2680 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2681 2681 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2682 2682 )
2683 2683
2684 2684 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)
2685 2685 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2686 2686 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2687 2687 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2688 2688
2689 2689 users_group = relationship('UserGroup')
2690 2690 permission = relationship('Permission')
2691 2691 group = relationship('RepoGroup')
2692 2692
2693 2693 @classmethod
2694 2694 def create(cls, user_group, repository_group, permission):
2695 2695 n = cls()
2696 2696 n.users_group = user_group
2697 2697 n.group = repository_group
2698 2698 n.permission = permission
2699 2699 Session().add(n)
2700 2700 return n
2701 2701
2702 2702 def __unicode__(self):
2703 2703 return u'<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
2704 2704
2705 2705
2706 2706 class Statistics(Base, BaseModel):
2707 2707 __tablename__ = 'statistics'
2708 2708 __table_args__ = (
2709 2709 UniqueConstraint('repository_id'),
2710 2710 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2711 2711 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2712 2712 )
2713 2713 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2714 2714 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
2715 2715 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
2716 2716 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
2717 2717 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
2718 2718 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
2719 2719
2720 2720 repository = relationship('Repository', single_parent=True)
2721 2721
2722 2722
2723 2723 class UserFollowing(Base, BaseModel):
2724 2724 __tablename__ = 'user_followings'
2725 2725 __table_args__ = (
2726 2726 UniqueConstraint('user_id', 'follows_repository_id'),
2727 2727 UniqueConstraint('user_id', 'follows_user_id'),
2728 2728 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2729 2729 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2730 2730 )
2731 2731
2732 2732 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2733 2733 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2734 2734 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
2735 2735 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
2736 2736 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2737 2737
2738 2738 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
2739 2739
2740 2740 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
2741 2741 follows_repository = relationship('Repository', order_by='Repository.repo_name')
2742 2742
2743 2743 @classmethod
2744 2744 def get_repo_followers(cls, repo_id):
2745 2745 return cls.query().filter(cls.follows_repo_id == repo_id)
2746 2746
2747 2747
2748 2748 class CacheKey(Base, BaseModel):
2749 2749 __tablename__ = 'cache_invalidation'
2750 2750 __table_args__ = (
2751 2751 UniqueConstraint('cache_key'),
2752 2752 Index('key_idx', 'cache_key'),
2753 2753 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2754 2754 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2755 2755 )
2756 2756 CACHE_TYPE_ATOM = 'ATOM'
2757 2757 CACHE_TYPE_RSS = 'RSS'
2758 2758 CACHE_TYPE_README = 'README'
2759 2759
2760 2760 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2761 2761 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
2762 2762 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
2763 2763 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
2764 2764
2765 2765 def __init__(self, cache_key, cache_args=''):
2766 2766 self.cache_key = cache_key
2767 2767 self.cache_args = cache_args
2768 2768 self.cache_active = False
2769 2769
2770 2770 def __unicode__(self):
2771 2771 return u"<%s('%s:%s[%s]')>" % (
2772 2772 self.__class__.__name__,
2773 2773 self.cache_id, self.cache_key, self.cache_active)
2774 2774
2775 2775 def _cache_key_partition(self):
2776 2776 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
2777 2777 return prefix, repo_name, suffix
2778 2778
2779 2779 def get_prefix(self):
2780 2780 """
2781 2781 Try to extract prefix from existing cache key. The key could consist
2782 2782 of prefix, repo_name, suffix
2783 2783 """
2784 2784 # this returns prefix, repo_name, suffix
2785 2785 return self._cache_key_partition()[0]
2786 2786
2787 2787 def get_suffix(self):
2788 2788 """
2789 2789 get suffix that might have been used in _get_cache_key to
2790 2790 generate self.cache_key. Only used for informational purposes
2791 2791 in repo_edit.html.
2792 2792 """
2793 2793 # prefix, repo_name, suffix
2794 2794 return self._cache_key_partition()[2]
2795 2795
2796 2796 @classmethod
2797 2797 def delete_all_cache(cls):
2798 2798 """
2799 2799 Delete all cache keys from database.
2800 2800 Should only be run when all instances are down and all entries
2801 2801 thus stale.
2802 2802 """
2803 2803 cls.query().delete()
2804 2804 Session().commit()
2805 2805
2806 2806 @classmethod
2807 2807 def get_cache_key(cls, repo_name, cache_type):
2808 2808 """
2809 2809
2810 2810 Generate a cache key for this process of RhodeCode instance.
2811 2811 Prefix most likely will be process id or maybe explicitly set
2812 2812 instance_id from .ini file.
2813 2813 """
2814 2814 import rhodecode
2815 2815 prefix = safe_unicode(rhodecode.CONFIG.get('instance_id') or '')
2816 2816
2817 2817 repo_as_unicode = safe_unicode(repo_name)
2818 2818 key = u'{}_{}'.format(repo_as_unicode, cache_type) \
2819 2819 if cache_type else repo_as_unicode
2820 2820
2821 2821 return u'{}{}'.format(prefix, key)
2822 2822
2823 2823 @classmethod
2824 2824 def set_invalidate(cls, repo_name, delete=False):
2825 2825 """
2826 2826 Mark all caches of a repo as invalid in the database.
2827 2827 """
2828 2828
2829 2829 try:
2830 2830 qry = Session().query(cls).filter(cls.cache_args == repo_name)
2831 2831 if delete:
2832 2832 log.debug('cache objects deleted for repo %s',
2833 2833 safe_str(repo_name))
2834 2834 qry.delete()
2835 2835 else:
2836 2836 log.debug('cache objects marked as invalid for repo %s',
2837 2837 safe_str(repo_name))
2838 2838 qry.update({"cache_active": False})
2839 2839
2840 2840 Session().commit()
2841 2841 except Exception:
2842 2842 log.exception(
2843 2843 'Cache key invalidation failed for repository %s',
2844 2844 safe_str(repo_name))
2845 2845 Session().rollback()
2846 2846
2847 2847 @classmethod
2848 2848 def get_active_cache(cls, cache_key):
2849 2849 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
2850 2850 if inv_obj:
2851 2851 return inv_obj
2852 2852 return None
2853 2853
2854 2854 @classmethod
2855 2855 def repo_context_cache(cls, compute_func, repo_name, cache_type,
2856 2856 thread_scoped=False):
2857 2857 """
2858 2858 @cache_region('long_term')
2859 2859 def _heavy_calculation(cache_key):
2860 2860 return 'result'
2861 2861
2862 2862 cache_context = CacheKey.repo_context_cache(
2863 2863 _heavy_calculation, repo_name, cache_type)
2864 2864
2865 2865 with cache_context as context:
2866 2866 context.invalidate()
2867 2867 computed = context.compute()
2868 2868
2869 2869 assert computed == 'result'
2870 2870 """
2871 2871 from rhodecode.lib import caches
2872 2872 return caches.InvalidationContext(
2873 2873 compute_func, repo_name, cache_type, thread_scoped=thread_scoped)
2874 2874
2875 2875
2876 2876 class ChangesetComment(Base, BaseModel):
2877 2877 __tablename__ = 'changeset_comments'
2878 2878 __table_args__ = (
2879 2879 Index('cc_revision_idx', 'revision'),
2880 2880 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2881 2881 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2882 2882 )
2883 2883
2884 2884 COMMENT_OUTDATED = u'comment_outdated'
2885 2885
2886 2886 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
2887 2887 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2888 2888 revision = Column('revision', String(40), nullable=True)
2889 2889 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
2890 2890 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
2891 2891 line_no = Column('line_no', Unicode(10), nullable=True)
2892 2892 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
2893 2893 f_path = Column('f_path', Unicode(1000), nullable=True)
2894 2894 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
2895 2895 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
2896 2896 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2897 2897 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2898 2898 renderer = Column('renderer', Unicode(64), nullable=True)
2899 2899 display_state = Column('display_state', Unicode(128), nullable=True)
2900 2900
2901 2901 author = relationship('User', lazy='joined')
2902 2902 repo = relationship('Repository')
2903 2903 status_change = relationship('ChangesetStatus', cascade="all, delete, delete-orphan")
2904 2904 pull_request = relationship('PullRequest', lazy='joined')
2905 2905 pull_request_version = relationship('PullRequestVersion')
2906 2906
2907 2907 @classmethod
2908 2908 def get_users(cls, revision=None, pull_request_id=None):
2909 2909 """
2910 2910 Returns user associated with this ChangesetComment. ie those
2911 2911 who actually commented
2912 2912
2913 2913 :param cls:
2914 2914 :param revision:
2915 2915 """
2916 2916 q = Session().query(User)\
2917 2917 .join(ChangesetComment.author)
2918 2918 if revision:
2919 2919 q = q.filter(cls.revision == revision)
2920 2920 elif pull_request_id:
2921 2921 q = q.filter(cls.pull_request_id == pull_request_id)
2922 2922 return q.all()
2923 2923
2924 2924 def render(self, mentions=False):
2925 2925 from rhodecode.lib import helpers as h
2926 2926 return h.render(self.text, renderer=self.renderer, mentions=mentions)
2927 2927
2928 2928 def __repr__(self):
2929 2929 if self.comment_id:
2930 2930 return '<DB:ChangesetComment #%s>' % self.comment_id
2931 2931 else:
2932 2932 return '<DB:ChangesetComment at %#x>' % id(self)
2933 2933
2934 2934
2935 2935 class ChangesetStatus(Base, BaseModel):
2936 2936 __tablename__ = 'changeset_statuses'
2937 2937 __table_args__ = (
2938 2938 Index('cs_revision_idx', 'revision'),
2939 2939 Index('cs_version_idx', 'version'),
2940 2940 UniqueConstraint('repo_id', 'revision', 'version'),
2941 2941 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2942 2942 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2943 2943 )
2944 2944 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
2945 2945 STATUS_APPROVED = 'approved'
2946 2946 STATUS_REJECTED = 'rejected'
2947 2947 STATUS_UNDER_REVIEW = 'under_review'
2948 2948
2949 2949 STATUSES = [
2950 2950 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
2951 2951 (STATUS_APPROVED, _("Approved")),
2952 2952 (STATUS_REJECTED, _("Rejected")),
2953 2953 (STATUS_UNDER_REVIEW, _("Under Review")),
2954 2954 ]
2955 2955
2956 2956 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
2957 2957 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2958 2958 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
2959 2959 revision = Column('revision', String(40), nullable=False)
2960 2960 status = Column('status', String(128), nullable=False, default=DEFAULT)
2961 2961 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
2962 2962 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
2963 2963 version = Column('version', Integer(), nullable=False, default=0)
2964 2964 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
2965 2965
2966 2966 author = relationship('User', lazy='joined')
2967 2967 repo = relationship('Repository')
2968 2968 comment = relationship('ChangesetComment', lazy='joined')
2969 2969 pull_request = relationship('PullRequest', lazy='joined')
2970 2970
2971 2971 def __unicode__(self):
2972 2972 return u"<%s('%s[%s]:%s')>" % (
2973 2973 self.__class__.__name__,
2974 2974 self.status, self.version, self.author
2975 2975 )
2976 2976
2977 2977 @classmethod
2978 2978 def get_status_lbl(cls, value):
2979 2979 return dict(cls.STATUSES).get(value)
2980 2980
2981 2981 @property
2982 2982 def status_lbl(self):
2983 2983 return ChangesetStatus.get_status_lbl(self.status)
2984 2984
2985 2985
2986 2986 class _PullRequestBase(BaseModel):
2987 2987 """
2988 2988 Common attributes of pull request and version entries.
2989 2989 """
2990 2990
2991 2991 # .status values
2992 2992 STATUS_NEW = u'new'
2993 2993 STATUS_OPEN = u'open'
2994 2994 STATUS_CLOSED = u'closed'
2995 2995
2996 2996 title = Column('title', Unicode(255), nullable=True)
2997 2997 description = Column(
2998 2998 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
2999 2999 nullable=True)
3000 3000 # new/open/closed status of pull request (not approve/reject/etc)
3001 3001 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
3002 3002 created_on = Column(
3003 3003 'created_on', DateTime(timezone=False), nullable=False,
3004 3004 default=datetime.datetime.now)
3005 3005 updated_on = Column(
3006 3006 'updated_on', DateTime(timezone=False), nullable=False,
3007 3007 default=datetime.datetime.now)
3008 3008
3009 3009 @declared_attr
3010 3010 def user_id(cls):
3011 3011 return Column(
3012 3012 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
3013 3013 unique=None)
3014 3014
3015 3015 # 500 revisions max
3016 3016 _revisions = Column(
3017 3017 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
3018 3018
3019 3019 @declared_attr
3020 3020 def source_repo_id(cls):
3021 3021 # TODO: dan: rename column to source_repo_id
3022 3022 return Column(
3023 3023 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3024 3024 nullable=False)
3025 3025
3026 3026 source_ref = Column('org_ref', Unicode(255), nullable=False)
3027 3027
3028 3028 @declared_attr
3029 3029 def target_repo_id(cls):
3030 3030 # TODO: dan: rename column to target_repo_id
3031 3031 return Column(
3032 3032 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3033 3033 nullable=False)
3034 3034
3035 3035 target_ref = Column('other_ref', Unicode(255), nullable=False)
3036 3036
3037 3037 # TODO: dan: rename column to last_merge_source_rev
3038 3038 _last_merge_source_rev = Column(
3039 3039 'last_merge_org_rev', String(40), nullable=True)
3040 3040 # TODO: dan: rename column to last_merge_target_rev
3041 3041 _last_merge_target_rev = Column(
3042 3042 'last_merge_other_rev', String(40), nullable=True)
3043 3043 _last_merge_status = Column('merge_status', Integer(), nullable=True)
3044 3044 merge_rev = Column('merge_rev', String(40), nullable=True)
3045 3045
3046 3046 @hybrid_property
3047 3047 def revisions(self):
3048 3048 return self._revisions.split(':') if self._revisions else []
3049 3049
3050 3050 @revisions.setter
3051 3051 def revisions(self, val):
3052 3052 self._revisions = ':'.join(val)
3053 3053
3054 3054 @declared_attr
3055 3055 def author(cls):
3056 3056 return relationship('User', lazy='joined')
3057 3057
3058 3058 @declared_attr
3059 3059 def source_repo(cls):
3060 3060 return relationship(
3061 3061 'Repository',
3062 3062 primaryjoin='%s.source_repo_id==Repository.repo_id' % cls.__name__)
3063 3063
3064 3064 @property
3065 3065 def source_ref_parts(self):
3066 3066 refs = self.source_ref.split(':')
3067 3067 return Reference(refs[0], refs[1], refs[2])
3068 3068
3069 3069 @declared_attr
3070 3070 def target_repo(cls):
3071 3071 return relationship(
3072 3072 'Repository',
3073 3073 primaryjoin='%s.target_repo_id==Repository.repo_id' % cls.__name__)
3074 3074
3075 3075 @property
3076 3076 def target_ref_parts(self):
3077 3077 refs = self.target_ref.split(':')
3078 3078 return Reference(refs[0], refs[1], refs[2])
3079 3079
3080 3080
3081 3081 class PullRequest(Base, _PullRequestBase):
3082 3082 __tablename__ = 'pull_requests'
3083 3083 __table_args__ = (
3084 3084 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3085 3085 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3086 3086 )
3087 3087
3088 3088 pull_request_id = Column(
3089 3089 'pull_request_id', Integer(), nullable=False, primary_key=True)
3090 3090
3091 3091 def __repr__(self):
3092 3092 if self.pull_request_id:
3093 3093 return '<DB:PullRequest #%s>' % self.pull_request_id
3094 3094 else:
3095 3095 return '<DB:PullRequest at %#x>' % id(self)
3096 3096
3097 3097 reviewers = relationship('PullRequestReviewers',
3098 3098 cascade="all, delete, delete-orphan")
3099 3099 statuses = relationship('ChangesetStatus')
3100 3100 comments = relationship('ChangesetComment',
3101 3101 cascade="all, delete, delete-orphan")
3102 3102 versions = relationship('PullRequestVersion',
3103 3103 cascade="all, delete, delete-orphan")
3104 3104
3105 3105 def is_closed(self):
3106 3106 return self.status == self.STATUS_CLOSED
3107 3107
3108 3108 def get_api_data(self):
3109 3109 from rhodecode.model.pull_request import PullRequestModel
3110 3110 pull_request = self
3111 3111 merge_status = PullRequestModel().merge_status(pull_request)
3112 3112 data = {
3113 3113 'pull_request_id': pull_request.pull_request_id,
3114 3114 'url': url('pullrequest_show', repo_name=self.target_repo.repo_name,
3115 3115 pull_request_id=self.pull_request_id,
3116 3116 qualified=True),
3117 3117 'title': pull_request.title,
3118 3118 'description': pull_request.description,
3119 3119 'status': pull_request.status,
3120 3120 'created_on': pull_request.created_on,
3121 3121 'updated_on': pull_request.updated_on,
3122 3122 'commit_ids': pull_request.revisions,
3123 3123 'review_status': pull_request.calculated_review_status(),
3124 3124 'mergeable': {
3125 3125 'status': merge_status[0],
3126 3126 'message': unicode(merge_status[1]),
3127 3127 },
3128 3128 'source': {
3129 3129 'clone_url': pull_request.source_repo.clone_url(),
3130 3130 'repository': pull_request.source_repo.repo_name,
3131 3131 'reference': {
3132 3132 'name': pull_request.source_ref_parts.name,
3133 3133 'type': pull_request.source_ref_parts.type,
3134 3134 'commit_id': pull_request.source_ref_parts.commit_id,
3135 3135 },
3136 3136 },
3137 3137 'target': {
3138 3138 'clone_url': pull_request.target_repo.clone_url(),
3139 3139 'repository': pull_request.target_repo.repo_name,
3140 3140 'reference': {
3141 3141 'name': pull_request.target_ref_parts.name,
3142 3142 'type': pull_request.target_ref_parts.type,
3143 3143 'commit_id': pull_request.target_ref_parts.commit_id,
3144 3144 },
3145 3145 },
3146 3146 'author': pull_request.author.get_api_data(include_secrets=False,
3147 3147 details='basic'),
3148 3148 'reviewers': [
3149 3149 {
3150 3150 'user': reviewer.get_api_data(include_secrets=False,
3151 3151 details='basic'),
3152 'reasons': reasons,
3152 3153 'review_status': st[0][1].status if st else 'not_reviewed',
3153 3154 }
3154 for reviewer, st in pull_request.reviewers_statuses()
3155 for reviewer, reasons, st in pull_request.reviewers_statuses()
3155 3156 ]
3156 3157 }
3157 3158
3158 3159 return data
3159 3160
3160 3161 def __json__(self):
3161 3162 return {
3162 3163 'revisions': self.revisions,
3163 3164 }
3164 3165
3165 3166 def calculated_review_status(self):
3166 3167 # TODO: anderson: 13.05.15 Used only on templates/my_account_pullrequests.html
3167 3168 # because it's tricky on how to use ChangesetStatusModel from there
3168 3169 warnings.warn("Use calculated_review_status from ChangesetStatusModel", DeprecationWarning)
3169 3170 from rhodecode.model.changeset_status import ChangesetStatusModel
3170 3171 return ChangesetStatusModel().calculated_review_status(self)
3171 3172
3172 3173 def reviewers_statuses(self):
3173 3174 warnings.warn("Use reviewers_statuses from ChangesetStatusModel", DeprecationWarning)
3174 3175 from rhodecode.model.changeset_status import ChangesetStatusModel
3175 3176 return ChangesetStatusModel().reviewers_statuses(self)
3176 3177
3177 3178
3178 3179 class PullRequestVersion(Base, _PullRequestBase):
3179 3180 __tablename__ = 'pull_request_versions'
3180 3181 __table_args__ = (
3181 3182 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3182 3183 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3183 3184 )
3184 3185
3185 3186 pull_request_version_id = Column(
3186 3187 'pull_request_version_id', Integer(), nullable=False, primary_key=True)
3187 3188 pull_request_id = Column(
3188 3189 'pull_request_id', Integer(),
3189 3190 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3190 3191 pull_request = relationship('PullRequest')
3191 3192
3192 3193 def __repr__(self):
3193 3194 if self.pull_request_version_id:
3194 3195 return '<DB:PullRequestVersion #%s>' % self.pull_request_version_id
3195 3196 else:
3196 3197 return '<DB:PullRequestVersion at %#x>' % id(self)
3197 3198
3198 3199
3199 3200 class PullRequestReviewers(Base, BaseModel):
3200 3201 __tablename__ = 'pull_request_reviewers'
3201 3202 __table_args__ = (
3202 3203 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3203 3204 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3204 3205 )
3205 3206
3206 def __init__(self, user=None, pull_request=None):
3207 def __init__(self, user=None, pull_request=None, reasons=None):
3207 3208 self.user = user
3208 3209 self.pull_request = pull_request
3210 self.reasons = reasons or []
3211
3212 @hybrid_property
3213 def reasons(self):
3214 if not self._reasons:
3215 return []
3216 return self._reasons
3217
3218 @reasons.setter
3219 def reasons(self, val):
3220 val = val or []
3221 if any(not isinstance(x, basestring) for x in val):
3222 raise Exception('invalid reasons type, must be list of strings')
3223 self._reasons = val
3209 3224
3210 3225 pull_requests_reviewers_id = Column(
3211 3226 'pull_requests_reviewers_id', Integer(), nullable=False,
3212 3227 primary_key=True)
3213 3228 pull_request_id = Column(
3214 3229 "pull_request_id", Integer(),
3215 3230 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3216 3231 user_id = Column(
3217 3232 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
3218 reason = Column('reason',
3219 UnicodeText().with_variant(UnicodeText(255), 'mysql'), nullable=True)
3233 _reasons = Column(
3234 'reason', MutationList.as_mutable(
3235 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
3220 3236
3221 3237 user = relationship('User')
3222 3238 pull_request = relationship('PullRequest')
3223 3239
3224 3240
3225 3241 class Notification(Base, BaseModel):
3226 3242 __tablename__ = 'notifications'
3227 3243 __table_args__ = (
3228 3244 Index('notification_type_idx', 'type'),
3229 3245 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3230 3246 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3231 3247 )
3232 3248
3233 3249 TYPE_CHANGESET_COMMENT = u'cs_comment'
3234 3250 TYPE_MESSAGE = u'message'
3235 3251 TYPE_MENTION = u'mention'
3236 3252 TYPE_REGISTRATION = u'registration'
3237 3253 TYPE_PULL_REQUEST = u'pull_request'
3238 3254 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
3239 3255
3240 3256 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
3241 3257 subject = Column('subject', Unicode(512), nullable=True)
3242 3258 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
3243 3259 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
3244 3260 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3245 3261 type_ = Column('type', Unicode(255))
3246 3262
3247 3263 created_by_user = relationship('User')
3248 3264 notifications_to_users = relationship('UserNotification', lazy='joined',
3249 3265 cascade="all, delete, delete-orphan")
3250 3266
3251 3267 @property
3252 3268 def recipients(self):
3253 3269 return [x.user for x in UserNotification.query()\
3254 3270 .filter(UserNotification.notification == self)\
3255 3271 .order_by(UserNotification.user_id.asc()).all()]
3256 3272
3257 3273 @classmethod
3258 3274 def create(cls, created_by, subject, body, recipients, type_=None):
3259 3275 if type_ is None:
3260 3276 type_ = Notification.TYPE_MESSAGE
3261 3277
3262 3278 notification = cls()
3263 3279 notification.created_by_user = created_by
3264 3280 notification.subject = subject
3265 3281 notification.body = body
3266 3282 notification.type_ = type_
3267 3283 notification.created_on = datetime.datetime.now()
3268 3284
3269 3285 for u in recipients:
3270 3286 assoc = UserNotification()
3271 3287 assoc.notification = notification
3272 3288
3273 3289 # if created_by is inside recipients mark his notification
3274 3290 # as read
3275 3291 if u.user_id == created_by.user_id:
3276 3292 assoc.read = True
3277 3293
3278 3294 u.notifications.append(assoc)
3279 3295 Session().add(notification)
3280 3296
3281 3297 return notification
3282 3298
3283 3299 @property
3284 3300 def description(self):
3285 3301 from rhodecode.model.notification import NotificationModel
3286 3302 return NotificationModel().make_description(self)
3287 3303
3288 3304
3289 3305 class UserNotification(Base, BaseModel):
3290 3306 __tablename__ = 'user_to_notification'
3291 3307 __table_args__ = (
3292 3308 UniqueConstraint('user_id', 'notification_id'),
3293 3309 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3294 3310 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3295 3311 )
3296 3312 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
3297 3313 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
3298 3314 read = Column('read', Boolean, default=False)
3299 3315 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
3300 3316
3301 3317 user = relationship('User', lazy="joined")
3302 3318 notification = relationship('Notification', lazy="joined",
3303 3319 order_by=lambda: Notification.created_on.desc(),)
3304 3320
3305 3321 def mark_as_read(self):
3306 3322 self.read = True
3307 3323 Session().add(self)
3308 3324
3309 3325
3310 3326 class Gist(Base, BaseModel):
3311 3327 __tablename__ = 'gists'
3312 3328 __table_args__ = (
3313 3329 Index('g_gist_access_id_idx', 'gist_access_id'),
3314 3330 Index('g_created_on_idx', 'created_on'),
3315 3331 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3316 3332 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3317 3333 )
3318 3334 GIST_PUBLIC = u'public'
3319 3335 GIST_PRIVATE = u'private'
3320 3336 DEFAULT_FILENAME = u'gistfile1.txt'
3321 3337
3322 3338 ACL_LEVEL_PUBLIC = u'acl_public'
3323 3339 ACL_LEVEL_PRIVATE = u'acl_private'
3324 3340
3325 3341 gist_id = Column('gist_id', Integer(), primary_key=True)
3326 3342 gist_access_id = Column('gist_access_id', Unicode(250))
3327 3343 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
3328 3344 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
3329 3345 gist_expires = Column('gist_expires', Float(53), nullable=False)
3330 3346 gist_type = Column('gist_type', Unicode(128), nullable=False)
3331 3347 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3332 3348 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3333 3349 acl_level = Column('acl_level', Unicode(128), nullable=True)
3334 3350
3335 3351 owner = relationship('User')
3336 3352
3337 3353 def __repr__(self):
3338 3354 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
3339 3355
3340 3356 @classmethod
3341 3357 def get_or_404(cls, id_):
3342 3358 res = cls.query().filter(cls.gist_access_id == id_).scalar()
3343 3359 if not res:
3344 3360 raise HTTPNotFound
3345 3361 return res
3346 3362
3347 3363 @classmethod
3348 3364 def get_by_access_id(cls, gist_access_id):
3349 3365 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
3350 3366
3351 3367 def gist_url(self):
3352 3368 import rhodecode
3353 3369 alias_url = rhodecode.CONFIG.get('gist_alias_url')
3354 3370 if alias_url:
3355 3371 return alias_url.replace('{gistid}', self.gist_access_id)
3356 3372
3357 3373 return url('gist', gist_id=self.gist_access_id, qualified=True)
3358 3374
3359 3375 @classmethod
3360 3376 def base_path(cls):
3361 3377 """
3362 3378 Returns base path when all gists are stored
3363 3379
3364 3380 :param cls:
3365 3381 """
3366 3382 from rhodecode.model.gist import GIST_STORE_LOC
3367 3383 q = Session().query(RhodeCodeUi)\
3368 3384 .filter(RhodeCodeUi.ui_key == URL_SEP)
3369 3385 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
3370 3386 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
3371 3387
3372 3388 def get_api_data(self):
3373 3389 """
3374 3390 Common function for generating gist related data for API
3375 3391 """
3376 3392 gist = self
3377 3393 data = {
3378 3394 'gist_id': gist.gist_id,
3379 3395 'type': gist.gist_type,
3380 3396 'access_id': gist.gist_access_id,
3381 3397 'description': gist.gist_description,
3382 3398 'url': gist.gist_url(),
3383 3399 'expires': gist.gist_expires,
3384 3400 'created_on': gist.created_on,
3385 3401 'modified_at': gist.modified_at,
3386 3402 'content': None,
3387 3403 'acl_level': gist.acl_level,
3388 3404 }
3389 3405 return data
3390 3406
3391 3407 def __json__(self):
3392 3408 data = dict(
3393 3409 )
3394 3410 data.update(self.get_api_data())
3395 3411 return data
3396 3412 # SCM functions
3397 3413
3398 3414 def scm_instance(self, **kwargs):
3399 3415 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
3400 3416 return get_vcs_instance(
3401 3417 repo_path=safe_str(full_repo_path), create=False)
3402 3418
3403 3419
3404 3420 class DbMigrateVersion(Base, BaseModel):
3405 3421 __tablename__ = 'db_migrate_version'
3406 3422 __table_args__ = (
3407 3423 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3408 3424 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3409 3425 )
3410 3426 repository_id = Column('repository_id', String(250), primary_key=True)
3411 3427 repository_path = Column('repository_path', Text)
3412 3428 version = Column('version', Integer)
3413 3429
3414 3430
3415 3431 class ExternalIdentity(Base, BaseModel):
3416 3432 __tablename__ = 'external_identities'
3417 3433 __table_args__ = (
3418 3434 Index('local_user_id_idx', 'local_user_id'),
3419 3435 Index('external_id_idx', 'external_id'),
3420 3436 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3421 3437 'mysql_charset': 'utf8'})
3422 3438
3423 3439 external_id = Column('external_id', Unicode(255), default=u'',
3424 3440 primary_key=True)
3425 3441 external_username = Column('external_username', Unicode(1024), default=u'')
3426 3442 local_user_id = Column('local_user_id', Integer(),
3427 3443 ForeignKey('users.user_id'), primary_key=True)
3428 3444 provider_name = Column('provider_name', Unicode(255), default=u'',
3429 3445 primary_key=True)
3430 3446 access_token = Column('access_token', String(1024), default=u'')
3431 3447 alt_token = Column('alt_token', String(1024), default=u'')
3432 3448 token_secret = Column('token_secret', String(1024), default=u'')
3433 3449
3434 3450 @classmethod
3435 3451 def by_external_id_and_provider(cls, external_id, provider_name,
3436 3452 local_user_id=None):
3437 3453 """
3438 3454 Returns ExternalIdentity instance based on search params
3439 3455
3440 3456 :param external_id:
3441 3457 :param provider_name:
3442 3458 :return: ExternalIdentity
3443 3459 """
3444 3460 query = cls.query()
3445 3461 query = query.filter(cls.external_id == external_id)
3446 3462 query = query.filter(cls.provider_name == provider_name)
3447 3463 if local_user_id:
3448 3464 query = query.filter(cls.local_user_id == local_user_id)
3449 3465 return query.first()
3450 3466
3451 3467 @classmethod
3452 3468 def user_by_external_id_and_provider(cls, external_id, provider_name):
3453 3469 """
3454 3470 Returns User instance based on search params
3455 3471
3456 3472 :param external_id:
3457 3473 :param provider_name:
3458 3474 :return: User
3459 3475 """
3460 3476 query = User.query()
3461 3477 query = query.filter(cls.external_id == external_id)
3462 3478 query = query.filter(cls.provider_name == provider_name)
3463 3479 query = query.filter(User.user_id == cls.local_user_id)
3464 3480 return query.first()
3465 3481
3466 3482 @classmethod
3467 3483 def by_local_user_id(cls, local_user_id):
3468 3484 """
3469 3485 Returns all tokens for user
3470 3486
3471 3487 :param local_user_id:
3472 3488 :return: ExternalIdentity
3473 3489 """
3474 3490 query = cls.query()
3475 3491 query = query.filter(cls.local_user_id == local_user_id)
3476 3492 return query
3477 3493
3478 3494
3479 3495 class Integration(Base, BaseModel):
3480 3496 __tablename__ = 'integrations'
3481 3497 __table_args__ = (
3482 3498 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3483 3499 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3484 3500 )
3485 3501
3486 3502 integration_id = Column('integration_id', Integer(), primary_key=True)
3487 3503 integration_type = Column('integration_type', String(255))
3488 3504 enabled = Column('enabled', Boolean(), nullable=False)
3489 3505 name = Column('name', String(255), nullable=False)
3490 3506 child_repos_only = Column('child_repos_only', Boolean(), nullable=False,
3491 3507 default=False)
3492 3508
3493 3509 settings = Column(
3494 3510 'settings_json', MutationObj.as_mutable(
3495 3511 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
3496 3512 repo_id = Column(
3497 3513 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
3498 3514 nullable=True, unique=None, default=None)
3499 3515 repo = relationship('Repository', lazy='joined')
3500 3516
3501 3517 repo_group_id = Column(
3502 3518 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
3503 3519 nullable=True, unique=None, default=None)
3504 3520 repo_group = relationship('RepoGroup', lazy='joined')
3505 3521
3506 3522 @property
3507 3523 def scope(self):
3508 3524 if self.repo:
3509 3525 return repr(self.repo)
3510 3526 if self.repo_group:
3511 3527 if self.child_repos_only:
3512 3528 return repr(self.repo_group) + ' (child repos only)'
3513 3529 else:
3514 3530 return repr(self.repo_group) + ' (recursive)'
3515 3531 if self.child_repos_only:
3516 3532 return 'root_repos'
3517 3533 return 'global'
3518 3534
3519 3535 def __repr__(self):
3520 3536 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
3521 3537
3522 3538
3523 3539 class RepoReviewRuleUser(Base, BaseModel):
3524 3540 __tablename__ = 'repo_review_rules_users'
3525 3541 __table_args__ = (
3526 3542 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3527 3543 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3528 3544 )
3529 3545 repo_review_rule_user_id = Column(
3530 3546 'repo_review_rule_user_id', Integer(), primary_key=True)
3531 3547 repo_review_rule_id = Column("repo_review_rule_id",
3532 3548 Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3533 3549 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'),
3534 3550 nullable=False)
3535 3551 user = relationship('User')
3536 3552
3537 3553
3538 3554 class RepoReviewRuleUserGroup(Base, BaseModel):
3539 3555 __tablename__ = 'repo_review_rules_users_groups'
3540 3556 __table_args__ = (
3541 3557 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3542 3558 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3543 3559 )
3544 3560 repo_review_rule_users_group_id = Column(
3545 3561 'repo_review_rule_users_group_id', Integer(), primary_key=True)
3546 3562 repo_review_rule_id = Column("repo_review_rule_id",
3547 3563 Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3548 3564 users_group_id = Column("users_group_id", Integer(),
3549 3565 ForeignKey('users_groups.users_group_id'), nullable=False)
3550 3566 users_group = relationship('UserGroup')
3551 3567
3552 3568
3553 3569 class RepoReviewRule(Base, BaseModel):
3554 3570 __tablename__ = 'repo_review_rules'
3555 3571 __table_args__ = (
3556 3572 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3557 3573 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3558 3574 )
3559 3575
3560 3576 repo_review_rule_id = Column(
3561 3577 'repo_review_rule_id', Integer(), primary_key=True)
3562 3578 repo_id = Column(
3563 3579 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
3564 3580 repo = relationship('Repository', backref='review_rules')
3565 3581
3566 3582 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'),
3567 3583 default=u'*') # glob
3568 3584 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'),
3569 3585 default=u'*') # glob
3570 3586
3571 3587 use_authors_for_review = Column("use_authors_for_review", Boolean(),
3572 3588 nullable=False, default=False)
3573 3589 rule_users = relationship('RepoReviewRuleUser')
3574 3590 rule_user_groups = relationship('RepoReviewRuleUserGroup')
3575 3591
3576 3592 @hybrid_property
3577 3593 def branch_pattern(self):
3578 3594 return self._branch_pattern or '*'
3579 3595
3580 3596 def _validate_glob(self, value):
3581 3597 re.compile('^' + glob2re(value) + '$')
3582 3598
3583 3599 @branch_pattern.setter
3584 3600 def branch_pattern(self, value):
3585 3601 self._validate_glob(value)
3586 3602 self._branch_pattern = value or '*'
3587 3603
3588 3604 @hybrid_property
3589 3605 def file_pattern(self):
3590 3606 return self._file_pattern or '*'
3591 3607
3592 3608 @file_pattern.setter
3593 3609 def file_pattern(self, value):
3594 3610 self._validate_glob(value)
3595 3611 self._file_pattern = value or '*'
3596 3612
3597 3613 def matches(self, branch, files_changed):
3598 3614 """
3599 3615 Check if this review rule matches a branch/files in a pull request
3600 3616
3601 3617 :param branch: branch name for the commit
3602 3618 :param files_changed: list of file paths changed in the pull request
3603 3619 """
3604 3620
3605 3621 branch = branch or ''
3606 3622 files_changed = files_changed or []
3607 3623
3608 3624 branch_matches = True
3609 3625 if branch:
3610 3626 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
3611 3627 branch_matches = bool(branch_regex.search(branch))
3612 3628
3613 3629 files_matches = True
3614 3630 if self.file_pattern != '*':
3615 3631 files_matches = False
3616 3632 file_regex = re.compile(glob2re(self.file_pattern))
3617 3633 for filename in files_changed:
3618 3634 if file_regex.search(filename):
3619 3635 files_matches = True
3620 3636 break
3621 3637
3622 3638 return branch_matches and files_matches
3623 3639
3624 3640 @property
3625 3641 def review_users(self):
3626 3642 """ Returns the users which this rule applies to """
3627 3643
3628 3644 users = set()
3629 3645 users |= set([
3630 3646 rule_user.user for rule_user in self.rule_users
3631 3647 if rule_user.user.active])
3632 3648 users |= set(
3633 3649 member.user
3634 3650 for rule_user_group in self.rule_user_groups
3635 3651 for member in rule_user_group.users_group.members
3636 3652 if member.user.active
3637 3653 )
3638 3654 return users
3639 3655
3640 3656 def __repr__(self):
3641 3657 return '<RepoReviewerRule(id=%r, repo=%r)>' % (
3642 3658 self.repo_review_rule_id, self.repo)
@@ -1,547 +1,551 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 this is forms validation classes
23 23 http://formencode.org/module-formencode.validators.html
24 24 for list off all availible validators
25 25
26 26 we can create our own validators
27 27
28 28 The table below outlines the options which can be used in a schema in addition to the validators themselves
29 29 pre_validators [] These validators will be applied before the schema
30 30 chained_validators [] These validators will be applied after the schema
31 31 allow_extra_fields False If True, then it is not an error when keys that aren't associated with a validator are present
32 32 filter_extra_fields False If True, then keys that aren't associated with a validator are removed
33 33 if_key_missing NoDefault If this is given, then any keys that aren't available but are expected will be replaced with this value (and then validated). This does not override a present .if_missing attribute on validators. NoDefault is a special FormEncode class to mean that no default values has been specified and therefore missing keys shouldn't take a default value.
34 34 ignore_key_missing False If True, then missing keys will be missing in the result, if the validator doesn't have .if_missing on it already
35 35
36 36
37 37 <name> = formencode.validators.<name of validator>
38 38 <name> must equal form name
39 39 list=[1,2,3,4,5]
40 40 for SELECT use formencode.All(OneOf(list), Int())
41 41
42 42 """
43 43
44 44 import deform
45 45 import logging
46 46 import formencode
47 47
48 48 from pkg_resources import resource_filename
49 49 from formencode import All, Pipe
50 50
51 51 from pylons.i18n.translation import _
52 52
53 53 from rhodecode import BACKENDS
54 54 from rhodecode.lib import helpers
55 55 from rhodecode.model import validators as v
56 56
57 57 log = logging.getLogger(__name__)
58 58
59 59
60 60 deform_templates = resource_filename('deform', 'templates')
61 61 rhodecode_templates = resource_filename('rhodecode', 'templates/forms')
62 62 search_path = (rhodecode_templates, deform_templates)
63 63
64 64
65 65 class RhodecodeFormZPTRendererFactory(deform.ZPTRendererFactory):
66 66 """ Subclass of ZPTRendererFactory to add rhodecode context variables """
67 67 def __call__(self, template_name, **kw):
68 68 kw['h'] = helpers
69 69 return self.load(template_name)(**kw)
70 70
71 71
72 72 form_renderer = RhodecodeFormZPTRendererFactory(search_path)
73 73 deform.Form.set_default_renderer(form_renderer)
74 74
75 75
76 76 def LoginForm():
77 77 class _LoginForm(formencode.Schema):
78 78 allow_extra_fields = True
79 79 filter_extra_fields = True
80 80 username = v.UnicodeString(
81 81 strip=True,
82 82 min=1,
83 83 not_empty=True,
84 84 messages={
85 85 'empty': _(u'Please enter a login'),
86 86 'tooShort': _(u'Enter a value %(min)i characters long or more')
87 87 }
88 88 )
89 89
90 90 password = v.UnicodeString(
91 91 strip=False,
92 92 min=3,
93 93 not_empty=True,
94 94 messages={
95 95 'empty': _(u'Please enter a password'),
96 96 'tooShort': _(u'Enter %(min)i characters or more')}
97 97 )
98 98
99 99 remember = v.StringBoolean(if_missing=False)
100 100
101 101 chained_validators = [v.ValidAuth()]
102 102 return _LoginForm
103 103
104 104
105 105 def UserForm(edit=False, available_languages=[], old_data={}):
106 106 class _UserForm(formencode.Schema):
107 107 allow_extra_fields = True
108 108 filter_extra_fields = True
109 109 username = All(v.UnicodeString(strip=True, min=1, not_empty=True),
110 110 v.ValidUsername(edit, old_data))
111 111 if edit:
112 112 new_password = All(
113 113 v.ValidPassword(),
114 114 v.UnicodeString(strip=False, min=6, not_empty=False)
115 115 )
116 116 password_confirmation = All(
117 117 v.ValidPassword(),
118 118 v.UnicodeString(strip=False, min=6, not_empty=False),
119 119 )
120 120 admin = v.StringBoolean(if_missing=False)
121 121 else:
122 122 password = All(
123 123 v.ValidPassword(),
124 124 v.UnicodeString(strip=False, min=6, not_empty=True)
125 125 )
126 126 password_confirmation = All(
127 127 v.ValidPassword(),
128 128 v.UnicodeString(strip=False, min=6, not_empty=False)
129 129 )
130 130
131 131 password_change = v.StringBoolean(if_missing=False)
132 132 create_repo_group = v.StringBoolean(if_missing=False)
133 133
134 134 active = v.StringBoolean(if_missing=False)
135 135 firstname = v.UnicodeString(strip=True, min=1, not_empty=False)
136 136 lastname = v.UnicodeString(strip=True, min=1, not_empty=False)
137 137 email = All(v.Email(not_empty=True), v.UniqSystemEmail(old_data))
138 138 extern_name = v.UnicodeString(strip=True)
139 139 extern_type = v.UnicodeString(strip=True)
140 140 language = v.OneOf(available_languages, hideList=False,
141 141 testValueList=True, if_missing=None)
142 142 chained_validators = [v.ValidPasswordsMatch()]
143 143 return _UserForm
144 144
145 145
146 146 def UserGroupForm(edit=False, old_data=None, available_members=None,
147 147 allow_disabled=False):
148 148 old_data = old_data or {}
149 149 available_members = available_members or []
150 150
151 151 class _UserGroupForm(formencode.Schema):
152 152 allow_extra_fields = True
153 153 filter_extra_fields = True
154 154
155 155 users_group_name = All(
156 156 v.UnicodeString(strip=True, min=1, not_empty=True),
157 157 v.ValidUserGroup(edit, old_data)
158 158 )
159 159 user_group_description = v.UnicodeString(strip=True, min=1,
160 160 not_empty=False)
161 161
162 162 users_group_active = v.StringBoolean(if_missing=False)
163 163
164 164 if edit:
165 165 users_group_members = v.OneOf(
166 166 available_members, hideList=False, testValueList=True,
167 167 if_missing=None, not_empty=False
168 168 )
169 169 # this is user group owner
170 170 user = All(
171 171 v.UnicodeString(not_empty=True),
172 172 v.ValidRepoUser(allow_disabled))
173 173 return _UserGroupForm
174 174
175 175
176 176 def RepoGroupForm(edit=False, old_data=None, available_groups=None,
177 177 can_create_in_root=False, allow_disabled=False):
178 178 old_data = old_data or {}
179 179 available_groups = available_groups or []
180 180
181 181 class _RepoGroupForm(formencode.Schema):
182 182 allow_extra_fields = True
183 183 filter_extra_fields = False
184 184
185 185 group_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
186 186 v.SlugifyName(),)
187 187 group_description = v.UnicodeString(strip=True, min=1,
188 188 not_empty=False)
189 189 group_copy_permissions = v.StringBoolean(if_missing=False)
190 190
191 191 group_parent_id = v.OneOf(available_groups, hideList=False,
192 192 testValueList=True, not_empty=True)
193 193 enable_locking = v.StringBoolean(if_missing=False)
194 194 chained_validators = [
195 195 v.ValidRepoGroup(edit, old_data, can_create_in_root)]
196 196
197 197 if edit:
198 198 # this is repo group owner
199 199 user = All(
200 200 v.UnicodeString(not_empty=True),
201 201 v.ValidRepoUser(allow_disabled))
202 202
203 203 return _RepoGroupForm
204 204
205 205
206 206 def RegisterForm(edit=False, old_data={}):
207 207 class _RegisterForm(formencode.Schema):
208 208 allow_extra_fields = True
209 209 filter_extra_fields = True
210 210 username = All(
211 211 v.ValidUsername(edit, old_data),
212 212 v.UnicodeString(strip=True, min=1, not_empty=True)
213 213 )
214 214 password = All(
215 215 v.ValidPassword(),
216 216 v.UnicodeString(strip=False, min=6, not_empty=True)
217 217 )
218 218 password_confirmation = All(
219 219 v.ValidPassword(),
220 220 v.UnicodeString(strip=False, min=6, not_empty=True)
221 221 )
222 222 active = v.StringBoolean(if_missing=False)
223 223 firstname = v.UnicodeString(strip=True, min=1, not_empty=False)
224 224 lastname = v.UnicodeString(strip=True, min=1, not_empty=False)
225 225 email = All(v.Email(not_empty=True), v.UniqSystemEmail(old_data))
226 226
227 227 chained_validators = [v.ValidPasswordsMatch()]
228 228
229 229 return _RegisterForm
230 230
231 231
232 232 def PasswordResetForm():
233 233 class _PasswordResetForm(formencode.Schema):
234 234 allow_extra_fields = True
235 235 filter_extra_fields = True
236 236 email = All(v.ValidSystemEmail(), v.Email(not_empty=True))
237 237 return _PasswordResetForm
238 238
239 239
240 240 def RepoForm(edit=False, old_data=None, repo_groups=None, landing_revs=None,
241 241 allow_disabled=False):
242 242 old_data = old_data or {}
243 243 repo_groups = repo_groups or []
244 244 landing_revs = landing_revs or []
245 245 supported_backends = BACKENDS.keys()
246 246
247 247 class _RepoForm(formencode.Schema):
248 248 allow_extra_fields = True
249 249 filter_extra_fields = False
250 250 repo_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
251 251 v.SlugifyName())
252 252 repo_group = All(v.CanWriteGroup(old_data),
253 253 v.OneOf(repo_groups, hideList=True))
254 254 repo_type = v.OneOf(supported_backends, required=False,
255 255 if_missing=old_data.get('repo_type'))
256 256 repo_description = v.UnicodeString(strip=True, min=1, not_empty=False)
257 257 repo_private = v.StringBoolean(if_missing=False)
258 258 repo_landing_rev = v.OneOf(landing_revs, hideList=True)
259 259 repo_copy_permissions = v.StringBoolean(if_missing=False)
260 260 clone_uri = All(v.UnicodeString(strip=True, min=1, not_empty=False))
261 261
262 262 repo_enable_statistics = v.StringBoolean(if_missing=False)
263 263 repo_enable_downloads = v.StringBoolean(if_missing=False)
264 264 repo_enable_locking = v.StringBoolean(if_missing=False)
265 265
266 266 if edit:
267 267 # this is repo owner
268 268 user = All(
269 269 v.UnicodeString(not_empty=True),
270 270 v.ValidRepoUser(allow_disabled))
271 271 clone_uri_change = v.UnicodeString(
272 272 not_empty=False, if_missing=v.Missing)
273 273
274 274 chained_validators = [v.ValidCloneUri(),
275 275 v.ValidRepoName(edit, old_data)]
276 276 return _RepoForm
277 277
278 278
279 279 def RepoPermsForm():
280 280 class _RepoPermsForm(formencode.Schema):
281 281 allow_extra_fields = True
282 282 filter_extra_fields = False
283 283 chained_validators = [v.ValidPerms(type_='repo')]
284 284 return _RepoPermsForm
285 285
286 286
287 287 def RepoGroupPermsForm(valid_recursive_choices):
288 288 class _RepoGroupPermsForm(formencode.Schema):
289 289 allow_extra_fields = True
290 290 filter_extra_fields = False
291 291 recursive = v.OneOf(valid_recursive_choices)
292 292 chained_validators = [v.ValidPerms(type_='repo_group')]
293 293 return _RepoGroupPermsForm
294 294
295 295
296 296 def UserGroupPermsForm():
297 297 class _UserPermsForm(formencode.Schema):
298 298 allow_extra_fields = True
299 299 filter_extra_fields = False
300 300 chained_validators = [v.ValidPerms(type_='user_group')]
301 301 return _UserPermsForm
302 302
303 303
304 304 def RepoFieldForm():
305 305 class _RepoFieldForm(formencode.Schema):
306 306 filter_extra_fields = True
307 307 allow_extra_fields = True
308 308
309 309 new_field_key = All(v.FieldKey(),
310 310 v.UnicodeString(strip=True, min=3, not_empty=True))
311 311 new_field_value = v.UnicodeString(not_empty=False, if_missing=u'')
312 312 new_field_type = v.OneOf(['str', 'unicode', 'list', 'tuple'],
313 313 if_missing='str')
314 314 new_field_label = v.UnicodeString(not_empty=False)
315 315 new_field_desc = v.UnicodeString(not_empty=False)
316 316
317 317 return _RepoFieldForm
318 318
319 319
320 320 def RepoForkForm(edit=False, old_data={}, supported_backends=BACKENDS.keys(),
321 321 repo_groups=[], landing_revs=[]):
322 322 class _RepoForkForm(formencode.Schema):
323 323 allow_extra_fields = True
324 324 filter_extra_fields = False
325 325 repo_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
326 326 v.SlugifyName())
327 327 repo_group = All(v.CanWriteGroup(),
328 328 v.OneOf(repo_groups, hideList=True))
329 329 repo_type = All(v.ValidForkType(old_data), v.OneOf(supported_backends))
330 330 description = v.UnicodeString(strip=True, min=1, not_empty=True)
331 331 private = v.StringBoolean(if_missing=False)
332 332 copy_permissions = v.StringBoolean(if_missing=False)
333 333 fork_parent_id = v.UnicodeString()
334 334 chained_validators = [v.ValidForkName(edit, old_data)]
335 335 landing_rev = v.OneOf(landing_revs, hideList=True)
336 336
337 337 return _RepoForkForm
338 338
339 339
340 340 def ApplicationSettingsForm():
341 341 class _ApplicationSettingsForm(formencode.Schema):
342 342 allow_extra_fields = True
343 343 filter_extra_fields = False
344 344 rhodecode_title = v.UnicodeString(strip=True, max=40, not_empty=False)
345 345 rhodecode_realm = v.UnicodeString(strip=True, min=1, not_empty=True)
346 346 rhodecode_pre_code = v.UnicodeString(strip=True, min=1, not_empty=False)
347 347 rhodecode_post_code = v.UnicodeString(strip=True, min=1, not_empty=False)
348 348 rhodecode_captcha_public_key = v.UnicodeString(strip=True, min=1, not_empty=False)
349 349 rhodecode_captcha_private_key = v.UnicodeString(strip=True, min=1, not_empty=False)
350 350
351 351 return _ApplicationSettingsForm
352 352
353 353
354 354 def ApplicationVisualisationForm():
355 355 class _ApplicationVisualisationForm(formencode.Schema):
356 356 allow_extra_fields = True
357 357 filter_extra_fields = False
358 358 rhodecode_show_public_icon = v.StringBoolean(if_missing=False)
359 359 rhodecode_show_private_icon = v.StringBoolean(if_missing=False)
360 360 rhodecode_stylify_metatags = v.StringBoolean(if_missing=False)
361 361
362 362 rhodecode_repository_fields = v.StringBoolean(if_missing=False)
363 363 rhodecode_lightweight_journal = v.StringBoolean(if_missing=False)
364 364 rhodecode_dashboard_items = v.Int(min=5, not_empty=True)
365 365 rhodecode_admin_grid_items = v.Int(min=5, not_empty=True)
366 366 rhodecode_show_version = v.StringBoolean(if_missing=False)
367 367 rhodecode_use_gravatar = v.StringBoolean(if_missing=False)
368 368 rhodecode_markup_renderer = v.OneOf(['markdown', 'rst'])
369 369 rhodecode_gravatar_url = v.UnicodeString(min=3)
370 370 rhodecode_clone_uri_tmpl = v.UnicodeString(min=3)
371 371 rhodecode_support_url = v.UnicodeString()
372 372 rhodecode_show_revision_number = v.StringBoolean(if_missing=False)
373 373 rhodecode_show_sha_length = v.Int(min=4, not_empty=True)
374 374
375 375 return _ApplicationVisualisationForm
376 376
377 377
378 378 class _BaseVcsSettingsForm(formencode.Schema):
379 379 allow_extra_fields = True
380 380 filter_extra_fields = False
381 381 hooks_changegroup_repo_size = v.StringBoolean(if_missing=False)
382 382 hooks_changegroup_push_logger = v.StringBoolean(if_missing=False)
383 383 hooks_outgoing_pull_logger = v.StringBoolean(if_missing=False)
384 384
385 385 extensions_largefiles = v.StringBoolean(if_missing=False)
386 386 phases_publish = v.StringBoolean(if_missing=False)
387 387
388 388 rhodecode_pr_merge_enabled = v.StringBoolean(if_missing=False)
389 389 rhodecode_use_outdated_comments = v.StringBoolean(if_missing=False)
390 390 rhodecode_hg_use_rebase_for_merging = v.StringBoolean(if_missing=False)
391 391
392 392 vcs_svn_proxy_http_requests_enabled = v.StringBoolean(if_missing=False)
393 393 vcs_svn_proxy_http_server_url = v.UnicodeString(strip=True, if_missing=None)
394 394
395 395
396 396 def ApplicationUiSettingsForm():
397 397 class _ApplicationUiSettingsForm(_BaseVcsSettingsForm):
398 398 web_push_ssl = v.StringBoolean(if_missing=False)
399 399 paths_root_path = All(
400 400 v.ValidPath(),
401 401 v.UnicodeString(strip=True, min=1, not_empty=True)
402 402 )
403 403 extensions_hgsubversion = v.StringBoolean(if_missing=False)
404 404 extensions_hggit = v.StringBoolean(if_missing=False)
405 405 new_svn_branch = v.ValidSvnPattern(section='vcs_svn_branch')
406 406 new_svn_tag = v.ValidSvnPattern(section='vcs_svn_tag')
407 407
408 408 return _ApplicationUiSettingsForm
409 409
410 410
411 411 def RepoVcsSettingsForm(repo_name):
412 412 class _RepoVcsSettingsForm(_BaseVcsSettingsForm):
413 413 inherit_global_settings = v.StringBoolean(if_missing=False)
414 414 new_svn_branch = v.ValidSvnPattern(
415 415 section='vcs_svn_branch', repo_name=repo_name)
416 416 new_svn_tag = v.ValidSvnPattern(
417 417 section='vcs_svn_tag', repo_name=repo_name)
418 418
419 419 return _RepoVcsSettingsForm
420 420
421 421
422 422 def LabsSettingsForm():
423 423 class _LabSettingsForm(formencode.Schema):
424 424 allow_extra_fields = True
425 425 filter_extra_fields = False
426 426
427 427 return _LabSettingsForm
428 428
429 429
430 430 def ApplicationPermissionsForm(register_choices, extern_activate_choices):
431 431 class _DefaultPermissionsForm(formencode.Schema):
432 432 allow_extra_fields = True
433 433 filter_extra_fields = True
434 434
435 435 anonymous = v.StringBoolean(if_missing=False)
436 436 default_register = v.OneOf(register_choices)
437 437 default_register_message = v.UnicodeString()
438 438 default_extern_activate = v.OneOf(extern_activate_choices)
439 439
440 440 return _DefaultPermissionsForm
441 441
442 442
443 443 def ObjectPermissionsForm(repo_perms_choices, group_perms_choices,
444 444 user_group_perms_choices):
445 445 class _ObjectPermissionsForm(formencode.Schema):
446 446 allow_extra_fields = True
447 447 filter_extra_fields = True
448 448 overwrite_default_repo = v.StringBoolean(if_missing=False)
449 449 overwrite_default_group = v.StringBoolean(if_missing=False)
450 450 overwrite_default_user_group = v.StringBoolean(if_missing=False)
451 451 default_repo_perm = v.OneOf(repo_perms_choices)
452 452 default_group_perm = v.OneOf(group_perms_choices)
453 453 default_user_group_perm = v.OneOf(user_group_perms_choices)
454 454
455 455 return _ObjectPermissionsForm
456 456
457 457
458 458 def UserPermissionsForm(create_choices, create_on_write_choices,
459 459 repo_group_create_choices, user_group_create_choices,
460 460 fork_choices, inherit_default_permissions_choices):
461 461 class _DefaultPermissionsForm(formencode.Schema):
462 462 allow_extra_fields = True
463 463 filter_extra_fields = True
464 464
465 465 anonymous = v.StringBoolean(if_missing=False)
466 466
467 467 default_repo_create = v.OneOf(create_choices)
468 468 default_repo_create_on_write = v.OneOf(create_on_write_choices)
469 469 default_user_group_create = v.OneOf(user_group_create_choices)
470 470 default_repo_group_create = v.OneOf(repo_group_create_choices)
471 471 default_fork_create = v.OneOf(fork_choices)
472 472 default_inherit_default_permissions = v.OneOf(inherit_default_permissions_choices)
473 473
474 474 return _DefaultPermissionsForm
475 475
476 476
477 477 def UserIndividualPermissionsForm():
478 478 class _DefaultPermissionsForm(formencode.Schema):
479 479 allow_extra_fields = True
480 480 filter_extra_fields = True
481 481
482 482 inherit_default_permissions = v.StringBoolean(if_missing=False)
483 483
484 484 return _DefaultPermissionsForm
485 485
486 486
487 487 def DefaultsForm(edit=False, old_data={}, supported_backends=BACKENDS.keys()):
488 488 class _DefaultsForm(formencode.Schema):
489 489 allow_extra_fields = True
490 490 filter_extra_fields = True
491 491 default_repo_type = v.OneOf(supported_backends)
492 492 default_repo_private = v.StringBoolean(if_missing=False)
493 493 default_repo_enable_statistics = v.StringBoolean(if_missing=False)
494 494 default_repo_enable_downloads = v.StringBoolean(if_missing=False)
495 495 default_repo_enable_locking = v.StringBoolean(if_missing=False)
496 496
497 497 return _DefaultsForm
498 498
499 499
500 500 def AuthSettingsForm():
501 501 class _AuthSettingsForm(formencode.Schema):
502 502 allow_extra_fields = True
503 503 filter_extra_fields = True
504 504 auth_plugins = All(v.ValidAuthPlugins(),
505 505 v.UniqueListFromString()(not_empty=True))
506 506
507 507 return _AuthSettingsForm
508 508
509 509
510 510 def UserExtraEmailForm():
511 511 class _UserExtraEmailForm(formencode.Schema):
512 512 email = All(v.UniqSystemEmail(), v.Email(not_empty=True))
513 513 return _UserExtraEmailForm
514 514
515 515
516 516 def UserExtraIpForm():
517 517 class _UserExtraIpForm(formencode.Schema):
518 518 ip = v.ValidIp()(not_empty=True)
519 519 return _UserExtraIpForm
520 520
521 521
522
522 523 def PullRequestForm(repo_id):
524 class ReviewerForm(formencode.Schema):
525 user_id = v.Int(not_empty=True)
526 reasons = All()
527
523 528 class _PullRequestForm(formencode.Schema):
524 529 allow_extra_fields = True
525 530 filter_extra_fields = True
526 531
527 532 user = v.UnicodeString(strip=True, required=True)
528 533 source_repo = v.UnicodeString(strip=True, required=True)
529 534 source_ref = v.UnicodeString(strip=True, required=True)
530 535 target_repo = v.UnicodeString(strip=True, required=True)
531 536 target_ref = v.UnicodeString(strip=True, required=True)
532 537 revisions = All(#v.NotReviewedRevisions(repo_id)(),
533 538 v.UniqueList()(not_empty=True))
534 review_members = v.UniqueList(convert=int)(not_empty=True)
535
539 review_members = formencode.ForEach(ReviewerForm())
536 540 pullrequest_title = v.UnicodeString(strip=True, required=True)
537 541 pullrequest_desc = v.UnicodeString(strip=True, required=False)
538 542
539 543 return _PullRequestForm
540 544
541 545
542 546 def IssueTrackerPatternsForm():
543 547 class _IssueTrackerPatternsForm(formencode.Schema):
544 548 allow_extra_fields = True
545 549 filter_extra_fields = False
546 550 chained_validators = [v.ValidPattern()]
547 551 return _IssueTrackerPatternsForm
@@ -1,1154 +1,1177 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 """
23 23 pull request model for RhodeCode
24 24 """
25 25
26 26 from collections import namedtuple
27 27 import json
28 28 import logging
29 29 import datetime
30 30
31 31 from pylons.i18n.translation import _
32 32 from pylons.i18n.translation import lazy_ugettext
33 33
34 34 from rhodecode.lib import helpers as h, hooks_utils, diffs
35 35 from rhodecode.lib.compat import OrderedDict
36 36 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
37 37 from rhodecode.lib.markup_renderer import (
38 38 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
39 39 from rhodecode.lib.utils import action_logger
40 40 from rhodecode.lib.utils2 import safe_unicode, safe_str, md5_safe
41 41 from rhodecode.lib.vcs.backends.base import (
42 42 Reference, MergeResponse, MergeFailureReason)
43 43 from rhodecode.lib.vcs.conf import settings as vcs_settings
44 44 from rhodecode.lib.vcs.exceptions import (
45 45 CommitDoesNotExistError, EmptyRepositoryError)
46 46 from rhodecode.model import BaseModel
47 47 from rhodecode.model.changeset_status import ChangesetStatusModel
48 48 from rhodecode.model.comment import ChangesetCommentsModel
49 49 from rhodecode.model.db import (
50 50 PullRequest, PullRequestReviewers, ChangesetStatus,
51 51 PullRequestVersion, ChangesetComment)
52 52 from rhodecode.model.meta import Session
53 53 from rhodecode.model.notification import NotificationModel, \
54 54 EmailNotificationModel
55 55 from rhodecode.model.scm import ScmModel
56 56 from rhodecode.model.settings import VcsSettingsModel
57 57
58 58
59 59 log = logging.getLogger(__name__)
60 60
61 61
62 62 class PullRequestModel(BaseModel):
63 63
64 64 cls = PullRequest
65 65
66 66 DIFF_CONTEXT = 3
67 67
68 68 MERGE_STATUS_MESSAGES = {
69 69 MergeFailureReason.NONE: lazy_ugettext(
70 70 'This pull request can be automatically merged.'),
71 71 MergeFailureReason.UNKNOWN: lazy_ugettext(
72 72 'This pull request cannot be merged because of an unhandled'
73 73 ' exception.'),
74 74 MergeFailureReason.MERGE_FAILED: lazy_ugettext(
75 75 'This pull request cannot be merged because of conflicts.'),
76 76 MergeFailureReason.PUSH_FAILED: lazy_ugettext(
77 77 'This pull request could not be merged because push to target'
78 78 ' failed.'),
79 79 MergeFailureReason.TARGET_IS_NOT_HEAD: lazy_ugettext(
80 80 'This pull request cannot be merged because the target is not a'
81 81 ' head.'),
82 82 MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES: lazy_ugettext(
83 83 'This pull request cannot be merged because the source contains'
84 84 ' more branches than the target.'),
85 85 MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
86 86 'This pull request cannot be merged because the target has'
87 87 ' multiple heads.'),
88 88 MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
89 89 'This pull request cannot be merged because the target repository'
90 90 ' is locked.'),
91 91 MergeFailureReason.MISSING_COMMIT: lazy_ugettext(
92 92 'This pull request cannot be merged because the target or the '
93 93 'source reference is missing.'),
94 94 }
95 95
96 96 def __get_pull_request(self, pull_request):
97 97 return self._get_instance(PullRequest, pull_request)
98 98
99 99 def _check_perms(self, perms, pull_request, user, api=False):
100 100 if not api:
101 101 return h.HasRepoPermissionAny(*perms)(
102 102 user=user, repo_name=pull_request.target_repo.repo_name)
103 103 else:
104 104 return h.HasRepoPermissionAnyApi(*perms)(
105 105 user=user, repo_name=pull_request.target_repo.repo_name)
106 106
107 107 def check_user_read(self, pull_request, user, api=False):
108 108 _perms = ('repository.admin', 'repository.write', 'repository.read',)
109 109 return self._check_perms(_perms, pull_request, user, api)
110 110
111 111 def check_user_merge(self, pull_request, user, api=False):
112 112 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
113 113 return self._check_perms(_perms, pull_request, user, api)
114 114
115 115 def check_user_update(self, pull_request, user, api=False):
116 116 owner = user.user_id == pull_request.user_id
117 117 return self.check_user_merge(pull_request, user, api) or owner
118 118
119 119 def check_user_change_status(self, pull_request, user, api=False):
120 120 reviewer = user.user_id in [x.user_id for x in
121 121 pull_request.reviewers]
122 122 return self.check_user_update(pull_request, user, api) or reviewer
123 123
124 124 def get(self, pull_request):
125 125 return self.__get_pull_request(pull_request)
126 126
127 127 def _prepare_get_all_query(self, repo_name, source=False, statuses=None,
128 128 opened_by=None, order_by=None,
129 129 order_dir='desc'):
130 130 repo = self._get_repo(repo_name)
131 131 q = PullRequest.query()
132 132 # source or target
133 133 if source:
134 134 q = q.filter(PullRequest.source_repo == repo)
135 135 else:
136 136 q = q.filter(PullRequest.target_repo == repo)
137 137
138 138 # closed,opened
139 139 if statuses:
140 140 q = q.filter(PullRequest.status.in_(statuses))
141 141
142 142 # opened by filter
143 143 if opened_by:
144 144 q = q.filter(PullRequest.user_id.in_(opened_by))
145 145
146 146 if order_by:
147 147 order_map = {
148 148 'name_raw': PullRequest.pull_request_id,
149 149 'title': PullRequest.title,
150 150 'updated_on_raw': PullRequest.updated_on
151 151 }
152 152 if order_dir == 'asc':
153 153 q = q.order_by(order_map[order_by].asc())
154 154 else:
155 155 q = q.order_by(order_map[order_by].desc())
156 156
157 157 return q
158 158
159 159 def count_all(self, repo_name, source=False, statuses=None,
160 160 opened_by=None):
161 161 """
162 162 Count the number of pull requests for a specific repository.
163 163
164 164 :param repo_name: target or source repo
165 165 :param source: boolean flag to specify if repo_name refers to source
166 166 :param statuses: list of pull request statuses
167 167 :param opened_by: author user of the pull request
168 168 :returns: int number of pull requests
169 169 """
170 170 q = self._prepare_get_all_query(
171 171 repo_name, source=source, statuses=statuses, opened_by=opened_by)
172 172
173 173 return q.count()
174 174
175 175 def get_all(self, repo_name, source=False, statuses=None, opened_by=None,
176 176 offset=0, length=None, order_by=None, order_dir='desc'):
177 177 """
178 178 Get all pull requests for a specific repository.
179 179
180 180 :param repo_name: target or source repo
181 181 :param source: boolean flag to specify if repo_name refers to source
182 182 :param statuses: list of pull request statuses
183 183 :param opened_by: author user of the pull request
184 184 :param offset: pagination offset
185 185 :param length: length of returned list
186 186 :param order_by: order of the returned list
187 187 :param order_dir: 'asc' or 'desc' ordering direction
188 188 :returns: list of pull requests
189 189 """
190 190 q = self._prepare_get_all_query(
191 191 repo_name, source=source, statuses=statuses, opened_by=opened_by,
192 192 order_by=order_by, order_dir=order_dir)
193 193
194 194 if length:
195 195 pull_requests = q.limit(length).offset(offset).all()
196 196 else:
197 197 pull_requests = q.all()
198 198
199 199 return pull_requests
200 200
201 201 def count_awaiting_review(self, repo_name, source=False, statuses=None,
202 202 opened_by=None):
203 203 """
204 204 Count the number of pull requests for a specific repository that are
205 205 awaiting review.
206 206
207 207 :param repo_name: target or source repo
208 208 :param source: boolean flag to specify if repo_name refers to source
209 209 :param statuses: list of pull request statuses
210 210 :param opened_by: author user of the pull request
211 211 :returns: int number of pull requests
212 212 """
213 213 pull_requests = self.get_awaiting_review(
214 214 repo_name, source=source, statuses=statuses, opened_by=opened_by)
215 215
216 216 return len(pull_requests)
217 217
218 218 def get_awaiting_review(self, repo_name, source=False, statuses=None,
219 219 opened_by=None, offset=0, length=None,
220 220 order_by=None, order_dir='desc'):
221 221 """
222 222 Get all pull requests for a specific repository that are awaiting
223 223 review.
224 224
225 225 :param repo_name: target or source repo
226 226 :param source: boolean flag to specify if repo_name refers to source
227 227 :param statuses: list of pull request statuses
228 228 :param opened_by: author user of the pull request
229 229 :param offset: pagination offset
230 230 :param length: length of returned list
231 231 :param order_by: order of the returned list
232 232 :param order_dir: 'asc' or 'desc' ordering direction
233 233 :returns: list of pull requests
234 234 """
235 235 pull_requests = self.get_all(
236 236 repo_name, source=source, statuses=statuses, opened_by=opened_by,
237 237 order_by=order_by, order_dir=order_dir)
238 238
239 239 _filtered_pull_requests = []
240 240 for pr in pull_requests:
241 241 status = pr.calculated_review_status()
242 242 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
243 243 ChangesetStatus.STATUS_UNDER_REVIEW]:
244 244 _filtered_pull_requests.append(pr)
245 245 if length:
246 246 return _filtered_pull_requests[offset:offset+length]
247 247 else:
248 248 return _filtered_pull_requests
249 249
250 250 def count_awaiting_my_review(self, repo_name, source=False, statuses=None,
251 251 opened_by=None, user_id=None):
252 252 """
253 253 Count the number of pull requests for a specific repository that are
254 254 awaiting review from a specific user.
255 255
256 256 :param repo_name: target or source repo
257 257 :param source: boolean flag to specify if repo_name refers to source
258 258 :param statuses: list of pull request statuses
259 259 :param opened_by: author user of the pull request
260 260 :param user_id: reviewer user of the pull request
261 261 :returns: int number of pull requests
262 262 """
263 263 pull_requests = self.get_awaiting_my_review(
264 264 repo_name, source=source, statuses=statuses, opened_by=opened_by,
265 265 user_id=user_id)
266 266
267 267 return len(pull_requests)
268 268
269 269 def get_awaiting_my_review(self, repo_name, source=False, statuses=None,
270 270 opened_by=None, user_id=None, offset=0,
271 271 length=None, order_by=None, order_dir='desc'):
272 272 """
273 273 Get all pull requests for a specific repository that are awaiting
274 274 review from a specific user.
275 275
276 276 :param repo_name: target or source repo
277 277 :param source: boolean flag to specify if repo_name refers to source
278 278 :param statuses: list of pull request statuses
279 279 :param opened_by: author user of the pull request
280 280 :param user_id: reviewer user of the pull request
281 281 :param offset: pagination offset
282 282 :param length: length of returned list
283 283 :param order_by: order of the returned list
284 284 :param order_dir: 'asc' or 'desc' ordering direction
285 285 :returns: list of pull requests
286 286 """
287 287 pull_requests = self.get_all(
288 288 repo_name, source=source, statuses=statuses, opened_by=opened_by,
289 289 order_by=order_by, order_dir=order_dir)
290 290
291 291 _my = PullRequestModel().get_not_reviewed(user_id)
292 292 my_participation = []
293 293 for pr in pull_requests:
294 294 if pr in _my:
295 295 my_participation.append(pr)
296 296 _filtered_pull_requests = my_participation
297 297 if length:
298 298 return _filtered_pull_requests[offset:offset+length]
299 299 else:
300 300 return _filtered_pull_requests
301 301
302 302 def get_not_reviewed(self, user_id):
303 303 return [
304 304 x.pull_request for x in PullRequestReviewers.query().filter(
305 305 PullRequestReviewers.user_id == user_id).all()
306 306 ]
307 307
308 308 def get_versions(self, pull_request):
309 309 """
310 310 returns version of pull request sorted by ID descending
311 311 """
312 312 return PullRequestVersion.query()\
313 313 .filter(PullRequestVersion.pull_request == pull_request)\
314 314 .order_by(PullRequestVersion.pull_request_version_id.asc())\
315 315 .all()
316 316
317 317 def create(self, created_by, source_repo, source_ref, target_repo,
318 318 target_ref, revisions, reviewers, title, description=None):
319 319 created_by_user = self._get_user(created_by)
320 320 source_repo = self._get_repo(source_repo)
321 321 target_repo = self._get_repo(target_repo)
322 322
323 323 pull_request = PullRequest()
324 324 pull_request.source_repo = source_repo
325 325 pull_request.source_ref = source_ref
326 326 pull_request.target_repo = target_repo
327 327 pull_request.target_ref = target_ref
328 328 pull_request.revisions = revisions
329 329 pull_request.title = title
330 330 pull_request.description = description
331 331 pull_request.author = created_by_user
332 332
333 333 Session().add(pull_request)
334 334 Session().flush()
335 335
336 reviewer_ids = set()
336 337 # members / reviewers
337 for user_id in set(reviewers):
338 for reviewer_object in reviewers:
339 if isinstance(reviewer_object, tuple):
340 user_id, reasons = reviewer_object
341 else:
342 user_id, reasons = reviewer_object, []
343
338 344 user = self._get_user(user_id)
339 reviewer = PullRequestReviewers(user, pull_request)
345 reviewer_ids.add(user.user_id)
346
347 reviewer = PullRequestReviewers(user, pull_request, reasons)
340 348 Session().add(reviewer)
341 349
342 350 # Set approval status to "Under Review" for all commits which are
343 351 # part of this pull request.
344 352 ChangesetStatusModel().set_status(
345 353 repo=target_repo,
346 354 status=ChangesetStatus.STATUS_UNDER_REVIEW,
347 355 user=created_by_user,
348 356 pull_request=pull_request
349 357 )
350 358
351 self.notify_reviewers(pull_request, reviewers)
359 self.notify_reviewers(pull_request, reviewer_ids)
352 360 self._trigger_pull_request_hook(
353 361 pull_request, created_by_user, 'create')
354 362
355 363 return pull_request
356 364
357 365 def _trigger_pull_request_hook(self, pull_request, user, action):
358 366 pull_request = self.__get_pull_request(pull_request)
359 367 target_scm = pull_request.target_repo.scm_instance()
360 368 if action == 'create':
361 369 trigger_hook = hooks_utils.trigger_log_create_pull_request_hook
362 370 elif action == 'merge':
363 371 trigger_hook = hooks_utils.trigger_log_merge_pull_request_hook
364 372 elif action == 'close':
365 373 trigger_hook = hooks_utils.trigger_log_close_pull_request_hook
366 374 elif action == 'review_status_change':
367 375 trigger_hook = hooks_utils.trigger_log_review_pull_request_hook
368 376 elif action == 'update':
369 377 trigger_hook = hooks_utils.trigger_log_update_pull_request_hook
370 378 else:
371 379 return
372 380
373 381 trigger_hook(
374 382 username=user.username,
375 383 repo_name=pull_request.target_repo.repo_name,
376 384 repo_alias=target_scm.alias,
377 385 pull_request=pull_request)
378 386
379 387 def _get_commit_ids(self, pull_request):
380 388 """
381 389 Return the commit ids of the merged pull request.
382 390
383 391 This method is not dealing correctly yet with the lack of autoupdates
384 392 nor with the implicit target updates.
385 393 For example: if a commit in the source repo is already in the target it
386 394 will be reported anyways.
387 395 """
388 396 merge_rev = pull_request.merge_rev
389 397 if merge_rev is None:
390 398 raise ValueError('This pull request was not merged yet')
391 399
392 400 commit_ids = list(pull_request.revisions)
393 401 if merge_rev not in commit_ids:
394 402 commit_ids.append(merge_rev)
395 403
396 404 return commit_ids
397 405
398 406 def merge(self, pull_request, user, extras):
399 407 log.debug("Merging pull request %s", pull_request.pull_request_id)
400 408 merge_state = self._merge_pull_request(pull_request, user, extras)
401 409 if merge_state.executed:
402 410 log.debug(
403 411 "Merge was successful, updating the pull request comments.")
404 412 self._comment_and_close_pr(pull_request, user, merge_state)
405 413 self._log_action('user_merged_pull_request', user, pull_request)
406 414 else:
407 415 log.warn("Merge failed, not updating the pull request.")
408 416 return merge_state
409 417
410 418 def _merge_pull_request(self, pull_request, user, extras):
411 419 target_vcs = pull_request.target_repo.scm_instance()
412 420 source_vcs = pull_request.source_repo.scm_instance()
413 421 target_ref = self._refresh_reference(
414 422 pull_request.target_ref_parts, target_vcs)
415 423
416 424 message = _(
417 425 'Merge pull request #%(pr_id)s from '
418 426 '%(source_repo)s %(source_ref_name)s\n\n %(pr_title)s') % {
419 427 'pr_id': pull_request.pull_request_id,
420 428 'source_repo': source_vcs.name,
421 429 'source_ref_name': pull_request.source_ref_parts.name,
422 430 'pr_title': pull_request.title
423 431 }
424 432
425 433 workspace_id = self._workspace_id(pull_request)
426 434 use_rebase = self._use_rebase_for_merging(pull_request)
427 435
428 436 callback_daemon, extras = prepare_callback_daemon(
429 437 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
430 438 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
431 439
432 440 with callback_daemon:
433 441 # TODO: johbo: Implement a clean way to run a config_override
434 442 # for a single call.
435 443 target_vcs.config.set(
436 444 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
437 445 merge_state = target_vcs.merge(
438 446 target_ref, source_vcs, pull_request.source_ref_parts,
439 447 workspace_id, user_name=user.username,
440 448 user_email=user.email, message=message, use_rebase=use_rebase)
441 449 return merge_state
442 450
443 451 def _comment_and_close_pr(self, pull_request, user, merge_state):
444 452 pull_request.merge_rev = merge_state.merge_commit_id
445 453 pull_request.updated_on = datetime.datetime.now()
446 454
447 455 ChangesetCommentsModel().create(
448 456 text=unicode(_('Pull request merged and closed')),
449 457 repo=pull_request.target_repo.repo_id,
450 458 user=user.user_id,
451 459 pull_request=pull_request.pull_request_id,
452 460 f_path=None,
453 461 line_no=None,
454 462 closing_pr=True
455 463 )
456 464
457 465 Session().add(pull_request)
458 466 Session().flush()
459 467 # TODO: paris: replace invalidation with less radical solution
460 468 ScmModel().mark_for_invalidation(
461 469 pull_request.target_repo.repo_name)
462 470 self._trigger_pull_request_hook(pull_request, user, 'merge')
463 471
464 472 def has_valid_update_type(self, pull_request):
465 473 source_ref_type = pull_request.source_ref_parts.type
466 474 return source_ref_type in ['book', 'branch', 'tag']
467 475
468 476 def update_commits(self, pull_request):
469 477 """
470 478 Get the updated list of commits for the pull request
471 479 and return the new pull request version and the list
472 480 of commits processed by this update action
473 481 """
474 482
475 483 pull_request = self.__get_pull_request(pull_request)
476 484 source_ref_type = pull_request.source_ref_parts.type
477 485 source_ref_name = pull_request.source_ref_parts.name
478 486 source_ref_id = pull_request.source_ref_parts.commit_id
479 487
480 488 if not self.has_valid_update_type(pull_request):
481 489 log.debug(
482 490 "Skipping update of pull request %s due to ref type: %s",
483 491 pull_request, source_ref_type)
484 492 return (None, None)
485 493
486 494 source_repo = pull_request.source_repo.scm_instance()
487 495 source_commit = source_repo.get_commit(commit_id=source_ref_name)
488 496 if source_ref_id == source_commit.raw_id:
489 497 log.debug("Nothing changed in pull request %s", pull_request)
490 498 return (None, None)
491 499
492 500 # Finally there is a need for an update
493 501 pull_request_version = self._create_version_from_snapshot(pull_request)
494 502 self._link_comments_to_version(pull_request_version)
495 503
496 504 target_ref_type = pull_request.target_ref_parts.type
497 505 target_ref_name = pull_request.target_ref_parts.name
498 506 target_ref_id = pull_request.target_ref_parts.commit_id
499 507 target_repo = pull_request.target_repo.scm_instance()
500 508
501 509 if target_ref_type in ('tag', 'branch', 'book'):
502 510 target_commit = target_repo.get_commit(target_ref_name)
503 511 else:
504 512 target_commit = target_repo.get_commit(target_ref_id)
505 513
506 514 # re-compute commit ids
507 515 old_commit_ids = set(pull_request.revisions)
508 516 pre_load = ["author", "branch", "date", "message"]
509 517 commit_ranges = target_repo.compare(
510 518 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
511 519 pre_load=pre_load)
512 520
513 521 ancestor = target_repo.get_common_ancestor(
514 522 target_commit.raw_id, source_commit.raw_id, source_repo)
515 523
516 524 pull_request.source_ref = '%s:%s:%s' % (
517 525 source_ref_type, source_ref_name, source_commit.raw_id)
518 526 pull_request.target_ref = '%s:%s:%s' % (
519 527 target_ref_type, target_ref_name, ancestor)
520 528 pull_request.revisions = [
521 529 commit.raw_id for commit in reversed(commit_ranges)]
522 530 pull_request.updated_on = datetime.datetime.now()
523 531 Session().add(pull_request)
524 532 new_commit_ids = set(pull_request.revisions)
525 533
526 534 changes = self._calculate_commit_id_changes(
527 535 old_commit_ids, new_commit_ids)
528 536
529 537 old_diff_data, new_diff_data = self._generate_update_diffs(
530 538 pull_request, pull_request_version)
531 539
532 540 ChangesetCommentsModel().outdate_comments(
533 541 pull_request, old_diff_data=old_diff_data,
534 542 new_diff_data=new_diff_data)
535 543
536 544 file_changes = self._calculate_file_changes(
537 545 old_diff_data, new_diff_data)
538 546
539 547 # Add an automatic comment to the pull request
540 548 update_comment = ChangesetCommentsModel().create(
541 549 text=self._render_update_message(changes, file_changes),
542 550 repo=pull_request.target_repo,
543 551 user=pull_request.author,
544 552 pull_request=pull_request,
545 553 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
546 554
547 555 # Update status to "Under Review" for added commits
548 556 for commit_id in changes.added:
549 557 ChangesetStatusModel().set_status(
550 558 repo=pull_request.source_repo,
551 559 status=ChangesetStatus.STATUS_UNDER_REVIEW,
552 560 comment=update_comment,
553 561 user=pull_request.author,
554 562 pull_request=pull_request,
555 563 revision=commit_id)
556 564
557 565 log.debug(
558 566 'Updated pull request %s, added_ids: %s, common_ids: %s, '
559 567 'removed_ids: %s', pull_request.pull_request_id,
560 568 changes.added, changes.common, changes.removed)
561 569 log.debug('Updated pull request with the following file changes: %s',
562 570 file_changes)
563 571
564 572 log.info(
565 573 "Updated pull request %s from commit %s to commit %s, "
566 574 "stored new version %s of this pull request.",
567 575 pull_request.pull_request_id, source_ref_id,
568 576 pull_request.source_ref_parts.commit_id,
569 577 pull_request_version.pull_request_version_id)
570 578 Session().commit()
571 579 self._trigger_pull_request_hook(pull_request, pull_request.author,
572 580 'update')
581
573 582 return (pull_request_version, changes)
574 583
575 584 def _create_version_from_snapshot(self, pull_request):
576 585 version = PullRequestVersion()
577 586 version.title = pull_request.title
578 587 version.description = pull_request.description
579 588 version.status = pull_request.status
580 589 version.created_on = pull_request.created_on
581 590 version.updated_on = pull_request.updated_on
582 591 version.user_id = pull_request.user_id
583 592 version.source_repo = pull_request.source_repo
584 593 version.source_ref = pull_request.source_ref
585 594 version.target_repo = pull_request.target_repo
586 595 version.target_ref = pull_request.target_ref
587 596
588 597 version._last_merge_source_rev = pull_request._last_merge_source_rev
589 598 version._last_merge_target_rev = pull_request._last_merge_target_rev
590 599 version._last_merge_status = pull_request._last_merge_status
591 600 version.merge_rev = pull_request.merge_rev
592 601
593 602 version.revisions = pull_request.revisions
594 603 version.pull_request = pull_request
595 604 Session().add(version)
596 605 Session().flush()
597 606
598 607 return version
599 608
600 609 def _generate_update_diffs(self, pull_request, pull_request_version):
601 610 diff_context = (
602 611 self.DIFF_CONTEXT +
603 612 ChangesetCommentsModel.needed_extra_diff_context())
604 613 old_diff = self._get_diff_from_pr_or_version(
605 614 pull_request_version, context=diff_context)
606 615 new_diff = self._get_diff_from_pr_or_version(
607 616 pull_request, context=diff_context)
608 617
609 618 old_diff_data = diffs.DiffProcessor(old_diff)
610 619 old_diff_data.prepare()
611 620 new_diff_data = diffs.DiffProcessor(new_diff)
612 621 new_diff_data.prepare()
613 622
614 623 return old_diff_data, new_diff_data
615 624
616 625 def _link_comments_to_version(self, pull_request_version):
617 626 """
618 627 Link all unlinked comments of this pull request to the given version.
619 628
620 629 :param pull_request_version: The `PullRequestVersion` to which
621 630 the comments shall be linked.
622 631
623 632 """
624 633 pull_request = pull_request_version.pull_request
625 634 comments = ChangesetComment.query().filter(
626 635 # TODO: johbo: Should we query for the repo at all here?
627 636 # Pending decision on how comments of PRs are to be related
628 637 # to either the source repo, the target repo or no repo at all.
629 638 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
630 639 ChangesetComment.pull_request == pull_request,
631 640 ChangesetComment.pull_request_version == None)
632 641
633 642 # TODO: johbo: Find out why this breaks if it is done in a bulk
634 643 # operation.
635 644 for comment in comments:
636 645 comment.pull_request_version_id = (
637 646 pull_request_version.pull_request_version_id)
638 647 Session().add(comment)
639 648
640 649 def _calculate_commit_id_changes(self, old_ids, new_ids):
641 650 added = new_ids.difference(old_ids)
642 651 common = old_ids.intersection(new_ids)
643 652 removed = old_ids.difference(new_ids)
644 653 return ChangeTuple(added, common, removed)
645 654
646 655 def _calculate_file_changes(self, old_diff_data, new_diff_data):
647 656
648 657 old_files = OrderedDict()
649 658 for diff_data in old_diff_data.parsed_diff:
650 659 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
651 660
652 661 added_files = []
653 662 modified_files = []
654 663 removed_files = []
655 664 for diff_data in new_diff_data.parsed_diff:
656 665 new_filename = diff_data['filename']
657 666 new_hash = md5_safe(diff_data['raw_diff'])
658 667
659 668 old_hash = old_files.get(new_filename)
660 669 if not old_hash:
661 670 # file is not present in old diff, means it's added
662 671 added_files.append(new_filename)
663 672 else:
664 673 if new_hash != old_hash:
665 674 modified_files.append(new_filename)
666 675 # now remove a file from old, since we have seen it already
667 676 del old_files[new_filename]
668 677
669 678 # removed files is when there are present in old, but not in NEW,
670 679 # since we remove old files that are present in new diff, left-overs
671 680 # if any should be the removed files
672 681 removed_files.extend(old_files.keys())
673 682
674 683 return FileChangeTuple(added_files, modified_files, removed_files)
675 684
676 685 def _render_update_message(self, changes, file_changes):
677 686 """
678 687 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
679 688 so it's always looking the same disregarding on which default
680 689 renderer system is using.
681 690
682 691 :param changes: changes named tuple
683 692 :param file_changes: file changes named tuple
684 693
685 694 """
686 695 new_status = ChangesetStatus.get_status_lbl(
687 696 ChangesetStatus.STATUS_UNDER_REVIEW)
688 697
689 698 changed_files = (
690 699 file_changes.added + file_changes.modified + file_changes.removed)
691 700
692 701 params = {
693 702 'under_review_label': new_status,
694 703 'added_commits': changes.added,
695 704 'removed_commits': changes.removed,
696 705 'changed_files': changed_files,
697 706 'added_files': file_changes.added,
698 707 'modified_files': file_changes.modified,
699 708 'removed_files': file_changes.removed,
700 709 }
701 710 renderer = RstTemplateRenderer()
702 711 return renderer.render('pull_request_update.mako', **params)
703 712
704 713 def edit(self, pull_request, title, description):
705 714 pull_request = self.__get_pull_request(pull_request)
706 715 if pull_request.is_closed():
707 716 raise ValueError('This pull request is closed')
708 717 if title:
709 718 pull_request.title = title
710 719 pull_request.description = description
711 720 pull_request.updated_on = datetime.datetime.now()
712 721 Session().add(pull_request)
713 722
714 def update_reviewers(self, pull_request, reviewers_ids):
715 reviewers_ids = set(reviewers_ids)
723 def update_reviewers(self, pull_request, reviewer_data):
724 """
725 Update the reviewers in the pull request
726
727 :param pull_request: the pr to update
728 :param reviewer_data: list of tuples [(user, ['reason1', 'reason2'])]
729 """
730
731 reviewers_reasons = {}
732 for user_id, reasons in reviewer_data:
733 if isinstance(user_id, (int, basestring)):
734 user_id = self._get_user(user_id).user_id
735 reviewers_reasons[user_id] = reasons
736
737 reviewers_ids = set(reviewers_reasons.keys())
716 738 pull_request = self.__get_pull_request(pull_request)
717 739 current_reviewers = PullRequestReviewers.query()\
718 740 .filter(PullRequestReviewers.pull_request ==
719 741 pull_request).all()
720 742 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
721 743
722 744 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
723 745 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
724 746
725 747 log.debug("Adding %s reviewers", ids_to_add)
726 748 log.debug("Removing %s reviewers", ids_to_remove)
727 749 changed = False
728 750 for uid in ids_to_add:
729 751 changed = True
730 752 _usr = self._get_user(uid)
731 reviewer = PullRequestReviewers(_usr, pull_request)
753 reasons = reviewers_reasons[uid]
754 reviewer = PullRequestReviewers(_usr, pull_request, reasons)
732 755 Session().add(reviewer)
733 756
734 757 self.notify_reviewers(pull_request, ids_to_add)
735 758
736 759 for uid in ids_to_remove:
737 760 changed = True
738 761 reviewer = PullRequestReviewers.query()\
739 762 .filter(PullRequestReviewers.user_id == uid,
740 763 PullRequestReviewers.pull_request == pull_request)\
741 764 .scalar()
742 765 if reviewer:
743 766 Session().delete(reviewer)
744 767 if changed:
745 768 pull_request.updated_on = datetime.datetime.now()
746 769 Session().add(pull_request)
747 770
748 771 return ids_to_add, ids_to_remove
749 772
750 773 def get_url(self, pull_request):
751 774 return h.url('pullrequest_show',
752 775 repo_name=safe_str(pull_request.target_repo.repo_name),
753 776 pull_request_id=pull_request.pull_request_id,
754 777 qualified=True)
755 778
756 779 def notify_reviewers(self, pull_request, reviewers_ids):
757 780 # notification to reviewers
758 781 if not reviewers_ids:
759 782 return
760 783
761 784 pull_request_obj = pull_request
762 785 # get the current participants of this pull request
763 786 recipients = reviewers_ids
764 787 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
765 788
766 789 pr_source_repo = pull_request_obj.source_repo
767 790 pr_target_repo = pull_request_obj.target_repo
768 791
769 792 pr_url = h.url(
770 793 'pullrequest_show',
771 794 repo_name=pr_target_repo.repo_name,
772 795 pull_request_id=pull_request_obj.pull_request_id,
773 796 qualified=True,)
774 797
775 798 # set some variables for email notification
776 799 pr_target_repo_url = h.url(
777 800 'summary_home',
778 801 repo_name=pr_target_repo.repo_name,
779 802 qualified=True)
780 803
781 804 pr_source_repo_url = h.url(
782 805 'summary_home',
783 806 repo_name=pr_source_repo.repo_name,
784 807 qualified=True)
785 808
786 809 # pull request specifics
787 810 pull_request_commits = [
788 811 (x.raw_id, x.message)
789 812 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
790 813
791 814 kwargs = {
792 815 'user': pull_request.author,
793 816 'pull_request': pull_request_obj,
794 817 'pull_request_commits': pull_request_commits,
795 818
796 819 'pull_request_target_repo': pr_target_repo,
797 820 'pull_request_target_repo_url': pr_target_repo_url,
798 821
799 822 'pull_request_source_repo': pr_source_repo,
800 823 'pull_request_source_repo_url': pr_source_repo_url,
801 824
802 825 'pull_request_url': pr_url,
803 826 }
804 827
805 828 # pre-generate the subject for notification itself
806 829 (subject,
807 830 _h, _e, # we don't care about those
808 831 body_plaintext) = EmailNotificationModel().render_email(
809 832 notification_type, **kwargs)
810 833
811 834 # create notification objects, and emails
812 835 NotificationModel().create(
813 836 created_by=pull_request.author,
814 837 notification_subject=subject,
815 838 notification_body=body_plaintext,
816 839 notification_type=notification_type,
817 840 recipients=recipients,
818 841 email_kwargs=kwargs,
819 842 )
820 843
821 844 def delete(self, pull_request):
822 845 pull_request = self.__get_pull_request(pull_request)
823 846 self._cleanup_merge_workspace(pull_request)
824 847 Session().delete(pull_request)
825 848
826 849 def close_pull_request(self, pull_request, user):
827 850 pull_request = self.__get_pull_request(pull_request)
828 851 self._cleanup_merge_workspace(pull_request)
829 852 pull_request.status = PullRequest.STATUS_CLOSED
830 853 pull_request.updated_on = datetime.datetime.now()
831 854 Session().add(pull_request)
832 855 self._trigger_pull_request_hook(
833 856 pull_request, pull_request.author, 'close')
834 857 self._log_action('user_closed_pull_request', user, pull_request)
835 858
836 859 def close_pull_request_with_comment(self, pull_request, user, repo,
837 860 message=None):
838 861 status = ChangesetStatus.STATUS_REJECTED
839 862
840 863 if not message:
841 864 message = (
842 865 _('Status change %(transition_icon)s %(status)s') % {
843 866 'transition_icon': '>',
844 867 'status': ChangesetStatus.get_status_lbl(status)})
845 868
846 869 internal_message = _('Closing with') + ' ' + message
847 870
848 871 comm = ChangesetCommentsModel().create(
849 872 text=internal_message,
850 873 repo=repo.repo_id,
851 874 user=user.user_id,
852 875 pull_request=pull_request.pull_request_id,
853 876 f_path=None,
854 877 line_no=None,
855 878 status_change=ChangesetStatus.get_status_lbl(status),
856 879 status_change_type=status,
857 880 closing_pr=True
858 881 )
859 882
860 883 ChangesetStatusModel().set_status(
861 884 repo.repo_id,
862 885 status,
863 886 user.user_id,
864 887 comm,
865 888 pull_request=pull_request.pull_request_id
866 889 )
867 890 Session().flush()
868 891
869 892 PullRequestModel().close_pull_request(
870 893 pull_request.pull_request_id, user)
871 894
872 895 def merge_status(self, pull_request):
873 896 if not self._is_merge_enabled(pull_request):
874 897 return False, _('Server-side pull request merging is disabled.')
875 898 if pull_request.is_closed():
876 899 return False, _('This pull request is closed.')
877 900 merge_possible, msg = self._check_repo_requirements(
878 901 target=pull_request.target_repo, source=pull_request.source_repo)
879 902 if not merge_possible:
880 903 return merge_possible, msg
881 904
882 905 try:
883 906 resp = self._try_merge(pull_request)
884 907 status = resp.possible, self.merge_status_message(
885 908 resp.failure_reason)
886 909 except NotImplementedError:
887 910 status = False, _('Pull request merging is not supported.')
888 911
889 912 return status
890 913
891 914 def _check_repo_requirements(self, target, source):
892 915 """
893 916 Check if `target` and `source` have compatible requirements.
894 917
895 918 Currently this is just checking for largefiles.
896 919 """
897 920 target_has_largefiles = self._has_largefiles(target)
898 921 source_has_largefiles = self._has_largefiles(source)
899 922 merge_possible = True
900 923 message = u''
901 924
902 925 if target_has_largefiles != source_has_largefiles:
903 926 merge_possible = False
904 927 if source_has_largefiles:
905 928 message = _(
906 929 'Target repository large files support is disabled.')
907 930 else:
908 931 message = _(
909 932 'Source repository large files support is disabled.')
910 933
911 934 return merge_possible, message
912 935
913 936 def _has_largefiles(self, repo):
914 937 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
915 938 'extensions', 'largefiles')
916 939 return largefiles_ui and largefiles_ui[0].active
917 940
918 941 def _try_merge(self, pull_request):
919 942 """
920 943 Try to merge the pull request and return the merge status.
921 944 """
922 945 log.debug(
923 946 "Trying out if the pull request %s can be merged.",
924 947 pull_request.pull_request_id)
925 948 target_vcs = pull_request.target_repo.scm_instance()
926 949 target_ref = self._refresh_reference(
927 950 pull_request.target_ref_parts, target_vcs)
928 951
929 952 target_locked = pull_request.target_repo.locked
930 953 if target_locked and target_locked[0]:
931 954 log.debug("The target repository is locked.")
932 955 merge_state = MergeResponse(
933 956 False, False, None, MergeFailureReason.TARGET_IS_LOCKED)
934 957 elif self._needs_merge_state_refresh(pull_request, target_ref):
935 958 log.debug("Refreshing the merge status of the repository.")
936 959 merge_state = self._refresh_merge_state(
937 960 pull_request, target_vcs, target_ref)
938 961 else:
939 962 possible = pull_request.\
940 963 _last_merge_status == MergeFailureReason.NONE
941 964 merge_state = MergeResponse(
942 965 possible, False, None, pull_request._last_merge_status)
943 966 log.debug("Merge response: %s", merge_state)
944 967 return merge_state
945 968
946 969 def _refresh_reference(self, reference, vcs_repository):
947 970 if reference.type in ('branch', 'book'):
948 971 name_or_id = reference.name
949 972 else:
950 973 name_or_id = reference.commit_id
951 974 refreshed_commit = vcs_repository.get_commit(name_or_id)
952 975 refreshed_reference = Reference(
953 976 reference.type, reference.name, refreshed_commit.raw_id)
954 977 return refreshed_reference
955 978
956 979 def _needs_merge_state_refresh(self, pull_request, target_reference):
957 980 return not(
958 981 pull_request.revisions and
959 982 pull_request.revisions[0] == pull_request._last_merge_source_rev and
960 983 target_reference.commit_id == pull_request._last_merge_target_rev)
961 984
962 985 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
963 986 workspace_id = self._workspace_id(pull_request)
964 987 source_vcs = pull_request.source_repo.scm_instance()
965 988 use_rebase = self._use_rebase_for_merging(pull_request)
966 989 merge_state = target_vcs.merge(
967 990 target_reference, source_vcs, pull_request.source_ref_parts,
968 991 workspace_id, dry_run=True, use_rebase=use_rebase)
969 992
970 993 # Do not store the response if there was an unknown error.
971 994 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
972 995 pull_request._last_merge_source_rev = pull_request.\
973 996 source_ref_parts.commit_id
974 997 pull_request._last_merge_target_rev = target_reference.commit_id
975 998 pull_request._last_merge_status = (
976 999 merge_state.failure_reason)
977 1000 Session().add(pull_request)
978 1001 Session().flush()
979 1002
980 1003 return merge_state
981 1004
982 1005 def _workspace_id(self, pull_request):
983 1006 workspace_id = 'pr-%s' % pull_request.pull_request_id
984 1007 return workspace_id
985 1008
986 1009 def merge_status_message(self, status_code):
987 1010 """
988 1011 Return a human friendly error message for the given merge status code.
989 1012 """
990 1013 return self.MERGE_STATUS_MESSAGES[status_code]
991 1014
992 1015 def generate_repo_data(self, repo, commit_id=None, branch=None,
993 1016 bookmark=None):
994 1017 all_refs, selected_ref = \
995 1018 self._get_repo_pullrequest_sources(
996 1019 repo.scm_instance(), commit_id=commit_id,
997 1020 branch=branch, bookmark=bookmark)
998 1021
999 1022 refs_select2 = []
1000 1023 for element in all_refs:
1001 1024 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1002 1025 refs_select2.append({'text': element[1], 'children': children})
1003 1026
1004 1027 return {
1005 1028 'user': {
1006 1029 'user_id': repo.user.user_id,
1007 1030 'username': repo.user.username,
1008 1031 'firstname': repo.user.firstname,
1009 1032 'lastname': repo.user.lastname,
1010 1033 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1011 1034 },
1012 1035 'description': h.chop_at_smart(repo.description, '\n'),
1013 1036 'refs': {
1014 1037 'all_refs': all_refs,
1015 1038 'selected_ref': selected_ref,
1016 1039 'select2_refs': refs_select2
1017 1040 }
1018 1041 }
1019 1042
1020 1043 def generate_pullrequest_title(self, source, source_ref, target):
1021 1044 return u'{source}#{at_ref} to {target}'.format(
1022 1045 source=source,
1023 1046 at_ref=source_ref,
1024 1047 target=target,
1025 1048 )
1026 1049
1027 1050 def _cleanup_merge_workspace(self, pull_request):
1028 1051 # Merging related cleanup
1029 1052 target_scm = pull_request.target_repo.scm_instance()
1030 1053 workspace_id = 'pr-%s' % pull_request.pull_request_id
1031 1054
1032 1055 try:
1033 1056 target_scm.cleanup_merge_workspace(workspace_id)
1034 1057 except NotImplementedError:
1035 1058 pass
1036 1059
1037 1060 def _get_repo_pullrequest_sources(
1038 1061 self, repo, commit_id=None, branch=None, bookmark=None):
1039 1062 """
1040 1063 Return a structure with repo's interesting commits, suitable for
1041 1064 the selectors in pullrequest controller
1042 1065
1043 1066 :param commit_id: a commit that must be in the list somehow
1044 1067 and selected by default
1045 1068 :param branch: a branch that must be in the list and selected
1046 1069 by default - even if closed
1047 1070 :param bookmark: a bookmark that must be in the list and selected
1048 1071 """
1049 1072
1050 1073 commit_id = safe_str(commit_id) if commit_id else None
1051 1074 branch = safe_str(branch) if branch else None
1052 1075 bookmark = safe_str(bookmark) if bookmark else None
1053 1076
1054 1077 selected = None
1055 1078
1056 1079 # order matters: first source that has commit_id in it will be selected
1057 1080 sources = []
1058 1081 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1059 1082 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1060 1083
1061 1084 if commit_id:
1062 1085 ref_commit = (h.short_id(commit_id), commit_id)
1063 1086 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1064 1087
1065 1088 sources.append(
1066 1089 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1067 1090 )
1068 1091
1069 1092 groups = []
1070 1093 for group_key, ref_list, group_name, match in sources:
1071 1094 group_refs = []
1072 1095 for ref_name, ref_id in ref_list:
1073 1096 ref_key = '%s:%s:%s' % (group_key, ref_name, ref_id)
1074 1097 group_refs.append((ref_key, ref_name))
1075 1098
1076 1099 if not selected:
1077 1100 if set([commit_id, match]) & set([ref_id, ref_name]):
1078 1101 selected = ref_key
1079 1102
1080 1103 if group_refs:
1081 1104 groups.append((group_refs, group_name))
1082 1105
1083 1106 if not selected:
1084 1107 ref = commit_id or branch or bookmark
1085 1108 if ref:
1086 1109 raise CommitDoesNotExistError(
1087 1110 'No commit refs could be found matching: %s' % ref)
1088 1111 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1089 1112 selected = 'branch:%s:%s' % (
1090 1113 repo.DEFAULT_BRANCH_NAME,
1091 1114 repo.branches[repo.DEFAULT_BRANCH_NAME]
1092 1115 )
1093 1116 elif repo.commit_ids:
1094 1117 rev = repo.commit_ids[0]
1095 1118 selected = 'rev:%s:%s' % (rev, rev)
1096 1119 else:
1097 1120 raise EmptyRepositoryError()
1098 1121 return groups, selected
1099 1122
1100 1123 def get_diff(self, pull_request, context=DIFF_CONTEXT):
1101 1124 pull_request = self.__get_pull_request(pull_request)
1102 1125 return self._get_diff_from_pr_or_version(pull_request, context=context)
1103 1126
1104 1127 def _get_diff_from_pr_or_version(self, pr_or_version, context):
1105 1128 source_repo = pr_or_version.source_repo
1106 1129
1107 1130 # we swap org/other ref since we run a simple diff on one repo
1108 1131 target_ref_id = pr_or_version.target_ref_parts.commit_id
1109 1132 source_ref_id = pr_or_version.source_ref_parts.commit_id
1110 1133 target_commit = source_repo.get_commit(
1111 1134 commit_id=safe_str(target_ref_id))
1112 1135 source_commit = source_repo.get_commit(commit_id=safe_str(source_ref_id))
1113 1136 vcs_repo = source_repo.scm_instance()
1114 1137
1115 1138 # TODO: johbo: In the context of an update, we cannot reach
1116 1139 # the old commit anymore with our normal mechanisms. It needs
1117 1140 # some sort of special support in the vcs layer to avoid this
1118 1141 # workaround.
1119 1142 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1120 1143 vcs_repo.alias == 'git'):
1121 1144 source_commit.raw_id = safe_str(source_ref_id)
1122 1145
1123 1146 log.debug('calculating diff between '
1124 1147 'source_ref:%s and target_ref:%s for repo `%s`',
1125 1148 target_ref_id, source_ref_id,
1126 1149 safe_unicode(vcs_repo.path))
1127 1150
1128 1151 vcs_diff = vcs_repo.get_diff(
1129 1152 commit1=target_commit, commit2=source_commit, context=context)
1130 1153 return vcs_diff
1131 1154
1132 1155 def _is_merge_enabled(self, pull_request):
1133 1156 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1134 1157 settings = settings_model.get_general_settings()
1135 1158 return settings.get('rhodecode_pr_merge_enabled', False)
1136 1159
1137 1160 def _use_rebase_for_merging(self, pull_request):
1138 1161 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
1139 1162 settings = settings_model.get_general_settings()
1140 1163 return settings.get('rhodecode_hg_use_rebase_for_merging', False)
1141 1164
1142 1165 def _log_action(self, action, user, pull_request):
1143 1166 action_logger(
1144 1167 user,
1145 1168 '{action}:{pr_id}'.format(
1146 1169 action=action, pr_id=pull_request.pull_request_id),
1147 1170 pull_request.target_repo)
1148 1171
1149 1172
1150 1173 ChangeTuple = namedtuple('ChangeTuple',
1151 1174 ['added', 'common', 'removed'])
1152 1175
1153 1176 FileChangeTuple = namedtuple('FileChangeTuple',
1154 1177 ['added', 'modified', 'removed'])
@@ -1,2167 +1,2170 b''
1 1 //Primary CSS
2 2
3 3 //--- IMPORTS ------------------//
4 4
5 5 @import 'helpers';
6 6 @import 'mixins';
7 7 @import 'rcicons';
8 8 @import 'fonts';
9 9 @import 'variables';
10 10 @import 'bootstrap-variables';
11 11 @import 'form-bootstrap';
12 12 @import 'codemirror';
13 13 @import 'legacy_code_styles';
14 14 @import 'progress-bar';
15 15
16 16 @import 'type';
17 17 @import 'alerts';
18 18 @import 'buttons';
19 19 @import 'tags';
20 20 @import 'code-block';
21 21 @import 'examples';
22 22 @import 'login';
23 23 @import 'main-content';
24 24 @import 'select2';
25 25 @import 'comments';
26 26 @import 'panels-bootstrap';
27 27 @import 'panels';
28 28 @import 'deform';
29 29
30 30 //--- BASE ------------------//
31 31 .noscript-error {
32 32 top: 0;
33 33 left: 0;
34 34 width: 100%;
35 35 z-index: 101;
36 36 text-align: center;
37 37 font-family: @text-semibold;
38 38 font-size: 120%;
39 39 color: white;
40 40 background-color: @alert2;
41 41 padding: 5px 0 5px 0;
42 42 }
43 43
44 44 html {
45 45 display: table;
46 46 height: 100%;
47 47 width: 100%;
48 48 }
49 49
50 50 body {
51 51 display: table-cell;
52 52 width: 100%;
53 53 }
54 54
55 55 //--- LAYOUT ------------------//
56 56
57 57 .hidden{
58 58 display: none !important;
59 59 }
60 60
61 61 .box{
62 62 float: left;
63 63 width: 100%;
64 64 }
65 65
66 66 .browser-header {
67 67 clear: both;
68 68 }
69 69 .main {
70 70 clear: both;
71 71 padding:0 0 @pagepadding;
72 72 height: auto;
73 73
74 74 &:after { //clearfix
75 75 content:"";
76 76 clear:both;
77 77 width:100%;
78 78 display:block;
79 79 }
80 80 }
81 81
82 82 .action-link{
83 83 margin-left: @padding;
84 84 padding-left: @padding;
85 85 border-left: @border-thickness solid @border-default-color;
86 86 }
87 87
88 88 input + .action-link, .action-link.first{
89 89 border-left: none;
90 90 }
91 91
92 92 .action-link.last{
93 93 margin-right: @padding;
94 94 padding-right: @padding;
95 95 }
96 96
97 97 .action-link.active,
98 98 .action-link.active a{
99 99 color: @grey4;
100 100 }
101 101
102 102 ul.simple-list{
103 103 list-style: none;
104 104 margin: 0;
105 105 padding: 0;
106 106 }
107 107
108 108 .main-content {
109 109 padding-bottom: @pagepadding;
110 110 }
111 111
112 112 .wrapper {
113 113 position: relative;
114 114 max-width: @wrapper-maxwidth;
115 115 margin: 0 auto;
116 116 }
117 117
118 118 #content {
119 119 clear: both;
120 120 padding: 0 @contentpadding;
121 121 }
122 122
123 123 .advanced-settings-fields{
124 124 input{
125 125 margin-left: @textmargin;
126 126 margin-right: @padding/2;
127 127 }
128 128 }
129 129
130 130 .cs_files_title {
131 131 margin: @pagepadding 0 0;
132 132 }
133 133
134 134 input.inline[type="file"] {
135 135 display: inline;
136 136 }
137 137
138 138 .error_page {
139 139 margin: 10% auto;
140 140
141 141 h1 {
142 142 color: @grey2;
143 143 }
144 144
145 145 .alert {
146 146 margin: @padding 0;
147 147 }
148
148
149 149 .error-branding {
150 150 font-family: @text-semibold;
151 151 color: @grey4;
152 152 }
153 153
154 154 .error_message {
155 155 font-family: @text-regular;
156 156 }
157 157
158 158 .sidebar {
159 159 min-height: 275px;
160 160 margin: 0;
161 161 padding: 0 0 @sidebarpadding @sidebarpadding;
162 162 border: none;
163 163 }
164 164
165 165 .main-content {
166 166 position: relative;
167 167 margin: 0 @sidebarpadding @sidebarpadding;
168 168 padding: 0 0 0 @sidebarpadding;
169 169 border-left: @border-thickness solid @grey5;
170 170
171 171 @media (max-width:767px) {
172 172 clear: both;
173 173 width: 100%;
174 174 margin: 0;
175 175 border: none;
176 176 }
177 177 }
178 178
179 179 .inner-column {
180 180 float: left;
181 181 width: 29.75%;
182 182 min-height: 150px;
183 183 margin: @sidebarpadding 2% 0 0;
184 184 padding: 0 2% 0 0;
185 185 border-right: @border-thickness solid @grey5;
186 186
187 187 @media (max-width:767px) {
188 188 clear: both;
189 189 width: 100%;
190 190 border: none;
191 191 }
192 192
193 193 ul {
194 194 padding-left: 1.25em;
195 195 }
196 196
197 197 &:last-child {
198 198 margin: @sidebarpadding 0 0;
199 199 border: none;
200 200 }
201 201
202 202 h4 {
203 203 margin: 0 0 @padding;
204 204 font-family: @text-semibold;
205 205 }
206 206 }
207 207 }
208 208 .error-page-logo {
209 209 width: 130px;
210 210 height: 160px;
211 211 }
212 212
213 213 // HEADER
214 214 .header {
215 215
216 216 // TODO: johbo: Fix login pages, so that they work without a min-height
217 217 // for the header and then remove the min-height. I chose a smaller value
218 218 // intentionally here to avoid rendering issues in the main navigation.
219 219 min-height: 49px;
220 220
221 221 position: relative;
222 222 vertical-align: bottom;
223 223 padding: 0 @header-padding;
224 224 background-color: @grey2;
225 225 color: @grey5;
226 226
227 227 .title {
228 228 overflow: visible;
229 229 }
230 230
231 231 &:before,
232 232 &:after {
233 233 content: "";
234 234 clear: both;
235 235 width: 100%;
236 236 }
237 237
238 238 // TODO: johbo: Avoids breaking "Repositories" chooser
239 239 .select2-container .select2-choice .select2-arrow {
240 240 display: none;
241 241 }
242 242 }
243 243
244 244 #header-inner {
245 245 &.title {
246 246 margin: 0;
247 247 }
248 248 &:before,
249 249 &:after {
250 250 content: "";
251 251 clear: both;
252 252 }
253 253 }
254 254
255 255 // Gists
256 256 #files_data {
257 257 clear: both; //for firefox
258 258 }
259 259 #gistid {
260 260 margin-right: @padding;
261 261 }
262 262
263 263 // Global Settings Editor
264 264 .textarea.editor {
265 265 float: left;
266 266 position: relative;
267 267 max-width: @texteditor-width;
268 268
269 269 select {
270 270 position: absolute;
271 271 top:10px;
272 272 right:0;
273 273 }
274 274
275 275 .CodeMirror {
276 276 margin: 0;
277 277 }
278 278
279 279 .help-block {
280 280 margin: 0 0 @padding;
281 281 padding:.5em;
282 282 background-color: @grey6;
283 283 }
284 284 }
285 285
286 286 ul.auth_plugins {
287 287 margin: @padding 0 @padding @legend-width;
288 288 padding: 0;
289 289
290 290 li {
291 291 margin-bottom: @padding;
292 292 line-height: 1em;
293 293 list-style-type: none;
294 294
295 295 .auth_buttons .btn {
296 296 margin-right: @padding;
297 297 }
298 298
299 299 &:before { content: none; }
300 300 }
301 301 }
302 302
303 303
304 304 // My Account PR list
305 305
306 306 #show_closed {
307 307 margin: 0 1em 0 0;
308 308 }
309 309
310 310 .pullrequestlist {
311 311 .closed {
312 312 background-color: @grey6;
313 313 }
314 314 .td-status {
315 315 padding-left: .5em;
316 316 }
317 317 .log-container .truncate {
318 318 height: 2.75em;
319 319 white-space: pre-line;
320 320 }
321 321 table.rctable .user {
322 322 padding-left: 0;
323 323 }
324 324 table.rctable {
325 325 td.td-description,
326 326 .rc-user {
327 327 min-width: auto;
328 328 }
329 329 }
330 330 }
331 331
332 332 // Pull Requests
333 333
334 334 .pullrequests_section_head {
335 335 display: block;
336 336 clear: both;
337 337 margin: @padding 0;
338 338 font-family: @text-bold;
339 339 }
340 340
341 341 .pr-origininfo, .pr-targetinfo {
342 342 position: relative;
343 343
344 344 .tag {
345 345 display: inline-block;
346 346 margin: 0 1em .5em 0;
347 347 }
348 348
349 349 .clone-url {
350 350 display: inline-block;
351 351 margin: 0 0 .5em 0;
352 352 padding: 0;
353 353 line-height: 1.2em;
354 354 }
355 355 }
356 356
357 357 .pr-pullinfo {
358 358 clear: both;
359 359 margin: .5em 0;
360 360 }
361 361
362 362 #pr-title-input {
363 363 width: 72%;
364 364 font-size: 1em;
365 365 font-family: @text-bold;
366 366 margin: 0;
367 367 padding: 0 0 0 @padding/4;
368 368 line-height: 1.7em;
369 369 color: @text-color;
370 370 letter-spacing: .02em;
371 371 }
372 372
373 373 #pullrequest_title {
374 374 width: 100%;
375 375 box-sizing: border-box;
376 376 }
377 377
378 378 #pr_open_message {
379 379 border: @border-thickness solid #fff;
380 380 border-radius: @border-radius;
381 381 padding: @padding-large-vertical @padding-large-vertical @padding-large-vertical 0;
382 382 text-align: right;
383 383 overflow: hidden;
384 384 }
385 385
386 386 .pr-submit-button {
387 387 float: right;
388 388 margin: 0 0 0 5px;
389 389 }
390 390
391 391 .pr-spacing-container {
392 392 padding: 20px;
393 393 clear: both
394 394 }
395 395
396 396 #pr-description-input {
397 397 margin-bottom: 0;
398 398 }
399 399
400 400 .pr-description-label {
401 401 vertical-align: top;
402 402 }
403 403
404 404 .perms_section_head {
405 405 min-width: 625px;
406 406
407 407 h2 {
408 408 margin-bottom: 0;
409 409 }
410 410
411 411 .label-checkbox {
412 412 float: left;
413 413 }
414 414
415 415 &.field {
416 416 margin: @space 0 @padding;
417 417 }
418 418
419 419 &:first-child.field {
420 420 margin-top: 0;
421 421
422 422 .label {
423 423 margin-top: 0;
424 424 padding-top: 0;
425 425 }
426 426
427 427 .radios {
428 428 padding-top: 0;
429 429 }
430 430 }
431 431
432 432 .radios {
433 433 float: right;
434 434 position: relative;
435 435 width: 405px;
436 436 }
437 437 }
438 438
439 439 //--- MODULES ------------------//
440 440
441 441
442 442 // Server Announcement
443 443 #server-announcement {
444 444 width: 95%;
445 445 margin: @padding auto;
446 446 padding: @padding;
447 447 border-width: 2px;
448 448 border-style: solid;
449 449 .border-radius(2px);
450 450 font-family: @text-bold;
451 451
452 452 &.info { border-color: @alert4; background-color: @alert4-inner; }
453 453 &.warning { border-color: @alert3; background-color: @alert3-inner; }
454 454 &.error { border-color: @alert2; background-color: @alert2-inner; }
455 455 &.success { border-color: @alert1; background-color: @alert1-inner; }
456 456 &.neutral { border-color: @grey3; background-color: @grey6; }
457 457 }
458 458
459 459 // Fixed Sidebar Column
460 460 .sidebar-col-wrapper {
461 461 padding-left: @sidebar-all-width;
462 462
463 463 .sidebar {
464 464 width: @sidebar-width;
465 465 margin-left: -@sidebar-all-width;
466 466 }
467 467 }
468 468
469 469 .sidebar-col-wrapper.scw-small {
470 470 padding-left: @sidebar-small-all-width;
471 471
472 472 .sidebar {
473 473 width: @sidebar-small-width;
474 474 margin-left: -@sidebar-small-all-width;
475 475 }
476 476 }
477 477
478 478
479 479 // FOOTER
480 480 #footer {
481 481 padding: 0;
482 482 text-align: center;
483 483 vertical-align: middle;
484 484 color: @grey2;
485 485 background-color: @grey6;
486 486
487 487 p {
488 488 margin: 0;
489 489 padding: 1em;
490 490 line-height: 1em;
491 491 }
492 492
493 493 .server-instance { //server instance
494 494 display: none;
495 495 }
496 496
497 497 .title {
498 498 float: none;
499 499 margin: 0 auto;
500 500 }
501 501 }
502 502
503 503 button.close {
504 504 padding: 0;
505 505 cursor: pointer;
506 506 background: transparent;
507 507 border: 0;
508 508 .box-shadow(none);
509 509 -webkit-appearance: none;
510 510 }
511 511
512 512 .close {
513 513 float: right;
514 514 font-size: 21px;
515 515 font-family: @text-bootstrap;
516 516 line-height: 1em;
517 517 font-weight: bold;
518 518 color: @grey2;
519 519
520 520 &:hover,
521 521 &:focus {
522 522 color: @grey1;
523 523 text-decoration: none;
524 524 cursor: pointer;
525 525 }
526 526 }
527 527
528 528 // GRID
529 529 .sorting,
530 530 .sorting_desc,
531 531 .sorting_asc {
532 532 cursor: pointer;
533 533 }
534 534 .sorting_desc:after {
535 535 content: "\00A0\25B2";
536 536 font-size: .75em;
537 537 }
538 538 .sorting_asc:after {
539 539 content: "\00A0\25BC";
540 540 font-size: .68em;
541 541 }
542 542
543 543
544 544 .user_auth_tokens {
545 545
546 546 &.truncate {
547 547 white-space: nowrap;
548 548 overflow: hidden;
549 549 text-overflow: ellipsis;
550 550 }
551 551
552 552 .fields .field .input {
553 553 margin: 0;
554 554 }
555 555
556 556 input#description {
557 557 width: 100px;
558 558 margin: 0;
559 559 }
560 560
561 561 .drop-menu {
562 562 // TODO: johbo: Remove this, should work out of the box when
563 563 // having multiple inputs inline
564 564 margin: 0 0 0 5px;
565 565 }
566 566 }
567 567 #user_list_table {
568 568 .closed {
569 569 background-color: @grey6;
570 570 }
571 571 }
572 572
573 573
574 574 input {
575 575 &.disabled {
576 576 opacity: .5;
577 577 }
578 578 }
579 579
580 580 // remove extra padding in firefox
581 581 input::-moz-focus-inner { border:0; padding:0 }
582 582
583 583 .adjacent input {
584 584 margin-bottom: @padding;
585 585 }
586 586
587 587 .permissions_boxes {
588 588 display: block;
589 589 }
590 590
591 591 //TODO: lisa: this should be in tables
592 592 .show_more_col {
593 593 width: 20px;
594 594 }
595 595
596 596 //FORMS
597 597
598 598 .medium-inline,
599 599 input#description.medium-inline {
600 600 display: inline;
601 601 width: @medium-inline-input-width;
602 602 min-width: 100px;
603 603 }
604 604
605 605 select {
606 606 //reset
607 607 -webkit-appearance: none;
608 608 -moz-appearance: none;
609 609
610 610 display: inline-block;
611 611 height: 28px;
612 612 width: auto;
613 613 margin: 0 @padding @padding 0;
614 614 padding: 0 18px 0 8px;
615 615 line-height:1em;
616 616 font-size: @basefontsize;
617 617 border: @border-thickness solid @rcblue;
618 618 background:white url("../images/dt-arrow-dn.png") no-repeat 100% 50%;
619 619 color: @rcblue;
620 620
621 621 &:after {
622 622 content: "\00A0\25BE";
623 623 }
624 624
625 625 &:focus {
626 626 outline: none;
627 627 }
628 628 }
629 629
630 630 option {
631 631 &:focus {
632 632 outline: none;
633 633 }
634 634 }
635 635
636 636 input,
637 637 textarea {
638 638 padding: @input-padding;
639 639 border: @input-border-thickness solid @border-highlight-color;
640 640 .border-radius (@border-radius);
641 641 font-family: @text-light;
642 642 font-size: @basefontsize;
643 643
644 644 &.input-sm {
645 645 padding: 5px;
646 646 }
647 647
648 648 &#description {
649 649 min-width: @input-description-minwidth;
650 650 min-height: 1em;
651 651 padding: 10px;
652 652 }
653 653 }
654 654
655 655 .field-sm {
656 656 input,
657 657 textarea {
658 658 padding: 5px;
659 659 }
660 660 }
661 661
662 662 textarea {
663 663 display: block;
664 664 clear: both;
665 665 width: 100%;
666 666 min-height: 100px;
667 667 margin-bottom: @padding;
668 668 .box-sizing(border-box);
669 669 overflow: auto;
670 670 }
671 671
672 672 label {
673 673 font-family: @text-light;
674 674 }
675 675
676 676 // GRAVATARS
677 677 // centers gravatar on username to the right
678 678
679 679 .gravatar {
680 680 display: inline;
681 681 min-width: 16px;
682 682 min-height: 16px;
683 683 margin: -5px 0;
684 684 padding: 0;
685 685 line-height: 1em;
686 686 border: 1px solid @grey4;
687 687
688 688 &.gravatar-large {
689 689 margin: -0.5em .25em -0.5em 0;
690 690 }
691 691
692 692 & + .user {
693 693 display: inline;
694 694 margin: 0;
695 695 padding: 0 0 0 .17em;
696 696 line-height: 1em;
697 697 }
698 698 }
699 699
700 700 .user-inline-data {
701 701 display: inline-block;
702 702 float: left;
703 703 padding-left: .5em;
704 704 line-height: 1.3em;
705 705 }
706 706
707 707 .rc-user { // gravatar + user wrapper
708 708 float: left;
709 709 position: relative;
710 710 min-width: 100px;
711 711 max-width: 200px;
712 712 min-height: (@gravatar-size + @border-thickness * 2); // account for border
713 713 display: block;
714 714 padding: 0 0 0 (@gravatar-size + @basefontsize/2 + @border-thickness * 2);
715 715
716 716
717 717 .gravatar {
718 718 display: block;
719 719 position: absolute;
720 720 top: 0;
721 721 left: 0;
722 722 min-width: @gravatar-size;
723 723 min-height: @gravatar-size;
724 724 margin: 0;
725 725 }
726 726
727 727 .user {
728 728 display: block;
729 729 max-width: 175px;
730 730 padding-top: 2px;
731 731 overflow: hidden;
732 732 text-overflow: ellipsis;
733 733 }
734 734 }
735 735
736 736 .gist-gravatar,
737 737 .journal_container {
738 738 .gravatar-large {
739 739 margin: 0 .5em -10px 0;
740 740 }
741 741 }
742 742
743 743
744 744 // ADMIN SETTINGS
745 745
746 746 // Tag Patterns
747 747 .tag_patterns {
748 748 .tag_input {
749 749 margin-bottom: @padding;
750 750 }
751 751 }
752 752
753 753 .locked_input {
754 754 position: relative;
755 755
756 756 input {
757 757 display: inline;
758 758 margin-top: 3px;
759 759 }
760 760
761 761 br {
762 762 display: none;
763 763 }
764 764
765 765 .error-message {
766 766 float: left;
767 767 width: 100%;
768 768 }
769 769
770 770 .lock_input_button {
771 771 display: inline;
772 772 }
773 773
774 774 .help-block {
775 775 clear: both;
776 776 }
777 777 }
778 778
779 779 // Notifications
780 780
781 781 .notifications_buttons {
782 782 margin: 0 0 @space 0;
783 783 padding: 0;
784 784
785 785 .btn {
786 786 display: inline-block;
787 787 }
788 788 }
789 789
790 790 .notification-list {
791 791
792 792 div {
793 793 display: inline-block;
794 794 vertical-align: middle;
795 795 }
796 796
797 797 .container {
798 798 display: block;
799 799 margin: 0 0 @padding 0;
800 800 }
801 801
802 802 .delete-notifications {
803 803 margin-left: @padding;
804 804 text-align: right;
805 805 cursor: pointer;
806 806 }
807 807
808 808 .read-notifications {
809 809 margin-left: @padding/2;
810 810 text-align: right;
811 811 width: 35px;
812 812 cursor: pointer;
813 813 }
814 814
815 815 .icon-minus-sign {
816 816 color: @alert2;
817 817 }
818 818
819 819 .icon-ok-sign {
820 820 color: @alert1;
821 821 }
822 822 }
823 823
824 824 .user_settings {
825 825 float: left;
826 826 clear: both;
827 827 display: block;
828 828 width: 100%;
829 829
830 830 .gravatar_box {
831 831 margin-bottom: @padding;
832 832
833 833 &:after {
834 834 content: " ";
835 835 clear: both;
836 836 width: 100%;
837 837 }
838 838 }
839 839
840 840 .fields .field {
841 841 clear: both;
842 842 }
843 843 }
844 844
845 845 .advanced_settings {
846 846 margin-bottom: @space;
847 847
848 848 .help-block {
849 849 margin-left: 0;
850 850 }
851 851
852 852 button + .help-block {
853 853 margin-top: @padding;
854 854 }
855 855 }
856 856
857 857 // admin settings radio buttons and labels
858 858 .label-2 {
859 859 float: left;
860 860 width: @label2-width;
861 861
862 862 label {
863 863 color: @grey1;
864 864 }
865 865 }
866 866 .checkboxes {
867 867 float: left;
868 868 width: @checkboxes-width;
869 869 margin-bottom: @padding;
870 870
871 871 .checkbox {
872 872 width: 100%;
873 873
874 874 label {
875 875 margin: 0;
876 876 padding: 0;
877 877 }
878 878 }
879 879
880 880 .checkbox + .checkbox {
881 881 display: inline-block;
882 882 }
883 883
884 884 label {
885 885 margin-right: 1em;
886 886 }
887 887 }
888 888
889 889 // CHANGELOG
890 890 .container_header {
891 891 float: left;
892 892 display: block;
893 893 width: 100%;
894 894 margin: @padding 0 @padding;
895 895
896 896 #filter_changelog {
897 897 float: left;
898 898 margin-right: @padding;
899 899 }
900 900
901 901 .breadcrumbs_light {
902 902 display: inline-block;
903 903 }
904 904 }
905 905
906 906 .info_box {
907 907 float: right;
908 908 }
909 909
910 910
911 911 #graph_nodes {
912 912 padding-top: 43px;
913 913 }
914 914
915 915 #graph_content{
916 916
917 917 // adjust for table headers so that graph renders properly
918 918 // #graph_nodes padding - table cell padding
919 919 padding-top: (@space - (@basefontsize * 2.4));
920 920
921 921 &.graph_full_width {
922 922 width: 100%;
923 923 max-width: 100%;
924 924 }
925 925 }
926 926
927 927 #graph {
928 928 .flag_status {
929 929 margin: 0;
930 930 }
931 931
932 932 .pagination-left {
933 933 float: left;
934 934 clear: both;
935 935 }
936 936
937 937 .log-container {
938 938 max-width: 345px;
939 939
940 940 .message{
941 941 max-width: 340px;
942 942 }
943 943 }
944 944
945 945 .graph-col-wrapper {
946 946 padding-left: 110px;
947 947
948 948 #graph_nodes {
949 949 width: 100px;
950 950 margin-left: -110px;
951 951 float: left;
952 952 clear: left;
953 953 }
954 954 }
955 955 }
956 956
957 957 #filter_changelog {
958 958 float: left;
959 959 }
960 960
961 961
962 962 //--- THEME ------------------//
963 963
964 964 #logo {
965 965 float: left;
966 966 margin: 9px 0 0 0;
967 967
968 968 .header {
969 969 background-color: transparent;
970 970 }
971 971
972 972 a {
973 973 display: inline-block;
974 974 }
975 975
976 976 img {
977 977 height:30px;
978 978 }
979 979 }
980 980
981 981 .logo-wrapper {
982 982 float:left;
983 983 }
984 984
985 985 .branding{
986 986 float: left;
987 987 padding: 9px 2px;
988 988 line-height: 1em;
989 989 font-size: @navigation-fontsize;
990 990 }
991 991
992 992 img {
993 993 border: none;
994 994 outline: none;
995 995 }
996 996 user-profile-header
997 997 label {
998 998
999 999 input[type="checkbox"] {
1000 1000 margin-right: 1em;
1001 1001 }
1002 1002 input[type="radio"] {
1003 1003 margin-right: 1em;
1004 1004 }
1005 1005 }
1006 1006
1007 1007 .flag_status {
1008 1008 margin: 2px 8px 6px 2px;
1009 1009 &.under_review {
1010 1010 .circle(5px, @alert3);
1011 1011 }
1012 1012 &.approved {
1013 1013 .circle(5px, @alert1);
1014 1014 }
1015 1015 &.rejected,
1016 1016 &.forced_closed{
1017 1017 .circle(5px, @alert2);
1018 1018 }
1019 1019 &.not_reviewed {
1020 1020 .circle(5px, @grey5);
1021 1021 }
1022 1022 }
1023 1023
1024 1024 .flag_status_comment_box {
1025 1025 margin: 5px 6px 0px 2px;
1026 1026 }
1027 1027 .test_pattern_preview {
1028 1028 margin: @space 0;
1029 1029
1030 1030 p {
1031 1031 margin-bottom: 0;
1032 1032 border-bottom: @border-thickness solid @border-default-color;
1033 1033 color: @grey3;
1034 1034 }
1035 1035
1036 1036 .btn {
1037 1037 margin-bottom: @padding;
1038 1038 }
1039 1039 }
1040 1040 #test_pattern_result {
1041 1041 display: none;
1042 1042 &:extend(pre);
1043 1043 padding: .9em;
1044 1044 color: @grey3;
1045 1045 background-color: @grey7;
1046 1046 border-right: @border-thickness solid @border-default-color;
1047 1047 border-bottom: @border-thickness solid @border-default-color;
1048 1048 border-left: @border-thickness solid @border-default-color;
1049 1049 }
1050 1050
1051 1051 #repo_vcs_settings {
1052 1052 #inherit_overlay_vcs_default {
1053 1053 display: none;
1054 1054 }
1055 1055 #inherit_overlay_vcs_custom {
1056 1056 display: custom;
1057 1057 }
1058 1058 &.inherited {
1059 1059 #inherit_overlay_vcs_default {
1060 1060 display: block;
1061 1061 }
1062 1062 #inherit_overlay_vcs_custom {
1063 1063 display: none;
1064 1064 }
1065 1065 }
1066 1066 }
1067 1067
1068 1068 .issue-tracker-link {
1069 1069 color: @rcblue;
1070 1070 }
1071 1071
1072 1072 // Issue Tracker Table Show/Hide
1073 1073 #repo_issue_tracker {
1074 1074 #inherit_overlay {
1075 1075 display: none;
1076 1076 }
1077 1077 #custom_overlay {
1078 1078 display: custom;
1079 1079 }
1080 1080 &.inherited {
1081 1081 #inherit_overlay {
1082 1082 display: block;
1083 1083 }
1084 1084 #custom_overlay {
1085 1085 display: none;
1086 1086 }
1087 1087 }
1088 1088 }
1089 1089 table.issuetracker {
1090 1090 &.readonly {
1091 1091 tr, td {
1092 1092 color: @grey3;
1093 1093 }
1094 1094 }
1095 1095 .edit {
1096 1096 display: none;
1097 1097 }
1098 1098 .editopen {
1099 1099 .edit {
1100 1100 display: inline;
1101 1101 }
1102 1102 .entry {
1103 1103 display: none;
1104 1104 }
1105 1105 }
1106 1106 tr td.td-action {
1107 1107 min-width: 117px;
1108 1108 }
1109 1109 td input {
1110 1110 max-width: none;
1111 1111 min-width: 30px;
1112 1112 width: 80%;
1113 1113 }
1114 1114 .issuetracker_pref input {
1115 1115 width: 40%;
1116 1116 }
1117 1117 input.edit_issuetracker_update {
1118 1118 margin-right: 0;
1119 1119 width: auto;
1120 1120 }
1121 1121 }
1122 1122
1123 1123 table.integrations {
1124 1124 .td-icon {
1125 1125 width: 20px;
1126 1126 .integration-icon {
1127 1127 height: 20px;
1128 1128 width: 20px;
1129 1129 }
1130 1130 }
1131 1131 }
1132 1132
1133 1133 .integrations {
1134 1134 a.integration-box {
1135 1135 color: @text-color;
1136 1136 &:hover {
1137 1137 .panel {
1138 1138 background: #fbfbfb;
1139 1139 }
1140 1140 }
1141 1141 .integration-icon {
1142 1142 width: 30px;
1143 1143 height: 30px;
1144 1144 margin-right: 20px;
1145 1145 float: left;
1146 1146 }
1147 1147
1148 1148 .panel-body {
1149 1149 padding: 10px;
1150 1150 }
1151 1151 .panel {
1152 1152 margin-bottom: 10px;
1153 1153 }
1154 1154 h2 {
1155 1155 display: inline-block;
1156 1156 margin: 0;
1157 1157 min-width: 140px;
1158 1158 }
1159 1159 }
1160 1160 }
1161 1161
1162 1162 //Permissions Settings
1163 1163 #add_perm {
1164 1164 margin: 0 0 @padding;
1165 1165 cursor: pointer;
1166 1166 }
1167 1167
1168 1168 .perm_ac {
1169 1169 input {
1170 1170 width: 95%;
1171 1171 }
1172 1172 }
1173 1173
1174 1174 .autocomplete-suggestions {
1175 1175 width: auto !important; // overrides autocomplete.js
1176 1176 margin: 0;
1177 1177 border: @border-thickness solid @rcblue;
1178 1178 border-radius: @border-radius;
1179 1179 color: @rcblue;
1180 1180 background-color: white;
1181 1181 }
1182 1182 .autocomplete-selected {
1183 1183 background: #F0F0F0;
1184 1184 }
1185 1185 .ac-container-wrap {
1186 1186 margin: 0;
1187 1187 padding: 8px;
1188 1188 border-bottom: @border-thickness solid @rclightblue;
1189 1189 list-style-type: none;
1190 1190 cursor: pointer;
1191 1191
1192 1192 &:hover {
1193 1193 background-color: @rclightblue;
1194 1194 }
1195 1195
1196 1196 img {
1197 1197 margin-right: 1em;
1198 1198 }
1199 1199
1200 1200 strong {
1201 1201 font-weight: normal;
1202 1202 }
1203 1203 }
1204 1204
1205 1205 // Settings Dropdown
1206 1206 .user-menu .container {
1207 1207 padding: 0 4px;
1208 1208 margin: 0;
1209 1209 }
1210 1210
1211 1211 .user-menu .gravatar {
1212 1212 cursor: pointer;
1213 1213 }
1214 1214
1215 1215 .codeblock {
1216 1216 margin-bottom: @padding;
1217 1217 clear: both;
1218 1218
1219 1219 .stats{
1220 1220 overflow: hidden;
1221 1221 }
1222 1222
1223 1223 .message{
1224 1224 textarea{
1225 1225 margin: 0;
1226 1226 }
1227 1227 }
1228 1228
1229 1229 .code-header {
1230 1230 .stats {
1231 1231 line-height: 2em;
1232 1232
1233 1233 .revision_id {
1234 1234 margin-left: 0;
1235 1235 }
1236 1236 .buttons {
1237 1237 padding-right: 0;
1238 1238 }
1239 1239 }
1240 1240
1241 1241 .item{
1242 1242 margin-right: 0.5em;
1243 1243 }
1244 1244 }
1245 1245
1246 1246 #editor_container{
1247 1247 position: relative;
1248 1248 margin: @padding;
1249 1249 }
1250 1250 }
1251 1251
1252 1252 #file_history_container {
1253 1253 display: none;
1254 1254 }
1255 1255
1256 1256 .file-history-inner {
1257 1257 margin-bottom: 10px;
1258 1258 }
1259 1259
1260 1260 // Pull Requests
1261 1261 .summary-details {
1262 1262 width: 72%;
1263 1263 }
1264 1264 .pr-summary {
1265 1265 border-bottom: @border-thickness solid @grey5;
1266 1266 margin-bottom: @space;
1267 1267 }
1268 1268 .reviewers-title {
1269 1269 width: 25%;
1270 1270 min-width: 200px;
1271 1271 }
1272 1272 .reviewers {
1273 1273 width: 25%;
1274 1274 min-width: 200px;
1275 1275 }
1276 1276 .reviewers ul li {
1277 1277 position: relative;
1278 1278 width: 100%;
1279 1279 margin-bottom: 8px;
1280 1280 }
1281 1281 .reviewers_member {
1282 1282 width: 100%;
1283 1283 overflow: auto;
1284 1284 }
1285 .reviewer_reason {
1286 padding-left: 20px;
1287 }
1285 1288 .reviewer_status {
1286 1289 display: inline-block;
1287 1290 vertical-align: top;
1288 1291 width: 7%;
1289 1292 min-width: 20px;
1290 1293 height: 1.2em;
1291 1294 margin-top: 3px;
1292 1295 line-height: 1em;
1293 1296 }
1294 1297
1295 1298 .reviewer_name {
1296 1299 display: inline-block;
1297 1300 max-width: 83%;
1298 1301 padding-right: 20px;
1299 1302 vertical-align: middle;
1300 1303 line-height: 1;
1301 1304
1302 1305 .rc-user {
1303 1306 min-width: 0;
1304 1307 margin: -2px 1em 0 0;
1305 1308 }
1306 1309
1307 1310 .reviewer {
1308 1311 float: left;
1309 1312 }
1310 1313
1311 1314 &.to-delete {
1312 1315 .user,
1313 1316 .reviewer {
1314 1317 text-decoration: line-through;
1315 1318 }
1316 1319 }
1317 1320 }
1318 1321
1319 1322 .reviewer_member_remove {
1320 1323 position: absolute;
1321 1324 right: 0;
1322 1325 top: 0;
1323 1326 width: 16px;
1324 1327 margin-bottom: 10px;
1325 1328 padding: 0;
1326 1329 color: black;
1327 1330 }
1328 1331 .reviewer_member_status {
1329 1332 margin-top: 5px;
1330 1333 }
1331 1334 .pr-summary #summary{
1332 1335 width: 100%;
1333 1336 }
1334 1337 .pr-summary .action_button:hover {
1335 1338 border: 0;
1336 1339 cursor: pointer;
1337 1340 }
1338 1341 .pr-details-title {
1339 1342 padding-bottom: 8px;
1340 1343 border-bottom: @border-thickness solid @grey5;
1341 1344 .action_button {
1342 1345 color: @rcblue;
1343 1346 }
1344 1347 }
1345 1348 .pr-details-content {
1346 1349 margin-top: @textmargin;
1347 1350 margin-bottom: @textmargin;
1348 1351 }
1349 1352 .pr-description {
1350 1353 white-space:pre-wrap;
1351 1354 }
1352 1355 .group_members {
1353 1356 margin-top: 0;
1354 1357 padding: 0;
1355 1358 list-style: outside none none;
1356 1359 }
1357 1360 .reviewer_ac .ac-input {
1358 1361 width: 92%;
1359 1362 margin-bottom: 1em;
1360 1363 }
1361 1364 #update_commits {
1362 1365 float: right;
1363 1366 }
1364 1367 .compare_view_commits tr{
1365 1368 height: 20px;
1366 1369 }
1367 1370 .compare_view_commits td {
1368 1371 vertical-align: top;
1369 1372 padding-top: 10px;
1370 1373 }
1371 1374 .compare_view_commits .author {
1372 1375 margin-left: 5px;
1373 1376 }
1374 1377
1375 1378 .compare_view_files {
1376 1379 width: 100%;
1377 1380
1378 1381 td {
1379 1382 vertical-align: middle;
1380 1383 }
1381 1384 }
1382 1385
1383 1386 .compare_view_filepath {
1384 1387 color: @grey1;
1385 1388 }
1386 1389
1387 1390 .show_more {
1388 1391 display: inline-block;
1389 1392 position: relative;
1390 1393 vertical-align: middle;
1391 1394 width: 4px;
1392 1395 height: @basefontsize;
1393 1396
1394 1397 &:after {
1395 1398 content: "\00A0\25BE";
1396 1399 display: inline-block;
1397 1400 width:10px;
1398 1401 line-height: 5px;
1399 1402 font-size: 12px;
1400 1403 cursor: pointer;
1401 1404 }
1402 1405 }
1403 1406
1404 1407 .journal_more .show_more {
1405 1408 display: inline;
1406 1409
1407 1410 &:after {
1408 1411 content: none;
1409 1412 }
1410 1413 }
1411 1414
1412 1415 .open .show_more:after,
1413 1416 .select2-dropdown-open .show_more:after {
1414 1417 .rotate(180deg);
1415 1418 margin-left: 4px;
1416 1419 }
1417 1420
1418 1421
1419 1422 .compare_view_commits .collapse_commit:after {
1420 1423 cursor: pointer;
1421 1424 content: "\00A0\25B4";
1422 1425 margin-left: -3px;
1423 1426 font-size: 17px;
1424 1427 color: @grey4;
1425 1428 }
1426 1429
1427 1430 .diff_links {
1428 1431 margin-left: 8px;
1429 1432 }
1430 1433
1431 1434 p.ancestor {
1432 1435 margin: @padding 0;
1433 1436 }
1434 1437
1435 1438 .cs_icon_td input[type="checkbox"] {
1436 1439 display: none;
1437 1440 }
1438 1441
1439 1442 .cs_icon_td .expand_file_icon:after {
1440 1443 cursor: pointer;
1441 1444 content: "\00A0\25B6";
1442 1445 font-size: 12px;
1443 1446 color: @grey4;
1444 1447 }
1445 1448
1446 1449 .cs_icon_td .collapse_file_icon:after {
1447 1450 cursor: pointer;
1448 1451 content: "\00A0\25BC";
1449 1452 font-size: 12px;
1450 1453 color: @grey4;
1451 1454 }
1452 1455
1453 1456 /*new binary
1454 1457 NEW_FILENODE = 1
1455 1458 DEL_FILENODE = 2
1456 1459 MOD_FILENODE = 3
1457 1460 RENAMED_FILENODE = 4
1458 1461 COPIED_FILENODE = 5
1459 1462 CHMOD_FILENODE = 6
1460 1463 BIN_FILENODE = 7
1461 1464 */
1462 1465 .cs_files_expand {
1463 1466 font-size: @basefontsize + 5px;
1464 1467 line-height: 1.8em;
1465 1468 float: right;
1466 1469 }
1467 1470
1468 1471 .cs_files_expand span{
1469 1472 color: @rcblue;
1470 1473 cursor: pointer;
1471 1474 }
1472 1475 .cs_files {
1473 1476 clear: both;
1474 1477 padding-bottom: @padding;
1475 1478
1476 1479 .cur_cs {
1477 1480 margin: 10px 2px;
1478 1481 font-weight: bold;
1479 1482 }
1480 1483
1481 1484 .node {
1482 1485 float: left;
1483 1486 }
1484 1487
1485 1488 .changes {
1486 1489 float: right;
1487 1490 color: white;
1488 1491 font-size: @basefontsize - 4px;
1489 1492 margin-top: 4px;
1490 1493 opacity: 0.6;
1491 1494 filter: Alpha(opacity=60); /* IE8 and earlier */
1492 1495
1493 1496 .added {
1494 1497 background-color: @alert1;
1495 1498 float: left;
1496 1499 text-align: center;
1497 1500 }
1498 1501
1499 1502 .deleted {
1500 1503 background-color: @alert2;
1501 1504 float: left;
1502 1505 text-align: center;
1503 1506 }
1504 1507
1505 1508 .bin {
1506 1509 background-color: @alert1;
1507 1510 text-align: center;
1508 1511 }
1509 1512
1510 1513 /*new binary*/
1511 1514 .bin.bin1 {
1512 1515 background-color: @alert1;
1513 1516 text-align: center;
1514 1517 }
1515 1518
1516 1519 /*deleted binary*/
1517 1520 .bin.bin2 {
1518 1521 background-color: @alert2;
1519 1522 text-align: center;
1520 1523 }
1521 1524
1522 1525 /*mod binary*/
1523 1526 .bin.bin3 {
1524 1527 background-color: @grey2;
1525 1528 text-align: center;
1526 1529 }
1527 1530
1528 1531 /*rename file*/
1529 1532 .bin.bin4 {
1530 1533 background-color: @alert4;
1531 1534 text-align: center;
1532 1535 }
1533 1536
1534 1537 /*copied file*/
1535 1538 .bin.bin5 {
1536 1539 background-color: @alert4;
1537 1540 text-align: center;
1538 1541 }
1539 1542
1540 1543 /*chmod file*/
1541 1544 .bin.bin6 {
1542 1545 background-color: @grey2;
1543 1546 text-align: center;
1544 1547 }
1545 1548 }
1546 1549 }
1547 1550
1548 1551 .cs_files .cs_added, .cs_files .cs_A,
1549 1552 .cs_files .cs_added, .cs_files .cs_M,
1550 1553 .cs_files .cs_added, .cs_files .cs_D {
1551 1554 height: 16px;
1552 1555 padding-right: 10px;
1553 1556 margin-top: 7px;
1554 1557 text-align: left;
1555 1558 }
1556 1559
1557 1560 .cs_icon_td {
1558 1561 min-width: 16px;
1559 1562 width: 16px;
1560 1563 }
1561 1564
1562 1565 .pull-request-merge {
1563 1566 padding: 10px 0;
1564 1567 margin-top: 10px;
1565 1568 margin-bottom: 20px;
1566 1569 }
1567 1570
1568 1571 .pull-request-merge .pull-request-wrap {
1569 1572 height: 25px;
1570 1573 padding: 5px 0;
1571 1574 }
1572 1575
1573 1576 .pull-request-merge span {
1574 1577 margin-right: 10px;
1575 1578 }
1576 1579 #close_pull_request {
1577 1580 margin-right: 0px;
1578 1581 }
1579 1582
1580 1583 .empty_data {
1581 1584 color: @grey4;
1582 1585 }
1583 1586
1584 1587 #changeset_compare_view_content {
1585 1588 margin-bottom: @space;
1586 1589 clear: both;
1587 1590 width: 100%;
1588 1591 box-sizing: border-box;
1589 1592 .border-radius(@border-radius);
1590 1593
1591 1594 .help-block {
1592 1595 margin: @padding 0;
1593 1596 color: @text-color;
1594 1597 }
1595 1598
1596 1599 .empty_data {
1597 1600 margin: @padding 0;
1598 1601 }
1599 1602
1600 1603 .alert {
1601 1604 margin-bottom: @space;
1602 1605 }
1603 1606 }
1604 1607
1605 1608 .table_disp {
1606 1609 .status {
1607 1610 width: auto;
1608 1611
1609 1612 .flag_status {
1610 1613 float: left;
1611 1614 }
1612 1615 }
1613 1616 }
1614 1617
1615 1618 .status_box_menu {
1616 1619 margin: 0;
1617 1620 }
1618 1621
1619 1622 .notification-table{
1620 1623 margin-bottom: @space;
1621 1624 display: table;
1622 1625 width: 100%;
1623 1626
1624 1627 .container{
1625 1628 display: table-row;
1626 1629
1627 1630 .notification-header{
1628 1631 border-bottom: @border-thickness solid @border-default-color;
1629 1632 }
1630 1633
1631 1634 .notification-subject{
1632 1635 display: table-cell;
1633 1636 }
1634 1637 }
1635 1638 }
1636 1639
1637 1640 // Notifications
1638 1641 .notification-header{
1639 1642 display: table;
1640 1643 width: 100%;
1641 1644 padding: floor(@basefontsize/2) 0;
1642 1645 line-height: 1em;
1643 1646
1644 1647 .desc, .delete-notifications, .read-notifications{
1645 1648 display: table-cell;
1646 1649 text-align: left;
1647 1650 }
1648 1651
1649 1652 .desc{
1650 1653 width: 1163px;
1651 1654 }
1652 1655
1653 1656 .delete-notifications, .read-notifications{
1654 1657 width: 35px;
1655 1658 min-width: 35px; //fixes when only one button is displayed
1656 1659 }
1657 1660 }
1658 1661
1659 1662 .notification-body {
1660 1663 .markdown-block,
1661 1664 .rst-block {
1662 1665 padding: @padding 0;
1663 1666 }
1664 1667
1665 1668 .notification-subject {
1666 1669 padding: @textmargin 0;
1667 1670 border-bottom: @border-thickness solid @border-default-color;
1668 1671 }
1669 1672 }
1670 1673
1671 1674
1672 1675 .notifications_buttons{
1673 1676 float: right;
1674 1677 }
1675 1678
1676 1679 #notification-status{
1677 1680 display: inline;
1678 1681 }
1679 1682
1680 1683 // Repositories
1681 1684
1682 1685 #summary.fields{
1683 1686 display: table;
1684 1687
1685 1688 .field{
1686 1689 display: table-row;
1687 1690
1688 1691 .label-summary{
1689 1692 display: table-cell;
1690 1693 min-width: @label-summary-minwidth;
1691 1694 padding-top: @padding/2;
1692 1695 padding-bottom: @padding/2;
1693 1696 padding-right: @padding/2;
1694 1697 }
1695 1698
1696 1699 .input{
1697 1700 display: table-cell;
1698 1701 padding: @padding/2;
1699 1702
1700 1703 input{
1701 1704 min-width: 29em;
1702 1705 padding: @padding/4;
1703 1706 }
1704 1707 }
1705 1708 .statistics, .downloads{
1706 1709 .disabled{
1707 1710 color: @grey4;
1708 1711 }
1709 1712 }
1710 1713 }
1711 1714 }
1712 1715
1713 1716 #summary{
1714 1717 width: 70%;
1715 1718 }
1716 1719
1717 1720
1718 1721 // Journal
1719 1722 .journal.title {
1720 1723 h5 {
1721 1724 float: left;
1722 1725 margin: 0;
1723 1726 width: 70%;
1724 1727 }
1725 1728
1726 1729 ul {
1727 1730 float: right;
1728 1731 display: inline-block;
1729 1732 margin: 0;
1730 1733 width: 30%;
1731 1734 text-align: right;
1732 1735
1733 1736 li {
1734 1737 display: inline;
1735 1738 font-size: @journal-fontsize;
1736 1739 line-height: 1em;
1737 1740
1738 1741 &:before { content: none; }
1739 1742 }
1740 1743 }
1741 1744 }
1742 1745
1743 1746 .filterexample {
1744 1747 position: absolute;
1745 1748 top: 95px;
1746 1749 left: @contentpadding;
1747 1750 color: @rcblue;
1748 1751 font-size: 11px;
1749 1752 font-family: @text-regular;
1750 1753 cursor: help;
1751 1754
1752 1755 &:hover {
1753 1756 color: @rcdarkblue;
1754 1757 }
1755 1758
1756 1759 @media (max-width:768px) {
1757 1760 position: relative;
1758 1761 top: auto;
1759 1762 left: auto;
1760 1763 display: block;
1761 1764 }
1762 1765 }
1763 1766
1764 1767
1765 1768 #journal{
1766 1769 margin-bottom: @space;
1767 1770
1768 1771 .journal_day{
1769 1772 margin-bottom: @textmargin/2;
1770 1773 padding-bottom: @textmargin/2;
1771 1774 font-size: @journal-fontsize;
1772 1775 border-bottom: @border-thickness solid @border-default-color;
1773 1776 }
1774 1777
1775 1778 .journal_container{
1776 1779 margin-bottom: @space;
1777 1780
1778 1781 .journal_user{
1779 1782 display: inline-block;
1780 1783 }
1781 1784 .journal_action_container{
1782 1785 display: block;
1783 1786 margin-top: @textmargin;
1784 1787
1785 1788 div{
1786 1789 display: inline;
1787 1790 }
1788 1791
1789 1792 div.journal_action_params{
1790 1793 display: block;
1791 1794 }
1792 1795
1793 1796 div.journal_repo:after{
1794 1797 content: "\A";
1795 1798 white-space: pre;
1796 1799 }
1797 1800
1798 1801 div.date{
1799 1802 display: block;
1800 1803 margin-bottom: @textmargin;
1801 1804 }
1802 1805 }
1803 1806 }
1804 1807 }
1805 1808
1806 1809 // Files
1807 1810 .edit-file-title {
1808 1811 border-bottom: @border-thickness solid @border-default-color;
1809 1812
1810 1813 .breadcrumbs {
1811 1814 margin-bottom: 0;
1812 1815 }
1813 1816 }
1814 1817
1815 1818 .edit-file-fieldset {
1816 1819 margin-top: @sidebarpadding;
1817 1820
1818 1821 .fieldset {
1819 1822 .left-label {
1820 1823 width: 13%;
1821 1824 }
1822 1825 .right-content {
1823 1826 width: 87%;
1824 1827 max-width: 100%;
1825 1828 }
1826 1829 .filename-label {
1827 1830 margin-top: 13px;
1828 1831 }
1829 1832 .commit-message-label {
1830 1833 margin-top: 4px;
1831 1834 }
1832 1835 .file-upload-input {
1833 1836 input {
1834 1837 display: none;
1835 1838 }
1836 1839 }
1837 1840 p {
1838 1841 margin-top: 5px;
1839 1842 }
1840 1843
1841 1844 }
1842 1845 .custom-path-link {
1843 1846 margin-left: 5px;
1844 1847 }
1845 1848 #commit {
1846 1849 resize: vertical;
1847 1850 }
1848 1851 }
1849 1852
1850 1853 .delete-file-preview {
1851 1854 max-height: 250px;
1852 1855 }
1853 1856
1854 1857 .new-file,
1855 1858 #filter_activate,
1856 1859 #filter_deactivate {
1857 1860 float: left;
1858 1861 margin: 0 0 0 15px;
1859 1862 }
1860 1863
1861 1864 h3.files_location{
1862 1865 line-height: 2.4em;
1863 1866 }
1864 1867
1865 1868 .browser-nav {
1866 1869 display: table;
1867 1870 margin-bottom: @space;
1868 1871
1869 1872
1870 1873 .info_box {
1871 1874 display: inline-table;
1872 1875 height: 2.5em;
1873 1876
1874 1877 .browser-cur-rev, .info_box_elem {
1875 1878 display: table-cell;
1876 1879 vertical-align: middle;
1877 1880 }
1878 1881
1879 1882 .info_box_elem {
1880 1883 border-top: @border-thickness solid @rcblue;
1881 1884 border-bottom: @border-thickness solid @rcblue;
1882 1885
1883 1886 #at_rev, a {
1884 1887 padding: 0.6em 0.9em;
1885 1888 margin: 0;
1886 1889 .box-shadow(none);
1887 1890 border: 0;
1888 1891 height: 12px;
1889 1892 }
1890 1893
1891 1894 input#at_rev {
1892 1895 max-width: 50px;
1893 1896 text-align: right;
1894 1897 }
1895 1898
1896 1899 &.previous {
1897 1900 border: @border-thickness solid @rcblue;
1898 1901 .disabled {
1899 1902 color: @grey4;
1900 1903 cursor: not-allowed;
1901 1904 }
1902 1905 }
1903 1906
1904 1907 &.next {
1905 1908 border: @border-thickness solid @rcblue;
1906 1909 .disabled {
1907 1910 color: @grey4;
1908 1911 cursor: not-allowed;
1909 1912 }
1910 1913 }
1911 1914 }
1912 1915
1913 1916 .browser-cur-rev {
1914 1917
1915 1918 span{
1916 1919 margin: 0;
1917 1920 color: @rcblue;
1918 1921 height: 12px;
1919 1922 display: inline-block;
1920 1923 padding: 0.7em 1em ;
1921 1924 border: @border-thickness solid @rcblue;
1922 1925 margin-right: @padding;
1923 1926 }
1924 1927 }
1925 1928 }
1926 1929
1927 1930 .search_activate {
1928 1931 display: table-cell;
1929 1932 vertical-align: middle;
1930 1933
1931 1934 input, label{
1932 1935 margin: 0;
1933 1936 padding: 0;
1934 1937 }
1935 1938
1936 1939 input{
1937 1940 margin-left: @textmargin;
1938 1941 }
1939 1942
1940 1943 }
1941 1944 }
1942 1945
1943 1946 .browser-cur-rev{
1944 1947 margin-bottom: @textmargin;
1945 1948 }
1946 1949
1947 1950 #node_filter_box_loading{
1948 1951 .info_text;
1949 1952 }
1950 1953
1951 1954 .browser-search {
1952 1955 margin: -25px 0px 5px 0px;
1953 1956 }
1954 1957
1955 1958 .node-filter {
1956 1959 font-size: @repo-title-fontsize;
1957 1960 padding: 4px 0px 0px 0px;
1958 1961
1959 1962 .node-filter-path {
1960 1963 float: left;
1961 1964 color: @grey4;
1962 1965 }
1963 1966 .node-filter-input {
1964 1967 float: left;
1965 1968 margin: -2px 0px 0px 2px;
1966 1969 input {
1967 1970 padding: 2px;
1968 1971 border: none;
1969 1972 font-size: @repo-title-fontsize;
1970 1973 }
1971 1974 }
1972 1975 }
1973 1976
1974 1977
1975 1978 .browser-result{
1976 1979 td a{
1977 1980 margin-left: 0.5em;
1978 1981 display: inline-block;
1979 1982
1980 1983 em{
1981 1984 font-family: @text-bold;
1982 1985 }
1983 1986 }
1984 1987 }
1985 1988
1986 1989 .browser-highlight{
1987 1990 background-color: @grey5-alpha;
1988 1991 }
1989 1992
1990 1993
1991 1994 // Search
1992 1995
1993 1996 .search-form{
1994 1997 #q {
1995 1998 width: @search-form-width;
1996 1999 }
1997 2000 .fields{
1998 2001 margin: 0 0 @space;
1999 2002 }
2000 2003
2001 2004 label{
2002 2005 display: inline-block;
2003 2006 margin-right: @textmargin;
2004 2007 padding-top: 0.25em;
2005 2008 }
2006 2009
2007 2010
2008 2011 .results{
2009 2012 clear: both;
2010 2013 margin: 0 0 @padding;
2011 2014 }
2012 2015 }
2013 2016
2014 2017 div.search-feedback-items {
2015 2018 display: inline-block;
2016 2019 padding:0px 0px 0px 96px;
2017 2020 }
2018 2021
2019 2022 div.search-code-body {
2020 2023 background-color: #ffffff; padding: 5px 0 5px 10px;
2021 2024 pre {
2022 2025 .match { background-color: #faffa6;}
2023 2026 .break { display: block; width: 100%; background-color: #DDE7EF; color: #747474; }
2024 2027 }
2025 2028 }
2026 2029
2027 2030 .expand_commit.search {
2028 2031 .show_more.open {
2029 2032 height: auto;
2030 2033 max-height: none;
2031 2034 }
2032 2035 }
2033 2036
2034 2037 .search-results {
2035 2038
2036 2039 h2 {
2037 2040 margin-bottom: 0;
2038 2041 }
2039 2042 .codeblock {
2040 2043 border: none;
2041 2044 background: transparent;
2042 2045 }
2043 2046
2044 2047 .codeblock-header {
2045 2048 border: none;
2046 2049 background: transparent;
2047 2050 }
2048 2051
2049 2052 .code-body {
2050 2053 border: @border-thickness solid @border-default-color;
2051 2054 .border-radius(@border-radius);
2052 2055 }
2053 2056
2054 2057 .td-commit {
2055 2058 &:extend(pre);
2056 2059 border-bottom: @border-thickness solid @border-default-color;
2057 2060 }
2058 2061
2059 2062 .message {
2060 2063 height: auto;
2061 2064 max-width: 350px;
2062 2065 white-space: normal;
2063 2066 text-overflow: initial;
2064 2067 overflow: visible;
2065 2068
2066 2069 .match { background-color: #faffa6;}
2067 2070 .break { background-color: #DDE7EF; width: 100%; color: #747474; display: block; }
2068 2071 }
2069 2072
2070 2073 }
2071 2074
2072 2075 table.rctable td.td-search-results div {
2073 2076 max-width: 100%;
2074 2077 }
2075 2078
2076 2079 #tip-box, .tip-box{
2077 2080 padding: @menupadding/2;
2078 2081 display: block;
2079 2082 border: @border-thickness solid @border-highlight-color;
2080 2083 .border-radius(@border-radius);
2081 2084 background-color: white;
2082 2085 z-index: 99;
2083 2086 white-space: pre-wrap;
2084 2087 }
2085 2088
2086 2089 #linktt {
2087 2090 width: 79px;
2088 2091 }
2089 2092
2090 2093 #help_kb .modal-content{
2091 2094 max-width: 750px;
2092 2095 margin: 10% auto;
2093 2096
2094 2097 table{
2095 2098 td,th{
2096 2099 border-bottom: none;
2097 2100 line-height: 2.5em;
2098 2101 }
2099 2102 th{
2100 2103 padding-bottom: @textmargin/2;
2101 2104 }
2102 2105 td.keys{
2103 2106 text-align: center;
2104 2107 }
2105 2108 }
2106 2109
2107 2110 .block-left{
2108 2111 width: 45%;
2109 2112 margin-right: 5%;
2110 2113 }
2111 2114 .modal-footer{
2112 2115 clear: both;
2113 2116 }
2114 2117 .key.tag{
2115 2118 padding: 0.5em;
2116 2119 background-color: @rcblue;
2117 2120 color: white;
2118 2121 border-color: @rcblue;
2119 2122 .box-shadow(none);
2120 2123 }
2121 2124 }
2122 2125
2123 2126
2124 2127
2125 2128 //--- IMPORTS FOR REFACTORED STYLES ------------------//
2126 2129
2127 2130 @import 'statistics-graph';
2128 2131 @import 'tables';
2129 2132 @import 'forms';
2130 2133 @import 'diff';
2131 2134 @import 'summary';
2132 2135 @import 'navigation';
2133 2136
2134 2137 //--- SHOW/HIDE SECTIONS --//
2135 2138
2136 2139 .btn-collapse {
2137 2140 float: right;
2138 2141 text-align: right;
2139 2142 font-family: @text-light;
2140 2143 font-size: @basefontsize;
2141 2144 cursor: pointer;
2142 2145 border: none;
2143 2146 color: @rcblue;
2144 2147 }
2145 2148
2146 2149 table.rctable,
2147 2150 table.dataTable {
2148 2151 .btn-collapse {
2149 2152 float: right;
2150 2153 text-align: right;
2151 2154 }
2152 2155 }
2153 2156
2154 2157
2155 2158 // TODO: johbo: Fix for IE10, this avoids that we see a border
2156 2159 // and padding around checkboxes and radio boxes. Move to the right place,
2157 2160 // or better: Remove this once we did the form refactoring.
2158 2161 input[type=checkbox],
2159 2162 input[type=radio] {
2160 2163 padding: 0;
2161 2164 border: none;
2162 2165 }
2163 2166
2164 2167 .toggle-ajax-spinner{
2165 2168 height: 16px;
2166 2169 width: 16px;
2167 2170 }
@@ -1,214 +1,217 b''
1 1 // # Copyright (C) 2010-2016 RhodeCode GmbH
2 2 // #
3 3 // # This program is free software: you can redistribute it and/or modify
4 4 // # it under the terms of the GNU Affero General Public License, version 3
5 5 // # (only), as published by the Free Software Foundation.
6 6 // #
7 7 // # This program is distributed in the hope that it will be useful,
8 8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 // # GNU General Public License for more details.
11 11 // #
12 12 // # You should have received a copy of the GNU Affero General Public License
13 13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 // #
15 15 // # This program is dual-licensed. If you wish to learn more about the
16 16 // # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 /**
20 20 * Pull request reviewers
21 21 */
22 22 var removeReviewMember = function(reviewer_id, mark_delete){
23 23 var reviewer = $('#reviewer_{0}'.format(reviewer_id));
24 24
25 25 if(typeof(mark_delete) === undefined){
26 26 mark_delete = false;
27 27 }
28 28
29 29 if(mark_delete === true){
30 30 if (reviewer){
31 31 // mark as to-remove
32 32 var obj = $('#reviewer_{0}_name'.format(reviewer_id));
33 33 obj.addClass('to-delete');
34 34 // now delete the input
35 $('#reviewer_{0}_input'.format(reviewer_id)).remove();
35 $('#reviewer_{0} input'.format(reviewer_id)).remove();
36 36 }
37 37 }
38 38 else{
39 39 $('#reviewer_{0}'.format(reviewer_id)).remove();
40 40 }
41 41 };
42 42
43 43 var addReviewMember = function(id, fname, lname, nname, gravatar_link, reasons) {
44 44 var members = $('#review_members').get(0);
45 45 var reasons_html = '';
46 var reasons_inputs = '';
47 var reasons = reasons || [];
46 48 if (reasons) {
47 49 for (var i = 0; i < reasons.length; i++) {
48 reasons_html += '<div class="reviewer_reason">- {0}</div>'.format(
49 reasons[i]
50 );
50 reasons_html += '<div class="reviewer_reason">- {0}</div>'.format(reasons[i]);
51 reasons_inputs += '<input type="hidden" name="reason" value="' + escapeHtml(reasons[i]) + '">';
51 52 }
52 53 }
53 54 var tmpl = '<li id="reviewer_{2}">'+
55 '<input type="hidden" name="__start__" value="reviewer:mapping">'+
54 56 '<div class="reviewer_status">'+
55 57 '<div class="flag_status not_reviewed pull-left reviewer_member_status"></div>'+
56 58 '</div>'+
57 59 '<img alt="gravatar" class="gravatar" src="{0}"/>'+
58 60 '<span class="reviewer_name user">{1}</span>'+
59 61 reasons_html +
60 '<input type="hidden" value="{2}" name="review_members" />'+
62 '<input type="hidden" name="user_id" value="{2}">'+
63 '<input type="hidden" name="__start__" value="reasons:sequence">'+
64 '{3}'+
65 '<input type="hidden" name="__end__" value="reasons:sequence">'+
61 66 '<div class="reviewer_member_remove action_button" onclick="removeReviewMember({2})">' +
62 67 '<i class="icon-remove-sign"></i>'+
63 68 '</div>'+
64 69 '</div>'+
70 '<input type="hidden" name="__end__" value="reviewer:mapping">'+
65 71 '</li>' ;
72
66 73 var displayname = "{0} ({1} {2})".format(
67 74 nname, escapeHtml(fname), escapeHtml(lname));
68 var element = tmpl.format(gravatar_link,displayname,id);
75 var element = tmpl.format(gravatar_link,displayname,id,reasons_inputs);
69 76 // check if we don't have this ID already in
70 77 var ids = [];
71 78 var _els = $('#review_members li').toArray();
72 79 for (el in _els){
73 80 ids.push(_els[el].id)
74 81 }
75 82 if(ids.indexOf('reviewer_'+id) == -1){
76 83 // only add if it's not there
77 84 members.innerHTML += element;
78 85 }
79 86
80 87 };
81 88
82 89 var _updatePullRequest = function(repo_name, pull_request_id, postData) {
83 90 var url = pyroutes.url(
84 91 'pullrequest_update',
85 92 {"repo_name": repo_name, "pull_request_id": pull_request_id});
86 postData.csrf_token = CSRF_TOKEN;
93 if (typeof postData === 'string' ) {
94 postData += '&csrf_token=' + CSRF_TOKEN;
95 } else {
96 postData.csrf_token = CSRF_TOKEN;
97 }
87 98 var success = function(o) {
88 99 window.location.reload();
89 100 };
90 101 ajaxPOST(url, postData, success);
91 102 };
92 103
93 104 var updateReviewers = function(reviewers_ids, repo_name, pull_request_id){
94 105 if (reviewers_ids === undefined){
95 var reviewers_ids = [];
96 var ids = $('#review_members input').toArray();
97 for(var i=0; i<ids.length;i++){
98 var id = ids[i].value
99 reviewers_ids.push(id);
100 }
106 var postData = '_method=put&' + $('#reviewers input').serialize();
107 _updatePullRequest(repo_name, pull_request_id, postData);
101 108 }
102 var postData = {
103 '_method':'put',
104 'reviewers_ids': reviewers_ids};
105 _updatePullRequest(repo_name, pull_request_id, postData);
106 109 };
107 110
108 111 /**
109 112 * PULL REQUEST reject & close
110 113 */
111 114 var closePullRequest = function(repo_name, pull_request_id) {
112 115 var postData = {
113 116 '_method': 'put',
114 117 'close_pull_request': true};
115 118 _updatePullRequest(repo_name, pull_request_id, postData);
116 119 };
117 120
118 121 /**
119 122 * PULL REQUEST update commits
120 123 */
121 124 var updateCommits = function(repo_name, pull_request_id) {
122 125 var postData = {
123 126 '_method': 'put',
124 127 'update_commits': true};
125 128 _updatePullRequest(repo_name, pull_request_id, postData);
126 129 };
127 130
128 131
129 132 /**
130 133 * PULL REQUEST edit info
131 134 */
132 135 var editPullRequest = function(repo_name, pull_request_id, title, description) {
133 136 var url = pyroutes.url(
134 137 'pullrequest_update',
135 138 {"repo_name": repo_name, "pull_request_id": pull_request_id});
136 139
137 140 var postData = {
138 141 '_method': 'put',
139 142 'title': title,
140 143 'description': description,
141 144 'edit_pull_request': true,
142 145 'csrf_token': CSRF_TOKEN
143 146 };
144 147 var success = function(o) {
145 148 window.location.reload();
146 149 };
147 150 ajaxPOST(url, postData, success);
148 151 };
149 152
150 153 var initPullRequestsCodeMirror = function (textAreaId) {
151 154 var ta = $(textAreaId).get(0);
152 155 var initialHeight = '100px';
153 156
154 157 // default options
155 158 var codeMirrorOptions = {
156 159 mode: "text",
157 160 lineNumbers: false,
158 161 indentUnit: 4,
159 162 theme: 'rc-input'
160 163 };
161 164
162 165 var codeMirrorInstance = CodeMirror.fromTextArea(ta, codeMirrorOptions);
163 166 // marker for manually set description
164 167 codeMirrorInstance._userDefinedDesc = false;
165 168 codeMirrorInstance.setSize(null, initialHeight);
166 169 codeMirrorInstance.on("change", function(instance, changeObj) {
167 170 var height = initialHeight;
168 171 var lines = instance.lineCount();
169 172 if (lines > 6 && lines < 20) {
170 173 height = "auto"
171 174 }
172 175 else if (lines >= 20) {
173 176 height = 20 * 15;
174 177 }
175 178 instance.setSize(null, height);
176 179
177 180 // detect if the change was trigger by auto desc, or user input
178 181 changeOrigin = changeObj.origin;
179 182
180 183 if (changeOrigin === "setValue") {
181 184 cmLog.debug('Change triggered by setValue');
182 185 }
183 186 else {
184 187 cmLog.debug('user triggered change !');
185 188 // set special marker to indicate user has created an input.
186 189 instance._userDefinedDesc = true;
187 190 }
188 191
189 192 });
190 193
191 194 return codeMirrorInstance
192 195 };
193 196
194 197 /**
195 198 * Reviewer autocomplete
196 199 */
197 200 var ReviewerAutoComplete = function(input_id) {
198 201 $('#'+input_id).autocomplete({
199 202 serviceUrl: pyroutes.url('user_autocomplete_data'),
200 203 minChars:2,
201 204 maxHeight:400,
202 205 deferRequestBy: 300, //miliseconds
203 206 showNoSuggestionNotice: true,
204 207 tabDisabled: true,
205 208 autoSelectFirst: true,
206 209 formatResult: autocompleteFormatResult,
207 210 lookupFilter: autocompleteFilterResult,
208 211 onSelect: function(suggestion, data){
209 212 addReviewMember(data.id, data.first_name, data.last_name,
210 213 data.username, data.icon_link);
211 214 $('#'+input_id).val('');
212 215 }
213 216 });
214 217 };
@@ -1,59 +1,62 b''
1 1 // # Copyright (C) 2010-2016 RhodeCode GmbH
2 2 // #
3 3 // # This program is free software: you can redistribute it and/or modify
4 4 // # it under the terms of the GNU Affero General Public License, version 3
5 5 // # (only), as published by the Free Software Foundation.
6 6 // #
7 7 // # This program is distributed in the hope that it will be useful,
8 8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 // # GNU General Public License for more details.
11 11 // #
12 12 // # You should have received a copy of the GNU Affero General Public License
13 13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 // #
15 15 // # This program is dual-licensed. If you wish to learn more about the
16 16 // # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 /**
20 20 * turns objects into GET query string
21 21 */
22 22 var toQueryString = function(o) {
23 if(typeof o === 'string') {
24 return o;
25 }
23 26 if(typeof o !== 'object') {
24 27 return false;
25 28 }
26 29 var _p, _qs = [];
27 30 for(_p in o) {
28 31 _qs.push(encodeURIComponent(_p) + '=' + encodeURIComponent(o[_p]));
29 32 }
30 33 return _qs.join('&');
31 34 };
32 35
33 36 /**
34 37 * ajax call wrappers
35 38 */
36 39 var ajaxGET = function(url, success) {
37 40 var sUrl = url;
38 41 var request = $.ajax({url: sUrl, headers: {'X-PARTIAL-XHR': true}})
39 42 .done(function(data){
40 43 success(data);
41 44 })
42 45 .fail(function(data, textStatus, xhr){
43 46 alert("error processing request: " + textStatus);
44 47 });
45 48 return request;
46 49 };
47 50 var ajaxPOST = function(url,postData,success) {
48 51 var sUrl = url;
49 52 var postData = toQueryString(postData);
50 53 var request = $.ajax({type: 'POST', data: postData, url: sUrl,
51 54 headers: {'X-PARTIAL-XHR': true}})
52 55 .done(function(data){
53 56 success(data);
54 57 })
55 58 .fail(function(data, textStatus, xhr){
56 59 alert("error processing request: " + textStatus);
57 60 });
58 61 return request;
59 62 };
@@ -1,567 +1,569 b''
1 1 <%inherit file="/base/base.html"/>
2 2
3 3 <%def name="title()">
4 4 ${c.repo_name} ${_('New pull request')}
5 5 </%def>
6 6
7 7 <%def name="breadcrumbs_links()">
8 8 ${_('New pull request')}
9 9 </%def>
10 10
11 11 <%def name="menu_bar_nav()">
12 12 ${self.menu_items(active='repositories')}
13 13 </%def>
14 14
15 15 <%def name="menu_bar_subnav()">
16 16 ${self.repo_menu(active='showpullrequest')}
17 17 </%def>
18 18
19 19 <%def name="main()">
20 20 <div class="box">
21 21 <div class="title">
22 22 ${self.repo_page_title(c.rhodecode_db_repo)}
23 23 ${self.breadcrumbs()}
24 24 </div>
25 25
26 26 ${h.secure_form(url('pullrequest', repo_name=c.repo_name), method='post', id='pull_request_form')}
27 27 <div class="box pr-summary">
28 28
29 29 <div class="summary-details block-left">
30 30
31 31 <div class="form">
32 32 <!-- fields -->
33 33
34 34 <div class="fields" >
35 35
36 36 <div class="field">
37 37 <div class="label">
38 38 <label for="pullrequest_title">${_('Title')}:</label>
39 39 </div>
40 40 <div class="input">
41 41 ${h.text('pullrequest_title', c.default_title, class_="medium autogenerated-title")}
42 42 </div>
43 43 </div>
44 44
45 45 <div class="field">
46 46 <div class="label label-textarea">
47 47 <label for="pullrequest_desc">${_('Description')}:</label>
48 48 </div>
49 49 <div class="textarea text-area editor">
50 50 ${h.textarea('pullrequest_desc',size=30, )}
51 51 <span class="help-block">
52 52 ${_('Write a short description on this pull request')}
53 53 </span>
54 54 </div>
55 55 </div>
56 56
57 57 <div class="field">
58 58 <div class="label label-textarea">
59 59 <label for="pullrequest_desc">${_('Commit flow')}:</label>
60 60 </div>
61 61
62 62 ## TODO: johbo: Abusing the "content" class here to get the
63 63 ## desired effect. Should be replaced by a proper solution.
64 64
65 65 ##ORG
66 66 <div class="content">
67 67 <strong>${_('Origin repository')}:</strong>
68 68 ${c.rhodecode_db_repo.description}
69 69 </div>
70 70 <div class="content">
71 71 ${h.hidden('source_repo')}
72 72 ${h.hidden('source_ref')}
73 73 </div>
74 74
75 75 ##OTHER, most Probably the PARENT OF THIS FORK
76 76 <div class="content">
77 77 ## filled with JS
78 78 <div id="target_repo_desc"></div>
79 79 </div>
80 80
81 81 <div class="content">
82 82 ${h.hidden('target_repo')}
83 83 ${h.hidden('target_ref')}
84 84 <span id="target_ref_loading" style="display: none">
85 85 ${_('Loading refs...')}
86 86 </span>
87 87 </div>
88 88 </div>
89 89
90 90 <div class="field">
91 91 <div class="label label-textarea">
92 92 <label for="pullrequest_submit"></label>
93 93 </div>
94 94 <div class="input">
95 95 <div class="pr-submit-button">
96 96 ${h.submit('save',_('Submit Pull Request'),class_="btn")}
97 97 </div>
98 98 <div id="pr_open_message"></div>
99 99 </div>
100 100 </div>
101 101
102 102 <div class="pr-spacing-container"></div>
103 103 </div>
104 104 </div>
105 105 </div>
106 106 <div>
107 107 <div class="reviewers-title block-right">
108 108 <div class="pr-details-title">
109 109 ${_('Pull request reviewers')}
110 110 </div>
111 111 </div>
112 112 <div id="reviewers" class="block-right pr-details-content reviewers">
113 113 ## members goes here, filled via JS based on initial selection !
114 <input type="hidden" name="__start__" value="review_members:sequence">
114 115 <ul id="review_members" class="group_members"></ul>
116 <input type="hidden" name="__end__" value="review_members:sequence">
115 117 <div id="add_reviewer_input" class='ac'>
116 118 <div class="reviewer_ac">
117 119 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer'))}
118 120 <div id="reviewers_container"></div>
119 121 </div>
120 122 </div>
121 123 </div>
122 124 </div>
123 125 </div>
124 126 <div class="box">
125 127 <div>
126 128 ## overview pulled by ajax
127 129 <div id="pull_request_overview"></div>
128 130 </div>
129 131 </div>
130 132 ${h.end_form()}
131 133 </div>
132 134
133 135 <script type="text/javascript">
134 136 $(function(){
135 137 var defaultSourceRepo = '${c.default_repo_data['source_repo_name']}';
136 138 var defaultSourceRepoData = ${c.default_repo_data['source_refs_json']|n};
137 139 var defaultTargetRepo = '${c.default_repo_data['target_repo_name']}';
138 140 var defaultTargetRepoData = ${c.default_repo_data['target_refs_json']|n};
139 141 var targetRepoName = '${c.repo_name}';
140 142
141 143 var $pullRequestForm = $('#pull_request_form');
142 144 var $sourceRepo = $('#source_repo', $pullRequestForm);
143 145 var $targetRepo = $('#target_repo', $pullRequestForm);
144 146 var $sourceRef = $('#source_ref', $pullRequestForm);
145 147 var $targetRef = $('#target_ref', $pullRequestForm);
146 148
147 149 var calculateContainerWidth = function() {
148 150 var maxWidth = 0;
149 151 var repoSelect2Containers = ['#source_repo', '#target_repo'];
150 152 $.each(repoSelect2Containers, function(idx, value) {
151 153 $(value).select2('container').width('auto');
152 154 var curWidth = $(value).select2('container').width();
153 155 if (maxWidth <= curWidth) {
154 156 maxWidth = curWidth;
155 157 }
156 158 $.each(repoSelect2Containers, function(idx, value) {
157 159 $(value).select2('container').width(maxWidth + 10);
158 160 });
159 161 });
160 162 };
161 163
162 164 var initRefSelection = function(selectedRef) {
163 165 return function(element, callback) {
164 166 // translate our select2 id into a text, it's a mapping to show
165 167 // simple label when selecting by internal ID.
166 168 var id, refData;
167 169 if (selectedRef === undefined) {
168 170 id = element.val();
169 171 refData = element.val().split(':');
170 172 } else {
171 173 id = selectedRef;
172 174 refData = selectedRef.split(':');
173 175 }
174 176
175 177 var text = refData[1];
176 178 if (refData[0] === 'rev') {
177 179 text = text.substring(0, 12);
178 180 }
179 181
180 182 var data = {id: id, text: text};
181 183
182 184 callback(data);
183 185 };
184 186 };
185 187
186 188 var formatRefSelection = function(item) {
187 189 var prefix = '';
188 190 var refData = item.id.split(':');
189 191 if (refData[0] === 'branch') {
190 192 prefix = '<i class="icon-branch"></i>';
191 193 }
192 194 else if (refData[0] === 'book') {
193 195 prefix = '<i class="icon-bookmark"></i>';
194 196 }
195 197 else if (refData[0] === 'tag') {
196 198 prefix = '<i class="icon-tag"></i>';
197 199 }
198 200
199 201 var originalOption = item.element;
200 202 return prefix + item.text;
201 203 };
202 204
203 205 // custom code mirror
204 206 var codeMirrorInstance = initPullRequestsCodeMirror('#pullrequest_desc');
205 207
206 208 var queryTargetRepo = function(self, query) {
207 209 // cache ALL results if query is empty
208 210 var cacheKey = query.term || '__';
209 211 var cachedData = self.cachedDataSource[cacheKey];
210 212
211 213 if (cachedData) {
212 214 query.callback({results: cachedData.results});
213 215 } else {
214 216 $.ajax({
215 217 url: pyroutes.url('pullrequest_repo_destinations', {'repo_name': targetRepoName}),
216 218 data: {query: query.term},
217 219 dataType: 'json',
218 220 type: 'GET',
219 221 success: function(data) {
220 222 self.cachedDataSource[cacheKey] = data;
221 223 query.callback({results: data.results});
222 224 },
223 225 error: function(data, textStatus, errorThrown) {
224 226 alert(
225 227 "Error while fetching entries.\nError code {0} ({1}).".format(data.status, data.statusText));
226 228 }
227 229 });
228 230 }
229 231 };
230 232
231 233 var queryTargetRefs = function(initialData, query) {
232 234 var data = {results: []};
233 235 // filter initialData
234 236 $.each(initialData, function() {
235 237 var section = this.text;
236 238 var children = [];
237 239 $.each(this.children, function() {
238 240 if (query.term.length === 0 ||
239 241 this.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0 ) {
240 242 children.push({'id': this.id, 'text': this.text})
241 243 }
242 244 });
243 245 data.results.push({'text': section, 'children': children})
244 246 });
245 247 query.callback({results: data.results});
246 248 };
247 249
248 250 var prButtonLock = function(lockEnabled, msg) {
249 251 if (lockEnabled) {
250 252 $('#save').attr('disabled', 'disabled');
251 253 }
252 254 else {
253 255 $('#save').removeAttr('disabled');
254 256 }
255 257
256 258 $('#pr_open_message').html(msg);
257 259
258 260 };
259 261
260 262 var loadRepoRefDiffPreview = function() {
261 263 var sourceRepo = $sourceRepo.eq(0).val();
262 264 var sourceRef = $sourceRef.eq(0).val().split(':');
263 265
264 266 var targetRepo = $targetRepo.eq(0).val();
265 267 var targetRef = $targetRef.eq(0).val().split(':');
266 268
267 269 var url_data = {
268 270 'repo_name': targetRepo,
269 271 'target_repo': sourceRepo,
270 272 'source_ref': targetRef[2],
271 273 'source_ref_type': 'rev',
272 274 'target_ref': sourceRef[2],
273 275 'target_ref_type': 'rev',
274 276 'merge': true,
275 277 '_': Date.now() // bypass browser caching
276 278 }; // gather the source/target ref and repo here
277 279
278 280 if (sourceRef.length !== 3 || targetRef.length !== 3) {
279 281 prButtonLock(true, "${_('Please select origin and destination')}");
280 282 return;
281 283 }
282 284 var url = pyroutes.url('compare_url', url_data);
283 285
284 286 // lock PR button, so we cannot send PR before it's calculated
285 287 prButtonLock(true, "${_('Loading compare ...')}");
286 288
287 289 if (loadRepoRefDiffPreview._currentRequest) {
288 290 loadRepoRefDiffPreview._currentRequest.abort();
289 291 }
290 292
291 293 loadRepoRefDiffPreview._currentRequest = $.get(url)
292 294 .error(function(data, textStatus, errorThrown) {
293 295 alert(
294 296 "Error while processing request.\nError code {0} ({1}).".format(
295 297 data.status, data.statusText));
296 298 })
297 299 .done(function(data) {
298 300 loadRepoRefDiffPreview._currentRequest = null;
299 301 $('#pull_request_overview').html(data);
300 302 var commitElements = $(data).find('tr[commit_id]');
301 303
302 304 var prTitleAndDesc = getTitleAndDescription(sourceRef[1],
303 305 commitElements, 5);
304 306
305 307 var title = prTitleAndDesc[0];
306 308 var proposedDescription = prTitleAndDesc[1];
307 309
308 310 var useGeneratedTitle = (
309 311 $('#pullrequest_title').hasClass('autogenerated-title') ||
310 312 $('#pullrequest_title').val() === "");
311 313
312 314 if (title && useGeneratedTitle) {
313 315 // use generated title if we haven't specified our own
314 316 $('#pullrequest_title').val(title);
315 317 $('#pullrequest_title').addClass('autogenerated-title');
316 318
317 319 }
318 320
319 321 var useGeneratedDescription = (
320 322 !codeMirrorInstance._userDefinedDesc ||
321 323 codeMirrorInstance.getValue() === "");
322 324
323 325 if (proposedDescription && useGeneratedDescription) {
324 326 // set proposed content, if we haven't defined our own,
325 327 // or we don't have description written
326 328 codeMirrorInstance._userDefinedDesc = false; // reset state
327 329 codeMirrorInstance.setValue(proposedDescription);
328 330 }
329 331
330 332 var msg = '';
331 333 if (commitElements.length === 1) {
332 334 msg = "${ungettext('This pull request will consist of __COMMITS__ commit.', 'This pull request will consist of __COMMITS__ commits.', 1)}";
333 335 } else {
334 336 msg = "${ungettext('This pull request will consist of __COMMITS__ commit.', 'This pull request will consist of __COMMITS__ commits.', 2)}";
335 337 }
336 338
337 339 msg += ' <a id="pull_request_overview_url" href="{0}" target="_blank">${_('Show detailed compare.')}</a>'.format(url);
338 340
339 341 if (commitElements.length) {
340 342 var commitsLink = '<a href="#pull_request_overview"><strong>{0}</strong></a>'.format(commitElements.length);
341 343 prButtonLock(false, msg.replace('__COMMITS__', commitsLink));
342 344 }
343 345 else {
344 346 prButtonLock(true, "${_('There are no commits to merge.')}");
345 347 }
346 348
347 349
348 350 });
349 351 };
350 352
351 353 /**
352 354 Generate Title and Description for a PullRequest.
353 355 In case of 1 commits, the title and description is that one commit
354 356 in case of multiple commits, we iterate on them with max N number of commits,
355 357 and build description in a form
356 358 - commitN
357 359 - commitN+1
358 360 ...
359 361
360 362 Title is then constructed from branch names, or other references,
361 363 replacing '-' and '_' into spaces
362 364
363 365 * @param sourceRef
364 366 * @param elements
365 367 * @param limit
366 368 * @returns {*[]}
367 369 */
368 370 var getTitleAndDescription = function(sourceRef, elements, limit) {
369 371 var title = '';
370 372 var desc = '';
371 373
372 374 $.each($(elements).get().reverse().slice(0, limit), function(idx, value) {
373 375 var rawMessage = $(value).find('td.td-description .message').data('messageRaw');
374 376 desc += '- ' + rawMessage.split('\n')[0].replace(/\n+$/, "") + '\n';
375 377 });
376 378 // only 1 commit, use commit message as title
377 379 if (elements.length == 1) {
378 380 title = $(elements[0]).find('td.td-description .message').data('messageRaw').split('\n')[0];
379 381 }
380 382 else {
381 383 // use reference name
382 384 title = sourceRef.replace(/-/g, ' ').replace(/_/g, ' ').capitalizeFirstLetter();
383 385 }
384 386
385 387 return [title, desc]
386 388 };
387 389
388 390 var Select2Box = function(element, overrides) {
389 391 var globalDefaults = {
390 392 dropdownAutoWidth: true,
391 393 containerCssClass: "drop-menu",
392 394 dropdownCssClass: "drop-menu-dropdown",
393 395 };
394 396
395 397 var initSelect2 = function(defaultOptions) {
396 398 var options = jQuery.extend(globalDefaults, defaultOptions, overrides);
397 399 element.select2(options);
398 400 };
399 401
400 402 return {
401 403 initRef: function() {
402 404 var defaultOptions = {
403 405 minimumResultsForSearch: 5,
404 406 formatSelection: formatRefSelection
405 407 };
406 408
407 409 initSelect2(defaultOptions);
408 410 },
409 411
410 412 initRepo: function(defaultValue, readOnly) {
411 413 var defaultOptions = {
412 414 initSelection : function (element, callback) {
413 415 var data = {id: defaultValue, text: defaultValue};
414 416 callback(data);
415 417 }
416 418 };
417 419
418 420 initSelect2(defaultOptions);
419 421
420 422 element.select2('val', defaultSourceRepo);
421 423 if (readOnly === true) {
422 424 element.select2('readonly', true);
423 425 };
424 426 }
425 427 };
426 428 };
427 429
428 430 var initTargetRefs = function(refsData, selectedRef){
429 431 Select2Box($targetRef, {
430 432 query: function(query) {
431 433 queryTargetRefs(refsData, query);
432 434 },
433 435 initSelection : initRefSelection(selectedRef)
434 436 }).initRef();
435 437
436 438 if (!(selectedRef === undefined)) {
437 439 $targetRef.select2('val', selectedRef);
438 440 }
439 441 };
440 442
441 443 var targetRepoChanged = function(repoData) {
442 444 // generate new DESC of target repo displayed next to select
443 445 $('#target_repo_desc').html(
444 446 "<strong>${_('Destination repository')}</strong>: {0}".format(repoData['description'])
445 447 );
446 448
447 449 // generate dynamic select2 for refs.
448 450 initTargetRefs(repoData['refs']['select2_refs'],
449 451 repoData['refs']['selected_ref']);
450 452
451 453 };
452 454
453 455 var sourceRefSelect2 = Select2Box(
454 456 $sourceRef, {
455 457 placeholder: "${_('Select commit reference')}",
456 458 query: function(query) {
457 459 var initialData = defaultSourceRepoData['refs']['select2_refs'];
458 460 queryTargetRefs(initialData, query)
459 461 },
460 462 initSelection: initRefSelection()
461 463 }
462 464 );
463 465
464 466 var sourceRepoSelect2 = Select2Box($sourceRepo, {
465 467 query: function(query) {}
466 468 });
467 469
468 470 var targetRepoSelect2 = Select2Box($targetRepo, {
469 471 cachedDataSource: {},
470 472 query: $.debounce(250, function(query) {
471 473 queryTargetRepo(this, query);
472 474 }),
473 475 formatResult: formatResult
474 476 });
475 477
476 478 sourceRefSelect2.initRef();
477 479
478 480 sourceRepoSelect2.initRepo(defaultSourceRepo, true);
479 481
480 482 targetRepoSelect2.initRepo(defaultTargetRepo, false);
481 483
482 484 $sourceRef.on('change', function(e){
483 485 loadRepoRefDiffPreview();
484 486 loadDefaultReviewers();
485 487 });
486 488
487 489 $targetRef.on('change', function(e){
488 490 loadRepoRefDiffPreview();
489 491 loadDefaultReviewers();
490 492 });
491 493
492 494 $targetRepo.on('change', function(e){
493 495 var repoName = $(this).val();
494 496 calculateContainerWidth();
495 497 $targetRef.select2('destroy');
496 498 $('#target_ref_loading').show();
497 499
498 500 $.ajax({
499 501 url: pyroutes.url('pullrequest_repo_refs',
500 502 {'repo_name': targetRepoName, 'target_repo_name':repoName}),
501 503 data: {},
502 504 dataType: 'json',
503 505 type: 'GET',
504 506 success: function(data) {
505 507 $('#target_ref_loading').hide();
506 508 targetRepoChanged(data);
507 509 loadRepoRefDiffPreview();
508 510 },
509 511 error: function(data, textStatus, errorThrown) {
510 512 alert("Error while fetching entries.\nError code {0} ({1}).".format(data.status, data.statusText));
511 513 }
512 514 })
513 515
514 516 });
515 517
516 518 var loadDefaultReviewers = function() {
517 519 if (loadDefaultReviewers._currentRequest) {
518 520 loadDefaultReviewers._currentRequest.abort();
519 521 }
520 522 var url = pyroutes.url('repo_default_reviewers_data', {'repo_name': targetRepoName});
521 523
522 524 var sourceRepo = $sourceRepo.eq(0).val();
523 525 var sourceRef = $sourceRef.eq(0).val().split(':');
524 526 var targetRepo = $targetRepo.eq(0).val();
525 527 var targetRef = $targetRef.eq(0).val().split(':');
526 528 url += '?source_repo=' + sourceRepo;
527 529 url += '&source_ref=' + sourceRef[2];
528 530 url += '&target_repo=' + targetRepo;
529 531 url += '&target_ref=' + targetRef[2];
530 532
531 533 loadDefaultReviewers._currentRequest = $.get(url)
532 534 .done(function(data) {
533 535 loadDefaultReviewers._currentRequest = null;
534 536
535 537 // reset && add the reviewer based on selected repo
536 538 $('#review_members').html('');
537 539 for (var i = 0; i < data.reviewers.length; i++) {
538 540 var reviewer = data.reviewers[i];
539 541 addReviewMember(
540 542 reviewer.user_id, reviewer.firstname,
541 543 reviewer.lastname, reviewer.username,
542 544 reviewer.gravatar_link, reviewer.reasons);
543 545 }
544 546 });
545 547 };
546 548 prButtonLock(true, "${_('Please select origin and destination')}");
547 549
548 550 // auto-load on init, the target refs select2
549 551 calculateContainerWidth();
550 552 targetRepoChanged(defaultTargetRepoData);
551 553
552 554 $('#pullrequest_title').on('keyup', function(e){
553 555 $(this).removeClass('autogenerated-title');
554 556 });
555 557
556 558 %if c.default_source_ref:
557 559 // in case we have a pre-selected value, use it now
558 560 $sourceRef.select2('val', '${c.default_source_ref}');
559 561 loadRepoRefDiffPreview();
560 562 loadDefaultReviewers();
561 563 %endif
562 564
563 565 ReviewerAutoComplete('user');
564 566 });
565 567 </script>
566 568
567 569 </%def>
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
General Comments 0
You need to be logged in to leave comments. Login now