##// END OF EJS Templates
landing-refs: create helpers for landing ref to make clear indication about type/name
marcink -
r4370:ef6d7bca stable
parent child Browse files
Show More
@@ -1,1070 +1,1070 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import os
22 22
23 23 import mock
24 24 import pytest
25 25
26 26 from rhodecode.apps.repository.tests.test_repo_compare import ComparePage
27 27 from rhodecode.apps.repository.views.repo_files import RepoFilesView
28 28 from rhodecode.lib import helpers as h
29 29 from rhodecode.lib.compat import OrderedDict
30 30 from rhodecode.lib.ext_json import json
31 31 from rhodecode.lib.vcs import nodes
32 32
33 33 from rhodecode.lib.vcs.conf import settings
34 34 from rhodecode.tests import assert_session_flash
35 35 from rhodecode.tests.fixture import Fixture
36 36 from rhodecode.model.db import Session
37 37
38 38 fixture = Fixture()
39 39
40 40
41 41 def get_node_history(backend_type):
42 42 return {
43 43 'hg': json.loads(fixture.load_resource('hg_node_history_response.json')),
44 44 'git': json.loads(fixture.load_resource('git_node_history_response.json')),
45 45 'svn': json.loads(fixture.load_resource('svn_node_history_response.json')),
46 46 }[backend_type]
47 47
48 48
49 49 def route_path(name, params=None, **kwargs):
50 50 import urllib
51 51
52 52 base_url = {
53 53 'repo_summary': '/{repo_name}',
54 54 'repo_archivefile': '/{repo_name}/archive/{fname}',
55 55 'repo_files_diff': '/{repo_name}/diff/{f_path}',
56 56 'repo_files_diff_2way_redirect': '/{repo_name}/diff-2way/{f_path}',
57 57 'repo_files': '/{repo_name}/files/{commit_id}/{f_path}',
58 58 'repo_files:default_path': '/{repo_name}/files/{commit_id}/',
59 59 'repo_files:default_commit': '/{repo_name}/files',
60 60 'repo_files:rendered': '/{repo_name}/render/{commit_id}/{f_path}',
61 61 'repo_files:annotated': '/{repo_name}/annotate/{commit_id}/{f_path}',
62 62 'repo_files:annotated_previous': '/{repo_name}/annotate-previous/{commit_id}/{f_path}',
63 63 'repo_files_nodelist': '/{repo_name}/nodelist/{commit_id}/{f_path}',
64 64 'repo_file_raw': '/{repo_name}/raw/{commit_id}/{f_path}',
65 65 'repo_file_download': '/{repo_name}/download/{commit_id}/{f_path}',
66 66 'repo_file_history': '/{repo_name}/history/{commit_id}/{f_path}',
67 67 'repo_file_authors': '/{repo_name}/authors/{commit_id}/{f_path}',
68 68 'repo_files_remove_file': '/{repo_name}/remove_file/{commit_id}/{f_path}',
69 69 'repo_files_delete_file': '/{repo_name}/delete_file/{commit_id}/{f_path}',
70 70 'repo_files_edit_file': '/{repo_name}/edit_file/{commit_id}/{f_path}',
71 71 'repo_files_update_file': '/{repo_name}/update_file/{commit_id}/{f_path}',
72 72 'repo_files_add_file': '/{repo_name}/add_file/{commit_id}/{f_path}',
73 73 'repo_files_create_file': '/{repo_name}/create_file/{commit_id}/{f_path}',
74 74 'repo_nodetree_full': '/{repo_name}/nodetree_full/{commit_id}/{f_path}',
75 75 'repo_nodetree_full:default_path': '/{repo_name}/nodetree_full/{commit_id}/',
76 76 }[name].format(**kwargs)
77 77
78 78 if params:
79 79 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
80 80 return base_url
81 81
82 82
83 83 def assert_files_in_response(response, files, params):
84 84 template = (
85 85 'href="/%(repo_name)s/files/%(commit_id)s/%(name)s"')
86 86 _assert_items_in_response(response, files, template, params)
87 87
88 88
89 89 def assert_dirs_in_response(response, dirs, params):
90 90 template = (
91 91 'href="/%(repo_name)s/files/%(commit_id)s/%(name)s"')
92 92 _assert_items_in_response(response, dirs, template, params)
93 93
94 94
95 95 def _assert_items_in_response(response, items, template, params):
96 96 for item in items:
97 97 item_params = {'name': item}
98 98 item_params.update(params)
99 99 response.mustcontain(template % item_params)
100 100
101 101
102 102 def assert_timeago_in_response(response, items, params):
103 103 for item in items:
104 104 response.mustcontain(h.age_component(params['date']))
105 105
106 106
107 107 @pytest.mark.usefixtures("app")
108 108 class TestFilesViews(object):
109 109
110 110 def test_show_files(self, backend):
111 111 response = self.app.get(
112 112 route_path('repo_files',
113 113 repo_name=backend.repo_name,
114 114 commit_id='tip', f_path='/'))
115 115 commit = backend.repo.get_commit()
116 116
117 117 params = {
118 118 'repo_name': backend.repo_name,
119 119 'commit_id': commit.raw_id,
120 120 'date': commit.date
121 121 }
122 122 assert_dirs_in_response(response, ['docs', 'vcs'], params)
123 123 files = [
124 124 '.gitignore',
125 125 '.hgignore',
126 126 '.hgtags',
127 127 # TODO: missing in Git
128 128 # '.travis.yml',
129 129 'MANIFEST.in',
130 130 'README.rst',
131 131 # TODO: File is missing in svn repository
132 132 # 'run_test_and_report.sh',
133 133 'setup.cfg',
134 134 'setup.py',
135 135 'test_and_report.sh',
136 136 'tox.ini',
137 137 ]
138 138 assert_files_in_response(response, files, params)
139 139 assert_timeago_in_response(response, files, params)
140 140
141 141 def test_show_files_links_submodules_with_absolute_url(self, backend_hg):
142 142 repo = backend_hg['subrepos']
143 143 response = self.app.get(
144 144 route_path('repo_files',
145 145 repo_name=repo.repo_name,
146 146 commit_id='tip', f_path='/'))
147 147 assert_response = response.assert_response()
148 148 assert_response.contains_one_link(
149 149 'absolute-path @ 000000000000', 'http://example.com/absolute-path')
150 150
151 151 def test_show_files_links_submodules_with_absolute_url_subpaths(
152 152 self, backend_hg):
153 153 repo = backend_hg['subrepos']
154 154 response = self.app.get(
155 155 route_path('repo_files',
156 156 repo_name=repo.repo_name,
157 157 commit_id='tip', f_path='/'))
158 158 assert_response = response.assert_response()
159 159 assert_response.contains_one_link(
160 160 'subpaths-path @ 000000000000',
161 161 'http://sub-base.example.com/subpaths-path')
162 162
163 163 @pytest.mark.xfail_backends("svn", reason="Depends on branch support")
164 164 def test_files_menu(self, backend):
165 165 new_branch = "temp_branch_name"
166 166 commits = [
167 167 {'message': 'a'},
168 168 {'message': 'b', 'branch': new_branch}
169 169 ]
170 170 backend.create_repo(commits)
171 171 backend.repo.landing_rev = "branch:%s" % new_branch
172 172 Session().commit()
173 173
174 174 # get response based on tip and not new commit
175 175 response = self.app.get(
176 176 route_path('repo_files',
177 177 repo_name=backend.repo_name,
178 178 commit_id='tip', f_path='/'))
179 179
180 180 # make sure Files menu url is not tip but new commit
181 landing_rev = backend.repo.landing_rev[1]
181 landing_rev = backend.repo.landing_ref_name
182 182 files_url = route_path('repo_files:default_path',
183 183 repo_name=backend.repo_name,
184 184 commit_id=landing_rev)
185 185
186 186 assert landing_rev != 'tip'
187 187 response.mustcontain(
188 188 '<li class="active"><a class="menulink" href="%s">' % files_url)
189 189
190 190 def test_show_files_commit(self, backend):
191 191 commit = backend.repo.get_commit(commit_idx=32)
192 192
193 193 response = self.app.get(
194 194 route_path('repo_files',
195 195 repo_name=backend.repo_name,
196 196 commit_id=commit.raw_id, f_path='/'))
197 197
198 198 dirs = ['docs', 'tests']
199 199 files = ['README.rst']
200 200 params = {
201 201 'repo_name': backend.repo_name,
202 202 'commit_id': commit.raw_id,
203 203 }
204 204 assert_dirs_in_response(response, dirs, params)
205 205 assert_files_in_response(response, files, params)
206 206
207 207 def test_show_files_different_branch(self, backend):
208 208 branches = dict(
209 209 hg=(150, ['git']),
210 210 # TODO: Git test repository does not contain other branches
211 211 git=(633, ['master']),
212 212 # TODO: Branch support in Subversion
213 213 svn=(150, [])
214 214 )
215 215 idx, branches = branches[backend.alias]
216 216 commit = backend.repo.get_commit(commit_idx=idx)
217 217 response = self.app.get(
218 218 route_path('repo_files',
219 219 repo_name=backend.repo_name,
220 220 commit_id=commit.raw_id, f_path='/'))
221 221
222 222 assert_response = response.assert_response()
223 223 for branch in branches:
224 224 assert_response.element_contains('.tags .branchtag', branch)
225 225
226 226 def test_show_files_paging(self, backend):
227 227 repo = backend.repo
228 228 indexes = [73, 92, 109, 1, 0]
229 229 idx_map = [(rev, repo.get_commit(commit_idx=rev).raw_id)
230 230 for rev in indexes]
231 231
232 232 for idx in idx_map:
233 233 response = self.app.get(
234 234 route_path('repo_files',
235 235 repo_name=backend.repo_name,
236 236 commit_id=idx[1], f_path='/'))
237 237
238 238 response.mustcontain("""r%s:%s""" % (idx[0], idx[1][:8]))
239 239
240 240 def test_file_source(self, backend):
241 241 commit = backend.repo.get_commit(commit_idx=167)
242 242 response = self.app.get(
243 243 route_path('repo_files',
244 244 repo_name=backend.repo_name,
245 245 commit_id=commit.raw_id, f_path='vcs/nodes.py'))
246 246
247 247 msgbox = """<div class="commit">%s</div>"""
248 248 response.mustcontain(msgbox % (commit.message, ))
249 249
250 250 assert_response = response.assert_response()
251 251 if commit.branch:
252 252 assert_response.element_contains(
253 253 '.tags.tags-main .branchtag', commit.branch)
254 254 if commit.tags:
255 255 for tag in commit.tags:
256 256 assert_response.element_contains('.tags.tags-main .tagtag', tag)
257 257
258 258 def test_file_source_annotated(self, backend):
259 259 response = self.app.get(
260 260 route_path('repo_files:annotated',
261 261 repo_name=backend.repo_name,
262 262 commit_id='tip', f_path='vcs/nodes.py'))
263 263 expected_commits = {
264 264 'hg': 'r356',
265 265 'git': 'r345',
266 266 'svn': 'r208',
267 267 }
268 268 response.mustcontain(expected_commits[backend.alias])
269 269
270 270 def test_file_source_authors(self, backend):
271 271 response = self.app.get(
272 272 route_path('repo_file_authors',
273 273 repo_name=backend.repo_name,
274 274 commit_id='tip', f_path='vcs/nodes.py'))
275 275 expected_authors = {
276 276 'hg': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
277 277 'git': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
278 278 'svn': ('marcin', 'lukasz'),
279 279 }
280 280
281 281 for author in expected_authors[backend.alias]:
282 282 response.mustcontain(author)
283 283
284 284 def test_file_source_authors_with_annotation(self, backend):
285 285 response = self.app.get(
286 286 route_path('repo_file_authors',
287 287 repo_name=backend.repo_name,
288 288 commit_id='tip', f_path='vcs/nodes.py',
289 289 params=dict(annotate=1)))
290 290 expected_authors = {
291 291 'hg': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
292 292 'git': ('Marcin Kuzminski', 'Lukasz Balcerzak'),
293 293 'svn': ('marcin', 'lukasz'),
294 294 }
295 295
296 296 for author in expected_authors[backend.alias]:
297 297 response.mustcontain(author)
298 298
299 299 def test_file_source_history(self, backend, xhr_header):
300 300 response = self.app.get(
301 301 route_path('repo_file_history',
302 302 repo_name=backend.repo_name,
303 303 commit_id='tip', f_path='vcs/nodes.py'),
304 304 extra_environ=xhr_header)
305 305 assert get_node_history(backend.alias) == json.loads(response.body)
306 306
307 307 def test_file_source_history_svn(self, backend_svn, xhr_header):
308 308 simple_repo = backend_svn['svn-simple-layout']
309 309 response = self.app.get(
310 310 route_path('repo_file_history',
311 311 repo_name=simple_repo.repo_name,
312 312 commit_id='tip', f_path='trunk/example.py'),
313 313 extra_environ=xhr_header)
314 314
315 315 expected_data = json.loads(
316 316 fixture.load_resource('svn_node_history_branches.json'))
317 317
318 318 assert expected_data == response.json
319 319
320 320 def test_file_source_history_with_annotation(self, backend, xhr_header):
321 321 response = self.app.get(
322 322 route_path('repo_file_history',
323 323 repo_name=backend.repo_name,
324 324 commit_id='tip', f_path='vcs/nodes.py',
325 325 params=dict(annotate=1)),
326 326
327 327 extra_environ=xhr_header)
328 328 assert get_node_history(backend.alias) == json.loads(response.body)
329 329
330 330 def test_tree_search_top_level(self, backend, xhr_header):
331 331 commit = backend.repo.get_commit(commit_idx=173)
332 332 response = self.app.get(
333 333 route_path('repo_files_nodelist',
334 334 repo_name=backend.repo_name,
335 335 commit_id=commit.raw_id, f_path='/'),
336 336 extra_environ=xhr_header)
337 337 assert 'nodes' in response.json
338 338 assert {'name': 'docs', 'type': 'dir'} in response.json['nodes']
339 339
340 340 def test_tree_search_missing_xhr(self, backend):
341 341 self.app.get(
342 342 route_path('repo_files_nodelist',
343 343 repo_name=backend.repo_name,
344 344 commit_id='tip', f_path='/'),
345 345 status=404)
346 346
347 347 def test_tree_search_at_path(self, backend, xhr_header):
348 348 commit = backend.repo.get_commit(commit_idx=173)
349 349 response = self.app.get(
350 350 route_path('repo_files_nodelist',
351 351 repo_name=backend.repo_name,
352 352 commit_id=commit.raw_id, f_path='/docs'),
353 353 extra_environ=xhr_header)
354 354 assert 'nodes' in response.json
355 355 nodes = response.json['nodes']
356 356 assert {'name': 'docs/api', 'type': 'dir'} in nodes
357 357 assert {'name': 'docs/index.rst', 'type': 'file'} in nodes
358 358
359 359 def test_tree_search_at_path_2nd_level(self, backend, xhr_header):
360 360 commit = backend.repo.get_commit(commit_idx=173)
361 361 response = self.app.get(
362 362 route_path('repo_files_nodelist',
363 363 repo_name=backend.repo_name,
364 364 commit_id=commit.raw_id, f_path='/docs/api'),
365 365 extra_environ=xhr_header)
366 366 assert 'nodes' in response.json
367 367 nodes = response.json['nodes']
368 368 assert {'name': 'docs/api/index.rst', 'type': 'file'} in nodes
369 369
370 370 def test_tree_search_at_path_missing_xhr(self, backend):
371 371 self.app.get(
372 372 route_path('repo_files_nodelist',
373 373 repo_name=backend.repo_name,
374 374 commit_id='tip', f_path='/docs'),
375 375 status=404)
376 376
377 377 def test_nodetree(self, backend, xhr_header):
378 378 commit = backend.repo.get_commit(commit_idx=173)
379 379 response = self.app.get(
380 380 route_path('repo_nodetree_full',
381 381 repo_name=backend.repo_name,
382 382 commit_id=commit.raw_id, f_path='/'),
383 383 extra_environ=xhr_header)
384 384
385 385 assert_response = response.assert_response()
386 386
387 387 for attr in ['data-commit-id', 'data-date', 'data-author']:
388 388 elements = assert_response.get_elements('[{}]'.format(attr))
389 389 assert len(elements) > 1
390 390
391 391 for element in elements:
392 392 assert element.get(attr)
393 393
394 394 def test_nodetree_if_file(self, backend, xhr_header):
395 395 commit = backend.repo.get_commit(commit_idx=173)
396 396 response = self.app.get(
397 397 route_path('repo_nodetree_full',
398 398 repo_name=backend.repo_name,
399 399 commit_id=commit.raw_id, f_path='README.rst'),
400 400 extra_environ=xhr_header)
401 401 assert response.body == ''
402 402
403 403 def test_nodetree_wrong_path(self, backend, xhr_header):
404 404 commit = backend.repo.get_commit(commit_idx=173)
405 405 response = self.app.get(
406 406 route_path('repo_nodetree_full',
407 407 repo_name=backend.repo_name,
408 408 commit_id=commit.raw_id, f_path='/dont-exist'),
409 409 extra_environ=xhr_header)
410 410
411 411 err = 'error: There is no file nor ' \
412 412 'directory at the given path'
413 413 assert err in response.body
414 414
415 415 def test_nodetree_missing_xhr(self, backend):
416 416 self.app.get(
417 417 route_path('repo_nodetree_full',
418 418 repo_name=backend.repo_name,
419 419 commit_id='tip', f_path='/'),
420 420 status=404)
421 421
422 422
423 423 @pytest.mark.usefixtures("app", "autologin_user")
424 424 class TestRawFileHandling(object):
425 425
426 426 def test_download_file(self, backend):
427 427 commit = backend.repo.get_commit(commit_idx=173)
428 428 response = self.app.get(
429 429 route_path('repo_file_download',
430 430 repo_name=backend.repo_name,
431 431 commit_id=commit.raw_id, f_path='vcs/nodes.py'),)
432 432
433 433 assert response.content_disposition == 'attachment; filename="nodes.py"; filename*=UTF-8\'\'nodes.py'
434 434 assert response.content_type == "text/x-python"
435 435
436 436 def test_download_file_wrong_cs(self, backend):
437 437 raw_id = u'ERRORce30c96924232dffcd24178a07ffeb5dfc'
438 438
439 439 response = self.app.get(
440 440 route_path('repo_file_download',
441 441 repo_name=backend.repo_name,
442 442 commit_id=raw_id, f_path='vcs/nodes.svg'),
443 443 status=404)
444 444
445 445 msg = """No such commit exists for this repository"""
446 446 response.mustcontain(msg)
447 447
448 448 def test_download_file_wrong_f_path(self, backend):
449 449 commit = backend.repo.get_commit(commit_idx=173)
450 450 f_path = 'vcs/ERRORnodes.py'
451 451
452 452 response = self.app.get(
453 453 route_path('repo_file_download',
454 454 repo_name=backend.repo_name,
455 455 commit_id=commit.raw_id, f_path=f_path),
456 456 status=404)
457 457
458 458 msg = (
459 459 "There is no file nor directory at the given path: "
460 460 "`%s` at commit %s" % (f_path, commit.short_id))
461 461 response.mustcontain(msg)
462 462
463 463 def test_file_raw(self, backend):
464 464 commit = backend.repo.get_commit(commit_idx=173)
465 465 response = self.app.get(
466 466 route_path('repo_file_raw',
467 467 repo_name=backend.repo_name,
468 468 commit_id=commit.raw_id, f_path='vcs/nodes.py'),)
469 469
470 470 assert response.content_type == "text/plain"
471 471
472 472 def test_file_raw_binary(self, backend):
473 473 commit = backend.repo.get_commit()
474 474 response = self.app.get(
475 475 route_path('repo_file_raw',
476 476 repo_name=backend.repo_name,
477 477 commit_id=commit.raw_id,
478 478 f_path='docs/theme/ADC/static/breadcrumb_background.png'),)
479 479
480 480 assert response.content_disposition == 'inline'
481 481
482 482 def test_raw_file_wrong_cs(self, backend):
483 483 raw_id = u'ERRORcce30c96924232dffcd24178a07ffeb5dfc'
484 484
485 485 response = self.app.get(
486 486 route_path('repo_file_raw',
487 487 repo_name=backend.repo_name,
488 488 commit_id=raw_id, f_path='vcs/nodes.svg'),
489 489 status=404)
490 490
491 491 msg = """No such commit exists for this repository"""
492 492 response.mustcontain(msg)
493 493
494 494 def test_raw_wrong_f_path(self, backend):
495 495 commit = backend.repo.get_commit(commit_idx=173)
496 496 f_path = 'vcs/ERRORnodes.py'
497 497 response = self.app.get(
498 498 route_path('repo_file_raw',
499 499 repo_name=backend.repo_name,
500 500 commit_id=commit.raw_id, f_path=f_path),
501 501 status=404)
502 502
503 503 msg = (
504 504 "There is no file nor directory at the given path: "
505 505 "`%s` at commit %s" % (f_path, commit.short_id))
506 506 response.mustcontain(msg)
507 507
508 508 def test_raw_svg_should_not_be_rendered(self, backend):
509 509 backend.create_repo()
510 510 backend.ensure_file("xss.svg")
511 511 response = self.app.get(
512 512 route_path('repo_file_raw',
513 513 repo_name=backend.repo_name,
514 514 commit_id='tip', f_path='xss.svg'),)
515 515 # If the content type is image/svg+xml then it allows to render HTML
516 516 # and malicious SVG.
517 517 assert response.content_type == "text/plain"
518 518
519 519
520 520 @pytest.mark.usefixtures("app")
521 521 class TestRepositoryArchival(object):
522 522
523 523 def test_archival(self, backend):
524 524 backend.enable_downloads()
525 525 commit = backend.repo.get_commit(commit_idx=173)
526 526 for a_type, content_type, extension in settings.ARCHIVE_SPECS:
527 527
528 528 short = commit.short_id + extension
529 529 fname = commit.raw_id + extension
530 530 filename = '%s-%s' % (backend.repo_name, short)
531 531 response = self.app.get(
532 532 route_path('repo_archivefile',
533 533 repo_name=backend.repo_name,
534 534 fname=fname))
535 535
536 536 assert response.status == '200 OK'
537 537 headers = [
538 538 ('Content-Disposition', 'attachment; filename=%s' % filename),
539 539 ('Content-Type', '%s' % content_type),
540 540 ]
541 541
542 542 for header in headers:
543 543 assert header in response.headers.items()
544 544
545 545 @pytest.mark.parametrize('arch_ext',[
546 546 'tar', 'rar', 'x', '..ax', '.zipz', 'tar.gz.tar'])
547 547 def test_archival_wrong_ext(self, backend, arch_ext):
548 548 backend.enable_downloads()
549 549 commit = backend.repo.get_commit(commit_idx=173)
550 550
551 551 fname = commit.raw_id + '.' + arch_ext
552 552
553 553 response = self.app.get(
554 554 route_path('repo_archivefile',
555 555 repo_name=backend.repo_name,
556 556 fname=fname))
557 557 response.mustcontain(
558 558 'Unknown archive type for: `{}`'.format(fname))
559 559
560 560 @pytest.mark.parametrize('commit_id', [
561 561 '00x000000', 'tar', 'wrong', '@$@$42413232', '232dffcd'])
562 562 def test_archival_wrong_commit_id(self, backend, commit_id):
563 563 backend.enable_downloads()
564 564 fname = '%s.zip' % commit_id
565 565
566 566 response = self.app.get(
567 567 route_path('repo_archivefile',
568 568 repo_name=backend.repo_name,
569 569 fname=fname))
570 570 response.mustcontain('Unknown commit_id')
571 571
572 572
573 573 @pytest.mark.usefixtures("app")
574 574 class TestFilesDiff(object):
575 575
576 576 @pytest.mark.parametrize("diff", ['diff', 'download', 'raw'])
577 577 def test_file_full_diff(self, backend, diff):
578 578 commit1 = backend.repo.get_commit(commit_idx=-1)
579 579 commit2 = backend.repo.get_commit(commit_idx=-2)
580 580
581 581 response = self.app.get(
582 582 route_path('repo_files_diff',
583 583 repo_name=backend.repo_name,
584 584 f_path='README'),
585 585 params={
586 586 'diff1': commit2.raw_id,
587 587 'diff2': commit1.raw_id,
588 588 'fulldiff': '1',
589 589 'diff': diff,
590 590 })
591 591
592 592 if diff == 'diff':
593 593 # use redirect since this is OLD view redirecting to compare page
594 594 response = response.follow()
595 595
596 596 # It's a symlink to README.rst
597 597 response.mustcontain('README.rst')
598 598 response.mustcontain('No newline at end of file')
599 599
600 600 def test_file_binary_diff(self, backend):
601 601 commits = [
602 602 {'message': 'First commit'},
603 603 {'message': 'Commit with binary',
604 604 'added': [nodes.FileNode('file.bin', content='\0BINARY\0')]},
605 605 ]
606 606 repo = backend.create_repo(commits=commits)
607 607
608 608 response = self.app.get(
609 609 route_path('repo_files_diff',
610 610 repo_name=backend.repo_name,
611 611 f_path='file.bin'),
612 612 params={
613 613 'diff1': repo.get_commit(commit_idx=0).raw_id,
614 614 'diff2': repo.get_commit(commit_idx=1).raw_id,
615 615 'fulldiff': '1',
616 616 'diff': 'diff',
617 617 })
618 618 # use redirect since this is OLD view redirecting to compare page
619 619 response = response.follow()
620 620 response.mustcontain('Collapse 1 commit')
621 621 file_changes = (1, 0, 0)
622 622
623 623 compare_page = ComparePage(response)
624 624 compare_page.contains_change_summary(*file_changes)
625 625
626 626 if backend.alias == 'svn':
627 627 response.mustcontain('new file 10644')
628 628 # TODO(marcink): SVN doesn't yet detect binary changes
629 629 else:
630 630 response.mustcontain('new file 100644')
631 631 response.mustcontain('binary diff hidden')
632 632
633 633 def test_diff_2way(self, backend):
634 634 commit1 = backend.repo.get_commit(commit_idx=-1)
635 635 commit2 = backend.repo.get_commit(commit_idx=-2)
636 636 response = self.app.get(
637 637 route_path('repo_files_diff_2way_redirect',
638 638 repo_name=backend.repo_name,
639 639 f_path='README'),
640 640 params={
641 641 'diff1': commit2.raw_id,
642 642 'diff2': commit1.raw_id,
643 643 })
644 644 # use redirect since this is OLD view redirecting to compare page
645 645 response = response.follow()
646 646
647 647 # It's a symlink to README.rst
648 648 response.mustcontain('README.rst')
649 649 response.mustcontain('No newline at end of file')
650 650
651 651 def test_requires_one_commit_id(self, backend, autologin_user):
652 652 response = self.app.get(
653 653 route_path('repo_files_diff',
654 654 repo_name=backend.repo_name,
655 655 f_path='README.rst'),
656 656 status=400)
657 657 response.mustcontain(
658 658 'Need query parameter', 'diff1', 'diff2', 'to generate a diff.')
659 659
660 660 def test_returns_no_files_if_file_does_not_exist(self, vcsbackend):
661 661 repo = vcsbackend.repo
662 662 response = self.app.get(
663 663 route_path('repo_files_diff',
664 664 repo_name=repo.name,
665 665 f_path='does-not-exist-in-any-commit'),
666 666 params={
667 667 'diff1': repo[0].raw_id,
668 668 'diff2': repo[1].raw_id
669 669 })
670 670
671 671 response = response.follow()
672 672 response.mustcontain('No files')
673 673
674 674 def test_returns_redirect_if_file_not_changed(self, backend):
675 675 commit = backend.repo.get_commit(commit_idx=-1)
676 676 response = self.app.get(
677 677 route_path('repo_files_diff_2way_redirect',
678 678 repo_name=backend.repo_name,
679 679 f_path='README'),
680 680 params={
681 681 'diff1': commit.raw_id,
682 682 'diff2': commit.raw_id,
683 683 })
684 684
685 685 response = response.follow()
686 686 response.mustcontain('No files')
687 687 response.mustcontain('No commits in this compare')
688 688
689 689 def test_supports_diff_to_different_path_svn(self, backend_svn):
690 690 #TODO: check this case
691 691 return
692 692
693 693 repo = backend_svn['svn-simple-layout'].scm_instance()
694 694 commit_id_1 = '24'
695 695 commit_id_2 = '26'
696 696
697 697 response = self.app.get(
698 698 route_path('repo_files_diff',
699 699 repo_name=backend_svn.repo_name,
700 700 f_path='trunk/example.py'),
701 701 params={
702 702 'diff1': 'tags/v0.2/example.py@' + commit_id_1,
703 703 'diff2': commit_id_2,
704 704 })
705 705
706 706 response = response.follow()
707 707 response.mustcontain(
708 708 # diff contains this
709 709 "Will print out a useful message on invocation.")
710 710
711 711 # Note: Expecting that we indicate the user what's being compared
712 712 response.mustcontain("trunk/example.py")
713 713 response.mustcontain("tags/v0.2/example.py")
714 714
715 715 def test_show_rev_redirects_to_svn_path(self, backend_svn):
716 716 #TODO: check this case
717 717 return
718 718
719 719 repo = backend_svn['svn-simple-layout'].scm_instance()
720 720 commit_id = repo[-1].raw_id
721 721
722 722 response = self.app.get(
723 723 route_path('repo_files_diff',
724 724 repo_name=backend_svn.repo_name,
725 725 f_path='trunk/example.py'),
726 726 params={
727 727 'diff1': 'branches/argparse/example.py@' + commit_id,
728 728 'diff2': commit_id,
729 729 },
730 730 status=302)
731 731 response = response.follow()
732 732 assert response.headers['Location'].endswith(
733 733 'svn-svn-simple-layout/files/26/branches/argparse/example.py')
734 734
735 735 def test_show_rev_and_annotate_redirects_to_svn_path(self, backend_svn):
736 736 #TODO: check this case
737 737 return
738 738
739 739 repo = backend_svn['svn-simple-layout'].scm_instance()
740 740 commit_id = repo[-1].raw_id
741 741 response = self.app.get(
742 742 route_path('repo_files_diff',
743 743 repo_name=backend_svn.repo_name,
744 744 f_path='trunk/example.py'),
745 745 params={
746 746 'diff1': 'branches/argparse/example.py@' + commit_id,
747 747 'diff2': commit_id,
748 748 'show_rev': 'Show at Revision',
749 749 'annotate': 'true',
750 750 },
751 751 status=302)
752 752 response = response.follow()
753 753 assert response.headers['Location'].endswith(
754 754 'svn-svn-simple-layout/annotate/26/branches/argparse/example.py')
755 755
756 756
757 757 @pytest.mark.usefixtures("app", "autologin_user")
758 758 class TestModifyFilesWithWebInterface(object):
759 759
760 760 def test_add_file_view(self, backend):
761 761 self.app.get(
762 762 route_path('repo_files_add_file',
763 763 repo_name=backend.repo_name,
764 764 commit_id='tip', f_path='/')
765 765 )
766 766
767 767 @pytest.mark.xfail_backends("svn", reason="Depends on online editing")
768 768 def test_add_file_into_repo_missing_content(self, backend, csrf_token):
769 769 backend.create_repo()
770 770 filename = 'init.py'
771 771 response = self.app.post(
772 772 route_path('repo_files_create_file',
773 773 repo_name=backend.repo_name,
774 774 commit_id='tip', f_path='/'),
775 775 params={
776 776 'content': "",
777 777 'filename': filename,
778 778 'csrf_token': csrf_token,
779 779 },
780 780 status=302)
781 781 expected_msg = 'Successfully committed new file `{}`'.format(os.path.join(filename))
782 782 assert_session_flash(response, expected_msg)
783 783
784 784 def test_add_file_into_repo_missing_filename(self, backend, csrf_token):
785 785 commit_id = backend.repo.get_commit().raw_id
786 786 response = self.app.post(
787 787 route_path('repo_files_create_file',
788 788 repo_name=backend.repo_name,
789 789 commit_id=commit_id, f_path='/'),
790 790 params={
791 791 'content': "foo",
792 792 'csrf_token': csrf_token,
793 793 },
794 794 status=302)
795 795
796 796 assert_session_flash(response, 'No filename specified')
797 797
798 798 def test_add_file_into_repo_errors_and_no_commits(
799 799 self, backend, csrf_token):
800 800 repo = backend.create_repo()
801 801 # Create a file with no filename, it will display an error but
802 802 # the repo has no commits yet
803 803 response = self.app.post(
804 804 route_path('repo_files_create_file',
805 805 repo_name=repo.repo_name,
806 806 commit_id='tip', f_path='/'),
807 807 params={
808 808 'content': "foo",
809 809 'csrf_token': csrf_token,
810 810 },
811 811 status=302)
812 812
813 813 assert_session_flash(response, 'No filename specified')
814 814
815 815 # Not allowed, redirect to the summary
816 816 redirected = response.follow()
817 817 summary_url = h.route_path('repo_summary', repo_name=repo.repo_name)
818 818
819 819 # As there are no commits, displays the summary page with the error of
820 820 # creating a file with no filename
821 821
822 822 assert redirected.request.path == summary_url
823 823
824 824 @pytest.mark.parametrize("filename, clean_filename", [
825 825 ('/abs/foo', 'abs/foo'),
826 826 ('../rel/foo', 'rel/foo'),
827 827 ('file/../foo/foo', 'file/foo/foo'),
828 828 ])
829 829 def test_add_file_into_repo_bad_filenames(self, filename, clean_filename, backend, csrf_token):
830 830 repo = backend.create_repo()
831 831 commit_id = repo.get_commit().raw_id
832 832
833 833 response = self.app.post(
834 834 route_path('repo_files_create_file',
835 835 repo_name=repo.repo_name,
836 836 commit_id=commit_id, f_path='/'),
837 837 params={
838 838 'content': "foo",
839 839 'filename': filename,
840 840 'csrf_token': csrf_token,
841 841 },
842 842 status=302)
843 843
844 844 expected_msg = 'Successfully committed new file `{}`'.format(clean_filename)
845 845 assert_session_flash(response, expected_msg)
846 846
847 847 @pytest.mark.parametrize("cnt, filename, content", [
848 848 (1, 'foo.txt', "Content"),
849 849 (2, 'dir/foo.rst', "Content"),
850 850 (3, 'dir/foo-second.rst', "Content"),
851 851 (4, 'rel/dir/foo.bar', "Content"),
852 852 ])
853 853 def test_add_file_into_empty_repo(self, cnt, filename, content, backend, csrf_token):
854 854 repo = backend.create_repo()
855 855 commit_id = repo.get_commit().raw_id
856 856 response = self.app.post(
857 857 route_path('repo_files_create_file',
858 858 repo_name=repo.repo_name,
859 859 commit_id=commit_id, f_path='/'),
860 860 params={
861 861 'content': content,
862 862 'filename': filename,
863 863 'csrf_token': csrf_token,
864 864 },
865 865 status=302)
866 866
867 867 expected_msg = 'Successfully committed new file `{}`'.format(filename)
868 868 assert_session_flash(response, expected_msg)
869 869
870 870 def test_edit_file_view(self, backend):
871 871 response = self.app.get(
872 872 route_path('repo_files_edit_file',
873 873 repo_name=backend.repo_name,
874 874 commit_id=backend.default_head_id,
875 875 f_path='vcs/nodes.py'),
876 876 status=200)
877 877 response.mustcontain("Module holding everything related to vcs nodes.")
878 878
879 879 def test_edit_file_view_not_on_branch(self, backend):
880 880 repo = backend.create_repo()
881 881 backend.ensure_file("vcs/nodes.py")
882 882
883 883 response = self.app.get(
884 884 route_path('repo_files_edit_file',
885 885 repo_name=repo.repo_name,
886 886 commit_id='tip',
887 887 f_path='vcs/nodes.py'),
888 888 status=302)
889 889 assert_session_flash(
890 890 response, 'Cannot modify file. Given commit `tip` is not head of a branch.')
891 891
892 892 def test_edit_file_view_commit_changes(self, backend, csrf_token):
893 893 repo = backend.create_repo()
894 894 backend.ensure_file("vcs/nodes.py", content="print 'hello'")
895 895
896 896 response = self.app.post(
897 897 route_path('repo_files_update_file',
898 898 repo_name=repo.repo_name,
899 899 commit_id=backend.default_head_id,
900 900 f_path='vcs/nodes.py'),
901 901 params={
902 902 'content': "print 'hello world'",
903 903 'message': 'I committed',
904 904 'filename': "vcs/nodes.py",
905 905 'csrf_token': csrf_token,
906 906 },
907 907 status=302)
908 908 assert_session_flash(
909 909 response, 'Successfully committed changes to file `vcs/nodes.py`')
910 910 tip = repo.get_commit(commit_idx=-1)
911 911 assert tip.message == 'I committed'
912 912
913 913 def test_edit_file_view_commit_changes_default_message(self, backend,
914 914 csrf_token):
915 915 repo = backend.create_repo()
916 916 backend.ensure_file("vcs/nodes.py", content="print 'hello'")
917 917
918 918 commit_id = (
919 919 backend.default_branch_name or
920 920 backend.repo.scm_instance().commit_ids[-1])
921 921
922 922 response = self.app.post(
923 923 route_path('repo_files_update_file',
924 924 repo_name=repo.repo_name,
925 925 commit_id=commit_id,
926 926 f_path='vcs/nodes.py'),
927 927 params={
928 928 'content': "print 'hello world'",
929 929 'message': '',
930 930 'filename': "vcs/nodes.py",
931 931 'csrf_token': csrf_token,
932 932 },
933 933 status=302)
934 934 assert_session_flash(
935 935 response, 'Successfully committed changes to file `vcs/nodes.py`')
936 936 tip = repo.get_commit(commit_idx=-1)
937 937 assert tip.message == 'Edited file vcs/nodes.py via RhodeCode Enterprise'
938 938
939 939 def test_delete_file_view(self, backend):
940 940 self.app.get(
941 941 route_path('repo_files_remove_file',
942 942 repo_name=backend.repo_name,
943 943 commit_id=backend.default_head_id,
944 944 f_path='vcs/nodes.py'),
945 945 status=200)
946 946
947 947 def test_delete_file_view_not_on_branch(self, backend):
948 948 repo = backend.create_repo()
949 949 backend.ensure_file('vcs/nodes.py')
950 950
951 951 response = self.app.get(
952 952 route_path('repo_files_remove_file',
953 953 repo_name=repo.repo_name,
954 954 commit_id='tip',
955 955 f_path='vcs/nodes.py'),
956 956 status=302)
957 957 assert_session_flash(
958 958 response, 'Cannot modify file. Given commit `tip` is not head of a branch.')
959 959
960 960 def test_delete_file_view_commit_changes(self, backend, csrf_token):
961 961 repo = backend.create_repo()
962 962 backend.ensure_file("vcs/nodes.py")
963 963
964 964 response = self.app.post(
965 965 route_path('repo_files_delete_file',
966 966 repo_name=repo.repo_name,
967 967 commit_id=backend.default_head_id,
968 968 f_path='vcs/nodes.py'),
969 969 params={
970 970 'message': 'i commited',
971 971 'csrf_token': csrf_token,
972 972 },
973 973 status=302)
974 974 assert_session_flash(
975 975 response, 'Successfully deleted file `vcs/nodes.py`')
976 976
977 977
978 978 @pytest.mark.usefixtures("app")
979 979 class TestFilesViewOtherCases(object):
980 980
981 981 def test_access_empty_repo_redirect_to_summary_with_alert_write_perms(
982 982 self, backend_stub, autologin_regular_user, user_regular,
983 983 user_util):
984 984
985 985 repo = backend_stub.create_repo()
986 986 user_util.grant_user_permission_to_repo(
987 987 repo, user_regular, 'repository.write')
988 988 response = self.app.get(
989 989 route_path('repo_files',
990 990 repo_name=repo.repo_name,
991 991 commit_id='tip', f_path='/'))
992 992
993 993 repo_file_add_url = route_path(
994 994 'repo_files_add_file',
995 995 repo_name=repo.repo_name,
996 996 commit_id=0, f_path='')
997 997
998 998 assert_session_flash(
999 999 response,
1000 1000 'There are no files yet. <a class="alert-link" '
1001 1001 'href="{}">Click here to add a new file.</a>'
1002 1002 .format(repo_file_add_url))
1003 1003
1004 1004 def test_access_empty_repo_redirect_to_summary_with_alert_no_write_perms(
1005 1005 self, backend_stub, autologin_regular_user):
1006 1006 repo = backend_stub.create_repo()
1007 1007 # init session for anon user
1008 1008 route_path('repo_summary', repo_name=repo.repo_name)
1009 1009
1010 1010 repo_file_add_url = route_path(
1011 1011 'repo_files_add_file',
1012 1012 repo_name=repo.repo_name,
1013 1013 commit_id=0, f_path='')
1014 1014
1015 1015 response = self.app.get(
1016 1016 route_path('repo_files',
1017 1017 repo_name=repo.repo_name,
1018 1018 commit_id='tip', f_path='/'))
1019 1019
1020 1020 assert_session_flash(response, no_=repo_file_add_url)
1021 1021
1022 1022 @pytest.mark.parametrize('file_node', [
1023 1023 'archive/file.zip',
1024 1024 'diff/my-file.txt',
1025 1025 'render.py',
1026 1026 'render',
1027 1027 'remove_file',
1028 1028 'remove_file/to-delete.txt',
1029 1029 ])
1030 1030 def test_file_names_equal_to_routes_parts(self, backend, file_node):
1031 1031 backend.create_repo()
1032 1032 backend.ensure_file(file_node)
1033 1033
1034 1034 self.app.get(
1035 1035 route_path('repo_files',
1036 1036 repo_name=backend.repo_name,
1037 1037 commit_id='tip', f_path=file_node),
1038 1038 status=200)
1039 1039
1040 1040
1041 1041 class TestAdjustFilePathForSvn(object):
1042 1042 """
1043 1043 SVN specific adjustments of node history in RepoFilesView.
1044 1044 """
1045 1045
1046 1046 def test_returns_path_relative_to_matched_reference(self):
1047 1047 repo = self._repo(branches=['trunk'])
1048 1048 self.assert_file_adjustment('trunk/file', 'file', repo)
1049 1049
1050 1050 def test_does_not_modify_file_if_no_reference_matches(self):
1051 1051 repo = self._repo(branches=['trunk'])
1052 1052 self.assert_file_adjustment('notes/file', 'notes/file', repo)
1053 1053
1054 1054 def test_does_not_adjust_partial_directory_names(self):
1055 1055 repo = self._repo(branches=['trun'])
1056 1056 self.assert_file_adjustment('trunk/file', 'trunk/file', repo)
1057 1057
1058 1058 def test_is_robust_to_patterns_which_prefix_other_patterns(self):
1059 1059 repo = self._repo(branches=['trunk', 'trunk/new', 'trunk/old'])
1060 1060 self.assert_file_adjustment('trunk/new/file', 'file', repo)
1061 1061
1062 1062 def assert_file_adjustment(self, f_path, expected, repo):
1063 1063 result = RepoFilesView.adjust_file_path_for_svn(f_path, repo)
1064 1064 assert result == expected
1065 1065
1066 1066 def _repo(self, branches=None):
1067 1067 repo = mock.Mock()
1068 1068 repo.branches = OrderedDict((name, '0') for name in branches or [])
1069 1069 repo.tags = {}
1070 1070 return repo
@@ -1,1603 +1,1603 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import itertools
22 22 import logging
23 23 import os
24 24 import shutil
25 25 import tempfile
26 26 import collections
27 27 import urllib
28 28 import pathlib2
29 29
30 30 from pyramid.httpexceptions import HTTPNotFound, HTTPBadRequest, HTTPFound
31 31 from pyramid.view import view_config
32 32 from pyramid.renderers import render
33 33 from pyramid.response import Response
34 34
35 35 import rhodecode
36 36 from rhodecode.apps._base import RepoAppView
37 37
38 38
39 39 from rhodecode.lib import diffs, helpers as h, rc_cache
40 40 from rhodecode.lib import audit_logger
41 41 from rhodecode.lib.view_utils import parse_path_ref
42 42 from rhodecode.lib.exceptions import NonRelativePathError
43 43 from rhodecode.lib.codeblocks import (
44 44 filenode_as_lines_tokens, filenode_as_annotated_lines_tokens)
45 45 from rhodecode.lib.utils2 import (
46 46 convert_line_endings, detect_mode, safe_str, str2bool, safe_int, sha1, safe_unicode)
47 47 from rhodecode.lib.auth import (
48 48 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired)
49 49 from rhodecode.lib.vcs import path as vcspath
50 50 from rhodecode.lib.vcs.backends.base import EmptyCommit
51 51 from rhodecode.lib.vcs.conf import settings
52 52 from rhodecode.lib.vcs.nodes import FileNode
53 53 from rhodecode.lib.vcs.exceptions import (
54 54 RepositoryError, CommitDoesNotExistError, EmptyRepositoryError,
55 55 ImproperArchiveTypeError, VCSError, NodeAlreadyExistsError,
56 56 NodeDoesNotExistError, CommitError, NodeError)
57 57
58 58 from rhodecode.model.scm import ScmModel
59 59 from rhodecode.model.db import Repository
60 60
61 61 log = logging.getLogger(__name__)
62 62
63 63
64 64 class RepoFilesView(RepoAppView):
65 65
66 66 @staticmethod
67 67 def adjust_file_path_for_svn(f_path, repo):
68 68 """
69 69 Computes the relative path of `f_path`.
70 70
71 71 This is mainly based on prefix matching of the recognized tags and
72 72 branches in the underlying repository.
73 73 """
74 74 tags_and_branches = itertools.chain(
75 75 repo.branches.iterkeys(),
76 76 repo.tags.iterkeys())
77 77 tags_and_branches = sorted(tags_and_branches, key=len, reverse=True)
78 78
79 79 for name in tags_and_branches:
80 80 if f_path.startswith('{}/'.format(name)):
81 81 f_path = vcspath.relpath(f_path, name)
82 82 break
83 83 return f_path
84 84
85 85 def load_default_context(self):
86 86 c = self._get_local_tmpl_context(include_app_defaults=True)
87 87 c.rhodecode_repo = self.rhodecode_vcs_repo
88 88 c.enable_downloads = self.db_repo.enable_downloads
89 89 return c
90 90
91 91 def _ensure_not_locked(self, commit_id='tip'):
92 92 _ = self.request.translate
93 93
94 94 repo = self.db_repo
95 95 if repo.enable_locking and repo.locked[0]:
96 96 h.flash(_('This repository has been locked by %s on %s')
97 97 % (h.person_by_id(repo.locked[0]),
98 98 h.format_date(h.time_to_datetime(repo.locked[1]))),
99 99 'warning')
100 100 files_url = h.route_path(
101 101 'repo_files:default_path',
102 102 repo_name=self.db_repo_name, commit_id=commit_id)
103 103 raise HTTPFound(files_url)
104 104
105 105 def forbid_non_head(self, is_head, f_path, commit_id='tip', json_mode=False):
106 106 _ = self.request.translate
107 107
108 108 if not is_head:
109 109 message = _('Cannot modify file. '
110 110 'Given commit `{}` is not head of a branch.').format(commit_id)
111 111 h.flash(message, category='warning')
112 112
113 113 if json_mode:
114 114 return message
115 115
116 116 files_url = h.route_path(
117 117 'repo_files', repo_name=self.db_repo_name, commit_id=commit_id,
118 118 f_path=f_path)
119 119 raise HTTPFound(files_url)
120 120
121 121 def check_branch_permission(self, branch_name, commit_id='tip', json_mode=False):
122 122 _ = self.request.translate
123 123
124 124 rule, branch_perm = self._rhodecode_user.get_rule_and_branch_permission(
125 125 self.db_repo_name, branch_name)
126 126 if branch_perm and branch_perm not in ['branch.push', 'branch.push_force']:
127 127 message = _('Branch `{}` changes forbidden by rule {}.').format(
128 128 branch_name, rule)
129 129 h.flash(message, 'warning')
130 130
131 131 if json_mode:
132 132 return message
133 133
134 134 files_url = h.route_path(
135 135 'repo_files:default_path', repo_name=self.db_repo_name, commit_id=commit_id)
136 136
137 137 raise HTTPFound(files_url)
138 138
139 139 def _get_commit_and_path(self):
140 default_commit_id = self.db_repo.landing_rev[1]
140 default_commit_id = self.db_repo.landing_ref_name
141 141 default_f_path = '/'
142 142
143 143 commit_id = self.request.matchdict.get(
144 144 'commit_id', default_commit_id)
145 145 f_path = self._get_f_path(self.request.matchdict, default_f_path)
146 146 return commit_id, f_path
147 147
148 148 def _get_default_encoding(self, c):
149 149 enc_list = getattr(c, 'default_encodings', [])
150 150 return enc_list[0] if enc_list else 'UTF-8'
151 151
152 152 def _get_commit_or_redirect(self, commit_id, redirect_after=True):
153 153 """
154 154 This is a safe way to get commit. If an error occurs it redirects to
155 155 tip with proper message
156 156
157 157 :param commit_id: id of commit to fetch
158 158 :param redirect_after: toggle redirection
159 159 """
160 160 _ = self.request.translate
161 161
162 162 try:
163 163 return self.rhodecode_vcs_repo.get_commit(commit_id)
164 164 except EmptyRepositoryError:
165 165 if not redirect_after:
166 166 return None
167 167
168 168 _url = h.route_path(
169 169 'repo_files_add_file',
170 170 repo_name=self.db_repo_name, commit_id=0, f_path='')
171 171
172 172 if h.HasRepoPermissionAny(
173 173 'repository.write', 'repository.admin')(self.db_repo_name):
174 174 add_new = h.link_to(
175 175 _('Click here to add a new file.'), _url, class_="alert-link")
176 176 else:
177 177 add_new = ""
178 178
179 179 h.flash(h.literal(
180 180 _('There are no files yet. %s') % add_new), category='warning')
181 181 raise HTTPFound(
182 182 h.route_path('repo_summary', repo_name=self.db_repo_name))
183 183
184 184 except (CommitDoesNotExistError, LookupError):
185 185 msg = _('No such commit exists for this repository')
186 186 h.flash(msg, category='error')
187 187 raise HTTPNotFound()
188 188 except RepositoryError as e:
189 189 h.flash(safe_str(h.escape(e)), category='error')
190 190 raise HTTPNotFound()
191 191
192 192 def _get_filenode_or_redirect(self, commit_obj, path):
193 193 """
194 194 Returns file_node, if error occurs or given path is directory,
195 195 it'll redirect to top level path
196 196 """
197 197 _ = self.request.translate
198 198
199 199 try:
200 200 file_node = commit_obj.get_node(path)
201 201 if file_node.is_dir():
202 202 raise RepositoryError('The given path is a directory')
203 203 except CommitDoesNotExistError:
204 204 log.exception('No such commit exists for this repository')
205 205 h.flash(_('No such commit exists for this repository'), category='error')
206 206 raise HTTPNotFound()
207 207 except RepositoryError as e:
208 208 log.warning('Repository error while fetching filenode `%s`. Err:%s', path, e)
209 209 h.flash(safe_str(h.escape(e)), category='error')
210 210 raise HTTPNotFound()
211 211
212 212 return file_node
213 213
214 214 def _is_valid_head(self, commit_id, repo):
215 215 branch_name = sha_commit_id = ''
216 216 is_head = False
217 217 log.debug('Checking if commit_id `%s` is a head for %s.', commit_id, repo)
218 218
219 219 for _branch_name, branch_commit_id in repo.branches.items():
220 220 # simple case we pass in branch name, it's a HEAD
221 221 if commit_id == _branch_name:
222 222 is_head = True
223 223 branch_name = _branch_name
224 224 sha_commit_id = branch_commit_id
225 225 break
226 226 # case when we pass in full sha commit_id, which is a head
227 227 elif commit_id == branch_commit_id:
228 228 is_head = True
229 229 branch_name = _branch_name
230 230 sha_commit_id = branch_commit_id
231 231 break
232 232
233 233 if h.is_svn(repo) and not repo.is_empty():
234 234 # Note: Subversion only has one head.
235 235 if commit_id == repo.get_commit(commit_idx=-1).raw_id:
236 236 is_head = True
237 237 return branch_name, sha_commit_id, is_head
238 238
239 239 # checked branches, means we only need to try to get the branch/commit_sha
240 240 if not repo.is_empty():
241 241 commit = repo.get_commit(commit_id=commit_id)
242 242 if commit:
243 243 branch_name = commit.branch
244 244 sha_commit_id = commit.raw_id
245 245
246 246 return branch_name, sha_commit_id, is_head
247 247
248 248 def _get_tree_at_commit(self, c, commit_id, f_path, full_load=False, at_rev=None):
249 249
250 250 repo_id = self.db_repo.repo_id
251 251 force_recache = self.get_recache_flag()
252 252
253 253 cache_seconds = safe_int(
254 254 rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
255 255 cache_on = not force_recache and cache_seconds > 0
256 256 log.debug(
257 257 'Computing FILE TREE for repo_id %s commit_id `%s` and path `%s`'
258 258 'with caching: %s[TTL: %ss]' % (
259 259 repo_id, commit_id, f_path, cache_on, cache_seconds or 0))
260 260
261 261 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
262 262 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
263 263
264 264 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid, condition=cache_on)
265 265 def compute_file_tree(ver, _name_hash, _repo_id, _commit_id, _f_path, _full_load, _at_rev):
266 266 log.debug('Generating cached file tree at ver:%s for repo_id: %s, %s, %s',
267 267 ver, _repo_id, _commit_id, _f_path)
268 268
269 269 c.full_load = _full_load
270 270 return render(
271 271 'rhodecode:templates/files/files_browser_tree.mako',
272 272 self._get_template_context(c), self.request, _at_rev)
273 273
274 274 return compute_file_tree(
275 275 rc_cache.FILE_TREE_CACHE_VER, self.db_repo.repo_name_hash,
276 276 self.db_repo.repo_id, commit_id, f_path, full_load, at_rev)
277 277
278 278 def _get_archive_spec(self, fname):
279 279 log.debug('Detecting archive spec for: `%s`', fname)
280 280
281 281 fileformat = None
282 282 ext = None
283 283 content_type = None
284 284 for a_type, content_type, extension in settings.ARCHIVE_SPECS:
285 285
286 286 if fname.endswith(extension):
287 287 fileformat = a_type
288 288 log.debug('archive is of type: %s', fileformat)
289 289 ext = extension
290 290 break
291 291
292 292 if not fileformat:
293 293 raise ValueError()
294 294
295 295 # left over part of whole fname is the commit
296 296 commit_id = fname[:-len(ext)]
297 297
298 298 return commit_id, ext, fileformat, content_type
299 299
300 300 def create_pure_path(self, *parts):
301 301 # Split paths and sanitize them, removing any ../ etc
302 302 sanitized_path = [
303 303 x for x in pathlib2.PurePath(*parts).parts
304 304 if x not in ['.', '..']]
305 305
306 306 pure_path = pathlib2.PurePath(*sanitized_path)
307 307 return pure_path
308 308
309 309 def _is_lf_enabled(self, target_repo):
310 310 lf_enabled = False
311 311
312 312 lf_key_for_vcs_map = {
313 313 'hg': 'extensions_largefiles',
314 314 'git': 'vcs_git_lfs_enabled'
315 315 }
316 316
317 317 lf_key_for_vcs = lf_key_for_vcs_map.get(target_repo.repo_type)
318 318
319 319 if lf_key_for_vcs:
320 320 lf_enabled = self._get_repo_setting(target_repo, lf_key_for_vcs)
321 321
322 322 return lf_enabled
323 323
324 324 @LoginRequired()
325 325 @HasRepoPermissionAnyDecorator(
326 326 'repository.read', 'repository.write', 'repository.admin')
327 327 @view_config(
328 328 route_name='repo_archivefile', request_method='GET',
329 329 renderer=None)
330 330 def repo_archivefile(self):
331 331 # archive cache config
332 332 from rhodecode import CONFIG
333 333 _ = self.request.translate
334 334 self.load_default_context()
335 335 default_at_path = '/'
336 336 fname = self.request.matchdict['fname']
337 337 subrepos = self.request.GET.get('subrepos') == 'true'
338 338 at_path = self.request.GET.get('at_path') or default_at_path
339 339
340 340 if not self.db_repo.enable_downloads:
341 341 return Response(_('Downloads disabled'))
342 342
343 343 try:
344 344 commit_id, ext, fileformat, content_type = \
345 345 self._get_archive_spec(fname)
346 346 except ValueError:
347 347 return Response(_('Unknown archive type for: `{}`').format(
348 348 h.escape(fname)))
349 349
350 350 try:
351 351 commit = self.rhodecode_vcs_repo.get_commit(commit_id)
352 352 except CommitDoesNotExistError:
353 353 return Response(_('Unknown commit_id {}').format(
354 354 h.escape(commit_id)))
355 355 except EmptyRepositoryError:
356 356 return Response(_('Empty repository'))
357 357
358 358 try:
359 359 at_path = commit.get_node(at_path).path or default_at_path
360 360 except Exception:
361 361 return Response(_('No node at path {} for this repository').format(at_path))
362 362
363 363 path_sha = sha1(at_path)[:8]
364 364
365 365 # original backward compat name of archive
366 366 clean_name = safe_str(self.db_repo_name.replace('/', '_'))
367 367 short_sha = safe_str(commit.short_id)
368 368
369 369 if at_path == default_at_path:
370 370 archive_name = '{}-{}{}{}'.format(
371 371 clean_name,
372 372 '-sub' if subrepos else '',
373 373 short_sha,
374 374 ext)
375 375 # custom path and new name
376 376 else:
377 377 archive_name = '{}-{}{}-{}{}'.format(
378 378 clean_name,
379 379 '-sub' if subrepos else '',
380 380 short_sha,
381 381 path_sha,
382 382 ext)
383 383
384 384 use_cached_archive = False
385 385 archive_cache_enabled = CONFIG.get(
386 386 'archive_cache_dir') and not self.request.GET.get('no_cache')
387 387 cached_archive_path = None
388 388
389 389 if archive_cache_enabled:
390 390 # check if we it's ok to write
391 391 if not os.path.isdir(CONFIG['archive_cache_dir']):
392 392 os.makedirs(CONFIG['archive_cache_dir'])
393 393 cached_archive_path = os.path.join(
394 394 CONFIG['archive_cache_dir'], archive_name)
395 395 if os.path.isfile(cached_archive_path):
396 396 log.debug('Found cached archive in %s', cached_archive_path)
397 397 fd, archive = None, cached_archive_path
398 398 use_cached_archive = True
399 399 else:
400 400 log.debug('Archive %s is not yet cached', archive_name)
401 401
402 402 if not use_cached_archive:
403 403 # generate new archive
404 404 fd, archive = tempfile.mkstemp()
405 405 log.debug('Creating new temp archive in %s', archive)
406 406 try:
407 407 commit.archive_repo(archive, kind=fileformat, subrepos=subrepos,
408 408 archive_at_path=at_path)
409 409 except ImproperArchiveTypeError:
410 410 return _('Unknown archive type')
411 411 if archive_cache_enabled:
412 412 # if we generated the archive and we have cache enabled
413 413 # let's use this for future
414 414 log.debug('Storing new archive in %s', cached_archive_path)
415 415 shutil.move(archive, cached_archive_path)
416 416 archive = cached_archive_path
417 417
418 418 # store download action
419 419 audit_logger.store_web(
420 420 'repo.archive.download', action_data={
421 421 'user_agent': self.request.user_agent,
422 422 'archive_name': archive_name,
423 423 'archive_spec': fname,
424 424 'archive_cached': use_cached_archive},
425 425 user=self._rhodecode_user,
426 426 repo=self.db_repo,
427 427 commit=True
428 428 )
429 429
430 430 def get_chunked_archive(archive_path):
431 431 with open(archive_path, 'rb') as stream:
432 432 while True:
433 433 data = stream.read(16 * 1024)
434 434 if not data:
435 435 if fd: # fd means we used temporary file
436 436 os.close(fd)
437 437 if not archive_cache_enabled:
438 438 log.debug('Destroying temp archive %s', archive_path)
439 439 os.remove(archive_path)
440 440 break
441 441 yield data
442 442
443 443 response = Response(app_iter=get_chunked_archive(archive))
444 444 response.content_disposition = str(
445 445 'attachment; filename=%s' % archive_name)
446 446 response.content_type = str(content_type)
447 447
448 448 return response
449 449
450 450 def _get_file_node(self, commit_id, f_path):
451 451 if commit_id not in ['', None, 'None', '0' * 12, '0' * 40]:
452 452 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
453 453 try:
454 454 node = commit.get_node(f_path)
455 455 if node.is_dir():
456 456 raise NodeError('%s path is a %s not a file'
457 457 % (node, type(node)))
458 458 except NodeDoesNotExistError:
459 459 commit = EmptyCommit(
460 460 commit_id=commit_id,
461 461 idx=commit.idx,
462 462 repo=commit.repository,
463 463 alias=commit.repository.alias,
464 464 message=commit.message,
465 465 author=commit.author,
466 466 date=commit.date)
467 467 node = FileNode(f_path, '', commit=commit)
468 468 else:
469 469 commit = EmptyCommit(
470 470 repo=self.rhodecode_vcs_repo,
471 471 alias=self.rhodecode_vcs_repo.alias)
472 472 node = FileNode(f_path, '', commit=commit)
473 473 return node
474 474
475 475 @LoginRequired()
476 476 @HasRepoPermissionAnyDecorator(
477 477 'repository.read', 'repository.write', 'repository.admin')
478 478 @view_config(
479 479 route_name='repo_files_diff', request_method='GET',
480 480 renderer=None)
481 481 def repo_files_diff(self):
482 482 c = self.load_default_context()
483 483 f_path = self._get_f_path(self.request.matchdict)
484 484 diff1 = self.request.GET.get('diff1', '')
485 485 diff2 = self.request.GET.get('diff2', '')
486 486
487 487 path1, diff1 = parse_path_ref(diff1, default_path=f_path)
488 488
489 489 ignore_whitespace = str2bool(self.request.GET.get('ignorews'))
490 490 line_context = self.request.GET.get('context', 3)
491 491
492 492 if not any((diff1, diff2)):
493 493 h.flash(
494 494 'Need query parameter "diff1" or "diff2" to generate a diff.',
495 495 category='error')
496 496 raise HTTPBadRequest()
497 497
498 498 c.action = self.request.GET.get('diff')
499 499 if c.action not in ['download', 'raw']:
500 500 compare_url = h.route_path(
501 501 'repo_compare',
502 502 repo_name=self.db_repo_name,
503 503 source_ref_type='rev',
504 504 source_ref=diff1,
505 505 target_repo=self.db_repo_name,
506 506 target_ref_type='rev',
507 507 target_ref=diff2,
508 508 _query=dict(f_path=f_path))
509 509 # redirect to new view if we render diff
510 510 raise HTTPFound(compare_url)
511 511
512 512 try:
513 513 node1 = self._get_file_node(diff1, path1)
514 514 node2 = self._get_file_node(diff2, f_path)
515 515 except (RepositoryError, NodeError):
516 516 log.exception("Exception while trying to get node from repository")
517 517 raise HTTPFound(
518 518 h.route_path('repo_files', repo_name=self.db_repo_name,
519 519 commit_id='tip', f_path=f_path))
520 520
521 521 if all(isinstance(node.commit, EmptyCommit)
522 522 for node in (node1, node2)):
523 523 raise HTTPNotFound()
524 524
525 525 c.commit_1 = node1.commit
526 526 c.commit_2 = node2.commit
527 527
528 528 if c.action == 'download':
529 529 _diff = diffs.get_gitdiff(node1, node2,
530 530 ignore_whitespace=ignore_whitespace,
531 531 context=line_context)
532 532 diff = diffs.DiffProcessor(_diff, format='gitdiff')
533 533
534 534 response = Response(self.path_filter.get_raw_patch(diff))
535 535 response.content_type = 'text/plain'
536 536 response.content_disposition = (
537 537 'attachment; filename=%s_%s_vs_%s.diff' % (f_path, diff1, diff2)
538 538 )
539 539 charset = self._get_default_encoding(c)
540 540 if charset:
541 541 response.charset = charset
542 542 return response
543 543
544 544 elif c.action == 'raw':
545 545 _diff = diffs.get_gitdiff(node1, node2,
546 546 ignore_whitespace=ignore_whitespace,
547 547 context=line_context)
548 548 diff = diffs.DiffProcessor(_diff, format='gitdiff')
549 549
550 550 response = Response(self.path_filter.get_raw_patch(diff))
551 551 response.content_type = 'text/plain'
552 552 charset = self._get_default_encoding(c)
553 553 if charset:
554 554 response.charset = charset
555 555 return response
556 556
557 557 # in case we ever end up here
558 558 raise HTTPNotFound()
559 559
560 560 @LoginRequired()
561 561 @HasRepoPermissionAnyDecorator(
562 562 'repository.read', 'repository.write', 'repository.admin')
563 563 @view_config(
564 564 route_name='repo_files_diff_2way_redirect', request_method='GET',
565 565 renderer=None)
566 566 def repo_files_diff_2way_redirect(self):
567 567 """
568 568 Kept only to make OLD links work
569 569 """
570 570 f_path = self._get_f_path_unchecked(self.request.matchdict)
571 571 diff1 = self.request.GET.get('diff1', '')
572 572 diff2 = self.request.GET.get('diff2', '')
573 573
574 574 if not any((diff1, diff2)):
575 575 h.flash(
576 576 'Need query parameter "diff1" or "diff2" to generate a diff.',
577 577 category='error')
578 578 raise HTTPBadRequest()
579 579
580 580 compare_url = h.route_path(
581 581 'repo_compare',
582 582 repo_name=self.db_repo_name,
583 583 source_ref_type='rev',
584 584 source_ref=diff1,
585 585 target_ref_type='rev',
586 586 target_ref=diff2,
587 587 _query=dict(f_path=f_path, diffmode='sideside',
588 588 target_repo=self.db_repo_name,))
589 589 raise HTTPFound(compare_url)
590 590
591 591 @LoginRequired()
592 592 @HasRepoPermissionAnyDecorator(
593 593 'repository.read', 'repository.write', 'repository.admin')
594 594 @view_config(
595 595 route_name='repo_files', request_method='GET',
596 596 renderer=None)
597 597 @view_config(
598 598 route_name='repo_files:default_path', request_method='GET',
599 599 renderer=None)
600 600 @view_config(
601 601 route_name='repo_files:default_commit', request_method='GET',
602 602 renderer=None)
603 603 @view_config(
604 604 route_name='repo_files:rendered', request_method='GET',
605 605 renderer=None)
606 606 @view_config(
607 607 route_name='repo_files:annotated', request_method='GET',
608 608 renderer=None)
609 609 def repo_files(self):
610 610 c = self.load_default_context()
611 611
612 612 view_name = getattr(self.request.matched_route, 'name', None)
613 613
614 614 c.annotate = view_name == 'repo_files:annotated'
615 615 # default is false, but .rst/.md files later are auto rendered, we can
616 616 # overwrite auto rendering by setting this GET flag
617 617 c.renderer = view_name == 'repo_files:rendered' or \
618 618 not self.request.GET.get('no-render', False)
619 619
620 620 commit_id, f_path = self._get_commit_and_path()
621 621
622 622 c.commit = self._get_commit_or_redirect(commit_id)
623 623 c.branch = self.request.GET.get('branch', None)
624 624 c.f_path = f_path
625 625 at_rev = self.request.GET.get('at')
626 626
627 627 # prev link
628 628 try:
629 629 prev_commit = c.commit.prev(c.branch)
630 630 c.prev_commit = prev_commit
631 631 c.url_prev = h.route_path(
632 632 'repo_files', repo_name=self.db_repo_name,
633 633 commit_id=prev_commit.raw_id, f_path=f_path)
634 634 if c.branch:
635 635 c.url_prev += '?branch=%s' % c.branch
636 636 except (CommitDoesNotExistError, VCSError):
637 637 c.url_prev = '#'
638 638 c.prev_commit = EmptyCommit()
639 639
640 640 # next link
641 641 try:
642 642 next_commit = c.commit.next(c.branch)
643 643 c.next_commit = next_commit
644 644 c.url_next = h.route_path(
645 645 'repo_files', repo_name=self.db_repo_name,
646 646 commit_id=next_commit.raw_id, f_path=f_path)
647 647 if c.branch:
648 648 c.url_next += '?branch=%s' % c.branch
649 649 except (CommitDoesNotExistError, VCSError):
650 650 c.url_next = '#'
651 651 c.next_commit = EmptyCommit()
652 652
653 653 # files or dirs
654 654 try:
655 655 c.file = c.commit.get_node(f_path)
656 656 c.file_author = True
657 657 c.file_tree = ''
658 658
659 659 # load file content
660 660 if c.file.is_file():
661 661 c.lf_node = {}
662 662
663 663 has_lf_enabled = self._is_lf_enabled(self.db_repo)
664 664 if has_lf_enabled:
665 665 c.lf_node = c.file.get_largefile_node()
666 666
667 667 c.file_source_page = 'true'
668 668 c.file_last_commit = c.file.last_commit
669 669
670 670 c.file_size_too_big = c.file.size > c.visual.cut_off_limit_file
671 671
672 672 if not (c.file_size_too_big or c.file.is_binary):
673 673 if c.annotate: # annotation has precedence over renderer
674 674 c.annotated_lines = filenode_as_annotated_lines_tokens(
675 675 c.file
676 676 )
677 677 else:
678 678 c.renderer = (
679 679 c.renderer and h.renderer_from_filename(c.file.path)
680 680 )
681 681 if not c.renderer:
682 682 c.lines = filenode_as_lines_tokens(c.file)
683 683
684 684 _branch_name, _sha_commit_id, is_head = self._is_valid_head(
685 685 commit_id, self.rhodecode_vcs_repo)
686 686 c.on_branch_head = is_head
687 687
688 688 branch = c.commit.branch if (
689 689 c.commit.branch and '/' not in c.commit.branch) else None
690 690 c.branch_or_raw_id = branch or c.commit.raw_id
691 691 c.branch_name = c.commit.branch or h.short_id(c.commit.raw_id)
692 692
693 693 author = c.file_last_commit.author
694 694 c.authors = [[
695 695 h.email(author),
696 696 h.person(author, 'username_or_name_or_email'),
697 697 1
698 698 ]]
699 699
700 700 else: # load tree content at path
701 701 c.file_source_page = 'false'
702 702 c.authors = []
703 703 # this loads a simple tree without metadata to speed things up
704 704 # later via ajax we call repo_nodetree_full and fetch whole
705 705 c.file_tree = self._get_tree_at_commit(c, c.commit.raw_id, f_path, at_rev=at_rev)
706 706
707 707 c.readme_data, c.readme_file = \
708 708 self._get_readme_data(self.db_repo, c.visual.default_renderer,
709 709 c.commit.raw_id, f_path)
710 710
711 711 except RepositoryError as e:
712 712 h.flash(safe_str(h.escape(e)), category='error')
713 713 raise HTTPNotFound()
714 714
715 715 if self.request.environ.get('HTTP_X_PJAX'):
716 716 html = render('rhodecode:templates/files/files_pjax.mako',
717 717 self._get_template_context(c), self.request)
718 718 else:
719 719 html = render('rhodecode:templates/files/files.mako',
720 720 self._get_template_context(c), self.request)
721 721 return Response(html)
722 722
723 723 @HasRepoPermissionAnyDecorator(
724 724 'repository.read', 'repository.write', 'repository.admin')
725 725 @view_config(
726 726 route_name='repo_files:annotated_previous', request_method='GET',
727 727 renderer=None)
728 728 def repo_files_annotated_previous(self):
729 729 self.load_default_context()
730 730
731 731 commit_id, f_path = self._get_commit_and_path()
732 732 commit = self._get_commit_or_redirect(commit_id)
733 733 prev_commit_id = commit.raw_id
734 734 line_anchor = self.request.GET.get('line_anchor')
735 735 is_file = False
736 736 try:
737 737 _file = commit.get_node(f_path)
738 738 is_file = _file.is_file()
739 739 except (NodeDoesNotExistError, CommitDoesNotExistError, VCSError):
740 740 pass
741 741
742 742 if is_file:
743 743 history = commit.get_path_history(f_path)
744 744 prev_commit_id = history[1].raw_id \
745 745 if len(history) > 1 else prev_commit_id
746 746 prev_url = h.route_path(
747 747 'repo_files:annotated', repo_name=self.db_repo_name,
748 748 commit_id=prev_commit_id, f_path=f_path,
749 749 _anchor='L{}'.format(line_anchor))
750 750
751 751 raise HTTPFound(prev_url)
752 752
753 753 @LoginRequired()
754 754 @HasRepoPermissionAnyDecorator(
755 755 'repository.read', 'repository.write', 'repository.admin')
756 756 @view_config(
757 757 route_name='repo_nodetree_full', request_method='GET',
758 758 renderer=None, xhr=True)
759 759 @view_config(
760 760 route_name='repo_nodetree_full:default_path', request_method='GET',
761 761 renderer=None, xhr=True)
762 762 def repo_nodetree_full(self):
763 763 """
764 764 Returns rendered html of file tree that contains commit date,
765 765 author, commit_id for the specified combination of
766 766 repo, commit_id and file path
767 767 """
768 768 c = self.load_default_context()
769 769
770 770 commit_id, f_path = self._get_commit_and_path()
771 771 commit = self._get_commit_or_redirect(commit_id)
772 772 try:
773 773 dir_node = commit.get_node(f_path)
774 774 except RepositoryError as e:
775 775 return Response('error: {}'.format(h.escape(safe_str(e))))
776 776
777 777 if dir_node.is_file():
778 778 return Response('')
779 779
780 780 c.file = dir_node
781 781 c.commit = commit
782 782 at_rev = self.request.GET.get('at')
783 783
784 784 html = self._get_tree_at_commit(
785 785 c, commit.raw_id, dir_node.path, full_load=True, at_rev=at_rev)
786 786
787 787 return Response(html)
788 788
789 789 def _get_attachement_headers(self, f_path):
790 790 f_name = safe_str(f_path.split(Repository.NAME_SEP)[-1])
791 791 safe_path = f_name.replace('"', '\\"')
792 792 encoded_path = urllib.quote(f_name)
793 793
794 794 return "attachment; " \
795 795 "filename=\"{}\"; " \
796 796 "filename*=UTF-8\'\'{}".format(safe_path, encoded_path)
797 797
798 798 @LoginRequired()
799 799 @HasRepoPermissionAnyDecorator(
800 800 'repository.read', 'repository.write', 'repository.admin')
801 801 @view_config(
802 802 route_name='repo_file_raw', request_method='GET',
803 803 renderer=None)
804 804 def repo_file_raw(self):
805 805 """
806 806 Action for show as raw, some mimetypes are "rendered",
807 807 those include images, icons.
808 808 """
809 809 c = self.load_default_context()
810 810
811 811 commit_id, f_path = self._get_commit_and_path()
812 812 commit = self._get_commit_or_redirect(commit_id)
813 813 file_node = self._get_filenode_or_redirect(commit, f_path)
814 814
815 815 raw_mimetype_mapping = {
816 816 # map original mimetype to a mimetype used for "show as raw"
817 817 # you can also provide a content-disposition to override the
818 818 # default "attachment" disposition.
819 819 # orig_type: (new_type, new_dispo)
820 820
821 821 # show images inline:
822 822 # Do not re-add SVG: it is unsafe and permits XSS attacks. One can
823 823 # for example render an SVG with javascript inside or even render
824 824 # HTML.
825 825 'image/x-icon': ('image/x-icon', 'inline'),
826 826 'image/png': ('image/png', 'inline'),
827 827 'image/gif': ('image/gif', 'inline'),
828 828 'image/jpeg': ('image/jpeg', 'inline'),
829 829 'application/pdf': ('application/pdf', 'inline'),
830 830 }
831 831
832 832 mimetype = file_node.mimetype
833 833 try:
834 834 mimetype, disposition = raw_mimetype_mapping[mimetype]
835 835 except KeyError:
836 836 # we don't know anything special about this, handle it safely
837 837 if file_node.is_binary:
838 838 # do same as download raw for binary files
839 839 mimetype, disposition = 'application/octet-stream', 'attachment'
840 840 else:
841 841 # do not just use the original mimetype, but force text/plain,
842 842 # otherwise it would serve text/html and that might be unsafe.
843 843 # Note: underlying vcs library fakes text/plain mimetype if the
844 844 # mimetype can not be determined and it thinks it is not
845 845 # binary.This might lead to erroneous text display in some
846 846 # cases, but helps in other cases, like with text files
847 847 # without extension.
848 848 mimetype, disposition = 'text/plain', 'inline'
849 849
850 850 if disposition == 'attachment':
851 851 disposition = self._get_attachement_headers(f_path)
852 852
853 853 stream_content = file_node.stream_bytes()
854 854
855 855 response = Response(app_iter=stream_content)
856 856 response.content_disposition = disposition
857 857 response.content_type = mimetype
858 858
859 859 charset = self._get_default_encoding(c)
860 860 if charset:
861 861 response.charset = charset
862 862
863 863 return response
864 864
865 865 @LoginRequired()
866 866 @HasRepoPermissionAnyDecorator(
867 867 'repository.read', 'repository.write', 'repository.admin')
868 868 @view_config(
869 869 route_name='repo_file_download', request_method='GET',
870 870 renderer=None)
871 871 @view_config(
872 872 route_name='repo_file_download:legacy', request_method='GET',
873 873 renderer=None)
874 874 def repo_file_download(self):
875 875 c = self.load_default_context()
876 876
877 877 commit_id, f_path = self._get_commit_and_path()
878 878 commit = self._get_commit_or_redirect(commit_id)
879 879 file_node = self._get_filenode_or_redirect(commit, f_path)
880 880
881 881 if self.request.GET.get('lf'):
882 882 # only if lf get flag is passed, we download this file
883 883 # as LFS/Largefile
884 884 lf_node = file_node.get_largefile_node()
885 885 if lf_node:
886 886 # overwrite our pointer with the REAL large-file
887 887 file_node = lf_node
888 888
889 889 disposition = self._get_attachement_headers(f_path)
890 890
891 891 stream_content = file_node.stream_bytes()
892 892
893 893 response = Response(app_iter=stream_content)
894 894 response.content_disposition = disposition
895 895 response.content_type = file_node.mimetype
896 896
897 897 charset = self._get_default_encoding(c)
898 898 if charset:
899 899 response.charset = charset
900 900
901 901 return response
902 902
903 903 def _get_nodelist_at_commit(self, repo_name, repo_id, commit_id, f_path):
904 904
905 905 cache_seconds = safe_int(
906 906 rhodecode.CONFIG.get('rc_cache.cache_repo.expiration_time'))
907 907 cache_on = cache_seconds > 0
908 908 log.debug(
909 909 'Computing FILE SEARCH for repo_id %s commit_id `%s` and path `%s`'
910 910 'with caching: %s[TTL: %ss]' % (
911 911 repo_id, commit_id, f_path, cache_on, cache_seconds or 0))
912 912
913 913 cache_namespace_uid = 'cache_repo.{}'.format(repo_id)
914 914 region = rc_cache.get_or_create_region('cache_repo', cache_namespace_uid)
915 915
916 916 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid, condition=cache_on)
917 917 def compute_file_search(_name_hash, _repo_id, _commit_id, _f_path):
918 918 log.debug('Generating cached nodelist for repo_id:%s, %s, %s',
919 919 _repo_id, commit_id, f_path)
920 920 try:
921 921 _d, _f = ScmModel().get_quick_filter_nodes(repo_name, _commit_id, _f_path)
922 922 except (RepositoryError, CommitDoesNotExistError, Exception) as e:
923 923 log.exception(safe_str(e))
924 924 h.flash(safe_str(h.escape(e)), category='error')
925 925 raise HTTPFound(h.route_path(
926 926 'repo_files', repo_name=self.db_repo_name,
927 927 commit_id='tip', f_path='/'))
928 928
929 929 return _d + _f
930 930
931 931 result = compute_file_search(self.db_repo.repo_name_hash, self.db_repo.repo_id,
932 932 commit_id, f_path)
933 933 return filter(lambda n: self.path_filter.path_access_allowed(n['name']), result)
934 934
935 935 @LoginRequired()
936 936 @HasRepoPermissionAnyDecorator(
937 937 'repository.read', 'repository.write', 'repository.admin')
938 938 @view_config(
939 939 route_name='repo_files_nodelist', request_method='GET',
940 940 renderer='json_ext', xhr=True)
941 941 def repo_nodelist(self):
942 942 self.load_default_context()
943 943
944 944 commit_id, f_path = self._get_commit_and_path()
945 945 commit = self._get_commit_or_redirect(commit_id)
946 946
947 947 metadata = self._get_nodelist_at_commit(
948 948 self.db_repo_name, self.db_repo.repo_id, commit.raw_id, f_path)
949 949 return {'nodes': metadata}
950 950
951 951 def _create_references(self, branches_or_tags, symbolic_reference, f_path, ref_type):
952 952 items = []
953 953 for name, commit_id in branches_or_tags.items():
954 954 sym_ref = symbolic_reference(commit_id, name, f_path, ref_type)
955 955 items.append((sym_ref, name, ref_type))
956 956 return items
957 957
958 958 def _symbolic_reference(self, commit_id, name, f_path, ref_type):
959 959 return commit_id
960 960
961 961 def _symbolic_reference_svn(self, commit_id, name, f_path, ref_type):
962 962 return commit_id
963 963
964 964 # NOTE(dan): old code we used in "diff" mode compare
965 965 new_f_path = vcspath.join(name, f_path)
966 966 return u'%s@%s' % (new_f_path, commit_id)
967 967
968 968 def _get_node_history(self, commit_obj, f_path, commits=None):
969 969 """
970 970 get commit history for given node
971 971
972 972 :param commit_obj: commit to calculate history
973 973 :param f_path: path for node to calculate history for
974 974 :param commits: if passed don't calculate history and take
975 975 commits defined in this list
976 976 """
977 977 _ = self.request.translate
978 978
979 979 # calculate history based on tip
980 980 tip = self.rhodecode_vcs_repo.get_commit()
981 981 if commits is None:
982 982 pre_load = ["author", "branch"]
983 983 try:
984 984 commits = tip.get_path_history(f_path, pre_load=pre_load)
985 985 except (NodeDoesNotExistError, CommitError):
986 986 # this node is not present at tip!
987 987 commits = commit_obj.get_path_history(f_path, pre_load=pre_load)
988 988
989 989 history = []
990 990 commits_group = ([], _("Changesets"))
991 991 for commit in commits:
992 992 branch = ' (%s)' % commit.branch if commit.branch else ''
993 993 n_desc = 'r%s:%s%s' % (commit.idx, commit.short_id, branch)
994 994 commits_group[0].append((commit.raw_id, n_desc, 'sha'))
995 995 history.append(commits_group)
996 996
997 997 symbolic_reference = self._symbolic_reference
998 998
999 999 if self.rhodecode_vcs_repo.alias == 'svn':
1000 1000 adjusted_f_path = RepoFilesView.adjust_file_path_for_svn(
1001 1001 f_path, self.rhodecode_vcs_repo)
1002 1002 if adjusted_f_path != f_path:
1003 1003 log.debug(
1004 1004 'Recognized svn tag or branch in file "%s", using svn '
1005 1005 'specific symbolic references', f_path)
1006 1006 f_path = adjusted_f_path
1007 1007 symbolic_reference = self._symbolic_reference_svn
1008 1008
1009 1009 branches = self._create_references(
1010 1010 self.rhodecode_vcs_repo.branches, symbolic_reference, f_path, 'branch')
1011 1011 branches_group = (branches, _("Branches"))
1012 1012
1013 1013 tags = self._create_references(
1014 1014 self.rhodecode_vcs_repo.tags, symbolic_reference, f_path, 'tag')
1015 1015 tags_group = (tags, _("Tags"))
1016 1016
1017 1017 history.append(branches_group)
1018 1018 history.append(tags_group)
1019 1019
1020 1020 return history, commits
1021 1021
1022 1022 @LoginRequired()
1023 1023 @HasRepoPermissionAnyDecorator(
1024 1024 'repository.read', 'repository.write', 'repository.admin')
1025 1025 @view_config(
1026 1026 route_name='repo_file_history', request_method='GET',
1027 1027 renderer='json_ext')
1028 1028 def repo_file_history(self):
1029 1029 self.load_default_context()
1030 1030
1031 1031 commit_id, f_path = self._get_commit_and_path()
1032 1032 commit = self._get_commit_or_redirect(commit_id)
1033 1033 file_node = self._get_filenode_or_redirect(commit, f_path)
1034 1034
1035 1035 if file_node.is_file():
1036 1036 file_history, _hist = self._get_node_history(commit, f_path)
1037 1037
1038 1038 res = []
1039 1039 for section_items, section in file_history:
1040 1040 items = []
1041 1041 for obj_id, obj_text, obj_type in section_items:
1042 1042 at_rev = ''
1043 1043 if obj_type in ['branch', 'bookmark', 'tag']:
1044 1044 at_rev = obj_text
1045 1045 entry = {
1046 1046 'id': obj_id,
1047 1047 'text': obj_text,
1048 1048 'type': obj_type,
1049 1049 'at_rev': at_rev
1050 1050 }
1051 1051
1052 1052 items.append(entry)
1053 1053
1054 1054 res.append({
1055 1055 'text': section,
1056 1056 'children': items
1057 1057 })
1058 1058
1059 1059 data = {
1060 1060 'more': False,
1061 1061 'results': res
1062 1062 }
1063 1063 return data
1064 1064
1065 1065 log.warning('Cannot fetch history for directory')
1066 1066 raise HTTPBadRequest()
1067 1067
1068 1068 @LoginRequired()
1069 1069 @HasRepoPermissionAnyDecorator(
1070 1070 'repository.read', 'repository.write', 'repository.admin')
1071 1071 @view_config(
1072 1072 route_name='repo_file_authors', request_method='GET',
1073 1073 renderer='rhodecode:templates/files/file_authors_box.mako')
1074 1074 def repo_file_authors(self):
1075 1075 c = self.load_default_context()
1076 1076
1077 1077 commit_id, f_path = self._get_commit_and_path()
1078 1078 commit = self._get_commit_or_redirect(commit_id)
1079 1079 file_node = self._get_filenode_or_redirect(commit, f_path)
1080 1080
1081 1081 if not file_node.is_file():
1082 1082 raise HTTPBadRequest()
1083 1083
1084 1084 c.file_last_commit = file_node.last_commit
1085 1085 if self.request.GET.get('annotate') == '1':
1086 1086 # use _hist from annotation if annotation mode is on
1087 1087 commit_ids = set(x[1] for x in file_node.annotate)
1088 1088 _hist = (
1089 1089 self.rhodecode_vcs_repo.get_commit(commit_id)
1090 1090 for commit_id in commit_ids)
1091 1091 else:
1092 1092 _f_history, _hist = self._get_node_history(commit, f_path)
1093 1093 c.file_author = False
1094 1094
1095 1095 unique = collections.OrderedDict()
1096 1096 for commit in _hist:
1097 1097 author = commit.author
1098 1098 if author not in unique:
1099 1099 unique[commit.author] = [
1100 1100 h.email(author),
1101 1101 h.person(author, 'username_or_name_or_email'),
1102 1102 1 # counter
1103 1103 ]
1104 1104
1105 1105 else:
1106 1106 # increase counter
1107 1107 unique[commit.author][2] += 1
1108 1108
1109 1109 c.authors = [val for val in unique.values()]
1110 1110
1111 1111 return self._get_template_context(c)
1112 1112
1113 1113 @LoginRequired()
1114 1114 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1115 1115 @view_config(
1116 1116 route_name='repo_files_check_head', request_method='POST',
1117 1117 renderer='json_ext', xhr=True)
1118 1118 def repo_files_check_head(self):
1119 1119 self.load_default_context()
1120 1120
1121 1121 commit_id, f_path = self._get_commit_and_path()
1122 1122 _branch_name, _sha_commit_id, is_head = \
1123 1123 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1124 1124
1125 1125 new_path = self.request.POST.get('path')
1126 1126 operation = self.request.POST.get('operation')
1127 1127 path_exist = ''
1128 1128
1129 1129 if new_path and operation in ['create', 'upload']:
1130 1130 new_f_path = os.path.join(f_path.lstrip('/'), new_path)
1131 1131 try:
1132 1132 commit_obj = self.rhodecode_vcs_repo.get_commit(commit_id)
1133 1133 # NOTE(dan): construct whole path without leading /
1134 1134 file_node = commit_obj.get_node(new_f_path)
1135 1135 if file_node is not None:
1136 1136 path_exist = new_f_path
1137 1137 except EmptyRepositoryError:
1138 1138 pass
1139 1139 except Exception:
1140 1140 pass
1141 1141
1142 1142 return {
1143 1143 'branch': _branch_name,
1144 1144 'sha': _sha_commit_id,
1145 1145 'is_head': is_head,
1146 1146 'path_exists': path_exist
1147 1147 }
1148 1148
1149 1149 @LoginRequired()
1150 1150 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1151 1151 @view_config(
1152 1152 route_name='repo_files_remove_file', request_method='GET',
1153 1153 renderer='rhodecode:templates/files/files_delete.mako')
1154 1154 def repo_files_remove_file(self):
1155 1155 _ = self.request.translate
1156 1156 c = self.load_default_context()
1157 1157 commit_id, f_path = self._get_commit_and_path()
1158 1158
1159 1159 self._ensure_not_locked()
1160 1160 _branch_name, _sha_commit_id, is_head = \
1161 1161 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1162 1162
1163 1163 self.forbid_non_head(is_head, f_path)
1164 1164 self.check_branch_permission(_branch_name)
1165 1165
1166 1166 c.commit = self._get_commit_or_redirect(commit_id)
1167 1167 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1168 1168
1169 1169 c.default_message = _(
1170 1170 'Deleted file {} via RhodeCode Enterprise').format(f_path)
1171 1171 c.f_path = f_path
1172 1172
1173 1173 return self._get_template_context(c)
1174 1174
1175 1175 @LoginRequired()
1176 1176 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1177 1177 @CSRFRequired()
1178 1178 @view_config(
1179 1179 route_name='repo_files_delete_file', request_method='POST',
1180 1180 renderer=None)
1181 1181 def repo_files_delete_file(self):
1182 1182 _ = self.request.translate
1183 1183
1184 1184 c = self.load_default_context()
1185 1185 commit_id, f_path = self._get_commit_and_path()
1186 1186
1187 1187 self._ensure_not_locked()
1188 1188 _branch_name, _sha_commit_id, is_head = \
1189 1189 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1190 1190
1191 1191 self.forbid_non_head(is_head, f_path)
1192 1192 self.check_branch_permission(_branch_name)
1193 1193
1194 1194 c.commit = self._get_commit_or_redirect(commit_id)
1195 1195 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1196 1196
1197 1197 c.default_message = _(
1198 1198 'Deleted file {} via RhodeCode Enterprise').format(f_path)
1199 1199 c.f_path = f_path
1200 1200 node_path = f_path
1201 1201 author = self._rhodecode_db_user.full_contact
1202 1202 message = self.request.POST.get('message') or c.default_message
1203 1203 try:
1204 1204 nodes = {
1205 1205 node_path: {
1206 1206 'content': ''
1207 1207 }
1208 1208 }
1209 1209 ScmModel().delete_nodes(
1210 1210 user=self._rhodecode_db_user.user_id, repo=self.db_repo,
1211 1211 message=message,
1212 1212 nodes=nodes,
1213 1213 parent_commit=c.commit,
1214 1214 author=author,
1215 1215 )
1216 1216
1217 1217 h.flash(
1218 1218 _('Successfully deleted file `{}`').format(
1219 1219 h.escape(f_path)), category='success')
1220 1220 except Exception:
1221 1221 log.exception('Error during commit operation')
1222 1222 h.flash(_('Error occurred during commit'), category='error')
1223 1223 raise HTTPFound(
1224 1224 h.route_path('repo_commit', repo_name=self.db_repo_name,
1225 1225 commit_id='tip'))
1226 1226
1227 1227 @LoginRequired()
1228 1228 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1229 1229 @view_config(
1230 1230 route_name='repo_files_edit_file', request_method='GET',
1231 1231 renderer='rhodecode:templates/files/files_edit.mako')
1232 1232 def repo_files_edit_file(self):
1233 1233 _ = self.request.translate
1234 1234 c = self.load_default_context()
1235 1235 commit_id, f_path = self._get_commit_and_path()
1236 1236
1237 1237 self._ensure_not_locked()
1238 1238 _branch_name, _sha_commit_id, is_head = \
1239 1239 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1240 1240
1241 1241 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1242 1242 self.check_branch_permission(_branch_name, commit_id=commit_id)
1243 1243
1244 1244 c.commit = self._get_commit_or_redirect(commit_id)
1245 1245 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1246 1246
1247 1247 if c.file.is_binary:
1248 1248 files_url = h.route_path(
1249 1249 'repo_files',
1250 1250 repo_name=self.db_repo_name,
1251 1251 commit_id=c.commit.raw_id, f_path=f_path)
1252 1252 raise HTTPFound(files_url)
1253 1253
1254 1254 c.default_message = _('Edited file {} via RhodeCode Enterprise').format(f_path)
1255 1255 c.f_path = f_path
1256 1256
1257 1257 return self._get_template_context(c)
1258 1258
1259 1259 @LoginRequired()
1260 1260 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1261 1261 @CSRFRequired()
1262 1262 @view_config(
1263 1263 route_name='repo_files_update_file', request_method='POST',
1264 1264 renderer=None)
1265 1265 def repo_files_update_file(self):
1266 1266 _ = self.request.translate
1267 1267 c = self.load_default_context()
1268 1268 commit_id, f_path = self._get_commit_and_path()
1269 1269
1270 1270 self._ensure_not_locked()
1271 1271
1272 1272 c.commit = self._get_commit_or_redirect(commit_id)
1273 1273 c.file = self._get_filenode_or_redirect(c.commit, f_path)
1274 1274
1275 1275 if c.file.is_binary:
1276 1276 raise HTTPFound(h.route_path('repo_files', repo_name=self.db_repo_name,
1277 1277 commit_id=c.commit.raw_id, f_path=f_path))
1278 1278
1279 1279 _branch_name, _sha_commit_id, is_head = \
1280 1280 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1281 1281
1282 1282 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1283 1283 self.check_branch_permission(_branch_name, commit_id=commit_id)
1284 1284
1285 1285 c.default_message = _('Edited file {} via RhodeCode Enterprise').format(f_path)
1286 1286 c.f_path = f_path
1287 1287
1288 1288 old_content = c.file.content
1289 1289 sl = old_content.splitlines(1)
1290 1290 first_line = sl[0] if sl else ''
1291 1291
1292 1292 r_post = self.request.POST
1293 1293 # line endings: 0 - Unix, 1 - Mac, 2 - DOS
1294 1294 line_ending_mode = detect_mode(first_line, 0)
1295 1295 content = convert_line_endings(r_post.get('content', ''), line_ending_mode)
1296 1296
1297 1297 message = r_post.get('message') or c.default_message
1298 1298 org_node_path = c.file.unicode_path
1299 1299 filename = r_post['filename']
1300 1300
1301 1301 root_path = c.file.dir_path
1302 1302 pure_path = self.create_pure_path(root_path, filename)
1303 1303 node_path = safe_unicode(bytes(pure_path))
1304 1304
1305 1305 default_redirect_url = h.route_path('repo_commit', repo_name=self.db_repo_name,
1306 1306 commit_id=commit_id)
1307 1307 if content == old_content and node_path == org_node_path:
1308 1308 h.flash(_('No changes detected on {}').format(h.escape(org_node_path)),
1309 1309 category='warning')
1310 1310 raise HTTPFound(default_redirect_url)
1311 1311
1312 1312 try:
1313 1313 mapping = {
1314 1314 org_node_path: {
1315 1315 'org_filename': org_node_path,
1316 1316 'filename': node_path,
1317 1317 'content': content,
1318 1318 'lexer': '',
1319 1319 'op': 'mod',
1320 1320 'mode': c.file.mode
1321 1321 }
1322 1322 }
1323 1323
1324 1324 commit = ScmModel().update_nodes(
1325 1325 user=self._rhodecode_db_user.user_id,
1326 1326 repo=self.db_repo,
1327 1327 message=message,
1328 1328 nodes=mapping,
1329 1329 parent_commit=c.commit,
1330 1330 )
1331 1331
1332 1332 h.flash(_('Successfully committed changes to file `{}`').format(
1333 1333 h.escape(f_path)), category='success')
1334 1334 default_redirect_url = h.route_path(
1335 1335 'repo_commit', repo_name=self.db_repo_name, commit_id=commit.raw_id)
1336 1336
1337 1337 except Exception:
1338 1338 log.exception('Error occurred during commit')
1339 1339 h.flash(_('Error occurred during commit'), category='error')
1340 1340
1341 1341 raise HTTPFound(default_redirect_url)
1342 1342
1343 1343 @LoginRequired()
1344 1344 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1345 1345 @view_config(
1346 1346 route_name='repo_files_add_file', request_method='GET',
1347 1347 renderer='rhodecode:templates/files/files_add.mako')
1348 1348 @view_config(
1349 1349 route_name='repo_files_upload_file', request_method='GET',
1350 1350 renderer='rhodecode:templates/files/files_upload.mako')
1351 1351 def repo_files_add_file(self):
1352 1352 _ = self.request.translate
1353 1353 c = self.load_default_context()
1354 1354 commit_id, f_path = self._get_commit_and_path()
1355 1355
1356 1356 self._ensure_not_locked()
1357 1357
1358 1358 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1359 1359 if c.commit is None:
1360 1360 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1361 1361
1362 1362 if self.rhodecode_vcs_repo.is_empty():
1363 1363 # for empty repository we cannot check for current branch, we rely on
1364 1364 # c.commit.branch instead
1365 1365 _branch_name, _sha_commit_id, is_head = c.commit.branch, '', True
1366 1366 else:
1367 1367 _branch_name, _sha_commit_id, is_head = \
1368 1368 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1369 1369
1370 1370 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1371 1371 self.check_branch_permission(_branch_name, commit_id=commit_id)
1372 1372
1373 1373 c.default_message = (_('Added file via RhodeCode Enterprise'))
1374 1374 c.f_path = f_path.lstrip('/') # ensure not relative path
1375 1375
1376 1376 return self._get_template_context(c)
1377 1377
1378 1378 @LoginRequired()
1379 1379 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1380 1380 @CSRFRequired()
1381 1381 @view_config(
1382 1382 route_name='repo_files_create_file', request_method='POST',
1383 1383 renderer=None)
1384 1384 def repo_files_create_file(self):
1385 1385 _ = self.request.translate
1386 1386 c = self.load_default_context()
1387 1387 commit_id, f_path = self._get_commit_and_path()
1388 1388
1389 1389 self._ensure_not_locked()
1390 1390
1391 1391 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1392 1392 if c.commit is None:
1393 1393 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1394 1394
1395 1395 # calculate redirect URL
1396 1396 if self.rhodecode_vcs_repo.is_empty():
1397 1397 default_redirect_url = h.route_path(
1398 1398 'repo_summary', repo_name=self.db_repo_name)
1399 1399 else:
1400 1400 default_redirect_url = h.route_path(
1401 1401 'repo_commit', repo_name=self.db_repo_name, commit_id='tip')
1402 1402
1403 1403 if self.rhodecode_vcs_repo.is_empty():
1404 1404 # for empty repository we cannot check for current branch, we rely on
1405 1405 # c.commit.branch instead
1406 1406 _branch_name, _sha_commit_id, is_head = c.commit.branch, '', True
1407 1407 else:
1408 1408 _branch_name, _sha_commit_id, is_head = \
1409 1409 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1410 1410
1411 1411 self.forbid_non_head(is_head, f_path, commit_id=commit_id)
1412 1412 self.check_branch_permission(_branch_name, commit_id=commit_id)
1413 1413
1414 1414 c.default_message = (_('Added file via RhodeCode Enterprise'))
1415 1415 c.f_path = f_path
1416 1416
1417 1417 r_post = self.request.POST
1418 1418 message = r_post.get('message') or c.default_message
1419 1419 filename = r_post.get('filename')
1420 1420 unix_mode = 0
1421 1421 content = convert_line_endings(r_post.get('content', ''), unix_mode)
1422 1422
1423 1423 if not filename:
1424 1424 # If there's no commit, redirect to repo summary
1425 1425 if type(c.commit) is EmptyCommit:
1426 1426 redirect_url = h.route_path(
1427 1427 'repo_summary', repo_name=self.db_repo_name)
1428 1428 else:
1429 1429 redirect_url = default_redirect_url
1430 1430 h.flash(_('No filename specified'), category='warning')
1431 1431 raise HTTPFound(redirect_url)
1432 1432
1433 1433 root_path = f_path
1434 1434 pure_path = self.create_pure_path(root_path, filename)
1435 1435 node_path = safe_unicode(bytes(pure_path).lstrip('/'))
1436 1436
1437 1437 author = self._rhodecode_db_user.full_contact
1438 1438 nodes = {
1439 1439 node_path: {
1440 1440 'content': content
1441 1441 }
1442 1442 }
1443 1443
1444 1444 try:
1445 1445
1446 1446 commit = ScmModel().create_nodes(
1447 1447 user=self._rhodecode_db_user.user_id,
1448 1448 repo=self.db_repo,
1449 1449 message=message,
1450 1450 nodes=nodes,
1451 1451 parent_commit=c.commit,
1452 1452 author=author,
1453 1453 )
1454 1454
1455 1455 h.flash(_('Successfully committed new file `{}`').format(
1456 1456 h.escape(node_path)), category='success')
1457 1457
1458 1458 default_redirect_url = h.route_path(
1459 1459 'repo_commit', repo_name=self.db_repo_name, commit_id=commit.raw_id)
1460 1460
1461 1461 except NonRelativePathError:
1462 1462 log.exception('Non Relative path found')
1463 1463 h.flash(_('The location specified must be a relative path and must not '
1464 1464 'contain .. in the path'), category='warning')
1465 1465 raise HTTPFound(default_redirect_url)
1466 1466 except (NodeError, NodeAlreadyExistsError) as e:
1467 1467 h.flash(_(h.escape(e)), category='error')
1468 1468 except Exception:
1469 1469 log.exception('Error occurred during commit')
1470 1470 h.flash(_('Error occurred during commit'), category='error')
1471 1471
1472 1472 raise HTTPFound(default_redirect_url)
1473 1473
1474 1474 @LoginRequired()
1475 1475 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
1476 1476 @CSRFRequired()
1477 1477 @view_config(
1478 1478 route_name='repo_files_upload_file', request_method='POST',
1479 1479 renderer='json_ext')
1480 1480 def repo_files_upload_file(self):
1481 1481 _ = self.request.translate
1482 1482 c = self.load_default_context()
1483 1483 commit_id, f_path = self._get_commit_and_path()
1484 1484
1485 1485 self._ensure_not_locked()
1486 1486
1487 1487 c.commit = self._get_commit_or_redirect(commit_id, redirect_after=False)
1488 1488 if c.commit is None:
1489 1489 c.commit = EmptyCommit(alias=self.rhodecode_vcs_repo.alias)
1490 1490
1491 1491 # calculate redirect URL
1492 1492 if self.rhodecode_vcs_repo.is_empty():
1493 1493 default_redirect_url = h.route_path(
1494 1494 'repo_summary', repo_name=self.db_repo_name)
1495 1495 else:
1496 1496 default_redirect_url = h.route_path(
1497 1497 'repo_commit', repo_name=self.db_repo_name, commit_id='tip')
1498 1498
1499 1499 if self.rhodecode_vcs_repo.is_empty():
1500 1500 # for empty repository we cannot check for current branch, we rely on
1501 1501 # c.commit.branch instead
1502 1502 _branch_name, _sha_commit_id, is_head = c.commit.branch, '', True
1503 1503 else:
1504 1504 _branch_name, _sha_commit_id, is_head = \
1505 1505 self._is_valid_head(commit_id, self.rhodecode_vcs_repo)
1506 1506
1507 1507 error = self.forbid_non_head(is_head, f_path, json_mode=True)
1508 1508 if error:
1509 1509 return {
1510 1510 'error': error,
1511 1511 'redirect_url': default_redirect_url
1512 1512 }
1513 1513 error = self.check_branch_permission(_branch_name, json_mode=True)
1514 1514 if error:
1515 1515 return {
1516 1516 'error': error,
1517 1517 'redirect_url': default_redirect_url
1518 1518 }
1519 1519
1520 1520 c.default_message = (_('Uploaded file via RhodeCode Enterprise'))
1521 1521 c.f_path = f_path
1522 1522
1523 1523 r_post = self.request.POST
1524 1524
1525 1525 message = c.default_message
1526 1526 user_message = r_post.getall('message')
1527 1527 if isinstance(user_message, list) and user_message:
1528 1528 # we take the first from duplicated results if it's not empty
1529 1529 message = user_message[0] if user_message[0] else message
1530 1530
1531 1531 nodes = {}
1532 1532
1533 1533 for file_obj in r_post.getall('files_upload') or []:
1534 1534 content = file_obj.file
1535 1535 filename = file_obj.filename
1536 1536
1537 1537 root_path = f_path
1538 1538 pure_path = self.create_pure_path(root_path, filename)
1539 1539 node_path = safe_unicode(bytes(pure_path).lstrip('/'))
1540 1540
1541 1541 nodes[node_path] = {
1542 1542 'content': content
1543 1543 }
1544 1544
1545 1545 if not nodes:
1546 1546 error = 'missing files'
1547 1547 return {
1548 1548 'error': error,
1549 1549 'redirect_url': default_redirect_url
1550 1550 }
1551 1551
1552 1552 author = self._rhodecode_db_user.full_contact
1553 1553
1554 1554 try:
1555 1555 commit = ScmModel().create_nodes(
1556 1556 user=self._rhodecode_db_user.user_id,
1557 1557 repo=self.db_repo,
1558 1558 message=message,
1559 1559 nodes=nodes,
1560 1560 parent_commit=c.commit,
1561 1561 author=author,
1562 1562 )
1563 1563 if len(nodes) == 1:
1564 1564 flash_message = _('Successfully committed {} new files').format(len(nodes))
1565 1565 else:
1566 1566 flash_message = _('Successfully committed 1 new file')
1567 1567
1568 1568 h.flash(flash_message, category='success')
1569 1569
1570 1570 default_redirect_url = h.route_path(
1571 1571 'repo_commit', repo_name=self.db_repo_name, commit_id=commit.raw_id)
1572 1572
1573 1573 except NonRelativePathError:
1574 1574 log.exception('Non Relative path found')
1575 1575 error = _('The location specified must be a relative path and must not '
1576 1576 'contain .. in the path')
1577 1577 h.flash(error, category='warning')
1578 1578
1579 1579 return {
1580 1580 'error': error,
1581 1581 'redirect_url': default_redirect_url
1582 1582 }
1583 1583 except (NodeError, NodeAlreadyExistsError) as e:
1584 1584 error = h.escape(e)
1585 1585 h.flash(error, category='error')
1586 1586
1587 1587 return {
1588 1588 'error': error,
1589 1589 'redirect_url': default_redirect_url
1590 1590 }
1591 1591 except Exception:
1592 1592 log.exception('Error occurred during commit')
1593 1593 error = _('Error occurred during commit')
1594 1594 h.flash(error, category='error')
1595 1595 return {
1596 1596 'error': error,
1597 1597 'redirect_url': default_redirect_url
1598 1598 }
1599 1599
1600 1600 return {
1601 1601 'error': None,
1602 1602 'redirect_url': default_redirect_url
1603 1603 }
@@ -1,266 +1,266 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22 import datetime
23 23 import formencode
24 24 import formencode.htmlfill
25 25
26 26 from pyramid.httpexceptions import HTTPFound
27 27 from pyramid.view import view_config
28 28 from pyramid.renderers import render
29 29 from pyramid.response import Response
30 30
31 31 from rhodecode import events
32 32 from rhodecode.apps._base import RepoAppView, DataGridAppView
33 33 from rhodecode.lib.auth import (
34 34 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous,
35 35 HasRepoPermissionAny, HasPermissionAnyDecorator, CSRFRequired)
36 36 import rhodecode.lib.helpers as h
37 37 from rhodecode.lib.celerylib.utils import get_task_id
38 38 from rhodecode.model.db import coalesce, or_, Repository, RepoGroup
39 39 from rhodecode.model.permission import PermissionModel
40 40 from rhodecode.model.repo import RepoModel
41 41 from rhodecode.model.forms import RepoForkForm
42 42 from rhodecode.model.scm import ScmModel, RepoGroupList
43 43 from rhodecode.lib.utils2 import safe_int, safe_unicode
44 44
45 45 log = logging.getLogger(__name__)
46 46
47 47
48 48 class RepoForksView(RepoAppView, DataGridAppView):
49 49
50 50 def load_default_context(self):
51 51 c = self._get_local_tmpl_context(include_app_defaults=True)
52 52 c.rhodecode_repo = self.rhodecode_vcs_repo
53 53
54 54 acl_groups = RepoGroupList(
55 55 RepoGroup.query().all(),
56 56 perm_set=['group.write', 'group.admin'])
57 57 c.repo_groups = RepoGroup.groups_choices(groups=acl_groups)
58 58 c.repo_groups_choices = map(lambda k: safe_unicode(k[0]), c.repo_groups)
59 59
60 60 c.personal_repo_group = c.rhodecode_user.personal_repo_group
61 61
62 62 return c
63 63
64 64 @LoginRequired()
65 65 @HasRepoPermissionAnyDecorator(
66 66 'repository.read', 'repository.write', 'repository.admin')
67 67 @view_config(
68 68 route_name='repo_forks_show_all', request_method='GET',
69 69 renderer='rhodecode:templates/forks/forks.mako')
70 70 def repo_forks_show_all(self):
71 71 c = self.load_default_context()
72 72 return self._get_template_context(c)
73 73
74 74 @LoginRequired()
75 75 @HasRepoPermissionAnyDecorator(
76 76 'repository.read', 'repository.write', 'repository.admin')
77 77 @view_config(
78 78 route_name='repo_forks_data', request_method='GET',
79 79 renderer='json_ext', xhr=True)
80 80 def repo_forks_data(self):
81 81 _ = self.request.translate
82 82 self.load_default_context()
83 83 column_map = {
84 84 'fork_name': 'repo_name',
85 85 'fork_date': 'created_on',
86 86 'last_activity': 'updated_on'
87 87 }
88 88 draw, start, limit = self._extract_chunk(self.request)
89 89 search_q, order_by, order_dir = self._extract_ordering(
90 90 self.request, column_map=column_map)
91 91
92 92 acl_check = HasRepoPermissionAny(
93 93 'repository.read', 'repository.write', 'repository.admin')
94 94 repo_id = self.db_repo.repo_id
95 95 allowed_ids = [-1]
96 96 for f in Repository.query().filter(Repository.fork_id == repo_id):
97 97 if acl_check(f.repo_name, 'get forks check'):
98 98 allowed_ids.append(f.repo_id)
99 99
100 100 forks_data_total_count = Repository.query()\
101 101 .filter(Repository.fork_id == repo_id)\
102 102 .filter(Repository.repo_id.in_(allowed_ids))\
103 103 .count()
104 104
105 105 # json generate
106 106 base_q = Repository.query()\
107 107 .filter(Repository.fork_id == repo_id)\
108 108 .filter(Repository.repo_id.in_(allowed_ids))\
109 109
110 110 if search_q:
111 111 like_expression = u'%{}%'.format(safe_unicode(search_q))
112 112 base_q = base_q.filter(or_(
113 113 Repository.repo_name.ilike(like_expression),
114 114 Repository.description.ilike(like_expression),
115 115 ))
116 116
117 117 forks_data_total_filtered_count = base_q.count()
118 118
119 119 sort_col = getattr(Repository, order_by, None)
120 120 if sort_col:
121 121 if order_dir == 'asc':
122 122 # handle null values properly to order by NULL last
123 123 if order_by in ['last_activity']:
124 124 sort_col = coalesce(sort_col, datetime.date.max)
125 125 sort_col = sort_col.asc()
126 126 else:
127 127 # handle null values properly to order by NULL last
128 128 if order_by in ['last_activity']:
129 129 sort_col = coalesce(sort_col, datetime.date.min)
130 130 sort_col = sort_col.desc()
131 131
132 132 base_q = base_q.order_by(sort_col)
133 133 base_q = base_q.offset(start).limit(limit)
134 134
135 135 fork_list = base_q.all()
136 136
137 137 def fork_actions(fork):
138 138 url_link = h.route_path(
139 139 'repo_compare',
140 140 repo_name=fork.repo_name,
141 source_ref_type=self.db_repo.landing_rev[0],
142 source_ref=self.db_repo.landing_rev[1],
143 target_ref_type=self.db_repo.landing_rev[0],
144 target_ref=self.db_repo.landing_rev[1],
141 source_ref_type=self.db_repo.landing_ref_type,
142 source_ref=self.db_repo.landing_ref_name,
143 target_ref_type=self.db_repo.landing_ref_type,
144 target_ref=self.db_repo.landing_ref_name,
145 145 _query=dict(merge=1, target_repo=f.repo_name))
146 146 return h.link_to(_('Compare fork'), url_link, class_='btn-link')
147 147
148 148 def fork_name(fork):
149 149 return h.link_to(fork.repo_name,
150 150 h.route_path('repo_summary', repo_name=fork.repo_name))
151 151
152 152 forks_data = []
153 153 for fork in fork_list:
154 154 forks_data.append({
155 155 "username": h.gravatar_with_user(self.request, fork.user.username),
156 156 "fork_name": fork_name(fork),
157 157 "description": fork.description_safe,
158 158 "fork_date": h.age_component(fork.created_on, time_is_local=True),
159 159 "last_activity": h.format_date(fork.updated_on),
160 160 "action": fork_actions(fork),
161 161 })
162 162
163 163 data = ({
164 164 'draw': draw,
165 165 'data': forks_data,
166 166 'recordsTotal': forks_data_total_count,
167 167 'recordsFiltered': forks_data_total_filtered_count,
168 168 })
169 169
170 170 return data
171 171
172 172 @LoginRequired()
173 173 @NotAnonymous()
174 174 @HasPermissionAnyDecorator('hg.admin', 'hg.fork.repository')
175 175 @HasRepoPermissionAnyDecorator(
176 176 'repository.read', 'repository.write', 'repository.admin')
177 177 @view_config(
178 178 route_name='repo_fork_new', request_method='GET',
179 179 renderer='rhodecode:templates/forks/forks.mako')
180 180 def repo_fork_new(self):
181 181 c = self.load_default_context()
182 182
183 183 defaults = RepoModel()._get_defaults(self.db_repo_name)
184 184 # alter the description to indicate a fork
185 185 defaults['description'] = (
186 186 'fork of repository: %s \n%s' % (
187 187 defaults['repo_name'], defaults['description']))
188 188 # add suffix to fork
189 189 defaults['repo_name'] = '%s-fork' % defaults['repo_name']
190 190
191 191 data = render('rhodecode:templates/forks/fork.mako',
192 192 self._get_template_context(c), self.request)
193 193 html = formencode.htmlfill.render(
194 194 data,
195 195 defaults=defaults,
196 196 encoding="UTF-8",
197 197 force_defaults=False
198 198 )
199 199 return Response(html)
200 200
201 201 @LoginRequired()
202 202 @NotAnonymous()
203 203 @HasPermissionAnyDecorator('hg.admin', 'hg.fork.repository')
204 204 @HasRepoPermissionAnyDecorator(
205 205 'repository.read', 'repository.write', 'repository.admin')
206 206 @CSRFRequired()
207 207 @view_config(
208 208 route_name='repo_fork_create', request_method='POST',
209 209 renderer='rhodecode:templates/forks/fork.mako')
210 210 def repo_fork_create(self):
211 211 _ = self.request.translate
212 212 c = self.load_default_context()
213 213
214 214 _form = RepoForkForm(self.request.translate,
215 215 old_data={'repo_type': self.db_repo.repo_type},
216 216 repo_groups=c.repo_groups_choices)()
217 217 post_data = dict(self.request.POST)
218 218
219 219 # forbid injecting other repo by forging a request
220 220 post_data['fork_parent_id'] = self.db_repo.repo_id
221 221 post_data['landing_rev'] = self.db_repo._landing_revision
222 222
223 223 form_result = {}
224 224 task_id = None
225 225 try:
226 226 form_result = _form.to_python(post_data)
227 227 copy_permissions = form_result.get('copy_permissions')
228 228 # create fork is done sometimes async on celery, db transaction
229 229 # management is handled there.
230 230 task = RepoModel().create_fork(
231 231 form_result, c.rhodecode_user.user_id)
232 232
233 233 task_id = get_task_id(task)
234 234 except formencode.Invalid as errors:
235 235 c.rhodecode_db_repo = self.db_repo
236 236
237 237 data = render('rhodecode:templates/forks/fork.mako',
238 238 self._get_template_context(c), self.request)
239 239 html = formencode.htmlfill.render(
240 240 data,
241 241 defaults=errors.value,
242 242 errors=errors.error_dict or {},
243 243 prefix_error=False,
244 244 encoding="UTF-8",
245 245 force_defaults=False
246 246 )
247 247 return Response(html)
248 248 except Exception:
249 249 log.exception(
250 250 u'Exception while trying to fork the repository %s', self.db_repo_name)
251 251 msg = _('An error occurred during repository forking %s') % (self.db_repo_name, )
252 252 h.flash(msg, category='error')
253 253 raise HTTPFound(h.route_path('home'))
254 254
255 255 repo_name = form_result.get('repo_name_full', self.db_repo_name)
256 256
257 257 affected_user_ids = [self._rhodecode_user.user_id]
258 258 if copy_permissions:
259 259 # permission flush is done in repo creating
260 260 pass
261 261
262 262 PermissionModel().trigger_permission_flush(affected_user_ids)
263 263
264 264 raise HTTPFound(
265 265 h.route_path('repo_creating', repo_name=repo_name,
266 266 _query=dict(task_id=task_id)))
@@ -1,5594 +1,5602 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Database Models for RhodeCode Enterprise
23 23 """
24 24
25 25 import re
26 26 import os
27 27 import time
28 28 import string
29 29 import hashlib
30 30 import logging
31 31 import datetime
32 32 import uuid
33 33 import warnings
34 34 import ipaddress
35 35 import functools
36 36 import traceback
37 37 import collections
38 38
39 39 from sqlalchemy import (
40 40 or_, and_, not_, func, cast, TypeDecorator, event,
41 41 Index, Sequence, UniqueConstraint, ForeignKey, CheckConstraint, Column,
42 42 Boolean, String, Unicode, UnicodeText, DateTime, Integer, LargeBinary,
43 43 Text, Float, PickleType, BigInteger)
44 44 from sqlalchemy.sql.expression import true, false, case
45 45 from sqlalchemy.sql.functions import coalesce, count # pragma: no cover
46 46 from sqlalchemy.orm import (
47 47 relationship, joinedload, class_mapper, validates, aliased)
48 48 from sqlalchemy.ext.declarative import declared_attr
49 49 from sqlalchemy.ext.hybrid import hybrid_property
50 50 from sqlalchemy.exc import IntegrityError # pragma: no cover
51 51 from sqlalchemy.dialects.mysql import LONGTEXT
52 52 from zope.cachedescriptors.property import Lazy as LazyProperty
53 53 from pyramid import compat
54 54 from pyramid.threadlocal import get_current_request
55 55 from webhelpers2.text import remove_formatting
56 56
57 57 from rhodecode.translation import _
58 58 from rhodecode.lib.vcs import get_vcs_instance, VCSError
59 59 from rhodecode.lib.vcs.backends.base import EmptyCommit, Reference
60 60 from rhodecode.lib.utils2 import (
61 61 str2bool, safe_str, get_commit_safe, safe_unicode, sha1_safe,
62 62 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
63 63 glob2re, StrictAttributeDict, cleaned_uri, datetime_to_time, OrderedDefaultDict)
64 64 from rhodecode.lib.jsonalchemy import MutationObj, MutationList, JsonType, \
65 65 JsonRaw
66 66 from rhodecode.lib.ext_json import json
67 67 from rhodecode.lib.caching_query import FromCache
68 68 from rhodecode.lib.encrypt import AESCipher, validate_and_get_enc_data
69 69 from rhodecode.lib.encrypt2 import Encryptor
70 70 from rhodecode.lib.exceptions import (
71 71 ArtifactMetadataDuplicate, ArtifactMetadataBadValueType)
72 72 from rhodecode.model.meta import Base, Session
73 73
74 74 URL_SEP = '/'
75 75 log = logging.getLogger(__name__)
76 76
77 77 # =============================================================================
78 78 # BASE CLASSES
79 79 # =============================================================================
80 80
81 81 # this is propagated from .ini file rhodecode.encrypted_values.secret or
82 82 # beaker.session.secret if first is not set.
83 83 # and initialized at environment.py
84 84 ENCRYPTION_KEY = None
85 85
86 86 # used to sort permissions by types, '#' used here is not allowed to be in
87 87 # usernames, and it's very early in sorted string.printable table.
88 88 PERMISSION_TYPE_SORT = {
89 89 'admin': '####',
90 90 'write': '###',
91 91 'read': '##',
92 92 'none': '#',
93 93 }
94 94
95 95
96 96 def display_user_sort(obj):
97 97 """
98 98 Sort function used to sort permissions in .permissions() function of
99 99 Repository, RepoGroup, UserGroup. Also it put the default user in front
100 100 of all other resources
101 101 """
102 102
103 103 if obj.username == User.DEFAULT_USER:
104 104 return '#####'
105 105 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
106 106 return prefix + obj.username
107 107
108 108
109 109 def display_user_group_sort(obj):
110 110 """
111 111 Sort function used to sort permissions in .permissions() function of
112 112 Repository, RepoGroup, UserGroup. Also it put the default user in front
113 113 of all other resources
114 114 """
115 115
116 116 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
117 117 return prefix + obj.users_group_name
118 118
119 119
120 120 def _hash_key(k):
121 121 return sha1_safe(k)
122 122
123 123
124 124 def in_filter_generator(qry, items, limit=500):
125 125 """
126 126 Splits IN() into multiple with OR
127 127 e.g.::
128 128 cnt = Repository.query().filter(
129 129 or_(
130 130 *in_filter_generator(Repository.repo_id, range(100000))
131 131 )).count()
132 132 """
133 133 if not items:
134 134 # empty list will cause empty query which might cause security issues
135 135 # this can lead to hidden unpleasant results
136 136 items = [-1]
137 137
138 138 parts = []
139 139 for chunk in xrange(0, len(items), limit):
140 140 parts.append(
141 141 qry.in_(items[chunk: chunk + limit])
142 142 )
143 143
144 144 return parts
145 145
146 146
147 147 base_table_args = {
148 148 'extend_existing': True,
149 149 'mysql_engine': 'InnoDB',
150 150 'mysql_charset': 'utf8',
151 151 'sqlite_autoincrement': True
152 152 }
153 153
154 154
155 155 class EncryptedTextValue(TypeDecorator):
156 156 """
157 157 Special column for encrypted long text data, use like::
158 158
159 159 value = Column("encrypted_value", EncryptedValue(), nullable=False)
160 160
161 161 This column is intelligent so if value is in unencrypted form it return
162 162 unencrypted form, but on save it always encrypts
163 163 """
164 164 impl = Text
165 165
166 166 def process_bind_param(self, value, dialect):
167 167 """
168 168 Setter for storing value
169 169 """
170 170 import rhodecode
171 171 if not value:
172 172 return value
173 173
174 174 # protect against double encrypting if values is already encrypted
175 175 if value.startswith('enc$aes$') \
176 176 or value.startswith('enc$aes_hmac$') \
177 177 or value.startswith('enc2$'):
178 178 raise ValueError('value needs to be in unencrypted format, '
179 179 'ie. not starting with enc$ or enc2$')
180 180
181 181 algo = rhodecode.CONFIG.get('rhodecode.encrypted_values.algorithm') or 'aes'
182 182 if algo == 'aes':
183 183 return 'enc$aes_hmac$%s' % AESCipher(ENCRYPTION_KEY, hmac=True).encrypt(value)
184 184 elif algo == 'fernet':
185 185 return Encryptor(ENCRYPTION_KEY).encrypt(value)
186 186 else:
187 187 ValueError('Bad encryption algorithm, should be fernet or aes, got: {}'.format(algo))
188 188
189 189 def process_result_value(self, value, dialect):
190 190 """
191 191 Getter for retrieving value
192 192 """
193 193
194 194 import rhodecode
195 195 if not value:
196 196 return value
197 197
198 198 algo = rhodecode.CONFIG.get('rhodecode.encrypted_values.algorithm') or 'aes'
199 199 enc_strict_mode = str2bool(rhodecode.CONFIG.get('rhodecode.encrypted_values.strict') or True)
200 200 if algo == 'aes':
201 201 decrypted_data = validate_and_get_enc_data(value, ENCRYPTION_KEY, enc_strict_mode)
202 202 elif algo == 'fernet':
203 203 return Encryptor(ENCRYPTION_KEY).decrypt(value)
204 204 else:
205 205 ValueError('Bad encryption algorithm, should be fernet or aes, got: {}'.format(algo))
206 206 return decrypted_data
207 207
208 208
209 209 class BaseModel(object):
210 210 """
211 211 Base Model for all classes
212 212 """
213 213
214 214 @classmethod
215 215 def _get_keys(cls):
216 216 """return column names for this model """
217 217 return class_mapper(cls).c.keys()
218 218
219 219 def get_dict(self):
220 220 """
221 221 return dict with keys and values corresponding
222 222 to this model data """
223 223
224 224 d = {}
225 225 for k in self._get_keys():
226 226 d[k] = getattr(self, k)
227 227
228 228 # also use __json__() if present to get additional fields
229 229 _json_attr = getattr(self, '__json__', None)
230 230 if _json_attr:
231 231 # update with attributes from __json__
232 232 if callable(_json_attr):
233 233 _json_attr = _json_attr()
234 234 for k, val in _json_attr.iteritems():
235 235 d[k] = val
236 236 return d
237 237
238 238 def get_appstruct(self):
239 239 """return list with keys and values tuples corresponding
240 240 to this model data """
241 241
242 242 lst = []
243 243 for k in self._get_keys():
244 244 lst.append((k, getattr(self, k),))
245 245 return lst
246 246
247 247 def populate_obj(self, populate_dict):
248 248 """populate model with data from given populate_dict"""
249 249
250 250 for k in self._get_keys():
251 251 if k in populate_dict:
252 252 setattr(self, k, populate_dict[k])
253 253
254 254 @classmethod
255 255 def query(cls):
256 256 return Session().query(cls)
257 257
258 258 @classmethod
259 259 def get(cls, id_):
260 260 if id_:
261 261 return cls.query().get(id_)
262 262
263 263 @classmethod
264 264 def get_or_404(cls, id_):
265 265 from pyramid.httpexceptions import HTTPNotFound
266 266
267 267 try:
268 268 id_ = int(id_)
269 269 except (TypeError, ValueError):
270 270 raise HTTPNotFound()
271 271
272 272 res = cls.query().get(id_)
273 273 if not res:
274 274 raise HTTPNotFound()
275 275 return res
276 276
277 277 @classmethod
278 278 def getAll(cls):
279 279 # deprecated and left for backward compatibility
280 280 return cls.get_all()
281 281
282 282 @classmethod
283 283 def get_all(cls):
284 284 return cls.query().all()
285 285
286 286 @classmethod
287 287 def delete(cls, id_):
288 288 obj = cls.query().get(id_)
289 289 Session().delete(obj)
290 290
291 291 @classmethod
292 292 def identity_cache(cls, session, attr_name, value):
293 293 exist_in_session = []
294 294 for (item_cls, pkey), instance in session.identity_map.items():
295 295 if cls == item_cls and getattr(instance, attr_name) == value:
296 296 exist_in_session.append(instance)
297 297 if exist_in_session:
298 298 if len(exist_in_session) == 1:
299 299 return exist_in_session[0]
300 300 log.exception(
301 301 'multiple objects with attr %s and '
302 302 'value %s found with same name: %r',
303 303 attr_name, value, exist_in_session)
304 304
305 305 def __repr__(self):
306 306 if hasattr(self, '__unicode__'):
307 307 # python repr needs to return str
308 308 try:
309 309 return safe_str(self.__unicode__())
310 310 except UnicodeDecodeError:
311 311 pass
312 312 return '<DB:%s>' % (self.__class__.__name__)
313 313
314 314
315 315 class RhodeCodeSetting(Base, BaseModel):
316 316 __tablename__ = 'rhodecode_settings'
317 317 __table_args__ = (
318 318 UniqueConstraint('app_settings_name'),
319 319 base_table_args
320 320 )
321 321
322 322 SETTINGS_TYPES = {
323 323 'str': safe_str,
324 324 'int': safe_int,
325 325 'unicode': safe_unicode,
326 326 'bool': str2bool,
327 327 'list': functools.partial(aslist, sep=',')
328 328 }
329 329 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
330 330 GLOBAL_CONF_KEY = 'app_settings'
331 331
332 332 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
333 333 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
334 334 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
335 335 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
336 336
337 337 def __init__(self, key='', val='', type='unicode'):
338 338 self.app_settings_name = key
339 339 self.app_settings_type = type
340 340 self.app_settings_value = val
341 341
342 342 @validates('_app_settings_value')
343 343 def validate_settings_value(self, key, val):
344 344 assert type(val) == unicode
345 345 return val
346 346
347 347 @hybrid_property
348 348 def app_settings_value(self):
349 349 v = self._app_settings_value
350 350 _type = self.app_settings_type
351 351 if _type:
352 352 _type = self.app_settings_type.split('.')[0]
353 353 # decode the encrypted value
354 354 if 'encrypted' in self.app_settings_type:
355 355 cipher = EncryptedTextValue()
356 356 v = safe_unicode(cipher.process_result_value(v, None))
357 357
358 358 converter = self.SETTINGS_TYPES.get(_type) or \
359 359 self.SETTINGS_TYPES['unicode']
360 360 return converter(v)
361 361
362 362 @app_settings_value.setter
363 363 def app_settings_value(self, val):
364 364 """
365 365 Setter that will always make sure we use unicode in app_settings_value
366 366
367 367 :param val:
368 368 """
369 369 val = safe_unicode(val)
370 370 # encode the encrypted value
371 371 if 'encrypted' in self.app_settings_type:
372 372 cipher = EncryptedTextValue()
373 373 val = safe_unicode(cipher.process_bind_param(val, None))
374 374 self._app_settings_value = val
375 375
376 376 @hybrid_property
377 377 def app_settings_type(self):
378 378 return self._app_settings_type
379 379
380 380 @app_settings_type.setter
381 381 def app_settings_type(self, val):
382 382 if val.split('.')[0] not in self.SETTINGS_TYPES:
383 383 raise Exception('type must be one of %s got %s'
384 384 % (self.SETTINGS_TYPES.keys(), val))
385 385 self._app_settings_type = val
386 386
387 387 @classmethod
388 388 def get_by_prefix(cls, prefix):
389 389 return RhodeCodeSetting.query()\
390 390 .filter(RhodeCodeSetting.app_settings_name.startswith(prefix))\
391 391 .all()
392 392
393 393 def __unicode__(self):
394 394 return u"<%s('%s:%s[%s]')>" % (
395 395 self.__class__.__name__,
396 396 self.app_settings_name, self.app_settings_value,
397 397 self.app_settings_type
398 398 )
399 399
400 400
401 401 class RhodeCodeUi(Base, BaseModel):
402 402 __tablename__ = 'rhodecode_ui'
403 403 __table_args__ = (
404 404 UniqueConstraint('ui_key'),
405 405 base_table_args
406 406 )
407 407
408 408 HOOK_REPO_SIZE = 'changegroup.repo_size'
409 409 # HG
410 410 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
411 411 HOOK_PULL = 'outgoing.pull_logger'
412 412 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
413 413 HOOK_PRETX_PUSH = 'pretxnchangegroup.pre_push'
414 414 HOOK_PUSH = 'changegroup.push_logger'
415 415 HOOK_PUSH_KEY = 'pushkey.key_push'
416 416
417 417 HOOKS_BUILTIN = [
418 418 HOOK_PRE_PULL,
419 419 HOOK_PULL,
420 420 HOOK_PRE_PUSH,
421 421 HOOK_PRETX_PUSH,
422 422 HOOK_PUSH,
423 423 HOOK_PUSH_KEY,
424 424 ]
425 425
426 426 # TODO: johbo: Unify way how hooks are configured for git and hg,
427 427 # git part is currently hardcoded.
428 428
429 429 # SVN PATTERNS
430 430 SVN_BRANCH_ID = 'vcs_svn_branch'
431 431 SVN_TAG_ID = 'vcs_svn_tag'
432 432
433 433 ui_id = Column(
434 434 "ui_id", Integer(), nullable=False, unique=True, default=None,
435 435 primary_key=True)
436 436 ui_section = Column(
437 437 "ui_section", String(255), nullable=True, unique=None, default=None)
438 438 ui_key = Column(
439 439 "ui_key", String(255), nullable=True, unique=None, default=None)
440 440 ui_value = Column(
441 441 "ui_value", String(255), nullable=True, unique=None, default=None)
442 442 ui_active = Column(
443 443 "ui_active", Boolean(), nullable=True, unique=None, default=True)
444 444
445 445 def __repr__(self):
446 446 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
447 447 self.ui_key, self.ui_value)
448 448
449 449
450 450 class RepoRhodeCodeSetting(Base, BaseModel):
451 451 __tablename__ = 'repo_rhodecode_settings'
452 452 __table_args__ = (
453 453 UniqueConstraint(
454 454 'app_settings_name', 'repository_id',
455 455 name='uq_repo_rhodecode_setting_name_repo_id'),
456 456 base_table_args
457 457 )
458 458
459 459 repository_id = Column(
460 460 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
461 461 nullable=False)
462 462 app_settings_id = Column(
463 463 "app_settings_id", Integer(), nullable=False, unique=True,
464 464 default=None, primary_key=True)
465 465 app_settings_name = Column(
466 466 "app_settings_name", String(255), nullable=True, unique=None,
467 467 default=None)
468 468 _app_settings_value = Column(
469 469 "app_settings_value", String(4096), nullable=True, unique=None,
470 470 default=None)
471 471 _app_settings_type = Column(
472 472 "app_settings_type", String(255), nullable=True, unique=None,
473 473 default=None)
474 474
475 475 repository = relationship('Repository')
476 476
477 477 def __init__(self, repository_id, key='', val='', type='unicode'):
478 478 self.repository_id = repository_id
479 479 self.app_settings_name = key
480 480 self.app_settings_type = type
481 481 self.app_settings_value = val
482 482
483 483 @validates('_app_settings_value')
484 484 def validate_settings_value(self, key, val):
485 485 assert type(val) == unicode
486 486 return val
487 487
488 488 @hybrid_property
489 489 def app_settings_value(self):
490 490 v = self._app_settings_value
491 491 type_ = self.app_settings_type
492 492 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
493 493 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
494 494 return converter(v)
495 495
496 496 @app_settings_value.setter
497 497 def app_settings_value(self, val):
498 498 """
499 499 Setter that will always make sure we use unicode in app_settings_value
500 500
501 501 :param val:
502 502 """
503 503 self._app_settings_value = safe_unicode(val)
504 504
505 505 @hybrid_property
506 506 def app_settings_type(self):
507 507 return self._app_settings_type
508 508
509 509 @app_settings_type.setter
510 510 def app_settings_type(self, val):
511 511 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
512 512 if val not in SETTINGS_TYPES:
513 513 raise Exception('type must be one of %s got %s'
514 514 % (SETTINGS_TYPES.keys(), val))
515 515 self._app_settings_type = val
516 516
517 517 def __unicode__(self):
518 518 return u"<%s('%s:%s:%s[%s]')>" % (
519 519 self.__class__.__name__, self.repository.repo_name,
520 520 self.app_settings_name, self.app_settings_value,
521 521 self.app_settings_type
522 522 )
523 523
524 524
525 525 class RepoRhodeCodeUi(Base, BaseModel):
526 526 __tablename__ = 'repo_rhodecode_ui'
527 527 __table_args__ = (
528 528 UniqueConstraint(
529 529 'repository_id', 'ui_section', 'ui_key',
530 530 name='uq_repo_rhodecode_ui_repository_id_section_key'),
531 531 base_table_args
532 532 )
533 533
534 534 repository_id = Column(
535 535 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
536 536 nullable=False)
537 537 ui_id = Column(
538 538 "ui_id", Integer(), nullable=False, unique=True, default=None,
539 539 primary_key=True)
540 540 ui_section = Column(
541 541 "ui_section", String(255), nullable=True, unique=None, default=None)
542 542 ui_key = Column(
543 543 "ui_key", String(255), nullable=True, unique=None, default=None)
544 544 ui_value = Column(
545 545 "ui_value", String(255), nullable=True, unique=None, default=None)
546 546 ui_active = Column(
547 547 "ui_active", Boolean(), nullable=True, unique=None, default=True)
548 548
549 549 repository = relationship('Repository')
550 550
551 551 def __repr__(self):
552 552 return '<%s[%s:%s]%s=>%s]>' % (
553 553 self.__class__.__name__, self.repository.repo_name,
554 554 self.ui_section, self.ui_key, self.ui_value)
555 555
556 556
557 557 class User(Base, BaseModel):
558 558 __tablename__ = 'users'
559 559 __table_args__ = (
560 560 UniqueConstraint('username'), UniqueConstraint('email'),
561 561 Index('u_username_idx', 'username'),
562 562 Index('u_email_idx', 'email'),
563 563 base_table_args
564 564 )
565 565
566 566 DEFAULT_USER = 'default'
567 567 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
568 568 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
569 569
570 570 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
571 571 username = Column("username", String(255), nullable=True, unique=None, default=None)
572 572 password = Column("password", String(255), nullable=True, unique=None, default=None)
573 573 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
574 574 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
575 575 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
576 576 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
577 577 _email = Column("email", String(255), nullable=True, unique=None, default=None)
578 578 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
579 579 last_activity = Column('last_activity', DateTime(timezone=False), nullable=True, unique=None, default=None)
580 580 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
581 581
582 582 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
583 583 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
584 584 _api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
585 585 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
586 586 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
587 587 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
588 588
589 589 user_log = relationship('UserLog')
590 590 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all, delete-orphan')
591 591
592 592 repositories = relationship('Repository')
593 593 repository_groups = relationship('RepoGroup')
594 594 user_groups = relationship('UserGroup')
595 595
596 596 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
597 597 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
598 598
599 599 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all, delete-orphan')
600 600 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all, delete-orphan')
601 601 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all, delete-orphan')
602 602
603 603 group_member = relationship('UserGroupMember', cascade='all')
604 604
605 605 notifications = relationship('UserNotification', cascade='all')
606 606 # notifications assigned to this user
607 607 user_created_notifications = relationship('Notification', cascade='all')
608 608 # comments created by this user
609 609 user_comments = relationship('ChangesetComment', cascade='all')
610 610 # user profile extra info
611 611 user_emails = relationship('UserEmailMap', cascade='all')
612 612 user_ip_map = relationship('UserIpMap', cascade='all')
613 613 user_auth_tokens = relationship('UserApiKeys', cascade='all')
614 614 user_ssh_keys = relationship('UserSshKeys', cascade='all')
615 615
616 616 # gists
617 617 user_gists = relationship('Gist', cascade='all')
618 618 # user pull requests
619 619 user_pull_requests = relationship('PullRequest', cascade='all')
620 620
621 621 # external identities
622 622 external_identities = relationship(
623 623 'ExternalIdentity',
624 624 primaryjoin="User.user_id==ExternalIdentity.local_user_id",
625 625 cascade='all')
626 626 # review rules
627 627 user_review_rules = relationship('RepoReviewRuleUser', cascade='all')
628 628
629 629 # artifacts owned
630 630 artifacts = relationship('FileStore', primaryjoin='FileStore.user_id==User.user_id')
631 631
632 632 # no cascade, set NULL
633 633 scope_artifacts = relationship('FileStore', primaryjoin='FileStore.scope_user_id==User.user_id')
634 634
635 635 def __unicode__(self):
636 636 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
637 637 self.user_id, self.username)
638 638
639 639 @hybrid_property
640 640 def email(self):
641 641 return self._email
642 642
643 643 @email.setter
644 644 def email(self, val):
645 645 self._email = val.lower() if val else None
646 646
647 647 @hybrid_property
648 648 def first_name(self):
649 649 from rhodecode.lib import helpers as h
650 650 if self.name:
651 651 return h.escape(self.name)
652 652 return self.name
653 653
654 654 @hybrid_property
655 655 def last_name(self):
656 656 from rhodecode.lib import helpers as h
657 657 if self.lastname:
658 658 return h.escape(self.lastname)
659 659 return self.lastname
660 660
661 661 @hybrid_property
662 662 def api_key(self):
663 663 """
664 664 Fetch if exist an auth-token with role ALL connected to this user
665 665 """
666 666 user_auth_token = UserApiKeys.query()\
667 667 .filter(UserApiKeys.user_id == self.user_id)\
668 668 .filter(or_(UserApiKeys.expires == -1,
669 669 UserApiKeys.expires >= time.time()))\
670 670 .filter(UserApiKeys.role == UserApiKeys.ROLE_ALL).first()
671 671 if user_auth_token:
672 672 user_auth_token = user_auth_token.api_key
673 673
674 674 return user_auth_token
675 675
676 676 @api_key.setter
677 677 def api_key(self, val):
678 678 # don't allow to set API key this is deprecated for now
679 679 self._api_key = None
680 680
681 681 @property
682 682 def reviewer_pull_requests(self):
683 683 return PullRequestReviewers.query() \
684 684 .options(joinedload(PullRequestReviewers.pull_request)) \
685 685 .filter(PullRequestReviewers.user_id == self.user_id) \
686 686 .all()
687 687
688 688 @property
689 689 def firstname(self):
690 690 # alias for future
691 691 return self.name
692 692
693 693 @property
694 694 def emails(self):
695 695 other = UserEmailMap.query()\
696 696 .filter(UserEmailMap.user == self) \
697 697 .order_by(UserEmailMap.email_id.asc()) \
698 698 .all()
699 699 return [self.email] + [x.email for x in other]
700 700
701 701 def emails_cached(self):
702 702 emails = UserEmailMap.query()\
703 703 .filter(UserEmailMap.user == self) \
704 704 .order_by(UserEmailMap.email_id.asc())
705 705
706 706 emails = emails.options(
707 707 FromCache("sql_cache_short", "get_user_{}_emails".format(self.user_id))
708 708 )
709 709
710 710 return [self.email] + [x.email for x in emails]
711 711
712 712 @property
713 713 def auth_tokens(self):
714 714 auth_tokens = self.get_auth_tokens()
715 715 return [x.api_key for x in auth_tokens]
716 716
717 717 def get_auth_tokens(self):
718 718 return UserApiKeys.query()\
719 719 .filter(UserApiKeys.user == self)\
720 720 .order_by(UserApiKeys.user_api_key_id.asc())\
721 721 .all()
722 722
723 723 @LazyProperty
724 724 def feed_token(self):
725 725 return self.get_feed_token()
726 726
727 727 def get_feed_token(self, cache=True):
728 728 feed_tokens = UserApiKeys.query()\
729 729 .filter(UserApiKeys.user == self)\
730 730 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)
731 731 if cache:
732 732 feed_tokens = feed_tokens.options(
733 733 FromCache("sql_cache_short", "get_user_feed_token_%s" % self.user_id))
734 734
735 735 feed_tokens = feed_tokens.all()
736 736 if feed_tokens:
737 737 return feed_tokens[0].api_key
738 738 return 'NO_FEED_TOKEN_AVAILABLE'
739 739
740 740 @LazyProperty
741 741 def artifact_token(self):
742 742 return self.get_artifact_token()
743 743
744 744 def get_artifact_token(self, cache=True):
745 745 artifacts_tokens = UserApiKeys.query()\
746 746 .filter(UserApiKeys.user == self)\
747 747 .filter(UserApiKeys.role == UserApiKeys.ROLE_ARTIFACT_DOWNLOAD)
748 748 if cache:
749 749 artifacts_tokens = artifacts_tokens.options(
750 750 FromCache("sql_cache_short", "get_user_artifact_token_%s" % self.user_id))
751 751
752 752 artifacts_tokens = artifacts_tokens.all()
753 753 if artifacts_tokens:
754 754 return artifacts_tokens[0].api_key
755 755 return 'NO_ARTIFACT_TOKEN_AVAILABLE'
756 756
757 757 @classmethod
758 758 def get(cls, user_id, cache=False):
759 759 if not user_id:
760 760 return
761 761
762 762 user = cls.query()
763 763 if cache:
764 764 user = user.options(
765 765 FromCache("sql_cache_short", "get_users_%s" % user_id))
766 766 return user.get(user_id)
767 767
768 768 @classmethod
769 769 def extra_valid_auth_tokens(cls, user, role=None):
770 770 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
771 771 .filter(or_(UserApiKeys.expires == -1,
772 772 UserApiKeys.expires >= time.time()))
773 773 if role:
774 774 tokens = tokens.filter(or_(UserApiKeys.role == role,
775 775 UserApiKeys.role == UserApiKeys.ROLE_ALL))
776 776 return tokens.all()
777 777
778 778 def authenticate_by_token(self, auth_token, roles=None, scope_repo_id=None):
779 779 from rhodecode.lib import auth
780 780
781 781 log.debug('Trying to authenticate user: %s via auth-token, '
782 782 'and roles: %s', self, roles)
783 783
784 784 if not auth_token:
785 785 return False
786 786
787 787 roles = (roles or []) + [UserApiKeys.ROLE_ALL]
788 788 tokens_q = UserApiKeys.query()\
789 789 .filter(UserApiKeys.user_id == self.user_id)\
790 790 .filter(or_(UserApiKeys.expires == -1,
791 791 UserApiKeys.expires >= time.time()))
792 792
793 793 tokens_q = tokens_q.filter(UserApiKeys.role.in_(roles))
794 794
795 795 crypto_backend = auth.crypto_backend()
796 796 enc_token_map = {}
797 797 plain_token_map = {}
798 798 for token in tokens_q:
799 799 if token.api_key.startswith(crypto_backend.ENC_PREF):
800 800 enc_token_map[token.api_key] = token
801 801 else:
802 802 plain_token_map[token.api_key] = token
803 803 log.debug(
804 804 'Found %s plain and %s encrypted tokens to check for authentication for this user',
805 805 len(plain_token_map), len(enc_token_map))
806 806
807 807 # plain token match comes first
808 808 match = plain_token_map.get(auth_token)
809 809
810 810 # check encrypted tokens now
811 811 if not match:
812 812 for token_hash, token in enc_token_map.items():
813 813 # NOTE(marcink): this is expensive to calculate, but most secure
814 814 if crypto_backend.hash_check(auth_token, token_hash):
815 815 match = token
816 816 break
817 817
818 818 if match:
819 819 log.debug('Found matching token %s', match)
820 820 if match.repo_id:
821 821 log.debug('Found scope, checking for scope match of token %s', match)
822 822 if match.repo_id == scope_repo_id:
823 823 return True
824 824 else:
825 825 log.debug(
826 826 'AUTH_TOKEN: scope mismatch, token has a set repo scope: %s, '
827 827 'and calling scope is:%s, skipping further checks',
828 828 match.repo, scope_repo_id)
829 829 return False
830 830 else:
831 831 return True
832 832
833 833 return False
834 834
835 835 @property
836 836 def ip_addresses(self):
837 837 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
838 838 return [x.ip_addr for x in ret]
839 839
840 840 @property
841 841 def username_and_name(self):
842 842 return '%s (%s %s)' % (self.username, self.first_name, self.last_name)
843 843
844 844 @property
845 845 def username_or_name_or_email(self):
846 846 full_name = self.full_name if self.full_name is not ' ' else None
847 847 return self.username or full_name or self.email
848 848
849 849 @property
850 850 def full_name(self):
851 851 return '%s %s' % (self.first_name, self.last_name)
852 852
853 853 @property
854 854 def full_name_or_username(self):
855 855 return ('%s %s' % (self.first_name, self.last_name)
856 856 if (self.first_name and self.last_name) else self.username)
857 857
858 858 @property
859 859 def full_contact(self):
860 860 return '%s %s <%s>' % (self.first_name, self.last_name, self.email)
861 861
862 862 @property
863 863 def short_contact(self):
864 864 return '%s %s' % (self.first_name, self.last_name)
865 865
866 866 @property
867 867 def is_admin(self):
868 868 return self.admin
869 869
870 870 @property
871 871 def language(self):
872 872 return self.user_data.get('language')
873 873
874 874 def AuthUser(self, **kwargs):
875 875 """
876 876 Returns instance of AuthUser for this user
877 877 """
878 878 from rhodecode.lib.auth import AuthUser
879 879 return AuthUser(user_id=self.user_id, username=self.username, **kwargs)
880 880
881 881 @hybrid_property
882 882 def user_data(self):
883 883 if not self._user_data:
884 884 return {}
885 885
886 886 try:
887 887 return json.loads(self._user_data)
888 888 except TypeError:
889 889 return {}
890 890
891 891 @user_data.setter
892 892 def user_data(self, val):
893 893 if not isinstance(val, dict):
894 894 raise Exception('user_data must be dict, got %s' % type(val))
895 895 try:
896 896 self._user_data = json.dumps(val)
897 897 except Exception:
898 898 log.error(traceback.format_exc())
899 899
900 900 @classmethod
901 901 def get_by_username(cls, username, case_insensitive=False,
902 902 cache=False, identity_cache=False):
903 903 session = Session()
904 904
905 905 if case_insensitive:
906 906 q = cls.query().filter(
907 907 func.lower(cls.username) == func.lower(username))
908 908 else:
909 909 q = cls.query().filter(cls.username == username)
910 910
911 911 if cache:
912 912 if identity_cache:
913 913 val = cls.identity_cache(session, 'username', username)
914 914 if val:
915 915 return val
916 916 else:
917 917 cache_key = "get_user_by_name_%s" % _hash_key(username)
918 918 q = q.options(
919 919 FromCache("sql_cache_short", cache_key))
920 920
921 921 return q.scalar()
922 922
923 923 @classmethod
924 924 def get_by_auth_token(cls, auth_token, cache=False):
925 925 q = UserApiKeys.query()\
926 926 .filter(UserApiKeys.api_key == auth_token)\
927 927 .filter(or_(UserApiKeys.expires == -1,
928 928 UserApiKeys.expires >= time.time()))
929 929 if cache:
930 930 q = q.options(
931 931 FromCache("sql_cache_short", "get_auth_token_%s" % auth_token))
932 932
933 933 match = q.first()
934 934 if match:
935 935 return match.user
936 936
937 937 @classmethod
938 938 def get_by_email(cls, email, case_insensitive=False, cache=False):
939 939
940 940 if case_insensitive:
941 941 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
942 942
943 943 else:
944 944 q = cls.query().filter(cls.email == email)
945 945
946 946 email_key = _hash_key(email)
947 947 if cache:
948 948 q = q.options(
949 949 FromCache("sql_cache_short", "get_email_key_%s" % email_key))
950 950
951 951 ret = q.scalar()
952 952 if ret is None:
953 953 q = UserEmailMap.query()
954 954 # try fetching in alternate email map
955 955 if case_insensitive:
956 956 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
957 957 else:
958 958 q = q.filter(UserEmailMap.email == email)
959 959 q = q.options(joinedload(UserEmailMap.user))
960 960 if cache:
961 961 q = q.options(
962 962 FromCache("sql_cache_short", "get_email_map_key_%s" % email_key))
963 963 ret = getattr(q.scalar(), 'user', None)
964 964
965 965 return ret
966 966
967 967 @classmethod
968 968 def get_from_cs_author(cls, author):
969 969 """
970 970 Tries to get User objects out of commit author string
971 971
972 972 :param author:
973 973 """
974 974 from rhodecode.lib.helpers import email, author_name
975 975 # Valid email in the attribute passed, see if they're in the system
976 976 _email = email(author)
977 977 if _email:
978 978 user = cls.get_by_email(_email, case_insensitive=True)
979 979 if user:
980 980 return user
981 981 # Maybe we can match by username?
982 982 _author = author_name(author)
983 983 user = cls.get_by_username(_author, case_insensitive=True)
984 984 if user:
985 985 return user
986 986
987 987 def update_userdata(self, **kwargs):
988 988 usr = self
989 989 old = usr.user_data
990 990 old.update(**kwargs)
991 991 usr.user_data = old
992 992 Session().add(usr)
993 993 log.debug('updated userdata with %s', kwargs)
994 994
995 995 def update_lastlogin(self):
996 996 """Update user lastlogin"""
997 997 self.last_login = datetime.datetime.now()
998 998 Session().add(self)
999 999 log.debug('updated user %s lastlogin', self.username)
1000 1000
1001 1001 def update_password(self, new_password):
1002 1002 from rhodecode.lib.auth import get_crypt_password
1003 1003
1004 1004 self.password = get_crypt_password(new_password)
1005 1005 Session().add(self)
1006 1006
1007 1007 @classmethod
1008 1008 def get_first_super_admin(cls):
1009 1009 user = User.query()\
1010 1010 .filter(User.admin == true()) \
1011 1011 .order_by(User.user_id.asc()) \
1012 1012 .first()
1013 1013
1014 1014 if user is None:
1015 1015 raise Exception('FATAL: Missing administrative account!')
1016 1016 return user
1017 1017
1018 1018 @classmethod
1019 1019 def get_all_super_admins(cls, only_active=False):
1020 1020 """
1021 1021 Returns all admin accounts sorted by username
1022 1022 """
1023 1023 qry = User.query().filter(User.admin == true()).order_by(User.username.asc())
1024 1024 if only_active:
1025 1025 qry = qry.filter(User.active == true())
1026 1026 return qry.all()
1027 1027
1028 1028 @classmethod
1029 1029 def get_all_user_ids(cls, only_active=True):
1030 1030 """
1031 1031 Returns all users IDs
1032 1032 """
1033 1033 qry = Session().query(User.user_id)
1034 1034
1035 1035 if only_active:
1036 1036 qry = qry.filter(User.active == true())
1037 1037 return [x.user_id for x in qry]
1038 1038
1039 1039 @classmethod
1040 1040 def get_default_user(cls, cache=False, refresh=False):
1041 1041 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
1042 1042 if user is None:
1043 1043 raise Exception('FATAL: Missing default account!')
1044 1044 if refresh:
1045 1045 # The default user might be based on outdated state which
1046 1046 # has been loaded from the cache.
1047 1047 # A call to refresh() ensures that the
1048 1048 # latest state from the database is used.
1049 1049 Session().refresh(user)
1050 1050 return user
1051 1051
1052 1052 @classmethod
1053 1053 def get_default_user_id(cls):
1054 1054 import rhodecode
1055 1055 return rhodecode.CONFIG['default_user_id']
1056 1056
1057 1057 def _get_default_perms(self, user, suffix=''):
1058 1058 from rhodecode.model.permission import PermissionModel
1059 1059 return PermissionModel().get_default_perms(user.user_perms, suffix)
1060 1060
1061 1061 def get_default_perms(self, suffix=''):
1062 1062 return self._get_default_perms(self, suffix)
1063 1063
1064 1064 def get_api_data(self, include_secrets=False, details='full'):
1065 1065 """
1066 1066 Common function for generating user related data for API
1067 1067
1068 1068 :param include_secrets: By default secrets in the API data will be replaced
1069 1069 by a placeholder value to prevent exposing this data by accident. In case
1070 1070 this data shall be exposed, set this flag to ``True``.
1071 1071
1072 1072 :param details: details can be 'basic|full' basic gives only a subset of
1073 1073 the available user information that includes user_id, name and emails.
1074 1074 """
1075 1075 user = self
1076 1076 user_data = self.user_data
1077 1077 data = {
1078 1078 'user_id': user.user_id,
1079 1079 'username': user.username,
1080 1080 'firstname': user.name,
1081 1081 'lastname': user.lastname,
1082 1082 'description': user.description,
1083 1083 'email': user.email,
1084 1084 'emails': user.emails,
1085 1085 }
1086 1086 if details == 'basic':
1087 1087 return data
1088 1088
1089 1089 auth_token_length = 40
1090 1090 auth_token_replacement = '*' * auth_token_length
1091 1091
1092 1092 extras = {
1093 1093 'auth_tokens': [auth_token_replacement],
1094 1094 'active': user.active,
1095 1095 'admin': user.admin,
1096 1096 'extern_type': user.extern_type,
1097 1097 'extern_name': user.extern_name,
1098 1098 'last_login': user.last_login,
1099 1099 'last_activity': user.last_activity,
1100 1100 'ip_addresses': user.ip_addresses,
1101 1101 'language': user_data.get('language')
1102 1102 }
1103 1103 data.update(extras)
1104 1104
1105 1105 if include_secrets:
1106 1106 data['auth_tokens'] = user.auth_tokens
1107 1107 return data
1108 1108
1109 1109 def __json__(self):
1110 1110 data = {
1111 1111 'full_name': self.full_name,
1112 1112 'full_name_or_username': self.full_name_or_username,
1113 1113 'short_contact': self.short_contact,
1114 1114 'full_contact': self.full_contact,
1115 1115 }
1116 1116 data.update(self.get_api_data())
1117 1117 return data
1118 1118
1119 1119
1120 1120 class UserApiKeys(Base, BaseModel):
1121 1121 __tablename__ = 'user_api_keys'
1122 1122 __table_args__ = (
1123 1123 Index('uak_api_key_idx', 'api_key'),
1124 1124 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
1125 1125 base_table_args
1126 1126 )
1127 1127 __mapper_args__ = {}
1128 1128
1129 1129 # ApiKey role
1130 1130 ROLE_ALL = 'token_role_all'
1131 1131 ROLE_HTTP = 'token_role_http'
1132 1132 ROLE_VCS = 'token_role_vcs'
1133 1133 ROLE_API = 'token_role_api'
1134 1134 ROLE_FEED = 'token_role_feed'
1135 1135 ROLE_ARTIFACT_DOWNLOAD = 'role_artifact_download'
1136 1136 ROLE_PASSWORD_RESET = 'token_password_reset'
1137 1137
1138 1138 ROLES = [ROLE_ALL, ROLE_HTTP, ROLE_VCS, ROLE_API, ROLE_FEED, ROLE_ARTIFACT_DOWNLOAD]
1139 1139
1140 1140 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1141 1141 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1142 1142 api_key = Column("api_key", String(255), nullable=False, unique=True)
1143 1143 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1144 1144 expires = Column('expires', Float(53), nullable=False)
1145 1145 role = Column('role', String(255), nullable=True)
1146 1146 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1147 1147
1148 1148 # scope columns
1149 1149 repo_id = Column(
1150 1150 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
1151 1151 nullable=True, unique=None, default=None)
1152 1152 repo = relationship('Repository', lazy='joined')
1153 1153
1154 1154 repo_group_id = Column(
1155 1155 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
1156 1156 nullable=True, unique=None, default=None)
1157 1157 repo_group = relationship('RepoGroup', lazy='joined')
1158 1158
1159 1159 user = relationship('User', lazy='joined')
1160 1160
1161 1161 def __unicode__(self):
1162 1162 return u"<%s('%s')>" % (self.__class__.__name__, self.role)
1163 1163
1164 1164 def __json__(self):
1165 1165 data = {
1166 1166 'auth_token': self.api_key,
1167 1167 'role': self.role,
1168 1168 'scope': self.scope_humanized,
1169 1169 'expired': self.expired
1170 1170 }
1171 1171 return data
1172 1172
1173 1173 def get_api_data(self, include_secrets=False):
1174 1174 data = self.__json__()
1175 1175 if include_secrets:
1176 1176 return data
1177 1177 else:
1178 1178 data['auth_token'] = self.token_obfuscated
1179 1179 return data
1180 1180
1181 1181 @hybrid_property
1182 1182 def description_safe(self):
1183 1183 from rhodecode.lib import helpers as h
1184 1184 return h.escape(self.description)
1185 1185
1186 1186 @property
1187 1187 def expired(self):
1188 1188 if self.expires == -1:
1189 1189 return False
1190 1190 return time.time() > self.expires
1191 1191
1192 1192 @classmethod
1193 1193 def _get_role_name(cls, role):
1194 1194 return {
1195 1195 cls.ROLE_ALL: _('all'),
1196 1196 cls.ROLE_HTTP: _('http/web interface'),
1197 1197 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
1198 1198 cls.ROLE_API: _('api calls'),
1199 1199 cls.ROLE_FEED: _('feed access'),
1200 1200 cls.ROLE_ARTIFACT_DOWNLOAD: _('artifacts downloads'),
1201 1201 }.get(role, role)
1202 1202
1203 1203 @property
1204 1204 def role_humanized(self):
1205 1205 return self._get_role_name(self.role)
1206 1206
1207 1207 def _get_scope(self):
1208 1208 if self.repo:
1209 1209 return 'Repository: {}'.format(self.repo.repo_name)
1210 1210 if self.repo_group:
1211 1211 return 'RepositoryGroup: {} (recursive)'.format(self.repo_group.group_name)
1212 1212 return 'Global'
1213 1213
1214 1214 @property
1215 1215 def scope_humanized(self):
1216 1216 return self._get_scope()
1217 1217
1218 1218 @property
1219 1219 def token_obfuscated(self):
1220 1220 if self.api_key:
1221 1221 return self.api_key[:4] + "****"
1222 1222
1223 1223
1224 1224 class UserEmailMap(Base, BaseModel):
1225 1225 __tablename__ = 'user_email_map'
1226 1226 __table_args__ = (
1227 1227 Index('uem_email_idx', 'email'),
1228 1228 UniqueConstraint('email'),
1229 1229 base_table_args
1230 1230 )
1231 1231 __mapper_args__ = {}
1232 1232
1233 1233 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1234 1234 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1235 1235 _email = Column("email", String(255), nullable=True, unique=False, default=None)
1236 1236 user = relationship('User', lazy='joined')
1237 1237
1238 1238 @validates('_email')
1239 1239 def validate_email(self, key, email):
1240 1240 # check if this email is not main one
1241 1241 main_email = Session().query(User).filter(User.email == email).scalar()
1242 1242 if main_email is not None:
1243 1243 raise AttributeError('email %s is present is user table' % email)
1244 1244 return email
1245 1245
1246 1246 @hybrid_property
1247 1247 def email(self):
1248 1248 return self._email
1249 1249
1250 1250 @email.setter
1251 1251 def email(self, val):
1252 1252 self._email = val.lower() if val else None
1253 1253
1254 1254
1255 1255 class UserIpMap(Base, BaseModel):
1256 1256 __tablename__ = 'user_ip_map'
1257 1257 __table_args__ = (
1258 1258 UniqueConstraint('user_id', 'ip_addr'),
1259 1259 base_table_args
1260 1260 )
1261 1261 __mapper_args__ = {}
1262 1262
1263 1263 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1264 1264 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1265 1265 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
1266 1266 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
1267 1267 description = Column("description", String(10000), nullable=True, unique=None, default=None)
1268 1268 user = relationship('User', lazy='joined')
1269 1269
1270 1270 @hybrid_property
1271 1271 def description_safe(self):
1272 1272 from rhodecode.lib import helpers as h
1273 1273 return h.escape(self.description)
1274 1274
1275 1275 @classmethod
1276 1276 def _get_ip_range(cls, ip_addr):
1277 1277 net = ipaddress.ip_network(safe_unicode(ip_addr), strict=False)
1278 1278 return [str(net.network_address), str(net.broadcast_address)]
1279 1279
1280 1280 def __json__(self):
1281 1281 return {
1282 1282 'ip_addr': self.ip_addr,
1283 1283 'ip_range': self._get_ip_range(self.ip_addr),
1284 1284 }
1285 1285
1286 1286 def __unicode__(self):
1287 1287 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
1288 1288 self.user_id, self.ip_addr)
1289 1289
1290 1290
1291 1291 class UserSshKeys(Base, BaseModel):
1292 1292 __tablename__ = 'user_ssh_keys'
1293 1293 __table_args__ = (
1294 1294 Index('usk_ssh_key_fingerprint_idx', 'ssh_key_fingerprint'),
1295 1295
1296 1296 UniqueConstraint('ssh_key_fingerprint'),
1297 1297
1298 1298 base_table_args
1299 1299 )
1300 1300 __mapper_args__ = {}
1301 1301
1302 1302 ssh_key_id = Column('ssh_key_id', Integer(), nullable=False, unique=True, default=None, primary_key=True)
1303 1303 ssh_key_data = Column('ssh_key_data', String(10240), nullable=False, unique=None, default=None)
1304 1304 ssh_key_fingerprint = Column('ssh_key_fingerprint', String(255), nullable=False, unique=None, default=None)
1305 1305
1306 1306 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1307 1307
1308 1308 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1309 1309 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True, default=None)
1310 1310 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1311 1311
1312 1312 user = relationship('User', lazy='joined')
1313 1313
1314 1314 def __json__(self):
1315 1315 data = {
1316 1316 'ssh_fingerprint': self.ssh_key_fingerprint,
1317 1317 'description': self.description,
1318 1318 'created_on': self.created_on
1319 1319 }
1320 1320 return data
1321 1321
1322 1322 def get_api_data(self):
1323 1323 data = self.__json__()
1324 1324 return data
1325 1325
1326 1326
1327 1327 class UserLog(Base, BaseModel):
1328 1328 __tablename__ = 'user_logs'
1329 1329 __table_args__ = (
1330 1330 base_table_args,
1331 1331 )
1332 1332
1333 1333 VERSION_1 = 'v1'
1334 1334 VERSION_2 = 'v2'
1335 1335 VERSIONS = [VERSION_1, VERSION_2]
1336 1336
1337 1337 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1338 1338 user_id = Column("user_id", Integer(), ForeignKey('users.user_id',ondelete='SET NULL'), nullable=True, unique=None, default=None)
1339 1339 username = Column("username", String(255), nullable=True, unique=None, default=None)
1340 1340 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id', ondelete='SET NULL'), nullable=True, unique=None, default=None)
1341 1341 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1342 1342 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1343 1343 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1344 1344 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1345 1345
1346 1346 version = Column("version", String(255), nullable=True, default=VERSION_1)
1347 1347 user_data = Column('user_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1348 1348 action_data = Column('action_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1349 1349
1350 1350 def __unicode__(self):
1351 1351 return u"<%s('id:%s:%s')>" % (
1352 1352 self.__class__.__name__, self.repository_name, self.action)
1353 1353
1354 1354 def __json__(self):
1355 1355 return {
1356 1356 'user_id': self.user_id,
1357 1357 'username': self.username,
1358 1358 'repository_id': self.repository_id,
1359 1359 'repository_name': self.repository_name,
1360 1360 'user_ip': self.user_ip,
1361 1361 'action_date': self.action_date,
1362 1362 'action': self.action,
1363 1363 }
1364 1364
1365 1365 @hybrid_property
1366 1366 def entry_id(self):
1367 1367 return self.user_log_id
1368 1368
1369 1369 @property
1370 1370 def action_as_day(self):
1371 1371 return datetime.date(*self.action_date.timetuple()[:3])
1372 1372
1373 1373 user = relationship('User')
1374 1374 repository = relationship('Repository', cascade='')
1375 1375
1376 1376
1377 1377 class UserGroup(Base, BaseModel):
1378 1378 __tablename__ = 'users_groups'
1379 1379 __table_args__ = (
1380 1380 base_table_args,
1381 1381 )
1382 1382
1383 1383 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1384 1384 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1385 1385 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1386 1386 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1387 1387 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1388 1388 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1389 1389 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1390 1390 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1391 1391
1392 1392 members = relationship('UserGroupMember', cascade="all, delete-orphan", lazy="joined")
1393 1393 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
1394 1394 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1395 1395 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1396 1396 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all')
1397 1397 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
1398 1398
1399 1399 user_group_review_rules = relationship('RepoReviewRuleUserGroup', cascade='all')
1400 1400 user = relationship('User', primaryjoin="User.user_id==UserGroup.user_id")
1401 1401
1402 1402 @classmethod
1403 1403 def _load_group_data(cls, column):
1404 1404 if not column:
1405 1405 return {}
1406 1406
1407 1407 try:
1408 1408 return json.loads(column) or {}
1409 1409 except TypeError:
1410 1410 return {}
1411 1411
1412 1412 @hybrid_property
1413 1413 def description_safe(self):
1414 1414 from rhodecode.lib import helpers as h
1415 1415 return h.escape(self.user_group_description)
1416 1416
1417 1417 @hybrid_property
1418 1418 def group_data(self):
1419 1419 return self._load_group_data(self._group_data)
1420 1420
1421 1421 @group_data.expression
1422 1422 def group_data(self, **kwargs):
1423 1423 return self._group_data
1424 1424
1425 1425 @group_data.setter
1426 1426 def group_data(self, val):
1427 1427 try:
1428 1428 self._group_data = json.dumps(val)
1429 1429 except Exception:
1430 1430 log.error(traceback.format_exc())
1431 1431
1432 1432 @classmethod
1433 1433 def _load_sync(cls, group_data):
1434 1434 if group_data:
1435 1435 return group_data.get('extern_type')
1436 1436
1437 1437 @property
1438 1438 def sync(self):
1439 1439 return self._load_sync(self.group_data)
1440 1440
1441 1441 def __unicode__(self):
1442 1442 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1443 1443 self.users_group_id,
1444 1444 self.users_group_name)
1445 1445
1446 1446 @classmethod
1447 1447 def get_by_group_name(cls, group_name, cache=False,
1448 1448 case_insensitive=False):
1449 1449 if case_insensitive:
1450 1450 q = cls.query().filter(func.lower(cls.users_group_name) ==
1451 1451 func.lower(group_name))
1452 1452
1453 1453 else:
1454 1454 q = cls.query().filter(cls.users_group_name == group_name)
1455 1455 if cache:
1456 1456 q = q.options(
1457 1457 FromCache("sql_cache_short", "get_group_%s" % _hash_key(group_name)))
1458 1458 return q.scalar()
1459 1459
1460 1460 @classmethod
1461 1461 def get(cls, user_group_id, cache=False):
1462 1462 if not user_group_id:
1463 1463 return
1464 1464
1465 1465 user_group = cls.query()
1466 1466 if cache:
1467 1467 user_group = user_group.options(
1468 1468 FromCache("sql_cache_short", "get_users_group_%s" % user_group_id))
1469 1469 return user_group.get(user_group_id)
1470 1470
1471 1471 def permissions(self, with_admins=True, with_owner=True,
1472 1472 expand_from_user_groups=False):
1473 1473 """
1474 1474 Permissions for user groups
1475 1475 """
1476 1476 _admin_perm = 'usergroup.admin'
1477 1477
1478 1478 owner_row = []
1479 1479 if with_owner:
1480 1480 usr = AttributeDict(self.user.get_dict())
1481 1481 usr.owner_row = True
1482 1482 usr.permission = _admin_perm
1483 1483 owner_row.append(usr)
1484 1484
1485 1485 super_admin_ids = []
1486 1486 super_admin_rows = []
1487 1487 if with_admins:
1488 1488 for usr in User.get_all_super_admins():
1489 1489 super_admin_ids.append(usr.user_id)
1490 1490 # if this admin is also owner, don't double the record
1491 1491 if usr.user_id == owner_row[0].user_id:
1492 1492 owner_row[0].admin_row = True
1493 1493 else:
1494 1494 usr = AttributeDict(usr.get_dict())
1495 1495 usr.admin_row = True
1496 1496 usr.permission = _admin_perm
1497 1497 super_admin_rows.append(usr)
1498 1498
1499 1499 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1500 1500 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1501 1501 joinedload(UserUserGroupToPerm.user),
1502 1502 joinedload(UserUserGroupToPerm.permission),)
1503 1503
1504 1504 # get owners and admins and permissions. We do a trick of re-writing
1505 1505 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1506 1506 # has a global reference and changing one object propagates to all
1507 1507 # others. This means if admin is also an owner admin_row that change
1508 1508 # would propagate to both objects
1509 1509 perm_rows = []
1510 1510 for _usr in q.all():
1511 1511 usr = AttributeDict(_usr.user.get_dict())
1512 1512 # if this user is also owner/admin, mark as duplicate record
1513 1513 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
1514 1514 usr.duplicate_perm = True
1515 1515 usr.permission = _usr.permission.permission_name
1516 1516 perm_rows.append(usr)
1517 1517
1518 1518 # filter the perm rows by 'default' first and then sort them by
1519 1519 # admin,write,read,none permissions sorted again alphabetically in
1520 1520 # each group
1521 1521 perm_rows = sorted(perm_rows, key=display_user_sort)
1522 1522
1523 1523 user_groups_rows = []
1524 1524 if expand_from_user_groups:
1525 1525 for ug in self.permission_user_groups(with_members=True):
1526 1526 for user_data in ug.members:
1527 1527 user_groups_rows.append(user_data)
1528 1528
1529 1529 return super_admin_rows + owner_row + perm_rows + user_groups_rows
1530 1530
1531 1531 def permission_user_groups(self, with_members=False):
1532 1532 q = UserGroupUserGroupToPerm.query()\
1533 1533 .filter(UserGroupUserGroupToPerm.target_user_group == self)
1534 1534 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1535 1535 joinedload(UserGroupUserGroupToPerm.target_user_group),
1536 1536 joinedload(UserGroupUserGroupToPerm.permission),)
1537 1537
1538 1538 perm_rows = []
1539 1539 for _user_group in q.all():
1540 1540 entry = AttributeDict(_user_group.user_group.get_dict())
1541 1541 entry.permission = _user_group.permission.permission_name
1542 1542 if with_members:
1543 1543 entry.members = [x.user.get_dict()
1544 1544 for x in _user_group.user_group.members]
1545 1545 perm_rows.append(entry)
1546 1546
1547 1547 perm_rows = sorted(perm_rows, key=display_user_group_sort)
1548 1548 return perm_rows
1549 1549
1550 1550 def _get_default_perms(self, user_group, suffix=''):
1551 1551 from rhodecode.model.permission import PermissionModel
1552 1552 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1553 1553
1554 1554 def get_default_perms(self, suffix=''):
1555 1555 return self._get_default_perms(self, suffix)
1556 1556
1557 1557 def get_api_data(self, with_group_members=True, include_secrets=False):
1558 1558 """
1559 1559 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1560 1560 basically forwarded.
1561 1561
1562 1562 """
1563 1563 user_group = self
1564 1564 data = {
1565 1565 'users_group_id': user_group.users_group_id,
1566 1566 'group_name': user_group.users_group_name,
1567 1567 'group_description': user_group.user_group_description,
1568 1568 'active': user_group.users_group_active,
1569 1569 'owner': user_group.user.username,
1570 1570 'sync': user_group.sync,
1571 1571 'owner_email': user_group.user.email,
1572 1572 }
1573 1573
1574 1574 if with_group_members:
1575 1575 users = []
1576 1576 for user in user_group.members:
1577 1577 user = user.user
1578 1578 users.append(user.get_api_data(include_secrets=include_secrets))
1579 1579 data['users'] = users
1580 1580
1581 1581 return data
1582 1582
1583 1583
1584 1584 class UserGroupMember(Base, BaseModel):
1585 1585 __tablename__ = 'users_groups_members'
1586 1586 __table_args__ = (
1587 1587 base_table_args,
1588 1588 )
1589 1589
1590 1590 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1591 1591 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1592 1592 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1593 1593
1594 1594 user = relationship('User', lazy='joined')
1595 1595 users_group = relationship('UserGroup')
1596 1596
1597 1597 def __init__(self, gr_id='', u_id=''):
1598 1598 self.users_group_id = gr_id
1599 1599 self.user_id = u_id
1600 1600
1601 1601
1602 1602 class RepositoryField(Base, BaseModel):
1603 1603 __tablename__ = 'repositories_fields'
1604 1604 __table_args__ = (
1605 1605 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1606 1606 base_table_args,
1607 1607 )
1608 1608
1609 1609 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1610 1610
1611 1611 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1612 1612 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1613 1613 field_key = Column("field_key", String(250))
1614 1614 field_label = Column("field_label", String(1024), nullable=False)
1615 1615 field_value = Column("field_value", String(10000), nullable=False)
1616 1616 field_desc = Column("field_desc", String(1024), nullable=False)
1617 1617 field_type = Column("field_type", String(255), nullable=False, unique=None)
1618 1618 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1619 1619
1620 1620 repository = relationship('Repository')
1621 1621
1622 1622 @property
1623 1623 def field_key_prefixed(self):
1624 1624 return 'ex_%s' % self.field_key
1625 1625
1626 1626 @classmethod
1627 1627 def un_prefix_key(cls, key):
1628 1628 if key.startswith(cls.PREFIX):
1629 1629 return key[len(cls.PREFIX):]
1630 1630 return key
1631 1631
1632 1632 @classmethod
1633 1633 def get_by_key_name(cls, key, repo):
1634 1634 row = cls.query()\
1635 1635 .filter(cls.repository == repo)\
1636 1636 .filter(cls.field_key == key).scalar()
1637 1637 return row
1638 1638
1639 1639
1640 1640 class Repository(Base, BaseModel):
1641 1641 __tablename__ = 'repositories'
1642 1642 __table_args__ = (
1643 1643 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1644 1644 base_table_args,
1645 1645 )
1646 1646 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1647 1647 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1648 1648 DEFAULT_CLONE_URI_SSH = 'ssh://{sys_user}@{hostname}/{repo}'
1649 1649
1650 1650 STATE_CREATED = 'repo_state_created'
1651 1651 STATE_PENDING = 'repo_state_pending'
1652 1652 STATE_ERROR = 'repo_state_error'
1653 1653
1654 1654 LOCK_AUTOMATIC = 'lock_auto'
1655 1655 LOCK_API = 'lock_api'
1656 1656 LOCK_WEB = 'lock_web'
1657 1657 LOCK_PULL = 'lock_pull'
1658 1658
1659 1659 NAME_SEP = URL_SEP
1660 1660
1661 1661 repo_id = Column(
1662 1662 "repo_id", Integer(), nullable=False, unique=True, default=None,
1663 1663 primary_key=True)
1664 1664 _repo_name = Column(
1665 1665 "repo_name", Text(), nullable=False, default=None)
1666 1666 repo_name_hash = Column(
1667 1667 "repo_name_hash", String(255), nullable=False, unique=True)
1668 1668 repo_state = Column("repo_state", String(255), nullable=True)
1669 1669
1670 1670 clone_uri = Column(
1671 1671 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1672 1672 default=None)
1673 1673 push_uri = Column(
1674 1674 "push_uri", EncryptedTextValue(), nullable=True, unique=False,
1675 1675 default=None)
1676 1676 repo_type = Column(
1677 1677 "repo_type", String(255), nullable=False, unique=False, default=None)
1678 1678 user_id = Column(
1679 1679 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1680 1680 unique=False, default=None)
1681 1681 private = Column(
1682 1682 "private", Boolean(), nullable=True, unique=None, default=None)
1683 1683 archived = Column(
1684 1684 "archived", Boolean(), nullable=True, unique=None, default=None)
1685 1685 enable_statistics = Column(
1686 1686 "statistics", Boolean(), nullable=True, unique=None, default=True)
1687 1687 enable_downloads = Column(
1688 1688 "downloads", Boolean(), nullable=True, unique=None, default=True)
1689 1689 description = Column(
1690 1690 "description", String(10000), nullable=True, unique=None, default=None)
1691 1691 created_on = Column(
1692 1692 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1693 1693 default=datetime.datetime.now)
1694 1694 updated_on = Column(
1695 1695 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1696 1696 default=datetime.datetime.now)
1697 1697 _landing_revision = Column(
1698 1698 "landing_revision", String(255), nullable=False, unique=False,
1699 1699 default=None)
1700 1700 enable_locking = Column(
1701 1701 "enable_locking", Boolean(), nullable=False, unique=None,
1702 1702 default=False)
1703 1703 _locked = Column(
1704 1704 "locked", String(255), nullable=True, unique=False, default=None)
1705 1705 _changeset_cache = Column(
1706 1706 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1707 1707
1708 1708 fork_id = Column(
1709 1709 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1710 1710 nullable=True, unique=False, default=None)
1711 1711 group_id = Column(
1712 1712 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1713 1713 unique=False, default=None)
1714 1714
1715 1715 user = relationship('User', lazy='joined')
1716 1716 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1717 1717 group = relationship('RepoGroup', lazy='joined')
1718 1718 repo_to_perm = relationship(
1719 1719 'UserRepoToPerm', cascade='all',
1720 1720 order_by='UserRepoToPerm.repo_to_perm_id')
1721 1721 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1722 1722 stats = relationship('Statistics', cascade='all', uselist=False)
1723 1723
1724 1724 followers = relationship(
1725 1725 'UserFollowing',
1726 1726 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
1727 1727 cascade='all')
1728 1728 extra_fields = relationship(
1729 1729 'RepositoryField', cascade="all, delete-orphan")
1730 1730 logs = relationship('UserLog')
1731 1731 comments = relationship(
1732 1732 'ChangesetComment', cascade="all, delete-orphan")
1733 1733 pull_requests_source = relationship(
1734 1734 'PullRequest',
1735 1735 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1736 1736 cascade="all, delete-orphan")
1737 1737 pull_requests_target = relationship(
1738 1738 'PullRequest',
1739 1739 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1740 1740 cascade="all, delete-orphan")
1741 1741 ui = relationship('RepoRhodeCodeUi', cascade="all")
1742 1742 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1743 1743 integrations = relationship('Integration', cascade="all, delete-orphan")
1744 1744
1745 1745 scoped_tokens = relationship('UserApiKeys', cascade="all")
1746 1746
1747 1747 # no cascade, set NULL
1748 1748 artifacts = relationship('FileStore', primaryjoin='FileStore.scope_repo_id==Repository.repo_id')
1749 1749
1750 1750 def __unicode__(self):
1751 1751 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1752 1752 safe_unicode(self.repo_name))
1753 1753
1754 1754 @hybrid_property
1755 1755 def description_safe(self):
1756 1756 from rhodecode.lib import helpers as h
1757 1757 return h.escape(self.description)
1758 1758
1759 1759 @hybrid_property
1760 1760 def landing_rev(self):
1761 # always should return [rev_type, rev]
1761 # always should return [rev_type, rev], e.g ['branch', 'master']
1762 1762 if self._landing_revision:
1763 1763 _rev_info = self._landing_revision.split(':')
1764 1764 if len(_rev_info) < 2:
1765 1765 _rev_info.insert(0, 'rev')
1766 1766 return [_rev_info[0], _rev_info[1]]
1767 1767 return [None, None]
1768 1768
1769 @property
1770 def landing_ref_type(self):
1771 return self.landing_rev[0]
1772
1773 @property
1774 def landing_ref_name(self):
1775 return self.landing_rev[1]
1776
1769 1777 @landing_rev.setter
1770 1778 def landing_rev(self, val):
1771 1779 if ':' not in val:
1772 1780 raise ValueError('value must be delimited with `:` and consist '
1773 1781 'of <rev_type>:<rev>, got %s instead' % val)
1774 1782 self._landing_revision = val
1775 1783
1776 1784 @hybrid_property
1777 1785 def locked(self):
1778 1786 if self._locked:
1779 1787 user_id, timelocked, reason = self._locked.split(':')
1780 1788 lock_values = int(user_id), timelocked, reason
1781 1789 else:
1782 1790 lock_values = [None, None, None]
1783 1791 return lock_values
1784 1792
1785 1793 @locked.setter
1786 1794 def locked(self, val):
1787 1795 if val and isinstance(val, (list, tuple)):
1788 1796 self._locked = ':'.join(map(str, val))
1789 1797 else:
1790 1798 self._locked = None
1791 1799
1792 1800 @classmethod
1793 1801 def _load_changeset_cache(cls, repo_id, changeset_cache_raw):
1794 1802 from rhodecode.lib.vcs.backends.base import EmptyCommit
1795 1803 dummy = EmptyCommit().__json__()
1796 1804 if not changeset_cache_raw:
1797 1805 dummy['source_repo_id'] = repo_id
1798 1806 return json.loads(json.dumps(dummy))
1799 1807
1800 1808 try:
1801 1809 return json.loads(changeset_cache_raw)
1802 1810 except TypeError:
1803 1811 return dummy
1804 1812 except Exception:
1805 1813 log.error(traceback.format_exc())
1806 1814 return dummy
1807 1815
1808 1816 @hybrid_property
1809 1817 def changeset_cache(self):
1810 1818 return self._load_changeset_cache(self.repo_id, self._changeset_cache)
1811 1819
1812 1820 @changeset_cache.setter
1813 1821 def changeset_cache(self, val):
1814 1822 try:
1815 1823 self._changeset_cache = json.dumps(val)
1816 1824 except Exception:
1817 1825 log.error(traceback.format_exc())
1818 1826
1819 1827 @hybrid_property
1820 1828 def repo_name(self):
1821 1829 return self._repo_name
1822 1830
1823 1831 @repo_name.setter
1824 1832 def repo_name(self, value):
1825 1833 self._repo_name = value
1826 1834 self.repo_name_hash = hashlib.sha1(safe_str(value)).hexdigest()
1827 1835
1828 1836 @classmethod
1829 1837 def normalize_repo_name(cls, repo_name):
1830 1838 """
1831 1839 Normalizes os specific repo_name to the format internally stored inside
1832 1840 database using URL_SEP
1833 1841
1834 1842 :param cls:
1835 1843 :param repo_name:
1836 1844 """
1837 1845 return cls.NAME_SEP.join(repo_name.split(os.sep))
1838 1846
1839 1847 @classmethod
1840 1848 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1841 1849 session = Session()
1842 1850 q = session.query(cls).filter(cls.repo_name == repo_name)
1843 1851
1844 1852 if cache:
1845 1853 if identity_cache:
1846 1854 val = cls.identity_cache(session, 'repo_name', repo_name)
1847 1855 if val:
1848 1856 return val
1849 1857 else:
1850 1858 cache_key = "get_repo_by_name_%s" % _hash_key(repo_name)
1851 1859 q = q.options(
1852 1860 FromCache("sql_cache_short", cache_key))
1853 1861
1854 1862 return q.scalar()
1855 1863
1856 1864 @classmethod
1857 1865 def get_by_id_or_repo_name(cls, repoid):
1858 1866 if isinstance(repoid, (int, long)):
1859 1867 try:
1860 1868 repo = cls.get(repoid)
1861 1869 except ValueError:
1862 1870 repo = None
1863 1871 else:
1864 1872 repo = cls.get_by_repo_name(repoid)
1865 1873 return repo
1866 1874
1867 1875 @classmethod
1868 1876 def get_by_full_path(cls, repo_full_path):
1869 1877 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1870 1878 repo_name = cls.normalize_repo_name(repo_name)
1871 1879 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1872 1880
1873 1881 @classmethod
1874 1882 def get_repo_forks(cls, repo_id):
1875 1883 return cls.query().filter(Repository.fork_id == repo_id)
1876 1884
1877 1885 @classmethod
1878 1886 def base_path(cls):
1879 1887 """
1880 1888 Returns base path when all repos are stored
1881 1889
1882 1890 :param cls:
1883 1891 """
1884 1892 q = Session().query(RhodeCodeUi)\
1885 1893 .filter(RhodeCodeUi.ui_key == cls.NAME_SEP)
1886 1894 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1887 1895 return q.one().ui_value
1888 1896
1889 1897 @classmethod
1890 1898 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1891 1899 case_insensitive=True, archived=False):
1892 1900 q = Repository.query()
1893 1901
1894 1902 if not archived:
1895 1903 q = q.filter(Repository.archived.isnot(true()))
1896 1904
1897 1905 if not isinstance(user_id, Optional):
1898 1906 q = q.filter(Repository.user_id == user_id)
1899 1907
1900 1908 if not isinstance(group_id, Optional):
1901 1909 q = q.filter(Repository.group_id == group_id)
1902 1910
1903 1911 if case_insensitive:
1904 1912 q = q.order_by(func.lower(Repository.repo_name))
1905 1913 else:
1906 1914 q = q.order_by(Repository.repo_name)
1907 1915
1908 1916 return q.all()
1909 1917
1910 1918 @property
1911 1919 def repo_uid(self):
1912 1920 return '_{}'.format(self.repo_id)
1913 1921
1914 1922 @property
1915 1923 def forks(self):
1916 1924 """
1917 1925 Return forks of this repo
1918 1926 """
1919 1927 return Repository.get_repo_forks(self.repo_id)
1920 1928
1921 1929 @property
1922 1930 def parent(self):
1923 1931 """
1924 1932 Returns fork parent
1925 1933 """
1926 1934 return self.fork
1927 1935
1928 1936 @property
1929 1937 def just_name(self):
1930 1938 return self.repo_name.split(self.NAME_SEP)[-1]
1931 1939
1932 1940 @property
1933 1941 def groups_with_parents(self):
1934 1942 groups = []
1935 1943 if self.group is None:
1936 1944 return groups
1937 1945
1938 1946 cur_gr = self.group
1939 1947 groups.insert(0, cur_gr)
1940 1948 while 1:
1941 1949 gr = getattr(cur_gr, 'parent_group', None)
1942 1950 cur_gr = cur_gr.parent_group
1943 1951 if gr is None:
1944 1952 break
1945 1953 groups.insert(0, gr)
1946 1954
1947 1955 return groups
1948 1956
1949 1957 @property
1950 1958 def groups_and_repo(self):
1951 1959 return self.groups_with_parents, self
1952 1960
1953 1961 @LazyProperty
1954 1962 def repo_path(self):
1955 1963 """
1956 1964 Returns base full path for that repository means where it actually
1957 1965 exists on a filesystem
1958 1966 """
1959 1967 q = Session().query(RhodeCodeUi).filter(
1960 1968 RhodeCodeUi.ui_key == self.NAME_SEP)
1961 1969 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1962 1970 return q.one().ui_value
1963 1971
1964 1972 @property
1965 1973 def repo_full_path(self):
1966 1974 p = [self.repo_path]
1967 1975 # we need to split the name by / since this is how we store the
1968 1976 # names in the database, but that eventually needs to be converted
1969 1977 # into a valid system path
1970 1978 p += self.repo_name.split(self.NAME_SEP)
1971 1979 return os.path.join(*map(safe_unicode, p))
1972 1980
1973 1981 @property
1974 1982 def cache_keys(self):
1975 1983 """
1976 1984 Returns associated cache keys for that repo
1977 1985 """
1978 1986 invalidation_namespace = CacheKey.REPO_INVALIDATION_NAMESPACE.format(
1979 1987 repo_id=self.repo_id)
1980 1988 return CacheKey.query()\
1981 1989 .filter(CacheKey.cache_args == invalidation_namespace)\
1982 1990 .order_by(CacheKey.cache_key)\
1983 1991 .all()
1984 1992
1985 1993 @property
1986 1994 def cached_diffs_relative_dir(self):
1987 1995 """
1988 1996 Return a relative to the repository store path of cached diffs
1989 1997 used for safe display for users, who shouldn't know the absolute store
1990 1998 path
1991 1999 """
1992 2000 return os.path.join(
1993 2001 os.path.dirname(self.repo_name),
1994 2002 self.cached_diffs_dir.split(os.path.sep)[-1])
1995 2003
1996 2004 @property
1997 2005 def cached_diffs_dir(self):
1998 2006 path = self.repo_full_path
1999 2007 return os.path.join(
2000 2008 os.path.dirname(path),
2001 2009 '.__shadow_diff_cache_repo_{}'.format(self.repo_id))
2002 2010
2003 2011 def cached_diffs(self):
2004 2012 diff_cache_dir = self.cached_diffs_dir
2005 2013 if os.path.isdir(diff_cache_dir):
2006 2014 return os.listdir(diff_cache_dir)
2007 2015 return []
2008 2016
2009 2017 def shadow_repos(self):
2010 2018 shadow_repos_pattern = '.__shadow_repo_{}'.format(self.repo_id)
2011 2019 return [
2012 2020 x for x in os.listdir(os.path.dirname(self.repo_full_path))
2013 2021 if x.startswith(shadow_repos_pattern)]
2014 2022
2015 2023 def get_new_name(self, repo_name):
2016 2024 """
2017 2025 returns new full repository name based on assigned group and new new
2018 2026
2019 2027 :param group_name:
2020 2028 """
2021 2029 path_prefix = self.group.full_path_splitted if self.group else []
2022 2030 return self.NAME_SEP.join(path_prefix + [repo_name])
2023 2031
2024 2032 @property
2025 2033 def _config(self):
2026 2034 """
2027 2035 Returns db based config object.
2028 2036 """
2029 2037 from rhodecode.lib.utils import make_db_config
2030 2038 return make_db_config(clear_session=False, repo=self)
2031 2039
2032 2040 def permissions(self, with_admins=True, with_owner=True,
2033 2041 expand_from_user_groups=False):
2034 2042 """
2035 2043 Permissions for repositories
2036 2044 """
2037 2045 _admin_perm = 'repository.admin'
2038 2046
2039 2047 owner_row = []
2040 2048 if with_owner:
2041 2049 usr = AttributeDict(self.user.get_dict())
2042 2050 usr.owner_row = True
2043 2051 usr.permission = _admin_perm
2044 2052 usr.permission_id = None
2045 2053 owner_row.append(usr)
2046 2054
2047 2055 super_admin_ids = []
2048 2056 super_admin_rows = []
2049 2057 if with_admins:
2050 2058 for usr in User.get_all_super_admins():
2051 2059 super_admin_ids.append(usr.user_id)
2052 2060 # if this admin is also owner, don't double the record
2053 2061 if usr.user_id == owner_row[0].user_id:
2054 2062 owner_row[0].admin_row = True
2055 2063 else:
2056 2064 usr = AttributeDict(usr.get_dict())
2057 2065 usr.admin_row = True
2058 2066 usr.permission = _admin_perm
2059 2067 usr.permission_id = None
2060 2068 super_admin_rows.append(usr)
2061 2069
2062 2070 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
2063 2071 q = q.options(joinedload(UserRepoToPerm.repository),
2064 2072 joinedload(UserRepoToPerm.user),
2065 2073 joinedload(UserRepoToPerm.permission),)
2066 2074
2067 2075 # get owners and admins and permissions. We do a trick of re-writing
2068 2076 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2069 2077 # has a global reference and changing one object propagates to all
2070 2078 # others. This means if admin is also an owner admin_row that change
2071 2079 # would propagate to both objects
2072 2080 perm_rows = []
2073 2081 for _usr in q.all():
2074 2082 usr = AttributeDict(_usr.user.get_dict())
2075 2083 # if this user is also owner/admin, mark as duplicate record
2076 2084 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
2077 2085 usr.duplicate_perm = True
2078 2086 # also check if this permission is maybe used by branch_permissions
2079 2087 if _usr.branch_perm_entry:
2080 2088 usr.branch_rules = [x.branch_rule_id for x in _usr.branch_perm_entry]
2081 2089
2082 2090 usr.permission = _usr.permission.permission_name
2083 2091 usr.permission_id = _usr.repo_to_perm_id
2084 2092 perm_rows.append(usr)
2085 2093
2086 2094 # filter the perm rows by 'default' first and then sort them by
2087 2095 # admin,write,read,none permissions sorted again alphabetically in
2088 2096 # each group
2089 2097 perm_rows = sorted(perm_rows, key=display_user_sort)
2090 2098
2091 2099 user_groups_rows = []
2092 2100 if expand_from_user_groups:
2093 2101 for ug in self.permission_user_groups(with_members=True):
2094 2102 for user_data in ug.members:
2095 2103 user_groups_rows.append(user_data)
2096 2104
2097 2105 return super_admin_rows + owner_row + perm_rows + user_groups_rows
2098 2106
2099 2107 def permission_user_groups(self, with_members=True):
2100 2108 q = UserGroupRepoToPerm.query()\
2101 2109 .filter(UserGroupRepoToPerm.repository == self)
2102 2110 q = q.options(joinedload(UserGroupRepoToPerm.repository),
2103 2111 joinedload(UserGroupRepoToPerm.users_group),
2104 2112 joinedload(UserGroupRepoToPerm.permission),)
2105 2113
2106 2114 perm_rows = []
2107 2115 for _user_group in q.all():
2108 2116 entry = AttributeDict(_user_group.users_group.get_dict())
2109 2117 entry.permission = _user_group.permission.permission_name
2110 2118 if with_members:
2111 2119 entry.members = [x.user.get_dict()
2112 2120 for x in _user_group.users_group.members]
2113 2121 perm_rows.append(entry)
2114 2122
2115 2123 perm_rows = sorted(perm_rows, key=display_user_group_sort)
2116 2124 return perm_rows
2117 2125
2118 2126 def get_api_data(self, include_secrets=False):
2119 2127 """
2120 2128 Common function for generating repo api data
2121 2129
2122 2130 :param include_secrets: See :meth:`User.get_api_data`.
2123 2131
2124 2132 """
2125 2133 # TODO: mikhail: Here there is an anti-pattern, we probably need to
2126 2134 # move this methods on models level.
2127 2135 from rhodecode.model.settings import SettingsModel
2128 2136 from rhodecode.model.repo import RepoModel
2129 2137
2130 2138 repo = self
2131 2139 _user_id, _time, _reason = self.locked
2132 2140
2133 2141 data = {
2134 2142 'repo_id': repo.repo_id,
2135 2143 'repo_name': repo.repo_name,
2136 2144 'repo_type': repo.repo_type,
2137 2145 'clone_uri': repo.clone_uri or '',
2138 2146 'push_uri': repo.push_uri or '',
2139 2147 'url': RepoModel().get_url(self),
2140 2148 'private': repo.private,
2141 2149 'created_on': repo.created_on,
2142 2150 'description': repo.description_safe,
2143 2151 'landing_rev': repo.landing_rev,
2144 2152 'owner': repo.user.username,
2145 2153 'fork_of': repo.fork.repo_name if repo.fork else None,
2146 2154 'fork_of_id': repo.fork.repo_id if repo.fork else None,
2147 2155 'enable_statistics': repo.enable_statistics,
2148 2156 'enable_locking': repo.enable_locking,
2149 2157 'enable_downloads': repo.enable_downloads,
2150 2158 'last_changeset': repo.changeset_cache,
2151 2159 'locked_by': User.get(_user_id).get_api_data(
2152 2160 include_secrets=include_secrets) if _user_id else None,
2153 2161 'locked_date': time_to_datetime(_time) if _time else None,
2154 2162 'lock_reason': _reason if _reason else None,
2155 2163 }
2156 2164
2157 2165 # TODO: mikhail: should be per-repo settings here
2158 2166 rc_config = SettingsModel().get_all_settings()
2159 2167 repository_fields = str2bool(
2160 2168 rc_config.get('rhodecode_repository_fields'))
2161 2169 if repository_fields:
2162 2170 for f in self.extra_fields:
2163 2171 data[f.field_key_prefixed] = f.field_value
2164 2172
2165 2173 return data
2166 2174
2167 2175 @classmethod
2168 2176 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
2169 2177 if not lock_time:
2170 2178 lock_time = time.time()
2171 2179 if not lock_reason:
2172 2180 lock_reason = cls.LOCK_AUTOMATIC
2173 2181 repo.locked = [user_id, lock_time, lock_reason]
2174 2182 Session().add(repo)
2175 2183 Session().commit()
2176 2184
2177 2185 @classmethod
2178 2186 def unlock(cls, repo):
2179 2187 repo.locked = None
2180 2188 Session().add(repo)
2181 2189 Session().commit()
2182 2190
2183 2191 @classmethod
2184 2192 def getlock(cls, repo):
2185 2193 return repo.locked
2186 2194
2187 2195 def is_user_lock(self, user_id):
2188 2196 if self.lock[0]:
2189 2197 lock_user_id = safe_int(self.lock[0])
2190 2198 user_id = safe_int(user_id)
2191 2199 # both are ints, and they are equal
2192 2200 return all([lock_user_id, user_id]) and lock_user_id == user_id
2193 2201
2194 2202 return False
2195 2203
2196 2204 def get_locking_state(self, action, user_id, only_when_enabled=True):
2197 2205 """
2198 2206 Checks locking on this repository, if locking is enabled and lock is
2199 2207 present returns a tuple of make_lock, locked, locked_by.
2200 2208 make_lock can have 3 states None (do nothing) True, make lock
2201 2209 False release lock, This value is later propagated to hooks, which
2202 2210 do the locking. Think about this as signals passed to hooks what to do.
2203 2211
2204 2212 """
2205 2213 # TODO: johbo: This is part of the business logic and should be moved
2206 2214 # into the RepositoryModel.
2207 2215
2208 2216 if action not in ('push', 'pull'):
2209 2217 raise ValueError("Invalid action value: %s" % repr(action))
2210 2218
2211 2219 # defines if locked error should be thrown to user
2212 2220 currently_locked = False
2213 2221 # defines if new lock should be made, tri-state
2214 2222 make_lock = None
2215 2223 repo = self
2216 2224 user = User.get(user_id)
2217 2225
2218 2226 lock_info = repo.locked
2219 2227
2220 2228 if repo and (repo.enable_locking or not only_when_enabled):
2221 2229 if action == 'push':
2222 2230 # check if it's already locked !, if it is compare users
2223 2231 locked_by_user_id = lock_info[0]
2224 2232 if user.user_id == locked_by_user_id:
2225 2233 log.debug(
2226 2234 'Got `push` action from user %s, now unlocking', user)
2227 2235 # unlock if we have push from user who locked
2228 2236 make_lock = False
2229 2237 else:
2230 2238 # we're not the same user who locked, ban with
2231 2239 # code defined in settings (default is 423 HTTP Locked) !
2232 2240 log.debug('Repo %s is currently locked by %s', repo, user)
2233 2241 currently_locked = True
2234 2242 elif action == 'pull':
2235 2243 # [0] user [1] date
2236 2244 if lock_info[0] and lock_info[1]:
2237 2245 log.debug('Repo %s is currently locked by %s', repo, user)
2238 2246 currently_locked = True
2239 2247 else:
2240 2248 log.debug('Setting lock on repo %s by %s', repo, user)
2241 2249 make_lock = True
2242 2250
2243 2251 else:
2244 2252 log.debug('Repository %s do not have locking enabled', repo)
2245 2253
2246 2254 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
2247 2255 make_lock, currently_locked, lock_info)
2248 2256
2249 2257 from rhodecode.lib.auth import HasRepoPermissionAny
2250 2258 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
2251 2259 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
2252 2260 # if we don't have at least write permission we cannot make a lock
2253 2261 log.debug('lock state reset back to FALSE due to lack '
2254 2262 'of at least read permission')
2255 2263 make_lock = False
2256 2264
2257 2265 return make_lock, currently_locked, lock_info
2258 2266
2259 2267 @property
2260 2268 def last_commit_cache_update_diff(self):
2261 2269 return time.time() - (safe_int(self.changeset_cache.get('updated_on')) or 0)
2262 2270
2263 2271 @classmethod
2264 2272 def _load_commit_change(cls, last_commit_cache):
2265 2273 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2266 2274 empty_date = datetime.datetime.fromtimestamp(0)
2267 2275 date_latest = last_commit_cache.get('date', empty_date)
2268 2276 try:
2269 2277 return parse_datetime(date_latest)
2270 2278 except Exception:
2271 2279 return empty_date
2272 2280
2273 2281 @property
2274 2282 def last_commit_change(self):
2275 2283 return self._load_commit_change(self.changeset_cache)
2276 2284
2277 2285 @property
2278 2286 def last_db_change(self):
2279 2287 return self.updated_on
2280 2288
2281 2289 @property
2282 2290 def clone_uri_hidden(self):
2283 2291 clone_uri = self.clone_uri
2284 2292 if clone_uri:
2285 2293 import urlobject
2286 2294 url_obj = urlobject.URLObject(cleaned_uri(clone_uri))
2287 2295 if url_obj.password:
2288 2296 clone_uri = url_obj.with_password('*****')
2289 2297 return clone_uri
2290 2298
2291 2299 @property
2292 2300 def push_uri_hidden(self):
2293 2301 push_uri = self.push_uri
2294 2302 if push_uri:
2295 2303 import urlobject
2296 2304 url_obj = urlobject.URLObject(cleaned_uri(push_uri))
2297 2305 if url_obj.password:
2298 2306 push_uri = url_obj.with_password('*****')
2299 2307 return push_uri
2300 2308
2301 2309 def clone_url(self, **override):
2302 2310 from rhodecode.model.settings import SettingsModel
2303 2311
2304 2312 uri_tmpl = None
2305 2313 if 'with_id' in override:
2306 2314 uri_tmpl = self.DEFAULT_CLONE_URI_ID
2307 2315 del override['with_id']
2308 2316
2309 2317 if 'uri_tmpl' in override:
2310 2318 uri_tmpl = override['uri_tmpl']
2311 2319 del override['uri_tmpl']
2312 2320
2313 2321 ssh = False
2314 2322 if 'ssh' in override:
2315 2323 ssh = True
2316 2324 del override['ssh']
2317 2325
2318 2326 # we didn't override our tmpl from **overrides
2319 2327 request = get_current_request()
2320 2328 if not uri_tmpl:
2321 2329 if hasattr(request, 'call_context') and hasattr(request.call_context, 'rc_config'):
2322 2330 rc_config = request.call_context.rc_config
2323 2331 else:
2324 2332 rc_config = SettingsModel().get_all_settings(cache=True)
2325 2333
2326 2334 if ssh:
2327 2335 uri_tmpl = rc_config.get(
2328 2336 'rhodecode_clone_uri_ssh_tmpl') or self.DEFAULT_CLONE_URI_SSH
2329 2337
2330 2338 else:
2331 2339 uri_tmpl = rc_config.get(
2332 2340 'rhodecode_clone_uri_tmpl') or self.DEFAULT_CLONE_URI
2333 2341
2334 2342 return get_clone_url(request=request,
2335 2343 uri_tmpl=uri_tmpl,
2336 2344 repo_name=self.repo_name,
2337 2345 repo_id=self.repo_id,
2338 2346 repo_type=self.repo_type,
2339 2347 **override)
2340 2348
2341 2349 def set_state(self, state):
2342 2350 self.repo_state = state
2343 2351 Session().add(self)
2344 2352 #==========================================================================
2345 2353 # SCM PROPERTIES
2346 2354 #==========================================================================
2347 2355
2348 2356 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None, maybe_unreachable=False):
2349 2357 return get_commit_safe(
2350 2358 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load,
2351 2359 maybe_unreachable=maybe_unreachable)
2352 2360
2353 2361 def get_changeset(self, rev=None, pre_load=None):
2354 2362 warnings.warn("Use get_commit", DeprecationWarning)
2355 2363 commit_id = None
2356 2364 commit_idx = None
2357 2365 if isinstance(rev, compat.string_types):
2358 2366 commit_id = rev
2359 2367 else:
2360 2368 commit_idx = rev
2361 2369 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
2362 2370 pre_load=pre_load)
2363 2371
2364 2372 def get_landing_commit(self):
2365 2373 """
2366 2374 Returns landing commit, or if that doesn't exist returns the tip
2367 2375 """
2368 2376 _rev_type, _rev = self.landing_rev
2369 2377 commit = self.get_commit(_rev)
2370 2378 if isinstance(commit, EmptyCommit):
2371 2379 return self.get_commit()
2372 2380 return commit
2373 2381
2374 2382 def flush_commit_cache(self):
2375 2383 self.update_commit_cache(cs_cache={'raw_id':'0'})
2376 2384 self.update_commit_cache()
2377 2385
2378 2386 def update_commit_cache(self, cs_cache=None, config=None):
2379 2387 """
2380 2388 Update cache of last commit for repository
2381 2389 cache_keys should be::
2382 2390
2383 2391 source_repo_id
2384 2392 short_id
2385 2393 raw_id
2386 2394 revision
2387 2395 parents
2388 2396 message
2389 2397 date
2390 2398 author
2391 2399 updated_on
2392 2400
2393 2401 """
2394 2402 from rhodecode.lib.vcs.backends.base import BaseChangeset
2395 2403 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2396 2404 empty_date = datetime.datetime.fromtimestamp(0)
2397 2405
2398 2406 if cs_cache is None:
2399 2407 # use no-cache version here
2400 2408 try:
2401 2409 scm_repo = self.scm_instance(cache=False, config=config)
2402 2410 except VCSError:
2403 2411 scm_repo = None
2404 2412 empty = scm_repo is None or scm_repo.is_empty()
2405 2413
2406 2414 if not empty:
2407 2415 cs_cache = scm_repo.get_commit(
2408 2416 pre_load=["author", "date", "message", "parents", "branch"])
2409 2417 else:
2410 2418 cs_cache = EmptyCommit()
2411 2419
2412 2420 if isinstance(cs_cache, BaseChangeset):
2413 2421 cs_cache = cs_cache.__json__()
2414 2422
2415 2423 def is_outdated(new_cs_cache):
2416 2424 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
2417 2425 new_cs_cache['revision'] != self.changeset_cache['revision']):
2418 2426 return True
2419 2427 return False
2420 2428
2421 2429 # check if we have maybe already latest cached revision
2422 2430 if is_outdated(cs_cache) or not self.changeset_cache:
2423 2431 _current_datetime = datetime.datetime.utcnow()
2424 2432 last_change = cs_cache.get('date') or _current_datetime
2425 2433 # we check if last update is newer than the new value
2426 2434 # if yes, we use the current timestamp instead. Imagine you get
2427 2435 # old commit pushed 1y ago, we'd set last update 1y to ago.
2428 2436 last_change_timestamp = datetime_to_time(last_change)
2429 2437 current_timestamp = datetime_to_time(last_change)
2430 2438 if last_change_timestamp > current_timestamp and not empty:
2431 2439 cs_cache['date'] = _current_datetime
2432 2440
2433 2441 _date_latest = parse_datetime(cs_cache.get('date') or empty_date)
2434 2442 cs_cache['updated_on'] = time.time()
2435 2443 self.changeset_cache = cs_cache
2436 2444 self.updated_on = last_change
2437 2445 Session().add(self)
2438 2446 Session().commit()
2439 2447
2440 2448 else:
2441 2449 if empty:
2442 2450 cs_cache = EmptyCommit().__json__()
2443 2451 else:
2444 2452 cs_cache = self.changeset_cache
2445 2453
2446 2454 _date_latest = parse_datetime(cs_cache.get('date') or empty_date)
2447 2455
2448 2456 cs_cache['updated_on'] = time.time()
2449 2457 self.changeset_cache = cs_cache
2450 2458 self.updated_on = _date_latest
2451 2459 Session().add(self)
2452 2460 Session().commit()
2453 2461
2454 2462 log.debug('updated repo `%s` with new commit cache %s, and last update_date: %s',
2455 2463 self.repo_name, cs_cache, _date_latest)
2456 2464
2457 2465 @property
2458 2466 def tip(self):
2459 2467 return self.get_commit('tip')
2460 2468
2461 2469 @property
2462 2470 def author(self):
2463 2471 return self.tip.author
2464 2472
2465 2473 @property
2466 2474 def last_change(self):
2467 2475 return self.scm_instance().last_change
2468 2476
2469 2477 def get_comments(self, revisions=None):
2470 2478 """
2471 2479 Returns comments for this repository grouped by revisions
2472 2480
2473 2481 :param revisions: filter query by revisions only
2474 2482 """
2475 2483 cmts = ChangesetComment.query()\
2476 2484 .filter(ChangesetComment.repo == self)
2477 2485 if revisions:
2478 2486 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
2479 2487 grouped = collections.defaultdict(list)
2480 2488 for cmt in cmts.all():
2481 2489 grouped[cmt.revision].append(cmt)
2482 2490 return grouped
2483 2491
2484 2492 def statuses(self, revisions=None):
2485 2493 """
2486 2494 Returns statuses for this repository
2487 2495
2488 2496 :param revisions: list of revisions to get statuses for
2489 2497 """
2490 2498 statuses = ChangesetStatus.query()\
2491 2499 .filter(ChangesetStatus.repo == self)\
2492 2500 .filter(ChangesetStatus.version == 0)
2493 2501
2494 2502 if revisions:
2495 2503 # Try doing the filtering in chunks to avoid hitting limits
2496 2504 size = 500
2497 2505 status_results = []
2498 2506 for chunk in xrange(0, len(revisions), size):
2499 2507 status_results += statuses.filter(
2500 2508 ChangesetStatus.revision.in_(
2501 2509 revisions[chunk: chunk+size])
2502 2510 ).all()
2503 2511 else:
2504 2512 status_results = statuses.all()
2505 2513
2506 2514 grouped = {}
2507 2515
2508 2516 # maybe we have open new pullrequest without a status?
2509 2517 stat = ChangesetStatus.STATUS_UNDER_REVIEW
2510 2518 status_lbl = ChangesetStatus.get_status_lbl(stat)
2511 2519 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
2512 2520 for rev in pr.revisions:
2513 2521 pr_id = pr.pull_request_id
2514 2522 pr_repo = pr.target_repo.repo_name
2515 2523 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
2516 2524
2517 2525 for stat in status_results:
2518 2526 pr_id = pr_repo = None
2519 2527 if stat.pull_request:
2520 2528 pr_id = stat.pull_request.pull_request_id
2521 2529 pr_repo = stat.pull_request.target_repo.repo_name
2522 2530 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
2523 2531 pr_id, pr_repo]
2524 2532 return grouped
2525 2533
2526 2534 # ==========================================================================
2527 2535 # SCM CACHE INSTANCE
2528 2536 # ==========================================================================
2529 2537
2530 2538 def scm_instance(self, **kwargs):
2531 2539 import rhodecode
2532 2540
2533 2541 # Passing a config will not hit the cache currently only used
2534 2542 # for repo2dbmapper
2535 2543 config = kwargs.pop('config', None)
2536 2544 cache = kwargs.pop('cache', None)
2537 2545 vcs_full_cache = kwargs.pop('vcs_full_cache', None)
2538 2546 if vcs_full_cache is not None:
2539 2547 # allows override global config
2540 2548 full_cache = vcs_full_cache
2541 2549 else:
2542 2550 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
2543 2551 # if cache is NOT defined use default global, else we have a full
2544 2552 # control over cache behaviour
2545 2553 if cache is None and full_cache and not config:
2546 2554 log.debug('Initializing pure cached instance for %s', self.repo_path)
2547 2555 return self._get_instance_cached()
2548 2556
2549 2557 # cache here is sent to the "vcs server"
2550 2558 return self._get_instance(cache=bool(cache), config=config)
2551 2559
2552 2560 def _get_instance_cached(self):
2553 2561 from rhodecode.lib import rc_cache
2554 2562
2555 2563 cache_namespace_uid = 'cache_repo_instance.{}'.format(self.repo_id)
2556 2564 invalidation_namespace = CacheKey.REPO_INVALIDATION_NAMESPACE.format(
2557 2565 repo_id=self.repo_id)
2558 2566 region = rc_cache.get_or_create_region('cache_repo_longterm', cache_namespace_uid)
2559 2567
2560 2568 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid)
2561 2569 def get_instance_cached(repo_id, context_id, _cache_state_uid):
2562 2570 return self._get_instance(repo_state_uid=_cache_state_uid)
2563 2571
2564 2572 # we must use thread scoped cache here,
2565 2573 # because each thread of gevent needs it's own not shared connection and cache
2566 2574 # we also alter `args` so the cache key is individual for every green thread.
2567 2575 inv_context_manager = rc_cache.InvalidationContext(
2568 2576 uid=cache_namespace_uid, invalidation_namespace=invalidation_namespace,
2569 2577 thread_scoped=True)
2570 2578 with inv_context_manager as invalidation_context:
2571 2579 cache_state_uid = invalidation_context.cache_data['cache_state_uid']
2572 2580 args = (self.repo_id, inv_context_manager.cache_key, cache_state_uid)
2573 2581
2574 2582 # re-compute and store cache if we get invalidate signal
2575 2583 if invalidation_context.should_invalidate():
2576 2584 instance = get_instance_cached.refresh(*args)
2577 2585 else:
2578 2586 instance = get_instance_cached(*args)
2579 2587
2580 2588 log.debug('Repo instance fetched in %.4fs', inv_context_manager.compute_time)
2581 2589 return instance
2582 2590
2583 2591 def _get_instance(self, cache=True, config=None, repo_state_uid=None):
2584 2592 log.debug('Initializing %s instance `%s` with cache flag set to: %s',
2585 2593 self.repo_type, self.repo_path, cache)
2586 2594 config = config or self._config
2587 2595 custom_wire = {
2588 2596 'cache': cache, # controls the vcs.remote cache
2589 2597 'repo_state_uid': repo_state_uid
2590 2598 }
2591 2599 repo = get_vcs_instance(
2592 2600 repo_path=safe_str(self.repo_full_path),
2593 2601 config=config,
2594 2602 with_wire=custom_wire,
2595 2603 create=False,
2596 2604 _vcs_alias=self.repo_type)
2597 2605 if repo is not None:
2598 2606 repo.count() # cache rebuild
2599 2607 return repo
2600 2608
2601 2609 def get_shadow_repository_path(self, workspace_id):
2602 2610 from rhodecode.lib.vcs.backends.base import BaseRepository
2603 2611 shadow_repo_path = BaseRepository._get_shadow_repository_path(
2604 2612 self.repo_full_path, self.repo_id, workspace_id)
2605 2613 return shadow_repo_path
2606 2614
2607 2615 def __json__(self):
2608 2616 return {'landing_rev': self.landing_rev}
2609 2617
2610 2618 def get_dict(self):
2611 2619
2612 2620 # Since we transformed `repo_name` to a hybrid property, we need to
2613 2621 # keep compatibility with the code which uses `repo_name` field.
2614 2622
2615 2623 result = super(Repository, self).get_dict()
2616 2624 result['repo_name'] = result.pop('_repo_name', None)
2617 2625 return result
2618 2626
2619 2627
2620 2628 class RepoGroup(Base, BaseModel):
2621 2629 __tablename__ = 'groups'
2622 2630 __table_args__ = (
2623 2631 UniqueConstraint('group_name', 'group_parent_id'),
2624 2632 base_table_args,
2625 2633 )
2626 2634 __mapper_args__ = {'order_by': 'group_name'}
2627 2635
2628 2636 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2629 2637
2630 2638 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2631 2639 _group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2632 2640 group_name_hash = Column("repo_group_name_hash", String(1024), nullable=False, unique=False)
2633 2641 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2634 2642 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2635 2643 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2636 2644 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2637 2645 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2638 2646 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2639 2647 personal = Column('personal', Boolean(), nullable=True, unique=None, default=None)
2640 2648 _changeset_cache = Column("changeset_cache", LargeBinary(), nullable=True) # JSON data
2641 2649
2642 2650 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
2643 2651 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
2644 2652 parent_group = relationship('RepoGroup', remote_side=group_id)
2645 2653 user = relationship('User')
2646 2654 integrations = relationship('Integration', cascade="all, delete-orphan")
2647 2655
2648 2656 # no cascade, set NULL
2649 2657 scope_artifacts = relationship('FileStore', primaryjoin='FileStore.scope_repo_group_id==RepoGroup.group_id')
2650 2658
2651 2659 def __init__(self, group_name='', parent_group=None):
2652 2660 self.group_name = group_name
2653 2661 self.parent_group = parent_group
2654 2662
2655 2663 def __unicode__(self):
2656 2664 return u"<%s('id:%s:%s')>" % (
2657 2665 self.__class__.__name__, self.group_id, self.group_name)
2658 2666
2659 2667 @hybrid_property
2660 2668 def group_name(self):
2661 2669 return self._group_name
2662 2670
2663 2671 @group_name.setter
2664 2672 def group_name(self, value):
2665 2673 self._group_name = value
2666 2674 self.group_name_hash = self.hash_repo_group_name(value)
2667 2675
2668 2676 @classmethod
2669 2677 def _load_changeset_cache(cls, repo_id, changeset_cache_raw):
2670 2678 from rhodecode.lib.vcs.backends.base import EmptyCommit
2671 2679 dummy = EmptyCommit().__json__()
2672 2680 if not changeset_cache_raw:
2673 2681 dummy['source_repo_id'] = repo_id
2674 2682 return json.loads(json.dumps(dummy))
2675 2683
2676 2684 try:
2677 2685 return json.loads(changeset_cache_raw)
2678 2686 except TypeError:
2679 2687 return dummy
2680 2688 except Exception:
2681 2689 log.error(traceback.format_exc())
2682 2690 return dummy
2683 2691
2684 2692 @hybrid_property
2685 2693 def changeset_cache(self):
2686 2694 return self._load_changeset_cache('', self._changeset_cache)
2687 2695
2688 2696 @changeset_cache.setter
2689 2697 def changeset_cache(self, val):
2690 2698 try:
2691 2699 self._changeset_cache = json.dumps(val)
2692 2700 except Exception:
2693 2701 log.error(traceback.format_exc())
2694 2702
2695 2703 @validates('group_parent_id')
2696 2704 def validate_group_parent_id(self, key, val):
2697 2705 """
2698 2706 Check cycle references for a parent group to self
2699 2707 """
2700 2708 if self.group_id and val:
2701 2709 assert val != self.group_id
2702 2710
2703 2711 return val
2704 2712
2705 2713 @hybrid_property
2706 2714 def description_safe(self):
2707 2715 from rhodecode.lib import helpers as h
2708 2716 return h.escape(self.group_description)
2709 2717
2710 2718 @classmethod
2711 2719 def hash_repo_group_name(cls, repo_group_name):
2712 2720 val = remove_formatting(repo_group_name)
2713 2721 val = safe_str(val).lower()
2714 2722 chars = []
2715 2723 for c in val:
2716 2724 if c not in string.ascii_letters:
2717 2725 c = str(ord(c))
2718 2726 chars.append(c)
2719 2727
2720 2728 return ''.join(chars)
2721 2729
2722 2730 @classmethod
2723 2731 def _generate_choice(cls, repo_group):
2724 2732 from webhelpers2.html import literal as _literal
2725 2733 _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k))
2726 2734 return repo_group.group_id, _name(repo_group.full_path_splitted)
2727 2735
2728 2736 @classmethod
2729 2737 def groups_choices(cls, groups=None, show_empty_group=True):
2730 2738 if not groups:
2731 2739 groups = cls.query().all()
2732 2740
2733 2741 repo_groups = []
2734 2742 if show_empty_group:
2735 2743 repo_groups = [(-1, u'-- %s --' % _('No parent'))]
2736 2744
2737 2745 repo_groups.extend([cls._generate_choice(x) for x in groups])
2738 2746
2739 2747 repo_groups = sorted(
2740 2748 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2741 2749 return repo_groups
2742 2750
2743 2751 @classmethod
2744 2752 def url_sep(cls):
2745 2753 return URL_SEP
2746 2754
2747 2755 @classmethod
2748 2756 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2749 2757 if case_insensitive:
2750 2758 gr = cls.query().filter(func.lower(cls.group_name)
2751 2759 == func.lower(group_name))
2752 2760 else:
2753 2761 gr = cls.query().filter(cls.group_name == group_name)
2754 2762 if cache:
2755 2763 name_key = _hash_key(group_name)
2756 2764 gr = gr.options(
2757 2765 FromCache("sql_cache_short", "get_group_%s" % name_key))
2758 2766 return gr.scalar()
2759 2767
2760 2768 @classmethod
2761 2769 def get_user_personal_repo_group(cls, user_id):
2762 2770 user = User.get(user_id)
2763 2771 if user.username == User.DEFAULT_USER:
2764 2772 return None
2765 2773
2766 2774 return cls.query()\
2767 2775 .filter(cls.personal == true()) \
2768 2776 .filter(cls.user == user) \
2769 2777 .order_by(cls.group_id.asc()) \
2770 2778 .first()
2771 2779
2772 2780 @classmethod
2773 2781 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2774 2782 case_insensitive=True):
2775 2783 q = RepoGroup.query()
2776 2784
2777 2785 if not isinstance(user_id, Optional):
2778 2786 q = q.filter(RepoGroup.user_id == user_id)
2779 2787
2780 2788 if not isinstance(group_id, Optional):
2781 2789 q = q.filter(RepoGroup.group_parent_id == group_id)
2782 2790
2783 2791 if case_insensitive:
2784 2792 q = q.order_by(func.lower(RepoGroup.group_name))
2785 2793 else:
2786 2794 q = q.order_by(RepoGroup.group_name)
2787 2795 return q.all()
2788 2796
2789 2797 @property
2790 2798 def parents(self, parents_recursion_limit=10):
2791 2799 groups = []
2792 2800 if self.parent_group is None:
2793 2801 return groups
2794 2802 cur_gr = self.parent_group
2795 2803 groups.insert(0, cur_gr)
2796 2804 cnt = 0
2797 2805 while 1:
2798 2806 cnt += 1
2799 2807 gr = getattr(cur_gr, 'parent_group', None)
2800 2808 cur_gr = cur_gr.parent_group
2801 2809 if gr is None:
2802 2810 break
2803 2811 if cnt == parents_recursion_limit:
2804 2812 # this will prevent accidental infinit loops
2805 2813 log.error('more than %s parents found for group %s, stopping '
2806 2814 'recursive parent fetching', parents_recursion_limit, self)
2807 2815 break
2808 2816
2809 2817 groups.insert(0, gr)
2810 2818 return groups
2811 2819
2812 2820 @property
2813 2821 def last_commit_cache_update_diff(self):
2814 2822 return time.time() - (safe_int(self.changeset_cache.get('updated_on')) or 0)
2815 2823
2816 2824 @classmethod
2817 2825 def _load_commit_change(cls, last_commit_cache):
2818 2826 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2819 2827 empty_date = datetime.datetime.fromtimestamp(0)
2820 2828 date_latest = last_commit_cache.get('date', empty_date)
2821 2829 try:
2822 2830 return parse_datetime(date_latest)
2823 2831 except Exception:
2824 2832 return empty_date
2825 2833
2826 2834 @property
2827 2835 def last_commit_change(self):
2828 2836 return self._load_commit_change(self.changeset_cache)
2829 2837
2830 2838 @property
2831 2839 def last_db_change(self):
2832 2840 return self.updated_on
2833 2841
2834 2842 @property
2835 2843 def children(self):
2836 2844 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2837 2845
2838 2846 @property
2839 2847 def name(self):
2840 2848 return self.group_name.split(RepoGroup.url_sep())[-1]
2841 2849
2842 2850 @property
2843 2851 def full_path(self):
2844 2852 return self.group_name
2845 2853
2846 2854 @property
2847 2855 def full_path_splitted(self):
2848 2856 return self.group_name.split(RepoGroup.url_sep())
2849 2857
2850 2858 @property
2851 2859 def repositories(self):
2852 2860 return Repository.query()\
2853 2861 .filter(Repository.group == self)\
2854 2862 .order_by(Repository.repo_name)
2855 2863
2856 2864 @property
2857 2865 def repositories_recursive_count(self):
2858 2866 cnt = self.repositories.count()
2859 2867
2860 2868 def children_count(group):
2861 2869 cnt = 0
2862 2870 for child in group.children:
2863 2871 cnt += child.repositories.count()
2864 2872 cnt += children_count(child)
2865 2873 return cnt
2866 2874
2867 2875 return cnt + children_count(self)
2868 2876
2869 2877 def _recursive_objects(self, include_repos=True, include_groups=True):
2870 2878 all_ = []
2871 2879
2872 2880 def _get_members(root_gr):
2873 2881 if include_repos:
2874 2882 for r in root_gr.repositories:
2875 2883 all_.append(r)
2876 2884 childs = root_gr.children.all()
2877 2885 if childs:
2878 2886 for gr in childs:
2879 2887 if include_groups:
2880 2888 all_.append(gr)
2881 2889 _get_members(gr)
2882 2890
2883 2891 root_group = []
2884 2892 if include_groups:
2885 2893 root_group = [self]
2886 2894
2887 2895 _get_members(self)
2888 2896 return root_group + all_
2889 2897
2890 2898 def recursive_groups_and_repos(self):
2891 2899 """
2892 2900 Recursive return all groups, with repositories in those groups
2893 2901 """
2894 2902 return self._recursive_objects()
2895 2903
2896 2904 def recursive_groups(self):
2897 2905 """
2898 2906 Returns all children groups for this group including children of children
2899 2907 """
2900 2908 return self._recursive_objects(include_repos=False)
2901 2909
2902 2910 def recursive_repos(self):
2903 2911 """
2904 2912 Returns all children repositories for this group
2905 2913 """
2906 2914 return self._recursive_objects(include_groups=False)
2907 2915
2908 2916 def get_new_name(self, group_name):
2909 2917 """
2910 2918 returns new full group name based on parent and new name
2911 2919
2912 2920 :param group_name:
2913 2921 """
2914 2922 path_prefix = (self.parent_group.full_path_splitted if
2915 2923 self.parent_group else [])
2916 2924 return RepoGroup.url_sep().join(path_prefix + [group_name])
2917 2925
2918 2926 def update_commit_cache(self, config=None):
2919 2927 """
2920 2928 Update cache of last commit for newest repository inside this repository group.
2921 2929 cache_keys should be::
2922 2930
2923 2931 source_repo_id
2924 2932 short_id
2925 2933 raw_id
2926 2934 revision
2927 2935 parents
2928 2936 message
2929 2937 date
2930 2938 author
2931 2939
2932 2940 """
2933 2941 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2934 2942 empty_date = datetime.datetime.fromtimestamp(0)
2935 2943
2936 2944 def repo_groups_and_repos(root_gr):
2937 2945 for _repo in root_gr.repositories:
2938 2946 yield _repo
2939 2947 for child_group in root_gr.children.all():
2940 2948 yield child_group
2941 2949
2942 2950 latest_repo_cs_cache = {}
2943 2951 for obj in repo_groups_and_repos(self):
2944 2952 repo_cs_cache = obj.changeset_cache
2945 2953 date_latest = latest_repo_cs_cache.get('date', empty_date)
2946 2954 date_current = repo_cs_cache.get('date', empty_date)
2947 2955 current_timestamp = datetime_to_time(parse_datetime(date_latest))
2948 2956 if current_timestamp < datetime_to_time(parse_datetime(date_current)):
2949 2957 latest_repo_cs_cache = repo_cs_cache
2950 2958 if hasattr(obj, 'repo_id'):
2951 2959 latest_repo_cs_cache['source_repo_id'] = obj.repo_id
2952 2960 else:
2953 2961 latest_repo_cs_cache['source_repo_id'] = repo_cs_cache.get('source_repo_id')
2954 2962
2955 2963 _date_latest = parse_datetime(latest_repo_cs_cache.get('date') or empty_date)
2956 2964
2957 2965 latest_repo_cs_cache['updated_on'] = time.time()
2958 2966 self.changeset_cache = latest_repo_cs_cache
2959 2967 self.updated_on = _date_latest
2960 2968 Session().add(self)
2961 2969 Session().commit()
2962 2970
2963 2971 log.debug('updated repo group `%s` with new commit cache %s, and last update_date: %s',
2964 2972 self.group_name, latest_repo_cs_cache, _date_latest)
2965 2973
2966 2974 def permissions(self, with_admins=True, with_owner=True,
2967 2975 expand_from_user_groups=False):
2968 2976 """
2969 2977 Permissions for repository groups
2970 2978 """
2971 2979 _admin_perm = 'group.admin'
2972 2980
2973 2981 owner_row = []
2974 2982 if with_owner:
2975 2983 usr = AttributeDict(self.user.get_dict())
2976 2984 usr.owner_row = True
2977 2985 usr.permission = _admin_perm
2978 2986 owner_row.append(usr)
2979 2987
2980 2988 super_admin_ids = []
2981 2989 super_admin_rows = []
2982 2990 if with_admins:
2983 2991 for usr in User.get_all_super_admins():
2984 2992 super_admin_ids.append(usr.user_id)
2985 2993 # if this admin is also owner, don't double the record
2986 2994 if usr.user_id == owner_row[0].user_id:
2987 2995 owner_row[0].admin_row = True
2988 2996 else:
2989 2997 usr = AttributeDict(usr.get_dict())
2990 2998 usr.admin_row = True
2991 2999 usr.permission = _admin_perm
2992 3000 super_admin_rows.append(usr)
2993 3001
2994 3002 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
2995 3003 q = q.options(joinedload(UserRepoGroupToPerm.group),
2996 3004 joinedload(UserRepoGroupToPerm.user),
2997 3005 joinedload(UserRepoGroupToPerm.permission),)
2998 3006
2999 3007 # get owners and admins and permissions. We do a trick of re-writing
3000 3008 # objects from sqlalchemy to named-tuples due to sqlalchemy session
3001 3009 # has a global reference and changing one object propagates to all
3002 3010 # others. This means if admin is also an owner admin_row that change
3003 3011 # would propagate to both objects
3004 3012 perm_rows = []
3005 3013 for _usr in q.all():
3006 3014 usr = AttributeDict(_usr.user.get_dict())
3007 3015 # if this user is also owner/admin, mark as duplicate record
3008 3016 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
3009 3017 usr.duplicate_perm = True
3010 3018 usr.permission = _usr.permission.permission_name
3011 3019 perm_rows.append(usr)
3012 3020
3013 3021 # filter the perm rows by 'default' first and then sort them by
3014 3022 # admin,write,read,none permissions sorted again alphabetically in
3015 3023 # each group
3016 3024 perm_rows = sorted(perm_rows, key=display_user_sort)
3017 3025
3018 3026 user_groups_rows = []
3019 3027 if expand_from_user_groups:
3020 3028 for ug in self.permission_user_groups(with_members=True):
3021 3029 for user_data in ug.members:
3022 3030 user_groups_rows.append(user_data)
3023 3031
3024 3032 return super_admin_rows + owner_row + perm_rows + user_groups_rows
3025 3033
3026 3034 def permission_user_groups(self, with_members=False):
3027 3035 q = UserGroupRepoGroupToPerm.query()\
3028 3036 .filter(UserGroupRepoGroupToPerm.group == self)
3029 3037 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
3030 3038 joinedload(UserGroupRepoGroupToPerm.users_group),
3031 3039 joinedload(UserGroupRepoGroupToPerm.permission),)
3032 3040
3033 3041 perm_rows = []
3034 3042 for _user_group in q.all():
3035 3043 entry = AttributeDict(_user_group.users_group.get_dict())
3036 3044 entry.permission = _user_group.permission.permission_name
3037 3045 if with_members:
3038 3046 entry.members = [x.user.get_dict()
3039 3047 for x in _user_group.users_group.members]
3040 3048 perm_rows.append(entry)
3041 3049
3042 3050 perm_rows = sorted(perm_rows, key=display_user_group_sort)
3043 3051 return perm_rows
3044 3052
3045 3053 def get_api_data(self):
3046 3054 """
3047 3055 Common function for generating api data
3048 3056
3049 3057 """
3050 3058 group = self
3051 3059 data = {
3052 3060 'group_id': group.group_id,
3053 3061 'group_name': group.group_name,
3054 3062 'group_description': group.description_safe,
3055 3063 'parent_group': group.parent_group.group_name if group.parent_group else None,
3056 3064 'repositories': [x.repo_name for x in group.repositories],
3057 3065 'owner': group.user.username,
3058 3066 }
3059 3067 return data
3060 3068
3061 3069 def get_dict(self):
3062 3070 # Since we transformed `group_name` to a hybrid property, we need to
3063 3071 # keep compatibility with the code which uses `group_name` field.
3064 3072 result = super(RepoGroup, self).get_dict()
3065 3073 result['group_name'] = result.pop('_group_name', None)
3066 3074 return result
3067 3075
3068 3076
3069 3077 class Permission(Base, BaseModel):
3070 3078 __tablename__ = 'permissions'
3071 3079 __table_args__ = (
3072 3080 Index('p_perm_name_idx', 'permission_name'),
3073 3081 base_table_args,
3074 3082 )
3075 3083
3076 3084 PERMS = [
3077 3085 ('hg.admin', _('RhodeCode Super Administrator')),
3078 3086
3079 3087 ('repository.none', _('Repository no access')),
3080 3088 ('repository.read', _('Repository read access')),
3081 3089 ('repository.write', _('Repository write access')),
3082 3090 ('repository.admin', _('Repository admin access')),
3083 3091
3084 3092 ('group.none', _('Repository group no access')),
3085 3093 ('group.read', _('Repository group read access')),
3086 3094 ('group.write', _('Repository group write access')),
3087 3095 ('group.admin', _('Repository group admin access')),
3088 3096
3089 3097 ('usergroup.none', _('User group no access')),
3090 3098 ('usergroup.read', _('User group read access')),
3091 3099 ('usergroup.write', _('User group write access')),
3092 3100 ('usergroup.admin', _('User group admin access')),
3093 3101
3094 3102 ('branch.none', _('Branch no permissions')),
3095 3103 ('branch.merge', _('Branch access by web merge')),
3096 3104 ('branch.push', _('Branch access by push')),
3097 3105 ('branch.push_force', _('Branch access by push with force')),
3098 3106
3099 3107 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
3100 3108 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
3101 3109
3102 3110 ('hg.usergroup.create.false', _('User Group creation disabled')),
3103 3111 ('hg.usergroup.create.true', _('User Group creation enabled')),
3104 3112
3105 3113 ('hg.create.none', _('Repository creation disabled')),
3106 3114 ('hg.create.repository', _('Repository creation enabled')),
3107 3115 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
3108 3116 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
3109 3117
3110 3118 ('hg.fork.none', _('Repository forking disabled')),
3111 3119 ('hg.fork.repository', _('Repository forking enabled')),
3112 3120
3113 3121 ('hg.register.none', _('Registration disabled')),
3114 3122 ('hg.register.manual_activate', _('User Registration with manual account activation')),
3115 3123 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
3116 3124
3117 3125 ('hg.password_reset.enabled', _('Password reset enabled')),
3118 3126 ('hg.password_reset.hidden', _('Password reset hidden')),
3119 3127 ('hg.password_reset.disabled', _('Password reset disabled')),
3120 3128
3121 3129 ('hg.extern_activate.manual', _('Manual activation of external account')),
3122 3130 ('hg.extern_activate.auto', _('Automatic activation of external account')),
3123 3131
3124 3132 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
3125 3133 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
3126 3134 ]
3127 3135
3128 3136 # definition of system default permissions for DEFAULT user, created on
3129 3137 # system setup
3130 3138 DEFAULT_USER_PERMISSIONS = [
3131 3139 # object perms
3132 3140 'repository.read',
3133 3141 'group.read',
3134 3142 'usergroup.read',
3135 3143 # branch, for backward compat we need same value as before so forced pushed
3136 3144 'branch.push_force',
3137 3145 # global
3138 3146 'hg.create.repository',
3139 3147 'hg.repogroup.create.false',
3140 3148 'hg.usergroup.create.false',
3141 3149 'hg.create.write_on_repogroup.true',
3142 3150 'hg.fork.repository',
3143 3151 'hg.register.manual_activate',
3144 3152 'hg.password_reset.enabled',
3145 3153 'hg.extern_activate.auto',
3146 3154 'hg.inherit_default_perms.true',
3147 3155 ]
3148 3156
3149 3157 # defines which permissions are more important higher the more important
3150 3158 # Weight defines which permissions are more important.
3151 3159 # The higher number the more important.
3152 3160 PERM_WEIGHTS = {
3153 3161 'repository.none': 0,
3154 3162 'repository.read': 1,
3155 3163 'repository.write': 3,
3156 3164 'repository.admin': 4,
3157 3165
3158 3166 'group.none': 0,
3159 3167 'group.read': 1,
3160 3168 'group.write': 3,
3161 3169 'group.admin': 4,
3162 3170
3163 3171 'usergroup.none': 0,
3164 3172 'usergroup.read': 1,
3165 3173 'usergroup.write': 3,
3166 3174 'usergroup.admin': 4,
3167 3175
3168 3176 'branch.none': 0,
3169 3177 'branch.merge': 1,
3170 3178 'branch.push': 3,
3171 3179 'branch.push_force': 4,
3172 3180
3173 3181 'hg.repogroup.create.false': 0,
3174 3182 'hg.repogroup.create.true': 1,
3175 3183
3176 3184 'hg.usergroup.create.false': 0,
3177 3185 'hg.usergroup.create.true': 1,
3178 3186
3179 3187 'hg.fork.none': 0,
3180 3188 'hg.fork.repository': 1,
3181 3189 'hg.create.none': 0,
3182 3190 'hg.create.repository': 1
3183 3191 }
3184 3192
3185 3193 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3186 3194 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
3187 3195 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
3188 3196
3189 3197 def __unicode__(self):
3190 3198 return u"<%s('%s:%s')>" % (
3191 3199 self.__class__.__name__, self.permission_id, self.permission_name
3192 3200 )
3193 3201
3194 3202 @classmethod
3195 3203 def get_by_key(cls, key):
3196 3204 return cls.query().filter(cls.permission_name == key).scalar()
3197 3205
3198 3206 @classmethod
3199 3207 def get_default_repo_perms(cls, user_id, repo_id=None):
3200 3208 q = Session().query(UserRepoToPerm, Repository, Permission)\
3201 3209 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
3202 3210 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
3203 3211 .filter(UserRepoToPerm.user_id == user_id)
3204 3212 if repo_id:
3205 3213 q = q.filter(UserRepoToPerm.repository_id == repo_id)
3206 3214 return q.all()
3207 3215
3208 3216 @classmethod
3209 3217 def get_default_repo_branch_perms(cls, user_id, repo_id=None):
3210 3218 q = Session().query(UserToRepoBranchPermission, UserRepoToPerm, Permission) \
3211 3219 .join(
3212 3220 Permission,
3213 3221 UserToRepoBranchPermission.permission_id == Permission.permission_id) \
3214 3222 .join(
3215 3223 UserRepoToPerm,
3216 3224 UserToRepoBranchPermission.rule_to_perm_id == UserRepoToPerm.repo_to_perm_id) \
3217 3225 .filter(UserRepoToPerm.user_id == user_id)
3218 3226
3219 3227 if repo_id:
3220 3228 q = q.filter(UserToRepoBranchPermission.repository_id == repo_id)
3221 3229 return q.order_by(UserToRepoBranchPermission.rule_order).all()
3222 3230
3223 3231 @classmethod
3224 3232 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
3225 3233 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
3226 3234 .join(
3227 3235 Permission,
3228 3236 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
3229 3237 .join(
3230 3238 Repository,
3231 3239 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
3232 3240 .join(
3233 3241 UserGroup,
3234 3242 UserGroupRepoToPerm.users_group_id ==
3235 3243 UserGroup.users_group_id)\
3236 3244 .join(
3237 3245 UserGroupMember,
3238 3246 UserGroupRepoToPerm.users_group_id ==
3239 3247 UserGroupMember.users_group_id)\
3240 3248 .filter(
3241 3249 UserGroupMember.user_id == user_id,
3242 3250 UserGroup.users_group_active == true())
3243 3251 if repo_id:
3244 3252 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
3245 3253 return q.all()
3246 3254
3247 3255 @classmethod
3248 3256 def get_default_repo_branch_perms_from_user_group(cls, user_id, repo_id=None):
3249 3257 q = Session().query(UserGroupToRepoBranchPermission, UserGroupRepoToPerm, Permission) \
3250 3258 .join(
3251 3259 Permission,
3252 3260 UserGroupToRepoBranchPermission.permission_id == Permission.permission_id) \
3253 3261 .join(
3254 3262 UserGroupRepoToPerm,
3255 3263 UserGroupToRepoBranchPermission.rule_to_perm_id == UserGroupRepoToPerm.users_group_to_perm_id) \
3256 3264 .join(
3257 3265 UserGroup,
3258 3266 UserGroupRepoToPerm.users_group_id == UserGroup.users_group_id) \
3259 3267 .join(
3260 3268 UserGroupMember,
3261 3269 UserGroupRepoToPerm.users_group_id == UserGroupMember.users_group_id) \
3262 3270 .filter(
3263 3271 UserGroupMember.user_id == user_id,
3264 3272 UserGroup.users_group_active == true())
3265 3273
3266 3274 if repo_id:
3267 3275 q = q.filter(UserGroupToRepoBranchPermission.repository_id == repo_id)
3268 3276 return q.order_by(UserGroupToRepoBranchPermission.rule_order).all()
3269 3277
3270 3278 @classmethod
3271 3279 def get_default_group_perms(cls, user_id, repo_group_id=None):
3272 3280 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
3273 3281 .join(
3274 3282 Permission,
3275 3283 UserRepoGroupToPerm.permission_id == Permission.permission_id)\
3276 3284 .join(
3277 3285 RepoGroup,
3278 3286 UserRepoGroupToPerm.group_id == RepoGroup.group_id)\
3279 3287 .filter(UserRepoGroupToPerm.user_id == user_id)
3280 3288 if repo_group_id:
3281 3289 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
3282 3290 return q.all()
3283 3291
3284 3292 @classmethod
3285 3293 def get_default_group_perms_from_user_group(
3286 3294 cls, user_id, repo_group_id=None):
3287 3295 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
3288 3296 .join(
3289 3297 Permission,
3290 3298 UserGroupRepoGroupToPerm.permission_id ==
3291 3299 Permission.permission_id)\
3292 3300 .join(
3293 3301 RepoGroup,
3294 3302 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
3295 3303 .join(
3296 3304 UserGroup,
3297 3305 UserGroupRepoGroupToPerm.users_group_id ==
3298 3306 UserGroup.users_group_id)\
3299 3307 .join(
3300 3308 UserGroupMember,
3301 3309 UserGroupRepoGroupToPerm.users_group_id ==
3302 3310 UserGroupMember.users_group_id)\
3303 3311 .filter(
3304 3312 UserGroupMember.user_id == user_id,
3305 3313 UserGroup.users_group_active == true())
3306 3314 if repo_group_id:
3307 3315 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
3308 3316 return q.all()
3309 3317
3310 3318 @classmethod
3311 3319 def get_default_user_group_perms(cls, user_id, user_group_id=None):
3312 3320 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
3313 3321 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
3314 3322 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
3315 3323 .filter(UserUserGroupToPerm.user_id == user_id)
3316 3324 if user_group_id:
3317 3325 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
3318 3326 return q.all()
3319 3327
3320 3328 @classmethod
3321 3329 def get_default_user_group_perms_from_user_group(
3322 3330 cls, user_id, user_group_id=None):
3323 3331 TargetUserGroup = aliased(UserGroup, name='target_user_group')
3324 3332 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
3325 3333 .join(
3326 3334 Permission,
3327 3335 UserGroupUserGroupToPerm.permission_id ==
3328 3336 Permission.permission_id)\
3329 3337 .join(
3330 3338 TargetUserGroup,
3331 3339 UserGroupUserGroupToPerm.target_user_group_id ==
3332 3340 TargetUserGroup.users_group_id)\
3333 3341 .join(
3334 3342 UserGroup,
3335 3343 UserGroupUserGroupToPerm.user_group_id ==
3336 3344 UserGroup.users_group_id)\
3337 3345 .join(
3338 3346 UserGroupMember,
3339 3347 UserGroupUserGroupToPerm.user_group_id ==
3340 3348 UserGroupMember.users_group_id)\
3341 3349 .filter(
3342 3350 UserGroupMember.user_id == user_id,
3343 3351 UserGroup.users_group_active == true())
3344 3352 if user_group_id:
3345 3353 q = q.filter(
3346 3354 UserGroupUserGroupToPerm.user_group_id == user_group_id)
3347 3355
3348 3356 return q.all()
3349 3357
3350 3358
3351 3359 class UserRepoToPerm(Base, BaseModel):
3352 3360 __tablename__ = 'repo_to_perm'
3353 3361 __table_args__ = (
3354 3362 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
3355 3363 base_table_args
3356 3364 )
3357 3365
3358 3366 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3359 3367 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3360 3368 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3361 3369 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
3362 3370
3363 3371 user = relationship('User')
3364 3372 repository = relationship('Repository')
3365 3373 permission = relationship('Permission')
3366 3374
3367 3375 branch_perm_entry = relationship('UserToRepoBranchPermission', cascade="all, delete-orphan", lazy='joined')
3368 3376
3369 3377 @classmethod
3370 3378 def create(cls, user, repository, permission):
3371 3379 n = cls()
3372 3380 n.user = user
3373 3381 n.repository = repository
3374 3382 n.permission = permission
3375 3383 Session().add(n)
3376 3384 return n
3377 3385
3378 3386 def __unicode__(self):
3379 3387 return u'<%s => %s >' % (self.user, self.repository)
3380 3388
3381 3389
3382 3390 class UserUserGroupToPerm(Base, BaseModel):
3383 3391 __tablename__ = 'user_user_group_to_perm'
3384 3392 __table_args__ = (
3385 3393 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
3386 3394 base_table_args
3387 3395 )
3388 3396
3389 3397 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3390 3398 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3391 3399 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3392 3400 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3393 3401
3394 3402 user = relationship('User')
3395 3403 user_group = relationship('UserGroup')
3396 3404 permission = relationship('Permission')
3397 3405
3398 3406 @classmethod
3399 3407 def create(cls, user, user_group, permission):
3400 3408 n = cls()
3401 3409 n.user = user
3402 3410 n.user_group = user_group
3403 3411 n.permission = permission
3404 3412 Session().add(n)
3405 3413 return n
3406 3414
3407 3415 def __unicode__(self):
3408 3416 return u'<%s => %s >' % (self.user, self.user_group)
3409 3417
3410 3418
3411 3419 class UserToPerm(Base, BaseModel):
3412 3420 __tablename__ = 'user_to_perm'
3413 3421 __table_args__ = (
3414 3422 UniqueConstraint('user_id', 'permission_id'),
3415 3423 base_table_args
3416 3424 )
3417 3425
3418 3426 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3419 3427 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3420 3428 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3421 3429
3422 3430 user = relationship('User')
3423 3431 permission = relationship('Permission', lazy='joined')
3424 3432
3425 3433 def __unicode__(self):
3426 3434 return u'<%s => %s >' % (self.user, self.permission)
3427 3435
3428 3436
3429 3437 class UserGroupRepoToPerm(Base, BaseModel):
3430 3438 __tablename__ = 'users_group_repo_to_perm'
3431 3439 __table_args__ = (
3432 3440 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
3433 3441 base_table_args
3434 3442 )
3435 3443
3436 3444 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3437 3445 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3438 3446 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3439 3447 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
3440 3448
3441 3449 users_group = relationship('UserGroup')
3442 3450 permission = relationship('Permission')
3443 3451 repository = relationship('Repository')
3444 3452 user_group_branch_perms = relationship('UserGroupToRepoBranchPermission', cascade='all')
3445 3453
3446 3454 @classmethod
3447 3455 def create(cls, users_group, repository, permission):
3448 3456 n = cls()
3449 3457 n.users_group = users_group
3450 3458 n.repository = repository
3451 3459 n.permission = permission
3452 3460 Session().add(n)
3453 3461 return n
3454 3462
3455 3463 def __unicode__(self):
3456 3464 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
3457 3465
3458 3466
3459 3467 class UserGroupUserGroupToPerm(Base, BaseModel):
3460 3468 __tablename__ = 'user_group_user_group_to_perm'
3461 3469 __table_args__ = (
3462 3470 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
3463 3471 CheckConstraint('target_user_group_id != user_group_id'),
3464 3472 base_table_args
3465 3473 )
3466 3474
3467 3475 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)
3468 3476 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3469 3477 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3470 3478 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3471 3479
3472 3480 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
3473 3481 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
3474 3482 permission = relationship('Permission')
3475 3483
3476 3484 @classmethod
3477 3485 def create(cls, target_user_group, user_group, permission):
3478 3486 n = cls()
3479 3487 n.target_user_group = target_user_group
3480 3488 n.user_group = user_group
3481 3489 n.permission = permission
3482 3490 Session().add(n)
3483 3491 return n
3484 3492
3485 3493 def __unicode__(self):
3486 3494 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
3487 3495
3488 3496
3489 3497 class UserGroupToPerm(Base, BaseModel):
3490 3498 __tablename__ = 'users_group_to_perm'
3491 3499 __table_args__ = (
3492 3500 UniqueConstraint('users_group_id', 'permission_id',),
3493 3501 base_table_args
3494 3502 )
3495 3503
3496 3504 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3497 3505 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3498 3506 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3499 3507
3500 3508 users_group = relationship('UserGroup')
3501 3509 permission = relationship('Permission')
3502 3510
3503 3511
3504 3512 class UserRepoGroupToPerm(Base, BaseModel):
3505 3513 __tablename__ = 'user_repo_group_to_perm'
3506 3514 __table_args__ = (
3507 3515 UniqueConstraint('user_id', 'group_id', 'permission_id'),
3508 3516 base_table_args
3509 3517 )
3510 3518
3511 3519 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3512 3520 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3513 3521 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
3514 3522 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3515 3523
3516 3524 user = relationship('User')
3517 3525 group = relationship('RepoGroup')
3518 3526 permission = relationship('Permission')
3519 3527
3520 3528 @classmethod
3521 3529 def create(cls, user, repository_group, permission):
3522 3530 n = cls()
3523 3531 n.user = user
3524 3532 n.group = repository_group
3525 3533 n.permission = permission
3526 3534 Session().add(n)
3527 3535 return n
3528 3536
3529 3537
3530 3538 class UserGroupRepoGroupToPerm(Base, BaseModel):
3531 3539 __tablename__ = 'users_group_repo_group_to_perm'
3532 3540 __table_args__ = (
3533 3541 UniqueConstraint('users_group_id', 'group_id'),
3534 3542 base_table_args
3535 3543 )
3536 3544
3537 3545 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)
3538 3546 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3539 3547 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
3540 3548 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3541 3549
3542 3550 users_group = relationship('UserGroup')
3543 3551 permission = relationship('Permission')
3544 3552 group = relationship('RepoGroup')
3545 3553
3546 3554 @classmethod
3547 3555 def create(cls, user_group, repository_group, permission):
3548 3556 n = cls()
3549 3557 n.users_group = user_group
3550 3558 n.group = repository_group
3551 3559 n.permission = permission
3552 3560 Session().add(n)
3553 3561 return n
3554 3562
3555 3563 def __unicode__(self):
3556 3564 return u'<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
3557 3565
3558 3566
3559 3567 class Statistics(Base, BaseModel):
3560 3568 __tablename__ = 'statistics'
3561 3569 __table_args__ = (
3562 3570 base_table_args
3563 3571 )
3564 3572
3565 3573 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3566 3574 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
3567 3575 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
3568 3576 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
3569 3577 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
3570 3578 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
3571 3579
3572 3580 repository = relationship('Repository', single_parent=True)
3573 3581
3574 3582
3575 3583 class UserFollowing(Base, BaseModel):
3576 3584 __tablename__ = 'user_followings'
3577 3585 __table_args__ = (
3578 3586 UniqueConstraint('user_id', 'follows_repository_id'),
3579 3587 UniqueConstraint('user_id', 'follows_user_id'),
3580 3588 base_table_args
3581 3589 )
3582 3590
3583 3591 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3584 3592 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3585 3593 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
3586 3594 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
3587 3595 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
3588 3596
3589 3597 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
3590 3598
3591 3599 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
3592 3600 follows_repository = relationship('Repository', order_by='Repository.repo_name')
3593 3601
3594 3602 @classmethod
3595 3603 def get_repo_followers(cls, repo_id):
3596 3604 return cls.query().filter(cls.follows_repo_id == repo_id)
3597 3605
3598 3606
3599 3607 class CacheKey(Base, BaseModel):
3600 3608 __tablename__ = 'cache_invalidation'
3601 3609 __table_args__ = (
3602 3610 UniqueConstraint('cache_key'),
3603 3611 Index('key_idx', 'cache_key'),
3604 3612 base_table_args,
3605 3613 )
3606 3614
3607 3615 CACHE_TYPE_FEED = 'FEED'
3608 3616
3609 3617 # namespaces used to register process/thread aware caches
3610 3618 REPO_INVALIDATION_NAMESPACE = 'repo_cache:{repo_id}'
3611 3619 SETTINGS_INVALIDATION_NAMESPACE = 'system_settings'
3612 3620
3613 3621 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3614 3622 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
3615 3623 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
3616 3624 cache_state_uid = Column("cache_state_uid", String(255), nullable=True, unique=None, default=None)
3617 3625 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
3618 3626
3619 3627 def __init__(self, cache_key, cache_args='', cache_state_uid=None):
3620 3628 self.cache_key = cache_key
3621 3629 self.cache_args = cache_args
3622 3630 self.cache_active = False
3623 3631 # first key should be same for all entries, since all workers should share it
3624 3632 self.cache_state_uid = cache_state_uid or self.generate_new_state_uid()
3625 3633
3626 3634 def __unicode__(self):
3627 3635 return u"<%s('%s:%s[%s]')>" % (
3628 3636 self.__class__.__name__,
3629 3637 self.cache_id, self.cache_key, self.cache_active)
3630 3638
3631 3639 def _cache_key_partition(self):
3632 3640 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
3633 3641 return prefix, repo_name, suffix
3634 3642
3635 3643 def get_prefix(self):
3636 3644 """
3637 3645 Try to extract prefix from existing cache key. The key could consist
3638 3646 of prefix, repo_name, suffix
3639 3647 """
3640 3648 # this returns prefix, repo_name, suffix
3641 3649 return self._cache_key_partition()[0]
3642 3650
3643 3651 def get_suffix(self):
3644 3652 """
3645 3653 get suffix that might have been used in _get_cache_key to
3646 3654 generate self.cache_key. Only used for informational purposes
3647 3655 in repo_edit.mako.
3648 3656 """
3649 3657 # prefix, repo_name, suffix
3650 3658 return self._cache_key_partition()[2]
3651 3659
3652 3660 @classmethod
3653 3661 def generate_new_state_uid(cls, based_on=None):
3654 3662 if based_on:
3655 3663 return str(uuid.uuid5(uuid.NAMESPACE_URL, safe_str(based_on)))
3656 3664 else:
3657 3665 return str(uuid.uuid4())
3658 3666
3659 3667 @classmethod
3660 3668 def delete_all_cache(cls):
3661 3669 """
3662 3670 Delete all cache keys from database.
3663 3671 Should only be run when all instances are down and all entries
3664 3672 thus stale.
3665 3673 """
3666 3674 cls.query().delete()
3667 3675 Session().commit()
3668 3676
3669 3677 @classmethod
3670 3678 def set_invalidate(cls, cache_uid, delete=False):
3671 3679 """
3672 3680 Mark all caches of a repo as invalid in the database.
3673 3681 """
3674 3682
3675 3683 try:
3676 3684 qry = Session().query(cls).filter(cls.cache_args == cache_uid)
3677 3685 if delete:
3678 3686 qry.delete()
3679 3687 log.debug('cache objects deleted for cache args %s',
3680 3688 safe_str(cache_uid))
3681 3689 else:
3682 3690 qry.update({"cache_active": False,
3683 3691 "cache_state_uid": cls.generate_new_state_uid()})
3684 3692 log.debug('cache objects marked as invalid for cache args %s',
3685 3693 safe_str(cache_uid))
3686 3694
3687 3695 Session().commit()
3688 3696 except Exception:
3689 3697 log.exception(
3690 3698 'Cache key invalidation failed for cache args %s',
3691 3699 safe_str(cache_uid))
3692 3700 Session().rollback()
3693 3701
3694 3702 @classmethod
3695 3703 def get_active_cache(cls, cache_key):
3696 3704 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
3697 3705 if inv_obj:
3698 3706 return inv_obj
3699 3707 return None
3700 3708
3701 3709 @classmethod
3702 3710 def get_namespace_map(cls, namespace):
3703 3711 return {
3704 3712 x.cache_key: x
3705 3713 for x in cls.query().filter(cls.cache_args == namespace)}
3706 3714
3707 3715
3708 3716 class ChangesetComment(Base, BaseModel):
3709 3717 __tablename__ = 'changeset_comments'
3710 3718 __table_args__ = (
3711 3719 Index('cc_revision_idx', 'revision'),
3712 3720 base_table_args,
3713 3721 )
3714 3722
3715 3723 COMMENT_OUTDATED = u'comment_outdated'
3716 3724 COMMENT_TYPE_NOTE = u'note'
3717 3725 COMMENT_TYPE_TODO = u'todo'
3718 3726 COMMENT_TYPES = [COMMENT_TYPE_NOTE, COMMENT_TYPE_TODO]
3719 3727
3720 3728 OP_IMMUTABLE = u'immutable'
3721 3729 OP_CHANGEABLE = u'changeable'
3722 3730
3723 3731 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
3724 3732 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3725 3733 revision = Column('revision', String(40), nullable=True)
3726 3734 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3727 3735 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
3728 3736 line_no = Column('line_no', Unicode(10), nullable=True)
3729 3737 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
3730 3738 f_path = Column('f_path', Unicode(1000), nullable=True)
3731 3739 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
3732 3740 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
3733 3741 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3734 3742 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3735 3743 renderer = Column('renderer', Unicode(64), nullable=True)
3736 3744 display_state = Column('display_state', Unicode(128), nullable=True)
3737 3745 immutable_state = Column('immutable_state', Unicode(128), nullable=True, default=OP_CHANGEABLE)
3738 3746
3739 3747 comment_type = Column('comment_type', Unicode(128), nullable=True, default=COMMENT_TYPE_NOTE)
3740 3748 resolved_comment_id = Column('resolved_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=True)
3741 3749
3742 3750 resolved_comment = relationship('ChangesetComment', remote_side=comment_id, back_populates='resolved_by')
3743 3751 resolved_by = relationship('ChangesetComment', back_populates='resolved_comment')
3744 3752
3745 3753 author = relationship('User', lazy='joined')
3746 3754 repo = relationship('Repository')
3747 3755 status_change = relationship('ChangesetStatus', cascade="all, delete-orphan", lazy='joined')
3748 3756 pull_request = relationship('PullRequest', lazy='joined')
3749 3757 pull_request_version = relationship('PullRequestVersion')
3750 3758
3751 3759 @classmethod
3752 3760 def get_users(cls, revision=None, pull_request_id=None):
3753 3761 """
3754 3762 Returns user associated with this ChangesetComment. ie those
3755 3763 who actually commented
3756 3764
3757 3765 :param cls:
3758 3766 :param revision:
3759 3767 """
3760 3768 q = Session().query(User)\
3761 3769 .join(ChangesetComment.author)
3762 3770 if revision:
3763 3771 q = q.filter(cls.revision == revision)
3764 3772 elif pull_request_id:
3765 3773 q = q.filter(cls.pull_request_id == pull_request_id)
3766 3774 return q.all()
3767 3775
3768 3776 @classmethod
3769 3777 def get_index_from_version(cls, pr_version, versions):
3770 3778 num_versions = [x.pull_request_version_id for x in versions]
3771 3779 try:
3772 3780 return num_versions.index(pr_version) +1
3773 3781 except (IndexError, ValueError):
3774 3782 return
3775 3783
3776 3784 @property
3777 3785 def outdated(self):
3778 3786 return self.display_state == self.COMMENT_OUTDATED
3779 3787
3780 3788 @property
3781 3789 def immutable(self):
3782 3790 return self.immutable_state == self.OP_IMMUTABLE
3783 3791
3784 3792 def outdated_at_version(self, version):
3785 3793 """
3786 3794 Checks if comment is outdated for given pull request version
3787 3795 """
3788 3796 return self.outdated and self.pull_request_version_id != version
3789 3797
3790 3798 def older_than_version(self, version):
3791 3799 """
3792 3800 Checks if comment is made from previous version than given
3793 3801 """
3794 3802 if version is None:
3795 3803 return self.pull_request_version_id is not None
3796 3804
3797 3805 return self.pull_request_version_id < version
3798 3806
3799 3807 @property
3800 3808 def resolved(self):
3801 3809 return self.resolved_by[0] if self.resolved_by else None
3802 3810
3803 3811 @property
3804 3812 def is_todo(self):
3805 3813 return self.comment_type == self.COMMENT_TYPE_TODO
3806 3814
3807 3815 @property
3808 3816 def is_inline(self):
3809 3817 return self.line_no and self.f_path
3810 3818
3811 3819 def get_index_version(self, versions):
3812 3820 return self.get_index_from_version(
3813 3821 self.pull_request_version_id, versions)
3814 3822
3815 3823 def __repr__(self):
3816 3824 if self.comment_id:
3817 3825 return '<DB:Comment #%s>' % self.comment_id
3818 3826 else:
3819 3827 return '<DB:Comment at %#x>' % id(self)
3820 3828
3821 3829 def get_api_data(self):
3822 3830 comment = self
3823 3831 data = {
3824 3832 'comment_id': comment.comment_id,
3825 3833 'comment_type': comment.comment_type,
3826 3834 'comment_text': comment.text,
3827 3835 'comment_status': comment.status_change,
3828 3836 'comment_f_path': comment.f_path,
3829 3837 'comment_lineno': comment.line_no,
3830 3838 'comment_author': comment.author,
3831 3839 'comment_created_on': comment.created_on,
3832 3840 'comment_resolved_by': self.resolved,
3833 3841 'comment_commit_id': comment.revision,
3834 3842 'comment_pull_request_id': comment.pull_request_id,
3835 3843 }
3836 3844 return data
3837 3845
3838 3846 def __json__(self):
3839 3847 data = dict()
3840 3848 data.update(self.get_api_data())
3841 3849 return data
3842 3850
3843 3851
3844 3852 class ChangesetStatus(Base, BaseModel):
3845 3853 __tablename__ = 'changeset_statuses'
3846 3854 __table_args__ = (
3847 3855 Index('cs_revision_idx', 'revision'),
3848 3856 Index('cs_version_idx', 'version'),
3849 3857 UniqueConstraint('repo_id', 'revision', 'version'),
3850 3858 base_table_args
3851 3859 )
3852 3860
3853 3861 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
3854 3862 STATUS_APPROVED = 'approved'
3855 3863 STATUS_REJECTED = 'rejected'
3856 3864 STATUS_UNDER_REVIEW = 'under_review'
3857 3865
3858 3866 STATUSES = [
3859 3867 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
3860 3868 (STATUS_APPROVED, _("Approved")),
3861 3869 (STATUS_REJECTED, _("Rejected")),
3862 3870 (STATUS_UNDER_REVIEW, _("Under Review")),
3863 3871 ]
3864 3872
3865 3873 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
3866 3874 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3867 3875 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
3868 3876 revision = Column('revision', String(40), nullable=False)
3869 3877 status = Column('status', String(128), nullable=False, default=DEFAULT)
3870 3878 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
3871 3879 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
3872 3880 version = Column('version', Integer(), nullable=False, default=0)
3873 3881 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3874 3882
3875 3883 author = relationship('User', lazy='joined')
3876 3884 repo = relationship('Repository')
3877 3885 comment = relationship('ChangesetComment', lazy='joined')
3878 3886 pull_request = relationship('PullRequest', lazy='joined')
3879 3887
3880 3888 def __unicode__(self):
3881 3889 return u"<%s('%s[v%s]:%s')>" % (
3882 3890 self.__class__.__name__,
3883 3891 self.status, self.version, self.author
3884 3892 )
3885 3893
3886 3894 @classmethod
3887 3895 def get_status_lbl(cls, value):
3888 3896 return dict(cls.STATUSES).get(value)
3889 3897
3890 3898 @property
3891 3899 def status_lbl(self):
3892 3900 return ChangesetStatus.get_status_lbl(self.status)
3893 3901
3894 3902 def get_api_data(self):
3895 3903 status = self
3896 3904 data = {
3897 3905 'status_id': status.changeset_status_id,
3898 3906 'status': status.status,
3899 3907 }
3900 3908 return data
3901 3909
3902 3910 def __json__(self):
3903 3911 data = dict()
3904 3912 data.update(self.get_api_data())
3905 3913 return data
3906 3914
3907 3915
3908 3916 class _SetState(object):
3909 3917 """
3910 3918 Context processor allowing changing state for sensitive operation such as
3911 3919 pull request update or merge
3912 3920 """
3913 3921
3914 3922 def __init__(self, pull_request, pr_state, back_state=None):
3915 3923 self._pr = pull_request
3916 3924 self._org_state = back_state or pull_request.pull_request_state
3917 3925 self._pr_state = pr_state
3918 3926 self._current_state = None
3919 3927
3920 3928 def __enter__(self):
3921 3929 log.debug('StateLock: entering set state context of pr %s, setting state to: `%s`',
3922 3930 self._pr, self._pr_state)
3923 3931 self.set_pr_state(self._pr_state)
3924 3932 return self
3925 3933
3926 3934 def __exit__(self, exc_type, exc_val, exc_tb):
3927 3935 if exc_val is not None:
3928 3936 log.error(traceback.format_exc(exc_tb))
3929 3937 return None
3930 3938
3931 3939 self.set_pr_state(self._org_state)
3932 3940 log.debug('StateLock: exiting set state context of pr %s, setting state to: `%s`',
3933 3941 self._pr, self._org_state)
3934 3942
3935 3943 @property
3936 3944 def state(self):
3937 3945 return self._current_state
3938 3946
3939 3947 def set_pr_state(self, pr_state):
3940 3948 try:
3941 3949 self._pr.pull_request_state = pr_state
3942 3950 Session().add(self._pr)
3943 3951 Session().commit()
3944 3952 self._current_state = pr_state
3945 3953 except Exception:
3946 3954 log.exception('Failed to set PullRequest %s state to %s', self._pr, pr_state)
3947 3955 raise
3948 3956
3949 3957
3950 3958 class _PullRequestBase(BaseModel):
3951 3959 """
3952 3960 Common attributes of pull request and version entries.
3953 3961 """
3954 3962
3955 3963 # .status values
3956 3964 STATUS_NEW = u'new'
3957 3965 STATUS_OPEN = u'open'
3958 3966 STATUS_CLOSED = u'closed'
3959 3967
3960 3968 # available states
3961 3969 STATE_CREATING = u'creating'
3962 3970 STATE_UPDATING = u'updating'
3963 3971 STATE_MERGING = u'merging'
3964 3972 STATE_CREATED = u'created'
3965 3973
3966 3974 title = Column('title', Unicode(255), nullable=True)
3967 3975 description = Column(
3968 3976 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
3969 3977 nullable=True)
3970 3978 description_renderer = Column('description_renderer', Unicode(64), nullable=True)
3971 3979
3972 3980 # new/open/closed status of pull request (not approve/reject/etc)
3973 3981 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
3974 3982 created_on = Column(
3975 3983 'created_on', DateTime(timezone=False), nullable=False,
3976 3984 default=datetime.datetime.now)
3977 3985 updated_on = Column(
3978 3986 'updated_on', DateTime(timezone=False), nullable=False,
3979 3987 default=datetime.datetime.now)
3980 3988
3981 3989 pull_request_state = Column("pull_request_state", String(255), nullable=True)
3982 3990
3983 3991 @declared_attr
3984 3992 def user_id(cls):
3985 3993 return Column(
3986 3994 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
3987 3995 unique=None)
3988 3996
3989 3997 # 500 revisions max
3990 3998 _revisions = Column(
3991 3999 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
3992 4000
3993 4001 common_ancestor_id = Column('common_ancestor_id', Unicode(255), nullable=True)
3994 4002
3995 4003 @declared_attr
3996 4004 def source_repo_id(cls):
3997 4005 # TODO: dan: rename column to source_repo_id
3998 4006 return Column(
3999 4007 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
4000 4008 nullable=False)
4001 4009
4002 4010 _source_ref = Column('org_ref', Unicode(255), nullable=False)
4003 4011
4004 4012 @hybrid_property
4005 4013 def source_ref(self):
4006 4014 return self._source_ref
4007 4015
4008 4016 @source_ref.setter
4009 4017 def source_ref(self, val):
4010 4018 parts = (val or '').split(':')
4011 4019 if len(parts) != 3:
4012 4020 raise ValueError(
4013 4021 'Invalid reference format given: {}, expected X:Y:Z'.format(val))
4014 4022 self._source_ref = safe_unicode(val)
4015 4023
4016 4024 _target_ref = Column('other_ref', Unicode(255), nullable=False)
4017 4025
4018 4026 @hybrid_property
4019 4027 def target_ref(self):
4020 4028 return self._target_ref
4021 4029
4022 4030 @target_ref.setter
4023 4031 def target_ref(self, val):
4024 4032 parts = (val or '').split(':')
4025 4033 if len(parts) != 3:
4026 4034 raise ValueError(
4027 4035 'Invalid reference format given: {}, expected X:Y:Z'.format(val))
4028 4036 self._target_ref = safe_unicode(val)
4029 4037
4030 4038 @declared_attr
4031 4039 def target_repo_id(cls):
4032 4040 # TODO: dan: rename column to target_repo_id
4033 4041 return Column(
4034 4042 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
4035 4043 nullable=False)
4036 4044
4037 4045 _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True)
4038 4046
4039 4047 # TODO: dan: rename column to last_merge_source_rev
4040 4048 _last_merge_source_rev = Column(
4041 4049 'last_merge_org_rev', String(40), nullable=True)
4042 4050 # TODO: dan: rename column to last_merge_target_rev
4043 4051 _last_merge_target_rev = Column(
4044 4052 'last_merge_other_rev', String(40), nullable=True)
4045 4053 _last_merge_status = Column('merge_status', Integer(), nullable=True)
4046 4054 last_merge_metadata = Column(
4047 4055 'last_merge_metadata', MutationObj.as_mutable(
4048 4056 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4049 4057
4050 4058 merge_rev = Column('merge_rev', String(40), nullable=True)
4051 4059
4052 4060 reviewer_data = Column(
4053 4061 'reviewer_data_json', MutationObj.as_mutable(
4054 4062 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4055 4063
4056 4064 @property
4057 4065 def reviewer_data_json(self):
4058 4066 return json.dumps(self.reviewer_data)
4059 4067
4060 4068 @property
4061 4069 def work_in_progress(self):
4062 4070 """checks if pull request is work in progress by checking the title"""
4063 4071 title = self.title.upper()
4064 4072 if re.match(r'^(\[WIP\]\s*|WIP:\s*|WIP\s+)', title):
4065 4073 return True
4066 4074 return False
4067 4075
4068 4076 @hybrid_property
4069 4077 def description_safe(self):
4070 4078 from rhodecode.lib import helpers as h
4071 4079 return h.escape(self.description)
4072 4080
4073 4081 @hybrid_property
4074 4082 def revisions(self):
4075 4083 return self._revisions.split(':') if self._revisions else []
4076 4084
4077 4085 @revisions.setter
4078 4086 def revisions(self, val):
4079 4087 self._revisions = u':'.join(val)
4080 4088
4081 4089 @hybrid_property
4082 4090 def last_merge_status(self):
4083 4091 return safe_int(self._last_merge_status)
4084 4092
4085 4093 @last_merge_status.setter
4086 4094 def last_merge_status(self, val):
4087 4095 self._last_merge_status = val
4088 4096
4089 4097 @declared_attr
4090 4098 def author(cls):
4091 4099 return relationship('User', lazy='joined')
4092 4100
4093 4101 @declared_attr
4094 4102 def source_repo(cls):
4095 4103 return relationship(
4096 4104 'Repository',
4097 4105 primaryjoin='%s.source_repo_id==Repository.repo_id' % cls.__name__)
4098 4106
4099 4107 @property
4100 4108 def source_ref_parts(self):
4101 4109 return self.unicode_to_reference(self.source_ref)
4102 4110
4103 4111 @declared_attr
4104 4112 def target_repo(cls):
4105 4113 return relationship(
4106 4114 'Repository',
4107 4115 primaryjoin='%s.target_repo_id==Repository.repo_id' % cls.__name__)
4108 4116
4109 4117 @property
4110 4118 def target_ref_parts(self):
4111 4119 return self.unicode_to_reference(self.target_ref)
4112 4120
4113 4121 @property
4114 4122 def shadow_merge_ref(self):
4115 4123 return self.unicode_to_reference(self._shadow_merge_ref)
4116 4124
4117 4125 @shadow_merge_ref.setter
4118 4126 def shadow_merge_ref(self, ref):
4119 4127 self._shadow_merge_ref = self.reference_to_unicode(ref)
4120 4128
4121 4129 @staticmethod
4122 4130 def unicode_to_reference(raw):
4123 4131 """
4124 4132 Convert a unicode (or string) to a reference object.
4125 4133 If unicode evaluates to False it returns None.
4126 4134 """
4127 4135 if raw:
4128 4136 refs = raw.split(':')
4129 4137 return Reference(*refs)
4130 4138 else:
4131 4139 return None
4132 4140
4133 4141 @staticmethod
4134 4142 def reference_to_unicode(ref):
4135 4143 """
4136 4144 Convert a reference object to unicode.
4137 4145 If reference is None it returns None.
4138 4146 """
4139 4147 if ref:
4140 4148 return u':'.join(ref)
4141 4149 else:
4142 4150 return None
4143 4151
4144 4152 def get_api_data(self, with_merge_state=True):
4145 4153 from rhodecode.model.pull_request import PullRequestModel
4146 4154
4147 4155 pull_request = self
4148 4156 if with_merge_state:
4149 4157 merge_response, merge_status, msg = \
4150 4158 PullRequestModel().merge_status(pull_request)
4151 4159 merge_state = {
4152 4160 'status': merge_status,
4153 4161 'message': safe_unicode(msg),
4154 4162 }
4155 4163 else:
4156 4164 merge_state = {'status': 'not_available',
4157 4165 'message': 'not_available'}
4158 4166
4159 4167 merge_data = {
4160 4168 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
4161 4169 'reference': (
4162 4170 pull_request.shadow_merge_ref._asdict()
4163 4171 if pull_request.shadow_merge_ref else None),
4164 4172 }
4165 4173
4166 4174 data = {
4167 4175 'pull_request_id': pull_request.pull_request_id,
4168 4176 'url': PullRequestModel().get_url(pull_request),
4169 4177 'title': pull_request.title,
4170 4178 'description': pull_request.description,
4171 4179 'status': pull_request.status,
4172 4180 'state': pull_request.pull_request_state,
4173 4181 'created_on': pull_request.created_on,
4174 4182 'updated_on': pull_request.updated_on,
4175 4183 'commit_ids': pull_request.revisions,
4176 4184 'review_status': pull_request.calculated_review_status(),
4177 4185 'mergeable': merge_state,
4178 4186 'source': {
4179 4187 'clone_url': pull_request.source_repo.clone_url(),
4180 4188 'repository': pull_request.source_repo.repo_name,
4181 4189 'reference': {
4182 4190 'name': pull_request.source_ref_parts.name,
4183 4191 'type': pull_request.source_ref_parts.type,
4184 4192 'commit_id': pull_request.source_ref_parts.commit_id,
4185 4193 },
4186 4194 },
4187 4195 'target': {
4188 4196 'clone_url': pull_request.target_repo.clone_url(),
4189 4197 'repository': pull_request.target_repo.repo_name,
4190 4198 'reference': {
4191 4199 'name': pull_request.target_ref_parts.name,
4192 4200 'type': pull_request.target_ref_parts.type,
4193 4201 'commit_id': pull_request.target_ref_parts.commit_id,
4194 4202 },
4195 4203 },
4196 4204 'merge': merge_data,
4197 4205 'author': pull_request.author.get_api_data(include_secrets=False,
4198 4206 details='basic'),
4199 4207 'reviewers': [
4200 4208 {
4201 4209 'user': reviewer.get_api_data(include_secrets=False,
4202 4210 details='basic'),
4203 4211 'reasons': reasons,
4204 4212 'review_status': st[0][1].status if st else 'not_reviewed',
4205 4213 }
4206 4214 for obj, reviewer, reasons, mandatory, st in
4207 4215 pull_request.reviewers_statuses()
4208 4216 ]
4209 4217 }
4210 4218
4211 4219 return data
4212 4220
4213 4221 def set_state(self, pull_request_state, final_state=None):
4214 4222 """
4215 4223 # goes from initial state to updating to initial state.
4216 4224 # initial state can be changed by specifying back_state=
4217 4225 with pull_request_obj.set_state(PullRequest.STATE_UPDATING):
4218 4226 pull_request.merge()
4219 4227
4220 4228 :param pull_request_state:
4221 4229 :param final_state:
4222 4230
4223 4231 """
4224 4232
4225 4233 return _SetState(self, pull_request_state, back_state=final_state)
4226 4234
4227 4235
4228 4236 class PullRequest(Base, _PullRequestBase):
4229 4237 __tablename__ = 'pull_requests'
4230 4238 __table_args__ = (
4231 4239 base_table_args,
4232 4240 )
4233 4241
4234 4242 pull_request_id = Column(
4235 4243 'pull_request_id', Integer(), nullable=False, primary_key=True)
4236 4244
4237 4245 def __repr__(self):
4238 4246 if self.pull_request_id:
4239 4247 return '<DB:PullRequest #%s>' % self.pull_request_id
4240 4248 else:
4241 4249 return '<DB:PullRequest at %#x>' % id(self)
4242 4250
4243 4251 reviewers = relationship('PullRequestReviewers', cascade="all, delete-orphan")
4244 4252 statuses = relationship('ChangesetStatus', cascade="all, delete-orphan")
4245 4253 comments = relationship('ChangesetComment', cascade="all, delete-orphan")
4246 4254 versions = relationship('PullRequestVersion', cascade="all, delete-orphan",
4247 4255 lazy='dynamic')
4248 4256
4249 4257 @classmethod
4250 4258 def get_pr_display_object(cls, pull_request_obj, org_pull_request_obj,
4251 4259 internal_methods=None):
4252 4260
4253 4261 class PullRequestDisplay(object):
4254 4262 """
4255 4263 Special object wrapper for showing PullRequest data via Versions
4256 4264 It mimics PR object as close as possible. This is read only object
4257 4265 just for display
4258 4266 """
4259 4267
4260 4268 def __init__(self, attrs, internal=None):
4261 4269 self.attrs = attrs
4262 4270 # internal have priority over the given ones via attrs
4263 4271 self.internal = internal or ['versions']
4264 4272
4265 4273 def __getattr__(self, item):
4266 4274 if item in self.internal:
4267 4275 return getattr(self, item)
4268 4276 try:
4269 4277 return self.attrs[item]
4270 4278 except KeyError:
4271 4279 raise AttributeError(
4272 4280 '%s object has no attribute %s' % (self, item))
4273 4281
4274 4282 def __repr__(self):
4275 4283 return '<DB:PullRequestDisplay #%s>' % self.attrs.get('pull_request_id')
4276 4284
4277 4285 def versions(self):
4278 4286 return pull_request_obj.versions.order_by(
4279 4287 PullRequestVersion.pull_request_version_id).all()
4280 4288
4281 4289 def is_closed(self):
4282 4290 return pull_request_obj.is_closed()
4283 4291
4284 4292 def is_state_changing(self):
4285 4293 return pull_request_obj.is_state_changing()
4286 4294
4287 4295 @property
4288 4296 def pull_request_version_id(self):
4289 4297 return getattr(pull_request_obj, 'pull_request_version_id', None)
4290 4298
4291 4299 attrs = StrictAttributeDict(pull_request_obj.get_api_data(with_merge_state=False))
4292 4300
4293 4301 attrs.author = StrictAttributeDict(
4294 4302 pull_request_obj.author.get_api_data())
4295 4303 if pull_request_obj.target_repo:
4296 4304 attrs.target_repo = StrictAttributeDict(
4297 4305 pull_request_obj.target_repo.get_api_data())
4298 4306 attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url
4299 4307
4300 4308 if pull_request_obj.source_repo:
4301 4309 attrs.source_repo = StrictAttributeDict(
4302 4310 pull_request_obj.source_repo.get_api_data())
4303 4311 attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url
4304 4312
4305 4313 attrs.source_ref_parts = pull_request_obj.source_ref_parts
4306 4314 attrs.target_ref_parts = pull_request_obj.target_ref_parts
4307 4315 attrs.revisions = pull_request_obj.revisions
4308 4316 attrs.common_ancestor_id = pull_request_obj.common_ancestor_id
4309 4317 attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref
4310 4318 attrs.reviewer_data = org_pull_request_obj.reviewer_data
4311 4319 attrs.reviewer_data_json = org_pull_request_obj.reviewer_data_json
4312 4320
4313 4321 return PullRequestDisplay(attrs, internal=internal_methods)
4314 4322
4315 4323 def is_closed(self):
4316 4324 return self.status == self.STATUS_CLOSED
4317 4325
4318 4326 def is_state_changing(self):
4319 4327 return self.pull_request_state != PullRequest.STATE_CREATED
4320 4328
4321 4329 def __json__(self):
4322 4330 return {
4323 4331 'revisions': self.revisions,
4324 4332 'versions': self.versions_count
4325 4333 }
4326 4334
4327 4335 def calculated_review_status(self):
4328 4336 from rhodecode.model.changeset_status import ChangesetStatusModel
4329 4337 return ChangesetStatusModel().calculated_review_status(self)
4330 4338
4331 4339 def reviewers_statuses(self):
4332 4340 from rhodecode.model.changeset_status import ChangesetStatusModel
4333 4341 return ChangesetStatusModel().reviewers_statuses(self)
4334 4342
4335 4343 @property
4336 4344 def workspace_id(self):
4337 4345 from rhodecode.model.pull_request import PullRequestModel
4338 4346 return PullRequestModel()._workspace_id(self)
4339 4347
4340 4348 def get_shadow_repo(self):
4341 4349 workspace_id = self.workspace_id
4342 4350 shadow_repository_path = self.target_repo.get_shadow_repository_path(workspace_id)
4343 4351 if os.path.isdir(shadow_repository_path):
4344 4352 vcs_obj = self.target_repo.scm_instance()
4345 4353 return vcs_obj.get_shadow_instance(shadow_repository_path)
4346 4354
4347 4355 @property
4348 4356 def versions_count(self):
4349 4357 """
4350 4358 return number of versions this PR have, e.g a PR that once been
4351 4359 updated will have 2 versions
4352 4360 """
4353 4361 return self.versions.count() + 1
4354 4362
4355 4363
4356 4364 class PullRequestVersion(Base, _PullRequestBase):
4357 4365 __tablename__ = 'pull_request_versions'
4358 4366 __table_args__ = (
4359 4367 base_table_args,
4360 4368 )
4361 4369
4362 4370 pull_request_version_id = Column(
4363 4371 'pull_request_version_id', Integer(), nullable=False, primary_key=True)
4364 4372 pull_request_id = Column(
4365 4373 'pull_request_id', Integer(),
4366 4374 ForeignKey('pull_requests.pull_request_id'), nullable=False)
4367 4375 pull_request = relationship('PullRequest')
4368 4376
4369 4377 def __repr__(self):
4370 4378 if self.pull_request_version_id:
4371 4379 return '<DB:PullRequestVersion #%s>' % self.pull_request_version_id
4372 4380 else:
4373 4381 return '<DB:PullRequestVersion at %#x>' % id(self)
4374 4382
4375 4383 @property
4376 4384 def reviewers(self):
4377 4385 return self.pull_request.reviewers
4378 4386
4379 4387 @property
4380 4388 def versions(self):
4381 4389 return self.pull_request.versions
4382 4390
4383 4391 def is_closed(self):
4384 4392 # calculate from original
4385 4393 return self.pull_request.status == self.STATUS_CLOSED
4386 4394
4387 4395 def is_state_changing(self):
4388 4396 return self.pull_request.pull_request_state != PullRequest.STATE_CREATED
4389 4397
4390 4398 def calculated_review_status(self):
4391 4399 return self.pull_request.calculated_review_status()
4392 4400
4393 4401 def reviewers_statuses(self):
4394 4402 return self.pull_request.reviewers_statuses()
4395 4403
4396 4404
4397 4405 class PullRequestReviewers(Base, BaseModel):
4398 4406 __tablename__ = 'pull_request_reviewers'
4399 4407 __table_args__ = (
4400 4408 base_table_args,
4401 4409 )
4402 4410
4403 4411 @hybrid_property
4404 4412 def reasons(self):
4405 4413 if not self._reasons:
4406 4414 return []
4407 4415 return self._reasons
4408 4416
4409 4417 @reasons.setter
4410 4418 def reasons(self, val):
4411 4419 val = val or []
4412 4420 if any(not isinstance(x, compat.string_types) for x in val):
4413 4421 raise Exception('invalid reasons type, must be list of strings')
4414 4422 self._reasons = val
4415 4423
4416 4424 pull_requests_reviewers_id = Column(
4417 4425 'pull_requests_reviewers_id', Integer(), nullable=False,
4418 4426 primary_key=True)
4419 4427 pull_request_id = Column(
4420 4428 "pull_request_id", Integer(),
4421 4429 ForeignKey('pull_requests.pull_request_id'), nullable=False)
4422 4430 user_id = Column(
4423 4431 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
4424 4432 _reasons = Column(
4425 4433 'reason', MutationList.as_mutable(
4426 4434 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
4427 4435
4428 4436 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4429 4437 user = relationship('User')
4430 4438 pull_request = relationship('PullRequest')
4431 4439
4432 4440 rule_data = Column(
4433 4441 'rule_data_json',
4434 4442 JsonType(dialect_map=dict(mysql=UnicodeText(16384))))
4435 4443
4436 4444 def rule_user_group_data(self):
4437 4445 """
4438 4446 Returns the voting user group rule data for this reviewer
4439 4447 """
4440 4448
4441 4449 if self.rule_data and 'vote_rule' in self.rule_data:
4442 4450 user_group_data = {}
4443 4451 if 'rule_user_group_entry_id' in self.rule_data:
4444 4452 # means a group with voting rules !
4445 4453 user_group_data['id'] = self.rule_data['rule_user_group_entry_id']
4446 4454 user_group_data['name'] = self.rule_data['rule_name']
4447 4455 user_group_data['vote_rule'] = self.rule_data['vote_rule']
4448 4456
4449 4457 return user_group_data
4450 4458
4451 4459 def __unicode__(self):
4452 4460 return u"<%s('id:%s')>" % (self.__class__.__name__,
4453 4461 self.pull_requests_reviewers_id)
4454 4462
4455 4463
4456 4464 class Notification(Base, BaseModel):
4457 4465 __tablename__ = 'notifications'
4458 4466 __table_args__ = (
4459 4467 Index('notification_type_idx', 'type'),
4460 4468 base_table_args,
4461 4469 )
4462 4470
4463 4471 TYPE_CHANGESET_COMMENT = u'cs_comment'
4464 4472 TYPE_MESSAGE = u'message'
4465 4473 TYPE_MENTION = u'mention'
4466 4474 TYPE_REGISTRATION = u'registration'
4467 4475 TYPE_PULL_REQUEST = u'pull_request'
4468 4476 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
4469 4477 TYPE_PULL_REQUEST_UPDATE = u'pull_request_update'
4470 4478
4471 4479 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
4472 4480 subject = Column('subject', Unicode(512), nullable=True)
4473 4481 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
4474 4482 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
4475 4483 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4476 4484 type_ = Column('type', Unicode(255))
4477 4485
4478 4486 created_by_user = relationship('User')
4479 4487 notifications_to_users = relationship('UserNotification', lazy='joined',
4480 4488 cascade="all, delete-orphan")
4481 4489
4482 4490 @property
4483 4491 def recipients(self):
4484 4492 return [x.user for x in UserNotification.query()\
4485 4493 .filter(UserNotification.notification == self)\
4486 4494 .order_by(UserNotification.user_id.asc()).all()]
4487 4495
4488 4496 @classmethod
4489 4497 def create(cls, created_by, subject, body, recipients, type_=None):
4490 4498 if type_ is None:
4491 4499 type_ = Notification.TYPE_MESSAGE
4492 4500
4493 4501 notification = cls()
4494 4502 notification.created_by_user = created_by
4495 4503 notification.subject = subject
4496 4504 notification.body = body
4497 4505 notification.type_ = type_
4498 4506 notification.created_on = datetime.datetime.now()
4499 4507
4500 4508 # For each recipient link the created notification to his account
4501 4509 for u in recipients:
4502 4510 assoc = UserNotification()
4503 4511 assoc.user_id = u.user_id
4504 4512 assoc.notification = notification
4505 4513
4506 4514 # if created_by is inside recipients mark his notification
4507 4515 # as read
4508 4516 if u.user_id == created_by.user_id:
4509 4517 assoc.read = True
4510 4518 Session().add(assoc)
4511 4519
4512 4520 Session().add(notification)
4513 4521
4514 4522 return notification
4515 4523
4516 4524
4517 4525 class UserNotification(Base, BaseModel):
4518 4526 __tablename__ = 'user_to_notification'
4519 4527 __table_args__ = (
4520 4528 UniqueConstraint('user_id', 'notification_id'),
4521 4529 base_table_args
4522 4530 )
4523 4531
4524 4532 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
4525 4533 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
4526 4534 read = Column('read', Boolean, default=False)
4527 4535 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
4528 4536
4529 4537 user = relationship('User', lazy="joined")
4530 4538 notification = relationship('Notification', lazy="joined",
4531 4539 order_by=lambda: Notification.created_on.desc(),)
4532 4540
4533 4541 def mark_as_read(self):
4534 4542 self.read = True
4535 4543 Session().add(self)
4536 4544
4537 4545
4538 4546 class UserNotice(Base, BaseModel):
4539 4547 __tablename__ = 'user_notices'
4540 4548 __table_args__ = (
4541 4549 base_table_args
4542 4550 )
4543 4551
4544 4552 NOTIFICATION_TYPE_MESSAGE = 'message'
4545 4553 NOTIFICATION_TYPE_NOTICE = 'notice'
4546 4554
4547 4555 NOTIFICATION_LEVEL_INFO = 'info'
4548 4556 NOTIFICATION_LEVEL_WARNING = 'warning'
4549 4557 NOTIFICATION_LEVEL_ERROR = 'error'
4550 4558
4551 4559 user_notice_id = Column('gist_id', Integer(), primary_key=True)
4552 4560
4553 4561 notice_subject = Column('notice_subject', Unicode(512), nullable=True)
4554 4562 notice_body = Column('notice_body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
4555 4563
4556 4564 notice_read = Column('notice_read', Boolean, default=False)
4557 4565
4558 4566 notification_level = Column('notification_level', String(1024), default=NOTIFICATION_LEVEL_INFO)
4559 4567 notification_type = Column('notification_type', String(1024), default=NOTIFICATION_TYPE_NOTICE)
4560 4568
4561 4569 notice_created_by = Column('notice_created_by', Integer(), ForeignKey('users.user_id'), nullable=True)
4562 4570 notice_created_on = Column('notice_created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4563 4571
4564 4572 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'))
4565 4573 user = relationship('User', lazy="joined", primaryjoin='User.user_id==UserNotice.user_id')
4566 4574
4567 4575 @classmethod
4568 4576 def create_for_user(cls, user, subject, body, notice_level=NOTIFICATION_LEVEL_INFO, allow_duplicate=False):
4569 4577
4570 4578 if notice_level not in [cls.NOTIFICATION_LEVEL_ERROR,
4571 4579 cls.NOTIFICATION_LEVEL_WARNING,
4572 4580 cls.NOTIFICATION_LEVEL_INFO]:
4573 4581 return
4574 4582
4575 4583 from rhodecode.model.user import UserModel
4576 4584 user = UserModel().get_user(user)
4577 4585
4578 4586 new_notice = UserNotice()
4579 4587 if not allow_duplicate:
4580 4588 existing_msg = UserNotice().query() \
4581 4589 .filter(UserNotice.user == user) \
4582 4590 .filter(UserNotice.notice_body == body) \
4583 4591 .filter(UserNotice.notice_read == false()) \
4584 4592 .scalar()
4585 4593 if existing_msg:
4586 4594 log.warning('Ignoring duplicate notice for user %s', user)
4587 4595 return
4588 4596
4589 4597 new_notice.user = user
4590 4598 new_notice.notice_subject = subject
4591 4599 new_notice.notice_body = body
4592 4600 new_notice.notification_level = notice_level
4593 4601 Session().add(new_notice)
4594 4602 Session().commit()
4595 4603
4596 4604
4597 4605 class Gist(Base, BaseModel):
4598 4606 __tablename__ = 'gists'
4599 4607 __table_args__ = (
4600 4608 Index('g_gist_access_id_idx', 'gist_access_id'),
4601 4609 Index('g_created_on_idx', 'created_on'),
4602 4610 base_table_args
4603 4611 )
4604 4612
4605 4613 GIST_PUBLIC = u'public'
4606 4614 GIST_PRIVATE = u'private'
4607 4615 DEFAULT_FILENAME = u'gistfile1.txt'
4608 4616
4609 4617 ACL_LEVEL_PUBLIC = u'acl_public'
4610 4618 ACL_LEVEL_PRIVATE = u'acl_private'
4611 4619
4612 4620 gist_id = Column('gist_id', Integer(), primary_key=True)
4613 4621 gist_access_id = Column('gist_access_id', Unicode(250))
4614 4622 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
4615 4623 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
4616 4624 gist_expires = Column('gist_expires', Float(53), nullable=False)
4617 4625 gist_type = Column('gist_type', Unicode(128), nullable=False)
4618 4626 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4619 4627 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4620 4628 acl_level = Column('acl_level', Unicode(128), nullable=True)
4621 4629
4622 4630 owner = relationship('User')
4623 4631
4624 4632 def __repr__(self):
4625 4633 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
4626 4634
4627 4635 @hybrid_property
4628 4636 def description_safe(self):
4629 4637 from rhodecode.lib import helpers as h
4630 4638 return h.escape(self.gist_description)
4631 4639
4632 4640 @classmethod
4633 4641 def get_or_404(cls, id_):
4634 4642 from pyramid.httpexceptions import HTTPNotFound
4635 4643
4636 4644 res = cls.query().filter(cls.gist_access_id == id_).scalar()
4637 4645 if not res:
4638 4646 raise HTTPNotFound()
4639 4647 return res
4640 4648
4641 4649 @classmethod
4642 4650 def get_by_access_id(cls, gist_access_id):
4643 4651 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
4644 4652
4645 4653 def gist_url(self):
4646 4654 from rhodecode.model.gist import GistModel
4647 4655 return GistModel().get_url(self)
4648 4656
4649 4657 @classmethod
4650 4658 def base_path(cls):
4651 4659 """
4652 4660 Returns base path when all gists are stored
4653 4661
4654 4662 :param cls:
4655 4663 """
4656 4664 from rhodecode.model.gist import GIST_STORE_LOC
4657 4665 q = Session().query(RhodeCodeUi)\
4658 4666 .filter(RhodeCodeUi.ui_key == URL_SEP)
4659 4667 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
4660 4668 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
4661 4669
4662 4670 def get_api_data(self):
4663 4671 """
4664 4672 Common function for generating gist related data for API
4665 4673 """
4666 4674 gist = self
4667 4675 data = {
4668 4676 'gist_id': gist.gist_id,
4669 4677 'type': gist.gist_type,
4670 4678 'access_id': gist.gist_access_id,
4671 4679 'description': gist.gist_description,
4672 4680 'url': gist.gist_url(),
4673 4681 'expires': gist.gist_expires,
4674 4682 'created_on': gist.created_on,
4675 4683 'modified_at': gist.modified_at,
4676 4684 'content': None,
4677 4685 'acl_level': gist.acl_level,
4678 4686 }
4679 4687 return data
4680 4688
4681 4689 def __json__(self):
4682 4690 data = dict(
4683 4691 )
4684 4692 data.update(self.get_api_data())
4685 4693 return data
4686 4694 # SCM functions
4687 4695
4688 4696 def scm_instance(self, **kwargs):
4689 4697 """
4690 4698 Get an instance of VCS Repository
4691 4699
4692 4700 :param kwargs:
4693 4701 """
4694 4702 from rhodecode.model.gist import GistModel
4695 4703 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
4696 4704 return get_vcs_instance(
4697 4705 repo_path=safe_str(full_repo_path), create=False,
4698 4706 _vcs_alias=GistModel.vcs_backend)
4699 4707
4700 4708
4701 4709 class ExternalIdentity(Base, BaseModel):
4702 4710 __tablename__ = 'external_identities'
4703 4711 __table_args__ = (
4704 4712 Index('local_user_id_idx', 'local_user_id'),
4705 4713 Index('external_id_idx', 'external_id'),
4706 4714 base_table_args
4707 4715 )
4708 4716
4709 4717 external_id = Column('external_id', Unicode(255), default=u'', primary_key=True)
4710 4718 external_username = Column('external_username', Unicode(1024), default=u'')
4711 4719 local_user_id = Column('local_user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
4712 4720 provider_name = Column('provider_name', Unicode(255), default=u'', primary_key=True)
4713 4721 access_token = Column('access_token', String(1024), default=u'')
4714 4722 alt_token = Column('alt_token', String(1024), default=u'')
4715 4723 token_secret = Column('token_secret', String(1024), default=u'')
4716 4724
4717 4725 @classmethod
4718 4726 def by_external_id_and_provider(cls, external_id, provider_name, local_user_id=None):
4719 4727 """
4720 4728 Returns ExternalIdentity instance based on search params
4721 4729
4722 4730 :param external_id:
4723 4731 :param provider_name:
4724 4732 :return: ExternalIdentity
4725 4733 """
4726 4734 query = cls.query()
4727 4735 query = query.filter(cls.external_id == external_id)
4728 4736 query = query.filter(cls.provider_name == provider_name)
4729 4737 if local_user_id:
4730 4738 query = query.filter(cls.local_user_id == local_user_id)
4731 4739 return query.first()
4732 4740
4733 4741 @classmethod
4734 4742 def user_by_external_id_and_provider(cls, external_id, provider_name):
4735 4743 """
4736 4744 Returns User instance based on search params
4737 4745
4738 4746 :param external_id:
4739 4747 :param provider_name:
4740 4748 :return: User
4741 4749 """
4742 4750 query = User.query()
4743 4751 query = query.filter(cls.external_id == external_id)
4744 4752 query = query.filter(cls.provider_name == provider_name)
4745 4753 query = query.filter(User.user_id == cls.local_user_id)
4746 4754 return query.first()
4747 4755
4748 4756 @classmethod
4749 4757 def by_local_user_id(cls, local_user_id):
4750 4758 """
4751 4759 Returns all tokens for user
4752 4760
4753 4761 :param local_user_id:
4754 4762 :return: ExternalIdentity
4755 4763 """
4756 4764 query = cls.query()
4757 4765 query = query.filter(cls.local_user_id == local_user_id)
4758 4766 return query
4759 4767
4760 4768 @classmethod
4761 4769 def load_provider_plugin(cls, plugin_id):
4762 4770 from rhodecode.authentication.base import loadplugin
4763 4771 _plugin_id = 'egg:rhodecode-enterprise-ee#{}'.format(plugin_id)
4764 4772 auth_plugin = loadplugin(_plugin_id)
4765 4773 return auth_plugin
4766 4774
4767 4775
4768 4776 class Integration(Base, BaseModel):
4769 4777 __tablename__ = 'integrations'
4770 4778 __table_args__ = (
4771 4779 base_table_args
4772 4780 )
4773 4781
4774 4782 integration_id = Column('integration_id', Integer(), primary_key=True)
4775 4783 integration_type = Column('integration_type', String(255))
4776 4784 enabled = Column('enabled', Boolean(), nullable=False)
4777 4785 name = Column('name', String(255), nullable=False)
4778 4786 child_repos_only = Column('child_repos_only', Boolean(), nullable=False,
4779 4787 default=False)
4780 4788
4781 4789 settings = Column(
4782 4790 'settings_json', MutationObj.as_mutable(
4783 4791 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4784 4792 repo_id = Column(
4785 4793 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
4786 4794 nullable=True, unique=None, default=None)
4787 4795 repo = relationship('Repository', lazy='joined')
4788 4796
4789 4797 repo_group_id = Column(
4790 4798 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
4791 4799 nullable=True, unique=None, default=None)
4792 4800 repo_group = relationship('RepoGroup', lazy='joined')
4793 4801
4794 4802 @property
4795 4803 def scope(self):
4796 4804 if self.repo:
4797 4805 return repr(self.repo)
4798 4806 if self.repo_group:
4799 4807 if self.child_repos_only:
4800 4808 return repr(self.repo_group) + ' (child repos only)'
4801 4809 else:
4802 4810 return repr(self.repo_group) + ' (recursive)'
4803 4811 if self.child_repos_only:
4804 4812 return 'root_repos'
4805 4813 return 'global'
4806 4814
4807 4815 def __repr__(self):
4808 4816 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
4809 4817
4810 4818
4811 4819 class RepoReviewRuleUser(Base, BaseModel):
4812 4820 __tablename__ = 'repo_review_rules_users'
4813 4821 __table_args__ = (
4814 4822 base_table_args
4815 4823 )
4816 4824
4817 4825 repo_review_rule_user_id = Column('repo_review_rule_user_id', Integer(), primary_key=True)
4818 4826 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
4819 4827 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False)
4820 4828 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4821 4829 user = relationship('User')
4822 4830
4823 4831 def rule_data(self):
4824 4832 return {
4825 4833 'mandatory': self.mandatory
4826 4834 }
4827 4835
4828 4836
4829 4837 class RepoReviewRuleUserGroup(Base, BaseModel):
4830 4838 __tablename__ = 'repo_review_rules_users_groups'
4831 4839 __table_args__ = (
4832 4840 base_table_args
4833 4841 )
4834 4842
4835 4843 VOTE_RULE_ALL = -1
4836 4844
4837 4845 repo_review_rule_users_group_id = Column('repo_review_rule_users_group_id', Integer(), primary_key=True)
4838 4846 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
4839 4847 users_group_id = Column("users_group_id", Integer(),ForeignKey('users_groups.users_group_id'), nullable=False)
4840 4848 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4841 4849 vote_rule = Column("vote_rule", Integer(), nullable=True, default=VOTE_RULE_ALL)
4842 4850 users_group = relationship('UserGroup')
4843 4851
4844 4852 def rule_data(self):
4845 4853 return {
4846 4854 'mandatory': self.mandatory,
4847 4855 'vote_rule': self.vote_rule
4848 4856 }
4849 4857
4850 4858 @property
4851 4859 def vote_rule_label(self):
4852 4860 if not self.vote_rule or self.vote_rule == self.VOTE_RULE_ALL:
4853 4861 return 'all must vote'
4854 4862 else:
4855 4863 return 'min. vote {}'.format(self.vote_rule)
4856 4864
4857 4865
4858 4866 class RepoReviewRule(Base, BaseModel):
4859 4867 __tablename__ = 'repo_review_rules'
4860 4868 __table_args__ = (
4861 4869 base_table_args
4862 4870 )
4863 4871
4864 4872 repo_review_rule_id = Column(
4865 4873 'repo_review_rule_id', Integer(), primary_key=True)
4866 4874 repo_id = Column(
4867 4875 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
4868 4876 repo = relationship('Repository', backref='review_rules')
4869 4877
4870 4878 review_rule_name = Column('review_rule_name', String(255))
4871 4879 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
4872 4880 _target_branch_pattern = Column("target_branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
4873 4881 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
4874 4882
4875 4883 use_authors_for_review = Column("use_authors_for_review", Boolean(), nullable=False, default=False)
4876 4884 forbid_author_to_review = Column("forbid_author_to_review", Boolean(), nullable=False, default=False)
4877 4885 forbid_commit_author_to_review = Column("forbid_commit_author_to_review", Boolean(), nullable=False, default=False)
4878 4886 forbid_adding_reviewers = Column("forbid_adding_reviewers", Boolean(), nullable=False, default=False)
4879 4887
4880 4888 rule_users = relationship('RepoReviewRuleUser')
4881 4889 rule_user_groups = relationship('RepoReviewRuleUserGroup')
4882 4890
4883 4891 def _validate_pattern(self, value):
4884 4892 re.compile('^' + glob2re(value) + '$')
4885 4893
4886 4894 @hybrid_property
4887 4895 def source_branch_pattern(self):
4888 4896 return self._branch_pattern or '*'
4889 4897
4890 4898 @source_branch_pattern.setter
4891 4899 def source_branch_pattern(self, value):
4892 4900 self._validate_pattern(value)
4893 4901 self._branch_pattern = value or '*'
4894 4902
4895 4903 @hybrid_property
4896 4904 def target_branch_pattern(self):
4897 4905 return self._target_branch_pattern or '*'
4898 4906
4899 4907 @target_branch_pattern.setter
4900 4908 def target_branch_pattern(self, value):
4901 4909 self._validate_pattern(value)
4902 4910 self._target_branch_pattern = value or '*'
4903 4911
4904 4912 @hybrid_property
4905 4913 def file_pattern(self):
4906 4914 return self._file_pattern or '*'
4907 4915
4908 4916 @file_pattern.setter
4909 4917 def file_pattern(self, value):
4910 4918 self._validate_pattern(value)
4911 4919 self._file_pattern = value or '*'
4912 4920
4913 4921 def matches(self, source_branch, target_branch, files_changed):
4914 4922 """
4915 4923 Check if this review rule matches a branch/files in a pull request
4916 4924
4917 4925 :param source_branch: source branch name for the commit
4918 4926 :param target_branch: target branch name for the commit
4919 4927 :param files_changed: list of file paths changed in the pull request
4920 4928 """
4921 4929
4922 4930 source_branch = source_branch or ''
4923 4931 target_branch = target_branch or ''
4924 4932 files_changed = files_changed or []
4925 4933
4926 4934 branch_matches = True
4927 4935 if source_branch or target_branch:
4928 4936 if self.source_branch_pattern == '*':
4929 4937 source_branch_match = True
4930 4938 else:
4931 4939 if self.source_branch_pattern.startswith('re:'):
4932 4940 source_pattern = self.source_branch_pattern[3:]
4933 4941 else:
4934 4942 source_pattern = '^' + glob2re(self.source_branch_pattern) + '$'
4935 4943 source_branch_regex = re.compile(source_pattern)
4936 4944 source_branch_match = bool(source_branch_regex.search(source_branch))
4937 4945 if self.target_branch_pattern == '*':
4938 4946 target_branch_match = True
4939 4947 else:
4940 4948 if self.target_branch_pattern.startswith('re:'):
4941 4949 target_pattern = self.target_branch_pattern[3:]
4942 4950 else:
4943 4951 target_pattern = '^' + glob2re(self.target_branch_pattern) + '$'
4944 4952 target_branch_regex = re.compile(target_pattern)
4945 4953 target_branch_match = bool(target_branch_regex.search(target_branch))
4946 4954
4947 4955 branch_matches = source_branch_match and target_branch_match
4948 4956
4949 4957 files_matches = True
4950 4958 if self.file_pattern != '*':
4951 4959 files_matches = False
4952 4960 if self.file_pattern.startswith('re:'):
4953 4961 file_pattern = self.file_pattern[3:]
4954 4962 else:
4955 4963 file_pattern = glob2re(self.file_pattern)
4956 4964 file_regex = re.compile(file_pattern)
4957 4965 for filename in files_changed:
4958 4966 if file_regex.search(filename):
4959 4967 files_matches = True
4960 4968 break
4961 4969
4962 4970 return branch_matches and files_matches
4963 4971
4964 4972 @property
4965 4973 def review_users(self):
4966 4974 """ Returns the users which this rule applies to """
4967 4975
4968 4976 users = collections.OrderedDict()
4969 4977
4970 4978 for rule_user in self.rule_users:
4971 4979 if rule_user.user.active:
4972 4980 if rule_user.user not in users:
4973 4981 users[rule_user.user.username] = {
4974 4982 'user': rule_user.user,
4975 4983 'source': 'user',
4976 4984 'source_data': {},
4977 4985 'data': rule_user.rule_data()
4978 4986 }
4979 4987
4980 4988 for rule_user_group in self.rule_user_groups:
4981 4989 source_data = {
4982 4990 'user_group_id': rule_user_group.users_group.users_group_id,
4983 4991 'name': rule_user_group.users_group.users_group_name,
4984 4992 'members': len(rule_user_group.users_group.members)
4985 4993 }
4986 4994 for member in rule_user_group.users_group.members:
4987 4995 if member.user.active:
4988 4996 key = member.user.username
4989 4997 if key in users:
4990 4998 # skip this member as we have him already
4991 4999 # this prevents from override the "first" matched
4992 5000 # users with duplicates in multiple groups
4993 5001 continue
4994 5002
4995 5003 users[key] = {
4996 5004 'user': member.user,
4997 5005 'source': 'user_group',
4998 5006 'source_data': source_data,
4999 5007 'data': rule_user_group.rule_data()
5000 5008 }
5001 5009
5002 5010 return users
5003 5011
5004 5012 def user_group_vote_rule(self, user_id):
5005 5013
5006 5014 rules = []
5007 5015 if not self.rule_user_groups:
5008 5016 return rules
5009 5017
5010 5018 for user_group in self.rule_user_groups:
5011 5019 user_group_members = [x.user_id for x in user_group.users_group.members]
5012 5020 if user_id in user_group_members:
5013 5021 rules.append(user_group)
5014 5022 return rules
5015 5023
5016 5024 def __repr__(self):
5017 5025 return '<RepoReviewerRule(id=%r, repo=%r)>' % (
5018 5026 self.repo_review_rule_id, self.repo)
5019 5027
5020 5028
5021 5029 class ScheduleEntry(Base, BaseModel):
5022 5030 __tablename__ = 'schedule_entries'
5023 5031 __table_args__ = (
5024 5032 UniqueConstraint('schedule_name', name='s_schedule_name_idx'),
5025 5033 UniqueConstraint('task_uid', name='s_task_uid_idx'),
5026 5034 base_table_args,
5027 5035 )
5028 5036
5029 5037 schedule_types = ['crontab', 'timedelta', 'integer']
5030 5038 schedule_entry_id = Column('schedule_entry_id', Integer(), primary_key=True)
5031 5039
5032 5040 schedule_name = Column("schedule_name", String(255), nullable=False, unique=None, default=None)
5033 5041 schedule_description = Column("schedule_description", String(10000), nullable=True, unique=None, default=None)
5034 5042 schedule_enabled = Column("schedule_enabled", Boolean(), nullable=False, unique=None, default=True)
5035 5043
5036 5044 _schedule_type = Column("schedule_type", String(255), nullable=False, unique=None, default=None)
5037 5045 schedule_definition = Column('schedule_definition_json', MutationObj.as_mutable(JsonType(default=lambda: "", dialect_map=dict(mysql=LONGTEXT()))))
5038 5046
5039 5047 schedule_last_run = Column('schedule_last_run', DateTime(timezone=False), nullable=True, unique=None, default=None)
5040 5048 schedule_total_run_count = Column('schedule_total_run_count', Integer(), nullable=True, unique=None, default=0)
5041 5049
5042 5050 # task
5043 5051 task_uid = Column("task_uid", String(255), nullable=False, unique=None, default=None)
5044 5052 task_dot_notation = Column("task_dot_notation", String(4096), nullable=False, unique=None, default=None)
5045 5053 task_args = Column('task_args_json', MutationObj.as_mutable(JsonType(default=list, dialect_map=dict(mysql=LONGTEXT()))))
5046 5054 task_kwargs = Column('task_kwargs_json', MutationObj.as_mutable(JsonType(default=dict, dialect_map=dict(mysql=LONGTEXT()))))
5047 5055
5048 5056 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5049 5057 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=None)
5050 5058
5051 5059 @hybrid_property
5052 5060 def schedule_type(self):
5053 5061 return self._schedule_type
5054 5062
5055 5063 @schedule_type.setter
5056 5064 def schedule_type(self, val):
5057 5065 if val not in self.schedule_types:
5058 5066 raise ValueError('Value must be on of `{}` and got `{}`'.format(
5059 5067 val, self.schedule_type))
5060 5068
5061 5069 self._schedule_type = val
5062 5070
5063 5071 @classmethod
5064 5072 def get_uid(cls, obj):
5065 5073 args = obj.task_args
5066 5074 kwargs = obj.task_kwargs
5067 5075 if isinstance(args, JsonRaw):
5068 5076 try:
5069 5077 args = json.loads(args)
5070 5078 except ValueError:
5071 5079 args = tuple()
5072 5080
5073 5081 if isinstance(kwargs, JsonRaw):
5074 5082 try:
5075 5083 kwargs = json.loads(kwargs)
5076 5084 except ValueError:
5077 5085 kwargs = dict()
5078 5086
5079 5087 dot_notation = obj.task_dot_notation
5080 5088 val = '.'.join(map(safe_str, [
5081 5089 sorted(dot_notation), args, sorted(kwargs.items())]))
5082 5090 return hashlib.sha1(val).hexdigest()
5083 5091
5084 5092 @classmethod
5085 5093 def get_by_schedule_name(cls, schedule_name):
5086 5094 return cls.query().filter(cls.schedule_name == schedule_name).scalar()
5087 5095
5088 5096 @classmethod
5089 5097 def get_by_schedule_id(cls, schedule_id):
5090 5098 return cls.query().filter(cls.schedule_entry_id == schedule_id).scalar()
5091 5099
5092 5100 @property
5093 5101 def task(self):
5094 5102 return self.task_dot_notation
5095 5103
5096 5104 @property
5097 5105 def schedule(self):
5098 5106 from rhodecode.lib.celerylib.utils import raw_2_schedule
5099 5107 schedule = raw_2_schedule(self.schedule_definition, self.schedule_type)
5100 5108 return schedule
5101 5109
5102 5110 @property
5103 5111 def args(self):
5104 5112 try:
5105 5113 return list(self.task_args or [])
5106 5114 except ValueError:
5107 5115 return list()
5108 5116
5109 5117 @property
5110 5118 def kwargs(self):
5111 5119 try:
5112 5120 return dict(self.task_kwargs or {})
5113 5121 except ValueError:
5114 5122 return dict()
5115 5123
5116 5124 def _as_raw(self, val):
5117 5125 if hasattr(val, 'de_coerce'):
5118 5126 val = val.de_coerce()
5119 5127 if val:
5120 5128 val = json.dumps(val)
5121 5129
5122 5130 return val
5123 5131
5124 5132 @property
5125 5133 def schedule_definition_raw(self):
5126 5134 return self._as_raw(self.schedule_definition)
5127 5135
5128 5136 @property
5129 5137 def args_raw(self):
5130 5138 return self._as_raw(self.task_args)
5131 5139
5132 5140 @property
5133 5141 def kwargs_raw(self):
5134 5142 return self._as_raw(self.task_kwargs)
5135 5143
5136 5144 def __repr__(self):
5137 5145 return '<DB:ScheduleEntry({}:{})>'.format(
5138 5146 self.schedule_entry_id, self.schedule_name)
5139 5147
5140 5148
5141 5149 @event.listens_for(ScheduleEntry, 'before_update')
5142 5150 def update_task_uid(mapper, connection, target):
5143 5151 target.task_uid = ScheduleEntry.get_uid(target)
5144 5152
5145 5153
5146 5154 @event.listens_for(ScheduleEntry, 'before_insert')
5147 5155 def set_task_uid(mapper, connection, target):
5148 5156 target.task_uid = ScheduleEntry.get_uid(target)
5149 5157
5150 5158
5151 5159 class _BaseBranchPerms(BaseModel):
5152 5160 @classmethod
5153 5161 def compute_hash(cls, value):
5154 5162 return sha1_safe(value)
5155 5163
5156 5164 @hybrid_property
5157 5165 def branch_pattern(self):
5158 5166 return self._branch_pattern or '*'
5159 5167
5160 5168 @hybrid_property
5161 5169 def branch_hash(self):
5162 5170 return self._branch_hash
5163 5171
5164 5172 def _validate_glob(self, value):
5165 5173 re.compile('^' + glob2re(value) + '$')
5166 5174
5167 5175 @branch_pattern.setter
5168 5176 def branch_pattern(self, value):
5169 5177 self._validate_glob(value)
5170 5178 self._branch_pattern = value or '*'
5171 5179 # set the Hash when setting the branch pattern
5172 5180 self._branch_hash = self.compute_hash(self._branch_pattern)
5173 5181
5174 5182 def matches(self, branch):
5175 5183 """
5176 5184 Check if this the branch matches entry
5177 5185
5178 5186 :param branch: branch name for the commit
5179 5187 """
5180 5188
5181 5189 branch = branch or ''
5182 5190
5183 5191 branch_matches = True
5184 5192 if branch:
5185 5193 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
5186 5194 branch_matches = bool(branch_regex.search(branch))
5187 5195
5188 5196 return branch_matches
5189 5197
5190 5198
5191 5199 class UserToRepoBranchPermission(Base, _BaseBranchPerms):
5192 5200 __tablename__ = 'user_to_repo_branch_permissions'
5193 5201 __table_args__ = (
5194 5202 base_table_args
5195 5203 )
5196 5204
5197 5205 branch_rule_id = Column('branch_rule_id', Integer(), primary_key=True)
5198 5206
5199 5207 repository_id = Column('repository_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
5200 5208 repo = relationship('Repository', backref='user_branch_perms')
5201 5209
5202 5210 permission_id = Column('permission_id', Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
5203 5211 permission = relationship('Permission')
5204 5212
5205 5213 rule_to_perm_id = Column('rule_to_perm_id', Integer(), ForeignKey('repo_to_perm.repo_to_perm_id'), nullable=False, unique=None, default=None)
5206 5214 user_repo_to_perm = relationship('UserRepoToPerm')
5207 5215
5208 5216 rule_order = Column('rule_order', Integer(), nullable=False)
5209 5217 _branch_pattern = Column('branch_pattern', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), default=u'*') # glob
5210 5218 _branch_hash = Column('branch_hash', UnicodeText().with_variant(UnicodeText(2048), 'mysql'))
5211 5219
5212 5220 def __unicode__(self):
5213 5221 return u'<UserBranchPermission(%s => %r)>' % (
5214 5222 self.user_repo_to_perm, self.branch_pattern)
5215 5223
5216 5224
5217 5225 class UserGroupToRepoBranchPermission(Base, _BaseBranchPerms):
5218 5226 __tablename__ = 'user_group_to_repo_branch_permissions'
5219 5227 __table_args__ = (
5220 5228 base_table_args
5221 5229 )
5222 5230
5223 5231 branch_rule_id = Column('branch_rule_id', Integer(), primary_key=True)
5224 5232
5225 5233 repository_id = Column('repository_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
5226 5234 repo = relationship('Repository', backref='user_group_branch_perms')
5227 5235
5228 5236 permission_id = Column('permission_id', Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
5229 5237 permission = relationship('Permission')
5230 5238
5231 5239 rule_to_perm_id = Column('rule_to_perm_id', Integer(), ForeignKey('users_group_repo_to_perm.users_group_to_perm_id'), nullable=False, unique=None, default=None)
5232 5240 user_group_repo_to_perm = relationship('UserGroupRepoToPerm')
5233 5241
5234 5242 rule_order = Column('rule_order', Integer(), nullable=False)
5235 5243 _branch_pattern = Column('branch_pattern', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), default=u'*') # glob
5236 5244 _branch_hash = Column('branch_hash', UnicodeText().with_variant(UnicodeText(2048), 'mysql'))
5237 5245
5238 5246 def __unicode__(self):
5239 5247 return u'<UserBranchPermission(%s => %r)>' % (
5240 5248 self.user_group_repo_to_perm, self.branch_pattern)
5241 5249
5242 5250
5243 5251 class UserBookmark(Base, BaseModel):
5244 5252 __tablename__ = 'user_bookmarks'
5245 5253 __table_args__ = (
5246 5254 UniqueConstraint('user_id', 'bookmark_repo_id'),
5247 5255 UniqueConstraint('user_id', 'bookmark_repo_group_id'),
5248 5256 UniqueConstraint('user_id', 'bookmark_position'),
5249 5257 base_table_args
5250 5258 )
5251 5259
5252 5260 user_bookmark_id = Column("user_bookmark_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
5253 5261 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
5254 5262 position = Column("bookmark_position", Integer(), nullable=False)
5255 5263 title = Column("bookmark_title", String(255), nullable=True, unique=None, default=None)
5256 5264 redirect_url = Column("bookmark_redirect_url", String(10240), nullable=True, unique=None, default=None)
5257 5265 created_on = Column("created_on", DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5258 5266
5259 5267 bookmark_repo_id = Column("bookmark_repo_id", Integer(), ForeignKey("repositories.repo_id"), nullable=True, unique=None, default=None)
5260 5268 bookmark_repo_group_id = Column("bookmark_repo_group_id", Integer(), ForeignKey("groups.group_id"), nullable=True, unique=None, default=None)
5261 5269
5262 5270 user = relationship("User")
5263 5271
5264 5272 repository = relationship("Repository")
5265 5273 repository_group = relationship("RepoGroup")
5266 5274
5267 5275 @classmethod
5268 5276 def get_by_position_for_user(cls, position, user_id):
5269 5277 return cls.query() \
5270 5278 .filter(UserBookmark.user_id == user_id) \
5271 5279 .filter(UserBookmark.position == position).scalar()
5272 5280
5273 5281 @classmethod
5274 5282 def get_bookmarks_for_user(cls, user_id, cache=True):
5275 5283 bookmarks = cls.query() \
5276 5284 .filter(UserBookmark.user_id == user_id) \
5277 5285 .options(joinedload(UserBookmark.repository)) \
5278 5286 .options(joinedload(UserBookmark.repository_group)) \
5279 5287 .order_by(UserBookmark.position.asc())
5280 5288
5281 5289 if cache:
5282 5290 bookmarks = bookmarks.options(
5283 5291 FromCache("sql_cache_short", "get_user_{}_bookmarks".format(user_id))
5284 5292 )
5285 5293
5286 5294 return bookmarks.all()
5287 5295
5288 5296 def __unicode__(self):
5289 5297 return u'<UserBookmark(%s @ %r)>' % (self.position, self.redirect_url)
5290 5298
5291 5299
5292 5300 class FileStore(Base, BaseModel):
5293 5301 __tablename__ = 'file_store'
5294 5302 __table_args__ = (
5295 5303 base_table_args
5296 5304 )
5297 5305
5298 5306 file_store_id = Column('file_store_id', Integer(), primary_key=True)
5299 5307 file_uid = Column('file_uid', String(1024), nullable=False)
5300 5308 file_display_name = Column('file_display_name', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), nullable=True)
5301 5309 file_description = Column('file_description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'), nullable=True)
5302 5310 file_org_name = Column('file_org_name', UnicodeText().with_variant(UnicodeText(10240), 'mysql'), nullable=False)
5303 5311
5304 5312 # sha256 hash
5305 5313 file_hash = Column('file_hash', String(512), nullable=False)
5306 5314 file_size = Column('file_size', BigInteger(), nullable=False)
5307 5315
5308 5316 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5309 5317 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True)
5310 5318 accessed_count = Column('accessed_count', Integer(), default=0)
5311 5319
5312 5320 enabled = Column('enabled', Boolean(), nullable=False, default=True)
5313 5321
5314 5322 # if repo/repo_group reference is set, check for permissions
5315 5323 check_acl = Column('check_acl', Boolean(), nullable=False, default=True)
5316 5324
5317 5325 # hidden defines an attachment that should be hidden from showing in artifact listing
5318 5326 hidden = Column('hidden', Boolean(), nullable=False, default=False)
5319 5327
5320 5328 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
5321 5329 upload_user = relationship('User', lazy='joined', primaryjoin='User.user_id==FileStore.user_id')
5322 5330
5323 5331 file_metadata = relationship('FileStoreMetadata', lazy='joined')
5324 5332
5325 5333 # scope limited to user, which requester have access to
5326 5334 scope_user_id = Column(
5327 5335 'scope_user_id', Integer(), ForeignKey('users.user_id'),
5328 5336 nullable=True, unique=None, default=None)
5329 5337 user = relationship('User', lazy='joined', primaryjoin='User.user_id==FileStore.scope_user_id')
5330 5338
5331 5339 # scope limited to user group, which requester have access to
5332 5340 scope_user_group_id = Column(
5333 5341 'scope_user_group_id', Integer(), ForeignKey('users_groups.users_group_id'),
5334 5342 nullable=True, unique=None, default=None)
5335 5343 user_group = relationship('UserGroup', lazy='joined')
5336 5344
5337 5345 # scope limited to repo, which requester have access to
5338 5346 scope_repo_id = Column(
5339 5347 'scope_repo_id', Integer(), ForeignKey('repositories.repo_id'),
5340 5348 nullable=True, unique=None, default=None)
5341 5349 repo = relationship('Repository', lazy='joined')
5342 5350
5343 5351 # scope limited to repo group, which requester have access to
5344 5352 scope_repo_group_id = Column(
5345 5353 'scope_repo_group_id', Integer(), ForeignKey('groups.group_id'),
5346 5354 nullable=True, unique=None, default=None)
5347 5355 repo_group = relationship('RepoGroup', lazy='joined')
5348 5356
5349 5357 @classmethod
5350 5358 def get_by_store_uid(cls, file_store_uid):
5351 5359 return FileStore.query().filter(FileStore.file_uid == file_store_uid).scalar()
5352 5360
5353 5361 @classmethod
5354 5362 def create(cls, file_uid, filename, file_hash, file_size, file_display_name='',
5355 5363 file_description='', enabled=True, hidden=False, check_acl=True,
5356 5364 user_id=None, scope_user_id=None, scope_repo_id=None, scope_repo_group_id=None):
5357 5365
5358 5366 store_entry = FileStore()
5359 5367 store_entry.file_uid = file_uid
5360 5368 store_entry.file_display_name = file_display_name
5361 5369 store_entry.file_org_name = filename
5362 5370 store_entry.file_size = file_size
5363 5371 store_entry.file_hash = file_hash
5364 5372 store_entry.file_description = file_description
5365 5373
5366 5374 store_entry.check_acl = check_acl
5367 5375 store_entry.enabled = enabled
5368 5376 store_entry.hidden = hidden
5369 5377
5370 5378 store_entry.user_id = user_id
5371 5379 store_entry.scope_user_id = scope_user_id
5372 5380 store_entry.scope_repo_id = scope_repo_id
5373 5381 store_entry.scope_repo_group_id = scope_repo_group_id
5374 5382
5375 5383 return store_entry
5376 5384
5377 5385 @classmethod
5378 5386 def store_metadata(cls, file_store_id, args, commit=True):
5379 5387 file_store = FileStore.get(file_store_id)
5380 5388 if file_store is None:
5381 5389 return
5382 5390
5383 5391 for section, key, value, value_type in args:
5384 5392 has_key = FileStoreMetadata().query() \
5385 5393 .filter(FileStoreMetadata.file_store_id == file_store.file_store_id) \
5386 5394 .filter(FileStoreMetadata.file_store_meta_section == section) \
5387 5395 .filter(FileStoreMetadata.file_store_meta_key == key) \
5388 5396 .scalar()
5389 5397 if has_key:
5390 5398 msg = 'key `{}` already defined under section `{}` for this file.'\
5391 5399 .format(key, section)
5392 5400 raise ArtifactMetadataDuplicate(msg, err_section=section, err_key=key)
5393 5401
5394 5402 # NOTE(marcink): raises ArtifactMetadataBadValueType
5395 5403 FileStoreMetadata.valid_value_type(value_type)
5396 5404
5397 5405 meta_entry = FileStoreMetadata()
5398 5406 meta_entry.file_store = file_store
5399 5407 meta_entry.file_store_meta_section = section
5400 5408 meta_entry.file_store_meta_key = key
5401 5409 meta_entry.file_store_meta_value_type = value_type
5402 5410 meta_entry.file_store_meta_value = value
5403 5411
5404 5412 Session().add(meta_entry)
5405 5413
5406 5414 try:
5407 5415 if commit:
5408 5416 Session().commit()
5409 5417 except IntegrityError:
5410 5418 Session().rollback()
5411 5419 raise ArtifactMetadataDuplicate('Duplicate section/key found for this file.')
5412 5420
5413 5421 @classmethod
5414 5422 def bump_access_counter(cls, file_uid, commit=True):
5415 5423 FileStore().query()\
5416 5424 .filter(FileStore.file_uid == file_uid)\
5417 5425 .update({FileStore.accessed_count: (FileStore.accessed_count + 1),
5418 5426 FileStore.accessed_on: datetime.datetime.now()})
5419 5427 if commit:
5420 5428 Session().commit()
5421 5429
5422 5430 def __json__(self):
5423 5431 data = {
5424 5432 'filename': self.file_display_name,
5425 5433 'filename_org': self.file_org_name,
5426 5434 'file_uid': self.file_uid,
5427 5435 'description': self.file_description,
5428 5436 'hidden': self.hidden,
5429 5437 'size': self.file_size,
5430 5438 'created_on': self.created_on,
5431 5439 'uploaded_by': self.upload_user.get_api_data(details='basic'),
5432 5440 'downloaded_times': self.accessed_count,
5433 5441 'sha256': self.file_hash,
5434 5442 'metadata': self.file_metadata,
5435 5443 }
5436 5444
5437 5445 return data
5438 5446
5439 5447 def __repr__(self):
5440 5448 return '<FileStore({})>'.format(self.file_store_id)
5441 5449
5442 5450
5443 5451 class FileStoreMetadata(Base, BaseModel):
5444 5452 __tablename__ = 'file_store_metadata'
5445 5453 __table_args__ = (
5446 5454 UniqueConstraint('file_store_id', 'file_store_meta_section_hash', 'file_store_meta_key_hash'),
5447 5455 Index('file_store_meta_section_idx', 'file_store_meta_section', mysql_length=255),
5448 5456 Index('file_store_meta_key_idx', 'file_store_meta_key', mysql_length=255),
5449 5457 base_table_args
5450 5458 )
5451 5459 SETTINGS_TYPES = {
5452 5460 'str': safe_str,
5453 5461 'int': safe_int,
5454 5462 'unicode': safe_unicode,
5455 5463 'bool': str2bool,
5456 5464 'list': functools.partial(aslist, sep=',')
5457 5465 }
5458 5466
5459 5467 file_store_meta_id = Column(
5460 5468 "file_store_meta_id", Integer(), nullable=False, unique=True, default=None,
5461 5469 primary_key=True)
5462 5470 _file_store_meta_section = Column(
5463 5471 "file_store_meta_section", UnicodeText().with_variant(UnicodeText(1024), 'mysql'),
5464 5472 nullable=True, unique=None, default=None)
5465 5473 _file_store_meta_section_hash = Column(
5466 5474 "file_store_meta_section_hash", String(255),
5467 5475 nullable=True, unique=None, default=None)
5468 5476 _file_store_meta_key = Column(
5469 5477 "file_store_meta_key", UnicodeText().with_variant(UnicodeText(1024), 'mysql'),
5470 5478 nullable=True, unique=None, default=None)
5471 5479 _file_store_meta_key_hash = Column(
5472 5480 "file_store_meta_key_hash", String(255), nullable=True, unique=None, default=None)
5473 5481 _file_store_meta_value = Column(
5474 5482 "file_store_meta_value", UnicodeText().with_variant(UnicodeText(20480), 'mysql'),
5475 5483 nullable=True, unique=None, default=None)
5476 5484 _file_store_meta_value_type = Column(
5477 5485 "file_store_meta_value_type", String(255), nullable=True, unique=None,
5478 5486 default='unicode')
5479 5487
5480 5488 file_store_id = Column(
5481 5489 'file_store_id', Integer(), ForeignKey('file_store.file_store_id'),
5482 5490 nullable=True, unique=None, default=None)
5483 5491
5484 5492 file_store = relationship('FileStore', lazy='joined')
5485 5493
5486 5494 @classmethod
5487 5495 def valid_value_type(cls, value):
5488 5496 if value.split('.')[0] not in cls.SETTINGS_TYPES:
5489 5497 raise ArtifactMetadataBadValueType(
5490 5498 'value_type must be one of %s got %s' % (cls.SETTINGS_TYPES.keys(), value))
5491 5499
5492 5500 @hybrid_property
5493 5501 def file_store_meta_section(self):
5494 5502 return self._file_store_meta_section
5495 5503
5496 5504 @file_store_meta_section.setter
5497 5505 def file_store_meta_section(self, value):
5498 5506 self._file_store_meta_section = value
5499 5507 self._file_store_meta_section_hash = _hash_key(value)
5500 5508
5501 5509 @hybrid_property
5502 5510 def file_store_meta_key(self):
5503 5511 return self._file_store_meta_key
5504 5512
5505 5513 @file_store_meta_key.setter
5506 5514 def file_store_meta_key(self, value):
5507 5515 self._file_store_meta_key = value
5508 5516 self._file_store_meta_key_hash = _hash_key(value)
5509 5517
5510 5518 @hybrid_property
5511 5519 def file_store_meta_value(self):
5512 5520 val = self._file_store_meta_value
5513 5521
5514 5522 if self._file_store_meta_value_type:
5515 5523 # e.g unicode.encrypted == unicode
5516 5524 _type = self._file_store_meta_value_type.split('.')[0]
5517 5525 # decode the encrypted value if it's encrypted field type
5518 5526 if '.encrypted' in self._file_store_meta_value_type:
5519 5527 cipher = EncryptedTextValue()
5520 5528 val = safe_unicode(cipher.process_result_value(val, None))
5521 5529 # do final type conversion
5522 5530 converter = self.SETTINGS_TYPES.get(_type) or self.SETTINGS_TYPES['unicode']
5523 5531 val = converter(val)
5524 5532
5525 5533 return val
5526 5534
5527 5535 @file_store_meta_value.setter
5528 5536 def file_store_meta_value(self, val):
5529 5537 val = safe_unicode(val)
5530 5538 # encode the encrypted value
5531 5539 if '.encrypted' in self.file_store_meta_value_type:
5532 5540 cipher = EncryptedTextValue()
5533 5541 val = safe_unicode(cipher.process_bind_param(val, None))
5534 5542 self._file_store_meta_value = val
5535 5543
5536 5544 @hybrid_property
5537 5545 def file_store_meta_value_type(self):
5538 5546 return self._file_store_meta_value_type
5539 5547
5540 5548 @file_store_meta_value_type.setter
5541 5549 def file_store_meta_value_type(self, val):
5542 5550 # e.g unicode.encrypted
5543 5551 self.valid_value_type(val)
5544 5552 self._file_store_meta_value_type = val
5545 5553
5546 5554 def __json__(self):
5547 5555 data = {
5548 5556 'artifact': self.file_store.file_uid,
5549 5557 'section': self.file_store_meta_section,
5550 5558 'key': self.file_store_meta_key,
5551 5559 'value': self.file_store_meta_value,
5552 5560 }
5553 5561
5554 5562 return data
5555 5563
5556 5564 def __repr__(self):
5557 5565 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.file_store_meta_section,
5558 5566 self.file_store_meta_key, self.file_store_meta_value)
5559 5567
5560 5568
5561 5569 class DbMigrateVersion(Base, BaseModel):
5562 5570 __tablename__ = 'db_migrate_version'
5563 5571 __table_args__ = (
5564 5572 base_table_args,
5565 5573 )
5566 5574
5567 5575 repository_id = Column('repository_id', String(250), primary_key=True)
5568 5576 repository_path = Column('repository_path', Text)
5569 5577 version = Column('version', Integer)
5570 5578
5571 5579 @classmethod
5572 5580 def set_version(cls, version):
5573 5581 """
5574 5582 Helper for forcing a different version, usually for debugging purposes via ishell.
5575 5583 """
5576 5584 ver = DbMigrateVersion.query().first()
5577 5585 ver.version = version
5578 5586 Session().commit()
5579 5587
5580 5588
5581 5589 class DbSession(Base, BaseModel):
5582 5590 __tablename__ = 'db_session'
5583 5591 __table_args__ = (
5584 5592 base_table_args,
5585 5593 )
5586 5594
5587 5595 def __repr__(self):
5588 5596 return '<DB:DbSession({})>'.format(self.id)
5589 5597
5590 5598 id = Column('id', Integer())
5591 5599 namespace = Column('namespace', String(255), primary_key=True)
5592 5600 accessed = Column('accessed', DateTime, nullable=False)
5593 5601 created = Column('created', DateTime, nullable=False)
5594 5602 data = Column('data', PickleType, nullable=False)
@@ -1,1201 +1,1201 b''
1 1 ## -*- coding: utf-8 -*-
2 2
3 3 <%!
4 4 ## base64 filter e.g ${ example | base64 }
5 5 def base64(text):
6 6 import base64
7 7 from rhodecode.lib.helpers import safe_str
8 8 return base64.encodestring(safe_str(text))
9 9 %>
10 10
11 11 <%inherit file="root.mako"/>
12 12
13 13 <%include file="/ejs_templates/templates.html"/>
14 14
15 15 <div class="outerwrapper">
16 16 <!-- HEADER -->
17 17 <div class="header">
18 18 <div id="header-inner" class="wrapper">
19 19 <div id="logo">
20 20 <div class="logo-wrapper">
21 21 <a href="${h.route_path('home')}"><img src="${h.asset('images/rhodecode-logo-white-60x60.png')}" alt="RhodeCode"/></a>
22 22 </div>
23 23 % if c.rhodecode_name:
24 24 <div class="branding">
25 25 <a href="${h.route_path('home')}">${h.branding(c.rhodecode_name)}</a>
26 26 </div>
27 27 % endif
28 28 </div>
29 29 <!-- MENU BAR NAV -->
30 30 ${self.menu_bar_nav()}
31 31 <!-- END MENU BAR NAV -->
32 32 </div>
33 33 </div>
34 34 ${self.menu_bar_subnav()}
35 35 <!-- END HEADER -->
36 36
37 37 <!-- CONTENT -->
38 38 <div id="content" class="wrapper">
39 39
40 40 <rhodecode-toast id="notifications"></rhodecode-toast>
41 41
42 42 <div class="main">
43 43 ${next.main()}
44 44 </div>
45 45 </div>
46 46 <!-- END CONTENT -->
47 47
48 48 </div>
49 49 <!-- FOOTER -->
50 50 <div id="footer">
51 51 <div id="footer-inner" class="title wrapper">
52 52 <div>
53 53 <p class="footer-link-right">
54 54 % if c.visual.show_version:
55 55 RhodeCode Enterprise ${c.rhodecode_version} ${c.rhodecode_edition}
56 56 % endif
57 57 &copy; 2010-${h.datetime.today().year}, <a href="${h.route_url('rhodecode_official')}" target="_blank">RhodeCode GmbH</a>. All rights reserved.
58 58 % if c.visual.rhodecode_support_url:
59 59 <a href="${c.visual.rhodecode_support_url}" target="_blank">${_('Support')}</a>
60 60 % endif
61 61 </p>
62 62 <% sid = 'block' if request.GET.get('showrcid') else 'none' %>
63 63 <p class="server-instance" style="display:${sid}">
64 64 ## display hidden instance ID if specially defined
65 65 % if c.rhodecode_instanceid:
66 66 ${_('RhodeCode instance id: {}').format(c.rhodecode_instanceid)}
67 67 % endif
68 68 </p>
69 69 </div>
70 70 </div>
71 71 </div>
72 72
73 73 <!-- END FOOTER -->
74 74
75 75 ### MAKO DEFS ###
76 76
77 77 <%def name="menu_bar_subnav()">
78 78 </%def>
79 79
80 80 <%def name="breadcrumbs(class_='breadcrumbs')">
81 81 <div class="${class_}">
82 82 ${self.breadcrumbs_links()}
83 83 </div>
84 84 </%def>
85 85
86 86 <%def name="admin_menu(active=None)">
87 87
88 88 <div id="context-bar">
89 89 <div class="wrapper">
90 90 <div class="title">
91 91 <div class="title-content">
92 92 <div class="title-main">
93 93 % if c.is_super_admin:
94 94 ${_('Super-admin Panel')}
95 95 % else:
96 96 ${_('Delegated Admin Panel')}
97 97 % endif
98 98 </div>
99 99 </div>
100 100 </div>
101 101
102 102 <ul id="context-pages" class="navigation horizontal-list">
103 103
104 104 ## super-admin case
105 105 % if c.is_super_admin:
106 106 <li class="${h.is_active('audit_logs', active)}"><a href="${h.route_path('admin_audit_logs')}">${_('Admin audit logs')}</a></li>
107 107 <li class="${h.is_active('repositories', active)}"><a href="${h.route_path('repos')}">${_('Repositories')}</a></li>
108 108 <li class="${h.is_active('repository_groups', active)}"><a href="${h.route_path('repo_groups')}">${_('Repository groups')}</a></li>
109 109 <li class="${h.is_active('users', active)}"><a href="${h.route_path('users')}">${_('Users')}</a></li>
110 110 <li class="${h.is_active('user_groups', active)}"><a href="${h.route_path('user_groups')}">${_('User groups')}</a></li>
111 111 <li class="${h.is_active('permissions', active)}"><a href="${h.route_path('admin_permissions_application')}">${_('Permissions')}</a></li>
112 112 <li class="${h.is_active('authentication', active)}"><a href="${h.route_path('auth_home', traverse='')}">${_('Authentication')}</a></li>
113 113 <li class="${h.is_active('integrations', active)}"><a href="${h.route_path('global_integrations_home')}">${_('Integrations')}</a></li>
114 114 <li class="${h.is_active('defaults', active)}"><a href="${h.route_path('admin_defaults_repositories')}">${_('Defaults')}</a></li>
115 115 <li class="${h.is_active('settings', active)}"><a href="${h.route_path('admin_settings')}">${_('Settings')}</a></li>
116 116
117 117 ## delegated admin
118 118 % elif c.is_delegated_admin:
119 119 <%
120 120 repositories=c.auth_user.repositories_admin or c.can_create_repo
121 121 repository_groups=c.auth_user.repository_groups_admin or c.can_create_repo_group
122 122 user_groups=c.auth_user.user_groups_admin or c.can_create_user_group
123 123 %>
124 124
125 125 %if repositories:
126 126 <li class="${h.is_active('repositories', active)} local-admin-repos"><a href="${h.route_path('repos')}">${_('Repositories')}</a></li>
127 127 %endif
128 128 %if repository_groups:
129 129 <li class="${h.is_active('repository_groups', active)} local-admin-repo-groups"><a href="${h.route_path('repo_groups')}">${_('Repository groups')}</a></li>
130 130 %endif
131 131 %if user_groups:
132 132 <li class="${h.is_active('user_groups', active)} local-admin-user-groups"><a href="${h.route_path('user_groups')}">${_('User groups')}</a></li>
133 133 %endif
134 134 % endif
135 135 </ul>
136 136
137 137 </div>
138 138 <div class="clear"></div>
139 139 </div>
140 140 </%def>
141 141
142 142 <%def name="dt_info_panel(elements)">
143 143 <dl class="dl-horizontal">
144 144 %for dt, dd, title, show_items in elements:
145 145 <dt>${dt}:</dt>
146 146 <dd title="${h.tooltip(title)}">
147 147 %if callable(dd):
148 148 ## allow lazy evaluation of elements
149 149 ${dd()}
150 150 %else:
151 151 ${dd}
152 152 %endif
153 153 %if show_items:
154 154 <span class="btn-collapse" data-toggle="item-${h.md5_safe(dt)[:6]}-details">${_('Show More')} </span>
155 155 %endif
156 156 </dd>
157 157
158 158 %if show_items:
159 159 <div class="collapsable-content" data-toggle="item-${h.md5_safe(dt)[:6]}-details" style="display: none">
160 160 %for item in show_items:
161 161 <dt></dt>
162 162 <dd>${item}</dd>
163 163 %endfor
164 164 </div>
165 165 %endif
166 166
167 167 %endfor
168 168 </dl>
169 169 </%def>
170 170
171 171 <%def name="tr_info_entry(element)">
172 172 <% key, val, title, show_items = element %>
173 173
174 174 <tr>
175 175 <td style="vertical-align: top">${key}</td>
176 176 <td title="${h.tooltip(title)}">
177 177 %if callable(val):
178 178 ## allow lazy evaluation of elements
179 179 ${val()}
180 180 %else:
181 181 ${val}
182 182 %endif
183 183 %if show_items:
184 184 <div class="collapsable-content" data-toggle="item-${h.md5_safe(val)[:6]}-details" style="display: none">
185 185 % for item in show_items:
186 186 <dt></dt>
187 187 <dd>${item}</dd>
188 188 % endfor
189 189 </div>
190 190 %endif
191 191 </td>
192 192 <td style="vertical-align: top">
193 193 %if show_items:
194 194 <span class="btn-collapse" data-toggle="item-${h.md5_safe(val)[:6]}-details">${_('Show More')} </span>
195 195 %endif
196 196 </td>
197 197 </tr>
198 198
199 199 </%def>
200 200
201 201 <%def name="gravatar(email, size=16, tooltip=False, tooltip_alt=None, user=None, extra_class=None)">
202 202 <%
203 203 if size > 16:
204 204 gravatar_class = ['gravatar','gravatar-large']
205 205 else:
206 206 gravatar_class = ['gravatar']
207 207
208 208 data_hovercard_url = ''
209 209 data_hovercard_alt = tooltip_alt.replace('<', '&lt;').replace('>', '&gt;') if tooltip_alt else ''
210 210
211 211 if tooltip:
212 212 gravatar_class += ['tooltip-hovercard']
213 213 if extra_class:
214 214 gravatar_class += extra_class
215 215 if tooltip and user:
216 216 if user.username == h.DEFAULT_USER:
217 217 gravatar_class.pop(-1)
218 218 else:
219 219 data_hovercard_url = request.route_path('hovercard_user', user_id=getattr(user, 'user_id', ''))
220 220 gravatar_class = ' '.join(gravatar_class)
221 221
222 222 %>
223 223 <%doc>
224 224 TODO: johbo: For now we serve double size images to make it smooth
225 225 for retina. This is how it worked until now. Should be replaced
226 226 with a better solution at some point.
227 227 </%doc>
228 228
229 229 <img class="${gravatar_class}" height="${size}" width="${size}" data-hovercard-url="${data_hovercard_url}" data-hovercard-alt="${data_hovercard_alt}" src="${h.gravatar_url(email, size * 2)}" />
230 230 </%def>
231 231
232 232
233 233 <%def name="gravatar_with_user(contact, size=16, show_disabled=False, tooltip=False, _class='rc-user')">
234 234 <%
235 235 email = h.email_or_none(contact)
236 236 rc_user = h.discover_user(contact)
237 237 %>
238 238
239 239 <div class="${_class}">
240 240 ${self.gravatar(email, size, tooltip=tooltip, tooltip_alt=contact, user=rc_user)}
241 241 <span class="${('user user-disabled' if show_disabled else 'user')}"> ${h.link_to_user(rc_user or contact)}</span>
242 242 </div>
243 243 </%def>
244 244
245 245
246 246 <%def name="user_group_icon(user_group=None, size=16, tooltip=False)">
247 247 <%
248 248 if (size > 16):
249 249 gravatar_class = 'icon-user-group-alt'
250 250 else:
251 251 gravatar_class = 'icon-user-group-alt'
252 252
253 253 if tooltip:
254 254 gravatar_class += ' tooltip-hovercard'
255 255
256 256 data_hovercard_url = request.route_path('hovercard_user_group', user_group_id=user_group.users_group_id)
257 257 %>
258 258 <%doc>
259 259 TODO: johbo: For now we serve double size images to make it smooth
260 260 for retina. This is how it worked until now. Should be replaced
261 261 with a better solution at some point.
262 262 </%doc>
263 263
264 264 <i style="font-size: ${size}px" class="${gravatar_class} x-icon-size-${size}" data-hovercard-url="${data_hovercard_url}"></i>
265 265 </%def>
266 266
267 267 <%def name="repo_page_title(repo_instance)">
268 268 <div class="title-content repo-title">
269 269
270 270 <div class="title-main">
271 271 ## SVN/HG/GIT icons
272 272 %if h.is_hg(repo_instance):
273 273 <i class="icon-hg"></i>
274 274 %endif
275 275 %if h.is_git(repo_instance):
276 276 <i class="icon-git"></i>
277 277 %endif
278 278 %if h.is_svn(repo_instance):
279 279 <i class="icon-svn"></i>
280 280 %endif
281 281
282 282 ## public/private
283 283 %if repo_instance.private:
284 284 <i class="icon-repo-private"></i>
285 285 %else:
286 286 <i class="icon-repo-public"></i>
287 287 %endif
288 288
289 289 ## repo name with group name
290 290 ${h.breadcrumb_repo_link(repo_instance)}
291 291
292 292 ## Context Actions
293 293 <div class="pull-right">
294 294 %if c.rhodecode_user.username != h.DEFAULT_USER:
295 295 <a href="${h.route_path('atom_feed_home', repo_name=c.rhodecode_db_repo.repo_uid, _query=dict(auth_token=c.rhodecode_user.feed_token))}" title="${_('RSS Feed')}" class="btn btn-sm"><i class="icon-rss-sign"></i>RSS</a>
296 296
297 297 <a href="#WatchRepo" onclick="toggleFollowingRepo(this, templateContext.repo_id); return false" title="${_('Watch this Repository and actions on it in your personalized journal')}" class="btn btn-sm ${('watching' if c.repository_is_user_following else '')}">
298 298 % if c.repository_is_user_following:
299 299 <i class="icon-eye-off"></i>${_('Unwatch')}
300 300 % else:
301 301 <i class="icon-eye"></i>${_('Watch')}
302 302 % endif
303 303
304 304 </a>
305 305 %else:
306 306 <a href="${h.route_path('atom_feed_home', repo_name=c.rhodecode_db_repo.repo_uid)}" title="${_('RSS Feed')}" class="btn btn-sm"><i class="icon-rss-sign"></i>RSS</a>
307 307 %endif
308 308 </div>
309 309
310 310 </div>
311 311
312 312 ## FORKED
313 313 %if repo_instance.fork:
314 314 <p class="discreet">
315 315 <i class="icon-code-fork"></i> ${_('Fork of')}
316 316 ${h.link_to_if(c.has_origin_repo_read_perm,repo_instance.fork.repo_name, h.route_path('repo_summary', repo_name=repo_instance.fork.repo_name))}
317 317 </p>
318 318 %endif
319 319
320 320 ## IMPORTED FROM REMOTE
321 321 %if repo_instance.clone_uri:
322 322 <p class="discreet">
323 323 <i class="icon-code-fork"></i> ${_('Clone from')}
324 324 <a href="${h.safe_str(h.hide_credentials(repo_instance.clone_uri))}">${h.hide_credentials(repo_instance.clone_uri)}</a>
325 325 </p>
326 326 %endif
327 327
328 328 ## LOCKING STATUS
329 329 %if repo_instance.locked[0]:
330 330 <p class="locking_locked discreet">
331 331 <i class="icon-repo-lock"></i>
332 332 ${_('Repository locked by %(user)s') % {'user': h.person_by_id(repo_instance.locked[0])}}
333 333 </p>
334 334 %elif repo_instance.enable_locking:
335 335 <p class="locking_unlocked discreet">
336 336 <i class="icon-repo-unlock"></i>
337 337 ${_('Repository not locked. Pull repository to lock it.')}
338 338 </p>
339 339 %endif
340 340
341 341 </div>
342 342 </%def>
343 343
344 344 <%def name="repo_menu(active=None)">
345 345 <%
346 346 ## determine if we have "any" option available
347 347 can_lock = h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name) and c.rhodecode_db_repo.enable_locking
348 348 has_actions = can_lock
349 349
350 350 %>
351 351 % if c.rhodecode_db_repo.archived:
352 352 <div class="alert alert-warning text-center">
353 353 <strong>${_('This repository has been archived. It is now read-only.')}</strong>
354 354 </div>
355 355 % endif
356 356
357 357 <!--- REPO CONTEXT BAR -->
358 358 <div id="context-bar">
359 359 <div class="wrapper">
360 360
361 361 <div class="title">
362 362 ${self.repo_page_title(c.rhodecode_db_repo)}
363 363 </div>
364 364
365 365 <ul id="context-pages" class="navigation horizontal-list">
366 366 <li class="${h.is_active('summary', active)}"><a class="menulink" href="${h.route_path('repo_summary', repo_name=c.repo_name)}"><div class="menulabel">${_('Summary')}</div></a></li>
367 367 <li class="${h.is_active('commits', active)}"><a class="menulink" href="${h.route_path('repo_commits', repo_name=c.repo_name)}"><div class="menulabel">${_('Commits')}</div></a></li>
368 <li class="${h.is_active('files', active)}"><a class="menulink" href="${h.route_path('repo_files', repo_name=c.repo_name, commit_id=c.rhodecode_db_repo.landing_rev[1], f_path='')}"><div class="menulabel">${_('Files')}</div></a></li>
368 <li class="${h.is_active('files', active)}"><a class="menulink" href="${h.route_path('repo_files', repo_name=c.repo_name, commit_id=c.rhodecode_db_repo.landing_ref_name, f_path='')}"><div class="menulabel">${_('Files')}</div></a></li>
369 369 <li class="${h.is_active('compare', active)}"><a class="menulink" href="${h.route_path('repo_compare_select',repo_name=c.repo_name)}"><div class="menulabel">${_('Compare')}</div></a></li>
370 370
371 371 ## TODO: anderson: ideally it would have a function on the scm_instance "enable_pullrequest() and enable_fork()"
372 372 %if c.rhodecode_db_repo.repo_type in ['git','hg']:
373 373 <li class="${h.is_active('showpullrequest', active)}">
374 374 <a class="menulink" href="${h.route_path('pullrequest_show_all', repo_name=c.repo_name)}" title="${h.tooltip(_('Show Pull Requests for %s') % c.repo_name)}">
375 375 <div class="menulabel">
376 376 ${_('Pull Requests')} <span class="menulink-counter">${c.repository_pull_requests}</span>
377 377 </div>
378 378 </a>
379 379 </li>
380 380 %endif
381 381
382 382 <li class="${h.is_active('artifacts', active)}">
383 383 <a class="menulink" href="${h.route_path('repo_artifacts_list',repo_name=c.repo_name)}">
384 384 <div class="menulabel">
385 385 ${_('Artifacts')} <span class="menulink-counter">${c.repository_artifacts}</span>
386 386 </div>
387 387 </a>
388 388 </li>
389 389
390 390 %if h.HasRepoPermissionAll('repository.admin')(c.repo_name):
391 391 <li class="${h.is_active('settings', active)}"><a class="menulink" href="${h.route_path('edit_repo',repo_name=c.repo_name)}"><div class="menulabel">${_('Repository Settings')}</div></a></li>
392 392 %endif
393 393
394 394 <li class="${h.is_active('options', active)}">
395 395 % if has_actions:
396 396 <a class="menulink dropdown">
397 397 <div class="menulabel">${_('Options')}<div class="show_more"></div></div>
398 398 </a>
399 399 <ul class="submenu">
400 400 %if can_lock:
401 401 %if c.rhodecode_db_repo.locked[0]:
402 402 <li><a class="locking_del" href="${h.route_path('repo_edit_toggle_locking',repo_name=c.repo_name)}">${_('Unlock Repository')}</a></li>
403 403 %else:
404 404 <li><a class="locking_add" href="${h.route_path('repo_edit_toggle_locking',repo_name=c.repo_name)}">${_('Lock Repository')}</a></li>
405 405 %endif
406 406 %endif
407 407 </ul>
408 408 % endif
409 409 </li>
410 410
411 411 </ul>
412 412 </div>
413 413 <div class="clear"></div>
414 414 </div>
415 415
416 416 <!--- REPO END CONTEXT BAR -->
417 417
418 418 </%def>
419 419
420 420 <%def name="repo_group_page_title(repo_group_instance)">
421 421 <div class="title-content">
422 422 <div class="title-main">
423 423 ## Repository Group icon
424 424 <i class="icon-repo-group"></i>
425 425
426 426 ## repo name with group name
427 427 ${h.breadcrumb_repo_group_link(repo_group_instance)}
428 428 </div>
429 429
430 430 <%namespace name="dt" file="/data_table/_dt_elements.mako"/>
431 431 <div class="repo-group-desc discreet">
432 432 ${dt.repo_group_desc(repo_group_instance.description_safe, repo_group_instance.personal, c.visual.stylify_metatags)}
433 433 </div>
434 434
435 435 </div>
436 436 </%def>
437 437
438 438
439 439 <%def name="repo_group_menu(active=None)">
440 440 <%
441 441 gr_name = c.repo_group.group_name if c.repo_group else None
442 442 # create repositories with write permission on group is set to true
443 443 group_admin = h.HasRepoGroupPermissionAny('group.admin')(gr_name, 'group admin index page')
444 444
445 445 %>
446 446
447 447
448 448 <!--- REPO GROUP CONTEXT BAR -->
449 449 <div id="context-bar">
450 450 <div class="wrapper">
451 451 <div class="title">
452 452 ${self.repo_group_page_title(c.repo_group)}
453 453 </div>
454 454
455 455 <ul id="context-pages" class="navigation horizontal-list">
456 456 <li class="${h.is_active('home', active)}">
457 457 <a class="menulink" href="${h.route_path('repo_group_home', repo_group_name=c.repo_group.group_name)}"><div class="menulabel">${_('Group Home')}</div></a>
458 458 </li>
459 459 % if c.is_super_admin or group_admin:
460 460 <li class="${h.is_active('settings', active)}">
461 461 <a class="menulink" href="${h.route_path('edit_repo_group',repo_group_name=c.repo_group.group_name)}" title="${_('You have admin right to this group, and can edit it')}"><div class="menulabel">${_('Group Settings')}</div></a>
462 462 </li>
463 463 % endif
464 464
465 465 </ul>
466 466 </div>
467 467 <div class="clear"></div>
468 468 </div>
469 469
470 470 <!--- REPO GROUP CONTEXT BAR -->
471 471
472 472 </%def>
473 473
474 474
475 475 <%def name="usermenu(active=False)">
476 476 <%
477 477 not_anonymous = c.rhodecode_user.username != h.DEFAULT_USER
478 478
479 479 gr_name = c.repo_group.group_name if (hasattr(c, 'repo_group') and c.repo_group) else None
480 480 # create repositories with write permission on group is set to true
481 481
482 482 can_fork = c.is_super_admin or h.HasPermissionAny('hg.fork.repository')()
483 483 create_on_write = h.HasPermissionAny('hg.create.write_on_repogroup.true')()
484 484 group_write = h.HasRepoGroupPermissionAny('group.write')(gr_name, 'can write into group index page')
485 485 group_admin = h.HasRepoGroupPermissionAny('group.admin')(gr_name, 'group admin index page')
486 486
487 487 can_create_repos = c.is_super_admin or c.can_create_repo
488 488 can_create_repo_groups = c.is_super_admin or c.can_create_repo_group
489 489
490 490 can_create_repos_in_group = c.is_super_admin or group_admin or (group_write and create_on_write)
491 491 can_create_repo_groups_in_group = c.is_super_admin or group_admin
492 492 %>
493 493
494 494 % if not_anonymous:
495 495 <%
496 496 default_target_group = dict()
497 497 if c.rhodecode_user.personal_repo_group:
498 498 default_target_group = dict(parent_group=c.rhodecode_user.personal_repo_group.group_id)
499 499 %>
500 500
501 501 ## create action
502 502 <li>
503 503 <a href="#create-actions" onclick="return false;" class="menulink childs">
504 504 <i class="tooltip icon-plus-circled" title="${_('Create')}"></i>
505 505 </a>
506 506
507 507 <div class="action-menu submenu">
508 508
509 509 <ol>
510 510 ## scope of within a repository
511 511 % if hasattr(c, 'rhodecode_db_repo') and c.rhodecode_db_repo:
512 512 <li class="submenu-title">${_('This Repository')}</li>
513 513 <li>
514 514 <a href="${h.route_path('pullrequest_new',repo_name=c.repo_name)}">${_('Create Pull Request')}</a>
515 515 </li>
516 516 % if can_fork:
517 517 <li>
518 518 <a href="${h.route_path('repo_fork_new',repo_name=c.repo_name,_query=default_target_group)}">${_('Fork this repository')}</a>
519 519 </li>
520 520 % endif
521 521 % endif
522 522
523 523 ## scope of within repository groups
524 524 % if hasattr(c, 'repo_group') and c.repo_group and (can_create_repos_in_group or can_create_repo_groups_in_group):
525 525 <li class="submenu-title">${_('This Repository Group')}</li>
526 526
527 527 % if can_create_repos_in_group:
528 528 <li>
529 529 <a href="${h.route_path('repo_new',_query=dict(parent_group=c.repo_group.group_id))}">${_('New Repository')}</a>
530 530 </li>
531 531 % endif
532 532
533 533 % if can_create_repo_groups_in_group:
534 534 <li>
535 535 <a href="${h.route_path('repo_group_new',_query=dict(parent_group=c.repo_group.group_id))}">${_(u'New Repository Group')}</a>
536 536 </li>
537 537 % endif
538 538 % endif
539 539
540 540 ## personal group
541 541 % if c.rhodecode_user.personal_repo_group:
542 542 <li class="submenu-title">Personal Group</li>
543 543
544 544 <li>
545 545 <a href="${h.route_path('repo_new',_query=dict(parent_group=c.rhodecode_user.personal_repo_group.group_id))}" >${_('New Repository')} </a>
546 546 </li>
547 547
548 548 <li>
549 549 <a href="${h.route_path('repo_group_new',_query=dict(parent_group=c.rhodecode_user.personal_repo_group.group_id))}">${_('New Repository Group')} </a>
550 550 </li>
551 551 % endif
552 552
553 553 ## Global actions
554 554 <li class="submenu-title">RhodeCode</li>
555 555 % if can_create_repos:
556 556 <li>
557 557 <a href="${h.route_path('repo_new')}" >${_('New Repository')}</a>
558 558 </li>
559 559 % endif
560 560
561 561 % if can_create_repo_groups:
562 562 <li>
563 563 <a href="${h.route_path('repo_group_new')}" >${_(u'New Repository Group')}</a>
564 564 </li>
565 565 % endif
566 566
567 567 <li>
568 568 <a href="${h.route_path('gists_new')}">${_(u'New Gist')}</a>
569 569 </li>
570 570
571 571 </ol>
572 572
573 573 </div>
574 574 </li>
575 575
576 576 ## notifications
577 577 <li>
578 578 <a class="${('empty' if c.unread_notifications == 0 else '')}" href="${h.route_path('notifications_show_all')}">
579 579 ${c.unread_notifications}
580 580 </a>
581 581 </li>
582 582 % endif
583 583
584 584 ## USER MENU
585 585 <li id="quick_login_li" class="${'active' if active else ''}">
586 586 % if c.rhodecode_user.username == h.DEFAULT_USER:
587 587 <a id="quick_login_link" class="menulink childs" href="${h.route_path('login', _query={'came_from': h.current_route_path(request)})}">
588 588 ${gravatar(c.rhodecode_user.email, 20)}
589 589 <span class="user">
590 590 <span>${_('Sign in')}</span>
591 591 </span>
592 592 </a>
593 593 % else:
594 594 ## logged in user
595 595 <a id="quick_login_link" class="menulink childs">
596 596 ${gravatar(c.rhodecode_user.email, 20)}
597 597 <span class="user">
598 598 <span class="menu_link_user">${c.rhodecode_user.username}</span>
599 599 <div class="show_more"></div>
600 600 </span>
601 601 </a>
602 602 ## subnav with menu for logged in user
603 603 <div class="user-menu submenu">
604 604 <div id="quick_login">
605 605 %if c.rhodecode_user.username != h.DEFAULT_USER:
606 606 <div class="">
607 607 <div class="big_gravatar">${gravatar(c.rhodecode_user.email, 48)}</div>
608 608 <div class="full_name">${c.rhodecode_user.full_name_or_username}</div>
609 609 <div class="email">${c.rhodecode_user.email}</div>
610 610 </div>
611 611 <div class="">
612 612 <ol class="links">
613 613 <li>${h.link_to(_(u'My account'),h.route_path('my_account_profile'))}</li>
614 614 % if c.rhodecode_user.personal_repo_group:
615 615 <li>${h.link_to(_(u'My personal group'), h.route_path('repo_group_home', repo_group_name=c.rhodecode_user.personal_repo_group.group_name))}</li>
616 616 % endif
617 617 <li>${h.link_to(_(u'Pull Requests'), h.route_path('my_account_pullrequests'))}</li>
618 618
619 619 % if c.debug_style:
620 620 <li>
621 621 <a class="menulink" title="${_('Style')}" href="${h.route_path('debug_style_home')}">
622 622 <div class="menulabel">${_('[Style]')}</div>
623 623 </a>
624 624 </li>
625 625 % endif
626 626
627 627 ## bookmark-items
628 628 <li class="bookmark-items">
629 629 ${_('Bookmarks')}
630 630 <div class="pull-right">
631 631 <a href="${h.route_path('my_account_bookmarks')}">
632 632
633 633 <i class="icon-cog"></i>
634 634 </a>
635 635 </div>
636 636 </li>
637 637 % if not c.bookmark_items:
638 638 <li>
639 639 <a href="${h.route_path('my_account_bookmarks')}">${_('No Bookmarks yet.')}</a>
640 640 </li>
641 641 % endif
642 642 % for item in c.bookmark_items:
643 643 <li>
644 644 % if item.repository:
645 645 <div>
646 646 <a class="bookmark-item" href="${h.route_path('my_account_goto_bookmark', bookmark_id=item.position)}">
647 647 <code>${item.position}</code>
648 648 % if item.repository.repo_type == 'hg':
649 649 <i class="icon-hg" title="${_('Repository')}" style="font-size: 16px"></i>
650 650 % elif item.repository.repo_type == 'git':
651 651 <i class="icon-git" title="${_('Repository')}" style="font-size: 16px"></i>
652 652 % elif item.repository.repo_type == 'svn':
653 653 <i class="icon-svn" title="${_('Repository')}" style="font-size: 16px"></i>
654 654 % endif
655 655 ${(item.title or h.shorter(item.repository.repo_name, 30))}
656 656 </a>
657 657 </div>
658 658 % elif item.repository_group:
659 659 <div>
660 660 <a class="bookmark-item" href="${h.route_path('my_account_goto_bookmark', bookmark_id=item.position)}">
661 661 <code>${item.position}</code>
662 662 <i class="icon-repo-group" title="${_('Repository group')}" style="font-size: 14px"></i>
663 663 ${(item.title or h.shorter(item.repository_group.group_name, 30))}
664 664 </a>
665 665 </div>
666 666 % else:
667 667 <a class="bookmark-item" href="${h.route_path('my_account_goto_bookmark', bookmark_id=item.position)}">
668 668 <code>${item.position}</code>
669 669 ${item.title}
670 670 </a>
671 671 % endif
672 672 </li>
673 673 % endfor
674 674
675 675 <li class="logout">
676 676 ${h.secure_form(h.route_path('logout'), request=request)}
677 677 ${h.submit('log_out', _(u'Sign Out'),class_="btn btn-primary")}
678 678 ${h.end_form()}
679 679 </li>
680 680 </ol>
681 681 </div>
682 682 %endif
683 683 </div>
684 684 </div>
685 685
686 686 % endif
687 687 </li>
688 688 </%def>
689 689
690 690 <%def name="menu_items(active=None)">
691 691 <%
692 692 notice_messages, notice_level = c.rhodecode_user.get_notice_messages()
693 693 notice_display = 'none' if len(notice_messages) == 0 else ''
694 694 %>
695 695 <style>
696 696
697 697 </style>
698 698
699 699 <ul id="quick" class="main_nav navigation horizontal-list">
700 700 ## notice box for important system messages
701 701 <li style="display: ${notice_display}">
702 702 <a class="notice-box" href="#openNotice" onclick="$('.notice-messages-container').toggle(); return false">
703 703 <div class="menulabel-notice ${notice_level}" >
704 704 ${len(notice_messages)}
705 705 </div>
706 706 </a>
707 707 </li>
708 708 <div class="notice-messages-container" style="display: none">
709 709 <div class="notice-messages">
710 710 <table class="rctable">
711 711 % for notice in notice_messages:
712 712 <tr id="notice-message-${notice['msg_id']}" class="notice-message-${notice['level']}">
713 713 <td style="vertical-align: text-top; width: 20px">
714 714 <i class="tooltip icon-info notice-color-${notice['level']}" title="${notice['level']}"></i>
715 715 </td>
716 716 <td>
717 717 <span><i class="icon-plus-squared cursor-pointer" onclick="$('#notice-${notice['msg_id']}').toggle()"></i> </span>
718 718 ${notice['subject']}
719 719
720 720 <div id="notice-${notice['msg_id']}" style="display: none">
721 721 ${h.render(notice['body'], renderer='markdown')}
722 722 </div>
723 723 </td>
724 724 <td style="vertical-align: text-top; width: 35px;">
725 725 <a class="tooltip" title="${_('dismiss')}" href="#dismiss" onclick="dismissNotice(${notice['msg_id']});return false">
726 726 <i class="icon-remove icon-filled-red"></i>
727 727 </a>
728 728 </td>
729 729 </tr>
730 730
731 731 % endfor
732 732 </table>
733 733 </div>
734 734 </div>
735 735 ## Main filter
736 736 <li>
737 737 <div class="menulabel main_filter_box">
738 738 <div class="main_filter_input_box">
739 739 <ul class="searchItems">
740 740
741 741 <li class="searchTag searchTagIcon">
742 742 <i class="icon-search"></i>
743 743 </li>
744 744
745 745 % if c.template_context['search_context']['repo_id']:
746 746 <li class="searchTag searchTagFilter searchTagHidable" >
747 747 ##<a href="${h.route_path('search_repo',repo_name=c.template_context['search_context']['repo_name'])}">
748 748 <span class="tag">
749 749 This repo
750 750 <a href="#removeGoToFilter" onclick="removeGoToFilter(); return false"><i class="icon-cancel-circled"></i></a>
751 751 </span>
752 752 ##</a>
753 753 </li>
754 754 % elif c.template_context['search_context']['repo_group_id']:
755 755 <li class="searchTag searchTagFilter searchTagHidable">
756 756 ##<a href="${h.route_path('search_repo_group',repo_group_name=c.template_context['search_context']['repo_group_name'])}">
757 757 <span class="tag">
758 758 This group
759 759 <a href="#removeGoToFilter" onclick="removeGoToFilter(); return false"><i class="icon-cancel-circled"></i></a>
760 760 </span>
761 761 ##</a>
762 762 </li>
763 763 % endif
764 764
765 765 <li class="searchTagInput">
766 766 <input class="main_filter_input" id="main_filter" size="25" type="text" name="main_filter" placeholder="${_('search / go to...')}" value="" />
767 767 </li>
768 768 <li class="searchTag searchTagHelp">
769 769 <a href="#showFilterHelp" onclick="showMainFilterBox(); return false">?</a>
770 770 </li>
771 771 </ul>
772 772 </div>
773 773 </div>
774 774
775 775 <div id="main_filter_help" style="display: none">
776 776 - Use '/' key to quickly access this field.
777 777
778 778 - Enter a name of repository, or repository group for quick search.
779 779
780 780 - Prefix query to allow special search:
781 781
782 782 user:admin, to search for usernames, always global
783 783
784 784 user_group:devops, to search for user groups, always global
785 785
786 786 pr:303, to search for pull request number, title, or description, always global
787 787
788 788 commit:efced4, to search for commits, scoped to repositories or groups
789 789
790 790 file:models.py, to search for file paths, scoped to repositories or groups
791 791
792 792 % if c.template_context['search_context']['repo_id']:
793 793 For advanced full text search visit: <a href="${h.route_path('search_repo',repo_name=c.template_context['search_context']['repo_name'])}">repository search</a>
794 794 % elif c.template_context['search_context']['repo_group_id']:
795 795 For advanced full text search visit: <a href="${h.route_path('search_repo_group',repo_group_name=c.template_context['search_context']['repo_group_name'])}">repository group search</a>
796 796 % else:
797 797 For advanced full text search visit: <a href="${h.route_path('search')}">global search</a>
798 798 % endif
799 799 </div>
800 800 </li>
801 801
802 802 ## ROOT MENU
803 803 <li class="${h.is_active('home', active)}">
804 804 <a class="menulink" title="${_('Home')}" href="${h.route_path('home')}">
805 805 <div class="menulabel">${_('Home')}</div>
806 806 </a>
807 807 </li>
808 808
809 809 %if c.rhodecode_user.username != h.DEFAULT_USER:
810 810 <li class="${h.is_active('journal', active)}">
811 811 <a class="menulink" title="${_('Show activity journal')}" href="${h.route_path('journal')}">
812 812 <div class="menulabel">${_('Journal')}</div>
813 813 </a>
814 814 </li>
815 815 %else:
816 816 <li class="${h.is_active('journal', active)}">
817 817 <a class="menulink" title="${_('Show Public activity journal')}" href="${h.route_path('journal_public')}">
818 818 <div class="menulabel">${_('Public journal')}</div>
819 819 </a>
820 820 </li>
821 821 %endif
822 822
823 823 <li class="${h.is_active('gists', active)}">
824 824 <a class="menulink childs" title="${_('Show Gists')}" href="${h.route_path('gists_show')}">
825 825 <div class="menulabel">${_('Gists')}</div>
826 826 </a>
827 827 </li>
828 828
829 829 % if c.is_super_admin or c.is_delegated_admin:
830 830 <li class="${h.is_active('admin', active)}">
831 831 <a class="menulink childs" title="${_('Admin settings')}" href="${h.route_path('admin_home')}">
832 832 <div class="menulabel">${_('Admin')} </div>
833 833 </a>
834 834 </li>
835 835 % endif
836 836
837 837 ## render extra user menu
838 838 ${usermenu(active=(active=='my_account'))}
839 839
840 840 </ul>
841 841
842 842 <script type="text/javascript">
843 843 var visualShowPublicIcon = "${c.visual.show_public_icon}" == "True";
844 844
845 845 var formatRepoResult = function(result, container, query, escapeMarkup) {
846 846 return function(data, escapeMarkup) {
847 847 if (!data.repo_id){
848 848 return data.text; // optgroup text Repositories
849 849 }
850 850
851 851 var tmpl = '';
852 852 var repoType = data['repo_type'];
853 853 var repoName = data['text'];
854 854
855 855 if(data && data.type == 'repo'){
856 856 if(repoType === 'hg'){
857 857 tmpl += '<i class="icon-hg"></i> ';
858 858 }
859 859 else if(repoType === 'git'){
860 860 tmpl += '<i class="icon-git"></i> ';
861 861 }
862 862 else if(repoType === 'svn'){
863 863 tmpl += '<i class="icon-svn"></i> ';
864 864 }
865 865 if(data['private']){
866 866 tmpl += '<i class="icon-lock" ></i> ';
867 867 }
868 868 else if(visualShowPublicIcon){
869 869 tmpl += '<i class="icon-unlock-alt"></i> ';
870 870 }
871 871 }
872 872 tmpl += escapeMarkup(repoName);
873 873 return tmpl;
874 874
875 875 }(result, escapeMarkup);
876 876 };
877 877
878 878 var formatRepoGroupResult = function(result, container, query, escapeMarkup) {
879 879 return function(data, escapeMarkup) {
880 880 if (!data.repo_group_id){
881 881 return data.text; // optgroup text Repositories
882 882 }
883 883
884 884 var tmpl = '';
885 885 var repoGroupName = data['text'];
886 886
887 887 if(data){
888 888
889 889 tmpl += '<i class="icon-repo-group"></i> ';
890 890
891 891 }
892 892 tmpl += escapeMarkup(repoGroupName);
893 893 return tmpl;
894 894
895 895 }(result, escapeMarkup);
896 896 };
897 897
898 898 var escapeRegExChars = function (value) {
899 899 return value.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
900 900 };
901 901
902 902 var getRepoIcon = function(repo_type) {
903 903 if (repo_type === 'hg') {
904 904 return '<i class="icon-hg"></i> ';
905 905 }
906 906 else if (repo_type === 'git') {
907 907 return '<i class="icon-git"></i> ';
908 908 }
909 909 else if (repo_type === 'svn') {
910 910 return '<i class="icon-svn"></i> ';
911 911 }
912 912 return ''
913 913 };
914 914
915 915 var autocompleteMainFilterFormatResult = function (data, value, org_formatter) {
916 916
917 917 if (value.split(':').length === 2) {
918 918 value = value.split(':')[1]
919 919 }
920 920
921 921 var searchType = data['type'];
922 922 var searchSubType = data['subtype'];
923 923 var valueDisplay = data['value_display'];
924 924 var valueIcon = data['value_icon'];
925 925
926 926 var pattern = '(' + escapeRegExChars(value) + ')';
927 927
928 928 valueDisplay = Select2.util.escapeMarkup(valueDisplay);
929 929
930 930 // highlight match
931 931 if (searchType != 'text') {
932 932 valueDisplay = valueDisplay.replace(new RegExp(pattern, 'gi'), '<strong>$1<\/strong>');
933 933 }
934 934
935 935 var icon = '';
936 936
937 937 if (searchType === 'hint') {
938 938 icon += '<i class="icon-repo-group"></i> ';
939 939 }
940 940 // full text search/hints
941 941 else if (searchType === 'search') {
942 942 if (valueIcon === undefined) {
943 943 icon += '<i class="icon-more"></i> ';
944 944 } else {
945 945 icon += valueIcon + ' ';
946 946 }
947 947
948 948 if (searchSubType !== undefined && searchSubType == 'repo') {
949 949 valueDisplay += '<div class="pull-right tag">repository</div>';
950 950 }
951 951 else if (searchSubType !== undefined && searchSubType == 'repo_group') {
952 952 valueDisplay += '<div class="pull-right tag">repo group</div>';
953 953 }
954 954 }
955 955 // repository
956 956 else if (searchType === 'repo') {
957 957
958 958 var repoIcon = getRepoIcon(data['repo_type']);
959 959 icon += repoIcon;
960 960
961 961 if (data['private']) {
962 962 icon += '<i class="icon-lock" ></i> ';
963 963 }
964 964 else if (visualShowPublicIcon) {
965 965 icon += '<i class="icon-unlock-alt"></i> ';
966 966 }
967 967 }
968 968 // repository groups
969 969 else if (searchType === 'repo_group') {
970 970 icon += '<i class="icon-repo-group"></i> ';
971 971 }
972 972 // user group
973 973 else if (searchType === 'user_group') {
974 974 icon += '<i class="icon-group"></i> ';
975 975 }
976 976 // user
977 977 else if (searchType === 'user') {
978 978 icon += '<img class="gravatar" src="{0}"/>'.format(data['icon_link']);
979 979 }
980 980 // pull request
981 981 else if (searchType === 'pull_request') {
982 982 icon += '<i class="icon-merge"></i> ';
983 983 }
984 984 // commit
985 985 else if (searchType === 'commit') {
986 986 var repo_data = data['repo_data'];
987 987 var repoIcon = getRepoIcon(repo_data['repository_type']);
988 988 if (repoIcon) {
989 989 icon += repoIcon;
990 990 } else {
991 991 icon += '<i class="icon-tag"></i>';
992 992 }
993 993 }
994 994 // file
995 995 else if (searchType === 'file') {
996 996 var repo_data = data['repo_data'];
997 997 var repoIcon = getRepoIcon(repo_data['repository_type']);
998 998 if (repoIcon) {
999 999 icon += repoIcon;
1000 1000 } else {
1001 1001 icon += '<i class="icon-tag"></i>';
1002 1002 }
1003 1003 }
1004 1004 // generic text
1005 1005 else if (searchType === 'text') {
1006 1006 icon = '';
1007 1007 }
1008 1008
1009 1009 var tmpl = '<div class="ac-container-wrap">{0}{1}</div>';
1010 1010 return tmpl.format(icon, valueDisplay);
1011 1011 };
1012 1012
1013 1013 var handleSelect = function(element, suggestion) {
1014 1014 if (suggestion.type === "hint") {
1015 1015 // we skip action
1016 1016 $('#main_filter').focus();
1017 1017 }
1018 1018 else if (suggestion.type === "text") {
1019 1019 // we skip action
1020 1020 $('#main_filter').focus();
1021 1021
1022 1022 } else {
1023 1023 window.location = suggestion['url'];
1024 1024 }
1025 1025 };
1026 1026
1027 1027 var autocompleteMainFilterResult = function (suggestion, originalQuery, queryLowerCase) {
1028 1028 if (queryLowerCase.split(':').length === 2) {
1029 1029 queryLowerCase = queryLowerCase.split(':')[1]
1030 1030 }
1031 1031 if (suggestion.type === "text") {
1032 1032 // special case we don't want to "skip" display for
1033 1033 return true
1034 1034 }
1035 1035 return suggestion.value_display.toLowerCase().indexOf(queryLowerCase) !== -1;
1036 1036 };
1037 1037
1038 1038 var cleanContext = {
1039 1039 repo_view_type: null,
1040 1040
1041 1041 repo_id: null,
1042 1042 repo_name: "",
1043 1043
1044 1044 repo_group_id: null,
1045 1045 repo_group_name: null
1046 1046 };
1047 1047 var removeGoToFilter = function () {
1048 1048 $('.searchTagHidable').hide();
1049 1049 $('#main_filter').autocomplete(
1050 1050 'setOptions', {params:{search_context: cleanContext}});
1051 1051 };
1052 1052
1053 1053 $('#main_filter').autocomplete({
1054 1054 serviceUrl: pyroutes.url('goto_switcher_data'),
1055 1055 params: {
1056 1056 "search_context": templateContext.search_context
1057 1057 },
1058 1058 minChars:2,
1059 1059 maxHeight:400,
1060 1060 deferRequestBy: 300, //miliseconds
1061 1061 tabDisabled: true,
1062 1062 autoSelectFirst: false,
1063 1063 containerClass: 'autocomplete-qfilter-suggestions',
1064 1064 formatResult: autocompleteMainFilterFormatResult,
1065 1065 lookupFilter: autocompleteMainFilterResult,
1066 1066 onSelect: function (element, suggestion) {
1067 1067 handleSelect(element, suggestion);
1068 1068 return false;
1069 1069 },
1070 1070 onSearchError: function (element, query, jqXHR, textStatus, errorThrown) {
1071 1071 if (jqXHR !== 'abort') {
1072 1072 var message = formatErrorMessage(jqXHR, textStatus, errorThrown);
1073 1073 SwalNoAnimation.fire({
1074 1074 icon: 'error',
1075 1075 title: _gettext('Error during search operation'),
1076 1076 html: '<span style="white-space: pre-line">{0}</span>'.format(message),
1077 1077 }).then(function(result) {
1078 1078 window.location.reload();
1079 1079 })
1080 1080 }
1081 1081 },
1082 1082 onSearchStart: function (params) {
1083 1083 $('.searchTag.searchTagIcon').html('<i class="icon-spin animate-spin"></i>')
1084 1084 },
1085 1085 onSearchComplete: function (query, suggestions) {
1086 1086 $('.searchTag.searchTagIcon').html('<i class="icon-search"></i>')
1087 1087 },
1088 1088 });
1089 1089
1090 1090 showMainFilterBox = function () {
1091 1091 $('#main_filter_help').toggle();
1092 1092 };
1093 1093
1094 1094 $('#main_filter').on('keydown.autocomplete', function (e) {
1095 1095
1096 1096 var BACKSPACE = 8;
1097 1097 var el = $(e.currentTarget);
1098 1098 if(e.which === BACKSPACE){
1099 1099 var inputVal = el.val();
1100 1100 if (inputVal === ""){
1101 1101 removeGoToFilter()
1102 1102 }
1103 1103 }
1104 1104 });
1105 1105
1106 1106 var dismissNotice = function(noticeId) {
1107 1107
1108 1108 var url = pyroutes.url('user_notice_dismiss',
1109 1109 {"user_id": templateContext.rhodecode_user.user_id});
1110 1110
1111 1111 var postData = {
1112 1112 'csrf_token': CSRF_TOKEN,
1113 1113 'notice_id': noticeId,
1114 1114 };
1115 1115
1116 1116 var success = function(response) {
1117 1117 $('#notice-message-' + noticeId).remove();
1118 1118 return false;
1119 1119 };
1120 1120 var failure = function(data, textStatus, xhr) {
1121 1121 alert("error processing request: " + textStatus);
1122 1122 return false;
1123 1123 };
1124 1124 ajaxPOST(url, postData, success, failure);
1125 1125 }
1126 1126 </script>
1127 1127 <script src="${h.asset('js/rhodecode/base/keyboard-bindings.js', ver=c.rhodecode_version_hash)}"></script>
1128 1128 </%def>
1129 1129
1130 1130 <div class="modal" id="help_kb" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
1131 1131 <div class="modal-dialog">
1132 1132 <div class="modal-content">
1133 1133 <div class="modal-header">
1134 1134 <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
1135 1135 <h4 class="modal-title" id="myModalLabel">${_('Keyboard shortcuts')}</h4>
1136 1136 </div>
1137 1137 <div class="modal-body">
1138 1138 <div class="block-left">
1139 1139 <table class="keyboard-mappings">
1140 1140 <tbody>
1141 1141 <tr>
1142 1142 <th></th>
1143 1143 <th>${_('Site-wide shortcuts')}</th>
1144 1144 </tr>
1145 1145 <%
1146 1146 elems = [
1147 1147 ('/', 'Use quick search box'),
1148 1148 ('g h', 'Goto home page'),
1149 1149 ('g g', 'Goto my private gists page'),
1150 1150 ('g G', 'Goto my public gists page'),
1151 1151 ('g 0-9', 'Goto bookmarked items from 0-9'),
1152 1152 ('n r', 'New repository page'),
1153 1153 ('n g', 'New gist page'),
1154 1154 ]
1155 1155 %>
1156 1156 %for key, desc in elems:
1157 1157 <tr>
1158 1158 <td class="keys">
1159 1159 <span class="key tag">${key}</span>
1160 1160 </td>
1161 1161 <td>${desc}</td>
1162 1162 </tr>
1163 1163 %endfor
1164 1164 </tbody>
1165 1165 </table>
1166 1166 </div>
1167 1167 <div class="block-left">
1168 1168 <table class="keyboard-mappings">
1169 1169 <tbody>
1170 1170 <tr>
1171 1171 <th></th>
1172 1172 <th>${_('Repositories')}</th>
1173 1173 </tr>
1174 1174 <%
1175 1175 elems = [
1176 1176 ('g s', 'Goto summary page'),
1177 1177 ('g c', 'Goto changelog page'),
1178 1178 ('g f', 'Goto files page'),
1179 1179 ('g F', 'Goto files page with file search activated'),
1180 1180 ('g p', 'Goto pull requests page'),
1181 1181 ('g o', 'Goto repository settings'),
1182 1182 ('g O', 'Goto repository access permissions settings'),
1183 1183 ]
1184 1184 %>
1185 1185 %for key, desc in elems:
1186 1186 <tr>
1187 1187 <td class="keys">
1188 1188 <span class="key tag">${key}</span>
1189 1189 </td>
1190 1190 <td>${desc}</td>
1191 1191 </tr>
1192 1192 %endfor
1193 1193 </tbody>
1194 1194 </table>
1195 1195 </div>
1196 1196 </div>
1197 1197 <div class="modal-footer">
1198 1198 </div>
1199 1199 </div><!-- /.modal-content -->
1200 1200 </div><!-- /.modal-dialog -->
1201 1201 </div><!-- /.modal -->
@@ -1,166 +1,166 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <!DOCTYPE html>
3 3
4 4 <%
5 5 c.template_context['repo_name'] = getattr(c, 'repo_name', '')
6 6 go_import_header = ''
7 7 if hasattr(c, 'rhodecode_db_repo'):
8 8 c.template_context['repo_type'] = c.rhodecode_db_repo.repo_type
9 c.template_context['repo_landing_commit'] = c.rhodecode_db_repo.landing_rev[1]
9 c.template_context['repo_landing_commit'] = c.rhodecode_db_repo.landing_ref_name
10 10 c.template_context['repo_id'] = c.rhodecode_db_repo.repo_id
11 11 c.template_context['repo_view_type'] = h.get_repo_view_type(request)
12 12
13 13 if getattr(c, 'repo_group', None):
14 14 c.template_context['repo_group_id'] = c.repo_group.group_id
15 15 c.template_context['repo_group_name'] = c.repo_group.group_name
16 16
17 17 if getattr(c, 'rhodecode_user', None) and c.rhodecode_user.user_id:
18 18 c.template_context['rhodecode_user']['user_id'] = c.rhodecode_user.user_id
19 19 c.template_context['rhodecode_user']['username'] = c.rhodecode_user.username
20 20 c.template_context['rhodecode_user']['email'] = c.rhodecode_user.email
21 21 c.template_context['rhodecode_user']['notification_status'] = c.rhodecode_user.get_instance().user_data.get('notification_status', True)
22 22 c.template_context['rhodecode_user']['first_name'] = c.rhodecode_user.first_name
23 23 c.template_context['rhodecode_user']['last_name'] = c.rhodecode_user.last_name
24 24
25 25 c.template_context['visual']['default_renderer'] = h.get_visual_attr(c, 'default_renderer')
26 26 c.template_context['default_user'] = {
27 27 'username': h.DEFAULT_USER,
28 28 'user_id': 1
29 29 }
30 30 c.template_context['search_context'] = {
31 31 'repo_group_id': c.template_context.get('repo_group_id'),
32 32 'repo_group_name': c.template_context.get('repo_group_name'),
33 33 'repo_id': c.template_context.get('repo_id'),
34 34 'repo_name': c.template_context.get('repo_name'),
35 35 'repo_view_type': c.template_context.get('repo_view_type'),
36 36 }
37 37
38 38 c.template_context['attachment_store'] = {
39 39 'max_file_size_mb': 10,
40 40 'image_ext': ["png", "jpg", "gif", "jpeg"]
41 41 }
42 42
43 43 %>
44 44 <html xmlns="http://www.w3.org/1999/xhtml">
45 45 <head>
46 46 <title>${self.title()}</title>
47 47 <meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
48 48
49 49 ${h.go_import_header(request, getattr(c, 'rhodecode_db_repo', None))}
50 50
51 51 % if 'safari' in (request.user_agent or '').lower():
52 52 <meta name="referrer" content="origin">
53 53 % else:
54 54 <meta name="referrer" content="origin-when-cross-origin">
55 55 % endif
56 56
57 57 <%def name="robots()">
58 58 <meta name="robots" content="index, nofollow"/>
59 59 </%def>
60 60 ${self.robots()}
61 61 <link rel="icon" href="${h.asset('images/favicon.ico', ver=c.rhodecode_version_hash)}" sizes="16x16 32x32" type="image/png" />
62 62 <script src="${h.asset('js/vendors/webcomponentsjs/custom-elements-es5-adapter.js', ver=c.rhodecode_version_hash)}"></script>
63 63 <script src="${h.asset('js/vendors/webcomponentsjs/webcomponents-bundle.js', ver=c.rhodecode_version_hash)}"></script>
64 64
65 65 ## CSS definitions
66 66 <%def name="css()">
67 67 <link rel="stylesheet" type="text/css" href="${h.asset('css/style.css', ver=c.rhodecode_version_hash)}" media="screen"/>
68 68 ## EXTRA FOR CSS
69 69 ${self.css_extra()}
70 70 </%def>
71 71 ## CSS EXTRA - optionally inject some extra CSS stuff needed for specific websites
72 72 <%def name="css_extra()">
73 73 </%def>
74 74
75 75 ${self.css()}
76 76
77 77 ## JAVASCRIPT
78 78 <%def name="js()">
79 79
80 80 <script src="${h.asset('js/rhodecode/i18n/%s.js' % c.language, ver=c.rhodecode_version_hash)}"></script>
81 81 <script type="text/javascript">
82 82 // register templateContext to pass template variables to JS
83 83 var templateContext = ${h.json.dumps(c.template_context)|n};
84 84
85 85 var APPLICATION_URL = "${h.route_path('home').rstrip('/')}";
86 86 var APPLICATION_PLUGINS = [];
87 87 var ASSET_URL = "${h.asset('')}";
88 88 var DEFAULT_RENDERER = "${h.get_visual_attr(c, 'default_renderer')}";
89 89 var CSRF_TOKEN = "${getattr(c, 'csrf_token', '')}";
90 90
91 91 var APPENLIGHT = {
92 92 enabled: ${'true' if getattr(c, 'appenlight_enabled', False) else 'false'},
93 93 key: '${getattr(c, "appenlight_api_public_key", "")}',
94 94 % if getattr(c, 'appenlight_server_url', None):
95 95 serverUrl: '${getattr(c, "appenlight_server_url", "")}',
96 96 % endif
97 97 requestInfo: {
98 98 % if getattr(c, 'rhodecode_user', None):
99 99 ip: '${c.rhodecode_user.ip_addr}',
100 100 username: '${c.rhodecode_user.username}'
101 101 % endif
102 102 },
103 103 tags: {
104 104 rhodecode_version: '${c.rhodecode_version}',
105 105 rhodecode_edition: '${c.rhodecode_edition}'
106 106 }
107 107 };
108 108
109 109 </script>
110 110 <%include file="/base/plugins_base.mako"/>
111 111 <!--[if lt IE 9]>
112 112 <script language="javascript" type="text/javascript" src="${h.asset('js/src/excanvas.min.js')}"></script>
113 113 <![endif]-->
114 114 <script language="javascript" type="text/javascript" src="${h.asset('js/rhodecode/routes.js', ver=c.rhodecode_version_hash)}"></script>
115 115 <script> var alertMessagePayloads = ${h.flash.json_alerts(request=request)|n}; </script>
116 116 ## avoide escaping the %N
117 117 <script language="javascript" type="text/javascript" src="${h.asset('js/scripts.min.js', ver=c.rhodecode_version_hash)}"></script>
118 118 <script>CodeMirror.modeURL = "${h.asset('') + 'js/mode/%N/%N.js?ver='+c.rhodecode_version_hash}";</script>
119 119
120 120
121 121 ## JAVASCRIPT EXTRA - optionally inject some extra JS for specificed templates
122 122 ${self.js_extra()}
123 123
124 124 <script type="text/javascript">
125 125 Rhodecode = (function() {
126 126 function _Rhodecode() {
127 127 this.comments = new CommentsController();
128 128 }
129 129 return new _Rhodecode();
130 130 })();
131 131
132 132 $(document).ready(function(){
133 133 show_more_event();
134 134 timeagoActivate();
135 135 tooltipActivate();
136 136 clipboardActivate();
137 137 })
138 138 </script>
139 139
140 140 </%def>
141 141
142 142 ## JAVASCRIPT EXTRA - optionally inject some extra JS for specificed templates
143 143 <%def name="js_extra()"></%def>
144 144 ${self.js()}
145 145
146 146 <%def name="head_extra()"></%def>
147 147 ${self.head_extra()}
148 148 ## extra stuff
149 149 %if c.pre_code:
150 150 ${c.pre_code|n}
151 151 %endif
152 152 </head>
153 153 <body id="body">
154 154 <noscript>
155 155 <div class="noscript-error">
156 156 ${_('Please enable JavaScript to use RhodeCode Enterprise')}
157 157 </div>
158 158 </noscript>
159 159
160 160 ${next.body()}
161 161 %if c.post_code:
162 162 ${c.post_code|n}
163 163 %endif
164 164 <rhodecode-app></rhodecode-app>
165 165 </body>
166 166 </html>
@@ -1,326 +1,326 b''
1 1 ## -*- coding: utf-8 -*-
2 2
3 3 <%inherit file="/base/base.mako"/>
4 4
5 5 <%def name="title()">
6 6 ${_('%s Changelog') % c.repo_name}
7 7 %if c.changelog_for_path:
8 8 /${c.changelog_for_path}
9 9 %endif
10 10 %if c.rhodecode_name:
11 11 &middot; ${h.branding(c.rhodecode_name)}
12 12 %endif
13 13 </%def>
14 14
15 15 <%def name="breadcrumbs_links()">
16 16 %if c.changelog_for_path:
17 17 /${c.changelog_for_path}
18 18 %endif
19 19 </%def>
20 20
21 21 <%def name="menu_bar_nav()">
22 22 ${self.menu_items(active='repositories')}
23 23 </%def>
24 24
25 25 <%def name="menu_bar_subnav()">
26 26 ${self.repo_menu(active='commits')}
27 27 </%def>
28 28
29 29 <%def name="main()">
30 30
31 31 <div class="box">
32 32
33 33 <div class="title">
34 34 <div id="filter_changelog">
35 35 ${h.hidden('branch_filter')}
36 36 %if c.selected_name:
37 37 <div class="btn btn-default" id="clear_filter" >
38 38 ${_('Clear filter')}
39 39 </div>
40 40 %endif
41 41 </div>
42 42 <div class="pull-left obsolete-toggle">
43 43 % if h.is_hg(c.rhodecode_repo):
44 44 % if c.show_hidden:
45 45 <a class="action-link" href="${h.current_route_path(request, evolve=0)}">${_('Hide obsolete/hidden')}</a>
46 46 % else:
47 47 <a class="action-link" href="${h.current_route_path(request, evolve=1)}">${_('Show obsolete/hidden')}</a>
48 48 % endif
49 49 % else:
50 50 <span class="action-link disabled">${_('Show hidden')}</span>
51 51 % endif
52 52 </div>
53 53 <ul class="links">
54 54 <li>
55 55
56 56 %if c.rhodecode_db_repo.fork:
57 57 <span>
58 58 <a id="compare_fork_button"
59 59 title="${h.tooltip(_('Compare fork with %s' % c.rhodecode_db_repo.fork.repo_name))}"
60 60 class="btn btn-small"
61 61 href="${h.route_path('repo_compare',
62 62 repo_name=c.rhodecode_db_repo.fork.repo_name,
63 source_ref_type=c.rhodecode_db_repo.landing_rev[0],
64 source_ref=c.rhodecode_db_repo.landing_rev[1],
65 target_ref_type='branch' if request.GET.get('branch') else c.rhodecode_db_repo.landing_rev[0],
66 target_ref=request.GET.get('branch') or c.rhodecode_db_repo.landing_rev[1],
63 source_ref_type=c.rhodecode_db_repo.landing_ref_type,
64 source_ref=c.rhodecode_db_repo.landing_ref_name,
65 target_ref_type='branch' if request.GET.get('branch') else c.rhodecode_db_repo.landing_ref_type,
66 target_ref=request.GET.get('branch') or c.rhodecode_db_repo.landing_ref_name,
67 67 _query=dict(merge=1, target_repo=c.repo_name))}"
68 68 >
69 69 ${_('Compare fork with Parent (%s)' % c.rhodecode_db_repo.fork.repo_name)}
70 70 </a>
71 71 </span>
72 72 %endif
73 73
74 74 ## pr open link
75 75 %if h.is_hg(c.rhodecode_repo) or h.is_git(c.rhodecode_repo):
76 76 <span>
77 77 <a id="open_new_pull_request" class="btn btn-small btn-success" href="${h.route_path('pullrequest_new',repo_name=c.repo_name)}">
78 78 ${_('Open new pull request')}
79 79 </a>
80 80 </span>
81 81 %endif
82 82
83 83 </li>
84 84 </ul>
85 85 </div>
86 86
87 87 % if c.pagination:
88 88 <script type="text/javascript" src="${h.asset('js/src/plugins/jquery.commits-graph.js')}"></script>
89 89
90 90 <div class="graph-header">
91 91 ${self.breadcrumbs('breadcrumbs_light')}
92 92 </div>
93 93
94 94 <div id="graph">
95 95 <div class="graph-col-wrapper">
96 96 <div id="graph_nodes">
97 97 <div id="graph_canvas"></div>
98 98 </div>
99 99 <div id="graph_content" class="graph_full_width">
100 100
101 101 <div class="table">
102 102 <table id="changesets" class="rctable">
103 103 <tr>
104 104 ## checkbox
105 105 <th colspan="4">
106 106 ## clear selection
107 107 <div title="${_('Clear selection')}" class="btn btn-sm" id="rev_range_clear" style="display:none">
108 108 <i class="icon-cancel-circled2"></i>
109 109 </div>
110 110 <div class="btn btn-sm disabled" disabled="disabled" id="rev_range_more" style="display:none;">${_('Select second commit')}</div>
111 111 <a href="#" class="btn btn-success btn-sm" id="rev_range_container" style="display:none;"></a>
112 112 </th>
113 113
114 114 ## commit message expand arrow
115 115 <th></th>
116 116 <th>${_('Commit Message')}</th>
117 117
118 118 <th>${_('Age')}</th>
119 119 <th>${_('Author')}</th>
120 120
121 121 <th>${_('Refs')}</th>
122 122 ## comments
123 123 <th></th>
124 124 </tr>
125 125
126 126 <tbody class="commits-range">
127 127 <%include file='changelog_elements.mako'/>
128 128 </tbody>
129 129 </table>
130 130 </div>
131 131 </div>
132 132 <div class="pagination-wh pagination-left">
133 133 ${c.pagination.render()}
134 134 </div>
135 135 <div id="commit-counter" data-total=${c.total_cs} class="pull-right">
136 136 ${_ungettext('showing %d out of %d commit', 'showing %d out of %d commits', c.showing_commits) % (c.showing_commits, c.total_cs)}
137 137 </div>
138 138 </div>
139 139
140 140 <script type="text/javascript">
141 141 var cache = {};
142 142 $(function(){
143 143
144 144 // Create links to commit ranges when range checkboxes are selected
145 145 var $commitCheckboxes = $('.commit-range');
146 146 // cache elements
147 147 var $commitRangeMore = $('#rev_range_more');
148 148 var $commitRangeContainer = $('#rev_range_container');
149 149 var $commitRangeClear = $('#rev_range_clear');
150 150
151 151 var checkboxRangeSelector = function(e){
152 152 var selectedCheckboxes = [];
153 153 for (pos in $commitCheckboxes){
154 154 if($commitCheckboxes[pos].checked){
155 155 selectedCheckboxes.push($commitCheckboxes[pos]);
156 156 }
157 157 }
158 158 var open_new_pull_request = $('#open_new_pull_request');
159 159
160 160 if (open_new_pull_request) {
161 161 var selected_changes = selectedCheckboxes.length;
162 162 open_new_pull_request.hide();
163 163 if (selected_changes == 1) {
164 164 open_new_pull_request.html(_gettext('Open new pull request for selected commit'));
165 165 } else {
166 166 open_new_pull_request.html(_gettext('Open new pull request'));
167 167 }
168 168 open_new_pull_request.show();
169 169 }
170 170
171 171 if (selectedCheckboxes.length > 0) {
172 172 $('#compare_fork_button').hide();
173 173 var commitStart = $(selectedCheckboxes[selectedCheckboxes.length-1]).data();
174 174
175 175 var revStart = commitStart.commitId;
176 176
177 177 var commitEnd = $(selectedCheckboxes[0]).data();
178 178 var revEnd = commitEnd.commitId;
179 179
180 180 var lbl_start = '{0}'.format(commitStart.commitIdx, commitStart.shortId);
181 181 var lbl_end = '{0}'.format(commitEnd.commitIdx, commitEnd.shortId);
182 182
183 183 var url = pyroutes.url('repo_commit', {'repo_name': '${c.repo_name}', 'commit_id': revStart+'...'+revEnd});
184 184 var link = _gettext('Show commit range {0} ... {1}').format(lbl_start, lbl_end);
185 185
186 186 if (selectedCheckboxes.length > 1) {
187 187 $commitRangeClear.show();
188 188 $commitRangeMore.hide();
189 189
190 190 $commitRangeContainer
191 191 .attr('href',url)
192 192 .html(link)
193 193 .show();
194 194
195 195
196 196 } else {
197 197 $commitRangeContainer.hide();
198 198 $commitRangeClear.show();
199 199 $commitRangeMore.show();
200 200 }
201 201
202 202 // pull-request link
203 203 if (selectedCheckboxes.length == 1){
204 204 var _url = pyroutes.url('pullrequest_new', {'repo_name': '${c.repo_name}', 'commit': revEnd});
205 205 open_new_pull_request.attr('href', _url);
206 206 } else {
207 207 var _url = pyroutes.url('pullrequest_new', {'repo_name': '${c.repo_name}'});
208 208 open_new_pull_request.attr('href', _url);
209 209 }
210 210
211 211 } else {
212 212 $commitRangeContainer.hide();
213 213 $commitRangeClear.hide();
214 214 $commitRangeMore.hide();
215 215
216 216 %if c.branch_name:
217 217 var _url = pyroutes.url('pullrequest_new', {'repo_name': '${c.repo_name}', 'branch':'${c.branch_name}'});
218 218 open_new_pull_request.attr('href', _url);
219 219 %else:
220 220 var _url = pyroutes.url('pullrequest_new', {'repo_name': '${c.repo_name}'});
221 221 open_new_pull_request.attr('href', _url);
222 222 %endif
223 223 $('#compare_fork_button').show();
224 224 }
225 225 };
226 226
227 227 $commitCheckboxes.on('click', checkboxRangeSelector);
228 228
229 229 $commitRangeClear.on('click',function(e) {
230 230 $commitCheckboxes.attr('checked', false);
231 231 checkboxRangeSelector();
232 232 e.preventDefault();
233 233 });
234 234
235 235 // make sure the buttons are consistent when navigate back and forth
236 236 checkboxRangeSelector();
237 237
238 238 var msgs = $('.message');
239 239 // get first element height
240 240 var el = $('#graph_content .container')[0];
241 241 var row_h = el.clientHeight;
242 242 for (var i=0; i < msgs.length; i++) {
243 243 var m = msgs[i];
244 244
245 245 var h = m.clientHeight;
246 246 var pad = $(m).css('padding');
247 247 if (h > row_h) {
248 248 var offset = row_h - (h+12);
249 249 $(m.nextElementSibling).css('display','block');
250 250 $(m.nextElementSibling).css('margin-top',offset+'px');
251 251 }
252 252 }
253 253
254 254 $("#clear_filter").on("click", function() {
255 255 var filter = {'repo_name': '${c.repo_name}'};
256 256 window.location = pyroutes.url('repo_commits', filter);
257 257 });
258 258
259 259 $("#branch_filter").select2({
260 260 'dropdownAutoWidth': true,
261 261 'width': 'resolve',
262 262 'placeholder': "${c.selected_name or _('Branch filter')}",
263 263 containerCssClass: "drop-menu",
264 264 dropdownCssClass: "drop-menu-dropdown",
265 265 query: function(query){
266 266 var key = 'cache';
267 267 var cached = cache[key] ;
268 268 if(cached) {
269 269 var data = {results: []};
270 270 //filter results
271 271 $.each(cached.results, function(){
272 272 var section = this.text;
273 273 var children = [];
274 274 $.each(this.children, function(){
275 275 if(query.term.length == 0 || this.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0 ){
276 276 children.push({'id': this.id, 'text': this.text, 'type': this.type})
277 277 }
278 278 });
279 279 data.results.push({'text': section, 'children': children});
280 280 query.callback({results: data.results});
281 281 });
282 282 }else{
283 283 $.ajax({
284 284 url: pyroutes.url('repo_refs_changelog_data', {'repo_name': '${c.repo_name}'}),
285 285 data: {},
286 286 dataType: 'json',
287 287 type: 'GET',
288 288 success: function(data) {
289 289 cache[key] = data;
290 290 query.callback({results: data.results});
291 291 }
292 292 })
293 293 }
294 294 }
295 295 });
296 296 $('#branch_filter').on('change', function(e){
297 297 var data = $('#branch_filter').select2('data');
298 298 //type: branch_closed
299 299 var selected = data.text;
300 300 var filter = {'repo_name': '${c.repo_name}'};
301 301 if(data.type == 'branch' || data.type == 'branch_closed'){
302 302 filter["branch"] = selected;
303 303 if (data.type == 'branch_closed') {
304 304 filter["evolve"] = '1';
305 305 }
306 306 }
307 307 else if (data.type == 'book'){
308 308 filter["bookmark"] = selected;
309 309 }
310 310 window.location = pyroutes.url('repo_commits', filter);
311 311 });
312 312
313 313 commitsController = new CommitsController();
314 314 % if not c.changelog_for_path:
315 315 commitsController.reloadGraph();
316 316 % endif
317 317
318 318 });
319 319
320 320 </script>
321 321 </div>
322 322 % else:
323 323 ${_('There are no changes yet')}
324 324 % endif
325 325 </div>
326 326 </%def>
@@ -1,308 +1,308 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="/base/base.mako"/>
3 3 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
4 4
5 5 <%def name="title()">
6 6 %if c.compare_home:
7 7 ${_('%s Compare') % c.repo_name}
8 8 %else:
9 9 ${_('%s Compare') % c.repo_name} - ${'%s@%s' % (c.source_repo.repo_name, c.source_ref)} &gt; ${'%s@%s' % (c.target_repo.repo_name, c.target_ref)}
10 10 %endif
11 11 %if c.rhodecode_name:
12 12 &middot; ${h.branding(c.rhodecode_name)}
13 13 %endif
14 14 </%def>
15 15
16 16 <%def name="breadcrumbs_links()"></%def>
17 17
18 18 <%def name="menu_bar_nav()">
19 19 ${self.menu_items(active='repositories')}
20 20 </%def>
21 21
22 22 <%def name="menu_bar_subnav()">
23 23 ${self.repo_menu(active='compare')}
24 24 </%def>
25 25
26 26 <%def name="main()">
27 27 <script type="text/javascript">
28 28 // set fake commitId on this commit-range page
29 29 templateContext.commit_data.commit_id = "${h.EmptyCommit().raw_id}";
30 30 </script>
31 31
32 32 <div class="box">
33 33 <div class="summary changeset">
34 34 <div class="summary-detail">
35 35 <div class="summary-detail-header">
36 36 <span class="breadcrumbs files_location">
37 37 <h4>
38 38 ${_('Compare Commits')}
39 39 % if c.file_path:
40 40 ${_('for file')} <a href="#${('a_' + h.FID('',c.file_path))}">${c.file_path}</a>
41 41 % endif
42 42
43 43 % if c.commit_ranges:
44 44 <code>
45 45 r${c.commit_ranges[0].idx}:${h.short_id(c.commit_ranges[0].raw_id)}...r${c.commit_ranges[-1].idx}:${h.short_id(c.commit_ranges[-1].raw_id)}
46 46 </code>
47 47 % endif
48 48 </h4>
49 49 </span>
50 50
51 51 <div class="clear-fix"></div>
52 52 </div>
53 53
54 54 <div class="fieldset">
55 55 <div class="left-label-summary">
56 56 <p class="spacing">${_('Target')}:</p>
57 57 <div class="right-label-summary">
58 58 <div class="code-header" >
59 59 <div class="compare_header">
60 60 ## The hidden elements are replaced with a select2 widget
61 61 ${h.hidden('compare_source')}
62 62 </div>
63 63 </div>
64 64 </div>
65 65 </div>
66 66 </div>
67 67
68 68 <div class="fieldset">
69 69 <div class="left-label-summary">
70 70 <p class="spacing">${_('Source')}:</p>
71 71 <div class="right-label-summary">
72 72 <div class="code-header" >
73 73 <div class="compare_header">
74 74 ## The hidden elements are replaced with a select2 widget
75 75 ${h.hidden('compare_target')}
76 76 </div>
77 77 </div>
78 78 </div>
79 79 </div>
80 80 </div>
81 81
82 82 <div class="fieldset">
83 83 <div class="left-label-summary">
84 84 <p class="spacing">${_('Actions')}:</p>
85 85 <div class="right-label-summary">
86 86 <div class="code-header" >
87 87 <div class="compare_header">
88 88 <div class="compare-buttons">
89 89 % if c.compare_home:
90 90 <a id="compare_revs" class="btn btn-primary"> ${_('Compare Commits')}</a>
91 91 %if c.rhodecode_db_repo.fork:
92 92
93 93 <a class="btn btn-default" title="${h.tooltip(_('Compare fork with %s' % c.rhodecode_db_repo.fork.repo_name))}"
94 94 href="${h.route_path('repo_compare',
95 95 repo_name=c.rhodecode_db_repo.fork.repo_name,
96 source_ref_type=c.rhodecode_db_repo.landing_rev[0],
97 source_ref=c.rhodecode_db_repo.landing_rev[1],
98 target_repo=c.repo_name,target_ref_type='branch' if request.GET.get('branch') else c.rhodecode_db_repo.landing_rev[0],
99 target_ref=request.GET.get('branch') or c.rhodecode_db_repo.landing_rev[1],
96 source_ref_type=c.rhodecode_db_repo.landing_ref_type,
97 source_ref=c.rhodecode_db_repo.landing_ref_name,
98 target_repo=c.repo_name,target_ref_type='branch' if request.GET.get('branch') else c.rhodecode_db_repo.landing_ref_type,
99 target_ref=request.GET.get('branch') or c.rhodecode_db_repo.landing_ref_name,
100 100 _query=dict(merge=1))}"
101 101 >
102 102 ${_('Compare with origin')}
103 103 </a>
104 104
105 105 %endif
106 106
107 107 <a class="btn disabled tooltip" disabled="disabled" title="${_('Action unavailable in current view')}">${_('Swap')}</a>
108 108 <a class="btn disabled tooltip" disabled="disabled" title="${_('Action unavailable in current view')}">${_('Comment')}</a>
109 109 <div id="changeset_compare_view_content">
110 110 <div class="help-block">${_('Compare commits, branches, bookmarks or tags.')}</div>
111 111 </div>
112 112
113 113 % elif c.preview_mode:
114 114 <a class="btn disabled tooltip" disabled="disabled" title="${_('Action unavailable in current view')}">${_('Compare Commits')}</a>
115 115 <a class="btn disabled tooltip" disabled="disabled" title="${_('Action unavailable in current view')}">${_('Swap')}</a>
116 116 <a class="btn disabled tooltip" disabled="disabled" title="${_('Action unavailable in current view')}">${_('Comment')}</a>
117 117
118 118 % else:
119 119 <a id="compare_revs" class="btn btn-primary"> ${_('Compare Commits')}</a>
120 120 <a id="btn-swap" class="btn btn-primary" href="${c.swap_url}">${_('Swap')}</a>
121 121
122 122 ## allow comment only if there are commits to comment on
123 123 % if c.diffset and c.diffset.files and c.commit_ranges:
124 124 <a id="compare_changeset_status_toggle" class="btn btn-primary">${_('Comment')}</a>
125 125 % else:
126 126 <a class="btn disabled tooltip" disabled="disabled" title="${_('Action unavailable in current view')}">${_('Comment')}</a>
127 127 % endif
128 128 % endif
129 129 </div>
130 130 </div>
131 131 </div>
132 132 </div>
133 133 </div>
134 134 </div>
135 135
136 136 ## commit status form
137 137 <div class="fieldset" id="compare_changeset_status" style="display: none; margin-bottom: -80px;">
138 138 <div class="left-label-summary">
139 139 <p class="spacing">${_('Commit status')}:</p>
140 140 <div class="right-label-summary">
141 141 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
142 142 ## main comment form and it status
143 143 <%
144 144 def revs(_revs):
145 145 form_inputs = []
146 146 for cs in _revs:
147 147 tmpl = '<input type="hidden" data-commit-id="%(cid)s" name="commit_ids" value="%(cid)s">' % {'cid': cs.raw_id}
148 148 form_inputs.append(tmpl)
149 149 return form_inputs
150 150 %>
151 151 <div>
152 152 ${comment.comments(h.route_path('repo_commit_comment_create', repo_name=c.repo_name, commit_id='0'*16), None, is_compare=True, form_extras=revs(c.commit_ranges))}
153 153 </div>
154 154 </div>
155 155 </div>
156 156 </div>
157 157 <div class="clear-fix"></div>
158 158 </div> <!-- end summary-detail -->
159 159 </div> <!-- end summary -->
160 160
161 161 ## use JS script to load it quickly before potentially large diffs render long time
162 162 ## this prevents from situation when large diffs block rendering of select2 fields
163 163 <script type="text/javascript">
164 164
165 165 var cache = {};
166 166
167 167 var formatSelection = function(repoName){
168 168 return function(data, container, escapeMarkup) {
169 169 var selection = data ? this.text(data) : "";
170 170 return escapeMarkup('{0}@{1}'.format(repoName, selection));
171 171 }
172 172 };
173 173
174 174 var feedCompareData = function(query, cachedValue){
175 175 var data = {results: []};
176 176 //filter results
177 177 $.each(cachedValue.results, function() {
178 178 var section = this.text;
179 179 var children = [];
180 180 $.each(this.children, function() {
181 181 if (query.term.length === 0 || this.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0) {
182 182 children.push({
183 183 'id': this.id,
184 184 'text': this.text,
185 185 'type': this.type
186 186 })
187 187 }
188 188 });
189 189 data.results.push({
190 190 'text': section,
191 191 'children': children
192 192 })
193 193 });
194 194 //push the typed in changeset
195 195 data.results.push({
196 196 'text': _gettext('specify commit'),
197 197 'children': [{
198 198 'id': query.term,
199 199 'text': query.term,
200 200 'type': 'rev'
201 201 }]
202 202 });
203 203 query.callback(data);
204 204 };
205 205
206 206 var loadCompareData = function(repoName, query, cache){
207 207 $.ajax({
208 208 url: pyroutes.url('repo_refs_data', {'repo_name': repoName}),
209 209 data: {},
210 210 dataType: 'json',
211 211 type: 'GET',
212 212 success: function(data) {
213 213 cache[repoName] = data;
214 214 query.callback({results: data.results});
215 215 }
216 216 })
217 217 };
218 218
219 219 var enable_fields = ${"false" if c.preview_mode else "true"};
220 220 $("#compare_source").select2({
221 221 placeholder: "${'%s@%s' % (c.source_repo.repo_name, c.source_ref)}",
222 222 containerCssClass: "drop-menu",
223 223 dropdownCssClass: "drop-menu-dropdown",
224 224 formatSelection: formatSelection("${c.source_repo.repo_name}"),
225 225 dropdownAutoWidth: true,
226 226 query: function(query) {
227 227 var repoName = '${c.source_repo.repo_name}';
228 228 var cachedValue = cache[repoName];
229 229
230 230 if (cachedValue){
231 231 feedCompareData(query, cachedValue);
232 232 }
233 233 else {
234 234 loadCompareData(repoName, query, cache);
235 235 }
236 236 }
237 237 }).select2("enable", enable_fields);
238 238
239 239 $("#compare_target").select2({
240 240 placeholder: "${'%s@%s' % (c.target_repo.repo_name, c.target_ref)}",
241 241 dropdownAutoWidth: true,
242 242 containerCssClass: "drop-menu",
243 243 dropdownCssClass: "drop-menu-dropdown",
244 244 formatSelection: formatSelection("${c.target_repo.repo_name}"),
245 245 query: function(query) {
246 246 var repoName = '${c.target_repo.repo_name}';
247 247 var cachedValue = cache[repoName];
248 248
249 249 if (cachedValue){
250 250 feedCompareData(query, cachedValue);
251 251 }
252 252 else {
253 253 loadCompareData(repoName, query, cache);
254 254 }
255 255 }
256 256 }).select2("enable", enable_fields);
257 257 var initial_compare_source = {id: "${c.source_ref}", type:"${c.source_ref_type}"};
258 258 var initial_compare_target = {id: "${c.target_ref}", type:"${c.target_ref_type}"};
259 259
260 260 $('#compare_revs').on('click', function(e) {
261 261 var source = $('#compare_source').select2('data') || initial_compare_source;
262 262 var target = $('#compare_target').select2('data') || initial_compare_target;
263 263 if (source && target) {
264 264 var url_data = {
265 265 repo_name: "${c.repo_name}",
266 266 source_ref: source.id,
267 267 source_ref_type: source.type,
268 268 target_ref: target.id,
269 269 target_ref_type: target.type
270 270 };
271 271 window.location = pyroutes.url('repo_compare', url_data);
272 272 }
273 273 });
274 274 $('#compare_changeset_status_toggle').on('click', function(e) {
275 275 $('#compare_changeset_status').toggle();
276 276 });
277 277
278 278 </script>
279 279
280 280 ## table diff data
281 281 <div class="table">
282 282 % if not c.compare_home:
283 283 <div id="changeset_compare_view_content">
284 284 <div class="pull-left">
285 285 <div class="btn-group">
286 286 <a class="${('collapsed' if c.collapse_all_commits else '')}" href="#expand-commits" onclick="toggleCommitExpand(this); return false" data-toggle-commits-cnt=${len(c.commit_ranges)} >
287 287 % if c.collapse_all_commits:
288 288 <i class="icon-plus-squared-alt icon-no-margin"></i>
289 289 ${_ungettext('Expand {} commit', 'Expand {} commits', len(c.commit_ranges)).format(len(c.commit_ranges))}
290 290 % else:
291 291 <i class="icon-minus-squared-alt icon-no-margin"></i>
292 292 ${_ungettext('Collapse {} commit', 'Collapse {} commits', len(c.commit_ranges)).format(len(c.commit_ranges))}
293 293 % endif
294 294 </a>
295 295 </div>
296 296 </div>
297 297 <div style="padding:0 10px 10px 0px" class="pull-left"></div>
298 298 ## commit compare generated below
299 299 <%include file="compare_commits.mako"/>
300 300 ${cbdiffs.render_diffset_menu(c.diffset)}
301 301 ${cbdiffs.render_diffset(c.diffset)}
302 302 </div>
303 303 % endif
304 304
305 305 </div>
306 306 </div>
307 307
308 308 </%def>
@@ -1,84 +1,84 b''
1 1
2 2 <div id="codeblock" class="browserblock">
3 3 <div class="browser-header">
4 4 <div class="browser-nav">
5 5
6 6 <div class="info_box">
7 7
8 8 <div class="info_box_elem previous">
9 9 <a id="prev_commit_link" data-commit-id="${c.prev_commit.raw_id}" class=" ${('disabled' if c.url_prev == '#' else '')}" href="${c.url_prev}" title="${_('Previous commit')}"><i class="icon-left"></i></a>
10 10 </div>
11 11
12 12 ${h.hidden('refs_filter')}
13 13
14 14 <div class="info_box_elem next">
15 15 <a id="next_commit_link" data-commit-id="${c.next_commit.raw_id}" class=" ${('disabled' if c.url_next == '#' else '')}" href="${c.url_next}" title="${_('Next commit')}"><i class="icon-right"></i></a>
16 16 </div>
17 17 </div>
18 18
19 19 % if h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name):
20 20 <div>
21 21 <a class="btn btn-primary new-file" href="${h.route_path('repo_files_upload_file',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path)}">
22 22 ${_('Upload File')}
23 23 </a>
24 24 <a class="btn btn-primary new-file" href="${h.route_path('repo_files_add_file',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path)}">
25 25 ${_('Add File')}
26 26 </a>
27 27 </div>
28 28 % endif
29 29
30 30 % if c.enable_downloads:
31 31 <% at_path = '{}'.format(request.GET.get('at') or c.commit.raw_id[:6]) %>
32 32 <div class="btn btn-default new-file">
33 33 % if c.f_path == '/':
34 34 <a href="${h.route_path('repo_archivefile',repo_name=c.repo_name, fname='{}.zip'.format(c.commit.raw_id))}">
35 35 ${_('Download full tree ZIP')}
36 36 </a>
37 37 % else:
38 38 <a href="${h.route_path('repo_archivefile',repo_name=c.repo_name, fname='{}.zip'.format(c.commit.raw_id), _query={'at_path':c.f_path})}">
39 39 ${_('Download this tree ZIP')}
40 40 </a>
41 41 % endif
42 42 </div>
43 43 % endif
44 44
45 45 <div class="files-quick-filter">
46 46 <ul class="files-filter-box">
47 47 <li class="files-filter-box-path">
48 48 <i class="icon-search"></i>
49 49 </li>
50 50 <li class="files-filter-box-input">
51 51 <input onkeydown="NodeFilter.initFilter(event)" class="init" type="text" placeholder="Quick filter" name="filter" size="25" id="node_filter" autocomplete="off">
52 52 </li>
53 53 </ul>
54 54 </div>
55 55 </div>
56 56
57 57 </div>
58 58
59 59 ## file tree is computed from caches, and filled in
60 60 <div id="file-tree">
61 61 ${c.file_tree |n}
62 62 </div>
63 63
64 64 %if c.readme_data:
65 65 <div id="readme" class="anchor">
66 66 <div class="box">
67 <div class="readme-title" title="${h.tooltip(_('Readme file from commit %s:%s') % (c.rhodecode_db_repo.landing_rev[0], c.rhodecode_db_repo.landing_rev[1]))}">
67 <div class="readme-title" title="${h.tooltip(_('Readme file from commit %s:%s') % (c.rhodecode_db_repo.landing_ref_type, c.rhodecode_db_repo.landing_ref_name))}">
68 68 <div>
69 69 <i class="icon-file-text"></i>
70 <a href="${h.route_path('repo_files',repo_name=c.repo_name,commit_id=c.rhodecode_db_repo.landing_rev[1],f_path=c.readme_file)}">
70 <a href="${h.route_path('repo_files',repo_name=c.repo_name,commit_id=c.rhodecode_db_repo.landing_ref_name,f_path=c.readme_file)}">
71 71 ${c.readme_file}
72 72 </a>
73 73 </div>
74 74 </div>
75 75 <div class="readme codeblock">
76 76 <div class="readme_box">
77 77 ${c.readme_data|n}
78 78 </div>
79 79 </div>
80 80 </div>
81 81 </div>
82 82 %endif
83 83
84 84 </div>
@@ -1,118 +1,118 b''
1 1 <%inherit file="/summary/summary_base.mako"/>
2 2
3 3 <%namespace name="components" file="/summary/components.mako"/>
4 4
5 5
6 6 <%def name="menu_bar_subnav()">
7 7 ${self.repo_menu(active='summary')}
8 8 </%def>
9 9
10 10 <%def name="main()">
11 11
12 12 <div id="repo-summary" class="summary">
13 13 ${components.summary_detail(breadcrumbs_links=self.breadcrumbs_links(), show_downloads=True)}
14 14 </div><!--end repo-summary-->
15 15
16 16
17 17 <div class="box">
18 18 %if not c.repo_commits:
19 19 <div class="empty-repo">
20 20 <div class="title">
21 21 <h3>${_('Quick start')}</h3>
22 22 </div>
23 23 <div class="clear-fix"></div>
24 24 </div>
25 25 %endif
26 26 <div class="table">
27 27 <div id="shortlog_data">
28 28 <%include file='summary_commits.mako'/>
29 29 </div>
30 30 </div>
31 31 </div>
32 32
33 33 %if c.readme_data:
34 34 <div id="readme" class="anchor">
35 35 <div class="box">
36 36
37 <div class="readme-title" title="${h.tooltip(_('Readme file from commit %s:%s') % (c.rhodecode_db_repo.landing_rev[0], c.rhodecode_db_repo.landing_rev[1]))}">
37 <div class="readme-title" title="${h.tooltip(_('Readme file from commit %s:%s') % (c.rhodecode_db_repo.landing_ref_type, c.rhodecode_db_repo.landing_ref_name))}">
38 38 <div>
39 39 <i class="icon-file-text"></i>
40 <a href="${h.route_path('repo_files',repo_name=c.repo_name,commit_id=c.rhodecode_db_repo.landing_rev[1],f_path=c.readme_file)}">
40 <a href="${h.route_path('repo_files',repo_name=c.repo_name,commit_id=c.rhodecode_db_repo.landing_ref_name,f_path=c.readme_file)}">
41 41 ${c.readme_file}
42 42 </a>
43 43 </div>
44 44 </div>
45 45 <div class="readme codeblock">
46 46 <div class="readme_box">
47 47 ${c.readme_data|n}
48 48 </div>
49 49 </div>
50 50 </div>
51 51 </div>
52 52 %endif
53 53
54 54 <script type="text/javascript">
55 55 $(document).ready(function(){
56 56
57 57 var showCloneField = function(clone_url_format){
58 58 $.each(['http', 'http_id', 'ssh'], function (idx, val) {
59 59 if(val === clone_url_format){
60 60 $('#clone_option_' + val).show();
61 61 $('#clone_option').val(val)
62 62 } else {
63 63 $('#clone_option_' + val).hide();
64 64 }
65 65 });
66 66 };
67 67 // default taken from session
68 68 showCloneField(templateContext.session_attrs.clone_url_format);
69 69
70 70 $('#clone_option').on('change', function(e) {
71 71 var selected = $(this).val();
72 72
73 73 storeUserSessionAttr('rc_user_session_attr.clone_url_format', selected);
74 74 showCloneField(selected)
75 75 });
76 76
77 77 var initialCommitData = {
78 78 id: null,
79 79 text: 'tip',
80 80 type: 'tag',
81 81 raw_id: null,
82 82 files_url: null
83 83 };
84 84
85 85 select2RefSwitcher('#download_options', initialCommitData);
86 86
87 87 // on change of download options
88 88 $('#download_options').on('change', function(e) {
89 89 // format of Object {text: "v0.0.3", type: "tag", id: "rev"}
90 90 var ext = '.zip';
91 91 var selected_cs = e.added;
92 92 var fname = e.added.raw_id + ext;
93 93 var href = pyroutes.url('repo_archivefile', {'repo_name': templateContext.repo_name, 'fname':fname});
94 94 // set new label
95 95 $('#archive_link').html('{0}{1}'.format(escapeHtml(e.added.text), ext));
96 96
97 97 // set new url to button,
98 98 $('#archive_link').attr('href', href)
99 99 });
100 100
101 101
102 102 // calculate size of repository
103 103 calculateSize = function () {
104 104
105 105 var callback = function (data) {
106 106 % if c.show_stats:
107 107 showRepoStats('lang_stats', data);
108 108 % endif
109 109 };
110 110
111 111 showRepoSize('repo_size_container', templateContext.repo_name, templateContext.repo_landing_commit, callback);
112 112
113 113 }
114 114
115 115 })
116 116 </script>
117 117
118 118 </%def>
General Comments 0
You need to be logged in to leave comments. Login now